dsh-router-core 0.4.7 → 0.4.9

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 (62) hide show
  1. package/README.md +14 -70
  2. package/cordis.patch.yml +2 -7
  3. package/docs/ext.md +3 -17
  4. package/docs/suppliers.md +1 -1
  5. package/lib/client-registry.js +137 -590
  6. package/lib/client.js +137 -590
  7. package/lib/ext-prompt/index.js +20 -15
  8. package/lib/index.js +0 -419
  9. package/lib/types/ext/contract.d.ts +1 -1
  10. package/lib/types/ext-prompt/content.d.ts +11 -0
  11. package/lib/types/ext-prompt/index.d.ts +7 -9
  12. package/lib/types/ext-prompt/mount.d.ts +2 -2
  13. package/lib/types/ext-prompt/plugin.d.ts +3 -5
  14. package/lib/types/router/types.d.ts +11 -0
  15. package/lib/types/shared.d.ts +2 -0
  16. package/lib/types/suppliers/registry.d.ts +0 -1
  17. package/package.json +1 -10
  18. package/src/client/CombosTab.tsx +29 -8
  19. package/src/client/ExtDetail.tsx +1 -13
  20. package/src/client/SupplierDetail.tsx +11 -5
  21. package/src/client/group-controls.test.ts +1 -1
  22. package/src/client/group-controls.ts +1 -1
  23. package/src/client/index.tsx +0 -17
  24. package/src/client/model-label.test.ts +33 -0
  25. package/src/client/model-label.ts +48 -0
  26. package/src/client/router-components.test.ts +1 -1
  27. package/src/client/router.css +24 -412
  28. package/src/ext/contract.ts +1 -1
  29. package/src/ext-prompt/content.ts +17 -12
  30. package/src/ext-prompt/ext-prompt.test.ts +3 -3
  31. package/src/ext-prompt/index.ts +7 -9
  32. package/src/ext-prompt/mount.ts +2 -2
  33. package/src/ext-prompt/plugin.ts +3 -5
  34. package/src/router/index.ts +2 -3
  35. package/src/router/models-cache.test.ts +21 -6
  36. package/src/router/supplier-switch.test.ts +1 -1
  37. package/src/router/types.ts +11 -0
  38. package/src/shared.ts +2 -0
  39. package/src/suppliers/registry.ts +0 -71
  40. package/lib/contract-D4BWZ7Xe.js +0 -13
  41. package/lib/ext-test/index.js +0 -36
  42. package/lib/ext-test/locale/en.json +0 -6
  43. package/lib/ext-test/locale/zh.json +0 -6
  44. package/lib/ext-test/package.json +0 -5
  45. package/lib/types/ext-test/index.d.ts +0 -21
  46. package/lib/types/ext-test/plugin.d.ts +0 -23
  47. package/lib/types/suppliers/probe.d.ts +0 -160
  48. package/src/client/ExtTestPanel.tsx +0 -392
  49. package/src/client/comp-button-scope.test.ts +0 -58
  50. package/src/client/comp-panel-layout.test.ts +0 -58
  51. package/src/client/ext-panels.test.ts +0 -44
  52. package/src/client/ext-panels.ts +0 -49
  53. package/src/client/model-choice.test.ts +0 -41
  54. package/src/client/model-choice.ts +0 -37
  55. package/src/ext-test/ext-test.test.ts +0 -60
  56. package/src/ext-test/index.ts +0 -35
  57. package/src/ext-test/locale/en.json +0 -6
  58. package/src/ext-test/locale/zh.json +0 -6
  59. package/src/ext-test/plugin.ts +0 -34
  60. package/src/ext-test/row.json +0 -5
  61. package/src/suppliers/probe.test.ts +0 -266
  62. package/src/suppliers/probe.ts +0 -475
@@ -1,4 +1,15 @@
1
- import { n as currentExts, t as currentExtStore } from "../contract-D4BWZ7Xe.js";
1
+ //#region src/ext/contract.ts
2
+ /** 读取当前 router.ext 表(同一 live 对象,可追加)——扩展插件用它注册自己。 */
3
+ function currentExts(ctx) {
4
+ const c = ctx;
5
+ return c.get?.("router.ext") ?? c.router?.ext;
6
+ }
7
+ /** 读取扩展存储服务——扩展插件用它读开关、读写自己存的数据。 */
8
+ function currentExtStore(ctx) {
9
+ const c = ctx;
10
+ return c.get?.("router.extStore") ?? c.router?.extStore;
11
+ }
12
+ //#endregion
2
13
  //#region src/ext-prompt/content.ts
