@zhushanwen/extension-protocol 0.11.0 → 0.13.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.
@@ -7,8 +7,8 @@
7
7
  * guard 写 exited 终态。写入协议:原子写 temp+rename + 统一文件锁内 RMW;
8
8
  * 解析失败/形状非法 → 重命名 .corrupt 保留现场 + 按空表重建。
9
9
  * - 读侧:taiji runtime 后台任务收殓器——按属主判定处置孤儿(见 ownerPiPid
10
- * 字段注释),orphaned 终态由 runtime 写入(收殓下沉,设计 docs/design/
11
- * file-lock-unification-and-reaper-sink.md D2)。
10
+ * 字段注释),orphaned 终态由 runtime 写入(收殓下沉,设计
11
+ * docs/architecture/file-lock-unification-and-reaper-sink.md D2)。
12
12
  *
13
13
  * 目录布局(per-session 隔离):
14
14
  * <agentDir>/base-tool-enhance/<sessionId>/registry.json
@@ -1,4 +1,4 @@
1
- import { B as BackgroundTaskState, a as BackgroundTaskRegistryEntry } from './background-task-Cu0p02O-.js';
1
+ import { B as BackgroundTaskState, a as BackgroundTaskRegistryEntry } from './background-task-B6pdI5nq.js';
2
2
 
3
3
  /**
4
4
  * 后台任务进程原语(pid 判活 / 进程树处置 / start time 身份判据)——跨端单一实现。
@@ -171,6 +171,11 @@ import { openSync, readSync, closeSync, statSync } from "fs";
171
171
  var TAIL_WINDOW_MARGIN_BYTES = 64;
172
172
  function readOutputTail(outputFile, opts, onLog) {
173
173
  const { maxBytes, maxLines } = opts;
174
+ if (!Number.isInteger(maxLines) || maxLines <= 0) {
175
+ throw new RangeError(
176
+ `readOutputTail: maxLines must be a positive integer, got ${maxLines} (outputFile: ${outputFile})`
177
+ );
178
+ }
174
179
  let size;
175
180
  try {
176
181
  size = statSync(outputFile).size;
@@ -182,7 +187,14 @@ function readOutputTail(outputFile, opts, onLog) {
182
187
  let fd;
183
188
  try {
184
189
  fd = openSync(outputFile, "r");
185
- readSync(fd, buffer, 0, windowSize, size - windowSize);
190
+ let totalRead = 0;
191
+ while (totalRead < windowSize) {
192
+ const bytesRead = readSync(fd, buffer, totalRead, windowSize - totalRead, size - windowSize + totalRead);
193
+ if (bytesRead <= 0) break;
194
+ totalRead += bytesRead;
195
+ }
196
+ const text = (totalRead === windowSize ? buffer : buffer.subarray(0, totalRead)).toString("utf8");
197
+ return finishTail(text, { size, windowSize, maxBytes, maxLines });
186
198
  } catch {
187
199
  return void 0;
188
200
  } finally {
@@ -194,13 +206,14 @@ function readOutputTail(outputFile, opts, onLog) {
194
206
  }
195
207
  }
196
208
  }
197
- const text = buffer.toString("utf8");
209
+ }
210
+ function finishTail(text, bounds) {
198
211
  const lines = text.split("\n");
199
- const firstLineIsPartial = windowSize < size && lines.length > 0;
212
+ const firstLineIsPartial = bounds.windowSize < bounds.size && lines.length > 0;
200
213
  const effectiveLines = firstLineIsPartial ? lines.slice(1) : lines;
201
- const byteTruncated = size > maxBytes;
202
- const shown = effectiveLines.slice(-maxLines).join("\n");
203
- return { text: shown, truncated: byteTruncated || effectiveLines.length > maxLines };
214
+ const byteTruncated = bounds.size > bounds.maxBytes;
215
+ const shown = effectiveLines.slice(-bounds.maxLines).join("\n");
216
+ return { text: shown, truncated: byteTruncated || effectiveLines.length > bounds.maxLines };
204
217
  }
205
218
  export {
206
219
  atomicWriteRegistry,
package/dist/index.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- export { b as BACKGROUND_TASK_ID_PREFIX, c as BACKGROUND_TASK_REGISTRY_FILENAME, d as BACKGROUND_TASK_REGISTRY_VERSION, e as BASE_TOOL_ENHANCE_DIRNAME, f as BackgroundTaskEndReason, a as BackgroundTaskRegistryEntry, g as BackgroundTaskRegistryFile, B as BackgroundTaskState, M as MAX_TERMINAL_REGISTRY_ENTRIES, i as isActiveBackgroundTaskState, h as isBackgroundTaskRegistryEntry, j as isTerminalBackgroundTaskState } from './background-task-Cu0p02O-.js';
1
+ export { b as BACKGROUND_TASK_ID_PREFIX, c as BACKGROUND_TASK_REGISTRY_FILENAME, d as BACKGROUND_TASK_REGISTRY_VERSION, e as BASE_TOOL_ENHANCE_DIRNAME, f as BackgroundTaskEndReason, a as BackgroundTaskRegistryEntry, g as BackgroundTaskRegistryFile, B as BackgroundTaskState, M as MAX_TERMINAL_REGISTRY_ENTRIES, i as isActiveBackgroundTaskState, h as isBackgroundTaskRegistryEntry, j as isTerminalBackgroundTaskState } from './background-task-B6pdI5nq.js';
2
2
 
3
3
  /**
4
4
  * GUI 渲染协议核心类型定义。
@@ -55,7 +55,7 @@ interface GuiComponentProps {
55
55
  * 自带编号的文本(goal criteria "1. xxx")不要开,避免双重编号 */
56
56
  numbered?: boolean;
57
57
  };
58
- /** 垂直组合容器——无视觉样式的透明分组。宿主壳层(WidgetArea)承担卡壳/head/折叠
58
+ /** 垂直组合容器——无视觉样式的透明分组。宿主壳层(托盘 widget 面板)承担卡壳/head/折叠
59
59
  * 后,widget 内容需要多组件组合时的组合根(替代「无头 card」的语义滥用) */
60
60
  'group': {
61
61
  children: GuiComponent[];
@@ -72,6 +72,13 @@ interface GuiComponentProps {
72
72
  active?: boolean;
73
73
  status?: 'done' | 'pending';
74
74
  }[];
75
+ /**
76
+ * 容器化分段(可选):第 i 段 = tabs[i] 激活时渲染的子树,与 tabs 等长。
77
+ * 缺省维持「纯展示 tab-bar」现状(旧 extension 一行不改);宿主本地持有
78
+ * active(首挂载取 tabs[i].active,点击只切本地索引,后续推送不重置用户
79
+ * 选择);与 tabs 长度不等时忽略 sections 退化为纯展示(见协议文档)。
80
+ */
81
+ sections?: GuiComponent[][];
75
82
  };
76
83
  /** 自定义组件——逃生口(仅限内置 extension 编译期注册) */
