dsh-router-core 0.4.5 → 0.4.7

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.
Files changed (75) hide show
  1. package/README.md +152 -12
  2. package/docs/ext.md +1 -1
  3. package/docs/protocols.md +198 -0
  4. package/docs/suppliers.md +2 -2
  5. package/lib/client-registry.js +146 -106
  6. package/lib/client.js +146 -106
  7. package/lib/ext-prompt/index.js +182 -17
  8. package/lib/index.js +2764 -63
  9. package/lib/suppliers/opencode/index.js +58 -30
  10. package/lib/types/ext/contract.d.ts +20 -1
  11. package/lib/types/ext-prompt/content.d.ts +7 -1
  12. package/lib/types/ext-prompt/mount.d.ts +12 -1
  13. package/lib/types/ext-prompt/plugin.d.ts +10 -0
  14. package/lib/types/ext-prompt/render.d.ts +14 -3
  15. package/lib/types/ext-prompt/sections.d.ts +95 -0
  16. package/lib/types/protocol/anthropic.d.ts +35 -0
  17. package/lib/types/protocol/chat.d.ts +25 -0
  18. package/lib/types/protocol/entry.d.ts +137 -0
  19. package/lib/types/protocol/gemini.d.ts +57 -0
  20. package/lib/types/protocol/index.d.ts +13 -0
  21. package/lib/types/protocol/responses.d.ts +41 -0
  22. package/lib/types/protocol/sse.d.ts +41 -0
  23. package/lib/types/protocol/stream.d.ts +92 -0
  24. package/lib/types/protocol/types.d.ts +224 -0
  25. package/lib/types/router/account-pool.d.ts +7 -2
  26. package/lib/types/router/index.d.ts +42 -1
  27. package/lib/types/router/types.d.ts +26 -3
  28. package/lib/types/router/usage-store.d.ts +4 -2
  29. package/lib/types/shared.d.ts +24 -0
  30. package/lib/types/suppliers/contract.d.ts +15 -1
  31. package/package.json +6 -6
  32. package/src/client/ExtDetail.tsx +25 -5
  33. package/src/client/group-controls.test.ts +53 -0
  34. package/src/client/group-controls.ts +43 -0
  35. package/src/client/router.css +20 -1
  36. package/src/ext/contract.ts +20 -1
  37. package/src/ext-prompt/assembly.test.ts +63 -0
  38. package/src/ext-prompt/content.ts +8 -10
  39. package/src/ext-prompt/ext-prompt.test.ts +38 -28
  40. package/src/ext-prompt/index.ts +50 -2
  41. package/src/ext-prompt/mount.test.ts +18 -1
  42. package/src/ext-prompt/mount.ts +22 -7
  43. package/src/ext-prompt/plugin.ts +64 -1
  44. package/src/ext-prompt/render.test.ts +1 -1
  45. package/src/ext-prompt/render.ts +17 -6
  46. package/src/ext-prompt/section-controls.test.ts +148 -0
  47. package/src/ext-prompt/section-filter.test.ts +140 -0
  48. package/src/ext-prompt/sections.test.ts +123 -0
  49. package/src/ext-prompt/sections.ts +193 -0
  50. package/src/ext-route.test.ts +16 -0
  51. package/src/index.ts +117 -6
  52. package/src/llm/adapter.test.ts +54 -0
  53. package/src/llm/adapter.ts +24 -2
  54. package/src/protocol/anthropic.ts +662 -0
  55. package/src/protocol/chat.ts +272 -0
  56. package/src/protocol/entry.ts +440 -0
  57. package/src/protocol/gemini.ts +548 -0
  58. package/src/protocol/index.ts +31 -0
  59. package/src/protocol/protocol.test.ts +728 -0
  60. package/src/protocol/responses.ts +551 -0
  61. package/src/protocol/sse.ts +94 -0
  62. package/src/protocol/stream.ts +195 -0
  63. package/src/protocol/types.ts +249 -0
  64. package/src/protocol/wire-e2e.test.ts +208 -0
  65. package/src/protocol/wiring.test.ts +225 -0
  66. package/src/router/account-pool.test.ts +35 -0
  67. package/src/router/account-pool.ts +15 -4
  68. package/src/router/index.ts +143 -24
  69. package/src/router/types.ts +26 -3
  70. package/src/router/usage-store.ts +4 -2
  71. package/src/shared.test.ts +35 -0
  72. package/src/shared.ts +33 -0
  73. package/src/suppliers/contract.ts +15 -2
  74. package/src/suppliers/opencode/plugin.test.ts +79 -25
  75. package/src/suppliers/opencode/plugin.ts +60 -29
@@ -2,46 +2,70 @@ import { n as registerSupplierRow, t as stateFromHttpStatus } from "../../http-s
2
2
  import { randomBytes } from "node:crypto";
3
3
  //#region src/suppliers/opencode/plugin.ts
