dsh-structured 0.9.1 → 0.9.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 +12 -0
- package/README.md +29 -10
- package/assets/screenshots/09-theme-light.png +0 -0
- package/assets/screenshots/10-theme-dark.png +0 -0
- package/assets/screenshots/11-config-page.png +0 -0
- package/lib/client.js +191 -2
- package/package.json +1 -1
- package/screenshots.json +4 -1
- package/src/client.js +191 -2
- package/test/client-config-page.test.mjs +329 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,17 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.9.3 — 2026-10-24
|
|
4
|
+
|
|
5
|
+
- 配置页主题分段:三个主题选项(DeepSeek / Codex / Anthropic)各带所代表主题的强调色——选项前 7px 圆点标识 + 选中态文字着色,明暗自适应(DeepSeek `#2563EB`/`#60A5FA`、Codex `#10A37F`/`#34D399`、Anthropic `#CC785C`/`#E8A087`,与视觉层三套 token 同源)。样式只作用于 `[data-dsh-structured-config]` 容器内,随配置页 slot 同生命周期注入/撤销。
|
|
6
|
+
- 截图:新增第 7 张 `assets/screenshots/11-config-page.png`(插件板块 → dsh-structured 详情页配置区实机截图,0.9.2 效果);`screenshots.json` 与 README Screenshots 节同步。
|
|
7
|
+
|
|
8
|
+
## 0.9.2 — 2026-10-24
|
|
9
|
+
|
|
10
|
+
- 插件详情页配置区:client 注册 `plugins.bundle.config`(键 `dsh-structured`),`theme`/`visual`/`defaultEnabled`/`tailReminder`/`includeSubagents`/`genui` 六字段在 插件板块 → dsh-structured 详情页直接编辑;写入走 configForms 的 `set`(落到 active profile 的 Cordis patch),`writable=false` 时控件只读并提示。
|
|
11
|
+
- 背景:宿主不会从 Config schema 自动生成表单——配置页必须由插件自己的浏览器半侧注册(`dsh-client-ui-plugin-manager` slot 合同的 `plugins.bundle.config`/`plugins.row.config`)。0.8.2 引入 Config 起、直到 0.9.1,插件从未注册配置页,所以详情页一直没有设置项;本次按官方伴生包模式(`whileServed` + `slots.inject`)补齐。
|
|
12
|
+
- 防御:宿主 describe 未服务本命名空间、`react/jsx-runtime`/`dsh-client-ui-primitives` 缺失时静默不出配置页;配置页注入与主题/视觉联动分开挂 child inject,互不影响;headless/CLI 不加载 client 半区。
|
|
13
|
+
- 文档修复:README 安装节三处 `0.7.4.tgz` 过时引用改为通用写法,移除三个「(发布后)」括号,实装实测记录更新为 0.9.1;`.gitignore` 增加 `.v2c/`、`out/`。
|
|
14
|
+
|
|
3
15
|
## 0.9.1 — 2026-10-23
|
|
4
16
|
|
|
5
17
|
- 代码/diff 隔离:` ```diff `/`patch` 代码块、`[data-diff]`/`.md-diff`/GenUI diff 区域不套插件的 code/table chrome;diff 行色与宿主渲染保持原样。代码/diff 回复允许代码块或 diff 卡直接作首块,不强制标题卡。
|
package/README.md
CHANGED
|
@@ -27,11 +27,11 @@ node scripts/desktop-check.mjs
|
|
|
27
27
|
|
|
28
28
|
`desktop` 由 Electron 独占只影响 **CLI 的 `--profile desktop` 子命令**,不影响插件本身;推荐走插件管理入口,或退出 Desktop 后手工 pnpm(方式二)。
|
|
29
29
|
|
|
30
|
-
###
|
|
30
|
+
### 方式一:插件市场
|
|
31
31
|
|
|
32
32
|
在 DSH 插件管理里搜索 `dsh-structured` 安装;市场收录后可用。桌面端由应用侧加载 client,不需要浏览器刷新。
|
|
33
33
|
|
|
34
|
-
### 方式二:npm
|
|
34
|
+
### 方式二:npm
|
|
35
35
|
|
|
36
36
|
```powershell
|
|
37
37
|
dsh plugin --profile <profile> add dsh-structured
|
|
@@ -39,13 +39,13 @@ dsh plugin --profile <profile> add dsh-structured
|
|
|
39
39
|
|
|
40
40
|
desktop profile 由 Electron 独占,使用插件管理搜索/安装,不要跑 `--profile desktop`。
|
|
41
41
|
|
|
42
|
-
### 方式三:GitHub Release
|
|
42
|
+
### 方式三:GitHub Release
|
|
43
43
|
|
|
44
|
-
从 GitHub Releases
|
|
44
|
+
从 GitHub Releases 下载最新版的 `dsh-structured-<version>.tgz`(当前 0.9.1),在插件管理里选择本地包;不使用 npm 时,市场 YAML 的 `tarball` 指向该 release。
|
|
45
45
|
|
|
46
46
|
### 开发方式:本地 tgz / 手工 pnpm(desktop profile)
|
|
47
47
|
|
|
48
|
-
本地包只用于开发验证:在 DSH
|
|
48
|
+
本地包只用于开发验证:在 DSH 插件管理里选择仓库根的 `dsh-structured-<version>.tgz`,或 `plugin_manager.install_bundle` 指向该文件。手工 pnpm 方式如下。
|
|
49
49
|
|
|
50
50
|
`desktop` profile 由 Electron 独占:**全局 `dsh plugin --profile desktop ...` 会被 CLI 直接拒绝**(`profile "desktop" is managed exclusively by the Electron application`)。不要用那条命令。要先**完全退出 Desktop**,再在 profile 目录里手工 pnpm:
|
|
51
51
|
|
|
@@ -56,7 +56,7 @@ Copy-Item "$dir\pnpm-lock.yaml" "$dir\pnpm-lock.yaml.bak-before-dsh-structured"
|
|
|
56
56
|
Copy-Item "$dir\cordis.patch.yml" "$dir\cordis.patch.yml.bak-before-dsh-structured"
|
|
57
57
|
|
|
58
58
|
Set-Location $dir
|
|
59
|
-
$tgz = "dsh-structured-0.
|
|
59
|
+
$tgz = "dsh-structured-0.9.1.tgz" # 把 tgz 放到当前目录,或写成实际路径
|
|
60
60
|
pnpm add $tgz
|
|
61
61
|
```
|
|
62
62
|
|
|
@@ -76,7 +76,7 @@ pnpm add $tgz
|
|
|
76
76
|
|
|
77
77
|
- 预检:`node scripts/desktop-check.mjs` 全部 PASS —— desktop profile 已初始化、bundle 栈含 `dsh-base`/`dsh-web-app`、`files` 含 `cordis.patch.yml`、`main`/`exports` 指向存在文件、`dsh.client.platform: web` 与 `exports["./client"]` 就位、`engines.dsh`/`dsh.compatibility.dshReleases` 已声明、运行时只 import `node:`、无 Electron 专有 API、无 profile 分支、状态走 `DSH_HOME`。
|
|
78
78
|
- **desktop 是主目标,Web 只做兼容。** 本会话本身运行在 desktop profile,`systemPrompt` 与 `commands` 由 live host 的 Service 查询确认;`dsh.client.platform: web` 是 DSH 客户端模块的格式(desktop 的 Web UI 也加载它),不代表 Web 优先。
|
|
79
|
-
- desktop
|
|
79
|
+
- desktop 实装实测:desktop profile 已装 0.9.1(`dependencies`/`bundles` 各一处,见 `docs/MAINTENANCE-LOG.md` 2026-10-23 条目);desktop 会话最新 `system/message` 命中注入(可用 `scripts/session-inspect.mjs` 复跑)。
|
|
80
80
|
- 视觉层由 desktop 应用侧加载,验收以应用内实际渲染为准;不要求用户手动刷新。
|
|
81
81
|
|
|
82
82
|
### 兼容性门禁
|
|
@@ -125,11 +125,30 @@ Phase 2 已落地 2.1–2.4:卡片类型(结论/判断/指标/风险/下一
|
|
|
125
125
|
|
|
126
126
|

|
|
127
127
|
|
|
128
|
-
|
|
128
|
+

|
|
129
|
+
|
|
130
|
+

|
|
131
|
+
|
|
132
|
+

|
|
133
|
+
|
|
134
|
+
`screenshots.json` 列出全部 7 张:前 4 张是实机裁图,覆盖标题卡、表格 + 表格结论、状态列、代码块与 800px 窄窗;09/10 两张是按 0.9.1 实际 token 渲染的主题对照图(不是桌面实机截图);第 7 张是 0.9.2 起插件板块 → dsh-structured 详情页的配置区实机截图(六字段 + 主题分段按钮带各主题强调色)。GenUI 截图因避免暴露工作区未收录。
|
|
129
135
|
|
|
130
136
|
### 配置
|
|
131
137
|
|
|
132
|
-
|
|
138
|
+
**首选:插件板块 → dsh-structured → 详情页的配置区**(0.9.2 起)。插件自己的浏览器半侧把六个字段注册进宿主 `plugins.bundle.config` slot,改完立即写入 active profile 的 Cordis patch:
|
|
139
|
+
|
|
140
|
+
```text
|
|
141
|
+
主题 / Theme deepseek | codex | anthropic
|
|
142
|
+
视觉层 / Visual layer 开关
|
|
143
|
+
新会话默认开启 开关
|
|
144
|
+
尾部提醒 / Tail reminder 开关
|
|
145
|
+
子代理注入 / Subagents 开关
|
|
146
|
+
GenUI 联动 auto | on | off
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
宿主 describe 未服务本插件、或部署缺少前端 primitives 时,配置区自动隐藏(此时用下面的 patch 方式)。会话级开关不在这里:用 `/structured on | off`。主题分段的三个选项各带所代表主题的强调色圆点,选中态文字同色(0.9.3 起)。
|
|
150
|
+
|
|
151
|
+
底层方式:在 profile 的 `cordis.patch.yml` 里覆盖同一行(注意:config 是整份替换,不是深合并):
|
|
133
152
|
|
|
134
153
|
```yaml
|
|
135
154
|
- id: dsh-structured
|
|
@@ -142,7 +161,7 @@ Phase 2 已落地 2.1–2.4:卡片类型(结论/判断/指标/风险/下一
|
|
|
142
161
|
genui: auto # auto | on | off;auto 检测到 GenUI 才追加 dsh-ui 路由
|
|
143
162
|
```
|
|
144
163
|
|
|
145
|
-
`theme`、`visual`、`defaultEnabled`、`tailReminder`、`includeSubagents`、`genui` 都是 `.volatile()`
|
|
164
|
+
`theme`、`visual`、`defaultEnabled`、`tailReminder`、`includeSubagents`、`genui` 都是 `.volatile()` 配置:插件板块详情页的配置区(0.9.2 起,见上)与标准 bundle 表单都可编辑,写入走 active profile 的 Cordis patch,home patch / CLI overlay 优先。若快照的 `writable` 为 false,说明当前配置被上层覆盖,配置区控件只读并提示,不会静默写入成功;请改上层配置或接受只读。`/structured on|off` 只影响 `state.json` 里的按会话提示词开关,不影响 `theme`/`visual`。
|
|
146
165
|
|
|
147
166
|
## 验证注入生效
|
|
148
167
|
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
package/lib/client.js
CHANGED
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
*/
|
|
13
13
|
window.__ModuleLoader__.load({
|
|
14
14
|
id: 'dsh-structured',
|
|
15
|
-
factory() {
|
|
15
|
+
factory(require) {
|
|
16
16
|
const CARD_ATTR = 'data-dsh-structured-card'
|
|
17
17
|
const CARD_TYPE_ATTR = 'data-dsh-structured-card-type'
|
|
18
18
|
const ICON_ATTR = 'data-dsh-structured-icon'
|
|
@@ -30,6 +30,63 @@ window.__ModuleLoader__.load({
|
|
|
30
30
|
const STYLE_ID = 'dsh-structured-skin-style'
|
|
31
31
|
const LEGACY_STYLE_ID = 'dsh-structured-card-style'
|
|
32
32
|
|
|
33
|
+
/** 配置页常量:宿主插件详情页的 bundle 配置 slot(键 = 包名)。 */
|
|
34
|
+
const CONFIG_NS = 'dsh-structured'
|
|
35
|
+
const BUNDLE_CONFIG_SLOT = 'plugins.bundle.config'
|
|
36
|
+
|
|
37
|
+
/** 六个配置字段的界面文案;field 与 host Config schema 对齐,fallback 为 schema 默认值。 */
|
|
38
|
+
const CONFIG_FIELDS = [
|
|
39
|
+
{ field: 'theme', kind: 'segmented', fallback: 'deepseek', label: '主题 / Theme', hint: '回复区配色,改完立即生效', options: [['deepseek', 'DeepSeek'], ['codex', 'Codex'], ['anthropic', 'Anthropic']] },
|
|
40
|
+
{ field: 'visual', kind: 'switch', fallback: true, label: '视觉层 / Visual layer', hint: '关闭后不加载回复区样式(标题卡、表格、代码块皮肤)' },
|
|
41
|
+
{ field: 'defaultEnabled', kind: 'switch', fallback: true, label: '新会话默认开启 / Default on', hint: '新会话默认注入结构化提示词' },
|
|
42
|
+
{ field: 'tailReminder', kind: 'switch', fallback: true, label: '尾部提醒 / Tail reminder', hint: '在 system prompt 末尾追加一句结构化提醒' },
|
|
43
|
+
{ field: 'includeSubagents', kind: 'switch', fallback: false, label: '子代理注入 / Subagents', hint: '子代理会话也注入(其产出不直接面向用户)' },
|
|
44
|
+
{ field: 'genui', kind: 'segmented', fallback: 'auto', label: 'GenUI 联动 / GenUI routing', hint: 'auto:检测到 dsh-genui 才追加 dsh-ui 路由规则', options: [['auto', 'auto'], ['on', 'on'], ['off', 'off']] },
|
|
45
|
+
]
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* 配置页专用样式:主题分段选择器的三个选项各带自己所代表主题的强调色
|
|
49
|
+
* (圆点标识 + 选中态文字着色,明暗自适应)。色值与视觉层三套 token 同源,
|
|
50
|
+
* 但作用域是设置面板(不在 _markdown_ 容器内),所以单独一份而不是复用 --ds-*。
|
|
51
|
+
*/
|
|
52
|
+
const CONFIG_STYLE_ID = 'dsh-structured-config-style'
|
|
53
|
+
const CONFIG_CSS = `
|
|
54
|
+
[data-dsh-structured-config] button[id^="dsh-structured-theme-"]{position:relative}
|
|
55
|
+
[data-dsh-structured-config] button[id^="dsh-structured-theme-"]::before{
|
|
56
|
+
content:"";display:inline-block;width:7px;height:7px;border-radius:50%;
|
|
57
|
+
margin-right:7px;vertical-align:baseline;background:var(--dsw-alias-label-tertiary,#9CA3AF);
|
|
58
|
+
}
|
|
59
|
+
[data-dsh-structured-config] button#dsh-structured-theme-deepseek::before{background:#2563EB}
|
|
60
|
+
[data-dsh-structured-config] button#dsh-structured-theme-codex::before{background:#10A37F}
|
|
61
|
+
[data-dsh-structured-config] button#dsh-structured-theme-anthropic::before{background:#CC785C}
|
|
62
|
+
[data-dsh-structured-config] button#dsh-structured-theme-deepseek[aria-selected="true"]{color:#2563EB;font-weight:600}
|
|
63
|
+
[data-dsh-structured-config] button#dsh-structured-theme-codex[aria-selected="true"]{color:#0B6B4F;font-weight:600}
|
|
64
|
+
[data-dsh-structured-config] button#dsh-structured-theme-anthropic[aria-selected="true"]{color:#A14A2E;font-weight:600}
|
|
65
|
+
body[data-ds-dark-theme] [data-dsh-structured-config] button#dsh-structured-theme-deepseek::before{background:#60A5FA}
|
|
66
|
+
body[data-ds-dark-theme] [data-dsh-structured-config] button#dsh-structured-theme-codex::before{background:#34D399}
|
|
67
|
+
body[data-ds-dark-theme] [data-dsh-structured-config] button#dsh-structured-theme-anthropic::before{background:#E8A087}
|
|
68
|
+
body[data-ds-dark-theme] [data-dsh-structured-config] button#dsh-structured-theme-deepseek[aria-selected="true"]{color:#60A5FA}
|
|
69
|
+
body[data-ds-dark-theme] [data-dsh-structured-config] button#dsh-structured-theme-codex[aria-selected="true"]{color:#34D399}
|
|
70
|
+
body[data-ds-dark-theme] [data-dsh-structured-config] button#dsh-structured-theme-anthropic[aria-selected="true"]{color:#E8A087}
|
|
71
|
+
`
|
|
72
|
+
|
|
73
|
+
/** 插入配置页样式;返回移除函数。document 不可用时是空操作。 */
|
|
74
|
+
const ensureConfigStyle = () => {
|
|
75
|
+
if (typeof document === 'undefined' || document.head === null) return () => {}
|
|
76
|
+
document.getElementById(CONFIG_STYLE_ID)?.remove()
|
|
77
|
+
const style = document.createElement('style')
|
|
78
|
+
style.id = CONFIG_STYLE_ID
|
|
79
|
+
style.textContent = CONFIG_CSS
|
|
80
|
+
document.head.appendChild(style)
|
|
81
|
+
return () => {
|
|
82
|
+
try {
|
|
83
|
+
style.remove()
|
|
84
|
+
} catch {
|
|
85
|
+
// 移除失败不影响宿主。
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
33
90
|
const CARD_TYPES = new Map([
|
|
34
91
|
['结论', 'conclusion'], ['总结', 'conclusion'], ['答案', 'conclusion'],
|
|
35
92
|
['判断', 'decision'], ['决策', 'decision'],
|
|
@@ -454,6 +511,125 @@ body[data-ds-dark-theme][data-dsh-structured-theme="anthropic"] [class*="_markdo
|
|
|
454
511
|
}
|
|
455
512
|
}
|
|
456
513
|
|
|
514
|
+
/**
|
|
515
|
+
* 在插件板块的插件详情页注册配置区(plugins.bundle.config,键 = 包名)。
|
|
516
|
+
* 宿主不会从 Config schema 自动生成表单——配置页必须由插件自己的浏览器半侧注册;
|
|
517
|
+
* 宿主 describe 未服务本命名空间(whileServed 不触发)或 primitives 缺失时不出配置页,
|
|
518
|
+
* 且任何失败都不影响视觉层与消息渲染。
|
|
519
|
+
*/
|
|
520
|
+
function setupConfigPage(child, form) {
|
|
521
|
+
if (typeof require !== 'function') return
|
|
522
|
+
if (typeof child?.configForms?.whileServed !== 'function') return
|
|
523
|
+
if (typeof child?.slots?.inject !== 'function' || typeof child?.slots?.register !== 'function') return
|
|
524
|
+
if (typeof form?.set !== 'function') return
|
|
525
|
+
let jsx
|
|
526
|
+
let jsxs
|
|
527
|
+
let ui
|
|
528
|
+
try {
|
|
529
|
+
const runtime = require('react/jsx-runtime')
|
|
530
|
+
jsx = runtime.jsx
|
|
531
|
+
jsxs = runtime.jsxs
|
|
532
|
+
ui = require('@deepseek-ai/dsh-client-ui-primitives')
|
|
533
|
+
if (typeof ui?.Switch !== 'function' || typeof ui?.SegmentedControl !== 'function') return
|
|
534
|
+
} catch {
|
|
535
|
+
// primitives 不可用(非 desktop 表层):不出配置页。
|
|
536
|
+
return
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
const labelStyle = { fontSize: '14px', lineHeight: '20px', color: 'var(--dsw-alias-label-primary, inherit)' }
|
|
540
|
+
const hintStyle = { fontSize: '12.5px', lineHeight: '18px', color: 'var(--dsw-alias-label-tertiary, inherit)', marginTop: '2px' }
|
|
541
|
+
const rowStyle = { display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: '16px', padding: '10px 0', borderBottom: '1px solid var(--dsw-alias-border-l2, rgba(127,127,127,.18))' }
|
|
542
|
+
const noteStyle = { fontSize: '12.5px', lineHeight: '18px', color: 'var(--dsw-alias-label-tertiary, inherit)', margin: '6px 0 2px' }
|
|
543
|
+
|
|
544
|
+
const fieldValue = (value, spec) => {
|
|
545
|
+
const raw = value?.[spec.field]
|
|
546
|
+
if (raw && typeof raw.get === 'function') return raw.get()
|
|
547
|
+
return raw === undefined || raw === null ? spec.fallback : raw
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
const renderRow = (state, setField, spec) => {
|
|
551
|
+
const writable = state.writable !== false
|
|
552
|
+
const control = spec.kind === 'switch'
|
|
553
|
+
? jsx(ui.Switch, {
|
|
554
|
+
checked: fieldValue(state.value, spec) === true,
|
|
555
|
+
disabled: !writable,
|
|
556
|
+
label: spec.label,
|
|
557
|
+
onChange: (next) => setField(spec.field, next === true),
|
|
558
|
+
})
|
|
559
|
+
: jsx(ui.SegmentedControl, {
|
|
560
|
+
id: 'dsh-structured-' + spec.field,
|
|
561
|
+
value: String(fieldValue(state.value, spec)),
|
|
562
|
+
options: spec.options.map(([value, text]) => ({ value, label: text })),
|
|
563
|
+
disabled: !writable,
|
|
564
|
+
label: spec.label,
|
|
565
|
+
onChange: (next) => setField(spec.field, next),
|
|
566
|
+
})
|
|
567
|
+
return jsxs('div', {
|
|
568
|
+
style: rowStyle,
|
|
569
|
+
children: [
|
|
570
|
+
jsxs('div', { style: { minWidth: 0 }, children: [
|
|
571
|
+
jsx('div', { style: labelStyle, children: spec.label }),
|
|
572
|
+
jsx('div', { style: hintStyle, children: spec.hint }),
|
|
573
|
+
] }),
|
|
574
|
+
control,
|
|
575
|
+
],
|
|
576
|
+
})
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
const StructuredConfigCard = (props) => {
|
|
580
|
+
if (props.view !== undefined && props.view !== 'page') return null
|
|
581
|
+
const read = props.useStructuredConfig ?? props.structuredConfig
|
|
582
|
+
const state = typeof read === 'function' ? read((snapshot) => snapshot) : null
|
|
583
|
+
if (!state || state.status !== 'ready' || typeof state.value !== 'object' || state.value === null) {
|
|
584
|
+
return jsx('p', { style: noteStyle, children: '插件未加载或配置暂不可用。' })
|
|
585
|
+
}
|
|
586
|
+
const setField = typeof props.setField === 'function' ? props.setField : () => {}
|
|
587
|
+
return jsxs('div', {
|
|
588
|
+
'data-dsh-structured-config': '1',
|
|
589
|
+
children: [
|
|
590
|
+
state.writable === false
|
|
591
|
+
? jsx('p', { style: noteStyle, children: '当前配置被更高层覆盖(home patch / CLI overlay),此处只读。' })
|
|
592
|
+
: null,
|
|
593
|
+
jsx('div', { children: CONFIG_FIELDS.map((spec) => renderRow(state, setField, spec)) }),
|
|
594
|
+
jsx('p', { style: noteStyle, children: '会话级开关请用 /structured on | off(按会话生效,不写入这里)。' }),
|
|
595
|
+
],
|
|
596
|
+
})
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
child.effect?.(
|
|
600
|
+
() =>
|
|
601
|
+
child.configForms.whileServed([CONFIG_NS], () => {
|
|
602
|
+
// 样式与 slot 注册同生命周期:命名空间不被服务时一并撤下。
|
|
603
|
+
const removeStyle = ensureConfigStyle()
|
|
604
|
+
const offSlot = child.slots.inject(BUNDLE_CONFIG_SLOT, () =>
|
|
605
|
+
child.slots.register({
|
|
606
|
+
name: BUNDLE_CONFIG_SLOT,
|
|
607
|
+
key: 'dsh-structured',
|
|
608
|
+
inject: () => ({
|
|
609
|
+
hooks: { structuredConfig: form },
|
|
610
|
+
setField: (field, value) => {
|
|
611
|
+
try {
|
|
612
|
+
form.set(field, value)
|
|
613
|
+
} catch {
|
|
614
|
+
// 写入失败不打断页面;快照未变化即用户可见的反馈。
|
|
615
|
+
}
|
|
616
|
+
},
|
|
617
|
+
}),
|
|
618
|
+
}, StructuredConfigCard),
|
|
619
|
+
)
|
|
620
|
+
return () => {
|
|
621
|
+
try {
|
|
622
|
+
offSlot?.()
|
|
623
|
+
} catch {
|
|
624
|
+
// slot 撤销失败交由宿主清理。
|
|
625
|
+
}
|
|
626
|
+
removeStyle()
|
|
627
|
+
}
|
|
628
|
+
}),
|
|
629
|
+
'dsh-structured: bundle config page',
|
|
630
|
+
)
|
|
631
|
+
}
|
|
632
|
+
|
|
457
633
|
let active = false
|
|
458
634
|
let disposeLayer = null
|
|
459
635
|
let currentTheme = 'deepseek'
|
|
@@ -548,7 +724,7 @@ body[data-ds-dark-theme][data-dsh-structured-theme="anthropic"] [class*="_markdo
|
|
|
548
724
|
|
|
549
725
|
return {
|
|
550
726
|
inject: [],
|
|
551
|
-
__test: { inDiff, isDiffCode, decorate },
|
|
727
|
+
__test: { inDiff, isDiffCode, decorate, setupConfigPage },
|
|
552
728
|
apply(ctx) {
|
|
553
729
|
// 默认主题/视觉先启用;随后由 profile Config 的 theme/visual 覆盖。
|
|
554
730
|
enable()
|
|
@@ -578,6 +754,19 @@ body[data-ds-dark-theme][data-dsh-structured-theme="anthropic"] [class*="_markdo
|
|
|
578
754
|
} catch {
|
|
579
755
|
// 无 ctx.inject 的宿主保持默认开启。
|
|
580
756
|
}
|
|
757
|
+
try {
|
|
758
|
+
// 插件详情页配置区与视觉层分开注入:配置页失败不影响主题/视觉联动。
|
|
759
|
+
ctx?.inject?.(['configForms', 'slots'], (child) => {
|
|
760
|
+
try {
|
|
761
|
+
const form = child.configForms?.get('dsh-structured')
|
|
762
|
+
if (form) setupConfigPage(child, form)
|
|
763
|
+
} catch {
|
|
764
|
+
// 配置页不可用不影响视觉层与注入。
|
|
765
|
+
}
|
|
766
|
+
})
|
|
767
|
+
} catch {
|
|
768
|
+
// 无 ctx.inject 的宿主没有配置页,视觉层照常。
|
|
769
|
+
}
|
|
581
770
|
try {
|
|
582
771
|
ctx?.effect?.(() => disable, 'dsh-structured: visual layer')
|
|
583
772
|
} catch {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-structured",
|
|
3
|
-
"version": "0.9.
|
|
3
|
+
"version": "0.9.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",
|
package/screenshots.json
CHANGED
|
@@ -2,5 +2,8 @@
|
|
|
2
2
|
"assets/screenshots/01-title-card.png",
|
|
3
3
|
"assets/screenshots/06-status-table.png",
|
|
4
4
|
"assets/screenshots/07-code-block.png",
|
|
5
|
-
"assets/screenshots/08-narrow.png"
|
|
5
|
+
"assets/screenshots/08-narrow.png",
|
|
6
|
+
"assets/screenshots/09-theme-light.png",
|
|
7
|
+
"assets/screenshots/10-theme-dark.png",
|
|
8
|
+
"assets/screenshots/11-config-page.png"
|
|
6
9
|
]
|
package/src/client.js
CHANGED
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
*/
|
|
11
11
|
window.__ModuleLoader__.load({
|
|
12
12
|
id: 'dsh-structured',
|
|
13
|
-
factory() {
|
|
13
|
+
factory(require) {
|
|
14
14
|
const CARD_ATTR = 'data-dsh-structured-card'
|
|
15
15
|
const CARD_TYPE_ATTR = 'data-dsh-structured-card-type'
|
|
16
16
|
const ICON_ATTR = 'data-dsh-structured-icon'
|
|
@@ -28,6 +28,63 @@ window.__ModuleLoader__.load({
|
|
|
28
28
|
const STYLE_ID = 'dsh-structured-skin-style'
|
|
29
29
|
const LEGACY_STYLE_ID = 'dsh-structured-card-style'
|
|
30
30
|
|
|
31
|
+
/** 配置页常量:宿主插件详情页的 bundle 配置 slot(键 = 包名)。 */
|
|
32
|
+
const CONFIG_NS = 'dsh-structured'
|
|
33
|
+
const BUNDLE_CONFIG_SLOT = 'plugins.bundle.config'
|
|
34
|
+
|
|
35
|
+
/** 六个配置字段的界面文案;field 与 host Config schema 对齐,fallback 为 schema 默认值。 */
|
|
36
|
+
const CONFIG_FIELDS = [
|
|
37
|
+
{ field: 'theme', kind: 'segmented', fallback: 'deepseek', label: '主题 / Theme', hint: '回复区配色,改完立即生效', options: [['deepseek', 'DeepSeek'], ['codex', 'Codex'], ['anthropic', 'Anthropic']] },
|
|
38
|
+
{ field: 'visual', kind: 'switch', fallback: true, label: '视觉层 / Visual layer', hint: '关闭后不加载回复区样式(标题卡、表格、代码块皮肤)' },
|
|
39
|
+
{ field: 'defaultEnabled', kind: 'switch', fallback: true, label: '新会话默认开启 / Default on', hint: '新会话默认注入结构化提示词' },
|
|
40
|
+
{ field: 'tailReminder', kind: 'switch', fallback: true, label: '尾部提醒 / Tail reminder', hint: '在 system prompt 末尾追加一句结构化提醒' },
|
|
41
|
+
{ field: 'includeSubagents', kind: 'switch', fallback: false, label: '子代理注入 / Subagents', hint: '子代理会话也注入(其产出不直接面向用户)' },
|
|
42
|
+
{ field: 'genui', kind: 'segmented', fallback: 'auto', label: 'GenUI 联动 / GenUI routing', hint: 'auto:检测到 dsh-genui 才追加 dsh-ui 路由规则', options: [['auto', 'auto'], ['on', 'on'], ['off', 'off']] },
|
|
43
|
+
]
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* 配置页专用样式:主题分段选择器的三个选项各带自己所代表主题的强调色
|
|
47
|
+
* (圆点标识 + 选中态文字着色,明暗自适应)。色值与视觉层三套 token 同源,
|
|
48
|
+
* 但作用域是设置面板(不在 _markdown_ 容器内),所以单独一份而不是复用 --ds-*。
|
|
49
|
+
*/
|
|
50
|
+
const CONFIG_STYLE_ID = 'dsh-structured-config-style'
|
|
51
|
+
const CONFIG_CSS = `
|
|
52
|
+
[data-dsh-structured-config] button[id^="dsh-structured-theme-"]{position:relative}
|
|
53
|
+
[data-dsh-structured-config] button[id^="dsh-structured-theme-"]::before{
|
|
54
|
+
content:"";display:inline-block;width:7px;height:7px;border-radius:50%;
|
|
55
|
+
margin-right:7px;vertical-align:baseline;background:var(--dsw-alias-label-tertiary,#9CA3AF);
|
|
56
|
+
}
|
|
57
|
+
[data-dsh-structured-config] button#dsh-structured-theme-deepseek::before{background:#2563EB}
|
|
58
|
+
[data-dsh-structured-config] button#dsh-structured-theme-codex::before{background:#10A37F}
|
|
59
|
+
[data-dsh-structured-config] button#dsh-structured-theme-anthropic::before{background:#CC785C}
|
|
60
|
+
[data-dsh-structured-config] button#dsh-structured-theme-deepseek[aria-selected="true"]{color:#2563EB;font-weight:600}
|
|
61
|
+
[data-dsh-structured-config] button#dsh-structured-theme-codex[aria-selected="true"]{color:#0B6B4F;font-weight:600}
|
|
62
|
+
[data-dsh-structured-config] button#dsh-structured-theme-anthropic[aria-selected="true"]{color:#A14A2E;font-weight:600}
|
|
63
|
+
body[data-ds-dark-theme] [data-dsh-structured-config] button#dsh-structured-theme-deepseek::before{background:#60A5FA}
|
|
64
|
+
body[data-ds-dark-theme] [data-dsh-structured-config] button#dsh-structured-theme-codex::before{background:#34D399}
|
|
65
|
+
body[data-ds-dark-theme] [data-dsh-structured-config] button#dsh-structured-theme-anthropic::before{background:#E8A087}
|
|
66
|
+
body[data-ds-dark-theme] [data-dsh-structured-config] button#dsh-structured-theme-deepseek[aria-selected="true"]{color:#60A5FA}
|
|
67
|
+
body[data-ds-dark-theme] [data-dsh-structured-config] button#dsh-structured-theme-codex[aria-selected="true"]{color:#34D399}
|
|
68
|
+
body[data-ds-dark-theme] [data-dsh-structured-config] button#dsh-structured-theme-anthropic[aria-selected="true"]{color:#E8A087}
|
|
69
|
+
`
|
|
70
|
+
|
|
71
|
+
/** 插入配置页样式;返回移除函数。document 不可用时是空操作。 */
|
|
72
|
+
const ensureConfigStyle = () => {
|
|
73
|
+
if (typeof document === 'undefined' || document.head === null) return () => {}
|
|
74
|
+
document.getElementById(CONFIG_STYLE_ID)?.remove()
|
|
75
|
+
const style = document.createElement('style')
|
|
76
|
+
style.id = CONFIG_STYLE_ID
|
|
77
|
+
style.textContent = CONFIG_CSS
|
|
78
|
+
document.head.appendChild(style)
|
|
79
|
+
return () => {
|
|
80
|
+
try {
|
|
81
|
+
style.remove()
|
|
82
|
+
} catch {
|
|
83
|
+
// 移除失败不影响宿主。
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
31
88
|
const CARD_TYPES = new Map([
|
|
32
89
|
['结论', 'conclusion'], ['总结', 'conclusion'], ['答案', 'conclusion'],
|
|
33
90
|
['判断', 'decision'], ['决策', 'decision'],
|
|
@@ -452,6 +509,125 @@ body[data-ds-dark-theme][data-dsh-structured-theme="anthropic"] [class*="_markdo
|
|
|
452
509
|
}
|
|
453
510
|
}
|
|
454
511
|
|
|
512
|
+
/**
|
|
513
|
+
* 在插件板块的插件详情页注册配置区(plugins.bundle.config,键 = 包名)。
|
|
514
|
+
* 宿主不会从 Config schema 自动生成表单——配置页必须由插件自己的浏览器半侧注册;
|
|
515
|
+
* 宿主 describe 未服务本命名空间(whileServed 不触发)或 primitives 缺失时不出配置页,
|
|
516
|
+
* 且任何失败都不影响视觉层与消息渲染。
|
|
517
|
+
*/
|
|
518
|
+
function setupConfigPage(child, form) {
|
|
519
|
+
if (typeof require !== 'function') return
|
|
520
|
+
if (typeof child?.configForms?.whileServed !== 'function') return
|
|
521
|
+
if (typeof child?.slots?.inject !== 'function' || typeof child?.slots?.register !== 'function') return
|
|
522
|
+
if (typeof form?.set !== 'function') return
|
|
523
|
+
let jsx
|
|
524
|
+
let jsxs
|
|
525
|
+
let ui
|
|
526
|
+
try {
|
|
527
|
+
const runtime = require('react/jsx-runtime')
|
|
528
|
+
jsx = runtime.jsx
|
|
529
|
+
jsxs = runtime.jsxs
|
|
530
|
+
ui = require('@deepseek-ai/dsh-client-ui-primitives')
|
|
531
|
+
if (typeof ui?.Switch !== 'function' || typeof ui?.SegmentedControl !== 'function') return
|
|
532
|
+
} catch {
|
|
533
|
+
// primitives 不可用(非 desktop 表层):不出配置页。
|
|
534
|
+
return
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
const labelStyle = { fontSize: '14px', lineHeight: '20px', color: 'var(--dsw-alias-label-primary, inherit)' }
|
|
538
|
+
const hintStyle = { fontSize: '12.5px', lineHeight: '18px', color: 'var(--dsw-alias-label-tertiary, inherit)', marginTop: '2px' }
|
|
539
|
+
const rowStyle = { display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: '16px', padding: '10px 0', borderBottom: '1px solid var(--dsw-alias-border-l2, rgba(127,127,127,.18))' }
|
|
540
|
+
const noteStyle = { fontSize: '12.5px', lineHeight: '18px', color: 'var(--dsw-alias-label-tertiary, inherit)', margin: '6px 0 2px' }
|
|
541
|
+
|
|
542
|
+
const fieldValue = (value, spec) => {
|
|
543
|
+
const raw = value?.[spec.field]
|
|
544
|
+
if (raw && typeof raw.get === 'function') return raw.get()
|
|
545
|
+
return raw === undefined || raw === null ? spec.fallback : raw
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
const renderRow = (state, setField, spec) => {
|
|
549
|
+
const writable = state.writable !== false
|
|
550
|
+
const control = spec.kind === 'switch'
|
|
551
|
+
? jsx(ui.Switch, {
|
|
552
|
+
checked: fieldValue(state.value, spec) === true,
|
|
553
|
+
disabled: !writable,
|
|
554
|
+
label: spec.label,
|
|
555
|
+
onChange: (next) => setField(spec.field, next === true),
|
|
556
|
+
})
|
|
557
|
+
: jsx(ui.SegmentedControl, {
|
|
558
|
+
id: 'dsh-structured-' + spec.field,
|
|
559
|
+
value: String(fieldValue(state.value, spec)),
|
|
560
|
+
options: spec.options.map(([value, text]) => ({ value, label: text })),
|
|
561
|
+
disabled: !writable,
|
|
562
|
+
label: spec.label,
|
|
563
|
+
onChange: (next) => setField(spec.field, next),
|
|
564
|
+
})
|
|
565
|
+
return jsxs('div', {
|
|
566
|
+
style: rowStyle,
|
|
567
|
+
children: [
|
|
568
|
+
jsxs('div', { style: { minWidth: 0 }, children: [
|
|
569
|
+
jsx('div', { style: labelStyle, children: spec.label }),
|
|
570
|
+
jsx('div', { style: hintStyle, children: spec.hint }),
|
|
571
|
+
] }),
|
|
572
|
+
control,
|
|
573
|
+
],
|
|
574
|
+
})
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
const StructuredConfigCard = (props) => {
|
|
578
|
+
if (props.view !== undefined && props.view !== 'page') return null
|
|
579
|
+
const read = props.useStructuredConfig ?? props.structuredConfig
|
|
580
|
+
const state = typeof read === 'function' ? read((snapshot) => snapshot) : null
|
|
581
|
+
if (!state || state.status !== 'ready' || typeof state.value !== 'object' || state.value === null) {
|
|
582
|
+
return jsx('p', { style: noteStyle, children: '插件未加载或配置暂不可用。' })
|
|
583
|
+
}
|
|
584
|
+
const setField = typeof props.setField === 'function' ? props.setField : () => {}
|
|
585
|
+
return jsxs('div', {
|
|
586
|
+
'data-dsh-structured-config': '1',
|
|
587
|
+
children: [
|
|
588
|
+
state.writable === false
|
|
589
|
+
? jsx('p', { style: noteStyle, children: '当前配置被更高层覆盖(home patch / CLI overlay),此处只读。' })
|
|
590
|
+
: null,
|
|
591
|
+
jsx('div', { children: CONFIG_FIELDS.map((spec) => renderRow(state, setField, spec)) }),
|
|
592
|
+
jsx('p', { style: noteStyle, children: '会话级开关请用 /structured on | off(按会话生效,不写入这里)。' }),
|
|
593
|
+
],
|
|
594
|
+
})
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
child.effect?.(
|
|
598
|
+
() =>
|
|
599
|
+
child.configForms.whileServed([CONFIG_NS], () => {
|
|
600
|
+
// 样式与 slot 注册同生命周期:命名空间不被服务时一并撤下。
|
|
601
|
+
const removeStyle = ensureConfigStyle()
|
|
602
|
+
const offSlot = child.slots.inject(BUNDLE_CONFIG_SLOT, () =>
|
|
603
|
+
child.slots.register({
|
|
604
|
+
name: BUNDLE_CONFIG_SLOT,
|
|
605
|
+
key: 'dsh-structured',
|
|
606
|
+
inject: () => ({
|
|
607
|
+
hooks: { structuredConfig: form },
|
|
608
|
+
setField: (field, value) => {
|
|
609
|
+
try {
|
|
610
|
+
form.set(field, value)
|
|
611
|
+
} catch {
|
|
612
|
+
// 写入失败不打断页面;快照未变化即用户可见的反馈。
|
|
613
|
+
}
|
|
614
|
+
},
|
|
615
|
+
}),
|
|
616
|
+
}, StructuredConfigCard),
|
|
617
|
+
)
|
|
618
|
+
return () => {
|
|
619
|
+
try {
|
|
620
|
+
offSlot?.()
|
|
621
|
+
} catch {
|
|
622
|
+
// slot 撤销失败交由宿主清理。
|
|
623
|
+
}
|
|
624
|
+
removeStyle()
|
|
625
|
+
}
|
|
626
|
+
}),
|
|
627
|
+
'dsh-structured: bundle config page',
|
|
628
|
+
)
|
|
629
|
+
}
|
|
630
|
+
|
|
455
631
|
let active = false
|
|
456
632
|
let disposeLayer = null
|
|
457
633
|
let currentTheme = 'deepseek'
|
|
@@ -546,7 +722,7 @@ body[data-ds-dark-theme][data-dsh-structured-theme="anthropic"] [class*="_markdo
|
|
|
546
722
|
|
|
547
723
|
return {
|
|
548
724
|
inject: [],
|
|
549
|
-
__test: { inDiff, isDiffCode, decorate },
|
|
725
|
+
__test: { inDiff, isDiffCode, decorate, setupConfigPage },
|
|
550
726
|
apply(ctx) {
|
|
551
727
|
// 默认主题/视觉先启用;随后由 profile Config 的 theme/visual 覆盖。
|
|
552
728
|
enable()
|
|
@@ -576,6 +752,19 @@ body[data-ds-dark-theme][data-dsh-structured-theme="anthropic"] [class*="_markdo
|
|
|
576
752
|
} catch {
|
|
577
753
|
// 无 ctx.inject 的宿主保持默认开启。
|
|
578
754
|
}
|
|
755
|
+
try {
|
|
756
|
+
// 插件详情页配置区与视觉层分开注入:配置页失败不影响主题/视觉联动。
|
|
757
|
+
ctx?.inject?.(['configForms', 'slots'], (child) => {
|
|
758
|
+
try {
|
|
759
|
+
const form = child.configForms?.get('dsh-structured')
|
|
760
|
+
if (form) setupConfigPage(child, form)
|
|
761
|
+
} catch {
|
|
762
|
+
// 配置页不可用不影响视觉层与注入。
|
|
763
|
+
}
|
|
764
|
+
})
|
|
765
|
+
} catch {
|
|
766
|
+
// 无 ctx.inject 的宿主没有配置页,视觉层照常。
|
|
767
|
+
}
|
|
579
768
|
try {
|
|
580
769
|
ctx?.effect?.(() => disable, 'dsh-structured: visual layer')
|
|
581
770
|
} catch {
|
|
@@ -0,0 +1,329 @@
|
|
|
1
|
+
import assert from 'node:assert/strict'
|
|
2
|
+
import { readFileSync } from 'node:fs'
|
|
3
|
+
import test from 'node:test'
|
|
4
|
+
import vm from 'node:vm'
|
|
5
|
+
|
|
6
|
+
// 插件板块详情页配置区:宿主不会从 Config schema 自动生成表单,
|
|
7
|
+
// 配置页必须由插件 client 注册进 plugins.bundle.config(键 = 包名)。
|
|
8
|
+
// 本文件验证:注册条件、slot 键、注入面、卡片渲染与字段控件。
|
|
9
|
+
|
|
10
|
+
function makeDom() {
|
|
11
|
+
const element = () => ({
|
|
12
|
+
children: [],
|
|
13
|
+
appendChild() {},
|
|
14
|
+
remove() {},
|
|
15
|
+
setAttribute() {},
|
|
16
|
+
removeAttribute() {},
|
|
17
|
+
querySelectorAll() {
|
|
18
|
+
return []
|
|
19
|
+
},
|
|
20
|
+
closest() {
|
|
21
|
+
return null
|
|
22
|
+
},
|
|
23
|
+
})
|
|
24
|
+
// 可观察的 style 表:createElement('style') 产物按 id 登记,remove 时注销。
|
|
25
|
+
const styles = new Map()
|
|
26
|
+
const styleElement = () => {
|
|
27
|
+
const style = element()
|
|
28
|
+
style.id = ''
|
|
29
|
+
style.textContent = ''
|
|
30
|
+
style.remove = () => {
|
|
31
|
+
if (style.id) styles.delete(style.id)
|
|
32
|
+
}
|
|
33
|
+
return style
|
|
34
|
+
}
|
|
35
|
+
return {
|
|
36
|
+
styles,
|
|
37
|
+
body: element(),
|
|
38
|
+
documentElement: { setAttribute() {}, removeAttribute() {} },
|
|
39
|
+
head: {
|
|
40
|
+
children: [],
|
|
41
|
+
appendChild(style) {
|
|
42
|
+
this.children.push(style)
|
|
43
|
+
if (style.id) styles.set(style.id, style)
|
|
44
|
+
},
|
|
45
|
+
},
|
|
46
|
+
createElement: (tag) => (tag === 'style' ? styleElement() : element()),
|
|
47
|
+
getElementById: (id) => styles.get(id) ?? null,
|
|
48
|
+
querySelectorAll: () => [],
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function fakeRuntime() {
|
|
53
|
+
const node = (type, props) => ({ type, props })
|
|
54
|
+
return {
|
|
55
|
+
require(spec) {
|
|
56
|
+
if (spec === 'react/jsx-runtime') {
|
|
57
|
+
return { jsx: (type, props) => node(type, props), jsxs: (type, props) => node(type, props) }
|
|
58
|
+
}
|
|
59
|
+
if (spec === '@deepseek-ai/dsh-client-ui-primitives') {
|
|
60
|
+
return {
|
|
61
|
+
Switch: function Switch(props) {
|
|
62
|
+
return { type: Switch, props }
|
|
63
|
+
},
|
|
64
|
+
SegmentedControl: function SegmentedControl(props) {
|
|
65
|
+
return { type: SegmentedControl, props }
|
|
66
|
+
},
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
throw new Error('unexpected require: ' + spec)
|
|
70
|
+
},
|
|
71
|
+
node,
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function loadClient(dom, fakeRequire) {
|
|
76
|
+
const code = readFileSync(new URL('../src/client.js', import.meta.url), 'utf8')
|
|
77
|
+
let module = null
|
|
78
|
+
const window = {
|
|
79
|
+
__ModuleLoader__: {
|
|
80
|
+
load(definition) {
|
|
81
|
+
module = definition.factory(fakeRequire)
|
|
82
|
+
},
|
|
83
|
+
},
|
|
84
|
+
}
|
|
85
|
+
vm.runInNewContext(code, {
|
|
86
|
+
window,
|
|
87
|
+
document: dom,
|
|
88
|
+
MutationObserver: class {
|
|
89
|
+
observe() {}
|
|
90
|
+
disconnect() {}
|
|
91
|
+
},
|
|
92
|
+
requestAnimationFrame: (fn) => fn(),
|
|
93
|
+
cancelAnimationFrame() {},
|
|
94
|
+
setTimeout: (fn) => fn(),
|
|
95
|
+
clearTimeout() {},
|
|
96
|
+
console: { info() {}, warn() {}, error() {} },
|
|
97
|
+
}, { filename: 'src/client.js' })
|
|
98
|
+
assert.ok(module, 'client module should register')
|
|
99
|
+
return module
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function makeChild(form, slots) {
|
|
103
|
+
const child = {
|
|
104
|
+
configForms: {
|
|
105
|
+
get(id) {
|
|
106
|
+
return id === 'dsh-structured' ? form : undefined
|
|
107
|
+
},
|
|
108
|
+
whileServed(namespaces, register) {
|
|
109
|
+
// 把 register 返回的组合 disposer 暴露给测试,模拟命名空间停服。
|
|
110
|
+
if (served) child.__disposeRegistration = register()
|
|
111
|
+
return () => {}
|
|
112
|
+
},
|
|
113
|
+
},
|
|
114
|
+
slots,
|
|
115
|
+
effect(fn) {
|
|
116
|
+
return fn()
|
|
117
|
+
},
|
|
118
|
+
}
|
|
119
|
+
return child
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
let served = true
|
|
123
|
+
|
|
124
|
+
function collectRegistration() {
|
|
125
|
+
let registered = null
|
|
126
|
+
const slots = {
|
|
127
|
+
inject(name, factory) {
|
|
128
|
+
assert.equal(name, 'plugins.bundle.config')
|
|
129
|
+
return factory()
|
|
130
|
+
},
|
|
131
|
+
register(options, component) {
|
|
132
|
+
registered = { options, component }
|
|
133
|
+
return () => {}
|
|
134
|
+
},
|
|
135
|
+
}
|
|
136
|
+
return { slots, get: () => registered }
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
test('config page:宿主服务 describe 命名空间时注册 bundle 配置页', () => {
|
|
140
|
+
served = true
|
|
141
|
+
const { require: fakeRequire } = fakeRuntime()
|
|
142
|
+
const module = loadClient(makeDom(), fakeRequire)
|
|
143
|
+
const snapshot = { status: 'ready', writable: true, value: { theme: 'deepseek' } }
|
|
144
|
+
const form = {
|
|
145
|
+
setCalls: [],
|
|
146
|
+
set(field, value) {
|
|
147
|
+
this.setCalls.push([field, value])
|
|
148
|
+
return Promise.resolve(true)
|
|
149
|
+
},
|
|
150
|
+
getSnapshot() {
|
|
151
|
+
return snapshot
|
|
152
|
+
},
|
|
153
|
+
subscribe() {
|
|
154
|
+
return () => {}
|
|
155
|
+
},
|
|
156
|
+
}
|
|
157
|
+
const { slots, get } = collectRegistration()
|
|
158
|
+
module.__test.setupConfigPage(makeChild(form, slots), form)
|
|
159
|
+
|
|
160
|
+
const registration = get()
|
|
161
|
+
assert.ok(registration, 'should register into plugins.bundle.config')
|
|
162
|
+
assert.equal(registration.options.name, 'plugins.bundle.config')
|
|
163
|
+
assert.equal(registration.options.key, 'dsh-structured')
|
|
164
|
+
|
|
165
|
+
const face = registration.options.inject()
|
|
166
|
+
assert.equal(face.hooks.structuredConfig, form)
|
|
167
|
+
assert.equal(typeof face.setField, 'function')
|
|
168
|
+
face.setField('visual', false)
|
|
169
|
+
assert.deepEqual(form.setCalls[0], ['visual', false])
|
|
170
|
+
})
|
|
171
|
+
|
|
172
|
+
test('config page:describe 未服务命名空间时不注册', () => {
|
|
173
|
+
served = false
|
|
174
|
+
const { require: fakeRequire } = fakeRuntime()
|
|
175
|
+
const module = loadClient(makeDom(), fakeRequire)
|
|
176
|
+
const form = { getSnapshot() { return null }, subscribe() { return () => {} } }
|
|
177
|
+
const { slots, get } = collectRegistration()
|
|
178
|
+
module.__test.setupConfigPage(makeChild(form, slots), form)
|
|
179
|
+
assert.equal(get(), null)
|
|
180
|
+
served = true
|
|
181
|
+
})
|
|
182
|
+
|
|
183
|
+
test('config page:primitives 或 require 缺失时静默跳过', () => {
|
|
184
|
+
const module = loadClient(makeDom(), undefined)
|
|
185
|
+
const form = { getSnapshot() { return null }, subscribe() { return () => {} } }
|
|
186
|
+
const { slots, get } = collectRegistration()
|
|
187
|
+
module.__test.setupConfigPage(makeChild(form, slots), form)
|
|
188
|
+
assert.equal(get(), null)
|
|
189
|
+
})
|
|
190
|
+
|
|
191
|
+
test('config page:卡片渲染六字段,switch/segmented 值来自快照', () => {
|
|
192
|
+
served = true
|
|
193
|
+
const { require: fakeRequire, node } = fakeRuntime()
|
|
194
|
+
const module = loadClient(makeDom(), fakeRequire)
|
|
195
|
+
const snapshot = {
|
|
196
|
+
status: 'ready',
|
|
197
|
+
writable: true,
|
|
198
|
+
value: {
|
|
199
|
+
theme: 'codex',
|
|
200
|
+
visual: false,
|
|
201
|
+
defaultEnabled: true,
|
|
202
|
+
tailReminder: true,
|
|
203
|
+
includeSubagents: false,
|
|
204
|
+
genui: 'auto',
|
|
205
|
+
},
|
|
206
|
+
}
|
|
207
|
+
const form = {
|
|
208
|
+
set() {
|
|
209
|
+
return Promise.resolve(true)
|
|
210
|
+
},
|
|
211
|
+
getSnapshot() {
|
|
212
|
+
return snapshot
|
|
213
|
+
},
|
|
214
|
+
subscribe() {
|
|
215
|
+
return () => {}
|
|
216
|
+
},
|
|
217
|
+
}
|
|
218
|
+
const { slots, get } = collectRegistration()
|
|
219
|
+
module.__test.setupConfigPage(makeChild(form, slots), form)
|
|
220
|
+
const { component } = get()
|
|
221
|
+
|
|
222
|
+
const tree = component({
|
|
223
|
+
view: 'page',
|
|
224
|
+
useStructuredConfig: (selector) => selector(snapshot),
|
|
225
|
+
setField: () => {},
|
|
226
|
+
})
|
|
227
|
+
assert.equal(tree.props['data-dsh-structured-config'], '1')
|
|
228
|
+
|
|
229
|
+
const flat = []
|
|
230
|
+
const walk = (value) => {
|
|
231
|
+
if (!value || typeof value !== 'object') return
|
|
232
|
+
flat.push(value)
|
|
233
|
+
if (Array.isArray(value.props?.children)) value.props.children.forEach((child) => walk(child))
|
|
234
|
+
else walk(value.props?.children)
|
|
235
|
+
}
|
|
236
|
+
walk(tree)
|
|
237
|
+
|
|
238
|
+
const switches = flat.filter((element) => element.type?.name === 'Switch')
|
|
239
|
+
const segmented = flat.filter((element) => element.type?.name === 'SegmentedControl')
|
|
240
|
+
assert.equal(switches.length, 4)
|
|
241
|
+
assert.equal(segmented.length, 2)
|
|
242
|
+
|
|
243
|
+
const visual = switches.find((element) => element.props.label.includes('视觉层'))
|
|
244
|
+
assert.equal(visual.props.checked, false)
|
|
245
|
+
const includeSubagents = switches.find((element) => element.props.label.includes('子代理'))
|
|
246
|
+
assert.equal(includeSubagents.props.checked, false)
|
|
247
|
+
const theme = segmented.find((element) => element.props.id === 'dsh-structured-theme')
|
|
248
|
+
assert.equal(theme.props.value, 'codex')
|
|
249
|
+
// 卡片在 vm 沙箱里构建:数组跨 realm,序列化后比较。
|
|
250
|
+
assert.equal(JSON.stringify(theme.props.options.map((option) => option.value)), JSON.stringify(['deepseek', 'codex', 'anthropic']))
|
|
251
|
+
const genui = segmented.find((element) => element.props.id === 'dsh-structured-genui')
|
|
252
|
+
assert.equal(genui.props.value, 'auto')
|
|
253
|
+
})
|
|
254
|
+
|
|
255
|
+
test('config page:插件未就绪与只读态的降级渲染', () => {
|
|
256
|
+
served = true
|
|
257
|
+
const { require: fakeRequire } = fakeRuntime()
|
|
258
|
+
const module = loadClient(makeDom(), fakeRequire)
|
|
259
|
+
const form = {
|
|
260
|
+
set() {
|
|
261
|
+
return Promise.resolve(true)
|
|
262
|
+
},
|
|
263
|
+
getSnapshot() {
|
|
264
|
+
return null
|
|
265
|
+
},
|
|
266
|
+
subscribe() {
|
|
267
|
+
return () => {}
|
|
268
|
+
},
|
|
269
|
+
}
|
|
270
|
+
const { slots, get } = collectRegistration()
|
|
271
|
+
module.__test.setupConfigPage(makeChild(form, slots), form)
|
|
272
|
+
const { component } = get()
|
|
273
|
+
|
|
274
|
+
const unavailable = component({ view: 'page', useStructuredConfig: (selector) => selector({ status: 'unavailable' }) })
|
|
275
|
+
assert.match(String(unavailable.props.children), /暂不可用/)
|
|
276
|
+
|
|
277
|
+
const readonly = component({
|
|
278
|
+
view: 'page',
|
|
279
|
+
useStructuredConfig: (selector) => selector({ status: 'ready', writable: false, value: { theme: 'deepseek' } }),
|
|
280
|
+
setField: () => {},
|
|
281
|
+
})
|
|
282
|
+
const flat = []
|
|
283
|
+
const walk = (value) => {
|
|
284
|
+
if (!value || typeof value !== 'object') return
|
|
285
|
+
flat.push(value)
|
|
286
|
+
if (Array.isArray(value.props?.children)) value.props.children.forEach((child) => walk(child))
|
|
287
|
+
else walk(value.props?.children)
|
|
288
|
+
}
|
|
289
|
+
walk(readonly)
|
|
290
|
+
const switches = flat.filter((element) => element.type?.name === 'Switch')
|
|
291
|
+
assert.equal(switches.length, 4)
|
|
292
|
+
for (const element of switches) assert.equal(element.props.disabled, true)
|
|
293
|
+
})
|
|
294
|
+
|
|
295
|
+
test('config page:主题分段按钮带各主题强调色,随注册注入/撤销移除', () => {
|
|
296
|
+
served = true
|
|
297
|
+
const { require: fakeRequire } = fakeRuntime()
|
|
298
|
+
const dom = makeDom()
|
|
299
|
+
const module = loadClient(dom, fakeRequire)
|
|
300
|
+
const form = {
|
|
301
|
+
set() {
|
|
302
|
+
return Promise.resolve(true)
|
|
303
|
+
},
|
|
304
|
+
getSnapshot() {
|
|
305
|
+
return { status: 'ready', writable: true, value: {} }
|
|
306
|
+
},
|
|
307
|
+
subscribe() {
|
|
308
|
+
return () => {}
|
|
309
|
+
},
|
|
310
|
+
}
|
|
311
|
+
const { slots } = collectRegistration()
|
|
312
|
+
const child = makeChild(form, slots)
|
|
313
|
+
module.__test.setupConfigPage(child, form)
|
|
314
|
+
|
|
315
|
+
const style = dom.getElementById('dsh-structured-config-style')
|
|
316
|
+
assert.ok(style, 'config style should be injected with the slot registration')
|
|
317
|
+
// 三套主题的强调色选择器齐全(明暗两态),与视觉层 token 同源。
|
|
318
|
+
for (const token of ['#2563EB', '#10A37F', '#CC785C', '#60A5FA', '#34D399', '#E8A087']) {
|
|
319
|
+
assert.ok(style.textContent.includes(token), 'missing theme token ' + token)
|
|
320
|
+
}
|
|
321
|
+
assert.match(style.textContent, /#dsh-structured-theme-deepseek\[aria-selected="true"\]/)
|
|
322
|
+
assert.match(style.textContent, /#dsh-structured-theme-codex\[aria-selected="true"\]/)
|
|
323
|
+
assert.match(style.textContent, /#dsh-structured-theme-anthropic\[aria-selected="true"\]/)
|
|
324
|
+
assert.match(style.textContent, /body\[data-ds-dark-theme\]/)
|
|
325
|
+
|
|
326
|
+
// 命名空间停服:slot 与样式一并撤下。
|
|
327
|
+
child.__disposeRegistration?.()
|
|
328
|
+
assert.equal(dom.getElementById('dsh-structured-config-style'), null)
|
|
329
|
+
})
|