feishu-codex-console 1.0.0-beta.6

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.
Files changed (113) hide show
  1. package/.env.example +101 -0
  2. package/.feishu-codex-policy.example.json +11 -0
  3. package/.feishu-codex-runbooks.example.json +36 -0
  4. package/CHANGELOG.md +129 -0
  5. package/CODE_OF_CONDUCT.md +7 -0
  6. package/CONTRIBUTING.md +52 -0
  7. package/LICENSE +21 -0
  8. package/README.en.md +81 -0
  9. package/README.md +398 -0
  10. package/ROADMAP.md +40 -0
  11. package/SECURITY.md +53 -0
  12. package/dist/account-quota-card.js +233 -0
  13. package/dist/account-quota.js +125 -0
  14. package/dist/app-server-client.js +281 -0
  15. package/dist/card-session.js +166 -0
  16. package/dist/codex-events.js +1 -0
  17. package/dist/codex-runner.js +875 -0
  18. package/dist/config.js +198 -0
  19. package/dist/confirmation-card.js +135 -0
  20. package/dist/control-card.js +345 -0
  21. package/dist/conversation-turn-session.js +209 -0
  22. package/dist/data-maintenance.js +71 -0
  23. package/dist/device-card.js +460 -0
  24. package/dist/device-health.js +94 -0
  25. package/dist/diagnostics.js +253 -0
  26. package/dist/doctor.js +250 -0
  27. package/dist/fallback-card-session.js +37 -0
  28. package/dist/health-file.js +75 -0
  29. package/dist/index.js +4330 -0
  30. package/dist/lark-cli.js +558 -0
  31. package/dist/lark-retry.js +34 -0
  32. package/dist/maintenance.js +140 -0
  33. package/dist/model-capabilities.js +31 -0
  34. package/dist/onboarding-card.js +312 -0
  35. package/dist/permission-lease.js +22 -0
  36. package/dist/policy.js +506 -0
  37. package/dist/progress.js +267 -0
  38. package/dist/project-card.js +303 -0
  39. package/dist/project-overview-card.js +182 -0
  40. package/dist/project-overview.js +278 -0
  41. package/dist/project-policy.js +160 -0
  42. package/dist/project-registry.js +259 -0
  43. package/dist/project-status.js +45 -0
  44. package/dist/project-workspace.js +55 -0
  45. package/dist/quota-card.js +94 -0
  46. package/dist/recovery-policy.js +26 -0
  47. package/dist/redaction.js +67 -0
  48. package/dist/remote-ready.js +112 -0
  49. package/dist/response-card.js +139 -0
  50. package/dist/result-card.js +166 -0
  51. package/dist/review-card.js +452 -0
  52. package/dist/runbook-card.js +272 -0
  53. package/dist/runbooks.js +191 -0
  54. package/dist/runtime-card.js +337 -0
  55. package/dist/session-card.js +128 -0
  56. package/dist/session-naming.js +14 -0
  57. package/dist/smoke.js +28 -0
  58. package/dist/state-backup.js +302 -0
  59. package/dist/state-store.js +874 -0
  60. package/dist/task-card.js +640 -0
  61. package/dist/task-center-card.js +176 -0
  62. package/dist/task-failure.js +43 -0
  63. package/dist/task-intent.js +76 -0
  64. package/dist/task-queue.js +187 -0
  65. package/dist/task-reconciliation.js +80 -0
  66. package/dist/task-review.js +497 -0
  67. package/dist/team-card.js +275 -0
  68. package/dist/team-directory.js +54 -0
  69. package/dist/team-policy.js +93 -0
  70. package/dist/types.js +1 -0
  71. package/dist/version.js +9 -0
  72. package/dist/workspace-session.js +64 -0
  73. package/docs/ARCHITECTURE.md +54 -0
  74. package/docs/COMPATIBILITY.md +55 -0
  75. package/docs/CONFIGURATION.md +88 -0
  76. package/docs/DEMO.md +45 -0
  77. package/docs/GOOD_FIRST_ISSUES.md +23 -0
  78. package/docs/INSTALLATION.md +207 -0
  79. package/docs/OPEN_SOURCE_PRODUCT_PLAN.md +113 -0
  80. package/docs/PRODUCT_REQUIREMENTS_MAP.md +591 -0
  81. package/docs/RELEASE_CHECKLIST.md +65 -0
  82. package/docs/TEAM_DEPLOYMENT.md +35 -0
  83. package/docs/TROUBLESHOOTING.md +130 -0
  84. package/docs/V4_WORKSPACE_SESSION_FLOW.md +232 -0
  85. package/docs/requirements/D10_MAINTENANCE_AND_ECOSYSTEM.md +103 -0
  86. package/docs/requirements/D1_INSTALLATION_AND_FIRST_CONNECTION.md +479 -0
  87. package/docs/requirements/D2_DEVICE_AND_CONNECTIVITY.md +54 -0
  88. package/docs/requirements/D3_PROJECTS_AND_SESSIONS.md +107 -0
  89. package/docs/requirements/D4_REMOTE_TASK_EXECUTION.md +102 -0
  90. package/docs/requirements/D5_CODEX_NATIVE_INTERACTIONS.md +99 -0
  91. package/docs/requirements/D6_RESULTS_AND_CODE_REVIEW.md +100 -0
  92. package/docs/requirements/D7_SECURITY_GOVERNANCE.md +106 -0
  93. package/docs/requirements/D8_RELIABILITY_AND_RECOVERY.md +182 -0
  94. package/docs/requirements/D9_TEAM_COLLABORATION.md +129 -0
  95. package/package.json +76 -0
  96. package/scripts/capability-probe.mjs +113 -0
  97. package/scripts/cli.mjs +919 -0
  98. package/scripts/config-file.mjs +137 -0
  99. package/scripts/discovery-lib.mjs +78 -0
  100. package/scripts/install-card.mjs +37 -0
  101. package/scripts/install-detection.mjs +126 -0
  102. package/scripts/install-state.mjs +107 -0
  103. package/scripts/launchd.mjs +161 -0
  104. package/scripts/migrate-legacy.mjs +97 -0
  105. package/scripts/package-smoke.mjs +163 -0
  106. package/scripts/release-dist-tag.mjs +7 -0
  107. package/scripts/runbook-template.mjs +36 -0
  108. package/scripts/service-health.mjs +110 -0
  109. package/scripts/service.mjs +24 -0
  110. package/scripts/setup-lib.mjs +118 -0
  111. package/scripts/systemd.mjs +96 -0
  112. package/scripts/upgrade-lib.mjs +99 -0
  113. package/scripts/verify-release.mjs +37 -0
