dsh-structured 0.7.4 → 0.8.2
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/CHANGELOG.md +18 -0
- package/README.md +10 -4
- package/cordis.patch.yml +2 -0
- package/docs/ACCEPTANCE-0.8.1.md +3 -0
- package/docs/ACCEPTANCE-0.8.2.md +3 -0
- package/docs/CARD-SYSTEM.md +1 -0
- package/docs/METRICS.md +2 -1
- package/docs/TROUBLESHOOT-VISUAL.md +78 -78
- package/docs/VISUAL-ACCEPTANCE.md +4 -4
- package/lib/client.js +153 -110
- package/lib/index.js +34 -12
- package/package.json +12 -2
- package/scripts/desktop-check.mjs +6 -2
- package/src/client.js +153 -110
- package/src/index.js +34 -12
- package/test/client-cards.test.mjs +140 -0
- package/test/client-visual.test.mjs +163 -0
- package/test/structured.test.mjs +37 -2
package/src/client.js
CHANGED
|
@@ -24,15 +24,15 @@ window.__ModuleLoader__.load({
|
|
|
24
24
|
const STYLE_ID = 'dsh-structured-skin-style'
|
|
25
25
|
const LEGACY_STYLE_ID = 'dsh-structured-card-style'
|
|
26
26
|
|
|
27
|
-
const CARD_TYPES = [
|
|
28
|
-
[
|
|
29
|
-
[
|
|
30
|
-
[
|
|
31
|
-
[
|
|
32
|
-
[
|
|
33
|
-
[
|
|
34
|
-
[
|
|
35
|
-
]
|
|
27
|
+
const CARD_TYPES = new Map([
|
|
28
|
+
['结论', 'conclusion'], ['总结', 'conclusion'], ['答案', 'conclusion'],
|
|
29
|
+
['判断', 'decision'], ['决策', 'decision'],
|
|
30
|
+
['指标', 'metric'], ['数据', 'metric'],
|
|
31
|
+
['风险', 'risk'], ['注意', 'risk'], ['警告', 'risk'],
|
|
32
|
+
['下一步', 'next'], ['行动', 'next'], ['待办', 'next'],
|
|
33
|
+
['验收', 'accept'], ['通过', 'accept'], ['完成', 'accept'],
|
|
34
|
+
['重点', 'key'], ['关键', 'key'],
|
|
35
|
+
])
|
|
36
36
|
const ICONS = [
|
|
37
37
|
[/结论|总结|答案|判断/, '◆'],
|
|
38
38
|
[/方案|计划|步骤|流程/, '▸'],
|
|
@@ -43,13 +43,6 @@ window.__ModuleLoader__.load({
|
|
|
43
43
|
[/背景|原因|根因/, '◎'],
|
|
44
44
|
[/指标|数据|结果/, '▦'],
|
|
45
45
|
]
|
|
46
|
-
const TONES = [
|
|
47
|
-
[/风险|注意|警告|坑/, 'warn'],
|
|
48
|
-
[/验收|通过|成功/, 'success'],
|
|
49
|
-
[/重点|关键|核心/, 'info'],
|
|
50
|
-
[/下一步|行动|待办/, 'info'],
|
|
51
|
-
[/结论|判断|答案/, 'info'],
|
|
52
|
-
]
|
|
53
46
|
|
|
54
47
|
const CSS = `
|
|
55
48
|
[class*="_markdown_"]{
|
|
@@ -172,19 +165,6 @@ window.__ModuleLoader__.load({
|
|
|
172
165
|
margin:14px 0;padding:10px 14px;border-left:3px solid var(--ds-accent);
|
|
173
166
|
background:color-mix(in srgb,var(--ds-accent) 6%,transparent);border-radius:0 10px 10px 0;
|
|
174
167
|
}
|
|
175
|
-
[class*="_markdown_"] blockquote[${LEAD_ATTR}="lead"]{
|
|
176
|
-
position:relative;border-left:4px solid var(--ds-accent);border-radius:12px;
|
|
177
|
-
padding:14px 16px 14px 38px;
|
|
178
|
-
background:linear-gradient(135deg,color-mix(in srgb,var(--ds-accent) 7%,var(--ds-surface)),var(--ds-surface) 62%);
|
|
179
|
-
box-shadow:0 1px 2px rgba(0,0,0,.04),0 12px 32px -24px color-mix(in srgb,var(--ds-accent) 32%,transparent);
|
|
180
|
-
}
|
|
181
|
-
[class*="_markdown_"] blockquote[${LEAD_ATTR}="lead"]::before{
|
|
182
|
-
content:"◆";position:absolute;left:14px;top:15px;
|
|
183
|
-
color:var(--ds-accent);font-size:13px;line-height:1;
|
|
184
|
-
}
|
|
185
|
-
[class*="_markdown_"] blockquote[${LEAD_ATTR}="lead"]>p:first-child{
|
|
186
|
-
margin-top:0;font-size:16.5px;line-height:1.6;font-weight:600;color:var(--ds-text);
|
|
187
|
-
}
|
|
188
168
|
[class*="_markdown_"] blockquote[${TONE_ATTR}="warn"]{--ds-accent:var(--dsw-alias-state-warn-primary,#d97706)}
|
|
189
169
|
[class*="_markdown_"] blockquote[${TONE_ATTR}="success"]{--ds-accent:var(--dsw-alias-state-success-primary,#16a34a)}
|
|
190
170
|
[class*="_markdown_"] blockquote[${CARD_ATTR}="1"]{
|
|
@@ -273,9 +253,24 @@ window.__ModuleLoader__.load({
|
|
|
273
253
|
return null
|
|
274
254
|
}
|
|
275
255
|
|
|
276
|
-
function
|
|
277
|
-
|
|
278
|
-
|
|
256
|
+
function firstParagraph(block) {
|
|
257
|
+
return [...(block.children ?? [])].find((element) => element.tagName === 'P') ?? null
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
function labelOf(block) {
|
|
261
|
+
const first = firstParagraph(block)
|
|
262
|
+
const strong = first?.firstElementChild
|
|
263
|
+
if (!strong || strong.tagName !== 'STRONG') return null
|
|
264
|
+
return (strong.textContent ?? '').replace(/[::]\s*$/u, '').trim()
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
function cardTypeFor(block) {
|
|
268
|
+
const label = labelOf(block)
|
|
269
|
+
return label === null ? null : (CARD_TYPES.get(label) ?? null)
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
function isLabelLike(text) {
|
|
273
|
+
return [...CARD_TYPES.keys()].some((label) => text.startsWith(label) && text !== label)
|
|
279
274
|
}
|
|
280
275
|
|
|
281
276
|
function inGenui(element) {
|
|
@@ -286,11 +281,6 @@ window.__ModuleLoader__.load({
|
|
|
286
281
|
}
|
|
287
282
|
}
|
|
288
283
|
|
|
289
|
-
function toneFor(text) {
|
|
290
|
-
for (const [pattern, tone] of TONES) if (pattern.test(text)) return tone
|
|
291
|
-
return null
|
|
292
|
-
}
|
|
293
|
-
|
|
294
284
|
function statusFor(text) {
|
|
295
285
|
if (/^✅/.test(text)) return 'success'
|
|
296
286
|
if (/^⚠/.test(text)) return 'warn'
|
|
@@ -298,24 +288,27 @@ window.__ModuleLoader__.load({
|
|
|
298
288
|
return null
|
|
299
289
|
}
|
|
300
290
|
|
|
301
|
-
function isFirstBlockquote(block) {
|
|
302
|
-
const markdown = block.closest?.('[class*="_markdown_"]')
|
|
303
|
-
return Boolean(markdown && markdown.querySelector('blockquote') === block)
|
|
304
|
-
}
|
|
305
|
-
|
|
306
291
|
function isStatementCard(block) {
|
|
307
292
|
const markdown = block.closest?.('[class*="_markdown_"]')
|
|
308
|
-
if (!markdown
|
|
293
|
+
if (!markdown) return false
|
|
294
|
+
const first = [...markdown.querySelectorAll('blockquote')].find((element) => !inGenui(element))
|
|
295
|
+
if (first !== block) return false
|
|
309
296
|
const paragraphs = [...block.children].filter((element) => element.tagName === 'P')
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
297
|
+
const firstParagraph = paragraphs[0]
|
|
298
|
+
const strong = firstParagraph?.firstElementChild
|
|
299
|
+
if (!strong || strong.tagName !== 'STRONG') return false
|
|
300
|
+
let previous = strong.previousSibling
|
|
301
|
+
while (previous) {
|
|
302
|
+
if (previous.nodeType === 1) return false
|
|
303
|
+
if (previous.nodeType === 3 && (previous.textContent ?? '').trim() !== '') return false
|
|
304
|
+
previous = previous.previousSibling
|
|
316
305
|
}
|
|
317
|
-
const
|
|
318
|
-
|
|
306
|
+
const title = (strong.textContent ?? '').trim()
|
|
307
|
+
if (title.length === 0 || isLabelLike(title)) return false
|
|
308
|
+
let rest = ''
|
|
309
|
+
for (let node = strong.nextSibling; node; node = node.nextSibling) rest += node.textContent ?? ''
|
|
310
|
+
const secondBody = paragraphs.slice(1).some((element) => (element.textContent ?? '').trim().length > 0)
|
|
311
|
+
return rest.trim().length > 0 || secondBody
|
|
319
312
|
}
|
|
320
313
|
|
|
321
314
|
function decorate(root) {
|
|
@@ -328,17 +321,13 @@ window.__ModuleLoader__.load({
|
|
|
328
321
|
|
|
329
322
|
for (const block of root.querySelectorAll('blockquote')) {
|
|
330
323
|
if (inGenui(block)) continue
|
|
331
|
-
|
|
332
|
-
let type = cardTypeFor(text)
|
|
324
|
+
let type = cardTypeFor(block)
|
|
333
325
|
if (type === null && isStatementCard(block)) type = 'statement'
|
|
334
326
|
if (type === null) {
|
|
335
327
|
block.removeAttribute(CARD_ATTR)
|
|
336
328
|
block.removeAttribute(CARD_TYPE_ATTR)
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
const tone = toneFor(text)
|
|
340
|
-
if (tone === null) block.removeAttribute(TONE_ATTR)
|
|
341
|
-
else block.setAttribute(TONE_ATTR, tone)
|
|
329
|
+
block.removeAttribute(LEAD_ATTR)
|
|
330
|
+
block.removeAttribute(TONE_ATTR)
|
|
342
331
|
continue
|
|
343
332
|
}
|
|
344
333
|
block.setAttribute(CARD_ATTR, '1')
|
|
@@ -396,69 +385,123 @@ window.__ModuleLoader__.load({
|
|
|
396
385
|
}
|
|
397
386
|
}
|
|
398
387
|
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
388
|
+
let active = false
|
|
389
|
+
let disposeLayer = null
|
|
390
|
+
|
|
391
|
+
const enable = () => {
|
|
392
|
+
if (active) return
|
|
393
|
+
if (typeof document === 'undefined' || typeof MutationObserver === 'undefined') return
|
|
394
|
+
if (document.body === null) return
|
|
395
|
+
active = true
|
|
404
396
|
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
397
|
+
document.getElementById(LEGACY_STYLE_ID)?.remove()
|
|
398
|
+
document.getElementById(STYLE_ID)?.remove()
|
|
399
|
+
const style = document.createElement('style')
|
|
400
|
+
style.id = STYLE_ID
|
|
401
|
+
style.textContent = CSS
|
|
402
|
+
document.head.appendChild(style)
|
|
403
|
+
document.documentElement.setAttribute(ROOT_ATTR, 'active')
|
|
404
|
+
try {
|
|
405
|
+
console.info('[dsh-structured] visual layer active')
|
|
406
|
+
} catch {
|
|
407
|
+
// console 不可用时忽略。
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
let disposed = false
|
|
411
|
+
let scheduled = false
|
|
412
|
+
let pending = null
|
|
413
|
+
const run = () => {
|
|
414
|
+
if (disposed) return
|
|
415
|
+
scheduled = false
|
|
416
|
+
pending = null
|
|
417
|
+
decorate(document)
|
|
418
|
+
}
|
|
419
|
+
const schedule = () => {
|
|
420
|
+
if (disposed || scheduled) return
|
|
421
|
+
scheduled = true
|
|
422
|
+
if (typeof requestAnimationFrame === 'function') pending = requestAnimationFrame(run)
|
|
423
|
+
else pending = setTimeout(run, 16)
|
|
424
|
+
}
|
|
425
|
+
const observer = new MutationObserver(schedule)
|
|
426
|
+
observer.observe(document.body, { childList: true, subtree: true, characterData: true })
|
|
427
|
+
run()
|
|
428
|
+
|
|
429
|
+
disposeLayer = () => {
|
|
430
|
+
if (disposed) return
|
|
431
|
+
disposed = true
|
|
432
|
+
active = false
|
|
433
|
+
disposeLayer = null
|
|
434
|
+
observer.disconnect()
|
|
435
|
+
if (pending !== null) {
|
|
436
|
+
if (typeof cancelAnimationFrame === 'function') cancelAnimationFrame(pending)
|
|
437
|
+
else clearTimeout(pending)
|
|
438
|
+
}
|
|
439
|
+
style.remove()
|
|
440
|
+
document.documentElement.removeAttribute(ROOT_ATTR)
|
|
441
|
+
for (const attr of [CARD_ATTR, CARD_TYPE_ATTR, ICON_ATTR, TONE_ATTR, LANG_ATTR, STATUS_ATTR, STATUS_TABLE_ATTR, LEAD_ATTR, ROW_ATTR]) {
|
|
442
|
+
for (const element of document.querySelectorAll('[' + attr + ']')) element.removeAttribute(attr)
|
|
443
|
+
}
|
|
412
444
|
try {
|
|
413
|
-
console.info('[dsh-structured] visual layer
|
|
445
|
+
console.info('[dsh-structured] visual layer disposed')
|
|
414
446
|
} catch {
|
|
415
447
|
// console 不可用时忽略。
|
|
416
448
|
}
|
|
449
|
+
}
|
|
450
|
+
}
|
|
417
451
|
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
observer.observe(document.body, { childList: true, subtree: true, characterData: true })
|
|
435
|
-
run()
|
|
452
|
+
const disable = () => {
|
|
453
|
+
const layer = disposeLayer
|
|
454
|
+
disposeLayer = null
|
|
455
|
+
if (typeof layer === 'function') layer()
|
|
456
|
+
if (typeof document === 'undefined') return
|
|
457
|
+
document.getElementById(STYLE_ID)?.remove()
|
|
458
|
+
document.documentElement.removeAttribute(ROOT_ATTR)
|
|
459
|
+
for (const attr of [CARD_ATTR, CARD_TYPE_ATTR, ICON_ATTR, TONE_ATTR, LANG_ATTR, STATUS_ATTR, STATUS_TABLE_ATTR, LEAD_ATTR, ROW_ATTR]) {
|
|
460
|
+
for (const element of document.querySelectorAll('[' + attr + ']')) element.removeAttribute(attr)
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
const applyVisual = (on) => {
|
|
465
|
+
if (on) enable()
|
|
466
|
+
else disable()
|
|
467
|
+
}
|
|
436
468
|
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
469
|
+
return {
|
|
470
|
+
inject: [],
|
|
471
|
+
apply(ctx) {
|
|
472
|
+
// 默认开启,保持 0.8.1 的现有观感;随后由插件设置里的 visual 覆盖。
|
|
473
|
+
enable()
|
|
474
|
+
try {
|
|
475
|
+
ctx?.inject?.(['configForms'], (child) => {
|
|
476
|
+
try {
|
|
477
|
+
const form = child.configForms?.get('dsh-structured')
|
|
478
|
+
if (!form || typeof form.getSnapshot !== 'function') return
|
|
479
|
+
const read = () => {
|
|
480
|
+
try {
|
|
481
|
+
const raw = form.getSnapshot()?.value?.visual
|
|
482
|
+
const visual = raw && typeof raw.get === 'function' ? raw.get() : raw
|
|
483
|
+
applyVisual(visual !== false)
|
|
484
|
+
} catch {
|
|
485
|
+
// 读取失败时保持当前状态,不打断回复。
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
read()
|
|
489
|
+
if (typeof form.subscribe === 'function') {
|
|
490
|
+
child.effect?.(() => form.subscribe(read), 'dsh-structured: visual preference')
|
|
491
|
+
}
|
|
492
|
+
} catch {
|
|
493
|
+
// configForms 不可用时保持默认开启。
|
|
494
|
+
}
|
|
495
|
+
})
|
|
496
|
+
} catch {
|
|
497
|
+
// 无 ctx.inject 的宿主保持默认开启。
|
|
455
498
|
}
|
|
456
499
|
try {
|
|
457
|
-
ctx?.effect?.(() =>
|
|
500
|
+
ctx?.effect?.(() => disable, 'dsh-structured: visual layer')
|
|
458
501
|
} catch {
|
|
459
|
-
|
|
502
|
+
// 没有 effect 释放器时,视觉层保持到页面卸载。
|
|
460
503
|
}
|
|
461
|
-
return
|
|
504
|
+
return disable
|
|
462
505
|
},
|
|
463
506
|
}
|
|
464
507
|
},
|
package/src/index.js
CHANGED
|
@@ -22,6 +22,7 @@ import {
|
|
|
22
22
|
} from 'node:fs'
|
|
23
23
|
import { homedir } from 'node:os'
|
|
24
24
|
import { dirname, join } from 'node:path'
|
|
25
|
+
import Schema from '@deepseek-ai/schemastery'
|
|
25
26
|
|
|
26
27
|
/** Cordis 插件名。 */
|
|
27
28
|
export const name = 'dsh-structured'
|
|
@@ -45,6 +46,24 @@ const REMINDER_ORDER = 10300
|
|
|
45
46
|
/** 会话条目上限:按最近使用淘汰,防止 state.json 无界增长。 */
|
|
46
47
|
export const MAX_SESSION_ENTRIES = 500
|
|
47
48
|
|
|
49
|
+
/** 把字段标记为 volatile,让 DSH 设置表单可以实时读写。 */
|
|
50
|
+
const k = (x) => (typeof x.volatile === 'function' ? x.volatile() : x)
|
|
51
|
+
|
|
52
|
+
/** volatile 字段在 host 侧是可读信号;普通字段原样返回。 */
|
|
53
|
+
const readConfig = (x) => (x && typeof x.get === 'function' ? x.get() : x)
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* DSH 插件配置 schema:设置表单据此渲染,并持久化到 active profile 的 Cordis patch。
|
|
57
|
+
* 不把 visual 写进 state.json,避免 structured-test 与 desktop 共享同一状态文件。
|
|
58
|
+
*/
|
|
59
|
+
export const Config = Schema.object({
|
|
60
|
+
visual: k(Schema.boolean().default(true)),
|
|
61
|
+
defaultEnabled: k(Schema.boolean().default(true)),
|
|
62
|
+
tailReminder: k(Schema.boolean().default(true)),
|
|
63
|
+
includeSubagents: k(Schema.boolean().default(false)),
|
|
64
|
+
genui: k(Schema.union(['auto', 'on', 'off']).default('auto').loose()),
|
|
65
|
+
})
|
|
66
|
+
|
|
48
67
|
/**
|
|
49
68
|
* 内置规则文本:中文,与 `~/.dsh/AGENTS.md` 第 7 节(响应格式)同源,只规定形状、不另立第二套写作规则。
|
|
50
69
|
* 逐条对应:首句结论(结论卡)、组件化正文、禁过程叙事与复述、结构化不等于变长、支撑信息带证据、
|
|
@@ -56,9 +75,9 @@ export const BUILTIN_STYLE_TEXT = [
|
|
|
56
75
|
'面向用户的实质回复按下面的形状组织。这是硬约束,不是风格建议。',
|
|
57
76
|
'',
|
|
58
77
|
'1. **首句给结论,并把它做成卡片**:第一段固定两行——`> **一句标题**`(加粗放大,≤60 个汉字或一句短句)→ `> 1–2 行正文`;标题即结论,细节放正文,不要写「结论」标签,不要再用普通段落重复。',
|
|
59
|
-
'2.
|
|
78
|
+
'2. **按信息类型选组件**:3 条以上要点用清单或表格;每条实质回复 1 个主卡(首段标题无标签);判断/指标/重点卡用 `> **判断**` / `> **指标**` / `> **重点**`;最多再 1 个行动卡(`风险` / `下一步` / `验收`);表格前先给一行 `**表格结论:** ...`;状态行/状态单元格用 `✅ 完成` / `⚠️ 待做` / `❌ 阻塞` 开头;表格优先 ≤5 行;信息确实多时拆表或使用宿主支持的大表,不硬砍内容;不为了格式加内容,也不为了字数删信息;短答不套卡,其余保持中性。',
|
|
60
79
|
'3. **一段一个主题**:同一信息只出现一次,不要正文和组件各说一遍;不写过程叙事(我先…/已经…/接下来…),不写收尾总结,不加「还需要我…吗?」。',
|
|
61
|
-
'4.
|
|
80
|
+
'4. **清晰优先,不设硬性篇幅上限**:组件是帮读者快速定位结论的工具,不是加信息的借口;短答不套卡,长答按信息量组织;判断标准是读者只看首段标题 + 表格首列/列表首句,就能知道结论和下一步;细节写文件,回复保留结论与路径。',
|
|
62
81
|
'5. **支撑信息带证据**:每条支撑尽量带数字、路径或结果;没有证据的判断明确标成推断。',
|
|
63
82
|
'6. **可复制内容用代码块**:提示词、命令、配置、JSON、SQL 等需要复制的文本,用带语言标签的代码块包裹(如 `text` / `powershell`),不要用引用块或普通段落;代码块右上角有复制按钮。',
|
|
64
83
|
].join('\n')
|
|
@@ -70,7 +89,7 @@ export const GENUI_ROUTING_TEXT = [
|
|
|
70
89
|
|
|
71
90
|
/** 尾部提醒:一句话重复契约中最容易衰减的四件事,长度控制在几十个 token。 */
|
|
72
91
|
export const TAIL_REMINDER_TEXT = [
|
|
73
|
-
'提醒:本轮最终回复第一段用无标签标题卡:加粗标题(≤60 字)/ 正文两行;表格前给 `**表格结论:**`;状态用 `✅ 完成` / `⚠️
|
|
92
|
+
'提醒:本轮最终回复第一段用无标签标题卡:加粗标题(≤60 字)/ 正文两行;表格前给 `**表格结论:**`;状态用 `✅ 完成` / `⚠️ 待做`;全文最多两个强调块;表格优先 ≤5 行;不设硬性篇幅上限;清晰优先;可复制内容用代码块。',
|
|
74
93
|
].join('\n')
|
|
75
94
|
|
|
76
95
|
/** DSH home:优先 DSH_HOME,缺省 ~/.dsh。 */
|
|
@@ -284,7 +303,7 @@ export function setEnabledFor(sessions, defaultEnabled, sessionId, enabled) {
|
|
|
284
303
|
|
|
285
304
|
/**
|
|
286
305
|
* 生成 /structured 的状态文本(不含副作用)。
|
|
287
|
-
* @param {{ sessionId: string | null, sessions: Record<string, { enabled: boolean, at: number }>, defaultEnabled: boolean, genui?: boolean }} input
|
|
306
|
+
* @param {{ sessionId: string | null, sessions: Record<string, { enabled: boolean, at: number }>, defaultEnabled: boolean, genui?: boolean, visual?: boolean }} input
|
|
288
307
|
* @returns {string}
|
|
289
308
|
*/
|
|
290
309
|
export function statusText(input) {
|
|
@@ -294,7 +313,7 @@ export function statusText(input) {
|
|
|
294
313
|
'结构化提示词:' + (enabled ? '已开启' : '已关闭') + '(本会话)',
|
|
295
314
|
'新会话默认:' + (input.defaultEnabled ? '开启' : '关闭'),
|
|
296
315
|
'规则来源:' + (style.source === 'custom' ? '自定义 ' + styleFile() : '内置(无标签标题卡 + 结构化组件)'),
|
|
297
|
-
'
|
|
316
|
+
'视觉层:' + (input.visual === false ? '已关闭' : '已开启') + '(由插件设置 visual 控制;/structured 只控制提示词)',
|
|
298
317
|
'GenUI 联动:' + (input.genui ? '已检测(图表/交互走 dsh-ui,普通卡片走 fallback)' : '未检测(纯 markdown fallback)'),
|
|
299
318
|
'状态文件:' + stateFile(),
|
|
300
319
|
'用法:/structured on | off | status',
|
|
@@ -311,7 +330,7 @@ export function handleCommand(invocation, api) {
|
|
|
311
330
|
const sessionId = sessionIdOf(invocation)
|
|
312
331
|
const arg = String(invocation?.rawInput ?? '').trim().toLowerCase()
|
|
313
332
|
if (arg === '' || arg === 'status') {
|
|
314
|
-
return { kind: 'success', text: statusText({ sessionId, sessions: api.sessions, defaultEnabled: api.defaultEnabled, genui: api.genui }) }
|
|
333
|
+
return { kind: 'success', text: statusText({ sessionId, sessions: api.sessions, defaultEnabled: api.defaultEnabled, genui: api.genui, visual: api.visual }) }
|
|
315
334
|
}
|
|
316
335
|
if (arg !== 'on' && arg !== 'off') {
|
|
317
336
|
return { kind: 'error', text: '未知参数:' + arg + '\n用法:/structured on | off | status' }
|
|
@@ -333,17 +352,19 @@ export function handleCommand(invocation, api) {
|
|
|
333
352
|
/**
|
|
334
353
|
* 挂载结构化输出:提示词 section + 尾部提醒 + 会话开关 + /structured 命令。
|
|
335
354
|
* @param {import('@deepseek-ai/cordis').Context} ctx host 根上下文。
|
|
336
|
-
* @param {{ defaultEnabled?: boolean, tailReminder?: boolean, includeSubagents?: boolean, genui?: 'auto' | 'on' | 'off' }} [config]
|
|
355
|
+
* @param {{ defaultEnabled?: boolean, tailReminder?: boolean, includeSubagents?: boolean, genui?: 'auto' | 'on' | 'off', visual?: boolean }} [config]
|
|
337
356
|
*/
|
|
338
357
|
export function apply(ctx, config = {}) {
|
|
339
|
-
const defaultEnabled = config?.defaultEnabled !== false
|
|
340
|
-
const
|
|
341
|
-
const
|
|
342
|
-
const
|
|
358
|
+
const defaultEnabled = readConfig(config?.defaultEnabled) !== false
|
|
359
|
+
const visualNow = () => readConfig(config?.visual) !== false
|
|
360
|
+
const tailReminder = readConfig(config?.tailReminder) !== false
|
|
361
|
+
const includeSubagents = readConfig(config?.includeSubagents) === true
|
|
362
|
+
const genuiConfig = { genui: readConfig(config?.genui) }
|
|
363
|
+
const genui = detectGenui(genuiConfig, ctx)
|
|
343
364
|
let genuiKnown = genui
|
|
344
365
|
const genuiNow = () => {
|
|
345
366
|
if (genuiKnown) return true
|
|
346
|
-
genuiKnown = detectGenui(
|
|
367
|
+
genuiKnown = detectGenui(genuiConfig, ctx)
|
|
347
368
|
return genuiKnown
|
|
348
369
|
}
|
|
349
370
|
const builtinStyleText = () => (genuiNow() ? BUILTIN_STYLE_TEXT + '\n\n' + GENUI_ROUTING_TEXT : BUILTIN_STYLE_TEXT)
|
|
@@ -460,6 +481,7 @@ export function apply(ctx, config = {}) {
|
|
|
460
481
|
sessions,
|
|
461
482
|
defaultEnabled,
|
|
462
483
|
genui: genuiNow(),
|
|
484
|
+
visual: visualNow(),
|
|
463
485
|
onChange: (sessionId, enabled, result) => {
|
|
464
486
|
warn(
|
|
465
487
|
'session ' + sessionId + ' structured ' + (enabled ? 'on' : 'off') + (result.ok ? '' : '(持久化失败)'),
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
import test from 'node:test'
|
|
2
|
+
import assert from 'node:assert/strict'
|
|
3
|
+
import { readFileSync } from 'node:fs'
|
|
4
|
+
import vm from 'node:vm'
|
|
5
|
+
import { parseHTML } from 'linkedom'
|
|
6
|
+
|
|
7
|
+
const clientSource = readFileSync('src/client.js', 'utf8')
|
|
8
|
+
|
|
9
|
+
function render(html) {
|
|
10
|
+
const { window, document } = parseHTML(`<!doctype html><html><head></head><body>${html}</body></html>`)
|
|
11
|
+
let definition
|
|
12
|
+
const sandbox = {
|
|
13
|
+
window: {
|
|
14
|
+
__ModuleLoader__: {
|
|
15
|
+
load(value) {
|
|
16
|
+
definition = value
|
|
17
|
+
},
|
|
18
|
+
},
|
|
19
|
+
},
|
|
20
|
+
document,
|
|
21
|
+
MutationObserver: class {
|
|
22
|
+
observe() {}
|
|
23
|
+
disconnect() {}
|
|
24
|
+
},
|
|
25
|
+
requestAnimationFrame(fn) {
|
|
26
|
+
fn()
|
|
27
|
+
return 1
|
|
28
|
+
},
|
|
29
|
+
cancelAnimationFrame() {},
|
|
30
|
+
setTimeout,
|
|
31
|
+
clearTimeout,
|
|
32
|
+
console,
|
|
33
|
+
}
|
|
34
|
+
sandbox.globalThis = sandbox
|
|
35
|
+
vm.runInNewContext(clientSource, sandbox)
|
|
36
|
+
assert.ok(definition, 'client module definition captured')
|
|
37
|
+
const plugin = definition.factory()
|
|
38
|
+
plugin.apply({
|
|
39
|
+
inject(_services, callback) {
|
|
40
|
+
callback({ configForms: { get: () => null } })
|
|
41
|
+
},
|
|
42
|
+
effect() {},
|
|
43
|
+
})
|
|
44
|
+
return document
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function block(document, id) {
|
|
48
|
+
const element = document.querySelector(`#${id}`)
|
|
49
|
+
assert.ok(element, `missing #${id}`)
|
|
50
|
+
return element
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function assertPlain(document, id) {
|
|
54
|
+
const element = block(document, id)
|
|
55
|
+
assert.equal(element.getAttribute('data-dsh-structured-card'), null, `${id} card`)
|
|
56
|
+
assert.equal(element.getAttribute('data-dsh-structured-card-type'), null, `${id} type`)
|
|
57
|
+
assert.equal(element.getAttribute('data-dsh-structured-tone'), null, `${id} tone`)
|
|
58
|
+
assert.equal(element.getAttribute('data-dsh-structured-lead'), null, `${id} lead`)
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function assertCard(document, id, type, tone) {
|
|
62
|
+
const element = block(document, id)
|
|
63
|
+
assert.equal(element.getAttribute('data-dsh-structured-card'), '1', `${id} card`)
|
|
64
|
+
assert.equal(element.getAttribute('data-dsh-structured-card-type'), type, `${id} type`)
|
|
65
|
+
assert.equal(element.getAttribute('data-dsh-structured-tone'), tone, `${id} tone`)
|
|
66
|
+
assert.equal(element.getAttribute('data-dsh-structured-lead'), null, `${id} lead`)
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
test('严格卡片:复合标签、普通引用不升级,完整标签才成卡', () => {
|
|
70
|
+
const document = render(`
|
|
71
|
+
<div class="_markdown_q2"><blockquote id="q2"><p><strong>风险提示</strong><br>影响:宿主升级后可能失效。</p></blockquote></div>
|
|
72
|
+
<div class="_markdown_test">
|
|
73
|
+
<blockquote id="q1"><p>风险提示:宿主升级后可能失效。</p></blockquote>
|
|
74
|
+
<blockquote id="q3"><p>结论:这是用户引用的原文。</p></blockquote>
|
|
75
|
+
<blockquote id="q4"><p>这是普通引用,没有加粗标题。</p></blockquote>
|
|
76
|
+
<blockquote id="q9"><p><strong>判断依据</strong><br>不应成卡。</p></blockquote>
|
|
77
|
+
</div>
|
|
78
|
+
<div class="_markdown_q10"><blockquote id="q10"><p>前缀 <strong>标题</strong></p></blockquote></div>
|
|
79
|
+
<div class="_markdown_q11"><blockquote id="q11"><p> <strong>标题</strong></p></blockquote></div>
|
|
80
|
+
<div class="_markdown_q18"><blockquote id="q18"><p>前缀 <strong>标题</strong>尾正文</p></blockquote></div>
|
|
81
|
+
<div class="_markdown_q16"><blockquote id="q16"><p><strong>结论性说明</strong><br>不应成卡。</p></blockquote></div>
|
|
82
|
+
<div class="_markdown_q17"><blockquote id="q17"><p><strong>指标数据</strong><br>不应成卡。</p></blockquote></div>
|
|
83
|
+
<div class="_markdown_test2">
|
|
84
|
+
<blockquote id="q5"><p><strong>Phase 1 完成</strong><br>28/28 通过。</p></blockquote>
|
|
85
|
+
<blockquote id="q5b"><p><strong>Phase 2 完成</strong><br>不应升级为 statement。</p></blockquote>
|
|
86
|
+
<blockquote id="q6"><p><strong>风险</strong><br>影响:宿主升级后可能失效。</p></blockquote>
|
|
87
|
+
<blockquote id="q7"><p><strong>下一步</strong><br>1. 安装测试 profile。</p></blockquote>
|
|
88
|
+
<blockquote id="q8"><p><strong>验收</strong><br>✅ 3/3</p></blockquote>
|
|
89
|
+
<blockquote id="q12"><p><strong>结论</strong><br>结论卡。</p></blockquote>
|
|
90
|
+
<blockquote id="q13"><p><strong>决策</strong><br>决策卡。</p></blockquote>
|
|
91
|
+
<blockquote id="q14"><p><strong>指标</strong><br>指标卡。</p></blockquote>
|
|
92
|
+
<blockquote id="q15"><p><strong>重点</strong><br>重点卡。</p></blockquote>
|
|
93
|
+
<div data-genui="1">
|
|
94
|
+
<blockquote id="gen"><p><strong>风险</strong><br>GenUI 内不处理。</p></blockquote>
|
|
95
|
+
</div>
|
|
96
|
+
</div>
|
|
97
|
+
<div class="_markdown_test5">
|
|
98
|
+
<div data-genui="1">
|
|
99
|
+
<blockquote id="genfirst"><p><strong>风险</strong><br>GenUI 内不处理,也不应阻断真实首块。</p></blockquote>
|
|
100
|
+
</div>
|
|
101
|
+
<blockquote id="real"><p><strong>Phase 9 完成</strong><br>真实首块仍应升级 statement。</p></blockquote>
|
|
102
|
+
</div>
|
|
103
|
+
`)
|
|
104
|
+
|
|
105
|
+
// 反例:前缀/复合词、普通引用、strong 前有正文或空白
|
|
106
|
+
assertPlain(document, 'q1')
|
|
107
|
+
assertPlain(document, 'q2')
|
|
108
|
+
assertPlain(document, 'q3')
|
|
109
|
+
assertPlain(document, 'q4')
|
|
110
|
+
assertPlain(document, 'q9')
|
|
111
|
+
assertPlain(document, 'q10')
|
|
112
|
+
assertPlain(document, 'q11')
|
|
113
|
+
assertPlain(document, 'q18')
|
|
114
|
+
assertPlain(document, 'q16')
|
|
115
|
+
assertPlain(document, 'q17')
|
|
116
|
+
|
|
117
|
+
// 正例:全部卡型 + 仅第一块可升级 + GenUI 前置不阻断真实首块
|
|
118
|
+
assertCard(document, 'q5', 'statement', 'info')
|
|
119
|
+
assertCard(document, 'q6', 'risk', 'warn')
|
|
120
|
+
assertCard(document, 'q7', 'next', 'info')
|
|
121
|
+
assertCard(document, 'q8', 'accept', 'success')
|
|
122
|
+
assertCard(document, 'q12', 'conclusion', 'info')
|
|
123
|
+
assertCard(document, 'q13', 'decision', 'info')
|
|
124
|
+
assertCard(document, 'q14', 'metric', 'info')
|
|
125
|
+
assertCard(document, 'q15', 'key', 'info')
|
|
126
|
+
assertCard(document, 'real', 'statement', 'info')
|
|
127
|
+
assertPlain(document, 'q5b')
|
|
128
|
+
|
|
129
|
+
// GenUI 区域跳过
|
|
130
|
+
assertPlain(document, 'gen')
|
|
131
|
+
assertPlain(document, 'genfirst')
|
|
132
|
+
})
|
|
133
|
+
|
|
134
|
+
test('严格卡片:不再有普通首引用 lead 与文本前缀 tone 逻辑', () => {
|
|
135
|
+
assert.doesNotMatch(clientSource, /isFirstBlockquote/)
|
|
136
|
+
assert.doesNotMatch(clientSource, /toneFor/)
|
|
137
|
+
assert.doesNotMatch(clientSource, /setAttribute\(LEAD_ATTR,\s*'lead'\)/)
|
|
138
|
+
assert.match(clientSource, /CARD_TYPES = new Map/)
|
|
139
|
+
assert.match(clientSource, /isStatementCard/)
|
|
140
|
+
})
|