dsh-router-core 0.4.6 → 0.4.8
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/README.md +156 -76
- package/cordis.patch.yml +2 -7
- package/docs/ext.md +4 -18
- package/docs/protocols.md +198 -0
- package/lib/client-registry.js +193 -668
- package/lib/client.js +193 -668
- package/lib/ext-prompt/index.js +202 -32
- package/lib/index.js +2805 -547
- package/lib/types/ext/contract.d.ts +20 -1
- package/lib/types/ext-prompt/content.d.ts +18 -1
- package/lib/types/ext-prompt/index.d.ts +7 -9
- package/lib/types/ext-prompt/mount.d.ts +14 -3
- package/lib/types/ext-prompt/plugin.d.ts +13 -5
- package/lib/types/ext-prompt/render.d.ts +14 -3
- package/lib/types/ext-prompt/sections.d.ts +95 -0
- package/lib/types/protocol/anthropic.d.ts +35 -0
- package/lib/types/protocol/chat.d.ts +25 -0
- package/lib/types/protocol/entry.d.ts +137 -0
- package/lib/types/protocol/gemini.d.ts +57 -0
- package/lib/types/protocol/index.d.ts +13 -0
- package/lib/types/protocol/responses.d.ts +41 -0
- package/lib/types/protocol/sse.d.ts +41 -0
- package/lib/types/protocol/stream.d.ts +92 -0
- package/lib/types/protocol/types.d.ts +224 -0
- package/lib/types/router/index.d.ts +42 -1
- package/lib/types/router/types.d.ts +19 -0
- package/lib/types/shared.d.ts +11 -0
- package/lib/types/suppliers/contract.d.ts +8 -0
- package/lib/types/suppliers/registry.d.ts +0 -1
- package/package.json +6 -15
- package/src/client/ExtDetail.tsx +26 -18
- package/src/client/group-controls.test.ts +53 -0
- package/src/client/group-controls.ts +43 -0
- package/src/client/index.tsx +0 -17
- package/src/client/router-components.test.ts +1 -1
- package/src/client/router.css +23 -355
- package/src/ext/contract.ts +20 -1
- package/src/ext-prompt/assembly.test.ts +63 -0
- package/src/ext-prompt/content.ts +25 -22
- package/src/ext-prompt/ext-prompt.test.ts +41 -31
- package/src/ext-prompt/index.ts +57 -11
- package/src/ext-prompt/mount.test.ts +18 -1
- package/src/ext-prompt/mount.ts +24 -9
- package/src/ext-prompt/plugin.ts +67 -6
- package/src/ext-prompt/render.test.ts +1 -1
- package/src/ext-prompt/render.ts +17 -6
- package/src/ext-prompt/section-controls.test.ts +148 -0
- package/src/ext-prompt/section-filter.test.ts +140 -0
- package/src/ext-prompt/sections.test.ts +123 -0
- package/src/ext-prompt/sections.ts +193 -0
- package/src/ext-route.test.ts +16 -0
- package/src/index.ts +109 -1
- package/src/protocol/anthropic.ts +662 -0
- package/src/protocol/chat.ts +272 -0
- package/src/protocol/entry.ts +440 -0
- package/src/protocol/gemini.ts +548 -0
- package/src/protocol/index.ts +31 -0
- package/src/protocol/protocol.test.ts +728 -0
- package/src/protocol/responses.ts +551 -0
- package/src/protocol/sse.ts +94 -0
- package/src/protocol/stream.ts +195 -0
- package/src/protocol/types.ts +249 -0
- package/src/protocol/wire-e2e.test.ts +208 -0
- package/src/protocol/wiring.test.ts +225 -0
- package/src/router/index.ts +144 -26
- package/src/router/models-cache.test.ts +5 -6
- package/src/router/supplier-switch.test.ts +1 -1
- package/src/router/types.ts +19 -0
- package/src/shared.ts +11 -0
- package/src/suppliers/contract.ts +8 -0
- package/src/suppliers/registry.ts +0 -71
- package/lib/contract-D4BWZ7Xe.js +0 -13
- package/lib/ext-test/index.js +0 -36
- package/lib/ext-test/locale/en.json +0 -6
- package/lib/ext-test/locale/zh.json +0 -6
- package/lib/ext-test/package.json +0 -5
- package/lib/types/ext-test/index.d.ts +0 -21
- package/lib/types/ext-test/plugin.d.ts +0 -23
- package/lib/types/suppliers/probe.d.ts +0 -160
- package/src/client/ExtTestPanel.tsx +0 -392
- package/src/client/comp-button-scope.test.ts +0 -58
- package/src/client/comp-panel-layout.test.ts +0 -58
- package/src/client/ext-panels.test.ts +0 -44
- package/src/client/ext-panels.ts +0 -49
- package/src/client/model-choice.test.ts +0 -41
- package/src/client/model-choice.ts +0 -37
- package/src/ext-test/ext-test.test.ts +0 -60
- package/src/ext-test/index.ts +0 -35
- package/src/ext-test/locale/en.json +0 -6
- package/src/ext-test/locale/zh.json +0 -6
- package/src/ext-test/plugin.ts +0 -34
- package/src/ext-test/row.json +0 -5
- package/src/suppliers/probe.test.ts +0 -266
- package/src/suppliers/probe.ts +0 -475
|
@@ -18,6 +18,7 @@ import { SystemPrompt, renderPrompt, type PromptAssembly } from '@deepseek-ai/ds
|
|
|
18
18
|
import { mountPromptSection, PROMPT_SECTION_NAME } from './mount.ts'
|
|
19
19
|
import { renderPromptText } from './render.ts'
|
|
20
20
|
import { PROMPT_CATEGORIES } from './content.ts'
|
|
21
|
+
import { createSectionCache, disabledSectionNames, filterSections } from './sections.ts'
|
|
21
22
|
|
|
22
23
|
/** 起一个真 systemPrompt 并挂上本扩展。 */
|
|
23
24
|
async function assemble(
|
|
@@ -77,3 +78,65 @@ test('装配结果与 renderPromptText 的内容一致(渲染逻辑只有一
|
|
|
77
78
|
const expected = renderPromptText(true, undefined)
|
|
78
79
|
assert.ok(text.includes(expected), '装配结果里找不到 renderPromptText 的输出 ⇒ 两处实现漂移了')
|
|
79
80
|
})
|
|
81
|
+
|
|
82
|
+
// ── 段落总控:真宿主装配链上的"关了就没了"(2026-10-21)──────────
|
|
83
|
+
/**
|
|
84
|
+
* ⚠️ 这里才是段落总控的**终局判据**。前面 `section-filter.test.ts` 验的是
|
|
85
|
+
* "监听器按规则返回了什么",而"宿主真把那个返回值当成最终装配"是**另一件事**
|
|
86
|
+
* —— 宿主源码里 waterfall 之后还有 complete-section 恢复、runtime-context
|
|
87
|
+
* 抑制等收尾,任何一处改动都可能让我们删的段回来。只有跑真 `assemble()`
|
|
88
|
+
* 才能钉住。
|
|
89
|
+
*/
|
|
90
|
+
async function assembleWithSections(
|
|
91
|
+
opts: { masterOn?: boolean; data?: unknown },
|
|
92
|
+
): Promise<{ names: string[]; text: string; dispose: () => void }> {
|
|
93
|
+
const ctx = new Context()
|
|
94
|
+
new SystemPrompt(ctx, { includeHarnessIdentity: true, personaPrefix: 'PERSONA-PREFIX-SENTINEL' })
|
|
95
|
+
let enabled = true
|
|
96
|
+
const cache = createSectionCache()
|
|
97
|
+
// 与 index.ts 的监听器**同一形状**:先 next()、喂清单、再按落盘过滤
|
|
98
|
+
const offAssemble = ctx.on('system-prompt/assemble', async (assembly, _c, next) => {
|
|
99
|
+
const result = await next()
|
|
100
|
+
cache.update(result.sections)
|
|
101
|
+
if ((opts.masterOn ?? enabled) !== true) return result
|
|
102
|
+
const off = disabledSectionNames(opts.data as never)
|
|
103
|
+
if (off.size === 0) return result
|
|
104
|
+
return { ...result, sections: filterSections(result.sections, off) }
|
|
105
|
+
})
|
|
106
|
+
// 本扩展自己的准则段(带正文哨兵,便于断言它没被段开关删掉)
|
|
107
|
+
const dispose = mountPromptSection({
|
|
108
|
+
getSystemPrompt: () => ctx.systemPrompt,
|
|
109
|
+
isEnabled: () => true,
|
|
110
|
+
readData: () => undefined,
|
|
111
|
+
})
|
|
112
|
+
await new Promise((r) => setTimeout(r, 10))
|
|
113
|
+
const a = await ctx.systemPrompt.assemble({})
|
|
114
|
+
return {
|
|
115
|
+
names: a.sections.map((s) => s.name),
|
|
116
|
+
text: renderPrompt(a),
|
|
117
|
+
dispose: () => { offAssemble(); dispose(); void ctx },
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
test('★ 真装配链:关掉某段落 ⇒ 它真的不在最终 prompt 里', async () => {
|
|
122
|
+
const on = await assembleWithSections({})
|
|
123
|
+
// 宿主自带的段(关掉 includeHarnessIdentity 也仍在表里的话更好,但优先用我们自己的)
|
|
124
|
+
assert.ok(on.names.includes(PROMPT_SECTION_NAME), `准则段不在装配里:${on.names.join(', ')}`)
|
|
125
|
+
const target = on.names.find((n) => n === 'deployment:persona-prefix')
|
|
126
|
+
assert.ok(target !== undefined, `找不到宿主 persona 段,实际段名:${on.names.join(', ')}`)
|
|
127
|
+
on.dispose()
|
|
128
|
+
|
|
129
|
+
const off = await assembleWithSections({ data: { sections: { [target]: false } } })
|
|
130
|
+
assert.equal(off.names.includes(target), false, `真装配链里 ${target} 关了却还在`)
|
|
131
|
+
assert.equal(off.text.includes('PERSONA-PREFIX-SENTINEL'), false, '段没了但正文还在 ⇒ 过滤没生效')
|
|
132
|
+
// 反向:别的段与准则不能陪葬
|
|
133
|
+
assert.ok(off.names.includes(PROMPT_SECTION_NAME), '关别人的段时把准则段一起删了')
|
|
134
|
+
off.dispose()
|
|
135
|
+
})
|
|
136
|
+
|
|
137
|
+
test('★ 真装配链:总开关关 ⇒ 一个段都不动(扩展关 = 不干预宿主)', async () => {
|
|
138
|
+
const r = await assembleWithSections({ masterOn: false, data: { sections: { 'deployment:persona-prefix': false } } })
|
|
139
|
+
assert.ok(r.names.includes('deployment:persona-prefix'), '总开关关掉后仍在过滤 ⇒ 关了还生效')
|
|
140
|
+
assert.ok(r.text.includes('PERSONA-PREFIX-SENTINEL'), '总开关关掉后 persona 正文不见了')
|
|
141
|
+
r.dispose()
|
|
142
|
+
})
|
|
@@ -20,6 +20,23 @@
|
|
|
20
20
|
* —— 它没有终止条件,每次收口都必然能再找出一件存量,等于无限循环。
|
|
21
21
|
* ② 固化:由"每次用户可见损失都必须固化"降级为"只对数据丢失/安全这类真损失
|
|
22
22
|
* 补检查",并删掉「不固化的教训等于决定再犯一次」这句无界义务。
|
|
23
|
+
*
|
|
24
|
+
* ⚠️ 2026-10-17 用户点名的两处改动:
|
|
25
|
+
* ① **删掉「固化」分类**(整条移除,id `hardening` 不复存在)。用户已存的
|
|
26
|
+
* `categories`/`text`/`order` 里残留的 `hardening` 键无须迁移 ——
|
|
27
|
+
* `resolveCategories` 规则 2 会忽略不认识的 id。
|
|
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
|
+
* 排在「全局收口」之后 —— 收口查"文档过期没有",这条管"文档该写什么"。
|
|
23
40
|
*/
|
|
24
41
|
|
|
25
42
|
/** 一个可独立开关的提示词分类。 */
|
|
@@ -45,7 +62,7 @@ export const PROMPT_CATEGORIES: readonly PromptCategory[] = [
|
|
|
45
62
|
title: '身份气质',
|
|
46
63
|
defaultOn: true,
|
|
47
64
|
body:
|
|
48
|
-
'身份气质:大肥鱼 🐋(资深开发者)
|
|
65
|
+
'身份气质:大肥鱼 🐋(资深开发者);懒=高效不是马虎;安静、克制、直接;回复、思考过程必须使用中文,直接给能跑的东西。',
|
|
49
66
|
},
|
|
50
67
|
{
|
|
51
68
|
id: 'scope',
|
|
@@ -81,19 +98,12 @@ export const PROMPT_CATEGORIES: readonly PromptCategory[] = [
|
|
|
81
98
|
'做完那块区域该比动手前更整或更少。',
|
|
82
99
|
},
|
|
83
100
|
{
|
|
84
|
-
id: '
|
|
85
|
-
title: '
|
|
86
|
-
defaultOn: true,
|
|
87
|
-
body:
|
|
88
|
-
'固化:只对数据丢失/安全这类真损失补检查——会变红(注入违规必须失败)、可重放(一条命令)、' +
|
|
89
|
-
'归属清晰(失败信息指向违规处);普通 bug 修好即止,不额外造闸门。',
|
|
90
|
-
},
|
|
91
|
-
{
|
|
92
|
-
id: 'verdifiable',
|
|
93
|
-
title: '可判定',
|
|
101
|
+
id: 'doc',
|
|
102
|
+
title: '文档',
|
|
94
103
|
defaultOn: true,
|
|
95
104
|
body:
|
|
96
|
-
'
|
|
105
|
+
'文档:docs/ 里的调研/设计/任务文档,只写权威内容——结论、约束、怎么用;' +
|
|
106
|
+
'不写流水账(过程、试错、时间线),历史归 git。',
|
|
97
107
|
},
|
|
98
108
|
{
|
|
99
109
|
id: 'root-cause',
|
|
@@ -125,13 +135,6 @@ export const PROMPT_CATEGORIES: readonly PromptCategory[] = [
|
|
|
125
135
|
'结构:先让结构对再让代码短;命名/边界/单一职责优先于行数;依赖单向无环、公共逻辑收敛一处;' +
|
|
126
136
|
'单文件单功能,软顶函数 ~40 行/文件 ~300 行。',
|
|
127
137
|
},
|
|
128
|
-
{
|
|
129
|
-
id: 'no-cheap',
|
|
130
|
-
title: '绝不偷懒',
|
|
131
|
-
defaultOn: true,
|
|
132
|
-
body:
|
|
133
|
-
'绝不偷懒:边界输入校验、防数据丢失、安全、可访问性、真实硬件校准;非平凡逻辑留个能跑的检查。',
|
|
134
|
-
},
|
|
135
138
|
{
|
|
136
139
|
id: 'delivery',
|
|
137
140
|
title: '交付',
|
|
@@ -145,8 +148,8 @@ export const PROMPT_CATEGORIES: readonly PromptCategory[] = [
|
|
|
145
148
|
title: '执行顺序',
|
|
146
149
|
defaultOn: true,
|
|
147
150
|
body:
|
|
148
|
-
'
|
|
149
|
-
'
|
|
151
|
+
'执行:拉记忆→读文档→看全貌→定位任务→建领地→循既有架构/提出更优架构→验证以测试/e2e 验收→' +
|
|
152
|
+
'收口复查全貌→更新文档→提交。',
|
|
150
153
|
},
|
|
151
154
|
] as const
|
|
152
155
|
|
|
@@ -155,7 +158,7 @@ export const PROMPT_CATEGORIES: readonly PromptCategory[] = [
|
|
|
155
158
|
*
|
|
156
159
|
* ⚠️ 拆分时差点丢它:原文是 `[…] ·身份气质:…`,标题挂在方括号里、
|
|
157
160
|
* **不属于任何单条分类**,按分隔符切分时自然被丢掉
|
|
158
|
-
* (只在 `content.ts` 头注出现过、没进任何 `body`)。⇒
|
|
161
|
+
* (只在 `content.ts` 头注出现过、没进任何 `body`)。⇒ 模型会看到一堆
|
|
159
162
|
* **没有名字**的规则,而"这套东西叫什么、为什么存在"恰恰是它需要的上下文。
|
|
160
163
|
*
|
|
161
164
|
* 形状:**不属于任何分类** ⇒ 不可被单条开关关掉(它一关,准则就无名无姓)。
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
* 1. 登记进 `router.ext` 表,带上可显示的身份(名字/说明/就绪);
|
|
4
4
|
* 2. **幂等**:同一个 id 已在表里就不顶掉别人的登记;
|
|
5
5
|
* 3. `source: 'builtin'`(随核心分发 ⇒ 插件页的原生行,自绘节不重复列);
|
|
6
|
-
* 4. **准则段落真的挂上了** ——
|
|
6
|
+
* 4. **准则段落真的挂上了** —— 这是本扩展唯一的执行面,必须单独验。
|
|
7
7
|
*
|
|
8
8
|
* 为什么第 4 条不可省:扩展的「卡片出现与否」取决于它在不在这张表里,而**准则
|
|
9
9
|
* 有没有进 prompt** 取决于段落挂没挂 —— 两者是**不同的失效**,都静默且都不报错。
|
|
@@ -20,7 +20,7 @@ import { PROMPT_CATEGORIES, PROMPT_TITLE } from './content.ts'
|
|
|
20
20
|
import type { RouterExtService } from '../ext/contract.ts'
|
|
21
21
|
|
|
22
22
|
test('扩展器带上面板要用的身份', () => {
|
|
23
|
-
const ext = createPromptExt({ isSystemPromptReady: () => true })
|
|
23
|
+
const ext = createPromptExt({ isSystemPromptReady: () => true, getSections: () => [] })
|
|
24
24
|
assert.equal(ext.id, EXT_PROMPT_ID)
|
|
25
25
|
assert.equal(ext.name, '分层提示词')
|
|
26
26
|
assert.equal(ext.source, 'builtin', '随核心分发必须标 builtin,否则插件页会重复列它')
|
|
@@ -34,7 +34,7 @@ test('★ `description` 是整段标题(不是自我介绍,也不内插分
|
|
|
34
34
|
// ③ 现在又要求**顶部显示标题** ⇒ 改成「等于 PROMPT_TITLE」。
|
|
35
35
|
// 两次反复的共同教训:**默认值/文案该是什么由用户拍板,判据只负责让改动可见**,
|
|
36
36
|
// 所以断言跟着改、并在注释里留着反复,免得下一个人以为是"又改坏了"。
|
|
37
|
-
const ext = createPromptExt({ isSystemPromptReady: () => true })
|
|
37
|
+
const ext = createPromptExt({ isSystemPromptReady: () => true, getSections: () => [] })
|
|
38
38
|
assert.equal(ext.description, PROMPT_TITLE, '详情页顶部要显示的就是整段标题')
|
|
39
39
|
// ⚠️ 尤其**不许**内插 `${PROMPT_CATEGORIES.length}`:加减分类后文案不跟着变,
|
|
40
40
|
// 就是同一件事的两个来源(「反查重复」)。
|
|
@@ -53,7 +53,7 @@ test('★ 插件页那一行的说明仍然在(来自 locale,不是 descript
|
|
|
53
53
|
})
|
|
54
54
|
|
|
55
55
|
test('注入:systemPrompt 不可用 ⇒ 报 not ready(不能静默不生效)', () => {
|
|
56
|
-
const ext = createPromptExt({ isSystemPromptReady: () => false })
|
|
56
|
+
const ext = createPromptExt({ isSystemPromptReady: () => false, getSections: () => [] })
|
|
57
57
|
const st = ext.getState()
|
|
58
58
|
assert.equal(st.ready, false, '无处注入却报就绪 = 卡片亮着、准则没进 prompt')
|
|
59
59
|
assert.ok((st.detail ?? '').length > 0, '不报还不说原因 = 面板上无从判断')
|
|
@@ -75,7 +75,7 @@ test('★ patch 里真的声明了这一行,且 id/name 逐字对得上', () =
|
|
|
75
75
|
assert.equal(block![1], 'dsh-router-core/ext-prompt', 'patch 的 name 必须是能解析的子路径说明符')
|
|
76
76
|
})
|
|
77
77
|
|
|
78
|
-
test('★
|
|
78
|
+
test('★ 那一行默认关闭', () => {
|
|
79
79
|
// `disabled: true` = 行默认关 ⇒ loader 不 import ⇒ 扩展不进 `router.ext` 表 ⇒
|
|
80
80
|
// 卡片不出现、准则不进 prompt。
|
|
81
81
|
//
|
|
@@ -102,17 +102,27 @@ test('★ 那一行默认关闭(与 ext-test 同款)', () => {
|
|
|
102
102
|
* 真 cordis 会如实复现时序(本机 4.0.4 实测:provide 后 inject 不触发,
|
|
103
103
|
* start() 之后才触发)。
|
|
104
104
|
*/
|
|
105
|
-
async function realCtx(opts: { enabled?: boolean; data?: unknown } = {}) {
|
|
105
|
+
async function realCtx(opts: { enabled?: boolean; data?: unknown; assembled?: { name: string; text: string }[] } = {}) {
|
|
106
106
|
const { Context } = await import('@deepseek-ai/cordis')
|
|
107
107
|
const table: RouterExtService = {}
|
|
108
108
|
const sections: { name: string; order: number; text: string | (() => string) }[] = []
|
|
109
|
+
const assembled = opts.assembled ?? []
|
|
110
|
+
let assembleCalls = 0
|
|
109
111
|
const ctx = new Context()
|
|
110
112
|
ctx.provide('systemPrompt', {
|
|
111
113
|
section: (s: { name: string; order: number; text: string | (() => string) }) => {
|
|
112
114
|
sections.push(s)
|
|
113
115
|
return () => {}
|
|
114
116
|
},
|
|
115
|
-
getSectionOrder: () => 100
|
|
117
|
+
// ⚠️ 这里曾经有个 `getSectionOrder: () => 100` 的桩:没人调用它(`mount.ts`
|
|
118
|
+
// 自己定 order),而 100 已经不是本插件的段落 order(2026-10-21 改为 -2000
|
|
119
|
+
// 放到最前)⇒ 留着就是一句与事实不符的注脚,删掉。
|
|
120
|
+
// 段落总控的预热/清单源(apply 里 `void sp.assemble()`);返回值直接决定
|
|
121
|
+
// 面板清单,测试用 `assembled` 注入。
|
|
122
|
+
assemble: async () => {
|
|
123
|
+
assembleCalls++
|
|
124
|
+
return { sections: assembled.map((s) => ({ ...s })) }
|
|
125
|
+
},
|
|
116
126
|
})
|
|
117
127
|
ctx.provide('router.ext', table)
|
|
118
128
|
ctx.provide('router.extStore', {
|
|
@@ -121,7 +131,7 @@ async function realCtx(opts: { enabled?: boolean; data?: unknown } = {}) {
|
|
|
121
131
|
readData: () => opts.data,
|
|
122
132
|
writeData: () => {},
|
|
123
133
|
})
|
|
124
|
-
return { ctx, table, sections }
|
|
134
|
+
return { ctx, table, sections, getAssembleCalls: () => assembleCalls }
|
|
125
135
|
}
|
|
126
136
|
|
|
127
137
|
test('apply 把扩展登记进 router.ext 表(这张表就是「卡片出现」的唯一依据)', async () => {
|
|
@@ -138,7 +148,7 @@ test('幂等:同一个 id 已在表里时不顶掉别人的登记', async () =
|
|
|
138
148
|
assert.equal(table[EXT_PROMPT_ID], first, '重复登记不该换成一个新对象')
|
|
139
149
|
})
|
|
140
150
|
|
|
141
|
-
test('★ apply 把准则挂成了 system prompt
|
|
151
|
+
test('★ apply 把准则挂成了 system prompt 段落', async () => {
|
|
142
152
|
const { ctx, sections } = await realCtx()
|
|
143
153
|
apply(ctx as unknown as Parameters<typeof apply>[0])
|
|
144
154
|
assert.equal(sections.length, 1, '准则没挂上 = 卡片亮着但 prompt 里没有准则')
|
|
@@ -163,14 +173,14 @@ test('注入:用户关掉某分类 ⇒ 该分类不进 prompt(可开关的
|
|
|
163
173
|
|
|
164
174
|
// ── 面板编辑文本(2026-09-29)────────────────────────────────
|
|
165
175
|
test('★ 每条都标 editable,面板才知道该不该给「修改」按钮', () => {
|
|
166
|
-
const ext = createPromptExt({ isSystemPromptReady: () => true, store: fakeStore({ categories: {} }) })
|
|
176
|
+
const ext = createPromptExt({ isSystemPromptReady: () => true, getSections: () => [], store: fakeStore({ categories: {} }) })
|
|
167
177
|
for (const c of ext.controls ?? []) assert.equal(c.editable, true, `${c.id} 没标 editable`)
|
|
168
178
|
assert.equal(typeof ext.setControlText, 'function', '没有 setControlText ⇒ 面板点了必然 400')
|
|
169
179
|
})
|
|
170
180
|
|
|
171
181
|
test('★ 编辑后 controls 立刻反映新标题/新正文(每次读都重算)', () => {
|
|
172
182
|
const store = fakeStore({ categories: {}, text: { identity: { title: '我是谁', body: '我是一个测试。' } } })
|
|
173
|
-
const ext = createPromptExt({ isSystemPromptReady: () => true, store })
|
|
183
|
+
const ext = createPromptExt({ isSystemPromptReady: () => true, getSections: () => [], store })
|
|
174
184
|
const c = (ext.controls ?? []).find((x) => x.id === 'identity')!
|
|
175
185
|
assert.equal(c.title, '我是谁')
|
|
176
186
|
assert.equal(c.body, '我是一个测试。')
|
|
@@ -182,7 +192,7 @@ test('★ controls 不是快照:改完之后**再读一次**要看到新值',
|
|
|
182
192
|
// 旧标题**(用户看到"保存了又变回去")。而我当时注释写着"现算不缓存"。
|
|
183
193
|
// ⇒ 判据必须**跨两次读**才抓得住(只读一次的写法抓不到)。
|
|
184
194
|
const store = fakeStore({ categories: {} })
|
|
185
|
-
const ext = createPromptExt({ isSystemPromptReady: () => true, store })
|
|
195
|
+
const ext = createPromptExt({ isSystemPromptReady: () => true, getSections: () => [], store })
|
|
186
196
|
const before = (ext.controls ?? []).find((x) => x.id === 'ladder')!
|
|
187
197
|
assert.equal(before.title, '懒人梯子')
|
|
188
198
|
ext.setControlText?.('ladder', { title: '我的梯子' })
|
|
@@ -192,7 +202,7 @@ test('★ controls 不是快照:改完之后**再读一次**要看到新值',
|
|
|
192
202
|
|
|
193
203
|
test('setControlText 只写覆盖值,且不碰 categories(同块的另一种数据)', () => {
|
|
194
204
|
const store = fakeStore({ categories: { identity: false }, keep: 'me' })
|
|
195
|
-
const ext = createPromptExt({ isSystemPromptReady: () => true, store })
|
|
205
|
+
const ext = createPromptExt({ isSystemPromptReady: () => true, getSections: () => [], store })
|
|
196
206
|
assert.equal(ext.setControlText?.('identity', { title: '新标题' }), true)
|
|
197
207
|
const after = store.readData<Record<string, unknown>>('')
|
|
198
208
|
assert.deepEqual(after?.categories, { identity: false }, '改标题把开关状态冲掉了')
|
|
@@ -207,7 +217,7 @@ test('★ 改一条**不碰其它条**的覆盖(整块重写 text 会静默丢
|
|
|
207
217
|
categories: {},
|
|
208
218
|
text: { identity: { title: 'A' }, scope: { body: 'B' } },
|
|
209
219
|
})
|
|
210
|
-
const ext = createPromptExt({ isSystemPromptReady: () => true, store })
|
|
220
|
+
const ext = createPromptExt({ isSystemPromptReady: () => true, getSections: () => [], store })
|
|
211
221
|
ext.setControlText?.('ladder', { body: '新内容' })
|
|
212
222
|
assert.deepEqual(store.readData<Record<string, unknown>>('')?.text, {
|
|
213
223
|
identity: { title: 'A' },
|
|
@@ -218,7 +228,7 @@ test('★ 改一条**不碰其它条**的覆盖(整块重写 text 会静默丢
|
|
|
218
228
|
|
|
219
229
|
test('改同一条的第二个字段 ⇒ 第一个字段仍在(部分更新,不是替换)', () => {
|
|
220
230
|
const store = fakeStore({ categories: {}, text: { identity: { title: 'A' } } })
|
|
221
|
-
const ext = createPromptExt({ isSystemPromptReady: () => true, store })
|
|
231
|
+
const ext = createPromptExt({ isSystemPromptReady: () => true, getSections: () => [], store })
|
|
222
232
|
ext.setControlText?.('identity', { body: 'B' })
|
|
223
233
|
assert.deepEqual(store.readData<Record<string, unknown>>('')?.text, {
|
|
224
234
|
identity: { title: 'A', body: 'B' },
|
|
@@ -227,7 +237,7 @@ test('改同一条的第二个字段 ⇒ 第一个字段仍在(部分更新,
|
|
|
227
237
|
|
|
228
238
|
test('注入:空标题/空正文返回 false(不写盘)', () => {
|
|
229
239
|
const store = fakeStore({ categories: {} })
|
|
230
|
-
const ext = createPromptExt({ isSystemPromptReady: () => true, store })
|
|
240
|
+
const ext = createPromptExt({ isSystemPromptReady: () => true, getSections: () => [], store })
|
|
231
241
|
for (const bad of [{ title: ' ' }, { title: '' }, { body: '' }]) {
|
|
232
242
|
assert.equal(ext.setControlText?.('identity', bad), false, `空串被接受了:${JSON.stringify(bad)}`)
|
|
233
243
|
}
|
|
@@ -236,7 +246,7 @@ test('注入:空标题/空正文返回 false(不写盘)', () => {
|
|
|
236
246
|
|
|
237
247
|
test('注入:未知 controlId 返回 false', () => {
|
|
238
248
|
const store = fakeStore({ categories: {} })
|
|
239
|
-
const ext = createPromptExt({ isSystemPromptReady: () => true, store })
|
|
249
|
+
const ext = createPromptExt({ isSystemPromptReady: () => true, getSections: () => [], store })
|
|
240
250
|
assert.equal(ext.setControlText?.('__evil__', { title: 'x' }), false)
|
|
241
251
|
})
|
|
242
252
|
|
|
@@ -262,7 +272,7 @@ test('★ controls 走合成列表:自建条目出现、标 custom、内置标
|
|
|
262
272
|
custom: [{ id: 'cu-1', title: '我的', defaultOn: true, body: 'B' }],
|
|
263
273
|
text: { ladder: { body: '改过的' } },
|
|
264
274
|
})
|
|
265
|
-
const ext = createPromptExt({ isSystemPromptReady: () => true, store })
|
|
275
|
+
const ext = createPromptExt({ isSystemPromptReady: () => true, getSections: () => [], store })
|
|
266
276
|
const list = ext.controls ?? []
|
|
267
277
|
assert.equal(list.length, PROMPT_CATEGORIES.length + 1, '自建条目没出现')
|
|
268
278
|
assert.equal(list.find((c) => c.id === 'cu-1')?.custom, true)
|
|
@@ -272,7 +282,7 @@ test('★ controls 走合成列表:自建条目出现、标 custom、内置标
|
|
|
272
282
|
|
|
273
283
|
test('★ 新增自建条目 ⇒ 返回新 id,且落盘(id 不撞内置)', () => {
|
|
274
284
|
const store = fakeStore({ categories: {} })
|
|
275
|
-
const ext = createPromptExt({ isSystemPromptReady: () => true, store })
|
|
285
|
+
const ext = createPromptExt({ isSystemPromptReady: () => true, getSections: () => [], store })
|
|
276
286
|
const id = ext.addCustomControl?.('我的准则', '做完了就收。')
|
|
277
287
|
assert.ok(id !== null && typeof id === 'string', '没返回新 id')
|
|
278
288
|
assert.notEqual(id, 'identity', '新 id 撞上了内置条目')
|
|
@@ -281,7 +291,7 @@ test('★ 新增自建条目 ⇒ 返回新 id,且落盘(id 不撞内置)',
|
|
|
281
291
|
|
|
282
292
|
test('注入:空标题/空正文新增 ⇒ 返回 null(不写盘)', () => {
|
|
283
293
|
const store = fakeStore({ categories: {} })
|
|
284
|
-
const ext = createPromptExt({ isSystemPromptReady: () => true, store })
|
|
294
|
+
const ext = createPromptExt({ isSystemPromptReady: () => true, getSections: () => [], store })
|
|
285
295
|
assert.equal(ext.addCustomControl?.(' ', 'x'), null)
|
|
286
296
|
assert.equal(ext.addCustomControl?.('x', ''), null)
|
|
287
297
|
assert.equal(store.readData<Record<string, unknown>>('')?.custom, undefined, '失败的写入留下痕迹')
|
|
@@ -294,7 +304,7 @@ test('★ 删除只允许自定义:内置条目被拒、自建条目删掉并
|
|
|
294
304
|
order: ['cu-1', 'ladder'],
|
|
295
305
|
custom: [{ id: 'cu-1', title: '我的', defaultOn: true, body: 'B' }],
|
|
296
306
|
})
|
|
297
|
-
const ext = createPromptExt({ isSystemPromptReady: () => true, store })
|
|
307
|
+
const ext = createPromptExt({ isSystemPromptReady: () => true, getSections: () => [], store })
|
|
298
308
|
assert.equal(ext.removeCustomControl?.('identity'), false, '内置条目不该被删(它是代码的一部分)')
|
|
299
309
|
assert.equal(ext.removeCustomControl?.('ladder'), false, '不存在的条目应被拒')
|
|
300
310
|
assert.equal(ext.removeCustomControl?.('cu-1'), true)
|
|
@@ -306,7 +316,7 @@ test('★ 删除只允许自定义:内置条目被拒、自建条目删掉并
|
|
|
306
316
|
|
|
307
317
|
test('★ 还原:只对被改过的内置条目成功,且回到内置文本', () => {
|
|
308
318
|
const store = fakeStore({ categories: {}, text: { ladder: { body: '改过的' } } })
|
|
309
|
-
const ext = createPromptExt({ isSystemPromptReady: () => true, store })
|
|
319
|
+
const ext = createPromptExt({ isSystemPromptReady: () => true, getSections: () => [], store })
|
|
310
320
|
assert.equal(ext.resetControlText?.('identity'), false, '没改过的内置条目不该「可还原」')
|
|
311
321
|
assert.equal(ext.resetControlText?.('cu-1'), false, '自建条目没有「内置版本」可回退')
|
|
312
322
|
assert.equal(ext.resetControlText?.('ladder'), true)
|
|
@@ -318,7 +328,7 @@ test('★ 还原:只对被改过的内置条目成功,且回到内置文本'
|
|
|
318
328
|
|
|
319
329
|
test('★ 排序:只存 id,且过滤掉不存在的 id(脏数据不留幽灵)', () => {
|
|
320
330
|
const store = fakeStore({ categories: {} })
|
|
321
|
-
const ext = createPromptExt({ isSystemPromptReady: () => true, store })
|
|
331
|
+
const ext = createPromptExt({ isSystemPromptReady: () => true, getSections: () => [], store })
|
|
322
332
|
assert.equal(ext.setControlOrder?.(['__ghost__', 'ladder', 'ladder']), true)
|
|
323
333
|
assert.deepEqual(store.readData<Record<string, unknown>>('')?.order, ['ladder'], '幽灵/重复 id 没被过滤')
|
|
324
334
|
})
|
|
@@ -330,7 +340,7 @@ test('★ 没有 data 记录时也能写("没改过" ≠ "不能写")', () =
|
|
|
330
340
|
// 当成"存储不可用"直接拒 ⇒ 「添加准则」永远报 `title and body are required`
|
|
331
341
|
// ——而 title/body 明明都传了,报错还指向错误的字段,排查成本极高。
|
|
332
342
|
const store = fakeStore(undefined) // **没有 data**
|
|
333
|
-
const ext = createPromptExt({ isSystemPromptReady: () => true, store })
|
|
343
|
+
const ext = createPromptExt({ isSystemPromptReady: () => true, getSections: () => [], store })
|
|
334
344
|
const id = ext.addCustomControl?.('我的准则', '做完就收。')
|
|
335
345
|
assert.ok(id !== null, '初始状态下添加失败(真实用户第一次用就是这个状态)')
|
|
336
346
|
assert.equal((ext.controls ?? []).some((c) => c.id === id), true, '写盘了但 controls 里没有')
|
|
@@ -338,7 +348,7 @@ test('★ 没有 data 记录时也能写("没改过" ≠ "不能写")', () =
|
|
|
338
348
|
|
|
339
349
|
test('★ 四个写入口在"无 data"时都可用(不只是 add)', () => {
|
|
340
350
|
const store = fakeStore(undefined)
|
|
341
|
-
const ext = createPromptExt({ isSystemPromptReady: () => true, store })
|
|
351
|
+
const ext = createPromptExt({ isSystemPromptReady: () => true, getSections: () => [], store })
|
|
342
352
|
assert.equal(ext.setControl?.('ladder', false), true, '开关在初始状态下不可用')
|
|
343
353
|
assert.equal(ext.setControlText?.('ladder', { body: 'x' }), true, '改文本在初始状态下不可用')
|
|
344
354
|
assert.equal(ext.setControlOrder?.(['ladder']), true, '排序在初始状态下不可用')
|
|
@@ -350,7 +360,7 @@ test('★ 还原:改过 ⇒ true(且覆盖被清掉);没改过 ⇒ false
|
|
|
350
360
|
// 而代码是对的。**假 store 比被守的东西更宽容 = 判据无效**(第三次栽在这)。
|
|
351
361
|
// ⇒ 假 store 必须**真的读回自己写的东西**:`fakeStore` 的 `box` 已在做。
|
|
352
362
|
const store = fakeStore({ categories: {} })
|
|
353
|
-
const ext = createPromptExt({ isSystemPromptReady: () => true, store })
|
|
363
|
+
const ext = createPromptExt({ isSystemPromptReady: () => true, getSections: () => [], store })
|
|
354
364
|
assert.equal(ext.resetControlText?.('identity'), false, '没改过的却说可还原')
|
|
355
365
|
ext.setControlText?.('ladder', { body: '改过的' })
|
|
356
366
|
assert.equal(ext.resetControlText?.('ladder'), true, '改过的却还原不了')
|
|
@@ -360,7 +370,7 @@ test('★ 还原:改过 ⇒ true(且覆盖被清掉);没改过 ⇒ false
|
|
|
360
370
|
// ── op 成功必须回整张表(2026-09-30)───────────────────────
|
|
361
371
|
test('★ 契约层:controls 上带 custom 标记(面板据此决定给删除还是还原)', () => {
|
|
362
372
|
const store = fakeStore({ categories: {}, custom: [{ id: 'cu-1', title: '我的', defaultOn: true, body: 'B' }] })
|
|
363
|
-
const ext = createPromptExt({ isSystemPromptReady: () => true, store })
|
|
373
|
+
const ext = createPromptExt({ isSystemPromptReady: () => true, getSections: () => [], store })
|
|
364
374
|
const mine = (ext.controls ?? []).find((c) => c.id === 'cu-1')
|
|
365
375
|
const builtin = (ext.controls ?? []).find((c) => c.id === 'identity')
|
|
366
376
|
assert.equal(mine?.custom, true, '自建条目没标 custom ⇒ 面板给"还原"而不是"删除"')
|
|
@@ -375,7 +385,7 @@ test('★ 自建条目的开关可切(判据要用合成列表,不能只用
|
|
|
375
385
|
// ⇒ 用户看到「自定义的开关控制报错」。而它**在 controls 列表里显示着** ——
|
|
376
386
|
// 列表用 `resolveCategories`、写入判据用 `PROMPT_CATEGORIES`,两处来源不一致。
|
|
377
387
|
const store = fakeStore({ categories: {}, custom: [{ id: 'cu-1', title: '我的', defaultOn: true, body: 'B' }] })
|
|
378
|
-
const ext = createPromptExt({ isSystemPromptReady: () => true, store })
|
|
388
|
+
const ext = createPromptExt({ isSystemPromptReady: () => true, getSections: () => [], store })
|
|
379
389
|
assert.equal((ext.controls ?? []).some((c) => c.id === 'cu-1'), true, '列表里没有它')
|
|
380
390
|
assert.equal(ext.setControl?.('cu-1', false), true, '列表里有、却切不了')
|
|
381
391
|
assert.equal(store.readData<Record<string, unknown>>('')?.categories && (store.readData<{ categories: Record<string, boolean> }>('')?.categories?.['cu-1']), false, '开关没落盘')
|
|
@@ -383,14 +393,14 @@ test('★ 自建条目的开关可切(判据要用合成列表,不能只用
|
|
|
383
393
|
|
|
384
394
|
test('★ 自建条目的文本可改(同一处来源不一致)', () => {
|
|
385
395
|
const store = fakeStore({ categories: {}, custom: [{ id: 'cu-1', title: '我的', defaultOn: true, body: 'B' }] })
|
|
386
|
-
const ext = createPromptExt({ isSystemPromptReady: () => true, store })
|
|
396
|
+
const ext = createPromptExt({ isSystemPromptReady: () => true, getSections: () => [], store })
|
|
387
397
|
assert.equal(ext.setControlText?.('cu-1', { title: '新标题' }), true, '自建条目改不了标题')
|
|
388
398
|
assert.equal((ext.controls ?? []).find((c) => c.id === 'cu-1')?.title, '新标题')
|
|
389
399
|
})
|
|
390
400
|
|
|
391
401
|
test('还原仍然只允许内置条目(自建没有「内置版本」可回退)', () => {
|
|
392
402
|
const store = fakeStore({ categories: {}, custom: [{ id: 'cu-1', title: '我的', defaultOn: true, body: 'B' }] })
|
|
393
|
-
const ext = createPromptExt({ isSystemPromptReady: () => true, store })
|
|
403
|
+
const ext = createPromptExt({ isSystemPromptReady: () => true, getSections: () => [], store })
|
|
394
404
|
assert.equal(ext.resetControlText?.('cu-1'), false, '自建条目不该能被「还原」')
|
|
395
405
|
})
|
|
396
406
|
|
|
@@ -418,7 +428,7 @@ test('★ 图标能被真实渲染(base64 必须解得出合法 SVG)', () =>
|
|
|
418
428
|
})
|
|
419
429
|
|
|
420
430
|
test('图标挂在 ext 上(不设的话面板退回默认闪电)', () => {
|
|
421
|
-
const ext = createPromptExt({ isSystemPromptReady: () => true })
|
|
431
|
+
const ext = createPromptExt({ isSystemPromptReady: () => true, getSections: () => [] })
|
|
422
432
|
assert.equal(ext.icon, PROMPT_LOGO_URL)
|
|
423
433
|
})
|
|
424
434
|
|
package/src/ext-prompt/index.ts
CHANGED
|
@@ -2,19 +2,25 @@
|
|
|
2
2
|
* 分层提示词扩展的 **cordis 行** —— 官方插件页(设置 → 插件 → dsh-router-core)详情页
|
|
3
3
|
* 原生「包含的组件」里的那一行。
|
|
4
4
|
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
* 本扩展要把准则真注入,所以额外等 `systemPrompt` service 就绪后挂 section。
|
|
5
|
+
* 壳的形状 = cordis 身份 + 登记,实现 = `plugin.ts`;**执行面的事**是挂
|
|
6
|
+
* system prompt 段落 —— 所以额外等 `systemPrompt` service 就绪后挂 section。
|
|
8
7
|
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
* 让准则默认缺席,会变成「用户不知道它存在 ⇒ 协作按另一套规则执行」。
|
|
8
|
+
* **默认关闭**(patch 里 `disabled: true`):开不开由用户决定。行关闭时 loader
|
|
9
|
+
* 根本不 import 本模块,扩展就不在 `router.ext` 表里 ——「打开才出现这张卡片」
|
|
10
|
+
* 是天然的,不需要额外状态位。
|
|
13
11
|
*
|
|
14
12
|
* 登记时把扩展开关置为开:本扩展**没有独立的运行期开关**,它的开关就是这一行
|
|
15
13
|
* (宿主管的那个)。不置上的话,用户开了行还要再点一次,两级开关说同一件事。
|
|
16
14
|
*/
|
|
17
15
|
import type { Context } from '@deepseek-ai/cordis'
|
|
16
|
+
// ⚠️ **只为一个目的而 import type**:让 cordis 的 `Events` 增强
|
|
17
|
+
// (`system-prompt/assemble` / `system-prompt/change`)进入本文件所在的编译单元。
|
|
18
|
+
// 不 import 的话,`ctx.on('system-prompt/assemble', …)` 的参数不是
|
|
19
|
+
// `keyof Events` ⇒ **声明构建(tsconfig.build.json,host 半边)直接报错**,
|
|
20
|
+
// 而主 tsconfig 因为 include 了 `assembly.test.ts`(它 import 了同一个包)
|
|
21
|
+
// 恰好把增强带进来 ⇒ `tsc --noEmit` 全绿、`npm run build` 才红。
|
|
22
|
+
// 这类"两种配置下结论不同"的坑,靠显式依赖修,不靠碰巧。
|
|
23
|
+
import type {} from '@deepseek-ai/dsh-system-prompt'
|
|
18
24
|
import {
|
|
19
25
|
currentExts,
|
|
20
26
|
currentExtStore,
|
|
@@ -24,6 +30,7 @@ import {
|
|
|
24
30
|
import { createPromptExt, EXT_PROMPT_ID } from './plugin.ts'
|
|
25
31
|
import { mountPromptSection, type SystemPromptFace } from './mount.ts'
|
|
26
32
|
import type { PromptExtData } from './render.ts'
|
|
33
|
+
import { createSectionCache, disabledSectionNames, filterSections } from './sections.ts'
|
|
27
34
|
|
|
28
35
|
/** cordis 插件身份(配置树里这一行的技术名;显示名走同目录的 locale)。 */
|
|
29
36
|
export const name = 'dsh-router-ext-prompt'
|
|
@@ -43,8 +50,8 @@ function systemPromptOf(sctx: Context): SystemPromptFace | undefined {
|
|
|
43
50
|
*
|
|
44
51
|
* ⚠️⚠️ **必须导出这个数组,且 `apply` 里直接用 `ctx.systemPrompt`**(2026-09-29 修)
|
|
45
52
|
* ——我原先写成「在 apply 里 `ctx.inject(['systemPrompt'], cb)`」,**准则一直不进
|
|
46
|
-
* prompt**:面板上卡片、开关、原文全都正常(`GET /router/api/ext`
|
|
47
|
-
*
|
|
53
|
+
* prompt**:面板上卡片、开关、原文全都正常(`GET /router/api/ext` 里 controls 一条
|
|
54
|
+
* 不少),但模型看到的 system prompt 里没有它。
|
|
48
55
|
*
|
|
49
56
|
* 根因(本机 cordis 4.0.4 实测):`ctx.inject(deps, cb)` 的回调**不是同步触发** ——
|
|
50
57
|
* provide 之后立刻 `inject`,回调要等到下一个异步点才跑。插件的 `apply` 是**同步
|
|
@@ -76,7 +83,7 @@ export function apply(ctx: Context): void {
|
|
|
76
83
|
const table = currentExts(ctx)
|
|
77
84
|
const store = currentExtStore(ctx)
|
|
78
85
|
|
|
79
|
-
// ① 准则段落。`section()`
|
|
86
|
+
// ① 准则段落。`section()` 注册一个 **provider 函数**作为 text ——
|
|
80
87
|
// 每次装配现算,所以改开关立即生效,无需重注册。
|
|
81
88
|
const dispose = mountPromptSection({
|
|
82
89
|
getSystemPrompt: () => systemPromptOf(ctx),
|
|
@@ -84,13 +91,50 @@ export function apply(ctx: Context): void {
|
|
|
84
91
|
readData: () => store?.readData<PromptExtData>(EXT_PROMPT_ID),
|
|
85
92
|
})
|
|
86
93
|
|
|
87
|
-
// ②
|
|
94
|
+
// ② 段落总控:挂 `system-prompt/assemble` waterfall —— 每轮装配时
|
|
95
|
+
// ⑴ 先把**未过滤的全量段落**喂给清单缓存(面板的事实源,在过滤前取 ⇒
|
|
96
|
+
// 关掉的段仍列在面板上,不会"关了就消失、开不回来");
|
|
97
|
+
// ⑵ 再按落盘的关段表删掉关闭的段(宿主以监听器返回值为权威结果
|
|
98
|
+
// —— `assemble()` 的 `return transformed`,删掉的段不会被恢复)。
|
|
99
|
+
//
|
|
100
|
+
// ⚠️ 总开关关 ⇒ 原样放行:扩展关 = 本扩展什么都不做,宿主段落恢复
|
|
101
|
+
// 自然注入(与准则段"关总开关 ⇒ 段文本空 ⇒ 整段被丢"同一语义)。
|
|
102
|
+
// ⚠️ 监听器注册在**无 scope 标签**的 ctx 上,而派发走
|
|
103
|
+
// `scopeTarget`(dsh-scope):无标签监听器收一切 scope 的装配
|
|
104
|
+
// —— 这正是宿主"全局注册、参与所有 agent 装配"的通道,与准则
|
|
105
|
+
// section 进 prompt 同一条 scope 规则。
|
|
106
|
+
const cache = createSectionCache()
|
|
107
|
+
let disposeAssemble: (() => boolean) | undefined
|
|
108
|
+
let disposeChange: (() => boolean) | undefined
|
|
109
|
+
const sp = systemPromptOf(ctx)
|
|
110
|
+
if (sp) {
|
|
111
|
+
disposeAssemble = ctx.on('system-prompt/assemble', async (assembly, _context, next) => {
|
|
112
|
+
const result = await next()
|
|
113
|
+
cache.update(result.sections)
|
|
114
|
+
if (store?.isEnabled(EXT_PROMPT_ID) !== true) return result
|
|
115
|
+
const off = disabledSectionNames(store.readData<PromptExtData>(EXT_PROMPT_ID))
|
|
116
|
+
if (off.size === 0) return result
|
|
117
|
+
return { ...result, sections: filterSections(result.sections, off) }
|
|
118
|
+
})
|
|
119
|
+
// 段在**没有新装配时**增删(插件开关变化等)⇒ 面板清单不会自己更新,
|
|
120
|
+
// 主动跑一次 assemble 刷新(它会再走上面的 waterfall ⇒ 缓存唯一写入点)。
|
|
121
|
+
disposeChange = ctx.on('system-prompt/change', () => {
|
|
122
|
+
void sp.assemble().catch(() => { /* 预热失败不致命:下次装配自然补上 */ })
|
|
123
|
+
})
|
|
124
|
+
// 预热:一次对话都还没发生时,面板也要有清单(无 scope 的 global 近似)。
|
|
125
|
+
void sp.assemble().catch(() => { /* 同上 */ })
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// ③ 面板登记。幂等:同一个 id 已在表里就不顶掉别人的登记。
|
|
88
129
|
if (table !== undefined && table[EXT_PROMPT_ID] === undefined) {
|
|
89
130
|
table[EXT_PROMPT_ID] = createPromptExt({
|
|
90
131
|
isSystemPromptReady: () => !!systemPromptOf(ctx),
|
|
91
132
|
// ⚠️ **store 必须在这时就能拿到**:拿不到的话面板上的分类开关显示可点、
|
|
92
133
|
// 点了 400 —— 「看起来能用但不能用」比「不显示」更难查。
|
|
93
134
|
store,
|
|
135
|
+
// 段清单来自 waterfall 缓存(若 ② 没挂上 ⇒ 空清单 ⇒ 段组不显示,
|
|
136
|
+
// 但不许这里给缺省 —— 见 createPromptExt 的 deps 注释)。
|
|
137
|
+
getSections: () => cache.list(),
|
|
94
138
|
})
|
|
95
139
|
store?.setEnabled(EXT_PROMPT_ID, true)
|
|
96
140
|
// 核心持有的是**同一个 live 对象**(它 provide 的空表),且 `/ext` 每次请求都
|
|
@@ -99,6 +143,8 @@ export function apply(ctx: Context): void {
|
|
|
99
143
|
|
|
100
144
|
ctx.effect(() => () => {
|
|
101
145
|
dispose()
|
|
146
|
+
disposeAssemble?.()
|
|
147
|
+
disposeChange?.()
|
|
102
148
|
if (table !== undefined) delete table[EXT_PROMPT_ID]
|
|
103
149
|
})
|
|
104
150
|
}
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
*/
|
|
8
8
|
import test from 'node:test'
|
|
9
9
|
import assert from 'node:assert/strict'
|
|
10
|
-
import { mountPromptSection, PROMPT_SECTION_NAME } from './mount.ts'
|
|
10
|
+
import { mountPromptSection, PROMPT_SECTION_NAME, OFFICIAL_LOWEST_SECTION_ORDER } from './mount.ts'
|
|
11
11
|
import { EXT_PROMPT_ID } from './plugin.ts'
|
|
12
12
|
|
|
13
13
|
/** 记录注册内容的假 systemPrompt。 */
|
|
@@ -25,6 +25,11 @@ function fakeSystemPrompt() {
|
|
|
25
25
|
disposals++
|
|
26
26
|
}
|
|
27
27
|
},
|
|
28
|
+
// 本文件测的是注册契约,不碰装配;给最小合法实现满足 `SystemPromptFace`
|
|
29
|
+
//(段清单与过滤的单测在 sections.test.ts / ext-prompt.test.ts)。
|
|
30
|
+
async assemble() {
|
|
31
|
+
return { sections: [] }
|
|
32
|
+
},
|
|
28
33
|
}
|
|
29
34
|
}
|
|
30
35
|
|
|
@@ -35,6 +40,18 @@ test('注册恰好一个段落,名字稳定', () => {
|
|
|
35
40
|
assert.equal(sp.registered[0]!.name, PROMPT_SECTION_NAME)
|
|
36
41
|
})
|
|
37
42
|
|
|
43
|
+
test('★ 段落 order 排在官方最低槽之前(2026-10-21 用户点名:准则放最前)', () => {
|
|
44
|
+
// 判据不写"是最小的"(谁将来再往前挪就误伤),写成**必须低于官方第一方最低槽**:
|
|
45
|
+
// 第一方开场白 `harness:identity` 在 -1000,别的部署可能没关它。
|
|
46
|
+
const sp = fakeSystemPrompt()
|
|
47
|
+
mountPromptSection({ getSystemPrompt: () => sp, isEnabled: () => true, readData: () => undefined })
|
|
48
|
+
const order = sp.registered[0]!.order
|
|
49
|
+
assert.ok(
|
|
50
|
+
order < OFFICIAL_LOWEST_SECTION_ORDER,
|
|
51
|
+
`order=${order} 未低于官方最低槽 ${OFFICIAL_LOWEST_SECTION_ORDER} ⇒ 准则排不到最前`,
|
|
52
|
+
)
|
|
53
|
+
})
|
|
54
|
+
|
|
38
55
|
test('⚠️ text 必须是 provider 函数而不是静态字符串(否则改开关不生效)', () => {
|
|
39
56
|
const sp = fakeSystemPrompt()
|
|
40
57
|
mountPromptSection({ getSystemPrompt: () => sp, isEnabled: () => true, readData: () => undefined })
|