4
4
  /**
5
- * opencode 供应商插件 —— 参考 9Router(open-sse) 的 opencode free 实现。
5
+ * opencode 供应商插件 —— OpenCode Zen 免费档(匿名 `Bearer public` 通道)。
6
6
  *
7
- * 上游:https://opencode.ai/zen/v1(OpenAI 兼容,免费档)
8
- * - chat: POST /zen/v1/chat/completions(**只能流式**)
9
- * - models: GET /zen/v1/models(透传,只取免费模型)
7
+ * 上游(2026-10-15 实测):
8
+ * - chat: POST https://opencode.ai/inference/v1/chat/completions(**只能流式**)
9
+ * - models: GET https://opencode.ai/inference/v1/models(两个端点都通,取免费模型)
10
10
  *
11
- * 「免费」不等于「无鉴权」:Zen 免费档仍要求**模拟真 CLI 的握手**,否则一律
12
- * 403 `FreeTierError: OpenCode's free tier can only be used from within OpenCode`
13
- * (2026-09-22 实测)。三个条件缺一即 403,见下方 `UA` / `sessionId` / `decoyTools`。
14
- * 免费档也没有账号可填 —— 所以无凭证/连接池/签到,面板只显示模型卡片
11
+ * ⚠️ `/zen/v1/chat/completions` **已废弃**:2026-10-15 全矩阵实测该端点对任何
12
+ * 头部/工具组合一律 403,迁到 `/inference/v1` 后同样请求立刻 200。旧的
13
+ * `zen/v1` 常量是本次故障的根因之一,别再改回去。
14
+ * 注:2026-10-16 复核,`/zen/v1/chat/completions` 在握手完全正确时也能 200
15
+ * (即"一律 403"仅对当时的随机 hex session 成立);两个端点的差异对正确握手
16
+ * 不可见。`/inference/v1` 仍是**实测更稳**的那个,保留不回退。
17
+ *
18
+ * 「免费」不等于「无鉴权」:仍要求**模拟真 CLI 的握手**,否则一律
19
+ * 403 `FreeTierError: OpenCode's free tier can only be used from within OpenCode`。
20
+ * 2026-10-15 逐项消融后,真正的门只剩两条(其余全不敏感):
21
+ *
22
+ * 1. `tools` 里必须存在一个**小写精确名为 `read`** 的工具。
23
+ * —— `parameters` 完全不看(实测 `properties:{}`、无 `parameters`、
24
+ * `parameters:null` 全部 200);但名字大小写敏感(`READ` → 403),且
25
+ * `bash`/`glob`/`write` 等**不能替代** `read`(各自单独都给 403)。
26
+ * 2. `x-opencode-session` 的 12 位 hex 前缀必须是**倒序时间戳**,且其低 12 位
27
+ * (counter)落在 `[0,63]`(见 `newSessionId`)。
28
+ *
29
+ * 已实测**不敏感**(改了照样 200,故不写死):UA 具体版本(`opencode/1.18.31`
30
+ * 与 `opencode/latest/2.0.29/cli` 等价)、`x-opencode-client` 的 `cli`/`desktop`、
31
+ * `x-opencode-request` 有无、`x-opencode-project` 是否 40-hex、工具 `description`。
32
+ *
33
+ * 免费档没有账号可填 —— 所以无凭证/连接池/签到,面板只显示模型卡片
15
34
  * (连接池、签到由能力检测自动隐藏)。
16
35
  */
17
36
  const id = "opencode";
18
37
  const name$1 = "OpenCode";
19
38
  /** 面板图标。 */
20
39
  const icon = "data:image/png;base64,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";
21
- const BASE = "https://opencode.ai/zen/v1";
40
+ /** 上游免费档的推理端点。`/zen/v1` 已于 2026-10-15 废弃(全组合 403)。 */
41
+ const BASE = "https://opencode.ai/inference/v1";
22
42
  const MODELS_URL = `${BASE}/models`;
23
43
  const CHAT_URL = `${BASE}/chat/completions`;
24
44
  /**
25
- * User-Agent 必须带**版本号**且 ≥ 1.18。
26
- *
27
- * 裸 `opencode` → 403 FreeTierError;`opencode/1.17.x` → **426 UpgradeRequired**
28
- * (上游明确要求升级客户端);`opencode/1.18.0` 起通过。别去掉版本号,
29
- * 也别写死成能过的最高版本——取一个已实测通过的稳定值即可。
45
+ * User-Agent。**不是**门禁(实测 `opencode/1.18.31` 与真 CLI 的
46
+ * `opencode/1.19.0 ai-sdk/...` 等价,裸 `opencode` 也不影响本门)。
47
+ * 保留一个带版本号的值只是别让上游按非 CLI 流量处理。
30
48
  */
31
49
  const UA = "opencode/1.18.31";
32
50
  /**
33
- * 免费档按 session 记账(每次生成一个新的即可——见 `chatOnce` 的实测说明),
34
- * `x-opencode-session` / `x-opencode-request` **必须**是上游认的形状,格式不对
35
- * 同样 403(不是忽略,是拒绝):
36
- * ses_<12 位小写 hex><14 位 base62>、msg_<同形状>
51
+ * 造上游认的 session/request id。
52
+ *
53
+ * ⚠️ 12 位 hex 前缀**不是任意 hex,也不是摘要派生**,它是真客户端的
54
+ * `~(Date.now() * 0x1000 + counter)`:倒序时间戳。2026-10-15 实测(逐项消融):
55
+ * - 倒序时间戳 counter=0/1/62/63 → 200;counter=64/65/100 → 403
56
+ * - 纯随机 hex → 403;sha256 派生 hex → 403(9router #4780 的成因)
57
+ * 即上游会把低 12 位当每毫秒计数器读,只接受 `[0,63]`;随机/派生值落进该窗口
58
+ * 只有约 1.6% 概率,所以旧实现几乎必然 403。(2026-10-16 每个边界值各打 3 次
59
+ * 复核:63 三次全 200、64 三次全 403,边界就是 63/64 之间。)
60
+ *
61
+ * counter 固定取 1:本插件是自己 session 的唯一写入方,做每毫秒计数器只增加状态。
37
62
  */
