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