@zhushanwen/pi-ask-user 7.2.4 → 7.3.0

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.
@@ -0,0 +1,119 @@
1
+ // src/form-adapter.ts
2
+ //
3
+ // ask-user 包内归一 adapter(设计 ui-presentation-protocol D3:askUserInteract 退役后,
4
+ // AskUserQuestion↔FormQuestion 的归一职责从协议包下沉到本包)。
5
+ //
6
+ // 三种问题形态在此互转(答案编解码不在本模块——FormAnswers 的 choice/text 部分与
7
+ // legacy AskUserAnswers 逐字兼容,解码统一走协议包 getAskUserAnswer/getAskUserOther):
8
+ // internal Question 本包 TUI 组件 + tool details 的形态(options 必填)
9
+ // FormQuestion 统一表单协议 wire 形态(UI_FORM_MARKER + uiFormInteract)
10
+ // legacy AskUserQuestion 旧 npm 子进程经 channel 传入的形态(D9 双读的旧 payload)
11
+ //
12
+ // 键位规则(设计 D2,与 renderer 归一层 / FormOverlay 逐字对齐):
13
+ // key = header ?? question;多选 value = JSON.stringify(labels[]);
14
+ // Other/自由文本答案键 `${key}__other`。
15
+ // schedule 问题类型非 ask-user 产生(scheduler 域),Form→本包形态的转换中跳过。
16
+
17
+ import type {
18
+ AskUserQuestion,
19
+ ChoiceQuestion,
20
+ FormQuestion,
21
+ } from "@zhushanwen/extension-protocol";
22
+
23
+ import type { Option, Question } from "./types";
24
+
25
+ /** internal Question[] → ChoiceQuestion[](ask_user 工具的 wire 形态)。
26
+ * 工具 schema 锁 options 必填(minItems 2)→ 恒映射为 choice;multiSelect → multi
27
+ * 重命名(D2);allowOther 固定 true——ask-user 无条件自动追加 Other,schema 不暴露该字段。 */
28
+ export function internalToFormQuestions(questions: Question[]): ChoiceQuestion[] {
29
+ return questions.map((q: Question): ChoiceQuestion => ({
30
+ type: "choice",
31
+ header: q.header,
32
+ question: q.question,
33
+ context: q.context,
34
+ options: q.options.map((o: Option) => ({
35
+ label: o.label,
36
+ description: o.description,
37
+ })),
38
+ multi: q.multiSelect,
39
+ allowOther: true,
40
+ }));
41
+ }
42
+
43
+ /** legacy AskUserQuestion[] → FormQuestion[](D9 双读的旧 payload 归一)。
44
+ * 有 options → choice(multiSelect → multi);无 options → text(协议值域完备保留,
45
+ * 答案只写 `${key}__other` 键,与现状纯 other 形态一致)。 */
46
+ export function askUserToFormQuestions(legacy: AskUserQuestion[]): FormQuestion[] {
47
+ return legacy.map((q: AskUserQuestion): FormQuestion => {
48
+ if (q.options === undefined) {
49
+ return {
50
+ type: "text",
51
+ header: q.header,
52
+ question: q.question,
53
+ context: q.context,
54
+ };
55
+ }
56
+ return {
57
+ type: "choice",
58
+ header: q.header,
59
+ question: q.question,
60
+ context: q.context,
61
+ options: q.options.map((o) => ({
62
+ label: o.label,
63
+ description: o.description,
64
+ })),
65
+ multi: q.multiSelect,
66
+ allowOther: q.allowOther,
67
+ };
68
+ });
69
+ }
70
+
71
+ /** FormQuestion[] → legacy AskUserQuestion[](解码视图:复用协议包
72
+ * getAskUserAnswer/getAskUserOther 的解码 SSOT,multi 还原为 multiSelect)。
73
+ * 保持顺序、跳过 schedule(ask-user 链路不产生;与 formToInternalQuestions 同规则)。 */
74
+ export function formToAskUserQuestions(form: FormQuestion[]): AskUserQuestion[] {
75
+ return form.flatMap((q: FormQuestion): AskUserQuestion[] => {
76
+ if (q.type === "schedule") return [];
77
+ const common = {
78
+ header: q.header,
79
+ question: q.question,
80
+ context: q.context,
81
+ };
82
+ return [
83
+ q.type === "choice"
84
+ ? {
85
+ ...common,
86
+ options: q.options,
87
+ multiSelect: q.multi,
88
+ allowOther: q.allowOther,
89
+ }
90
+ : common,
91
+ ];
92
+ });
93
+ }
94
+
95
+ /** FormQuestion[] → internal Question[](TUI 组件渲染形态)。
96
+ * choice → options 原样(multiSelect 还原);text → 空 options(组件渲染为仅 Other
97
+ * 行的自由输入,与旧 payload 无 options 问题的现状处理一致);schedule 跳过
98
+ * (防止把草稿题渲染成自由文本框——ask-user channel 不携带该类型)。 */
99
+ export function formToInternalQuestions(form: FormQuestion[]): Question[] {
100
+ return form.flatMap((q: FormQuestion): Question[] => {
101
+ if (q.type === "schedule") return [];
102
+ const opts: Option[] =
103
+ q.type === "choice"
104
+ ? q.options.map((o) => ({
105
+ label: o.label,
106
+ ...(o.description !== undefined ? { description: o.description } : {}),
107
+ }))
108
+ : [];
109
+ return [
110
+ {
111
+ question: q.question,
112
+ ...(q.header !== undefined ? { header: q.header } : {}),
113
+ ...(q.context !== undefined ? { context: q.context } : {}),
114
+ options: opts,
115
+ ...(q.type === "choice" && q.multi !== undefined ? { multiSelect: q.multi } : {}),
116
+ },
117
+ ];
118
+ });
119
+ }
package/src/index.ts CHANGED
@@ -4,14 +4,15 @@ import type { AgentToolResult, AgentToolUpdateCallback, ExtensionAPI, ExtensionC
4
4
  import { type Static } from "typebox";
5
5
  import {
6
6
  type AskUserAnswers,
7
- askUserInteract,
8
7
  type AskUserQuestion,
9
8
  getAskUserAnswer,
10
9
  getAskUserOther,
10
+ uiFormInteract,
11
11
  } from "@zhushanwen/extension-protocol";
12
12
  import { toErrorMessage } from "@zhushanwen/pi-ext-guards";
13
13
 
14
14
  import { createAskUserChannelHandler } from "./channel-handler";
15
+ import { internalToFormQuestions } from "./form-adapter";
15
16
  import { registerAskUserChannelHandler } from "./channel-registry-register";
16
17
  import { AskUserComponent } from "./component";
17
18
  import { answerValueText } from "./submit-view";
@@ -178,8 +179,11 @@ function protoAnswersToResult(
178
179
  /**
179
180
  * RPC 模式(taiji GUI)交互入口。
180
181
  *
181
- * 走 askUserInteract(select 通道 + ASK_USER_MARKER),前端 AskUserOverlay 渲染富交互 UI。
182
- * 返回 Result(正常/取消),或抛错(select 异常 / 非 RPC 模式调用了此函数)。
182
+ * 走 uiFormInteract(统一表单协议:select 通道 + UI_FORM_MARKER),问题经包内归一
183
+ * adapter 转为 FormQuestion 家族(choice 形态),前端 FormOverlay 渲染表单。
184
+ * 回包 FormAnswers 的 choice 部分与旧 AskUserAnswers 逐字兼容(键位规则 D2),
185
+ * 解码复用 getAskUserAnswer/getAskUserOther 零改动。
186
+ * 返回 Result(正常/取消),或抛错(通道契约破坏 / 非 RPC 模式调用了此函数)。
183
187
  *
184
188
  * 注意:从 ExtensionContext 构造 GuiContext 子集传入,而非直接传 ctx——
185
189
  * ExtensionContext.ui.custom 的泛型签名与 GuiContext.ui.custom 不兼容(前者复杂泛型,后者简化签名),
@@ -191,19 +195,38 @@ async function runRpcInteraction(
191
195
  ctx: ExtensionContext,
192
196
  ): Promise<Result> {
193
197
  const protoQuestions = toProtoQuestions(questions);
198
+ const formQuestions = internalToFormQuestions(questions);
194
199
  const guiCtx = {
195
200
  mode: ctx.mode,
196
201
  hasUI: ctx.hasUI,
197
202
  ui: { select: ctx.ui.select.bind(ctx.ui) },
198
203
  };
199
- const answers = await askUserInteract(guiCtx, protoQuestions, { signal, allowCancel: true });
204
+ // 通道失败留痕(select throw / echo 命中):channel-error 折叠 throw 时透出根因
205
+ let lastChannelLog = "";
206
+ const interact = await uiFormInteract(guiCtx, formQuestions, {
207
+ signal,
208
+ allowCancel: true,
209
+ log: (msg, detail) => {
210
+ lastChannelLog = detail === undefined ? msg : `${msg} ${JSON.stringify(detail)}`;
211
+ },
212
+ });
200
213
 
201
- if (answers === null) {
214
+ if (!interact.ok) {
215
+ if (interact.reason === "channel-error") {
216
+ // 通道契约破坏:echo 检测命中的旧宿主组合(D7 下三角,message 携带升级指引)
217
+ // 或 select 抛错。throw 而非折叠 cancelled——「明确报错」让用户与 LLM 拿到
218
+ // 恢复动作,误报「用户取消」会诱导 LLM 俊等用户。
219
+ throw new Error(
220
+ interact.message ?? `form channel error${lastChannelLog ? ` (${lastChannelLog})` : ""}`,
221
+ );
222
+ }
223
+ // cancelled / timeout / non-json → 用户未作答或回包不可解,与旧 askUserInteract
224
+ // 的 null 语义一致,折叠 cancelled result
202
225
  return { questions, answers: {}, cancelled: true };
203
226
  }
204
227
  return {
205
228
  questions,
206
- answers: protoAnswersToResult(questions, protoQuestions, answers),
229
+ answers: protoAnswersToResult(questions, protoQuestions, interact.answers),
207
230
  cancelled: false,
208
231
  };
209
232
  }
@@ -295,7 +318,7 @@ Don't:
295
318
  );
296
319
  }
297
320
 
298
- // 4. 交互执行:TUI 走 ctx.ui.custom,RPC(taiji GUI)走 askUserInteract。
321
+ // 4. 交互执行:TUI 走 ctx.ui.custom,RPC(taiji GUI)走 uiFormInteract(统一表单协议)。
299
322
  // 注意:hasUI 在 TUI 和 RPC 模式都为 true(dialog-capable),不能用于区分——
300
323
  // 用 ctx.mode === 'rpc' 判定 GUI 渲染通道。
301
324
  const useRpc = ctx.mode === "rpc";