38
63
  function newSessionId(prefix) {
39
64
  const B62 = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
40
- const bytes = randomBytes(20);
41
- let time = "";
42
- for (let i = 0; i < 6; i++) time += bytes[i].toString(16).padStart(2, "0");
65
+ const time = (~(BigInt(Date.now()) * 4096n + 1n) & 281474976710655n).toString(16).padStart(12, "0");
66
+ const bytes = randomBytes(14);
43
67
  let rand = "";
44
- for (let i = 6; i < 20; i++) rand += B62[bytes[i] % 62] ?? "";
68
+ for (let i = 0; i < 14; i++) rand += B62[bytes[i] % 62] ?? "";
45
69
  return `${prefix}${time}${rand}`;
46
70
  }
47
71
  /**
@@ -58,17 +82,21 @@ function sessionHeaders() {
58
82
  };
59
83
  }
60
84
  /**
61
- * 免费档要求 tools 里**同时**存在 `bash` 和 `read`,否则 403。
85
+ * 免费档要求的占位工具:**只需要 `read` 一个**,小写精确。
62
86
  *
63
- * 为什么是「注入」而不是「覆盖」:上游只是**校验这两个名字在不在**,真正的
64
- * 工具照常工作(实测真实工具在前、占位在后,混合下发 200)。所以调用方已经
65
- * 带了同名工具时保持原样不动,缺哪个补哪个——绝不覆盖调用方自己的定义,
66
- * 否则会把真实工具签名糊掉。
87
+ * 2026-10-15 逐项消融实测:
88
+ * - `read` 单独 → 200;`bash` 单独 → 403;`glob`/`write`/`shell` 单独 → 403
89
+ * - `read` + 任意其他(含空 schema 的 bash/glob/grep…)→ 200
90
+ * - 把 `read` 改名成 `READ` → 403(大小写敏感)
91
+ * - `read` 的 `parameters` 完全不看:`properties:{}`、无 `parameters`、
92
+ * `parameters:null`、`parameters:{}` 全部 200
93
+ *
94
+ * 所以旧版「必须同时有 bash 和 read」是错的——**`bash` 不是门,`read` 才是**。
95
+ * 为什么是「注入」而不是「覆盖」:上游只校验名字在不在,真实工具照常工作,
96
+ * 所以调用方已带同名工具时保持原样不动,缺哪个补哪个——绝不覆盖调用方自己的
97
+ * 定义,否则会把真实工具签名糊掉。
67
98
  */
68
99
  const DECOY_TOOLS = [{
69
- name: "bash",
70
- description: "This tool is currently unavailable and must not be used."
71
- }, {
72
100
  name: "read",
73
101
  description: "This tool is currently unavailable and must not be used."
74
102
  }];
