dsh-plugin-tlrp 0.0.0-stage → 0.4.0
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/LICENSE +21 -0
- package/README.md +218 -2
- package/client.js +2079 -0
- package/cordis.patch.yml +18 -0
- package/icon.svg +18 -0
- package/index.cjs +37 -0
- package/index.js +34 -0
- package/lib/core.cjs +2636 -0
- package/lib/memory-bridge.cjs +265 -0
- package/lib/resolve.cjs +74 -0
- package/lib/schema.cjs +192 -0
- package/locale/en.json +20 -0
- package/locale/zh.json +20 -0
- package/package.json +73 -3
- package/scripts/apply-check.mjs +400 -0
- package/scripts/client-check.mjs +502 -0
- package/scripts/install-into-profile.mjs +167 -0
- package/scripts/mutate-check.mjs +284 -0
- package/scripts/rewrite-persona.mjs +106 -0
- package/test/core.test.mjs +270 -0
- package/test/memory-bridge.test.mjs +77 -0
- package/test/style.test.mjs +336 -0
|
@@ -0,0 +1,400 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* tlrp:apply() 端到端集成检查(真 SystemPrompt + 真 Config + 真插件代码)。
|
|
3
|
+
*
|
|
4
|
+
* 覆盖:
|
|
5
|
+
* 1. 装配:变量 / 核心 section / 尾部 context / 工具 guard / 6 条指令 / pre-step 监听器;
|
|
6
|
+
* 2. 组装:`SystemPrompt.assemble()` + `renderPrompt()` 不抛错(unknown prompt variable = 0),
|
|
7
|
+
* 渲染文本里确实有 tlrp section、角色块、模式准则、当前时间;
|
|
8
|
+
* 3. 双态:auto 闲聊→RP、工程词→WORK,`{{tlrp_directive}}` 随之切换;
|
|
9
|
+
* 4. 拦截:RP 态拒绝 ops_bash、放行 tlmemory_save;WORK 态不拦;关闭态不拦;
|
|
10
|
+
* 5. 指令:/rp 剥离前缀并改写消息、/char 与 /scene 写入 mutate、/tlrp status 汇报;
|
|
11
|
+
* 6. 触发:命中「橡木蛋糕卷」后 context 与 directive 里都出现注入设定;
|
|
12
|
+
* 7. 多角色:`activeCharacterId: sam_tactical` 的独立装配渲染出萨姆人格。
|
|
13
|
+
*
|
|
14
|
+
* 运行:
|
|
15
|
+
* cd ~/.dsh/profiles/desktop
|
|
16
|
+
* node D:/Code/my-dsh-plugins/packages/tlrp/scripts/apply-check.mjs
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
import { createRequire } from 'node:module'
|
|
20
|
+
|
|
21
|
+
const require = createRequire(process.env.TLRP_PROFILE_BASE || `${process.cwd()}/package.json`)
|
|
22
|
+
|
|
23
|
+
const { SystemPrompt, renderPrompt, renderContextSections } = require('@deepseek-ai/dsh-system-prompt')
|
|
24
|
+
const plugin = require('dsh-plugin-tlrp')
|
|
25
|
+
|
|
26
|
+
const failures = []
|
|
27
|
+
const check = (label, ok, detail) => {
|
|
28
|
+
if (ok) console.log(`PASS ${label}${detail ? ` — ${detail}` : ''}`)
|
|
29
|
+
else {
|
|
30
|
+
console.log(`FAIL ${label}${detail ? ` — ${detail}` : ''}`)
|
|
31
|
+
failures.push(label)
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
const noop = () => {}
|
|
35
|
+
|
|
36
|
+
if (typeof plugin.apply !== 'function' || !plugin.Config) {
|
|
37
|
+
console.error('FAIL: dsh-plugin-tlrp 需要同时导出 apply 与 Config')
|
|
38
|
+
process.exit(2)
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* 搭一个最小宿主:只实现插件与 systemPrompt 真正调用到的成员。
|
|
43
|
+
* 关键点是 effect() 必须把 ScopedLayers 传进来的 generator 驱动到第一次 yield,
|
|
44
|
+
* 否则注册根本不会发生(真实 Cordis 就是这么做的)。
|
|
45
|
+
*/
|
|
46
|
+
function createHarness(configOverrides = {}, options = {}) {
|
|
47
|
+
const services = {}
|
|
48
|
+
const handlers = new Map()
|
|
49
|
+
const mutations = []
|
|
50
|
+
const followed = []
|
|
51
|
+
|
|
52
|
+
const ctx = {
|
|
53
|
+
logger: () => ({ info: noop, warn: console.warn, error: console.error, debug: noop }),
|
|
54
|
+
reflect: { provide: noop },
|
|
55
|
+
emit: noop,
|
|
56
|
+
on(event, listener) {
|
|
57
|
+
handlers.set(event, listener)
|
|
58
|
+
return noop
|
|
59
|
+
},
|
|
60
|
+
effect(fn) {
|
|
61
|
+
const result = typeof fn === 'function' ? fn() : undefined
|
|
62
|
+
if (result && typeof result.next === 'function') {
|
|
63
|
+
result.next()
|
|
64
|
+
return () => {
|
|
65
|
+
try {
|
|
66
|
+
result.return?.()
|
|
67
|
+
} catch {
|
|
68
|
+
/* 忽略 */
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
return typeof result === 'function' ? result : noop
|
|
73
|
+
},
|
|
74
|
+
inject(deps, callback) {
|
|
75
|
+
if (deps.every((dep) => services[dep] !== undefined)) callback(ctx)
|
|
76
|
+
},
|
|
77
|
+
async waterfall(_carrier, _event, _payload, _context, fallback) {
|
|
78
|
+
return fallback()
|
|
79
|
+
},
|
|
80
|
+
get systemPrompt() {
|
|
81
|
+
return services.systemPrompt
|
|
82
|
+
},
|
|
83
|
+
get tools() {
|
|
84
|
+
return services.tools
|
|
85
|
+
},
|
|
86
|
+
get commands() {
|
|
87
|
+
return services.commands
|
|
88
|
+
},
|
|
89
|
+
get agents() {
|
|
90
|
+
return services.agents
|
|
91
|
+
},
|
|
92
|
+
get settings() {
|
|
93
|
+
return services.settings
|
|
94
|
+
},
|
|
95
|
+
get sessionProjections() {
|
|
96
|
+
return services.sessionProjections
|
|
97
|
+
},
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
services.systemPrompt = new SystemPrompt(ctx, { includeHarnessIdentity: false, includeRuntimeContext: true })
|
|
101
|
+
services.tools = {
|
|
102
|
+
guards: [],
|
|
103
|
+
guard(fn) {
|
|
104
|
+
this.guards.push(fn)
|
|
105
|
+
return noop
|
|
106
|
+
},
|
|
107
|
+
}
|
|
108
|
+
services.commands = {
|
|
109
|
+
defs: [],
|
|
110
|
+
register(definition) {
|
|
111
|
+
this.defs.push(definition)
|
|
112
|
+
return noop
|
|
113
|
+
},
|
|
114
|
+
}
|
|
115
|
+
services.agents = {}
|
|
116
|
+
services.sessionProjections = { stateOf: () => undefined }
|
|
117
|
+
/**
|
|
118
|
+
* 真实宿主里 bundle 行会被包进 include,设置命名空间因此是 `include:tlrp`
|
|
119
|
+
* 而不是配置里写的 `tlrp`。这里按这个现实建模:只认 include:tlrp,写 tlrp
|
|
120
|
+
* 直接报「没有这个可配置条目」——用来验证插件的命名空间解析。
|
|
121
|
+
*/
|
|
122
|
+
services.settings = {
|
|
123
|
+
describe() {
|
|
124
|
+
return [
|
|
125
|
+
{
|
|
126
|
+
ns: 'include:tlrp',
|
|
127
|
+
value: { mode: 'auto', activeCharacterId: 'firefly', customSceneOverride: '', characters: [], triggers: [] },
|
|
128
|
+
},
|
|
129
|
+
{ ns: 'include:tool-bash', value: { publishBashTool: true } },
|
|
130
|
+
]
|
|
131
|
+
},
|
|
132
|
+
async mutate(ns, ops, expectedRevision) {
|
|
133
|
+
if (ns !== 'include:tlrp') throw new Error(`No configurable plugin entry "${ns}"`)
|
|
134
|
+
mutations.push({ ns, ops, expectedRevision })
|
|
135
|
+
return undefined
|
|
136
|
+
},
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
const agent = {
|
|
140
|
+
session: { seq: 0, eventAt: () => undefined },
|
|
141
|
+
followup(input) {
|
|
142
|
+
followed.push(input)
|
|
143
|
+
},
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// 模拟 prompt-clock 先注册 now/cwd 的部署:tlrp 必须让位而不是抛 duplicate
|
|
147
|
+
if (options.preRegistered) {
|
|
148
|
+
for (const [name, value] of Object.entries(options.preRegistered)) {
|
|
149
|
+
services.systemPrompt.variable(name, () => value)
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
plugin.apply(ctx, plugin.Config(configOverrides))
|
|
154
|
+
const userMessage = (text) => ({
|
|
155
|
+
id: `m-${Math.random().toString(36).slice(2)}`,
|
|
156
|
+
role: 'user',
|
|
157
|
+
content: [{ type: 'text', text }],
|
|
158
|
+
source: { kind: 'user' },
|
|
159
|
+
})
|
|
160
|
+
|
|
161
|
+
async function run(text) {
|
|
162
|
+
const preStep = handlers.get('agent/pre-step')
|
|
163
|
+
const decision = await preStep({ agent, turn: 1, step: 1, signal: { aborted: false } }, async () => ({
|
|
164
|
+
kind: 'enter',
|
|
165
|
+
messages: [userMessage(text)],
|
|
166
|
+
}))
|
|
167
|
+
const assembly = await services.systemPrompt.assemble({ agent })
|
|
168
|
+
return {
|
|
169
|
+
decision,
|
|
170
|
+
prompt: renderPrompt(assembly),
|
|
171
|
+
contexts: renderContextSections(assembly)
|
|
172
|
+
.map((entry) => entry.text)
|
|
173
|
+
.join('\n'),
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
return {
|
|
178
|
+
ctx,
|
|
179
|
+
services,
|
|
180
|
+
handlers,
|
|
181
|
+
mutations,
|
|
182
|
+
followed,
|
|
183
|
+
agent,
|
|
184
|
+
run,
|
|
185
|
+
guard: (name) => services.tools.guards[0]({ name, agent }),
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/* ── 1. 装配注册 ── */
|
|
190
|
+
const h = createHarness({ mode: 'auto' })
|
|
191
|
+
check('注册了 pre-step 监听器', typeof h.handlers.get('agent/pre-step') === 'function')
|
|
192
|
+
check('注册了内置指令 + 形态别名', ['auto','mode','char','scene','title','trigger','tlrp'].every((name) => h.services.commands.defs.some((d) => d.name === name)), h.services.commands.defs.map((d) => `/${d.name}`).join(' '))
|
|
193
|
+
check(
|
|
194
|
+
'v0.2.0 新命令 /title 与 /trigger 在位',
|
|
195
|
+
['title', 'trigger'].every((name) => h.services.commands.defs.some((definition) => definition.name === name)),
|
|
196
|
+
h.services.commands.defs.map((d) => `/${d.name}`).join(' '),
|
|
197
|
+
)
|
|
198
|
+
check('注册了工具 guard', h.services.tools.guards.length === 1)
|
|
199
|
+
|
|
200
|
+
/* ── 2. 组装 + 渲染 ── */
|
|
201
|
+
const plain = await h.run('今天有点想你了')
|
|
202
|
+
check('renderPrompt 不抛错(unknown prompt variable = 0)', plain.prompt.length > 0, `${plain.prompt.length} 字符`)
|
|
203
|
+
check('核心 section 已装配', plain.prompt.includes('tlrp 角色与形态装配'))
|
|
204
|
+
check('角色块已装配', plain.prompt.includes('流萤(Firefly)') && plain.prompt.includes('绝对身份锁'))
|
|
205
|
+
check('时间变量已解析', /20\d\d-\d\d-\d\d .+(Asia\/Shanghai)/.test(plain.prompt), plain.prompt.match(/当前时间:[^\n]+/)?.[0])
|
|
206
|
+
check('auto 闲聊判 RP', plain.prompt.includes('本轮运行模式:少女陪伴态(rp)'))
|
|
207
|
+
check('RP 准则含斜体与禁清单红线', plain.prompt.includes('斜体') && plain.prompt.includes('严禁助理式项目符号清单'))
|
|
208
|
+
check('尾部 context 已生成', plain.contexts.includes('tlrp 运行时状态') && plain.contexts.includes('运行模式:少女陪伴态(rp)'))
|
|
209
|
+
|
|
210
|
+
const work = await h.run('看下 nginx 报错和端口占用')
|
|
211
|
+
check('auto 工程词判 WORK', work.prompt.includes('本轮运行模式:战术工作态(work)'), work.prompt.match(/本轮运行模式:[^\n]+/)?.[0])
|
|
212
|
+
check('WORK 准则含七节纪律', work.prompt.includes('七、说完就停'))
|
|
213
|
+
check('WORK 尾部 context 声明不写面板', work.contexts.includes('状态面板:本轮关闭'))
|
|
214
|
+
|
|
215
|
+
/* ── 3. 工具拦截 ── */
|
|
216
|
+
check('WORK 态放行 ops_bash', h.guard('ops_bash') === undefined)
|
|
217
|
+
await h.run('/rp 我们聊会儿天')
|
|
218
|
+
const denial = h.guard('ops_bash')
|
|
219
|
+
check('RP 态拦截 ops_bash', typeof denial === 'string' && denial.includes('不在工具白名单内'), typeof denial === 'string' ? denial.split('\n')[0] : String(denial))
|
|
220
|
+
check('RP 态放行 tlmemory_save', h.guard('tlmemory_save') === undefined, '记忆类工具白名单')
|
|
221
|
+
check('RP 态放行 ask_user_question', h.guard('ask_user_question') === undefined)
|
|
222
|
+
|
|
223
|
+
/* ── 4. 指令 ── */
|
|
224
|
+
const rpRun = await h.run('/rp 陪我坐一会儿')
|
|
225
|
+
check('/rp 剥离指令前缀', rpRun.decision?.messages?.[0]?.content?.[0]?.text === '陪我坐一会儿', JSON.stringify(rpRun.decision?.messages?.[0]?.content?.[0]?.text))
|
|
226
|
+
check(
|
|
227
|
+
'/rp 写入 settings.mutate(解析到被服务的 include:tlrp)',
|
|
228
|
+
h.mutations.some((entry) => entry.ns === 'include:tlrp' && entry.ops?.some((op) => op.path?.[0] === 'mode' && op.value === 'rp')),
|
|
229
|
+
JSON.stringify(h.mutations.at(-1)),
|
|
230
|
+
)
|
|
231
|
+
|
|
232
|
+
await h.run('/char sam_tactical')
|
|
233
|
+
check(
|
|
234
|
+
'/char 写入 activeCharacterId',
|
|
235
|
+
h.mutations.some((entry) => entry.ops?.some((op) => op.path?.[0] === 'activeCharacterId' && op.value === 'sam_tactical')),
|
|
236
|
+
JSON.stringify(h.mutations.at(-1)),
|
|
237
|
+
)
|
|
238
|
+
|
|
239
|
+
const sceneRun = await h.run('/scene 现实宇宙·星舰医疗舱外')
|
|
240
|
+
check('/scene 覆盖情景', sceneRun.prompt.includes('星舰医疗舱外') && sceneRun.contexts.includes('星舰医疗舱外'))
|
|
241
|
+
check(
|
|
242
|
+
'/scene 写入 customSceneOverride',
|
|
243
|
+
h.mutations.some((entry) => entry.ops?.some((op) => op.path?.[0] === 'customSceneOverride' && op.value === '现实宇宙·星舰医疗舱外')),
|
|
244
|
+
)
|
|
245
|
+
|
|
246
|
+
const titleHandler = h.services.commands.defs.find((definition) => definition.name === 'title')?.handler
|
|
247
|
+
const titled = await titleHandler({ agent: h.agent, rawInput: '站长' })
|
|
248
|
+
check(
|
|
249
|
+
'/title 写入 characters[].userTitle',
|
|
250
|
+
titled?.kind === 'success' &&
|
|
251
|
+
h.mutations.some((entry) => entry.ops?.some((op) => op.path?.[0] === 'characters' && op.value?.[0]?.userTitle === '站长')),
|
|
252
|
+
titled?.text?.split('\n')[0],
|
|
253
|
+
)
|
|
254
|
+
|
|
255
|
+
const triggerHandler = h.services.commands.defs.find((definition) => definition.name === 'trigger')?.handler
|
|
256
|
+
const added = await triggerHandler({ agent: h.agent, rawInput: 'add 烤焦的肉,烤肉=那天大家都吃了焦炭味的肉干' })
|
|
257
|
+
check(
|
|
258
|
+
'/trigger add 写入 triggers',
|
|
259
|
+
added?.kind === 'success' &&
|
|
260
|
+
h.mutations.some((entry) =>
|
|
261
|
+
entry.ops?.some((op) => op.path?.[0] === 'triggers' && op.value?.some?.((item) => item.keywords?.includes('烤肉'))),
|
|
262
|
+
),
|
|
263
|
+
added?.text?.split('\n')[0],
|
|
264
|
+
)
|
|
265
|
+
|
|
266
|
+
const statusHandler = h.services.commands.defs.find((definition) => definition.name === 'tlrp')?.handler
|
|
267
|
+
const status = await statusHandler({ agent: h.agent, rawInput: 'status' })
|
|
268
|
+
check('/tlrp status 返回汇总', status?.kind === 'success' && status.text.includes('已注册形态'), status?.text?.split('\n')[1])
|
|
269
|
+
|
|
270
|
+
/* ── 4b. 风格层开关(/style) ── */
|
|
271
|
+
const styleHandler = h.services.commands.defs.find((definition) => definition.name === 'style')?.handler
|
|
272
|
+
const styleIdle = await styleHandler({ agent: h.agent, rawInput: '' })
|
|
273
|
+
check('/style 无参汇报开关状态', styleIdle?.kind === 'success' && styleIdle.text.includes('风格层'), styleIdle?.text?.split('\n')[0])
|
|
274
|
+
const styleOn = await styleHandler({ agent: h.agent, rawInput: 'on' })
|
|
275
|
+
check('/style on 打开会话级风格层', styleOn?.kind === 'success' && styleOn.text.includes('只带语气'), styleOn?.text)
|
|
276
|
+
const styled = await h.run('这一版跑得怎么样')
|
|
277
|
+
check('风格层打开后尾部 context 声明来源', styled.contexts.includes('风格层:开'), styled.contexts.match(/- 风格层:[^\n]+/)?.[0])
|
|
278
|
+
check('风格层打开后准则里出现风格块', styled.prompt.includes('### 风格层(本轮生效'))
|
|
279
|
+
const styleDialogRp = await styleHandler({ agent: h.agent, rawInput: 'dialog' })
|
|
280
|
+
check('/style dialog 在陪伴态生效', styleDialogRp?.kind === 'success' && styleDialogRp.text.includes('对话体'), styleDialogRp?.text)
|
|
281
|
+
const rpDialogRun = await h.run('陪我坐一会儿')
|
|
282
|
+
check('陪伴态对话体取消斜体动作要求', !rpDialogRun.prompt.includes('自然穿插 *斜体包裹的微动作'), rpDialogRun.prompt.includes('本轮是对话体') ? '已改为只说话' : '未命中')
|
|
283
|
+
check('陪伴态对话体红线改口径', rpDialogRun.contexts.includes('格式红线(陪伴族 · 对话体)'))
|
|
284
|
+
await styleHandler({ agent: h.agent, rawInput: 'on' })
|
|
285
|
+
const workStyle = createHarness({ mode: 'work' })
|
|
286
|
+
const workStyleHandler = workStyle.services.commands.defs.find((definition) => definition.name === 'style')?.handler
|
|
287
|
+
await workStyleHandler({ agent: workStyle.agent, rawInput: 'on' })
|
|
288
|
+
const workStyleRun = await workStyle.run('这一版跑得怎么样')
|
|
289
|
+
check('WORK 形态下风格块补齐语言风格', workStyleRun.prompt.includes('#### 语言风格与禁用项(照此执行)'))
|
|
290
|
+
const styleDialog = await workStyleHandler({ agent: workStyle.agent, rawInput: 'dialog' })
|
|
291
|
+
check('/style dialog 切到对话体', styleDialog?.kind === 'success' && styleDialog.text.includes('对话体'), styleDialog?.text)
|
|
292
|
+
const dialogRun = await workStyle.run('这一版跑得怎么样')
|
|
293
|
+
check(
|
|
294
|
+
'对话体在状态块里写明只说话',
|
|
295
|
+
dialogRun.contexts.includes('对话体:只说话,不写旁白'),
|
|
296
|
+
dialogRun.contexts.includes('对话体') ? 'context 已声明对话体' : String(dialogRun.contexts).slice(0, 80),
|
|
297
|
+
)
|
|
298
|
+
check('对话体标题替换默认标题', dialogRun.prompt.includes('### 风格层(本轮生效:对话体,只说话不演)'))
|
|
299
|
+
check('对话体给出表演强度红线', dialogRun.prompt.includes('#### 本轮的表演强度'))
|
|
300
|
+
check('对话体禁止旁白与环境描写', dialogRun.prompt.includes('禁止环境与天气描写'))
|
|
301
|
+
await workStyleHandler({ agent: workStyle.agent, rawInput: 'default' })
|
|
302
|
+
const afterDialog = await workStyle.run('这一版跑得怎么样')
|
|
303
|
+
check('交回后不再要求对话体', !afterDialog.prompt.includes('#### 本轮的表演强度'))
|
|
304
|
+
const styleOff = await styleHandler({ agent: h.agent, rawInput: 'off' })
|
|
305
|
+
check('/style off 关闭会话级风格层', styleOff?.kind === 'success' && styleOff.text.includes('「关」'), styleOff?.text)
|
|
306
|
+
const withoutStyle = await h.run('这一版跑得怎么样')
|
|
307
|
+
check('关闭后不再注入风格块', !withoutStyle.prompt.includes('### 风格层(本轮生效'))
|
|
308
|
+
const styleBack = await styleHandler({ agent: h.agent, rawInput: 'default' })
|
|
309
|
+
check('/style default 交回形态与全局', styleBack?.kind === 'success' && styleBack.text.includes('交回'), styleBack?.text)
|
|
310
|
+
const styleBad = await styleHandler({ agent: h.agent, rawInput: 'maybe' })
|
|
311
|
+
check('/style 非法参数给用法', styleBad?.kind === 'error' && styleBad.text.includes('用法'), styleBad?.text)
|
|
312
|
+
|
|
313
|
+
/* ── 5. 触发映射 ── */
|
|
314
|
+
const triggered = await h.run('好想吃橡木蛋糕卷啊')
|
|
315
|
+
check('命中橡木蛋糕卷触发器', triggered.contexts.includes('两万信用点'), triggered.contexts.match(/\[oak-cake\][^\n]{0,40}/)?.[0])
|
|
316
|
+
check('注入内容进入 {{tlrp_directive}}', triggered.prompt.includes('橡木蛋糕卷') && triggered.prompt.includes('两万信用点'))
|
|
317
|
+
|
|
318
|
+
/* ── 6. 多角色切换 ── */
|
|
319
|
+
const sam = createHarness({ activeCharacterId: 'sam_tactical' })
|
|
320
|
+
const samRun = await sam.run('报一下状态')
|
|
321
|
+
check('sam_tactical 渲染独立人格', samRun.prompt.includes('萨姆(SAM · 纯战术态)'), samRun.prompt.match(/角色设定(当前激活:[^\n]+/)?.[0])
|
|
322
|
+
check('sam_tactical 的语言风格生效', samRun.prompt.includes('极度简练'))
|
|
323
|
+
check('sam_tactical 无状态面板模板', !samRun.prompt.includes('底部状态同步面板模板'))
|
|
324
|
+
|
|
325
|
+
/* ── 7. 与 prompt-clock 共存:now/cwd 已被占用时 tlrp 让位 ── */
|
|
326
|
+
const coexisting = createHarness({ mode: 'auto' }, { preRegistered: { now: 'SENTINEL-NOW', cwd: 'SENTINEL-CWD' } })
|
|
327
|
+
const coexistRun = await coexisting.run('今天有点想你了')
|
|
328
|
+
check('now/cwd 已被占用时 apply 不崩溃', coexistRun.prompt.length > 0, `${coexistRun.prompt.length} 字符`)
|
|
329
|
+
check('让位后沿用既有 provider', coexistRun.prompt.includes('SENTINEL-NOW') && coexistRun.prompt.includes('SENTINEL-CWD'))
|
|
330
|
+
check('让位后 tlrp 自身的变量仍工作', coexistRun.prompt.includes('流萤(Firefly)') && coexistRun.prompt.includes('少女陪伴态(rp)'))
|
|
331
|
+
|
|
332
|
+
/* ── 8. 关闭态 ── */
|
|
333
|
+
const off = createHarness({ enabled: false })
|
|
334
|
+
const offRun = await off.run('看下 docker 日志')
|
|
335
|
+
check('关闭态准则中性', offRun.prompt.includes('tlrp:未启用'))
|
|
336
|
+
check('关闭态不拦截工具', off.guard('ops_bash') === undefined)
|
|
337
|
+
check('关闭态 context 声明忽略', offRun.contexts.includes('插件已关闭'))
|
|
338
|
+
|
|
339
|
+
/* ── 9. v0.3.0:自定义第 5 个形态 + 动态指令切换 ── */
|
|
340
|
+
const debugMode = {
|
|
341
|
+
id: 'debug',
|
|
342
|
+
name: '排障态',
|
|
343
|
+
commandAliases: ['/debug', '【排障】'],
|
|
344
|
+
autoKeywords: '堆栈|崩溃|内存泄漏',
|
|
345
|
+
autoPriority: 99,
|
|
346
|
+
directivePrompt: '## 本轮运行模式:排障态(debug)\n- 只输出根因与最小修复步骤,不写抒情段落。',
|
|
347
|
+
toolPolicy: 'block_all',
|
|
348
|
+
allowedTools: '',
|
|
349
|
+
injectCharacterPersona: false,
|
|
350
|
+
enableStatusFooter: false,
|
|
351
|
+
statusFooterOverride: '',
|
|
352
|
+
bindCharacterId: '',
|
|
353
|
+
}
|
|
354
|
+
const fiveModes = [...plugin.__internals.DEFAULT_MODES, debugMode]
|
|
355
|
+
|
|
356
|
+
const custom = createHarness({ mode: 'auto', modes: fiveModes })
|
|
357
|
+
check(
|
|
358
|
+
'自定义第 5 个形态注册了斜杠指令',
|
|
359
|
+
custom.services.commands.defs.some((definition) => definition.name === 'debug'),
|
|
360
|
+
custom.services.commands.defs.map((d) => `/${d.name}`).join(' '),
|
|
361
|
+
)
|
|
362
|
+
|
|
363
|
+
const debugRun = await custom.run('帮忙看下这个崩溃堆栈和内存泄漏')
|
|
364
|
+
check('自定义形态被 auto 关键词命中', debugRun.prompt.includes('排障态'), debugRun.prompt.match(/本轮运行模式:[^\n]+/)?.[0])
|
|
365
|
+
check('自定义形态专属准则生效', debugRun.prompt.includes('只输出根因与最小修复步骤'))
|
|
366
|
+
check('自定义形态精简注入人设', !debugRun.prompt.includes('外貌与感官细节'))
|
|
367
|
+
check('自定义形态不输出状态面板', debugRun.contexts.includes('状态面板:本轮关闭'), (debugRun.contexts.match(/- 状态面板:.*/) ?? [])[0])
|
|
368
|
+
const debugDenial = custom.guard('ops_bash')
|
|
369
|
+
check('自定义形态 toolPolicy=block_all 拦截工具', typeof debugDenial === 'string', String(debugDenial).split('\n')[0])
|
|
370
|
+
|
|
371
|
+
const switcher = createHarness({ mode: 'auto', modes: fiveModes })
|
|
372
|
+
const byModeCommand = await switcher.run('/mode sam 开始吧')
|
|
373
|
+
check(
|
|
374
|
+
'/mode <id> 写入 mode',
|
|
375
|
+
switcher.mutations.some((entry) => entry.ops?.some((op) => op.path?.[0] === 'mode' && op.value === 'sam')),
|
|
376
|
+
JSON.stringify(switcher.mutations.at(-1)),
|
|
377
|
+
)
|
|
378
|
+
check('/mode <id> 剥离正文', byModeCommand.decision?.messages?.[0]?.content?.[0]?.text === '开始吧', JSON.stringify(byModeCommand.decision?.messages?.[0]?.content?.[0]?.text))
|
|
379
|
+
|
|
380
|
+
const byAlias = await switcher.run('【排障】先别抒情')
|
|
381
|
+
check(
|
|
382
|
+
'自定义括号别名切形态',
|
|
383
|
+
switcher.mutations.some((entry) => entry.ops?.some((op) => op.path?.[0] === 'mode' && op.value === 'debug')),
|
|
384
|
+
JSON.stringify(switcher.mutations.at(-1)),
|
|
385
|
+
)
|
|
386
|
+
check('自定义括号别名剥离标记', byAlias.decision?.messages?.[0]?.content?.[0]?.text === '先别抒情', JSON.stringify(byAlias.decision?.messages?.[0]?.content?.[0]?.text))
|
|
387
|
+
|
|
388
|
+
const slashAlias = await switcher.run('/debug 只给结论')
|
|
389
|
+
check(
|
|
390
|
+
'自定义斜杠别名切形态',
|
|
391
|
+
switcher.mutations.some((entry) => entry.ops?.some((op) => op.path?.[0] === 'mode' && op.value === 'debug')),
|
|
392
|
+
JSON.stringify(switcher.mutations.at(-1)),
|
|
393
|
+
)
|
|
394
|
+
|
|
395
|
+
console.log('')
|
|
396
|
+
if (failures.length > 0) {
|
|
397
|
+
console.error(`RESULT: FAIL (${failures.length}) → ${failures.join(', ')}`)
|
|
398
|
+
process.exit(1)
|
|
399
|
+
}
|
|
400
|
+
console.log('RESULT: ALL PASS')
|