dsh-plugin-choice-refresh 0.1.3 → 0.1.5

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 ADDED
@@ -0,0 +1,27 @@
1
+ # Changelog
2
+
3
+ ## [0.1.5] - 2026-09-04
4
+
5
+ - 适配 dsh 0.1.2-rc.1:conversation.composer 链条目 select 入参由 `{interactions}` 改为 `{pendingInteraction}`(原生 PendingQuestion 单对象),按 `kind`/questions 鸭子类型认领,plan-review 继续放行;
6
+ - 待答载体 PendingChoice 改为包装 PendingQuestion(`.answer({answers})` / `.cancel()` 直调,替代旧 `wait.respond` 协议);
7
+ - steer 兜底路径改为 `ctx.remote.session.prompt`(0.1.2 起 remote 命名空间为单数且要求 requestId);主路径 sessions.binding().session.prompt 不变。
8
+ # Changelog
9
+
10
+ ## [0.1.4] - 2026-08-29
11
+
12
+ - 文档:兼容性补实测版本声明(DSH 0.1.1-rc.2);
13
+ - 元数据:description 加「生态唯一」钩子,keywords 10 → 16;
14
+ - 新增英文版 README.en.md 与双语切换行;CHANGELOG.md 纳入 npm files。
15
+ ## [0.1.3] - 2026-08-25
16
+
17
+ - 相关插件段新增 dsh-plugin-windows-guard(Windows 环境防坑守则 skill 插件,互相引流)。
18
+
19
+
20
+ ## [0.1.2] - 2026-08-21
21
+
22
+ - README 新增「相关插件」互相引用段(列出同系列已发布插件,npm / GitHub 链接 + 一句话说明),互相引流。
23
+
24
+ ## [0.1.1] - 2026-08-19
25
+
26
+ - 首发:选择增强(「重新生成选项」/「更多选项」按钮),纯前端实现,支持文字与图片选项卡。
27
+ - 发布 npm、GitHub topics、dsh-market 收录(issue #40)。
package/README.en.md ADDED
@@ -0,0 +1,117 @@
1
+ [中文](./README.md) | **English**
2
+
3
+ # dsh-plugin-choice-refresh
4
+
5
+ ![npm version](https://img.shields.io/npm/v/dsh-plugin-choice-refresh)
6
+ ![License](https://img.shields.io/github/license/Pasumao/dsh-plugin-choice-refresh)
7
+ ![AI Assisted](https://img.shields.io/badge/AI-Assisted-8A2BE2)
8
+
9
+ **The only choice-enhancement plugin in the dsh plugin market**: it adds two buttons to the choice cards of `ask_user_question` / `ask_user_choice` —
10
+ not happy with any of the options? One click has the model come back with a completely different batch. Too few options? One click has the model add more.
11
+
12
+ | Button | Scenario | Effect |
13
+ | --- | --- | --- |
14
+ | 🔄 **Regenerate options** (「重新生成选项」) | None of the offered options works for you | One click has the model re-ask with a completely different batch of options (new angle / style / dimension, 5–8 options) |
15
+ | ➕ **More options** (「更多选项」) | Too few options to choose from | One click has the model re-ask while keeping the original options and adding new ones (6–10 options in total) |
16
+
17
+ Purely client-side interaction, **registers no new tools and modifies no core packages**; works on both native text option cards and the
18
+ [dsh-plugin-image-tools](https://github.com/Pasumao/dsh-plugin-image-tools) image option cards (image cards are rendered directly by this plugin, reusing its image routes).
19
+
20
+ ## Screenshot
21
+
22
+ ![choice-refresh in action: two buttons appear at the bottom of the choice card](docs/choice-buttons.png)
23
+
24
+ Two extra buttons appear automatically at the bottom of the card:
25
+
26
+ ![choice-refresh in action: after clicking "More options", the model adds options and re-asks](docs/choice-more-options.png)
27
+
28
+ After clicking "➕ More options": the model keeps the original options, adds new ones up to 6–10 total, and re-asks.
29
+
30
+ > Real Web GUI screenshots.
31
+
32
+ ## Features
33
+
34
+ - 🔄 **Regenerate options**: when none of the options offered by `ask_user_question` / `ask_user_choice` works for you, one click cancels the current question and has the model re-ask with a completely different batch of options (new angle / style / dimension, 5–8 options);
35
+ - ➕ **More options**: when the options are too few to choose from, one click has the model keep the original options and add new ones (6–10 in total);
36
+ - Purely client-side interaction, **registers no new tools and modifies no core packages**;
37
+ - Works on native text option cards and `dsh-plugin-image-tools` image option cards alike;
38
+ - Compatible with the `plan-review` plan review card, without affecting the native flow.
39
+
40
+ ## Configuration
41
+
42
+ No configuration needed — install and it works:
43
+
44
+ - Reads no environment variables, needs no API key / token, writes no config files;
45
+ - Button labels follow `ctx.locale` (zh / en bilingual), with no extra setup;
46
+ - Behavior is driven by the `【系统 · 选项刷新】` (system · option refresh) / `【系统 · 更多选项】` (system · more options) messages injected by the buttons; the model re-asks according to those instructions.
47
+
48
+ ## Installation
49
+
50
+ ```powershell
51
+ # npm (recommended)
52
+ dsh plugin --profile web add dsh-plugin-choice-refresh
53
+ # or GitHub
54
+ dsh plugin --profile web add github:Pasumao/dsh-plugin-choice-refresh
55
+ ```
56
+
57
+ Install from source (local development / debugging):
58
+
59
+ ```bash
60
+ git clone https://github.com/Pasumao/dsh-plugin-choice-refresh.git
61
+ cd dsh-plugin-choice-refresh
62
+ npm install # or pnpm install
63
+ # Mount into the profile via link:, see 设计说明.md (design notes) for details
64
+ ```
65
+
66
+ Once installed, refresh the browser and it takes effect (hot-reloads automatically when the profile already has dsh-client-hmr — no restart needed).
67
+ For local development, you can mount the plugin via `link:` instead — see `设计说明.md` (design notes) for details.
68
+
69
+ ## Usage
70
+
71
+ When the model asks a question as usual via `ask_user_question` / `ask_user_choice` (e.g. following the novel-* skills), two extra buttons — **Regenerate options** and **More options** — appear at the bottom of the choice card:
72
+
73
+ - Click **Regenerate options**: the current question is cancelled, and a `【系统 · 选项刷新】` user message is injected for the model, which immediately re-asks with a fresh set of options;
74
+ - Click **More options**: same as above — a `【系统 · 更多选项】` message is injected, and the model keeps the original options and adds more.
75
+
76
+ > The injected message stays short (one or two lines) and remains in the session as a user message so the model can understand the instruction;
77
+ > the UI displays it verbatim.
78
+
79
+ > Note: the refresh / top-up is done by the model, which re-invokes the question tool after reading the instruction — this is a "soft" enhancement, and the model may occasionally adjust the question itself. If you are still not satisfied with the result, click the button again or simply type what you want.
80
+
81
+ ## Self-check
82
+
83
+ ```powershell
84
+ # In the plugin directory (local dev clone / link mount)
85
+ npm run smoke # selfcheck + smoke-client
86
+ ```
87
+
88
+ ## Compatibility
89
+
90
+ - Tested on DSH `0.1.1-rc.2`; requires the DSH web GUI (an entry in the `conversation.composer` chain, priority -300).
91
+ - Directly compatible with the image cards of a co-installed `dsh-plugin-image-tools`; without it, plain text options work as usual.
92
+ - `plan-review` intents (plan review cards) are passed through to the native `PlanReviewPanel`, unaffected.
93
+ - Depends on the client services `slots` / `locale` / `sessions` / `remote`.
94
+
95
+ ## Related plugins
96
+
97
+ This plugin is part of **Pasumao's dsh plugin ecosystem**; the other published plugins in the series pair well with it:
98
+
99
+ | Plugin (npm) | GitHub | Description |
100
+ |---|---|---|
101
+ | [dsh-notify](https://www.npmjs.com/package/dsh-notify) | [GitHub repo](https://github.com/Pasumao/dsh-plugin-notify) | Native Windows notifications + system tray |
102
+ | [dsh-plugin-dev-kb](https://www.npmjs.com/package/dsh-plugin-dev-kb) | [GitHub repo](https://github.com/Pasumao/dsh-plugin-dev-kb) | Plugin development knowledge base (full mirror of the official docs + skill) |
103
+ | [dsh-plugin-image-tools](https://www.npmjs.com/package/dsh-plugin-image-tools) | [GitHub repo](https://github.com/Pasumao/dsh-plugin-image-tools) | Image choice cards + inline images in replies + image handoff for text-only models |
104
+ | [dsh-plugin-table-zoom](https://www.npmjs.com/package/dsh-plugin-table-zoom) | [GitHub repo](https://github.com/Pasumao/dsh-plugin-table-zoom) | Floating-window viewer for long chat tables + one-click Markdown copy |
105
+ | [dsh-plugin-windows-guard](https://www.npmjs.com/package/dsh-plugin-windows-guard) | [GitHub repo](https://github.com/Pasumao/dsh-plugin-windows-guard) | Windows environment guard: rule skills + mojibake detection / dangerous-write blocking / encoding diagnostics & repair |
106
+ | [dsh-plugin-workbench](https://www.npmjs.com/package/dsh-plugin-workbench) | [GitHub repo](https://github.com/Pasumao/dsh-plugin-workbench) | VS Code-style file explorer + editable preview |
107
+
108
+ > For the rest of the series, see [Pasumao · dsh plugins](https://github.com/Pasumao); if you find them useful, a ⭐ on GitHub is always appreciated.
109
+
110
+ ## AI generation disclosure
111
+
112
+ Code and documentation are AI-assisted (DeepSeek Harness), all human-reviewed and verified on a live install
113
+ (`npm run smoke`: selfcheck + fake-client smoke test).
114
+
115
+ ## License
116
+
117
+ [MIT](./LICENSE)
package/README.md CHANGED
@@ -4,6 +4,8 @@
4
4
  ![License](https://img.shields.io/github/license/Pasumao/dsh-plugin-choice-refresh)
5
5
  ![AI Assisted](https://img.shields.io/badge/AI-Assisted-8A2BE2)
6
6
 
7
+ [**中文**](./README.md) | [English](./README.en.md)
8
+
7
9
  **dsh 插件市场里唯一的选择增强插件**:给 `ask_user_question` / `ask_user_choice` 的选择卡加上两个按钮——
8
10
  选项都不满意?一键让模型换一批完全不同的。选项太少?一键让模型补充。
9
11
 
@@ -16,6 +18,18 @@
16
18
  [dsh-plugin-image-tools](https://github.com/Pasumao/dsh-plugin-image-tools) 的
17
19
  图片选项卡都生效(图片卡由本插件直接渲染,复用其图片路由)。
18
20
 
21
+ ## 效果图
22
+
23
+ ![choice-refresh 实机截图:选择卡底部出现「重新生成选项 / 更多选项」按钮](docs/choice-buttons.png)
24
+
25
+ 选择卡底部自动多出两个按钮:
26
+
27
+ ![choice-refresh 实机截图:点「更多选项」后模型补充选项并重新提问](docs/choice-more-options.png)
28
+
29
+ 点「➕ 更多选项」后:模型保留原选项、补充到 6~10 个并重新提问。
30
+
31
+ > 真实 Web GUI 截图。
32
+
19
33
  ## 功能
20
34
 
21
35
  - 🔄 **重新生成选项**:`ask_user_question` / `ask_user_choice` 给出的选项都不满意时,
@@ -78,7 +92,7 @@ npm run smoke # selfcheck + smoke-client
78
92
 
79
93
  ## 兼容性
80
94
 
81
- - 需要 DSH web GUI(`conversation.composer` 链条目,priority -300)。
95
+ - 实测于 DSH `0.1.2-rc.1`(0.1.5 起适配该版的 `pendingInteraction` / `PendingQuestion` 新协议);需要 DSH web GUI(`conversation.composer` 链条目,priority -300)。
82
96
  - 与 `dsh-plugin-image-tools` 共存的图片卡直接兼容;未安装时纯文字选项照常。
83
97
  - `plan-review` 意图(计划审阅卡)放行给原生 `PlanReviewPanel`,不受影响。
84
98
  - 依赖客户端服务:`slots` / `locale` / `sessions` / `remote`。
@@ -93,7 +107,7 @@ npm run smoke # selfcheck + smoke-client
93
107
  | [dsh-plugin-dev-kb](https://www.npmjs.com/package/dsh-plugin-dev-kb) | [GitHub 仓库](https://github.com/Pasumao/dsh-plugin-dev-kb) | 插件开发知识库(官方文档完整镜像 + 技能) |
94
108
  | [dsh-plugin-image-tools](https://www.npmjs.com/package/dsh-plugin-image-tools) | [GitHub 仓库](https://github.com/Pasumao/dsh-plugin-image-tools) | 图片选择卡 + 回复内嵌图片 + 盲模型收图 |
95
109
  | [dsh-plugin-table-zoom](https://www.npmjs.com/package/dsh-plugin-table-zoom) | [GitHub 仓库](https://github.com/Pasumao/dsh-plugin-table-zoom) | 聊天长表格浮窗查看 + 一键复制 Markdown |
96
- | [dsh-plugin-windows-guard](https://www.npmjs.com/package/dsh-plugin-windows-guard) | [GitHub 仓库](https://github.com/Pasumao/dsh-plugin-windows-guard) | Windows 环境防坑守则 skill(编码/转义/路径/进程/乱码预防) |
110
+ | [dsh-plugin-windows-guard](https://www.npmjs.com/package/dsh-plugin-windows-guard) | [GitHub 仓库](https://github.com/Pasumao/dsh-plugin-windows-guard) | Windows 环境防坑:守则技能 + 乱码检测 / 危险写拦截 / 编码诊断修复 |
97
111
  | [dsh-plugin-workbench](https://www.npmjs.com/package/dsh-plugin-workbench) | [GitHub 仓库](https://github.com/Pasumao/dsh-plugin-workbench) | VS Code 风格文件浏览器 + 可编辑预览 |
98
112
 
99
113
  > 本系列其余插件见 [Pasumao · dsh 插件](https://github.com/Pasumao);觉得好用欢迎到 GitHub 点 ⭐。
Binary file
Binary file
package/lib/client.js CHANGED
@@ -178,23 +178,20 @@ window.__ModuleLoader__.load({
178
178
  }
179
179
 
180
180
  // ------------------------------------------------------------------
181
- // 待答载体(与服务端回答协议一致)
181
+ // 待答载体(dsh 0.1.2 起 composer 链的 matched 即原生 PendingQuestion:
182
+ // .answer({answers}) / .cancel() / .key / .sessionId / .questions,
183
+ // 这里包一层维持组件既有协议)
182
184
  // ------------------------------------------------------------------
183
185
  var PendingChoice = class {
184
- constructor(wait) { this.wait = wait; }
185
- get key() { return this.wait.key; }
186
- get sessionId() { return this.wait.sessionId; }
187
- get questions() { return this.wait.payload.questions; }
186
+ constructor(matched) { this.matched = matched; }
187
+ get key() { return this.matched.key; }
188
+ get sessionId() { return this.matched.sessionId; }
189
+ get questions() { return this.matched.questions; }
188
190
  async answer(answer) {
189
- var receipt = await this.wait.respond({ ok: true, value: { sessionId: this.wait.sessionId, answer: answer } });
190
- if (!receipt.accepted) throw new Error('question response rejected: ' + receipt.reason);
191
+ await this.matched.answer(answer);
191
192
  }
192
193
  async cancel() {
193
- var receipt = await this.wait.respond({
194
- ok: false,
195
- error: { code: 'cancelled', message: 'the user closed this question request', details: {} }
196
- });
197
- if (!receipt.accepted) throw new Error('question cancellation rejected: ' + receipt.reason);
194
+ await this.matched.cancel();
198
195
  }
199
196
  };
200
197
 
@@ -420,8 +417,12 @@ window.__ModuleLoader__.load({
420
417
  if (binding !== void 0 && binding.session !== void 0 && typeof binding.session.prompt === 'function') {
421
418
  return binding.session.prompt([{ type: 'text', text: text }], 'steer');
422
419
  }
423
- if (ctx.remote !== void 0 && ctx.remote.sessions !== void 0 && typeof ctx.remote.sessions.prompt === 'function') {
424
- return ctx.remote.sessions.prompt({ sessionId: sessionId, mode: 'steer', content: [{ type: 'text', text: text }] });
420
+ if (ctx.remote !== void 0 && ctx.remote.session !== void 0 && typeof ctx.remote.session.prompt === 'function') {
421
+ // dsh 0.1.2 remote 命名空间为 session(单数)且要求 requestId。
422
+ var requestId = typeof crypto !== 'undefined' && typeof crypto.randomUUID === 'function'
423
+ ? crypto.randomUUID()
424
+ : 'dsr-' + Date.now() + '-' + Math.random().toString(36).slice(2);
425
+ return ctx.remote.session.prompt({ requestId: requestId, sessionId: sessionId, mode: 'steer', content: [{ type: 'text', text: text }] });
425
426
  }
426
427
  throw new Error('choice-refresh: no session prompt transport available');
427
428
  };
@@ -758,18 +759,22 @@ window.__ModuleLoader__.load({
758
759
  * 认领所有「非 plan-review」的 question 交互(priority -300,先于
759
760
  * image-tools 的 -100 与原生 UI 的 0)。plan-review 意图放行给原生
760
761
  * PlanReviewPanel;纯文字 / 带图片标记的问题都归本插件渲染。
762
+ * dsh 0.1.2 起 select 入参为 { pendingInteraction }(单对象,即原生
763
+ * PendingQuestion 实例;无交互时为 null),不再是 { interactions } 数组。
761
764
  */
765
+ function isPendingQuestion(value) {
766
+ return value !== null && typeof value === 'object'
767
+ && Array.isArray(value.questions)
768
+ && typeof value.answer === 'function'
769
+ && typeof value.cancel === 'function';
770
+ }
762
771
  function selectChoice(_ref) {
763
- var interactions = _ref.interactions;
764
- for (var i = 0; i < interactions.length; i++) {
765
- var item = interactions[i];
766
- if (item.kind !== 'question') continue;
767
- var payload = item.payload;
768
- var questions = payload !== null && typeof payload === 'object' && Array.isArray(payload.questions) ? payload.questions : [];
769
- if (isPlanReviewBatch(questions)) continue;
770
- return item;
771
- }
772
- return null;
772
+ var pendingInteraction = _ref.pendingInteraction;
773
+ if (!isPendingQuestion(pendingInteraction)) return null;
774
+ if (pendingInteraction.kind === 'plan-review') return null;
775
+ // 兜底再判一次 plan-review 意图(老版本宿主无 kind 字段时仍能放行)。
776
+ if (isPlanReviewBatch(pendingInteraction.questions)) return null;
777
+ return pendingInteraction;
773
778
  }
774
779
 
775
780
  // ------------------------------------------------------------------
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "dsh-plugin-choice-refresh",
3
- "description": "DSH 选择增强插件:在 ask_user_question / ask_user_choice 的选择卡上提供「重新生成选项」(都不满意一键刷新)与「更多选项」(选项太少一键补充)按钮。纯前端交互实现(取消当前问题 + steer 提示模型重新提问),不改核心包、不新增工具。",
4
- "version": "0.1.3",
3
+ "description": "DSH 选择增强插件(生态唯一):给 ask_user_question / ask_user_choice 的选择卡加「重新生成选项 / 更多选项」两个按钮——选项都不满意一键换一批(换角度/维度,5~8 个),选项太少一键补充到 6~10 个。纯前端交互实现,不改核心包、不新增工具,原生文字卡与图片选择卡通吃。",
4
+ "version": "0.1.5",
5
5
  "type": "module",
6
6
  "main": "lib/index.js",
7
7
  "exports": {
@@ -12,8 +12,11 @@
12
12
  "files": [
13
13
  "lib",
14
14
  "scripts",
15
+ "docs",
15
16
  "cordis.patch.yml",
16
17
  "README.md",
18
+ "README.en.md",
19
+ "CHANGELOG.md",
17
20
  "LICENSE",
18
21
  "设计说明.md"
19
22
  ],
@@ -33,6 +36,10 @@
33
36
  "peerDependencies": {
34
37
  "@deepseek-ai/cordis": "^4.0.1"
35
38
  },
39
+ "devDependencies": {
40
+ "react": "^19.0.0",
41
+ "react-dom": "^19.0.0"
42
+ },
36
43
  "engines": {
37
44
  "node": ">=18"
38
45
  },
@@ -45,7 +52,13 @@
45
52
  "plugin",
46
53
  "choice",
47
54
  "refresh",
48
- "regenerate",
55
+ "regenerate-options",
56
+ "more-options",
57
+ "ask-user",
58
+ "question",
59
+ "ux",
60
+ "chat-ui",
61
+ "frontend",
49
62
  "options"
50
63
  ],
51
64
  "license": "MIT",
@@ -10,9 +10,11 @@ import { strict as assert } from 'node:assert'
10
10
 
11
11
  const require = createRequire(import.meta.url)
12
12
  const ROOT = join(dirname(fileURLToPath(import.meta.url)), '..')
13
- const profileRequire = createRequire(join('C:/Users/18303/.dsh/profiles/node_modules', 'noop.cjs'))
14
- const reactRoot = dirname(profileRequire.resolve('react'))
15
- const reactModule = profileRequire('react')
13
+ // 优先用插件自带的 devDependencies react(0.1.5 起 dsh-app 根部的 react 不再保证
14
+ // 17+、无 jsx-runtime);本地没有时退回 profile 共享树。
15
+ const localRequire = createRequire(join(ROOT, 'noop.cjs'))
16
+ const reactRoot = dirname(localRequire.resolve('react'))
17
+ const reactModule = localRequire('react')
16
18
 
17
19
  // 1) 模拟浏览器模块加载器并物化 factory
18
20
  let spec = null
@@ -107,26 +109,36 @@ ok('parseRecommendedLabel strips suffix', () => {
107
109
  assert.deepEqual(mod.parseRecommendedLabel('A'), { label: 'A', recommended: false })
108
110
  })
109
111
 
110
- // --- selectChoice ---
112
+ // --- selectChoice(dsh 0.1.2 协议:入参 { pendingInteraction },载体即原生
113
+ // PendingQuestion——questions 直挂、带 answer()/cancel()) ---
114
+ function fakePending(questions, extra) {
115
+ return {
116
+ kind: 'question',
117
+ key: 'q:test',
118
+ sessionId: 's1',
119
+ questions,
120
+ answer: async () => {},
121
+ cancel: async () => {},
122
+ ...extra,
123
+ }
124
+ }
111
125
  ok('selectChoice claims normal question', () => {
112
- const q = { kind: 'question', key: 'q:1', sessionId: 's1', payload: { questions: [{ id: 'a', question: 'q', options: [{ label: 'A' }] }] } }
113
- assert.equal(mod.selectChoice({ interactions: [q] }), q)
126
+ const pending = fakePending([{ id: 'a', question: 'q', options: [{ label: 'A' }] }])
127
+ assert.equal(mod.selectChoice({ pendingInteraction: pending }), pending)
114
128
  })
115
- ok('selectChoice skips plan-review and approval', () => {
116
- const plan = { kind: 'question', key: 'q:1', sessionId: 's1', payload: { questions: [{ id: 'a', question: 'q', detail: 'plan', intent: { kind: 'plan-review', approve: '执行' }, options: [{ label: '执行' }] }] } }
117
- const approval = { kind: 'approval', key: 'a:1', sessionId: 's1', payload: {} }
118
- assert.equal(mod.selectChoice({ interactions: [approval, plan] }), null, 'plan-review 应放行给原生 PlanReviewPanel')
129
+ ok('selectChoice passes plan-review to native', () => {
130
+ const plan = fakePending([{ id: 'a', question: 'q', detail: 'plan', intent: { kind: 'plan-review', approve: '执行' }, options: [{ label: '执行' }] }], { kind: 'plan-review' })
131
+ assert.equal(mod.selectChoice({ pendingInteraction: plan }), null, 'plan-review 应放行给原生 PlanReviewPanel')
119
132
  })
120
- ok('selectChoice prefers question over approval and falls through to later', () => {
121
- const q = { kind: 'question', key: 'q:2', sessionId: 's2', payload: { questions: [{ id: 'b', question: 'q2' }] } }
122
- const approval = { kind: 'approval', key: 'a:1', sessionId: 's1', payload: {} }
123
- assert.equal(mod.selectChoice({ interactions: [approval, q] }), q)
124
- assert.equal(mod.selectChoice({ interactions: [approval] }), null)
133
+ ok('selectChoice rejects non-question values', () => {
134
+ assert.equal(mod.selectChoice({ pendingInteraction: null }), null)
135
+ assert.equal(mod.selectChoice({ pendingInteraction: { questions: [] } }), null)
136
+ assert.equal(mod.selectChoice({}), null)
125
137
  })
126
138
  ok('selectChoice claims image-marked question too', () => {
127
139
  const marker = '<!--dsh-pick:v1:' + encodeBase64Url(JSON.stringify({ pickId: 'p1', images: [0] })) + '-->'
128
- const q = { kind: 'question', key: 'q:3', sessionId: 's3', payload: { questions: [{ id: 'c', question: '选图', detail: marker, options: [{ label: 'A' }] }] } }
129
- assert.equal(mod.selectChoice({ interactions: [q] }), q, '带图问题应由本插件渲染(含刷新/更多按钮)')
140
+ const pending = fakePending([{ id: 'c', question: '选图', detail: marker, options: [{ label: 'A' }] }])
141
+ assert.equal(mod.selectChoice({ pendingInteraction: pending }), pending, '带图问题应由本插件渲染(含刷新/更多按钮)')
130
142
  })
131
143
 
132
144
  console.log(`\n[dsh-plugin-choice-refresh] ${passed} checks passed`)
@@ -18,11 +18,13 @@ import { strict as assert } from 'node:assert'
18
18
 
19
19
  const require = createRequire(import.meta.url)
20
20
  const ROOT = join(dirname(fileURLToPath(import.meta.url)), '..')
21
- const profileRequire = createRequire(join('C:/Users/18303/.dsh/profiles/node_modules', 'noop.cjs'))
22
- const reactRoot = dirname(profileRequire.resolve('react'))
21
+ // 优先用插件自带的 devDependencies react(0.1.5 起 dsh-app 根部 react 可能被降级
22
+ // 16.8、无 jsx-runtime,与 react-dom 19 不同源);本地没有时退回 profile 共享树。
23
+ const localRequire = createRequire(join(ROOT, 'noop.cjs'))
24
+ const reactRoot = dirname(localRequire.resolve('react'))
23
25
  const jsxRuntime = join(reactRoot, 'jsx-runtime.js')
24
- const reactModule = profileRequire('react')
25
- const reactDomServer = profileRequire('react-dom/server')
26
+ const reactModule = localRequire('react')
27
+ const reactDomServer = localRequire('react-dom/server')
26
28
 
27
29
  // 1) 模拟浏览器模块加载器
28
30
  let spec = null
@@ -70,21 +72,20 @@ function encodeBase64Url(json) {
70
72
  }
71
73
 
72
74
  // 2) 纯文字问题:渲染卡片 + 两个增强按钮
75
+ // (dsh 0.1.2 起 matched 即原生 PendingQuestion:questions 直挂、answer()/cancel())
73
76
  const plainWait = {
77
+ kind: 'question',
74
78
  key: 'q:1',
75
79
  sessionId: 's1',
76
- payload: {
77
- type: 'question/requested',
78
- sessionId: 's1',
79
- questions: [
80
- { id: 'a', question: '选一个封面风格', header: '封面', detail: '请选择你喜欢的风格', multiSelect: false, options: [
81
- { label: '赛博 (Recommended)', description: '霓虹质感' },
82
- { label: '水墨' },
83
- { label: '极简' },
84
- ] },
85
- ],
86
- },
87
- respond: async (r) => ({ accepted: true }),
80
+ questions: [
81
+ { id: 'a', question: '选一个封面风格', header: '封面', detail: '请选择你喜欢的风格', multiSelect: false, options: [
82
+ { label: '赛博 (Recommended)', description: '霓虹质感' },
83
+ { label: '水墨' },
84
+ { label: '极简' },
85
+ ] },
86
+ ],
87
+ answer: async () => {},
88
+ cancel: async () => {},
88
89
  }
89
90
  let html = renderToString(reactModule.createElement(mod.ChoiceComposer, { matched: plainWait, t: fakeT, ctx: fakeCtx }))
90
91
  assert.ok(html.includes('dsr-card'), '卡片未渲染')
@@ -98,20 +99,18 @@ assert.ok(!html.includes('<!--dsh-pick:v1:'), '图片标记不应出现')
98
99
  // 3) 带图片标记的问题:渲染图片卡片(image-tools 路由)
99
100
  const marker = '<!--dsh-pick:v1:' + encodeBase64Url(JSON.stringify({ pickId: 'pick-7', images: [0, 2] })) + '-->'
100
101
  const imageWait = {
102
+ kind: 'question',
101
103
  key: 'q:2',
102
104
  sessionId: 's2',
103
- payload: {
104
- type: 'question/requested',
105
- sessionId: 's2',
106
- questions: [
107
- { id: 'b', question: '选一张封面图', detail: marker, multiSelect: false, options: [
108
- { label: '深海' },
109
- { label: '极光' },
110
- { label: '沙漠' },
111
- ] },
112
- ],
113
- },
114
- respond: async (r) => ({ accepted: true }),
105
+ questions: [
106
+ { id: 'b', question: '选一张封面图', detail: marker, multiSelect: false, options: [
107
+ { label: '深海' },
108
+ { label: '极光' },
109
+ { label: '沙漠' },
110
+ ] },
111
+ ],
112
+ answer: async () => {},
113
+ cancel: async () => {},
115
114
  }
116
115
  html = renderToString(reactModule.createElement(mod.ChoiceComposer, { matched: imageWait, t: fakeT, ctx: fakeCtx }))
117
116
  assert.ok(html.includes('/dsh-plugin-image-tools/pick-7/0'), '第一张图片 URL 缺失')
@@ -121,21 +120,18 @@ assert.ok(html.includes('放大查看'), '放大触发按钮缺失')
121
120
  assert.ok(html.includes('重新生成选项'), '带图问题的刷新按钮缺失')
122
121
  assert.ok(!html.includes('<!--dsh-pick:v1:'), '图片标记不应泄漏到界面')
123
122
 
124
- // 4) plan-review 批放行
123
+ // 4) plan-review 批放行(dsh 0.1.2:载体 kind 即为 'plan-review')
125
124
  const planWait = {
126
- kind: 'question',
125
+ kind: 'plan-review',
127
126
  key: 'q:3',
128
127
  sessionId: 's3',
129
- payload: {
130
- type: 'question/requested',
131
- sessionId: 's3',
132
- questions: [
133
- { id: 'c', question: '确认执行计划', detail: '# 计划\n1. 做 A', intent: { kind: 'plan-review', approve: '确认执行' }, options: [{ label: '确认执行' }, { label: '拒绝' }] },
134
- ],
135
- },
136
- respond: async (r) => ({ accepted: true }),
128
+ questions: [
129
+ { id: 'c', question: '确认执行计划', detail: '# 计划\n1. 做 A', intent: { kind: 'plan-review', approve: '确认执行' }, options: [{ label: '确认执行' }, { label: '拒绝' }] },
130
+ ],
131
+ answer: async () => {},
132
+ cancel: async () => {},
137
133
  }
138
- assert.equal(mod.selectChoice({ interactions: [planWait] }), null, 'plan-review 不应被本插件认领')
134
+ assert.equal(mod.selectChoice({ pendingInteraction: planWait }), null, 'plan-review 不应被本插件认领')
139
135
 
140
136
  // 5) Lightbox 单独渲染
141
137
  const lightboxHtml = renderToString(reactModule.createElement(mod.Lightbox, {