@@ -65,7 +65,7 @@ export interface RouterExt {
65
65
  * 扩展自带的**子开关**(可选;不声明 = 只有总开关)。
66
66
  *
67
67
  * 为什么需要:有些扩展的「开/关」不是一件事而是**几件独立的事** —— 例如分层
68
- * 提示词有 14 个准则分类,每条都该能单独关。硬塞进一个总开关 = 要么全给、
68
+ * 提示词的准则分类,每条都该能单独关。硬塞进一个总开关 = 要么全给、
69
69
  * 要么全不给。
70
70
  *
71
71
  * ⚠️ **与总开关的关系**:子开关是**叠加**在总开关之上的,总开关关 ⇒ 全部子开关
@@ -178,6 +178,25 @@ export interface ExtControl {
178
178
  readonly custom?: boolean;
179
179
  /** 文本被用户改过(决定「还原」按钮是否可点)。 */
180
180
  readonly overridden?: boolean;
181
+ /**
182
+ * 分组名(可选;缺省 = 无组)—— 面板按它把控制条**分节**渲染,组间插一条
183
+ * 组标题行,组内顺序照旧。**无组的条目照旧平铺** ⇒ 没用这个字段的扩展
184
+ * (ext-test 等)面板行为一个像素都不变。
185
+ *
186
+ * 为什么要有:一条扩展同时管两类东西时(分层提示词 = 准则分类 + 宿主段落),
187
+ * 平铺成一长列会让"哪条是准则、哪条是宿主的段"无从分辨 —— 分组是**归属
188
+ * 信息**,不是样式。
189
+ */
190
+ readonly group?: string;
191
+ /**
192
+ * 这一条参与拖动排序(可选;缺省 = 参与,保持既有行为)。
193
+ *
194
+ * 为什么需要显式关掉:排序落盘只认扩展自己的条目 id(`setControlOrder`
195
+ * 过滤未知 id)—— 宿主段落的 id 不在其中,拖它只会被静默丢弃 ⇒ 看起来能拖、
196
+ * 松手弹回,是假交互。**由扩展逐条声明**,与 `editable` 同一理由:能力在
197
+ * 实现方手里,由它说哪条有。
198
+ */
199
+ readonly sortable?: boolean;
181
200
  }
182
201
  /** 面板/API 用的一条扩展器信息 = 核心存的开关 + 插件报的运行时事实。 */
183
202
  export interface ExtInfo {
@@ -20,6 +20,12 @@
20
20
  * —— 它没有终止条件,每次收口都必然能再找出一件存量,等于无限循环。
21
21
  * ② 固化:由"每次用户可见损失都必须固化"降级为"只对数据丢失/安全这类真损失
22
22
  * 补检查",并删掉「不固化的教训等于决定再犯一次」这句无界义务。
23
+ *
24
+ * ⚠️ 2026-10-17 用户点名的两处改动:
25
+ * ① **删掉「固化」分类**(整条移除,id `hardening` 不复存在)。用户已存的
26
+ * `categories`/`text`/`order` 里残留的 `hardening` 键无须迁移 ——
27
+ * `resolveCategories` 规则 2 会忽略不认识的 id。
28
+ * ② 身份气质:「默认中文」→「回复、思考过程必须使用中文」。
23
29
  */
24
30
  /** 一个可独立开关的提示词分类。 */
25
31
  export interface PromptCategory {
@@ -43,7 +49,7 @@ export declare const PROMPT_CATEGORIES: readonly PromptCategory[];
43
49
  *
44
50
  * ⚠️ 拆分时差点丢它:原文是 `[…] ·身份气质:…`,标题挂在方括号里、
45
51
  * **不属于任何单条分类**,按分隔符切分时自然被丢掉
46
- * (只在 `content.ts` 头注出现过、没进任何 `body`)。⇒ 模型会看到 14 条
52
+ * (只在 `content.ts` 头注出现过、没进任何 `body`)。⇒ 模型会看到一堆
47
53
  * **没有名字**的规则,而"这套东西叫什么、为什么存在"恰恰是它需要的上下文。
48
54
  *
49
55
  * 形状:**不属于任何分类** ⇒ 不可被单条开关关掉(它一关,准则就无名无姓)。
@@ -23,10 +23,21 @@ export interface SystemPromptFace {
23
23
  order: number;
24
24
  text: string | (() => string);
25
25
  }): () => void;
26
- getSectionOrder?(name: string): number;
26
+ /**
27
+ * 段落总控用(`index.ts` 的 waterfall 预热):宿主 `SystemPrompt.assemble()`。
28
+ * 只声明用得到的最小形状 —— 返回的 `sections` 已按 order/name 排好序。
29
+ */
30
+ assemble(context?: Record<string, unknown>): Promise<{
31
+ sections: {
32
+ name: string;
33
+ text: string;
34
+ }[];
35
+ }>;
27
36
  }
28
37
  /** 段落名(**稳定**):改名会让已注册的作用域 section 与旧名并存。 */
29
38
  export declare const PROMPT_SECTION_NAME = "dsh-router-ext-prompt:guideline";
39
+ /** 官方 `SECTION_ORDERS` 里最低的那档(第一方开场白),测试用它当"最前"的判据。 */
40
+ export declare const OFFICIAL_LOWEST_SECTION_ORDER = -1000;
30
41
  export interface MountPromptDeps {
31
42
  /** 取 `ctx.systemPrompt`(可能尚未就绪 ⇒ undefined)。 */
32
43
  getSystemPrompt: () => SystemPromptFace | undefined;
@@ -13,6 +13,7 @@
13
13
  * 真正的注入在 `mount.ts`。
14
14
  */
15
15
  import type { ExtStoreService, RouterExt } from '../ext/contract.ts';
16
+ import { type SectionEntry } from './sections.ts';
16
17
  /** 扩展开关表里的注册键。 */
17
18
  export declare const EXT_PROMPT_ID = "prompt";
18
19
  /**
@@ -29,4 +30,13 @@ export declare const EXT_PROMPT_ID = "prompt";
29
30
  export declare function createPromptExt(deps: {
30
31
  isSystemPromptReady: () => boolean;
31
32
  store?: ExtStoreService;
33
+ /**
34
+ * 当前已装配的段落清单(最近一次 `assemble` 的快照,来自 waterfall 缓存)。
35
+ *
36
+ * ⚠️ **必选,不给缺省**:缺省成 `() => []` 的话,`index.ts` 忘接这个参数
37
+ * 就是"段清单恒空"的静默失效(面板无段、过滤不生效,但一切照常跑)——
38
+ * 让类型红比让它静默强。**空清单是合法值**(还没预热完/宿主一段都没有),
39
+ * 与"没接线"的区分靠这里不给缺省,不靠运行时猜。
40
+ */
41
+ getSections: () => SectionEntry[];
32
42
  }): RouterExt;
