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.
- package/README.md +36 -16
- package/config.sample.json +4 -1
- package/package.json +1 -1
- package/src/__tests__/builtin-config.test.ts +3 -0
- package/src/__tests__/cards.test.ts +6 -6
- package/src/__tests__/config-reload.test.ts +19 -8
- package/src/__tests__/config-sample.test.ts +6 -2
- package/src/__tests__/orchestrator.test.ts +84 -5
- package/src/__tests__/sim-platform.test.ts +10 -0
- package/src/__tests__/web-ui.test.ts +40 -0
- package/src/cards.ts +7 -5
- package/src/config.ts +41 -10
- package/src/index.ts +13 -0
- package/src/orchestrator.ts +1553 -1488
- package/src/web-ui.ts +164 -46
package/src/orchestrator.ts
CHANGED
|
@@ -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
|
-
|
|
380
|
-
|
|
381
|
-
|
|
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 === "
|
|
409
|
-
const
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
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
|
-
}
|
|
1304
|
-
|
|
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 =
|
|
2147
|
-
|
|
2148
|
-
|
|
2149
|
-
|
|
2150
|
-
|
|
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
|
+
}
|