chatccc 0.2.219 → 0.2.220

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