Skip to content

Sync Sub2API v0.2.8 with protected Zero One contracts - #59

Merged
XiaoSiKe merged 248 commits into
mainfrom
codex/upgrade-v0.2.8
Sep 24, 2026
Merged

XiaoSiKe merged 248 commits into
mainfrom
codex/upgrade-v0.2.8

Conversation

@XiaoSiKe

Copy link
Copy Markdown
Owner

What changed

  • Pin the upstream baseline to Sub2API v0.2.8 (fd80b08c90b55edcad5b00171b53f08721d30da1) with a recorded two-parent merge and deterministic 473-path change map.
  • Preserve Zero One product contracts and the shipped frontend assets while adapting gateway, pricing, affiliate, API compatibility, and dependency wiring to the new upstream source.
  • Keep the delivered version at 0.2.8. Advance the reviewed visual baseline only for the version badge glyph; 19 desktop/mobile screenshots changed and no production UI asset bytes changed.
  • Add migration-239-aware data fingerprinting so the intended legacy pricing-key conversion is checked against the exact expected result while all other existing business values remain protected.
  • Fix buffered signed-thinking propagation, Wire generation, the affiliate offline-withdraw handler, and test/type compatibility exposed by the upstream merge.

Verification

  • make test-affected passed with full impact: backend ordinary/unit/integration, lint, frontend and landing checks/builds, deployment contracts, complete Chromium suite (290 passed, 78 intentionally skipped), Go vulnerability scan, and pnpm dependency audit.
  • Isolated candidate --migrate-only drill: three new SQL filenames present, repeatable ledger/data, checksum tampering rejected.
  • Upstream release-tool Python suite on Linux: 10 tests passed.
  • Migration-239 projection checked against actual PostgreSQL execution, including an unrelated-mutation rejection.

Production gate

After merge, publish Backend and Edge images from the same current main SHA and use their digests. Complete a new encrypted off-host backup and actual restore drill, prove old/new image schema compatibility, then run the phase-based Backend-first production controller, dedicated-user smoke tests, and 30-minute observation. Do not reuse historical recovery proofs.

coo1white and others added 30 commits July 25, 2026 19:57
UpdateS3Config only encrypted the SecretAccessKey on the branch where the
caller supplied a new one. The other branch inherits the stored secret
through loadS3Config, which *decrypts* it — so that path wrote the
plaintext straight back to the settings row and overwrote the ciphertext.

It is the normal path, not an edge case. UpdateS3Config clears the secret
in its own return value, so the admin form's field is empty afterwards.
Any later save — changing the endpoint, saving the schedule, pressing save
twice — sends an empty secret and takes the inheriting branch. One extra
save is enough to leave the secret in plaintext at rest.

Nothing surfaced because loadS3Config falls back to the raw value when
decryption fails, for compatibility with pre-encryption rows. Backups kept
working and the only trace was a line per read:

  [Backup] S3 SecretAccessKey 解密失败(可能是旧的未加密数据):
  decrypt: cipher: message authentication failed

Seen on a live install: the stored value was 64 characters, exactly the
plaintext length of an R2 secret, where AES-GCM + base64 would have been
124. The compatibility fallback turns a one-time write bug into a
permanent plaintext secret.

It also feeds itself: pg_dump includes the settings table, so the backup
uploaded to the bucket carries the credentials for that same bucket.

