dsh-router-core 0.4.2 → 0.4.3

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 (49) hide show
  1. package/README.md +9 -10
  2. package/cordis.patch.yml +10 -0
  3. package/docs/ext.md +64 -2
  4. package/lib/client-registry.js +633 -50
  5. package/lib/client.js +633 -50
  6. package/lib/contract-D4BWZ7Xe.js +13 -0
  7. package/lib/ext-prompt/index.js +527 -0
  8. package/lib/ext-prompt/locale/en.json +6 -0
  9. package/lib/ext-prompt/locale/zh.json +6 -0
  10. package/lib/ext-prompt/package.json +5 -0
  11. package/lib/ext-test/index.js +1 -12
  12. package/lib/index.js +249 -117
  13. package/lib/types/ext/contract.d.ts +113 -0
  14. package/lib/types/ext-prompt/content.d.ts +57 -0
  15. package/lib/types/ext-prompt/index.d.ts +53 -0
  16. package/lib/types/ext-prompt/logo.d.ts +17 -0
  17. package/lib/types/ext-prompt/mount.d.ts +51 -0
  18. package/lib/types/ext-prompt/plugin.d.ts +32 -0
  19. package/lib/types/ext-prompt/render.d.ts +105 -0
  20. package/lib/types/host-version.d.ts +7 -8
  21. package/lib/types/index.d.ts +1 -1
  22. package/lib/types/shared.d.ts +33 -0
  23. package/package.json +17 -7
  24. package/src/client/EndpointTab.tsx +15 -18
  25. package/src/client/ExtDetail.tsx +552 -9
  26. package/src/client/index.tsx +7 -28
  27. package/src/client/router.css +132 -62
  28. package/src/ext/contract.ts +111 -0
  29. package/src/ext-prompt/assembly.test.ts +79 -0
  30. package/src/ext-prompt/content.ts +174 -0
  31. package/src/ext-prompt/ext-prompt.test.ts +432 -0
  32. package/src/ext-prompt/index.ts +104 -0
  33. package/src/ext-prompt/locale/en.json +6 -0
  34. package/src/ext-prompt/locale/zh.json +6 -0
  35. package/src/ext-prompt/logo.svg +14 -0
  36. package/src/ext-prompt/logo.ts +18 -0
  37. package/src/ext-prompt/mount.test.ts +95 -0
  38. package/src/ext-prompt/mount.ts +79 -0
  39. package/src/ext-prompt/plugin.ts +223 -0
  40. package/src/ext-prompt/render.test.ts +256 -0
  41. package/src/ext-prompt/render.ts +216 -0
  42. package/src/ext-prompt/row.json +5 -0
  43. package/src/ext-route.test.ts +78 -0
  44. package/src/host-version.test.ts +21 -28
  45. package/src/host-version.ts +39 -39
  46. package/src/index.ts +216 -94
  47. package/src/llm/adapter.test.ts +19 -42
  48. package/src/llm/adapter.ts +20 -47
  49. package/src/shared.ts +34 -0
