dsh-plugin-choice-refresh 0.1.4 → 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 CHANGED
@@ -1,5 +1,12 @@
1
1
  # Changelog
2
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
+
3
10
  ## [0.1.4] - 2026-08-29
4
11
 
5
12
  - 文档:兼容性补实测版本声明(DSH 0.1.1-rc.2);
package/README.md CHANGED
@@ -92,7 +92,7 @@ npm run smoke # selfcheck + smoke-client
92
92
 
93
93
  ## 兼容性
94
94
 
95
- - 实测于 DSH `0.1.1-rc.2`;需要 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)。
96
96
  - 与 `dsh-plugin-image-tools` 共存的图片卡直接兼容;未安装时纯文字选项照常。
97
97
  - `plan-review` 意图(计划审阅卡)放行给原生 `PlanReviewPanel`,不受影响。
98
98
  - 依赖客户端服务:`slots` / `locale` / `sessions` / `remote`。
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
3
  "description": "DSH 选择增强插件(生态唯一):给 ask_user_question / ask_user_choice 的选择卡加「重新生成选项 / 更多选项」两个按钮——选项都不满意一键换一批(换角度/维度,5~8 个),选项太少一键补充到 6~10 个。纯前端交互实现,不改核心包、不新增工具,原生文字卡与图片选择卡通吃。",
4
- "version": "0.1.4",
4
+ "version": "0.1.5",
5
5
  "type": "module",
6
6
  "main": "lib/index.js",
7
7
  "exports": {
@@ -36,6 +36,10 @@
36
36
  "peerDependencies": {
37
37
  "@deepseek-ai/cordis": "^4.0.1"
38
38
  },
39
+ "devDependencies": {
40
+ "react": "^19.0.0",
41
+ "react-dom": "^19.0.0"
42
+ },
39
43
  "engines": {
40
44
  "node": ">=18"
41
45
  },
@@ -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, {