chatccc 0.2.252 → 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/os-prompts/darwin.md +8 -8
- package/deepccc-agent/os-prompts/linux.md +8 -8
- package/deepccc-agent/os-prompts/win32.md +11 -11
- package/deepccc-agent/package.json +65 -65
- 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 +76 -74
- 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 -877
- 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 -710
- 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
package/src/orchestrator.ts
DELETED
|
@@ -1,2543 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* orchestrator.ts — 平台无关的消息命令处理
|
|
3
|
-
*
|
|
4
|
-
* Phase 1: 从 index.ts 抽出 handleCommand 及辅助函数。
|
|
5
|
-
* 所有 IM 平台操作通过 PlatformAdapter 接口注入,不直接依赖 feishu-platform.ts。
|
|
6
|
-
*/
|
|
7
|
-
|
|
8
|
-
import { execSync, spawn, type ChildProcess, type StdioOptions } from "node:child_process";
|
|
9
|
-
import { readdir, stat } from "node:fs/promises";
|
|
10
|
-
import { appendFileSync, closeSync, existsSync, mkdirSync, openSync, readFileSync, writeFileSync } from "node:fs";
|
|
11
|
-
import { join, resolve, dirname } from "node:path";
|
|
12
|
-
import { homedir } from "node:os";
|
|
13
|
-
|
|
14
|
-
import { makeTraceId, logTrace } from "./trace.ts";
|
|
15
|
-
import { appendStartupTrace } from "./shared.ts";
|
|
16
|
-
import {
|
|
17
|
-
CLAUDE_EFFORT,
|
|
18
|
-
CLAUDE_MODEL,
|
|
19
|
-
CLAUDE_SUBAGENT_MODEL,
|
|
20
|
-
CONFIG_FILE,
|
|
21
|
-
GIT_TIMEOUT_MS,
|
|
22
|
-
PROJECT_ROOT,
|
|
23
|
-
anthropicConfigDisplay,
|
|
24
|
-
config,
|
|
25
|
-
fileLog,
|
|
26
|
-
getAllEffortsForTool,
|
|
27
|
-
getAllModelsForTool,
|
|
28
|
-
getDefaultEffortForTool,
|
|
29
|
-
getDefaultCwd,
|
|
30
|
-
LOG_DIR,
|
|
31
|
-
setDefaultCwd,
|
|
32
|
-
getRecentDirs,
|
|
33
|
-
addRecentDir,
|
|
34
|
-
resolveDefaultAgentTool,
|
|
35
|
-
sessionPrefixForTool,
|
|
36
|
-
toolDisplayName,
|
|
37
|
-
ts,
|
|
38
|
-
type AgentTool,
|
|
39
|
-
} from "./config.ts";
|
|
40
|
-
import {
|
|
41
|
-
buildHelpCard,
|
|
42
|
-
buildEffortCard,
|
|
43
|
-
buildFastModeCard,
|
|
44
|
-
buildModelCard,
|
|
45
|
-
buildStatusCard,
|
|
46
|
-
buildCdContent,
|
|
47
|
-
buildCdCard,
|
|
48
|
-
buildSessionsCard,
|
|
49
|
-
buildQueuedCard,
|
|
50
|
-
buildQueueFullCard,
|
|
51
|
-
buildCodexUsageCard,
|
|
52
|
-
} from "./cards.ts";
|
|
53
|
-
import {
|
|
54
|
-
formatGitResult,
|
|
55
|
-
gitResultHeaderTemplate,
|
|
56
|
-
runGitCommand,
|
|
57
|
-
} from "./git-command.ts";
|
|
58
|
-
import {
|
|
59
|
-
clearSessionModelOverride,
|
|
60
|
-
clearSessionEffortOverride,
|
|
61
|
-
getSessionStatus,
|
|
62
|
-
getAllSessionsStatus,
|
|
63
|
-
initClaudeSession,
|
|
64
|
-
lastMsgTimestamps,
|
|
65
|
-
resumeAndPrompt,
|
|
66
|
-
sessionInfoMap,
|
|
67
|
-
setSessionModelOverride,
|
|
68
|
-
setSessionEffortOverride,
|
|
69
|
-
switchChatBinding,
|
|
70
|
-
recordSessionRegistry,
|
|
71
|
-
getAdapterForTool,
|
|
72
|
-
getEffectiveModelForTool,
|
|
73
|
-
getEffectiveEffortForTool,
|
|
74
|
-
getEffectiveFastModeForTool,
|
|
75
|
-
setSessionFastModeOverride,
|
|
76
|
-
stopSession,
|
|
77
|
-
loadSessionRegistryForBinding,
|
|
78
|
-
removeSessionRegistryRecord,
|
|
79
|
-
saveSessionTool,
|
|
80
|
-
recordChatPlatform,
|
|
81
|
-
} from "./session.ts";
|
|
82
|
-
import {
|
|
83
|
-
bindChatToSession,
|
|
84
|
-
unbindChatFromSession,
|
|
85
|
-
isSessionRunning,
|
|
86
|
-
displayCards,
|
|
87
|
-
recordLastActiveChat,
|
|
88
|
-
enqueueMessage,
|
|
89
|
-
cancelQueuedMessage,
|
|
90
|
-
} from "./session-chat-binding.ts";
|
|
91
|
-
import { getCodexUsageSummary, getTenantAccessToken, sendPostMessage } from "./feishu-platform.ts";
|
|
92
|
-
import { getCursorUsageSummary, type CursorUsageSummary } from "./cursor-usage.ts";
|
|
93
|
-
import { getChatGptSubscriptionStatus, type ChatGptSubscriptionResult } from "./chatgpt-subscription.ts";
|
|
94
|
-
import { applySharedPrefix } from "./shared-prefix.ts";
|
|
95
|
-
import { cwdDisplayName, sessionChatName } from "./session-name.ts";
|
|
96
|
-
import { reloadRuntimeConfig } from "./runtime-reload.ts";
|
|
97
|
-
import { acquireUpdateCommandGuard } from "./update-command-guard.ts";
|
|
98
|
-
import { createInternalRestartEnv } from "./startup-lifecycle.ts";
|
|
99
|
-
export { type PlatformAdapter } from "./platform-adapter.ts";
|
|
100
|
-
import type { ChatAvatarUsageHints, PlatformAdapter } from "./platform-adapter.ts";
|
|
101
|
-
import type { CodexUsageSummary } from "./feishu-api.ts";
|
|
102
|
-
|
|
103
|
-
// ---------------------------------------------------------------------------
|
|
104
|
-
// 辅助函数
|
|
105
|
-
// ---------------------------------------------------------------------------
|
|
106
|
-
|
|
107
|
-
/** 模型模糊匹配:精确匹配优先,否则找子串匹配(模型名越短越优先) */
|
|
108
|
-
function findModelMatch(input: string, models: string[]): string | null {
|
|
109
|
-
if (models.length === 0) return null;
|
|
110
|
-
const inputLower = input.toLowerCase();
|
|
111
|
-
// 1) 精确匹配(忽略大小写)
|
|
112
|
-
for (const m of models) {
|
|
113
|
-
if (m.toLowerCase() === inputLower) return m;
|
|
114
|
-
}
|
|
115
|
-
// 2) 子串匹配:模型全名包含输入,按模型名长度升序(越短越优先)
|
|
116
|
-
const candidates = models
|
|
117
|
-
.filter(m => m.toLowerCase().includes(inputLower))
|
|
118
|
-
.sort((a, b) => a.length - b.length);
|
|
119
|
-
return candidates[0] ?? null;
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
function formatCodexUsageSummary(usage: CodexUsageSummary, chatGptSubscription: ChatGptSubscriptionResult | null = null): string {
|
|
123
|
-
const progressBar = (usedPercent: number) => {
|
|
124
|
-
const width = 20;
|
|
125
|
-
const usedBlocks = Math.max(0, Math.min(width, Math.round((usedPercent / 100) * width)));
|
|
126
|
-
return `[${"█".repeat(usedBlocks)}${"░".repeat(width - usedBlocks)}]`;
|
|
127
|
-
};
|
|
128
|
-
|
|
129
|
-
const formatDuration = (seconds: number | null) => {
|
|
130
|
-
if (seconds === null) return "";
|
|
131
|
-
if (seconds <= 0) return "(已到重置时间)";
|
|
132
|
-
const totalMinutes = Math.max(1, Math.floor(seconds / 60));
|
|
133
|
-
const days = Math.floor(totalMinutes / 1440);
|
|
134
|
-
const hours = Math.floor((totalMinutes % 1440) / 60);
|
|
135
|
-
const minutes = totalMinutes % 60;
|
|
136
|
-
const parts: string[] = [];
|
|
137
|
-
if (days > 0) parts.push(`${days}天`);
|
|
138
|
-
if (hours > 0) parts.push(`${hours}小时`);
|
|
139
|
-
if (minutes > 0 || parts.length === 0) parts.push(`${minutes}分钟`);
|
|
140
|
-
return `(约 ${parts.join("")}后)`;
|
|
141
|
-
};
|
|
142
|
-
|
|
143
|
-
const formatResetTime = (balance: NonNullable<CodexUsageSummary["fiveHour"]>) => {
|
|
144
|
-
if (balance.resetAtEpochSeconds === null) return "暂无数据";
|
|
145
|
-
const date = new Date(balance.resetAtEpochSeconds * 1000);
|
|
146
|
-
const pad = (value: number) => String(value).padStart(2, "0");
|
|
147
|
-
const absolute = [
|
|
148
|
-
date.getFullYear(),
|
|
149
|
-
"-",
|
|
150
|
-
pad(date.getMonth() + 1),
|
|
151
|
-
"-",
|
|
152
|
-
pad(date.getDate()),
|
|
153
|
-
" ",
|
|
154
|
-
pad(date.getHours()),
|
|
155
|
-
":",
|
|
156
|
-
pad(date.getMinutes()),
|
|
157
|
-
].join("");
|
|
158
|
-
return `${absolute}${formatDuration(balance.resetAfterSeconds)}`;
|
|
159
|
-
};
|
|
160
|
-
|
|
161
|
-
const formatWindow = (label: string, balance: CodexUsageSummary["fiveHour"] | null) => {
|
|
162
|
-
if (!balance) return `**${label}:** 暂无数据`;
|
|
163
|
-
return [
|
|
164
|
-
`**${label}:** 已用 ${balance.usedPercent}%,剩余 ${balance.remainingPercent}%,重置: ${formatResetTime(balance)}`,
|
|
165
|
-
progressBar(balance.usedPercent),
|
|
166
|
-
].join("\n");
|
|
167
|
-
};
|
|
168
|
-
|
|
169
|
-
const formatResetCredits = () => {
|
|
170
|
-
if (usage.rateLimitResetCreditsAvailable === null) return "**主动重置:** 暂无数据";
|
|
171
|
-
const lines = [`**主动重置:** 剩余 ${usage.rateLimitResetCreditsAvailable} 次`];
|
|
172
|
-
const credits = usage.rateLimitResetCredits ?? [];
|
|
173
|
-
if (credits.length > 0) {
|
|
174
|
-
const pad = (value: number) => String(value).padStart(2, "0");
|
|
175
|
-
const formatExpiresAt = (value: string) => {
|
|
176
|
-
const date = new Date(value);
|
|
177
|
-
if (!Number.isFinite(date.getTime())) return value;
|
|
178
|
-
return [
|
|
179
|
-
date.getFullYear(),
|
|
180
|
-
"-",
|
|
181
|
-
pad(date.getMonth() + 1),
|
|
182
|
-
"-",
|
|
183
|
-
pad(date.getDate()),
|
|
184
|
-
" ",
|
|
185
|
-
pad(date.getHours()),
|
|
186
|
-
":",
|
|
187
|
-
pad(date.getMinutes()),
|
|
188
|
-
":",
|
|
189
|
-
pad(date.getSeconds()),
|
|
190
|
-
].join("");
|
|
191
|
-
};
|
|
192
|
-
lines.push("**过期时间:**");
|
|
193
|
-
for (const credit of credits) {
|
|
194
|
-
lines.push(`- ${formatExpiresAt(credit.expiresAt)}`);
|
|
195
|
-
}
|
|
196
|
-
}
|
|
197
|
-
return lines.join("\n");
|
|
198
|
-
};
|
|
199
|
-
|
|
200
|
-
const formatSubscriptionFailureReason = (result: ChatGptSubscriptionResult) => {
|
|
201
|
-
const port = result.chromeCdp.port;
|
|
202
|
-
switch (result.code) {
|
|
203
|
-
case "chrome_cdp_unreachable":
|
|
204
|
-
return `Chrome CDP 端口 ${port} 不可访问。请确认常驻 Chrome 已启动,或重启 ChatCCC。`;
|
|
205
|
-
case "chrome_cdp_occupied":
|
|
206
|
-
return `${port} 端口可访问,但不是健康的 Chrome CDP。请释放该端口或修改 chromeDevtools.port。`;
|
|
207
|
-
case "chatgpt_page_missing":
|
|
208
|
-
return `没有可用的 ChatGPT 页面。请在 ${port} 端口对应的 Chrome 浏览器中打开 https://chatgpt.com/ 并登录。`;
|
|
209
|
-
case "chatgpt_session_missing":
|
|
210
|
-
return `请在 ${port} 端口对应的 Chrome 浏览器中登录 ChatGPT。`;
|
|
211
|
-
case "chatgpt_subscription_failed":
|
|
212
|
-
return "ChatGPT 订阅接口探测失败。可能是页面未加载完成、ChatGPT 接口变更或网络异常。";
|
|
213
|
-
case "chrome_cdp_disabled":
|
|
214
|
-
return "";
|
|
215
|
-
case "ok":
|
|
216
|
-
return "";
|
|
217
|
-
}
|
|
218
|
-
};
|
|
219
|
-
|
|
220
|
-
const formatChatGptSubscriptionFailure = () => {
|
|
221
|
-
if (!chatGptSubscription || chatGptSubscription.ok || !chatGptSubscription.chromeCdp.enabled) return "";
|
|
222
|
-
const lines = [
|
|
223
|
-
"**ChatGPT 订阅查询失败:**",
|
|
224
|
-
`- 原因: ${formatSubscriptionFailureReason(chatGptSubscription) || "暂无数据"}`,
|
|
225
|
-
];
|
|
226
|
-
const detail = chatGptSubscription.reason?.replace(/\s+/g, " ").trim();
|
|
227
|
-
if (detail) {
|
|
228
|
-
lines.push(`- 详情: ${detail.length > 240 ? `${detail.slice(0, 240)}...` : detail}`);
|
|
229
|
-
}
|
|
230
|
-
return lines.join("\n");
|
|
231
|
-
};
|
|
232
|
-
|
|
233
|
-
const formatChatGptSubscription = () => {
|
|
234
|
-
if (!chatGptSubscription?.ok || !chatGptSubscription.subscription) return "";
|
|
235
|
-
const subscription = chatGptSubscription.subscription;
|
|
236
|
-
const pad = (value: number) => String(value).padStart(2, "0");
|
|
237
|
-
const formatExpiresAt = (value: string | null) => {
|
|
238
|
-
if (!value) return "暂无数据";
|
|
239
|
-
const date = new Date(value);
|
|
240
|
-
if (!Number.isFinite(date.getTime())) return value;
|
|
241
|
-
return [
|
|
242
|
-
date.getFullYear(),
|
|
243
|
-
"-",
|
|
244
|
-
pad(date.getMonth() + 1),
|
|
245
|
-
"-",
|
|
246
|
-
pad(date.getDate()),
|
|
247
|
-
" ",
|
|
248
|
-
pad(date.getHours()),
|
|
249
|
-
":",
|
|
250
|
-
pad(date.getMinutes()),
|
|
251
|
-
].join("");
|
|
252
|
-
};
|
|
253
|
-
const remaining = typeof subscription.remainingDays === "number"
|
|
254
|
-
? `(剩余 ${subscription.remainingDays} 天)`
|
|
255
|
-
: "";
|
|
256
|
-
return [
|
|
257
|
-
"**ChatGPT 订阅:**",
|
|
258
|
-
`- 套餐: ${subscription.plan ?? "暂无数据"}`,
|
|
259
|
-
`- 到期: ${formatExpiresAt(subscription.expiresAt)}${remaining}`,
|
|
260
|
-
`- 自动续费: ${subscription.willRenew === null ? "暂无数据" : subscription.willRenew ? "是" : "否"}`,
|
|
261
|
-
].join("\n");
|
|
262
|
-
};
|
|
263
|
-
|
|
264
|
-
return [
|
|
265
|
-
"Codex 用量:",
|
|
266
|
-
"",
|
|
267
|
-
formatChatGptSubscription(),
|
|
268
|
-
formatChatGptSubscriptionFailure(),
|
|
269
|
-
formatResetCredits(),
|
|
270
|
-
"",
|
|
271
|
-
usage.fiveHour ? formatWindow("5h", usage.fiveHour) : "",
|
|
272
|
-
usage.weekly ? formatWindow("7天", usage.weekly) : "",
|
|
273
|
-
].filter((line, index, arr) => line !== "" || (index > 0 && arr[index - 1] !== "")).join("\n");
|
|
274
|
-
}
|
|
275
|
-
|
|
276
|
-
function formatCursorUsageSummary(usage: CursorUsageSummary): string {
|
|
277
|
-
const timeZone = Intl.DateTimeFormat().resolvedOptions().timeZone;
|
|
278
|
-
const dateFormatter = new Intl.DateTimeFormat("zh-CN", {
|
|
279
|
-
timeZone,
|
|
280
|
-
year: "numeric",
|
|
281
|
-
month: "2-digit",
|
|
282
|
-
day: "2-digit",
|
|
283
|
-
hour: "2-digit",
|
|
284
|
-
minute: "2-digit",
|
|
285
|
-
second: "2-digit",
|
|
286
|
-
hour12: false,
|
|
287
|
-
timeZoneName: "shortOffset",
|
|
288
|
-
});
|
|
289
|
-
const formatDate = (value: string | undefined) => {
|
|
290
|
-
const timestamp = Number(value);
|
|
291
|
-
if (!Number.isFinite(timestamp)) return "暂无数据";
|
|
292
|
-
return dateFormatter.format(new Date(timestamp));
|
|
293
|
-
};
|
|
294
|
-
const formatMoney = (value: number | undefined) => {
|
|
295
|
-
if (typeof value !== "number" || !Number.isFinite(value)) return "暂无数据";
|
|
296
|
-
return `$${(value / 100).toFixed(2)}`;
|
|
297
|
-
};
|
|
298
|
-
const formatPercent = (value: number | undefined) => {
|
|
299
|
-
if (typeof value !== "number" || !Number.isFinite(value)) return "暂无数据";
|
|
300
|
-
return `${value}%`;
|
|
301
|
-
};
|
|
302
|
-
const plan = usage.planUsage;
|
|
303
|
-
const spendLimit = usage.spendLimitUsage;
|
|
304
|
-
|
|
305
|
-
return [
|
|
306
|
-
"Cursor 用量:",
|
|
307
|
-
"",
|
|
308
|
-
`**计费周期:** ${formatDate(usage.billingCycleStart)} - ${formatDate(usage.billingCycleEnd)}`,
|
|
309
|
-
"",
|
|
310
|
-
"**Included usage:**",
|
|
311
|
-
`- Total: ${formatMoney(plan?.totalSpend)} / ${formatMoney(plan?.limit)} (${formatPercent(plan?.totalPercentUsed)})`,
|
|
312
|
-
`- Included: ${formatMoney(plan?.includedSpend)}`,
|
|
313
|
-
`- Bonus: ${formatMoney(plan?.bonusSpend)}`,
|
|
314
|
-
`- Auto: ${formatPercent(plan?.autoPercentUsed)}`,
|
|
315
|
-
`- API: ${formatPercent(plan?.apiPercentUsed)}`,
|
|
316
|
-
"",
|
|
317
|
-
"**On-Demand / Spend limit:**",
|
|
318
|
-
`- Individual used: ${formatMoney(spendLimit?.individualUsed)}`,
|
|
319
|
-
`- Pool used: ${formatMoney(spendLimit?.pooledUsed)} / ${formatMoney(spendLimit?.pooledLimit)}`,
|
|
320
|
-
`- Pool remaining: ${formatMoney(spendLimit?.pooledRemaining)}`,
|
|
321
|
-
`- Limit type: ${spendLimit?.limitType ?? "暂无数据"}`,
|
|
322
|
-
`- Display threshold: ${formatMoney(usage.displayThreshold)}`,
|
|
323
|
-
"",
|
|
324
|
-
`**Enabled:** ${usage.enabled === undefined ? "暂无数据" : String(usage.enabled)}`,
|
|
325
|
-
usage.displayMessage ? `**Message:** ${usage.displayMessage}` : "",
|
|
326
|
-
usage.autoModelSelectedDisplayMessage ? `**Auto model:** ${usage.autoModelSelectedDisplayMessage}` : "",
|
|
327
|
-
usage.namedModelSelectedDisplayMessage ? `**Named model:** ${usage.namedModelSelectedDisplayMessage}` : "",
|
|
328
|
-
usage.autoBucketModels?.length ? `**Auto bucket models:** ${usage.autoBucketModels.join(", ")}` : "",
|
|
329
|
-
].filter(Boolean).join("\n");
|
|
330
|
-
}
|
|
331
|
-
|
|
332
|
-
function usageHelpLine(tool: string): string {
|
|
333
|
-
if (tool === "codex") return "\n发送 **/usage** 查看 Codex 实际存在的 5h/7天用量窗口,以及查询/使用主动重置卡。";
|
|
334
|
-
if (tool === "cursor") return "\n发送 **/usage** 查看 Cursor 用量。";
|
|
335
|
-
return "";
|
|
336
|
-
}
|
|
337
|
-
|
|
338
|
-
function fastHelpAfterModel(tool: string): string {
|
|
339
|
-
return tool === "codex"
|
|
340
|
-
? "\n发送 **/fast** 查看或切换当前会话的 Fast 模式。"
|
|
341
|
-
: "";
|
|
342
|
-
}
|
|
343
|
-
|
|
344
|
-
function setChatAvatarForSession(
|
|
345
|
-
platform: PlatformAdapter,
|
|
346
|
-
chatId: string,
|
|
347
|
-
tool: string,
|
|
348
|
-
status: string,
|
|
349
|
-
sessionId?: string,
|
|
350
|
-
usageHints?: ChatAvatarUsageHints,
|
|
351
|
-
): Promise<void> {
|
|
352
|
-
const fastMode = getEffectiveFastModeForTool(tool, sessionId);
|
|
353
|
-
if (!usageHints && !fastMode) return platform.setChatAvatar(chatId, tool, status);
|
|
354
|
-
return platform.setChatAvatar(chatId, tool, status, {
|
|
355
|
-
...usageHints,
|
|
356
|
-
...(fastMode ? { fastMode: true } : {}),
|
|
357
|
-
});
|
|
358
|
-
}
|
|
359
|
-
|
|
360
|
-
async function sendFastModeStatus(
|
|
361
|
-
platform: PlatformAdapter,
|
|
362
|
-
chatId: string,
|
|
363
|
-
enabled: boolean,
|
|
364
|
-
): Promise<void> {
|
|
365
|
-
if (platform.kind === "wechat") {
|
|
366
|
-
const mode = enabled ? "ON (Fast)" : "OFF (Standard)";
|
|
367
|
-
await platform.sendText(
|
|
368
|
-
chatId,
|
|
369
|
-
`Codex Fast 模式: ${mode}\n输入 /fast on 或 /fast off 切换。切换将在下一条消息生效,当前生成不中断。`,
|
|
370
|
-
);
|
|
371
|
-
return;
|
|
372
|
-
}
|
|
373
|
-
await platform.sendRawCard(chatId, buildFastModeCard(enabled));
|
|
374
|
-
}
|
|
375
|
-
|
|
376
|
-
async function resolveUsageTarget(chatId: string): Promise<{ tool: "codex" | "cursor" | "ccc"; sessionId?: string }> {
|
|
377
|
-
try {
|
|
378
|
-
const registry = await loadSessionRegistryForBinding();
|
|
379
|
-
const record = registry[chatId];
|
|
380
|
-
const tool = record?.tool;
|
|
381
|
-
if (tool === "cursor") return { tool: "cursor", sessionId: record?.sessionId };
|
|
382
|
-
if (tool === "ccc") return { tool: "ccc", sessionId: record?.sessionId };
|
|
383
|
-
return { tool: "codex", sessionId: record?.sessionId };
|
|
384
|
-
} catch {
|
|
385
|
-
return { tool: "codex" };
|
|
386
|
-
}
|
|
387
|
-
}
|
|
388
|
-
|
|
389
|
-
function isOfficialDeepSeek(baseURL: string): boolean {
|
|
390
|
-
try {
|
|
391
|
-
return new URL(baseURL).host === "api.deepseek.com";
|
|
392
|
-
} catch {
|
|
393
|
-
return false;
|
|
394
|
-
}
|
|
395
|
-
}
|
|
396
|
-
|
|
397
|
-
interface DeepSeekBalance {
|
|
398
|
-
is_available: boolean;
|
|
399
|
-
balance_infos?: Array<{
|
|
400
|
-
currency: string;
|
|
401
|
-
total_balance: string;
|
|
402
|
-
topped_up_balance: string;
|
|
403
|
-
granted_balance: string;
|
|
404
|
-
}>;
|
|
405
|
-
}
|
|
406
|
-
|
|
407
|
-
async function fetchDeepSeekBalance(apiKey: string, baseURL: string): Promise<DeepSeekBalance> {
|
|
408
|
-
const apiOrigin = new URL(baseURL).origin;
|
|
409
|
-
const resp = await fetch(`${apiOrigin}/user/balance`, {
|
|
410
|
-
headers: { Authorization: `Bearer ${apiKey}` },
|
|
411
|
-
});
|
|
412
|
-
if (!resp.ok) {
|
|
413
|
-
throw new Error(`DeepSeek 余额查询失败: HTTP ${resp.status}`);
|
|
414
|
-
}
|
|
415
|
-
return (await resp.json()) as DeepSeekBalance;
|
|
416
|
-
}
|
|
417
|
-
|
|
418
|
-
function formatDeepSeekBalance(balance: DeepSeekBalance): string {
|
|
419
|
-
if (!balance.is_available) return "**DeepSeek 余额:** 暂无数据";
|
|
420
|
-
const infos = balance.balance_infos ?? [];
|
|
421
|
-
if (infos.length === 0) return "**DeepSeek 余额:** 暂无数据";
|
|
422
|
-
const lines = infos.map((info) => {
|
|
423
|
-
const parts = [`**${info.currency}:**`];
|
|
424
|
-
parts.push(`- 总余额: ${info.total_balance}`);
|
|
425
|
-
if (info.topped_up_balance) parts.push(`- 充值余额: ${info.topped_up_balance}`);
|
|
426
|
-
if (info.granted_balance) parts.push(`- 赠送余额: ${info.granted_balance}`);
|
|
427
|
-
return parts.join("\n");
|
|
428
|
-
});
|
|
429
|
-
return `**DeepSeek 余额:**\n${lines.join("\n")}`;
|
|
430
|
-
}
|
|
431
|
-
|
|
432
|
-
function refreshUsageAvatar(
|
|
433
|
-
platform: PlatformAdapter,
|
|
434
|
-
chatId: string,
|
|
435
|
-
tool: "codex" | "cursor",
|
|
436
|
-
status: "busy" | "idle",
|
|
437
|
-
usageHints: ChatAvatarUsageHints,
|
|
438
|
-
sessionId?: string,
|
|
439
|
-
): void {
|
|
440
|
-
setChatAvatarForSession(platform, chatId, tool, status, sessionId, usageHints).catch((err) => {
|
|
441
|
-
console.warn(`[${ts()}] [AVATAR] usage refresh failed: chatId=${chatId} tool=${tool} ${(err as Error).message}`);
|
|
442
|
-
});
|
|
443
|
-
}
|
|
444
|
-
|
|
445
|
-
async function sendUsageSummary(
|
|
446
|
-
platform: PlatformAdapter,
|
|
447
|
-
chatId: string,
|
|
448
|
-
tool: "codex" | "cursor" | "ccc",
|
|
449
|
-
avatarStatus: "busy" | "idle" = "idle",
|
|
450
|
-
sessionId?: string,
|
|
451
|
-
): Promise<void> {
|
|
452
|
-
if (tool === "ccc") {
|
|
453
|
-
const baseURL = config.ccc.DEEPSEEK_BASE_URL;
|
|
454
|
-
if (!isOfficialDeepSeek(baseURL)) {
|
|
455
|
-
const msg = "CCC 用量查询仅支持官方 DeepSeek API (api.deepseek.com),当前使用的非官方接口不支持余额查询。";
|
|
456
|
-
if (platform.kind === "wechat") {
|
|
457
|
-
await platform.sendText(chatId, msg).catch(() => {});
|
|
458
|
-
} else {
|
|
459
|
-
await platform.sendCard(chatId, "CCC Usage", msg, "blue");
|
|
460
|
-
}
|
|
461
|
-
return;
|
|
462
|
-
}
|
|
463
|
-
const balance = await fetchDeepSeekBalance(config.ccc.DEEPSEEK_API_KEY, baseURL);
|
|
464
|
-
const content = formatDeepSeekBalance(balance);
|
|
465
|
-
if (platform.kind === "wechat") {
|
|
466
|
-
await platform.sendText(chatId, content).catch(() => {});
|
|
467
|
-
} else {
|
|
468
|
-
await platform.sendCard(chatId, "CCC Usage", content, "blue");
|
|
469
|
-
}
|
|
470
|
-
return;
|
|
471
|
-
}
|
|
472
|
-
|
|
473
|
-
if (tool === "cursor") {
|
|
474
|
-
const usage = await getCursorUsageSummary();
|
|
475
|
-
const content = formatCursorUsageSummary(usage);
|
|
476
|
-
if (platform.kind === "wechat") {
|
|
477
|
-
await platform.sendText(chatId, content).catch(() => {});
|
|
478
|
-
} else {
|
|
479
|
-
await platform.sendCard(chatId, "Cursor Usage", content, "blue");
|
|
480
|
-
}
|
|
481
|
-
refreshUsageAvatar(platform, chatId, tool, avatarStatus, { cursorUsage: usage }, sessionId);
|
|
482
|
-
return;
|
|
483
|
-
}
|
|
484
|
-
|
|
485
|
-
const [usage, chatGptSubscription] = await Promise.all([
|
|
486
|
-
getCodexUsageSummary(),
|
|
487
|
-
getChatGptSubscriptionStatus().catch(() => null),
|
|
488
|
-
]);
|
|
489
|
-
const content = formatCodexUsageSummary(usage, chatGptSubscription);
|
|
490
|
-
if (platform.kind === "wechat") {
|
|
491
|
-
await platform.sendText(chatId, content).catch(() => {});
|
|
492
|
-
} else if (platform.kind === "feishu") {
|
|
493
|
-
await platform.sendRawCard(chatId, buildCodexUsageCard(content, usage.rateLimitResetCreditsAvailable));
|
|
494
|
-
} else {
|
|
495
|
-
await platform.sendCard(chatId, "Codex Usage", content, "blue");
|
|
496
|
-
}
|
|
497
|
-
refreshUsageAvatar(platform, chatId, tool, avatarStatus, { codexUsage: usage }, sessionId);
|
|
498
|
-
}
|
|
499
|
-
|
|
500
|
-
async function sendUsageError(platform: PlatformAdapter, chatId: string, tool: "codex" | "cursor" | "ccc", err: unknown): Promise<void> {
|
|
501
|
-
const toolLabel = tool === "cursor" ? "Cursor" : tool === "ccc" ? "CCC" : "Codex";
|
|
502
|
-
const message = `${toolLabel} 用量获取失败:${(err as Error).message}`;
|
|
503
|
-
if (platform.kind === "wechat") {
|
|
504
|
-
await platform.sendText(chatId, message).catch(() => {});
|
|
505
|
-
} else {
|
|
506
|
-
await platform.sendCard(chatId, `${toolLabel} Usage`, message, "red");
|
|
507
|
-
}
|
|
508
|
-
}
|
|
509
|
-
|
|
510
|
-
function isUntitledSessionChatName(name: string): boolean {
|
|
511
|
-
return name === "新会话" || name.startsWith("新会话-");
|
|
512
|
-
}
|
|
513
|
-
|
|
514
|
-
function shouldSendWechatProcessingAck(
|
|
515
|
-
platform: PlatformAdapter,
|
|
516
|
-
isCommandText: boolean,
|
|
517
|
-
chatType: string,
|
|
518
|
-
): boolean {
|
|
519
|
-
return platform.kind === "wechat" && chatType === "p2p" && !isCommandText;
|
|
520
|
-
}
|
|
521
|
-
|
|
522
|
-
/** 飞书私聊是专属会话容器;显式 /new 才创建独立群聊。 */
|
|
523
|
-
function isFeishuP2p(platform: PlatformAdapter, chatType: string): boolean {
|
|
524
|
-
return chatType === "p2p" && platform.kind === "feishu";
|
|
525
|
-
}
|
|
526
|
-
|
|
527
|
-
async function sendStateCard(
|
|
528
|
-
platform: PlatformAdapter,
|
|
529
|
-
chatId: string,
|
|
530
|
-
sessionId: string | null,
|
|
531
|
-
toolLabel: string,
|
|
532
|
-
traceId: string,
|
|
533
|
-
): Promise<void> {
|
|
534
|
-
const status = sessionId ? await getSessionStatus(chatId) : null;
|
|
535
|
-
const isActive = sessionId ? isSessionRunning(sessionId) : false;
|
|
536
|
-
const stateLabel = sessionId
|
|
537
|
-
? (isActive ? "🟢 运行中" : "⚪ 空闲")
|
|
538
|
-
: "⚪ 未建立会话";
|
|
539
|
-
const statusText = [
|
|
540
|
-
`**群名:** ${status?.chatName || "—"}`,
|
|
541
|
-
`**Session ID:** ${sessionId ? `\`${status?.sessionId ?? sessionId}\`` : "—"}`,
|
|
542
|
-
`**工具:** ${toolLabel}`,
|
|
543
|
-
`**状态:** ${stateLabel}`,
|
|
544
|
-
`**已对话轮数:** ${status?.turnCount ?? 0}`,
|
|
545
|
-
`**模型:** ${sessionId ? (status?.model ?? anthropicConfigDisplay(CLAUDE_MODEL)) : "—"}`,
|
|
546
|
-
];
|
|
547
|
-
if (status?.effort != null) {
|
|
548
|
-
statusText.push(`**Effort:** ${status.effort}`);
|
|
549
|
-
}
|
|
550
|
-
if (isActive && status) {
|
|
551
|
-
const elapsed = Math.floor((Date.now() - status.startTime) / 1000);
|
|
552
|
-
const mins = Math.floor(elapsed / 60);
|
|
553
|
-
const secs = elapsed % 60;
|
|
554
|
-
statusText.push(`**本轮已运行:** ${mins}分${secs}秒`);
|
|
555
|
-
statusText.push(`**已产出总字符:** ${status.accumulatedLength.toLocaleString()}`);
|
|
556
|
-
}
|
|
557
|
-
if (status?.lastContextTokens) {
|
|
558
|
-
statusText.push(`**上下文 Token 数:** ~${status.lastContextTokens.toLocaleString()}`);
|
|
559
|
-
}
|
|
560
|
-
const card = buildStatusCard(statusText.join("\n"), isActive ? "blue" : "green");
|
|
561
|
-
const ok = await platform.sendRawCard(chatId, card);
|
|
562
|
-
console.log(`[${ts()}] [STATUS] card sent, ok=${ok}`);
|
|
563
|
-
logTrace(traceId, "DONE", {
|
|
564
|
-
outcome: sessionId ? "status" : "status_no_session",
|
|
565
|
-
ok,
|
|
566
|
-
});
|
|
567
|
-
}
|
|
568
|
-
|
|
569
|
-
interface FeishuP2pRegistryRecord {
|
|
570
|
-
sessionId: string;
|
|
571
|
-
tool: string;
|
|
572
|
-
chatType?: string;
|
|
573
|
-
chatName?: string;
|
|
574
|
-
}
|
|
575
|
-
|
|
576
|
-
type FeishuP2pAgentResolution =
|
|
577
|
-
| { kind: "ready"; sessionId: string; tool: string }
|
|
578
|
-
| { kind: "waiting"; sessionId: string; tool: string; desiredTool: AgentTool }
|
|
579
|
-
| { kind: "error"; previousTool: string; desiredTool: AgentTool; error: Error };
|
|
580
|
-
|
|
581
|
-
// 同一个飞书私聊可能短时间收到多条消息。切换期间共享同一个 Promise,避免
|
|
582
|
-
// 为同一次默认 Agent 变化创建多个空会话。
|
|
583
|
-
const feishuP2pAgentSwitches = new Map<string, Promise<FeishuP2pAgentResolution>>();
|
|
584
|
-
|
|
585
|
-
async function resolveFeishuP2pAgent(
|
|
586
|
-
platform: PlatformAdapter,
|
|
587
|
-
chatId: string,
|
|
588
|
-
text: string,
|
|
589
|
-
record: FeishuP2pRegistryRecord,
|
|
590
|
-
traceId: string,
|
|
591
|
-
): Promise<FeishuP2pAgentResolution> {
|
|
592
|
-
const desiredTool = resolveDefaultAgentTool();
|
|
593
|
-
if (record.tool === desiredTool) {
|
|
594
|
-
return { kind: "ready", sessionId: record.sessionId, tool: record.tool };
|
|
595
|
-
}
|
|
596
|
-
|
|
597
|
-
if (isSessionRunning(record.sessionId)) {
|
|
598
|
-
return {
|
|
599
|
-
kind: "waiting",
|
|
600
|
-
sessionId: record.sessionId,
|
|
601
|
-
tool: record.tool,
|
|
602
|
-
desiredTool,
|
|
603
|
-
};
|
|
604
|
-
}
|
|
605
|
-
|
|
606
|
-
const existingSwitch = feishuP2pAgentSwitches.get(chatId);
|
|
607
|
-
if (existingSwitch) return existingSwitch;
|
|
608
|
-
|
|
609
|
-
const switchOperation = (async (): Promise<FeishuP2pAgentResolution> => {
|
|
610
|
-
try {
|
|
611
|
-
// 异步创建开始前重新读取一次,防止另一个请求刚完成了绑定切换。
|
|
612
|
-
const latestRecord = (await loadSessionRegistryForBinding())[chatId];
|
|
613
|
-
if (!latestRecord?.sessionId || !latestRecord.tool || latestRecord.chatType !== "p2p") {
|
|
614
|
-
return {
|
|
615
|
-
kind: "error",
|
|
616
|
-
previousTool: record.tool,
|
|
617
|
-
desiredTool,
|
|
618
|
-
error: new Error("飞书私聊绑定在切换前已发生变化"),
|
|
619
|
-
};
|
|
620
|
-
}
|
|
621
|
-
if (latestRecord.tool === desiredTool) {
|
|
622
|
-
return { kind: "ready", sessionId: latestRecord.sessionId, tool: latestRecord.tool };
|
|
623
|
-
}
|
|
624
|
-
if (isSessionRunning(latestRecord.sessionId)) {
|
|
625
|
-
return {
|
|
626
|
-
kind: "waiting",
|
|
627
|
-
sessionId: latestRecord.sessionId,
|
|
628
|
-
tool: latestRecord.tool,
|
|
629
|
-
desiredTool,
|
|
630
|
-
};
|
|
631
|
-
}
|
|
632
|
-
|
|
633
|
-
const cwd = homedir();
|
|
634
|
-
const init = await initClaudeSession(desiredTool, cwd);
|
|
635
|
-
const chatName = sessionChatName(text.slice(0, 10) || "私聊会话", cwd);
|
|
636
|
-
const switchResult = await switchChatBinding({
|
|
637
|
-
chatId,
|
|
638
|
-
chatType: "p2p",
|
|
639
|
-
oldSessionId: latestRecord.sessionId,
|
|
640
|
-
newSessionId: init.sessionId,
|
|
641
|
-
tool: desiredTool,
|
|
642
|
-
chatName,
|
|
643
|
-
newDescription: `${sessionPrefixForTool(desiredTool)} ${init.sessionId}`,
|
|
644
|
-
updateChatInfoFn: (id, name, desc) => platform.updateChatInfo(id, name, desc),
|
|
645
|
-
});
|
|
646
|
-
if (!switchResult.ok) {
|
|
647
|
-
return {
|
|
648
|
-
kind: "error",
|
|
649
|
-
previousTool: latestRecord.tool,
|
|
650
|
-
desiredTool,
|
|
651
|
-
error: switchResult.error ?? new Error("更新飞书私聊绑定失败"),
|
|
652
|
-
};
|
|
653
|
-
}
|
|
654
|
-
|
|
655
|
-
const previousLabel = toolDisplayName(latestRecord.tool);
|
|
656
|
-
const desiredLabel = toolDisplayName(desiredTool);
|
|
657
|
-
logTrace(traceId, "BRANCH", {
|
|
658
|
-
reason: "switch_feishu_p2p_default_agent",
|
|
659
|
-
chatId,
|
|
660
|
-
oldSessionId: latestRecord.sessionId,
|
|
661
|
-
newSessionId: init.sessionId,
|
|
662
|
-
oldTool: latestRecord.tool,
|
|
663
|
-
newTool: desiredTool,
|
|
664
|
-
});
|
|
665
|
-
await platform.sendCard(
|
|
666
|
-
chatId,
|
|
667
|
-
"默认 Agent 已切换",
|
|
668
|
-
`检测到默认 Agent 已变化:**${previousLabel} → ${desiredLabel}**。\n\n已创建新的空白 ${desiredLabel} 私聊会话,并从本条消息开始使用。`,
|
|
669
|
-
"green",
|
|
670
|
-
).catch(() => {});
|
|
671
|
-
setChatAvatarForSession(platform, chatId, desiredTool, "new", init.sessionId).catch(() => {});
|
|
672
|
-
return { kind: "ready", sessionId: init.sessionId, tool: desiredTool };
|
|
673
|
-
} catch (err) {
|
|
674
|
-
return {
|
|
675
|
-
kind: "error",
|
|
676
|
-
previousTool: record.tool,
|
|
677
|
-
desiredTool,
|
|
678
|
-
error: err as Error,
|
|
679
|
-
};
|
|
680
|
-
}
|
|
681
|
-
})();
|
|
682
|
-
|
|
683
|
-
feishuP2pAgentSwitches.set(chatId, switchOperation);
|
|
684
|
-
try {
|
|
685
|
-
return await switchOperation;
|
|
686
|
-
} finally {
|
|
687
|
-
if (feishuP2pAgentSwitches.get(chatId) === switchOperation) {
|
|
688
|
-
feishuP2pAgentSwitches.delete(chatId);
|
|
689
|
-
}
|
|
690
|
-
}
|
|
691
|
-
}
|
|
692
|
-
|
|
693
|
-
/** 检测当前进程是否从 npm 全局安装启动 */
|
|
694
|
-
function isRunningFromGlobalNpm(): boolean {
|
|
695
|
-
try {
|
|
696
|
-
const globalRoot = execSync("npm root -g", { encoding: "utf8", timeout: 5000, windowsHide: true }).trim();
|
|
697
|
-
return resolve(PROJECT_ROOT).startsWith(resolve(globalRoot));
|
|
698
|
-
} catch {
|
|
699
|
-
return false;
|
|
700
|
-
}
|
|
701
|
-
}
|
|
702
|
-
|
|
703
|
-
const UPDATE_LOG = join(homedir(), ".chatccc", "logs", "update-watcher.log");
|
|
704
|
-
|
|
705
|
-
function updLog(msg: string): void {
|
|
706
|
-
const ts = new Date().toISOString();
|
|
707
|
-
try { appendFileSync(UPDATE_LOG, `${ts} [UPDATE-SYNC] ${msg}\n`, "utf-8"); } catch {}
|
|
708
|
-
}
|
|
709
|
-
|
|
710
|
-
/** 同步更新 npm 全局包并 spawn 新进程重启。不依赖 systemd 或任何服务管理器。 */
|
|
711
|
-
function syncUpdateAndRestart(): ChildProcess | undefined {
|
|
712
|
-
updLog(`sync update start, pid=${process.pid}`);
|
|
713
|
-
appendStartupTrace("update: sync update start", { pid: process.pid });
|
|
714
|
-
|
|
715
|
-
const npmCmd = process.platform === "win32" ? "npm.cmd" : "npm";
|
|
716
|
-
|
|
717
|
-
// 1. npm update
|
|
718
|
-
updLog(`running: ${npmCmd} update -g chatccc`);
|
|
719
|
-
appendStartupTrace("update: npm update begin", { npmCmd });
|
|
720
|
-
const t0 = Date.now();
|
|
721
|
-
try {
|
|
722
|
-
const out = execSync(`${npmCmd} update -g chatccc 2>&1`, { encoding: "utf8", timeout: 120000, windowsHide: true });
|
|
723
|
-
const elapsed = Date.now() - t0;
|
|
724
|
-
updLog(`npm update OK (${elapsed}ms): ${out.slice(0, 500)}`);
|
|
725
|
-
appendStartupTrace("update: npm update OK", { elapsedMs: elapsed, outputLen: out.length });
|
|
726
|
-
} catch (e) {
|
|
727
|
-
const elapsed = Date.now() - t0;
|
|
728
|
-
const err = e as Error & { stderr?: string; stdout?: string; status?: number };
|
|
729
|
-
updLog(`npm update failed (${elapsed}ms): message=${err.message}, stderr=${(err.stderr || "").slice(0, 500)}, stdout=${(err.stdout || "").slice(0, 200)}`);
|
|
730
|
-
appendStartupTrace("update: npm update failed", { elapsedMs: elapsed, message: err.message, stderrLen: (err.stderr || "").length });
|
|
731
|
-
|
|
732
|
-
// fallback
|
|
733
|
-
updLog(`fallback: ${npmCmd} install -g chatccc@latest`);
|
|
734
|
-
appendStartupTrace("update: npm install fallback begin", { npmCmd });
|
|
735
|
-
const t1 = Date.now();
|
|
736
|
-
try {
|
|
737
|
-
const out2 = execSync(`${npmCmd} install -g chatccc@latest 2>&1`, { encoding: "utf8", timeout: 120000, windowsHide: true });
|
|
738
|
-
const elapsed2 = Date.now() - t1;
|
|
739
|
-
updLog(`npm install fallback OK (${elapsed2}ms): ${out2.slice(0, 500)}`);
|
|
740
|
-
appendStartupTrace("update: npm install fallback OK", { elapsedMs: elapsed2, outputLen: out2.length });
|
|
741
|
-
} catch (e2) {
|
|
742
|
-
const elapsed2 = Date.now() - t1;
|
|
743
|
-
const err2 = e2 as Error & { stderr?: string; stdout?: string };
|
|
744
|
-
updLog(`npm install fallback also failed (${elapsed2}ms): message=${err2.message}, stderr=${(err2.stderr || "").slice(0, 500)}`);
|
|
745
|
-
appendStartupTrace("update: npm install fallback failed", { elapsedMs: elapsed2, message: err2.message });
|
|
746
|
-
}
|
|
747
|
-
}
|
|
748
|
-
|
|
749
|
-
// 2. resolve bin path
|
|
750
|
-
const npmPrefix = process.env.NPM_PREFIX || "";
|
|
751
|
-
const binName = process.platform === "win32" ? "chatccc.cmd" : "chatccc";
|
|
752
|
-
const binPath = npmPrefix ? join(npmPrefix, binName) : "chatccc";
|
|
753
|
-
updLog(`bin path: npmPrefix=${npmPrefix || "(empty)"}, binPath=${binPath}`);
|
|
754
|
-
appendStartupTrace("update: spawn begin", { npmPrefix: npmPrefix || "(empty)", binPath });
|
|
755
|
-
|
|
756
|
-
// 3. spawn new chatccc:优先 node + 全局包入口绝对路径(不依赖 PATH/shell),
|
|
757
|
-
// 避免继承环境 PATH 异常时秒退;失败时回退到 binPath(走 shell)。
|
|
758
|
-
try {
|
|
759
|
-
let spawnSpec: { command: string; args: string[] } | null = null;
|
|
760
|
-
if (npmPrefix) {
|
|
761
|
-
const entry = join(npmPrefix, "node_modules", "chatccc", "bin", "chatccc.mjs");
|
|
762
|
-
if (existsSync(entry)) {
|
|
763
|
-
spawnSpec = { command: process.execPath, args: [entry] };
|
|
764
|
-
}
|
|
765
|
-
}
|
|
766
|
-
const child = spawnSpec
|
|
767
|
-
? spawn(spawnSpec.command, spawnSpec.args, {
|
|
768
|
-
detached: true,
|
|
769
|
-
stdio: "ignore",
|
|
770
|
-
shell: false,
|
|
771
|
-
env: createInternalRestartEnv(),
|
|
772
|
-
})
|
|
773
|
-
: spawn(binPath, [], {
|
|
774
|
-
detached: true,
|
|
775
|
-
stdio: "ignore",
|
|
776
|
-
shell: true,
|
|
777
|
-
env: createInternalRestartEnv(),
|
|
778
|
-
});
|
|
779
|
-
child.unref();
|
|
780
|
-
const spawnedAs = spawnSpec ? `${spawnSpec.command} ${spawnSpec.args.join(" ")}` : binPath;
|
|
781
|
-
updLog(`spawn new chatccc OK, childPid=${child.pid}, bin=${spawnedAs}`);
|
|
782
|
-
appendStartupTrace("update: spawn OK", {
|
|
783
|
-
childPid: child.pid,
|
|
784
|
-
binPath: spawnSpec ? spawnSpec.args[0] : binPath,
|
|
785
|
-
});
|
|
786
|
-
return child;
|
|
787
|
-
} catch (e) {
|
|
788
|
-
const errMsg = (e as Error).message;
|
|
789
|
-
updLog(`spawn new chatccc failed: ${errMsg}`);
|
|
790
|
-
appendStartupTrace("update: spawn failed", { error: errMsg });
|
|
791
|
-
return undefined;
|
|
792
|
-
}
|
|
793
|
-
}
|
|
794
|
-
|
|
795
|
-
// ---------------------------------------------------------------------------
|
|
796
|
-
// /restart — 自重启子进程(不经过 npx/npm,避免 PATH 注入秒退;防空窗兜底)
|
|
797
|
-
// ---------------------------------------------------------------------------
|
|
798
|
-
|
|
799
|
-
/** 父进程等待子进程稳定启动的时间窗口(毫秒)。 */
|
|
800
|
-
export const RESTART_CHILD_READY_MS = 3000;
|
|
801
|
-
|
|
802
|
-
/**
|
|
803
|
-
* 构建自重启的 spawn 参数:直接使用 node 可执行文件 + 本地 tsx CLI 绝对路径,
|
|
804
|
-
* 不经过 npx/npm。原实现 spawn("npx", ["tsx", "src/index.ts"], { shell: true })
|
|
805
|
-
* 在继承环境 PATH 异常(npm 子进程找不到 node/tsx)时会秒退,且错误被
|
|
806
|
-
* stdio:"ignore" 吞掉,导致 restart 静默失败、服务空窗。
|
|
807
|
-
*/
|
|
808
|
-
export function buildRestartSpawnSpec(
|
|
809
|
-
projectRoot: string = PROJECT_ROOT,
|
|
810
|
-
): { command: string; args: string[] } {
|
|
811
|
-
const tsxCli = join(projectRoot, "node_modules", "tsx", "dist", "cli.mjs");
|
|
812
|
-
return { command: process.execPath, args: [tsxCli, "src/index.ts"] };
|
|
813
|
-
}
|
|
814
|
-
|
|
815
|
-
export interface RestartSpawnDeps {
|
|
816
|
-
projectRoot?: string;
|
|
817
|
-
spawnImpl?: typeof spawn;
|
|
818
|
-
trace?: typeof appendStartupTrace;
|
|
819
|
-
/** restart 子进程 stderr 的落盘目录;默认 ~/.chatccc/logs */
|
|
820
|
-
restartLogDir?: string;
|
|
821
|
-
/** 终端检测(默认按 process.stdout/stderr.isTTY);测试注入 */
|
|
822
|
-
isTty?: () => boolean;
|
|
823
|
-
}
|
|
824
|
-
|
|
825
|
-
/**
|
|
826
|
-
* spawn 自重启子进程。
|
|
827
|
-
*
|
|
828
|
-
* stdout/stderr 按启动方式分流:
|
|
829
|
-
* - **终端(TTY)场景**(用户从 cmd/PowerShell/node.exe 窗口启动):stdio 用
|
|
830
|
-
* ["ignore", "inherit", "inherit"] 直接继承终端句柄,restart 后窗口日志不中断。
|
|
831
|
-
* 终端句柄的生命周期不随父进程退出而关闭,因此不存在 EPIPE 风险。
|
|
832
|
-
* - **非终端场景**(守护进程/黑匣子等管道或文件启动):stderr 重定向到磁盘日志
|
|
833
|
-
* 文件(restart-*.log),子进程继承文件句柄,父进程退出不影响写入。
|
|
834
|
-
* 不能用 pipe 收集:pipe 读端随父进程退出关闭后,子进程(尤其经 tsx 包装)
|
|
835
|
-
* 再写 stderr(如飞书 SDK 内部 console.warn)会 EPIPE → uncaughtException
|
|
836
|
-
* → 整个服务崩溃。若日志文件打开失败,退回 pipe 收集(旧行为),并记录 trace。
|
|
837
|
-
*/
|
|
838
|
-
export function spawnRestartChild(deps: RestartSpawnDeps = {}): ChildProcess {
|
|
839
|
-
const projectRoot = deps.projectRoot ?? PROJECT_ROOT;
|
|
840
|
-
const spawnImpl = deps.spawnImpl ?? spawn;
|
|
841
|
-
const trace = deps.trace ?? appendStartupTrace;
|
|
842
|
-
const restartLogDir = deps.restartLogDir ?? LOG_DIR;
|
|
843
|
-
const isTty = deps.isTty ?? (() => process.stdout.isTTY === true || process.stderr.isTTY === true);
|
|
844
|
-
const { command, args } = buildRestartSpawnSpec(projectRoot);
|
|
845
|
-
|
|
846
|
-
let stderrFd: number | undefined;
|
|
847
|
-
const stdio: StdioOptions = ["ignore", "ignore", "pipe"];
|
|
848
|
-
const tty = isTty();
|
|
849
|
-
if (tty) {
|
|
850
|
-
// 终端场景:全部 inherit(含 stdin)。注意 stdin 不能是 "ignore":
|
|
851
|
-
// Windows 上 detached + stdio[0]=ignore 的组合(DETACHED_PROCESS)会让
|
|
852
|
-
// 子进程丢失控制台关联,或在部分 Node/libuv 组合下触发 CREATE_NEW_CONSOLE
|
|
853
|
-
// 弹出新窗口——日志全跑到新窗口,用户当前窗口反而看不到。
|
|
854
|
-
// 全 inherit 让子进程直接复用当前终端句柄,父进程退出后窗口日志不中断,
|
|
855
|
-
// 且终端句柄不随父进程退出关闭,天然无 EPIPE 风险。
|
|
856
|
-
stdio[0] = "inherit";
|
|
857
|
-
stdio[1] = "inherit";
|
|
858
|
-
stdio[2] = "inherit";
|
|
859
|
-
} else {
|
|
860
|
-
try {
|
|
861
|
-
mkdirSync(restartLogDir, { recursive: true });
|
|
862
|
-
const timestamp = new Date().toISOString().replace(/[:.]/g, "-");
|
|
863
|
-
const restartLogPath = join(restartLogDir, `restart-${timestamp}.log`);
|
|
864
|
-
stderrFd = openSync(restartLogPath, "a");
|
|
865
|
-
stdio[2] = stderrFd;
|
|
866
|
-
} catch (err) {
|
|
867
|
-
trace("restart: stderr log open failed, falling back to pipe", {
|
|
868
|
-
error: err instanceof Error ? err.message : String(err),
|
|
869
|
-
});
|
|
870
|
-
}
|
|
871
|
-
}
|
|
872
|
-
|
|
873
|
-
// 运行时自检:记录本次 restart 子进程的启动方式,便于确认日志走向。
|
|
874
|
-
trace("restart: spawn child", {
|
|
875
|
-
isTty: tty,
|
|
876
|
-
stdio: JSON.stringify(stdio),
|
|
877
|
-
});
|
|
878
|
-
|
|
879
|
-
const child = spawnImpl(command, args, {
|
|
880
|
-
cwd: projectRoot,
|
|
881
|
-
detached: true,
|
|
882
|
-
stdio,
|
|
883
|
-
shell: false,
|
|
884
|
-
env: createInternalRestartEnv(),
|
|
885
|
-
});
|
|
886
|
-
// 子进程已继承 stderr 文件句柄;父进程关闭自己的副本,避免"子进程早退、
|
|
887
|
-
// 父进程留下继续服务"时 fd 泄漏。
|
|
888
|
-
if (stderrFd !== undefined) {
|
|
889
|
-
try { closeSync(stderrFd); } catch { /* ignore */ }
|
|
890
|
-
}
|
|
891
|
-
|
|
892
|
-
child.on("error", (err) => {
|
|
893
|
-
trace("restart: spawn error", { error: err.message });
|
|
894
|
-
});
|
|
895
|
-
child.on("exit", (code, signal) => {
|
|
896
|
-
trace("restart: child exit", {
|
|
897
|
-
childPid: child.pid,
|
|
898
|
-
code,
|
|
899
|
-
signal,
|
|
900
|
-
});
|
|
901
|
-
});
|
|
902
|
-
return child;
|
|
903
|
-
}
|
|
904
|
-
|
|
905
|
-
/**
|
|
906
|
-
* 决定父进程是否应退出(防空窗兜底):
|
|
907
|
-
* - 窗口内子进程已退出(死亡或信号终止)→ 返回 false,父进程留下继续服务;
|
|
908
|
-
* - 子进程存活满整个窗口 → 返回 true,父进程退出并把端口让给新进程。
|
|
909
|
-
*/
|
|
910
|
-
export async function decideRestartParentExit(
|
|
911
|
-
child: Pick<ChildProcess, "pid" | "exitCode" | "signalCode">,
|
|
912
|
-
timeoutMs: number,
|
|
913
|
-
pollMs = 500,
|
|
914
|
-
trace: typeof appendStartupTrace = appendStartupTrace,
|
|
915
|
-
): Promise<boolean> {
|
|
916
|
-
const deadline = Date.now() + timeoutMs;
|
|
917
|
-
while (Date.now() < deadline) {
|
|
918
|
-
if (child.exitCode !== null || child.signalCode !== null) {
|
|
919
|
-
trace("restart: child died during window, keeping parent", {
|
|
920
|
-
childPid: child.pid,
|
|
921
|
-
exitCode: child.exitCode,
|
|
922
|
-
signalCode: child.signalCode,
|
|
923
|
-
});
|
|
924
|
-
return false;
|
|
925
|
-
}
|
|
926
|
-
await new Promise((resolve) => setTimeout(resolve, pollMs));
|
|
927
|
-
}
|
|
928
|
-
trace("restart: child alive after window, parent exiting", { childPid: child.pid });
|
|
929
|
-
return true;
|
|
930
|
-
}
|
|
931
|
-
|
|
932
|
-
// ---------------------------------------------------------------------------
|
|
933
|
-
// handleCommand — 平台无关的命令分发
|
|
934
|
-
// ---------------------------------------------------------------------------
|
|
935
|
-
|
|
936
|
-
export async function handleCommand(
|
|
937
|
-
platform: PlatformAdapter,
|
|
938
|
-
text: string,
|
|
939
|
-
chatId: string,
|
|
940
|
-
openId: string,
|
|
941
|
-
msgTimestamp: number,
|
|
942
|
-
chatType = "group",
|
|
943
|
-
traceId?: string,
|
|
944
|
-
commandId?: string,
|
|
945
|
-
): Promise<void> {
|
|
946
|
-
const tid = traceId ?? makeTraceId();
|
|
947
|
-
const sharedPrefix = applySharedPrefix(text);
|
|
948
|
-
const promptText = sharedPrefix.text;
|
|
949
|
-
text = sharedPrefix.body;
|
|
950
|
-
const textLower = text.toLowerCase();
|
|
951
|
-
const isCommandText = !sharedPrefix.matched && textLower.startsWith("/");
|
|
952
|
-
recordChatPlatform(chatId, platform);
|
|
953
|
-
|
|
954
|
-
if (isCommandText && textLower === "/reload") {
|
|
955
|
-
logTrace(tid, "BRANCH", { cmd: "/reload" });
|
|
956
|
-
try {
|
|
957
|
-
const result = await reloadRuntimeConfig("chat-command");
|
|
958
|
-
await platform.sendText(
|
|
959
|
-
chatId,
|
|
960
|
-
[
|
|
961
|
-
"配置已重新加载。",
|
|
962
|
-
`默认 Agent: ${toolDisplayName(result.defaultAgent)}`,
|
|
963
|
-
`配置文件: ${result.configPath}`,
|
|
964
|
-
"后续新会话会使用最新配置;飞书私聊会在下一条普通消息时跟随默认 Agent,正在生成的会话不会被中断。",
|
|
965
|
-
].join("\n"),
|
|
966
|
-
).catch(() => {});
|
|
967
|
-
logTrace(tid, "DONE", { outcome: "reload", defaultAgent: result.defaultAgent });
|
|
968
|
-
} catch (err) {
|
|
969
|
-
await platform.sendText(chatId, `配置重载失败:${(err as Error).message}`).catch(() => {});
|
|
970
|
-
logTrace(tid, "DONE", { outcome: "reload_fail", error: (err as Error).message });
|
|
971
|
-
}
|
|
972
|
-
return;
|
|
973
|
-
}
|
|
974
|
-
|
|
975
|
-
if (isCommandText && textLower === "/restart") {
|
|
976
|
-
logTrace(tid, "BRANCH", { cmd: "/restart" });
|
|
977
|
-
await platform.sendText(chatId, "重启中...请几秒后发消息唤醒我").catch(() => {});
|
|
978
|
-
logTrace(tid, "DONE", { outcome: "restart" });
|
|
979
|
-
|
|
980
|
-
appendStartupTrace("restart: spawn begin", { fromPid: process.pid });
|
|
981
|
-
const child = spawnRestartChild();
|
|
982
|
-
child.unref();
|
|
983
|
-
|
|
984
|
-
// 子进程存活满窗口才退出父进程;若子进程在窗口内死亡,父进程留下继续
|
|
985
|
-
// 服务(防空窗),并已把子进程 stderr 写入 startup-trace 供排查。
|
|
986
|
-
void decideRestartParentExit(child, RESTART_CHILD_READY_MS).then((shouldExit) => {
|
|
987
|
-
if (!shouldExit) return;
|
|
988
|
-
appendStartupTrace("restart: parent exit", { childPid: child.pid });
|
|
989
|
-
process.exit(0);
|
|
990
|
-
});
|
|
991
|
-
return;
|
|
992
|
-
}
|
|
993
|
-
|
|
994
|
-
if (isCommandText && textLower === "/update") {
|
|
995
|
-
logTrace(tid, "BRANCH", { cmd: "/update" });
|
|
996
|
-
const isGlobal = isRunningFromGlobalNpm();
|
|
997
|
-
appendStartupTrace("update: command received", { isGlobal, chatId });
|
|
998
|
-
if (!isGlobal) {
|
|
999
|
-
await platform.sendText(chatId, "当前进程非 npm 全局安装,无法使用 /update 更新。请通过 npm install -g chatccc 安装后使用。").catch(() => {});
|
|
1000
|
-
logTrace(tid, "DONE", { outcome: "update_not_global" });
|
|
1001
|
-
return;
|
|
1002
|
-
}
|
|
1003
|
-
|
|
1004
|
-
// `/update` 会主动重启进程,内存 processedMessages 随之丢失。必须在发送
|
|
1005
|
-
// “正在更新”以及执行 npm 命令之前同步落盘,才能挡住新进程收到的飞书重投。
|
|
1006
|
-
// 该护栏只位于此分支,不改变普通消息和 `/restart` 的现有去重行为。
|
|
1007
|
-
const updateGuard = acquireUpdateCommandGuard({ commandId });
|
|
1008
|
-
appendStartupTrace("update: command guard checked", {
|
|
1009
|
-
allowed: updateGuard.allowed,
|
|
1010
|
-
reason: updateGuard.reason,
|
|
1011
|
-
hasCommandId: Boolean(commandId),
|
|
1012
|
-
});
|
|
1013
|
-
if (!updateGuard.allowed) {
|
|
1014
|
-
if (updateGuard.reason === "duplicate_id") {
|
|
1015
|
-
// 同一条飞书消息的重投静默丢弃,避免用户再次看到重复提示。
|
|
1016
|
-
logTrace(tid, "DONE", { outcome: "update_duplicate_id" });
|
|
1017
|
-
return;
|
|
1018
|
-
}
|
|
1019
|
-
await platform.sendText(
|
|
1020
|
-
chatId,
|
|
1021
|
-
"无法写入更新保护状态。为避免连续更新和重启,本次 /update 未执行。",
|
|
1022
|
-
).catch(() => {});
|
|
1023
|
-
logTrace(tid, "DONE", { outcome: "update_guard_write_failed" });
|
|
1024
|
-
return;
|
|
1025
|
-
}
|
|
1026
|
-
|
|
1027
|
-
await platform.sendText(chatId, "正在更新并重启,请稍候...").catch(() => {});
|
|
1028
|
-
logTrace(tid, "DONE", { outcome: "update" });
|
|
1029
|
-
appendStartupTrace("update: sync update begin", { fromPid: process.pid });
|
|
1030
|
-
const child = syncUpdateAndRestart();
|
|
1031
|
-
if (child) {
|
|
1032
|
-
// 子进程存活满窗口才退出父进程;若子进程在窗口内死亡,父进程留下继续
|
|
1033
|
-
// 服务(防空窗)。
|
|
1034
|
-
void decideRestartParentExit(child, RESTART_CHILD_READY_MS).then((shouldExit) => {
|
|
1035
|
-
if (!shouldExit) return;
|
|
1036
|
-
appendStartupTrace("update: parent exit", { childPid: child.pid });
|
|
1037
|
-
process.exit(0);
|
|
1038
|
-
});
|
|
1039
|
-
} else {
|
|
1040
|
-
// spawn 失败:没有子进程可等,给残留日志写入时间后退出
|
|
1041
|
-
setTimeout(() => process.exit(0), 2000);
|
|
1042
|
-
}
|
|
1043
|
-
return;
|
|
1044
|
-
}
|
|
1045
|
-
|
|
1046
|
-
if (isCommandText && textLower === "/usage") {
|
|
1047
|
-
const usageTarget = await resolveUsageTarget(chatId);
|
|
1048
|
-
const usageTool = usageTarget.tool;
|
|
1049
|
-
const avatarStatus = usageTarget.sessionId && isSessionRunning(usageTarget.sessionId) ? "busy" : "idle";
|
|
1050
|
-
logTrace(tid, "BRANCH", { cmd: "/usage", tool: usageTool });
|
|
1051
|
-
try {
|
|
1052
|
-
await sendUsageSummary(platform, chatId, usageTool, avatarStatus, usageTarget.sessionId);
|
|
1053
|
-
logTrace(tid, "DONE", { outcome: "usage", tool: usageTool });
|
|
1054
|
-
} catch (err) {
|
|
1055
|
-
await sendUsageError(platform, chatId, usageTool, err);
|
|
1056
|
-
logTrace(tid, "DONE", { outcome: "usage_fail", tool: usageTool, error: (err as Error).message });
|
|
1057
|
-
}
|
|
1058
|
-
return;
|
|
1059
|
-
}
|
|
1060
|
-
|
|
1061
|
-
if (isCommandText && (textLower === "/cd" || textLower.startsWith("/cd "))) {
|
|
1062
|
-
logTrace(tid, "BRANCH", {
|
|
1063
|
-
cmd: "/cd",
|
|
1064
|
-
arg: text.slice(3).trim() || "(none)",
|
|
1065
|
-
});
|
|
1066
|
-
const currentDir = await getDefaultCwd(chatId);
|
|
1067
|
-
|
|
1068
|
-
// 获取当前会话的实际工作路径(若在会话群内)
|
|
1069
|
-
let sessionCwd: string | undefined;
|
|
1070
|
-
try {
|
|
1071
|
-
const chatInfo = await platform.getChatInfo(chatId);
|
|
1072
|
-
const sessionInfoResult = platform.extractSessionInfo(
|
|
1073
|
-
chatInfo.description,
|
|
1074
|
-
);
|
|
1075
|
-
if (sessionInfoResult) {
|
|
1076
|
-
const adapter = getAdapterForTool(sessionInfoResult.tool, sessionInfoResult.sessionId);
|
|
1077
|
-
const info = await adapter.getSessionInfo(sessionInfoResult.sessionId);
|
|
1078
|
-
sessionCwd = info?.cwd;
|
|
1079
|
-
}
|
|
1080
|
-
} catch {
|
|
1081
|
-
/* 非会话群或获取失败,不显示 */
|
|
1082
|
-
}
|
|
1083
|
-
|
|
1084
|
-
const arg = text.slice(3).trim();
|
|
1085
|
-
|
|
1086
|
-
// Resolve target directory
|
|
1087
|
-
let targetDir: string;
|
|
1088
|
-
if (!arg) {
|
|
1089
|
-
targetDir = currentDir;
|
|
1090
|
-
} else if (arg === "..") {
|
|
1091
|
-
targetDir = dirname(currentDir);
|
|
1092
|
-
} else {
|
|
1093
|
-
targetDir = resolve(currentDir, arg);
|
|
1094
|
-
}
|
|
1095
|
-
|
|
1096
|
-
// Verify the target exists and is a directory
|
|
1097
|
-
try {
|
|
1098
|
-
const s = await stat(targetDir);
|
|
1099
|
-
if (!s.isDirectory()) {
|
|
1100
|
-
logTrace(tid, "DONE", { outcome: "cd_not_dir", targetDir });
|
|
1101
|
-
await platform.sendCard(
|
|
1102
|
-
chatId,
|
|
1103
|
-
"新会话工作路径",
|
|
1104
|
-
`路径存在但不是目录:\n\`${targetDir}\``,
|
|
1105
|
-
"red",
|
|
1106
|
-
);
|
|
1107
|
-
return;
|
|
1108
|
-
}
|
|
1109
|
-
} catch {
|
|
1110
|
-
logTrace(tid, "DONE", { outcome: "cd_not_found", targetDir });
|
|
1111
|
-
await platform.sendCard(
|
|
1112
|
-
chatId,
|
|
1113
|
-
"新会话工作路径",
|
|
1114
|
-
`路径不存在:\n\`${targetDir}\``,
|
|
1115
|
-
"red",
|
|
1116
|
-
);
|
|
1117
|
-
return;
|
|
1118
|
-
}
|
|
1119
|
-
|
|
1120
|
-
// Change working dir if user provided a path
|
|
1121
|
-
const isUpdate = !!arg && targetDir !== currentDir;
|
|
1122
|
-
if (isUpdate) {
|
|
1123
|
-
await setDefaultCwd(targetDir, chatId);
|
|
1124
|
-
await addRecentDir(targetDir);
|
|
1125
|
-
}
|
|
1126
|
-
|
|
1127
|
-
// Read directory entries
|
|
1128
|
-
let entries: string[];
|
|
1129
|
-
try {
|
|
1130
|
-
entries = await readdir(targetDir);
|
|
1131
|
-
} catch (err) {
|
|
1132
|
-
logTrace(tid, "DONE", {
|
|
1133
|
-
outcome: "cd_readdir_fail",
|
|
1134
|
-
error: (err as Error).message,
|
|
1135
|
-
});
|
|
1136
|
-
await platform.sendCard(
|
|
1137
|
-
chatId,
|
|
1138
|
-
"新会话工作路径",
|
|
1139
|
-
`无法读取目录:\n\`${targetDir}\`\n\n${(err as Error).message}`,
|
|
1140
|
-
"red",
|
|
1141
|
-
);
|
|
1142
|
-
return;
|
|
1143
|
-
}
|
|
1144
|
-
|
|
1145
|
-
// Sort: directories first, then files, alphabetically within each group
|
|
1146
|
-
const withStats: { name: string; isDir: boolean }[] = [];
|
|
1147
|
-
for (const name of entries) {
|
|
1148
|
-
try {
|
|
1149
|
-
const s = await stat(resolve(targetDir, name));
|
|
1150
|
-
withStats.push({ name, isDir: s.isDirectory() });
|
|
1151
|
-
} catch {
|
|
1152
|
-
withStats.push({ name, isDir: false });
|
|
1153
|
-
}
|
|
1154
|
-
}
|
|
1155
|
-
withStats.sort((a, b) => {
|
|
1156
|
-
if (a.isDir !== b.isDir) return a.isDir ? -1 : 1;
|
|
1157
|
-
return a.name.localeCompare(b.name);
|
|
1158
|
-
});
|
|
1159
|
-
|
|
1160
|
-
if (!arg) {
|
|
1161
|
-
// /cd 无参数:展示卡片(含最近使用路径按钮)
|
|
1162
|
-
const recentDirs = await getRecentDirs();
|
|
1163
|
-
const card = buildCdCard(targetDir, withStats, recentDirs, sessionCwd);
|
|
1164
|
-
const ok = await platform.sendRawCard(chatId, card);
|
|
1165
|
-
console.log(
|
|
1166
|
-
`[${ts()}] [CD] card sent, ok=${ok}, recentDirs=${recentDirs.length}`,
|
|
1167
|
-
);
|
|
1168
|
-
logTrace(tid, "DONE", { outcome: "cd_card", ok });
|
|
1169
|
-
} else {
|
|
1170
|
-
// /cd <path>:切换目录,发送文本卡片
|
|
1171
|
-
const content = buildCdContent(targetDir, withStats, isUpdate, sessionCwd);
|
|
1172
|
-
await platform.sendCard(chatId, "新会话工作路径", content, "blue");
|
|
1173
|
-
logTrace(tid, "DONE", { outcome: "cd_path", targetDir, isUpdate });
|
|
1174
|
-
|
|
1175
|
-
// 微信模式下,若用户没有活跃会话,自动创建新会话
|
|
1176
|
-
if (platform.kind === "wechat" && !sessionInfoMap.has(chatId)) {
|
|
1177
|
-
logTrace(tid, "BRANCH", { cmd: "/new", trigger: "auto_after_cd" });
|
|
1178
|
-
await handleCommand(platform, "/new", chatId, openId, msgTimestamp, chatType, traceId);
|
|
1179
|
-
}
|
|
1180
|
-
}
|
|
1181
|
-
return;
|
|
1182
|
-
}
|
|
1183
|
-
|
|
1184
|
-
if (isCommandText && (textLower === "/new" || textLower.startsWith("/new "))) {
|
|
1185
|
-
const toolArg = text.slice(5).trim().toLowerCase();
|
|
1186
|
-
const tool = toolArg || resolveDefaultAgentTool();
|
|
1187
|
-
logTrace(tid, "BRANCH", { cmd: "/new", tool });
|
|
1188
|
-
const validTools = ["claude", "cursor", "codex", "ccc"];
|
|
1189
|
-
if (!validTools.includes(tool)) {
|
|
1190
|
-
logTrace(tid, "DONE", { outcome: "new_invalid_tool", tool });
|
|
1191
|
-
await platform.sendCard(
|
|
1192
|
-
chatId,
|
|
1193
|
-
"Error",
|
|
1194
|
-
`未知的工具类型: "${toolArg}"。支持: claude (Claude Code), cursor (Cursor), codex (Codex), ccc (CCC Agent)。`,
|
|
1195
|
-
"red",
|
|
1196
|
-
);
|
|
1197
|
-
return;
|
|
1198
|
-
}
|
|
1199
|
-
const toolLabel = toolDisplayName(tool);
|
|
1200
|
-
|
|
1201
|
-
if (!openId) {
|
|
1202
|
-
logTrace(tid, "DONE", { outcome: "new_no_openid" });
|
|
1203
|
-
console.log(`[${ts()}] [WARN] Cannot get sender open_id`);
|
|
1204
|
-
await platform.sendCard(
|
|
1205
|
-
chatId,
|
|
1206
|
-
"Error",
|
|
1207
|
-
"Cannot identify sender.",
|
|
1208
|
-
"red",
|
|
1209
|
-
);
|
|
1210
|
-
return;
|
|
1211
|
-
}
|
|
1212
|
-
|
|
1213
|
-
let sessionId: string;
|
|
1214
|
-
let sessionCwd: string;
|
|
1215
|
-
try {
|
|
1216
|
-
const init = await initClaudeSession(tool, undefined, chatId);
|
|
1217
|
-
sessionId = init.sessionId;
|
|
1218
|
-
sessionCwd = init.cwd;
|
|
1219
|
-
console.log(
|
|
1220
|
-
`[${ts()}] [STEP 1/4] ${toolLabel} session created: ${sessionId} → OK`,
|
|
1221
|
-
);
|
|
1222
|
-
} catch (err) {
|
|
1223
|
-
console.error(`[${ts()}] [STEP 1/4] FAIL: ${(err as Error).message}`);
|
|
1224
|
-
logTrace(tid, "DONE", {
|
|
1225
|
-
outcome: "new_session_fail",
|
|
1226
|
-
error: (err as Error).message,
|
|
1227
|
-
});
|
|
1228
|
-
await platform.sendCard(
|
|
1229
|
-
chatId,
|
|
1230
|
-
"Error",
|
|
1231
|
-
`Failed to initialize ${toolLabel} session:\n${(err as Error).message}`,
|
|
1232
|
-
"red",
|
|
1233
|
-
);
|
|
1234
|
-
return;
|
|
1235
|
-
}
|
|
1236
|
-
|
|
1237
|
-
const cwd = sessionCwd;
|
|
1238
|
-
const initialName = sessionChatName("新会话", cwd);
|
|
1239
|
-
|
|
1240
|
-
// /new 的平台语义保持不同:微信在当前私聊新建 session;飞书显式
|
|
1241
|
-
// /new 始终创建独立群聊。飞书私聊自己的常驻 session 不在这里切换。
|
|
1242
|
-
if (chatType === "p2p" && platform.kind === "wechat") {
|
|
1243
|
-
// 先解绑旧 session(如果存在),避免旧 session 的 display loop
|
|
1244
|
-
// 继续往同一个 chat 推送内容(/newh 走 switchChatBinding 已有此逻辑,
|
|
1245
|
-
// 但 /new p2p 之前遗漏了解绑)。
|
|
1246
|
-
const oldRegistry = await loadSessionRegistryForBinding();
|
|
1247
|
-
const oldRecord = oldRegistry[chatId];
|
|
1248
|
-
if (oldRecord?.sessionId && oldRecord.sessionId !== sessionId) {
|
|
1249
|
-
unbindChatFromSession(oldRecord.sessionId, chatId);
|
|
1250
|
-
displayCards.delete(chatId);
|
|
1251
|
-
cancelQueuedMessage(oldRecord.sessionId);
|
|
1252
|
-
}
|
|
1253
|
-
bindChatToSession(sessionId, chatId);
|
|
1254
|
-
sessionInfoMap.set(chatId, {
|
|
1255
|
-
sessionId,
|
|
1256
|
-
turnCount: 0,
|
|
1257
|
-
lastContextTokens: 0,
|
|
1258
|
-
startTime: Date.now(),
|
|
1259
|
-
tool,
|
|
1260
|
-
});
|
|
1261
|
-
await setDefaultCwd(cwd, chatId);
|
|
1262
|
-
await recordSessionRegistry({
|
|
1263
|
-
chatId,
|
|
1264
|
-
sessionId,
|
|
1265
|
-
tool,
|
|
1266
|
-
chatType,
|
|
1267
|
-
chatName: initialName,
|
|
1268
|
-
turnCount: 0,
|
|
1269
|
-
startTime: Date.now(),
|
|
1270
|
-
running: false,
|
|
1271
|
-
});
|
|
1272
|
-
await saveSessionTool(sessionId, tool, initialName);
|
|
1273
|
-
await platform.sendCard(
|
|
1274
|
-
chatId,
|
|
1275
|
-
`${toolLabel} Session Ready`,
|
|
1276
|
-
`这是你的 **${toolLabel}** 私聊会话。\n\n` +
|
|
1277
|
-
`**Session ID:** ${sessionId}\n` +
|
|
1278
|
-
`**工作目录:** \`${cwd}\`\n\n` +
|
|
1279
|
-
`直接在这里发消息即可与 ${toolLabel} 对话。\n\n` +
|
|
1280
|
-
`发送 **/cd** 切换新建会话的默认目录。\n` +
|
|
1281
|
-
`发送 **/model** 查看或切换当前会话的模型。${fastHelpAfterModel(tool)}\n` +
|
|
1282
|
-
`发送 **/new** 创建新会话,**/newh** 重置当前会话(沿用工作目录)。\n` +
|
|
1283
|
-
`发送 **/sessions** 查看所有会话状态。\n` +
|
|
1284
|
-
`发送 \`/git <子命令>\` 在本会话工作目录执行 git,例如 \`/git status\`、\`/git log --oneline -n 5\`。` +
|
|
1285
|
-
usageHelpLine(tool),
|
|
1286
|
-
"green",
|
|
1287
|
-
);
|
|
1288
|
-
console.log(
|
|
1289
|
-
`[${ts()}] [NEW] P2P session created: ${sessionId} (${toolLabel})`,
|
|
1290
|
-
);
|
|
1291
|
-
logTrace(tid, "DONE", {
|
|
1292
|
-
outcome: "session_ready_p2p",
|
|
1293
|
-
chatId,
|
|
1294
|
-
sessionId,
|
|
1295
|
-
tool,
|
|
1296
|
-
});
|
|
1297
|
-
return;
|
|
1298
|
-
}
|
|
1299
|
-
|
|
1300
|
-
let newChatId: string;
|
|
1301
|
-
try {
|
|
1302
|
-
newChatId = await platform.createGroup(initialName, [openId]);
|
|
1303
|
-
console.log(
|
|
1304
|
-
`[${ts()}] [STEP 2/4] Created Feishu group: ${newChatId} → OK`,
|
|
1305
|
-
);
|
|
1306
|
-
} catch (err) {
|
|
1307
|
-
console.error(`[${ts()}] [STEP 2/4] FAIL: ${(err as Error).message}`);
|
|
1308
|
-
logTrace(tid, "DONE", {
|
|
1309
|
-
outcome: "new_group_fail",
|
|
1310
|
-
error: (err as Error).message,
|
|
1311
|
-
});
|
|
1312
|
-
await platform.sendCard(
|
|
1313
|
-
chatId,
|
|
1314
|
-
"Error",
|
|
1315
|
-
`Failed to create group:\n${(err as Error).message}`,
|
|
1316
|
-
"red",
|
|
1317
|
-
);
|
|
1318
|
-
return;
|
|
1319
|
-
}
|
|
1320
|
-
|
|
1321
|
-
try {
|
|
1322
|
-
const descPrefix = sessionPrefixForTool(tool);
|
|
1323
|
-
await platform.updateChatInfo(
|
|
1324
|
-
newChatId,
|
|
1325
|
-
initialName,
|
|
1326
|
-
`${descPrefix} ${sessionId}`,
|
|
1327
|
-
);
|
|
1328
|
-
console.log(
|
|
1329
|
-
`[${ts()}] [STEP 3/4] Renamed group → name="${initialName}" (${toolLabel}) → OK`,
|
|
1330
|
-
);
|
|
1331
|
-
} catch (err) {
|
|
1332
|
-
console.error(`[${ts()}] [STEP 3/4] FAIL: ${(err as Error).message}`);
|
|
1333
|
-
logTrace(tid, "DONE", {
|
|
1334
|
-
outcome: "new_rename_fail",
|
|
1335
|
-
error: (err as Error).message,
|
|
1336
|
-
});
|
|
1337
|
-
await platform.sendCard(
|
|
1338
|
-
chatId,
|
|
1339
|
-
"Error",
|
|
1340
|
-
`Group created but rename failed:\n${(err as Error).message}`,
|
|
1341
|
-
"yellow",
|
|
1342
|
-
);
|
|
1343
|
-
return;
|
|
1344
|
-
}
|
|
1345
|
-
|
|
1346
|
-
// 让新群的默认工作目录继承当前会话的 cwd
|
|
1347
|
-
await setDefaultCwd(cwd, newChatId);
|
|
1348
|
-
bindChatToSession(sessionId, newChatId);
|
|
1349
|
-
await recordSessionRegistry({
|
|
1350
|
-
chatId: newChatId,
|
|
1351
|
-
sessionId,
|
|
1352
|
-
tool,
|
|
1353
|
-
chatType: "group",
|
|
1354
|
-
chatName: initialName,
|
|
1355
|
-
turnCount: 0,
|
|
1356
|
-
startTime: Date.now(),
|
|
1357
|
-
running: false,
|
|
1358
|
-
});
|
|
1359
|
-
await saveSessionTool(sessionId, tool, initialName);
|
|
1360
|
-
|
|
1361
|
-
await platform.sendCard(
|
|
1362
|
-
newChatId,
|
|
1363
|
-
`${toolLabel} Session Ready`,
|
|
1364
|
-
`群聊已创建,这是你的 **${toolLabel}** 会话群。\n\n` +
|
|
1365
|
-
`**Session ID:** ${sessionId}\n` +
|
|
1366
|
-
`**工作目录:** \`${cwd}\`\n\n` +
|
|
1367
|
-
`直接在这里发消息即可与 ${toolLabel} 对话。\n\n` +
|
|
1368
|
-
`发送 **/cd** 切换新建会话的默认目录。\n` +
|
|
1369
|
-
`发送 **/model** 查看或切换当前会话的模型。${fastHelpAfterModel(tool)}\n` +
|
|
1370
|
-
`发送 **/new** 创建新会话,**/newh** 重置当前会话(沿用工作目录)。\n` +
|
|
1371
|
-
`发送 **/sessions** 查看所有会话状态。\n` +
|
|
1372
|
-
`发送 \`/git <子命令>\` 在本会话工作目录执行 git,例如 \`/git status\`、\`/git log --oneline -n 5\`。` +
|
|
1373
|
-
usageHelpLine(tool),
|
|
1374
|
-
"green",
|
|
1375
|
-
);
|
|
1376
|
-
|
|
1377
|
-
console.log(`[${ts()}] [STEP 4/4] Replied to new group → OK`);
|
|
1378
|
-
logTrace(tid, "DONE", {
|
|
1379
|
-
outcome: "session_ready",
|
|
1380
|
-
newChatId,
|
|
1381
|
-
sessionId,
|
|
1382
|
-
tool,
|
|
1383
|
-
});
|
|
1384
|
-
setChatAvatarForSession(platform, newChatId, tool, "new", sessionId).catch(() => {});
|
|
1385
|
-
console.log(`${"=".repeat(60)}`);
|
|
1386
|
-
return;
|
|
1387
|
-
}
|
|
1388
|
-
|
|
1389
|
-
// 检测会话上下文:群聊从 description 获取,飞书/微信私聊都从
|
|
1390
|
-
// session-registry 获取。私聊 chatId 是稳定容器,进程重启后仍恢复绑定。
|
|
1391
|
-
let sessionId: string | null = null;
|
|
1392
|
-
let descriptionTool: string | null = null;
|
|
1393
|
-
let toolLabel: string | null = null;
|
|
1394
|
-
let pendingFeishuP2pDefaultTool: AgentTool | null = null;
|
|
1395
|
-
let chatInfo: Awaited<ReturnType<PlatformAdapter["getChatInfo"]>> | undefined;
|
|
1396
|
-
let description: string | undefined;
|
|
1397
|
-
|
|
1398
|
-
if (chatType !== "p2p") {
|
|
1399
|
-
try {
|
|
1400
|
-
chatInfo = await platform.getChatInfo(chatId);
|
|
1401
|
-
description = chatInfo.description;
|
|
1402
|
-
const sessionInfo = platform.extractSessionInfo(description);
|
|
1403
|
-
if (sessionInfo) {
|
|
1404
|
-
sessionId = sessionInfo.sessionId;
|
|
1405
|
-
descriptionTool = sessionInfo.tool;
|
|
1406
|
-
toolLabel = toolDisplayName(descriptionTool);
|
|
1407
|
-
|
|
1408
|
-
// 群描述是群聊会话路由的权威来源。历史群可能早于 registry 创建,
|
|
1409
|
-
// 或在冷启动时没有被重建进内存映射;若只解析 sessionId 而不补绑定,
|
|
1410
|
-
// prompt 虽能启动,却找不到生成卡片目标,收尾也无法清除 running。
|
|
1411
|
-
const registry = await loadSessionRegistryForBinding();
|
|
1412
|
-
const record = registry[chatId];
|
|
1413
|
-
if (record?.sessionId && record.sessionId !== sessionId) {
|
|
1414
|
-
unbindChatFromSession(record.sessionId, chatId);
|
|
1415
|
-
}
|
|
1416
|
-
bindChatToSession(sessionId, chatId);
|
|
1417
|
-
|
|
1418
|
-
const memoryInfo = sessionInfoMap.get(chatId);
|
|
1419
|
-
if (!memoryInfo || memoryInfo.sessionId !== sessionId) {
|
|
1420
|
-
sessionInfoMap.set(chatId, {
|
|
1421
|
-
sessionId,
|
|
1422
|
-
tool: descriptionTool,
|
|
1423
|
-
turnCount: record?.sessionId === sessionId ? record.turnCount : 0,
|
|
1424
|
-
lastContextTokens:
|
|
1425
|
-
record?.sessionId === sessionId ? record.lastContextTokens : 0,
|
|
1426
|
-
startTime:
|
|
1427
|
-
record?.sessionId === sessionId
|
|
1428
|
-
? record.startTime
|
|
1429
|
-
: Date.now(),
|
|
1430
|
-
});
|
|
1431
|
-
}
|
|
1432
|
-
|
|
1433
|
-
// 同步自愈持久化记录,使下一次重启可以直接重建绑定。running 取实际
|
|
1434
|
-
// 内存状态,顺便修复旧故障遗留的 stale running=true。
|
|
1435
|
-
await recordSessionRegistry({
|
|
1436
|
-
chatId,
|
|
1437
|
-
sessionId,
|
|
1438
|
-
tool: descriptionTool,
|
|
1439
|
-
chatType,
|
|
1440
|
-
chatName: chatInfo.name,
|
|
1441
|
-
running: isSessionRunning(sessionId),
|
|
1442
|
-
});
|
|
1443
|
-
}
|
|
1444
|
-
} catch (err) {
|
|
1445
|
-
logTrace(tid, "BRANCH", {
|
|
1446
|
-
reason: "get_chat_info_failed",
|
|
1447
|
-
error: (err as Error).message,
|
|
1448
|
-
});
|
|
1449
|
-
console.log(
|
|
1450
|
-
`[${ts()}] [INFO] Cannot get chat info for ${chatId}: ${(err as Error).message}`,
|
|
1451
|
-
);
|
|
1452
|
-
}
|
|
1453
|
-
} else if (platform.kind === "wechat" || platform.kind === "feishu") {
|
|
1454
|
-
// 私聊没有可写的群描述,因此会话绑定只持久化在 session-registry.json。
|
|
1455
|
-
try {
|
|
1456
|
-
const registry = await loadSessionRegistryForBinding();
|
|
1457
|
-
const record = registry[chatId];
|
|
1458
|
-
// 旧版飞书曾把私聊视为建群入口,并会清理私聊 registry。没有 p2p
|
|
1459
|
-
// 标记的残留记录不能证明它是在固定用户目录创建的,因此只迁移一次:
|
|
1460
|
-
// 先解除旧绑定;若本条是普通消息,随后在用户目录创建新的私聊 session。
|
|
1461
|
-
if (platform.kind === "feishu" && record?.sessionId && record.chatType !== "p2p") {
|
|
1462
|
-
unbindChatFromSession(record.sessionId, chatId);
|
|
1463
|
-
displayCards.delete(chatId);
|
|
1464
|
-
cancelQueuedMessage(record.sessionId);
|
|
1465
|
-
sessionInfoMap.delete(chatId);
|
|
1466
|
-
await removeSessionRegistryRecord(chatId);
|
|
1467
|
-
logTrace(tid, "BRANCH", {
|
|
1468
|
-
reason: "migrate_legacy_feishu_p2p_binding",
|
|
1469
|
-
chatId,
|
|
1470
|
-
oldSessionId: record.sessionId,
|
|
1471
|
-
});
|
|
1472
|
-
} else if (record && record.sessionId && record.tool) {
|
|
1473
|
-
let resolvedRecord: { sessionId: string; tool: string } = record;
|
|
1474
|
-
if (platform.kind === "feishu" && !isCommandText && record.chatType === "p2p") {
|
|
1475
|
-
const resolution = await resolveFeishuP2pAgent(platform, chatId, text, record, tid);
|
|
1476
|
-
if (resolution.kind === "error") {
|
|
1477
|
-
const previousLabel = toolDisplayName(resolution.previousTool);
|
|
1478
|
-
const desiredLabel = toolDisplayName(resolution.desiredTool);
|
|
1479
|
-
console.error(
|
|
1480
|
-
`[${ts()}] [P2P-SWITCH] ${previousLabel} -> ${desiredLabel} FAIL: ${resolution.error.message}`,
|
|
1481
|
-
);
|
|
1482
|
-
logTrace(tid, "DONE", {
|
|
1483
|
-
outcome: "switch_feishu_p2p_default_agent_fail",
|
|
1484
|
-
oldTool: resolution.previousTool,
|
|
1485
|
-
newTool: resolution.desiredTool,
|
|
1486
|
-
error: resolution.error.message,
|
|
1487
|
-
});
|
|
1488
|
-
await platform.sendCard(
|
|
1489
|
-
chatId,
|
|
1490
|
-
"Agent 切换失败",
|
|
1491
|
-
`无法从 ${previousLabel} 切换到 ${desiredLabel}:\n${resolution.error.message}`,
|
|
1492
|
-
"red",
|
|
1493
|
-
).catch(() => {});
|
|
1494
|
-
return;
|
|
1495
|
-
}
|
|
1496
|
-
resolvedRecord = resolution;
|
|
1497
|
-
if (resolution.kind === "waiting") {
|
|
1498
|
-
pendingFeishuP2pDefaultTool = resolution.desiredTool;
|
|
1499
|
-
}
|
|
1500
|
-
}
|
|
1501
|
-
|
|
1502
|
-
sessionId = resolvedRecord.sessionId;
|
|
1503
|
-
descriptionTool = resolvedRecord.tool;
|
|
1504
|
-
toolLabel = toolDisplayName(descriptionTool);
|
|
1505
|
-
// 确保内存状态在冷启动后恢复;bindChatToSession 是幂等的。
|
|
1506
|
-
if (!sessionInfoMap.has(chatId)) {
|
|
1507
|
-
sessionInfoMap.set(chatId, {
|
|
1508
|
-
sessionId,
|
|
1509
|
-
turnCount: record.turnCount ?? 0,
|
|
1510
|
-
lastContextTokens: record.lastContextTokens ?? 0,
|
|
1511
|
-
startTime: record.startTime ?? Date.now(),
|
|
1512
|
-
tool: descriptionTool,
|
|
1513
|
-
});
|
|
1514
|
-
}
|
|
1515
|
-
bindChatToSession(sessionId, chatId);
|
|
1516
|
-
}
|
|
1517
|
-
} catch (err) {
|
|
1518
|
-
console.log(
|
|
1519
|
-
`[${ts()}] [INFO] Cannot load registry for p2p ${chatId}: ${(err as Error).message}`,
|
|
1520
|
-
);
|
|
1521
|
-
}
|
|
1522
|
-
}
|
|
1523
|
-
|
|
1524
|
-
if (sessionId && descriptionTool && toolLabel) {
|
|
1525
|
-
// 有会话上下文 — 路由到命令处理或 prompt
|
|
1526
|
-
logTrace(tid, "BRANCH", { sessionId, tool: descriptionTool });
|
|
1527
|
-
const routeKind = isCommandText ? "command" : "prompt";
|
|
1528
|
-
const chatKind = chatType === "p2p" ? "p2p chat" : "session group";
|
|
1529
|
-
console.log(
|
|
1530
|
-
`[${ts()}] [ROUTE] ${toolLabel} ${chatKind} ${routeKind} detected, session=${sessionId} tool=${descriptionTool}`,
|
|
1531
|
-
);
|
|
1532
|
-
|
|
1533
|
-
if (
|
|
1534
|
-
chatType !== "p2p" &&
|
|
1535
|
-
isUntitledSessionChatName(chatInfo!.name) &&
|
|
1536
|
-
!isCommandText
|
|
1537
|
-
) {
|
|
1538
|
-
const MAX_PREFIX = 10;
|
|
1539
|
-
const prefix = text.slice(0, MAX_PREFIX);
|
|
1540
|
-
const adapter = getAdapterForTool(descriptionTool, sessionId);
|
|
1541
|
-
const info = await adapter
|
|
1542
|
-
.getSessionInfo(sessionId)
|
|
1543
|
-
.catch(() => undefined);
|
|
1544
|
-
const sessionCwd = info?.cwd ?? (await getDefaultCwd(chatId));
|
|
1545
|
-
const newName = sessionChatName(prefix, sessionCwd);
|
|
1546
|
-
try {
|
|
1547
|
-
await platform.updateChatInfo(chatId, newName, description!);
|
|
1548
|
-
console.log(
|
|
1549
|
-
`[${ts()}] [RENAME] First message → group renamed to "${newName}"`,
|
|
1550
|
-
);
|
|
1551
|
-
await recordSessionRegistry({
|
|
1552
|
-
chatId,
|
|
1553
|
-
sessionId,
|
|
1554
|
-
tool: descriptionTool,
|
|
1555
|
-
chatName: newName,
|
|
1556
|
-
}).catch(() => {});
|
|
1557
|
-
await saveSessionTool(sessionId, descriptionTool, newName).catch(
|
|
1558
|
-
() => {},
|
|
1559
|
-
);
|
|
1560
|
-
} catch (err) {
|
|
1561
|
-
console.error(
|
|
1562
|
-
`[${ts()}] [RENAME] Failed: ${(err as Error).message}`,
|
|
1563
|
-
);
|
|
1564
|
-
}
|
|
1565
|
-
}
|
|
1566
|
-
|
|
1567
|
-
// P2P:首条非指令消息只更新 registry 中的展示名,不修改私聊信息。
|
|
1568
|
-
if (
|
|
1569
|
-
chatType === "p2p" &&
|
|
1570
|
-
(platform.kind === "wechat" || platform.kind === "feishu") &&
|
|
1571
|
-
!isCommandText
|
|
1572
|
-
) {
|
|
1573
|
-
try {
|
|
1574
|
-
const reg = await loadSessionRegistryForBinding();
|
|
1575
|
-
const rec = reg[chatId];
|
|
1576
|
-
if (
|
|
1577
|
-
rec &&
|
|
1578
|
-
rec.sessionId === sessionId &&
|
|
1579
|
-
isUntitledSessionChatName(rec.chatName ?? "")
|
|
1580
|
-
) {
|
|
1581
|
-
const MAX_PREFIX = 10;
|
|
1582
|
-
const prefix = text.slice(0, MAX_PREFIX);
|
|
1583
|
-
const adapter = getAdapterForTool(descriptionTool!, sessionId);
|
|
1584
|
-
const info = await adapter
|
|
1585
|
-
.getSessionInfo(sessionId)
|
|
1586
|
-
.catch(() => undefined);
|
|
1587
|
-
const sessionCwd =
|
|
1588
|
-
info?.cwd ?? (await getDefaultCwd(chatId));
|
|
1589
|
-
const newName2 = sessionChatName(prefix, sessionCwd);
|
|
1590
|
-
await recordSessionRegistry({
|
|
1591
|
-
chatId,
|
|
1592
|
-
sessionId,
|
|
1593
|
-
tool: descriptionTool!,
|
|
1594
|
-
chatName: newName2,
|
|
1595
|
-
}).catch(() => {});
|
|
1596
|
-
await saveSessionTool(sessionId, descriptionTool!, newName2).catch(
|
|
1597
|
-
() => {},
|
|
1598
|
-
);
|
|
1599
|
-
console.log(
|
|
1600
|
-
`[${ts()}] [RENAME] ${platform.kind} P2P → "${newName2}"`,
|
|
1601
|
-
);
|
|
1602
|
-
}
|
|
1603
|
-
} catch (err) {
|
|
1604
|
-
console.error(
|
|
1605
|
-
`[${ts()}] [RENAME] ${platform.kind} P2P failed: ${(err as Error).message}`,
|
|
1606
|
-
);
|
|
1607
|
-
}
|
|
1608
|
-
}
|
|
1609
|
-
|
|
1610
|
-
if (isCommandText && textLower === "/stop") {
|
|
1611
|
-
logTrace(tid, "BRANCH", { cmd: "/stop" });
|
|
1612
|
-
if (stopSession(sessionId)) {
|
|
1613
|
-
console.log(`[${ts()}] [STOP] User sent /stop, session=${sessionId}`);
|
|
1614
|
-
logTrace(tid, "DONE", { outcome: "stop_requested" });
|
|
1615
|
-
} else {
|
|
1616
|
-
await platform
|
|
1617
|
-
.sendText(chatId, "当前没有正在进行的会话。")
|
|
1618
|
-
.catch(() => {});
|
|
1619
|
-
logTrace(tid, "DONE", { outcome: "stop_no_session" });
|
|
1620
|
-
}
|
|
1621
|
-
return;
|
|
1622
|
-
}
|
|
1623
|
-
|
|
1624
|
-
if (isCommandText && textLower === "/cancel") {
|
|
1625
|
-
logTrace(tid, "BRANCH", { cmd: "/cancel" });
|
|
1626
|
-
if (cancelQueuedMessage(sessionId)) {
|
|
1627
|
-
console.log(`[${ts()}] [CANCEL] Queue cancelled for session=${sessionId}`);
|
|
1628
|
-
await platform.sendText(chatId, "已取消缓存队列中的消息。").catch(() => {});
|
|
1629
|
-
logTrace(tid, "DONE", { outcome: "cancelled" });
|
|
1630
|
-
} else {
|
|
1631
|
-
await platform.sendText(chatId, "当前缓存队列中没有消息。").catch(() => {});
|
|
1632
|
-
logTrace(tid, "DONE", { outcome: "cancel_no_queue" });
|
|
1633
|
-
}
|
|
1634
|
-
return;
|
|
1635
|
-
}
|
|
1636
|
-
|
|
1637
|
-
if (isCommandText && textLower === "/test") {
|
|
1638
|
-
logTrace(tid, "BRANCH", { cmd: "/test" });
|
|
1639
|
-
const tableHeaders = ["名称", "版本", "状态"];
|
|
1640
|
-
const tableRows = [
|
|
1641
|
-
["ChatCCC", "0.2.96", "运行中"],
|
|
1642
|
-
["Claude SDK", "0.50.0", "已连接"],
|
|
1643
|
-
["Feishu API", "v1", "正常"],
|
|
1644
|
-
];
|
|
1645
|
-
const mdTable = [
|
|
1646
|
-
`| ${tableHeaders.join(" | ")} |`,
|
|
1647
|
-
`| ${tableHeaders.map(() => "---").join(" | ")} |`,
|
|
1648
|
-
...tableRows.map((row) => `| ${row.join(" | ")} |`),
|
|
1649
|
-
].join("\n");
|
|
1650
|
-
|
|
1651
|
-
if (platform.kind === "feishu") {
|
|
1652
|
-
try {
|
|
1653
|
-
const token = await getTenantAccessToken();
|
|
1654
|
-
const postContent: unknown[][] = [
|
|
1655
|
-
// 先尝试富文本表格
|
|
1656
|
-
[{ tag: "table", cells: [tableHeaders, ...tableRows] }],
|
|
1657
|
-
// 再用代码块包起来
|
|
1658
|
-
[{ tag: "text", text: `\n表格(代码块格式):\n\`\`\`\n${mdTable}\n\`\`\`` }],
|
|
1659
|
-
];
|
|
1660
|
-
await sendPostMessage(token, chatId, "测试表格", postContent);
|
|
1661
|
-
} catch (err) {
|
|
1662
|
-
console.error(`[${ts()}] [TEST] post message failed: ${(err as Error).message}`);
|
|
1663
|
-
// Fallback to markdown card
|
|
1664
|
-
await platform.sendText(chatId, `表格(代码块格式):\n\`\`\`\n${mdTable}\n\`\`\``).catch(() => {});
|
|
1665
|
-
}
|
|
1666
|
-
} else {
|
|
1667
|
-
// WeChat / other platforms: just send code block
|
|
1668
|
-
await platform.sendText(chatId, `表格(代码块格式):\n\`\`\`\n${mdTable}\n\`\`\``).catch(() => {});
|
|
1669
|
-
}
|
|
1670
|
-
logTrace(tid, "DONE", { outcome: "test" });
|
|
1671
|
-
return;
|
|
1672
|
-
}
|
|
1673
|
-
|
|
1674
|
-
if (isCommandText && textLower === "/state") {
|
|
1675
|
-
logTrace(tid, "BRANCH", { cmd: "/state" });
|
|
1676
|
-
await sendStateCard(platform, chatId, sessionId, toolLabel, tid);
|
|
1677
|
-
return;
|
|
1678
|
-
}
|
|
1679
|
-
|
|
1680
|
-
if (isCommandText && textLower === "/sessions") {
|
|
1681
|
-
logTrace(tid, "BRANCH", { cmd: "/sessions" });
|
|
1682
|
-
const allSessions = await getAllSessionsStatus();
|
|
1683
|
-
const now = Date.now();
|
|
1684
|
-
const cardData = allSessions.map((s) => ({
|
|
1685
|
-
sessionId: s.sessionId,
|
|
1686
|
-
chatName: s.chatName,
|
|
1687
|
-
chatId: s.chatId,
|
|
1688
|
-
chatType: s.chatType,
|
|
1689
|
-
active: s.active,
|
|
1690
|
-
turnCount: s.turnCount,
|
|
1691
|
-
elapsedSeconds: s.active
|
|
1692
|
-
? Math.floor((now - s.startTime) / 1000)
|
|
1693
|
-
: null,
|
|
1694
|
-
model: s.model,
|
|
1695
|
-
tool: s.tool,
|
|
1696
|
-
}));
|
|
1697
|
-
const card = buildSessionsCard(cardData, {
|
|
1698
|
-
defaultToolLabel: toolDisplayName(resolveDefaultAgentTool()),
|
|
1699
|
-
fixedPrivateSession: isFeishuP2p(platform, chatType),
|
|
1700
|
-
});
|
|
1701
|
-
const ok = await platform.sendRawCard(chatId, card);
|
|
1702
|
-
console.log(
|
|
1703
|
-
`[${ts()}] [SESSIONS] card sent, ok=${ok}, count=${cardData.length}`,
|
|
1704
|
-
);
|
|
1705
|
-
logTrace(tid, "DONE", { outcome: "sessions", ok, count: cardData.length });
|
|
1706
|
-
return;
|
|
1707
|
-
}
|
|
1708
|
-
|
|
1709
|
-
if (isCommandText && textLower === "/newh") {
|
|
1710
|
-
logTrace(tid, "BRANCH", { cmd: "/newh" });
|
|
1711
|
-
let cwd: string;
|
|
1712
|
-
if (isFeishuP2p(platform, chatType)) {
|
|
1713
|
-
// 飞书私聊不支持切换工作目录。即使 /cd 已为后续 /new 群聊
|
|
1714
|
-
// 保存了其他默认目录,/newh 仍必须在运行 ChatCCC 的用户目录重建。
|
|
1715
|
-
cwd = homedir();
|
|
1716
|
-
} else {
|
|
1717
|
-
const adapter = getAdapterForTool(descriptionTool, sessionId);
|
|
1718
|
-
try {
|
|
1719
|
-
const info = await adapter.getSessionInfo(sessionId);
|
|
1720
|
-
cwd = info?.cwd ?? (await getDefaultCwd(chatId));
|
|
1721
|
-
} catch {
|
|
1722
|
-
cwd = await getDefaultCwd(chatId);
|
|
1723
|
-
}
|
|
1724
|
-
}
|
|
1725
|
-
|
|
1726
|
-
// 第一步:创建新 session(此时尚未碰任何内存绑定,失败可直接返回,
|
|
1727
|
-
// 旧 session 状态完全保留)。
|
|
1728
|
-
let newSessionId: string;
|
|
1729
|
-
try {
|
|
1730
|
-
const init = await initClaudeSession(descriptionTool, cwd);
|
|
1731
|
-
newSessionId = init.sessionId;
|
|
1732
|
-
} catch (err) {
|
|
1733
|
-
logTrace(tid, "DONE", {
|
|
1734
|
-
outcome: "newh_session_fail",
|
|
1735
|
-
error: (err as Error).message,
|
|
1736
|
-
});
|
|
1737
|
-
await platform.sendCard(
|
|
1738
|
-
chatId,
|
|
1739
|
-
"Error",
|
|
1740
|
-
`Failed to create new session:\n${(err as Error).message}`,
|
|
1741
|
-
"red",
|
|
1742
|
-
);
|
|
1743
|
-
return;
|
|
1744
|
-
}
|
|
1745
|
-
|
|
1746
|
-
// 第二步:事务式切换 chat 绑定
|
|
1747
|
-
const descPrefix = sessionPrefixForTool(descriptionTool);
|
|
1748
|
-
const newName = sessionChatName("新会话", cwd);
|
|
1749
|
-
const switchResult = await switchChatBinding({
|
|
1750
|
-
chatId,
|
|
1751
|
-
chatType,
|
|
1752
|
-
oldSessionId: sessionId,
|
|
1753
|
-
newSessionId,
|
|
1754
|
-
tool: descriptionTool,
|
|
1755
|
-
chatName: newName,
|
|
1756
|
-
newDescription: `${descPrefix} ${newSessionId}`,
|
|
1757
|
-
updateChatInfoFn: (cid, name, desc) =>
|
|
1758
|
-
platform.updateChatInfo(cid, name, desc),
|
|
1759
|
-
});
|
|
1760
|
-
if (!switchResult.ok) {
|
|
1761
|
-
logTrace(tid, "DONE", {
|
|
1762
|
-
outcome: "newh_update_chat_fail",
|
|
1763
|
-
error: switchResult.error?.message,
|
|
1764
|
-
});
|
|
1765
|
-
await platform.sendCard(
|
|
1766
|
-
chatId,
|
|
1767
|
-
"Error",
|
|
1768
|
-
`更新群描述失败,会话未切换(新 session 已创建但未启用):\n${switchResult.error?.message}`,
|
|
1769
|
-
"red",
|
|
1770
|
-
);
|
|
1771
|
-
return;
|
|
1772
|
-
}
|
|
1773
|
-
if (chatType !== "p2p") {
|
|
1774
|
-
console.log(
|
|
1775
|
-
`[${ts()}] [NEWH] Group updated: name="${newName}" desc="${descPrefix} ${newSessionId}"`,
|
|
1776
|
-
);
|
|
1777
|
-
}
|
|
1778
|
-
|
|
1779
|
-
setChatAvatarForSession(platform, chatId, descriptionTool, "new", newSessionId).catch(() => {});
|
|
1780
|
-
|
|
1781
|
-
await platform.sendCard(
|
|
1782
|
-
chatId,
|
|
1783
|
-
`${toolLabel} Session Reset`,
|
|
1784
|
-
`会话已重置为新的 **${toolLabel}** 会话。\n\n` +
|
|
1785
|
-
`**Session ID:** ${newSessionId}\n` +
|
|
1786
|
-
`**工作目录:** \`${cwd}\`${isFeishuP2p(platform, chatType) ? "(飞书私聊固定使用系统用户目录)" : "(沿用当前会话目录)"}\n\n` +
|
|
1787
|
-
`直接在这里发消息即可继续对话。\n` +
|
|
1788
|
-
`发送 **/cd** 可切换新建会话的默认目录。\n` +
|
|
1789
|
-
`发送 **/model** 查看或切换当前会话的模型。${fastHelpAfterModel(descriptionTool)}`,
|
|
1790
|
-
"green",
|
|
1791
|
-
);
|
|
1792
|
-
|
|
1793
|
-
console.log(
|
|
1794
|
-
`[${ts()}] [NEWH] Session ${sessionId} → ${newSessionId} (same cwd=${cwd})`,
|
|
1795
|
-
);
|
|
1796
|
-
logTrace(tid, "DONE", { outcome: "newh", newSessionId, cwd });
|
|
1797
|
-
return;
|
|
1798
|
-
}
|
|
1799
|
-
|
|
1800
|
-
if (isCommandText && textLower === "/deleteg") {
|
|
1801
|
-
logTrace(tid, "BRANCH", { cmd: "/deleteg" });
|
|
1802
|
-
if (chatType === "p2p") {
|
|
1803
|
-
await platform
|
|
1804
|
-
.sendText(chatId, "私聊无法使用 /deleteg,该指令仅用于群聊。")
|
|
1805
|
-
.catch(() => {});
|
|
1806
|
-
logTrace(tid, "DONE", { outcome: "deleteg_p2p" });
|
|
1807
|
-
return;
|
|
1808
|
-
}
|
|
1809
|
-
console.log(
|
|
1810
|
-
`[${ts()}] [DELETEG] Disbanding group chat ${chatId}, session=${sessionId}`,
|
|
1811
|
-
);
|
|
1812
|
-
|
|
1813
|
-
// 先解绑 session(不删除 Agent 会话)
|
|
1814
|
-
unbindChatFromSession(sessionId, chatId);
|
|
1815
|
-
displayCards.delete(chatId);
|
|
1816
|
-
sessionInfoMap.delete(chatId);
|
|
1817
|
-
await removeSessionRegistryRecord(chatId);
|
|
1818
|
-
|
|
1819
|
-
await platform
|
|
1820
|
-
.sendText(chatId, "群聊已解散,Agent 会话保留。")
|
|
1821
|
-
.catch(() => {});
|
|
1822
|
-
|
|
1823
|
-
// 解散群聊
|
|
1824
|
-
try {
|
|
1825
|
-
await platform.disbandChat(chatId);
|
|
1826
|
-
console.log(`[${ts()}] [DELETEG] Group disbanded: ${chatId}`);
|
|
1827
|
-
} catch (err) {
|
|
1828
|
-
console.error(
|
|
1829
|
-
`[${ts()}] [DELETEG] Disband API failed: ${(err as Error).message}`,
|
|
1830
|
-
);
|
|
1831
|
-
}
|
|
1832
|
-
|
|
1833
|
-
logTrace(tid, "DONE", { outcome: "deleteg", chatId, sessionId });
|
|
1834
|
-
return;
|
|
1835
|
-
}
|
|
1836
|
-
|
|
1837
|
-
// /session <number>:切换到 /sessions 列表中的指定会话
|
|
1838
|
-
const sessionMatch = isCommandText ? textLower.match(/^\/session\s+(\d+)$/) : null;
|
|
1839
|
-
if (sessionMatch) {
|
|
1840
|
-
// 飞书私聊有自己唯一的常驻 session;历史群聊 session 只能回到对应群聊
|
|
1841
|
-
// 继续,禁止通过 /session 把它们重新绑定进私聊。
|
|
1842
|
-
if (isFeishuP2p(platform, chatType)) {
|
|
1843
|
-
await platform.sendCard(
|
|
1844
|
-
chatId,
|
|
1845
|
-
"/session",
|
|
1846
|
-
"飞书私聊不能通过 /session 切换到历史群聊会话;它会在下一条普通消息时跟随默认 Agent。请回到对应群聊继续,或发送 /new 新建群聊。",
|
|
1847
|
-
"yellow",
|
|
1848
|
-
);
|
|
1849
|
-
logTrace(tid, "DONE", { outcome: "session_switch_disabled_feishu_p2p" });
|
|
1850
|
-
return;
|
|
1851
|
-
}
|
|
1852
|
-
|
|
1853
|
-
const index = parseInt(sessionMatch[1], 10) - 1;
|
|
1854
|
-
logTrace(tid, "BRANCH", { cmd: "/session", index: index + 1 });
|
|
1855
|
-
const allSessions = await getAllSessionsStatus();
|
|
1856
|
-
const claudeOrdered = allSessions.filter(
|
|
1857
|
-
(s) => s.tool !== "cursor" && s.tool !== "codex",
|
|
1858
|
-
);
|
|
1859
|
-
const cursorOrdered = allSessions.filter((s) => s.tool === "cursor");
|
|
1860
|
-
const codexOrdered = allSessions.filter((s) => s.tool === "codex");
|
|
1861
|
-
const ordered = [
|
|
1862
|
-
...claudeOrdered,
|
|
1863
|
-
...cursorOrdered,
|
|
1864
|
-
...codexOrdered,
|
|
1865
|
-
];
|
|
1866
|
-
if (ordered.length === 0) {
|
|
1867
|
-
await platform.sendCard(
|
|
1868
|
-
chatId,
|
|
1869
|
-
"/session",
|
|
1870
|
-
"暂无历史会话。",
|
|
1871
|
-
"yellow",
|
|
1872
|
-
);
|
|
1873
|
-
logTrace(tid, "DONE", { outcome: "session_no_sessions" });
|
|
1874
|
-
return;
|
|
1875
|
-
}
|
|
1876
|
-
if (index < 0 || index >= ordered.length) {
|
|
1877
|
-
await platform.sendCard(
|
|
1878
|
-
chatId,
|
|
1879
|
-
"/session",
|
|
1880
|
-
`序号超出范围,当前共 ${ordered.length} 个会话。`,
|
|
1881
|
-
"yellow",
|
|
1882
|
-
);
|
|
1883
|
-
logTrace(tid, "DONE", {
|
|
1884
|
-
outcome: "session_out_of_range",
|
|
1885
|
-
index: index + 1,
|
|
1886
|
-
total: ordered.length,
|
|
1887
|
-
});
|
|
1888
|
-
return;
|
|
1889
|
-
}
|
|
1890
|
-
const target = ordered[index];
|
|
1891
|
-
|
|
1892
|
-
// 切换到当前已在使用的会话:no-op,避免解绑再重绑的抖动
|
|
1893
|
-
if (target.sessionId === sessionId) {
|
|
1894
|
-
await platform.sendCard(
|
|
1895
|
-
chatId,
|
|
1896
|
-
"/session",
|
|
1897
|
-
"已经是当前会话。",
|
|
1898
|
-
"green",
|
|
1899
|
-
);
|
|
1900
|
-
logTrace(tid, "DONE", { outcome: "session_already_current", sessionId });
|
|
1901
|
-
return;
|
|
1902
|
-
}
|
|
1903
|
-
|
|
1904
|
-
const targetAdapter = getAdapterForTool(target.tool, target.sessionId);
|
|
1905
|
-
let cwd2: string;
|
|
1906
|
-
try {
|
|
1907
|
-
const targetInfo = await targetAdapter.getSessionInfo(
|
|
1908
|
-
target.sessionId,
|
|
1909
|
-
);
|
|
1910
|
-
cwd2 = targetInfo?.cwd ?? (await getDefaultCwd(chatId));
|
|
1911
|
-
} catch {
|
|
1912
|
-
cwd2 = await getDefaultCwd(chatId);
|
|
1913
|
-
}
|
|
1914
|
-
|
|
1915
|
-
const descPrefix2 = sessionPrefixForTool(target.tool);
|
|
1916
|
-
const newName2 = target.chatName || sessionChatName("新会话", cwd2);
|
|
1917
|
-
const switchResult = await switchChatBinding({
|
|
1918
|
-
chatId,
|
|
1919
|
-
chatType,
|
|
1920
|
-
oldSessionId: sessionId,
|
|
1921
|
-
newSessionId: target.sessionId,
|
|
1922
|
-
tool: target.tool,
|
|
1923
|
-
chatName: newName2,
|
|
1924
|
-
newDescription: `${descPrefix2} ${target.sessionId}`,
|
|
1925
|
-
initialTurnCount: target.turnCount,
|
|
1926
|
-
initialContextTokens: 0,
|
|
1927
|
-
updateChatInfoFn: (cid, name, desc) =>
|
|
1928
|
-
platform.updateChatInfo(cid, name, desc),
|
|
1929
|
-
});
|
|
1930
|
-
if (!switchResult.ok) {
|
|
1931
|
-
logTrace(tid, "DONE", {
|
|
1932
|
-
outcome: "session_update_chat_fail",
|
|
1933
|
-
error: switchResult.error?.message,
|
|
1934
|
-
});
|
|
1935
|
-
await platform.sendCard(
|
|
1936
|
-
chatId,
|
|
1937
|
-
"Error",
|
|
1938
|
-
`更新群描述失败,会话未切换:\n${switchResult.error?.message}`,
|
|
1939
|
-
"red",
|
|
1940
|
-
);
|
|
1941
|
-
return;
|
|
1942
|
-
}
|
|
1943
|
-
if (chatType !== "p2p") {
|
|
1944
|
-
console.log(
|
|
1945
|
-
`[${ts()}] [SESSION] Switched to session ${target.sessionId} (#${index + 1}), name="${newName2}"`,
|
|
1946
|
-
);
|
|
1947
|
-
}
|
|
1948
|
-
|
|
1949
|
-
setChatAvatarForSession(platform, chatId, target.tool, "new", target.sessionId).catch(() => {});
|
|
1950
|
-
|
|
1951
|
-
const targetToolLabel = toolDisplayName(target.tool);
|
|
1952
|
-
const busyNote = isSessionRunning(target.sessionId)
|
|
1953
|
-
? "\n\n⚠️ 该会话当前正在生成中,请等待完成后再发送消息。"
|
|
1954
|
-
: "";
|
|
1955
|
-
await platform.sendCard(
|
|
1956
|
-
chatId,
|
|
1957
|
-
`${targetToolLabel} Session Switched`,
|
|
1958
|
-
`已切换到 **${targetToolLabel}** 会话。\n\n` +
|
|
1959
|
-
`**序号:** ${index + 1}\n` +
|
|
1960
|
-
`**Session ID:** ${target.sessionId}\n` +
|
|
1961
|
-
`**工作目录:** \`${cwd2}\`\n\n` +
|
|
1962
|
-
`直接在这里发消息即可继续对话。\n` +
|
|
1963
|
-
`发送 **/model** 查看或切换当前会话的模型。${fastHelpAfterModel(descriptionTool)}${busyNote}`,
|
|
1964
|
-
"green",
|
|
1965
|
-
);
|
|
1966
|
-
|
|
1967
|
-
logTrace(tid, "DONE", {
|
|
1968
|
-
outcome: "session_switch",
|
|
1969
|
-
sessionId: target.sessionId,
|
|
1970
|
-
index: index + 1,
|
|
1971
|
-
cwd: cwd2,
|
|
1972
|
-
});
|
|
1973
|
-
return;
|
|
1974
|
-
}
|
|
1975
|
-
|
|
1976
|
-
if (isCommandText && (textLower === "/fast" || textLower.startsWith("/fast "))) {
|
|
1977
|
-
const fastArg = text.slice(5).trim().toLowerCase();
|
|
1978
|
-
logTrace(tid, "BRANCH", { cmd: "/fast", arg: fastArg, sessionId, tool: descriptionTool });
|
|
1979
|
-
|
|
1980
|
-
if (descriptionTool !== "codex") {
|
|
1981
|
-
const msg = `当前 ${toolLabel} 会话不支持 Fast 模式;/fast 仅适用于 Codex。`;
|
|
1982
|
-
await (platform.kind === "wechat"
|
|
1983
|
-
? platform.sendText(chatId, msg)
|
|
1984
|
-
: platform.sendCard(chatId, "Codex Fast 模式", msg, "yellow")
|
|
1985
|
-
).catch(() => {});
|
|
1986
|
-
logTrace(tid, "DONE", { outcome: "fast_unsupported", tool: descriptionTool });
|
|
1987
|
-
return;
|
|
1988
|
-
}
|
|
1989
|
-
|
|
1990
|
-
if (fastArg && fastArg !== "on" && fastArg !== "off") {
|
|
1991
|
-
const msg = "用法: /fast、/fast on 或 /fast off";
|
|
1992
|
-
await (platform.kind === "wechat"
|
|
1993
|
-
? platform.sendText(chatId, msg)
|
|
1994
|
-
: platform.sendCard(chatId, "Codex Fast 模式", msg, "yellow")
|
|
1995
|
-
).catch(() => {});
|
|
1996
|
-
logTrace(tid, "DONE", { outcome: "fast_invalid", arg: fastArg });
|
|
1997
|
-
return;
|
|
1998
|
-
}
|
|
1999
|
-
|
|
2000
|
-
if (fastArg) {
|
|
2001
|
-
setSessionFastModeOverride(sessionId, fastArg === "on");
|
|
2002
|
-
}
|
|
2003
|
-
const enabled = getEffectiveFastModeForTool("codex", sessionId);
|
|
2004
|
-
await sendFastModeStatus(platform, chatId, enabled).catch(() => {});
|
|
2005
|
-
if (fastArg) {
|
|
2006
|
-
const avatarStatus = isSessionRunning(sessionId) ? "busy" : "idle";
|
|
2007
|
-
await platform.setChatAvatar(chatId, "codex", avatarStatus, { fastMode: enabled }).catch((err) => {
|
|
2008
|
-
console.warn(`[${ts()}] [AVATAR] Fast mode refresh failed: chatId=${chatId} ${(err as Error).message}`);
|
|
2009
|
-
});
|
|
2010
|
-
}
|
|
2011
|
-
logTrace(tid, "DONE", {
|
|
2012
|
-
outcome: fastArg ? "fast_switched" : "fast_query",
|
|
2013
|
-
enabled,
|
|
2014
|
-
sessionId,
|
|
2015
|
-
});
|
|
2016
|
-
return;
|
|
2017
|
-
}
|
|
2018
|
-
|
|
2019
|
-
// /model clear — 清除当前 session 的模型覆盖
|
|
2020
|
-
if (isCommandText && textLower === "/model clear") {
|
|
2021
|
-
logTrace(tid, "BRANCH", { cmd: "/model clear", sessionId });
|
|
2022
|
-
clearSessionModelOverride(sessionId);
|
|
2023
|
-
const defaultModel = getEffectiveModelForTool(descriptionTool);
|
|
2024
|
-
const toolLabel = toolDisplayName(descriptionTool);
|
|
2025
|
-
const msg = `已清除当前 ${toolLabel} 会话的模型覆盖,恢复使用: \`${defaultModel || "(未指定)"}\``;
|
|
2026
|
-
await (platform.kind === "wechat"
|
|
2027
|
-
? platform.sendText(chatId, msg)
|
|
2028
|
-
: platform.sendCard(chatId, "模型切换", msg, "green")
|
|
2029
|
-
).catch(() => {});
|
|
2030
|
-
logTrace(tid, "DONE", { outcome: "model_cleared", sessionId, tool: descriptionTool });
|
|
2031
|
-
return;
|
|
2032
|
-
}
|
|
2033
|
-
|
|
2034
|
-
// /model <name> — 切换当前 session 的模型(支持所有 agent,模糊匹配)
|
|
2035
|
-
if (isCommandText && textLower.startsWith("/model ")) {
|
|
2036
|
-
const modelArg = text.slice(7).trim();
|
|
2037
|
-
if (!modelArg) return; // 纯 "/model " 不处理,交给上面的 /model 分支
|
|
2038
|
-
logTrace(tid, "BRANCH", { cmd: "/model", arg: modelArg, sessionId, tool: descriptionTool });
|
|
2039
|
-
|
|
2040
|
-
const models = getAllModelsForTool(descriptionTool as AgentTool);
|
|
2041
|
-
const toolLabel = toolDisplayName(descriptionTool);
|
|
2042
|
-
|
|
2043
|
-
// 查找目标模型:精确匹配优先,否则子串匹配(模型名越短越优先)
|
|
2044
|
-
const target = findModelMatch(modelArg, models);
|
|
2045
|
-
|
|
2046
|
-
if (!target) {
|
|
2047
|
-
const msg = models.length > 0
|
|
2048
|
-
? `未找到匹配 "${modelArg}" 的模型。当前 ${toolLabel} 可选模型:\n${models.map(m => ` \`${m}\``).join("\n")}`
|
|
2049
|
-
: `当前 ${toolLabel} 没有可切换的模型。请在 config.json 中配置模型字段。`;
|
|
2050
|
-
await (platform.kind === "wechat"
|
|
2051
|
-
? platform.sendText(chatId, msg)
|
|
2052
|
-
: platform.sendCard(chatId, "模型切换", msg, "red")
|
|
2053
|
-
).catch(() => {});
|
|
2054
|
-
logTrace(tid, "DONE", { outcome: "model_not_found", arg: modelArg, tool: descriptionTool });
|
|
2055
|
-
return;
|
|
2056
|
-
}
|
|
2057
|
-
|
|
2058
|
-
setSessionModelOverride(sessionId, target);
|
|
2059
|
-
const msg = `已切换当前 ${toolLabel} 会话模型为: \`${target}\``;
|
|
2060
|
-
await (platform.kind === "wechat"
|
|
2061
|
-
? platform.sendText(chatId, msg)
|
|
2062
|
-
: platform.sendCard(chatId, "模型切换", msg, "green")
|
|
2063
|
-
).catch(() => {});
|
|
2064
|
-
logTrace(tid, "DONE", { outcome: "model_switched", arg: modelArg, target, sessionId, tool: descriptionTool });
|
|
2065
|
-
return;
|
|
2066
|
-
}
|
|
2067
|
-
|
|
2068
|
-
// /model — 查看当前会话的可用模型(根据会话 Agent 类型)
|
|
2069
|
-
if (isCommandText && textLower === "/model") {
|
|
2070
|
-
logTrace(tid, "BRANCH", { cmd: "/model", sessionId, tool: descriptionTool });
|
|
2071
|
-
const models = getAllModelsForTool(descriptionTool as AgentTool);
|
|
2072
|
-
const currentModel = getEffectiveModelForTool(descriptionTool, sessionId);
|
|
2073
|
-
|
|
2074
|
-
if (platform.kind === "wechat") {
|
|
2075
|
-
const lines = [currentModel ? `当前模型 (${toolLabel}): ${currentModel}` : `当前模型 (${toolLabel}): 未指定`];
|
|
2076
|
-
if (models.length > 0) {
|
|
2077
|
-
lines.push("", "可切换模型:");
|
|
2078
|
-
for (const m of models) lines.push(` ${m}`);
|
|
2079
|
-
lines.push("", "输入 /model <模型名> 切换模型");
|
|
2080
|
-
} else {
|
|
2081
|
-
lines.push("", "没有可切换的模型。请在 config.json 中配置模型字段。");
|
|
2082
|
-
}
|
|
2083
|
-
if (descriptionTool === "codex") {
|
|
2084
|
-
lines.push("输入 /fast 查看或切换当前会话的 Fast 模式");
|
|
2085
|
-
}
|
|
2086
|
-
await platform.sendText(chatId, lines.join("\n")).catch(() => {});
|
|
2087
|
-
} else {
|
|
2088
|
-
const card = buildModelCard(currentModel, models, descriptionTool);
|
|
2089
|
-
await platform.sendRawCard(chatId, card);
|
|
2090
|
-
}
|
|
2091
|
-
logTrace(tid, "DONE", { outcome: "model_query", tool: descriptionTool });
|
|
2092
|
-
return;
|
|
2093
|
-
}
|
|
2094
|
-
|
|
2095
|
-
// /git <args>:在「当前会话工作目录」执行 git 命令
|
|
2096
|
-
if (isCommandText && textLower === "/effort clear") {
|
|
2097
|
-
logTrace(tid, "BRANCH", { cmd: "/effort clear", sessionId, tool: descriptionTool });
|
|
2098
|
-
const efforts = getAllEffortsForTool(descriptionTool as AgentTool);
|
|
2099
|
-
const toolLabel = toolDisplayName(descriptionTool);
|
|
2100
|
-
if (efforts.length === 0) {
|
|
2101
|
-
const msg = `当前 ${toolLabel} 不支持 effort 切换。`;
|
|
2102
|
-
await (platform.kind === "wechat"
|
|
2103
|
-
? platform.sendText(chatId, msg)
|
|
2104
|
-
: platform.sendCard(chatId, "Effort 切换", msg, "red")
|
|
2105
|
-
).catch(() => {});
|
|
2106
|
-
logTrace(tid, "DONE", { outcome: "effort_unsupported", tool: descriptionTool });
|
|
2107
|
-
return;
|
|
2108
|
-
}
|
|
2109
|
-
|
|
2110
|
-
clearSessionEffortOverride(sessionId);
|
|
2111
|
-
const defaultEffort = getDefaultEffortForTool(descriptionTool as AgentTool);
|
|
2112
|
-
const msg = `已清除当前 ${toolLabel} 会话的 effort 覆盖,恢复使用: \`${defaultEffort || "(未指定)"}\``;
|
|
2113
|
-
await (platform.kind === "wechat"
|
|
2114
|
-
? platform.sendText(chatId, msg)
|
|
2115
|
-
: platform.sendCard(chatId, "Effort 切换", msg, "green")
|
|
2116
|
-
).catch(() => {});
|
|
2117
|
-
logTrace(tid, "DONE", { outcome: "effort_cleared", sessionId, tool: descriptionTool });
|
|
2118
|
-
return;
|
|
2119
|
-
}
|
|
2120
|
-
|
|
2121
|
-
if (isCommandText && textLower.startsWith("/effort ")) {
|
|
2122
|
-
const effortArg = text.slice(8).trim();
|
|
2123
|
-
if (!effortArg) return;
|
|
2124
|
-
logTrace(tid, "BRANCH", { cmd: "/effort", arg: effortArg, sessionId, tool: descriptionTool });
|
|
2125
|
-
|
|
2126
|
-
const efforts = getAllEffortsForTool(descriptionTool as AgentTool);
|
|
2127
|
-
const toolLabel = toolDisplayName(descriptionTool);
|
|
2128
|
-
if (efforts.length === 0) {
|
|
2129
|
-
const msg = `当前 ${toolLabel} 不支持 effort 切换。`;
|
|
2130
|
-
await (platform.kind === "wechat"
|
|
2131
|
-
? platform.sendText(chatId, msg)
|
|
2132
|
-
: platform.sendCard(chatId, "Effort 切换", msg, "red")
|
|
2133
|
-
).catch(() => {});
|
|
2134
|
-
logTrace(tid, "DONE", { outcome: "effort_unsupported", tool: descriptionTool });
|
|
2135
|
-
return;
|
|
2136
|
-
}
|
|
2137
|
-
|
|
2138
|
-
const target = findModelMatch(effortArg, efforts);
|
|
2139
|
-
if (!target) {
|
|
2140
|
-
const msg = `未找到匹配 "${effortArg}" 的 effort。当前 ${toolLabel} 可选 effort:\n${efforts.map(e => ` \`${e}\``).join("\n")}`;
|
|
2141
|
-
await (platform.kind === "wechat"
|
|
2142
|
-
? platform.sendText(chatId, msg)
|
|
2143
|
-
: platform.sendCard(chatId, "Effort 切换", msg, "red")
|
|
2144
|
-
).catch(() => {});
|
|
2145
|
-
logTrace(tid, "DONE", { outcome: "effort_not_found", arg: effortArg, tool: descriptionTool });
|
|
2146
|
-
return;
|
|
2147
|
-
}
|
|
2148
|
-
|
|
2149
|
-
setSessionEffortOverride(sessionId, target);
|
|
2150
|
-
const msg = `已切换当前 ${toolLabel} 会话 effort 为: \`${target}\``;
|
|
2151
|
-
await (platform.kind === "wechat"
|
|
2152
|
-
? platform.sendText(chatId, msg)
|
|
2153
|
-
: platform.sendCard(chatId, "Effort 切换", msg, "green")
|
|
2154
|
-
).catch(() => {});
|
|
2155
|
-
logTrace(tid, "DONE", { outcome: "effort_switched", arg: effortArg, target, sessionId, tool: descriptionTool });
|
|
2156
|
-
return;
|
|
2157
|
-
}
|
|
2158
|
-
|
|
2159
|
-
if (isCommandText && textLower === "/effort") {
|
|
2160
|
-
logTrace(tid, "BRANCH", { cmd: "/effort", sessionId, tool: descriptionTool });
|
|
2161
|
-
const efforts = getAllEffortsForTool(descriptionTool as AgentTool);
|
|
2162
|
-
const currentEffort = getEffectiveEffortForTool(descriptionTool, sessionId);
|
|
2163
|
-
const toolLabel = toolDisplayName(descriptionTool);
|
|
2164
|
-
|
|
2165
|
-
if (efforts.length === 0) {
|
|
2166
|
-
const msg = `当前 ${toolLabel} 不支持 effort 切换。`;
|
|
2167
|
-
await (platform.kind === "wechat"
|
|
2168
|
-
? platform.sendText(chatId, msg)
|
|
2169
|
-
: platform.sendCard(chatId, "Effort 切换", msg, "red")
|
|
2170
|
-
).catch(() => {});
|
|
2171
|
-
} else if (platform.kind === "wechat") {
|
|
2172
|
-
const lines = [currentEffort ? `当前 effort (${toolLabel}): ${currentEffort}` : `当前 effort (${toolLabel}): 未指定`];
|
|
2173
|
-
lines.push("", "可切换 effort:");
|
|
2174
|
-
for (const e of efforts) lines.push(` ${e}`);
|
|
2175
|
-
lines.push("", "输入 /effort <effort> 切换 effort");
|
|
2176
|
-
await platform.sendText(chatId, lines.join("\n")).catch(() => {});
|
|
2177
|
-
} else {
|
|
2178
|
-
const card = buildEffortCard(currentEffort, efforts, descriptionTool);
|
|
2179
|
-
await platform.sendRawCard(chatId, card);
|
|
2180
|
-
}
|
|
2181
|
-
logTrace(tid, "DONE", { outcome: "effort_query", tool: descriptionTool });
|
|
2182
|
-
return;
|
|
2183
|
-
}
|
|
2184
|
-
|
|
2185
|
-
if (isCommandText && (textLower.startsWith("/git ") || textLower === "/git")) {
|
|
2186
|
-
const args = text === "/git" ? "" : text.slice(5).trim();
|
|
2187
|
-
logTrace(tid, "BRANCH", { cmd: "/git", args: args || "(none)" });
|
|
2188
|
-
if (!args) {
|
|
2189
|
-
logTrace(tid, "DONE", { outcome: "git_no_args" });
|
|
2190
|
-
await platform.sendCard(
|
|
2191
|
-
chatId,
|
|
2192
|
-
"/git",
|
|
2193
|
-
"用法:`/git <子命令> [参数]`,例如 `/git status`、`/git log --oneline -n 5`。",
|
|
2194
|
-
"yellow",
|
|
2195
|
-
);
|
|
2196
|
-
return;
|
|
2197
|
-
}
|
|
2198
|
-
|
|
2199
|
-
const adapter = getAdapterForTool(descriptionTool, sessionId);
|
|
2200
|
-
let cwd: string | undefined;
|
|
2201
|
-
try {
|
|
2202
|
-
const info = await adapter.getSessionInfo(sessionId);
|
|
2203
|
-
cwd = info?.cwd;
|
|
2204
|
-
} catch (err) {
|
|
2205
|
-
console.error(
|
|
2206
|
-
`[${ts()}] [GIT] getSessionInfo FAIL: ${(err as Error).message}`,
|
|
2207
|
-
);
|
|
2208
|
-
}
|
|
2209
|
-
if (!cwd) {
|
|
2210
|
-
logTrace(tid, "DONE", { outcome: "git_no_cwd", tool: descriptionTool });
|
|
2211
|
-
const isCursor = descriptionTool === "cursor";
|
|
2212
|
-
const hint = isCursor
|
|
2213
|
-
? "无法获取当前 Cursor 会话的工作目录(缺少 sessionId→cwd 持久化映射)。请先在本群发送一条普通消息(让 adapter 从 cursor-agent 流中自动补回 cwd),然后再试 /git;若仍失败,可用 /new 重建会话。"
|
|
2214
|
-
: `无法获取当前会话的工作目录(${toolLabel} adapter 未返回 cwd)。请先与 AI 对话一次再试,或检查会话是否仍存在。`;
|
|
2215
|
-
await platform.sendCard(chatId, "/git", hint, "red");
|
|
2216
|
-
return;
|
|
2217
|
-
}
|
|
2218
|
-
|
|
2219
|
-
console.log(
|
|
2220
|
-
`[${ts()}] [GIT] chat=${chatId} cwd=${cwd} cmd="git ${args}" timeoutMs=${GIT_TIMEOUT_MS}`,
|
|
2221
|
-
);
|
|
2222
|
-
const result = await runGitCommand(args, cwd, {
|
|
2223
|
-
timeoutMs: GIT_TIMEOUT_MS,
|
|
2224
|
-
});
|
|
2225
|
-
console.log(
|
|
2226
|
-
`[${ts()}] [GIT] exitCode=${result.exitCode}, durationMs=${result.durationMs}, truncated=${result.truncated}, timedOut=${result.timedOut}`,
|
|
2227
|
-
);
|
|
2228
|
-
const content = formatGitResult(args, cwd, result);
|
|
2229
|
-
const template = gitResultHeaderTemplate(result);
|
|
2230
|
-
await platform.sendCard(chatId, "/git 输出", content, template);
|
|
2231
|
-
logTrace(tid, "DONE", {
|
|
2232
|
-
outcome: "git_result",
|
|
2233
|
-
exitCode: result.exitCode,
|
|
2234
|
-
durationMs: result.durationMs,
|
|
2235
|
-
});
|
|
2236
|
-
return;
|
|
2237
|
-
}
|
|
2238
|
-
|
|
2239
|
-
const lastTs = lastMsgTimestamps.get(chatId);
|
|
2240
|
-
if (lastTs !== undefined && msgTimestamp <= lastTs) {
|
|
2241
|
-
logTrace(tid, "DONE", {
|
|
2242
|
-
outcome: "skip_old_message_no_session",
|
|
2243
|
-
msgTimestamp,
|
|
2244
|
-
lastTimestamp: lastTs,
|
|
2245
|
-
});
|
|
2246
|
-
console.log(
|
|
2247
|
-
`[${ts()}] [SKIP] Older message (${msgTimestamp} <= ${lastTs}), no active session, ignoring`,
|
|
2248
|
-
);
|
|
2249
|
-
return;
|
|
2250
|
-
}
|
|
2251
|
-
|
|
2252
|
-
// 并发检查:同一 session 只能有一个活跃 prompt,多余消息进入队列
|
|
2253
|
-
if (isSessionRunning(sessionId)) {
|
|
2254
|
-
const queued = enqueueMessage(sessionId, {
|
|
2255
|
-
text: promptText, chatId, openId, msgTimestamp, chatType, traceId: tid,
|
|
2256
|
-
});
|
|
2257
|
-
if (queued) {
|
|
2258
|
-
logTrace(tid, "QUEUED", { sessionId });
|
|
2259
|
-
console.log(
|
|
2260
|
-
`[${ts()}] [QUEUED] Session ${sessionId} is busy, message from chat ${chatId} enqueued`,
|
|
2261
|
-
);
|
|
2262
|
-
if (platform.kind === "wechat") {
|
|
2263
|
-
await platform.sendText(chatId, "当前会话正在生成中,你的消息已进入缓存队列,生成完成后会立即处理。发送 /cancel 可取消缓存。").catch(() => {});
|
|
2264
|
-
} else {
|
|
2265
|
-
if (isFeishuP2p(platform, chatType) && pendingFeishuP2pDefaultTool) {
|
|
2266
|
-
const currentLabel = toolDisplayName(descriptionTool);
|
|
2267
|
-
const desiredLabel = toolDisplayName(pendingFeishuP2pDefaultTool);
|
|
2268
|
-
await platform.sendCard(
|
|
2269
|
-
chatId,
|
|
2270
|
-
"Agent 切换等待中",
|
|
2271
|
-
`当前 ${currentLabel} 正在生成;完成后会切换到 ${desiredLabel},并用新的空会话处理这条消息。\n\n发送 **/cancel** 可取消缓存。`,
|
|
2272
|
-
"blue",
|
|
2273
|
-
).catch(() => {});
|
|
2274
|
-
} else {
|
|
2275
|
-
await platform.sendRawCard(chatId, buildQueuedCard(text)).catch(() => {});
|
|
2276
|
-
}
|
|
2277
|
-
}
|
|
2278
|
-
} else {
|
|
2279
|
-
logTrace(tid, "QUEUE_FULL", { sessionId });
|
|
2280
|
-
console.log(
|
|
2281
|
-
`[${ts()}] [QUEUE_FULL] Session ${sessionId} queue full, rejecting message from chat ${chatId}`,
|
|
2282
|
-
);
|
|
2283
|
-
if (platform.kind === "wechat") {
|
|
2284
|
-
await platform.sendText(chatId, "当前缓存队列中已有消息等待处理,请等待或发送 /stop(停止生成)或 /cancel(取消缓存)。").catch(() => {});
|
|
2285
|
-
} else {
|
|
2286
|
-
await platform.sendRawCard(chatId, buildQueueFullCard()).catch(() => {});
|
|
2287
|
-
}
|
|
2288
|
-
}
|
|
2289
|
-
return;
|
|
2290
|
-
}
|
|
2291
|
-
|
|
2292
|
-
if (shouldSendWechatProcessingAck(platform, isCommandText, chatType)) {
|
|
2293
|
-
await platform.sendText(chatId, "生成中...").catch(() => {});
|
|
2294
|
-
}
|
|
2295
|
-
|
|
2296
|
-
try {
|
|
2297
|
-
logTrace(tid, "RESUME", { sessionId, tool: descriptionTool });
|
|
2298
|
-
const resumeOutcome = await resumeAndPrompt(
|
|
2299
|
-
sessionId,
|
|
2300
|
-
promptText,
|
|
2301
|
-
platform,
|
|
2302
|
-
chatId,
|
|
2303
|
-
msgTimestamp,
|
|
2304
|
-
descriptionTool,
|
|
2305
|
-
tid,
|
|
2306
|
-
);
|
|
2307
|
-
if (resumeOutcome === "error") {
|
|
2308
|
-
logTrace(tid, "DONE", { outcome: "resume_error", sessionId });
|
|
2309
|
-
console.error(`[${ts()}] [RESUME] Session ${sessionId} ended with an Agent error`);
|
|
2310
|
-
} else {
|
|
2311
|
-
logTrace(tid, "DONE", { outcome: "resume_done", sessionId, sessionOutcome: resumeOutcome });
|
|
2312
|
-
console.log(`[${ts()}] [RESUME] Session ${sessionId} done (${resumeOutcome})`);
|
|
2313
|
-
}
|
|
2314
|
-
} catch (err) {
|
|
2315
|
-
logTrace(tid, "DONE", {
|
|
2316
|
-
outcome: "resume_fail",
|
|
2317
|
-
error: (err as Error).message,
|
|
2318
|
-
});
|
|
2319
|
-
console.error(`[${ts()}] [RESUME] FAIL: ${(err as Error).message}`);
|
|
2320
|
-
fileLog.flush();
|
|
2321
|
-
await platform.sendCard(
|
|
2322
|
-
chatId,
|
|
2323
|
-
"Error",
|
|
2324
|
-
`Failed to resume ${toolLabel} session:\n${(err as Error).message}`,
|
|
2325
|
-
"red",
|
|
2326
|
-
);
|
|
2327
|
-
}
|
|
2328
|
-
return;
|
|
2329
|
-
}
|
|
2330
|
-
|
|
2331
|
-
if (isCommandText && (textLower === "/fast" || textLower.startsWith("/fast "))) {
|
|
2332
|
-
const defaultTool = resolveDefaultAgentTool();
|
|
2333
|
-
const fastArg = text.slice(5).trim().toLowerCase();
|
|
2334
|
-
if (defaultTool !== "codex") {
|
|
2335
|
-
const msg = `当前默认 Agent (${toolDisplayName(defaultTool)}) 不支持 Fast 模式;/fast 仅适用于 Codex。`;
|
|
2336
|
-
await (platform.kind === "wechat"
|
|
2337
|
-
? platform.sendText(chatId, msg)
|
|
2338
|
-
: platform.sendCard(chatId, "Codex Fast 模式", msg, "yellow")
|
|
2339
|
-
).catch(() => {});
|
|
2340
|
-
logTrace(tid, "DONE", { outcome: "fast_unsupported", defaultTool });
|
|
2341
|
-
return;
|
|
2342
|
-
}
|
|
2343
|
-
if (fastArg) {
|
|
2344
|
-
const msg = "当前没有绑定 Codex 会话,无法设置会话覆盖。请先创建或进入 Codex 会话;全局默认值可在 Web UI 中设置。";
|
|
2345
|
-
await (platform.kind === "wechat"
|
|
2346
|
-
? platform.sendText(chatId, msg)
|
|
2347
|
-
: platform.sendCard(chatId, "Codex Fast 模式", msg, "yellow")
|
|
2348
|
-
).catch(() => {});
|
|
2349
|
-
logTrace(tid, "DONE", { outcome: "fast_no_session", arg: fastArg });
|
|
2350
|
-
return;
|
|
2351
|
-
}
|
|
2352
|
-
const enabled = getEffectiveFastModeForTool("codex");
|
|
2353
|
-
await sendFastModeStatus(platform, chatId, enabled).catch(() => {});
|
|
2354
|
-
logTrace(tid, "DONE", { outcome: "fast_query", enabled, defaultTool });
|
|
2355
|
-
return;
|
|
2356
|
-
}
|
|
2357
|
-
|
|
2358
|
-
// 无会话上下文 → 检查是否是 /model 查询
|
|
2359
|
-
if (isCommandText && textLower === "/model") {
|
|
2360
|
-
const defaultTool = resolveDefaultAgentTool();
|
|
2361
|
-
const models = getAllModelsForTool(defaultTool);
|
|
2362
|
-
let currentModel = "";
|
|
2363
|
-
if (defaultTool === "cursor") currentModel = config.cursor.model;
|
|
2364
|
-
else if (defaultTool === "codex") currentModel = config.codex.model;
|
|
2365
|
-
else if (defaultTool === "ccc") currentModel = config.ccc.model;
|
|
2366
|
-
else currentModel = CLAUDE_MODEL;
|
|
2367
|
-
|
|
2368
|
-
if (platform.kind === "wechat") {
|
|
2369
|
-
const lines = [currentModel ? `当前模型 (${defaultTool}): ${currentModel}` : `当前模型 (${defaultTool}): 未指定`];
|
|
2370
|
-
if (models.length > 0) {
|
|
2371
|
-
lines.push("", "可切换模型:");
|
|
2372
|
-
for (const m of models) lines.push(` ${m}`);
|
|
2373
|
-
lines.push("", "在会话中输入 /model <模型名> 切换模型");
|
|
2374
|
-
} else {
|
|
2375
|
-
lines.push("", "没有可切换的模型。请在 config.json 中配置模型字段。");
|
|
2376
|
-
}
|
|
2377
|
-
if (defaultTool === "codex") {
|
|
2378
|
-
lines.push("输入 /fast 查看当前 Codex Fast 模式");
|
|
2379
|
-
}
|
|
2380
|
-
await platform.sendText(chatId, lines.join("\n")).catch(() => {});
|
|
2381
|
-
} else {
|
|
2382
|
-
const card = buildModelCard(currentModel, models, defaultTool);
|
|
2383
|
-
await platform.sendRawCard(chatId, card);
|
|
2384
|
-
}
|
|
2385
|
-
logTrace(tid, "DONE", { outcome: "model_query", defaultTool });
|
|
2386
|
-
return;
|
|
2387
|
-
}
|
|
2388
|
-
|
|
2389
|
-
// A private /state query is useful even before the first Agent session exists.
|
|
2390
|
-
// Keep it read-only and render the same status-card shape as established chats.
|
|
2391
|
-
if (isCommandText && textLower === "/state" && isFeishuP2p(platform, chatType)) {
|
|
2392
|
-
logTrace(tid, "BRANCH", { cmd: "/state", scope: "unbound_p2p" });
|
|
2393
|
-
await sendStateCard(
|
|
2394
|
-
platform,
|
|
2395
|
-
chatId,
|
|
2396
|
-
null,
|
|
2397
|
-
toolDisplayName(resolveDefaultAgentTool()),
|
|
2398
|
-
tid,
|
|
2399
|
-
);
|
|
2400
|
-
return;
|
|
2401
|
-
}
|
|
2402
|
-
|
|
2403
|
-
// 无会话上下文 → /sessions 仍是有效指令,不触发飞书私聊自动建群。
|
|
2404
|
-
if (isCommandText && textLower === "/effort") {
|
|
2405
|
-
const defaultTool = resolveDefaultAgentTool();
|
|
2406
|
-
const efforts = getAllEffortsForTool(defaultTool);
|
|
2407
|
-
const currentEffort = getDefaultEffortForTool(defaultTool);
|
|
2408
|
-
const toolLabel = toolDisplayName(defaultTool);
|
|
2409
|
-
|
|
2410
|
-
if (efforts.length === 0) {
|
|
2411
|
-
const msg = `当前默认 agent (${toolLabel}) 不支持 effort 切换。`;
|
|
2412
|
-
await (platform.kind === "wechat"
|
|
2413
|
-
? platform.sendText(chatId, msg)
|
|
2414
|
-
: platform.sendCard(chatId, "Effort 切换", msg, "red")
|
|
2415
|
-
).catch(() => {});
|
|
2416
|
-
} else if (platform.kind === "wechat") {
|
|
2417
|
-
const lines = [currentEffort ? `当前默认 effort (${toolLabel}): ${currentEffort}` : `当前默认 effort (${toolLabel}): 未指定`];
|
|
2418
|
-
lines.push("", "可切换 effort:");
|
|
2419
|
-
for (const e of efforts) lines.push(` ${e}`);
|
|
2420
|
-
lines.push("", "在会话中输入 /effort <effort> 切换 effort");
|
|
2421
|
-
await platform.sendText(chatId, lines.join("\n")).catch(() => {});
|
|
2422
|
-
} else {
|
|
2423
|
-
const card = buildEffortCard(currentEffort, efforts, defaultTool);
|
|
2424
|
-
await platform.sendRawCard(chatId, card);
|
|
2425
|
-
}
|
|
2426
|
-
logTrace(tid, "DONE", { outcome: "effort_query", defaultTool });
|
|
2427
|
-
return;
|
|
2428
|
-
}
|
|
2429
|
-
|
|
2430
|
-
if (isCommandText && textLower.startsWith("/effort ")) {
|
|
2431
|
-
const defaultTool = resolveDefaultAgentTool();
|
|
2432
|
-
const toolLabel = toolDisplayName(defaultTool);
|
|
2433
|
-
const msg = `当前没有绑定会话。请先进入 Claude/Codex 会话,再输入 /effort <effort> 切换当前会话的 effort。当前默认 agent: ${toolLabel}`;
|
|
2434
|
-
await (platform.kind === "wechat"
|
|
2435
|
-
? platform.sendText(chatId, msg)
|
|
2436
|
-
: platform.sendCard(chatId, "Effort 切换", msg, "yellow")
|
|
2437
|
-
).catch(() => {});
|
|
2438
|
-
logTrace(tid, "DONE", { outcome: "effort_no_session", defaultTool });
|
|
2439
|
-
return;
|
|
2440
|
-
}
|
|
2441
|
-
|
|
2442
|
-
if (isCommandText && textLower === "/sessions") {
|
|
2443
|
-
logTrace(tid, "BRANCH", { cmd: "/sessions", scope: "global" });
|
|
2444
|
-
const allSessions = await getAllSessionsStatus();
|
|
2445
|
-
const now = Date.now();
|
|
2446
|
-
const cardData = allSessions.map((s) => ({
|
|
2447
|
-
sessionId: s.sessionId,
|
|
2448
|
-
chatName: s.chatName,
|
|
2449
|
-
chatId: s.chatId,
|
|
2450
|
-
chatType: s.chatType,
|
|
2451
|
-
active: s.active,
|
|
2452
|
-
turnCount: s.turnCount,
|
|
2453
|
-
elapsedSeconds: s.active
|
|
2454
|
-
? Math.floor((now - s.startTime) / 1000)
|
|
2455
|
-
: null,
|
|
2456
|
-
model: s.model,
|
|
2457
|
-
tool: s.tool,
|
|
2458
|
-
}));
|
|
2459
|
-
const card = buildSessionsCard(cardData, {
|
|
2460
|
-
defaultToolLabel: toolDisplayName(resolveDefaultAgentTool()),
|
|
2461
|
-
fixedPrivateSession: isFeishuP2p(platform, chatType),
|
|
2462
|
-
});
|
|
2463
|
-
const ok = await platform.sendRawCard(chatId, card);
|
|
2464
|
-
console.log(
|
|
2465
|
-
`[${ts()}] [SESSIONS] card sent, ok=${ok}, count=${cardData.length}`,
|
|
2466
|
-
);
|
|
2467
|
-
logTrace(tid, "DONE", { outcome: "sessions", ok, count: cardData.length });
|
|
2468
|
-
return;
|
|
2469
|
-
}
|
|
2470
|
-
|
|
2471
|
-
// 飞书私聊普通消息:首次使用时在当前私聊创建并持久化一个专属 session,
|
|
2472
|
-
// 随后的消息会在上面的 registry 路由中继续该 session。只有显式 /new 才建群。
|
|
2473
|
-
if (isFeishuP2p(platform, chatType) && !isCommandText) {
|
|
2474
|
-
const tool = resolveDefaultAgentTool();
|
|
2475
|
-
const toolLabel = toolDisplayName(tool);
|
|
2476
|
-
// 私聊 cwd 故意不读取 /cd 的 chatId 默认值:/cd 只为之后显式
|
|
2477
|
-
// /new 创建的群聊服务,飞书私聊始终从 ChatCCC 运行账号的用户目录启动。
|
|
2478
|
-
const cwd = homedir();
|
|
2479
|
-
logTrace(tid, "BRANCH", { cmd: "auto_new_feishu_p2p", tool, cwd });
|
|
2480
|
-
|
|
2481
|
-
try {
|
|
2482
|
-
const init = await initClaudeSession(tool, cwd);
|
|
2483
|
-
const sessionId = init.sessionId;
|
|
2484
|
-
const chatName = sessionChatName(text.slice(0, 10) || "私聊会话", cwd);
|
|
2485
|
-
const switchResult = await switchChatBinding({
|
|
2486
|
-
chatId,
|
|
2487
|
-
chatType,
|
|
2488
|
-
oldSessionId: null,
|
|
2489
|
-
newSessionId: sessionId,
|
|
2490
|
-
tool,
|
|
2491
|
-
chatName,
|
|
2492
|
-
newDescription: `${sessionPrefixForTool(tool)} ${sessionId}`,
|
|
2493
|
-
updateChatInfoFn: (cid, name, desc) =>
|
|
2494
|
-
platform.updateChatInfo(cid, name, desc),
|
|
2495
|
-
});
|
|
2496
|
-
if (!switchResult.ok) {
|
|
2497
|
-
throw switchResult.error ?? new Error("Failed to bind Feishu private session");
|
|
2498
|
-
}
|
|
2499
|
-
|
|
2500
|
-
await resumeAndPrompt(
|
|
2501
|
-
sessionId,
|
|
2502
|
-
promptText,
|
|
2503
|
-
platform,
|
|
2504
|
-
chatId,
|
|
2505
|
-
msgTimestamp,
|
|
2506
|
-
tool,
|
|
2507
|
-
tid,
|
|
2508
|
-
);
|
|
2509
|
-
logTrace(tid, "DONE", {
|
|
2510
|
-
outcome: "auto_new_feishu_p2p_prompt_done",
|
|
2511
|
-
chatId,
|
|
2512
|
-
sessionId,
|
|
2513
|
-
tool,
|
|
2514
|
-
cwd,
|
|
2515
|
-
});
|
|
2516
|
-
} catch (err) {
|
|
2517
|
-
console.error(`[${ts()}] [AUTO-P2P] FAIL: ${(err as Error).message}`);
|
|
2518
|
-
logTrace(tid, "DONE", {
|
|
2519
|
-
outcome: "auto_new_feishu_p2p_fail",
|
|
2520
|
-
error: (err as Error).message,
|
|
2521
|
-
});
|
|
2522
|
-
await platform.sendCard(
|
|
2523
|
-
chatId,
|
|
2524
|
-
"Error",
|
|
2525
|
-
`Failed to create ${toolLabel} private session:\n${(err as Error).message}`,
|
|
2526
|
-
"red",
|
|
2527
|
-
);
|
|
2528
|
-
}
|
|
2529
|
-
return;
|
|
2530
|
-
}
|
|
2531
|
-
|
|
2532
|
-
// 无会话上下文 → help card
|
|
2533
|
-
logTrace(tid, "SEND", { method: "help_card", chatId });
|
|
2534
|
-
const card = buildHelpCard(text, { defaultToolLabel: toolDisplayName(resolveDefaultAgentTool()) });
|
|
2535
|
-
const ok = await platform.sendRawCard(chatId, card);
|
|
2536
|
-
if (!ok) {
|
|
2537
|
-
console.error(`[${ts()}] [SEND] help_card FAIL: chatId=${chatId}`);
|
|
2538
|
-
logTrace(tid, "DONE", { outcome: "help_card_fail" });
|
|
2539
|
-
} else {
|
|
2540
|
-
console.log(`[${ts()}] [SEND] help_card OK: chatId=${chatId}`);
|
|
2541
|
-
logTrace(tid, "DONE", { outcome: "help_card_sent" });
|
|
2542
|
-
}
|
|
2543
|
-
}
|