@@ -0,0 +1,57 @@
1
+ /**
2
+ * 提示词内容层 —— 准则 v5 的**逐字**副本,按原分隔拆成分类。
3
+ *
4
+ * ════════════════════════════════════════════════════════════════════
5
+ * 为什么一字不改
6
+ * ════════════════════════════════════════════════════════════════════
7
+ * 这些字是**用户自己写的准则**,不是本插件的文案。任何"顺手润色"都是擅自
8
+ * 改写用户的规则 —— 改出来的版本和用户心里的版本不一致,而用户看不见差异,
9
+ * 于是准则开始按**另一份**文本执行。所以这里是逐字复制,**不做任何措辞优化**。
10
+ *
11
+ * 拆分只为「可开关」,不为了重排:顺序与原文出现顺序一致,`order` 也按此递增。
12
+ *
13
+ * ⚠️ **本文件是唯一事实源**。准则原本存放在 Mnemon 记忆系统的 `USER.md`;
14
+ * 那份已经在插件验证生效后删除(否则新旧两份注入,每轮白烧一次 token 且
15
+ * 永远有两份可能漂移的规则)。**改动准则 = 只改这里。**
16
+ */
17
+ /** 一个可独立开关的提示词分类。 */
18
+ export interface PromptCategory {
19
+ /** 稳定 id:面板开关与 `router.extStore` 数据块的键,**改名会丢用户配置**。 */
20
+ readonly id: string;
21
+ /** 面板显示名。 */
22
+ readonly title: string;
23
+ /**
24
+ * 是否默认开启。
25
+ *
26
+ * 默认**关**的只有「可以按项目关掉」的那几条(领地/结构/交付/执行);
27
+ * 身份、底线、收口这类是准则的骨架,一律默认开 —— 默认关掉等于静默移除规则。
28
+ */
29
+ readonly defaultOn: boolean;
30
+ /** 段落正文(逐字)。空串 = 该分类不产出任何 prompt 文本。 */
31
+ readonly body: string;
32
+ }
33
+ export declare const PROMPT_CATEGORIES: readonly PromptCategory[];
34
+ /**
35
+ * 整段的标题(**与分类无关**)。
36
+ *
37
+ * ⚠️ 拆分时差点丢它:原文是 `[…] ·身份气质:…`,标题挂在方括号里、
38
+ * **不属于任何单条分类**,按分隔符切分时自然被丢掉
39
+ * (只在 `content.ts` 头注出现过、没进任何 `body`)。⇒ 模型会看到 14 条
40
+ * **没有名字**的规则,而"这套东西叫什么、为什么存在"恰恰是它需要的上下文。
41
+ *
42
+ * 形状:**不属于任何分类** ⇒ 不可被单条开关关掉(它一关,准则就无名无姓)。
43
+ * 要关掉整个准则,用 ext 的总开关。
44
+ *
45
+ * ⚠️ 2026-09-29 用户把副题去掉(原 `[准则 v5 · 懒人梯子 + 全局收口 + 固化]`)——
46
+ * 那三个词是**内容**(三条纪律的名字),挂在标题上等于"标题在重复正文",
47
+ * 而正文里本来就有各自的分类标题。改成短标题,分类名留给下面每一行。
48
+ */
49
+ export declare const PROMPT_TITLE = "[\u51C6\u5219 v5]";
50
+ /**
51
+ * 分类的默认开关状态。
52
+ *
53
+ * ⚠️ **已保存的配置优先**:用户在面板改过的分类以 `extStore` 里的为准,
54
+ * 这里只在「用户从没碰过这一类」时提供默认值。所以**改 `defaultOn` 不会
55
+ * 覆盖用户已有选择**(这是刻意的 —— 否则改默认值等于静默改用户配置)。
56
+ */
57
+ export declare function defaultToggles(): Record<string, boolean>;
@@ -0,0 +1,53 @@
1
+ /**
2
+ * 分层提示词扩展的 **cordis 行** —— 官方插件页(设置 → 插件 → dsh-router-core)详情页
3
+ * 原生「包含的组件」里的那一行。
4
+ *
5
+ * 壳的形状与 `ext-test` 同款(cordis 身份 + 登记,实现 = `plugin.ts`),
6
+ * 但**多一件执行面的事**:挂 system prompt 段落。ext-test 不做拦截、只登记;
7
+ * 本扩展要把准则真注入,所以额外等 `systemPrompt` service 就绪后挂 section。
8
+ *
9
+ * **默认开启**(patch 里不带 `disabled`,与三行内置供应商一致):
10
+ * 协作准则是每次协作都要的基础设施,不是可选增强。与 `ext-test` 的
11
+ * `disabled: true` **刻意不同** —— 自检是「想跑才跑」的工具,准则不是;
12
+ * 让准则默认缺席,会变成「用户不知道它存在 ⇒ 协作按另一套规则执行」。
13
+ *
14
+ * 登记时把扩展开关置为开:本扩展**没有独立的运行期开关**,它的开关就是这一行
15
+ * (宿主管的那个)。不置上的话,用户开了行还要再点一次,两级开关说同一件事。
16
+ */
17
+ import type { Context } from '@deepseek-ai/cordis';
18
+ /** cordis 插件身份(配置树里这一行的技术名;显示名走同目录的 locale)。 */
19
+ export declare const name = "dsh-router-ext-prompt";
20
+ /**
21
+ * 声明本扩展依赖的 service。
22
+ *
23
+ * ⚠️⚠️ **必须导出这个数组,且 `apply` 里直接用 `ctx.systemPrompt`**(2026-09-29 修)
24
+ * ——我原先写成「在 apply 里 `ctx.inject(['systemPrompt'], cb)`」,**准则一直不进
25
+ * prompt**:面板上卡片、开关、原文全都正常(`GET /router/api/ext` 实测有 14 条
26
+ * controls),但模型看到的 system prompt 里没有它。
27
+ *
28
+ * 根因(本机 cordis 4.0.4 实测):`ctx.inject(deps, cb)` 的回调**不是同步触发** ——
29
+ * provide 之后立刻 `inject`,回调要等到下一个异步点才跑。插件的 `apply` 是**同步
30
+ * 函数**,返回时那次回调还没发生;而真实加载路径不保证之后还有机会去等它,
31
+ * 于是 `mountPromptSection` 从没被执行 ⇒ **准则一次都没进过 prompt**。
32
+ *
33
+ * ⚠️ 我第一版把这个机制写成「要等 `ctx.start()`」——**那是错的**:`start` 根本不在
34
+ * Context 的公开面上(原型只有 extend/isolate/intercept)。准确说法是「下一个
35
+ * 异步点」,具体机制随版本变。**结论不依赖这个细节**,只依赖「apply 里手动
36
+ * inject 不可靠」这一事实。
37
+ *
38
+ * 正确形状(对照 `dsh-client-ui-deliverables` 等真实使用者):导出 `inject` 数组,
39
+ * cordis 看到它就**把整个 apply 推迟到这些 service 就绪之后**再调用 ——
40
+ * 于是 `ctx.systemPrompt` 一定是活的,`section()` 可以直接调。
41
+ *
42
+ * ⚠️ `router.ext` 也要列:那一半(面板登记)此前"碰巧"能用,是因为
43
+ * `dsh-router` 核心先 provide 了它、`apply` 又跑在它之后。**那是时序上的运气,
44
+ * 不是保证** —— 一并列进 `inject`,两种 service 都由 cordis 编排。
45
+ */
46
+ export declare const inject: string[];
47
+ /**
48
+ * 登记本扩展**并**挂载准则段落。
49
+ *
50
+ * 此刻 `ctx.systemPrompt` / `ctx.router.ext` / `ctx.router.extStore` **必定已就绪**
51
+ * (见上面 `inject` 的注释),所以下面**直接用**,不再做二次判空。
52
+ */
53
+ export declare function apply(ctx: Context): void;
@@ -0,0 +1,17 @@
1
+ /**
2
+ * 分层提示词扩展的图标 —— **一张深色圆角瓦片**,以本地 base64 内联(离线可用)。
3
+ *
4
+ * 为什么这样画:它做的事是「一份可以逐条开关的规则清单」⇒ 图形 = 清单 + 勾选。
5
+ * 不用齿轮(那是设置)、不用扳手(那是工具)—— 那两样也在同一个面板里,
6
+ * 混用会让人分不清「改行为」和「改这套规则本身」。
7
+ *
8
+ * ⚠️ **单色 + 深底**(与 RTK logo 同款处理):`.dshr-providerIcon` 只给圆角与
9
+ * 尺寸、**不给底色**,所以底色必须由本图自带 ⇒ 深浅两套主题下都成立。
10
+ * 勾选用略暖的绿(`#A7C0A5`)与清单的灰白拉开,暗示"这些可以勾掉"。
11
+ *
12
+ * ⚠️ **base64 必须由 `logo.svg` 生成**(`base64 -i logo.svg | tr -d '\n'`)。
13
+ * 我第一版**手编**了一串 base64,解出来是乱码(`tept-Gradient`、`#17118=D`)——
14
+ * 而客户端的 `onError` 会把加载失败的图**静默藏掉**、退回默认闪电图标,
15
+ * **看起来"正常"、实际图标根本没换**。编造的常量不会报错,这是它最险的地方。
16
+ */
17
+ export declare const PROMPT_LOGO_URL = "data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCA2NCA2NCI+CiAgPHJlY3Qgd2lkdGg9IjY0IiBoZWlnaHQ9IjY0IiByeD0iMTYiIGZpbGw9IiMxNzE4MUQiLz4KICA8ZyBmaWxsPSJub25lIiBzdHJva2U9IiNFREVERUIiIHN0cm9rZS13aWR0aD0iMyIgc3Ryb2tlLWxpbmVjYXA9InJvdW5kIiBzdHJva2UtbGluZWpvaW49InJvdW5kIj4KICAgIDxwYXRoIGQ9Ik0xOCAxNmgyMCIvPgogICAgPHBhdGggZD0iTTE4IDI3aDI0Ii8+CiAgICA8cGF0aCBkPSJNMTggMzhoMjQiLz4KICAgIDxwYXRoIGQ9Ik0xOCA0OWgyNCIvPgogIDwvZz4KICA8ZyBmaWxsPSJub25lIiBzdHJva2U9IiNBN0MwQTUiIHN0cm9rZS13aWR0aD0iMy40IiBzdHJva2UtbGluZWNhcD0icm91bmQiIHN0cm9rZS1saW5lam9pbj0icm91bmQiPgogICAgPHBhdGggZD0iTTkgMTYuNWwyLjUgMi41TDE2IDE0LjUiLz4KICAgIDxwYXRoIGQ9Ik05IDI3LjVsMi41IDIuNUwxNiAyNS41Ii8+CiAgICA8cGF0aCBkPSJNOSAzOC41bDIuNSAyLjVMMTYgMzYuNSIvPgogIDwvZz4KPC9zdmc+Cg==";
@@ -0,0 +1,51 @@
1
+ /**
2
+ * 挂载层 —— 把准则注册成 `ctx.systemPrompt` 的**一个**段落。
3
+ *
4
+ * ════════════════════════════════════════════════════════════════════
5
+ * 为什么只注册一段,而不是 14 段
6
+ * ════════════════════════════════════════════════════════════════════
7
+ * 面板开关的粒度是「分类」,但 prompt 读起来应该是一段连续的准则。分成 14 段
8
+ * 会让 KV cache 每个分类各占一段,且**增删任意一个分类都让它之后的全部分位**。
9
+ * 一段 + 拼装 ⇒ 分类增删只改这一段内部,段的位置与前缀稳定。
10
+ *
11
+ * ⚠️ **空串即关闭,这是平台原生语义**(`renderPrompt` 的注释原文:
12
+ * "drop empty sections")⇒ "可开关"不需要任何自造机制:本函数返回 `''`,
13
+ * 上游就把整段丢掉。**不要自己实现"删段落"的逻辑**,那会和平台重复且会漂移。
14
+ *
15
+ * ⚠️ 文本用 provider 函数(`(context) => string`)而不是静态字符串:
16
+ * 分类开关随时可改,静态文本**注册那一刻就定死了**,改开关不生效。
17
+ */
18
+ import { type PromptExtData } from './render.ts';
19
+ /** `ctx.systemPrompt` 的最小可见面。 */
20
+ export interface SystemPromptFace {
21
+ section(section: {
22
+ name: string;
23
+ order: number;
24
+ text: string | (() => string);
25
+ }): () => void;
26
+ getSectionOrder?(name: string): number;
27
+ }
28
+ /** 段落名(**稳定**):改名会让已注册的作用域 section 与旧名并存。 */
29
+ export declare const PROMPT_SECTION_NAME = "dsh-router-ext-prompt:guideline";
30
+ export interface MountPromptDeps {
31
+ /** 取 `ctx.systemPrompt`(可能尚未就绪 ⇒ undefined)。 */
32
+ getSystemPrompt: () => SystemPromptFace | undefined;
33
+ /** 总开关(问核心 `router.extStore`)。 */
34
+ isEnabled: () => boolean;
35
+ /**
36
+ * 读本扩展的数据块(分类开关)。
37
+ *
38
+ * 落盘 JSON 不可信 ⇒ 返回值**不保证**是合法 `PromptExtData`,
39
+ * `renderPromptText` 负责容错(见 render.test.ts 的注入用例)。
40
+ */
41
+ readData: () => PromptExtData | undefined;
42
+ }
43
+ /**
44
+ * 挂载准则段落。返回卸载函数。
45
+ *
46
+ * ⚠️ `systemPrompt` 尚不可用时**不挂**(返回 no-op 清理),并由 `ext.getState()`
47
+ * 报 `ready: false` 让面板红字显示。**不做轮询重试** —— 显式"没就绪"好过
48
+ * 半开的注入:后者会让准则随机时有时无,而用户看不出为什么。
49
+ * `index.ts` 用 `ctx.inject(['systemPrompt'])` 保证挂载时机,本函数只管挂。
50
+ */
51
+ export declare function mountPromptSection(deps: MountPromptDeps): () => void;
@@ -0,0 +1,32 @@
1
+ /**
2
+ * 分层提示词扩展(`router.ext` 侧的实现)—— 声明与状态都在这里。
3
+ *
4
+ * 与 `ext-test`(插件自检)同款:**不挂任何监听、不改写任何东西**,
5
+ * 唯一作用是让用户在官方插件页里看到一张卡片、能开关它,并把协作准则
6
+ * 注册成一段 system prompt。
7
+ *
8
+ * 与 `rtk` / `jev` 那类扩展的区别要写清楚:那两家挂 `tools/execute`
9
+ * 改写**工具调用**;本扩展只往 **system prompt** 加一段。名字同族是因为
10
+ * 都在 `router.ext` 这张表里,拦截面不重叠。
11
+ *
12
+ * 复用 `router.ext` 通道只为拿到**发现 + 开关**两件事(见 ext-test 的同款说明),
13
+ * 真正的注入在 `mount.ts`。
14
+ */
15
+ import type { ExtStoreService, RouterExt } from '../ext/contract.ts';
16
+ /** 扩展开关表里的注册键。 */
17
+ export declare const EXT_PROMPT_ID = "prompt";
18
+ /**
19
+ * 构造这个扩展器。
20
+ *
21
+ * `getState()` 报**唯一的真实就绪条件**:`ctx.systemPrompt` 不可用时无处注入
22
+ * (section 是唯一的注入通道),此时必须红字报出,而不是静默不生效 ——
23
+ * 判据能变红比"看起来一直开着"有用。
24
+ *
25
+ * @param deps.store - `router.extStore`(读写分类开关的落盘数据块)。缺省时
26
+ * `controls` 报全部默认、`setControl` 恒 false —— 那样面板显示可点但点了
27
+ * 没反应,**比不显示更坏**。
28
+ */
29
+ export declare function createPromptExt(deps: {
30
+ isSystemPromptReady: () => boolean;
31
+ store?: ExtStoreService;
32
+ }): RouterExt;
@@ -0,0 +1,105 @@
1
+ /**
2
+ * 纯逻辑层 —— 分类开关的读取、校验与 prompt 文本渲染。
3
+ *
4
+ * 为什么单独成文件:**不 import cordis / 任何 DSH 包**,所以能被 `node --test`
5
+ * 直接跑。DSH 侧的注册/挂载(ext.ts / inject.ts)只是薄壳;所有会出错的判断
6
+ * 都在这里,且这里能注入违规用例证明它们会变红。
7
+ */
8
+ import { type PromptCategory } from './content.ts';
9
+ /** 落盘在 `router.extStore` 数据块里的形状。 */
10
+ export interface PromptExtData {
11
+ /** 分类开关:`{ [categoryId]: boolean }`。缺项走 `defaultOn`。 */
12
+ categories?: Record<string, boolean>;
13
+ /**
14
+ * 用户在面板里改过的文本:`{ [categoryId]: { title?, body? } }`。
15
+ *
16
+ * ⚠️ **与 `categories` 分开两个键**:开关是"开不开"、文本是"写成什么样"。
17
+ * 挤在一个键里,改标题会顺手碰到开关状态(`writeData` 是**整块替换**,
18
+ * 少一个键就是少一份数据)。
19
+ *
20
+ * ⚠️ **只存覆盖值,不存全文**:没改过的分类不出现在这里 ⇒ 内置内容仍是唯一
21
+ * 事实源,升级内置文案时没改过的那几条会跟着更新。存全文 = 用户改一个字
22
+ * 就永久冻结了整段,之后内置更新对它们完全失效。
23
+ */
24
+ text?: Record<string, PromptCategoryOverride>;
25
+ /**
26
+ * 用户自建的准则条目。**与内置的 `PROMPT_CATEGORIES` 分开存** ——
27
+ * 内置那 14 条是**代码常量**(发版即定),混在一起就会出现"代码里有一条、
28
+ * 落盘里又有一条"的归属问题:升级时新加的内置条目会和用户的旧数据打架。
29
+ *
30
+ * 存的是**完整条目**(不是覆盖值):自建的东西没有"内置版本"可回退。
31
+ */
32
+ custom?: PromptCategory[];
33
+ /**
34
+ * 展示顺序:`[id, …]`,含内置与自建。
35
+ *
36
+ * ⚠️ **只存 id 顺序,不重排内置数组本身**:内置 14 条的次序是 `content.ts`
37
+ * 里的代码顺序,它同时是"准则作为一份文档的阅读顺序";让用户拖动去改它,
38
+ * 等于让代码顺序随某个用户的操作漂移 —— 别人拿到新版本看到的次序就变了。
39
+ * 落盘只记"这次会话里按什么顺序显示",代码顺序永远是兜底。
40
+ */
41
+ order?: string[];
42
+ }
43
+ /** 一条分类上用户改过的字段(只记改了的那些)。 */
44
+ export interface PromptCategoryOverride {
45
+ title?: string;
46
+ body?: string;
47
+ }
48
+ /** 解出某分类**生效用**的文本:用户覆盖优先,否则用内置的。 */
49
+ export declare function effectiveText(id: string, override: PromptCategoryOverride | undefined, base: PromptCategory): {
50
+ title: string;
51
+ body: string;
52
+ };
53
+ /** 分类 id 的全集(校验用)。 */
54
+ export declare function knownCategoryIds(): string[];
55
+ /**
56
+ * 解出「哪些分类要渲染」。
57
+ *
58
+ * 三层语义,**顺序不可换**:
59
+ * ① 总开关关 ⇒ 一条都不渲染(不是"渲染但没内容"——那样面板显示已开启、实际无效果);
60
+ * ② 数据块里显式写了的分类,用**用户存的值**(含显式 `false`);
61
+ * ③ 没写过的分类,用 `defaultOn`。
62
+ *
63
+ * ⚠️ 为什么 ② 要区别于「没写」:`false` 与 `undefined` 语义完全不同 ——
64
+ * 前者是用户主动关掉,后者是"没碰过"。把两者当同一件事,用户第一次关掉某分类
65
+ * 后升级插件、新增同名分类,那条就永远是关的(用户没要求过)。
66
+ */
67
+ export declare function resolveEnabledCategories(masterOn: boolean, data: PromptExtData | undefined): Set<string>;
68
+ /**
69
+ * 渲染成**一个**段落文本(多分类拼一起)。
70
+ *
71
+ * 为什么要拼成一段而不是一段一个:面板开关是"分类"粒度,而 prompt 读起来
72
+ * 是一段连续的准则 —— 分成 14 个 section 会让 KV cache 每个分类各占一段、
73
+ * 增删任意一个分类都让其后全部位移。这里**注册一个 section,文本由开关拼装**。
74
+ *
75
+ * @returns 关闭的分类不进结果;全关 ⇒ 空串(上游 `renderPrompt` 会丢弃空段)。
76
+ */
77
+ export declare function renderCategories(enabled: ReadonlySet<string>, categories?: readonly PromptCategory[], overrides?: PromptExtData['text'],
78
+ /** 有没有加整段标题。默认 true;传**外部分类表**时传 false(见下方注释)。 */
79
+ withTitle?: boolean): string;
80
+ /**
81
+ * 渲染当前应注入的完整 prompt 文本(总开关 + 分类开关都算进去)。
82
+ *
83
+ * 总开关关 ⇒ 空串,让上游 `renderPrompt` 丢弃该段 —— **不是**注入一段空白。
84
+ */
85
+ export declare function renderPromptText(masterOn: boolean, data: PromptExtData | undefined): string;
86
+ /**
87
+ * 合成后的**一条**准则条目:内置或自建,已应用标题/正文覆盖。
88
+ * `builtin` 用来决定按钮(内置没有「删除」,只有「还原」)。
89
+ */
90
+ export interface ResolvedCategory extends PromptCategory {
91
+ /** 自建(用户加的)——可删除;内置的不可删除。 */
92
+ readonly custom: boolean;
93
+ /** 文本被用户改过(决定「还原」按钮是否可点)。 */
94
+ readonly overridden: boolean;
95
+ }
96
+ /**
97
+ * 内置 + 自建 + 排序 ⇒ 有序的最终条目列表。
98
+ *
99
+ * ⚠️ 三条规则(顺序不可换):
100
+ * 1. **内置在前、自建在后**,除非 `order` 显式给了别的次序。代码顺序是
101
+ * "准则作为一份文档的阅读顺序",不该被用户拖动改掉(见 `PromptExtData.order`)。
102
+ * 2. `order` 里**不认识的 id 一律忽略**,落盘不可信(手改的 ext.json、上一版的残渣)。
103
+ * 3. `order` **没提到的条目按原序补在后面** —— 丢了某条 ≠ 它消失了。
104
+ */
105
+ export declare function resolveCategories(data: PromptExtData | undefined): ResolvedCategory[];
@@ -1,15 +1,14 @@
1
1
  /**
2
2
  * 探测 profile 里安装的 DSH 宿主版本。
3
3
  * @param baseUrl 宿主注入的 profile 锚点(`ctx.baseUrl`)
4
- * @returns 形如 `0.1.7-alpha.2`;读不到返回 undefined(调用方按「不支持」处理)
4
+ * @returns 形如 `0.2.0-rc.1`;读不到返回 undefined
5
5
  */