@@ -0,0 +1,640 @@
1
+ import { effectiveTestRuns, testEvidenceState, } from "./progress.js";
2
+ import { taskFailurePresentation } from "./task-failure.js";
3
+ import { prepareRemoteMarkdown, redactSensitiveText } from "./redaction.js";
4
+ import { taskModeCapturesReview, taskModeExpectsTests, taskModeLabel, taskModeOf, } from "./task-intent.js";
5
+ const CARD_TEXT_LIMIT = 12_000;
6
+ const RESULT_SUMMARY_LIMIT = 1_800;
7
+ const PHASE_STYLE = {
8
+ queued: {
9
+ template: "grey",
10
+ tagColor: "blue",
11
+ background: "grey-50",
12
+ label: "排队中",
13
+ title: "Codex 等待开始",
14
+ },
15
+ running: {
16
+ template: "blue",
17
+ tagColor: "blue",
18
+ background: "blue-50",
19
+ label: "运行中",
20
+ title: "Codex 正在工作",
21
+ },
22
+ succeeded: {
23
+ template: "green",
24
+ tagColor: "green",
25
+ background: "grey-50",
26
+ label: "已完成",
27
+ title: "Codex 已完成",
28
+ },
29
+ failed: {
30
+ template: "red",
31
+ tagColor: "red",
32
+ background: "red-50",
33
+ label: "执行失败",
34
+ title: "Codex 执行失败",
35
+ },
36
+ cancelled: {
37
+ template: "grey",
38
+ tagColor: "neutral",
39
+ background: "grey-50",
40
+ label: "已停止",
41
+ title: "Codex 已停止",
42
+ },
43
+ interrupted: {
44
+ template: "grey",
45
+ tagColor: "neutral",
46
+ background: "grey-50",
47
+ label: "已中断",
48
+ title: "Codex 任务已中断",
49
+ },
50
+ };
51
+ export function renderTaskCard(progress, now = Date.now()) {
52
+ const baseStyle = PHASE_STYLE[progress.phase];
53
+ const mode = taskModeOf(progress);
54
+ const modeStyle = {
55
+ ...baseStyle,
56
+ title: taskPhaseTitle(progress.phase, mode),
57
+ };
58
+ const style = progress.phase === "succeeded" && testEvidenceState(progress.commandRuns) === "failed"
59
+ ? {
60
+ ...modeStyle,
61
+ template: "red",
62
+ tagColor: "red",
63
+ background: "red-50",
64
+ label: "测试未通过",
65
+ title: "Codex 已完成,测试未通过",
66
+ }
67
+ : modeStyle;
68
+ const executionBlock = {
69
+ tag: "column_set",
70
+ element_id: "execution_block",
71
+ flex_mode: "none",
72
+ columns: [
73
+ {
74
+ tag: "column",
75
+ width: "weighted",
76
+ weight: 1,
77
+ background_style: style.background,
78
+ padding: "12px 12px 12px 12px",
79
+ vertical_spacing: "4px",
80
+ elements: [
81
+ {
82
+ tag: "markdown",
83
+ element_id: "stream_text",
84
+ content: renderStreamContent(progress),
85
+ },
86
+ ],
87
+ },
88
+ ],
89
+ };
90
+ const active = progress.phase === "queued" || progress.phase === "running";
91
+ const elements = active
92
+ ? [
93
+ metadataRow(progress, now),
94
+ executionContext(progress),
95
+ ...(progress.initiatorLabel && progress.controllerLabel
96
+ ? [collaborationPanel(progress)]
97
+ : []),
98
+ executionBlock,
99
+ ]
100
+ : [executionBlock, metadataRow(progress, now), executionContext(progress)];
101
+ if (progress.actionNote) {
102
+ elements.push({
103
+ tag: "column_set",
104
+ element_id: "action_note_panel",
105
+ flex_mode: "none",
106
+ columns: [
107
+ {
108
+ tag: "column",
109
+ width: "weighted",
110
+ weight: 1,
111
+ background_style: "blue-50",
112
+ padding: "10px 12px 10px 12px",
113
+ elements: [
114
+ {
115
+ tag: "markdown",
116
+ element_id: "action_note",
117
+ text_size: "notation",
118
+ content: `<font color='blue'>${safeUserText(progress.actionNote)}</font>`,
119
+ },
120
+ ],
121
+ },
122
+ ],
123
+ });
124
+ }
125
+ elements.push(actionButtons(progress));
126
+ elements.push({
127
+ tag: "markdown",
128
+ element_id: "safety_note",
129
+ text_size: "notation",
130
+ text_align: "center",
131
+ content: `<font color='grey'>${followUpHint(progress, active)} · ${safeUserText(progress.permissionLabel)} · 提交、推送、部署和 PR 仍需确认</font>`,
132
+ });
133
+ return {
134
+ schema: "2.0",
135
+ config: {
136
+ update_multi: true,
137
+ width_mode: "default",
138
+ enable_forward: false,
139
+ streaming_mode: true,
140
+ summary: { content: `${style.title} · ${progress.projectLabel}` },
141
+ style: {
142
+ text_size: {
143
+ caption: { default: "notation", pc: "notation", mobile: "notation" },
144
+ },
145
+ color: {
146
+ "bridge-muted": {
147
+ light_mode: "rgba(100,106,115,1)",
148
+ dark_mode: "rgba(150,155,163,1)",
149
+ },
150
+ },
151
+ },
152
+ },
153
+ header: {
154
+ title: { tag: "plain_text", content: style.title },
155
+ subtitle: {
156
+ tag: "plain_text",
157
+ content: truncatePlain(redactSensitiveText(progress.prompt).replace(/\s+/g, " "), 72),
158
+ },
159
+ template: style.template,
160
+ icon: { tag: "standard_icon", token: "myai_colorful" },
161
+ text_tag_list: [
162
+ {
163
+ tag: "text_tag",
164
+ text: { tag: "plain_text", content: style.label },
165
+ color: style.tagColor,
166
+ },
167
+ {
168
+ tag: "text_tag",
169
+ text: { tag: "plain_text", content: taskModeLabel(mode) },
170
+ color: "turquoise",
171
+ },
172
+ {
173
+ tag: "text_tag",
174
+ text: { tag: "plain_text", content: `#${truncatePlain(progress.taskId, 12)}` },
175
+ color: "neutral",
176
+ },
177
+ ],
178
+ },
179
+ body: {
180
+ direction: "vertical",
181
+ padding: "12px 12px 20px 12px",
182
+ vertical_spacing: "12px",
183
+ elements,
184
+ },
185
+ };
186
+ }
187
+ export function renderStreamContent(progress) {
188
+ if (progress.phase === "succeeded") {
189
+ const response = progress.finalResponse || progress.partialResponse;
190
+ return [
191
+ `**${resultSectionTitle(taskModeOf(progress))}**`,
192
+ safeUserText(summarizeResponse(response || "任务已完成,但没有返回文字说明。")),
193
+ renderTestLine(progress),
194
+ renderReviewLine(progress),
195
+ renderChangedFileLine(progress),
196
+ ]
197
+ .filter(Boolean)
198
+ .join("\n\n");
199
+ }
200
+ if (progress.phase === "failed") {
201
+ const error = progress.error || "未知错误";
202
+ const presentation = taskFailurePresentation(error);
203
+ return [
204
+ `**${presentation.title}**`,
205
+ `<font color='grey'>原因</font> ${safeUserText(truncate(error, CARD_TEXT_LIMIT))}`,
206
+ `**下一步** ${safeUserText(presentation.nextAction)}`,
207
+ renderTestLine(progress),
208
+ renderReviewLine(progress),
209
+ renderChangedFileLine(progress),
210
+ ]
211
+ .filter(Boolean)
212
+ .join("\n\n");
213
+ }
214
+ if (progress.phase === "cancelled") {
215
+ return [
216
+ "**任务已停止**",
217
+ safeUserText(truncate(progress.error || "已收到停止请求", CARD_TEXT_LIMIT)),
218
+ progress.changedFiles.length > 0
219
+ ? `已写入磁盘的 ${progress.changedFiles.length} 个文件变更已保留,不会自动撤销。`
220
+ : "停止前没有记录到文件变更。",
221
+ renderChangedFileLine(progress),
222
+ ]
223
+ .filter(Boolean)
224
+ .join("\n\n");
225
+ }
226
+ if (progress.phase === "interrupted") {
227
+ return [
228
+ "**服务中断了这个任务**",
229
+ safeUserText(truncate(progress.error || "桥接服务在任务运行期间停止", CARD_TEXT_LIMIT)),
230
+ "为避免重复修改,任务没有自动重跑。请先查看文件变更,再决定是否重新执行。",
231
+ renderChangedFileLine(progress),
232
+ ]
233
+ .filter(Boolean)
234
+ .join("\n\n");
235
+ }
236
+ const lines = progress.logs.length > 0 ? progress.logs : [progress.activity];
237
+ const timeline = lines.map((line, index) => {
238
+ const isCurrent = index === lines.length - 1;
239
+ const state = isCurrent ? "进行中" : "已完成";
240
+ const color = isCurrent ? "blue" : "grey";
241
+ return `<font color='${color}'>${state}</font> ${safeUserText(line)}`;
242
+ });
243
+ if (progress.partialResponse) {
244
+ timeline.push(`<font color='blue'>回复中</font> ${safeUserText(truncate(progress.partialResponse, 2_400))}`);
245
+ }
246
+ const changed = renderChangedFileLine(progress);
247
+ if (changed)
248
+ timeline.push(changed);
249
+ return [`**${activitySectionTitle(taskModeOf(progress))}**`, ...timeline].join("\n\n");
250
+ }
251
+ export function renderChangedFiles(progress) {
252
+ if (progress.changedFiles.length === 0)
253
+ return "这个任务尚未产生可见的文件变更。";
254
+ const labels = { add: "新增", update: "修改", delete: "删除" };
255
+ return [
256
+ `任务 ${progress.taskId} 的文件变更(${progress.changedFiles.length})`,
257
+ "",
258
+ ...progress.changedFiles.map((change) => `- ${labels[change.kind]}:${safeUserText(change.path)}`),
259
+ ].join("\n");
260
+ }
261
+ function metadataRow(progress, now) {
262
+ const elapsedFrom = progress.startedAt ?? progress.createdAt;
263
+ const elapsedTo = progress.finishedAt ?? now;
264
+ const elapsed = formatDuration(Math.max(0, elapsedTo - elapsedFrom));
265
+ const uncachedInput = progress.usage ? newInputTokens(progress.usage) : 0;
266
+ const value = progress.usage
267
+ ? `${formatNumber(uncachedInput)} / ${formatNumber(progress.usage.cached_input_tokens)}`
268
+ : progress.phase === "queued"
269
+ ? `队列第 ${progress.queuePosition} 位`
270
+ : elapsed;
271
+ const label = progress.usage
272
+ ? "新增 / 缓存"
273
+ : progress.phase === "queued"
274
+ ? "本会话队列"
275
+ : "耗时";
276
+ return {
277
+ tag: "column_set",
278
+ element_id: "task_metadata",
279
+ flex_mode: "none",
280
+ horizontal_spacing: "8px",
281
+ columns: [
282
+ metadataColumn("项目", progress.projectLabel, "left"),
283
+ metadataColumn("权限", progress.permissionLabel, "center"),
284
+ metadataColumn(label, value, "right"),
285
+ ],
286
+ };
287
+ }
288
+ function executionContext(progress) {
289
+ const model = progress.modelLabel || "Codex 默认";
290
+ const reasoning = progress.reasoningLabel || "默认推理";
291
+ const session = progress.threadId
292
+ ? `会话 #${progress.threadId.slice(0, 8)}`
293
+ : progress.sessionLabel || "新会话";
294
+ const steered = progress.steerCount ? ` · 已追加 ${progress.steerCount} 条` : "";
295
+ const source = progress.runbookLabel
296
+ ? `\n<font color='grey'>来源</font> 团队运行手册 · ${safeUserText(progress.runbookLabel)}`
297
+ : "";
298
+ const usage = progress.usage
299
+ ? `\n<font color='grey'>用量</font> 累计输入 ${formatNumber(progress.usage.input_tokens)}(新增 ${formatNumber(newInputTokens(progress.usage))} · 缓存 ${formatNumber(progress.usage.cached_input_tokens)}) · 输出 ${formatNumber(progress.usage.output_tokens)}${progress.usage.model_calls ? ` · ${progress.usage.model_calls} 次模型调用` : ""}`
300
+ : "";
301
+ const task = ` · 任务 #${progress.taskId.slice(0, 8)}`;
302
+ return {
303
+ tag: "markdown",
304
+ element_id: "execution_context",
305
+ text_size: "notation",
306
+ content: `<font color='grey'>模型</font> ${safeUserText(model)} · ${safeUserText(reasoning)}\n<font color='grey'>上下文</font> ${safeUserText(session)}${steered}${task}${source}${usage}`,
307
+ };
308
+ }
309
+ function collaborationPanel(progress) {
310
+ const active = progress.phase === "queued" || progress.phase === "running";
311
+ const options = (progress.handoffOptions ?? []).slice(0, 50);
312
+ const controls = [];
313
+ if (active && progress.teamMode && options.length > 1) {
314
+ controls.push({
315
+ tag: "select_static",
316
+ element_id: "task_handoff_select",
317
+ name: "task_controller",
318
+ width: "fill",
319
+ ...(progress.controllerSelector
320
+ ? { initial_option: progress.controllerSelector }
321
+ : { placeholder: { tag: "plain_text", content: "选择新的任务控制者" } }),
322
+ options: options.map((option) => ({
323
+ text: { tag: "plain_text", content: truncatePlain(option.label, 60) },
324
+ value: option.value,
325
+ })),
326
+ confirm: {
327
+ title: { tag: "plain_text", content: "确认转交任务?" },
328
+ text: {
329
+ tag: "plain_text",
330
+ content: "新的控制者可以停止任务和处理运行时确认;发起人与执行权限不会改变。",
331
+ },
332
+ },
333
+ behaviors: [
334
+ {
335
+ type: "callback",
336
+ value: {
337
+ bridge: "feishu-codex-v3",
338
+ action: "handoff",
339
+ task_id: progress.taskId,
340
+ },
341
+ },
342
+ ],
343
+ });
344
+ controls.push({
345
+ tag: "button",
346
+ element_id: "task_takeover_btn",
347
+ text: { tag: "plain_text", content: "接管任务" },
348
+ type: "default",
349
+ width: "fill",
350
+ confirm: {
351
+ title: { tag: "plain_text", content: "接管这个任务?" },
352
+ text: {
353
+ tag: "plain_text",
354
+ content: "仅任务发起人和团队管理员可以接管,操作会写入审计日志。",
355
+ },
356
+ },
357
+ behaviors: [
358
+ {
359
+ type: "callback",
360
+ value: {
361
+ bridge: "feishu-codex-v3",
362
+ action: "takeover",
363
+ task_id: progress.taskId,
364
+ },
365
+ },
366
+ ],
367
+ });
368
+ }
369
+ return {
370
+ tag: "column_set",
371
+ element_id: "task_collaboration",
372
+ flex_mode: "none",
373
+ columns: [
374
+ {
375
+ tag: "column",
376
+ width: "weighted",
377
+ weight: 1,
378
+ background_style: "grey-50",
379
+ padding: "10px 12px 10px 12px",
380
+ vertical_spacing: "8px",
381
+ elements: [
382
+ {
383
+ tag: "markdown",
384
+ element_id: "task_collab_summary",
385
+ text_size: "notation",
386
+ content: `<font color='grey'>发起人</font> ${safeUserText(progress.initiatorLabel ?? "未知成员")}` +
387
+ `\n<font color='grey'>当前控制者</font> **${safeUserText(progress.controllerLabel ?? "未知成员")}**`,
388
+ },
389
+ ...controls,
390
+ ],
391
+ },
392
+ ],
393
+ };
394
+ }
395
+ function metadataColumn(label, value, align) {
396
+ return {
397
+ tag: "column",
398
+ width: "weighted",
399
+ weight: 1,
400
+ background_style: "grey-50",
401
+ padding: "8px 4px 8px 4px",
402
+ vertical_spacing: "2px",
403
+ elements: [
404
+ {
405
+ tag: "markdown",
406
+ text_align: align,
407
+ text_size: "notation",
408
+ content: `<font color='grey'>${label}</font>`,
409
+ },
410
+ {
411
+ tag: "markdown",
412
+ text_align: align,
413
+ content: `**${safeUserText(truncatePlain(value, 40))}**`,
414
+ },
415
+ ],
416
+ };
417
+ }
418
+ function actionButtons(progress) {
419
+ const active = progress.phase === "queued" || progress.phase === "running";
420
+ const mode = taskModeOf(progress);
421
+ const terminalActions = [];
422
+ if (!active) {
423
+ if (hasLongTaskResult(progress)) {
424
+ terminalActions.push(buttonColumn("result_btn", "完整结果", "primary_filled", progress.taskId, "result"));
425
+ }
426
+ if (hasTaskValidation(progress)) {
427
+ terminalActions.push(buttonColumn("review_btn", "查看验证", terminalActions.length === 0 ? "primary_filled" : "default", progress.taskId, "review"));
428
+ }
429
+ const retry = retryPresentation(mode);
430
+ terminalActions.push(buttonColumn("retry_btn", retry.label, "default", progress.taskId, "retry", {
431
+ title: retry.title,
432
+ text: retry.detail,
433
+ }));
434
+ if (terminalActions.length < 3) {
435
+ terminalActions.push(buttonColumn("new_btn", "新会话", "default", progress.taskId, "new", {
436
+ title: "开启新会话?",
437
+ text: "当前聊天保存的 Codex 上下文会被清除。",
438
+ }));
439
+ }
440
+ }
441
+ return {
442
+ tag: "column_set",
443
+ element_id: "task_actions",
444
+ flex_mode: "none",
445
+ horizontal_spacing: "8px",
446
+ columns: active
447
+ ? progress.phase === "running"
448
+ ? [
449
+ ...(taskModeCapturesReview(mode)
450
+ ? [buttonColumn("review_btn", mode === "write" ? "查看文件" : "实时审阅", "primary_filled", progress.taskId, "review")]
451
+ : []),
452
+ buttonColumn("cancel_btn", "停止", taskModeCapturesReview(mode) ? "default" : "primary_filled", progress.taskId, "cancel", {
453
+ title: `停止这次${taskModeLabel(mode)}?`,
454
+ text: taskModeCapturesReview(mode)
455
+ ? "已完成的文件修改不会自动撤销。"
456
+ : "当前回答会停止,已生成的文字仍会保留。",
457
+ }),
458
+ ]
459
+ : [
460
+ buttonColumn("cancel_btn", "取消排队", "default", progress.taskId, "cancel", {
461
+ title: "取消这个排队任务?",
462
+ text: "任务尚未开始,不会产生新的文件修改。",
463
+ }),
464
+ ]
465
+ : terminalActions,
466
+ };
467
+ }
468
+ function buttonColumn(elementId, label, type, taskId, action, confirm) {
469
+ const button = {
470
+ tag: "button",
471
+ element_id: elementId,
472
+ text: { tag: "plain_text", content: label },
473
+ type,
474
+ width: "fill",
475
+ behaviors: [
476
+ {
477
+ type: "callback",
478
+ value: { bridge: "feishu-codex-v3", action, task_id: taskId },
479
+ },
480
+ ],
481
+ };
482
+ if (confirm) {
483
+ button.confirm = {
484
+ title: { tag: "plain_text", content: confirm.title },
485
+ text: { tag: "plain_text", content: confirm.text },
486
+ };
487
+ }
488
+ return {
489
+ tag: "column",
490
+ width: "weighted",
491
+ weight: 1,
492
+ elements: [button],
493
+ };
494
+ }
495
+ function renderChangedFileLine(progress) {
496
+ if (progress.changedFiles.length === 0)
497
+ return "";
498
+ const visible = progress.changedFiles.slice(-3).map((change) => change.path);
499
+ const extra = progress.changedFiles.length > visible.length
500
+ ? ` 等 ${progress.changedFiles.length} 个文件`
501
+ : "";
502
+ return `<font color='grey'>文件变更</font> ${safeUserText(visible.join("、"))}${extra}`;
503
+ }
504
+ function renderTestLine(progress) {
505
+ const tests = effectiveTestRuns(progress.commandRuns);
506
+ if (tests.length === 0) {
507
+ return taskModeExpectsTests(taskModeOf(progress))
508
+ ? "<font color='grey'>测试</font> 未检测到测试命令"
509
+ : "";
510
+ }
511
+ const failed = tests.filter((run) => run.status === "failed").length;
512
+ const running = tests.filter((run) => run.status === "running").length;
513
+ const passed = tests.filter((run) => run.status === "passed").length;
514
+ const result = failed > 0
515
+ ? `${failed} 次失败 · ${passed} 次通过`
516
+ : running > 0
517
+ ? `${running} 次运行中 · ${passed} 次通过`
518
+ : `${passed} 次通过`;
519
+ return `<font color='grey'>测试</font> ${result}`;
520
+ }
521
+ function renderReviewLine(progress) {
522
+ if (!progress.review)
523
+ return "";
524
+ if (progress.review.availability === "no_changes") {
525
+ return "";
526
+ }
527
+ if (progress.review.availability === "unavailable") {
528
+ return `<font color='grey'>${taskModeOf(progress) === "write" ? "文件审阅" : "代码审阅"}</font> 基线不可用,请回本机确认`;
529
+ }
530
+ const attribution = progress.review.attribution === "task"
531
+ ? "任务基线清晰"
532
+ : progress.review.attribution === "mixed"
533
+ ? "含任务前已有修改"
534
+ : "归因待确认";
535
+ return `<font color='grey'>${taskModeOf(progress) === "write" ? "文件审阅" : "代码审阅"}</font> ${progress.review.totalFiles} 个文件 · +${progress.review.totalAdditions} / -${progress.review.totalDeletions} · ${attribution}`;
536
+ }
537
+ export function hasLongTaskResult(progress) {
538
+ if (progress.phase !== "succeeded")
539
+ return false;
540
+ return (progress.finalResponse || progress.partialResponse).trim().length > RESULT_SUMMARY_LIMIT;
541
+ }
542
+ export function hasTaskValidation(progress) {
543
+ const hasReview = progress.review?.availability === "ready" && progress.review.totalFiles > 0;
544
+ return hasReview || testEvidenceState(progress.commandRuns) !== "missing";
545
+ }
546
+ function summarizeResponse(value) {
547
+ const normalized = truncate(value.trim(), CARD_TEXT_LIMIT);
548
+ if (normalized.length <= RESULT_SUMMARY_LIMIT)
549
+ return normalized;
550
+ const candidate = normalized.slice(0, RESULT_SUMMARY_LIMIT - 90);
551
+ const paragraphBreak = candidate.lastIndexOf("\n\n");
552
+ const lineBreak = candidate.lastIndexOf("\n");
553
+ const cutAt = Math.max(paragraphBreak, lineBreak, RESULT_SUMMARY_LIMIT - 420);
554
+ return `${candidate.slice(0, cutAt).trimEnd()}\n\n<font color='grey'>内容较长,点击“完整结果”查看全部。</font>`;
555
+ }
556
+ function safeUserText(value) {
557
+ return prepareRemoteMarkdown(value)
558
+ .replaceAll("<at", "<at")
559
+ .replaceAll("</at>", "</at>")
560
+ .replaceAll("<person", "<person")
561
+ .replaceAll("</person>", "</person>");
562
+ }
563
+ function truncate(value, max) {
564
+ if (value.length <= max)
565
+ return value;
566
+ return `${value.slice(0, Math.max(0, max - 18))}\n\n[内容已截断]`;
567
+ }
568
+ function truncatePlain(value, max) {
569
+ if (value.length <= max)
570
+ return value;
571
+ return `${value.slice(0, Math.max(0, max - 1))}…`;
572
+ }
573
+ function formatDuration(milliseconds) {
574
+ const seconds = Math.floor(milliseconds / 1_000);
575
+ if (seconds < 60)
576
+ return `${seconds} 秒`;
577
+ const minutes = Math.floor(seconds / 60);
578
+ const remainder = seconds % 60;
579
+ if (minutes < 60)
580
+ return `${minutes}分${remainder}秒`;
581
+ return `${Math.floor(minutes / 60)}时${minutes % 60}分`;
582
+ }
583
+ function formatNumber(value) {
584
+ return new Intl.NumberFormat("zh-CN").format(value);
585
+ }
586
+ function newInputTokens(usage) {
587
+ return Math.max(0, usage.input_tokens - usage.cached_input_tokens);
588
+ }
589
+ function taskPhaseTitle(phase, mode) {
590
+ if (phase === "queued") {
591
+ return { answer: "回答等待开始", analyze: "分析等待开始", write: "内容等待生成", code: "Codex 等待开始" }[mode];
592
+ }
593
+ if (phase === "running") {
594
+ return { answer: "Codex 正在回答", analyze: "Codex 正在分析", write: "Codex 正在生成内容", code: "Codex 正在执行" }[mode];
595
+ }
596
+ if (phase === "succeeded") {
597
+ return { answer: "回答已完成", analyze: "分析已完成", write: "内容已生成", code: "Codex 已完成" }[mode];
598
+ }
599
+ return PHASE_STYLE[phase].title;
600
+ }
601
+ function resultSectionTitle(mode) {
602
+ return { answer: "回答", analyze: "分析结论", write: "交付内容", code: "结果摘要" }[mode];
603
+ }
604
+ function activitySectionTitle(mode) {
605
+ return { answer: "回答进度", analyze: "分析进度", write: "生成进度", code: "执行动态" }[mode];
606
+ }
607
+ function retryPresentation(mode) {
608
+ return {
609
+ answer: {
610
+ label: "再次回答",
611
+ title: "再次回答这个问题?",
612
+ detail: "会在同一项目中重新处理原问题。",
613
+ },
614
+ analyze: {
615
+ label: "重新分析",
616
+ title: "重新分析这个问题?",
617
+ detail: "会重新读取当前项目,但不会主动修改文件。",
618
+ },
619
+ write: {
620
+ label: "再次生成",
621
+ title: "再次生成这些内容?",
622
+ detail: "可能会再次写入或覆盖任务涉及的文件。",
623
+ },
624
+ code: {
625
+ label: "重新执行",
626
+ title: "重新执行这个任务?",
627
+ detail: "可能会重复产生代码修改。",
628
+ },
629
+ }[mode];
630
+ }
631
+ function followUpHint(progress, active) {
632
+ if (!active)
633
+ return "直接回复可继续当前会话";
634
+ const mode = taskModeOf(progress);
635
+ return mode === "answer"
636
+ ? "直接发送补充问题可加入当前回答"
637
+ : mode === "analyze"
638
+ ? "直接发送补充要求可加入当前分析"
639
+ : "直接发送补充要求可追加到当前任务";
640
+ }