Fix: hoist the encryption out of the if/else so it runs for a non-empty
secret whichever way it arrived. The ephemeral-key guard (#4524) moves
with it, and the no-secret-at-all case still skips encryption entirely, so
an update with no stored secret keeps working under an auto-generated key.

The existing KeepExistingSecret test passed either way, because it only
asserted loadS3Config returns the plaintext — true with the fallback too.
The new test asserts on the row itself: still ciphertext after a second
save, not double-wrapped after a third.

Checked the new test fails without the fix and passes with it.
…pstream EOF

The streaming forward loops treated upstream EOF as the only normal exit
condition; response.completed/[DONE] only set a flag. With keep-alive /
HTTP/2 connection reuse to the upstream, the upstream may hold the
stream open for 8-46s after the terminal event, during which the
gateway could only emit downstream keepalives, inflating tail latency.

Finish the stream as soon as the terminal event frame has been fully
written downstream (usage is already parsed from the terminal event),
and close the upstream body to unblock the reader goroutine. Applied
to the guarded/async, sync, and passthrough streaming paths. The WS
HTTP bridge already behaved this way.

Adds a regression test with an upstream that hangs after
response.completed.
…d items fallback

- Handle JSON Schema Draft 2020-12 prefixItems (tuple arrays) by converting to a best-match items schema
- Ensure array schema always defines a valid items schema to prevent Gemini 400 'missing field: items.items' error
- Fix recursive traversal when a schema defines both properties and items
- Add comprehensive unit test cases for tuple and array schemas
- Fix test assertion target location for whereItems prefixItems
- Support const keyword normalization to enum and infer scalar type
- Infer scalar type for enum-only schema to prevent object reason injection
- Recursively clean properties and items after anyOf/oneOf merge
- Ensure empty prefixItems is safely deleted
Upstream Anthropic keepalive frames (event: ping) leaked into OpenAI
Chat Completions SSE streams, crashing strict clients (Cursor, Hermes,
OpenAI SDKs) on unexpected event types. Error events still forward.

Fixes #5203
- Replace unchecked single-value type assertions with comma-ok form
  guarded by require.True (errcheck check-type-assertions)
- Remove trailing blank line flagged by gofmt

Co-Authored-By: Claude Code <noreply@anthropic.com>
….done

Gemini/Antigravity delivers tool arguments as one complete object, so this
converter synthesizes the Responses tool-call stream itself and therefore owns
its self-consistency. The done event was built without Arguments, leaving it ""
while the preceding delta carried the entire JSON. Clients that reconcile the
done payload against the accumulated deltas (OpenAI Responses SDK); Qoder's
reconcileToolCall) reject that mismatch as inconsistent_tool_call and abort the
turn non-retryably, so every tool call through the gateway failed.

Emit state.CurrentArgs, which closeCurrentResponsesItem already uses for the
item, restoring sum(deltas) == done.arguments == item.arguments.

Regression test asserts the delta sum equals the done payload and fails closed
when Arguments is dropped.
管理员在站外给邀请人打款后,系统里原本无法扣减对应的返利额度,用户仍可把
这部分额度转入余额。现在可在「邀请返利 → 提取记录」点「登记线下提现」,
从该用户的可提取返利额度中扣除已打款金额:

- POST /api/v1/admin/affiliates/users/:user_id/withdraw,请求体只含 amount,
  金额按 8 位小数舍入后校验
- 先解冻已到期的冻结额度,再以 aff_quota >= amount 为条件原子扣减,写入
  action=withdraw 的流水并带额度快照;额度不足返回 AFFILIATE_QUOTA_INSUFFICIENT
- 不新增字段与迁移:线下提现复用已有 action 列区分,操作人由管理端审计日志记录
- 提取记录同时列出转入余额与线下提现,新增「类型」列
- 弹窗选择用户后展示可提取额度,支持一键填入全部;从未产生返利的用户按 0 处理

返利记录改为 LEFT JOIN 订单与被邀请人,兑换码、管理员充值来源的返利也会出现
在明细中,接口里订单相关字段与被邀请人 ID 可为 null。
alfadb and others added 28 commits September 23, 2026 13:35
出站伪装用的 Claude Code 版本号此前硬编码在 claude.CLICurrentVersion,
只能靠环境变量 SUB2API_CLAUDE_CLI_VERSION 覆盖且需重启;Anthropic 抬高
版本下限时(如 Opus 5.5 要求 >= 2.1.280)必须改代码发版才能跟上。

改为每小时从官方 GitHub Releases 同步最新稳定版,无需重启也无需发版。

- 新增 ClaudeCodeVersionSyncService:1 小时间隔,数据源 anthropics/claude-code
  的 releases/latest,失败回退最近 30 条列表;过滤 draft/prerelease;
  只向前推进,拉取失败保留上次值不降级;面板可关闭
- 新增三个设置项:claude_code_client_version(管理员手动固定)、
  claude_code_client_version_synced(同步服务独占写)、
  claude_code_version_auto_sync_enabled(开关,默认开)
- 取值优先级:面板手动值 -> 同步值 -> SUB2API_CLAUDE_CLI_VERSION -> 内置基线,
  每层经 IsSupportedCLIVersion 校验,含 60s 缓存与 singleflight
- 版本号运行期化:claude.DefaultHeaders 由包级 var 改为函数,新增
  EffectiveCLIVersion/DefaultUserAgent 与 SetCLIVersionResolver(未注入时
  与 CLIVersion() 等价);identity 的 defaultFingerprint 不再在包 init 固化,
  指纹下限与主版本超前校验改用运行期值
- 守住一致性铁律:buildUpstreamRequest 与 count_tokens 各取一次 UA 字符串,
  同时喂给出站 User-Agent 头与请求体 billing 的 cc_version,避免缓存翻转
  瞬间头体版本不一致被判非正版客户端