6
6
  export declare function detectHostVersion(baseUrl?: string): string | undefined;
7
7
  /**
8
- * 宿主是否 >= 0.1.7。三处兼容分叉共用这一个判据:
9
- * - 「最近命中」徽章(composer.dock 位置正确)
10
- * - 设置命名空间(Config 驱动而非 installSection)
11
- * - adapter 的独立 role:'tool' 消息形态
8
+ * 宿主是否在本插件支持的地板之上(>= 0.2.0)。
9
+ *
10
+ * **只用于报警,不用于分流** —— 低于地板时插件照常加载,但要在日志里说清「这台宿主
11
+ * 不再兼容、会出现什么症状」,把静默的错变成可查的错。读不到版本 → false(当作
12
+ * 「无法确认」,同样值得说一句)。
12
13
  */
13
- export declare function isHost017Plus(baseUrl?: string): boolean;
14
- /** 「最近命中」徽章是否可用(= 宿主 >= 0.1.7)。 */
15
- export declare function supportsLastHitDock(baseUrl?: string): boolean;
14
+ export declare function isHostSupported(baseUrl?: string): boolean;
@@ -6,7 +6,7 @@ export declare const name = "dsh-router-core";
6
6
  /** Services required before mounting: the webserver (routes) + llm (设置-模型). */
