| 文件 | 最后提交记录 | 最后更新时间 |
|---|---|---|
fix(channels): 商汤扫码链路重建 + 双区登录 + 凭据过期口径 + 四条老化判据 ① 商汤(Raccoon)扫码「链接无效」——两个真根因 - 登录页 /login/mp 已被站点删除(Next 路由 404,实测只有 404 页); - 旧实现本地随机造 qrcode_code,而上游早已不认自造码:旧端点 /api/web/auth/v1/login_with_qrcode_code 对任意 code 恒回 pending (服务端没有该会话)⇒ 扫码永远完不成,账号一直是空凭据。 修法:改为上游现行 IAM 流程(逐行对齐其网页客户端 auth chunk)—— POST /api/iam/v1/auth/mp/qrcode 建会话 → 链接用 /login?code=…&appname=…&source=platform(实测 200 + 微信扫码面板) → 轮询 /api/iam/v1/auth/mp/qrcode/login(IAM code:"OK" 与旧 code:0 都认)。 服务端建码失败才退回旧本地码并打 warn。UI 对扫码流程改文案为 「用微信扫码」(原本文案会让人以为点开就自动登录)。 生产实测:链接 HTTP 200、轮询回 pending(会话真实存在)。 ② 双区登录(P7) - 凭据增加 region(intl|cn,缺省=历史行为);start_login 接受 region, 会话按区域选端点,完成登录写进凭据; - CodeBuddy:cn=copilot.tencent.com(platform=ide)/ intl=www.workbuddy.ai(platform=workbuddy-ai,X-Domain、 X-Product-Code、UA 按区域;实测 auth/state 回 200 信封); - Qoder:intl=openapi.qoder.sh+qoder.com(client_id e883ade2-…)/ cn=openapi.qoder.com.cn+qoder.cn(client_id 不同 732aef47-…,照参考 QODER_CN)。CN 无公开 chat 端点 ⇒ CN 只做登录/领取; - UI:buddy/qoder 详情页增加「+国际区」按钮。 ③ 凭据过期时刻的时区口径(P6 真根因) credentialExpiresAtMs 把 ISO 过期时刻按本地时区解析,而 codearts 给的是 UTC ISO(2026-10-19T16:42:43.06Z)⇒ 本机 +08 下过期时刻虚早 8 小时 ⇒ refresh_creds 每 30 分钟都判"已过期"并对华为云真打一次 STS(token 本还有 23 小时寿命)。改为按声明时区解析(Z / ±hh:mm / 无后缀=UTC 三种形态同一 epoch),补 TestCredentialExpiresAtMsParsesISOAsUTC。 ④ 四条老化/假红判据 - TestCreditsPoolReason*:account_add 触发的选号会用构造期默认余额 (0,0) 先查一次并缓存 60s ⇒ 之后改假上游读不到("余额 100 却报 exhausted")。 harness 改余额后清 TTL 缓存(__ttl_reset_probe,与生产同一实现)。 - TestAccountReorderActually... / TestPoolListOrderIsDeterministic:随 「一通道一源」失效(按 channels-buddy-<acct> 过滤恒得空)。改写为—— 重排必须切换服务账号(新增 __chosen_account_probe),顺序确定性改为断言 选号顺序(__rank_accounts_probe)由 id 稳定决定。 - channels.lua 顶层 local 余量:区域表与 5 份适配器源码常量收进 plugin.K (TestChannelsLuaHasLocalHeadroom 需要 ≥10 槽位);ZCode 适配器源码里的 CLI_PREFIX 必须留在源码内(适配器沙箱没有 plugin 全局)。 ⑤ 错误文案 openai_error 增补腾讯 codebuddy 三层嵌套 {error:{data:{code,msg}}}:实测 429 code=14018「额度已用尽」此前落到"不识别",客户端只看到通用文案。 验证:core 434s 全绿、lua 62s 全绿、provider 全绿;生产已部署并实测 raccoon 链接 200 / trae 非流式 200 / zcode 适配器加载。 (cherry picked from commit 662f8f02b6282d71d243ecd01a0bbfbcface88d4) | 16 天前 | |
fix(channels,provider): 渠道模型不可用 + 额度异常的三个根因 ## 背景 用户报三问题:① channel 注册的模型无法使用;② 额度显示依旧异常; ③ 其他渠道应也存在类似问题。三条全部核实并给出结论(③ 见文档)。 ## ① 网关解不开 源名:模型名 限定名(internal/provider/registry.go) ResolvePinned 用 strings.IndexAny(model, "-:/") 找**第一个**分隔符。 而渠道源名形如 channels-trae-b8dd6b3df30e(**含 -**),于是 channels-trae-b8dd6b3df30e:Doubao-Seed-2.1-Pro 被切成 src="channels"、model="trae-b8dd6b3df30e:Doubao-Seed-2.1-Pro" ⇒ 源名永不匹配 ⇒ Resolve() 返回 nil ⇒ 客户端看到 model "..." is not configured。 **为什么这个写法必须支持**:本实例有 **44 个模型被多源同时提供** (glm-5.2 由 6 个源提供),WebUI 的模型下拉框用 源名:模型名 作为 option value 是**有意设计** —— 用户必须能指定"从哪个源调用"。 修法:抽 splitPinned(),**按已知源名做最长前缀匹配**(源名是已知集合, 这是唯一可靠办法),保留旧 -:/ 兜底以兼容非源名形态。 EffectiveModel 同步修(同一拆分规则,避免两处漂移)。 ## ② trae 适配器三连错(internal/lua/plugins/channels.lua) 三个错误逐层叠加,每个都被上一层掩盖: **(a) 没有 build_headers** ⇒ 内核兜底发 Authorization: Bearer, 而 trae 推理端点要 Cloud-IDE-JWT + 设备身份头 ⇒ 401「We're sorry, but we are not able to authenticate you」。 → 与上一轮「额度不可用」是**同一根因的两个出口**(额度端点修了、推理端点漏了)。 教训:修"某一族请求的鉴权"时,必须把该族的**所有出口**列出来逐个对齐。 **(b) header 常量的值是错的**(照参考 trae-product.ts 的 TRAE 配置逐项核对): · X-App-Id 抄成 6eefa6c9,真实是 6eefa01c-1036-4c7e-9ca5-d891f63bfcd8 · app_version_code 用了 0.1.52,真实是**日期式** 20260811 ⇒ 上游 4001 bad request: binding: expr_path=app_version_code, cause=parameter type does not match binding data **(c) 流式转换完全不对**:TRAE 是 **SOLO 自定义 SSE**: event:output → data:{"response":"###","reasoning_content":null,...} 正文在 response,推理在 reasoning_content。 · 套 __prelude.openai_stream_chunk ⇒ 无 choices ⇒ empty stream · 改成 OpenAI 外壳 {choices:[{delta}]} **仍然空** —— 内核 json.Unmarshal(unified, &UnifiedChunk) 解的是**扁平结构**,外壳字段全被丢弃 ⇒ 必须输出扁平 {content, reasoning_content, usage, done, finish_reason}。 ⚠️ 内核只把 data: 行交给适配器(event: 行在 provider.go 被丢弃), 因此判别帧类型只能靠**内容特征**(response / reasoning_content / finish_reason / prompt_tokens),拿不到 event 名。 ## ③ 余额恒为 0(同文件 balance.total_fn) 读的是 p.credits_limit(pack **顶层**)—— 该字段**根本不存在**。 真实路径是 pack.entitlement_base_info.quota.credits_limit (参考 trae-credits.ts:341)+ pack.usage.credits_amount。 实测上游:免费(无该字段) / 每月登录赠送(500) / 签到奖励(100) ⇒ 实际 600。 修法:读嵌套路径 + 保留顶层兼容 + usage_summary.{total,consumed}_amount 兜底。 修后余额 600 → 实测 598.516(真实扣减)。 ## ④ 夹具与生产形状不一致(internal/core/channels_trae_test.go) 夹具原先返回**扁平** {"Token":...},与生产 {"Result":{"Token":...}} 不符 ⇒ 「代码只读顶层」这个缺陷在夹具里**恰好是对的**,所有 trae 判据都绿。 **夹具形状错 = 判据必然虚假通过**。这是 ExchangeToken 缺陷藏了两轮的原因。 ## 判据(新增,变异 4/4 咬住) - internal/provider/registry_pinned_name_test.go(4 用例) - internal/core/channels_trae_chat_headers_test.go(3 条) - internal/core/channels_trae_solo_sse_test.go(2 条) - internal/core/channels_trae_credits_hdr_test.go(1 条) - internal/core/channels_trae_exchange_shape_test.go(2 条) ## 文档 docs/channel-repair-plan.md:现状快照(生产实测)+ 各渠道缺陷矩阵 + 后续计划(P0-P4)。**如实记录**:其余 4 个渠道(zcode/cline/gemini/qoder) 有同类缺陷风险,但**账号数均为 0、无法做生产验证**。 ## 生产已实测 - channels-trae-...:Doubao-Seed-2.1-Pro → 80 帧,正文 27 + 推理 299 - channels-trae-...:glm-5.2 / channels-buddy-...:hy4-preview → OK - credits/status → active:true, dailyCredit:100, todayCheckedIn:true - credits/balances → total:598.516 credits | 19 天前 | |
chore(main): 回流 —— 第三批逐模块对照文档 按纪律先在 release 提交再内容对齐。校验:diff vs release = 0 行。 | 22 天前 | |
feat(lua): 插件可调内核动作面(R4-c)+ 凭据保险库(R4-d) 用户指出「接口不全」——此前插件只有 __llmsproxy_adapters 三个手写闭包, 够不到内核。对照参考插件(dsh-our-free-model,13 个账号渠道)的真实能力, 缺的正是这两块: 插件自己驱动链接认证 → 存凭据 → 定时领额度 → 把源接进内核 ## R4-c 内核动作面:__llmsproxy_kernel 18 个动作,全部映射到既有 Core 方法(不是新造能力): 只读 6:status / list_sources / list_models / list_adapters / list_auto_rules / adapter_owner 写 8:reload / reset_health / probe_all / save_auto_rules / save_key_auto / add_source / create_key / update_key 破坏性 4:remove_source / delete_key / remove_adapter / set_plugin_enabled **add_source 是「插件注册自己的上游」的落点**——插件拿到凭据后可以自己把 端点接进网关,无需运维改 config.yaml。已端到端验证:插件调它 → 内核源列表 出现 → 模型进入可路由列表。 依赖方向:lua 不能 import core,所以接口定义在 lua、实现放 core (kernel_actions.go),走 SetKernelAPI 注入——与 SetOutboundHosts 同一先例。 **凭据边界**:动作面**有意排除** list_keys/find_key。Core.ListKeys() 返回 明文网关密钥,交给第三方 Lua 等于交出整个部署的凭据面。有回归测试锁死 这条(TestKernelActionSurfaceExcludesPlaintextKeys)。list_sources 返回的 api_key 也强制掩码(只给 ***+后4位)。 **权限**(用户选「全开+设置页可配」):18 个动作全部可达,但**默认拒绝**, 授权落盘 .kernel-perms.json(独立文件,不并进 .access.json——后者是 map[string]string,塞进去会破坏旧二进制能读的格式)。danger 级不接受通配符。 插件可在源码 plugin.kernel={...} **申请**,设置页显示申请 vs 已授予。 **Go 侧 deadline**(用户选定):每个动作有超时预算,超时把 (nil,err) 还给 Lua。这可行的原因:Lua 只是等返回值,不占 Lua 栈执行权——与「Lua 死循环 不可中断」是两件事。 ## R4-d 凭据保险库:__llmsproxy_secrets 此前插件存 token 只能写 plugin.state:0755 目录、明文、与计费累计量同文件、 会被 PUT /state 整体替换、会进 WebUI。现在独立文件: <plugin_dir>/.secrets/<plugin>.json 文件 0600,目录 0700 临时文件**先 chmod 再 rename**,全程不存在宽权限窗口。读取身份在插件 boot 时绑定(不从 Lua 状态现读)⇒ 跨插件读不可行是机制而非约定。插件删除时清空 (DeleteAllSecrets + 启动 PurgeOrphanVaults)。 ## 开发中被测试抓出的两个真实缺陷(我自己引入的) 写了 TestNoKernelActionDeadlocksFromLua 对**每个**动作断言「在插件 Lua 调用 (此时持有 p.mu)内不得死锁」,抓出: 1. **自死锁**:set_plugin_enabled → SetEnabled → p.mu.Lock(),而死锁发生在 调用者已持有的同一把锁上。 2. **更隐蔽的迟到副作用**:超时把 error 返回给插件后,动作仍在幕后执行完成—— 插件以为失败,实际已生效。 修法:标注 Deferred 的动作在本次调用返回后异步执行,如实返回 accepted:true 而不是谎报成功。18 个动作全部复验无死锁,另有单独测试确认副作用真的落地 (只测「不死锁」的话,一个全空转的 dispatch 也能通过)。 测试 11 个(core 8 + lua 3):动作可用/密钥掩码/默认拒绝/授权计数/ 插件注册源并变为可路由/授权落盘重启保留/未知动作名被拒/凭据面排除明文/ 全动作无死锁/破坏性动作生效/Lua 侧桥接可用。 | 25 天前 | |
docs: 对齐分支命名实际用法 + 修正示例配置的存储位置说明 三处都是"文档说的"与"仓库实际做的"不一致,读文档的人会被误导。 1. 分支命名:文档写 release/vX.Y.Z 且举例 release/v1.4.2,实际从 release/v1.4.x 起一律用字面 x(release/v1.5.x / v1.7.x)。改成实际情况, 并说明"一条 minor 分支跨多个 patch"是刻意的:patch 是同批功能的修订, hotfix 落同一条分支,回流 main 时不必处理多条 release 分支间的依赖。 2. 退役规则与实践不符:文档说"下个版本发布就删上一个 release 分支",但 release/v1.4.x / v1.5.x 至今仍在。保留无害(hotfix 已回流),但与规则 矛盾。两份文档都如实记下这个出入,并记录特性分支**确实**在清理——本次 删除的四个分支都逐提交用 git patch-id 核对过,工作已全部进入 main (唯一 patch-id 不同的是 bf0657b 的发布前变体,diff 过 api.go 改动逐字节 相同)。留着只是给下个版本制造 cherry-pick/merge 陷阱。 3. 存储位置:示例配置说"WebUI 新增/编辑的源会写入 runtime_file",且默认 模型注释说 AUTO"按各源 priority 自动选最高可用源"。两处都与实现相反—— 上游源、网关密钥、AUTO 链都在 config.yaml(AddSource 走 UpsertSourceInYAML,密钥与链走 cfg.Save);runtime.json 只剩源模板、 删除标记和预置模板名单。已核实 store.Upsert(runtime 源)已无调用点, store 里的 Keys/Auto 只被加解密、从不写入,是遗留字段。 priority 本身不是完全没用,所以注释保留并说清它的两个真实用途:首次启动 seedAuto 的初值,以及 AUTO 生图链未配置时挑"最佳模型"(bestChatModel / bestImageModel 按 priority 取最大)。 | 1 个月前 | |
docs: 对齐分支命名实际用法 + 修正示例配置的存储位置说明 三处都是"文档说的"与"仓库实际做的"不一致,读文档的人会被误导。 1. 分支命名:文档写 release/vX.Y.Z 且举例 release/v1.4.2,实际从 release/v1.4.x 起一律用字面 x(release/v1.5.x / v1.7.x)。改成实际情况, 并说明"一条 minor 分支跨多个 patch"是刻意的:patch 是同批功能的修订, hotfix 落同一条分支,回流 main 时不必处理多条 release 分支间的依赖。 2. 退役规则与实践不符:文档说"下个版本发布就删上一个 release 分支",但 release/v1.4.x / v1.5.x 至今仍在。保留无害(hotfix 已回流),但与规则 矛盾。两份文档都如实记下这个出入,并记录特性分支**确实**在清理——本次 删除的四个分支都逐提交用 git patch-id 核对过,工作已全部进入 main (唯一 patch-id 不同的是 bf0657b 的发布前变体,diff 过 api.go 改动逐字节 相同)。留着只是给下个版本制造 cherry-pick/merge 陷阱。 3. 存储位置:示例配置说"WebUI 新增/编辑的源会写入 runtime_file",且默认 模型注释说 AUTO"按各源 priority 自动选最高可用源"。两处都与实现相反—— 上游源、网关密钥、AUTO 链都在 config.yaml(AddSource 走 UpsertSourceInYAML,密钥与链走 cfg.Save);runtime.json 只剩源模板、 删除标记和预置模板名单。已核实 store.Upsert(runtime 源)已无调用点, store 里的 Keys/Auto 只被加解密、从不写入,是遗留字段。 priority 本身不是完全没用,所以注释保留并说清它的两个真实用途:首次启动 seedAuto 的初值,以及 AUTO 生图链未配置时挑"最佳模型"(bestChatModel / bestImageModel 按 priority 取最大)。 | 1 个月前 | |
feat(adapter): move per-source error condensing into transform_error hooks Every upstream formats errors differently, which is adapter territory: the protocol gains an optional transform_error(status, body) hook and all built-in adapters implement their own envelope parsing (zen free-pool labels, anthropic/gemini/ollama/mistral shapes, sensenova quota notes, agentrouter WAF pages). The core keeps a single uniform fallback: when no hook yields a reason clients get "api error <status>: unknown error" and the raw body goes to server logs only. | 2 个月前 | |
feat(adapters): 入向端点移交适配器 —— adapter.ingress + deserialize_request,内核主动让出 plan.md 的 R2 目标里有三行钩子契约,出向两条(serialize_response/serialize_chunk) Phase 2 已落地,第三条 parse_request(protocol_body) -> unified 一直空着: /v1/messages 的入向解析硬编码在内核 Go 里(anthropicRequest.toUnified), Anthropic 改协议就得改内核。本次补齐这条对偶。 ## 机制(与出向序列化对称) - 适配器声明 adapter.ingress = "/v1/messages" 认领入站路径,并实现 deserialize_request(raw_body) -> unified_json。 - 内核 handleMessages 经新增的 parseViaIngress 判定: - 有适配器认领该路径且实现了钩子 → 让出,解析由适配器完成; - 无适配器认领 → 内核内置解析运行(R5 兜底,未配置部署仍可服务)。 - 认领了但钩子执行失败 → 硬错误 400,不静默换用另一种协议解析 (客户端说的正是该适配器声称支持的协议,悄悄换解析器只会错误调度)。 - 这是内核里唯一的新判定点,与 serializerFor 对称:出向选序列化器,入向选解析器。 ## 内核改动 - internal/lua/vm.go:staticInfo 增 ingress 字段;inspectScript 读取; 新增 VM.AdapterForIngress(path)(确定性选择:同名多认领取字典序最小)。 - internal/gateway/messages.go:handleMessages 改为先读 body → parseViaIngress (让出)→ 未认领时回落内置 anthropicRequest.toUnified。 内置解析与 anthropicRequest 类型保留——它们是兜底,不是死代码。 ## 适配器改动 - internal/lua/adapters/anthropic.lua:声明 adapter.ingress = "/v1/messages", 新增 deserialize_request,把内核原 Go 解析逻辑逐字段移植(system 两种拼写、 content 原样透传、缺省 thinking={enabled:false}、缺 messages 报错)。 内置 anthropic 默认随包播种,因此真实部署里内核总是让出给它, Go 侧 anthropicRequest.toUnified 退化为纯兜底。 ## 测试 - gateway:TestMessagesIngressYieldsToBundledAnthropic(让出后仍 200)、 TestAdapterForIngressClaim(声明可被找到、未声明路径为空、钩子存在)、 TestMessagesKernelFallbackWhenNoAdapterClaims(移除认领适配器后内核兜底仍 200)。 - lua:TestAnthropicDeserializeRequestShape 逐字段核对 Lua 与 Go 产出一致 (system 拼接、content、max_tokens/stream/temperature/top_p/top_k/stop_sequences、 缺省 thinking),TestAnthropicDeserializeRejectsMissingMessages。 - 全量回归:gateway/lua/core/config/types/provider/scheduler/e2e 全绿。 - 文档:docs/lua-adapters.md 补「入向让出钩子」章节 + plan.md 台账更新。 | 24 天前 | |
docs(plugins): 补「前缀代理」章节 —— 插件自起服务挂到网关同端口 | 21 天前 |
| 文件 | 最后提交记录 | 最后更新时间 |
|---|---|---|
| 16 天前 | ||
| 19 天前 | ||
| 22 天前 | ||
| 25 天前 | ||
| 1 个月前 | ||
| 1 个月前 | ||
| 2 个月前 | ||
| 24 天前 | ||
| 21 天前 |