chatccc 0.2.267 → 0.2.269
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 +11 -5
- package/dist/src/agent-delegate-task-rpc.js +40 -27
- package/dist/src/agent-delegate-task.js +8 -3
- package/dist/src/agent-set-cwd-rpc.js +73 -0
- package/dist/src/agent-team/application/main-agent-service.js +11 -2
- package/dist/src/agent-team/application/task-execution-service.js +34 -5
- package/dist/src/agent-team/domain/task-run.js +4 -0
- package/dist/src/agent-team/http/board-routes.js +6 -0
- package/dist/src/agent-team/infrastructure/task-execution-runtime.js +4 -0
- package/dist/src/agent-team/web/agent-team-page.js +243 -229
- package/dist/src/cards.js +3 -0
- package/dist/src/engines/engine-manager.js +7 -0
- package/dist/src/execution-transcript.js +110 -0
- package/dist/src/im-skills.js +2 -2
- package/dist/src/index.js +9 -2
- package/dist/src/orchestrator.js +194 -6
- package/dist/src/safe-maintenance.js +271 -0
- package/dist/src/session-chat-binding.js +12 -0
- package/dist/src/session.js +16 -2
- package/dist/src/stream-state.js +1 -0
- package/dist/src/web-ui.js +5 -0
- package/im-skills/feishu-skill/skill.md +18 -4
- package/package.json +76 -76
package/dist/src/cards.js
CHANGED
|
@@ -143,7 +143,10 @@ export function buildHelpCard(userText, opts = {}) {
|
|
|
143
143
|
"发送 **/ask** 以问答模式提问(只读,不执行写操作)",
|
|
144
144
|
"发送 **/usage** 查看当前 Agent 的用量或余额",
|
|
145
145
|
"发送 **/restart** 重启 ChatCCC 进程",
|
|
146
|
+
"发送 **/restart safe** 等待现有任务完成后安全重启",
|
|
146
147
|
"发送 **/update** 更新并重启(仅 npm 全局安装可用)",
|
|
148
|
+
"发送 **/update safe** 等待现有任务完成后安全更新",
|
|
149
|
+
"发送 **/safestatus** 查看安全维护状态,**/cancelsf** 取消等待中的预约",
|
|
147
150
|
ABD_HELP_LINE,
|
|
148
151
|
].join("\n");
|
|
149
152
|
return JSON.stringify({
|
|
@@ -4,6 +4,7 @@ import { existsSync } from "node:fs";
|
|
|
4
4
|
import { mkdir, readFile, readdir, rename, rm, stat, writeFile } from "node:fs/promises";
|
|
5
5
|
import { homedir } from "node:os";
|
|
6
6
|
import { delimiter, dirname, join, relative, resolve } from "node:path";
|
|
7
|
+
import { isSafeMaintenanceAdmissionClosed } from "../safe-maintenance.js";
|
|
7
8
|
const STEP_DEFINITIONS = [
|
|
8
9
|
["preflight", "检查运行环境"],
|
|
9
10
|
["prepare", "准备临时目录"],
|
|
@@ -100,6 +101,9 @@ export class EngineManager {
|
|
|
100
101
|
listSpecs() {
|
|
101
102
|
return [...this.specs.values()];
|
|
102
103
|
}
|
|
104
|
+
getActiveInstallIds() {
|
|
105
|
+
return [...this.activeInstalls.keys()].sort();
|
|
106
|
+
}
|
|
103
107
|
getSpec(engineId) {
|
|
104
108
|
const spec = this.specs.get(engineId);
|
|
105
109
|
if (!spec)
|
|
@@ -133,6 +137,9 @@ export class EngineManager {
|
|
|
133
137
|
const running = this.activeInstalls.get(engineId);
|
|
134
138
|
if (running)
|
|
135
139
|
return this.readJob(this.getSpec(engineId)).then((job) => job ?? this.newJob(this.getSpec(engineId)));
|
|
140
|
+
if (isSafeMaintenanceAdmissionClosed()) {
|
|
141
|
+
throw new Error("ChatCCC 正在等待安全维护,暂不接受新的依赖安装任务。");
|
|
142
|
+
}
|
|
136
143
|
const spec = this.getSpec(engineId);
|
|
137
144
|
const job = this.newJob(spec);
|
|
138
145
|
await this.persistJob(spec, job);
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
export function appendExecutionTranscriptBlock(block, state, at = new Date().toISOString()) {
|
|
2
|
+
switch (block.type) {
|
|
3
|
+
case "text_reset":
|
|
4
|
+
state.transcript = [];
|
|
5
|
+
return;
|
|
6
|
+
case "text":
|
|
7
|
+
appendText(state, block.text, at);
|
|
8
|
+
return;
|
|
9
|
+
case "text_final":
|
|
10
|
+
// Some adapters emit both deltas and an authoritative final snapshot. Keep the
|
|
11
|
+
// ordered deltas when present, otherwise retain the snapshot as the full reply.
|
|
12
|
+
if (!state.transcript.some((entry) => entry.type === "text"))
|
|
13
|
+
appendText(state, block.text, at);
|
|
14
|
+
return;
|
|
15
|
+
case "thinking":
|
|
16
|
+
appendEntry(state, { type: "thinking", at, text: block.thinking });
|
|
17
|
+
return;
|
|
18
|
+
case "tool_use":
|
|
19
|
+
appendEntry(state, {
|
|
20
|
+
type: "tool_use",
|
|
21
|
+
at,
|
|
22
|
+
name: block.name,
|
|
23
|
+
...(block.id ? { toolUseId: block.id } : {}),
|
|
24
|
+
input: stringifyTranscriptValue(block.input),
|
|
25
|
+
});
|
|
26
|
+
return;
|
|
27
|
+
case "tool_result":
|
|
28
|
+
appendEntry(state, {
|
|
29
|
+
type: "tool_result",
|
|
30
|
+
at,
|
|
31
|
+
...(findToolName(state.transcript, block.tool_use_id) ? { name: findToolName(state.transcript, block.tool_use_id) } : {}),
|
|
32
|
+
toolUseId: block.tool_use_id,
|
|
33
|
+
output: stringifyTranscriptValue(block.content),
|
|
34
|
+
...(block.is_error !== undefined ? { isError: block.is_error } : {}),
|
|
35
|
+
});
|
|
36
|
+
return;
|
|
37
|
+
case "search_result":
|
|
38
|
+
appendEntry(state, { type: "search", at, text: block.query });
|
|
39
|
+
return;
|
|
40
|
+
case "compact_boundary":
|
|
41
|
+
appendEntry(state, {
|
|
42
|
+
type: "compact",
|
|
43
|
+
at,
|
|
44
|
+
text: `${block.trigger === "manual" ? "手动" : "自动"}压缩:${block.pre_tokens} → ${block.post_tokens ?? "?"} tokens`,
|
|
45
|
+
});
|
|
46
|
+
return;
|
|
47
|
+
case "agent_status": {
|
|
48
|
+
const text = block.status === "compacting" ? "正在压缩上下文" : "正在生成回复";
|
|
49
|
+
const previous = state.transcript.at(-1);
|
|
50
|
+
if (previous?.type !== "status" || previous.text !== text)
|
|
51
|
+
appendEntry(state, { type: "status", at, text });
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
case "redacted_thinking":
|
|
55
|
+
appendEntry(state, { type: "notice", at, text: "部分思考内容已被安全过滤" });
|
|
56
|
+
return;
|
|
57
|
+
case "agent_progress":
|
|
58
|
+
// Heartbeats carry no content and can occur very frequently. Persisting them
|
|
59
|
+
// would add noise without helping users reconstruct what happened.
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
export function isExecutionTranscriptEntry(value) {
|
|
64
|
+
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
65
|
+
return false;
|
|
66
|
+
const entry = value;
|
|
67
|
+
const types = [
|
|
68
|
+
"prompt", "thinking", "text", "tool_use", "tool_result", "search", "compact", "status", "notice",
|
|
69
|
+
];
|
|
70
|
+
if (!types.includes(entry.type) || typeof entry.at !== "string")
|
|
71
|
+
return false;
|
|
72
|
+
for (const field of ["text", "name", "toolUseId", "input", "output"]) {
|
|
73
|
+
if (entry[field] !== undefined && typeof entry[field] !== "string")
|
|
74
|
+
return false;
|
|
75
|
+
}
|
|
76
|
+
return entry.isError === undefined || typeof entry.isError === "boolean";
|
|
77
|
+
}
|
|
78
|
+
function findToolName(entries, toolUseId) {
|
|
79
|
+
for (let index = entries.length - 1; index >= 0; index--) {
|
|
80
|
+
const entry = entries[index];
|
|
81
|
+
if (entry?.type === "tool_use" && entry.toolUseId === toolUseId)
|
|
82
|
+
return entry.name;
|
|
83
|
+
}
|
|
84
|
+
return undefined;
|
|
85
|
+
}
|
|
86
|
+
function appendText(state, text, at) {
|
|
87
|
+
if (!text)
|
|
88
|
+
return;
|
|
89
|
+
const previous = state.transcript.at(-1);
|
|
90
|
+
if (previous?.type === "text") {
|
|
91
|
+
previous.text = (previous.text ?? "") + text;
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
appendEntry(state, { type: "text", at, text });
|
|
95
|
+
}
|
|
96
|
+
function appendEntry(state, entry) {
|
|
97
|
+
state.transcript.push(entry);
|
|
98
|
+
}
|
|
99
|
+
function stringifyTranscriptValue(value) {
|
|
100
|
+
if (typeof value === "string")
|
|
101
|
+
return value;
|
|
102
|
+
if (value === undefined)
|
|
103
|
+
return "";
|
|
104
|
+
try {
|
|
105
|
+
return JSON.stringify(value, null, 2);
|
|
106
|
+
}
|
|
107
|
+
catch {
|
|
108
|
+
return String(value);
|
|
109
|
+
}
|
|
110
|
+
}
|
package/dist/src/im-skills.js
CHANGED
|
@@ -56,8 +56,8 @@ function promptCacheKey(input) {
|
|
|
56
56
|
const names = input.enabledSkillNames
|
|
57
57
|
? [...input.enabledSkillNames].sort().join(",")
|
|
58
58
|
: "*";
|
|
59
|
-
const { session_id, cwd } = input.variables;
|
|
60
|
-
return `${input.skillsDir ?? DEFAULT_IM_SKILLS_DIR}|${names}|${session_id}|${cwd}`;
|
|
59
|
+
const { session_id, cwd, open_id } = input.variables;
|
|
60
|
+
return `${input.skillsDir ?? DEFAULT_IM_SKILLS_DIR}|${names}|${session_id}|${cwd}|${open_id}`;
|
|
61
61
|
}
|
|
62
62
|
/** 带会话级缓存的 buildImSkillsPrompt。同 session + 同 cwd 时直接返回缓存的渲染结果。 */
|
|
63
63
|
export async function buildImSkillsPromptCached(input) {
|
package/dist/src/index.js
CHANGED
|
@@ -40,6 +40,7 @@ import { handleAgentFileRequest } from "./agent-file-rpc.js";
|
|
|
40
40
|
import { handleAgentDelegateTaskRequest } from "./agent-delegate-task-rpc.js";
|
|
41
41
|
import { handleAgentStopStuckRequest } from "./agent-stop-stuck.js";
|
|
42
42
|
import { handleAgentReloadConfigRequest } from "./agent-reload-config-rpc.js";
|
|
43
|
+
import { handleAgentSetCwdRequest } from "./agent-set-cwd-rpc.js";
|
|
43
44
|
import { handleChatGptSubscriptionRequest } from "./chatgpt-subscription-rpc.js";
|
|
44
45
|
import { applyPrivacy } from "./privacy.js";
|
|
45
46
|
import { createCardKitCard, sendCardKitMessage, updateCardKitCard, } from "./cardkit.js";
|
|
@@ -47,7 +48,7 @@ import { loadSessionRegistryForBinding, rebuildBindingsFromRegistry, resetState,
|
|
|
47
48
|
import { startChromeDevtoolsGuard, stopChromeDevtoolsGuard } from "./chrome-devtools-guard.js";
|
|
48
49
|
import { rebuildSessionChatsFromRegistry, setQueueConsumer, } from "./session-chat-binding.js";
|
|
49
50
|
import { fixStaleStreamStates } from "./stream-state.js";
|
|
50
|
-
import { handleCommand } from "./orchestrator.js";
|
|
51
|
+
import { configureSafeMaintenanceRuntime, handleCommand, recoverSafeMaintenanceAfterStartup, } from "./orchestrator.js";
|
|
51
52
|
import { createWechatAdapter, startWechatPlatform } from "./wechat-platform.js";
|
|
52
53
|
import { handleCodexResetCardAction } from "./codex-reset-actions.js";
|
|
53
54
|
import { resolveFeishuCardActionChatType } from "./card-action-routing.js";
|
|
@@ -103,11 +104,12 @@ function createFeishuAdapter() {
|
|
|
103
104
|
}
|
|
104
105
|
const feishuPlatform = createFeishuAdapter();
|
|
105
106
|
const wechatPlatform = createWechatAdapter();
|
|
107
|
+
configureSafeMaintenanceRuntime([feishuPlatform, wechatPlatform]);
|
|
106
108
|
setSessionPlatform(feishuPlatform);
|
|
107
109
|
configureAgentTeamMainAgent(feishuPlatform);
|
|
108
110
|
// 注册队列消费回调:session 生成完成后自动处理缓存消息
|
|
109
111
|
setQueueConsumer((platform, msg) => {
|
|
110
|
-
handleCommand(platform, msg.text, msg.chatId, msg.openId, msg.msgTimestamp, msg.chatType, msg.traceId).catch(err => console.error(`[${ts()}] Queue consume failed: ${err.message}`));
|
|
112
|
+
handleCommand(platform, msg.text, msg.chatId, msg.openId, msg.msgTimestamp, msg.chatType, msg.traceId, undefined, true).catch(err => console.error(`[${ts()}] Queue consume failed: ${err.message}`));
|
|
111
113
|
});
|
|
112
114
|
function getInnerEvent(data) {
|
|
113
115
|
return (data.event ?? data);
|
|
@@ -560,6 +562,7 @@ async function main() {
|
|
|
560
562
|
if (injected)
|
|
561
563
|
return true;
|
|
562
564
|
return (await handleAgentReloadConfigRequest(req, res))
|
|
565
|
+
|| (await handleAgentSetCwdRequest(req, res))
|
|
563
566
|
|| (await handleAgentImageRequest(req, res))
|
|
564
567
|
|| (await handleAgentFileRequest(req, res))
|
|
565
568
|
|| (await handleAgentDelegateTaskRequest(req, res, feishuPlatform))
|
|
@@ -596,6 +599,7 @@ async function main() {
|
|
|
596
599
|
appendStartupTrace(opened ? "web-ui: opening simulate browser" : "web-ui: simulate browser unavailable", { url });
|
|
597
600
|
}
|
|
598
601
|
installShutdownHandlers(simServer, serviceLifecycle);
|
|
602
|
+
await recoverSafeMaintenanceAfterStartup();
|
|
599
603
|
return;
|
|
600
604
|
}
|
|
601
605
|
if (Number.isNaN(CHATCCC_PORT) || CHATCCC_PORT < 1 || CHATCCC_PORT > 65535) {
|
|
@@ -620,6 +624,7 @@ async function main() {
|
|
|
620
624
|
});
|
|
621
625
|
setExtraApiHandler(async (req, res) => {
|
|
622
626
|
return (await handleAgentReloadConfigRequest(req, res))
|
|
627
|
+
|| (await handleAgentSetCwdRequest(req, res))
|
|
623
628
|
|| (await handleAgentImageRequest(req, res))
|
|
624
629
|
|| (await handleAgentFileRequest(req, res))
|
|
625
630
|
|| (await handleAgentDelegateTaskRequest(req, res, feishuPlatform))
|
|
@@ -647,6 +652,7 @@ async function main() {
|
|
|
647
652
|
});
|
|
648
653
|
try {
|
|
649
654
|
await startConfiguredPlatforms(httpServer, { failOnFeishuError: true });
|
|
655
|
+
await recoverSafeMaintenanceAfterStartup();
|
|
650
656
|
return { ok: true };
|
|
651
657
|
}
|
|
652
658
|
catch (err) {
|
|
@@ -714,6 +720,7 @@ async function main() {
|
|
|
714
720
|
});
|
|
715
721
|
}
|
|
716
722
|
await startConfiguredPlatforms(httpServer, { failOnFeishuError: false });
|
|
723
|
+
await recoverSafeMaintenanceAfterStartup();
|
|
717
724
|
}
|
|
718
725
|
/**
|
|
719
726
|
* 生命周期健康检查发现 HTTP Server 已停止监听时,优先原地恢复同一个 Server。
|
package/dist/src/orchestrator.js
CHANGED
|
@@ -17,7 +17,7 @@ import { CLAUDE_MODEL, GIT_TIMEOUT_MS, PROJECT_ROOT, anthropicConfigDisplay, con
|
|
|
17
17
|
import { buildHelpCard, buildEffortCard, buildFastModeCard, buildModelCard, buildStatusCard, buildCdContent, buildCdCard, buildSessionsCard, buildQueuedCard, buildQueueFullCard, buildCodexUsageCard, } from "./cards.js";
|
|
18
18
|
import { formatGitResult, gitResultHeaderTemplate, runGitCommand, } from "./git-command.js";
|
|
19
19
|
import { clearSessionModelOverride, clearSessionEffortOverride, getSessionStatus, getAllSessionsStatus, initClaudeSession, lastMsgTimestamps, resumeAndPrompt, sessionInfoMap, setSessionModelOverride, setSessionEffortOverride, switchChatBinding, recordSessionRegistry, getAdapterForTool, getEffectiveModelForTool, getEffectiveEffortForTool, getEffectiveFastModeForTool, setSessionFastModeOverride, stopSession, loadSessionRegistryForBinding, removeSessionRegistryRecord, saveSessionTool, recordChatPlatform, } from "./session.js";
|
|
20
|
-
import { bindChatToSession, unbindChatFromSession, isSessionRunning, displayCards, enqueueMessage, cancelQueuedMessage, } from "./session-chat-binding.js";
|
|
20
|
+
import { bindChatToSession, unbindChatFromSession, isSessionRunning, displayCards, enqueueMessage, cancelQueuedMessage, getSessionDrainSnapshot, } from "./session-chat-binding.js";
|
|
21
21
|
import { getCodexUsageSummary, getTenantAccessToken, sendPostMessage } from "./feishu-platform.js";
|
|
22
22
|
import { getCursorUsageSummary } from "./cursor-usage.js";
|
|
23
23
|
import { getChatGptSubscriptionStatus } from "./chatgpt-subscription.js";
|
|
@@ -25,8 +25,10 @@ import { applySharedPrefix } from "./shared-prefix.js";
|
|
|
25
25
|
import { sessionChatName } from "./session-name.js";
|
|
26
26
|
import { reloadRuntimeConfig } from "./runtime-reload.js";
|
|
27
27
|
import { acquireUpdateCommandGuard } from "./update-command-guard.js";
|
|
28
|
-
import { createInternalRestartEnv } from "./startup-lifecycle.js";
|
|
28
|
+
import { createInternalRestartEnv, INTERNAL_RESTART_ENV_VAR } from "./startup-lifecycle.js";
|
|
29
29
|
import { resolveChatCccRuntimeSpawnSpec } from "./runtime-entry.js";
|
|
30
|
+
import { engineManager } from "./engines/engine-specs.js";
|
|
31
|
+
import { beginSafeMaintenanceTrackedWork, isSafeMaintenanceAdmissionClosed, safeMaintenanceCoordinator, } from "./safe-maintenance.js";
|
|
30
32
|
import { feishuP2pContactStore, isValidFeishuOpenId, } from "./agent-team/repositories/feishu-p2p-contact-store.js";
|
|
31
33
|
// ---------------------------------------------------------------------------
|
|
32
34
|
// 辅助函数
|
|
@@ -577,10 +579,11 @@ function updLog(msg) {
|
|
|
577
579
|
catch { }
|
|
578
580
|
}
|
|
579
581
|
/** 同步更新 npm 全局包并 spawn 新进程重启。不依赖 systemd 或任何服务管理器。 */
|
|
580
|
-
function syncUpdateAndRestart() {
|
|
582
|
+
function syncUpdateAndRestart(options = {}) {
|
|
581
583
|
updLog(`sync update start, pid=${process.pid}`);
|
|
582
584
|
appendStartupTrace("update: sync update start", { pid: process.pid });
|
|
583
585
|
const npmCmd = process.platform === "win32" ? "npm.cmd" : "npm";
|
|
586
|
+
let updateSucceeded = false;
|
|
584
587
|
// 1. npm update
|
|
585
588
|
updLog(`running: ${npmCmd} update -g chatccc`);
|
|
586
589
|
appendStartupTrace("update: npm update begin", { npmCmd });
|
|
@@ -590,6 +593,7 @@ function syncUpdateAndRestart() {
|
|
|
590
593
|
const elapsed = Date.now() - t0;
|
|
591
594
|
updLog(`npm update OK (${elapsed}ms): ${out.slice(0, 500)}`);
|
|
592
595
|
appendStartupTrace("update: npm update OK", { elapsedMs: elapsed, outputLen: out.length });
|
|
596
|
+
updateSucceeded = true;
|
|
593
597
|
}
|
|
594
598
|
catch (e) {
|
|
595
599
|
const elapsed = Date.now() - t0;
|
|
@@ -605,6 +609,7 @@ function syncUpdateAndRestart() {
|
|
|
605
609
|
const elapsed2 = Date.now() - t1;
|
|
606
610
|
updLog(`npm install fallback OK (${elapsed2}ms): ${out2.slice(0, 500)}`);
|
|
607
611
|
appendStartupTrace("update: npm install fallback OK", { elapsedMs: elapsed2, outputLen: out2.length });
|
|
612
|
+
updateSucceeded = true;
|
|
608
613
|
}
|
|
609
614
|
catch (e2) {
|
|
610
615
|
const elapsed2 = Date.now() - t1;
|
|
@@ -613,6 +618,11 @@ function syncUpdateAndRestart() {
|
|
|
613
618
|
appendStartupTrace("update: npm install fallback failed", { elapsedMs: elapsed2, message: err2.message });
|
|
614
619
|
}
|
|
615
620
|
}
|
|
621
|
+
if (!updateSucceeded && options.spawnOnUpdateFailure === false) {
|
|
622
|
+
updLog("safe update aborted: both npm update and fallback install failed");
|
|
623
|
+
appendStartupTrace("update: safe update aborted before restart", {});
|
|
624
|
+
return undefined;
|
|
625
|
+
}
|
|
616
626
|
// 2. resolve bin path
|
|
617
627
|
const npmPrefix = process.env.NPM_PREFIX || "";
|
|
618
628
|
const binName = process.platform === "win32" ? "chatccc.cmd" : "chatccc";
|
|
@@ -771,15 +781,106 @@ export async function decideRestartParentExit(child, timeoutMs, pollMs = 500, tr
|
|
|
771
781
|
trace("restart: child alive after window, parent exiting", { childPid: child.pid });
|
|
772
782
|
return true;
|
|
773
783
|
}
|
|
784
|
+
const safeMaintenancePlatforms = new Map();
|
|
785
|
+
export function configureSafeMaintenanceRuntime(platforms) {
|
|
786
|
+
safeMaintenancePlatforms.clear();
|
|
787
|
+
for (const platform of platforms) {
|
|
788
|
+
if (platform.kind)
|
|
789
|
+
safeMaintenancePlatforms.set(platform.kind, platform);
|
|
790
|
+
}
|
|
791
|
+
safeMaintenanceCoordinator.configure({
|
|
792
|
+
getSnapshot() {
|
|
793
|
+
const sessions = getSessionDrainSnapshot();
|
|
794
|
+
return {
|
|
795
|
+
...sessions,
|
|
796
|
+
activeEngineIds: engineManager.getActiveInstallIds(),
|
|
797
|
+
activeWorkLabels: [],
|
|
798
|
+
};
|
|
799
|
+
},
|
|
800
|
+
execute(kind) {
|
|
801
|
+
return kind === "update" ? executeSafeUpdate() : executeSafeRestart();
|
|
802
|
+
},
|
|
803
|
+
async notify(requester, message) {
|
|
804
|
+
const platform = safeMaintenancePlatforms.get(requester.platform);
|
|
805
|
+
if (!platform)
|
|
806
|
+
throw new Error(`Unavailable platform: ${requester.platform}`);
|
|
807
|
+
await platform.sendText(requester.chatId, message);
|
|
808
|
+
},
|
|
809
|
+
});
|
|
810
|
+
}
|
|
811
|
+
export function recoverSafeMaintenanceAfterStartup() {
|
|
812
|
+
return safeMaintenanceCoordinator.recoverAfterStartup(process.env[INTERNAL_RESTART_ENV_VAR] === "1");
|
|
813
|
+
}
|
|
814
|
+
async function executeSafeRestart() {
|
|
815
|
+
fileLog.flush();
|
|
816
|
+
appendStartupTrace("safe-maintenance: restart spawn begin", { fromPid: process.pid });
|
|
817
|
+
const child = spawnRestartChild();
|
|
818
|
+
child.unref();
|
|
819
|
+
const shouldExit = await decideRestartParentExit(child, RESTART_CHILD_READY_MS);
|
|
820
|
+
if (!shouldExit)
|
|
821
|
+
return false;
|
|
822
|
+
appendStartupTrace("safe-maintenance: restart parent exit", { childPid: child.pid });
|
|
823
|
+
process.exit(0);
|
|
824
|
+
}
|
|
825
|
+
async function executeSafeUpdate() {
|
|
826
|
+
fileLog.flush();
|
|
827
|
+
appendStartupTrace("safe-maintenance: update begin", { fromPid: process.pid });
|
|
828
|
+
const child = syncUpdateAndRestart({ spawnOnUpdateFailure: false });
|
|
829
|
+
if (!child)
|
|
830
|
+
return false;
|
|
831
|
+
child.unref();
|
|
832
|
+
const shouldExit = await decideRestartParentExit(child, RESTART_CHILD_READY_MS);
|
|
833
|
+
if (!shouldExit)
|
|
834
|
+
return false;
|
|
835
|
+
appendStartupTrace("safe-maintenance: update parent exit", { childPid: child.pid });
|
|
836
|
+
process.exit(0);
|
|
837
|
+
}
|
|
838
|
+
function safeMaintenanceRequester(platform, chatId, openId) {
|
|
839
|
+
return { platform: platform.kind ?? "feishu", chatId, openId };
|
|
840
|
+
}
|
|
841
|
+
function safeMaintenancePhaseLabel(phase) {
|
|
842
|
+
const labels = {
|
|
843
|
+
draining: "等待现有任务结束",
|
|
844
|
+
executing: "正在执行维护",
|
|
845
|
+
completed: "已完成",
|
|
846
|
+
failed: "执行失败",
|
|
847
|
+
};
|
|
848
|
+
return labels[phase] ?? phase;
|
|
849
|
+
}
|
|
850
|
+
async function safeMaintenanceStatusText() {
|
|
851
|
+
const status = await safeMaintenanceCoordinator.status();
|
|
852
|
+
if (!status.job)
|
|
853
|
+
return "当前没有安全维护预约。";
|
|
854
|
+
const snapshot = status.snapshot;
|
|
855
|
+
return [
|
|
856
|
+
`安全维护:${status.job.kind === "update" ? "更新并重启" : "重启"}`,
|
|
857
|
+
`状态:${safeMaintenancePhaseLabel(status.job.phase)}`,
|
|
858
|
+
`执行中/收尾会话:${snapshot.activeSessionIds.length}`,
|
|
859
|
+
`已接受的缓存消息:${snapshot.queuedSessionIds.length}`,
|
|
860
|
+
`依赖安装任务:${snapshot.activeEngineIds.length}`,
|
|
861
|
+
`其他处理中入口:${Math.max(0, snapshot.activeWorkLabels.length - 1)}`,
|
|
862
|
+
...(status.job.lastError ? [`错误:${status.job.lastError}`] : []),
|
|
863
|
+
].join("\n");
|
|
864
|
+
}
|
|
774
865
|
// ---------------------------------------------------------------------------
|
|
775
866
|
// handleCommand — 平台无关的命令分发
|
|
776
867
|
// ---------------------------------------------------------------------------
|
|
777
|
-
export async function handleCommand(platform, text, chatId, openId, msgTimestamp, chatType = "group", traceId, commandId) {
|
|
868
|
+
export async function handleCommand(platform, text, chatId, openId, msgTimestamp, chatType = "group", traceId, commandId, acceptedBeforeSafeMaintenance = false) {
|
|
869
|
+
const release = beginSafeMaintenanceTrackedWork(acceptedBeforeSafeMaintenance ? "accepted-queued-message" : "chat-command");
|
|
870
|
+
try {
|
|
871
|
+
await handleCommandInternal(platform, text, chatId, openId, msgTimestamp, chatType, traceId, commandId, acceptedBeforeSafeMaintenance);
|
|
872
|
+
}
|
|
873
|
+
finally {
|
|
874
|
+
release();
|
|
875
|
+
}
|
|
876
|
+
}
|
|
877
|
+
async function handleCommandInternal(platform, text, chatId, openId, msgTimestamp, chatType = "group", traceId, commandId, acceptedBeforeSafeMaintenance = false) {
|
|
778
878
|
const tid = traceId ?? makeTraceId();
|
|
779
879
|
const sharedPrefix = applySharedPrefix(text);
|
|
780
880
|
const promptText = sharedPrefix.text;
|
|
781
881
|
text = sharedPrefix.body;
|
|
782
882
|
const textLower = text.toLowerCase();
|
|
883
|
+
const normalizedCommandText = textLower.trim().replace(/\s+/g, " ");
|
|
783
884
|
const isCommandText = !sharedPrefix.matched && textLower.startsWith("/");
|
|
784
885
|
recordChatPlatform(chatId, platform);
|
|
785
886
|
if (platform.kind === "feishu" && chatType === "p2p" && isValidFeishuOpenId(openId)) {
|
|
@@ -788,6 +889,77 @@ export async function handleCommand(platform, text, chatId, openId, msgTimestamp
|
|
|
788
889
|
console.error(`[${ts()}] [AGENT-TEAM] Failed to remember Feishu private contact: ${err.message}`);
|
|
789
890
|
});
|
|
790
891
|
}
|
|
892
|
+
if (isCommandText && normalizedCommandText === "/safestatus") {
|
|
893
|
+
logTrace(tid, "BRANCH", { cmd: "/safestatus" });
|
|
894
|
+
await platform.sendText(chatId, await safeMaintenanceStatusText()).catch(() => { });
|
|
895
|
+
logTrace(tid, "DONE", { outcome: "safe_maintenance_status" });
|
|
896
|
+
return;
|
|
897
|
+
}
|
|
898
|
+
if (isCommandText && normalizedCommandText === "/cancelsf") {
|
|
899
|
+
logTrace(tid, "BRANCH", { cmd: "/cancelsf" });
|
|
900
|
+
const canceled = await safeMaintenanceCoordinator.cancel();
|
|
901
|
+
if (!canceled) {
|
|
902
|
+
await platform.sendText(chatId, "当前没有可取消的安全维护预约;已经开始执行的维护也不能取消。").catch(() => { });
|
|
903
|
+
}
|
|
904
|
+
logTrace(tid, "DONE", { outcome: canceled ? "safe_maintenance_canceled" : "safe_maintenance_cancel_unavailable" });
|
|
905
|
+
return;
|
|
906
|
+
}
|
|
907
|
+
if (isCommandText && normalizedCommandText === "/restart safe") {
|
|
908
|
+
logTrace(tid, "BRANCH", { cmd: "/restart safe" });
|
|
909
|
+
try {
|
|
910
|
+
const job = await safeMaintenanceCoordinator.schedule("restart", safeMaintenanceRequester(platform, chatId, openId));
|
|
911
|
+
await platform.sendText(chatId, job.phase === "executing"
|
|
912
|
+
? "安全维护已经开始执行,无法重复预约。"
|
|
913
|
+
: job.kind === "update"
|
|
914
|
+
? "已经存在优先级更高的安全更新预约,将继续等待现有任务完成。发送 /safestatus 查看状态,/cancelsf 取消预约。"
|
|
915
|
+
: "已预约安全重启。现有会话、缓存消息和依赖安装会先自然完成;现在起不再接受新的普通任务。发送 /safestatus 查看状态,/cancelsf 取消预约。").catch(() => { });
|
|
916
|
+
logTrace(tid, "DONE", { outcome: "safe_restart_scheduled", jobId: job.jobId });
|
|
917
|
+
}
|
|
918
|
+
catch (error) {
|
|
919
|
+
await platform.sendText(chatId, `安全重启预约失败:${error.message}`).catch(() => { });
|
|
920
|
+
logTrace(tid, "DONE", { outcome: "safe_restart_schedule_failed", error: error.message });
|
|
921
|
+
}
|
|
922
|
+
return;
|
|
923
|
+
}
|
|
924
|
+
if (isCommandText && normalizedCommandText === "/update safe") {
|
|
925
|
+
logTrace(tid, "BRANCH", { cmd: "/update safe" });
|
|
926
|
+
const isGlobal = isRunningFromGlobalNpm();
|
|
927
|
+
if (!isGlobal) {
|
|
928
|
+
await platform.sendText(chatId, "当前进程非 npm 全局安装,无法使用 /update safe。请通过 npm install -g chatccc 安装后使用。").catch(() => { });
|
|
929
|
+
logTrace(tid, "DONE", { outcome: "safe_update_not_global" });
|
|
930
|
+
return;
|
|
931
|
+
}
|
|
932
|
+
const updateGuard = acquireUpdateCommandGuard({ commandId });
|
|
933
|
+
if (!updateGuard.allowed) {
|
|
934
|
+
if (updateGuard.reason !== "duplicate_id") {
|
|
935
|
+
await platform.sendText(chatId, "无法写入更新保护状态。为避免连续更新和重启,本次 /update safe 未预约。").catch(() => { });
|
|
936
|
+
}
|
|
937
|
+
logTrace(tid, "DONE", { outcome: updateGuard.reason === "duplicate_id" ? "safe_update_duplicate_id" : "safe_update_guard_failed" });
|
|
938
|
+
return;
|
|
939
|
+
}
|
|
940
|
+
try {
|
|
941
|
+
const job = await safeMaintenanceCoordinator.schedule("update", safeMaintenanceRequester(platform, chatId, openId));
|
|
942
|
+
await platform.sendText(chatId, job.phase === "executing"
|
|
943
|
+
? "安全维护已经开始执行,无法重复预约。"
|
|
944
|
+
: "已预约安全更新。现有会话、缓存消息和依赖安装会先自然完成;现在起不再接受新的普通任务。发送 /safestatus 查看状态,/cancelsf 取消预约。").catch(() => { });
|
|
945
|
+
logTrace(tid, "DONE", { outcome: "safe_update_scheduled", jobId: job.jobId });
|
|
946
|
+
}
|
|
947
|
+
catch (error) {
|
|
948
|
+
await platform.sendText(chatId, `安全更新预约失败:${error.message}`).catch(() => { });
|
|
949
|
+
logTrace(tid, "DONE", { outcome: "safe_update_schedule_failed", error: error.message });
|
|
950
|
+
}
|
|
951
|
+
return;
|
|
952
|
+
}
|
|
953
|
+
const maintenanceAllowedCommands = new Set([
|
|
954
|
+
"/stop", "/cancel", "/state", "/sessions", "/usage", "/safestatus", "/cancelsf", "/restart", "/update", "/reload", "/help",
|
|
955
|
+
]);
|
|
956
|
+
if (isSafeMaintenanceAdmissionClosed()
|
|
957
|
+
&& !acceptedBeforeSafeMaintenance
|
|
958
|
+
&& (!isCommandText || !maintenanceAllowedCommands.has(normalizedCommandText))) {
|
|
959
|
+
await platform.sendText(chatId, "ChatCCC 正在等待安全重启或更新,当前不接受新的任务。请在维护完成后重新发送;可用 /safestatus 查看状态。").catch(() => { });
|
|
960
|
+
logTrace(tid, "DONE", { outcome: "safe_maintenance_admission_closed" });
|
|
961
|
+
return;
|
|
962
|
+
}
|
|
791
963
|
if (isCommandText && textLower === "/reload") {
|
|
792
964
|
logTrace(tid, "BRANCH", { cmd: "/reload" });
|
|
793
965
|
try {
|
|
@@ -808,6 +980,14 @@ export async function handleCommand(platform, text, chatId, openId, msgTimestamp
|
|
|
808
980
|
}
|
|
809
981
|
if (isCommandText && textLower === "/restart") {
|
|
810
982
|
logTrace(tid, "BRANCH", { cmd: "/restart" });
|
|
983
|
+
const safeStatus = await safeMaintenanceCoordinator.status();
|
|
984
|
+
if (safeStatus.job?.phase === "executing") {
|
|
985
|
+
await platform.sendText(chatId, "安全维护已经开始执行,请勿重复重启。").catch(() => { });
|
|
986
|
+
logTrace(tid, "DONE", { outcome: "restart_blocked_by_executing_safe_maintenance" });
|
|
987
|
+
return;
|
|
988
|
+
}
|
|
989
|
+
if (safeStatus.job?.phase === "draining")
|
|
990
|
+
await safeMaintenanceCoordinator.cancel(false);
|
|
811
991
|
await platform.sendText(chatId, "重启中...请几秒后发消息唤醒我").catch(() => { });
|
|
812
992
|
logTrace(tid, "DONE", { outcome: "restart" });
|
|
813
993
|
appendStartupTrace("restart: spawn begin", { fromPid: process.pid });
|
|
@@ -825,6 +1005,12 @@ export async function handleCommand(platform, text, chatId, openId, msgTimestamp
|
|
|
825
1005
|
}
|
|
826
1006
|
if (isCommandText && textLower === "/update") {
|
|
827
1007
|
logTrace(tid, "BRANCH", { cmd: "/update" });
|
|
1008
|
+
const safeStatus = await safeMaintenanceCoordinator.status();
|
|
1009
|
+
if (safeStatus.job?.phase === "executing") {
|
|
1010
|
+
await platform.sendText(chatId, "安全维护已经开始执行,请勿重复更新。").catch(() => { });
|
|
1011
|
+
logTrace(tid, "DONE", { outcome: "update_blocked_by_executing_safe_maintenance" });
|
|
1012
|
+
return;
|
|
1013
|
+
}
|
|
828
1014
|
const isGlobal = isRunningFromGlobalNpm();
|
|
829
1015
|
appendStartupTrace("update: command received", { isGlobal, chatId });
|
|
830
1016
|
if (!isGlobal) {
|
|
@@ -851,6 +1037,8 @@ export async function handleCommand(platform, text, chatId, openId, msgTimestamp
|
|
|
851
1037
|
logTrace(tid, "DONE", { outcome: "update_guard_write_failed" });
|
|
852
1038
|
return;
|
|
853
1039
|
}
|
|
1040
|
+
if (safeStatus.job?.phase === "draining")
|
|
1041
|
+
await safeMaintenanceCoordinator.cancel(false);
|
|
854
1042
|
await platform.sendText(chatId, "正在更新并重启,请稍候...").catch(() => { });
|
|
855
1043
|
logTrace(tid, "DONE", { outcome: "update" });
|
|
856
1044
|
appendStartupTrace("update: sync update begin", { fromPid: process.pid });
|
|
@@ -1902,7 +2090,7 @@ export async function handleCommand(platform, text, chatId, openId, msgTimestamp
|
|
|
1902
2090
|
}
|
|
1903
2091
|
try {
|
|
1904
2092
|
logTrace(tid, "RESUME", { sessionId, tool: descriptionTool });
|
|
1905
|
-
const resumeOutcome = await resumeAndPrompt(sessionId, promptText, platform, chatId, msgTimestamp, descriptionTool, tid);
|
|
2093
|
+
const resumeOutcome = await resumeAndPrompt(sessionId, promptText, platform, chatId, msgTimestamp, descriptionTool, tid, openId);
|
|
1906
2094
|
if (resumeOutcome === "error") {
|
|
1907
2095
|
logTrace(tid, "DONE", { outcome: "resume_error", sessionId });
|
|
1908
2096
|
console.error(`[${ts()}] [RESUME] Session ${sessionId} ended with an Agent error`);
|
|
@@ -2081,7 +2269,7 @@ export async function handleCommand(platform, text, chatId, openId, msgTimestamp
|
|
|
2081
2269
|
if (!switchResult.ok) {
|
|
2082
2270
|
throw switchResult.error ?? new Error("Failed to bind Feishu private session");
|
|
2083
2271
|
}
|
|
2084
|
-
await resumeAndPrompt(sessionId, promptText, platform, chatId, msgTimestamp, tool, tid);
|
|
2272
|
+
await resumeAndPrompt(sessionId, promptText, platform, chatId, msgTimestamp, tool, tid, openId);
|
|
2085
2273
|
logTrace(tid, "DONE", {
|
|
2086
2274
|
outcome: "auto_new_feishu_p2p_prompt_done",
|
|
2087
2275
|
chatId,
|