@@ -24,7 +24,7 @@ export interface PromptExtData {
24
24
  text?: Record<string, PromptCategoryOverride>;
25
25
  /**
26
26
  * 用户自建的准则条目。**与内置的 `PROMPT_CATEGORIES` 分开存** ——
27
- * 内置那 14 条是**代码常量**(发版即定),混在一起就会出现"代码里有一条、
27
+ * 内置条目是**代码常量**(发版即定),混在一起就会出现"代码里有一条、
28
28
  * 落盘里又有一条"的归属问题:升级时新加的内置条目会和用户的旧数据打架。
29
29
  *
30
30
  * 存的是**完整条目**(不是覆盖值):自建的东西没有"内置版本"可回退。
@@ -33,12 +33,23 @@ export interface PromptExtData {
33
33
  /**
34
34
  * 展示顺序:`[id, …]`,含内置与自建。
35
35
  *
36
- * ⚠️ **只存 id 顺序,不重排内置数组本身**:内置 14 条的次序是 `content.ts`
36
+ * ⚠️ **只存 id 顺序,不重排内置数组本身**:内置条目的次序是 `content.ts`
37
37
  * 里的代码顺序,它同时是"准则作为一份文档的阅读顺序";让用户拖动去改它,
38
38
  * 等于让代码顺序随某个用户的操作漂移 —— 别人拿到新版本看到的次序就变了。
39
39
  * 落盘只记"这次会话里按什么顺序显示",代码顺序永远是兜底。
40
40
  */
41
41
  order?: string[];
42
+ /**
43
+ * 段落级开关:`{ [sectionName]: boolean }`,**只存关闭项**(`false` = 关)。
44
+ *
45
+ * 缺项 = 开(默认全开)——与 `text` 的"只存覆盖值"同一纪律:宿主的段落随
46
+ * 宿主/别的插件增删,存全文表就得在升级后逐条对账;只记"用户主动关了谁"
47
+ * 让新增段落自然落回默认(开),用户的选择不会被静默迁移。
48
+ *
49
+ * key 是**段的注册名**(如 `tool:write`),不是控制条 id(`sec:` 前缀是
50
+ * 传输层编码,落盘用裸名,见 sections.ts)。
51
+ */
52
+ sections?: Record<string, boolean>;
42
53
  }
43
54
  /** 一条分类上用户改过的字段(只记改了的那些)。 */