77
84
  'custom': {
@@ -84,7 +91,7 @@ interface GuiRenderResult {
84
91
  v: typeof PROTOCOL_VERSION;
85
92
  component: GuiComponent;
86
93
  /**
87
- * widget 宿主元数据(M17 对话流 widget 面板消费):标题/状态点/进度计数由
94
+ * widget 宿主元数据(composer 任务托盘的协议 widget 区消费):标题/状态点/进度计数由
88
95
  * 宿主壳层统一渲染成单一 head(含折叠交互),extension 不再用 card 原语
89
96
  * 的 header 表达这些(壳层 head 与 payload card header 双头重复的根因修复)。
90
97
  * 可选:不发时宿主 fallback 到 viewId 标题、无状态点/进度。
@@ -108,6 +115,21 @@ interface WidgetMeta {
108
115
  /** fill 语义色(预算阈值映射);缺省按 meta.status(done→success,否则 accent) */
109
116
  severity?: 'ok' | 'warn' | 'danger';
110
117
  };
118
+ /**
119
+ * 托盘 icon:icon key 字符串(宿主按 lucide 名解析)或自定义形状 `{ paths }`。
120
+ * **形状归 extension、风格归宿主锁死**——extension 只给 path d 数组,线宽/颜色/
121
+ * 尺寸由宿主固定(与 @lucide 细线 icon 同构),故不会出现风格失控。
122
+ * 自定义形状经 `validateWidgetIconPaths` 白名单校验(字符集/条数/长度上限),
123
+ * 超限落兜底 icon + warn。缺省 → 宿主按 widgetKey 内置映射 → 通用 widget icon。
124
+ */
125
+ icon?: string | {
126
+ paths: string[];
127
+ };
128
+ /**
129
+ * 托盘 badge:extension 全权格式化的短文本('2' / '42%' / '!'),建议 ≤6 字符
130
+ * (宿主超长 truncate 至 6,全文进 title)。缺省 → 宿主按 progress 派生 → 无 badge。
131
+ */
132
+ badge?: string;
111
133
  }
