mslxdff 0.1.158 → 0.1.159
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +3 -3
- package/docs/ARCHITECTURE.md +0 -426
- package/docs/FEATURE_TREE.md +0 -164
- package/docs/MOBILE.md +0 -82
- package/docs/adr/0001-reasoning-content-injection.md +0 -14
- package/docs/adr/0002-models-free-filter.md +0 -12
- package/docs/adr/0003-zero-state-no-auth.md +0 -10
- package/docs/adr/0004-bearer-token.md +0 -18
- package/docs/adr/0005-peer-mesh.md +0 -53
- package/docs/adr/0006-broadband-member.md +0 -103
- package/docs/adr/0007-multi-provider-prefix.md +0 -25
- package/docs/adr/0008-share-keys-to-peers.md +0 -52
- package/docs/adr/0009-chat-repl.md +0 -37
- package/docs/adr/0010-allowlist.md +0 -36
- package/docs/adr/0011-broadband-stream.md +0 -30
- package/docs/adr/0012-responses-endpoint-codex-sync.md +0 -48
- package/docs/adr/0013-node16-compat.md +0 -41
- package/docs/adr/0014-deepseek-provider.md +0 -52
- package/docs/adr/0015-upstream-probe-routing.md +0 -50
- package/docs/adr/0016-model-capabilities.md +0 -27
- package/docs/adr/0017-ai-sdk-upstream-engine.md +0 -59
- package/docs/adr/0018-zen-client-identity.md +0 -52
- package/docs/adr/0019-share-keys-always-lend.md +0 -59
- package/docs/adr/0020-zen-free-lane-agent-shape.md +0 -52
- package/docs/adr/0021-usage-report-jsonl.md +0 -47
- package/docs/adr/0022-models-capability-merge.md +0 -61
- package/docs/adr/0023-key-provider-default-direct.md +0 -58
- package/docs/adr/0024-node18-baseline.md +0 -63
- package/docs/adr/0025-workbuddy-authdir-follows-state.md +0 -71
- package/docs/adr/0026-cline-provider-id-unify.md +0 -67
- package/docs/adr/0027-codearts-provider.md +0 -48
- package/docs/adr/0028-traework-provider.md +0 -34
- package/docs/adr/0029-qoder-native-provider.md +0 -60
- package/docs/adr/0030-models-list-scoped-by-picks.md +0 -53
- package/docs/adr/0031-qoder-true-streaming.md +0 -50
- package/docs/adr/0032-generic-responses-channel.md +0 -72
- package/docs/adr/0033-cline-allowlist-auto-sync.md +0 -75
- package/docs/adr/0034-request-level-human-readable-observability.md +0 -60
- package/docs/adr/0035-sdk-channel-headers-timeout.md +0 -49
- package/docs/adr/0036-qoder-per-request-sticky-account.md +0 -82
- package/docs/adr/0037-qwenwork-independent-provider.md +0 -82
- package/docs/adr/0038-zcode-provider.md +0 -140
- package/docs/agents/domain.md +0 -51
- package/docs/agents/issue-tracker.md +0 -30
- package/docs/agents/triage-labels.md +0 -15
- package/docs/cli_help.md +0 -1391
- package/docs/plans/bench-via-latency-2026-09-01.md +0 -215
- package/docs/plugins.md +0 -187
|
@@ -1,61 +0,0 @@
|
|
|
1
|
-
# ADR-0022: /v1/models 能力富化(capabilities 内联)
|
|
2
|
-
|
|
3
|
-
日期:2026-09-26 状态:已实现
|
|
4
|
-
|
|
5
|
-
## 背景
|
|
6
|
-
|
|
7
|
-
ADR-0016 决定"`/v1/models` 形状不动(Codex 红线)",能力元数据走独立端点
|
|
8
|
-
`GET /v1/models/capabilities`。实践发现两个问题:
|
|
9
|
-
|
|
10
|
-
1. **客户端不可见**:OpenAI 兼容客户端(PI-Desktop、Cline、Codex 等)从 `/models`
|
|
11
|
-
只拿 `id`,不会探测我们的自定义 capabilities 端点——PI-Desktop 源码实测
|
|
12
|
-
(`apps/desktop/electron/main/model-discovery.ts` 只取 `id`/`display_name`),
|
|
13
|
-
能力靠 models.dev 目录按 id 匹配,我们的 `-free` 后缀与自建网关前缀
|
|
14
|
-
(`opencode-go/`、`workbuddy/`)匹配不上,全部落 128k 通用基线。
|
|
15
|
-
2. **自己也看不清**:CLI/`curl` 一条命令看不到"这个模型能干嘛"。
|
|
16
|
-
|
|
17
|
-
## 决策
|
|
18
|
-
|
|
19
|
-
把能力合并进 `/v1/models` 每条 data 条目的 **`capabilities` 子对象**(嵌套,不与
|
|
20
|
-
上游透传字段冲突):
|
|
21
|
-
|
|
22
|
-
```json
|
|
23
|
-
{ "id": "glm-5.3-free", "object": "model", "owned_by": "opencode",
|
|
24
|
-
"capabilities": { "reasoning": true, "effortType": "effort",
|
|
25
|
-
"effortValues": ["low","high","max"], "imageInput": false,
|
|
26
|
-
"inputModalities": ["text"], "outputModalities": ["text"], "toolCall": true,
|
|
27
|
-
"context": 1000000, "maxOutput": 131072, "costIn": 1.4, "costOut": 4.4,
|
|
28
|
-
"endpoints": ["chat","responses"], "upstreamApi": "chat" } }
|
|
29
|
-
```
|
|
30
|
-
|
|
31
|
-
字段语义:
|
|
32
|
-
|
|
33
|
-
- `reasoning`/`effortType`/`effortValues`/`defaultEffort`(workbuddy 原生源才有):
|
|
34
|
-
推理能力与档位(同 ADR-0016 caps 形状)
|
|
35
|
-
- `imageInput`/`inputModalities`/`outputModalities`:模态
|
|
36
|
-
- `toolCall`:工具调用;`context`/`maxOutput`:上下文/最大输出 token
|
|
37
|
-
- `costIn`/`costOut`:$/M tokens
|
|
38
|
-
- `endpoints`:网关两侧端点恒 `["chat","responses"]`(`/v1/responses` 复用
|
|
39
|
-
ChatPipeline 翻译层,任何模型都收)
|
|
40
|
-
- `upstreamApi`:上游原生协议(`chat` | `responses`)——muse-spark* 与 models.dev
|
|
41
|
-
标 `npm:@ai-sdk/openai` 的模型为 `responses`
|
|
42
|
-
|
|
43
|
-
实现要点(`src/model-capabilities/merge.js` + `modelsHandler` 挂接):
|
|
44
|
-
|
|
45
|
-
- **逃生门**:`GET /v1/models?raw=1` 回 ADR-0016 原始透传形状
|
|
46
|
-
- **Codex 不变**:codex 调用者(UA `codex_*`/`?client_version=`)不富化,
|
|
47
|
-
仍原始形状 + 顶层空 `models:[]`(ADR-0012 红线保持)
|
|
48
|
-
- **零阻塞**:能力目录走 `readyWarm()`(内存/磁盘缓存热身,绝不网络请求);
|
|
49
|
-
冷缓存时后台拉新、本请求原样返回,下次请求即富化
|
|
50
|
-
- **匹配链**:裸 id 精确 → 剥 `-free` 后缀 → 二级厂商前缀
|
|
51
|
-
(`clinebot/deepseek/x` → `deepseek/x`)→ workbuddy 走上游原生字段兜底
|
|
52
|
-
- **best-effort**:目录/workbuddy 源不可用 → 对应条目原样(无 capabilities 键),
|
|
53
|
-
绝不让 /models 失败
|
|
54
|
-
|
|
55
|
-
## 后果
|
|
56
|
-
|
|
57
|
-
- 响应体变大(每条 +~300B);标准 OpenAI 客户端忽略多余字段,向后兼容
|
|
58
|
-
- ADR-0016 的"`/v1/models` 形状不动"由本 ADR 修订;`?raw=1` 保留旧行为
|
|
59
|
-
- tests:`test/models-capabilities-merge.test.js`(merge 纯函数 + 降级路径)
|
|
60
|
-
|
|
61
|
-
> 备注(2026-09-20 追加,原文不改):0.1.x 起 Cline 供应商 id 统一为 `cline`(历史 `clinebot` / `cline-bot` 仅作一次性入站归一,见 ADR-0026)。
|
|
@@ -1,58 +0,0 @@
|
|
|
1
|
-
# ADR-0023: key 供应商默认直连(仅 opencode 免费池走 peer 兜底)
|
|
2
|
-
|
|
3
|
-
**日期**: 2026-09-20
|
|
4
|
-
**状态**: 已实施
|
|
5
|
-
|
|
6
|
-
## 背景
|
|
7
|
-
|
|
8
|
-
组员 peer 质量不稳定:实测 key 供应商(bai/ocgo 等)本机直连可用时,失败仍走 peer 竞速,
|
|
9
|
-
peer 侧大量 `502 peers failed: 127.0.0.1:3500 fetch failed / 141.98.198.197:8989 fetch failed /
|
|
10
|
-
152.67.222.233:62028 aborted / 172.93.221.187:8989 嵌套 peers failed`,错误详情反压回调用方,
|
|
11
|
-
把"本可直连成功"的请求拖成整单 502。用户明确要求:**所有使用 apikey 的上游供应商默认不要走
|
|
12
|
-
peer,本机直连失败就直接返回上游错误**;opencode 免费池(无 key、图额度)保留 peer 兜底。
|
|
13
|
-
|
|
14
|
-
此前 cline 系已按此语义落地(`classifyProvider` local-only + `isHardLocalOnly`),但 bai/sensenova/
|
|
15
|
-
aihubmix/ocgo/internapi/tokenrouter 等 latency-compare 供应商仍在失败后走 hedge/peer/broadband/
|
|
16
|
-
via-route,本 ADR 把"默认直连"从 cline/workbuddy 扩大到**一切带前缀的 key 供应商**。
|
|
17
|
-
|
|
18
|
-
## 决策
|
|
19
|
-
|
|
20
|
-
### 1. 路由语义(`src/state/schemas/use-group.js` 单点收敛)
|
|
21
|
-
|
|
22
|
-
- `providerHeadOf(model)`:取供应商 head,兼容 canonical(`bai/x`)、dash 别名(经
|
|
23
|
-
`getModelAlias` 还原后取 head)、裸 id(归 `opencode`);`oc/` 经 `normalizeProviderId`
|
|
24
|
-
归一到 `opencode`(别名表 key 小写,先小写再归一)。
|
|
25
|
-
- `isKeyProviderDirectOnly(model)`:head 非空且非 `opencode` 即默认直连;local-only
|
|
26
|
-
(cline/workbuddy)恒 true;`MSLXDFF_USE_GROUP_KEYS=1/on` 可显式开回(cline/workbuddy
|
|
27
|
-
仍硬禁,不受该 env 影响)。
|
|
28
|
-
- `shouldUseGroupForModel`:`isHardLocalOnly → false`;`isKeyProviderDirectOnly → false`;
|
|
29
|
-
否则沿用全局开关(`MSLXDFF_USE_GROUP` env > `state.json useGroup`,默认 on)。
|
|
30
|
-
即:**全局 off 时一切都不走组员;全局 on 时也只有 opencode 能走组员**。
|
|
31
|
-
|
|
32
|
-
### 2. 收敛点(零散逻辑不动,只换判断 + 补日志)
|
|
33
|
-
|
|
34
|
-
- `src/chat-pipeline/serial-trial.js`:via-route 前置、hedge(经 `canUseGroup`)、peer、
|
|
35
|
-
broadband 四处沿用 `shouldUseGroupForModel`;`group-skip` reason 三态
|
|
36
|
-
(local-only / key-default-direct / useGroup=off)并带 `(via-route|peer|broadband)`
|
|
37
|
-
后缀;via-route 被跳过时也打一条 `group-skip`(此前静默)。
|
|
38
|
-
- `-use-group` CLI 查询/设置文案同步(opencode 专用 + keys env 说明)。
|
|
39
|
-
- 后台探针(`upstream-probe`)与 `bench --via` 不动:探针继续为 key 供应商保鲜
|
|
40
|
-
`via-routes.json`,`MSLXDFF_USE_GROUP_KEYS=1` 开回后即有数据可用。
|
|
41
|
-
|
|
42
|
-
### 3. 逃生门
|
|
43
|
-
|
|
44
|
-
- `MSLXDFF_USE_GROUP_KEYS=1`:把 key 供应商开回组员(调试/弱网应急);cline/workbuddy
|
|
45
|
-
仍硬禁;同样受全局 `MSLXDFF_USE_GROUP=0` / `-use-group off` 约束。
|
|
46
|
-
|
|
47
|
-
## 后果
|
|
48
|
-
|
|
49
|
-
- key 供应商失败路径:本机直连错误直接返回(`exhaustedLocal`),不再产生 peer 竞速流量,
|
|
50
|
-
不再把 peer 的嵌套 502 拼进调用方错误体。
|
|
51
|
-
- opencode 行为不变:直连先行、429/5xx 后 peer/broadband 兜底(quota-pool)。
|
|
52
|
-
- `via-routes.json` 里 key 供应商的旧 `best: via:...` 条目在默认模式下不再被读取
|
|
53
|
-
(`shouldUseGroupForModel` 前置拦截),开回后自动恢复,无需清表。
|
|
54
|
-
|
|
55
|
-
## 相关文件
|
|
56
|
-
|
|
57
|
-
`src/state/schemas/use-group.js`、`src/chat-pipeline/serial-trial.js`、
|
|
58
|
-
`src/cli/commands/use-group.js`、`test/use-group.test.js`
|
|
@@ -1,63 +0,0 @@
|
|
|
1
|
-
# ADR-0024: 运行底线改为 Node 18+(撤销 ADR-0013 的 Node 16 兼容层)
|
|
2
|
-
|
|
3
|
-
**日期**: 2026-09-20
|
|
4
|
-
**状态**: 已实施(取代 ADR-0013)
|
|
5
|
-
|
|
6
|
-
## 背景
|
|
7
|
-
|
|
8
|
-
ADR-0013 为了让 CentOS VPS 上的 Node v16.14.2 能跑,把 `engines` 降到 `>=16`,新增
|
|
9
|
-
`src/compat.js` 作为 fetch / AbortSignal.timeout / structuredClone / randomUUID 的单一
|
|
10
|
-
出口,并在 ADR 结论里写下了「**Node 16 全功能可用(daemon + `-chat` + bench + providers)**」。
|
|
11
|
-
|
|
12
|
-
该结论经实测**不成立**。Node 16 缺的不只是 `fetch`,还缺 Web 标准全局对象
|
|
13
|
-
`Response` / `Headers` / `ReadableStream` / `TransformStream`(真 Node 16.20.2 实测,
|
|
14
|
-
四者全为 `undefined`;Node 18.0 起才默认暴露)。而本项目的响应构造处处依赖它们:
|
|
15
|
-
|
|
16
|
-
- `src/free-lane.js`(`aggregateChatSse`:**非流式 + 免费模型**的必经聚合路径)
|
|
17
|
-
- `src/upstream.js`(anon 重试 / 非 JSON 兜底重包)
|
|
18
|
-
- `src/upstream-responses.js`(Responses 端点翻译层)
|
|
19
|
-
- `src/upstream-engine/sdk/{attempt,responses}.js`(AI SDK 通道)
|
|
20
|
-
- `src/providers/workbuddy/{chat,reshape}.js`、`src/providers/cline/chat.js`
|
|
21
|
-
- `src/providers/dispatcher.js`(allowlist 403 响应)
|
|
22
|
-
|
|
23
|
-
合计 **29 处**裸 `new Response(...)` / `new Headers(...)` / `new ReadableStream(...)` /
|
|
24
|
-
`new TransformStream(...)`,而 `src/compat.js` 从未补这几个全局。
|
|
25
|
-
|
|
26
|
-
**实测证据**(本次改动期间用真 Node 16.20.2 执行 `aggregateChatSse`):
|
|
27
|
-
|
|
28
|
-
```
|
|
29
|
-
node: v16.20.2
|
|
30
|
-
THROWS: ReferenceError - Response is not defined
|
|
31
|
-
```
|
|
32
|
-
|
|
33
|
-
即 Node 16 上「非流式 + 免费模型」这条最常用路径 100% 崩,且 `src/runtime/bootstrap.js`
|
|
34
|
-
之后的转发链路同样必崩——所谓「全功能可用」是假。
|
|
35
|
-
|
|
36
|
-
## 决策
|
|
37
|
-
|
|
38
|
-
1. **运行底线 = Node >=18**:`package.json` 与 `package-lock.json` 的 `engines.node`
|
|
39
|
-
改为 `">=18"`。
|
|
40
|
-
2. **入口硬拦(规则可执行,不靠自觉)**:`src/compat.js` 新增 `MIN_NODE_MAJOR = 18` 与
|
|
41
|
-
`assertMinNode()`(人话报错 + 升级指引,返回 `false`),由 `src/cli/index.js` 的
|
|
42
|
-
`run()` 第一行调用,低于 18 时 `exit 1`——不给原生堆栈。`src/readline-compat.js` 的
|
|
43
|
-
`assertChatNode({ min })` 默认值改为引用 `MIN_NODE_MAJOR`,其 `nodeMajor()` 改为从
|
|
44
|
-
`compat.js` 再导出(消除双份实现,版本常量单一来源)。
|
|
45
|
-
3. **`src/compat.js` 保留,但降级为「fetch 实现的单一出口」**:undici 仍是显式依赖
|
|
46
|
-
(`Agent` 连接池 / keep-alive / connect.timeout 要用),故 `compatFetch` /
|
|
47
|
-
`timeoutSignal` / `clone` / `uuid` / `getUndici` 全部保留,且**禁止绕过它直接用
|
|
48
|
-
`globalThis.fetch`**;`AbortSignal.timeout` / `structuredClone` 在 18+ 已原生,
|
|
49
|
-
函数内兜底只为可读报错,**不再承诺 <18 可用**。
|
|
50
|
-
4. **18+ 的 Web 全局可直接用**:`Response` / `Headers` / `ReadableStream` /
|
|
51
|
-
`TransformStream` / `fetch` 无需再包一层 shim(这正是 ADR-0013 漏掉的部分),
|
|
52
|
-
29 处裸用保持原样。
|
|
53
|
-
|
|
54
|
-
## 后果
|
|
55
|
-
|
|
56
|
-
- 旧 VPS(Node 16)**必须升级**才能跑新版;CLI 会在入口打印
|
|
57
|
-
`[运行环境不满足] mslxdff 需要 Node 18+,当前 v16.x` 并 `exit 1`,
|
|
58
|
-
而不是跑到一半 `ReferenceError`。
|
|
59
|
-
- `undici` 仍锁 `^5.28.4`(8.x 需 Node 22.19+),本次不动——18+ 上 undici 5 完全可用。
|
|
60
|
-
- 未来若真要回退到 <18,**必须**先为 `Response` / `Headers` / `ReadableStream` /
|
|
61
|
-
`TransformStream` 提供 shim 并补齐测试,不能只改 `engines`(ADR-0013 就是这么错的)。
|
|
62
|
-
- 相关同步:`AGENTS.md` 新增「运行环境(强制):Node >=18」章节;ARCHITECTURE.md §7
|
|
63
|
-
`compat.js` 说明、§8 索引(0013 标注被撤销 + 本条目)。
|
|
@@ -1,71 +0,0 @@
|
|
|
1
|
-
# ADR-0025: WorkBuddy 凭据目录跟随 state 文件(撤销 cwd 兜底)
|
|
2
|
-
|
|
3
|
-
**日期**: 2026-09-20
|
|
4
|
-
**状态**: 已实施
|
|
5
|
-
|
|
6
|
-
## 背景
|
|
7
|
-
|
|
8
|
-
`resolveAuthDir()` 旧实现的判定链(`src/providers/workbuddy/account-store.js`):
|
|
9
|
-
|
|
10
|
-
```js
|
|
11
|
-
if (process.env.WORKBUDDY_AUTH_DIR) return ... // ① 显式 → 听用户的
|
|
12
|
-
const sf = process.env.MSLXDFF_STATE_FILE || "";
|
|
13
|
-
if (sf.includes("mslxdff-test") || isTestEnv()) return tmpdir/auths; // ② 测试 → 临时目录
|
|
14
|
-
if (sf && sf.includes("mslxdff-")) return dirname(sf) + "/auths"; // ③ ← 生产里恒不命中
|
|
15
|
-
return join(process.cwd(), "auths"); // ④ ← 生产实际默认
|
|
16
|
-
```
|
|
17
|
-
|
|
18
|
-
第 ③ 条要求 state 路径里出现**连字符** `mslxdff-`,而默认路径是 `~/.config/mslxdff/state.json`
|
|
19
|
-
(`mslxdff` 后紧跟 `/`)→ 不命中。全仓 grep 确认**没有任何生产代码设置 `MSLXDFF_STATE_FILE`**
|
|
20
|
-
(只有测试设,且测试值含 `mslxdff-test`,已被第 ② 条截走)。于是 ③ 是死代码,
|
|
21
|
-
**④「进程 cwd 下的 auths」才是生产环境的实际默认**。
|
|
22
|
-
|
|
23
|
-
而 `workbuddy-<uid>.json` 里装的是企业 `accessToken` + 长效 `refreshToken`(兜底 60 天)。
|
|
24
|
-
|
|
25
|
-
**真实事故(本机实测)**:`state.json` 登记 2 个账号,但文件分居两处 ——
|
|
26
|
-
`a06ef5f8…` 在 `~/.config/mslxdff/auths/`,`c1d764f5…` 在 **`项目根/auths/`**;
|
|
27
|
-
后者正是 cwd 兜底的产物(09/03 那次 daemon 是在项目根起的)。同目录还混进了测试残留
|
|
28
|
-
`workbuddy-uid1.json`(内容是测试夹具 `k-new`/`rt-new`,已删除)。
|
|
29
|
-
|
|
30
|
-
危害两条:
|
|
31
|
-
|
|
32
|
-
1. **安全**:全局安装(`npm i -g mslxdff`)后可在任意目录起服务 → 企业长效 refreshToken
|
|
33
|
-
写进那个目录;而 `.gitignore` 只管自己所在的仓库,一次 `git add -A` 就可能把长效凭据
|
|
34
|
-
提交进第三方仓库。这违反项目安全红线「凭据必须落在 `.gitignore` 覆盖范围内」。
|
|
35
|
-
2. **功能**:路径依赖「当初在哪敲命令」,换目录启动就读不到旧账号,表现为「配过却像没登录」,
|
|
36
|
-
且没有任何报错,极难排查。
|
|
37
|
-
|
|
38
|
-
(本次未泄露:`auths/` 在本仓 `.gitignore` 第 21 行 `**/auths/`,`git ls-files` 确认无 auths 文件被跟踪。)
|
|
39
|
-
|
|
40
|
-
## 决策
|
|
41
|
-
|
|
42
|
-
1. **写入唯一目标改为「跟账本走」**:新增纯函数
|
|
43
|
-
`authDirFor({ explicit, testEnv, stateFile })`,优先级
|
|
44
|
-
`WORKBUDDY_AUTH_DIR` > 测试隔离(`tmpdir()/mslxdff-test-auths`)>
|
|
45
|
-
`dirname(state文件)/auths`(默认 `~/.config/mslxdff/auths`)。
|
|
46
|
-
**彻底删掉 cwd 兜底**。抽成纯函数是为了可测:`node --test` 下 `isTestEnv()` 恒真,
|
|
47
|
-
环境态没法在测试里翻面。
|
|
48
|
-
2. **读取保留一层迁移兼容(只读)**:`authDirCandidates()` 返回
|
|
49
|
-
`[主位置, <cwd>/auths]`(显式指定或测试环境不兜底、与主位置相同时去重);
|
|
50
|
-
`listAccountDocs()` 按 uid 去重且**主位置优先**。旧位置里的账号因此不会「消失」,
|
|
51
|
-
下次 refresh 时自然写进主位置 —— 不做自动搬迁,避免误删唯一副本。
|
|
52
|
-
3. **读取侧收敛为单一出口 `listAccountDocs()`**:provider 构造
|
|
53
|
-
(`src/providers/workbuddy/index.js`)、CLI 账号加载与摘除(`src/cli/commands/workbuddy.js`)、
|
|
54
|
-
`-provider add`(`src/cli/commands/provider/add.js`)、独立脚本 `workbuddy-token-auto.js`
|
|
55
|
-
全部改走它 —— 此前是 **4 份各自 `readdir` + 各自 cwd 判定**的复制品。
|
|
56
|
-
4. **摘除要扫全部候选目录**:旧实现只删 `cwd/auths` 那一份,留下的旧副本会被读取兜底
|
|
57
|
-
「复活」账号。
|
|
58
|
-
|
|
59
|
-
## 后果
|
|
60
|
-
|
|
61
|
-
- 凭据与账本同处一地:`~/.config/mslxdff/{state.json,auths/}`,永远在 `.gitignore`
|
|
62
|
-
覆盖范围内(本仓另有 `auths/`、`**/auths/` 规则兜项目内场景)。
|
|
63
|
-
- 项目根那份历史副本**不迁移、不删除**:只读兜底继续读它,等该号下次 refresh 写进主位置。
|
|
64
|
-
手动清理用 `-workbuddy remove <uid>`(会扫全部候选目录)。
|
|
65
|
-
- `cli_help.md` / `cli_help_mini.md` 双镜像与 ARCHITECTURE Env 表的
|
|
66
|
-
`auths/workbuddy-<uid>.json` 表述统一为「`<state 目录>/auths/…`」。
|
|
67
|
-
- 边界:若显式把 state 指成无目录的相对名(`MSLXDFF_STATE_FILE=state.json`),
|
|
68
|
-
`dirname` 得 `.` → auths 仍相对 cwd。但这属于「用户主动把账本放进项目内」,
|
|
69
|
-
本仓 `.gitignore` 的 `auths/` 规则覆盖该场景。
|
|
70
|
-
- 测试:新增 `test/workbuddy-authdir.test.js`(6 例:写入目标不再依赖 cwd、显式/测试分支、
|
|
71
|
-
候选目录去重、主位置优先去重、非目标文件与坏 JSON 跳过)。
|
|
@@ -1,67 +0,0 @@
|
|
|
1
|
-
# ADR-0026:Cline 供应商 id 统一为 `cline`(+ 免费目录即清单)
|
|
2
|
-
|
|
3
|
-
- 状态:**已采纳**
|
|
4
|
-
- 日期:2026-09-20
|
|
5
|
-
- 关联:ADR-0015(供应商三态路由:`cline` = local-only)、ADR-0019(share-keys 硬排除 refresh-token 型凭据)、ADR-0022(`/v1/models` 能力合并)、`src/cli/commands/provider/cline-login.js`、`src/cli/commands/provider/cline-free.js`、`src/providers/cline/`、`src/state/migrations/cline-unify.js`
|
|
6
|
-
|
|
7
|
-
## 背景
|
|
8
|
-
|
|
9
|
-
Cline 供应商历史上挂了**两个 id**(`cline` 与 `clinebot`,另有别名 `cline-bot`),这不是笔误而是两个**写入点**造成的:
|
|
10
|
-
|
|
11
|
-
1. **双写**:`cline-login.js` 的 `for (const pid of ["cline", "clinebot"])` —— 每次 `login` 无条件写两份配置。
|
|
12
|
-
2. **轮换回写**:`src/providers/cline/index.js` 的 `saveFn` 里 `altId = id === "cline" ? "clinebot" : "cline"` —— 每次 refreshToken 轮换后把新 token 回写另一 id。
|
|
13
|
-
|
|
14
|
-
只要有一次 token 轮换,两份就被续上,**`clinebot` 无法自然消亡**。后果:
|
|
15
|
-
|
|
16
|
-
- `providers-setup.js` 为 `providerConfigs` 的**每个键**实例化一个 provider → daemon 内同时存在两个实例(各自持有相同账号池)。
|
|
17
|
-
- `/v1/models` 可能重复暴露 `cline/x` 与 `clinebot/x` 两套 id,CLI(`-providers list`/`bench`/`status`)也出现两条目。
|
|
18
|
-
- 用户面对"同一个供应商两个名字",无法判断该删哪个、留哪个。
|
|
19
|
-
|
|
20
|
-
## 决策
|
|
21
|
-
|
|
22
|
-
**一个供应商 = 一个 id = `cline`**:
|
|
23
|
-
|
|
24
|
-
1. **对外只产出 `cline`**:login 只写 `cline`,refreshToken 轮换只回写 `cline`,日志/CLI 只报 `cline`,`/v1/models` 只有 `cline/<上游裸 id>`(例 `cline/z-ai/glm-5.3-flash`)。
|
|
25
|
-
2. **入站一次性归一**:CLI 入口 `normalizeProviderId()` 把 `clinebot` / `cline-bot` 归一为 `cline`;`registry.js` 删掉 id 分支但**保留** `baseUrl.includes("cline.bot")` 兜底(残留旧配置仍命中 cline 工厂,不会降级成 generic 供应商)。
|
|
26
|
-
3. **老配置迁移**:`providerConfigs.clinebot` 合并进 `cline` 后删除旧键 —— keys 去重 + 剔除 `sk_` 形态(`cline` 只认 refreshToken)、allowlist 求并(裸 id 可直接搬,`cline-free/...` 不会被 `normalizeAllowedModel` 误剥,因为前缀 `cline-free` ≠ `cline`)、baseUrl 归一到 `https://api.cline.bot`。真会改动前先备份 `state.json.bak-<ISO 时间戳>`,并 append 一条 `cline-unify-migrated` 事件留痕(**不含任何凭据**)。迁移**幂等**(无 `clinebot` 即 no-op),三处触发:daemon bootstrap(providers 实例化之前)、`-provider cline migrate [--dry-run]`、login 写盘前。
|
|
27
|
-
4. **免费目录即清单**:上游 `GET /api/v1/ai/cline/recommended-models`(公开免鉴权)只取 `free` 数组(当前 5 个)。新增两个只读/一次写命令:
|
|
28
|
-
- `mslxdff -provider cline free [--json]`:**只读**列目录 + 与当前 `allowlist` 的差异;
|
|
29
|
-
- `mslxdff -provider cline free sync [--yes] [--json] [--keep-extra]`:把目录落成 `allowlist`(写裸 id),**默认 dry-run 预览,`--yes` 才落盘**,`--keep-extra` 只增不删。
|
|
30
|
-
手写 `allowlist` 仍是唯一对外清单(空名单 + `allowAny OFF` = `403` 的安全默认不变);接口只回目录不回余额,免费额度用尽仍靠上游 `429` 反推。
|
|
31
|
-
5. **`cline` 恒为 local-only(硬约束,不可回退)**:不经组员转发(`shouldUseGroupForModel("cline/…") === false`)、**不借出 key**(share-keys 硬排除 refresh-token 型凭据:借出后对端刷新会轮换 token,与本机互踢下线)、只走本地直连。历史别名 `clinebot` / `cline-bot` 同样硬排除 —— 只删除字符串、**绝不删除 `cline` 本身**。
|
|
32
|
-
|
|
33
|
-
## 备选
|
|
34
|
-
|
|
35
|
-
- **继续双 id,只在文档里说"另一个是别名"**:否决 —— 双实例双份 `/v1/models` 是实测行为,文档注释解决不了;且 token 轮换回写会让两份永远同时存在。
|
|
36
|
-
- **反向统一为 `clinebot`**:否决 —— `cline` 是 `createClineProvider` 的默认 id、`classify.js`/`share-keys.js`/`use-group.js` 既有保护都以 `cline` 书写,改名成本更高且语义更差。
|
|
37
|
-
- **用 `free sync` 取代手写 allowlist(自动跟随上游)**:否决 —— 上游目录变更会在用户不知情时改变对外模型集合;保持"同步是显式动作、默认 dry-run"。
|
|
38
|
-
- **把 `cline` 放开走组员以摊限流**:否决(安全/正确性)—— refresh-token 型凭据一旦借出,对端刷新即轮换,双方互踢下线;免费额度收益远小于掉线成本。
|
|
39
|
-
|
|
40
|
-
## 后果
|
|
41
|
-
|
|
42
|
-
- daemon 内只剩一个 `cline` 实例,`/v1/models` 只有一套 `cline/` 前缀;`-provider cline free sync --yes` 可把白名单一步对齐上游免费目录。
|
|
43
|
-
- 老脚本调 `-provider clinebot ...` 不会断:入口归一 + registry 的 baseUrl 兜底 + 迁移合并三处覆盖。
|
|
44
|
-
- 迁移不动 `modelPicks` / `modelErrors` / `modelLatencies` 里可能残留的 `clinebot/*` 键(避免误删),由既有 `prune --orphans` 机制清理。
|
|
45
|
-
- 文档双镜像(`cli_help.md` / `cli_help_mini.md` 根与 `docs/` 各两份)逐字节同步,改完以 SHA256 复验。
|
|
46
|
-
|
|
47
|
-
## 验证
|
|
48
|
-
|
|
49
|
-
- `test/providers-share-keys.test.js`:`cline` 仍在硬排除集合(refresh 型凭据不外借)。
|
|
50
|
-
- `test/use-group.test.js`:`shouldUseGroupForModel("cline/…") === false`(local-only 未被弱化)。
|
|
51
|
-
- `test/cline-provider.test.js` / `test/cline-chat-id.test.js`:前缀由注入 `id: "cline"` 派生,上游收到裸 id。
|
|
52
|
-
- `node --test --test-concurrency=1 test/*.test.js` + `node scripts/docs-check.js`。
|
|
53
|
-
|
|
54
|
-
## 补记(2026-09-20 端到端实测,原文不改)
|
|
55
|
-
|
|
56
|
-
§3 写的「baseUrl 归一到 `https://api.cline.bot`」**当时是错的**,真机会拼错 URL:
|
|
57
|
-
|
|
58
|
-
- 对话 URL = `baseUrl` + `loadProviderChatPath()`,而 `loadProviderChatPath()` **缺省恒返回
|
|
59
|
-
`/chat/completions`** —— `src/providers/cline/index.js` 里那个「按 `/api/v1` 智能兜底」的
|
|
60
|
-
`defaultChat` 因此是**死代码**,永远轮不到生效。
|
|
61
|
-
- 于是 `baseUrl=https://api.cline.bot` → `https://api.cline.bot/chat/completions` → 上游 **404**。
|
|
62
|
-
- 迷惑点:`/models` 路径由 `resolveModelsUrl` 自动补 `/api/v1`,所以只测列模型**看不出问题**,
|
|
63
|
-
必须真发一次 chat 才暴露(早期测试断言甚至把这个 bug 写了进去)。
|
|
64
|
-
|
|
65
|
-
已修:`normalizeClineBaseUrl()` 对 cline 官方域自动补全为 `https://api.cline.bot/api/v1`
|
|
66
|
-
(自定义/代理 baseUrl 不做猜测),线上用 `-provider cline set-url` 修正。教训:**凭据形状对了、
|
|
67
|
-
模型列表出来了,都不等于对话链路通了 —— 必须端到端发一次 chat。**
|
|
@@ -1,48 +0,0 @@
|
|
|
1
|
-
# ADR-0027:codearts 供应商(华为云 CodeArts Agent 盘古助手免费模型反代)
|
|
2
|
-
|
|
3
|
-
- 状态:**已采纳**
|
|
4
|
-
- 日期:2026-09-20
|
|
5
|
-
- 关联:ADR-0001(reasoning 占位注入)、ADR-0007(前缀路由)、ADR-0015(供应商三态路由:`codearts` = local-only)、ADR-0019(share-keys 硬排除 refresh-token 型凭据)、参考实现 [HITZY2002/codearts2api](https://github.com/HITZY2002/codearts2api)(Go,逆向协议来源)
|
|
6
|
-
|
|
7
|
-
## 背景
|
|
8
|
-
|
|
9
|
-
华为云 CodeArts Agent(盘古助手)向登录用户发放少量免费福利模型(种子集 `deepseek-v4-flash-0731` / `deepseek-v4-pro-0813` / `glm-5.3-flash`,以福利网关目录为准)。上游是 `snap-access.cn-north-4.myhuaweicloud.com` 的 maas 网关,鉴权不是单一 Bearer key,而是**三重链**:
|
|
10
|
-
|
|
11
|
-
1. **SDK-HMAC-SHA256 请求签名**:AK/SK/securityToken(STS 临时凭证,约 1h 过期)对 method+canonical URI(尾补 `/`)+ SignedHeaders 全头 + body sha256 hex 签名,签名头 `Authorization`/`X-Sdk-Date`/`x-stage` 等;`Chat-Id`/`Session-Id` 在签名后追加,`maas_type: benefit` 在签名前写入 body。
|
|
12
|
-
2. **DPoP proof**:`ES256`(P-256,低 S 归一化)JWT,公钥与 `refresh_token`、`client_id`、`code_verifier` **三重绑定**——换钥匙即死号,JWK 必须持久化。
|
|
13
|
-
3. **STS 刷新**:`POST https://sts.cn-north-4.myhuaweicloud.com/v1/oauth2/tokens` 用 refreshToken+codeVerifier+DPoP proof 换新临时凭证,**refresh_token 单次轮换**(用一次换一个,旧的立即作废)。
|
|
14
|
-
|
|
15
|
-
mslxdff 以原生 provider 接入(不做外部反代),纳入既有 keyring/allowlist/dispatcher/classify 体系。
|
|
16
|
-
|
|
17
|
-
## 决策
|
|
18
|
-
|
|
19
|
-
1. **前缀路由 `codearts/<modelId>`**(裸 id 恒指 opencode,ADR-0007 不变);`registry.js` 注册工厂,`classify.js` 归 local-only。
|
|
20
|
-
2. **凭证模型 = 一华为账号一 blob**:`providerConfigs.codearts.keys` 每元素是一个 JSON blob 字符串(`userId/userName/domainId/refreshToken/clientId/codeVerifier/dpopJwk/ak/sk/securityToken/expiration`)。多账号 = keyring 多 key 自动轮转。blob 存 state.json(已被 `**/*state*.json` gitignore,凭据零入库红线不破)。
|
|
21
|
-
3. **签名链单文件化**:`sign.js`(SDK-HMAC-SHA256,接受 `now` 参数便于 golden 测试)+ `dpop.js`(ES256 P-256 低 S;node:crypto 必须 `sign/verify("SHA256", input, {dsaEncoding:"ieee-p1363"})` 标准 JWS 形状——`dsaEncoding` 只管 r‖s 编码,`sign(null, sha256(input))` 的 noneWithECDSA 把摘要当标量:自验能过但 WebCrypto/华为 STS 拒签(真机 400 STS5.1804 实锤,已修 + WebCrypto 仲裁回归)。
|
|
22
|
-
10. **login 双通道并发(真机首登补记,2026-09-20)**:portal 带 `port` 参数的授权只走浏览器回调(ticket 永不下发),旧实现 `await` 轮询占死主循环 → 页面显示"回调已收到"但终端轮询到超时。改后台并发(`AbortController` 胜出方停另一方),回调通道绝不阻塞。`test/codearts-login.test.js` 死锁回归锁定。
|
|
23
|
-
4. **STS 生命周期**:临期(默认提前 30min)单飞刷新(inflight 去重);refresh_token 轮换后**原位换 blob 写回** `providerConfigs.codearts.keys`(saveFn 同时重排 ring/pool);瞬时失败保留旧凭证继续用(对齐 cline 纪律);`invalid_grant` 等终态 → 死号 + 人话提示 `mslxdff -provider codearts login`。
|
|
24
|
-
5. **三路模型发现**:`GET /v1/model/builtin`(模型 id 大小写归一 `canonical()`,对话按归一名发送)+ `GET /v1/agent-center/agents/useragents`(代理型)+ 福利网关 `GET /api/v1/gateway/config`(`benefit` 模型自动 `POST /api/v1/benefit/claim` 领取,`error_code:"0000"` 幂等视为成功;`MSLXDFF_CODEARTS_AUTO_CLAIM=0` 关)。
|
|
25
|
-
6. **对话恒 `stream:true`**(上游无非流式通道):SSE 事件的 `text` 字段是**全文快照**(非增量)——流式客户端转换为 OpenAI delta(快照差分),非流式聚合回 `chat.completion`。401/403 → invalidate + 强制刷新重试一次 → 仍败切号;**HTTP 200 内嵌错误码映射**:`tm.00001041`/tpm/并发会话 → 429、`002002009`/not registered/`4004.200`/benefit not found → 400、其余 → 502。
|
|
26
|
-
7. **chat_id 32hex**:body 透传 hex ≥32 位或 sha256 派生;`Session-Id` = `sha256("codearts-session:"+chatId)` 前 32 位(签名后追加头)。
|
|
27
|
-
8. **reasoning 占位注入沿用 ADR-0001**:`deepseek*` → 全部 assistant 补 `" "`;`kimi-*` → 仅 tool_calls 消息。
|
|
28
|
-
9. **login**:PKCE(verifier 64B base64url)+ 本地回调 server 收 code + ticket 轮询双通道;成功后 blob 落盘 `providerConfigs.codearts.keys`,默认 `allowAnyModels=true`(`--no-allow-any` 关),多账号重复 login 追加 = keyring 轮转。
|
|
29
|
-
10. **恒 local-only(硬约束,延续 0015/0019)**:不经组员转发/via-route;share-keys 硬排除(refresh_token+DPoP 绑定型凭据,外借 = 对端刷新轮换与本机互踢下线,同 cline)。`classify.js` 归 local-only + `share-keys.js` `NEVER_SHARE_IDS` 显式加 `codearts` 双保险。
|
|
30
|
-
|
|
31
|
-
## 备选
|
|
32
|
-
|
|
33
|
-
- **走外部 codearts2api Go 反代 + generic provider 接入**:否决 —— 多一个常驻进程与部署面,且丢失账号池/轮转写回/allowlist/dispatcher 语义的原生整合;Go 实现仅作协议参考。
|
|
34
|
-
- **DPoP 私钥放内存、每次启动重生成**:否决 —— refresh_token 与公钥三重绑定,重启即全号 401 死锁;JWK 必须随 blob 持久化。
|
|
35
|
-
- **模拟非流式(stream:false 直发上游)**:否决 —— 上游恒 stream:true,非流式只能 SSE 聚合回 JSON(`aggregateToCompletion`)。
|
|
36
|
-
- **AK/SK 明文单独存 keys(Bearer 形态复用 keyring)**:否决 —— AK/SK 是 STS 临时凭证快照,离开 refreshToken/codeVerifier/DPoP 三元组无法续期;按账号聚合 blob 才能整体轮转。
|
|
37
|
-
|
|
38
|
-
## 后果
|
|
39
|
-
|
|
40
|
-
- 新增 `src/providers/codearts/`(12 文件,均 ≤10KB)+ `src/cli/commands/provider/codearts-login.js`;`registry.js`/`classify.js`/`share-keys.js`/`provider models` 直查接入。
|
|
41
|
-
- `/v1/models` 增加 `codearts/<modelId>` 前缀条目(benefit 模型带 `tags:["free:benefit"]`)。
|
|
42
|
-
- 福利模型额度小、并发限制严(429 `tm.00001041` 等),由 keyring 冷却与切号按既有语义消化。
|
|
43
|
-
- 真机链路需华为账号完成一次浏览器 PKCE 登录;单元测试以本地 http stub 全链路覆盖(发现+claim/流式/非流式/签名头/401→刷新→轮转写回/死号/200 内嵌错误映射,28 用例)。
|
|
44
|
-
|
|
45
|
-
## 验证
|
|
46
|
-
|
|
47
|
-
- `test/codearts-sign.test.js`(golden 签名向量)、`test/codearts-dpop.test.js`(低 S / JWK 恢复 / node:crypto 独立验签)、`test/codearts-sse.test.js`(快照替换语义 / done / 错误帧)、`test/codearts-provider.test.js`(stub 端到端)。
|
|
48
|
-
- `node --test --test-concurrency=1 test/*.test.js` + `npm run docs:check`。
|
|
@@ -1,34 +0,0 @@
|
|
|
1
|
-
# ADR-0028:traework 供应商(TRAE SOLO CN 免费对话通道反代)
|
|
2
|
-
|
|
3
|
-
- 状态:**已采纳**
|
|
4
|
-
- 日期:2026-09-20
|
|
5
|
-
- 关联:ADR-0007(前缀路由)、ADR-0015(供应商三态路由:`traework` = local-only)、ADR-0019(share-keys 硬排除)、参考实现 [Sliverkiss/traework2api](https://github.com/Sliverkiss/traework2api)(Go,逆向协议来源,常量/头/payload 规则照抄,禁止改值)
|
|
6
|
-
|
|
7
|
-
## 背景
|
|
8
|
-
|
|
9
|
-
TRAE SOLO(CN)向登录用户开放免费对话通道:`POST https://trae-api-cn.mchost.guru/api/agent/v3/llm_utils_chat`(恒 SSE),模型 32 个 `config_name`(`glm-5.2` / `DeepSeek-V4-Pro` / `kimi-k3` …,`POST /api/ide/v1/get_detail_param` 动态拉取)。鉴权是 IDE 登录态而非 API key:`Authorization: Cloud-IDE-JWT <accessToken>` + 15 个 IDE 指纹头(`X-Cloudide-Token`/`X-Ide-Token`/`X-Uid`/`X-App-Id`/`X-Ide-Version`/`X-Machine-Id`/`X-Device-Id`…,UA `Trae/0.1.43`)。accessToken 由 `refreshToken` 经 `api.trae.com.cn/cloudide/api/v3/trae/oauth/ExchangeToken` 换取(refreshToken 单次轮换)。
|
|
10
|
-
|
|
11
|
-
mslxdff 以原生 provider 接入(不做外部反代),纳入既有 keyring/allowlist/dispatcher/classify 体系。
|
|
12
|
-
|
|
13
|
-
## 决策
|
|
14
|
-
|
|
15
|
-
1. **前缀路由 `traework/<modelId>`**(裸 id 恒指 opencode,ADR-0007 不变);`registry.js` 注册工厂(match `id==="traework"` 或 baseUrl 含 `trae`),`classify.js` 归 local-only。
|
|
16
|
-
2. **凭证 = `auths/trae-<uid>.json`(0600 tmp+rename)+ state `providerConfigs.traework={baseUrl,keys,auths}` 双写**(`keys[i]`↔`auths[i]` 平行数组,`auths` 行含 `uid/refreshToken/machineId/deviceId/apiHost/domain`);auth 目录跟账本走(state 同目录 `auths/`,`TRAWEWORK_AUTH_DIR` 显式覆盖),复用 `auths/` gitignore 红线。device/machine id 在 login 时随机生成并随账号落盘(对话头必须一致携带)。
|
|
17
|
-
3. **对话恒 `stream:true`**:OpenAI body 改写(`function:"solo_work_lite"`、`config_name`+`model` 双写、content 字符串→`[{type:"text"}]`、assistant `tool_calls[].function`→`function_call`(无 name 剔除)、`tool_choice` 归一(none→删 tools、function→name 字符串)、`tools[].function.parameters` 对象→JSON 字符串);SOLO SSE(`output` 增量 response/reasoning_content/tool_calls、`token_usage`、`done.finish_reason`、`error.code/message`)→ OpenAI SSE 透传(`function_call`→`function`,删 `namespace`/`partial_arguments`),非流式聚合回 `chat.completion`(tool_calls 按 index 合并、arguments 拼接)。
|
|
18
|
-
4. **错误分级(照抄 traework2api Classify)**:body 含 `"code":1005`(或 1005+plan)→ plan_limit(长冷却 12h);401 → session_dead(禁用换号);429 → soft_rate(60s);404 → 短冷却不累计;5xx → server。SSE 流内 `event:error code=1005` 同样归 plan_limit。keyring 冷却 + 同请求最多轮转 3 号。
|
|
19
|
-
5. **token 生命周期**:过期前 24h(REFRESH_SKEW)请求前预刷新(inflight 由 uid 串行);`ExchangeToken` 毫秒 `TokenExpireAt` → 秒归一(1e12 分界);刷新失败保留旧凭证继续用,`no token in response` 人话提示重登。
|
|
20
|
-
6. **模型发现**:`get_detail_param` 动态(10min 缓存)失败回退静态 32 个(`owned_by:"trae-solo"`);映射:空/`auto`→`glm-5.2`,去 `__` 后缀,下划线→横线宽松匹配 + 大小写不敏感兜底,未知 → 400。
|
|
21
|
-
7. **login 复刻 login.sh**:`mslxdff -provider traework login` 随机 hex16 machine/device id → 构造 `https://www.trae.cn/authorization?...` 授权链接(`auth_from=solo`、`auth_callback_url=http://127.0.0.1:18080/authorize`)→ 浏览器登录后粘贴回调链接 → 解析 `refreshToken/userInfo/userJwt` → ExchangeToken → GetUserInfo → 落盘 + 自动签到(`checkin_credits/status`+`claim`)+ 查积分(`ide_user_ent_usage` credits_limit 求和)。多账号重复 login 追加 = keyring 轮转。
|
|
22
|
-
8. **恒 local-only(硬约束,延续 0015/0019)**:不经组员转发/via-route;`share-keys.js` `NEVER_SHARE_IDS` 显式加 `traework`(本机账号绑定型,uid+machineId/deviceId 绑定,外借无意义且对端刷新会轮换)。
|
|
23
|
-
|
|
24
|
-
## 备选
|
|
25
|
-
|
|
26
|
-
- **走外部 traework2api Go 反代 + generic provider 接入**:否决 —— 多一个常驻进程与部署面,且丢失账号池/轮转写回/allowlist/dispatcher 语义的原生整合;Go 实现仅作协议参考。
|
|
27
|
-
- **复用 workbuddy 的 account-store**:否决 —— 文件名形状(`trae-<uid>.json`)、auth 行字段(apiHost/machineId/deviceId)与刷新端点完全不同,仅借鉴其「目录跟账本走 + 0600 tmp+rename + 平行数组双写」范式。
|
|
28
|
-
- **Bearer 形态把 accessToken 塞 keyring 直连**:否决 —— SOLO 头是 15+ 指纹头的组合(Cloud-IDE-JWT scheme 而非 Bearer),必须整组随账号走。
|
|
29
|
-
|
|
30
|
-
## 后果
|
|
31
|
-
|
|
32
|
-
- 新增 `src/providers/traework/`(11 文件,均 ≤10KB)+ `src/cli/commands/provider/traework-login.js`;`registry.js`/`classify.js`/`share-keys.js`/`providers-setup.js`(auth 号型无 keys 也启用)/`provider-row.js` 接入。
|
|
33
|
-
- 未做(后续迭代):`-provider traework bench` 测速接入、每日定时签到任务(对齐 workbuddy-checkin)、`/status` 账号积分展示。
|
|
34
|
-
- 上游协议来自逆向(traework2api 实测),指纹常量(ClientID/IdeVersion/AppID)变更需同步 `constants.js`。
|
|
@@ -1,60 +0,0 @@
|
|
|
1
|
-
# ADR-0029: qoder 供应商(Qoder 上游原生直连)
|
|
2
|
-
|
|
3
|
-
日期:2026-09-21 · 状态:已采纳
|
|
4
|
-
|
|
5
|
-
## 背景
|
|
6
|
-
|
|
7
|
-
Qoder(qoder.com / qoder.com.cn,阿里系 cosy 签名体系)提供免费额度(每日签到 +100 credits),
|
|
8
|
-
模型池 15 个(qfmodel=Qwen3.8-Flash / qmodel_38max=Qwen3.8-Max / kmodel_latest=Kimi-K3 /
|
|
9
|
-
gmodel=GLM-5.3 / dmodel=DeepSeek-V4-Pro 等)。上游不是 OpenAI 兼容:COSY 自定义 base64 编码 +
|
|
10
|
-
RSA(PKCS1v15)/AES-CBC 会话 + md5 五段签名 + 信封 SSE。社区方案 qoder2api(Go 桥)可参考但引入
|
|
11
|
-
额外常驻进程;实测自研签名栈一次打通("Signature invalid" 根因是手抄 RSA 公钥一字之差)。
|
|
12
|
-
|
|
13
|
-
## 决策
|
|
14
|
-
|
|
15
|
-
1. **原生直连,零桥依赖**:`src/providers/qoder/` 自研完整 cosy 栈(不再部署 qoder2api Go 进程)。
|
|
16
|
-
2. 凭证双写:`auths/qoder-<uid>.json`(0600,device_token/refresh_token/region)+ state
|
|
17
|
-
`providerConfigs.qoder.keys`(JSON blob)+ `auths` 行;login 走 OAuth PKCE 设备授权轮询。
|
|
18
|
-
3. 对话恒上游 `stream:true`(信封 SSE),客户端 stream=true→OpenAI SSE 回放 / false→聚合 JSON;
|
|
19
|
-
模型映射 `mapModel`(家族关键字双向 substring,auto/空→qfmodel)。
|
|
20
|
-
4. 多账号 keyring round-robin;恒 local-only 不借出 key(classify/share-keys 硬排除)。
|
|
21
|
-
5. baseprompt.json(65KB 模板,{UUID}/{TIME} 占位符)vendor 进模块目录。
|
|
22
|
-
|
|
23
|
-
## 后果
|
|
24
|
-
|
|
25
|
-
- `-provider qoder login/models` 可用;`qoder/<key>` 前缀路由;allowAny 默认 on(免费福利供应商)。
|
|
26
|
-
- 上游改协议时需同步 constants/session/encode(风险与 traework 同级)。
|
|
27
|
-
- 已实现:`-provider qoder checkin`(按每号 region 选域名;cn 走 `daily-check-in`,global 走
|
|
28
|
-
`campaigns`)+ daemon 每日 09:00 自动(`MSLXDFF_QODER_CHECKIN`)与 quota 余额查询。
|
|
29
|
-
- 留待后续:CN 区真机(国内号)签到链路复验(本机目前只有国际号)、CN 区双实例对话验证。
|
|
30
|
-
|
|
31
|
-
## 补充:签到链路与区服差异(2026-09-21 实测)
|
|
32
|
-
|
|
33
|
-
移植 qoder2api `checkin.go` 时真机对照出两处硬伤,本实现已修正:
|
|
34
|
-
|
|
35
|
-
1. **Go 版签到域名硬编码 `openapi.qoder.com.cn`**(国内站),三处(`checkin.go` 常量、`cmd/checkin`、
|
|
36
|
-
`scripts/*.py`)一致 → 只服务国内号。实测同一 device token 打 CN 域返回
|
|
37
|
-
`401 {"code":"TOKEN_EXPIRE"}`,打 Global 域(`openapi.qoder.sh`)200 —— 两站账号体系互不通用。
|
|
38
|
-
本实现按每号 `auths/qoder-<uid>.json` 的 `region` 选域名。
|
|
39
|
-
2. **Go 版只认 `actionType=CLAIM_BENEFIT`**,国际站活动是 `VIEW_DETAILS`(促销:Pro/Pro+ 首月 Credits 翻倍)
|
|
40
|
-
→ 该判据下国际号被误判为「无可用签到活动」。实测该条目 `claimStatus=CLAIMABLE`,`POST /campaigns/{id}/claim`
|
|
41
|
-
返回 `status=CLAIMED, replayed=false`、活动 `claimable` 翻 false(上游确已记账),但免费号 quota
|
|
42
|
-
仍为 0(该活动奖励是订阅折扣而非积分)——故本实现默认只领 `CLAIM_BENEFIT`,促销条目需 `checkin --any` 显式点名。
|
|
43
|
-
|
|
44
|
-
端点差异(同 base 只换域名):Global `openapi.qoder.sh` 无 `/sash/api/v1/me/daily-check-in/*`
|
|
45
|
-
(404)→ 只能走 campaigns;CN `openapi.qoder.com.cn` 两者皆有(`daily-check-in/status|claim`,
|
|
46
|
-
409=今日已领,`campaignKey` 形如 `cn_daily_check_in_legacy`,每日 100 credits)。
|
|
47
|
-
认证只需 `Bearer <device_token>` + `cosy-clienttype: 10`(无 COSY 签名、claim 空 body);
|
|
48
|
-
额度 `GET /api/v2/quota/usage` → `userQuota{total,used,remaining,unit}`。
|
|
49
|
-
|
|
50
|
-
## 补充 2:门禁修复(同日)
|
|
51
|
-
|
|
52
|
-
daemon 装配门禁(providers-setup)曾要求定制供应商「baseUrl 与 keys 皆有」才启用——qoder 凭证在
|
|
53
|
-
auths 目录且端点自包含(qoder://native,baseUrl 空)→ 被 continue 静默跳过:/v1/models 无 qoder、
|
|
54
|
-
-models 挑不到、网关报 Model qoder/qfmodel is not supported。修复:门禁抽为纯函数
|
|
55
|
-
(src/runtime/provider-gate.js shouldEnableCustomProvider + loadAuthDocs),auth 号型
|
|
56
|
-
(workbuddy/traework/qoder)三选一即可启用(keys/auths/auth 目录),其余定制有 keys 即启用。
|
|
57
|
-
另修 -models 交互候选(src/cli/commands/model/list-live.js + live-models.js):allowAny 空白名单
|
|
58
|
-
供应商的模型并入网关 live,否则挑不到;provider-row.js 的启用口径同步放宽(qoder 显示 qoder://native)。
|
|
59
|
-
|
|
60
|
-
决策记忆与后续契约:门禁判据与波及三处见 `.agents/notes/implemented/bug-fix/2026-09-21-auth-doc-provider-gate.md`;签到区服取舍见 `.agents/notes/implemented/feature/2026-09-21-qoder-checkin-region-endpoints.md`;qoder 模型如何进入对外目录见 [ADR-0030](0030-models-list-scoped-by-picks.md)。
|
|
@@ -1,53 +0,0 @@
|
|
|
1
|
-
# ADR-0030: `/v1/models` 目录按勾选集裁剪
|
|
2
|
-
|
|
3
|
-
日期:2026-09-21 · 状态:已采纳
|
|
4
|
-
|
|
5
|
-
## 背景
|
|
6
|
-
|
|
7
|
-
`-models` 交互式多选把常用模型写进 `modelPicks`,但该勾选此前只影响两处:`auto` 择优池
|
|
8
|
-
(`src/auto.js` pickedPool)与 `-setto` 客户端同步。网关 `GET /v1/models` 走多供应商聚合
|
|
9
|
-
(`src/models.js` aggLoad),只按各供应商 `allowlist`/`allowAnyModels` 裁剪,从不读 `modelPicks`。
|
|
10
|
-
|
|
11
|
-
结果:qoder 接入后(15 模型、`allowAnyModels=true` 空白名单),`/v1/models` 一次吐出 46 条,
|
|
12
|
-
用户在 `qoder` 只勾了 `qoder/auto`/`qoder/qfmodel` 两项时,客户端(opencode/Codex/WorkBuddy 拉目录)
|
|
13
|
-
仍看到全部 46 条可选,勾选与实际可用面脱节。
|
|
14
|
-
|
|
15
|
-
## 决策
|
|
16
|
-
|
|
17
|
-
`modelPicks` 非空即成为 `GET /v1/models` 的对外白名单(勾选即目录),在能力富化
|
|
18
|
-
(ADR-0022 `mergeModelsList`)之后、`models:list` 插件 hook 之前按 `id` 精确裁剪;条目形状不变。
|
|
19
|
-
|
|
20
|
-
三条边界,缺一即错:
|
|
21
|
-
|
|
22
|
-
1. **空勾选 = 不过滤**:`modelPicks` 为空数组(新装、`-model pick clear` 后)时返回全量目录。
|
|
23
|
-
若空即拒绝,一条 `pick clear` 会让所有下游客户端当场零模型可用。
|
|
24
|
-
2. **`?all=1` 逃生门**:`GET /v1/models?all=1` 绕过裁剪。`-models` 交互候选取数
|
|
25
|
-
(`src/cli/commands/model/live-models.js`)恒走此口,否则用户取消勾选后那个模型
|
|
26
|
-
再也回不到候选列表里 —— 裁剪会变成不可逆的单向棘轮。
|
|
27
|
-
3. **裁剪只看勾选,不看健康状态**:`modelErrors`/冷却中的模型若在勾选集内照样出现在目录,
|
|
28
|
-
与 `POST /v1/chat/completions` 的可用性判定解耦(目录=授权面,健康=择路面)。
|
|
29
|
-
|
|
30
|
-
`src/routes/models-route.js` 的 `filterByPicks(data, picks)` 为纯函数,`modelsHandler`
|
|
31
|
-
新增可选注入参数 `loadPicks`(缺省动态 import `state.loadModelPicks`)。
|
|
32
|
-
|
|
33
|
-
## 备选方案
|
|
34
|
-
|
|
35
|
-
- **不做裁剪,只改 `-setto` 同步范围**(复用现状):客户端目录由同步时挑,网关保持全量。
|
|
36
|
-
最强论据是零契约风险。否决理由:`-setto opencode/chatgpt/workbuddy` 已按 picks 剪枝,
|
|
37
|
-
但任何**直接**打 `GET /v1/models` 的第三方工具(新接的 IDE/自建脚本)拿到的仍是全量,
|
|
38
|
-
授权面只覆盖三条同步链,用户"勾完就只该用这些"的意图在网关层不成立。
|
|
39
|
-
- **把勾选写进各供应商 `allowlist`**(复用既有裁剪机制,零新代码):`-models` 保存时同步写
|
|
40
|
-
`providerConfigs.<id>.allowedModels`。最强论据是复用同一条过滤路径、无新语义。否决理由:
|
|
41
|
-
`allowlist` 是**供应商准入安全阀**(空即 `403`,`allowAnyModels=false` 默认全拦,见 AGENTS.md
|
|
42
|
-
契约),与"用户偏好"是两个所有权;写在一起会让 `allowAny off` 的收紧语义被勾选动作静默改写,
|
|
43
|
-
且 qoder/codearts 这类免费福利供应商的 `allowAnyModels=true` 会被降级成白名单模式。
|
|
44
|
-
- **按 picks 裁剪但不留逃生门**:实现最简。否决理由即上述棘轮问题,不可接受。
|
|
45
|
-
|
|
46
|
-
## 后果
|
|
47
|
-
|
|
48
|
-
- 收益:勾选一次即同时决定 auto 候选池、`-setto` 同步面、网关对外目录三处,客户端零改配置。
|
|
49
|
-
- 代价:`/v1/models` 的返回条数不再是"部署了多少供应商"的固定读数,排障时看条数变少要同时查
|
|
50
|
-
`modelPicks`(`-model picks`);`?raw=1`(原始形状)与 `?all=1`(全量)两个正交参数易混淆,
|
|
51
|
-
双镜像文档必须逐条列出。
|
|
52
|
-
- 已验证:`test/models-picks-filter.test.js` 4 用例(picks 裁剪/空不过滤/`?all=1`/纯函数);
|
|
53
|
-
真机 29 勾选 → 默认 25 条、`?all=1` 46 条、`qoder/qfmodel` 对话 200。
|
|
@@ -1,50 +0,0 @@
|
|
|
1
|
-
# ADR-0031: qoder 对话改真流式(边收边吐 + 非流式聚合双管线)
|
|
2
|
-
|
|
3
|
-
日期:2026-09-21 · 状态:已采纳
|
|
4
|
-
|
|
5
|
-
## 背景
|
|
6
|
-
|
|
7
|
-
客户端 `stream:true` 时 `qoder/*` 回复长时间无输出、随后整段涌出:`createChatService.runChat`
|
|
8
|
-
在返回 `Response` 之前先 `await callQoder()` 把上游整条流读完收进 `deltas[]`,流式分支再用
|
|
9
|
-
生成器"回放"已聚合的数组。这不是流式,是披着 SSE 外衣的假流式。
|
|
10
|
-
|
|
11
|
-
首字延迟 = 上游全程耗时:`calls.log` 2026-09-21 共 36 条 `qoder/qfmodel` 记录,p50 ≈ 37s、
|
|
12
|
-
p90 ≈ 72s(同口径 workbuddy p90 ≈ 8.5s)。请求方 `stream:false` 同样走这条"读完再说"路径。
|
|
13
|
-
|
|
14
|
-
## 决策
|
|
15
|
-
|
|
16
|
-
`stream:true` 走真流式管线:上游 `data:` 帧一到即 `extractDelta` 转 OpenAI chunk 并
|
|
17
|
-
`enqueue`,不再经 `deltas[]` 中转;`stream:false` 才走聚合管线。两条管线共享
|
|
18
|
-
"请求装配 + 信封解析 + 错误分类"三段,在 `runChat` 分支点之后完全分离。
|
|
19
|
-
|
|
20
|
-
同时补 `errorStatus` 缺失的 `auth` 支:上游非 200 由 `mapUpstreamError` 标成
|
|
21
|
-
`kind:"auth"`(401/403)后落到兜底 `502`(`errRes(502, detail, "auth")` 自相矛盾),
|
|
22
|
-
客户端会把"device token 失效"误判成"上游坏了"而无限重试。现 `auth` 透传 401/403,
|
|
23
|
-
与 `qoder/index.js` 无账号返 401 `auth_error`、`checkin.js` 的"401 → 请重新 login"口径一致。
|
|
24
|
-
|
|
25
|
-
模块拆分(按"先拆后写"):`chat.js` 3574B 薄门面只留 `stream` 分支与错误出口;
|
|
26
|
-
`request.js` 1210B 纯装配(COSY 签名请求 `url/headers/bodyStr`,两条管线共享单一签名口,
|
|
27
|
-
防两处签名漂移);`stream.js` 3302B 转发(对标 traework `reshapeSoloStream` 的
|
|
28
|
-
`reader.read()` 循环 + 即时 `enqueue`);`aggregate.js` 2111B 聚合 + `toCompletionJson`。
|
|
29
|
-
|
|
30
|
-
## 备选方案
|
|
31
|
-
|
|
32
|
-
- **不做(保持回放)**:零改动、零回归风险,非流式调用方完全不受影响。否决理由:SSE 存在的
|
|
33
|
-
意义就是首字即达,实测 p50 37s 的首字延迟已让用户报"不流畅",披皮回放违背协议语义。
|
|
34
|
-
- **`pipeThrough(new TransformStream(...))`**:标准 Web Streams 写法、无手动 reader 循环。
|
|
35
|
-
否决理由:qoder 信封是 `{statusCodeValue,body:"<内层JSON>"}` 双层结构,chunk 边界 ≠ SSE 行
|
|
36
|
-
边界,跨 chunk 拼行缓冲仍要手写,复杂度与手动循环相当且偏离既有 traework 范式。
|
|
37
|
-
- **流/非流共用"边读边攒、读完再分",只把回放提前**:改动行数最少。否决理由:攒数组本身就是
|
|
38
|
-
延迟根因,提前回放仍需等"读完",首字延迟一分不降。
|
|
39
|
-
- **只加首字超时熔断、不改管线**:复用网关层 `STREAM_TIMEOUT_MS`。否决理由:熔断只把
|
|
40
|
-
"慢"变成"失败",用户要的是内容开始流出来。
|
|
41
|
-
|
|
42
|
-
## 后果
|
|
43
|
-
|
|
44
|
-
- 收益:首字延迟从"上游全程"降为"上游首帧 + 转发开销";`stream:false` 行为零变。
|
|
45
|
-
- 代价:`runChat` 有两条返回路径(流 Response vs 聚合 JSON),后续改帧格式要同时看
|
|
46
|
-
`stream.js` / `aggregate.js`;上游 usage 只在尾帧带,故 usage 仍附在 finish chunk 上一次
|
|
47
|
-
发出(`stream:false` 聚合口径不变),上游若支持增量 usage 才值得再提前。
|
|
48
|
-
- 修订 ADR-0029 中"客户端 SSE 回放/非流式聚合"的表述:回放已不存在。
|
|
49
|
-
- 已验证:`test/qoder-provider.test.js` 新增"qoder 真流式"4 用例(首帧在上游 gate 未释放前
|
|
50
|
-
即已可读、非流式聚合不变、上游 401 双路径映射、空流 502)→ 28/28 全绿。
|