dsh-router-core 0.4.6 → 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 (63) hide show
  1. package/README.md +152 -12
  2. package/docs/ext.md +1 -1
  3. package/docs/protocols.md +198 -0
  4. package/lib/client-registry.js +146 -106
  5. package/lib/client.js +146 -106
  6. package/lib/ext-prompt/index.js +182 -17
  7. package/lib/index.js +2732 -55
  8. package/lib/types/ext/contract.d.ts +20 -1
  9. package/lib/types/ext-prompt/content.d.ts +7 -1
  10. package/lib/types/ext-prompt/mount.d.ts +12 -1
  11. package/lib/types/ext-prompt/plugin.d.ts +10 -0
  12. package/lib/types/ext-prompt/render.d.ts +14 -3
  13. package/lib/types/ext-prompt/sections.d.ts +95 -0
  14. package/lib/types/protocol/anthropic.d.ts +35 -0
  15. package/lib/types/protocol/chat.d.ts +25 -0
  16. package/lib/types/protocol/entry.d.ts +137 -0
  17. package/lib/types/protocol/gemini.d.ts +57 -0
  18. package/lib/types/protocol/index.d.ts +13 -0
  19. package/lib/types/protocol/responses.d.ts +41 -0
  20. package/lib/types/protocol/sse.d.ts +41 -0
  21. package/lib/types/protocol/stream.d.ts +92 -0
  22. package/lib/types/protocol/types.d.ts +224 -0
  23. package/lib/types/router/index.d.ts +42 -1
  24. package/lib/types/router/types.d.ts +19 -0
  25. package/lib/types/shared.d.ts +11 -0
  26. package/lib/types/suppliers/contract.d.ts +8 -0
  27. package/package.json +6 -6
  28. package/src/client/ExtDetail.tsx +25 -5
  29. package/src/client/group-controls.test.ts +53 -0
  30. package/src/client/group-controls.ts +43 -0
  31. package/src/client/router.css +20 -1
  32. package/src/ext/contract.ts +20 -1
  33. package/src/ext-prompt/assembly.test.ts +63 -0
  34. package/src/ext-prompt/content.ts +8 -10
  35. package/src/ext-prompt/ext-prompt.test.ts +38 -28
  36. package/src/ext-prompt/index.ts +50 -2
  37. package/src/ext-prompt/mount.test.ts +18 -1
  38. package/src/ext-prompt/mount.ts +22 -7
  39. package/src/ext-prompt/plugin.ts +64 -1
  40. package/src/ext-prompt/render.test.ts +1 -1
  41. package/src/ext-prompt/render.ts +17 -6
  42. package/src/ext-prompt/section-controls.test.ts +148 -0
  43. package/src/ext-prompt/section-filter.test.ts +140 -0
  44. package/src/ext-prompt/sections.test.ts +123 -0
  45. package/src/ext-prompt/sections.ts +193 -0
  46. package/src/ext-route.test.ts +16 -0
  47. package/src/index.ts +109 -1
  48. package/src/protocol/anthropic.ts +662 -0
  49. package/src/protocol/chat.ts +272 -0
  50. package/src/protocol/entry.ts +440 -0
  51. package/src/protocol/gemini.ts +548 -0
  52. package/src/protocol/index.ts +31 -0
  53. package/src/protocol/protocol.test.ts +728 -0
  54. package/src/protocol/responses.ts +551 -0
  55. package/src/protocol/sse.ts +94 -0
  56. package/src/protocol/stream.ts +195 -0
  57. package/src/protocol/types.ts +249 -0
  58. package/src/protocol/wire-e2e.test.ts +208 -0
  59. package/src/protocol/wiring.test.ts +225 -0
  60. package/src/router/index.ts +142 -23
  61. package/src/router/types.ts +19 -0
  62. package/src/shared.ts +11 -0
  63. package/src/suppliers/contract.ts +8 -0
