chatccc 0.2.226 → 0.2.228

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 (67) hide show
  1. package/.agents/skills/create-chatccc-feishu-app/SKILL.md +85 -85
  2. package/.claude/skills/create-chatccc-feishu-app/SKILL.md +85 -85
  3. package/.cursor/skills/create-chatccc-feishu-app/SKILL.md +85 -85
  4. package/README.md +90 -90
  5. package/package.json +1 -1
  6. package/src/__tests__/agent-activity.test.ts +76 -76
  7. package/src/__tests__/builtin-chat-session.test.ts +350 -350
  8. package/src/__tests__/builtin-config.test.ts +26 -26
  9. package/src/__tests__/builtin-context.test.ts +163 -163
  10. package/src/__tests__/builtin-file-tools.test.ts +275 -275
  11. package/src/__tests__/builtin-permissions.test.ts +211 -211
  12. package/src/__tests__/builtin-session-select.test.ts +116 -116
  13. package/src/__tests__/builtin-skills.test.ts +252 -141
  14. package/src/__tests__/builtin-web-tools.test.ts +220 -0
  15. package/src/__tests__/card-action-routing.test.ts +18 -18
  16. package/src/__tests__/ccc-adapter.test.ts +136 -136
  17. package/src/__tests__/claude-adapter.test.ts +614 -614
  18. package/src/__tests__/codex-adapter.test.ts +58 -58
  19. package/src/__tests__/codex-raw-stream-log.test.ts +170 -170
  20. package/src/__tests__/cursor-adapter.test.ts +268 -268
  21. package/src/__tests__/feishu-avatar.test.ts +164 -164
  22. package/src/__tests__/feishu-message-ingress.test.ts +138 -138
  23. package/src/__tests__/package-files.test.ts +24 -24
  24. package/src/__tests__/progress-reducer.test.ts +110 -110
  25. package/src/__tests__/response-stall.test.ts +49 -49
  26. package/src/__tests__/sim-platform.test.ts +16 -16
  27. package/src/__tests__/startup-lifecycle.test.ts +231 -231
  28. package/src/__tests__/stop-session.test.ts +34 -34
  29. package/src/__tests__/terminal-renderer.test.ts +247 -247
  30. package/src/__tests__/update-command-guard.test.ts +144 -144
  31. package/src/__tests__/web-ui.test.ts +326 -326
  32. package/src/adapters/adapter-interface.ts +18 -18
  33. package/src/adapters/ccc-adapter.ts +131 -131
  34. package/src/adapters/claude-adapter.ts +620 -620
  35. package/src/adapters/codex-adapter.ts +426 -426
  36. package/src/adapters/cursor-adapter.ts +681 -681
  37. package/src/agent-activity.ts +170 -170
  38. package/src/agent-delegate-task.ts +91 -91
  39. package/src/builtin/cli.ts +61 -2
  40. package/src/builtin/config.ts +84 -84
  41. package/src/builtin/context.ts +323 -323
  42. package/src/builtin/file-log.ts +38 -38
  43. package/src/builtin/file-tools.ts +37 -0
  44. package/src/builtin/index.ts +44 -24
  45. package/src/builtin/proc-tree-kill.ts +61 -61
  46. package/src/builtin/progress/cards-helpers.ts +76 -76
  47. package/src/builtin/progress/reducer.ts +108 -108
  48. package/src/builtin/progress/terminal-renderer.ts +294 -294
  49. package/src/builtin/progress/view.ts +77 -77
  50. package/src/builtin/raw-stream-log.ts +124 -124
  51. package/src/builtin/session-select.ts +48 -48
  52. package/src/builtin/skills.ts +190 -108
  53. package/src/builtin/web-tools.ts +313 -0
  54. package/src/card-action-routing.ts +14 -14
  55. package/src/feishu-api.ts +193 -193
  56. package/src/feishu-message-ingress.ts +195 -195
  57. package/src/index.ts +306 -306
  58. package/src/orchestrator.ts +2388 -2388
  59. package/src/platform-adapter.ts +6 -6
  60. package/src/progress/reducer.ts +108 -108
  61. package/src/progress/terminal-renderer.ts +294 -294
  62. package/src/progress/view.ts +77 -77
  63. package/src/response-stall.ts +28 -28
  64. package/src/session-chat-binding.ts +82 -82
  65. package/src/startup-lifecycle.ts +250 -250
  66. package/src/stream-state.ts +18 -18
  67. package/src/update-command-guard.ts +165 -165
package/src/index.ts CHANGED
@@ -28,19 +28,19 @@ import { WSClient, EventDispatcher, Domain } from "@larksuiteoapi/node-sdk";
28
28
  import WebSocket from "ws";
29
29
 
30
30
  import { appendStartupTrace, attachRelayWebSocket, ensureSingleInstance, freeRelayListenPort, installCrashLogging, waitForPortFree } from "./shared.ts";