3
14
  const PROMPT_CATEGORIES = [
4
15
  {
@@ -32,10 +43,10 @@ const PROMPT_CATEGORIES = [
32
43
  body: "收口(只在任务完成时做,与跑测试同级):①反查过期:改动让哪些注释/文档/结论/代码引用点(凡以名字对齐的调用方、开关名、注册键)不符事实?当场改或删;②反查重复:新旧成两份就并成一处;③反查形状:该合的合、该归档的归位。做完那块区域该比动手前更整或更少。"
33
44
  },
34
45
  {
35
- id: "verdifiable",
36
- title: "可判定",
46
+ id: "doc",
47
+ title: "文档",
37
48
  defaultOn: true,
38
- body: "可判定:只有清单和会变红的闸门生效,形容词不生效。新约束一律写成\"动作+闸门\"。"
49
+ body: "文档:docs/ 里的调研/设计/任务文档,只写权威内容——结论、约束、怎么用;不写流水账(过程、试错、时间线),历史归 git。"
39
50
  },
40
51
  {
41
52
  id: "root-cause",
@@ -61,12 +72,6 @@ const PROMPT_CATEGORIES = [
61
72
  defaultOn: true,
62
73
  body: "结构:先让结构对再让代码短;命名/边界/单一职责优先于行数;依赖单向无环、公共逻辑收敛一处;单文件单功能,软顶函数 ~40 行/文件 ~300 行。"
63
74
  },
64
- {
65
- id: "no-cheap",
66
- title: "绝不偷懒",
67
- defaultOn: true,
68
- body: "绝不偷懒:边界输入校验、防数据丢失、安全、可访问性、真实硬件校准;非平凡逻辑留个能跑的检查。"
69
- },
70
75
  {
71
76
  id: "delivery",
72
77
  title: "交付",
@@ -77,7 +82,7 @@ const PROMPT_CATEGORIES = [
77
82
  id: "execution",
78
83
  title: "执行顺序",
79
84
  defaultOn: true,
80
- body: "执行:拉记忆→看全貌→建领地→定位任务→查结构行号→声明「任务/文件/验证/提交」→读码取切片→循既有骨架→验证以闸门为准(混合状态下的绿灯不作数)→收口复查全貌→提交。"
85
+ body: "执行:拉记忆→读文档→看全貌→定位任务→建领地→循既有架构/提出更优架构→验证以测试/e2e 验收→收口复查全貌→更新文档→提交。"
81
86
  }
82
87
  ];
83
88
  /**
@@ -541,9 +546,9 @@ function createPromptExt(deps) {
541
546
  * 挂载层 —— 把准则注册成 `ctx.systemPrompt` 的**一个**段落。
542
547
  *
543
548
  * ════════════════════════════════════════════════════════════════════
544
- * 为什么只注册一段,而不是 14 段
549
+ * 为什么只注册一段,而不是一段一个分类
545
550
  * ════════════════════════════════════════════════════════════════════
546
- * 面板开关的粒度是「分类」,但 prompt 读起来应该是一段连续的准则。分成 14 段
551
+ * 面板开关的粒度是「分类」,但 prompt 读起来应该是一段连续的准则。分成多段
547
552
  * 会让 KV cache 每个分类各占一段,且**增删任意一个分类都让它之后的全部分位**。
548
553
  * 一段 + 拼装 ⇒ 分类增删只改这一段内部,段的位置与前缀稳定。
549
554
  *
@@ -612,8 +617,8 @@ function systemPromptOf(sctx) {
612
617
  *
613
618
  * ⚠️⚠️ **必须导出这个数组,且 `apply` 里直接用 `ctx.systemPrompt`**(2026-09-29 修)
614
619
  * ——我原先写成「在 apply 里 `ctx.inject(['systemPrompt'], cb)`」,**准则一直不进
615
- * prompt**:面板上卡片、开关、原文全都正常(`GET /router/api/ext` 实测有 14 条
616
- * controls),但模型看到的 system prompt 里没有它。
620
+ * prompt**:面板上卡片、开关、原文全都正常(`GET /router/api/ext` 里 controls 一条
621
+ * 不少),但模型看到的 system prompt 里没有它。
617
622
  *
618
623
  * 根因(本机 cordis 4.0.4 实测):`ctx.inject(deps, cb)` 的回调**不是同步触发** ——
619
624
  * provide 之后立刻 `inject`,回调要等到下一个异步点才跑。插件的 `apply` 是**同步
package/lib/index.js CHANGED
@@ -6005,355 +6005,6 @@ async function loadSuppliers(opts) {
6005
6005
  };
6006
6006
  }
6007
6007
  //#endregion
6008
- //#region src/suppliers/probe.ts
6009
- /**
6010
- * 契约成员清单(体检的唯一事实来源)。
6011
- *
6012
- * `required: false` = 差异化能力,按存在性暴露端点/UI;`probe: 'skip'` 的理由都写在这里,
6013
- * 面板会把 `skipReason` 原样显示 —— 用户能知道「有这个能力,只是体检不替你调」。
6014
- */
6015
- const SUPPLIER_CONTRACT_MEMBERS = [
6016
- {
6017
- key: "id",
6018
- label: "供应商 id",
6019
- required: true,
6020
- probe: "safe"
6021
- },
6022
- {
6023
- key: "name",
6024
- label: "显示名",
6025
- required: true,
6026
- probe: "safe"
6027
- },
6028
- {
6029
- key: "priority",
6030
- label: "优先级",
6031
- required: false,
6032
- probe: "safe"
6033
- },
6034
- {
6035
- key: "icon",
6036
- label: "图标",
6037
- required: false,
6038
- probe: "safe"
6039
- },
6040
- {
6041
- key: "apiKeyHint",
6042
- label: "API key 提示文案",
6043
- required: false,
6044
- probe: "safe"
6045
- },
6046
- {
6047
- key: "status",
6048
- label: "账号状态",
6049
- required: true,
6050
- probe: "safe"
6051
- },
6052
- {
6053
- key: "listModels",
6054
- label: "模型列表",
6055
- required: true,
6056
- probe: "safe"
6057
- },
6058
- {
6059
- key: "chatOnce",
6060
- label: "调用上游",
6061
- required: true,
6062
- probe: "skip",
6063
- skipReason: "会真的发一次请求(消耗额度)。用上面的「跑一次访问测试」单独测,或在供应商详情里测模型。"
6064
- },
6065
- {
6066
- key: "checkinNow",
6067
- label: "签到",
6068
- required: false,
6069
- probe: "skip",
6070
- skipReason: "会替这个连接真的去签到。"
6071
- }
6072
- ];
6073
- /** 读原始模块实例(`wrapModule` 挂在 wrapper 上)。 */
6074
- function rawModule(loaded) {
6075
- return loaded.supplier.__module ?? {};
6076
- }
6077
- /** 读出账号状态列表(只读,不改任何东西)。 */
6078
- function accountStates(status) {
6079
- if (typeof status !== "object" || status === null) return [];
6080
- const accounts = status.accounts;
6081
- if (!Array.isArray(accounts)) return [];
6082
- return accounts.flatMap((a) => {
6083
- const uid = a?.uid;
6084
- if (typeof uid !== "string") return [];
6085
- return [{
6086
- uid,
6087
- state: typeof a.state === "string" ? a.state : "ok",
6088
- cooling: a.cooling === true
6089
- }];
6090
- });
6091
- }
6092
- /** 第一个连接 uid(`checkinNow` 之类要真实连接时用)。 */
6093
- function firstAccountUid(m) {
6094
- return accountStates(m.status())[0]?.uid;
6095
- }
6096
- /** 账号状态分布:`正常 2、冷却 1`、`session_dead 1` 这样。 */
6097
- function accountTally(accounts) {
6098
- const counts = /* @__PURE__ */ new Map();
6099
- for (const a of accounts) {
6100
- const key = a.cooling ? "冷却" : a.state === "ok" ? "正常" : a.state;
6101
- counts.set(key, (counts.get(key) ?? 0) + 1);
6102
- }
6103
- return [...counts].map(([k, n]) => `${k} ${n}`).join("、");
6104
- }
6105
- /**
6106
- * 摘要:把返回值压成一行**对排查有用**的文本。
6107
- *
6108
- * 原先只说「返回 number」「返回 data:image/svg+xml,%3Csvg…」——等于没说话:
6109
- * 体检的意义就是让人一眼看出「priority 是几」「图标给了没有」。
6110
- */
6111
- function describe(value) {
6112
- if (value === void 0) return "未提供(可选)";
6113
- if (value === null) return "返回 null";
6114
- if (Array.isArray(value)) return `${value.length} 项`;
6115
- if (typeof value === "string") {
6116
- if (value.startsWith("data:")) return `已提供(内联 ${value.slice(5, value.indexOf("/") || 12)})`;
6117
- if (value === "") return "返回空串";
6118
- return value.length > 48 ? `返回 ${value.slice(0, 48)}…` : `返回 ${value}`;
6119
- }
6120
- if (typeof value === "number" || typeof value === "boolean") return `返回 ${value}`;
6121
- if (typeof value === "object") {
6122
- const o = value;
6123
- if (typeof o.ok === "boolean") return o.ok === false ? `ok:false ${String(o.error ?? o.message ?? "")}`.trim() : "ok:true";
6124
- return Object.keys(o).slice(0, 4).join(", ") || "空对象";
6125
- }
6126
- return `返回 ${typeof value}`;
6127
- }
6128
- /** 异步取一个值并压成摘要(同步抛错也要接住)。 */
6129
- async function probeValue(call) {
6130
- try {
6131
- return {
6132
- ok: true,
6133
- detail: describe(await call())
6134
- };
6135
- } catch (err) {
6136
- return {
6137
- ok: false,
6138
- detail: err.message
6139
- };
6140
- }
6141
- }
6142
- /**
6143
- *(这里原本有一份「永不实跑」名单:`dispose` / `addApiKey`。
6144
- * 两者都已移出体检清单,名单随之作废 —— 留着空机制比删掉更糟:
6145
- * 它看起来像个可配置的开关,其实没有任何成员会命中。
6146
- * 「为什么要移」见 PROBE_EXCLUDED_MEMBERS。)
6147
- */
6148
- /** 深度档用来「不碰真实状态就跑通代码路径」的探针输入。 */
6149
- const PROBE_UID = "__probe_no_such_uid__";
6150
- const PROBE_CALLBACK = "https://invalid.example/__probe__";
6151
- /**
6152
- * 体检一个已装载的供应商。
6153
- *
6154
- * 只跑 `probe: 'safe'` 的成员;`skip` 的只报存在性。绝不代替用户调有副作用的成员。
6155
- *
6156
- * @param models - 该供应商当前的模型启用状态(由调用方用 `router.modelsOf` 取,
6157
- * 走核心缓存不额外打上游)。取不到就传 undefined —— 报告里会如实写「拿不到」,
6158
- * 不能拿 0 冒充「一个模型都没有」。
6159
- */
6160
- async function probeSupplier(loaded, input = {}) {
6161
- const m = rawModule(loaded);
6162
- const members = [];
6163
- for (const member of SUPPLIER_CONTRACT_MEMBERS) {
6164
- const present = member.key in m ? m[member.key] !== void 0 : false;
6165
- const base = {
6166
- key: member.key,
6167
- label: member.label,
6168
- required: member.required,
6169
- present,
6170
- executed: present ? "ran" : "no"
6171
- };
6172
- if (!present) {
6173
- members.push({
6174
- ...base,
6175
- state: member.required ? "fail" : "absent",
6176
- ...member.required ? { detail: "必填成员缺失,插件不完整" } : {}
6177
- });
6178
- continue;
6179
- }
6180
- const outcome = member.probe === "safe" ? await runSafeProbe(member.key, m, input) : await runProbedMember(member.key, m, input);
6181
- members.push({
6182
- ...base,
6183
- state: outcome.ok ? "ok" : outcome.detail.startsWith("无法验证") ? "unverified" : "fail",
6184
- detail: outcome.detail
6185
- });
6186
- }
6187
- const bulkResult = await runBulkRoundTrip(input);
6188
- input.models;
6189
- const summary = {
6190
- total: members.length,
6191
- implemented: members.filter((x) => x.present).length,
6192
- ok: members.filter((x) => x.state === "ok").length,
6193
- fail: members.filter((x) => x.state === "fail").length,
6194
- absent: members.filter((x) => x.state === "absent").length,
6195
- unverified: members.filter((x) => x.state === "unverified").length
6196
- };
6197
- const listModels = members.find((x) => x.key === "listModels");
6198
- return {
6199
- supplier: loaded.supplier.id,
6200
- name: loaded.supplier.name,
6201
- members,
6202
- core: await coreReport(input, listModels?.state === "ok", bulkResult),
6203
- summary: {
6204
- ...summary,
6205
- ran: members.filter((x) => x.executed === "ran" && x.present).length,
6206
- bulk: bulkResult.ran ? bulkResult.ok === true ? "ok" : "fail" : "unverified"
6207
- }
6208
- };
6209
- }
6210
- /**
6211
- * 实跑「有副作用但可以安全试」的成员:要么用**无害探针输入**跑通代码路径,要么用
6212
- * **连接池里的真实连接**真跑一次(前提由使用者保证:池里已放好可用 token)。
6213
- */
6214
- async function runProbedMember(key, m, input) {
6215
- try {
6216
- switch (key) {
6217
- case "chatOnce": {
6218
- if (input.runChatOnce === void 0) return {
6219
- ok: false,
6220
- detail: "无法验证:没有发起真实调用的入口"
6221
- };
6222
- const model = input.models?.find((x) => x.enabled)?.id ?? input.models?.[0]?.id;
6223
- if (model === void 0) return {
6224
- ok: false,
6225
- detail: "无法验证:没有已启用的模型(先启用至少一个)"
6226
- };
6227
- const r = await input.runChatOnce(model);
6228
- return {
6229
- ok: r.ok,
6230
- detail: `真实调用 ${model}:${r.ok ? "通了" : `失败(${r.detail})`}`
6231
- };
6232
- }
6233
- case "checkinNow": {
6234
- const uid = firstAccountUid(m);
6235
- if (uid === void 0) return {
6236
- ok: false,
6237
- detail: "无法验证:连接池里没有可用连接(先加一个 token)"
6238
- };
6239
- const v = await m.checkinNow?.(uid);
6240
- return {
6241
- ok: v?.ok !== false,
6242
- detail: `对连接 ${uid} 真实签到:${describe(v)}`
6243
- };
6244
- }
6245
- case "generateLoginUrl": return {
6246
- ok: true,
6247
- detail: describe(await m.generateLoginUrl?.())
6248
- };
6249
- case "completeLogin": return {
6250
- ok: true,
6251
- detail: `无效回调实跑:${describe(await m.completeLogin?.(PROBE_CALLBACK))}(没抛错即通过)`
6252
- };
6253
- case "removeLink": {
6254
- const v = await m.removeLink?.(PROBE_UID);
6255
- return {
6256
- ok: v !== false,
6257
- detail: `不存在的连接实跑:${describe(v)}`
6258
- };
6259
- }
6260
- default: return {
6261
- ok: true,
6262
- detail: "未纳入自动实跑"
6263
- };
6264
- }
6265
- } catch (err) {
6266
- return {
6267
- ok: false,
6268
- detail: `实跑抛错(这本身就是一条问题):${err.message}`
6269
- };
6270
- }
6271
- }
6272
- /** 核心代劳的能力区:模型启用/禁用状态 + 那几个改配置的操作(只报可用性)。 */
6273
- /** 核心区:模型启用状态 + 「全部启用/禁用」实跑结果。 */
6274
- async function coreReport(input, modelsProbed, bulk) {
6275
- const models = input.models;
6276
- const enabled = models?.filter((m) => m.enabled).length;
6277
- const disabled = models === void 0 ? void 0 : models.length - (enabled ?? 0);
6278
- return {
6279
- models: {
6280
- total: models?.length ?? null,
6281
- enabled: enabled ?? 0,
6282
- disabled: disabled ?? 0,
6283
- ...models === void 0 ? { note: modelsProbed ? "拿不到模型列表" : "listModels 不可用,无法读启用状态" } : {}
6284
- },
6285
- operations: [{
6286
- key: "models.bulk",
6287
- label: "模型全部启用 / 全部禁用",
6288
- ran: bulk.ran,
6289
- ...bulk.ok === void 0 ? {} : { ok: bulk.ok },
6290
- detail: bulk.detail
6291
- }]
6292
- };
6293
- }
6294
- /**
6295
- * 实跑「全部禁用 → 看列表 → 全部启用」。
6296
- *
6297
- * 违规的插件会在这里露馅:它把已禁用的模型从 `listModels` 里过滤掉了,于是全部禁用
6298
- * 之后核心**看不到任何一个模型** —— 用户点「全部启用」也就再也点不回来了
6299
- * (路由里没有可用的模型 id 可传)。
6300
- */
6301
- async function runBulkRoundTrip(input) {
6302
- if (input.runBulkToggleRoundTrip === void 0) return {
6303
- ran: false,
6304
- detail: "没有模型可禁用"
6305
- };
6306
- try {
6307
- const r = await input.runBulkToggleRoundTrip();
6308
- return {
6309
- ran: true,
6310
- ok: r.ok,
6311
- detail: r.detail
6312
- };
6313
- } catch (err) {
6314
- return {
6315
- ran: true,
6316
- ok: false,
6317
- detail: `实跑抛错:${err.message}`
6318
- };
6319
- }
6320
- }
6321
- /** 无副作用成员的实跑。**每个 case 都必须没有副作用** —— 新增成员时照此办理。 */
6322
- async function runSafeProbe(key, m, input) {
6323
- switch (key) {
6324
- case "id":
6325
- case "name":
6326
- case "priority":
6327
- case "icon":
6328
- case "apiKeyHint": {
6329
- const value = m[key];
6330
- if (value === void 0) return {
6331
- ok: true,
6332
- detail: "未提供(可选)"
6333
- };
6334
- return {
6335
- ok: true,
6336
- detail: describe(value)
6337
- };
6338
- }
6339
- case "status": {
6340
- const r = await probeValue(() => m.status());
6341
- if (!r.ok) return r;
6342
- const accounts = accountStates(m.status());
6343
- return {
6344
- ok: true,
6345
- detail: accounts.length === 0 ? "无账号(无账号直连型供应商)" : `${accounts.length} 个账号:${accountTally(accounts)}`
6346
- };
6347
- }
6348
- case "listModels": return await probeValue(() => m.listModels());
6349
- case "pollLogin": return await probeValue(() => m.pollLogin?.());
6350
- default: return {
6351
- ok: true,
6352
- detail: "未纳入自动探测"
6353
- };
6354
- }
6355
- }
6356
- //#endregion
6357
6008
  //#region src/suppliers/registry.ts
6358
6009
  function writeJson$1(res, status, body) {
6359
6010
  const payload = JSON.stringify(body);
@@ -6487,76 +6138,6 @@ function supplierRoutes(base, loaded, store, router) {
6487
6138
  });
6488
6139
  }
6489
6140
  });
