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,176 @@
1
+ import { hasTaskValidation } from "./task-card.js";
2
+ import { taskModeLabel, taskModeOf } from "./task-intent.js";
3
+ export function renderTaskCenterCard(snapshot) {
4
+ return {
5
+ schema: "2.0",
6
+ config: {
7
+ update_multi: true,
8
+ width_mode: "default",
9
+ enable_forward: false,
10
+ summary: { content: `Codex 任务中心 · 运行 ${snapshot.running} · 排队 ${snapshot.queued}` },
11
+ },
12
+ header: {
13
+ title: { tag: "plain_text", content: "Codex 任务中心" },
14
+ subtitle: { tag: "plain_text", content: snapshot.scopeLabel },
15
+ template: snapshot.running > 0 ? "blue" : "green",
16
+ icon: { tag: "standard_icon", token: "tasklist_colorful" },
17
+ text_tag_list: [
18
+ { tag: "text_tag", text: { tag: "plain_text", content: `运行 ${snapshot.running}` }, color: "blue" },
19
+ { tag: "text_tag", text: { tag: "plain_text", content: `排队 ${snapshot.queued}` }, color: "neutral" },
20
+ ],
21
+ },
22
+ body: {
23
+ direction: "vertical",
24
+ padding: "12px 12px 18px 12px",
25
+ vertical_spacing: "10px",
26
+ elements: [
27
+ ...(snapshot.tasks.length > 0
28
+ ? snapshot.tasks
29
+ .slice(0, 8)
30
+ .map((task, index) => taskRow(task, index, snapshot.canOperate, snapshot.people?.[task.id]))
31
+ : [
32
+ {
33
+ tag: "markdown",
34
+ element_id: "tasks_empty",
35
+ content: "**暂无任务**\n<font color='grey'>直接发送问题、分析要求、文档需求或代码任务即可开始。</font>",
36
+ },
37
+ ]),
38
+ actionGrid("task_actions", snapshot.canOperate
39
+ ? [
40
+ button("停止本会话", "tasks_stop", "danger", undefined, {
41
+ title: "停止本会话任务?",
42
+ text: "当前会话的运行和排队任务都会停止,已写入文件的修改不会撤销。",
43
+ }),
44
+ button("本会话重置", "tasks_new", "primary"),
45
+ button("控制中心", "tasks_settings", "default"),
46
+ button("刷新", "tasks_refresh", "default"),
47
+ ]
48
+ : [
49
+ button("控制中心", "tasks_settings", "default"),
50
+ button("刷新", "tasks_refresh", "default"),
51
+ ]),
52
+ ],
53
+ },
54
+ };
55
+ }
56
+ function taskRow(task, index, canOperate, people) {
57
+ const final = task.progress.finalResponse || task.progress.error || task.progress.activity;
58
+ const active = task.status === "running" || task.status === "queued";
59
+ return {
60
+ tag: "column_set",
61
+ element_id: `task_row_${index}`,
62
+ flex_mode: "none",
63
+ columns: [
64
+ {
65
+ tag: "column",
66
+ width: "weighted",
67
+ weight: 1,
68
+ background_style: task.status === "running" ? "blue-50" : "grey-50",
69
+ padding: "10px 12px 10px 12px",
70
+ vertical_spacing: "4px",
71
+ elements: [
72
+ {
73
+ tag: "markdown",
74
+ element_id: `task_title_${index}`,
75
+ content: `**${safe(task.prompt).slice(0, 90)}** ${statusTag(task.status)} <text_tag color='turquoise'>${taskModeLabel(taskModeOf(task.progress))}</text_tag>\n<font color='grey'>#${safe(task.id).slice(0, 12)} · ${safe(task.project.name)} · ${relativeTime(task.updatedAt)}</font>${people ? `\n<font color='grey'>发起 ${safe(people.initiator)} · 控制 ${safe(people.controller)}</font>` : ""}\n${safe(final).slice(0, 160)}`,
76
+ },
77
+ ],
78
+ },
79
+ ...(active && canOperate
80
+ ? [
81
+ {
82
+ tag: "column",
83
+ width: "auto",
84
+ vertical_align: "center",
85
+ elements: [
86
+ button("停止", "tasks_cancel_one", "danger", task.id, {
87
+ title: "停止这个任务?",
88
+ text: "已写入文件的修改不会自动撤销。",
89
+ }, `task_cancel_${index}`),
90
+ ],
91
+ },
92
+ ]
93
+ : !active && hasTaskValidation(task.progress)
94
+ ? [
95
+ {
96
+ tag: "column",
97
+ width: "auto",
98
+ vertical_align: "center",
99
+ elements: [
100
+ button("审阅", "tasks_review", "primary", task.id, undefined, `task_review_${index}`),
101
+ ],
102
+ },
103
+ ]
104
+ : []),
105
+ ],
106
+ };
107
+ }
108
+ function button(text, action, type, taskId, confirm, elementId = action.slice(0, 20)) {
109
+ const result = {
110
+ tag: "button",
111
+ element_id: elementId,
112
+ text: { tag: "plain_text", content: text },
113
+ type: type === "danger" ? "danger_filled" : type === "primary" ? "primary_filled" : "default",
114
+ width: "fill",
115
+ behaviors: [
116
+ {
117
+ type: "callback",
118
+ value: {
119
+ bridge: "feishu-codex-v5",
120
+ action,
121
+ ...(taskId ? { task_id: taskId } : {}),
122
+ },
123
+ },
124
+ ],
125
+ };
126
+ if (confirm) {
127
+ result.confirm = {
128
+ title: { tag: "plain_text", content: confirm.title },
129
+ text: { tag: "plain_text", content: confirm.text },
130
+ };
131
+ }
132
+ return result;
133
+ }
134
+ function actionGrid(id, actions) {
135
+ return {
136
+ tag: "column_set",
137
+ element_id: id,
138
+ flex_mode: "none",
139
+ horizontal_spacing: "8px",
140
+ columns: actions.map((action) => ({
141
+ tag: "column",
142
+ width: "weighted",
143
+ weight: 1,
144
+ elements: [action],
145
+ })),
146
+ };
147
+ }
148
+ function statusTag(status) {
149
+ if (status === "running")
150
+ return "<text_tag color='blue'>运行中</text_tag>";
151
+ if (status === "queued")
152
+ return "<text_tag color='neutral'>排队中</text_tag>";
153
+ if (status === "succeeded")
154
+ return "<text_tag color='green'>已完成</text_tag>";
155
+ if (status === "failed")
156
+ return "<text_tag color='red'>失败</text_tag>";
157
+ if (status === "interrupted")
158
+ return "<text_tag color='neutral'>已中断</text_tag>";
159
+ return "<text_tag color='neutral'>已停止</text_tag>";
160
+ }
161
+ function relativeTime(value) {
162
+ const timestamp = Date.parse(value);
163
+ if (!Number.isFinite(timestamp))
164
+ return "未知时间";
165
+ const seconds = Math.max(0, Math.floor((Date.now() - timestamp) / 1_000));
166
+ if (seconds < 60)
167
+ return "刚刚";
168
+ if (seconds < 3_600)
169
+ return `${Math.floor(seconds / 60)} 分钟前`;
170
+ if (seconds < 86_400)
171
+ return `${Math.floor(seconds / 3_600)} 小时前`;
172
+ return `${Math.floor(seconds / 86_400)} 天前`;
173
+ }
174
+ function safe(value) {
175
+ return value.replaceAll("<", "<").replaceAll(">", ">").replaceAll("*", "*").replaceAll("`", "`");
176
+ }
@@ -0,0 +1,43 @@
1
+ export function taskFailurePresentation(message) {
2
+ const normalized = message.toLocaleLowerCase();
3
+ if (/timeout|timed out|超时/.test(normalized)) {
4
+ return {
5
+ kind: "timeout",
6
+ title: "任务超过运行时限",
7
+ nextAction: "先查看已经写入的文件变更,再缩小任务范围或调整 CODEX_TIMEOUT_MS 后重试。",
8
+ };
9
+ }
10
+ if (/安全闸门|policy|not allowed|blocked command|禁止/.test(normalized)) {
11
+ return {
12
+ kind: "policy",
13
+ title: "安全策略已停止任务",
14
+ nextAction: "修改任务要求;如果确实需要提交、推送、部署或 PR,请通过确认卡单独授权。",
15
+ };
16
+ }
17
+ if (/permission denied|eacces|operation not permitted|权限不足|无权/.test(normalized)) {
18
+ return {
19
+ kind: "permission",
20
+ title: "当前权限不足",
21
+ nextAction: "检查成员角色、项目 ACL 和本轮任务权限;不要直接扩大到完全访问。",
22
+ };
23
+ }
24
+ if (/app-server|connection|socket|econn|broken pipe|连接|断开|closed/.test(normalized)) {
25
+ return {
26
+ kind: "connection",
27
+ title: "Codex 连接中断",
28
+ nextAction: "先在设备控制台重连 Codex,确认在线后再重试;已写入的文件不会自动回滚。",
29
+ };
30
+ }
31
+ if (/queue|队列/.test(normalized)) {
32
+ return {
33
+ kind: "queue",
34
+ title: "任务未能进入队列",
35
+ nextAction: "等待当前任务结束,或停止不再需要的排队任务后重试。",
36
+ };
37
+ }
38
+ return {
39
+ kind: "unknown",
40
+ title: "任务执行失败",
41
+ nextAction: "先查看文件变更和错误原因;可以重试,仍失败时运行 doctor 并查看本地日志。",
42
+ };
43
+ }
@@ -0,0 +1,76 @@
1
+ const QUESTION_OPENING = /^(?:请问|问[::]?|为什么|为何|什么是|是什么|怎么|如何|能否|能不能|是否|有没有|解释(?:一下)?|说明(?:一下)?|告诉我|帮我解释)/i;
2
+ const FOLLOWED_BY_MUTATION = /(?:[,,::;;。]|并|然后|同时|顺便|接着).{0,16}(?:修复|修改|实现|开发|新增|添加|删除|重构|升级|接入|迁移|替换|改成|写入|优化|调整)/i;
3
+ const DOCUMENT_TARGET = /(?:文档|说明(?:书)?|报告|方案|总结|纪要|教程|指南|README|CHANGELOG|markdown|\.md\b)/i;
4
+ const DOCUMENT_ACTION = /(?:写|撰写|起草|生成|创建|整理|补充|更新|修改|完善|优化|润色|改写|翻译|输出)/i;
5
+ const GENERIC_WRITING = /(?:写(?:入|一个|一份)?(?:文件|内容)|保存为文件|落到文件)/i;
6
+ const CODE_MUTATION = /(?:修复|修改|改一下|改成|实现|开发|新增|添加|删除|移除|重构|升级|接入|迁移|替换|调整|优化|配置|部署|发布|编写.{0,12}(?:代码|脚本|组件|接口|测试)|补(?:齐|充)?.{0,8}(?:代码|测试)|运行.{0,8}(?:测试|lint|build|构建))/i;
7
+ const ANALYSIS_OPENING = /^(?:读取|阅读|查看|看看|看一下|分析|检查|审查|评审|诊断|定位|梳理|总结|评估|调研|研究|搜索|查找|找出|列出|统计|了解|解释这个项目)/i;
8
+ const ANALYSIS_PHRASE = /(?:原因是什么|有哪些问题|有没有问题|项目结构|代码结构|现状|差异|风险|可行性|给出建议)/i;
9
+ /**
10
+ * Local presentation/safety routing only. Codex still receives the original
11
+ * prompt unchanged, so a heuristic miss cannot alter the requested content.
12
+ */
13
+ export function inferTaskMode(prompt) {
14
+ const normalized = prompt.trim().replace(/\s+/g, " ");
15
+ if (!normalized)
16
+ return "code";
17
+ if (QUESTION_OPENING.test(normalized) && !FOLLOWED_BY_MUTATION.test(normalized)) {
18
+ return "answer";
19
+ }
20
+ if ((DOCUMENT_TARGET.test(normalized) && DOCUMENT_ACTION.test(normalized)) ||
21
+ GENERIC_WRITING.test(normalized)) {
22
+ return "write";
23
+ }
24
+ // “看看/分析有没有可以优化的地方” is a request for findings, not
25
+ // authorization to modify the workspace. Only an explicit follow-up
26
+ // mutation (“然后修复”) promotes an analysis opening into a write task.
27
+ if (ANALYSIS_OPENING.test(normalized) && !FOLLOWED_BY_MUTATION.test(normalized)) {
28
+ return "analyze";
29
+ }
30
+ if (CODE_MUTATION.test(normalized))
31
+ return "code";
32
+ if (ANALYSIS_PHRASE.test(normalized))
33
+ return "analyze";
34
+ if (/[??]\s*$/.test(normalized))
35
+ return "answer";
36
+ return "code";
37
+ }
38
+ export function taskModeOf(value) {
39
+ return value.taskMode ?? inferTaskMode(value.prompt);
40
+ }
41
+ export function taskModeLabel(mode) {
42
+ return {
43
+ answer: "问答",
44
+ analyze: "分析",
45
+ write: "内容",
46
+ code: "代码",
47
+ }[mode];
48
+ }
49
+ export function taskModeAllowsWrites(mode) {
50
+ return mode === "write" || mode === "code";
51
+ }
52
+ export function taskModeCapturesReview(mode) {
53
+ return taskModeAllowsWrites(mode);
54
+ }
55
+ export function taskModeExpectsTests(mode) {
56
+ return mode === "code";
57
+ }
58
+ export function sandboxForTaskMode(mode, requested) {
59
+ return taskModeAllowsWrites(mode) ? requested : "read-only";
60
+ }
61
+ export function runningActivity(mode) {
62
+ return {
63
+ answer: "Codex 正在回答",
64
+ analyze: "Codex 正在分析",
65
+ write: "Codex 正在生成内容",
66
+ code: "Codex 正在执行任务",
67
+ }[mode];
68
+ }
69
+ export function completedActivity(mode) {
70
+ return {
71
+ answer: "回答完成",
72
+ analyze: "分析完成",
73
+ write: "内容已生成",
74
+ code: "任务完成",
75
+ }[mode];
76
+ }
@@ -0,0 +1,187 @@
1
+ export class QueueCapacityError extends Error {
2
+ conversationKey;
3
+ constructor(conversationKey) {
4
+ super("This conversation already has the maximum number of queued tasks");
5
+ this.conversationKey = conversationKey;
6
+ this.name = "QueueCapacityError";
7
+ }
8
+ }
9
+ export class TaskQueue {
10
+ maxConcurrency;
11
+ maxQueuedPerConversation;
12
+ pending = [];
13
+ activeJobs = new Map();
14
+ activeConversations = new Set();
15
+ activeResources = new Set();
16
+ idleWaiters = new Set();
17
+ paused = false;
18
+ constructor(maxConcurrency = 2, maxQueuedPerConversation = 5) {
19
+ this.maxConcurrency = maxConcurrency;
20
+ this.maxQueuedPerConversation = maxQueuedPerConversation;
21
+ if (!Number.isSafeInteger(maxConcurrency) || maxConcurrency <= 0) {
22
+ throw new Error("maxConcurrency must be a positive integer");
23
+ }
24
+ if (!Number.isSafeInteger(maxQueuedPerConversation) || maxQueuedPerConversation <= 0) {
25
+ throw new Error("maxQueuedPerConversation must be a positive integer");
26
+ }
27
+ }
28
+ enqueue(job) {
29
+ if (this.paused)
30
+ throw new Error("Task queue is paused");
31
+ if (this.queuedForConversation(job.conversationKey) >= this.maxQueuedPerConversation) {
32
+ throw new QueueCapacityError(job.conversationKey);
33
+ }
34
+ this.pending.push(job);
35
+ const position = this.positionForTask(job.id, job.conversationKey);
36
+ this.notifyPositions();
37
+ void this.drain();
38
+ return position;
39
+ }
40
+ nextPosition(conversationKey) {
41
+ return (this.queuedForConversation(conversationKey) +
42
+ (this.activeConversations.has(conversationKey) ? 1 : 0) +
43
+ 1);
44
+ }
45
+ canEnqueue(conversationKey) {
46
+ return this.queuedForConversation(conversationKey) < this.maxQueuedPerConversation;
47
+ }
48
+ cancelPending(conversationKey) {
49
+ let removed = 0;
50
+ for (let index = this.pending.length - 1; index >= 0; index -= 1) {
51
+ if (this.pending[index]?.conversationKey === conversationKey) {
52
+ const [job] = this.pending.splice(index, 1);
53
+ if (job)
54
+ this.notifyCancelled(job);
55
+ removed += 1;
56
+ }
57
+ }
58
+ if (removed > 0)
59
+ this.notifyPositions();
60
+ return removed;
61
+ }
62
+ cancelAllPending() {
63
+ const count = this.pending.length;
64
+ const jobs = this.pending.splice(0);
65
+ for (const job of jobs)
66
+ this.notifyCancelled(job);
67
+ if (count > 0)
68
+ this.notifyPositions();
69
+ return count;
70
+ }
71
+ cancelTask(taskId) {
72
+ const index = this.pending.findIndex((job) => job.id === taskId);
73
+ if (index < 0)
74
+ return false;
75
+ const [job] = this.pending.splice(index, 1);
76
+ if (job)
77
+ this.notifyCancelled(job);
78
+ this.notifyPositions();
79
+ return true;
80
+ }
81
+ get pendingCount() {
82
+ return this.pending.length;
83
+ }
84
+ get active() {
85
+ return this.activeJobs.values().next().value ?? null;
86
+ }
87
+ get activeCount() {
88
+ return this.activeJobs.size;
89
+ }
90
+ getActiveTask(conversationKey) {
91
+ for (const job of this.activeJobs.values()) {
92
+ if (job.conversationKey === conversationKey)
93
+ return job.id;
94
+ }
95
+ return undefined;
96
+ }
97
+ pause() {
98
+ this.paused = true;
99
+ }
100
+ waitForIdle() {
101
+ if (this.activeJobs.size === 0)
102
+ return Promise.resolve();
103
+ return new Promise((resolve) => this.idleWaiters.add(resolve));
104
+ }
105
+ queuedForConversation(conversationKey) {
106
+ return this.pending.filter((job) => job.conversationKey === conversationKey).length;
107
+ }
108
+ async drain() {
109
+ if (this.paused)
110
+ return;
111
+ while (this.activeJobs.size < this.maxConcurrency) {
112
+ const index = this.pending.findIndex((job) => !this.activeConversations.has(job.conversationKey) &&
113
+ !this.activeResources.has(this.resourceKey(job)));
114
+ if (index < 0)
115
+ return;
116
+ const [next] = this.pending.splice(index, 1);
117
+ if (!next)
118
+ return;
119
+ this.activeJobs.set(next.id, next);
120
+ this.activeConversations.add(next.conversationKey);
121
+ this.activeResources.add(this.resourceKey(next));
122
+ this.notifyPositions();
123
+ void this.runJob(next);
124
+ }
125
+ }
126
+ async runJob(job) {
127
+ try {
128
+ await job.run();
129
+ }
130
+ catch (error) {
131
+ console.error(`[queue] unhandled task error id=${job.id}`, error);
132
+ }
133
+ finally {
134
+ this.activeJobs.delete(job.id);
135
+ this.activeConversations.delete(job.conversationKey);
136
+ this.activeResources.delete(this.resourceKey(job));
137
+ this.notifyPositions();
138
+ if (this.activeJobs.size === 0) {
139
+ for (const resolve of this.idleWaiters)
140
+ resolve();
141
+ this.idleWaiters.clear();
142
+ }
143
+ void this.drain();
144
+ }
145
+ }
146
+ resourceKey(job) {
147
+ return job.resourceKey ?? `conversation:${job.conversationKey}`;
148
+ }
149
+ positionForTask(taskId, conversationKey) {
150
+ let position = this.activeConversations.has(conversationKey) ? 1 : 0;
151
+ for (const job of this.pending) {
152
+ if (job.conversationKey !== conversationKey)
153
+ continue;
154
+ position += 1;
155
+ if (job.id === taskId)
156
+ return position;
157
+ }
158
+ return Math.max(1, position);
159
+ }
160
+ notifyCancelled(job) {
161
+ try {
162
+ void Promise.resolve(job.onCancel?.()).catch((error) => {
163
+ console.error(`[queue] cancellation hook failed id=${job.id}`, error);
164
+ });
165
+ }
166
+ catch (error) {
167
+ console.error(`[queue] cancellation hook failed id=${job.id}`, error);
168
+ }
169
+ }
170
+ notifyPositions() {
171
+ const positions = new Map();
172
+ for (const conversationKey of this.activeConversations)
173
+ positions.set(conversationKey, 1);
174
+ for (const job of this.pending) {
175
+ const position = (positions.get(job.conversationKey) ?? 0) + 1;
176
+ positions.set(job.conversationKey, position);
177
+ try {
178
+ void Promise.resolve(job.onPositionChange?.(position)).catch((error) => {
179
+ console.error(`[queue] position hook failed id=${job.id}`, error);
180
+ });
181
+ }
182
+ catch (error) {
183
+ console.error(`[queue] position hook failed id=${job.id}`, error);
184
+ }
185
+ }
186
+ }
187
+ }
@@ -0,0 +1,80 @@
1
+ const TERMINAL = new Set([
2
+ "succeeded",
3
+ "failed",
4
+ "cancelled",
5
+ "interrupted",
6
+ ]);
7
+ /**
8
+ * Reconciles the independently persisted task status and CardKit progress state.
9
+ * The result deliberately prefers "do not replay" over convenience: any evidence
10
+ * that a task started or reached a terminal state prevents it from being treated
11
+ * as a fresh queued task after a crash.
12
+ */
13
+ export function reconcilePersistedTask(task) {
14
+ const recordStatus = task.status;
15
+ const cardStatus = task.progress.phase;
16
+ const canonical = canonicalStatus(task);
17
+ const changed = recordStatus !== canonical || cardStatus !== canonical;
18
+ if (!changed)
19
+ return { task, changed: false };
20
+ const reason = `状态对账:任务记录 ${recordStatus}、卡片 ${cardStatus},统一为 ${canonical}`;
21
+ return {
22
+ changed: true,
23
+ reason,
24
+ task: {
25
+ ...task,
26
+ status: canonical,
27
+ progress: reconcileProgress(task.progress, canonical, reason, task.updatedAt),
28
+ updatedAt: new Date().toISOString(),
29
+ },
30
+ };
31
+ }
32
+ function canonicalStatus(task) {
33
+ if (TERMINAL.has(task.status))
34
+ return task.status;
35
+ if (TERMINAL.has(task.progress.phase))
36
+ return task.progress.phase;
37
+ if (task.status === "running" ||
38
+ task.progress.phase === "running" ||
39
+ (task.progress.startedAt !== null && task.progress.finishedAt === null) ||
40
+ Boolean(task.progress.threadId)) {
41
+ return "running";
42
+ }
43
+ return "queued";
44
+ }
45
+ function reconcileProgress(progress, status, reason, updatedAt) {
46
+ const terminal = TERMINAL.has(status);
47
+ const finishedAt = terminal
48
+ ? progress.finishedAt ?? validTime(updatedAt) ?? Date.now()
49
+ : null;
50
+ return {
51
+ ...progress,
52
+ phase: status,
53
+ activity: activityFor(status),
54
+ startedAt: status === "running" && progress.startedAt === null
55
+ ? validTime(updatedAt) ?? progress.createdAt
56
+ : progress.startedAt,
57
+ finishedAt,
58
+ actionNote: [progress.actionNote, reason].filter(Boolean).join(" · ").slice(0, 1_000),
59
+ };
60
+ }
61
+ function activityFor(status) {
62
+ switch (status) {
63
+ case "queued":
64
+ return "等待恢复队列";
65
+ case "running":
66
+ return "检测到已启动任务,等待安全中断处理";
67
+ case "succeeded":
68
+ return "任务已完成";
69
+ case "failed":
70
+ return "任务执行失败";
71
+ case "cancelled":
72
+ return "任务已取消";
73
+ case "interrupted":
74
+ return "任务被服务中断";
75
+ }
76
+ }
77
+ function validTime(value) {
78
+ const parsed = Date.parse(value);
79
+ return Number.isFinite(parsed) ? parsed : undefined;
80
+ }