7
7
  export declare const inject: string[];
8
8
  /**
9
- * 0.1.7 的设置镜像只认「插件导出的 Config + 配置树行 id」这条收录链,且空 object
9
+ * 设置镜像只认「插件导出的 Config + 配置树行 id」这条收录链,且空 object
10
10
  * 会被 volatileForm 过滤掉 —— 顶层 `.volatile()` 才能让 Router 卡片出现在设置-模型。
11
11
  * Router 的真配置在自己的 state.json(路由系统面板),这里故意不暴露任何字段。
12
12
  */
@@ -158,6 +158,39 @@ export interface RouterExtItem {
158
158
  * 「包含的组件」里占一行了,两处都列就是同一个东西显示两遍(与内置供应商同理)。
159
159
  */
160
160
  source?: 'builtin';
161
+ /**
162
+ * 扩展自带的子开关(如准则的每一条)。**由扩展自报**,核心只搬运。
163
+ *
164
+ * ⚠️ 与 `enabled`(总开关)不同:子开关**不改变扩展是否被加载**,只改它
165
+ * 自己的行为。总开关关时这些子开关一并失效(面板上禁用,不隐藏)。
166
+ */
167
+ controls?: ExtControlItem[];
168
+ }
169
+ /** `/router/api/ext` 里的一条子开关(与 `router.ext` 契约的 `ExtControl` 对应)。 */
170
+ export interface ExtControlItem {
171
+ /** 稳定 id(持久化键;改名会丢用户已保存的选择)。 */
172
+ id: string;
173
+ title: string;
174
+ /** 当前是否开启(扩展自算:默认值 + 用户已存选择)。 */
175
+ on: boolean;
176
+ /** 一句话说明(面板副行,可选)。 */
177
+ detail?: string;
178
+ /** 原文(面板点开即可读到;不给出就是在盲切,见 `ExtControl.body`)。 */
179
+ body?: string;
180
+ /** 文本可编辑(缺省 = 只读;见 `ExtControl.editable`)。 */
181
+ editable?: boolean;
182
+ /**
183
+ * 自建条目(可删)还是内置(不可删、只有开关与还原)。
184
+ *
185
+ * ⚠️ **必填,不给可选**(2026-09-30 实测踩过):曾声明成 `custom?: boolean`,
186
+ * 而服务端只在 `custom === true` 时才带这个字段 ⇒ 内置条目拿到的是
187
+ * `undefined`,面板判 `c.custom === true` 为假,**恰好是对的** ——
188
+ * 但那条正确性建立在"缺省恰好等于 false"上,改任何一边的默认值都会翻转。
189
+ * ⇒ 让服务端**总是**给显式布尔,两边不必猜。
190
+ */
191
+ custom: boolean;
192
+ /** 文本被改过(决定「还原」是否可点)。必填,理由同 `custom`。 */
193
+ overridden: boolean;
161
194
  }
