dsh-structured 0.8.1 → 0.8.3
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 +14 -0
- package/README.md +9 -3
- package/cordis.patch.yml +2 -0
- package/docs/ACCEPTANCE-0.8.2.md +3 -0
- package/docs/ACCEPTANCE-0.8.3.md +5 -0
- package/docs/VISUAL-ACCEPTANCE.md +4 -4
- package/lib/client.js +153 -110
- package/lib/index.js +33 -11
- package/package.json +11 -2
- package/scripts/desktop-check.mjs +6 -2
- package/src/client.js +153 -110
- package/src/index.js +33 -11
- package/test/client-cards.test.mjs +140 -0
- package/test/client-visual.test.mjs +163 -0
- package/test/structured.test.mjs +30 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,19 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.8.3 — 2026-10-22
|
|
4
|
+
|
|
5
|
+
- 提示词优先级加固:首段 markdown 标题卡即使后文使用 `dsh-ui` 也必须存在;`dsh-ui` 只能跟在标题卡与正文之后,不得用其 `title` 充当首卡,不得把结论/判断/风险/下一步做成 dsh-ui 组件;宿主默认要出 UI 时以插件规则为准。
|
|
6
|
+
- 尾部提醒同步强调「第一段必须是 markdown 标题卡,组件只能排在标题卡之后、不得充当首卡」。
|
|
7
|
+
- 背景:观察到报告型回复把 dsh-ui 当主卡、跳过标题卡;该会话的注入与视觉层均正常,属提示词优先级问题。
|
|
8
|
+
|
|
9
|
+
## 0.8.2 — 2026-10-22
|
|
10
|
+
|
|
11
|
+
- 视觉层从“始终开启”改为 profile 级 `visual` 配置(`Config` + `.volatile()`,默认 true);通过 DSH 插件设置页(bundle 详情表单)切换,不再写共享的 `state.json`。
|
|
12
|
+
- `/structured on|off` 保持原义:只控制按会话的提示词注入;视觉层由 `visual` 控制,`/structured status` 会显示两者状态。
|
|
13
|
+
- client 通过 `configForms.get('dsh-structured')` 读取 `visual` 并启用/停用视觉层;`configForms` 不可用时保持默认开启,不阻断回复。
|
|
14
|
+
- 严格卡片识别:只认完整标签(结论/总结/答案、判断/决策、指标/数据、风险/注意/警告、下一步/行动/待办、验收/通过/完成、重点/关键);前缀/复合词不染色;普通引用不再自动 lead;statement 仅第一块;GenUI 跳过。
|
|
15
|
+
- 新增 `@deepseek-ai/schemastery` peer/dev 依赖;`desktop-check` 允许宿主 schemastery import 并检查 `Config` 导出;版本 0.8.2。
|
|
16
|
+
|
|
3
17
|
## 0.8.1 — 2026-10-22
|
|
4
18
|
|
|
5
19
|
- 规则文本从“500 字硬上限”改为清晰度约束:标题即结论、按信息类型选组件、一段一主题、证据优先、短答不套卡、长答不设硬性篇幅上限。
|
package/README.md
CHANGED
|
@@ -81,7 +81,7 @@ pnpm add $tgz
|
|
|
81
81
|
|
|
82
82
|
### 兼容性门禁
|
|
83
83
|
|
|
84
|
-
|
|
84
|
+
本包声明 `@deepseek-ai/schemastery` peer:`Config`(visual/defaultEnabled/tailReminder/includeSubagents/genui)用它生成设置表单;这是 host 提供的服务,不是门禁。真正门禁是 `engines.dsh`(`>=0.1.7-rc.2 <0.3.0-0`),`dsh-app-boot` 在有 `engines.dsh` 时读取它;`dsh.compatibility.dshReleases` 是 advisory。实测 DSH CLI `0.1.7-rc.2` 与 desktop `0.2.1-rc.2`。
|
|
85
85
|
|
|
86
86
|
## 使用
|
|
87
87
|
|
|
@@ -94,6 +94,7 @@ pnpm add $tgz
|
|
|
94
94
|
|
|
95
95
|
- 开关按会话独立,状态落在 `$DSH_HOME/dsh-structured/state.json`(tmp + rename 原子写)。
|
|
96
96
|
- 新会话默认开启;`defaultEnabled: false` 可改默认值(见配置)。
|
|
97
|
+
- 视觉层由插件设置 `visual` 控制(默认开启),不写 `state.json`;`/structured on|off` 只控制提示词注入,不影响视觉层。
|
|
97
98
|
- 自定义规则:写 `$DSH_HOME/dsh-structured/style.md`,**非空即整份覆盖**内置文本,保存后下一轮生效;文件为空或删除时回落内置。
|
|
98
99
|
- 内置文本与 `~/.dsh/AGENTS.md` 第 7 节同源:首句结论、组件化正文、一段一主题、禁过程叙事与复述、证据优先、短答不套卡;清晰优先,不设硬性篇幅上限。
|
|
99
100
|
|
|
@@ -103,7 +104,7 @@ pnpm add $tgz
|
|
|
103
104
|
|
|
104
105
|
视觉方向只做一套:**Linear 的秩序 + Notion 的卡片 + Stripe 的信息密度 + Vercel AI Elements 的回复结构**,见 [`docs/DESIGN-REFERENCES.md`](docs/DESIGN-REFERENCES.md)。卡片用图标方块 + 13px 标签 + 淡色表面;状态只在 68px 状态列着色,不再整行 pastel;表格结论用强调左规线。不做主题菜单。
|
|
105
106
|
|
|
106
|
-
设计与分阶段计划见 [`docs/VISUAL-PLAN.md`](docs/VISUAL-PLAN.md),视觉热点/层级诊断见 [`docs/VISUAL-CRITIQUE.md`](docs/VISUAL-CRITIQUE.md),卡片类型与热点预算见 [`docs/CARD-SYSTEM.md`](docs/CARD-SYSTEM.md)
|
|
107
|
+
设计与分阶段计划见 [`docs/VISUAL-PLAN.md`](docs/VISUAL-PLAN.md),视觉热点/层级诊断见 [`docs/VISUAL-CRITIQUE.md`](docs/VISUAL-CRITIQUE.md),卡片类型与热点预算见 [`docs/CARD-SYSTEM.md`](docs/CARD-SYSTEM.md)。视觉层由插件设置 `visual` 控制(默认开启);`/structured on|off` 只控制提示词注入。仍只提供一套「工程蓝图」视觉,不做主题选择。
|
|
107
108
|
|
|
108
109
|
Phase 2 已落地 2.1–2.4:卡片类型(结论/判断/指标/风险/下一步/验收/重点)、`data-dsh-structured-card-type` 归属标记、GenUI 软联动(检测到才追加 `dsh-ui` 路由,未检测走纯 markdown fallback,不硬依赖)。
|
|
109
110
|
|
|
@@ -130,8 +131,11 @@ Phase 2 已落地 2.1–2.4:卡片类型(结论/判断/指标/风险/下一
|
|
|
130
131
|
tailReminder: true # system prompt 末尾追加一句提醒
|
|
131
132
|
includeSubagents: false # 子代理会话不注入(它的产出不直接面向用户)
|
|
132
133
|
genui: auto # auto | on | off;auto 检测到 GenUI 才追加 dsh-ui 路由
|
|
134
|
+
visual: true # 客户端视觉层;由插件设置页切换,不写 state.json
|
|
133
135
|
```
|
|
134
136
|
|
|
137
|
+
`visual`、`defaultEnabled`、`tailReminder`、`includeSubagents`、`genui` 都是 `.volatile()` 配置,插件设置页(bundle 详情表单)可编辑;写入走 active profile 的 Cordis patch,home patch / CLI overlay 优先。`/structured on|off` 只影响 `state.json` 里的按会话提示词开关。
|
|
138
|
+
|
|
135
139
|
## 验证注入生效
|
|
136
140
|
|
|
137
141
|
会话日志是 `$DSH_HOME/sessions/<cwd-group>/<session-id>/session.v4.jsonl.zstd`(独立 zstd 帧拼接,普通 grep 读不了)。仓库自带读取脚本:
|
|
@@ -153,7 +157,7 @@ node scripts/session-inspect.mjs --session session-xxxx --last-system --grep "
|
|
|
153
157
|
搜索:"结构化输出(本会话已启用" → 命中 1 条 system/message
|
|
154
158
|
```
|
|
155
159
|
|
|
156
|
-
关闭后(`/structured off` 或把该会话在 `state.json` 里置为 `false`),同一个会话下一轮应看到 `命中 0
|
|
160
|
+
关闭后(`/structured off` 或把该会话在 `state.json` 里置为 `false`),同一个会话下一轮应看到 `命中 0 条`。视觉层由插件设置 `visual` 控制,`/structured` 只控制提示词注入,不关闭 client 视觉层。
|
|
157
161
|
|
|
158
162
|
> 以上命令在仓库根执行。已装到 profile 时,脚本路径是 `<profile>\node_modules\dsh-structured\scripts\`;把命令里的 `scripts/` 换成 `node_modules/dsh-structured/scripts/` 即可。
|
|
159
163
|
|
|
@@ -222,6 +226,8 @@ scripts/desktop-check.mjs desktop 兼容性预检
|
|
|
222
226
|
scripts/measure-prose.mjs 散文率统计(可复跑)
|
|
223
227
|
scripts/lib/session-log.mjs v4 会话日志 zstd 帧读取
|
|
224
228
|
test/structured.test.mjs 单元测试 + 边界测试
|
|
229
|
+
test/client-visual.test.mjs client visual 配置开关测试
|
|
230
|
+
test/client-cards.test.mjs 严格卡片识别 DOM 测试
|
|
225
231
|
docs/METRICS.md A/B 指标与原始明细
|
|
226
232
|
docs/ROADMAP.md Phase 1/2 路线图与 Phase 2 任务
|
|
227
233
|
docs/CARD-SYSTEM.md 卡片类型、anatomy 与热点预算
|
package/cordis.patch.yml
CHANGED
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
# tailReminder 是否在 system prompt 末尾追加一句提醒(默认 true)
|
|
7
7
|
# includeSubagents 是否也给子代理会话注入(默认 false,子代理产出不直接面向用户)
|
|
8
8
|
# genui auto | on | off;auto 检测到 dsh-genui 才追加 dsh-ui 路由(默认 auto)
|
|
9
|
+
# visual 是否启用客户端视觉层(默认 true;由插件设置页切换,不写 state.json)
|
|
9
10
|
#
|
|
10
11
|
# 需要改默认值时,在 profile 的 cordis.patch.yml 里覆盖同一行(整份 config 替换,不是深合并):
|
|
11
12
|
# - id: dsh-structured
|
|
@@ -18,3 +19,4 @@
|
|
|
18
19
|
tailReminder: true
|
|
19
20
|
includeSubagents: false
|
|
20
21
|
genui: auto
|
|
22
|
+
visual: true
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
[AC-001/002] `npm test` 36/36(含严格卡片 10 反例 + 9 正例 + GenUI 跳过);`node scripts/desktop-check.mjs` 全 PASS;`lib/index.js`、`lib/client.js` 与 src 一致。
|
|
2
|
+
[AC-003] `structured-test` 已安装 0.8.2(B 构建,`lib/client.js` hash 与仓库一致),headless 一轮 `completed`;`session-inspect --session session-343e363b-73ca-4c9c-80ed-52d099f5cb8b --last-system --grep "结构化输出(本会话已启用"` 命中 1;`npm pack` 38 文件 / 763106 B / unpacked 962368 B。
|
|
3
|
+
[AC-004/005] `visual` 配置与 client `configForms` 读取测试通过,`/structured off` 仍只控制提示词;桌面 bundle 详情页 `visual` 字段的实机可见/可切换 **未验**(未触碰 desktop profile);未 push、未 publish、未提市场 PR。
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
# dsh-structured 0.8.3 — 验收记录
|
|
2
|
+
|
|
3
|
+
[AC-001/002] `npm test` 36/36(含新增 GenUI 优先级断言);`node scripts/desktop-check.mjs` 全 PASS;`lib/index.js` 与 src 一致。
|
|
4
|
+
[AC-003] 0.8.3 已装 `structured-test`(`lib/index.js` sha256 `62f3a37d…` 与仓库一致);headless 两轮 `session-2aa045fb-…`、`session-c1253adf-…` 首段均为 `> **` 标题卡,system prompt 含新尾提醒「组件只能排在标题卡之后」;`npm pack` 39 文件 / unpacked 964444 B。
|
|
5
|
+
[AC-004] 用 keyvalue 组件的请求在 headless 不触发 GenUI(无 client half),desktop 的 dsh-ui 场景未验;未 push、未 publish、未改市场 PR。
|
|
@@ -55,7 +55,7 @@ pnpm 和 npm 二选一,给结论和一张不超过 4 行的对比表。
|
|
|
55
55
|
|
|
56
56
|
| 门 | 判据 | 方法 |
|
|
57
57
|
| --- | --- | --- |
|
|
58
|
-
| R1 卡层级 | 标签 13px;标题独立成块、16px/700;正文 15.5px
|
|
58
|
+
| R1 卡层级 | 标签 13px;标题独立成块、16px/700;正文 15.5px;普通首段不升级 lead,只有 `**表格结论:**` 段落使用表格 lead | DOM 测字号;无 DevTools 时只判层级(标签 < 标题 < 正文),字号记未验证 |
|
|
59
59
|
| R2 热点预算 | 强调块 ≤2;无卡中卡;结论不在普通段落重复一遍。强调块 = 卡片 blockquote + 带色 callout,不含表格与代码 | 原文 + 截图 |
|
|
60
60
|
| R3 表格 | 表前一行 `**表格结论:**`;无竖线;表头底色可辨;状态只染单元格/行首,不整行铺色 | 原文 + 截图 |
|
|
61
61
|
| R4 状态 | ✅/⚠️/❌ 字形保留;色不是唯一语义;状态文字对比度 ≥4.5:1 | DOM 取色;无 DevTools 时目检并标未验证 |
|
|
@@ -73,7 +73,7 @@ pnpm 和 npm 二选一,给结论和一张不超过 4 行的对比表。
|
|
|
73
73
|
| C3 状态符号 | 凡状态行/状态表,保留 ✅/⚠️/❌ | 100% |
|
|
74
74
|
| C4 代码语言 | 凡围栏代码块,有语言标签 | 100% |
|
|
75
75
|
| C5 短答克制 | P06:0 卡、0 表、≤2 句 | 1/1 |
|
|
76
|
-
| C6
|
|
76
|
+
| C6 篇幅 | 不设硬性字数上限;长回复信息完整、不因字数删信息(人工抽查) | 100% |
|
|
77
77
|
|
|
78
78
|
> 目标组件命中率(实际出现的卡型是否等于期望)只**报告**,不单独判失败;低于 8/12 时复盘 prompt 与注入契约,不要直接改 CSS。
|
|
79
79
|
|
|
@@ -157,7 +157,7 @@ pnpm 和 npm 二选一,给结论和一张不超过 4 行的对比表。
|
|
|
157
157
|
| 级别 | 定义 | 处理 |
|
|
158
158
|
| --- | --- | --- |
|
|
159
159
|
| S1 | 渲染断裂:卡没样式、标题塌成正文、溢出/裁切、对比度不达标、GenUI 双渲染 | 停跑,修完重跑全套 |
|
|
160
|
-
| S2 | 契约未达:无卡、>2
|
|
160
|
+
| S2 | 契约未达:无卡、>2 卡、缺表格结论、缺状态符号、代码无语言、信息不完整或为凑长度删信息 | 记录,修完只复跑相关用例 |
|
|
161
161
|
| S3 | 观感:间距、图标语义、色调偏差 | 记录,可延后 |
|
|
162
162
|
|
|
163
163
|
- 每个用例只按第一次结果计分。模型没按格式写,就是没写,不靠重跑刷到通过;诊断性重跑单独记,不计入分数。
|
|
@@ -165,7 +165,7 @@ pnpm 和 npm 二选一,给结论和一张不超过 4 行的对比表。
|
|
|
165
165
|
|
|
166
166
|
## 8. 跑之前必须确认的已知缺口
|
|
167
167
|
|
|
168
|
-
1. **卡标题层级**:0.
|
|
168
|
+
1. **卡标题层级**:0.8.2 起只认完整卡型标签;只有第一块、首行加粗标题、且有正文/第二段的 blockquote 才升级 statement;普通 one-p 引用不再兜底 lead;表格 lead 只属于 `**表格结论:**` 段落。
|
|
169
169
|
2. **GenUI 是否安装未知。** 以 `/structured status` 为准;未装时 P12 记 `N/A`,不能算通过,覆盖数写 11/12。
|
|
170
170
|
3. **DevTools 不一定可用。** desktop 是 Electron,若打不开控制台,R1/R4 退化为 200% 截图目检,R8 的控制台项记“未验证”,并在报告方法列写明,不要写成测量。
|
|
171
171
|
|
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
|
* 逐条对应:首句结论(结论卡)、组件化正文、禁过程叙事与复述、结构化不等于变长、支撑信息带证据、
|
|
@@ -67,12 +86,12 @@ export const BUILTIN_STYLE_TEXT = [
|
|
|
67
86
|
|
|
68
87
|
/** GenUI 已安装时才追加的路由规则:交互/图表走 dsh-ui,普通卡片仍走 markdown。 */
|
|
69
88
|
export const GENUI_ROUTING_TEXT = [
|
|
70
|
-
'**GenUI
|
|
89
|
+
'**GenUI 已安装**:只有图表、可排序表格、表单、超过 8 行的大表才用 ` ```dsh-ui ` fence;`dsh-ui` 只能出现在 markdown 标题卡与正文之后,**不得用它的 title 充当首卡,不得把结论/判断/风险/下一步做成 dsh-ui 组件**;宿主默认要出 UI 时以本节为准,不要为每条回复生成 JSON,也不要重复渲染同一内容。',
|
|
71
90
|
].join('\n')
|
|
72
91
|
|
|
73
92
|
/** 尾部提醒:一句话重复契约中最容易衰减的四件事,长度控制在几十个 token。 */
|
|
74
93
|
export const TAIL_REMINDER_TEXT = [
|
|
75
|
-
'
|
|
94
|
+
'提醒:本轮最终回复第一段必须是 markdown 无标签标题卡:加粗标题(≤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.8.
|
|
3
|
+
"version": "0.8.3",
|
|
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": {
|
|
@@ -82,5 +84,12 @@
|
|
|
82
84
|
"engines": {
|
|
83
85
|
"node": ">=22",
|
|
84
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"
|
|
85
94
|
}
|
|
86
95
|
}
|