6490
- routes.push({
6491
- kind: "exact",
6492
- path: `${p}/probe`,
6493
- handler: async (req, res) => {
6494
- if (req.method !== "POST") {
6495
- writeJson$1(res, 405, {
6496
- ok: false,
6497
- error: "probe requires POST"
6498
- });
6499
- return;
6500
- }
6501
- try {
6502
- let pick = {};
6503
- const raw = (await readBody$2(req)).trim();
6504
- if (raw !== "") {
6505
- const body = JSON.parse(raw);
6506
- pick = {
6507
- ...typeof body.model === "string" ? { model: body.model } : {},
6508
- ...typeof body.uid === "string" ? { uid: body.uid } : {}
6509
- };
6510
- }
6511
- writeJson$1(res, 200, {
6512
- ok: true,
6513
- report: await probeSupplier(loaded, {
6514
- models: await router.modelsOf(s.id).catch(() => void 0),
6515
- runBulkToggleRoundTrip: async () => {
6516
- const original = [...store.get(s.id).disabled];
6517
- const ids = (await router.modelsOf(s.id)).map((mm) => mm.id);
6518
- if (ids.length === 0) return {
6519
- ok: true,
6520
- detail: "该供应商没有模型,跳过"
6521
- };
6522
- try {
6523
- store.setAllModelsEnabled(s.id, false, ids);
6524
- router.invalidateModels(s.id);
6525
- const after = await router.modelsOf(s.id, true);
6526
- const hidden = after.filter((mm) => !mm.enabled).length;
6527
- const missing = ids.length - after.length;
6528
- if (hidden !== ids.length || missing !== 0) return {
6529
- ok: false,
6530
- detail: `全部禁用后只剩 ${after.length}/${ids.length} 个 —— 插件在 listModels 里藏了已禁用的模型(启用状态由核心合并)`
6531
- };
6532
- return {
6533
- ok: true,
6534
- detail: `全部禁用后 ${after.length} 个都在,再启用可恢复`
6535
- };
6536
- } finally {
6537
- store.setAllModelsEnabled(s.id, true, ids);
6538
- for (const id of original) store.setAllModelsEnabled(s.id, false, [id]);
6539
- router.invalidateModels(s.id);
6540
- await router.modelsOf(s.id, true);
6541
- }
6542
- },
6543
- runChatOnce: async (model) => {
6544
- const r = await router.testModel(s.id, model, pick.uid);
6545
- return {
6546
- ok: r.ok,
6547
- detail: r.ok ? "通了" : r.error ?? "上游没给原因"
6548
- };
6549
- }
6550
- })
6551
- });
6552
- } catch (err) {
6553
- writeJson$1(res, 500, {
6554
- ok: false,
6555
- error: err.message
6556
- });
6557
- }
6558
- }
6559
- });
6560
6141
  routes.push({
6561
6142
  kind: "exact",
6562
6143
  path: `${p}/alias`,
@@ -181,7 +181,7 @@ export interface ExtControl {
181
181
  /**
182
182
  * 分组名(可选;缺省 = 无组)—— 面板按它把控制条**分节**渲染,组间插一条
183
183
  * 组标题行,组内顺序照旧。**无组的条目照旧平铺** ⇒ 没用这个字段的扩展
184
- * (ext-test 等)面板行为一个像素都不变。
184
+ * 面板行为一个像素都不变。
185
185
  *
186
186
  * 为什么要有:一条扩展同时管两类东西时(分层提示词 = 准则分类 + 宿主段落),
187
187
  * 平铺成一长列会让"哪条是准则、哪条是宿主的段"无从分辨 —— 分组是**归属
@@ -26,6 +26,17 @@
26
26
  * `categories`/`text`/`order` 里残留的 `hardening` 键无须迁移 ——
27
27
  * `resolveCategories` 规则 2 会忽略不认识的 id。
28
28
  * ② 身份气质:「默认中文」→「回复、思考过程必须使用中文」。
29
+ *
30
+ * ⚠️ 2026-10-27 用户点名的四处改动:
31
+ * ① **删掉「可判定」分类**(整条移除,id `verdifiable`);已存配置里的残留键
32
+ * 由 `resolveCategories` 忽略,无须迁移。
33
+ * ② **删掉「绝不偷懒」分类**(整条移除,id `no-cheap`)——用户判定它与别处重复。
34
+ * ③ 执行顺序重写:去掉「查结构行号 / 声明任务四件套 / 读码取切片 / 循既有骨架 /
35
+ * 以闸门为准」几段,改为「循既有架构/提出更优架构 → 验证以测试/e2e 验收」;
36
+ * 并在首尾补上「读文档」与「更新文档」—— 任务不是从零开始,也不以提交收尾。
37
+ * ④ **新增「文档」分类**(id `doc`):用户口述「文档不写流水账、只写权威内容」
38
+ * + 点名范围「docs/ 下的调研、设计、任务」,本文件只做润色。
39
+ * 排在「全局收口」之后 —— 收口查"文档过期没有",这条管"文档该写什么"。
29
40
  */
30
41
  /** 一个可独立开关的提示词分类。 */
31
42
  export interface PromptCategory {
@@ -2,14 +2,12 @@
2
2
  * 分层提示词扩展的 **cordis 行** —— 官方插件页(设置 → 插件 → dsh-router-core)详情页
3
3
  * 原生「包含的组件」里的那一行。
4
4
  *
5
- * 壳的形状与 `ext-test` 同款(cordis 身份 + 登记,实现 = `plugin.ts`),
6
- * 但**多一件执行面的事**:挂 system prompt 段落。ext-test 不做拦截、只登记;
7
- * 本扩展要把准则真注入,所以额外等 `systemPrompt` service 就绪后挂 section。
5
+ * 壳的形状 = cordis 身份 + 登记,实现 = `plugin.ts`;**执行面的事**是挂
6
+ * system prompt 段落 —— 所以额外等 `systemPrompt` service 就绪后挂 section。
8
7
  *
9
- * **默认开启**(patch 里不带 `disabled`,与三行内置供应商一致):
10
- * 协作准则是每次协作都要的基础设施,不是可选增强。与 `ext-test` 的
11
- * `disabled: true` **刻意不同** —— 自检是「想跑才跑」的工具,准则不是;
12
- * 让准则默认缺席,会变成「用户不知道它存在 ⇒ 协作按另一套规则执行」。
8
+ * **默认关闭**(patch 里 `disabled: true`):开不开由用户决定。行关闭时 loader
9
+ * 根本不 import 本模块,扩展就不在 `router.ext` 表里 ——「打开才出现这张卡片」
10
+ * 是天然的,不需要额外状态位。
13
11
  *
14
12
  * 登记时把扩展开关置为开:本扩展**没有独立的运行期开关**,它的开关就是这一行
15
13
  * (宿主管的那个)。不置上的话,用户开了行还要再点一次,两级开关说同一件事。
@@ -22,8 +20,8 @@ export declare const name = "dsh-router-ext-prompt";
22
20
  *
23
21
  * ⚠️⚠️ **必须导出这个数组,且 `apply` 里直接用 `ctx.systemPrompt`**(2026-09-29 修)
24
22
  * ——我原先写成「在 apply 里 `ctx.inject(['systemPrompt'], cb)`」,**准则一直不进
25
- * prompt**:面板上卡片、开关、原文全都正常(`GET /router/api/ext` 实测有 14 条
26
- * controls),但模型看到的 system prompt 里没有它。
23
+ * prompt**:面板上卡片、开关、原文全都正常(`GET /router/api/ext` 里 controls 一条
24
+ * 不少),但模型看到的 system prompt 里没有它。
27
25
  *
28
26
  * 根因(本机 cordis 4.0.4 实测):`ctx.inject(deps, cb)` 的回调**不是同步触发** ——
29
27
  * provide 之后立刻 `inject`,回调要等到下一个异步点才跑。插件的 `apply` 是**同步
@@ -2,9 +2,9 @@
2
2
  * 挂载层 —— 把准则注册成 `ctx.systemPrompt` 的**一个**段落。
3
3
  *
4
4
  * ════════════════════════════════════════════════════════════════════
5
- * 为什么只注册一段,而不是 14 段
5
+ * 为什么只注册一段,而不是一段一个分类
6
6
  * ════════════════════════════════════════════════════════════════════
7
- * 面板开关的粒度是「分类」,但 prompt 读起来应该是一段连续的准则。分成 14 段
7
+ * 面板开关的粒度是「分类」,但 prompt 读起来应该是一段连续的准则。分成多段
8
8
  * 会让 KV cache 每个分类各占一段,且**增删任意一个分类都让它之后的全部分位**。
9
9
  * 一段 + 拼装 ⇒ 分类增删只改这一段内部,段的位置与前缀稳定。
10
10
  *
@@ -1,16 +1,14 @@
1
1
  /**
2
2
  * 分层提示词扩展(`router.ext` 侧的实现)—— 声明与状态都在这里。
3
3
  *
4
- * 与 `ext-test`(插件自检)同款:**不挂任何监听、不改写任何东西**,
5
- * 唯一作用是让用户在官方插件页里看到一张卡片、能开关它,并把协作准则
6
- * 注册成一段 system prompt。
4
+ * **不挂任何监听、不改写任何东西**,唯一作用是让用户在官方插件页里看到一张
5
+ * 卡片、能开关它,并把协作准则注册成一段 system prompt。
7
6
  *
8
7
  * 与 `rtk` / `jev` 那类扩展的区别要写清楚:那两家挂 `tools/execute`
9
8
  * 改写**工具调用**;本扩展只往 **system prompt** 加一段。名字同族是因为
10
9
  * 都在 `router.ext` 这张表里,拦截面不重叠。
11
10
  *
12
- * 复用 `router.ext` 通道只为拿到**发现 + 开关**两件事(见 ext-test 的同款说明),
13
- * 真正的注入在 `mount.ts`。
11
+ * 复用 `router.ext` 通道只为拿到**发现 + 开关**两件事,真正的注入在 `mount.ts`。
14
12
  */
15
13
  import type { ExtStoreService, RouterExt } from '../ext/contract.ts';
16
14
  import { type SectionEntry } from './sections.ts';
@@ -32,7 +32,18 @@ export interface SupplierStatus {
32
32
  }
33
33
  /** OpenAI 模型条目。 */
34
34
  export interface ModelInfo {
35
+ /** 上游的模型 key(**唯一标识**:组合存它、路由认它、上游收它)。 */
35
36
  id: string;
37
+ /**
38
+ * 可读名(上游自己写的,如 `Qwen3.8-Max`)。**只用于显示**,缺省 = `id`。
39
+ *
40
+ * 为什么需要它:有些上游的 key 是内部标识(Qoder 的 `qmodel_38max`、
41
+ * `kmodel_latest`),面板上只剩一串猜不出含义的字母。既然上游在目录里
42
+ * 同时下发了 `display_name`,丢掉它就是把可读信息换成噪声。
43
+ * 反过来,**凡是"发给上游/存下来"的地方都得继续用 `id`**——用名字去请求
44
+ * 上游只会拿到 404。
45
+ */
46
+ name?: string;
36
47
  context_length?: number;
37
48
  }
38
49
  /** 面板展示的模型(含启用状态)。 */
@@ -218,6 +218,8 @@ export interface RouterExtResponse {
218
218
  /** 供应商模型(含启用状态)。 */
219
219
  export interface RouterSupplierModel {
220
220
  id: string;
221
+ /** 可读名(上游写的,只用于显示;缺省 = id)。见 `ModelInfo.name`。 */
222
+ name?: string;
221
223
  context_length?: number;
222
224
  enabled: boolean;
223
225
  /** 是否为自定义(手动添加 / 拉取)模型,可删除。 */
@@ -3,7 +3,6 @@
3
3
  *
4
4
  * 通用能力(dsh-router 核心,所有供应商自动可用,js 无需实现):
5
5
  * GET /suppliers/:id/models models + alias(listModels 合并启用状态)
6
- * POST /suppliers/:id/probe 契约体检:逐成员报「实现/可用/不执行」
7
6
  * PATCH /suppliers/:id/enabled {enabled} —— 供应商开关(关掉不参与路由)
8
7
  * POST /suppliers/:id/models/toggle {id, enabled}
9
8
  * POST /suppliers/:id/models/add {id}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-router-core",
3
- "version": "0.4.7",
3
+ "version": "0.4.9",
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": {
@@ -36,12 +36,6 @@
36
36
  },
37
37
  "./suppliers/nvidia/package.json": "./lib/suppliers/nvidia/package.json",
38
38
  "./suppliers/nvidia/locale/*.json": "./lib/suppliers/nvidia/locale/*.json",
39
- "./ext-test": {
40
- "types": "./lib/types/ext-test/index.d.ts",
41
- "default": "./lib/ext-test/index.js"
42
- },
43
- "./ext-test/package.json": "./lib/ext-test/package.json",
44
- "./ext-test/locale/*.json": "./lib/ext-test/locale/*.json",
45
39
  "./ext-prompt": {
46
40
  "types": "./lib/types/ext-prompt/index.d.ts",
47
41
  "default": "./lib/ext-prompt/index.js"
@@ -79,9 +73,6 @@
79
73
  "lib/suppliers/*/index.js",
80
74
  "lib/suppliers/*/package.json",
81
75
  "lib/suppliers/*/locale/*.json",
82
- "lib/ext-test/index.js",
83
- "lib/ext-test/package.json",
84
- "lib/ext-test/locale/*.json",
85
76
  "lib/ext-prompt/index.js",
86
77
  "lib/ext-prompt/package.json",
87
78
  "lib/ext-prompt/locale/*.json",