162
195
  /** `/router/api/ext` response(扩展插件列表 + 开关)。 */
163
196
  export interface RouterExtResponse {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-router-core",
3
- "version": "0.4.2",
3
+ "version": "0.4.3",
4
4
  "description": "DSH plugin: a simplified 9router (AI routing gateway). Adds a 路由系统 (Routing System) sidebar entry beside 记忆系统; the panel shows suppliers, account pool, combos, and endpoint/API keys.",
5
5
  "type": "module",
6
6
  "repository": {
@@ -42,6 +42,12 @@
42
42
  },
43
43
  "./ext-test/package.json": "./lib/ext-test/package.json",
44
44
  "./ext-test/locale/*.json": "./lib/ext-test/locale/*.json",
45
+ "./ext-prompt": {
46
+ "types": "./lib/types/ext-prompt/index.d.ts",
47
+ "default": "./lib/ext-prompt/index.js"
48
+ },
49
+ "./ext-prompt/package.json": "./lib/ext-prompt/package.json",
50
+ "./ext-prompt/locale/*.json": "./lib/ext-prompt/locale/*.json",
45
51
  "./client": "./lib/client.js",
46
52
  "./src/*": "./src/*",
47
53
  "./package.json": "./package.json"
@@ -76,6 +82,9 @@
76
82
  "lib/ext-test/index.js",
77
83
  "lib/ext-test/package.json",
78
84
  "lib/ext-test/locale/*.json",
85
+ "lib/ext-prompt/index.js",
86
+ "lib/ext-prompt/package.json",
87
+ "lib/ext-prompt/locale/*.json",
79
88
  "lib/types/**/*.d.ts",
80
89
  "src",
81
90
  "cordis.patch.yml",
@@ -99,12 +108,13 @@
99
108
  "@deepseek-ai/schemastery": "^3.18.4"
100
109
  },
