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.
- package/.agents/skills/create-chatccc-feishu-app/SKILL.md +85 -85
- package/.claude/skills/create-chatccc-feishu-app/SKILL.md +85 -85
- package/.cursor/skills/create-chatccc-feishu-app/SKILL.md +85 -85
- package/README.md +90 -90
- package/package.json +1 -1
- package/src/__tests__/agent-activity.test.ts +76 -76
- package/src/__tests__/builtin-chat-session.test.ts +350 -350
- package/src/__tests__/builtin-config.test.ts +26 -26
- package/src/__tests__/builtin-context.test.ts +163 -163
- package/src/__tests__/builtin-file-tools.test.ts +275 -275
- package/src/__tests__/builtin-permissions.test.ts +211 -211
- package/src/__tests__/builtin-session-select.test.ts +116 -116
- package/src/__tests__/builtin-skills.test.ts +252 -141
- package/src/__tests__/builtin-web-tools.test.ts +220 -0
- package/src/__tests__/card-action-routing.test.ts +18 -18
- package/src/__tests__/ccc-adapter.test.ts +136 -136
- package/src/__tests__/claude-adapter.test.ts +614 -614
- package/src/__tests__/codex-adapter.test.ts +58 -58
- package/src/__tests__/codex-raw-stream-log.test.ts +170 -170
- package/src/__tests__/cursor-adapter.test.ts +268 -268
- package/src/__tests__/feishu-avatar.test.ts +164 -164
- package/src/__tests__/feishu-message-ingress.test.ts +138 -138
- package/src/__tests__/package-files.test.ts +24 -24
- package/src/__tests__/progress-reducer.test.ts +110 -110
- package/src/__tests__/response-stall.test.ts +49 -49
- package/src/__tests__/sim-platform.test.ts +16 -16
- package/src/__tests__/startup-lifecycle.test.ts +231 -231
- package/src/__tests__/stop-session.test.ts +34 -34
- package/src/__tests__/terminal-renderer.test.ts +247 -247
- package/src/__tests__/update-command-guard.test.ts +144 -144
- package/src/__tests__/web-ui.test.ts +326 -326
- package/src/adapters/adapter-interface.ts +18 -18
- package/src/adapters/ccc-adapter.ts +131 -131
- package/src/adapters/claude-adapter.ts +620 -620
- package/src/adapters/codex-adapter.ts +426 -426
- package/src/adapters/cursor-adapter.ts +681 -681
- package/src/agent-activity.ts +170 -170
- package/src/agent-delegate-task.ts +91 -91
- package/src/builtin/cli.ts +61 -2
- package/src/builtin/config.ts +84 -84
- package/src/builtin/context.ts +323 -323
- package/src/builtin/file-log.ts +38 -38
- package/src/builtin/file-tools.ts +37 -0
- package/src/builtin/index.ts +44 -24
- package/src/builtin/proc-tree-kill.ts +61 -61
- package/src/builtin/progress/cards-helpers.ts +76 -76
- package/src/builtin/progress/reducer.ts +108 -108
- package/src/builtin/progress/terminal-renderer.ts +294 -294
- package/src/builtin/progress/view.ts +77 -77
- package/src/builtin/raw-stream-log.ts +124 -124
- package/src/builtin/session-select.ts +48 -48
- package/src/builtin/skills.ts +190 -108
- package/src/builtin/web-tools.ts +313 -0
- package/src/card-action-routing.ts +14 -14
- package/src/feishu-api.ts +193 -193
- package/src/feishu-message-ingress.ts +195 -195
- package/src/index.ts +306 -306
- package/src/orchestrator.ts +2388 -2388
- package/src/platform-adapter.ts +6 -6
- package/src/progress/reducer.ts +108 -108
- package/src/progress/terminal-renderer.ts +294 -294
- package/src/progress/view.ts +77 -77
- package/src/response-stall.ts +28 -28
- package/src/session-chat-binding.ts +82 -82
- package/src/startup-lifecycle.ts +250 -250
- package/src/stream-state.ts +18 -18
- 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) });
|