fix(channels): 四渠道修复 —— qoder 裸 token 领取 / trae+buddy 非流式 / opencode 目录 / 隐藏适配器

生产实测逐渠道排查(2026-10-19):

* qoder:PKCE 设备码流无 register 第二跳,落库凭据是裸 token 字符串;
  qoderClaim 硬要求结构化 table 导致「缺少凭据对象」(而 account_test
  走 token 路径正常 —— 自相矛盾状态)。改为非空裸 tok 就地包
  {access_token=tok};补 TestQoderClaimAcceptsBareTokenCredential。

* trae(SOLO 自装适配器):上游对非流式请求也回 SSE(event: 前缀帧),
  transform_response 不识别 → 整个 body 原样透传 → Go unmarshal 报
  invalid character 'e'。补 aggregate_solo_sse 聚合。

* buddy(CodeBuddy):上游 400 11101 只支持流式。transform_request 强制
  stream=true;transform_response 补 SSE 聚合(tool_calls 按 index 拼分片)。

* provider 流式:真实 tool-call 片段之后出现 finish_reason=stop 时归一为
  tool_calls(trae 实测如此发);buddy/trae 不再序列化空 tool_calls 表。

* opencode:远端目录端点改为 /zen/v1/models(裸 /v1/models 回 HTML);
  兜底目录同步 2026-10-19 实测(space-bunny-free 410 下架等 5 项变化)。
  opencodezen 适配器 v1.1.0:会话亲和(client_session / 首 user 消息指纹
  分桶),保留免费层门禁四要素。

* 内核:适配器可声明 adapter.hidden = true 退出用户可见列表;被源引用的
  隐藏适配器保持可见(源编辑器要回显当前值)。opencodezen 默认隐藏。

* channels UI:切渠道立即清空账号/模型区 + 响应代际校验,修复 qoder 账号
  闪现在 opencode 卡片下的渲染竞态。

* channels request_end hook:挂接丢失的 isModelGone/recordDeadModel ——
  真实 404(如 codearts GLM-5.3 "The model is not registered")现在会进
  deadModels,不再每轮 404 仍可选中。
This commit is contained in:
root
2026-10-19 22:32:18 +08:00
parent c9f34538ec
commit 2113bfc125
11 changed files with 585 additions and 21 deletions

View File

@ -93,6 +93,12 @@ type staticInfo struct {
// but once an adapter claims the endpoint, protocol iteration lives in the
// adapter, not the kernel. Empty means the adapter claims no ingress.
ingress string
// hidden marks an adapter that should NOT appear in the user-facing adapter
// list (GET /api/adapters → UI 的适配器页与源编辑器的适配器下拉)。隐藏的
// 适配器仍然正常加载、仍然可以被源引用——它只是不从选择列表里冒出来。
// 用途:厂商探针类/内部实验类适配器(如 opencodezen 的免费层门禁形态),
// 直接暴露会让运维误当成通用 OpenAI 适配器去选。声明方式:adapter.hidden = true。
hidden bool
}
// adapterPool owns the worker states for one adapter. Idle workers are held in
@ -596,6 +602,11 @@ type VM struct {
dir string
pools map[string]*adapterPool
// hiddenRefs 记录"被源引用的隐藏适配器"。hidden=true 的适配器默认不进
// ListAdapters,但被源引用时必须保持可见(源编辑器要能回显当前值)。
// 由内核在配置装载/源变更时调用 SetHiddenRefs 维护;nil = 未标记。
hiddenRefs map[string]bool
// janitor drives elastic shrinking. One goroutine serves every pool (not
// one per adapter), so an idle gateway costs a single sleeping goroutine.
janitorOnce sync.Once
@ -603,6 +614,21 @@ type VM struct {
janitorDone chan struct{}
}
// SetHiddenRefs 供内核同步"哪些隐藏适配器正被源引用"。传 nil/空集 = 没有引用。
func (v *VM) SetHiddenRefs(names []string) {
v.mu.Lock()
defer v.mu.Unlock()
if len(names) == 0 {
v.hiddenRefs = nil
return
}
m := make(map[string]bool, len(names))
for _, n := range names {
m[n] = true
}
v.hiddenRefs = m
}
func NewVM(dir string) *VM {
return &VM{
dir: dir,
@ -799,14 +825,32 @@ func (v *VM) ConfigureConcurrency(adapterConcurrency map[string]int) {
func (v *VM) ListAdapters() []APIAdapter {
v.mu.RLock()
defer v.mu.RUnlock()
// hidden=true 的适配器不进用户可见列表(仍正常加载、仍可被源引用)。
// 例外:**正在被某个源引用**的隐藏适配器必须仍出现在列表里——否则源编辑器
// 打开时找不到当前值,下拉会被重置成空,造成"保存后适配器悄悄丢了"。
// 引用关系由内核持有(core 在配置装载时标记),VM 只看这个位。
keepHidden := v.hiddenKeep()
list := make([]APIAdapter, 0, len(v.pools))
for name, p := range v.pools {
if p.static.hidden && !keepHidden[name] {
continue
}
list = append(list, APIAdapter{Name: name, Version: p.static.version})
}
sort.Slice(list, func(i, j int) bool { return list[i].Name < list[j].Name })
return list
}
// hiddenKeep returns the set of hidden adapter names that must STAY visible
// because at least one source references them. nil 表示内核未标记(测试环境/
// 未装载配置),此时隐藏适配器一律隐藏——与"新适配器默认隐藏"的预期一致。
func (v *VM) hiddenKeep() map[string]bool {
if v.hiddenRefs == nil {
return nil
}
return v.hiddenRefs
}
// AdapterForIngress returns the registered name of the adapter that CLAIMS the
// given client-facing endpoint path (its static.ingress), or "" when none does.
//
@ -1255,6 +1299,10 @@ func inspectScript(name, code string) (*staticInfo, error) {
info.endpoint = readString(L, -1, "endpoint")
info.endpointStream = readString(L, -1, "endpoint_stream")
info.ingress = readString(L, -1, "ingress")
// hidden:适配器可声明 adapter.hidden = true 退出用户可见列表(仍可被源引用)。
L.GetField(-1, "hidden")
info.hidden = L.ToBoolean(-1)
L.Pop(1)
if getFieldBatch(L, "headers") {
luaTableToMap(L, -1, info.headers)
L.Pop(1)