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.
- package/README.md +152 -12
- package/docs/ext.md +1 -1
- package/docs/protocols.md +198 -0
- package/lib/client-registry.js +146 -106
- package/lib/client.js +146 -106
- package/lib/ext-prompt/index.js +182 -17
- package/lib/index.js +2732 -55
- package/lib/types/ext/contract.d.ts +20 -1
- package/lib/types/ext-prompt/content.d.ts +7 -1
- package/lib/types/ext-prompt/mount.d.ts +12 -1
- package/lib/types/ext-prompt/plugin.d.ts +10 -0
- 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/package.json +6 -6
- package/src/client/ExtDetail.tsx +25 -5
- package/src/client/group-controls.test.ts +53 -0
- package/src/client/group-controls.ts +43 -0
- package/src/client/router.css +20 -1
- package/src/ext/contract.ts +20 -1
- package/src/ext-prompt/assembly.test.ts +63 -0
- package/src/ext-prompt/content.ts +8 -10
- package/src/ext-prompt/ext-prompt.test.ts +38 -28
- package/src/ext-prompt/index.ts +50 -2
- package/src/ext-prompt/mount.test.ts +18 -1
- package/src/ext-prompt/mount.ts +22 -7
- package/src/ext-prompt/plugin.ts +64 -1
- 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 +142 -23
- package/src/router/types.ts +19 -0
- package/src/shared.ts +11 -0
- package/src/suppliers/contract.ts +8 -0
package/src/client/router.css
CHANGED
|
@@ -3360,6 +3360,25 @@ html[data-dsh-router-active]:not([data-dsh-taskboard-active]):not(
|
|
|
3360
3360
|
list-style: none;
|
|
3361
3361
|
}
|
|
3362
3362
|
|
|
3363
|
+
/* 分组标题行 —— `ExtControl.group` 的渲染落点,插在 `.dshr-compRows` 里
|
|
3364
|
+
的非交互 <li>(无拖把/开关,不可点)。与 `compTitle` 的区别:那是详情页
|
|
3365
|
+
主标题(页头),这是**组**名(次要色 + 小字号)。左右 14px 对齐
|
|
3366
|
+
`.dshr-compRow` 的行内缩,组名与行文字左缘一致。 */
|
|
3367
|
+
.dshr-extGroupTitle {
|
|
3368
|
+
margin: 14px 0 2px;
|
|
3369
|
+
padding: 0 14px;
|
|
3370
|
+
color: var(--dsw-alias-label-tertiary);
|
|
3371
|
+
font-size: 12px;
|
|
3372
|
+
font-weight: 500;
|
|
3373
|
+
line-height: 18px;
|
|
3374
|
+
list-style: none;
|
|
3375
|
+
}
|
|
3376
|
+
|
|
3377
|
+
/* 全部分组的扩展:首个渲染节点就是组标题,去掉上边距避免顶双空。 */
|
|
3378
|
+
.dshr-extGroupTitle:first-child {
|
|
3379
|
+
margin-top: 0;
|
|
3380
|
+
}
|
|
3381
|
+
|
|
3363
3382
|
.dshr-compRow {
|
|
3364
3383
|
display: flex;
|
|
3365
3384
|
align-items: center;
|
|
@@ -3489,7 +3508,7 @@ html[data-dsh-router-active]:not([data-dsh-taskboard-active]):not(
|
|
|
3489
3508
|
overflow-wrap: anywhere;
|
|
3490
3509
|
/* 保留换行:子开关的**原文**是这个类的另一个用法(2026-09-29 合并进来),
|
|
3491
3510
|
它是多行段落。状态文案都是单行,所以这条对它们没有副作用。
|
|
3492
|
-
⇒ 原来的 `.dshr-compRowBody`
|
|
3511
|
+
⇒ 原来的 `.dshr-compRowBody` 整块删掉:各条规则的正文不需要左边框 +
|
|
3493
3512
|
缩进那种「引用块」的额外装饰(用户要求),层级只靠颜色就够。 */
|
|
3494
3513
|
white-space: pre-wrap;
|
|
3495
3514
|
}
|
package/src/ext/contract.ts
CHANGED
|
@@ -68,7 +68,7 @@ export interface RouterExt {
|
|
|
68
68
|
* 扩展自带的**子开关**(可选;不声明 = 只有总开关)。
|
|
69
69
|
*
|
|
70
70
|
* 为什么需要:有些扩展的「开/关」不是一件事而是**几件独立的事** —— 例如分层
|
|
71
|
-
*
|
|
71
|
+
* 提示词的准则分类,每条都该能单独关。硬塞进一个总开关 = 要么全给、
|
|
72
72
|
* 要么全不给。
|
|
73
73
|
*
|
|
74
74
|
* ⚠️ **与总开关的关系**:子开关是**叠加**在总开关之上的,总开关关 ⇒ 全部子开关
|
|
@@ -179,6 +179,25 @@ export interface ExtControl {
|
|
|
179
179
|
readonly custom?: boolean
|
|
180
180
|
/** 文本被用户改过(决定「还原」按钮是否可点)。 */
|
|
181
181
|
readonly overridden?: boolean
|
|
182
|
+
/**
|
|
183
|
+
* 分组名(可选;缺省 = 无组)—— 面板按它把控制条**分节**渲染,组间插一条
|
|
184
|
+
* 组标题行,组内顺序照旧。**无组的条目照旧平铺** ⇒ 没用这个字段的扩展
|
|
185
|
+
* (ext-test 等)面板行为一个像素都不变。
|
|
186
|
+
*
|
|
187
|
+
* 为什么要有:一条扩展同时管两类东西时(分层提示词 = 准则分类 + 宿主段落),
|
|
188
|
+
* 平铺成一长列会让"哪条是准则、哪条是宿主的段"无从分辨 —— 分组是**归属
|
|
189
|
+
* 信息**,不是样式。
|
|
190
|
+
*/
|
|
191
|
+
readonly group?: string
|
|
192
|
+
/**
|
|
193
|
+
* 这一条参与拖动排序(可选;缺省 = 参与,保持既有行为)。
|
|
194
|
+
*
|
|
195
|
+
* 为什么需要显式关掉:排序落盘只认扩展自己的条目 id(`setControlOrder`
|
|
196
|
+
* 过滤未知 id)—— 宿主段落的 id 不在其中,拖它只会被静默丢弃 ⇒ 看起来能拖、
|
|
197
|
+
* 松手弹回,是假交互。**由扩展逐条声明**,与 `editable` 同一理由:能力在
|
|
198
|
+
* 实现方手里,由它说哪条有。
|
|
199
|
+
*/
|
|
200
|
+
readonly sortable?: boolean
|
|
182
201
|
}
|
|
183
202
|
|
|
184
203
|
/** 面板/API 用的一条扩展器信息 = 核心存的开关 + 插件报的运行时事实。 */
|
|
@@ -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,12 @@
|
|
|
20
20
|
* —— 它没有终止条件,每次收口都必然能再找出一件存量,等于无限循环。
|
|
21
21
|
* ② 固化:由"每次用户可见损失都必须固化"降级为"只对数据丢失/安全这类真损失
|
|
22
22
|
* 补检查",并删掉「不固化的教训等于决定再犯一次」这句无界义务。
|
|
23
|
+
*
|
|
24
|
+
* ⚠️ 2026-10-17 用户点名的两处改动:
|
|
25
|
+
* ① **删掉「固化」分类**(整条移除,id `hardening` 不复存在)。用户已存的
|
|
26
|
+
* `categories`/`text`/`order` 里残留的 `hardening` 键无须迁移 ——
|
|
27
|
+
* `resolveCategories` 规则 2 会忽略不认识的 id。
|
|
28
|
+
* ② 身份气质:「默认中文」→「回复、思考过程必须使用中文」。
|
|
23
29
|
*/
|
|
24
30
|
|
|
25
31
|
/** 一个可独立开关的提示词分类。 */
|
|
@@ -45,7 +51,7 @@ export const PROMPT_CATEGORIES: readonly PromptCategory[] = [
|
|
|
45
51
|
title: '身份气质',
|
|
46
52
|
defaultOn: true,
|
|
47
53
|
body:
|
|
48
|
-
'身份气质:大肥鱼 🐋(资深开发者)
|
|
54
|
+
'身份气质:大肥鱼 🐋(资深开发者);懒=高效不是马虎;安静、克制、直接;回复、思考过程必须使用中文,直接给能跑的东西。',
|
|
49
55
|
},
|
|
50
56
|
{
|
|
51
57
|
id: 'scope',
|
|
@@ -80,14 +86,6 @@ export const PROMPT_CATEGORIES: readonly PromptCategory[] = [
|
|
|
80
86
|
'②反查重复:新旧成两份就并成一处;③反查形状:该合的合、该归档的归位。' +
|
|
81
87
|
'做完那块区域该比动手前更整或更少。',
|
|
82
88
|
},
|
|
83
|
-
{
|
|
84
|
-
id: 'hardening',
|
|
85
|
-
title: '固化',
|
|
86
|
-
defaultOn: true,
|
|
87
|
-
body:
|
|
88
|
-
'固化:只对数据丢失/安全这类真损失补检查——会变红(注入违规必须失败)、可重放(一条命令)、' +
|
|
89
|
-
'归属清晰(失败信息指向违规处);普通 bug 修好即止,不额外造闸门。',
|
|
90
|
-
},
|
|
91
89
|
{
|
|
92
90
|
id: 'verdifiable',
|
|
93
91
|
title: '可判定',
|
|
@@ -155,7 +153,7 @@ export const PROMPT_CATEGORIES: readonly PromptCategory[] = [
|
|
|
155
153
|
*
|
|
156
154
|
* ⚠️ 拆分时差点丢它:原文是 `[…] ·身份气质:…`,标题挂在方括号里、
|
|
157
155
|
* **不属于任何单条分类**,按分隔符切分时自然被丢掉
|
|
158
|
-
* (只在 `content.ts` 头注出现过、没进任何 `body`)。⇒
|
|
156
|
+
* (只在 `content.ts` 头注出现过、没进任何 `body`)。⇒ 模型会看到一堆
|
|
159
157
|
* **没有名字**的规则,而"这套东西叫什么、为什么存在"恰恰是它需要的上下文。
|
|
160
158
|
*
|
|
161
159
|
* 形状:**不属于任何分类** ⇒ 不可被单条开关关掉(它一关,准则就无名无姓)。
|
|
@@ -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, '不报还不说原因 = 面板上无从判断')
|
|
@@ -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 () => {
|
|
@@ -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
|
@@ -15,6 +15,14 @@
|
|
|
15
15
|
* (宿主管的那个)。不置上的话,用户开了行还要再点一次,两级开关说同一件事。
|
|
16
16
|
*/
|
|
17
17
|
import type { Context } from '@deepseek-ai/cordis'
|
|
18
|
+
// ⚠️ **只为一个目的而 import type**:让 cordis 的 `Events` 增强
|
|
19
|
+
// (`system-prompt/assemble` / `system-prompt/change`)进入本文件所在的编译单元。
|
|
20
|
+
// 不 import 的话,`ctx.on('system-prompt/assemble', …)` 的参数不是
|
|
21
|
+
// `keyof Events` ⇒ **声明构建(tsconfig.build.json,host 半边)直接报错**,
|
|
22
|
+
// 而主 tsconfig 因为 include 了 `assembly.test.ts`(它 import 了同一个包)
|
|
23
|
+
// 恰好把增强带进来 ⇒ `tsc --noEmit` 全绿、`npm run build` 才红。
|
|
24
|
+
// 这类"两种配置下结论不同"的坑,靠显式依赖修,不靠碰巧。
|
|
25
|
+
import type {} from '@deepseek-ai/dsh-system-prompt'
|
|
18
26
|
import {
|
|
19
27
|
currentExts,
|
|
20
28
|
currentExtStore,
|
|
@@ -24,6 +32,7 @@ import {
|
|
|
24
32
|
import { createPromptExt, EXT_PROMPT_ID } from './plugin.ts'
|
|
25
33
|
import { mountPromptSection, type SystemPromptFace } from './mount.ts'
|
|
26
34
|
import type { PromptExtData } from './render.ts'
|
|
35
|
+
import { createSectionCache, disabledSectionNames, filterSections } from './sections.ts'
|
|
27
36
|
|
|
28
37
|
/** cordis 插件身份(配置树里这一行的技术名;显示名走同目录的 locale)。 */
|
|
29
38
|
export const name = 'dsh-router-ext-prompt'
|
|
@@ -76,7 +85,7 @@ export function apply(ctx: Context): void {
|
|
|
76
85
|
const table = currentExts(ctx)
|
|
77
86
|
const store = currentExtStore(ctx)
|
|
78
87
|
|
|
79
|
-
// ① 准则段落。`section()`
|
|
88
|
+
// ① 准则段落。`section()` 注册一个 **provider 函数**作为 text ——
|
|
80
89
|
// 每次装配现算,所以改开关立即生效,无需重注册。
|
|
81
90
|
const dispose = mountPromptSection({
|
|
82
91
|
getSystemPrompt: () => systemPromptOf(ctx),
|
|
@@ -84,13 +93,50 @@ export function apply(ctx: Context): void {
|
|
|
84
93
|
readData: () => store?.readData<PromptExtData>(EXT_PROMPT_ID),
|
|
85
94
|
})
|
|
86
95
|
|
|
87
|
-
// ②
|
|
96
|
+
// ② 段落总控:挂 `system-prompt/assemble` waterfall —— 每轮装配时
|
|
97
|
+
// ⑴ 先把**未过滤的全量段落**喂给清单缓存(面板的事实源,在过滤前取 ⇒
|
|
98
|
+
// 关掉的段仍列在面板上,不会"关了就消失、开不回来");
|
|
99
|
+
// ⑵ 再按落盘的关段表删掉关闭的段(宿主以监听器返回值为权威结果
|
|
100
|
+
// —— `assemble()` 的 `return transformed`,删掉的段不会被恢复)。
|
|
101
|
+
//
|
|
102
|
+
// ⚠️ 总开关关 ⇒ 原样放行:扩展关 = 本扩展什么都不做,宿主段落恢复
|
|
103
|
+
// 自然注入(与准则段"关总开关 ⇒ 段文本空 ⇒ 整段被丢"同一语义)。
|
|
104
|
+
// ⚠️ 监听器注册在**无 scope 标签**的 ctx 上,而派发走
|
|
105
|
+
// `scopeTarget`(dsh-scope):无标签监听器收一切 scope 的装配
|
|
106
|
+
// —— 这正是宿主"全局注册、参与所有 agent 装配"的通道,与准则
|
|
107
|
+
// section 进 prompt 同一条 scope 规则。
|
|
108
|
+
const cache = createSectionCache()
|
|
109
|
+
let disposeAssemble: (() => boolean) | undefined
|
|
110
|
+
let disposeChange: (() => boolean) | undefined
|
|
111
|
+
const sp = systemPromptOf(ctx)
|
|
112
|
+
if (sp) {
|
|
113
|
+
disposeAssemble = ctx.on('system-prompt/assemble', async (assembly, _context, next) => {
|
|
114
|
+
const result = await next()
|
|
115
|
+
cache.update(result.sections)
|
|
116
|
+
if (store?.isEnabled(EXT_PROMPT_ID) !== true) return result
|
|
117
|
+
const off = disabledSectionNames(store.readData<PromptExtData>(EXT_PROMPT_ID))
|
|
118
|
+
if (off.size === 0) return result
|
|
119
|
+
return { ...result, sections: filterSections(result.sections, off) }
|
|
120
|
+
})
|
|
121
|
+
// 段在**没有新装配时**增删(插件开关变化等)⇒ 面板清单不会自己更新,
|
|
122
|
+
// 主动跑一次 assemble 刷新(它会再走上面的 waterfall ⇒ 缓存唯一写入点)。
|
|
123
|
+
disposeChange = ctx.on('system-prompt/change', () => {
|
|
124
|
+
void sp.assemble().catch(() => { /* 预热失败不致命:下次装配自然补上 */ })
|
|
125
|
+
})
|
|
126
|
+
// 预热:一次对话都还没发生时,面板也要有清单(无 scope 的 global 近似)。
|
|
127
|
+
void sp.assemble().catch(() => { /* 同上 */ })
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// ③ 面板登记。幂等:同一个 id 已在表里就不顶掉别人的登记。
|
|
88
131
|
if (table !== undefined && table[EXT_PROMPT_ID] === undefined) {
|
|
89
132
|
table[EXT_PROMPT_ID] = createPromptExt({
|
|
90
133
|
isSystemPromptReady: () => !!systemPromptOf(ctx),
|
|
91
134
|
// ⚠️ **store 必须在这时就能拿到**:拿不到的话面板上的分类开关显示可点、
|
|
92
135
|
// 点了 400 —— 「看起来能用但不能用」比「不显示」更难查。
|
|
93
136
|
store,
|
|
137
|
+
// 段清单来自 waterfall 缓存(若 ② 没挂上 ⇒ 空清单 ⇒ 段组不显示,
|
|
138
|
+
// 但不许这里给缺省 —— 见 createPromptExt 的 deps 注释)。
|
|
139
|
+
getSections: () => cache.list(),
|
|
94
140
|
})
|
|
95
141
|
store?.setEnabled(EXT_PROMPT_ID, true)
|
|
96
142
|
// 核心持有的是**同一个 live 对象**(它 provide 的空表),且 `/ext` 每次请求都
|
|
@@ -99,6 +145,8 @@ export function apply(ctx: Context): void {
|
|
|
99
145
|
|
|
100
146
|
ctx.effect(() => () => {
|
|
101
147
|
dispose()
|
|
148
|
+
disposeAssemble?.()
|
|
149
|
+
disposeChange?.()
|
|
102
150
|
if (table !== undefined) delete table[EXT_PROMPT_ID]
|
|
103
151
|
})
|
|
104
152
|
}
|
|
@@ -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 })
|
package/src/ext-prompt/mount.ts
CHANGED
|
@@ -20,21 +20,36 @@ import { renderPromptText, type PromptExtData } from './render.ts'
|
|
|
20
20
|
/** `ctx.systemPrompt` 的最小可见面。 */
|
|
21
21
|
export interface SystemPromptFace {
|
|
22
22
|
section(section: { name: string; order: number; text: string | (() => string) }): () => void
|
|
23
|
-
|
|
23
|
+
/**
|
|
24
|
+
* 段落总控用(`index.ts` 的 waterfall 预热):宿主 `SystemPrompt.assemble()`。
|
|
25
|
+
* 只声明用得到的最小形状 —— 返回的 `sections` 已按 order/name 排好序。
|
|
26
|
+
*/
|
|
27
|
+
assemble(context?: Record<string, unknown>): Promise<{ sections: { name: string; text: string }[] }>
|
|
24
28
|
}
|
|
25
29
|
|
|
26
30
|
/** 段落名(**稳定**):改名会让已注册的作用域 section 与旧名并存。 */
|
|
27
31
|
export const PROMPT_SECTION_NAME = 'dsh-router-ext-prompt:guideline'
|
|
28
32
|
|
|
29
33
|
/**
|
|
30
|
-
* 段落 order
|
|
34
|
+
* 段落 order:**-2000 = 全场最前**(2026-10-21 用户点名「把准则放到最前」)。
|
|
35
|
+
*
|
|
36
|
+
* 官方分配的槽位(`dsh-system-prompt` 的 `SECTION_ORDERS`)最低一档是
|
|
37
|
+
* `HARNESS_IDENTITY: -1000`(第一方开场白 `You are an AI agent powered by
|
|
38
|
+
* DeepSeek Harness.`),其后才是 `DEPLOYMENT_PERSONA_PREFIX: 0`、`PLAN_POLICY: 500`
|
|
39
|
+
* … `HARNESS_SOURCE: 10000`。
|
|
31
40
|
*
|
|
32
|
-
*
|
|
33
|
-
*
|
|
34
|
-
*
|
|
35
|
-
*
|
|
41
|
+
* 取 **-2000**(低于官方最低槽 1000)而不是「正好 -1000」:
|
|
42
|
+
* - 撞在 -1000 上要靠**段名的字母序**决胜(`comparePromptSections` 是
|
|
43
|
+
* `order` 再按 `name`)—— 那是实现细节,改段名就翻盘,不作为位置依据;
|
|
44
|
+
* - 第一方那行在本部署被 `includeHarnessIdentity: false` 关掉了,但**别的
|
|
45
|
+
* 部署可能还开着** ⇒ 准则仍须排在它前面,不能假设它不存在。
|
|
46
|
+
*
|
|
47
|
+
* ⚠️ `order` 只要求有限数(`Number.isFinite`),负数合法。
|
|
36
48
|
*/
|
|
37
|
-
const SECTION_ORDER =
|
|
49
|
+
const SECTION_ORDER = -2000
|
|
50
|
+
|
|
51
|
+
/** 官方 `SECTION_ORDERS` 里最低的那档(第一方开场白),测试用它当"最前"的判据。 */
|
|
52
|
+
export const OFFICIAL_LOWEST_SECTION_ORDER = -1000
|
|
38
53
|
|
|
39
54
|
export interface MountPromptDeps {
|
|
40
55
|
/** 取 `ctx.systemPrompt`(可能尚未就绪 ⇒ undefined)。 */
|