44
55
  export interface PromptCategoryOverride {
@@ -69,7 +80,7 @@ export declare function resolveEnabledCategories(masterOn: boolean, data: Prompt
69
80
  * 渲染成**一个**段落文本(多分类拼一起)。
70
81
  *
71
82
  * 为什么要拼成一段而不是一段一个:面板开关是"分类"粒度,而 prompt 读起来
72
- * 是一段连续的准则 —— 分成 14 个 section 会让 KV cache 每个分类各占一段、
83
+ * 是一段连续的准则 —— 分成多个 section 会让 KV cache 每个分类各占一段、
73
84
  * 增删任意一个分类都让其后全部位移。这里**注册一个 section,文本由开关拼装**。
74
85
  *
75
86
  * @returns 关闭的分类不进结果;全关 ⇒ 空串(上游 `renderPrompt` 会丢弃空段)。
@@ -0,0 +1,95 @@
1
+ /**
2
+ * 系统提示词**段落级**总控的纯逻辑与清单缓存 —— 把整体系统提示词拆成
3
+ * 每一段(`harness:identity`、`tool:write`、`plan:policy` …),面板分组列出、
4
+ * 逐段开关,关掉的段在装配时被过滤掉(`index.ts` 的 waterfall)。
5
+ *
6
+ * ════════════════════════════════════════════════════════════════════
7
+ * 清单从哪来:`assemble()` 的真实产物,不写死宿主段名表
8
+ * ════════════════════════════════════════════════════════════════════
9
+ * 段名只有个别是宿主导出常量(`harness:identity`、`deployment:persona-*`),
10
+ * 其余(`tool:*`、`mcp:*`、第三方插件段)都埋在各自插件里 —— 写死清单必然漂移。
11
+ * 装配 waterfall(`system-prompt/assemble`)每轮都携带**全量已排序段落**,
12
+ * 顺路缓存一份就是面板的事实源;apply 时预热一次 assemble,没开始对话也有清单。
13
+ *
14
+ * ⚠️ 清单是**最近一次装配的快照**(近似):`assemble()` 不带 scope 时只含
15
+ * global 段,真实对话是 agent scope;且多会话并发时缓存归"最后一次装配"。
16
+ * 面板展示可接受,**开关判定不受影响** —— 过滤按段名,名字缺席时无害跳过。
17
+ *
18
+ * ════════════════════════════════════════════════════════════════════
19
+ * 开关的生效面:waterfall 删段,不碰别人的注册
20
+ * ════════════════════════════════════════════════════════════════════
21
+ * 宿主没有"禁用别人的段"的 API。两条路里选了 waterfall:`assemble()` 的
22
+ * 监听器返回值是权威结果(宿主源码 `return transformed`,complete 段除外),
23
+ * 删掉的段不会被恢复。同名覆盖(scoped section 返回 `''`)只适合"顶掉某一个
24
+ * 已知名字",对同层自己注册的段还会 throw —— 删段才是对全部段通用的开关。
25
+ */
26
+ import type { PromptExtData } from './render.ts';
27
+ /** 面板控制条 id 的段前缀(**传输层编码**;落盘用裸段名,不带前缀)。 */
28
+ export declare const SECTION_CONTROL_PREFIX = "sec:";
29
+ /**
30
+ * 自己的准则段:**不进清单、不被过滤**。
31
+ *
32
+ * 它已有独立的开关体系(扩展总开关 + 14 个分类开关)——再列一条"段开关"
33
+ * 就是同一件事的第二个开关,两边状态会漂移;过滤它还会让分类开关看起来
34
+ * 失效(段没了,分类怎么切都看不到效果)。
35
+ */
36
+ export declare const OWN_SECTION_NAME = "dsh-router-ext-prompt:guideline";
37
+ /** 一条已装配的段(面板清单用)。 */
38
+ export interface SectionEntry {
39
+ /** 段注册名(稳定;也是开关的落盘键)。 */
40
+ name: string;
41
+ /** 面板副行显示的原文(装配时 provider 现算的结果,**未做 `{{变量}}` 插值**)。 */
42
+ text: string;
43
+ }
44
+ /** `controlId`(`sec:…`)→ 段名;非段条目返回 null。 */
45
+ export declare function sectionNameOf(controlId: string): string | null;
46
+ /** 段名 → 面板控制条 id。 */
47
+ export declare function sectionControlId(name: string): string;
48
+ /**
49
+ * 段名的分组名。
50
+ *
51
+ * 查表顺序:**整名**(无冒号段名,如 `mcp-resource-servers`)→ **第一段前缀**
52
+ * (`tool:write` → `tool`)。两级都查不到 ⇒ 有冒号时用第一段原样当组名、
53
+ * 无冒号归「其他」。
54
+ */
55
+ export declare function groupLabelOf(name: string): string;
56
+ /** 面板条目标题:去掉组前缀的部分(`tool:write` → `write`;无冒号 = 全名)。 */
57
+ export declare function sectionTitleOf(name: string): string;
58
+ /**
59
+ * 当前被关掉的段名集合。**只认显式 `false`** —— 缺项是"没碰过"(默认开),
60
+ * 与 `resolveEnabledCategories` 对 `undefined` 的处置同一纪律。
61
+ */
62
+ export declare function disabledSectionNames(data: PromptExtData | undefined): Set<string>;
63
+ /**
64
+ * 过滤掉关闭的段(保序)。自己那段**恒放行**(理由见 `OWN_SECTION_NAME`)。
65
+ *
66
+ * ⚠️ 名字不在清单里的关闭项静默跳过:宿主段随版本增删,旧数据里的名字
67
+ * 失效是正常生命周期,报错只会把面板变成红字噪音。
68
+ */
69
+ export declare function filterSections<T extends {
70
+ name: string;
71
+ }>(sections: readonly T[], off: ReadonlySet<string>): T[];
72
+ /**
73
+ * 段清单缓存:waterfall 监听器是**唯一写入者**(装配路径顺路喂),
74
+ * 面板 controls getter 只读。分组与排序都在读取端做 —— 缓存保持
75
+ * 装配原序(`assemble` 已按 order/name 排好),组的次序自然等于 prompt 次序。
76
+ */
77
+ export declare function createSectionCache(): {
78
+ /** 用一次装配结果刷新(整表替换;自己的段不进清单)。 */
79
+ update: (sections: readonly {
80
+ name: string;
81
+ text: string;
82
+ }[]) => void;
83
+ /** 当前清单(拷贝,防调用方改缓存内部)。 */
84
+ list: () => SectionEntry[];
85
+ };
86
+ /** 一个分组(组名 + 组内条目,组内保持清单序)。 */
87
+ export interface SectionGroup {
88
+ label: string;
89
+ items: SectionEntry[];
90
+ }
91
+ /**
92
+ * 按段名前缀分组;**组的先后 = 首次出现序**(清单已按 prompt 序 ⇒ 组序即
93
+ * 段落在 prompt 里的先后)——不按字母重排,面板顺序与模型看到的顺序一致。
94
+ */
95
+ export declare function groupSections(list: readonly SectionEntry[]): SectionGroup[];
@@ -0,0 +1,35 @@
1
+ /**
2
+ * Anthropic Messages 协议。
3
+ *
4
+ * ════════════════════════════════════════════════════════════════════
5
+ * 为什么第一个做它
6
+ * ════════════════════════════════════════════════════════════════════
7
+ * Claude Code 只会说这一种协议,而它对 `ANTHROPIC_BASE_URL` 的支持是现成的:
8
+ * 把它指到我们的端点,就**直接**能用上账号池/组合/用量看板。其余三种协议
9
+ * (Responses/Gemini)没有这样的「一个客户端在等」的理由。
10
+ *
11
+ * ════════════════════════════════════════════════════════════════════
12
+ * 翻译表(唯一的事实来源)
13
+ * ════════════════════════════════════════════════════════════════════
14
+ * 请求 system → messages[0] 的 system-role 消息(字符串或块数组都要吃)
15
+ * tools[].input_schema → tools[].function.parameters
16
+ * tool_use / tool_result 块 → assistant.tool_calls / role:"tool"
17
+ * max_tokens / temperature / stop_sequences → 同名(下划线差异两处)
18
+ * 响应 message_start → usage(input_tokens 在这帧)
19
+ * content_block_start(text) → text 增量开始
20
+ * content_block_delta(text_delta) → text 增量
21
+ * content_block_delta(input_json_delta) → tool-args 增量
22
+ * content_block_delta(thinking_delta) → reasoning 增量
23
+ * content_block_stop → 该 index 结束(无对应规范事件,吃掉)
24
+ * message_delta → usage + finish_reason
25
+ * message_stop → 无对应事件(finish 已由 message_delta 给)
26
+ *
27
+ * 丢弃(明确不做翻译,见下):
28
+ * - `cache_control`:Anthropic 的提示缓存标记。跨到 Chat 后失去意义
29
+ * (Chat 的缓存是上游自动的),硬造一个 `prompt_cache_key` 反而是猜。
30
+ * - `thinking.budget_tokens`:只在响应侧保留为 reasoning 增量。
31
+ * - `mcp_servers` / `container` / `context_management`:客户端侧特性,
32
+ * 转发到只懂 Chat 的上游只会得到 400。
33
+ */
34
+ import { type ProtocolCodec } from './types.ts';
35
+ export declare const anthropicCodec: ProtocolCodec;
@@ -0,0 +1,25 @@
1
+ /**
2
+ * OpenAI Chat Completions 协议 —— **规范形态本身**,所以编解码是恒等变换。
3
+ *
4
+ * 这个文件承担两件别处不做的事:
5
+ *
6
+ * 1. **请求侧恒等**:`toChat`/`fromChat` 原样返回。供应商契约
7
+ * (`suppliers/contract.ts` 的 `chatOnce`)吃的就是 Chat Completions 请求体,
8
+ * 多一层拷贝只会让「哪一层改的」变难查。
9
+ *
10
+ * 2. **响应侧的规范事件翻译**:OpenAI SSE → `ChatEvent`,以及反向。这是**所有
11
+ * 协议共用的脊柱** —— Anthropic/Gemini 的 `decodeFrame` 拿到自己的帧后,
12
+ * 也要靠这里的逻辑把内容映射成同一组事件,才能让「Anthropic 客户端问、
13
+ * Gemini 供应商答」这条路只有两个模块参与,而不是 12 条路径。
14
+ *
15
+ * ⚠️ 这里的映射规则与 `llm/adapter.ts` 的 `translateSse` 是**同一套语义的两种
16
+ * 落点**:那边翻译成 DSH 的 `StreamChunk`(给宿主 agent loop 用),这边翻译
17
+ * 成 `ChatEvent`(给协议矩阵用)。两处都留着,因为消费方不同、且
18
+ * `translateSse` 里那些踩过坑的处理(残缺 tool_call 收尾、空串 name 剥除)
19
+ * 只对 DSH 有意义;但**改任何一处规则都必须看另一处**,否则同一个上游在
20
+ * 两个入口表现不一致。
21
+ */
22
+ import { type ProtocolCodec, finishOf } from './types.ts';
23
+ export declare const chatCodec: ProtocolCodec;
24
+ /** 收尾原因 → 规范形态(在各协议模块里共用,从这再导出省一次 import)。 */
25
+ export { finishOf };
@@ -0,0 +1,137 @@
1
+ /**
2
+ * 入站协议的四个入口 —— 把四种协议的请求翻成规范请求、交给 Router,
3
+ * 再把 Router 的响应翻回客户端说的协议。
4
+ *
5
+ * ════════════════════════════════════════════════════════════════════
6
+ * 为什么 Router 不认识协议,翻译却全在这
7
+ * ════════════════════════════════════════════════════════════════════
8
+ * Router 的核心职责是「选号/冷却/回退/记账」,它不该知道世界上有几种协议。
9
+ * 所以这里只做三件与协议有关的事:认路径、翻请求体、翻响应体。剩下全部
10
+ * 委托给 `Router.chatCompletions` —— 于是四种协议**自动**获得账号池、
11
+ * 组合回退、首字节预算、断流补帧、用量统计,一个都不用重写。
12
+ *
13
+ * 挂载点见 `src/index.ts` 的 `route(...)` 调用处;新增第五种协议只改
14
+ * 这里 + `protocol/` 下加一个模块,`src/index.ts` 只多一行 route。
15
+ */
16
+ import type { IncomingMessage, ServerResponse } from 'node:http';
17
+ import { StreamState } from './stream.ts';
18
+ import type { JsonObject, ProtocolId } from './types.ts';
19
+ /** 一次入站请求解析后的结果。 */
20
+ export interface InboundRequest {
21
+ /** 规范请求体(Chat Completions 形状)。 */
22
+ body: JsonObject;
23
+ /** 客户端要的流式。 */
24
+ stream: boolean;
25
+ /** 模型名(已从路径或请求体取出)。 */
26
+ model: string;
27
+ /** 推理等级;缺省 'auto'。 */
28
+ lv: string;
29
+ /** 客户端协议。 */
30
+ protocol: ProtocolId;
31
+ }
32
+ /**
33
+ * 读请求体 + 按协议翻成规范请求。
34
+ *
35
+ * 模型名从哪来因协议而异:Chat/Anthropic/Responses 在 body 的 `model`,
36
+ * Gemini 在 URL 路径(`:generateContent` 之前那段)—— 由 `modelFromPath`
37
+ * 调用方先解析好传进来。
38
+ */
39
+ export declare function parseInbound(req: IncomingMessage, protocol: ProtocolId, opts: {
40
+ limitBytes: number;
41
+ modelFromPath?: string;
42
+ lvFromBody?: (body: JsonObject) => string;
43
+ /**
44
+ * 路径里已声明的流式意图。
45
+ *
46
+ * Gemini 是唯一把「流不流式」放在 **URL 路径**(`:streamGenerateContent`
47
+ * vs `:generateContent`)而不是 body 里的协议。不认它,Gemini 的流式请求
48
+ * 会被当成非流式处理 —— 客户端拿到一整个 JSON,而它按 SSE 解析 ⇒ 空响应
49
+ * (2026-10-21 在 3080 实测)。
50
+ *
51
+ * `undefined` = 路径没说,看 body;`true/false` = 路径已明确,body 不算数。
52
+ */
53
+ streamFromPath?: boolean;
54
+ }): Promise<InboundRequest | {
55
+ error: string;
56
+ status: number;
57
+ }>;
58
+ /**
59
+ * 给 Router 的**非 chat 出口**:收集 OpenAI 形态的响应,翻译成客户端协议。
60
+ *
61
+ * ════════════════════════════════════════════════════════════════════
62
+ * 两条路径分开处理,因为时序完全不同
63
+ * ════════════════════════════════════════════════════════════════════
64
+ * 非流式:Router 一次 `head(200) + end(json)`。整份 JSON 已在手里,
65
+ * `decodeJson` → `encodeJson` 一个来回就完。
66
+ * 流式: Router 分很多次 `write(bytes)`,且**第一个字节前可能失败换号**。
67
+ * 不能等收齐再翻(那就没有流式了);每收到一段就翻一段。
68
+ *
69
+ * 流式那条为什么插一层 sink 而不是让 Router 直接写翻译后的字节:Router 的
70
+ * `writeChatResult` 里有一整套「客户端断开就取消上游流」「断流补 `[DONE]`」
71
+ * 的逻辑,那些必须继续对真实的 `res` 生效。sink 只是**转发的中继**:
72
+ * 它翻译字节、写给真正的 res;断开时调用方取消本层(见 index.ts 挂载处),
73
+ * Router 那侧的取消链路原样传到上游。
74
+ */
75
+ export declare function outboundSink(protocol: ProtocolId, model: string, res: ServerResponse): ResponseSinkLike;
76
+ /** Router 的 ResponseSink 形状(本地声明,避免 protocol → router 的循环依赖)。 */
77
+ export interface ResponseSinkLike {
78
+ head?: (status: number, headers: Record<string, string>) => void;
79
+ write: (bytes: Uint8Array) => void;
80
+ end: () => void;
81
+ }
82
+ /**
83
+ * 一段上游 OpenAI SSE 文本的转码器 —— **有状态**。
84
+ *
85
+ * 上游一帧被拆在两个 TCP 段里是常态(Router 按拿到的字节原样转交),所以
86
+ * 分帧缓冲必须跨 `write()` 调用存活。这里用 `sse.ts` 的 `SseSplitter`(它内部
87
+ * 留着半个帧)而不是每次现切 —— 早先每次现切会把不完整帧当完整帧解析,
88
+ * 表现是「文本丢一半、工具参数截断」,而 HTTP 200,客户端看不到任何错误。
89
+ *
90
+ * 生命周期与 sink 一致:一个请求一个实例。
91
+ */
92
+ export declare class ChunkTranslator {
93
+ private readonly splitter;
94
+ private readonly state;
95
+ constructor(state: StreamState);
96
+ /** 喂一段文本,返回可写给客户端的帧。 */
97
+ push(text: string): string[];
98
+ /** 流结束:冲刷残留帧(上游不发尾空行是有的),返回收尾帧。 */
99
+ end(): string[];
100
+ private emit;
101
+ }
102
+ /**
103
+ * 单次调用版:把一段**完整**的 SSE 文本转成客户端帧。
104
+ *
105
+ * 只适合「手里已经有一整段完整数据」的场合(测试、非流式上游误发了 SSE)。
106
+ * 真正的流式路径必须用 `ChunkTranslator` 保持跨段状态。
107
+ */
108
+ export declare function translateChunk(state: StreamState, text: string): string[];
109
+ /**
110
+ * 从 Gemini 的 URL 路径里取模型名。
111
+ *
112
+ * 形如 `/v1beta/models/<model>:generateContent`(或 `:streamGenerateContent`),
113
+ * `<model>` 本身可以含斜杠,所以取 `models/` 之后到最后一个 `:` 之前那段。
114
+ *
115
+ * **必须先切掉 query**:Gemini CLI 默认带 `?alt=sse`,不切的话 `$` 锚点匹配
116
+ * 不上,模型名变空 —— 表现为「404 model not found」而不是参数错误,很难查。
117
+ * 顺便支持 `/v1beta/models/<model>` 这种带版本号但无方法的形态(走 GET 列表时
118
+ * 会撞上,这里取不到 `/` 就是取不到,返回 undefined 让上层报 400)。
119
+ */
120
+ export declare function geminiModelFromPath(pathname: string): string | undefined;
121
+ /**
122
+ * Gemini 的流式意图在**路径尾部**:`:streamGenerateContent` 是流式、
123
+ * `:generateContent` 是非流式。返回 `undefined` 表示路径没表态(不是 Gemini
124
+ * 形态,或路径不可识别),交给 body 的 `stream` 决定。
125
+ *
126
+ * 为什么不能只看 body:Gemini 的请求体里**没有** `stream` 字段 —— 它的官方
127
+ * 契约就是靠 URL 区分两种方法。不认这个,流式请求会被当成非流式,客户端按
128
+ * SSE 解析一整个 JSON ⇒ 空响应(2026-10-21 在 3080 实测到)。
129
+ */
130
+ export declare function geminiStreamFromPath(pathname: string): boolean | undefined;
131
+ /**
132
+ * 从请求体取推理等级(各协议字段名不同)。
133
+ *
134
+ * 只认「明确说了要多少思考」的形态,其余一律 'auto':把「有这个字段」当成
135
+ * 「要高档推理」会让所有带 thinking 块的请求都按 high 走,费用与延迟都变。
136
+ */
137
+ export declare function lvOf(body: JsonObject, protocol: ProtocolId): string;