112
134
  interface StatItem {
113
135
  label?: string;
@@ -123,6 +145,46 @@ interface TreeItem {
123
145
  children?: TreeItem[];
124
146
  }
125
147
  type TreeItemIcon = 'arrow' | 'check' | 'cross' | 'circle' | 'dot' | 'pause' | 'branch';
148
+ /**
149
+ * 计划产物文档元数据——agent 调 register-doc 登记的一份产物。
150
+ * version 由 extension 侧维护:修订后重写文档须重调 register-doc(version+1),
151
+ * 前端凭 version 变化重拉 file.read 刷新渲染。
152
+ */
153
+ interface PlanDocMeta {
154
+ /** 文件名(drawer L2 文档 tab 标题,不含目录) */
155
+ fileName: string;
156
+ /** 文件绝对路径(前端 file.read RPC 带 sessionId 读取) */
157
+ absPath: string;
158
+ /** 来源技能名(挂载 --skills 时产出该文档的技能;模板流程产出时为空串) */
159
+ sourceSkill: string;
160
+ /** 修订版本,从 1 起,每次修订重登记 +1 */
161
+ version: number;
162
+ }
163
+ /**
164
+ * submit-review 挂起审批时的 select payload(序列化为 options[0] JSON)。
165
+ * extension 侧解析失败走 E5(logger.warn + tool result 报错提示重挂,垃圾数据不进对话流)。
166
+ */
167
+ interface PlanReviewRequest {
168
+ docs: PlanDocMeta[];
169
+ }
170
+ /** 审批两键裁决。revise 携带评论、approve 不携带(结构上不可混带)。 */
171
+ type PlanReviewDecision = 'approve' | 'revise';
172
+ /** 用户对某文档划选段落的一条评论:quote 是划选引文(agent 定位段落用),comment 是评语。 */
173
+ interface PlanReviewComment {
174
+ quote: string;
175
+ comment: string;
176
+ }
177
+ /**
178
+ * 审批条 respond 回传(判别联合:approve 无评论字段,revise 必带评论数组)。
179
+ * extension 消费:approve → 走现状 complete 执行方式 select;revise → 评论清单以
180
+ * 显式 deliverAs:'steer' 注入 + reviewState=revising。
181
+ */
182
+ type PlanReviewResponse = {
183
+ decision: 'approve';
184
+ } | {
185
+ decision: 'revise';
186
+ comments: PlanReviewComment[];
187
+ };
126
188
 
127
189
  /**
128
190
  * 通用传输 marker。extension 用 guiSetWidget 编码进 string[],
@@ -131,6 +193,17 @@ type TreeItemIcon = 'arrow' | 'check' | 'cross' | 'circle' | 'dot' | 'pause' | '
131
193
  * NUL 字符开头的 marker,不会出现在正常文本中。
132
194
  */
133
195
  declare const GUI_WIDGET_MARKER = "\0TAIJI_GUI_WIDGET:";
196
+ /**
197
+ * plan 审阅请求的 select title marker。plan extension 的 submit-review 挂起审批时
198
+ * 以此 marker 为 title 发 ctx.ui.select(options[0] = PlanReviewRequest JSON,
199
+ * 类型见 core/types.ts),runtime event-adapter 检测后广播 extension_ui_request
200
+ * (planReview 标记,与 askUser 标记同构分流),前端审批条渲染三键审批而非
201
+ * 原始 dialog——marker 控制符 title 落入通用 dialog 会渲染成乱码。
202
+ *
203
+ * NUL 前缀确保不会与 extension 正常的 select title 冲突,
204
+ * 与 GUI_WIDGET_MARKER / ASK_USER_MARKER 同族(select 通道 marker)。
205
+ */
206
+ declare const PLAN_REVIEW_MARKER = "\0TAIJI_PLAN_REVIEW:";
134
207
 
135
208
  /**
136
209
  * helpers 需要的 ctx 最小接口。
@@ -171,7 +244,7 @@ interface GuiContext {
171
244
  */
172
245
  declare function isGuiCapable(ctx: GuiContext): boolean;
173
246
  /**
174
- * 构造 GuiRenderResult。当前主用途是 guiSetWidget 的载荷(M17 对话流 widget 面板);
247
+ * 构造 GuiRenderResult。当前主用途是 guiSetWidget 的载荷(composer 任务托盘的协议 widget 区);
175
248
  * details.__gui__(tool result 通道)为遗留兼容路径,todo/goal 已不再使用。
176
249
  * stripUndefined 确保序列化不含 undefined(JSON.stringify 会丢弃 undefined 字段)。
177
250
  */
@@ -215,9 +288,43 @@ interface DualWidgetContent {
215
288
  * hasUI 守卫留在调用方(goal FR-6.6 语义,本 helper 不感知)。
216
289
  */
217
290
  declare function setWidgetDual(ctx: GuiContext, key: string, content: DualWidgetContent | undefined): void;
291
+ /** 拒绝原因(宿主 warn 文案与 fallback 分诊用;判定顺序同 validateWidgetIconPaths 实现)。 */
292
+ type WidgetIconPathsRejection =
293
+ /** 入参不是数组(含 undefined / null / 其他类型) */
294
+ 'not-array'
295
+ /** 空数组——渲染无形状,等同无效自定义 icon */
296
+ | 'empty'
297
+ /** 条数 > 8 */
298
+ | 'too-many'
299
+ /** 存在非 string 条目 */
300
+ | 'non-string'
301
+ /** 单条 > 512 字符 */
302
+ | 'too-long'
303
+ /** 全部条目总长 > 2048 字符 */
304
+ | 'total-too-long'
305
+ /** 含白名单外字符 */
306
+ | 'illegal-char';
307
+ /** icon paths 校验判定:合法携带白名单内的 paths 副本;非法携带原因(不抛异常)。 */
308
+ type WidgetIconPathsValidation = {
309
+ valid: true;
310
+ paths: string[];
311
+ } | {
312
+ valid: false;
313
+ reason: WidgetIconPathsRejection;
314
+ };
315
+ /**
316
+ * 校验 WidgetMeta.icon 的自定义形状 paths(`{ paths: string[] }` 的内层数组)。
317
+ * 不抛异常——非法即返回原因,宿主据此落兜底 icon + warn(协议对 extension 的
318
+ * 宽容边界:坏数据不崩渲染,只是拿不到自定义形状)。
319
+ */
320
+ declare function validateWidgetIconPaths(value: unknown): WidgetIconPathsValidation;
218
321
  /**
219
322
  * 从 details 中提取 GuiRenderResult。前端统一用此函数读取 __gui__,
220
323
  * 集中校验版本号,避免散落的 as 断言。
324
+ * component 字段校验(质量收敛批升级):从「仅查存在性」升级为复用 isGuiComponent
325
+ * 结构校验(type 字符串 + props 对象——含 null 排除;component 不是函数,可调用性
326
+ * 检查对其无意义)。非法值 → undefined,走调用方既有降级路径(AnsiText/纯文本兜底),
327
+ * 畸形 component 不再进渲染层。
221
328
  */
222
329
  declare function extractGui(details: Record<string, unknown> | undefined): GuiRenderResult | undefined;
223
330
  /**
@@ -308,13 +415,15 @@ declare function callMarkerRpc(ctx: GuiContext, marker: string, payload: string,
308
415
  /**
309
416
  * ask-user extension 的富交互类型定义。
310
417
  *
311
- * custom() 在 RPC 模式不可用(Component 是代码不是数据),ask-user 的「表单类」
312
- * 交互改走 select 通道:askUserInteract() 把 AskUserQuestion[] 序列化进 select 的
313
- * options[0],runtime event-adapter 检测 ASK_USER_MARKER 透传 questions,
314
- * 前端 AskUserOverlay 渲染富交互 UI。
418
+ * GUI 提问交互已迁移统一表单协议(../ui-form:FormQuestion 类型化问题集 +
419
+ * UI_FORM_MARKER + uiFormInteract,前端 FormOverlay 渲染)——ask_user 工具的
420
+ * 问题在 ask-user 包内归一 adapter 转为 FormQuestion(choice/text 形态)发送。
315
421
  *
316
- * 这是 ask-user 的定制协议,不是通用富交互协议。
317
- * 设计参考 ask-user 的 Question 结构。
422
+ * 本模块保留的 AskUserQuestion / AskUserAnswers 是 ask-user 的 LLM 入参契约与
423
+ * TUI/解码消费形态:FormAnswers 的 choice/text 部分与 AskUserAnswers 逐字兼容
424
+ * (键位规则与多选序列化一致),解码 helper(getAskUserAnswer/getAskUserOther)
425
+ * 消费本类型。legacy 帧(ASK_USER_MARKER)由 runtime event-adapter 分支归一转
426
+ * FormQuestion(D7 兼容窗口,归一层在 runtime 侧),随窗口末清理退役。
318
427
  */
319
428
  /**
320
429
  * ask-user 富交互问题声明。
@@ -368,30 +477,18 @@ type AskUserAnswers = Record<string, string>;
368
477
  declare const ASK_USER_MARKER = "\0TAIJI_ASK_USER";
369
478
 
370
479
  /**
371
- * ask-user extension 的富交互 helper(定制层)。
480
+ * ask-user extension 的富交互 helper(解码层)。
372
481
  *
373
- * askUserInteract() 是 ask-user extension 在 RPC 模式下的交互入口。
374
- * TUI 模式下 extension 必须自行调 ctx.ui.custom()。
482
+ * GUI 提问交互已迁移统一表单协议(ui-form 模块的 uiFormInteract + UI_FORM_MARKER,
483
+ * 设计 ui-presentation-protocol D3)——askUserInteract 传输 helper 随之退役,不保留
484
+ * deprecated 别名;AskUserQuestion↔FormQuestion 的归一职责下沉 ask-user 包内 adapter
485
+ * (extensions/universal/ask-user/src/form-adapter.ts)。
375
486
  *
376
- * 走 select 通道 + marker(ASK_USER_MARKER),复用 select 的全部管道逻辑
377
- * (队列 / 超时 / 回传 / abort),零重复代码。
487
+ * 本模块保留 answers 解码 helper(getAskUserAnswer / getAskUserOther):FormAnswers 的
488
+ * choice/text 部分与 AskUserAnswers 逐字兼容(键位规则:key = header ?? question,
489
+ * 多选 = JSON 数组,Other = `${key}__other`),解码契约在此单源。
378
490
  */
379
491
 
380
- /**
381
- * ask-user 富交互入口(RPC 模式专用)。
382
- *
383
- * RPC 模式:用 select 通道携带 questions 数据,前端渲染富交互 UI。
384
- * TUI 模式:抛错。extension 必须自行调 ctx.ui.custom()。
385
- *
386
- * @param ctx ExtensionContext(pi 提供)
387
- * @param questions 交互问题声明
388
- * @param options 可选:signal(abort)、allowCancel(前端是否显示取消按钮)
389
- * @returns answers(key=header/question, value=JSON编码的答案),用户取消返回 null
390
- */
391
- declare function askUserInteract(ctx: GuiContext, questions: AskUserQuestion[], options?: {
392
- signal?: AbortSignal;
393
- allowCancel?: boolean;
394
- }): Promise<AskUserAnswers | null>;
395
492
  /**
396
493
  * 从 answers 中提取某个问题的选中值(单选返回 string,多选返回 string[])。
397
494
  *
@@ -403,10 +500,288 @@ declare function getAskUserAnswer(answers: AskUserAnswers, question: AskUserQues
403
500
  declare function getAskUserOther(answers: AskUserAnswers, question: AskUserQuestion): string | undefined;
404
501
  /**
405
502
  * 类型守卫:验证 unknown 是否为合法的 AskUserQuestion。
406
- * 用于前端从 runtime 透传的 askUserQuestions(unknown[])中安全收窄。
503
+ * 供 legacy 帧消费方(runtime marker 分支侧形状参照 / 扩展自测)收窄 unknown[] 用。
407
504
  */
408
505
  declare function isAskUserQuestion(value: unknown): value is AskUserQuestion;
409
506
 
507
+ /**
508
+ * scheduler 创建确认弹框的协议类型(Draft / FormResult)。
509
+ *
510
+ * GUI 创建确认已迁移统一表单协议(ui-form 模块的 uiFormInteract + UI_FORM_MARKER,
511
+ * 交互入口 extensions/universal/scheduler/src/tool.ts:Draft 经 initial 预填为
512
+ * ScheduleQuestion 单问整表单,前端 FormOverlay 的 schedule 渲染器回传
513
+ * ScheduleFormResult)——ScheduleDraft / ScheduleFormResult 即该表单的预填与
514
+ * 回传形状,TUI 侧 ScheduleCreateComponent 同用本类型。
515
+ *
516
+ * 取消不走 FormResult:interact 取消 resolve undefined,execute 返回
517
+ * cancelled result(D5),任务不创建。
518
+ *
519
+ * SCHEDULE_CREATE_MARKER / ScheduleCreateOverlay 为 legacy 窗口语义(旧 npm 帧
520
+ * 由 runtime event-adapter 识别 / 迁移溯源锚点),随 D7 窗口末清理退役。
521
+ *
522
+ * 这是 scheduler 创建路径的定制协议,不是通用富交互协议(与 ask-user 同理)。
523
+ */
524
+ /**
525
+ * 执行模式:once = 一次性(schedule 为 dateToOnceCron 折叠的一次性 cron);
526
+ * recurring = 循环(duration 或 cron)。
527
+ */
528
+ type ScheduleKind = 'once' | 'recurring';
529
+ /** 创建确认草稿(extension → 前端,LLM 参数即预填值) */
530
+ interface ScheduleDraft {
531
+ /** LLM 判定的执行模式,用户可改 */
532
+ kind: ScheduleKind;
533
+ /**
534
+ * recurring:duration(如 5m/2h)或 cron;once:已折叠的一次性 cron
535
+ * (唯一时间来源,表单时间初值由 onceCronToDate 还原,D2)
536
+ */
537
+ schedule: string;
538
+ /** scoped model id;缺省 = 跟随会话当前模型 */
539
+ model?: string;
540
+ /** agent 起草的提示词 */
541
+ prompt: string;
542
+ /** 任务名(同现有工具参数,高级选项) */
543
+ name?: string;
544
+ /** 过期时间(同现有工具参数,高级选项) */
545
+ expires?: string;
546
+ /** scoped models 列表,前端不再自取 */
547
+ models: string[];
548
+ /** 会话当前模型 id(前端预选标记) */
549
+ currentModel?: string;
550
+ }
551
+ /**
552
+ * 创建确认回传(前端 → extension)。
553
+ * 取消不走此形状:select resolve undefined → execute 返回 cancelled result(D5)。
554
+ */
555
+ interface ScheduleFormResult {
556
+ action: 'create';
557
+ kind: ScheduleKind;
558
+ /** 用户确认后的最终形态(once = 经 dateToOnceCron 折叠的一次性 cron) */
559
+ schedule: string;
560
+ /** 用户最终选择;未选 = 缺省(跟随会话当前模型) */
561
+ model?: string;
562
+ prompt: string;
563
+ name?: string;
564
+ expires?: string;
565
+ }
566
+
567
+ /**
568
+ * scheduler 创建确认请求的 title marker。runtime event-adapter 和前端 useExtensionUI
569
+ * 检测此 marker 区分 schedule 创建确认请求与普通 select。
570
+ *
571
+ * NUL 前缀确保不会与 extension 正常的 select title 冲突。
572
+ * 与 ASK_USER_MARKER 同规范(ask-user 模块同包 marker.ts)。
573
+ */
574
+ declare const SCHEDULE_CREATE_MARKER = "\0TAIJI_SCHEDULE_CREATE";
575
+
576
+ /**
577
+ * scheduler 创建确认的共享协议资产(定制交互 helper 已退役)。
578
+ *
579
+ * scheduleCreateInteract() 已随统一提问表单协议迁移退役(ui-presentation-protocol
580
+ * D3/u6,直接退役不保留 deprecated 别名):scheduler 的创建确认改走
581
+ * uiFormInteract() + UI_FORM_MARKER(ScheduleQuestion 形态,initial 预填草稿),
582
+ * 交互入口在 extensions/universal/scheduler/src/tool.ts。
583
+ *
584
+ * 本模块保留三类跨端共享资产:
585
+ * - isScheduleDraft / isScheduleFormResult 形状守卫:前者供 runtime event-adapter
586
+ * legacy 分支(SCHEDULE_CREATE_MARKER 帧 payload 判定)与 ui-form 守卫的
587
+ * schedule 分支(initial 深度校验)消费;后者供 scheduler 包回包判别
588
+ * (uiFormInteract answers[key] 解包后收窄为 ScheduleFormResult)。
589
+ * - dateToOnceCron / onceCronToDate 时间折叠单点(D2):GUI(ScheduleForm 渲染器)
590
+ * 与 TUI(ScheduleCreateComponent)共用本实现,禁止双端各写一份——时间语义
591
+ * 漂移正是本协议要关闭的失败模式(F1)。
592
+ *
593
+ * 时区语义:按本地墙钟时间(croner 对 cron 默认本地解释,datetime 控件与 TUI
594
+ * 掩码编辑均产出本地墙钟时间)。
595
+ */
596
+
597
+ /**
598
+ * 类型守卫:验证 unknown 是否为合法的 ScheduleDraft。
599
+ * 用于 runtime event-adapter 判定 marker select 的 payload 是否为本协议 draft
600
+ * (检测失败降级普通 select),以及前端从 runtime 透传值中安全收窄。
601
+ * 字段白名单校验(与 isAskUserQuestion 同构):可选字段校验类型,未知附加字段忽略。
602
+ */
603
+ declare function isScheduleDraft(value: unknown): value is ScheduleDraft;
604
+ /** 回传形状守卫(导出消费:scheduler 包对 uiFormInteract answers[key] 解包值收窄;
605
+ * FormOverlay schedule 渲染器提交的 value = JSON.stringify(ScheduleFormResult)) */
606
+ declare function isScheduleFormResult(value: unknown): value is ScheduleFormResult;
607
+ /**
608
+ * 本地墙钟时刻 → 一次性 cron(5 段 `分 时 日 月 *`)。
609
+ *
610
+ * 一次性 cron 语义:具体日 + 具体月 + 星期通配 → croner 天然只命中一次(S8/D2),
611
+ * scheduler 现有 parseSchedule 直接接受,service 层零感知。
612
+ */
613
+ declare function dateToOnceCron(date: Date): string;
614
+ declare function onceCronToDate(cron: string, now?: Date): Date | null;
615
+
616
+ /**
617
+ * 统一提问表单协议(ui-form)的类型定义。
618
+ *
619
+ * 把 plan / scheduler / ask-user 三个 extension 的提问交互收口为一个表单协议:
620
+ * 问题即数据(类型化问题集),GUI 链路统一(wire 协议 + 渲染器)——多问 = 多 tab,
621
+ * 单问 = 单视图。extension 侧经 uiFormInteract() 序列化进 select 的 options[0],
622
+ * runtime event-adapter 检测 UI_FORM_MARKER 广播 form 帧,前端 FormOverlay
623
+ * 按问题 type 分派渲染器(choice / text / schedule)。
624
+ *
625
+ * 这是提问交互的通用协议(区别于 ask-user / scheduler-create 的定制协议)。
626
+ * formQuestions 在 TUI 无呈现语义——TUI 数据通路各 extension 自行渲染(设计 D8
627
+ * 有意取舍),消费方为 extension 自有组件。
628
+ */
629
+
630
+ /**
631
+ * choice 问题:互斥/多选选项。
632
+ * 对应现 AskUserQuestion 有 options 的形态(multiSelect → multi 重命名)。
633
+ */
634
+ interface ChoiceQuestion {
635
+ type: 'choice';
636
+ /** Tab 标签 / 简短标题。answers key 缺省 fallback 到 question 全文(与 askUserKey 同规则) */
637
+ header?: string;
638
+ /** 完整问题文本。也作为 answers 的 fallback key(header 缺失时) */
639
+ question: string;
640
+ /** 上下文摘要(可选)。显示在问题上方,帮用户理解背景 */
641
+ context?: string;
642
+ /** 互斥选项列表(无 options 的纯自由文本问题用 TextQuestion 表达) */
643
+ options: FormOption[];
644
+ /** 是否允许多选 */
645
+ multi?: boolean;
646
+ /** 是否允许自由文本输入(Other)。默认 true(同 ask-user 现状),设 false 则不追加 */
647
+ allowOther?: boolean;
648
+ }
649
+ /** 选项声明(label 即选中值:协议无独立 value 字段,回传统一用 label) */
650
+ interface FormOption {
651
+ label: string;
652
+ /** 描述(可选)。显示在 label 下方,解释 tradeoff */
653
+ description?: string;
654
+ }
655
+ /**
656
+ * text 问题:纯自由文本输入(无 options)。
657
+ * 协议值域完备性要求保留:AskUserQuestion.options 声明可选,归一层 type 推断
658
+ * 与消费端必须能表达「无 options」的合法旧形态(设计 D2)。
659
+ */
660
+ interface TextQuestion {
661
+ type: 'text';
662
+ header?: string;
663
+ question: string;
664
+ context?: string;
665
+ }
666
+ /**
667
+ * schedule 问题:时间输入领域原语(非 scheduler 专属形状)。
668
+ * 渲染器 = scheduler 创建确认整表单(ScheduleForm,单 tab 整表单收编,设计 D2);
669
+ * 预填草稿经 initial 直传,打开即可一键确认。
670
+ */
671
+ interface ScheduleQuestion {
672
+ type: 'schedule';
673
+ header?: string;
674
+ question: string;
675
+ context?: string;
676
+ /** 预填草稿(可选)。ScheduleDraft 复用 scheduler-create 模块定义(零新增依赖方向) */
677
+ initial?: ScheduleDraft;
678
+ }
679
+ /** 类型化问题集(判别联合:按 type 分派渲染器) */
680
+ type FormQuestion = ChoiceQuestion | TextQuestion | ScheduleQuestion;
681
+ /**
682
+ * 统一表单回传结果。key = header ?? question(fallback 规则与 askUserKey 一致)。
683
+ *
684
+ * 答案编码规则(choice/text 部分与现 AskUserAnswers 逐字兼容,含 Other 键规则
685
+ * 与多选序列化——ask-user 消费方 getAskUserAnswer / getAskUserOther 零改动):
686
+ * - choice 单选:value = 选中项的 label
687
+ * - choice 多选:value = JSON.stringify(选中项 label 数组),如 '["pg","mysql"]'
688
+ * - Other 文本:单独 key `${key}__other`,value = 自由文本(不混进选中项数组)
689
+ * - text:value = 自由文本,**键位 `${key}__other`**(逐字继承现状纯 other 形态——
690
+ * 无 options 题答案只写 __other 不写主 key,写主 key 会令 getAskUserAnswer 键位翻转)
691
+ * - schedule:value = JSON.stringify(ScheduleFormResult)(单问表单下 answers 恰一键)
692
+ */
693
+ type FormAnswers = Record<string, string>;
694
+
695
+ /**
696
+ * 统一提问表单(ui-form)请求的 title marker。runtime event-adapter 和前端
697
+ * useExtensionUI 检测此 marker 区分表单请求与普通 select。
698
+ *
699
+ * NUL 前缀确保不会与 extension 正常的 select title 冲突。
700
+ * 与 ASK_USER_MARKER 同规范(ask-user 模块同包 marker.ts)。
701
+ */
702
+ declare const UI_FORM_MARKER = "\0TAIJI_UI_FORM";
703
+
704
+ /**
705
+ * 统一提问表单(ui-form)协议 helper。
706
+ *
707
+ * uiFormInteract() 是 plan / scheduler / ask-user 三方提问在 RPC 模式下的统一入口
708
+ * (设计 ui-presentation-protocol D3)。走 select 通道 + marker(UI_FORM_MARKER),
709
+ * 传输核复用 callMarkerRpc 原语,四态判别返回而非抛错(除 RPC 门外):
710
+ * - cancelled / timeout → 调用方按「用户未作答」折叠;
711
+ * - channel-error → 通道契约破坏(含 echo 检测命中的旧宿主组合),调用方按各自
712
+ * 策略折叠(scheduler 禁用工具 + throw / plan 折 cancelled result 留 plan mode);
713
+ * - non-json → 协议版本错配类故障。
714
+ *
715
+ * TUI 模式不代劳渲染(formQuestions 在 TUI 无呈现语义,各 extension 自有组件
716
+ * 消费,设计 D8):误调抛错指明恢复动作,返回判别失败会与「用户取消」混淆。
717
+ */
718
+
719
+ /** uiFormInteract 的判别结果:成功收窄为 FormAnswers;失败按 callMarkerRpc 四态折叠。
720
+ * message 仅 channel-error 的 echo 命中态携带(升级指引,见 uiFormInteract 内注释),
721
+ * 供调用方折叠时透出——各调用方对 channel-error 的折叠策略不同,文案由调用方决定是否使用 */
722
+ type UiFormInteractResult = {
723
+ ok: true;
724
+ answers: FormAnswers;
725
+ } | {
726
+ ok: false;
727
+ reason: 'cancelled' | 'timeout' | 'channel-error' | 'non-json';
728
+ message?: string;
729
+ };
730
+ interface UiFormInteractOptions {
731
+ /** 透传 select dialog:abort 后 pi 本地 resolve(undefined) → cancelled */
732
+ signal?: AbortSignal;
733
+ /** 前端是否显示取消按钮(默认 true,沿已退役的 askUserInteract 的默认值先例) */
734
+ allowCancel?: boolean;
735
+ /**
736
+ * 源元数据:本次表单提交后是否有 turn 预期(respond 分型判据,form-submit-busy-convergence D1)。
737
+ *
738
+ * 三态语义(D2):`true | undefined` = 有 turn 预期——提交型桥接 message_start 照旧
739
+ * (undefined 为缺省态:存量扩展与未声明者,桥接 = 现状,方向安全);
740
+ * `false` = 无 turn 预期——命令 handler 内 select(提交后结构性无 turn),respond 侧
741
+ * 提交即收尾,消除 30s 假忙。消费方判定必须是 `=== false` 显式式:truthy 简化
742
+ * (`!expectTurn`)会把 undefined 也当无 turn、误清桥接(D2 被否谱系)。
743
+ *
744
+ * 缺省(undefined)经 stripUndefined 剥键——未声明者的 wire payload 逐字节不变,
745
+ * 旧 runtime 读不到即忽略(JSON.parse 容忍未知键)。
746
+ */
747
+ expectTurn?: boolean;
748
+ /** 失败留痕注入(echo 命中 / 形状错),日志策略归调用方 */
749
+ log?: (msg: string, detail?: object) => void;
750
+ }
751
+ /**
752
+ * 统一提问表单入口(RPC 模式专用)。
753
+ *
754
+ * RPC 模式:select 通道携带类型化问题集,前端 FormOverlay 渲染表单,回传 FormAnswers。
755
+ * TUI 模式:抛错,extension 自行渲染(ctx.ui.custom 或原生 select)。
756
+ *
757
+ * 确认交互不设墙钟超时(任务级正常路径无墙钟,AGENTS.md 超时默认原则):
758
+ * options 不提供 timeout 入口,timeout 态仅由「未 abort 的 undefined resolve」产生。
759
+ */
760
+ declare function uiFormInteract(ctx: GuiContext, form: FormQuestion[], opts?: UiFormInteractOptions): Promise<UiFormInteractResult>;
761
+
762
+ /**
763
+ * 统一提问表单(ui-form)的形状守卫。
764
+ *
765
+ * isFormQuestion 按 type 判别分支逐项校验(白名单风格:必填字段类型校验、可选
766
+ * 字段校验、未知字段忽略——与 isAskUserQuestion / isScheduleDraft 同构语义)。
767
+ * 三个消费点失败策略各异(设计 D2):
768
+ * - 发送侧(uiFormInteract 内):不合法项抛错(调用方编码 bug,fail-fast);
769
+ * - event-adapter:逐项过滤,合法项 ≥1 才产 form 帧、全不合法降级普通 select;
770
+ * - renderer:复核守卫,不合法项跳过 + warn,全部不合法 → overlay 错误占位仅取消可点。
771
+ *
772
+ * isFormAnswers 校验回包形状(Record<string, string>):键动态(key = header ??
773
+ * question)无法枚举白名单,故只校验值域全 string;JSON 合法但非本形状由
774
+ * uiFormInteract 折叠 non-json(协议版本错配类故障)。
775
+ */
776
+
777
+ /**
778
+ * 类型守卫:验证 unknown 是否为合法的 FormQuestion(choice / text / schedule 三分支)。
779
+ * 用于发送侧 fail-fast、event-adapter 逐项过滤与 renderer 复核(三消费点见文件头)。
780
+ */
781
+ declare function isFormQuestion(value: unknown): value is FormQuestion;
782
+ /** 类型守卫:验证 unknown 是否为合法的 FormAnswers(值域全 string 的普通对象) */
783
+ declare function isFormAnswers(value: unknown): value is FormAnswers;
784
+
410
785
  /**
411
786
  * session-manager 请求的 title marker。runtime event-adapter 和 handler
412
787
  * 检测此 marker 区分 session-manager 请求与普通 select。
@@ -795,4 +1170,4 @@ declare function collectActivePendingIds(entries: unknown[], opts?: CollectPendi
795
1170
  type MappedPendingStatus = 'completed' | 'failed' | 'cancelled' | 'expired' | 'time_limited' | 'aborted';
796
1171
  declare function mapReasonToStatus(reason: string): MappedPendingStatus;
797
1172
 
798
- export { ASK_USER_MARKER, type AskUserAnswers, type AskUserOption, type AskUserQuestion, BRIDGE_MARKER, BRIDGE_METHODS, type BridgeErrorResponse, type BridgeInterceptResponse, type BridgeMethod, type BridgeRequest, type BridgeSyncPayload, type BridgeToolExecuteResponse, type ChannelErrorResult, type CollectPendingIdsOptions, type DualWidgetContent, GUI_WIDGET_MARKER, type GuiComponent, type GuiComponentProps, type GuiComponentType, type GuiContext, type GuiRenderResult, INFLIGHT_REPORT_ACK, type MappedPendingStatus, type MarkerRpcOptions, type MarkerRpcResult, PENDING_REGISTER_ENTRY_TYPE, PENDING_UNREGISTER_ENTRY_TYPE, PROTOCOL_VERSION, type PendingEntriesScan, SESSION_MANAGER_ACTIONS, SESSION_MANAGER_MARKER, SUBAGENTS_ENGINES_FILENAME, SUBAGENT_INFLIGHT_MARKER, type SessionManagerAbortParams, type SessionManagerAbortResult, type SessionManagerAction, type SessionManagerCreateParams, type SessionManagerCreateResult, type SessionManagerErrorResult, type SessionManagerHistoryParams, type SessionManagerHistoryResult, type SessionManagerListParams, type SessionManagerListResult, type SessionManagerParams, type SessionManagerSendParams, type SessionManagerSendResult, type SessionManagerSessionSummary, type SessionManagerStatusParams, type SessionManagerStatusResult, type StatItem, type SubagentEngineConfigView, type SubagentEnginesFile, type SubagentInFlightReport, type TreeItem, type TreeItemIcon, type WidgetMeta, applyPendingDiff, askUserInteract, callMarkerRpc, collectActivePendingIds, extractGui, firstContentText, formatChannelErrorText, getAskUserAnswer, getAskUserOther, guiComponent, guiResult, guiSetWidget, isAskUserQuestion, isBridgeErrorResponse, isBridgeInterceptResponse, isBridgeSyncPayload, isBridgeToolExecuteResponse, isChannelErrorResult, isGuiCapable, isGuiComponent, isGuiRenderResult, isInFlightReportAck, isSessionManagerAbortParams, isSessionManagerCreateParams, isSessionManagerHistoryParams, isSessionManagerListParams, isSessionManagerSendParams, isSessionManagerStatusParams, isSubagentInFlightReport, isSyncedTool, mapReasonToStatus, scanPendingEntries, setWidgetDual };
1173
+ export { ASK_USER_MARKER, type AskUserAnswers, type AskUserOption, type AskUserQuestion, BRIDGE_MARKER, BRIDGE_METHODS, type BridgeErrorResponse, type BridgeInterceptResponse, type BridgeMethod, type BridgeRequest, type BridgeSyncPayload, type BridgeToolExecuteResponse, type ChannelErrorResult, type ChoiceQuestion, type CollectPendingIdsOptions, type DualWidgetContent, type FormAnswers, type FormOption, type FormQuestion, GUI_WIDGET_MARKER, type GuiComponent, type GuiComponentProps, type GuiComponentType, type GuiContext, type GuiRenderResult, INFLIGHT_REPORT_ACK, type MappedPendingStatus, type MarkerRpcOptions, type MarkerRpcResult, PENDING_REGISTER_ENTRY_TYPE, PENDING_UNREGISTER_ENTRY_TYPE, PLAN_REVIEW_MARKER, PROTOCOL_VERSION, type PendingEntriesScan, type PlanDocMeta, type PlanReviewComment, type PlanReviewDecision, type PlanReviewRequest, type PlanReviewResponse, SCHEDULE_CREATE_MARKER, SESSION_MANAGER_ACTIONS, SESSION_MANAGER_MARKER, SUBAGENTS_ENGINES_FILENAME, SUBAGENT_INFLIGHT_MARKER, type ScheduleDraft, type ScheduleFormResult, type ScheduleKind, type ScheduleQuestion, type SessionManagerAbortParams, type SessionManagerAbortResult, type SessionManagerAction, type SessionManagerCreateParams, type SessionManagerCreateResult, type SessionManagerErrorResult, type SessionManagerHistoryParams, type SessionManagerHistoryResult, type SessionManagerListParams, type SessionManagerListResult, type SessionManagerParams, type SessionManagerSendParams, type SessionManagerSendResult, type SessionManagerSessionSummary, type SessionManagerStatusParams, type SessionManagerStatusResult, type StatItem, type SubagentEngineConfigView, type SubagentEnginesFile, type SubagentInFlightReport, type TextQuestion, type TreeItem, type TreeItemIcon, UI_FORM_MARKER, type UiFormInteractOptions, type UiFormInteractResult, type WidgetIconPathsRejection, type WidgetIconPathsValidation, type WidgetMeta, applyPendingDiff, callMarkerRpc, collectActivePendingIds, dateToOnceCron, extractGui, firstContentText, formatChannelErrorText, getAskUserAnswer, getAskUserOther, guiComponent, guiResult, guiSetWidget, isAskUserQuestion, isBridgeErrorResponse, isBridgeInterceptResponse, isBridgeSyncPayload, isBridgeToolExecuteResponse, isChannelErrorResult, isFormAnswers, isFormQuestion, isGuiCapable, isGuiComponent, isGuiRenderResult, isInFlightReportAck, isScheduleDraft, isScheduleFormResult, isSessionManagerAbortParams, isSessionManagerCreateParams, isSessionManagerHistoryParams, isSessionManagerListParams, isSessionManagerSendParams, isSessionManagerStatusParams, isSubagentInFlightReport, isSyncedTool, mapReasonToStatus, onceCronToDate, scanPendingEntries, setWidgetDual, uiFormInteract, validateWidgetIconPaths };
package/dist/index.js CHANGED
@@ -14,6 +14,7 @@ var PROTOCOL_VERSION = 1;
14
14
 
15
15
  // src/core/markers.ts
16
16
  var GUI_WIDGET_MARKER = "\0TAIJI_GUI_WIDGET:";
17
+ var PLAN_REVIEW_MARKER = "\0TAIJI_PLAN_REVIEW:";
17
18
 
18
19
  // src/core/helpers.ts
19
20
  function isGuiCapable(ctx) {
@@ -51,9 +52,30 @@ function setWidgetDual(ctx, key, content) {
51
52
  ctx.ui.setWidget(key, content.text);
52
53
  }
53
54
  }
55
+ var ICON_PATH_D_ALLOWED_RE = /^[MLCQAZHVSTmlcqazhvst0-9 ,.\-]+$/;
56
+ var ICON_PATHS_MAX_ENTRIES = 8;
57
+ var ICON_PATH_MAX_CHARS = 512;
58
+ var ICON_PATHS_MAX_TOTAL_CHARS = 2048;
59
+ function validateWidgetIconPaths(value) {
60
+ if (!Array.isArray(value)) return { valid: false, reason: "not-array" };
61
+ if (value.length === 0) return { valid: false, reason: "empty" };
62
+ if (value.length > ICON_PATHS_MAX_ENTRIES) return { valid: false, reason: "too-many" };
63
+ const entries = value;
64
+ const paths = [];
65
+ let total = 0;
66
+ for (const entry of entries) {
67
+ if (typeof entry !== "string") return { valid: false, reason: "non-string" };
68
+ if (entry.length > ICON_PATH_MAX_CHARS) return { valid: false, reason: "too-long" };
69
+ if (!ICON_PATH_D_ALLOWED_RE.test(entry)) return { valid: false, reason: "illegal-char" };
70
+ paths.push(entry);
71
+ total += entry.length;
72
+ }
73
+ if (total > ICON_PATHS_MAX_TOTAL_CHARS) return { valid: false, reason: "total-too-long" };
74
+ return { valid: true, paths };
75
+ }
54
76
  function extractGui(details) {
55
77
  const g = details?.__gui__;
56
- if (g && typeof g === "object" && "v" in g && "component" in g && g.v === PROTOCOL_VERSION) {
78
+ if (g && typeof g === "object" && "v" in g && isGuiComponent(g.component) && g.v === PROTOCOL_VERSION) {
57
79
  return g;
58
80
  }
59
81
  return void 0;
@@ -137,31 +159,6 @@ async function callMarkerRpc(ctx, marker, payload, opts) {
137
159
  var ASK_USER_MARKER = "\0TAIJI_ASK_USER";
138
160
 
139
161
  // src/extensions/ask-user/helpers.ts
140
- async function askUserInteract(ctx, questions, options) {
141
- if (questions.length === 0) return {};
142
- if (isGuiCapable(ctx) && ctx.ui?.select) {
143
- const payload = JSON.stringify(stripUndefined({
144
- questions,
145
- allowCancel: options?.allowCancel ?? true
146
- }));
147
- const value = await ctx.ui.select(
148
- ASK_USER_MARKER,
149
- // title = marker,runtime/前端据此识别
150
- [payload],
151
- // options[0] = JSON payload(runtime 解析)
152
- { signal: options?.signal }
153
- );
154
- if (value === void 0) return null;
155
- try {
156
- return JSON.parse(value);
157
- } catch {
158
- return null;
159
- }
160
- }
161
- throw new Error(
162
- "askUserInteract() is only available in RPC mode. In TUI mode, use ctx.ui.custom() with your own Component directly."
163
- );
164
- }
165
162
  function askUserKey(question) {
166
163
  return question.header ?? question.question;
167
164
  }
@@ -188,18 +185,173 @@ function isAskUserQuestion(value) {
188
185
  return typeof q.question === "string" && (q.header === void 0 || typeof q.header === "string") && (q.options === void 0 || Array.isArray(q.options)) && (q.multiSelect === void 0 || typeof q.multiSelect === "boolean") && (q.allowOther === void 0 || typeof q.allowOther === "boolean");
189
186
  }
190
187
 
188
+ // src/extensions/scheduler-create/marker.ts
189
+ var SCHEDULE_CREATE_MARKER = "\0TAIJI_SCHEDULE_CREATE";
190
+
191
+ // src/extensions/scheduler-create/helpers.ts
192
+ function isPlainRecord(value) {
193
+ return typeof value === "object" && value !== null && !Array.isArray(value);
194
+ }
195
+ function isOptionalString(v) {
196
+ return v === void 0 || typeof v === "string";
197
+ }
198
+ function isStringArray(v) {
199
+ return Array.isArray(v) && v.every((m) => typeof m === "string");
200
+ }
201
+ function isScheduleDraft(value) {
202
+ if (!isPlainRecord(value)) return false;
203
+ const d = value;
204
+ return (d.kind === "once" || d.kind === "recurring") && typeof d.schedule === "string" && isOptionalString(d.model) && typeof d.prompt === "string" && isOptionalString(d.name) && isOptionalString(d.expires) && isStringArray(d.models) && isOptionalString(d.currentModel);
205
+ }
206
+ function isScheduleFormResult(value) {
207
+ if (typeof value !== "object" || value === null || Array.isArray(value)) return false;
208
+ const r = value;
209
+ return r.action === "create" && (r.kind === "once" || r.kind === "recurring") && typeof r.schedule === "string" && (r.model === void 0 || typeof r.model === "string") && typeof r.prompt === "string" && (r.name === void 0 || typeof r.name === "string") && (r.expires === void 0 || typeof r.expires === "string");
210
+ }
211
+ function dateToOnceCron(date) {
212
+ return `${date.getMinutes()} ${date.getHours()} ${date.getDate()} ${date.getMonth() + 1} *`;
213
+ }
214
+ var ONCE_CRON_FIELD_COUNT = 5;
215
+ var CRON_MINUTE_MAX = 59;
216
+ var CRON_HOUR_MAX = 23;
217
+ var CRON_DAY_MIN = 1;
218
+ var CRON_DAY_MAX = 31;
219
+ var CRON_MONTH_MIN = 1;
220
+ var CRON_MONTH_MAX = 12;
221
+ var ONCE_CRON_MAX_YEAR_LOOKAHEAD = 8;
222
+ function parseOnceCronNumbers(parts) {
223
+ const [minute, hour, day, month] = parts;
224
+ if (![minute, hour, day, month].every((seg) => /^\d+$/.test(seg))) return null;
225
+ return [Number(minute), Number(hour), Number(day), Number(month)];
226
+ }
227
+ function isOnceCronValueOutOfRange(m, h, d, mon) {
228
+ return m > CRON_MINUTE_MAX || h > CRON_HOUR_MAX || d < CRON_DAY_MIN || d > CRON_DAY_MAX || mon < CRON_MONTH_MIN || mon > CRON_MONTH_MAX;
229
+ }
230
+ function onceCronToDate(cron, now = /* @__PURE__ */ new Date()) {
231
+ const parts = cron.trim().split(/\s+/);
232
+ if (parts.length !== ONCE_CRON_FIELD_COUNT || parts[4] !== "*") return null;
233
+ const numbers = parseOnceCronNumbers(parts);
234
+ if (numbers === null) return null;
235
+ const [m, h, d, mon] = numbers;
236
+ if (isOnceCronValueOutOfRange(m, h, d, mon)) return null;
237
+ const startYear = now.getFullYear();
238
+ for (let year = startYear; year <= startYear + ONCE_CRON_MAX_YEAR_LOOKAHEAD; year++) {
239
+ const candidate = new Date(year, mon - 1, d, h, m, 0, 0);
240
+ if (candidate.getMonth() !== mon - 1 || candidate.getDate() !== d) continue;
241
+ if (candidate.getTime() >= now.getTime()) return candidate;
242
+ }
243
+ return null;
244
+ }
245
+
246
+ // src/extensions/ui-form/marker.ts
247
+ var UI_FORM_MARKER = "\0TAIJI_UI_FORM";
248
+
249
+ // src/extensions/ui-form/guards.ts
250
+ function isPlainRecord2(value) {
251
+ return typeof value === "object" && value !== null && !Array.isArray(value);
252
+ }
253
+ function isFormOption(value) {
254
+ if (!isPlainRecord2(value)) return false;
255
+ const o = value;
256
+ return typeof o.label === "string" && (o.description === void 0 || typeof o.description === "string");
257
+ }
258
+ function isFormOptionArray(value) {
259
+ return Array.isArray(value) && value.every(isFormOption);
260
+ }
261
+ function isFormQuestion(value) {
262
+ if (!isPlainRecord2(value)) return false;
263
+ const q = value;
264
+ if (typeof q.question !== "string") return false;
265
+ if (q.header !== void 0 && typeof q.header !== "string") return false;
266
+ if (q.context !== void 0 && typeof q.context !== "string") return false;
267
+ if (q.type === "choice") {
268
+ return isFormOptionArray(q.options) && (q.multi === void 0 || typeof q.multi === "boolean") && (q.allowOther === void 0 || typeof q.allowOther === "boolean");
269
+ }
270
+ if (q.type === "text") return true;
271
+ if (q.type === "schedule") {
272
+ return q.initial === void 0 || isScheduleDraft(q.initial);
273
+ }
274
+ return false;
275
+ }
276
+ function isFormAnswers(value) {
277
+ if (!isPlainRecord2(value)) return false;
278
+ return Object.values(value).every((v) => typeof v === "string");
279
+ }
280
+
281
+ // src/extensions/ui-form/helpers.ts
282
+ var RESPONSE_PREVIEW_LENGTH2 = 200;
283
+ var ECHO_UPGRADE_HINT = "taiji host too old for form protocol \u2014 upgrade taiji or pin extension version";
284
+ function assertFormQuestionsValid(form) {
285
+ const invalidIndex = form.findIndex((q) => !isFormQuestion(q));
286
+ if (invalidIndex >= 0) {
287
+ throw new Error(
288
+ `uiFormInteract(): form[${invalidIndex}] is not a valid FormQuestion (type must be 'choice' | 'text' | 'schedule' with required fields present) \u2014 fix the question definition at the call site.`
289
+ );
290
+ }
291
+ }
292
+ function assertRpcModeAvailable(ctx) {
293
+ if (!(isGuiCapable(ctx) && ctx.ui?.select)) {
294
+ throw new Error(
295
+ "uiFormInteract() is only available in RPC mode. In TUI mode, use ctx.ui.custom() with your own Component directly."
296
+ );
297
+ }
298
+ }
299
+ function parseFormAnswersResponse(rpcValue, payload, opts) {
300
+ if (rpcValue === payload) {
301
+ opts?.log?.("ui-form response echoed the request payload (host does not understand UI_FORM_MARKER)", {
302
+ responseHead: rpcValue.slice(0, RESPONSE_PREVIEW_LENGTH2)
303
+ });
304
+ return { ok: false, reason: "channel-error", message: ECHO_UPGRADE_HINT };
305
+ }
306
+ let parsed;
307
+ try {
308
+ parsed = JSON.parse(rpcValue);
309
+ } catch {
310
+ return { ok: false, reason: "non-json" };
311
+ }
312
+ if (!isFormAnswers(parsed)) {
313
+ opts?.log?.("ui-form response is not a FormAnswers record", {
314
+ responseHead: rpcValue.slice(0, RESPONSE_PREVIEW_LENGTH2)
315
+ });
316
+ return { ok: false, reason: "non-json" };
317
+ }
318
+ return { ok: true, answers: parsed };
319
+ }
320
+ async function uiFormInteract(ctx, form, opts) {
321
+ if (form.length === 0) return { ok: true, answers: {} };
322
+ assertFormQuestionsValid(form);
323
+ assertRpcModeAvailable(ctx);
324
+ const payload = JSON.stringify(stripUndefined({
325
+ formQuestions: form,
326
+ allowCancel: opts?.allowCancel ?? true,
327
+ // expectTurn 原样透传、不设 ?? true 缺省:undefined 由 stripUndefined 剥键保持
328
+ // 存量 wire 逐字节不变,三态归 respond 分型按 === false 显式判定(D1/D2)
329
+ expectTurn: opts?.expectTurn
330
+ }));
331
+ const rpcResult = await callMarkerRpc(
332
+ ctx,
333
+ UI_FORM_MARKER,
334
+ payload,
335
+ { signal: opts?.signal, log: opts?.log }
336
+ );
337
+ if (!rpcResult.ok) {
338
+ return { ok: false, reason: rpcResult.reason };
339
+ }
340
+ return parseFormAnswersResponse(rpcResult.value, payload, opts);
341
+ }
342
+
191
343
  // src/extensions/session-manager/types.ts
192
344
  function isRecord(v) {
193
345
  return typeof v === "object" && v !== null && !Array.isArray(v);
194
346
  }
195
- function isOptionalString(v) {
347
+ function isOptionalString2(v) {
196
348
  return v === void 0 || typeof v === "string";
197
349
  }
198
350
  function isSessionIdParams(v) {
199
351
  return isRecord(v) && typeof v.sessionId === "string";
200
352
  }
201
353
  function isSessionManagerCreateParams(v) {
202
- return isRecord(v) && isOptionalString(v.cwd) && isOptionalString(v.label) && isOptionalString(v.prompt);
354
+ return isRecord(v) && isOptionalString2(v.cwd) && isOptionalString2(v.label) && isOptionalString2(v.prompt);
203
355
  }
204
356
  function isSessionManagerSendParams(v) {
205
357
  return isRecord(v) && typeof v.sessionId === "string" && typeof v.prompt === "string";
@@ -345,15 +497,18 @@ export {
345
497
  MAX_TERMINAL_REGISTRY_ENTRIES,
346
498
  PENDING_REGISTER_ENTRY_TYPE,
347
499
  PENDING_UNREGISTER_ENTRY_TYPE,
500
+ PLAN_REVIEW_MARKER,
348
501
  PROTOCOL_VERSION,
502
+ SCHEDULE_CREATE_MARKER,
349
503
  SESSION_MANAGER_ACTIONS,
350
504
  SESSION_MANAGER_MARKER,
351
505
  SUBAGENTS_ENGINES_FILENAME,
352
506
  SUBAGENT_INFLIGHT_MARKER,
507
+ UI_FORM_MARKER,
353
508
  applyPendingDiff,
354
- askUserInteract,
355
509
  callMarkerRpc,
356
510
  collectActivePendingIds,
511
+ dateToOnceCron,
357
512
  extractGui,
358
513
  firstContentText,
359
514
  formatChannelErrorText,
@@ -370,10 +525,14 @@ export {
370
525
  isBridgeSyncPayload,
371
526
  isBridgeToolExecuteResponse,
372
527
  isChannelErrorResult,
528
+ isFormAnswers,
529
+ isFormQuestion,
373
530
  isGuiCapable,
374
531
  isGuiComponent,
375
532
  isGuiRenderResult,
376
533
  isInFlightReportAck,
534
+ isScheduleDraft,
535
+ isScheduleFormResult,
377
536
  isSessionManagerAbortParams,
378
537
  isSessionManagerCreateParams,
379
538
  isSessionManagerHistoryParams,
@@ -384,6 +543,9 @@ export {
384
543
  isSyncedTool,
385
544
  isTerminalBackgroundTaskState,
386
545
  mapReasonToStatus,
546
+ onceCronToDate,
387
547
  scanPendingEntries,
388
- setWidgetDual
548
+ setWidgetDual,
549
+ uiFormInteract,
550
+ validateWidgetIconPaths
389
551
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zhushanwen/extension-protocol",
3
- "version": "0.11.0",
3
+ "version": "0.13.0",
4
4
  "description": "Cross-layer pi extension contract package: dual-mode (TUI/GUI) rendering helpers, session/background-task/pending-entries protocols, and shared behavior primitives",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",