chatccc 0.2.120 → 0.2.122

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.
@@ -154,67 +154,67 @@ describe("handleCommand WeChat processing ack", () => {
154
154
  expect(platform.sendCard).not.toHaveBeenCalled();
155
155
  });
156
156
 
157
- it("sends the WeChat processing ack after the busy check for normal prompts", async () => {
158
- const platform = mockPlatform();
159
- await recordSessionRegistry({
160
- chatId: "wx-chat",
161
- sessionId: "sid-wechat",
157
+ it("sends the WeChat processing ack after the busy check for normal prompts", async () => {
158
+ const platform = mockPlatform();
159
+ await recordSessionRegistry({
160
+ chatId: "wx-chat",
161
+ sessionId: "sid-wechat",
162
162
  tool: "claude",
163
163
  chatName: "ready-session",
164
164
  running: false,
165
165
  });
166
166
 
167
167
  await handleCommand(platform, "继续说明", "wx-chat", "wx-user", Date.now(), "p2p");
168
-
169
- expect(platform.sendText).toHaveBeenCalledWith("wx-chat", "生成中...");
170
- });
171
-
172
- it.each([
173
- { tool: "claude" as const, sessionId: "sid-claude", text: "/ask 只回答不要改文件" },
174
- { tool: "cursor" as const, sessionId: "sid-cursor", text: "/plan 先给我计划" },
175
- ])("passes unsupported mode commands through as raw prompts for $tool", async ({ tool, sessionId, text }) => {
176
- const platform = mockPlatform();
177
- const prompt = vi.fn(async function* (_sessionId: string, userText: string) {
178
- yield {
179
- type: "assistant" as const,
180
- blocks: [{ type: "text" as const, text: `收到: ${userText}` }],
181
- };
182
- });
183
- _setAdapterForToolForTest(tool, {
184
- displayName: tool,
185
- sessionDescPrefix: `${tool} Session:`,
186
- createSession: vi.fn(async () => ({ sessionId })),
187
- prompt,
188
- getSessionInfo: async (sessionId: string): Promise<SessionInfo> => ({
189
- sessionId,
190
- cwd: "F:\\repo",
191
- }),
192
- closeSession: async () => {},
193
- });
194
- await recordSessionRegistry({
195
- chatId: "wx-chat",
196
- sessionId,
197
- tool,
198
- chatName: "ready-session",
199
- running: false,
200
- });
201
-
202
- await handleCommand(platform, text, "wx-chat", "wx-user", Date.now(), "p2p");
203
-
204
- expect(platform.sendText).not.toHaveBeenCalledWith("wx-chat", expect.stringContaining("仅在以下 Agent 会话中可用"));
205
- expect(prompt).toHaveBeenCalledWith(
206
- sessionId,
207
- expect.stringContaining(text),
208
- "F:\\repo",
209
- expect.any(AbortSignal),
210
- expect.objectContaining({ permissionMode: undefined }),
211
- );
212
- });
213
-
214
- it("cleans stale Feishu p2p binding, creates a group, and sends the private message as first prompt", async () => {
215
- const platform = mockPlatform("feishu");
216
- const prompt = vi.fn(async function* (_sessionId: string, userText: string) {
217
- yield {
168
+
169
+ expect(platform.sendText).toHaveBeenCalledWith("wx-chat", "生成中...");
170
+ });
171
+
172
+ it.each([
173
+ { tool: "claude" as const, sessionId: "sid-claude", text: "/ask 只回答不要改文件" },
174
+ { tool: "cursor" as const, sessionId: "sid-cursor", text: "/plan 先给我计划" },
175
+ ])("passes unsupported mode commands through as raw prompts for $tool", async ({ tool, sessionId, text }) => {
176
+ const platform = mockPlatform();
177
+ const prompt = vi.fn(async function* (_sessionId: string, userText: string) {
178
+ yield {
179
+ type: "assistant" as const,
180
+ blocks: [{ type: "text" as const, text: `收到: ${userText}` }],
181
+ };
182
+ });
183
+ _setAdapterForToolForTest(tool, {
184
+ displayName: tool,
185
+ sessionDescPrefix: `${tool} Session:`,
186
+ createSession: vi.fn(async () => ({ sessionId })),
187
+ prompt,
188
+ getSessionInfo: async (sessionId: string): Promise<SessionInfo> => ({
189
+ sessionId,
190
+ cwd: "F:\\repo",
191
+ }),
192
+ closeSession: async () => {},
193
+ });
194
+ await recordSessionRegistry({
195
+ chatId: "wx-chat",
196
+ sessionId,
197
+ tool,
198
+ chatName: "ready-session",
199
+ running: false,
200
+ });
201
+
202
+ await handleCommand(platform, text, "wx-chat", "wx-user", Date.now(), "p2p");
203
+
204
+ expect(platform.sendText).not.toHaveBeenCalledWith("wx-chat", expect.stringContaining("仅在以下 Agent 会话中可用"));
205
+ expect(prompt).toHaveBeenCalledWith(
206
+ sessionId,
207
+ expect.stringContaining(text),
208
+ "F:\\repo",
209
+ expect.any(AbortSignal),
210
+ expect.objectContaining({ permissionMode: undefined }),
211
+ );
212
+ });
213
+
214
+ it("cleans stale Feishu p2p binding, creates a group, and sends the private message as first prompt", async () => {
215
+ const platform = mockPlatform("feishu");
216
+ const prompt = vi.fn(async function* (_sessionId: string, userText: string) {
217
+ yield {
218
218
  type: "assistant" as const,
219
219
  blocks: [{ type: "text" as const, text: `收到: ${userText}` }],
220
220
  };
@@ -1,141 +1,141 @@
1
- // =============================================================================
2
- // resource-monitor.ts — CLI 进程资源监控(CPU + 内存)
3
- // =============================================================================
4
- // 对所有 chatccc 启动的 CLI 进程持续监控 CPU 和内存占用。
5
- // 若连续 3 分钟两项指标均无变化,判定为僵死,发出 "stuck" 事件。
6
- // =============================================================================
7
-
8
- import { exec } from "node:child_process";
9
- import { EventEmitter } from "node:events";
10
-
11
- const CHECK_INTERVAL_MS = 30_000; // 30 秒检查一次
12
- const STUCK_THRESHOLD = 6; // 连续 6 次无变化 = 3 分钟
13
-
14
- interface ProcessSnapshot {
15
- cpu: number;
16
- memory: number;
17
- unchangedCount: number;
18
- }
19
-
20
- interface TrackedProcess {
21
- pid: number;
22
- sessionId: string;
23
- snapshot: ProcessSnapshot;
24
- }
25
-
26
- export const resourceMonitor = new EventEmitter();
27
-
28
- const tracked = new Map<number, TrackedProcess>();
29
-
30
- let timer: ReturnType<typeof setInterval> | null = null;
31
-
32
- function startIfNeeded(): void {
33
- if (timer) return;
34
- timer = setInterval(checkAll, CHECK_INTERVAL_MS);
35
- timer.unref?.();
36
- }
37
-
38
- function stopIfIdle(): void {
39
- if (tracked.size > 0) return;
40
- if (timer) { clearInterval(timer); timer = null; }
41
- }
42
-
43
- export function registerProcess(pid: number, sessionId: string): void {
44
- tracked.set(pid, { pid, sessionId, snapshot: { cpu: -1, memory: -1, unchangedCount: 0 } });
45
- startIfNeeded();
46
- }
47
-
48
- export function unregisterProcess(pid: number): void {
49
- tracked.delete(pid);
50
- stopIfIdle();
51
- }
52
-
53
- // ---------------------------------------------------------------------------
54
- // 批量查询进程指标(Windows PowerShell)
55
- // ---------------------------------------------------------------------------
56
-
57
- function execPowerShell(script: string, timeoutMs: number): Promise<string> {
58
- return new Promise((resolve, reject) => {
59
- exec(
60
- `powershell -NoProfile -Command "${script}"`,
61
- { timeout: timeoutMs, windowsHide: true },
62
- (err, stdout) => {
63
- if (err) reject(err);
64
- else resolve(stdout);
65
- },
66
- );
67
- });
68
- }
69
-
70
- async function getProcessMetrics(pids: number[]): Promise<Map<number, { cpu: number; memory: number }>> {
71
- const result = new Map<number, { cpu: number; memory: number }>();
72
- if (pids.length === 0) return result;
73
-
74
- const psScript = `Get-Process -Id ${pids.join(",")} -ErrorAction SilentlyContinue | ForEach-Object { "$($_.Id)|$($_.CPU)|$($_.WorkingSet64)" }`;
75
-
76
- try {
77
- const stdout = await execPowerShell(psScript, 10_000);
78
- for (const line of stdout.trim().split(/\r?\n/)) {
79
- const trimmed = line.trim();
80
- if (!trimmed) continue;
81
- const [idStr, cpuStr, memStr] = trimmed.split("|");
82
- const id = parseInt(idStr, 10);
83
- const cpu = parseFloat(cpuStr);
84
- const memory = parseInt(memStr, 10);
85
- if (!isNaN(id) && !isNaN(cpu) && !isNaN(memory)) {
86
- result.set(id, { cpu, memory });
87
- }
88
- }
89
- } catch {
90
- // PowerShell 查询失败时跳过本轮,下次重试
91
- }
92
-
93
- return result;
94
- }
95
-
96
- // ---------------------------------------------------------------------------
97
- // 定时检查
98
- // ---------------------------------------------------------------------------
99
-
100
- async function checkAll(): Promise<void> {
101
- if (tracked.size === 0) return;
102
-
103
- const pids = [...tracked.keys()];
104
- const metrics = await getProcessMetrics(pids);
105
-
106
- for (const [pid, tp] of tracked) {
107
- const m = metrics.get(pid);
108
- if (!m) {
109
- // 进程已不存在,停止追踪
110
- tracked.delete(pid);
111
- continue;
112
- }
113
-
114
- const prev = tp.snapshot;
115
- const cpuChanged = m.cpu !== prev.cpu;
116
- // 内存允许 ±1% 波动,避免正常抖动触发误判
117
- const memTolerance = prev.memory > 0 ? Math.max(prev.memory * 0.01, 1024 * 1024) : 1024 * 1024;
118
- const memChanged = Math.abs(m.memory - prev.memory) > memTolerance;
119
-
120
- if (!cpuChanged && !memChanged) {
121
- tp.snapshot.unchangedCount++;
122
- if (tp.snapshot.unchangedCount >= STUCK_THRESHOLD) {
123
- const idleMinutes = Math.round(
124
- (tp.snapshot.unchangedCount * CHECK_INTERVAL_MS) / 60_000,
125
- );
126
- resourceMonitor.emit("stuck", {
127
- pid: tp.pid,
128
- sessionId: tp.sessionId,
129
- idleMinutes,
130
- });
131
- tracked.delete(pid);
132
- }
133
- } else {
134
- tp.snapshot.unchangedCount = 0;
135
- }
136
- tp.snapshot.cpu = m.cpu;
137
- tp.snapshot.memory = m.memory;
138
- }
139
-
140
- stopIfIdle();
1
+ // =============================================================================
2
+ // resource-monitor.ts — CLI 进程资源监控(CPU + 内存)
3
+ // =============================================================================
4
+ // 对所有 chatccc 启动的 CLI 进程持续监控 CPU 和内存占用。
5
+ // 若连续 3 分钟两项指标均无变化,判定为僵死,发出 "stuck" 事件。
6
+ // =============================================================================
7
+
8
+ import { exec } from "node:child_process";
9
+ import { EventEmitter } from "node:events";
10
+
11
+ const CHECK_INTERVAL_MS = 30_000; // 30 秒检查一次
12
+ const STUCK_THRESHOLD = 6; // 连续 6 次无变化 = 3 分钟
13
+
14
+ interface ProcessSnapshot {
15
+ cpu: number;
16
+ memory: number;
17
+ unchangedCount: number;
18
+ }
19
+
20
+ interface TrackedProcess {
21
+ pid: number;
22
+ sessionId: string;
23
+ snapshot: ProcessSnapshot;
24
+ }
25
+
26
+ export const resourceMonitor = new EventEmitter();
27
+
28
+ const tracked = new Map<number, TrackedProcess>();
29
+
30
+ let timer: ReturnType<typeof setInterval> | null = null;
31
+
32
+ function startIfNeeded(): void {
33
+ if (timer) return;
34
+ timer = setInterval(checkAll, CHECK_INTERVAL_MS);
35
+ timer.unref?.();
36
+ }
37
+
38
+ function stopIfIdle(): void {
39
+ if (tracked.size > 0) return;
40
+ if (timer) { clearInterval(timer); timer = null; }
41
+ }
42
+
43
+ export function registerProcess(pid: number, sessionId: string): void {
44
+ tracked.set(pid, { pid, sessionId, snapshot: { cpu: -1, memory: -1, unchangedCount: 0 } });
45
+ startIfNeeded();
46
+ }
47
+
48
+ export function unregisterProcess(pid: number): void {
49
+ tracked.delete(pid);
50
+ stopIfIdle();
51
+ }
52
+
53
+ // ---------------------------------------------------------------------------
54
+ // 批量查询进程指标(Windows PowerShell)
55
+ // ---------------------------------------------------------------------------
56
+
57
+ function execPowerShell(script: string, timeoutMs: number): Promise<string> {
58
+ return new Promise((resolve, reject) => {
59
+ exec(
60
+ `powershell -NoProfile -Command "${script}"`,
61
+ { timeout: timeoutMs, windowsHide: true },
62
+ (err, stdout) => {
63
+ if (err) reject(err);
64
+ else resolve(stdout);
65
+ },
66
+ );
67
+ });
68
+ }
69
+
70
+ async function getProcessMetrics(pids: number[]): Promise<Map<number, { cpu: number; memory: number }>> {
71
+ const result = new Map<number, { cpu: number; memory: number }>();
72
+ if (pids.length === 0) return result;
73
+
74
+ const psScript = `Get-Process -Id ${pids.join(",")} -ErrorAction SilentlyContinue | ForEach-Object { "$($_.Id)|$($_.CPU)|$($_.WorkingSet64)" }`;
75
+
76
+ try {
77
+ const stdout = await execPowerShell(psScript, 10_000);
78
+ for (const line of stdout.trim().split(/\r?\n/)) {
79
+ const trimmed = line.trim();
80
+ if (!trimmed) continue;
81
+ const [idStr, cpuStr, memStr] = trimmed.split("|");
82
+ const id = parseInt(idStr, 10);
83
+ const cpu = parseFloat(cpuStr);
84
+ const memory = parseInt(memStr, 10);
85
+ if (!isNaN(id) && !isNaN(cpu) && !isNaN(memory)) {
86
+ result.set(id, { cpu, memory });
87
+ }
88
+ }
89
+ } catch {
90
+ // PowerShell 查询失败时跳过本轮,下次重试
91
+ }
92
+
93
+ return result;
94
+ }
95
+
96
+ // ---------------------------------------------------------------------------
97
+ // 定时检查
98
+ // ---------------------------------------------------------------------------
99
+
100
+ async function checkAll(): Promise<void> {
101
+ if (tracked.size === 0) return;
102
+
103
+ const pids = [...tracked.keys()];
104
+ const metrics = await getProcessMetrics(pids);
105
+
106
+ for (const [pid, tp] of tracked) {
107
+ const m = metrics.get(pid);
108
+ if (!m) {
109
+ // 进程已不存在,停止追踪
110
+ tracked.delete(pid);
111
+ continue;
112
+ }
113
+
114
+ const prev = tp.snapshot;
115
+ const cpuChanged = m.cpu !== prev.cpu;
116
+ // 内存允许 ±1% 波动,避免正常抖动触发误判
117
+ const memTolerance = prev.memory > 0 ? Math.max(prev.memory * 0.01, 1024 * 1024) : 1024 * 1024;
118
+ const memChanged = Math.abs(m.memory - prev.memory) > memTolerance;
119
+
120
+ if (!cpuChanged && !memChanged) {
121
+ tp.snapshot.unchangedCount++;
122
+ if (tp.snapshot.unchangedCount >= STUCK_THRESHOLD) {
123
+ const idleMinutes = Math.round(
124
+ (tp.snapshot.unchangedCount * CHECK_INTERVAL_MS) / 60_000,
125
+ );
126
+ resourceMonitor.emit("stuck", {
127
+ pid: tp.pid,
128
+ sessionId: tp.sessionId,
129
+ idleMinutes,
130
+ });
131
+ tracked.delete(pid);
132
+ }
133
+ } else {
134
+ tp.snapshot.unchangedCount = 0;
135
+ }
136
+ tp.snapshot.cpu = m.cpu;
137
+ tp.snapshot.memory = m.memory;
138
+ }
139
+
140
+ stopIfIdle();
141
141
  }
@@ -1254,132 +1254,132 @@ export async function handleCommand(
1254
1254
  }
1255
1255
 
1256
1256
  // /plan <message> — 只读/规划模式
1257
- if (textLower.startsWith("/plan ")) {
1258
- const planText = text.slice(6).trim();
1259
- const resolved = resolveModeCommand("/plan");
1260
- if (!planText) {
1261
- await platform.sendText(chatId, "用法:`/plan <消息>` — 在只读/规划模式下提问。").catch(() => {});
1262
- logTrace(tid, "DONE", { outcome: "plan_no_message" });
1263
- return;
1264
- }
1265
- if (resolved?.supportedTools.includes(descriptionTool) === true) {
1266
- logTrace(tid, "BRANCH", { cmd: "/plan", sessionId, tool: descriptionTool });
1267
-
1268
- await renameUntitledSession(
1269
- platform, chatId, chatType, sessionId, descriptionTool,
1270
- planText, description, chatInfo,
1271
- );
1272
-
1273
- if (isSessionRunning(sessionId)) {
1274
- const queued = enqueueMessage(sessionId, {
1275
- text, chatId, openId, msgTimestamp, chatType, traceId: tid,
1276
- });
1277
- if (queued) {
1278
- logTrace(tid, "QUEUED", { sessionId, mode: "plan" });
1279
- if (platform.kind === "wechat") {
1280
- await platform.sendText(chatId, "当前会话正在生成中,你的消息已进入缓存队列,生成完成后会立即处理。发送 /cancel 可取消缓存。").catch(() => {});
1281
- } else {
1282
- await platform.sendRawCard(chatId, buildQueuedCard(planText)).catch(() => {});
1283
- }
1284
- } else {
1285
- logTrace(tid, "QUEUE_FULL", { sessionId });
1286
- if (platform.kind === "wechat") {
1287
- await platform.sendText(chatId, "当前缓存队列中已有消息等待处理,请等待或发送 /stop(停止生成)或 /cancel(取消缓存)。").catch(() => {});
1288
- } else {
1289
- await platform.sendRawCard(chatId, buildQueueFullCard()).catch(() => {});
1290
- }
1291
- }
1292
- return;
1293
- }
1294
-
1295
- if (shouldSendWechatProcessingAck(platform, planText.toLowerCase(), chatType)) {
1296
- await platform.sendText(chatId, "生成中...").catch(() => {});
1297
- }
1298
-
1299
- try {
1300
- logTrace(tid, "RESUME", { sessionId, tool: descriptionTool, mode: "plan" });
1301
- await resumeAndPrompt(
1302
- sessionId, planText, platform, chatId, msgTimestamp,
1303
- descriptionTool, tid, "plan",
1304
- );
1305
- logTrace(tid, "DONE", { outcome: "plan_done", sessionId });
1306
- } catch (err) {
1307
- logTrace(tid, "DONE", { outcome: "plan_fail", error: (err as Error).message });
1308
- await platform.sendCard(
1309
- chatId, "Error",
1310
- `Failed to run plan mode:\n${(err as Error).message}`,
1311
- "red",
1312
- );
1313
- }
1314
- return;
1315
- }
1316
-
1317
- logTrace(tid, "BRANCH", { cmd: "/plan", sessionId, tool: descriptionTool, fallback: "raw_message" });
1318
- }
1257
+ if (textLower.startsWith("/plan ")) {
1258
+ const planText = text.slice(6).trim();
1259
+ const resolved = resolveModeCommand("/plan");
1260
+ if (!planText) {
1261
+ await platform.sendText(chatId, "用法:`/plan <消息>` — 在只读/规划模式下提问。").catch(() => {});
1262
+ logTrace(tid, "DONE", { outcome: "plan_no_message" });
1263
+ return;
1264
+ }
1265
+ if (resolved?.supportedTools.includes(descriptionTool) === true) {
1266
+ logTrace(tid, "BRANCH", { cmd: "/plan", sessionId, tool: descriptionTool });
1267
+
1268
+ await renameUntitledSession(
1269
+ platform, chatId, chatType, sessionId, descriptionTool,
1270
+ planText, description, chatInfo,
1271
+ );
1272
+
1273
+ if (isSessionRunning(sessionId)) {
1274
+ const queued = enqueueMessage(sessionId, {
1275
+ text, chatId, openId, msgTimestamp, chatType, traceId: tid,
1276
+ });
1277
+ if (queued) {
1278
+ logTrace(tid, "QUEUED", { sessionId, mode: "plan" });
1279
+ if (platform.kind === "wechat") {
1280
+ await platform.sendText(chatId, "当前会话正在生成中,你的消息已进入缓存队列,生成完成后会立即处理。发送 /cancel 可取消缓存。").catch(() => {});
1281
+ } else {
1282
+ await platform.sendRawCard(chatId, buildQueuedCard(planText)).catch(() => {});
1283
+ }
1284
+ } else {
1285
+ logTrace(tid, "QUEUE_FULL", { sessionId });
1286
+ if (platform.kind === "wechat") {
1287
+ await platform.sendText(chatId, "当前缓存队列中已有消息等待处理,请等待或发送 /stop(停止生成)或 /cancel(取消缓存)。").catch(() => {});
1288
+ } else {
1289
+ await platform.sendRawCard(chatId, buildQueueFullCard()).catch(() => {});
1290
+ }
1291
+ }
1292
+ return;
1293
+ }
1294
+
1295
+ if (shouldSendWechatProcessingAck(platform, planText.toLowerCase(), chatType)) {
1296
+ await platform.sendText(chatId, "生成中...").catch(() => {});
1297
+ }
1298
+
1299
+ try {
1300
+ logTrace(tid, "RESUME", { sessionId, tool: descriptionTool, mode: "plan" });
1301
+ await resumeAndPrompt(
1302
+ sessionId, planText, platform, chatId, msgTimestamp,
1303
+ descriptionTool, tid, "plan",
1304
+ );
1305
+ logTrace(tid, "DONE", { outcome: "plan_done", sessionId });
1306
+ } catch (err) {
1307
+ logTrace(tid, "DONE", { outcome: "plan_fail", error: (err as Error).message });
1308
+ await platform.sendCard(
1309
+ chatId, "Error",
1310
+ `Failed to run plan mode:\n${(err as Error).message}`,
1311
+ "red",
1312
+ );
1313
+ }
1314
+ return;
1315
+ }
1316
+
1317
+ logTrace(tid, "BRANCH", { cmd: "/plan", sessionId, tool: descriptionTool, fallback: "raw_message" });
1318
+ }
1319
1319
 
1320
1320
  // /ask <message> — 只读/问答模式
1321
- if (textLower.startsWith("/ask ")) {
1322
- const askText = text.slice(5).trim();
1323
- const resolved = resolveModeCommand("/ask");
1324
- if (!askText) {
1325
- await platform.sendText(chatId, "用法:`/ask <消息>` — 在只读/问答模式下提问。").catch(() => {});
1326
- logTrace(tid, "DONE", { outcome: "ask_no_message" });
1327
- return;
1328
- }
1329
- if (resolved?.supportedTools.includes(descriptionTool) === true) {
1330
- logTrace(tid, "BRANCH", { cmd: "/ask", sessionId, tool: descriptionTool });
1331
-
1332
- await renameUntitledSession(
1333
- platform, chatId, chatType, sessionId, descriptionTool,
1334
- askText, description, chatInfo,
1335
- );
1336
-
1337
- if (isSessionRunning(sessionId)) {
1338
- const queued = enqueueMessage(sessionId, {
1339
- text, chatId, openId, msgTimestamp, chatType, traceId: tid,
1340
- });
1341
- if (queued) {
1342
- logTrace(tid, "QUEUED", { sessionId, mode: "ask" });
1343
- if (platform.kind === "wechat") {
1344
- await platform.sendText(chatId, "当前会话正在生成中,你的消息已进入缓存队列,生成完成后会立即处理。发送 /cancel 可取消缓存。").catch(() => {});
1345
- } else {
1346
- await platform.sendRawCard(chatId, buildQueuedCard(askText)).catch(() => {});
1347
- }
1348
- } else {
1349
- logTrace(tid, "QUEUE_FULL", { sessionId });
1350
- if (platform.kind === "wechat") {
1351
- await platform.sendText(chatId, "当前缓存队列中已有消息等待处理,请等待或发送 /stop(停止生成)或 /cancel(取消缓存)。").catch(() => {});
1352
- } else {
1353
- await platform.sendRawCard(chatId, buildQueueFullCard()).catch(() => {});
1354
- }
1355
- }
1356
- return;
1357
- }
1358
-
1359
- if (shouldSendWechatProcessingAck(platform, askText.toLowerCase(), chatType)) {
1360
- await platform.sendText(chatId, "生成中...").catch(() => {});
1361
- }
1362
-
1363
- try {
1364
- logTrace(tid, "RESUME", { sessionId, tool: descriptionTool, mode: "ask" });
1365
- await resumeAndPrompt(
1366
- sessionId, askText, platform, chatId, msgTimestamp,
1367
- descriptionTool, tid, "ask",
1368
- );
1369
- logTrace(tid, "DONE", { outcome: "ask_done", sessionId });
1370
- } catch (err) {
1371
- logTrace(tid, "DONE", { outcome: "ask_fail", error: (err as Error).message });
1372
- await platform.sendCard(
1373
- chatId, "Error",
1374
- `Failed to run ask mode:\n${(err as Error).message}`,
1375
- "red",
1376
- );
1377
- }
1378
- return;
1379
- }
1380
-
1381
- logTrace(tid, "BRANCH", { cmd: "/ask", sessionId, tool: descriptionTool, fallback: "raw_message" });
1382
- }
1321
+ if (textLower.startsWith("/ask ")) {
1322
+ const askText = text.slice(5).trim();
1323
+ const resolved = resolveModeCommand("/ask");
1324
+ if (!askText) {
1325
+ await platform.sendText(chatId, "用法:`/ask <消息>` — 在只读/问答模式下提问。").catch(() => {});
1326
+ logTrace(tid, "DONE", { outcome: "ask_no_message" });
1327
+ return;
1328
+ }
1329
+ if (resolved?.supportedTools.includes(descriptionTool) === true) {
1330
+ logTrace(tid, "BRANCH", { cmd: "/ask", sessionId, tool: descriptionTool });
1331
+
1332
+ await renameUntitledSession(
1333
+ platform, chatId, chatType, sessionId, descriptionTool,
1334
+ askText, description, chatInfo,
1335
+ );
1336
+
1337
+ if (isSessionRunning(sessionId)) {
1338
+ const queued = enqueueMessage(sessionId, {
1339
+ text, chatId, openId, msgTimestamp, chatType, traceId: tid,
1340
+ });
1341
+ if (queued) {
1342
+ logTrace(tid, "QUEUED", { sessionId, mode: "ask" });
1343
+ if (platform.kind === "wechat") {
1344
+ await platform.sendText(chatId, "当前会话正在生成中,你的消息已进入缓存队列,生成完成后会立即处理。发送 /cancel 可取消缓存。").catch(() => {});
1345
+ } else {
1346
+ await platform.sendRawCard(chatId, buildQueuedCard(askText)).catch(() => {});
1347
+ }
1348
+ } else {
1349
+ logTrace(tid, "QUEUE_FULL", { sessionId });
1350
+ if (platform.kind === "wechat") {
1351
+ await platform.sendText(chatId, "当前缓存队列中已有消息等待处理,请等待或发送 /stop(停止生成)或 /cancel(取消缓存)。").catch(() => {});
1352
+ } else {
1353
+ await platform.sendRawCard(chatId, buildQueueFullCard()).catch(() => {});
1354
+ }
1355
+ }
1356
+ return;
1357
+ }
1358
+
1359
+ if (shouldSendWechatProcessingAck(platform, askText.toLowerCase(), chatType)) {
1360
+ await platform.sendText(chatId, "生成中...").catch(() => {});
1361
+ }
1362
+
1363
+ try {
1364
+ logTrace(tid, "RESUME", { sessionId, tool: descriptionTool, mode: "ask" });
1365
+ await resumeAndPrompt(
1366
+ sessionId, askText, platform, chatId, msgTimestamp,
1367
+ descriptionTool, tid, "ask",
1368
+ );
1369
+ logTrace(tid, "DONE", { outcome: "ask_done", sessionId });
1370
+ } catch (err) {
1371
+ logTrace(tid, "DONE", { outcome: "ask_fail", error: (err as Error).message });
1372
+ await platform.sendCard(
1373
+ chatId, "Error",
1374
+ `Failed to run ask mode:\n${(err as Error).message}`,
1375
+ "red",
1376
+ );
1377
+ }
1378
+ return;
1379
+ }
1380
+
1381
+ logTrace(tid, "BRANCH", { cmd: "/ask", sessionId, tool: descriptionTool, fallback: "raw_message" });
1382
+ }
1383
1383
 
1384
1384
  const lastTs = lastMsgTimestamps.get(chatId);
1385
1385
  if (lastTs !== undefined && msgTimestamp <= lastTs) {