dsh-structured 0.9.4 → 0.9.9

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.
@@ -1,290 +1,299 @@
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
- function makeDom() {
7
- const styles = new Map()
8
- const attrs = new Map()
9
- const element = () => ({
10
- children: [],
11
- appendChild(child) {
12
- this.children.push(child)
13
- },
14
- remove() {},
15
- setAttribute(name, value) {
16
- this[name] = value
17
- },
18
- removeAttribute(name) {
19
- delete this[name]
20
- },
21
- querySelectorAll() {
22
- return []
23
- },
24
- closest() {
25
- return null
26
- },
27
- tagName: 'P',
28
- })
29
- const document = {
30
- body: element(),
31
- documentElement: {
32
- setAttribute(name, value) {
33
- attrs.set(name, value)
34
- },
35
- removeAttribute(name) {
36
- attrs.delete(name)
37
- },
38
- getAttribute(name) {
39
- return attrs.get(name)
40
- },
41
- },
42
- head: {
43
- children: [],
44
- appendChild(style) {
45
- this.children.push(style)
46
- styles.set(style.id, style)
47
- },
48
- },
49
- createElement() {
50
- const style = element()
51
- style.textContent = ''
52
- style.id = ''
53
- style.remove = () => {
54
- styles.delete(style.id)
55
- }
56
- return style
57
- },
58
- getElementById(id) {
59
- return styles.get(id) ?? null
60
- },
61
- querySelectorAll() {
62
- return []
63
- },
64
- }
65
- return { document, styles, attrs }
66
- }
67
-
68
- function loadClient(dom) {
69
- const code = readFileSync(new URL('../src/client.js', import.meta.url), 'utf8')
70
- let module = null
71
- const window = {
72
- __ModuleLoader__: {
73
- load(definition) {
74
- module = definition.factory()
75
- },
76
- },
77
- }
78
- const context = {
79
- window,
80
- document: dom.document,
81
- MutationObserver: class {
82
- observe() {}
83
- disconnect() {}
84
- },
85
- requestAnimationFrame: (fn) => fn(),
86
- cancelAnimationFrame: () => {},
87
- setTimeout: (fn) => fn(),
88
- clearTimeout: () => {},
89
- console: { info() {}, warn() {}, error() {} },
90
- }
91
- vm.runInNewContext(code, context, { filename: 'src/client.js' })
92
- assert.ok(module, 'client module should register')
93
- return module
94
- }
95
-
96
- function makeContext(form) {
97
- return {
98
- inject(services, callback) {
99
- callback({
100
- configForms: {
101
- get(id) {
102
- return id === 'dsh-structured' ? form : undefined
103
- },
104
- },
105
- effect(fn) {
106
- return fn()
107
- },
108
- })
109
- },
110
- effect(fn) {
111
- return fn()
112
- },
113
- }
114
- }
115
-
116
- test('client:visual=false 停用视觉层,visual=true 恢复', () => {
117
- const dom = makeDom()
118
- const form = {
119
- value: { visual: false, theme: 'codex' },
120
- listeners: new Set(),
121
- getSnapshot() {
122
- return { value: this.value }
123
- },
124
- subscribe(listener) {
125
- this.listeners.add(listener)
126
- return () => this.listeners.delete(listener)
127
- },
128
- }
129
- const module = loadClient(dom)
130
- const dispose = module.apply(makeContext(form))
131
-
132
- assert.equal(dom.styles.has('dsh-structured-skin-style'), false)
133
- assert.equal(dom.attrs.has('data-dsh-structured'), false)
134
- assert.equal(dom.document.body['data-dsh-structured-theme'], undefined)
135
-
136
- form.value = { visual: true, theme: 'codex' }
137
- for (const listener of form.listeners) listener()
138
- assert.equal(dom.styles.has('dsh-structured-skin-style'), true)
139
- assert.equal(dom.attrs.get('data-dsh-structured'), 'active')
140
- assert.equal(dom.document.body['data-dsh-structured-theme'], 'codex')
141
-
142
- form.value = { visual: true, theme: 'anthropic' }
143
- for (const listener of form.listeners) listener()
144
- assert.equal(dom.document.body['data-dsh-structured-theme'], 'anthropic')
145
-
146
- form.value = { visual: true, theme: 'bogus' }
147
- for (const listener of form.listeners) listener()
148
- assert.equal(dom.document.body['data-dsh-structured-theme'], 'deepseek')
149
-
150
- form.value = { visual: false, theme: 'anthropic' }
151
- for (const listener of form.listeners) listener()
152
- assert.equal(dom.styles.has('dsh-structured-skin-style'), false)
153
- assert.equal(dom.attrs.has('data-dsh-structured'), false)
154
- assert.equal(dom.document.body['data-dsh-structured-theme'], undefined)
155
-
156
- dispose()
157
- })
158
-
159
- test('client:configForms 不可用时视觉层保持默认开启', () => {
160
- const dom = makeDom()
161
- const module = loadClient(dom)
162
- const ctx = {
163
- inject(services, callback) {
164
- // 模拟服务缺失:不调用 callback
165
- },
166
- effect(fn) {
167
- return fn()
168
- },
169
- }
170
- const dispose = module.apply(ctx)
171
- assert.equal(dom.styles.has('dsh-structured-skin-style'), true)
172
- assert.equal(dom.attrs.get('data-dsh-structured'), 'active')
173
- dispose()
174
- })
175
-
176
- function attrsElement(extra = {}) {
177
- const attrs = new Map()
178
- return {
179
- attrs,
180
- setAttribute(name, value) {
181
- attrs.set(name, value)
182
- },
183
- removeAttribute(name) {
184
- attrs.delete(name)
185
- },
186
- getAttribute(name) {
187
- return attrs.get(name)
188
- },
189
- ...extra,
190
- }
191
- }
192
-
193
- test('client:diff 代码块不套插件 chrome,普通代码块才套', () => {
194
- const dom = makeDom()
195
- const module = loadClient(dom)
196
- const diffCode = attrsElement({
197
- querySelector: () => ({ textContent: 'diff' }),
198
- closest: () => null,
199
- })
200
- const jsCode = attrsElement({
201
- querySelector: () => ({ textContent: 'js' }),
202
- closest: () => null,
203
- })
204
- const diffCopyCode = attrsElement({
205
- querySelector: () => ({ textContent: 'diff复制' }),
206
- closest: () => null,
207
- })
208
- assert.equal(module.__test.isDiffCode(diffCopyCode), true)
209
- const root = {
210
- querySelectorAll(selector) {
211
- if (selector.includes('.md-code-block')) return [diffCode, jsCode, diffCopyCode]
212
- return []
213
- },
214
- }
215
- module.__test.decorate(root)
216
- assert.equal(diffCode.attrs.get('data-dsh-structured-skip'), 'code')
217
- assert.equal(diffCode.attrs.get('data-dsh-structured-code'), undefined)
218
- assert.equal(diffCode.attrs.get('data-dsh-structured-lang'), undefined)
219
- assert.equal(diffCopyCode.attrs.get('data-dsh-structured-skip'), 'code')
220
- assert.equal(jsCode.attrs.get('data-dsh-structured-code'), '1')
221
- assert.equal(jsCode.attrs.get('data-dsh-structured-lang'), 'js')
222
- })
223
-
224
- test('client:普通状态表仍套插件表格样式并标状态行', () => {
225
- const dom = makeDom()
226
- const module = loadClient(dom)
227
- const row = attrsElement({
228
- textContent: '✅ 完成',
229
- cells: [{ textContent: '✅ 完成' }],
230
- closest: () => null,
231
- })
232
- const table = attrsElement({
233
- closest: () => null,
234
- querySelectorAll: (selector) => (selector === 'tbody tr' ? [row] : []),
235
- })
236
- const root = {
237
- querySelectorAll(selector) {
238
- if (selector === '[class*="_markdown_"] table') return [table]
239
- if (selector === '[class*="_markdown_"] tbody tr') return [row]
240
- if (selector === '[class*="_markdown_"] table tr') return [row]
241
- return []
242
- },
243
- }
244
- module.__test.decorate(root)
245
- assert.equal(table.attrs.get('data-dsh-structured-status-table'), '1')
246
- assert.equal(table.attrs.get('data-dsh-structured-skip'), undefined)
247
- assert.equal(row.attrs.get('data-dsh-structured-row'), 'success')
248
- })
249
-
250
- test('client:GenUI 区域不写入 skip,保持旧行为', () => {
251
- const dom = makeDom()
252
- const module = loadClient(dom)
253
- const code = attrsElement({
254
- querySelector: () => ({ textContent: 'js' }),
255
- closest: (selector) => (selector.includes('genui') ? {} : null),
256
- })
257
- const table = attrsElement({
258
- closest: (selector) => (selector.includes('genui') ? {} : null),
259
- querySelectorAll: () => [],
260
- })
261
- const root = {
262
- querySelectorAll(selector) {
263
- if (selector.includes('.md-code-block')) return [code]
264
- if (selector === '[class*="_markdown_"] table') return [table]
265
- return []
266
- },
267
- }
268
- module.__test.decorate(root)
269
- assert.equal(code.attrs.get('data-dsh-structured-skip'), undefined)
270
- assert.equal(code.attrs.get('data-dsh-structured-code'), undefined)
271
- assert.equal(table.attrs.get('data-dsh-structured-skip'), undefined)
272
- })
273
-
274
- test('client:diff 表格不套插件表格样式', () => {
275
- const dom = makeDom()
276
- const module = loadClient(dom)
277
- const table = attrsElement({
278
- closest: (selector) => (selector.includes('[data-diff]') ? {} : null),
279
- querySelectorAll: () => [],
280
- })
281
- const root = {
282
- querySelectorAll(selector) {
283
- if (selector === '[class*="_markdown_"] table') return [table]
284
- return []
285
- },
286
- }
287
- module.__test.decorate(root)
288
- assert.equal(table.attrs.get('data-dsh-structured-skip'), 'table')
289
- assert.equal(table.attrs.get('data-dsh-structured-status-table'), undefined)
290
- })
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
+ function makeDom() {
7
+ const styles = new Map()
8
+ const attrs = new Map()
9
+ const element = () => ({
10
+ children: [],
11
+ appendChild(child) {
12
+ this.children.push(child)
13
+ },
14
+ remove() {},
15
+ setAttribute(name, value) {
16
+ this[name] = value
17
+ },
18
+ removeAttribute(name) {
19
+ delete this[name]
20
+ },
21
+ querySelectorAll() {
22
+ return []
23
+ },
24
+ closest() {
25
+ return null
26
+ },
27
+ tagName: 'P',
28
+ })
29
+ const document = {
30
+ body: element(),
31
+ documentElement: {
32
+ setAttribute(name, value) {
33
+ attrs.set(name, value)
34
+ },
35
+ removeAttribute(name) {
36
+ attrs.delete(name)
37
+ },
38
+ getAttribute(name) {
39
+ return attrs.get(name)
40
+ },
41
+ },
42
+ head: {
43
+ children: [],
44
+ appendChild(style) {
45
+ this.children.push(style)
46
+ styles.set(style.id, style)
47
+ },
48
+ },
49
+ createElement() {
50
+ const style = element()
51
+ style.textContent = ''
52
+ style.id = ''
53
+ style.remove = () => {
54
+ styles.delete(style.id)
55
+ }
56
+ return style
57
+ },
58
+ getElementById(id) {
59
+ return styles.get(id) ?? null
60
+ },
61
+ querySelectorAll() {
62
+ return []
63
+ },
64
+ }
65
+ return { document, styles, attrs }
66
+ }
67
+
68
+ function loadClient(dom) {
69
+ const code = readFileSync(new URL('../src/client.js', import.meta.url), 'utf8')
70
+ let module = null
71
+ const window = {
72
+ __ModuleLoader__: {
73
+ load(definition) {
74
+ module = definition.factory()
75
+ },
76
+ },
77
+ }
78
+ const context = {
79
+ window,
80
+ document: dom.document,
81
+ MutationObserver: class {
82
+ observe() {}
83
+ disconnect() {}
84
+ },
85
+ requestAnimationFrame: (fn) => fn(),
86
+ cancelAnimationFrame: () => {},
87
+ setTimeout: (fn) => fn(),
88
+ clearTimeout: () => {},
89
+ console: { info() {}, warn() {}, error() {} },
90
+ }
91
+ vm.runInNewContext(code, context, { filename: 'src/client.js' })
92
+ assert.ok(module, 'client module should register')
93
+ return module
94
+ }
95
+
96
+ function makeContext(form) {
97
+ return {
98
+ inject(services, callback) {
99
+ callback({
100
+ configForms: {
101
+ get(id) {
102
+ return id === 'dsh-structured' ? form : undefined
103
+ },
104
+ },
105
+ effect(fn) {
106
+ return fn()
107
+ },
108
+ })
109
+ },
110
+ effect(fn) {
111
+ return fn()
112
+ },
113
+ }
114
+ }
115
+
116
+ test('client:visual=false 停用视觉层,visual=true 恢复', () => {
117
+ const dom = makeDom()
118
+ const form = {
119
+ value: { visual: false, theme: 'codex' },
120
+ listeners: new Set(),
121
+ getSnapshot() {
122
+ return { value: this.value }
123
+ },
124
+ subscribe(listener) {
125
+ this.listeners.add(listener)
126
+ return () => this.listeners.delete(listener)
127
+ },
128
+ }
129
+ const module = loadClient(dom)
130
+ const dispose = module.apply(makeContext(form))
131
+
132
+ assert.equal(dom.styles.has('dsh-structured-skin-style'), false)
133
+ assert.equal(dom.attrs.has('data-dsh-structured'), false)
134
+ assert.equal(dom.document.body['data-dsh-structured-theme'], undefined)
135
+
136
+ form.value = { visual: true, theme: 'codex' }
137
+ for (const listener of form.listeners) listener()
138
+ assert.equal(dom.styles.has('dsh-structured-skin-style'), true)
139
+ assert.equal(dom.attrs.get('data-dsh-structured'), 'active')
140
+ assert.equal(dom.document.body['data-dsh-structured-theme'], 'codex')
141
+
142
+ form.value = { visual: true, theme: 'anthropic' }
143
+ for (const listener of form.listeners) listener()
144
+ assert.equal(dom.document.body['data-dsh-structured-theme'], 'anthropic')
145
+
146
+ form.value = { visual: true, theme: 'bogus' }
147
+ for (const listener of form.listeners) listener()
148
+ assert.equal(dom.document.body['data-dsh-structured-theme'], 'deepseek')
149
+
150
+ form.value = { visual: false, theme: 'anthropic' }
151
+ for (const listener of form.listeners) listener()
152
+ assert.equal(dom.styles.has('dsh-structured-skin-style'), false)
153
+ assert.equal(dom.attrs.has('data-dsh-structured'), false)
154
+ assert.equal(dom.document.body['data-dsh-structured-theme'], undefined)
155
+
156
+ dispose()
157
+ })
158
+
159
+ test('client:configForms 不可用时视觉层保持默认开启', () => {
160
+ const dom = makeDom()
161
+ const module = loadClient(dom)
162
+ const ctx = {
163
+ inject(services, callback) {
164
+ // 模拟服务缺失:不调用 callback
165
+ },
166
+ effect(fn) {
167
+ return fn()
168
+ },
169
+ }
170
+ const dispose = module.apply(ctx)
171
+ assert.equal(dom.styles.has('dsh-structured-skin-style'), true)
172
+ assert.equal(dom.attrs.get('data-dsh-structured'), 'active')
173
+ dispose()
174
+ })
175
+
176
+ function attrsElement(extra = {}) {
177
+ const attrs = new Map()
178
+ return {
179
+ attrs,
180
+ setAttribute(name, value) {
181
+ attrs.set(name, value)
182
+ },
183
+ removeAttribute(name) {
184
+ attrs.delete(name)
185
+ },
186
+ getAttribute(name) {
187
+ return attrs.get(name)
188
+ },
189
+ ...extra,
190
+ }
191
+ }
192
+
193
+ test('client:diff 代码块不套插件 chrome,普通代码块才套', () => {
194
+ const dom = makeDom()
195
+ const module = loadClient(dom)
196
+ const diffCode = attrsElement({
197
+ querySelector: () => ({ textContent: 'diff' }),
198
+ closest: () => null,
199
+ })
200
+ const jsCode = attrsElement({
201
+ querySelector: () => ({ textContent: 'js' }),
202
+ closest: () => null,
203
+ })
204
+ const diffCopyCode = attrsElement({
205
+ querySelector: () => ({ textContent: 'diff复制' }),
206
+ closest: () => null,
207
+ })
208
+ assert.equal(module.__test.isDiffCode(diffCopyCode), true)
209
+ const root = {
210
+ querySelectorAll(selector) {
211
+ if (selector.includes('.md-code-block')) return [diffCode, jsCode, diffCopyCode]
212
+ return []
213
+ },
214
+ }
215
+ module.__test.decorate(root)
216
+ assert.equal(diffCode.attrs.get('data-dsh-structured-skip'), 'code')
217
+ assert.equal(diffCode.attrs.get('data-dsh-structured-code'), undefined)
218
+ assert.equal(diffCode.attrs.get('data-dsh-structured-lang'), undefined)
219
+ assert.equal(diffCopyCode.attrs.get('data-dsh-structured-skip'), 'code')
220
+ assert.equal(jsCode.attrs.get('data-dsh-structured-code'), '1')
221
+ assert.equal(jsCode.attrs.get('data-dsh-structured-lang'), 'js')
222
+ })
223
+
224
+ test('client:普通状态表仍套插件表格样式并标状态行', () => {
225
+ const dom = makeDom()
226
+ const module = loadClient(dom)
227
+ const row = attrsElement({
228
+ textContent: '✅ 完成',
229
+ cells: [{ textContent: '✅ 完成' }],
230
+ closest: () => null,
231
+ })
232
+ const table = attrsElement({
233
+ closest: () => null,
234
+ querySelectorAll: (selector) => (selector === 'tbody tr' ? [row] : []),
235
+ })
236
+ const root = {
237
+ querySelectorAll(selector) {
238
+ if (selector === '[class*="_markdown_"] table') return [table]
239
+ if (selector === '[class*="_markdown_"] tbody tr') return [row]
240
+ if (selector === '[class*="_markdown_"] table tr') return [row]
241
+ return []
242
+ },
243
+ }
244
+ module.__test.decorate(root)
245
+ assert.equal(table.attrs.get('data-dsh-structured-status-table'), '1')
246
+ assert.equal(table.attrs.get('data-dsh-structured-skip'), undefined)
247
+ assert.equal(row.attrs.get('data-dsh-structured-row'), 'success')
248
+ })
249
+
250
+ test('client:GenUI 区域不写入 skip,保持旧行为', () => {
251
+ const dom = makeDom()
252
+ const module = loadClient(dom)
253
+ const code = attrsElement({
254
+ querySelector: () => ({ textContent: 'js' }),
255
+ closest: (selector) => (selector.includes('genui') ? {} : null),
256
+ })
257
+ const table = attrsElement({
258
+ closest: (selector) => (selector.includes('genui') ? {} : null),
259
+ querySelectorAll: () => [],
260
+ })
261
+ const root = {
262
+ querySelectorAll(selector) {
263
+ if (selector.includes('.md-code-block')) return [code]
264
+ if (selector === '[class*="_markdown_"] table') return [table]
265
+ return []
266
+ },
267
+ }
268
+ module.__test.decorate(root)
269
+ assert.equal(code.attrs.get('data-dsh-structured-skip'), undefined)
270
+ assert.equal(code.attrs.get('data-dsh-structured-code'), undefined)
271
+ assert.equal(table.attrs.get('data-dsh-structured-skip'), undefined)
272
+ })
273
+
274
+ test('client:diff 表格不套插件表格样式', () => {
275
+ const dom = makeDom()
276
+ const module = loadClient(dom)
277
+ const table = attrsElement({
278
+ closest: (selector) => (selector.includes('[data-diff]') ? {} : null),
279
+ querySelectorAll: () => [],
280
+ })
281
+ const root = {
282
+ querySelectorAll(selector) {
283
+ if (selector === '[class*="_markdown_"] table') return [table]
284
+ return []
285
+ },
286
+ }
287
+ module.__test.decorate(root)
288
+ assert.equal(table.attrs.get('data-dsh-structured-skip'), 'table')
289
+ assert.equal(table.attrs.get('data-dsh-structured-status-table'), undefined)
290
+ })
291
+
292
+ test('client:标题图标使用悬挂缩进,换行行不顶回左边', () => {
293
+ const dom = makeDom()
294
+ loadClient(dom).apply(makeContext(null))
295
+ const css = dom.styles.get('dsh-structured-skin-style')?.textContent ?? ''
296
+ assert.match(css, /h2\[data-dsh-structured-icon\]\{padding-left:42px;text-indent:-30px\}/)
297
+ assert.match(css, /h3\[data-dsh-structured-icon\]\{padding-left:30px;text-indent:-30px\}/)
298
+ assert.match(css, /h2\[data-dsh-structured-icon\]::before,[^{]*\{[^}]*text-indent:0/)
299
+ })
@@ -0,0 +1,80 @@
1
+ <!doctype html>
2
+ <html lang="zh-CN">
3
+ <head>
4
+ <meta charset="utf-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
6
+ <title>dsh-structured 0.9.6 fixture</title>
7
+ <style>
8
+ html,body{margin:0;padding:0;background:#f4f6f8;color:#111827;font-family:system-ui,-apple-system,"Segoe UI","Microsoft YaHei",sans-serif}
9
+ .stage{padding:40px 28px 64px;display:flex;justify-content:center}
10
+ .reply{width:980px;max-width:calc(100vw - 56px)}
11
+ .meta{font-size:12px;color:#6b7280;margin:0 0 16px 2px}
12
+ .surface{background:#fff;border:1px solid #e5e7eb;border-radius:16px;padding:28px}
13
+ .markdown{font-size:15px;line-height:1.72}
14
+ body[data-dsh-structured-theme="codex"]{background:#f7fbf9}
15
+ body[data-dsh-structured-theme="anthropic"]{background:#f5f1e8}
16
+ body[data-dsh-structured-theme="anthropic"] .surface{background:#faf9f5;border-color:#e5e0d5}
17
+ body[data-ds-dark-theme]{background:#0b1220;color:#e5e7eb}
18
+ body[data-ds-dark-theme] .surface{background:#111827;border-color:#374151}
19
+ body[data-ds-dark-theme][data-dsh-structured-theme="codex"]{background:#07100d}
20
+ body[data-ds-dark-theme][data-dsh-structured-theme="codex"] .surface{background:#0b1512;border-color:#233a31}
21
+ body[data-ds-dark-theme][data-dsh-structured-theme="anthropic"]{background:#141413}
22
+ body[data-ds-dark-theme][data-dsh-structured-theme="anthropic"] .surface{background:#1f1e1d;border-color:#3a3733}
23
+ html:has(body[data-ds-dark-theme]){background:#0b1220}
24
+ html:has(body[data-ds-dark-theme][data-dsh-structured-theme="codex"]){background:#07100d}
25
+ html:has(body[data-ds-dark-theme][data-dsh-structured-theme="anthropic"]){background:#141413}
26
+ </style>
27
+ </head>
28
+ <body>
29
+ <div class="stage">
30
+ <div class="reply">
31
+ <div class="meta">助手回复</div>
32
+ <div class="surface">
33
+ <div class="_markdown_fixture markdown">
34
+ <blockquote data-dsh-structured-card="1" data-dsh-structured-card-type="statement" data-dsh-structured-tone="info">
35
+ <p><strong>这个方案可以推进,但先补一次真实用户验证</strong><br>方向、成本、排期都成立;真实用户验证是唯一缺口。</p>
36
+ </blockquote>
37
+ <p data-dsh-structured-lead="table"><strong>表格结论:</strong> 三项里两项成立,一项待验证。</p>
38
+ <table data-dsh-structured-status-table="1">
39
+ <thead><tr><th>项</th><th>状态</th><th>说明</th></tr></thead>
40
+ <tbody>
41
+ <tr data-dsh-structured-row="success"><td>✅ 完成</td><td>方向</td><td>和现有目标一致</td></tr>
42
+ <tr data-dsh-structured-row="success"><td>✅ 完成</td><td>成本</td><td>不新增预算</td></tr>
43
+ <tr data-dsh-structured-row="warn"><td>⚠️ 待做</td><td>用户验证</td><td>需要 5 个真实用户</td></tr>
44
+ </tbody>
45
+ </table>
46
+ <blockquote data-dsh-structured-card="1" data-dsh-structured-card-type="risk" data-dsh-structured-tone="warn">
47
+ <p><strong>风险</strong><br>用户验证没做,直接推进会把假设当结论。</p>
48
+ </blockquote>
49
+ <blockquote data-dsh-structured-card="1" data-dsh-structured-card-type="next" data-dsh-structured-tone="info">
50
+ <p><strong>下一步</strong></p>
51
+ <ol>
52
+ <li>找 5 个真实用户,跑核心路径。</li>
53
+ <li>看他们卡在哪,不听他们说要什么。</li>
54
+ <li>验证过了再排期,没过就停。</li>
55
+ </ol>
56
+ </blockquote>
57
+ </div>
58
+ </div>
59
+ </div>
60
+ </div>
61
+ <script>
62
+ window.__ModuleLoader__ = {
63
+ load(definition) {
64
+ window.__client = definition
65
+ },
66
+ }
67
+ </script>
68
+ <script src="../../src/client.js"></script>
69
+ <script>
70
+ window.addEventListener('load', () => {
71
+ const module = window.__client.factory()
72
+ module.apply({
73
+ inject() {},
74
+ effect(fn) { fn() },
75
+ })
76
+ document.body.setAttribute('data-dsh-structured-theme', 'deepseek')
77
+ })
78
+ </script>
79
+ </body>
80
+ </html>
@@ -0,0 +1,8 @@
1
+ # Gate fixture:无 persona
2
+
3
+ 仅使用 `BUILTIN_STYLE_TEXT` 的结构规则;不追加 persona。
4
+
5
+ ## 观察点
6
+
7
+ - 首屏标题卡/表格/列表/结论是否正常。
8
+ - 无个人信息、无额外 system 文本。