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,460 @@
1
+ import { renderAccountQuotaSummary } from "./account-quota-card.js";
2
+ export function renderDeviceCard(snapshot) {
3
+ const presentation = statePresentation(snapshot.availability.state);
4
+ const elements = [
5
+ availabilityPanel(snapshot),
6
+ remoteReadyPanel(snapshot),
7
+ healthMetrics(snapshot),
8
+ ...(snapshot.accountQuota
9
+ ? [
10
+ renderAccountQuotaSummary(snapshot.accountQuota, "dq", snapshot.availability.canInteract
11
+ ? {
12
+ bridge: "feishu-codex-v4",
13
+ action: "device_quota",
14
+ }
15
+ : undefined),
16
+ ]
17
+ : []),
18
+ currentProjectPanel(snapshot),
19
+ ];
20
+ if (snapshot.feedback) {
21
+ elements.push({
22
+ tag: "column_set",
23
+ element_id: "feedback_panel",
24
+ flex_mode: "none",
25
+ columns: [
26
+ {
27
+ tag: "column",
28
+ width: "weighted",
29
+ weight: 1,
30
+ background_style: "blue-50",
31
+ padding: "10px 12px 10px 12px",
32
+ elements: [
33
+ {
34
+ tag: "markdown",
35
+ element_id: "device_feedback",
36
+ text_size: "notation",
37
+ content: `<font color='blue'>${safe(snapshot.feedback)}</font>`,
38
+ },
39
+ ],
40
+ },
41
+ ],
42
+ });
43
+ }
44
+ elements.push(safetyNote(snapshot));
45
+ if (!snapshot.availability.canInteract) {
46
+ elements.push(connectionNotice(snapshot));
47
+ }
48
+ else if (snapshot.availability.canExecute) {
49
+ elements.push(primaryActions(), workspaceActions());
50
+ }
51
+ else {
52
+ elements.push(recoveryActions(snapshot));
53
+ }
54
+ if (snapshot.availability.canInteract) {
55
+ const remote = remoteActions(snapshot);
56
+ if (remote)
57
+ elements.push(remote);
58
+ }
59
+ return {
60
+ schema: "2.0",
61
+ config: {
62
+ update_multi: true,
63
+ width_mode: "default",
64
+ enable_forward: false,
65
+ summary: {
66
+ content: `本地 Codex 控制台 · ${truncatePlain(snapshot.deviceName, 40)} · ${presentation.label}`,
67
+ },
68
+ style: {
69
+ text_size: {
70
+ device_title: { default: "heading-4", pc: "heading-4", mobile: "normal" },
71
+ device_caption: { default: "notation", pc: "notation", mobile: "notation" },
72
+ },
73
+ },
74
+ },
75
+ header: {
76
+ title: { tag: "plain_text", content: "本地 Codex 控制台" },
77
+ subtitle: {
78
+ tag: "plain_text",
79
+ content: `${truncatePlain(snapshot.deviceName, 32)} · ${truncatePlain(snapshot.project.name, 36)}`,
80
+ },
81
+ template: presentation.template,
82
+ icon: { tag: "standard_icon", token: "myai_colorful" },
83
+ text_tag_list: [
84
+ {
85
+ tag: "text_tag",
86
+ text: { tag: "plain_text", content: presentation.tag },
87
+ color: presentation.color,
88
+ },
89
+ {
90
+ tag: "text_tag",
91
+ text: {
92
+ tag: "plain_text",
93
+ content: !snapshot.remoteReady.supported
94
+ ? "唤醒不支持"
95
+ : snapshot.remoteReady.active
96
+ ? "持续唤醒"
97
+ : "按需唤醒",
98
+ },
99
+ color: snapshot.remoteReady.active ? "blue" : "neutral",
100
+ },
101
+ ],
102
+ },
103
+ body: {
104
+ direction: "vertical",
105
+ padding: "12px 12px 20px 12px",
106
+ vertical_spacing: "12px",
107
+ elements,
108
+ },
109
+ };
110
+ }
111
+ function availabilityPanel(snapshot) {
112
+ const presentation = statePresentation(snapshot.availability.state);
113
+ const lastSuccess = snapshot.availability.lastSuccessfulTaskAt
114
+ ? formatTime(snapshot.availability.lastSuccessfulTaskAt)
115
+ : "暂无成功任务";
116
+ return {
117
+ tag: "column_set",
118
+ element_id: "device_state_panel",
119
+ flex_mode: "none",
120
+ columns: [
121
+ {
122
+ tag: "column",
123
+ width: "weighted",
124
+ weight: 1,
125
+ background_style: presentation.background,
126
+ padding: "12px 12px 12px 12px",
127
+ vertical_spacing: "4px",
128
+ elements: [
129
+ {
130
+ tag: "markdown",
131
+ element_id: "device_state_title",
132
+ text_size: "device_title",
133
+ content: `**<font color='${presentation.textColor}'>${safe(snapshot.availability.title)}</font>**`,
134
+ },
135
+ {
136
+ tag: "markdown",
137
+ element_id: "device_state_detail",
138
+ text_size: "device_caption",
139
+ content: safe(snapshot.availability.detail),
140
+ },
141
+ {
142
+ tag: "markdown",
143
+ element_id: "device_state_next",
144
+ text_size: "device_caption",
145
+ content: `<font color='grey'>${safe(snapshot.availability.nextAction)}\n状态采样 ${safe(formatTime(snapshot.availability.sampledAt))} · 本人最后成功 ${safe(lastSuccess)}</font>`,
146
+ },
147
+ ],
148
+ },
149
+ ],
150
+ };
151
+ }
152
+ function remoteReadyPanel(snapshot) {
153
+ const status = !snapshot.remoteReady.supported
154
+ ? "当前系统不支持远程就绪"
155
+ : snapshot.remoteReady.active
156
+ ? "远程就绪已开启"
157
+ : snapshot.remoteReady.enabled
158
+ ? "远程就绪启动失败"
159
+ : "远程就绪未开启";
160
+ const color = snapshot.remoteReady.active
161
+ ? "green"
162
+ : snapshot.remoteReady.enabled
163
+ ? "red"
164
+ : "grey";
165
+ const background = snapshot.remoteReady.active
166
+ ? "blue-50"
167
+ : snapshot.remoteReady.enabled
168
+ ? "red-50"
169
+ : "grey-50";
170
+ const detail = !snapshot.remoteReady.supported
171
+ ? "Remote Ready 目前仅支持 macOS;仍需自行保证电源、网络和系统唤醒"
172
+ : snapshot.remoteReady.lastError
173
+ ? safe(truncate(snapshot.remoteReady.lastError, 240))
174
+ : snapshot.remoteReady.active
175
+ ? "桥接服务运行期间,macOS 不会因空闲自动睡眠"
176
+ : "开启后可降低人在外面时本机因空闲而离线的概率";
177
+ return {
178
+ tag: "column_set",
179
+ element_id: "remote_ready_panel",
180
+ flex_mode: "none",
181
+ columns: [
182
+ {
183
+ tag: "column",
184
+ width: "weighted",
185
+ weight: 1,
186
+ background_style: background,
187
+ padding: "12px 12px 12px 12px",
188
+ vertical_spacing: "4px",
189
+ elements: [
190
+ {
191
+ tag: "markdown",
192
+ element_id: "remote_ready_status",
193
+ text_size: "device_title",
194
+ content: `**<font color='${color}'>${status}</font>**`,
195
+ },
196
+ {
197
+ tag: "markdown",
198
+ element_id: "remote_ready_detail",
199
+ text_size: "device_caption",
200
+ content: `<font color='grey'>${safe(snapshot.powerLabel)} · ${detail}</font>`,
201
+ },
202
+ ],
203
+ },
204
+ ],
205
+ };
206
+ }
207
+ function healthMetrics(snapshot) {
208
+ const codexLabel = snapshot.codex.state === "online"
209
+ ? "在线"
210
+ : snapshot.codex.state === "error"
211
+ ? "异常"
212
+ : "待命";
213
+ const taskLabel = snapshot.activeTask
214
+ ? `运行 1 · 等待 ${snapshot.queuedForConversation}`
215
+ : snapshot.queuedForConversation > 0
216
+ ? `等待 ${snapshot.queuedForConversation}`
217
+ : "空闲";
218
+ const listenerLabel = statePresentation(snapshot.availability.state).metric;
219
+ return {
220
+ tag: "column_set",
221
+ element_id: "health_metrics",
222
+ flex_mode: "none",
223
+ horizontal_spacing: "8px",
224
+ columns: [
225
+ metricColumn("codex_metric", codexLabel, "Codex 引擎"),
226
+ metricColumn("task_metric", taskLabel, "当前聊天"),
227
+ metricColumn("listen_metric", listenerLabel, "飞书连接"),
228
+ ],
229
+ };
230
+ }
231
+ function metricColumn(elementId, value, label) {
232
+ return {
233
+ tag: "column",
234
+ width: "weighted",
235
+ weight: 1,
236
+ background_style: "grey-50",
237
+ padding: "10px 4px 10px 4px",
238
+ vertical_spacing: "2px",
239
+ elements: [
240
+ {
241
+ tag: "markdown",
242
+ element_id: `${elementId}_value`,
243
+ text_align: "center",
244
+ content: `**${safe(truncatePlain(value, 24))}**`,
245
+ },
246
+ {
247
+ tag: "markdown",
248
+ element_id: `${elementId}_label`,
249
+ text_align: "center",
250
+ text_size: "device_caption",
251
+ content: `<font color='grey'>${label}</font>`,
252
+ },
253
+ ],
254
+ };
255
+ }
256
+ function currentProjectPanel(snapshot) {
257
+ const taskLine = snapshot.activeTask
258
+ ? `任务 ${safe(snapshot.activeTask)} 正在执行`
259
+ : `全局 ${snapshot.activeTasks} 个运行中 · ${snapshot.queuedTasks} 个排队中`;
260
+ return {
261
+ tag: "column_set",
262
+ element_id: "device_project_panel",
263
+ flex_mode: "none",
264
+ columns: [
265
+ {
266
+ tag: "column",
267
+ width: "weighted",
268
+ weight: 1,
269
+ background_style: "grey-50",
270
+ padding: "12px 12px 12px 12px",
271
+ vertical_spacing: "4px",
272
+ elements: [
273
+ {
274
+ tag: "markdown",
275
+ element_id: "device_project_label",
276
+ text_size: "device_caption",
277
+ content: "<font color='grey'>当前项目</font>",
278
+ },
279
+ {
280
+ tag: "markdown",
281
+ element_id: "device_project_name",
282
+ content: `**${safe(snapshot.project.name)}** ${snapshot.project.isGitRepository ? "<text_tag color='blue'>Git</text_tag>" : "<text_tag color='neutral'>文件夹</text_tag>"}`,
283
+ },
284
+ {
285
+ tag: "markdown",
286
+ element_id: "device_project_path",
287
+ text_size: "device_caption",
288
+ content: `<font color='grey'>${safe(snapshot.project.displayPath)}\n${taskLine} · 已保存 ${snapshot.threadCount} 个会话\n${safe(snapshot.osLabel)} · 已运行 ${safe(snapshot.uptimeLabel)}</font>`,
289
+ },
290
+ ],
291
+ },
292
+ ],
293
+ };
294
+ }
295
+ function safetyNote(snapshot) {
296
+ const network = snapshot.networkEnabled ? "网络已开启" : "网络已关闭";
297
+ return {
298
+ tag: "markdown",
299
+ element_id: "device_safety_note",
300
+ text_size: "device_caption",
301
+ content: `<font color='grey'>${safe(snapshot.sandboxLabel)} · ${network} · 提交、推送、部署和 PR 仍需单独确认。合盖后 macOS 仍可能休眠。</font>`,
302
+ };
303
+ }
304
+ function primaryActions() {
305
+ return {
306
+ tag: "column_set",
307
+ element_id: "primary_actions",
308
+ flex_mode: "none",
309
+ horizontal_spacing: "8px",
310
+ columns: [
311
+ actionColumn("device_refresh", "刷新状态", "primary_filled", "device_refresh"),
312
+ actionColumn("device_projects", "切换项目", "default", "device_projects"),
313
+ actionColumn("device_new_session", "新会话", "default", "device_new_session", {
314
+ title: "开启新会话?",
315
+ text: "会清除当前聊天保存的 Codex 上下文,并停止未完成任务。",
316
+ }),
317
+ ],
318
+ };
319
+ }
320
+ function workspaceActions() {
321
+ return {
322
+ tag: "column_set",
323
+ element_id: "workspace_actions",
324
+ flex_mode: "none",
325
+ horizontal_spacing: "8px",
326
+ columns: [
327
+ actionColumn("device_settings", "模型设置", "default", "device_settings"),
328
+ actionColumn("device_sessions", "历史会话", "default", "device_sessions"),
329
+ actionColumn("device_tasks", "任务中心", "default", "device_tasks"),
330
+ ],
331
+ };
332
+ }
333
+ function recoveryActions(snapshot) {
334
+ const columns = [
335
+ actionColumn("device_refresh", "重新检查", "primary_filled", "device_refresh"),
336
+ ];
337
+ if (snapshot.availability.state === "error") {
338
+ columns.push(actionColumn("device_reconnect", "重连 Codex", "default", "device_reconnect"));
339
+ }
340
+ else {
341
+ columns.push(actionColumn("device_tasks", "查看任务", "default", "device_tasks"));
342
+ }
343
+ return {
344
+ tag: "column_set",
345
+ element_id: "recovery_actions",
346
+ flex_mode: "none",
347
+ horizontal_spacing: "8px",
348
+ columns,
349
+ };
350
+ }
351
+ function connectionNotice(snapshot) {
352
+ return {
353
+ tag: "markdown",
354
+ element_id: "connection_notice",
355
+ text_size: "device_caption",
356
+ content: `<font color='grey'>控制按钮已暂时隐藏,避免把无法送达的操作显示成可执行。${safe(snapshot.availability.nextAction)}</font>`,
357
+ };
358
+ }
359
+ function remoteActions(snapshot) {
360
+ const remoteAction = snapshot.remoteReady.active
361
+ ? "remote_ready_disable"
362
+ : "remote_ready_enable";
363
+ const remoteLabel = snapshot.remoteReady.active ? "关闭远程就绪" : "开启远程就绪";
364
+ const columns = [];
365
+ if (snapshot.remoteReady.supported) {
366
+ columns.push(actionColumn("remote_ready_toggle", remoteLabel, snapshot.remoteReady.active ? "default" : "primary_filled", remoteAction, snapshot.remoteReady.active
367
+ ? undefined
368
+ : {
369
+ title: "开启远程就绪?",
370
+ text: "桥接服务运行期间会阻止 macOS 因空闲自动睡眠,电池耗电可能增加。",
371
+ }));
372
+ }
373
+ if (snapshot.activeTask) {
374
+ columns.push(actionColumn("device_stop", "停止当前任务", "default", "device_stop", {
375
+ title: "停止当前任务?",
376
+ text: "已经完成的文件修改不会自动撤销。",
377
+ }));
378
+ }
379
+ return columns.length === 0 ? null : {
380
+ tag: "column_set",
381
+ element_id: "remote_actions",
382
+ flex_mode: "none",
383
+ horizontal_spacing: "8px",
384
+ columns,
385
+ };
386
+ }
387
+ function actionColumn(elementId, label, type, action, confirm) {
388
+ const button = {
389
+ tag: "button",
390
+ element_id: elementId,
391
+ text: { tag: "plain_text", content: label },
392
+ type,
393
+ width: "fill",
394
+ behaviors: [
395
+ {
396
+ type: "callback",
397
+ value: { bridge: "feishu-codex-v4", action },
398
+ },
399
+ ],
400
+ };
401
+ if (confirm) {
402
+ button.confirm = {
403
+ title: { tag: "plain_text", content: confirm.title },
404
+ text: { tag: "plain_text", content: confirm.text },
405
+ };
406
+ }
407
+ return {
408
+ tag: "column",
409
+ width: "weighted",
410
+ weight: 1,
411
+ elements: [button],
412
+ };
413
+ }
414
+ function statePresentation(state) {
415
+ switch (state) {
416
+ case "online":
417
+ return { label: "在线", tag: "可以执行", metric: "已连接", template: "blue", color: "green", textColor: "green", background: "blue-50" };
418
+ case "degraded":
419
+ return { label: "部分可用", tag: "部分可用", metric: "部分连接", template: "orange", color: "orange", textColor: "orange", background: "orange-50" };
420
+ case "error":
421
+ return { label: "异常", tag: "需要修复", metric: "连接正常", template: "red", color: "red", textColor: "red", background: "red-50" };
422
+ case "maintenance":
423
+ return { label: "维护中", tag: "暂停任务", metric: "维护中", template: "grey", color: "neutral", textColor: "grey", background: "grey-50" };
424
+ case "offline":
425
+ return { label: "离线", tag: "设备离线", metric: "已离线", template: "grey", color: "neutral", textColor: "grey", background: "grey-50" };
426
+ case "connecting":
427
+ return { label: "连接中", tag: "正在连接", metric: "连接中", template: "orange", color: "orange", textColor: "orange", background: "orange-50" };
428
+ }
429
+ }
430
+ function formatTime(value) {
431
+ const date = new Date(value);
432
+ if (!Number.isFinite(date.getTime()))
433
+ return "时间未知";
434
+ return date.toLocaleString("zh-CN", {
435
+ month: "2-digit",
436
+ day: "2-digit",
437
+ hour: "2-digit",
438
+ minute: "2-digit",
439
+ hour12: false,
440
+ });
441
+ }
442
+ function safe(value) {
443
+ return value
444
+ .replaceAll("&", "&")
445
+ .replaceAll("<", "<")
446
+ .replaceAll(">", ">")
447
+ .replaceAll("*", "*")
448
+ .replaceAll("_", "_")
449
+ .replaceAll("`", "`")
450
+ .replaceAll("[", "[")
451
+ .replaceAll("]", "]");
452
+ }
453
+ function truncate(value, maxLength) {
454
+ if (value.length <= maxLength)
455
+ return value;
456
+ return `${value.slice(0, Math.max(0, maxLength - 1))}…`;
457
+ }
458
+ function truncatePlain(value, maxLength) {
459
+ return truncate(value.replaceAll("<", "<").replaceAll(">", ">"), maxLength);
460
+ }
@@ -0,0 +1,94 @@
1
+ export function deriveDeviceAvailability({ consumers, codex, api, sampledAt = new Date().toISOString(), serviceHeartbeatAt, heartbeatTimeoutMs = 45_000, lastSuccessfulTaskAt, maintenance = false, }) {
2
+ const common = {
3
+ sampledAt,
4
+ ...(lastSuccessfulTaskAt ? { lastSuccessfulTaskAt } : {}),
5
+ };
6
+ if (maintenance) {
7
+ return {
8
+ state: "maintenance",
9
+ title: "设备正在维护",
10
+ detail: "服务在线,但暂不接受新的 Codex 任务。",
11
+ nextAction: "维护结束后刷新状态。",
12
+ canExecute: false,
13
+ canInteract: true,
14
+ ...common,
15
+ };
16
+ }
17
+ if (serviceHeartbeatAt) {
18
+ const sampled = Date.parse(sampledAt);
19
+ const heartbeat = Date.parse(serviceHeartbeatAt);
20
+ if (!Number.isFinite(heartbeat) || sampled - heartbeat > heartbeatTimeoutMs) {
21
+ return {
22
+ state: "offline",
23
+ title: "本地设备已离线",
24
+ detail: "服务健康心跳已经过期,当前卡片不能证明设备仍可执行任务。",
25
+ nextAction: "检查电脑电源、网络和后台服务;恢复后重新发送“状态”。",
26
+ canExecute: false,
27
+ canInteract: false,
28
+ ...common,
29
+ };
30
+ }
31
+ }
32
+ const messages = consumers.find((consumer) => consumer.eventKey === "im.message.receive_v1");
33
+ const cards = consumers.find((consumer) => consumer.eventKey === "card.action.trigger");
34
+ if (!messages?.ready) {
35
+ return {
36
+ state: "connecting",
37
+ title: "飞书连接正在恢复",
38
+ detail: "消息事件尚未就绪,此时发送的新任务可能无法被本机接收。",
39
+ nextAction: "等待连接恢复后重新发送“状态”。",
40
+ canExecute: false,
41
+ canInteract: false,
42
+ ...common,
43
+ };
44
+ }
45
+ if (!cards?.ready) {
46
+ return {
47
+ state: "degraded",
48
+ title: "设备部分可用",
49
+ detail: "文字消息可用,但卡片按钮连接正在恢复。",
50
+ nextAction: "可以直接发送文字任务;需要按钮操作时稍后刷新状态。",
51
+ canExecute: true,
52
+ canInteract: false,
53
+ ...common,
54
+ };
55
+ }
56
+ if (api?.state === "degraded") {
57
+ return {
58
+ state: "degraded",
59
+ title: "飞书发送链路正在恢复",
60
+ detail: `消息接收正常,但最近 ${api.consecutiveFailures} 次飞书 API 请求失败;可靠文本回复会继续重试。`,
61
+ nextAction: "任务仍可发送;若长时间没有回复,请检查网络后刷新状态。",
62
+ canExecute: true,
63
+ canInteract: true,
64
+ ...common,
65
+ };
66
+ }
67
+ if (codex.lastError && !codex.ready) {
68
+ return {
69
+ state: "error",
70
+ title: "Codex 引擎异常",
71
+ detail: truncate(codex.lastError, 240),
72
+ nextAction: "点击“重连 Codex”验证登录和本地引擎。",
73
+ canExecute: false,
74
+ canInteract: true,
75
+ ...common,
76
+ };
77
+ }
78
+ return {
79
+ state: "online",
80
+ title: "可以开始任务",
81
+ detail: codex.ready
82
+ ? "飞书消息、卡片和 Codex 引擎均已连接。"
83
+ : "飞书连接正常;Codex 引擎将在首个任务时按需启动。",
84
+ nextAction: "直接发送完整任务,或先切换项目与模型。",
85
+ canExecute: true,
86
+ canInteract: true,
87
+ ...common,
88
+ };
89
+ }
90
+ function truncate(value, maxLength) {
91
+ if (value.length <= maxLength)
92
+ return value;
93
+ return `${value.slice(0, Math.max(0, maxLength - 1))}…`;
94
+ }