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/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
- [/^结论|^总结|^答案/, 'conclusion'],
31
- [/^判断|^决策/, 'decision'],
32
- [/^指标|^数据/, 'metric'],
33
- [/^风险|^注意|^警告/, 'risk'],
34
- [/^下一步|^行动|^待办/, 'next'],
35
- [/^验收|^通过|^完成/, 'accept'],
36
- [/^重点|^关键/, 'key'],
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 cardTypeFor(text) {
279
- for (const [pattern, type] of CARD_TYPES) if (pattern.test(text)) return type
280
- return null
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 || markdown.querySelector('blockquote') !== block) return false
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
- if (paragraphs.length < 2) {
313
- const first = paragraphs[0]
314
- const strong = first?.firstElementChild
315
- if (!strong || strong.tagName !== 'STRONG') return false
316
- const rest = first.textContent.slice(strong.textContent.length).trim()
317
- return rest.length > 0 || Boolean(first.querySelector('br'))
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 first = paragraphs[0]
320
- return Boolean(first?.firstElementChild && first.firstElementChild.tagName === 'STRONG')
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
- const text = (block.textContent ?? '').trimStart()
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
- if (isFirstBlockquote(block)) block.setAttribute(LEAD_ATTR, 'lead')
340
- else block.removeAttribute(LEAD_ATTR)
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
- return {
402
- inject: [],
403
- apply(ctx) {
404
- if (typeof document === 'undefined' || typeof MutationObserver === 'undefined') return
405
- if (document.body === null) return
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
- document.getElementById(LEGACY_STYLE_ID)?.remove()
408
- document.getElementById(STYLE_ID)?.remove()
409
- const style = document.createElement('style')
410
- style.id = STYLE_ID
411
- style.textContent = CSS
412
- document.head.appendChild(style)
413
- document.documentElement.setAttribute(ROOT_ATTR, 'active')
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 active')
447
+ console.info('[dsh-structured] visual layer disposed')
416
448
  } catch {
417
449
  // console 不可用时忽略。
418
450
  }
451
+ }
452
+ }
419
453
 
420
- let disposed = false
421
- let scheduled = false
422
- let pending = null
423
- const run = () => {
424
- if (disposed) return
425
- scheduled = false
426
- pending = null
427
- decorate(document)
428
- }
429
- const schedule = () => {
430
- if (disposed || scheduled) return
431
- scheduled = true
432
- if (typeof requestAnimationFrame === 'function') pending = requestAnimationFrame(run)
433
- else pending = setTimeout(run, 16)
434
- }
435
- const observer = new MutationObserver(schedule)
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
- const dispose = () => {
440
- if (disposed) return
441
- disposed = true
442
- observer.disconnect()
443
- if (pending !== null) {
444
- if (typeof cancelAnimationFrame === 'function') cancelAnimationFrame(pending)
445
- else clearTimeout(pending)
446
- }
447
- style.remove()
448
- document.documentElement.removeAttribute(ROOT_ATTR)
449
- for (const attr of [CARD_ATTR, CARD_TYPE_ATTR, ICON_ATTR, TONE_ATTR, LANG_ATTR, STATUS_ATTR, STATUS_TABLE_ATTR, LEAD_ATTR, ROW_ATTR]) {
450
- for (const element of document.querySelectorAll('[' + attr + ']')) element.removeAttribute(attr)
451
- }
452
- try {
453
- console.info('[dsh-structured] visual layer disposed')
454
- } catch {
455
- // console 不可用时忽略。
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?.(() => dispose, 'dsh-structured: visual layer')
502
+ ctx?.effect?.(() => disable, 'dsh-structured: visual layer')
460
503
  } catch {
461
- dispose()
504
+ // 没有 effect 释放器时,视觉层保持到页面卸载。
462
505
  }
463
- return dispose
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. **按内容选卡,最多两个强调块**:3 条以上要点用清单或表格;每条实质回复 1 个主卡(首段标题无标签);判断/指标/重点卡用 `> **判断**` / `> **指标**` / `> **重点**`;最多再 1 个行动卡(`风险` / `下一步` / `验收`);表格前先给一行 `**表格结论:** ...`;状态行/状态单元格用 `✅ 完成` / `⚠️ 待做` / `❌ 阻塞` 开头;表格 ≤5 行,正文 ≤500 字(含表格);短答不套卡,其余保持中性。',
80
+ '2. **按信息类型选组件**:3 条以上要点用清单或表格;每条实质回复 1 个主卡(首段标题无标签);判断/指标/重点卡用 `> **判断**` / `> **指标**` / `> **重点**`;最多再 1 个行动卡(`风险` / `下一步` / `验收`);表格前先给一行 `**表格结论:** ...`;状态行/状态单元格用 `✅ 完成` / `⚠️ 待做` / `❌ 阻塞` 开头;表格优先 ≤5 行;信息确实多时拆表或使用宿主支持的大表,不硬砍内容;不为了格式加内容,也不为了字数删信息;短答不套卡,其余保持中性。',
62
81
  '3. **一段一个主题**:同一信息只出现一次,不要正文和组件各说一遍;不写过程叙事(我先…/已经…/接下来…),不写收尾总结,不加「还需要我…吗?」。',
63
- '4. **结构化不等于变长**:组件是压缩阅读时间的工具,不是加信息的借口;对话汇报正文不超过 500 字,超出的细节写进文件,回复只留结论加文件路径。',
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 字)/ 正文两行;表格前给 `**表格结论:**`;状态用 `✅ 完成` / `⚠️ 待做`;全文最多两个强调块;表格 ≤5 行,正文不超过 500 字;可复制内容用代码块。',
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
- '视觉层:始终开启(/structured 只控制提示词注入,视觉样式不随开关关闭)',
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 tailReminder = config?.tailReminder !== false
343
- const includeSubagents = config?.includeSubagents === true
344
- const genui = detectGenui(config, ctx)
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(config, ctx)
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.7.4",
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 nonNodeImports = imports.filter((spec) => !spec.startsWith('node:'))
109
- check('运行时只 import node: 内置模块', nonNodeImports.length === 0, nonNodeImports.join(', ') || 'ok')
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), '')