@sendevia/pi-ask-author 1.0.1 → 1.0.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/README.md +3 -3
- package/dist/component.d.ts +322 -0
- package/{src/format.ts → dist/format.d.ts} +12 -118
- package/dist/index.d.ts +51 -0
- package/dist/index.js +1 -0
- package/dist/model.d.ts +260 -0
- package/dist/novel-markdown.d.ts +28 -0
- package/dist/sanitize.d.ts +18 -0
- package/dist/schema.d.ts +129 -0
- package/dist/texts.d.ts +494 -0
- package/{src/theme.ts → dist/theme.d.ts} +0 -2
- package/dist/view-rows.d.ts +69 -0
- package/dist/view-summary.d.ts +27 -0
- package/package.json +10 -10
- package/src/component.ts +0 -1219
- package/src/index.ts +0 -419
- package/src/model.ts +0 -748
- package/src/novel-markdown.ts +0 -673
- package/src/sanitize.ts +0 -279
- package/src/schema.ts +0 -108
- package/src/texts.ts +0 -636
- package/src/view-rows.ts +0 -217
- package/src/view-summary.ts +0 -110
- package/tsconfig.json +0 -16
package/src/index.ts
DELETED
|
@@ -1,419 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @file index.ts
|
|
3
|
-
* @description 作者请示插件入口:注册 `ask_author` 工具与 `/ask-author` 命令
|
|
4
|
-
*
|
|
5
|
-
* 核心架构特性:
|
|
6
|
-
* - 四类出口(取消 / 结构化错误 / 运行期异常 / 正常完成)由四个结果工厂统一产出;其中取消、结构化错误与运行期异常三类共享 `ResultContext` 的缺省收敛,完成结果由表单模型直接供给
|
|
7
|
-
* - `prepareArguments` 防御性清洗兼容批量问卷与单题平铺双模式;返回文本恒以 `TEXTS.markdown` 的固定封套前缀开头(`[Author Decision Finalized]` / `[Author Decision Cancelled]` / `[Author Consultation Error]`),前缀字节稳定利于 Prompt Cache 命中
|
|
8
|
-
* - 性能与生命周期:调用卡片按 args 身份记忆化派生、结果卡片复用槽位 `lastComponent`;AbortSignal 经组件级联取消;本文件不注册任何监听器与定时器,`AskAuthorComponent.dispose()`(幂等)负责回收定时器、Abort 监听与全部缓存池
|
|
9
|
-
*
|
|
10
|
-
* 依赖方向:index.ts 为 11 模块单向依赖图(theme → texts → {format, schema} → {model, sanitize} → {novel-markdown, view-rows, view-summary} → component → index)的入口;旁支 `sanitize.ts` 仅由本文件装配(`prepareArguments` 与 `renderCall` 两处)
|
|
11
|
-
*/
|
|
12
|
-
|
|
13
|
-
import { keyHint, type AgentToolResult, type ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
14
|
-
import { Text, type Component } from "@earendil-works/pi-tui";
|
|
15
|
-
import { AskAuthorComponent } from "./component.js";
|
|
16
|
-
import { cleanOptional } from "./format.js";
|
|
17
|
-
import { AskAuthorFormModel, type AskAuthorResult, type AuthorAnswerItem, type NormalizedQuestion } from "./model.js";
|
|
18
|
-
import { AskAuthorParams, type RawParams } from "./schema.js";
|
|
19
|
-
import { sanitizeAskAuthorArgs } from "./sanitize.js";
|
|
20
|
-
import { TEXTS } from "./texts.js";
|
|
21
|
-
|
|
22
|
-
/** `ask_author` 工具执行结果结构(基于 Pi 官方 `AgentToolResult` 结构特化) */
|
|
23
|
-
export type AskAuthorToolResult = AgentToolResult<AskAuthorResult>;
|
|
24
|
-
|
|
25
|
-
/** 结果构造上下文:题目快照与标题/描述的可选覆盖(缺省值统一由 {@link resolveResultContext} 收敛) */
|
|
26
|
-
export interface ResultContext {
|
|
27
|
-
/** 题目快照(缺省空数组:取消/错误可能发生在模型构建前) */
|
|
28
|
-
questions?: NormalizedQuestion[];
|
|
29
|
-
/** 问卷总标题(缺省 `TEXTS.fallbacks.defaultFormTitle`) */
|
|
30
|
-
formTitle?: string;
|
|
31
|
-
/** 问卷全局描述(缺省 undefined,不携带该字段内容) */
|
|
32
|
-
formDescription?: string;
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
/**
|
|
36
|
-
* 提取异常对象的用户可读消息(`Error` 取自 `message`,其余经 `String` 字符串化)
|
|
37
|
-
* @param err - 未知异常对象
|
|
38
|
-
* @returns `Error` 实例读取其 `message`(可为空串),其余类型取自 `String(err)`(对象为 `[object Object]`)
|
|
39
|
-
*/
|
|
40
|
-
function toErrorMessage(err: unknown): string {
|
|
41
|
-
return err instanceof Error ? err.message : String(err);
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
/**
|
|
45
|
-
* 收敛结果构造上下文的缺省值(唯一兜底点:`formTitle` 恒非空、`questions` 恒为数组,`formDescription` 保留 undefined 语义)
|
|
46
|
-
* @param ctx - 结果构造上下文(可为空对象或省略)
|
|
47
|
-
* @returns 收敛后的 `formTitle` / `formDescription` / `questions` 三元组(`formTitle` 缺省为 `TEXTS.fallbacks.defaultFormTitle`)
|
|
48
|
-
*/
|
|
49
|
-
function resolveResultContext(ctx: ResultContext = {}): {
|
|
50
|
-
formTitle: string;
|
|
51
|
-
formDescription: string | undefined;
|
|
52
|
-
questions: NormalizedQuestion[];
|
|
53
|
-
} {
|
|
54
|
-
return {
|
|
55
|
-
formTitle: ctx.formTitle ?? TEXTS.fallbacks.defaultFormTitle,
|
|
56
|
-
formDescription: ctx.formDescription,
|
|
57
|
-
questions: ctx.questions ?? [],
|
|
58
|
-
};
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
/**
|
|
62
|
-
* 复用渲染槽位既有 Text 组件就地更新卡片内容(Pi 渲染器 `lastComponent` 复用最佳实践)
|
|
63
|
-
* @param content - 卡片文本内容
|
|
64
|
-
* @param lastComponent - 该槽位上帧返回的组件实例(非 Text 实例时新建零边距 Text)
|
|
65
|
-
* @returns 复用或新建的 `Text`(已 `setText`,其内部行缓存随 `setText` 失效)
|
|
66
|
-
*/
|
|
67
|
-
function renderCardText(content: string, lastComponent: Component | undefined): Text {
|
|
68
|
-
const text = lastComponent instanceof Text ? lastComponent : new Text("", 0, 0);
|
|
69
|
-
text.setText(content);
|
|
70
|
-
return text;
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
/**
|
|
74
|
-
* 构造取消结果(智能体封套 `[Author Decision Cancelled]` + `cancelled: true`,`answers` 恒为空数组)
|
|
75
|
-
* @param ctx - 结果构造上下文(题目快照与被取消时的表单快照)
|
|
76
|
-
* @returns 取消结果(`content` 为取消封套单行文本,`details` 不含 `error` 字段)
|
|
77
|
-
*/
|
|
78
|
-
function createCancelledResult(ctx: ResultContext = {}): AskAuthorToolResult {
|
|
79
|
-
const { formTitle, formDescription, questions } = resolveResultContext(ctx);
|
|
80
|
-
return {
|
|
81
|
-
content: [{ type: "text", text: TEXTS.markdown.envelopeCancelled }],
|
|
82
|
-
details: { formTitle, formDescription, questions, answers: [], cancelled: true },
|
|
83
|
-
};
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
/**
|
|
87
|
-
* 构造错误结果(智能体错误封套 + 用户界面提示)
|
|
88
|
-
* @param reason - 面向智能体的原因
|
|
89
|
-
* @param guidance - 面向智能体的纠正指引
|
|
90
|
-
* @param userFacingError - 面向用户的界面提示
|
|
91
|
-
* @param ctx - 结果构造上下文(缺省值由 `resolveResultContext` 收敛)
|
|
92
|
-
* @returns 错误结果对象(`cancelled: false` + `error` 详情,无作答内容)
|
|
93
|
-
*/
|
|
94
|
-
function createErrorResult(
|
|
95
|
-
reason: string,
|
|
96
|
-
guidance: string,
|
|
97
|
-
userFacingError: string,
|
|
98
|
-
ctx: ResultContext = {},
|
|
99
|
-
): AskAuthorToolResult {
|
|
100
|
-
const { formTitle, formDescription, questions } = resolveResultContext(ctx);
|
|
101
|
-
return {
|
|
102
|
-
content: [{ type: "text", text: TEXTS.markdown.envelopeError(reason, guidance) }],
|
|
103
|
-
details: { formTitle, formDescription, questions, answers: [], cancelled: false, error: userFacingError },
|
|
104
|
-
};
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
/**
|
|
108
|
-
* 构造运行期异常结果(模型构建抛出错误与 `execute` 异常捕获两条路径共用本工厂)
|
|
109
|
-
* @param err - 异常对象或消息(字符串经 {@link toErrorMessage} 原样返回)
|
|
110
|
-
* @param ctx - 结果构造上下文(题目快照与表单快照)
|
|
111
|
-
* @returns 运行期异常结果(封套原因携带 {@link toErrorMessage} 归一后的消息)
|
|
112
|
-
*/
|
|
113
|
-
function createRuntimeErrorResult(err: unknown, ctx: ResultContext = {}): AskAuthorToolResult {
|
|
114
|
-
const errorMsg = toErrorMessage(err);
|
|
115
|
-
return createErrorResult(
|
|
116
|
-
TEXTS.markdown.errorReasons.runtimeError(errorMsg),
|
|
117
|
-
TEXTS.markdown.errorGuidances.runtimeError,
|
|
118
|
-
TEXTS.status.errorExecutionFailed(errorMsg),
|
|
119
|
-
ctx,
|
|
120
|
-
);
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
/**
|
|
124
|
-
* 构造完成结果(`[Author Decision Finalized]` 封套正文由模型编译,空答卷渲染为空答卷提示)
|
|
125
|
-
* @param formModel - 本次请示的表单模型(详情表单快照来源,与组件回传结果同源)
|
|
126
|
-
* @param answers - 组件编译完成的答卷条目(封套正文与详情 `answers` 共用同一数组)
|
|
127
|
-
* @returns 完成结果(`cancelled: false`、无 `error` 字段;`answers` 为空数组时正文为 `TEXTS.markdown.envelopeEmpty`)
|
|
128
|
-
*/
|
|
129
|
-
function createSuccessResult(formModel: AskAuthorFormModel, answers: AuthorAnswerItem[]): AskAuthorToolResult {
|
|
130
|
-
return {
|
|
131
|
-
content: [{ type: "text", text: formModel.formatAnswers(answers) }],
|
|
132
|
-
details: {
|
|
133
|
-
formTitle: formModel.formTitle,
|
|
134
|
-
formDescription: formModel.formDescription,
|
|
135
|
-
questions: formModel.questions,
|
|
136
|
-
answers,
|
|
137
|
-
cancelled: false,
|
|
138
|
-
},
|
|
139
|
-
};
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
// ===== 宿主边界收窄与结果卡片数据派生 =====
|
|
143
|
-
|
|
144
|
-
/**
|
|
145
|
-
* 结果卡片宿主边界的**唯一**类型收窄点:把 `renderResult` 的 `details` 从不可信类型提升为 `AskAuthorResult`(工具入参边界另由 `sanitizeAskAuthorArgs` 做运行时守卫,与本函数互不覆盖)
|
|
146
|
-
* 会话回放时 `details` 由转录 JSON 反序列化而来(运行期形状不可信),故先做形状闸门再断言一次;数组元素由本扩展自身写入,不在闸门范围
|
|
147
|
-
* 校验项:普通对象、`cancelled`/`answers`/`questions`/`formTitle` 键存在性、`cancelled` 布尔、`answers`/`questions` 数组、`formTitle` 字符串
|
|
148
|
-
* 可选 `error` / `formDescription` 不设闸门:二者仅参与字符串插值,非字符串至多渲染为退化文本(`fg` 为模板拼接,不抛出错误)
|
|
149
|
-
* @param value - 宿主传入的不可信 `details` 值
|
|
150
|
-
* @returns 通过形状闸门的 `AskAuthorResult`
|
|
151
|
-
* @throws 任一校验项失败时抛出错误(就地崩溃)
|
|
152
|
-
*/
|
|
153
|
-
function narrowResultDetails(value: unknown): AskAuthorResult {
|
|
154
|
-
if (typeof value !== "object" || value === null || Array.isArray(value)) {
|
|
155
|
-
throw new Error(`ask_author 结果详情应为普通对象,实际收到 ${JSON.stringify(value)}`);
|
|
156
|
-
}
|
|
157
|
-
if (!("cancelled" in value) || !("answers" in value) || !("questions" in value) || !("formTitle" in value)) {
|
|
158
|
-
throw new Error("ask_author 结果详情缺少 cancelled/answers/questions/formTitle 字段");
|
|
159
|
-
}
|
|
160
|
-
if (typeof value.cancelled !== "boolean") throw new Error("ask_author 结果详情的 cancelled 字段应为布尔值");
|
|
161
|
-
if (!Array.isArray(value.answers) || !Array.isArray(value.questions)) {
|
|
162
|
-
throw new Error("ask_author 结果详情的 answers/questions 字段应为数组");
|
|
163
|
-
}
|
|
164
|
-
if (typeof value.formTitle !== "string") throw new Error("ask_author 结果详情的 formTitle 字段应为字符串");
|
|
165
|
-
return value as AskAuthorResult;
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
/**
|
|
169
|
-
* 将单条答案展开为明细内容行(多选以 `TEXTS.common.optionSeparator` 连接并附选项批注角标,零勾选回退 `未选`;单选无选中项时回退题目补充说明,两者皆空再回退 `未选`)
|
|
170
|
-
* @param ans - 答卷条目
|
|
171
|
-
* @returns 单行明细文本(不含换行;题目级补充说明以 `TEXTS.card.questionNoteTag` 角标追加)
|
|
172
|
-
*/
|
|
173
|
-
function formatAnswerLine(ans: AuthorAnswerItem): string {
|
|
174
|
-
const questionNoteStr = ans.customText ? TEXTS.card.questionNoteTag(ans.customText) : "";
|
|
175
|
-
/**
|
|
176
|
-
* 把带专属批注的选项标签追加为卡片角标形式(`note` 缺省或空串时原样返回 `label`)
|
|
177
|
-
* @param label - 选项标签
|
|
178
|
-
* @param note - 选项专属批注(可选)
|
|
179
|
-
* @returns 携带角标的标签;无批注时即原 `label`
|
|
180
|
-
*/
|
|
181
|
-
const withNote = (label: string, note?: string): string => (note ? TEXTS.card.optionWithNote(label, note) : label);
|
|
182
|
-
|
|
183
|
-
if (ans.multiSelect) {
|
|
184
|
-
const optItems = (ans.selectedOptions ?? []).map((o) => withNote(o.label, o.customText));
|
|
185
|
-
return (optItems.join(TEXTS.common.optionSeparator) || TEXTS.fallbacks.unselectedOption) + questionNoteStr;
|
|
186
|
-
}
|
|
187
|
-
if (ans.selectedOption) {
|
|
188
|
-
return withNote(ans.selectedOption.label, ans.selectedOption.customText) + questionNoteStr;
|
|
189
|
-
}
|
|
190
|
-
return ans.customText || TEXTS.fallbacks.unselectedOption;
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
/** 调用卡片派生数据:与主题无关的纯计算结果(颜色合成每帧按当前主题现场计算) */
|
|
194
|
-
export interface CallCardData {
|
|
195
|
-
/** 卡片标题(已清洗的 `formTitle`) */
|
|
196
|
-
title: string;
|
|
197
|
-
/** 计数标签(多题模式为 `2 题 • 6 选项`,单题模式仅 `6 选项`) */
|
|
198
|
-
countLabel: string;
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
/**
|
|
202
|
-
* `renderCall` 记忆化状态载荷:宿主按工具执行行创建一次、跨帧复用的可变对象(`ToolRenderContext.state`,同一行的 `renderCall` 与 `renderResult` 共享该实例)
|
|
203
|
-
* 以 **args 对象身份**为键(`cachedArgs` 记录身份、`cachedCard` 记录派生数据),依赖宿主「流式补全整体替换 `args` 引用」的不变量(宿主经 `ToolExecutionComponent.updateArgs` 赋入新对象):身份变化即失效重新计算;若宿主原地改写同一对象,缓存将滞留旧数据
|
|
204
|
-
*/
|
|
205
|
-
export interface CallRenderState {
|
|
206
|
-
/** 上一次派生所用的 args 对象身份(仅作 `===` 引用比对,不读取任何属性) */
|
|
207
|
-
cachedArgs?: object;
|
|
208
|
-
/** 与 `cachedArgs` 配对的派生数据;两者身份同时命中时直接复用 */
|
|
209
|
-
cachedCard?: CallCardData;
|
|
210
|
-
}
|
|
211
|
-
|
|
212
|
-
/**
|
|
213
|
-
* 派生调用卡片数据(标题 + 计数标签:题目数 > 1 时附带题量,单题模式仅选项数)
|
|
214
|
-
* 入参清洗、模型构建与计数归约仅在 **args 对象身份变化时**执行一次,同帧后续渲染只做一次引用比对
|
|
215
|
-
* 缓存只存储与主题无关的数据——`theme` 是身份稳定的 Proxy,缓存 ANSI 着色结果会在主题切换后滞留
|
|
216
|
-
* @param args - 本次渲染的原始工具入参(未经 `sanitizeAskAuthorArgs` 清洗,流式期间可能为不完整对象)
|
|
217
|
-
* @param state - 宿主按工具行复用、跨帧身份稳定的可变状态载荷
|
|
218
|
-
* @returns 派生卡片数据(`state` 身份命中时直接复用,否则重新计算并写入 `state.cachedCard`)
|
|
219
|
-
*/
|
|
220
|
-
function resolveCallCard(args: RawParams, state: CallRenderState): CallCardData {
|
|
221
|
-
if (state.cachedArgs === args && state.cachedCard) return state.cachedCard;
|
|
222
|
-
|
|
223
|
-
const model = new AskAuthorFormModel(sanitizeAskAuthorArgs(args));
|
|
224
|
-
const totalOpts = model.questions.reduce((sum, q) => sum + q.options.length, 0);
|
|
225
|
-
const card: CallCardData = {
|
|
226
|
-
title: model.formTitle,
|
|
227
|
-
countLabel:
|
|
228
|
-
model.questions.length > 1
|
|
229
|
-
? TEXTS.card.callQuestionAndOptionCount(model.questions.length, totalOpts)
|
|
230
|
-
: TEXTS.card.callOptionCountOnly(totalOpts),
|
|
231
|
-
};
|
|
232
|
-
|
|
233
|
-
state.cachedArgs = args;
|
|
234
|
-
state.cachedCard = card;
|
|
235
|
-
return card;
|
|
236
|
-
}
|
|
237
|
-
|
|
238
|
-
// ===== 插件入口与生命周期集成 =====
|
|
239
|
-
|
|
240
|
-
/**
|
|
241
|
-
* `ask-author` 扩展插件主入口工厂(仅完成注册,启动期不执行任何交互)
|
|
242
|
-
* @param pi - Pi 扩展运行时 API 实例
|
|
243
|
-
*/
|
|
244
|
-
export default function askAuthorExtension(pi: ExtensionAPI): void {
|
|
245
|
-
// 注册 ask_author 工具:显式实例化 TState
|
|
246
|
-
pi.registerTool<typeof AskAuthorParams, AskAuthorResult, CallRenderState>({
|
|
247
|
-
name: TEXTS.tool.name,
|
|
248
|
-
label: TEXTS.tool.label,
|
|
249
|
-
description: TEXTS.tool.description,
|
|
250
|
-
promptSnippet: TEXTS.tool.promptSnippet,
|
|
251
|
-
// 宿主接口要求可变 `string[]`;TEXTS 为 `as const` 只读元组,故展开构造可变副本
|
|
252
|
-
promptGuidelines: [...TEXTS.tool.promptGuidelines],
|
|
253
|
-
parameters: AskAuthorParams,
|
|
254
|
-
// 交互式 UI 独占编辑器(`ctx.ui.custom`):串行执行
|
|
255
|
-
executionMode: "sequential",
|
|
256
|
-
// 宿主在 Schema 校验前调用:清洗为白名单形状,并兼容未按当前 Schema 书写的别名/包裹入参
|
|
257
|
-
prepareArguments: sanitizeAskAuthorArgs,
|
|
258
|
-
|
|
259
|
-
/**
|
|
260
|
-
* 工具执行主流程:归一化标题 → 守卫 → 构建模型 → 启动交互式 TUI → 映射四类结果
|
|
261
|
-
* 守卫顺序:信号已中止 → 取消;非 TUI 模式 → 非交互错误;模型构建抛出错误 → 运行期异常;无有效题目 → 空题目错误;`ui.custom` 回传结果映射为取消 / 完成;`finally` 兜底 `dispose()`
|
|
262
|
-
* @param _toolCallId - 宿主工具调用 ID(本实现不使用)
|
|
263
|
-
* @param rawParams - `prepareArguments` 清洗并经宿主 Schema 校验后的入参(宿主 `structuredClone` 副本,与清洗产物非同一引用)
|
|
264
|
-
* @param signal - 宿主取消信号(可缺省;Esc / Ctrl+C 级联终止交互界面)
|
|
265
|
-
* @param _onUpdate - 流式进度回调(本实现不使用;本工具不上报部分结果)
|
|
266
|
-
* @param ctx - Pi 扩展上下文(`mode` 守卫与 `ui.custom` 组件挂载)
|
|
267
|
-
* @returns 取消 / 结构化报错 / 运行期异常 / 完成四类结果之一;模型构建与 `ui.custom` 阶段的异常均被捕获并转换为运行期异常结果
|
|
268
|
-
*/
|
|
269
|
-
async execute(_toolCallId, rawParams, signal, _onUpdate, ctx) {
|
|
270
|
-
const initialFormTitle = cleanOptional(rawParams?.formTitle) ?? TEXTS.fallbacks.defaultFormTitle;
|
|
271
|
-
|
|
272
|
-
if (signal?.aborted) {
|
|
273
|
-
return createCancelledResult({ formTitle: initialFormTitle });
|
|
274
|
-
}
|
|
275
|
-
|
|
276
|
-
if (ctx.mode !== "tui") {
|
|
277
|
-
return createErrorResult(
|
|
278
|
-
TEXTS.markdown.errorReasons.nonTui,
|
|
279
|
-
TEXTS.markdown.errorGuidances.nonTui,
|
|
280
|
-
TEXTS.status.errorNonTui,
|
|
281
|
-
{ formTitle: initialFormTitle },
|
|
282
|
-
);
|
|
283
|
-
}
|
|
284
|
-
|
|
285
|
-
let formModel: AskAuthorFormModel;
|
|
286
|
-
try {
|
|
287
|
-
formModel = new AskAuthorFormModel(rawParams);
|
|
288
|
-
} catch (err) {
|
|
289
|
-
return createRuntimeErrorResult(err, { formTitle: initialFormTitle });
|
|
290
|
-
}
|
|
291
|
-
|
|
292
|
-
if (formModel.questions.length === 0) {
|
|
293
|
-
return createErrorResult(
|
|
294
|
-
TEXTS.markdown.errorReasons.noQuestions,
|
|
295
|
-
TEXTS.markdown.errorGuidances.noQuestions,
|
|
296
|
-
TEXTS.status.errorNoQuestions,
|
|
297
|
-
{ questions: [], formTitle: formModel.formTitle, formDescription: formModel.formDescription },
|
|
298
|
-
);
|
|
299
|
-
}
|
|
300
|
-
|
|
301
|
-
let comp: AskAuthorComponent | undefined;
|
|
302
|
-
try {
|
|
303
|
-
// 宿主回调约定 `(tui, theme, keybindings, done)`:按键分派由组件内部 `matchesKey` 自行持有,
|
|
304
|
-
// 注入的 keybindings 管理器未被消费,故以 `_kb` 标记未使用
|
|
305
|
-
const result = await ctx.ui.custom<AskAuthorResult>((tui, theme, _kb, done) => {
|
|
306
|
-
comp = new AskAuthorComponent(tui, theme, formModel, done);
|
|
307
|
-
comp.bindAbortSignal(signal);
|
|
308
|
-
return comp;
|
|
309
|
-
});
|
|
310
|
-
|
|
311
|
-
if (!result || result.cancelled) {
|
|
312
|
-
return createCancelledResult({
|
|
313
|
-
questions: result?.questions ?? formModel.questions,
|
|
314
|
-
formTitle: result?.formTitle ?? formModel.formTitle,
|
|
315
|
-
formDescription: result?.formDescription ?? formModel.formDescription,
|
|
316
|
-
});
|
|
317
|
-
}
|
|
318
|
-
|
|
319
|
-
return createSuccessResult(formModel, result.answers);
|
|
320
|
-
} catch (err) {
|
|
321
|
-
return createRuntimeErrorResult(err, {
|
|
322
|
-
questions: formModel.questions,
|
|
323
|
-
formTitle: formModel.formTitle,
|
|
324
|
-
formDescription: formModel.formDescription,
|
|
325
|
-
});
|
|
326
|
-
} finally {
|
|
327
|
-
// dispose() 幂等,宿主亦会卸载组件;此处仅兜住「组件工厂执行后、宿主接管前」的抛出错误路径
|
|
328
|
-
comp?.dispose();
|
|
329
|
-
}
|
|
330
|
-
},
|
|
331
|
-
|
|
332
|
-
/**
|
|
333
|
-
* 会话回放调用卡片渲染(复用槽位既有 Text;派生按 args 身份记忆化)
|
|
334
|
-
* `args` 由宿主原样透传(流式期间可能不完整,且未经 Schema 校验;此处 `RawParams` 类型只是宿主泛型的静态断言),故必须先经 `sanitizeAskAuthorArgs` 收敛再构建模型
|
|
335
|
-
* @param args - 原始工具入参(运行时未经清洗,可能为不完整对象)
|
|
336
|
-
* @param theme - 当前 TUI 主题实例
|
|
337
|
-
* @param context - 宿主渲染上下文(`state` 记忆化载荷 / `lastComponent` 槽位复用)
|
|
338
|
-
* @returns 调用卡片 `Text`(复用 `context.lastComponent` 就地更新)
|
|
339
|
-
*/
|
|
340
|
-
renderCall(args, theme, context) {
|
|
341
|
-
const card = resolveCallCard(args, context.state);
|
|
342
|
-
const label = theme.fg("accent", TEXTS.tool.label);
|
|
343
|
-
const body = `${theme.bold(card.title)} ${theme.fg("muted", `(${card.countLabel})`)}`;
|
|
344
|
-
return renderCardText(`${label} ${body}`, context.lastComponent);
|
|
345
|
-
},
|
|
346
|
-
|
|
347
|
-
/**
|
|
348
|
-
* 会话回放结果卡片渲染(错误 / 取消 / 完成三态,展开态追加答卷明细)
|
|
349
|
-
* 错误态优先于取消态判定:`details.error` 为真值时即使 `cancelled` 为 true 也按错误呈现(本扩展产出的 details 中二者互斥:`createErrorResult` 恒设置 `cancelled: false`)
|
|
350
|
-
* @param result - 工具执行结果(`details` 必须经由 {@link narrowResultDetails} 收窄)
|
|
351
|
-
* @param expanded - 是否展开答卷明细(同组的 `isPartial` 未使用:本工具不上报流式部分结果)
|
|
352
|
-
* @param theme - 当前 TUI 主题实例
|
|
353
|
-
* @param context - 宿主渲染上下文(`lastComponent` 槽位复用;`state` 由 `renderCall` 独占使用)
|
|
354
|
-
* @returns 结果卡片 `Text`(复用 `context.lastComponent` 就地更新)
|
|
355
|
-
*/
|
|
356
|
-
renderResult(result, { expanded }, theme, context) {
|
|
357
|
-
const details = narrowResultDetails(result.details);
|
|
358
|
-
|
|
359
|
-
if (details.error) {
|
|
360
|
-
const errorTitle = `${theme.fg("error", theme.bold(TEXTS.card.resultErrorShort))} ${theme.fg("muted", `${TEXTS.common.bulletPrefix}${details.error}`)}`;
|
|
361
|
-
if (!expanded) {
|
|
362
|
-
return renderCardText(
|
|
363
|
-
`${errorTitle} (${keyHint("app.tools.expand", TEXTS.card.expandHint)})`,
|
|
364
|
-
context.lastComponent,
|
|
365
|
-
);
|
|
366
|
-
}
|
|
367
|
-
return renderCardText(
|
|
368
|
-
`${errorTitle}\n${TEXTS.card.errorBullet(theme.fg("error", details.error))}`,
|
|
369
|
-
context.lastComponent,
|
|
370
|
-
);
|
|
371
|
-
}
|
|
372
|
-
|
|
373
|
-
if (details.cancelled) {
|
|
374
|
-
return renderCardText(theme.fg("warning", TEXTS.card.resultCancelled), context.lastComponent);
|
|
375
|
-
}
|
|
376
|
-
|
|
377
|
-
const countLabel = `${theme.fg("success", theme.bold(TEXTS.card.resultCompleted))} ${theme.fg(
|
|
378
|
-
"muted",
|
|
379
|
-
`${TEXTS.common.bulletPrefix}${TEXTS.card.resultCount(details.answers.length)}`,
|
|
380
|
-
)}`;
|
|
381
|
-
if (!expanded) {
|
|
382
|
-
return renderCardText(
|
|
383
|
-
`${countLabel} (${keyHint("app.tools.expand", TEXTS.card.expandHint)})`,
|
|
384
|
-
context.lastComponent,
|
|
385
|
-
);
|
|
386
|
-
}
|
|
387
|
-
|
|
388
|
-
const lines: string[] = [
|
|
389
|
-
countLabel,
|
|
390
|
-
...details.answers.map((ans) =>
|
|
391
|
-
TEXTS.card.optionBullet(theme.bold(ans.questionTitle), theme.fg("accent", formatAnswerLine(ans))),
|
|
392
|
-
),
|
|
393
|
-
];
|
|
394
|
-
return renderCardText(lines.join("\n"), context.lastComponent);
|
|
395
|
-
},
|
|
396
|
-
});
|
|
397
|
-
|
|
398
|
-
// 注册 /ask-author 辅助命令
|
|
399
|
-
pi.registerCommand(TEXTS.command.name, {
|
|
400
|
-
description: TEXTS.command.description,
|
|
401
|
-
/**
|
|
402
|
-
* 命令处理函数:向作者回显 ask_author 使用提示(不解析参数,通知后立即完成)
|
|
403
|
-
* @param _args - 命令参数(本命令不接收参数,不使用)
|
|
404
|
-
* @param ctx - Pi 扩展上下文(经 `ui.notify` 投递 info 级通知)
|
|
405
|
-
*/
|
|
406
|
-
handler: async (_args, ctx) => {
|
|
407
|
-
ctx.ui.notify(TEXTS.command.notify, "info");
|
|
408
|
-
},
|
|
409
|
-
});
|
|
410
|
-
}
|
|
411
|
-
|
|
412
|
-
// ===== 外部与二次开发具名导出 =====
|
|
413
|
-
|
|
414
|
-
export { askAuthorExtension };
|
|
415
|
-
export { AskAuthorFormModel, type AskAuthorResult, type AuthorAnswerItem, type NormalizedQuestion } from "./model.js";
|
|
416
|
-
export { AskAuthorParams, type RawParams } from "./schema.js";
|
|
417
|
-
export { sanitizeAskAuthorArgs } from "./sanitize.js";
|
|
418
|
-
export { TEXTS, LAYOUT_CONFIG, ICONS } from "./texts.js";
|
|
419
|
-
|