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