Sync Sub2API v0.2.8 with protected Zero One contracts - #59
Merged
Merged
Conversation
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。
出站伪装用的 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 平台账号 + 多厂商挂载复用)
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What changed
fd80b08c90b55edcad5b00171b53f08721d30da1) with a recorded two-parent merge and deterministic 473-path change map.Verification
make test-affectedpassed 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.--migrate-onlydrill: three new SQL filenames present, repeatable ledger/data, checksum tampering rejected.Production gate
After merge, publish Backend and Edge images from the same current
mainSHA 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.