- 管理后台设置页新增配置区块,含中英文案与「当前同步到」只读展示
feat(models): support GPT-6 Sol, Luna and Claude Opus 5.5
feat(claude): 自动同步 Claude Code 客户端版本号,无需改代码跟随官方版本
OpenCode Go 上游(https://opencode.ai/zen/go/v1)官方提供用量端点
GET /zen/go/v1/usage(Bearer 复用消费 key),返回 rolling(5h)/weekly/monthly
三窗口的 percent 与 resetsAt。本提交为 openai apikey 账号(base_url 为
opencode.ai/zen/go/v1)接入该用量窗口:

- 后端:OpenCodeGoUsageService——手动刷新(每账号 10s 节流)、
  周期自动刷新(全局开关 + 账号开关双开启,interval 5-1440 分钟,
  leader lock + 每轮 20 账号 + 并发 4)、失败指数退避(上限 24h)、
  Retry-After 尊重;401→unauthorized、403→failed;snapshot 持久化于
  accounts.extra(单账号写入,无分组 CAS、无会话加密)
- admin API:settings GET/PUT、/:id/opencode-go-usage GET、
  auto-refresh PUT、refresh POST(与 Ollama Cloud usage 同构)
- 前端:账号列表行内三窗口进度条(复用 UsageProgressBar)、
  编辑弹窗内配置面板(详情+手动刷新+auto-refresh)、全局设置卡片、
  en/zh i18n

验证:
- 上游端点已用生产真实 Go key 实测 200,响应
  {"usage":{"rolling":{"status":"ok","percent":6,...},...}}(2026-08-13)
- go build ./... OK;service/handler/repo/dto/cmd 全量回归 OK
- 新增单测 20 例(解析 401/403/畸形 JSON、退避、节流等)全绿
- frontend vitest 新增 7 例 + Ollama 回归 4 例全绿;vue-tsc 零错误
与 Ollama Cloud usage 对齐:OpenCode Go 作为同 Key 聚合订阅,
多个 openai/apikey/base_url=opencode.ai/zen/go/v1 账号共享同一份
用量状态与刷新节奏:

- 组共享:组指纹 sha256("opencode.ai\0"+api_key);auto_refresh 开关
  与 snapshot 按组写(事务 + FOR NO KEY UPDATE + 锚点 CAS + 纯合并,
  写 snapshot 不会抹掉开关);列表/详情经 ResolveAccounts 组内共享;
  账号换 Key → IdentityChanged → 组级关闭 auto_refresh 并清快照
- 活动防抖 + 超窗强刷:settings 新增 debounce_minutes(默认 1,
  1-60,必须小于 interval_minutes);模型请求活动经网关 5 处调用点
  驱动刷新,due = min(lastUsed+debounce, fetchedAt+maxWait),
  成功路径有 5 分钟最小抓取间隔,失败路径退避优先
- 手动刷新限频 10s→30s 且按组;singleflight/RunDue 按组去重;
  ListDue 在 SQL 内按组算 due(CTE 分组取组内 MAX(last_used_at))

验证:go build + 五包回归 OK;新增组共享/三态 due/组单飞/
IdentityChanged/debounce 校验单测全绿;vitest 11 例全绿;
vue-tsc 零错误。
OpenCode Go 有两个都合法的 base_url:https://opencode.ai/zen/go/v1(OpenAI
协议档,DefaultOpenCodeGoBaseURL)与 https://opencode.ai/zen/go(Anthropic
协议档,前端 defaultCNBaseUrl 会写入)。旧拼接只做 strings.TrimRight(base,
"/") + "/usage",Anthropic 档账号的额度探测必然打到 /zen/go/usage。

实测证据(curl 无鉴权,401=端点存在,404=不存在):
  401 https://opencode.ai/zen/go/v1/usage
  404 https://opencode.ai/zen/go/usage

修法沿用同包 kimiQuotaURL(coding/v1 与 coding 两种 base)的既有惯例:
TrimSuffix(base, "/v1") 后统一拼回 /v1/usage,两个变体与尾斜杠写法都归一到
https://opencode.ai/zen/go/v1/usage;自定义 base 保持同一语义。
feat(opencode): 支持 OpenCode Go 用量窗口(opencode_go 平台账号 + 多厂商挂载复用)
@XiaoSiKe
XiaoSiKe merged commit ce899e5 into main Sep 24, 2026
12 checks passed
@XiaoSiKe
XiaoSiKe deleted the codex/upgrade-v0.2.8 branch September 24, 2026 02:21
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.