chatccc 0.2.251 → 0.2.253
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/README.md +1 -1
- package/bin/cccagent.mjs +12 -3
- package/bin/chatccc.mjs +15 -6
- package/deepccc-agent/package.json +1 -1
- package/dist/deepccc-agent/src/cli.js +634 -0
- package/dist/deepccc-agent/src/config.js +76 -0
- package/dist/deepccc-agent/src/context.js +348 -0
- package/dist/deepccc-agent/src/file-log.js +34 -0
- package/dist/deepccc-agent/src/file-tools.js +1208 -0
- package/dist/deepccc-agent/src/index.js +571 -0
- package/dist/deepccc-agent/src/permissions.js +170 -0
- package/dist/deepccc-agent/src/privacy.js +124 -0
- package/dist/deepccc-agent/src/proc-tree-kill.js +60 -0
- package/dist/deepccc-agent/src/progress/cards-helpers.js +70 -0
- package/dist/deepccc-agent/src/progress/reducer.js +102 -0
- package/dist/deepccc-agent/src/progress/terminal-renderer.js +264 -0
- package/dist/deepccc-agent/src/progress/view.js +30 -0
- package/dist/deepccc-agent/src/raw-stream-log.js +106 -0
- package/dist/deepccc-agent/src/session-search.js +276 -0
- package/dist/deepccc-agent/src/session-select.js +23 -0
- package/dist/deepccc-agent/src/sigint.js +26 -0
- package/dist/deepccc-agent/src/skills.js +178 -0
- package/dist/deepccc-agent/src/web-tools.js +246 -0
- package/dist/src/adapters/adapter-interface.js +19 -0
- package/dist/src/adapters/ccc-adapter.js +112 -0
- package/dist/src/adapters/claude-adapter.js +497 -0
- package/dist/src/adapters/claude-session-meta-store.js +92 -0
- package/dist/src/adapters/codex-adapter.js +279 -0
- package/dist/src/adapters/codex-session-meta-store.js +94 -0
- package/dist/src/adapters/cursor-adapter.js +491 -0
- package/dist/src/adapters/cursor-session-meta-store.js +116 -0
- package/dist/src/adapters/jsonl-stream.js +104 -0
- package/{src/adapters/proc-tree-kill.ts → dist/src/adapters/proc-tree-kill.js} +94 -97
- package/dist/src/adapters/raw-stream-log.js +106 -0
- package/dist/src/adapters/resource-monitor.js +113 -0
- package/dist/src/agent-activity.js +133 -0
- package/dist/src/agent-delegate-task-rpc.js +129 -0
- package/dist/src/agent-delegate-task.js +48 -0
- package/dist/src/agent-file-rpc.js +152 -0
- package/dist/src/agent-image-rpc.js +148 -0
- package/dist/src/agent-platform-routing.js +13 -0
- package/dist/src/agent-reload-config-rpc.js +23 -0
- package/dist/src/agent-rpc-body.js +87 -0
- package/dist/src/agent-stop-stuck.js +110 -0
- package/dist/src/card-action-routing.js +7 -0
- package/dist/src/card-plain-text.js +101 -0
- package/dist/src/cardkit.js +158 -0
- package/dist/src/cards.js +573 -0
- package/dist/src/chatgpt-subscription-rpc.js +18 -0
- package/dist/src/chatgpt-subscription.js +199 -0
- package/dist/src/chrome-devtools-guard.js +238 -0
- package/dist/src/claude-sdk-installer.js +249 -0
- package/dist/src/codex-reset-actions.js +143 -0
- package/dist/src/config-utils.js +149 -0
- package/dist/src/config.js +804 -0
- package/dist/src/cursor-usage.js +77 -0
- package/dist/src/exit-banner.js +28 -0
- package/dist/src/feishu-api.js +1404 -0
- package/dist/src/feishu-message-ingress.js +137 -0
- package/dist/src/feishu-platform.js +97 -0
- package/dist/src/format-message.js +252 -0
- package/dist/src/git-command.js +155 -0
- package/dist/src/im-skills.js +121 -0
- package/dist/src/index.js +833 -0
- package/dist/src/litellm-proxy.js +300 -0
- package/dist/src/orchestrator.js +2078 -0
- package/dist/src/package-root.js +26 -0
- package/dist/src/platform-adapter.js +7 -0
- package/dist/src/platform-startup.js +6 -0
- package/dist/src/privacy.js +100 -0
- package/dist/src/progress/reducer.js +102 -0
- package/dist/src/progress/terminal-renderer.js +264 -0
- package/dist/src/progress/view.js +30 -0
- package/dist/src/response-stall.js +14 -0
- package/dist/src/runtime-entry.js +13 -0
- package/dist/src/runtime-reload.js +19 -0
- package/dist/src/session-chat-binding.js +183 -0
- package/dist/src/session-name.js +7 -0
- package/dist/src/session.js +2144 -0
- package/dist/src/shared-prefix.js +16 -0
- package/dist/src/shared.js +493 -0
- package/dist/src/sim-agent.js +105 -0
- package/dist/src/sim-platform.js +142 -0
- package/dist/src/sim-store.js +231 -0
- package/dist/src/simplify.js +99 -0
- package/dist/src/startup-lifecycle.js +209 -0
- package/dist/src/stream-state.js +141 -0
- package/dist/src/terminal-error.js +100 -0
- package/dist/src/trace.js +50 -0
- package/dist/src/turn-cards.js +92 -0
- package/dist/src/update-command-guard.js +114 -0
- package/{src/web-ui.ts → dist/src/web-ui.js} +749 -823
- package/dist/src/wechat-platform.js +545 -0
- package/package.json +7 -5
- package/deepccc-agent/LICENSE +0 -201
- package/deepccc-agent/bin/deepccc.mjs +0 -26
- package/deepccc-agent/docs/cache-hit-rate-1.jpg +0 -0
- package/deepccc-agent/docs/cache-hit-rate-2.jpg +0 -0
- package/deepccc-agent/package-lock.json +0 -2027
- package/deepccc-agent/src/__tests__/chat-session.test.ts +0 -852
- package/deepccc-agent/src/__tests__/cli-json.test.ts +0 -49
- package/deepccc-agent/src/__tests__/config.test.ts +0 -34
- package/deepccc-agent/src/__tests__/context.test.ts +0 -341
- package/deepccc-agent/src/__tests__/file-tools.test.ts +0 -240
- package/deepccc-agent/src/__tests__/permissions.test.ts +0 -199
- package/deepccc-agent/src/__tests__/privacy.test.ts +0 -318
- package/deepccc-agent/src/__tests__/progress-reducer.test.ts +0 -121
- package/deepccc-agent/src/__tests__/session-search.test.ts +0 -262
- package/deepccc-agent/src/__tests__/session-select.test.ts +0 -116
- package/deepccc-agent/src/__tests__/sigint.test.ts +0 -56
- package/deepccc-agent/src/__tests__/skills.test.ts +0 -284
- package/deepccc-agent/src/__tests__/terminal-renderer.test.ts +0 -247
- package/deepccc-agent/src/__tests__/web-tools.test.ts +0 -220
- package/deepccc-agent/src/cli.ts +0 -682
- package/deepccc-agent/src/config.ts +0 -101
- package/deepccc-agent/src/context.ts +0 -465
- package/deepccc-agent/src/file-log.ts +0 -38
- package/deepccc-agent/src/file-tools.ts +0 -1493
- package/deepccc-agent/src/index.ts +0 -676
- package/deepccc-agent/src/permissions.ts +0 -226
- package/deepccc-agent/src/privacy.ts +0 -141
- package/deepccc-agent/src/proc-tree-kill.ts +0 -61
- package/deepccc-agent/src/progress/cards-helpers.ts +0 -76
- package/deepccc-agent/src/progress/reducer.ts +0 -113
- package/deepccc-agent/src/progress/terminal-renderer.ts +0 -294
- package/deepccc-agent/src/progress/view.ts +0 -77
- package/deepccc-agent/src/raw-stream-log.ts +0 -124
- package/deepccc-agent/src/session-search.ts +0 -370
- package/deepccc-agent/src/session-select.ts +0 -48
- package/deepccc-agent/src/sigint.ts +0 -50
- package/deepccc-agent/src/skills.ts +0 -205
- package/deepccc-agent/src/web-tools.ts +0 -313
- package/deepccc-agent/tsconfig.build.json +0 -13
- package/deepccc-agent/tsconfig.json +0 -13
- package/deepccc-agent/vitest.config.ts +0 -7
- package/src/__tests__/adapter-interface.test.ts +0 -152
- package/src/__tests__/agent-activity.test.ts +0 -86
- package/src/__tests__/agent-delegate-task-rpc.test.ts +0 -165
- package/src/__tests__/agent-image-rpc.test.ts +0 -34
- package/src/__tests__/agent-platform-routing.test.ts +0 -26
- package/src/__tests__/agent-reload-config-rpc.test.ts +0 -99
- package/src/__tests__/agent-rpc-body.test.ts +0 -42
- package/src/__tests__/builtin-chat-session.test.ts +0 -532
- package/src/__tests__/builtin-cli-json.test.ts +0 -39
- package/src/__tests__/builtin-config.test.ts +0 -26
- package/src/__tests__/builtin-context.test.ts +0 -319
- package/src/__tests__/builtin-file-tools.test.ts +0 -240
- package/src/__tests__/builtin-permissions.test.ts +0 -219
- package/src/__tests__/builtin-session-search.test.ts +0 -262
- package/src/__tests__/builtin-session-select.test.ts +0 -116
- package/src/__tests__/builtin-sigint.test.ts +0 -56
- package/src/__tests__/builtin-skills.test.ts +0 -284
- package/src/__tests__/builtin-web-tools.test.ts +0 -220
- package/src/__tests__/card-action-routing.test.ts +0 -18
- package/src/__tests__/card-plain-text.test.ts +0 -45
- package/src/__tests__/cardkit.test.ts +0 -60
- package/src/__tests__/cards.test.ts +0 -607
- package/src/__tests__/ccc-adapter.test.ts +0 -194
- package/src/__tests__/chatgpt-subscription-rpc.test.ts +0 -89
- package/src/__tests__/chatgpt-subscription.test.ts +0 -135
- package/src/__tests__/chrome-devtools-guard.test.ts +0 -165
- package/src/__tests__/claude-adapter.test.ts +0 -614
- package/src/__tests__/claude-raw-stream-log.test.ts +0 -96
- package/src/__tests__/claude-sdk-installer.test.ts +0 -285
- package/src/__tests__/codex-adapter.test.ts +0 -331
- package/src/__tests__/codex-raw-stream-log.test.ts +0 -170
- package/src/__tests__/codex-reset-actions.test.ts +0 -146
- package/src/__tests__/config-reload.test.ts +0 -284
- package/src/__tests__/config-sample.test.ts +0 -97
- package/src/__tests__/config-utils.test.ts +0 -40
- package/src/__tests__/config.test.ts +0 -395
- package/src/__tests__/crash-logging.test.ts +0 -360
- package/src/__tests__/cursor-adapter.test.ts +0 -890
- package/src/__tests__/cursor-session-meta-store.test.ts +0 -212
- package/src/__tests__/feishu-api.test.ts +0 -60
- package/src/__tests__/feishu-avatar.test.ts +0 -504
- package/src/__tests__/feishu-message-ingress.test.ts +0 -138
- package/src/__tests__/feishu-platform.test.ts +0 -75
- package/src/__tests__/fixtures/codex_simple_text.jsonl +0 -4
- package/src/__tests__/fixtures/codex_with_tool.jsonl +0 -6
- package/src/__tests__/fixtures/cursor_partial_only.jsonl +0 -5
- package/src/__tests__/fixtures/cursor_partial_with_final.jsonl +0 -13
- package/src/__tests__/fixtures/cursor_with_tool_call.jsonl +0 -12
- package/src/__tests__/format-message.test.ts +0 -316
- package/src/__tests__/git-command.test.ts +0 -288
- package/src/__tests__/im-skills.test.ts +0 -125
- package/src/__tests__/jsonl-stream.test.ts +0 -79
- package/src/__tests__/orchestrator.test.ts +0 -1268
- package/src/__tests__/package-files.test.ts +0 -24
- package/src/__tests__/platform-startup.test.ts +0 -19
- package/src/__tests__/privacy.test.ts +0 -198
- package/src/__tests__/proc-tree-kill.test.ts +0 -108
- package/src/__tests__/progress-reducer.test.ts +0 -121
- package/src/__tests__/raw-stream-log.test.ts +0 -106
- package/src/__tests__/response-stall.test.ts +0 -49
- package/src/__tests__/restart.test.ts +0 -232
- package/src/__tests__/session-ccc-config.test.ts +0 -66
- package/src/__tests__/session.test.ts +0 -3004
- package/src/__tests__/shared-prefix.test.ts +0 -36
- package/src/__tests__/sim-agent.test.ts +0 -174
- package/src/__tests__/sim-platform.test.ts +0 -93
- package/src/__tests__/sim-store.test.ts +0 -214
- package/src/__tests__/simplify.test.ts +0 -283
- package/src/__tests__/startup-lifecycle.test.ts +0 -231
- package/src/__tests__/stop-session.test.ts +0 -162
- package/src/__tests__/stream-state.test.ts +0 -164
- package/src/__tests__/terminal-error.test.ts +0 -54
- package/src/__tests__/terminal-renderer.test.ts +0 -247
- package/src/__tests__/update-command-guard.test.ts +0 -144
- package/src/__tests__/web-ui.test.ts +0 -438
- package/src/__tests__/wechat-platform.test.ts +0 -111
- package/src/adapters/adapter-interface.ts +0 -217
- package/src/adapters/ccc-adapter.ts +0 -150
- package/src/adapters/claude-adapter.ts +0 -673
- package/src/adapters/claude-session-meta-store.ts +0 -120
- package/src/adapters/codex-adapter.ts +0 -426
- package/src/adapters/codex-session-meta-store.ts +0 -131
- package/src/adapters/cursor-adapter.ts +0 -681
- package/src/adapters/cursor-session-meta-store.ts +0 -154
- package/src/adapters/jsonl-stream.ts +0 -157
- package/src/adapters/raw-stream-log.ts +0 -124
- package/src/adapters/resource-monitor.ts +0 -141
- package/src/agent-activity.ts +0 -175
- package/src/agent-delegate-task-rpc.ts +0 -153
- package/src/agent-delegate-task.ts +0 -91
- package/src/agent-file-rpc.ts +0 -172
- package/src/agent-image-rpc.ts +0 -168
- package/src/agent-platform-routing.ts +0 -28
- package/src/agent-reload-config-rpc.ts +0 -34
- package/src/agent-rpc-body.ts +0 -92
- package/src/agent-stop-stuck.ts +0 -129
- package/src/card-action-routing.ts +0 -14
- package/src/card-plain-text.ts +0 -108
- package/src/cardkit.ts +0 -179
- package/src/cards.ts +0 -684
- package/src/chatgpt-subscription-rpc.ts +0 -27
- package/src/chatgpt-subscription.ts +0 -299
- package/src/chrome-devtools-guard.ts +0 -318
- package/src/claude-sdk-installer.ts +0 -324
- package/src/codex-reset-actions.ts +0 -184
- package/src/config-utils.ts +0 -211
- package/src/config.ts +0 -1063
- package/src/cursor-usage.ts +0 -128
- package/src/exit-banner.ts +0 -33
- package/src/feishu-api.ts +0 -1616
- package/src/feishu-message-ingress.ts +0 -195
- package/src/feishu-platform.ts +0 -159
- package/src/format-message.ts +0 -293
- package/src/git-command.ts +0 -202
- package/src/im-skills.ts +0 -149
- package/src/index.ts +0 -1089
- package/src/litellm-proxy.ts +0 -374
- package/src/orchestrator.ts +0 -2543
- package/src/platform-adapter.ts +0 -70
- package/src/platform-startup.ts +0 -16
- package/src/privacy.ts +0 -118
- package/src/progress/reducer.ts +0 -113
- package/src/progress/terminal-renderer.ts +0 -294
- package/src/progress/view.ts +0 -77
- package/src/response-stall.ts +0 -28
- package/src/runtime-reload.ts +0 -34
- package/src/session-chat-binding.ts +0 -292
- package/src/session-name.ts +0 -8
- package/src/session.ts +0 -2659
- package/src/shared-prefix.ts +0 -29
- package/src/shared.ts +0 -552
- package/src/sim-agent.ts +0 -167
- package/src/sim-platform.ts +0 -177
- package/src/sim-store.ts +0 -317
- package/src/simplify.ts +0 -120
- package/src/startup-lifecycle.ts +0 -250
- package/src/stream-state.ts +0 -177
- package/src/terminal-error.ts +0 -129
- package/src/trace.ts +0 -51
- package/src/turn-cards.ts +0 -118
- package/src/update-command-guard.ts +0 -165
- package/src/wechat-platform.ts +0 -680
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* sim-platform.ts — SimulatedPlatform: 零飞书依赖的本地模拟实现
|
|
3
|
+
*
|
|
4
|
+
* 所有飞书 API 调用都由本地 SimStore 替代:
|
|
5
|
+
* - 消息通过 SimStore 写入内存 + JSONL + 事件推送
|
|
6
|
+
* - 群聊/账户信息由 SimStore 统一管理
|
|
7
|
+
* - createGroupChat 生成 "sim_<uuid>" 格式的 chat_id
|
|
8
|
+
*
|
|
9
|
+
* 纯函数(extractSessionInfo / formatDelayNotice / reportPermissionResults)
|
|
10
|
+
* 直接从 feishu-api.ts re-export,不重新实现。
|
|
11
|
+
*/
|
|
12
|
+
import { join } from "node:path";
|
|
13
|
+
import { extractSessionInfo as realExtractSessionInfo, extractSessionId as realExtractSessionId, formatDelayNotice as realFormatDelayNotice, reportPermissionResults as realReportPermissionResults, } from "./feishu-api.js";
|
|
14
|
+
import { simStore } from "./sim-store.js";
|
|
15
|
+
// ---------------------------------------------------------------------------
|
|
16
|
+
// 持久化路径
|
|
17
|
+
// ---------------------------------------------------------------------------
|
|
18
|
+
import { homedir } from "node:os";
|
|
19
|
+
const SIM_DIR = join(homedir(), ".chatccc", "sim");
|
|
20
|
+
// ---------------------------------------------------------------------------
|
|
21
|
+
// 工具函数
|
|
22
|
+
// ---------------------------------------------------------------------------
|
|
23
|
+
function ts() {
|
|
24
|
+
return new Date().toISOString().replace("T", " ").slice(0, 19);
|
|
25
|
+
}
|
|
26
|
+
// ---------------------------------------------------------------------------
|
|
27
|
+
// SimulatedPlatform
|
|
28
|
+
// ---------------------------------------------------------------------------
|
|
29
|
+
export const SimulatedPlatform = {
|
|
30
|
+
// ---- 认证 ----
|
|
31
|
+
async getTenantAccessToken() {
|
|
32
|
+
return "sim_token";
|
|
33
|
+
},
|
|
34
|
+
// ---- 消息发送:委托给 simStore ----
|
|
35
|
+
async sendTextReply(_token, chatId, text) {
|
|
36
|
+
console.log(`[${ts()}] [SIM:SEND] text → ${chatId}: ${text.slice(0, 80)}`);
|
|
37
|
+
simStore.sendReply(chatId, "text", text);
|
|
38
|
+
return true;
|
|
39
|
+
},
|
|
40
|
+
async sendCardReply(_token, chatId, title, content, _template) {
|
|
41
|
+
console.log(`[${ts()}] [SIM:SEND] card → ${chatId}: [${title}]`);
|
|
42
|
+
const text = `**[${title}]**\n${content}`;
|
|
43
|
+
simStore.sendReply(chatId, "card", text);
|
|
44
|
+
return true;
|
|
45
|
+
},
|
|
46
|
+
async sendImageReply(_token, chatId, imagePath) {
|
|
47
|
+
console.log(`[${ts()}] [SIM:SEND] image → ${chatId}: ${imagePath}`);
|
|
48
|
+
simStore.sendReply(chatId, "image", imagePath);
|
|
49
|
+
return true;
|
|
50
|
+
},
|
|
51
|
+
async sendFileReply(_token, chatId, filePath) {
|
|
52
|
+
console.log(`[${ts()}] [SIM:SEND] file → ${chatId}: ${filePath}`);
|
|
53
|
+
simStore.sendReply(chatId, "file", filePath);
|
|
54
|
+
return true;
|
|
55
|
+
},
|
|
56
|
+
async sendRawCard(_token, chatId, cardJson) {
|
|
57
|
+
console.log(`[${ts()}] [SIM:SEND] raw_card → ${chatId}: ${cardJson.slice(0, 80)}...`);
|
|
58
|
+
simStore.sendReply(chatId, "raw_card", cardJson.slice(0, 500));
|
|
59
|
+
return true;
|
|
60
|
+
},
|
|
61
|
+
async sendPostMessage(_token, chatId, title, postContent) {
|
|
62
|
+
console.log(`[${ts()}] [SIM:SEND] post → ${chatId}: title="${title}" paragraphs=${postContent.length}`);
|
|
63
|
+
simStore.sendReply(chatId, "post", `[${title}] ${postContent.length} paragraphs`);
|
|
64
|
+
return true;
|
|
65
|
+
},
|
|
66
|
+
// ---- 消息管理 ----
|
|
67
|
+
async addReaction(_token, _messageId, _emojiType) {
|
|
68
|
+
// 模拟模式不需要表情回应
|
|
69
|
+
},
|
|
70
|
+
async recallMessage(_token, _messageId) {
|
|
71
|
+
return true;
|
|
72
|
+
},
|
|
73
|
+
async updateCardMessage(_token, _messageId, content) {
|
|
74
|
+
// 模拟模式:记录卡片更新到控制台(无 chatId,不写入消息历史)
|
|
75
|
+
console.log(`[${ts()}] [SIM:CARD] update: msgId=${_messageId} preview=${content.slice(0, 80)}`);
|
|
76
|
+
return true;
|
|
77
|
+
},
|
|
78
|
+
// ---- 群聊管理 ----
|
|
79
|
+
async createGroupChat(_token, name, userIds) {
|
|
80
|
+
const chat = simStore.createGroupChat(name, userIds);
|
|
81
|
+
console.log(`[${ts()}] [SIM:CHAT] created: ${chat.id} name="${name}" members=${userIds.length}`);
|
|
82
|
+
return chat.id;
|
|
83
|
+
},
|
|
84
|
+
async updateChatInfo(_token, chatId, name, description) {
|
|
85
|
+
simStore.updateChatInfo(chatId, name, description);
|
|
86
|
+
console.log(`[${ts()}] [SIM:CHAT] updated: ${chatId} name="${name}"`);
|
|
87
|
+
},
|
|
88
|
+
async getChatInfo(_token, chatId) {
|
|
89
|
+
const chat = simStore.getChat(chatId);
|
|
90
|
+
if (!chat)
|
|
91
|
+
throw new Error(`[99999] chat not found: ${chatId}`);
|
|
92
|
+
return { name: chat.name, description: chat.description };
|
|
93
|
+
},
|
|
94
|
+
async disbandChat(_token, chatId) {
|
|
95
|
+
simStore.disbandChat(chatId);
|
|
96
|
+
console.log(`[${ts()}] [SIM:CHAT] disbanded: ${chatId}`);
|
|
97
|
+
},
|
|
98
|
+
// ---- 头像 ----
|
|
99
|
+
async setChatAvatar(_token, _chatId, _tool, _status) {
|
|
100
|
+
// 模拟模式不需要头像
|
|
101
|
+
},
|
|
102
|
+
async getCodexUsageSummary() {
|
|
103
|
+
return {
|
|
104
|
+
fiveHour: { usedPercent: 0, remainingPercent: 100, resetAtEpochSeconds: null, resetAfterSeconds: null },
|
|
105
|
+
weekly: { usedPercent: 0, remainingPercent: 100, resetAtEpochSeconds: null, resetAfterSeconds: null },
|
|
106
|
+
rateLimitResetCreditsAvailable: null,
|
|
107
|
+
rateLimitResetCredits: null,
|
|
108
|
+
};
|
|
109
|
+
},
|
|
110
|
+
async consumeCodexRateLimitResetCredit(_redeemRequestId) {
|
|
111
|
+
return { code: "no_credit", windowsReset: 0 };
|
|
112
|
+
},
|
|
113
|
+
// ---- 图片下载 ----
|
|
114
|
+
async getOrDownloadImage(_token, _messageId, fileKey) {
|
|
115
|
+
return join(SIM_DIR, "images", fileKey);
|
|
116
|
+
},
|
|
117
|
+
// ---- 权限验证 ----
|
|
118
|
+
async verifyAllPermissions(_token) {
|
|
119
|
+
return [
|
|
120
|
+
{ scope: "im:chat", description: "模拟", ok: true, detail: "模拟模式跳过" },
|
|
121
|
+
{ scope: "im:message:send_as_bot", description: "模拟", ok: true, detail: "模拟模式跳过" },
|
|
122
|
+
{ scope: "im:message:reaction", description: "模拟", ok: true, detail: "模拟模式跳过" },
|
|
123
|
+
{ scope: "im:message", description: "模拟", ok: true, detail: "模拟模式跳过" },
|
|
124
|
+
{ scope: "cardkit:card", description: "模拟", ok: true, detail: "模拟模式跳过" },
|
|
125
|
+
];
|
|
126
|
+
},
|
|
127
|
+
// ---- 纯函数:直接从真实实现 re-export ----
|
|
128
|
+
extractSessionInfo: realExtractSessionInfo,
|
|
129
|
+
extractSessionId: realExtractSessionId,
|
|
130
|
+
formatDelayNotice: realFormatDelayNotice,
|
|
131
|
+
reportPermissionResults: realReportPermissionResults,
|
|
132
|
+
// ---- 其他 ----
|
|
133
|
+
async sendRestartCard(_token) {
|
|
134
|
+
// 模拟模式不需要重启通知
|
|
135
|
+
},
|
|
136
|
+
async getMergeForwardMessages(_token, _messageId) {
|
|
137
|
+
// 模拟模式没有真实 API,返回空数组让调用方降级到 preview
|
|
138
|
+
return [];
|
|
139
|
+
},
|
|
140
|
+
};
|
|
141
|
+
/** 模拟模式下的默认 chat_id(重新导出以保持向后兼容) */
|
|
142
|
+
export { SIM_DEFAULT_CHAT_ID } from "./sim-store.js";
|
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* sim-store.ts — 模拟飞书环境的状态管理核心
|
|
3
|
+
*
|
|
4
|
+
* SimStore 是一个单例的 EventEmitter,管理所有模拟状态:
|
|
5
|
+
* - 账户(bot + 多个用户)
|
|
6
|
+
* - 会话(群聊 + 私聊)
|
|
7
|
+
* - 消息历史(内存 + JSONL 持久化)
|
|
8
|
+
*
|
|
9
|
+
* 同时暴露 setMessageHandler / dispatchMessage,让 SimAgent 进程内触发
|
|
10
|
+
* handleCommand 而无需通过 HTTP。
|
|
11
|
+
*/
|
|
12
|
+
import { randomUUID } from "node:crypto";
|
|
13
|
+
import { appendFile, mkdir } from "node:fs/promises";
|
|
14
|
+
import { homedir } from "node:os";
|
|
15
|
+
import { join } from "node:path";
|
|
16
|
+
import { EventEmitter } from "node:events";
|
|
17
|
+
// ---------------------------------------------------------------------------
|
|
18
|
+
// 持久化路径
|
|
19
|
+
// ---------------------------------------------------------------------------
|
|
20
|
+
const SIM_DIR = join(homedir(), ".chatccc", "sim");
|
|
21
|
+
const MESSAGES_FILE = join(SIM_DIR, "messages.jsonl");
|
|
22
|
+
async function ensureDir() {
|
|
23
|
+
await mkdir(SIM_DIR, { recursive: true });
|
|
24
|
+
}
|
|
25
|
+
async function appendJsonl(line) {
|
|
26
|
+
await ensureDir();
|
|
27
|
+
await appendFile(MESSAGES_FILE, JSON.stringify(line) + "\n", "utf-8");
|
|
28
|
+
}
|
|
29
|
+
// ---------------------------------------------------------------------------
|
|
30
|
+
// SimStore
|
|
31
|
+
// ---------------------------------------------------------------------------
|
|
32
|
+
export class SimStore extends EventEmitter {
|
|
33
|
+
accounts = new Map();
|
|
34
|
+
chats = new Map();
|
|
35
|
+
// ---- 初始化 ----
|
|
36
|
+
constructor() {
|
|
37
|
+
super();
|
|
38
|
+
this._initDefaults();
|
|
39
|
+
}
|
|
40
|
+
_initDefaults() {
|
|
41
|
+
// 注册 bot 账户
|
|
42
|
+
this.accounts.set("bot", { id: "bot", kind: "bot", name: "ChatCCC" });
|
|
43
|
+
// 注册默认用户
|
|
44
|
+
this.accounts.set("sim_user_001", { id: "sim_user_001", kind: "user", name: "Developer" });
|
|
45
|
+
// 创建默认群聊
|
|
46
|
+
const defaultChatId = "sim_default";
|
|
47
|
+
this.chats.set(defaultChatId, {
|
|
48
|
+
id: defaultChatId,
|
|
49
|
+
type: "group",
|
|
50
|
+
name: "默认模拟会话",
|
|
51
|
+
description: "",
|
|
52
|
+
memberIds: ["bot", "sim_user_001"],
|
|
53
|
+
messages: [],
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
// ---- 账户管理 ----
|
|
57
|
+
registerAccount(account) {
|
|
58
|
+
if (this.accounts.has(account.id)) {
|
|
59
|
+
throw new Error(`Account "${account.id}" already exists`);
|
|
60
|
+
}
|
|
61
|
+
this.accounts.set(account.id, account);
|
|
62
|
+
}
|
|
63
|
+
getAccount(id) {
|
|
64
|
+
return this.accounts.get(id);
|
|
65
|
+
}
|
|
66
|
+
// ---- 会话管理 ----
|
|
67
|
+
createGroupChat(name, memberIds) {
|
|
68
|
+
const chatId = `sim_${randomUUID().slice(0, 8)}`;
|
|
69
|
+
// bot 始终是成员
|
|
70
|
+
const allMembers = ["bot", ...memberIds.filter((id) => id !== "bot")];
|
|
71
|
+
const chat = {
|
|
72
|
+
id: chatId,
|
|
73
|
+
type: "group",
|
|
74
|
+
name,
|
|
75
|
+
description: "",
|
|
76
|
+
memberIds: allMembers,
|
|
77
|
+
messages: [],
|
|
78
|
+
};
|
|
79
|
+
this.chats.set(chatId, chat);
|
|
80
|
+
// 通知被拉入群的用户
|
|
81
|
+
for (const uid of memberIds) {
|
|
82
|
+
this.emit("member_added", { chatId, userId: uid });
|
|
83
|
+
}
|
|
84
|
+
this.emit("chat_created", { chat });
|
|
85
|
+
return chat;
|
|
86
|
+
}
|
|
87
|
+
createP2pChat(userId) {
|
|
88
|
+
// p2p 命名规则:p2p_<userId>_bot
|
|
89
|
+
const chatId = `p2p_${userId}_bot`;
|
|
90
|
+
const existing = this.chats.get(chatId);
|
|
91
|
+
if (existing)
|
|
92
|
+
return existing;
|
|
93
|
+
const user = this.accounts.get(userId);
|
|
94
|
+
const chat = {
|
|
95
|
+
id: chatId,
|
|
96
|
+
type: "p2p",
|
|
97
|
+
name: user ? `${user.name} 的私聊` : `私聊`,
|
|
98
|
+
description: "",
|
|
99
|
+
memberIds: ["bot", userId],
|
|
100
|
+
messages: [],
|
|
101
|
+
};
|
|
102
|
+
this.chats.set(chatId, chat);
|
|
103
|
+
this.emit("chat_created", { chat });
|
|
104
|
+
return chat;
|
|
105
|
+
}
|
|
106
|
+
getChat(chatId) {
|
|
107
|
+
return this.chats.get(chatId);
|
|
108
|
+
}
|
|
109
|
+
getChatsForUser(userId) {
|
|
110
|
+
return [...this.chats.values()].filter((c) => c.memberIds.includes(userId));
|
|
111
|
+
}
|
|
112
|
+
addMember(chatId, userId) {
|
|
113
|
+
const chat = this.chats.get(chatId);
|
|
114
|
+
if (!chat)
|
|
115
|
+
throw new Error(`Chat not found: ${chatId}`);
|
|
116
|
+
if (!chat.memberIds.includes(userId)) {
|
|
117
|
+
chat.memberIds.push(userId);
|
|
118
|
+
this.emit("member_added", { chatId, userId });
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
updateChatInfo(chatId, name, description) {
|
|
122
|
+
const chat = this.chats.get(chatId);
|
|
123
|
+
if (chat) {
|
|
124
|
+
chat.name = name;
|
|
125
|
+
chat.description = description;
|
|
126
|
+
}
|
|
127
|
+
else {
|
|
128
|
+
// 模拟模式下机器人可能尝试更新不存在的群(比如从旧状态恢复),兼容处理
|
|
129
|
+
this.chats.set(chatId, {
|
|
130
|
+
id: chatId,
|
|
131
|
+
type: "group",
|
|
132
|
+
name,
|
|
133
|
+
description,
|
|
134
|
+
memberIds: ["bot", "sim_user_001"],
|
|
135
|
+
messages: [],
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
disbandChat(chatId) {
|
|
140
|
+
this.chats.delete(chatId);
|
|
141
|
+
}
|
|
142
|
+
// ---- 消息管理 ----
|
|
143
|
+
/** 任何参与者发送消息 */
|
|
144
|
+
recordMessage(chatId, senderId, type, content) {
|
|
145
|
+
const chat = this.chats.get(chatId);
|
|
146
|
+
if (!chat)
|
|
147
|
+
throw new Error(`Chat not found: ${chatId}`);
|
|
148
|
+
const msg = {
|
|
149
|
+
id: randomUUID(),
|
|
150
|
+
chatId,
|
|
151
|
+
senderId,
|
|
152
|
+
type,
|
|
153
|
+
content,
|
|
154
|
+
timestamp: Date.now(),
|
|
155
|
+
};
|
|
156
|
+
chat.messages.push(msg);
|
|
157
|
+
// 写 JSONL
|
|
158
|
+
appendJsonl({
|
|
159
|
+
direction: senderId === "bot" ? "send" : "recv",
|
|
160
|
+
chat_id: chatId,
|
|
161
|
+
sender_id: senderId,
|
|
162
|
+
msg_type: type,
|
|
163
|
+
content,
|
|
164
|
+
timestamp: msg.timestamp,
|
|
165
|
+
}).catch(() => { });
|
|
166
|
+
// 通知所有订阅者
|
|
167
|
+
this.emit("message", { chatId, message: msg });
|
|
168
|
+
return msg;
|
|
169
|
+
}
|
|
170
|
+
/** bot 发送回复——即使 chat 不存在也记录(兼容未知 chat) */
|
|
171
|
+
sendReply(chatId, type, content) {
|
|
172
|
+
const msg = {
|
|
173
|
+
id: randomUUID(),
|
|
174
|
+
chatId,
|
|
175
|
+
senderId: "bot",
|
|
176
|
+
type,
|
|
177
|
+
content,
|
|
178
|
+
timestamp: Date.now(),
|
|
179
|
+
};
|
|
180
|
+
// 如果 chat 存在,追加到消息历史
|
|
181
|
+
const chat = this.chats.get(chatId);
|
|
182
|
+
if (chat) {
|
|
183
|
+
chat.messages.push(msg);
|
|
184
|
+
}
|
|
185
|
+
// 写 JSONL
|
|
186
|
+
appendJsonl({
|
|
187
|
+
direction: "send",
|
|
188
|
+
chat_id: chatId,
|
|
189
|
+
msg_type: type,
|
|
190
|
+
content,
|
|
191
|
+
timestamp: msg.timestamp,
|
|
192
|
+
}).catch(() => { });
|
|
193
|
+
// 通知订阅者
|
|
194
|
+
this.emit("message", { chatId, message: msg });
|
|
195
|
+
return msg;
|
|
196
|
+
}
|
|
197
|
+
/** 获取指定会话的消息 */
|
|
198
|
+
getMessages(chatId, requesterId) {
|
|
199
|
+
const chat = this.chats.get(chatId);
|
|
200
|
+
if (!chat)
|
|
201
|
+
return [];
|
|
202
|
+
// 只有成员能看消息
|
|
203
|
+
if (!chat.memberIds.includes(requesterId))
|
|
204
|
+
return [];
|
|
205
|
+
return chat.messages;
|
|
206
|
+
}
|
|
207
|
+
/** 重置所有状态(测试用) */
|
|
208
|
+
reset() {
|
|
209
|
+
this.accounts.clear();
|
|
210
|
+
this.chats.clear();
|
|
211
|
+
this.removeAllListeners();
|
|
212
|
+
this._initDefaults();
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
// ---------------------------------------------------------------------------
|
|
216
|
+
// 单例 + 消息分发
|
|
217
|
+
// ---------------------------------------------------------------------------
|
|
218
|
+
export const simStore = new SimStore();
|
|
219
|
+
export const SIM_DEFAULT_CHAT_ID = "sim_default";
|
|
220
|
+
let _messageHandler = null;
|
|
221
|
+
/** 注册 handleCommand 回调(由 index.ts 在模拟模式启动时调用) */
|
|
222
|
+
export function setMessageHandler(handler) {
|
|
223
|
+
_messageHandler = handler;
|
|
224
|
+
}
|
|
225
|
+
/** 模拟用户发送消息 → 记录消息 + 触发 bot 处理 */
|
|
226
|
+
export async function dispatchMessage(text, chatId, openId, chatType) {
|
|
227
|
+
if (!_messageHandler)
|
|
228
|
+
throw new Error("Message handler not registered — is simulate mode running?");
|
|
229
|
+
simStore.recordMessage(chatId, openId, "text", text);
|
|
230
|
+
await _messageHandler(text, chatId, openId, Date.now(), chatType);
|
|
231
|
+
}
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
2
|
+
import { resolve as resolvePath } from "node:path";
|
|
3
|
+
import { PROJECT_ROOT } from "./config.js";
|
|
4
|
+
let config = null;
|
|
5
|
+
let loaded = false;
|
|
6
|
+
function loadConfig() {
|
|
7
|
+
const filePath = resolvePath(PROJECT_ROOT, "simplify.json");
|
|
8
|
+
if (!existsSync(filePath))
|
|
9
|
+
return {};
|
|
10
|
+
try {
|
|
11
|
+
const raw = readFileSync(filePath, "utf-8");
|
|
12
|
+
const parsed = JSON.parse(raw);
|
|
13
|
+
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
|
|
14
|
+
console.error("[SIMPLIFY] simplify.json 格式错误:应为对象");
|
|
15
|
+
return {};
|
|
16
|
+
}
|
|
17
|
+
return parsed;
|
|
18
|
+
}
|
|
19
|
+
catch (err) {
|
|
20
|
+
console.error(`[SIMPLIFY] 读取 simplify.json 失败: ${err.message}`);
|
|
21
|
+
return {};
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
function getConfig() {
|
|
25
|
+
if (!loaded) {
|
|
26
|
+
config = loadConfig();
|
|
27
|
+
loaded = true;
|
|
28
|
+
}
|
|
29
|
+
return config;
|
|
30
|
+
}
|
|
31
|
+
/** 热重载 */
|
|
32
|
+
export function reloadSimplifyConfig() {
|
|
33
|
+
loaded = false;
|
|
34
|
+
config = null;
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* 对模板字符串中的 {field} 占位符做替换。
|
|
38
|
+
* fields 为可用字段映射,extra 是额外的上下文(如 tool_use_id 的 id)。
|
|
39
|
+
*/
|
|
40
|
+
function resolveTemplate(template, fields, extra) {
|
|
41
|
+
let result = template;
|
|
42
|
+
if (extra) {
|
|
43
|
+
for (const [k, v] of Object.entries(extra)) {
|
|
44
|
+
result = result.split(`{${k}}`).join(v);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
for (const [k, v] of Object.entries(fields)) {
|
|
48
|
+
const strVal = typeof v === "string" ? v : JSON.stringify(v);
|
|
49
|
+
result = result.split(`{${k}}`).join(strVal);
|
|
50
|
+
}
|
|
51
|
+
return result;
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* 简化 tool_use 展示。
|
|
55
|
+
* 返回 null 表示无规则,调用方应回退到默认格式化。
|
|
56
|
+
*/
|
|
57
|
+
export function simplifyToolUse(name, input) {
|
|
58
|
+
const cfg = getConfig();
|
|
59
|
+
const rules = cfg.tool_use;
|
|
60
|
+
if (!rules)
|
|
61
|
+
return null;
|
|
62
|
+
const rule = rules[name];
|
|
63
|
+
if (!rule)
|
|
64
|
+
return null;
|
|
65
|
+
const fields = typeof input === "object" && input !== null
|
|
66
|
+
? input
|
|
67
|
+
: {};
|
|
68
|
+
let result = resolveTemplate(rule.template, fields);
|
|
69
|
+
if (result.length > rule.maxLength) {
|
|
70
|
+
result = result.slice(0, rule.maxLength) + "...";
|
|
71
|
+
}
|
|
72
|
+
return result;
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* 简化 tool_result 展示。
|
|
76
|
+
* 返回 null 表示无规则,调用方应回退到默认格式化。
|
|
77
|
+
* toolCallInput 为对应的 tool_use 输入(可选),用于在 result 模板中引用输入字段。
|
|
78
|
+
*/
|
|
79
|
+
export function simplifyToolResult(name, toolUseId, isError, toolCallInput) {
|
|
80
|
+
const cfg = getConfig();
|
|
81
|
+
const rules = cfg.tool_result;
|
|
82
|
+
if (!rules)
|
|
83
|
+
return null;
|
|
84
|
+
const rule = rules[name];
|
|
85
|
+
if (!rule)
|
|
86
|
+
return null;
|
|
87
|
+
const id = toolUseId.slice(-6);
|
|
88
|
+
const extra = { id };
|
|
89
|
+
const fields = toolCallInput && typeof toolCallInput === "object"
|
|
90
|
+
? toolCallInput
|
|
91
|
+
: {};
|
|
92
|
+
let result = resolveTemplate(rule.template, fields, extra);
|
|
93
|
+
if (isError)
|
|
94
|
+
result = "❌ " + result;
|
|
95
|
+
if (result.length > rule.maxLength) {
|
|
96
|
+
result = result.slice(0, rule.maxLength) + "...";
|
|
97
|
+
}
|
|
98
|
+
return result;
|
|
99
|
+
}
|
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
const DEFAULT_SERVICE_HEALTH_INTERVAL_MS = 10_000;
|
|
3
|
+
/**
|
|
4
|
+
* 为 ChatCCC 这种常驻服务建立一个明确的进程生命周期锚点。
|
|
5
|
+
*
|
|
6
|
+
* 正常情况下,正在 listen 的 HTTP Server 自己就足以维持事件循环;额外的
|
|
7
|
+
* referenced timer 是最后一道保险,避免某个依赖升级或异常 close/unref 让进程在
|
|
8
|
+
* 没有信号、异常或退出码的情况下静默消失。定时检查同时会重新 ref Server,并在
|
|
9
|
+
* Server 确实停止监听时串行触发恢复,避免只把一个失去服务能力的僵尸进程留下来。
|
|
10
|
+
*/
|
|
11
|
+
export function createServiceLifecycleGuard(options = {}) {
|
|
12
|
+
const intervalMs = options.intervalMs ?? DEFAULT_SERVICE_HEALTH_INTERVAL_MS;
|
|
13
|
+
const setIntervalImpl = options.setIntervalImpl
|
|
14
|
+
?? ((callback, delayMs) => setInterval(callback, delayMs));
|
|
15
|
+
const clearIntervalImpl = options.clearIntervalImpl
|
|
16
|
+
?? ((timer) => clearInterval(timer));
|
|
17
|
+
const tracer = options.tracer ?? (() => { });
|
|
18
|
+
const getActiveResourcesInfo = options.getActiveResourcesInfo
|
|
19
|
+
?? (() => process.getActiveResourcesInfo());
|
|
20
|
+
let timer = null;
|
|
21
|
+
let server = null;
|
|
22
|
+
let recoverServer;
|
|
23
|
+
let recoveryPromise = null;
|
|
24
|
+
let shuttingDown = false;
|
|
25
|
+
const trace = (message, extra) => {
|
|
26
|
+
try {
|
|
27
|
+
tracer(message, extra);
|
|
28
|
+
}
|
|
29
|
+
catch { /* 诊断路径不能反过来打断服务 */ }
|
|
30
|
+
};
|
|
31
|
+
const serverAddress = () => {
|
|
32
|
+
try {
|
|
33
|
+
return server?.address() ?? null;
|
|
34
|
+
}
|
|
35
|
+
catch {
|
|
36
|
+
return null;
|
|
37
|
+
}
|
|
38
|
+
};
|
|
39
|
+
const activeResources = () => {
|
|
40
|
+
try {
|
|
41
|
+
return getActiveResourcesInfo();
|
|
42
|
+
}
|
|
43
|
+
catch {
|
|
44
|
+
return [];
|
|
45
|
+
}
|
|
46
|
+
};
|
|
47
|
+
const diagnostics = (extra = {}) => ({
|
|
48
|
+
...extra,
|
|
49
|
+
uptimeSeconds: Math.floor(process.uptime()),
|
|
50
|
+
activeResources: activeResources(),
|
|
51
|
+
serverAttached: server !== null,
|
|
52
|
+
serverListening: server?.listening ?? false,
|
|
53
|
+
serverAddress: serverAddress(),
|
|
54
|
+
});
|
|
55
|
+
const start = () => {
|
|
56
|
+
if (shuttingDown || timer)
|
|
57
|
+
return;
|
|
58
|
+
timer = setIntervalImpl(() => { void checkNow(); }, intervalMs);
|
|
59
|
+
// Node 的 Timeout 默认就是 ref 状态;显式 ref 让常驻服务契约不会依赖默认值。
|
|
60
|
+
try {
|
|
61
|
+
timer.ref?.();
|
|
62
|
+
}
|
|
63
|
+
catch { /* ignore */ }
|
|
64
|
+
trace("service-lifecycle: guard started", { intervalMs });
|
|
65
|
+
};
|
|
66
|
+
const checkNow = async () => {
|
|
67
|
+
if (shuttingDown || !server)
|
|
68
|
+
return;
|
|
69
|
+
if (server.listening) {
|
|
70
|
+
try {
|
|
71
|
+
server.ref();
|
|
72
|
+
}
|
|
73
|
+
catch (err) {
|
|
74
|
+
trace("service-lifecycle: HTTP server ref failed", diagnostics({
|
|
75
|
+
error: err.message,
|
|
76
|
+
}));
|
|
77
|
+
}
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
if (recoveryPromise)
|
|
81
|
+
return recoveryPromise;
|
|
82
|
+
trace("service-lifecycle: HTTP server inactive", diagnostics());
|
|
83
|
+
if (!recoverServer)
|
|
84
|
+
return;
|
|
85
|
+
recoveryPromise = Promise.resolve()
|
|
86
|
+
.then(() => recoverServer?.())
|
|
87
|
+
.then(() => {
|
|
88
|
+
if (server?.listening) {
|
|
89
|
+
try {
|
|
90
|
+
server.ref();
|
|
91
|
+
}
|
|
92
|
+
catch { /* 下一轮健康检查会再次尝试 */ }
|
|
93
|
+
trace("service-lifecycle: HTTP server recovered", diagnostics());
|
|
94
|
+
}
|
|
95
|
+
else {
|
|
96
|
+
trace("service-lifecycle: HTTP recovery completed without listening", diagnostics());
|
|
97
|
+
}
|
|
98
|
+
})
|
|
99
|
+
.catch((err) => {
|
|
100
|
+
trace("service-lifecycle: HTTP server recovery failed", diagnostics({
|
|
101
|
+
error: err instanceof Error ? err.message : String(err),
|
|
102
|
+
}));
|
|
103
|
+
})
|
|
104
|
+
.finally(() => {
|
|
105
|
+
recoveryPromise = null;
|
|
106
|
+
});
|
|
107
|
+
return recoveryPromise;
|
|
108
|
+
};
|
|
109
|
+
const attachServer = (nextServer, nextRecoverServer) => {
|
|
110
|
+
server = nextServer;
|
|
111
|
+
recoverServer = nextRecoverServer;
|
|
112
|
+
if (server.listening) {
|
|
113
|
+
try {
|
|
114
|
+
server.ref();
|
|
115
|
+
}
|
|
116
|
+
catch { /* 下一轮健康检查会记录 */ }
|
|
117
|
+
}
|
|
118
|
+
trace("service-lifecycle: HTTP server attached", diagnostics());
|
|
119
|
+
};
|
|
120
|
+
const handleBeforeExit = (code) => {
|
|
121
|
+
if (shuttingDown)
|
|
122
|
+
return;
|
|
123
|
+
trace("service-lifecycle: unexpected beforeExit", diagnostics({ code }));
|
|
124
|
+
start();
|
|
125
|
+
void checkNow();
|
|
126
|
+
};
|
|
127
|
+
const beginShutdown = (reason) => {
|
|
128
|
+
if (shuttingDown)
|
|
129
|
+
return;
|
|
130
|
+
shuttingDown = true;
|
|
131
|
+
if (timer) {
|
|
132
|
+
try {
|
|
133
|
+
clearIntervalImpl(timer);
|
|
134
|
+
}
|
|
135
|
+
catch { /* process 即将退出 */ }
|
|
136
|
+
timer = null;
|
|
137
|
+
}
|
|
138
|
+
trace("service-lifecycle: shutdown requested", { reason });
|
|
139
|
+
};
|
|
140
|
+
return { start, attachServer, checkNow, handleBeforeExit, beginShutdown };
|
|
141
|
+
}
|
|
142
|
+
/**
|
|
143
|
+
* ChatCCC 自己拉起替代进程时使用的内部标记。
|
|
144
|
+
*
|
|
145
|
+
* 不能用“是否已有配置”判断是否打开控制台:首次配置和日常直接启动都应该
|
|
146
|
+
* 打开,而 `/restart`、`/update` 和 Web UI 重启都不应该打扰用户。环境变量
|
|
147
|
+
* 会自然穿过 cmd/bash/npx 这几层启动器,因此也适用于 Windows 与 Linux。
|
|
148
|
+
*/
|
|
149
|
+
export const INTERNAL_RESTART_ENV_VAR = "CHATCCC_INTERNAL_RESTART";
|
|
150
|
+
export function createInternalRestartEnv(inherited = process.env) {
|
|
151
|
+
return {
|
|
152
|
+
...inherited,
|
|
153
|
+
[INTERNAL_RESTART_ENV_VAR]: "1",
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
export function shouldAutoOpenWebUi(options = {}) {
|
|
157
|
+
const env = options.env ?? process.env;
|
|
158
|
+
return options.openOnStart !== false && env[INTERNAL_RESTART_ENV_VAR] !== "1";
|
|
159
|
+
}
|
|
160
|
+
/** Web UI 始终使用 localhost,并跟随实际配置端口。 */
|
|
161
|
+
export function buildWebUiUrl(port) {
|
|
162
|
+
return `http://localhost:${port}/`;
|
|
163
|
+
}
|
|
164
|
+
/**
|
|
165
|
+
* 调用操作系统默认浏览器打开 Web UI,与 Chrome CDP 守护功能完全独立。
|
|
166
|
+
* 返回值仅表示打开请求是否成功发起;浏览器是否复用标签页由系统浏览器决定。
|
|
167
|
+
*/
|
|
168
|
+
export function openWebUiInDefaultBrowser(port, deps = {}) {
|
|
169
|
+
const url = buildWebUiUrl(port);
|
|
170
|
+
const platform = deps.platform ?? process.platform;
|
|
171
|
+
const env = deps.env ?? process.env;
|
|
172
|
+
const spawnImpl = deps.spawnImpl ?? spawn;
|
|
173
|
+
const onError = deps.onError ?? ((message) => console.error(message));
|
|
174
|
+
const onInfo = deps.onInfo ?? ((message) => console.log(message));
|
|
175
|
+
// Linux 服务器通常没有图形会话。此时调用 xdg-open 只会制造噪音;
|
|
176
|
+
// 直接给出可复制的 SSH 隧道命令,让用户从自己的电脑访问本地 Web UI。
|
|
177
|
+
if (platform === "linux" && !env.DISPLAY && !env.WAYLAND_DISPLAY) {
|
|
178
|
+
onInfo(`[WEB-UI] 未检测到 Linux 图形桌面,跳过自动打开浏览器。` +
|
|
179
|
+
`可在本机执行 ssh -L ${port}:127.0.0.1:${port} <user>@<server>,` +
|
|
180
|
+
`然后访问 ${url}`);
|
|
181
|
+
return false;
|
|
182
|
+
}
|
|
183
|
+
try {
|
|
184
|
+
let child;
|
|
185
|
+
if (platform === "win32") {
|
|
186
|
+
// `start` 会把第一个带引号的参数当窗口标题,空字符串是必要占位符。
|
|
187
|
+
child = spawnImpl("cmd.exe", ["/c", "start", "", url], {
|
|
188
|
+
detached: true,
|
|
189
|
+
stdio: "ignore",
|
|
190
|
+
windowsHide: true,
|
|
191
|
+
});
|
|
192
|
+
}
|
|
193
|
+
else if (platform === "darwin") {
|
|
194
|
+
child = spawnImpl("open", [url], { detached: true, stdio: "ignore" });
|
|
195
|
+
}
|
|
196
|
+
else {
|
|
197
|
+
child = spawnImpl("xdg-open", [url], { detached: true, stdio: "ignore" });
|
|
198
|
+
}
|
|
199
|
+
child.on("error", (err) => {
|
|
200
|
+
onError(`[WEB-UI] 自动打开浏览器失败: ${err.message}`);
|
|
201
|
+
});
|
|
202
|
+
child.unref();
|
|
203
|
+
return true;
|
|
204
|
+
}
|
|
205
|
+
catch (err) {
|
|
206
|
+
onError(`[WEB-UI] 自动打开浏览器失败: ${err.message}`);
|
|
207
|
+
return false;
|
|
208
|
+
}
|
|
209
|
+
}
|