101
110
  "peerDependencies": {
102
- "@deepseek-ai/cordis": "^4.0.2",
103
- "@deepseek-ai/dsh-client-locale": "^0.1.5-rc.2",
104
- "@deepseek-ai/dsh-client-ui-primitives": "^0.1.5-rc.2",
105
- "@deepseek-ai/dsh-client-ui-slots": "^0.1.5-rc.2",
106
- "@deepseek-ai/dsh-llm": "^0.1.5-rc.2",
107
- "@deepseek-ai/dsh-settings": "^0.1.5-rc.2",
111
+ "@deepseek-ai/cordis": "^4.0.4",
112
+ "@deepseek-ai/dsh-client-locale": "^0.2.0-rc.2",
113
+ "@deepseek-ai/dsh-client-ui-primitives": "^0.2.0-rc.2",
114
+ "@deepseek-ai/dsh-client-ui-slots": "^0.2.0-rc.2",
115
+ "@deepseek-ai/dsh-llm": "^0.2.0-rc.2",
116
+ "@deepseek-ai/dsh-system-prompt": "^0.2.0-rc.2",
117
+ "@deepseek-ai/dsh-settings": "^0.2.0-rc.2",
108
118
  "cordis": "^4.0.0-rc.7",
109
119
  "react": "^18.2.0",
110
120
  "react-dom": "^18.2.0"
@@ -4,6 +4,7 @@
4
4
  * Create/暂停确认/Created 弹窗)。