@@ -5,7 +5,7 @@ const PROMPT_CATEGORIES = [
5
5
  id: "identity",
6
6
  title: "身份气质",
7
7
  defaultOn: true,
8
- body: "身份气质:大肥鱼 🐋(资深开发者);懒=高效不是马虎;安静、克制、直接;默认中文,直接给能跑的东西。"
8
+ body: "身份气质:大肥鱼 🐋(资深开发者);懒=高效不是马虎;安静、克制、直接;回复、思考过程必须使用中文,直接给能跑的东西。"
9
9
  },
10
10
  {
11
11
  id: "scope",
@@ -31,12 +31,6 @@ const PROMPT_CATEGORIES = [
31
31
  defaultOn: true,
32
32
  body: "收口(只在任务完成时做,与跑测试同级):①反查过期:改动让哪些注释/文档/结论/代码引用点(凡以名字对齐的调用方、开关名、注册键)不符事实?当场改或删;②反查重复:新旧成两份就并成一处;③反查形状:该合的合、该归档的归位。做完那块区域该比动手前更整或更少。"
33
33
  },
34
- {
35
- id: "hardening",
36
- title: "固化",
37
- defaultOn: true,
38
- body: "固化:只对数据丢失/安全这类真损失补检查——会变红(注入违规必须失败)、可重放(一条命令)、归属清晰(失败信息指向违规处);普通 bug 修好即止,不额外造闸门。"
39
- },
40
34
  {
41
35
  id: "verdifiable",
42
36
  title: "可判定",
@@ -91,7 +85,7 @@ const PROMPT_CATEGORIES = [
91
85
  *
92
86
  * ⚠️ 拆分时差点丢它:原文是 `[…] ·身份气质:…`,标题挂在方括号里、
93
87
  * **不属于任何单条分类**,按分隔符切分时自然被丢掉
94
- * (只在 `content.ts` 头注出现过、没进任何 `body`)。⇒ 模型会看到 14 条
88
+ * (只在 `content.ts` 头注出现过、没进任何 `body`)。⇒ 模型会看到一堆
95
89
  * **没有名字**的规则,而"这套东西叫什么、为什么存在"恰恰是它需要的上下文。
96
90
  *
97
91
  * 形状:**不属于任何分类** ⇒ 不可被单条开关关掉(它一关,准则就无名无姓)。
@@ -165,7 +159,7 @@ function resolveEnabledCategories(masterOn, data) {
165
159
  * 渲染成**一个**段落文本(多分类拼一起)。
166
160
  *
167
161
  * 为什么要拼成一段而不是一段一个:面板开关是"分类"粒度,而 prompt 读起来
168
- * 是一段连续的准则 —— 分成 14 个 section 会让 KV cache 每个分类各占一段、
162
+ * 是一段连续的准则 —— 分成多个 section 会让 KV cache 每个分类各占一段、
169
163
  * 增删任意一个分类都让其后全部位移。这里**注册一个 section,文本由开关拼装**。
170
164
  *
171
165
  * @returns 关闭的分类不进结果;全关 ⇒ 空串(上游 `renderPrompt` 会丢弃空段)。
@@ -224,6 +218,120 @@ function resolveCategories(data) {
224
218
  return out;
225
219
  }
226
220
  //#endregion
221
+ //#region src/ext-prompt/sections.ts
222
+ /** 面板控制条 id 的段前缀(**传输层编码**;落盘用裸段名,不带前缀)。 */
223
+ const SECTION_CONTROL_PREFIX = "sec:";
224
+ /**
225
+ * 自己的准则段:**不进清单、不被过滤**。
226
+ *
227
+ * 它已有独立的开关体系(扩展总开关 + 14 个分类开关)——再列一条"段开关"
228
+ * 就是同一件事的第二个开关,两边状态会漂移;过滤它还会让分类开关看起来
229
+ * 失效(段没了,分类怎么切都看不到效果)。
230
+ */
231
+ const OWN_SECTION_NAME = "dsh-router-ext-prompt:guideline";
232
+ /**
233
+ * 段名前缀 → 面板分组名。**未知前缀原样当组名**(不硬编全部前缀):
234
+ * 宿主还会加新段(`web:`、`structured:`…),映射漏了顶多组名不雅,
235
+ * 条目照常列出照常开关 —— 映射表漂移不允许变成"段消失"。
236
+ */
237
+ /**
238
+ * 段名前缀 → 面板分组名。
239
+ *
240
+ * ⚠️ 键是**段名的第一段**(冒号前),不是"看起来像"的前缀。2026-10-21 实测
241
+ * 真宿主(3081,23 条 controls / 9 段)纠正过两处脑补:
242
+ * - `harness:source`(运行时上下文里 Harness 检出的位置)第一段是 `harness`,
243
+ * 按前缀映射会被并入「开场身份」—— 而它不是身份,是运行环境说明;
244
+ * - `app:web-surface`(Web GUI 说明)第一段是 `app`,与"界面与交付"无关。
245
+ * 所以 `harness` 刻意**不映射**:`harness:identity` 是身份、`harness:source` 不是,
246
+ * 靠第一段分不开 —— 让它们各自留下真实前缀(`harness` / `app`)比归错组诚实。
247
+ * `ui:` 才是界面与交付那一组(`ui:deliverable-file-references`)。
248
+ *
249
+ * 映射漏了或故意不映射的前缀,组名**原样显示前缀**、条目照常能列能开关 ——
250
+ * 映射表漂移不允许变成"段消失"。
251
+ */
252
+ const GROUP_LABELS = {
253
+ deployment: "部署人设",
254
+ plan: "计划模式",
255
+ team: "团队",
256
+ tool: "工具说明",
257
+ mcp: "MCP 服务",
258
+ context: "运行上下文",
259
+ ui: "界面与交付"
260
+ };
261
+ /**
262
+ * 无冒号段名的映射(宿主有几个段名整名不带冒号)。
263
+ *
264
+ * `harness:identity` 才是开场身份,它带冒号走前缀表;这里只收无冒号的整名。
265
+ */
266
+ const WHOLE_NAME_LABELS = { "mcp-resource-servers": "MCP 资源" };
267
+ /** `controlId`(`sec:…`)→ 段名;非段条目返回 null。 */
268
+ function sectionNameOf(controlId) {
269
+ return controlId.startsWith("sec:") ? controlId.slice(4) : null;
270
+ }
271
+ /** 段名 → 面板控制条 id。 */
272
+ function sectionControlId(name) {
273
+ return SECTION_CONTROL_PREFIX + name;
274
+ }
275
+ /**
276
+ * 段名的分组名。
277
+ *
278
+ * 查表顺序:**整名**(无冒号段名,如 `mcp-resource-servers`)→ **第一段前缀**
279
+ * (`tool:write` → `tool`)。两级都查不到 ⇒ 有冒号时用第一段原样当组名、
280
+ * 无冒号归「其他」。
281
+ */
282
+ function groupLabelOf(name) {
283
+ const whole = WHOLE_NAME_LABELS[name];
284
+ if (whole !== void 0) return whole;
285
+ const i = name.indexOf(":");
286
+ if (i < 0) return "其他";
287
+ const head = name.slice(0, i);
288
+ return GROUP_LABELS[head] ?? head;
289
+ }
290
+ /** 面板条目标题:去掉组前缀的部分(`tool:write` → `write`;无冒号 = 全名)。 */
291
+ function sectionTitleOf(name) {
292
+ const i = name.indexOf(":");
293
+ return i >= 0 ? name.slice(i + 1) : name;
294
+ }
295
+ /**
296
+ * 当前被关掉的段名集合。**只认显式 `false`** —— 缺项是"没碰过"(默认开),
297
+ * 与 `resolveEnabledCategories` 对 `undefined` 的处置同一纪律。
298
+ */
299
+ function disabledSectionNames(data) {
300
+ const out = /* @__PURE__ */ new Set();
301
+ const saved = data?.sections;
302
+ if (!saved) return out;
303
+ for (const [name, on] of Object.entries(saved)) if (on === false) out.add(name);
304
+ return out;
305
+ }
306
+ /**
307
+ * 过滤掉关闭的段(保序)。自己那段**恒放行**(理由见 `OWN_SECTION_NAME`)。
308
+ *
309
+ * ⚠️ 名字不在清单里的关闭项静默跳过:宿主段随版本增删,旧数据里的名字
310
+ * 失效是正常生命周期,报错只会把面板变成红字噪音。
311
+ */
312
+ function filterSections(sections, off) {
313
+ return sections.filter((s) => s.name === "dsh-router-ext-prompt:guideline" || !off.has(s.name));
314
+ }
315
+ /**
316
+ * 段清单缓存:waterfall 监听器是**唯一写入者**(装配路径顺路喂),
317
+ * 面板 controls getter 只读。分组与排序都在读取端做 —— 缓存保持
318
+ * 装配原序(`assemble` 已按 order/name 排好),组的次序自然等于 prompt 次序。
319
+ */
320
+ function createSectionCache() {
321
+ let entries = [];
322
+ return {
323
+ update(sections) {
324
+ entries = sections.filter((s) => s.name !== OWN_SECTION_NAME).map((s) => ({
325
+ name: s.name,
326
+ text: s.text
327
+ }));
328
+ },
329
+ list() {
330
+ return entries.map((e) => ({ ...e }));
331
+ }
332
+ };
333
+ }
334
+ //#endregion
227
335
  //#region src/ext-prompt/plugin.ts
228
336
  /** 从对象里去掉某个键(不改原对象)。`writeData` 是整块替换 ⇒ 删一个键要重写。 */
229
337
  function omit(obj, key) {
@@ -278,7 +386,7 @@ function createPromptExt(deps) {
278
386
  get controls() {
279
387
  const data = readData();
280
388
  const on = resolveEnabledCategories(true, data);
281
- return resolveCategories(data).map((c) => {
389
+ const categories = resolveCategories(data).map((c) => {
282
390
  const t = effectiveText(c.id, data?.text?.[c.id], c);
283
391
  return {
284
392
  id: c.id,
@@ -290,9 +398,34 @@ function createPromptExt(deps) {
290
398
  on: on.has(c.id)
291
399
  };
292
400
  });
401
+ const off = disabledSectionNames(data);
402
+ const sections = deps.getSections().filter((s) => s.name !== OWN_SECTION_NAME).map((s) => ({
403
+ id: sectionControlId(s.name),
404
+ title: sectionTitleOf(s.name),
405
+ on: !off.has(s.name),
406
+ detail: s.name,
407
+ body: s.text === "" ? "(此段当前为空)" : s.text,
408
+ group: groupLabelOf(s.name),
409
+ sortable: false
410
+ }));
411
+ return [...categories, ...sections];
293
412
  },
294
413
  setControl: (controlId, on) => {
295
414
  if (!store) return false;
415
+ const secName = sectionNameOf(controlId);
416
+ if (secName !== null) {
417
+ if (secName === "dsh-router-ext-prompt:guideline") return false;
418
+ if (!deps.getSections().some((s) => s.name === secName)) return false;
419
+ const data = readData();
420
+ const sections = { ...data.sections ?? {} };
421
+ if (on) delete sections[secName];
422
+ else sections[secName] = false;
423
+ const next = { ...data };
424
+ if (Object.keys(sections).length > 0) next.sections = sections;
425
+ else delete next.sections;
426
+ store.writeData(EXT_PROMPT_ID, next);
427
+ return true;
428
+ }
296
429
  const data = readData();
297
430
  if (!resolveCategories(data).some((c) => c.id === controlId)) return false;
298
431
  store.writeData(EXT_PROMPT_ID, {
@@ -424,14 +557,22 @@ function createPromptExt(deps) {
424
557
  /** 段落名(**稳定**):改名会让已注册的作用域 section 与旧名并存。 */
425
558
  const PROMPT_SECTION_NAME = "dsh-router-ext-prompt:guideline";
426
559
  /**
427
- * 段落 order:100。
560
+ * 段落 order:**-2000 = 全场最前**(2026-10-21 用户点名「把准则放到最前」)。
428
561
  *
429
- * 官方分配的槽位(`dsh-system-prompt` 的 `SECTION_ORDERS`):
430
- * `PERSONA_PREFIX: 0` · `PLAN_POLICY: 500` · `TEAM_POLICY: 600` …·
431
- * 取 100 = 紧随 persona、远早于任何工具段 ⇒ 准则出现在身份之后、工具说明之前,
432
- * 与用户看到的记忆快照位置一致。
562
+ * 官方分配的槽位(`dsh-system-prompt` 的 `SECTION_ORDERS`)最低一档是
563
+ * `HARNESS_IDENTITY: -1000`(第一方开场白 `You are an AI agent powered by
564
+ * DeepSeek Harness.`),其后才是 `DEPLOYMENT_PERSONA_PREFIX: 0`、`PLAN_POLICY: 500`
565
+ * … `HARNESS_SOURCE: 10000`。
566
+ *
567
+ * 取 **-2000**(低于官方最低槽 1000)而不是「正好 -1000」:
568
+ * - 撞在 -1000 上要靠**段名的字母序**决胜(`comparePromptSections` 是
569
+ * `order` 再按 `name`)—— 那是实现细节,改段名就翻盘,不作为位置依据;
570
+ * - 第一方那行在本部署被 `includeHarnessIdentity: false` 关掉了,但**别的
571
+ * 部署可能还开着** ⇒ 准则仍须排在它前面,不能假设它不存在。
572
+ *
573
+ * ⚠️ `order` 只要求有限数(`Number.isFinite`),负数合法。
433
574
  */
434
- const SECTION_ORDER = 100;
575
+ const SECTION_ORDER = -2e3;
435
576
  /**
436
577
  * 挂载准则段落。返回卸载函数。
437
578
  *
@@ -511,15 +652,39 @@ function apply(ctx) {
511
652
  isEnabled: () => store?.isEnabled(EXT_PROMPT_ID) === true,
512
653
  readData: () => store?.readData(EXT_PROMPT_ID)
513
654
  });
655
+ const cache = createSectionCache();
656
+ let disposeAssemble;
657
+ let disposeChange;
658
+ const sp = systemPromptOf(ctx);
659
+ if (sp) {
660
+ disposeAssemble = ctx.on("system-prompt/assemble", async (assembly, _context, next) => {
661
+ const result = await next();
662
+ cache.update(result.sections);
663
+ if (store?.isEnabled("prompt") !== true) return result;
664
+ const off = disabledSectionNames(store.readData(EXT_PROMPT_ID));
665
+ if (off.size === 0) return result;
666
+ return {
667
+ ...result,
668
+ sections: filterSections(result.sections, off)
669
+ };
670
+ });
671
+ disposeChange = ctx.on("system-prompt/change", () => {
672
+ sp.assemble().catch(() => {});
673
+ });
674
+ sp.assemble().catch(() => {});
675
+ }
514
676
  if (table !== void 0 && table["prompt"] === void 0) {
515
677
  table[EXT_PROMPT_ID] = createPromptExt({
516
678
  isSystemPromptReady: () => !!systemPromptOf(ctx),
517
- store
679
+ store,
680
+ getSections: () => cache.list()
518
681
  });
519
682
  store?.setEnabled(EXT_PROMPT_ID, true);
520
683
  }
521
684
  ctx.effect(() => () => {
522
685
  dispose();
686
+ disposeAssemble?.();
687
+ disposeChange?.();
523
688
  if (table !== void 0) delete table[EXT_PROMPT_ID];
524
689
  });
525
690
  }