31
- import { createUiRouter, setExtraApiHandler, setReloadConfigHook, startSetupMode } from "./web-ui.ts";
32
- import {
33
- buildWebUiUrl,
34
- createServiceLifecycleGuard,
35
- openWebUiInDefaultBrowser,
36
- shouldAutoOpenWebUi,
37
- } from "./startup-lifecycle.ts";
31
+ import { createUiRouter, setExtraApiHandler, setReloadConfigHook, startSetupMode } from "./web-ui.ts";
32
+ import {
33
+ buildWebUiUrl,
34
+ createServiceLifecycleGuard,
35
+ openWebUiInDefaultBrowser,
36
+ shouldAutoOpenWebUi,
37
+ } from "./startup-lifecycle.ts";
38
38
  import { buildPlatformStartupPlan } from "./platform-startup.ts";
39
39
  import { makeTraceId, logTrace } from "./trace.ts";
40
40
  import {
41
- CHATCCC_PORT,
42
- config,
43
- APP_ID,
41
+ CHATCCC_PORT,
42
+ config,
43
+ APP_ID,
44
44
  APP_SECRET,
45
45
  FEISHU_ENABLED,
46
46
  FEISHU_PLATFORM_TYPE,
@@ -96,9 +96,9 @@ import {
96
96
  sendCardKitMessage,
97
97
  updateCardKitCard,
98
98
  } from "./cardkit.ts";
99
- import {
100
- loadSessionRegistryForBinding,
101
- rebuildBindingsFromRegistry,
99
+ import {
100
+ loadSessionRegistryForBinding,
101
+ rebuildBindingsFromRegistry,
102
102
  resetState,
103
103
  setSessionPlatform,
104
104
  startUnifiedDisplayLoop,
@@ -111,13 +111,13 @@ import {
111
111
  import { fixStaleStreamStates } from "./stream-state.ts";
112
112
  import { handleCommand, type PlatformAdapter } from "./orchestrator.ts";
113
113
  import { createWechatAdapter, startWechatPlatform } from "./wechat-platform.ts";
114
- import { handleCodexResetCardAction } from "./codex-reset-actions.ts";
115
- import { resolveFeishuCardActionChatType } from "./card-action-routing.ts";
116
- import { reloadRuntimeConfig } from "./runtime-reload.ts";
117
- import {
118
- createAckFirstEventHandler,
119
- feishuMessageLedger,
120
- } from "./feishu-message-ingress.ts";
114
+ import { handleCodexResetCardAction } from "./codex-reset-actions.ts";
115
+ import { resolveFeishuCardActionChatType } from "./card-action-routing.ts";
116
+ import { reloadRuntimeConfig } from "./runtime-reload.ts";
117
+ import {
118
+ createAckFirstEventHandler,
119
+ feishuMessageLedger,
120
+ } from "./feishu-message-ingress.ts";
121
121
 
122
122
  // ---------------------------------------------------------------------------
123
123
  // Feishu 平台适配器
@@ -199,21 +199,21 @@ function getInnerEvent(data: Evt): InnerEvent {
199
199
  return (data.event ?? data) as InnerEvent;
200
200
  }
201
201
 
202
- import { formatMessageContent } from "./format-message.ts";
203
- import {
204
- buildUpdateCommandId,
205
- extractFeishuEventId,
206
- } from "./update-command-guard.ts";
202
+ import { formatMessageContent } from "./format-message.ts";
203
+ import {
204
+ buildUpdateCommandId,
205
+ extractFeishuEventId,
206
+ } from "./update-command-guard.ts";
207
207
 
208
208
  // ---------------------------------------------------------------------------
209
209
  // Card action helper: parse button click into text command
210
210
  // ---------------------------------------------------------------------------
211
211
 
212
- interface CardActionResult {
213
- text: string;
214
- chatId: string;
215
- openId: string;
216
- commandId?: string;
212
+ interface CardActionResult {
213
+ text: string;
214
+ chatId: string;
215
+ openId: string;
216
+ commandId?: string;
217
217
  }
218
218
 
219
219
  function parseCardAction(data: unknown): CardActionResult | null {
@@ -252,130 +252,130 @@ function parseCardAction(data: unknown): CardActionResult | null {
252
252
  ((raw as Record<string, unknown>).operator as Record<string, unknown>)?.open_id as string ??
253
253
  "";
254
254
 
255
- return {
256
- text,
257
- chatId,
258
- openId,
259
- commandId: buildUpdateCommandId("card", extractFeishuEventId(data)),
260
- };
255
+ return {
256
+ text,
257
+ chatId,
258
+ openId,
259
+ commandId: buildUpdateCommandId("card", extractFeishuEventId(data)),
260
+ };
261
261
  }
262
262
 
263
263
  // ---------------------------------------------------------------------------
264
264
  // WebSocket relay broadcast
265
265
  // ---------------------------------------------------------------------------
266
266
 
267
- let broadcastToRelay: (data: unknown) => void = () => {};
268
- const wechatSignal = { stopped: false };
269
-
270
- async function processFeishuMessageEvent(data: Evt): Promise<void> {
271
- const traceId = makeTraceId();
272
- try {
273
- broadcastToRelay(data);
274
-
275
- const event = getInnerEvent(data);
276
- const message = event.message;
277
- if (!message) return;
278
-
279
- const messageId = message.message_id;
280
- const chatId = message.chat_id ?? "";
281
- const chatType = message.chat_type ?? "group";
282
- const msgTimestamp = parseInt(message.create_time ?? "0", 10) || Date.now();
283
- const disposition = await feishuMessageLedger.accept({
284
- messageId,
285
- chatId,
286
- createTime: msgTimestamp,
287
- });
288
-
289
- if (disposition === "duplicate") {
290
- console.log(`[MSG] Duplicate message ignored: ${messageId ?? "(missing id)"}`);
291
- return;
292
- }
293
- if (disposition === "stale") {
294
- logTrace(traceId, "DONE", {
295
- outcome: "skip_stale_feishu_message",
296
- messageId,
297
- chatId,
298
- msgTimestamp,
299
- });
300
- console.log(
301
- `[${ts()}] [SKIP] Stale Feishu message ignored: messageId=${messageId ?? "(missing id)"} chatId=${chatId} createTime=${msgTimestamp}`,
302
- );
303
- return;
304
- }
305
-
306
- const text = await formatMessageContent(message);
307
- const openId = event.sender?.sender_id?.open_id ?? "";
308
-
309
- console.log(
310
- `[MSG] id=${messageId ?? "(missing)"} sender=${openId} chat=${chatId} type=${chatType} text="${text}"`,
311
- );
312
- appendChatLog(chatId, openId, text);
313
-
314
- if (messageId) {
315
- getTenantAccessToken().then((freshToken) =>
316
- addReaction(freshToken, messageId).catch((err) =>
317
- console.error(`[${ts()}] Reaction failed: ${(err as Error).message}`)
318
- )
319
- ).catch((err) =>
320
- console.error(`[${ts()}] Reaction token failed: ${(err as Error).message}`)
321
- );
322
- }
323
-
324
- if (!text) return;
325
- logTrace(traceId, "RECV", {
326
- messageId,
327
- chatId,
328
- chatType,
329
- text: text.slice(0, 100),
330
- });
331
-
332
- const delayNotice = formatDelayNotice(msgTimestamp, text);
333
- if (delayNotice) {
334
- const delayToken = await getTenantAccessToken();
335
- await sendCardReply(delayToken, chatId, "延迟送达", delayNotice, "yellow").catch(() => {});
336
- // 延迟送达的旧消息只发提醒、不转发给 agent 执行:
337
- // 断线重连或补推时,早已过期的消息不应未经确认就触发新任务。
338
- // 用户如需执行可自行重发。
339
- logTrace(traceId, "DONE", {
340
- outcome: "skip_delayed_feishu_message",
341
- chatId,
342
- msgTimestamp,
343
- delayMinutes: Math.floor((Date.now() - msgTimestamp) / 60000),
344
- });
345
- console.log(
346
- `[${ts()}] [SKIP] Delayed Feishu message not forwarded to agent: messageId=${messageId ?? "(missing id)"} chatId=${chatId} createTime=${msgTimestamp}`,
347
- );
348
- return;
349
- }
350
-
351
- await handleCommand(
352
- feishuPlatform,
353
- text,
354
- chatId,
355
- openId,
356
- msgTimestamp,
357
- chatType,
358
- traceId,
359
- buildUpdateCommandId("message", messageId),
360
- );
361
- } catch (err) {
362
- logTrace(traceId, "ERROR", { message: (err as Error).message });
363
- console.error(
364
- `[${ts()}] [FATAL] im.message.receive_v1 worker crashed: ${(err as Error).message}`,
365
- );
366
- }
367
- }
368
-
369
- const handleFeishuMessageEventAckFirst = createAckFirstEventHandler(
370
- processFeishuMessageEvent,
371
- (err) => console.error(
372
- `[${ts()}] [FATAL] Detached Feishu event worker failed: ${(err as Error).message}`,
373
- ),
374
- );
375
-
376
- // ---------------------------------------------------------------------------
377
- // Simulate mode: inject message via HTTP
378
- // ---------------------------------------------------------------------------
267
+ let broadcastToRelay: (data: unknown) => void = () => {};
268
+ const wechatSignal = { stopped: false };
269
+
270
+ async function processFeishuMessageEvent(data: Evt): Promise<void> {
271
+ const traceId = makeTraceId();
272
+ try {
273
+ broadcastToRelay(data);
274
+
275
+ const event = getInnerEvent(data);
276
+ const message = event.message;
277
+ if (!message) return;
278
+
279
+ const messageId = message.message_id;
280
+ const chatId = message.chat_id ?? "";
281
+ const chatType = message.chat_type ?? "group";
282
+ const msgTimestamp = parseInt(message.create_time ?? "0", 10) || Date.now();
283
+ const disposition = await feishuMessageLedger.accept({
284
+ messageId,
285
+ chatId,
286
+ createTime: msgTimestamp,
287
+ });
288
+
289
+ if (disposition === "duplicate") {
290
+ console.log(`[MSG] Duplicate message ignored: ${messageId ?? "(missing id)"}`);
291
+ return;
292
+ }
293
+ if (disposition === "stale") {
294
+ logTrace(traceId, "DONE", {
295
+ outcome: "skip_stale_feishu_message",
296
+ messageId,
297
+ chatId,
298
+ msgTimestamp,
299
+ });
300
+ console.log(
301
+ `[${ts()}] [SKIP] Stale Feishu message ignored: messageId=${messageId ?? "(missing id)"} chatId=${chatId} createTime=${msgTimestamp}`,
302
+ );
303
+ return;
304
+ }
305
+
306
+ const text = await formatMessageContent(message);
307
+ const openId = event.sender?.sender_id?.open_id ?? "";
308
+
309
+ console.log(
310
+ `[MSG] id=${messageId ?? "(missing)"} sender=${openId} chat=${chatId} type=${chatType} text="${text}"`,
311
+ );
312
+ appendChatLog(chatId, openId, text);
313
+
314
+ if (messageId) {
315
+ getTenantAccessToken().then((freshToken) =>
316
+ addReaction(freshToken, messageId).catch((err) =>
317
+ console.error(`[${ts()}] Reaction failed: ${(err as Error).message}`)
318
+ )
319
+ ).catch((err) =>
320
+ console.error(`[${ts()}] Reaction token failed: ${(err as Error).message}`)
321
+ );
322
+ }
323
+
324
+ if (!text) return;
325
+ logTrace(traceId, "RECV", {
326
+ messageId,
327
+ chatId,
328
+ chatType,
329
+ text: text.slice(0, 100),
330
+ });
331
+
332
+ const delayNotice = formatDelayNotice(msgTimestamp, text);
333
+ if (delayNotice) {
334
+ const delayToken = await getTenantAccessToken();
335
+ await sendCardReply(delayToken, chatId, "延迟送达", delayNotice, "yellow").catch(() => {});
336
+ // 延迟送达的旧消息只发提醒、不转发给 agent 执行:
337
+ // 断线重连或补推时,早已过期的消息不应未经确认就触发新任务。
338
+ // 用户如需执行可自行重发。
339
+ logTrace(traceId, "DONE", {
340
+ outcome: "skip_delayed_feishu_message",
341
+ chatId,
342
+ msgTimestamp,
343
+ delayMinutes: Math.floor((Date.now() - msgTimestamp) / 60000),
344
+ });
345
+ console.log(
346
+ `[${ts()}] [SKIP] Delayed Feishu message not forwarded to agent: messageId=${messageId ?? "(missing id)"} chatId=${chatId} createTime=${msgTimestamp}`,
347
+ );
348
+ return;
349
+ }
350
+
351
+ await handleCommand(
352
+ feishuPlatform,
353
+ text,
354
+ chatId,
355
+ openId,
356
+ msgTimestamp,
357
+ chatType,
358
+ traceId,
359
+ buildUpdateCommandId("message", messageId),
360
+ );
361
+ } catch (err) {
362
+ logTrace(traceId, "ERROR", { message: (err as Error).message });
363
+ console.error(
364
+ `[${ts()}] [FATAL] im.message.receive_v1 worker crashed: ${(err as Error).message}`,
365
+ );
366
+ }
367
+ }
368
+
369
+ const handleFeishuMessageEventAckFirst = createAckFirstEventHandler(
370
+ processFeishuMessageEvent,
371
+ (err) => console.error(
372
+ `[${ts()}] [FATAL] Detached Feishu event worker failed: ${(err as Error).message}`,
373
+ ),
374
+ );
375
+
376
+ // ---------------------------------------------------------------------------
377
+ // Simulate mode: inject message via HTTP
378
+ // ---------------------------------------------------------------------------
379
379
 
380
380
  async function handleSimInjectMessage(req: IncomingMessage, res: ServerResponse): Promise<boolean> {
381
381
  const url = new URL(req.url ?? "/", "http://127.0.0.1");
@@ -507,9 +507,9 @@ async function startBotServiceCore(): Promise<void> {
507
507
 
508
508
  console.log(`[${ts()}] [AUTH] Token obtained`);
509
509
 
510
- const eventDispatcher = new EventDispatcher({});
511
- eventDispatcher.register({
512
- "im.message.receive_v1": handleFeishuMessageEventAckFirst,
510
+ const eventDispatcher = new EventDispatcher({});
511
+ eventDispatcher.register({
512
+ "im.message.receive_v1": handleFeishuMessageEventAckFirst,
513
513
 
514
514
  "card.action.trigger": async (data: Evt) => {
515
515
  try {
@@ -552,21 +552,21 @@ async function startBotServiceCore(): Promise<void> {
552
552
  });
553
553
  if (handledCodexReset) return;
554
554
 
555
- const result = parseCardAction(data);
556
- if (!result) return;
557
- const registry = await loadSessionRegistryForBinding();
558
- const chatType = resolveFeishuCardActionChatType(result.chatId, registry);
559
- console.log(`[BTN] chat=${result.chatId} chatType=${chatType} text="${result.text}"`);
560
- handleCommand(
561
- feishuPlatform,
562
- result.text,
563
- result.chatId,
564
- result.openId,
565
- Date.now(),
566
- chatType,
567
- undefined,
568
- result.commandId,
569
- ).catch((err) =>
555
+ const result = parseCardAction(data);
556
+ if (!result) return;
557
+ const registry = await loadSessionRegistryForBinding();
558
+ const chatType = resolveFeishuCardActionChatType(result.chatId, registry);
559
+ console.log(`[BTN] chat=${result.chatId} chatType=${chatType} text="${result.text}"`);
560
+ handleCommand(
561
+ feishuPlatform,
562
+ result.text,
563
+ result.chatId,
564
+ result.openId,
565
+ Date.now(),
566
+ chatType,
567
+ undefined,
568
+ result.commandId,
569
+ ).catch((err) =>
570
570
  console.error(`[${ts()}] [BTN] handleCommand failed: ${(err as Error).message}`)
571
571
  );
572
572
  } catch (err) {
@@ -589,20 +589,20 @@ async function startBotServiceCore(): Promise<void> {
589
589
  ws.on("message", async (raw: Buffer) => {
590
590
  try {
591
591
  const data = JSON.parse(raw.toString()) as Evt;
592
- const action = parseCardAction(data);
593
- if (action) {
594
- const registry = await loadSessionRegistryForBinding();
595
- const chatType = resolveFeishuCardActionChatType(action.chatId, registry);
596
- handleCommand(
597
- feishuPlatform,
598
- action.text,
599
- action.chatId,
600
- action.openId,
601
- Date.now(),
602
- chatType,
603
- undefined,
604
- action.commandId,
605
- ).catch((err) =>
592
+ const action = parseCardAction(data);
593
+ if (action) {
594
+ const registry = await loadSessionRegistryForBinding();
595
+ const chatType = resolveFeishuCardActionChatType(action.chatId, registry);
596
+ handleCommand(
597
+ feishuPlatform,
598
+ action.text,
599
+ action.chatId,
600
+ action.openId,
601
+ Date.now(),
602
+ chatType,
603
+ undefined,
604
+ action.commandId,
605
+ ).catch((err) =>
606
606
  console.error(`[${ts()}] [BTN] handleCommand failed: ${(err as Error).message}`)
607
607
  );
608
608
  return;
@@ -636,8 +636,8 @@ async function startBotServiceCore(): Promise<void> {
636
636
  } else {
637
637
  // 进程首次启动:此时所有 Map 都是空的,resetState 主要是打个 LOG 标识"开始
638
638
  // 干净状态"。修正残留的 running stream-state 并重建 session→chat 映射。
639
- resetState();
640
- await feishuMessageLedger.load();
639
+ resetState();
640
+ await feishuMessageLedger.load();
641
641
  startUnifiedDisplayLoop();
642
642
  fixStaleStreamStates().then(async () => {
643
643
  const registry = await loadSessionRegistryForBinding();
@@ -776,32 +776,32 @@ async function startConfiguredPlatforms(
776
776
  // Main
777
777
  // ---------------------------------------------------------------------------
778
778
 
779
- async function main(): Promise<void> {
780
- // 用户直接运行 chatccc 时打开系统默认浏览器;由 `/restart`、`/update`
781
- // 或 Web UI 拉起的替代进程会携带内部标记,不重复打扰用户。
782
- const autoOpenWebUi = shouldAutoOpenWebUi({
783
- openOnStart: config.webUi.openOnStart,
784
- });
785
- appendStartupTrace("main: entered", {
786
- argv: process.argv.join(" ").slice(0, 400),
787
- CHATCCC_PORT,
788
- PROJECT_ROOT,
789
- autoOpenWebUi,
779
+ async function main(): Promise<void> {
780
+ // 用户直接运行 chatccc 时打开系统默认浏览器;由 `/restart`、`/update`
781
+ // 或 Web UI 拉起的替代进程会携带内部标记,不重复打扰用户。
782
+ const autoOpenWebUi = shouldAutoOpenWebUi({
783
+ openOnStart: config.webUi.openOnStart,
784
+ });
785
+ appendStartupTrace("main: entered", {
786
+ argv: process.argv.join(" ").slice(0, 400),
787
+ CHATCCC_PORT,
788
+ PROJECT_ROOT,
789
+ autoOpenWebUi,
790
790
  });
791
791
 
792
- // ChatCCC 是常驻服务,不能把“当前刚好有没有 Agent session”当作进程生命周期。
793
- // referenced timer 明确锚定服务进程;HTTP Server 接入后还会定期 ref/健康检查。
794
- // `/restart`、`/update` 都使用 process.exit(),不会被 timer 阻止。
795
- const serviceLifecycle = createServiceLifecycleGuard({ tracer: appendStartupTrace });
796
- serviceLifecycle.start();
797
-
798
- // 黑匣子:所有未捕获异常 / 信号 / beforeExit 都同步写入 startup-trace.log(appendFileSync)。
792
+ // ChatCCC 是常驻服务,不能把“当前刚好有没有 Agent session”当作进程生命周期。
793
+ // referenced timer 明确锚定服务进程;HTTP Server 接入后还会定期 ref/健康检查。
794
+ // `/restart`、`/update` 都使用 process.exit(),不会被 timer 阻止。
795
+ const serviceLifecycle = createServiceLifecycleGuard({ tracer: appendStartupTrace });
796
+ serviceLifecycle.start();
797
+
798
+ // 黑匣子:所有未捕获异常 / 信号 / beforeExit 都同步写入 startup-trace.log(appendFileSync)。
799
799
  // 越早装越好——后续任何一行抛错都有兜底;它独立于 SIGINT 清理(见末尾的
800
800
  // server.close)——只负责诊断与默认致命退出,不替代清理逻辑。
801
- installCrashLogging({
802
- flush: () => fileLog.flush(),
803
- onBeforeExit: (code) => serviceLifecycle.handleBeforeExit(code),
804
- });
801
+ installCrashLogging({
802
+ flush: () => fileLog.flush(),
803
+ onBeforeExit: (code) => serviceLifecycle.handleBeforeExit(code),
804
+ });
805
805
 
806
806
  // 模拟模式:独立端口 18079,不与 SDK 实例冲突,不走飞书凭证/权限/WSClient
807
807
  if (USE_SIMULATE) {
@@ -841,33 +841,33 @@ async function main(): Promise<void> {
841
841
  simServer.once("error", onError);
842
842
  simServer.once("listening", onListening);
843
843
  simServer.listen(SIM_PORT, "127.0.0.1");
844
- }).catch((err: NodeJS.ErrnoException) => {
844
+ }).catch((err: NodeJS.ErrnoException) => {
845
845
  console.error(`\n[启动] 监听失败:端口 ${SIM_PORT}(${err.code ?? "?"} — ${err.message})`);
846
- process.exit(1);
847
- });
848
- serviceLifecycle.attachServer(
849
- simServer,
850
- () => recoverHttpServer(simServer, SIM_PORT),
851
- );
846
+ process.exit(1);
847
+ });
848
+ serviceLifecycle.attachServer(
849
+ simServer,
850
+ () => recoverHttpServer(simServer, SIM_PORT),
851
+ );
852
852
 
853
853
  console.log(`\n${"=".repeat(60)}`);
854
854
  console.log(` ChatCCC — 模拟飞书环境模式`);
855
855
  console.log(`${"=".repeat(60)}`);
856
856
  console.log(` 发送消息: POST http://127.0.0.1:${SIM_PORT}/api/sim/inject-message`);
857
857
  console.log(` 消息日志: ~/.chatccc/sim/messages.jsonl`);
858
- console.log(`${"=".repeat(60)}\n`);
859
-
860
- if (autoOpenWebUi) {
861
- const url = buildWebUiUrl(SIM_PORT);
862
- const opened = openWebUiInDefaultBrowser(SIM_PORT);
863
- appendStartupTrace(
864
- opened ? "web-ui: opening simulate browser" : "web-ui: simulate browser unavailable",
865
- { url },
866
- );
867
- }
868
-
869
- installShutdownHandlers(simServer, serviceLifecycle);
870
- return;
858
+ console.log(`${"=".repeat(60)}\n`);
859
+
860
+ if (autoOpenWebUi) {
861
+ const url = buildWebUiUrl(SIM_PORT);
862
+ const opened = openWebUiInDefaultBrowser(SIM_PORT);
863
+ appendStartupTrace(
864
+ opened ? "web-ui: opening simulate browser" : "web-ui: simulate browser unavailable",
865
+ { url },
866
+ );
867
+ }
868
+
869
+ installShutdownHandlers(simServer, serviceLifecycle);
870
+ return;
871
871
  }
872
872
 
873
873
  if (Number.isNaN(CHATCCC_PORT) || CHATCCC_PORT < 1 || CHATCCC_PORT > 65535) {
@@ -914,32 +914,32 @@ async function main(): Promise<void> {
914
914
  if (!APP_ID.trim() || !APP_SECRET.trim()) {
915
915
  // 凭证不全:进 setup 向导。注入 onActivate 回调让用户点"保存并启动"
916
916
  // 时,原地(同进程)调用 startBotService,复用 setup HTTP server。
917
- const setupServer = startSetupMode(CHATCCC_PORT, {
918
- openBrowser: autoOpenWebUi,
919
- onActivate: async (httpServer: Server) => {
917
+ const setupServer = startSetupMode(CHATCCC_PORT, {
918
+ openBrowser: autoOpenWebUi,
919
+ onActivate: async (httpServer: Server) => {
920
920
  reloadRuntimeConfig("setup-activate");
921
921
  appendStartupTrace("setup-activate: reloaded config from disk", {
922
922
  appIdMaskAfterReload: maskAppId(APP_ID),
923
923
  });
924
924
  try {
925
925
  await startConfiguredPlatforms(httpServer, { failOnFeishuError: true });
926
- return { ok: true };
926
+ return { ok: true };
927
927
  } catch (err) {
928
928
  appendStartupTrace("setup-activate: startConfiguredPlatforms failed", {
929
929
  message: (err as Error).message,
930
930
  });
931
931
  return { ok: false, error: (err as Error).message };
932
932
  }
933
- },
934
- });
935
- setupServer.once("listening", () => {
936
- serviceLifecycle.attachServer(
937
- setupServer,
938
- () => recoverHttpServer(setupServer, CHATCCC_PORT),
939
- );
940
- });
941
- installShutdownHandlers(setupServer, serviceLifecycle);
942
- return;
933
+ },
934
+ });
935
+ setupServer.once("listening", () => {
936
+ serviceLifecycle.attachServer(
937
+ setupServer,
938
+ () => recoverHttpServer(setupServer, CHATCCC_PORT),
939
+ );
940
+ });
941
+ installShutdownHandlers(setupServer, serviceLifecycle);
942
+ return;
943
943
  }
944
944
  console.log(` 必填项校验通过(App ID 摘要: ${maskAppId(APP_ID)})。\n`);
945
945
  appendStartupTrace("main: feishu credentials ok", { appIdMask: maskAppId(APP_ID) });
@@ -958,59 +958,59 @@ async function main(): Promise<void> {
958
958
  await waitForPortFree(CHATCCC_PORT);
959
959
  appendStartupTrace("main: port free confirmed", { CHATCCC_PORT });
960
960
  }
961
- const httpServer = createServer(createUiRouter());
962
- await listenWithRetry(httpServer, CHATCCC_PORT, "127.0.0.1").catch((err: NodeJS.ErrnoException) => {
961
+ const httpServer = createServer(createUiRouter());
962
+ await listenWithRetry(httpServer, CHATCCC_PORT, "127.0.0.1").catch((err: NodeJS.ErrnoException) => {
963
963
  console.error(`\n[启动] 本地中继 WebSocket 监听失败:端口 ${CHATCCC_PORT}(${err.code ?? "?"} — ${err.message})`);
964
964
  console.error(
965
965
  " 处理建议: 关闭占用该端口的其它程序,或在 config.json 的 port 字段里改成其它未占用端口(如 18081)。"
966
966
  );
967
967
  printServiceDidNotStart(`本地中继端口 ${CHATCCC_PORT} 无法监听(${err.code ?? "?"} — ${err.message})`);
968
- process.exit(1);
969
- });
970
- serviceLifecycle.attachServer(
971
- httpServer,
972
- () => recoverHttpServer(httpServer, CHATCCC_PORT),
973
- );
974
- // 平台鉴权/长连接启动可能耗时;此时也必须允许 Ctrl+C / SIGTERM 正常退出,
975
- // 不能只留下更早安装的信号日志 listener 把默认退出行为吞掉。
976
- installShutdownHandlers(httpServer, serviceLifecycle);
977
-
978
- // 必须等 HTTP server 真正监听后再发起打开请求,避免浏览器先到一步看到
979
- // ERR_CONNECTION_REFUSED。Chrome CDP 守护仍保持自己原有的独立行为。
980
- if (autoOpenWebUi) {
981
- const url = buildWebUiUrl(CHATCCC_PORT);
982
- const opened = openWebUiInDefaultBrowser(CHATCCC_PORT);
983
- if (opened) {
984
- console.log(`[WEB-UI] 已请求系统默认浏览器打开: ${url}`);
985
- appendStartupTrace("web-ui: opening default browser", { url });
986
- } else {
987
- appendStartupTrace("web-ui: default browser unavailable", { url });
988
- }
989
- } else {
990
- appendStartupTrace("web-ui: default browser skipped by lifecycle or preference", {
991
- url: buildWebUiUrl(CHATCCC_PORT),
992
- openOnStart: config.webUi.openOnStart,
993
- });
994
- }
995
-
996
- await startConfiguredPlatforms(httpServer, { failOnFeishuError: false });
997
- }
998
-
999
- /**
1000
- * 生命周期健康检查发现 HTTP Server 已停止监听时,优先原地恢复同一个 Server。
1001
- * router、WebSocket upgrade listener 都挂在这个对象上,复用它能保留现有服务绑定;
1002
- * 恢复失败会被 lifecycle guard 记录,并在下一轮检查重试。
1003
- */
1004
- async function recoverHttpServer(httpServer: Server, port: number): Promise<void> {
1005
- if (httpServer.listening) {
1006
- httpServer.ref();
1007
- return;
1008
- }
1009
- appendStartupTrace("service-lifecycle: HTTP recovery begin", { port });
1010
- await listenWithRetry(httpServer, port, "127.0.0.1");
1011
- httpServer.ref();
1012
- appendStartupTrace("service-lifecycle: HTTP recovery listen succeeded", { port });
1013
- }
968
+ process.exit(1);
969
+ });
970
+ serviceLifecycle.attachServer(
971
+ httpServer,
972
+ () => recoverHttpServer(httpServer, CHATCCC_PORT),
973
+ );
974
+ // 平台鉴权/长连接启动可能耗时;此时也必须允许 Ctrl+C / SIGTERM 正常退出,
975
+ // 不能只留下更早安装的信号日志 listener 把默认退出行为吞掉。
976
+ installShutdownHandlers(httpServer, serviceLifecycle);
977
+
978
+ // 必须等 HTTP server 真正监听后再发起打开请求,避免浏览器先到一步看到
979
+ // ERR_CONNECTION_REFUSED。Chrome CDP 守护仍保持自己原有的独立行为。
980
+ if (autoOpenWebUi) {
981
+ const url = buildWebUiUrl(CHATCCC_PORT);
982
+ const opened = openWebUiInDefaultBrowser(CHATCCC_PORT);
983
+ if (opened) {
984
+ console.log(`[WEB-UI] 已请求系统默认浏览器打开: ${url}`);
985
+ appendStartupTrace("web-ui: opening default browser", { url });
986
+ } else {
987
+ appendStartupTrace("web-ui: default browser unavailable", { url });
988
+ }
989
+ } else {
990
+ appendStartupTrace("web-ui: default browser skipped by lifecycle or preference", {
991
+ url: buildWebUiUrl(CHATCCC_PORT),
992
+ openOnStart: config.webUi.openOnStart,
993
+ });
994
+ }
995
+
996
+ await startConfiguredPlatforms(httpServer, { failOnFeishuError: false });
997
+ }
998
+
999
+ /**
1000
+ * 生命周期健康检查发现 HTTP Server 已停止监听时,优先原地恢复同一个 Server。
1001
+ * router、WebSocket upgrade listener 都挂在这个对象上,复用它能保留现有服务绑定;
1002
+ * 恢复失败会被 lifecycle guard 记录,并在下一轮检查重试。
1003
+ */
1004
+ async function recoverHttpServer(httpServer: Server, port: number): Promise<void> {
1005
+ if (httpServer.listening) {
1006
+ httpServer.ref();
1007
+ return;
1008
+ }
1009
+ appendStartupTrace("service-lifecycle: HTTP recovery begin", { port });
1010
+ await listenWithRetry(httpServer, port, "127.0.0.1");
1011
+ httpServer.ref();
1012
+ appendStartupTrace("service-lifecycle: HTTP recovery listen succeeded", { port });
1013
+ }
1014
1014
 
1015
1015
  /**
1016
1016
  * 带重试的 server.listen,Windows 端口释放有延迟时自动重试。
@@ -1048,26 +1048,26 @@ async function listenWithRetry(
1048
1048
  * Node EventEmitter 按注册顺序触发,installCrashLogging 装得更早 → 同步 trace
1049
1049
  * 先写盘,再走这里。
1050
1050
  */
1051
- function installShutdownHandlers(
1052
- httpServer: Server,
1053
- serviceLifecycle: ReturnType<typeof createServiceLifecycleGuard>,
1054
- ): void {
1055
- process.on("SIGINT", () => {
1056
- console.log("\nShutting down...");
1057
- serviceLifecycle.beginShutdown("SIGINT");
1058
- wechatSignal.stopped = true;
1059
- stopChromeDevtoolsGuard();
1060
- httpServer.close();
1061
- process.exit(0);
1062
- });
1063
- process.on("SIGTERM", () => {
1064
- serviceLifecycle.beginShutdown("SIGTERM");
1065
- wechatSignal.stopped = true;
1066
- stopChromeDevtoolsGuard();
1067
- httpServer.close();
1068
- process.exit(0);
1069
- });
1070
- }
1051
+ function installShutdownHandlers(
1052
+ httpServer: Server,
1053
+ serviceLifecycle: ReturnType<typeof createServiceLifecycleGuard>,
1054
+ ): void {
1055
+ process.on("SIGINT", () => {
1056
+ console.log("\nShutting down...");
1057
+ serviceLifecycle.beginShutdown("SIGINT");
1058
+ wechatSignal.stopped = true;
1059
+ stopChromeDevtoolsGuard();
1060
+ httpServer.close();
1061
+ process.exit(0);
1062
+ });
1063
+ process.on("SIGTERM", () => {
1064
+ serviceLifecycle.beginShutdown("SIGTERM");
1065
+ wechatSignal.stopped = true;
1066
+ stopChromeDevtoolsGuard();
1067
+ httpServer.close();
1068
+ process.exit(0);
1069
+ });
1070
+ }
1071
1071
 
1072
1072
  main().catch((err: Error) => {
1073
1073
  appendStartupTrace("main: catch fatal", { message: err.message, stack: err.stack?.slice(0, 800) });