5
5
  */
6
6
  import { useCallback, useEffect, useRef, useState } from 'react'
7
+ import { Switch } from '@deepseek-ai/dsh-client-ui-primitives'
7
8
  import {
8
9
  ROUTER_API_BASE,
9
10
  type RouterKey,
@@ -219,16 +220,16 @@ export function EndpointTab(): JSX.Element {
219
220
  <p className="dshr-requireTitle">Require API key</p>
220
221
  <p className="dshr-requireDesc">不带有效 key 的请求将被拒绝</p>
221
222
  </div>
222
- <button
223
- type="button"
224
- className={`dshr-toggle ${requireApiKey ? 'dshr-toggle-on' : ''}`}
225
- role="switch"
226
- aria-checked={requireApiKey}
227
- onClick={() => void toggleRequire(!requireApiKey)}
223
+ {/* ⚠️ 用 DSH 原生 `Switch`,不用自造的 `dshr-toggle` 按钮。
224
+ 2026-09-29 用户指出的:同一个设置页里两处开关长得不一样(这一页自造、
225
+ 扩展页用原生)⇒ 深浅色两套主题、焦点环、键盘行为都会分叉。
226
+ 原生那个还自带 `label`(读屏可读),自造那个只有 `role="switch"`。 */}
227
+ <Switch
228
+ checked={requireApiKey}
229
+ label="Require API key 开关"
228
230
  title={requireApiKey ? '关闭鉴权' : '开启鉴权'}
229
- >
230
- <span className="dshr-toggleKnob" />
231
- </button>
231
+ onChange={(next) => { void toggleRequire(next) }}
232
+ />
232
233
  </div>
233
234
 
234
235
  {/* key 列表 / 空状态 */}
@@ -272,16 +273,12 @@ export function EndpointTab(): JSX.Element {
272
273
  </p>
273
274
  </div>
274
275
  <div className="dshr-keyOps">
275
- <button
276
- type="button"
277
- className={`dshr-toggle ${key.isActive ? 'dshr-toggle-on' : ''}`}
278
- role="switch"
279
- aria-checked={key.isActive}
276
+ <Switch
277
+ checked={key.isActive}
278
+ label={`${key.name} 开关`}
280
279
  title={key.isActive ? '暂停 key' : '恢复 key'}
281
- onClick={() => onKeySwitch(key, !key.isActive)}
282
- >
283
- <span className="dshr-toggleKnob" />
284
- </button>
280
+ onChange={(next) => { onKeySwitch(key, next) }}
281
+ />
285
282
  </div>
286
283
  </div>
287
284
  ))}