chatccc 0.2.180 → 0.2.181

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/src/feishu-api.ts CHANGED
@@ -304,6 +304,8 @@ const AVATAR_COMBINATIONS_DIR = resolvePath(AVATAR_DIR, "combinations");
304
304
  const AVATAR_KEY_CACHE_FILE = resolvePath(USER_DATA_DIR, "state", "avatar-image-keys.json");
305
305
  const CODEX_AUTH_FILE = resolvePath(homedir(), ".codex", "auth.json");
306
306
  const CODEX_USAGE_URL = "https://chatgpt.com/backend-api/wham/usage";
307
+ const CODEX_RESET_CREDITS_URL = "https://chatgpt.com/backend-api/wham/rate-limit-reset-credits";
308
+ const CODEX_RESET_CONSUME_URL = "https://chatgpt.com/backend-api/wham/rate-limit-reset-credits/consume";
307
309
  const AVATAR_SOURCES: Record<string, string> = {
308
310
  new: resolvePath(AVATAR_DIR, "status_new.png"),
309
311
  busy: resolvePath(AVATAR_DIR, "status_busy.png"),
@@ -327,9 +329,23 @@ export interface CodexUsageBalance {
327
329
  resetAfterSeconds: number | null;
328
330
  }
329
331
 
332
+ export interface CodexRateLimitResetCredit {
333
+ grantedAt: string | null;
334
+ expiresAt: string;
335
+ }
336
+
330
337
  export interface CodexUsageSummary {
331
338
  fiveHour: CodexUsageBalance;
332
339
  weekly: CodexUsageBalance | null;
340
+ rateLimitResetCreditsAvailable: number | null;
341
+ rateLimitResetCredits: CodexRateLimitResetCredit[] | null;
342
+ }
343
+
344
+ export type CodexResetConsumeCode = "reset" | "nothing_to_reset" | "no_credit" | "already_redeemed";
345
+
346
+ export interface CodexResetConsumeResult {
347
+ code: CodexResetConsumeCode;
348
+ windowsReset: number;
333
349
  }
334
350
 
335
351
  const avatarKeyCache = new Map<string, string>();
@@ -421,35 +437,109 @@ function parseOptionalUsageWindow(rateLimit: Record<string, unknown>, keys: stri
421
437
  return null;
422
438
  }
423
439
 
424
- async function getCodexAccessToken(): Promise<string | null> {
440
+ function parseRateLimitResetCredits(data: Record<string, unknown>): number | null {
441
+ const raw = data.rate_limit_reset_credits;
442
+ if (!raw || typeof raw !== "object") return null;
443
+ const availableCount = Number((raw as { available_count?: unknown }).available_count);
444
+ if (!Number.isFinite(availableCount)) return null;
445
+ return Math.max(0, Math.trunc(availableCount));
446
+ }
447
+
448
+ interface CodexAuth {
449
+ accessToken: string;
450
+ accountId?: string;
451
+ }
452
+
453
+ async function getCodexAuth(): Promise<CodexAuth | null> {
425
454
  try {
426
455
  const raw = await readFile(CODEX_AUTH_FILE, "utf-8");
427
- const parsed = JSON.parse(raw) as { tokens?: { access_token?: unknown } };
456
+ const parsed = JSON.parse(raw) as { tokens?: { access_token?: unknown; account_id?: unknown } };
428
457
  const token = parsed.tokens?.access_token;
429
- return typeof token === "string" && token.trim() ? token : null;
458
+ if (typeof token !== "string" || !token.trim()) return null;
459
+ const accountId = parsed.tokens?.account_id;
460
+ return {
461
+ accessToken: token,
462
+ accountId: typeof accountId === "string" && accountId.trim() ? accountId : undefined,
463
+ };
430
464
  } catch {
431
465
  return null;
432
466
  }
433
467
  }
434
468
 
469
+ async function getCodexAccessToken(): Promise<string | null> {
470
+ return (await getCodexAuth())?.accessToken ?? null;
471
+ }
472
+
473
+ function codexAuthHeaders(auth: CodexAuth): Record<string, string> {
474
+ const headers: Record<string, string> = {
475
+ Authorization: `Bearer ${auth.accessToken}`,
476
+ "OpenAI-Beta": "codex-1",
477
+ originator: "Codex Desktop",
478
+ };
479
+ if (auth.accountId) headers["ChatGPT-Account-ID"] = auth.accountId;
480
+ return headers;
481
+ }
482
+
483
+ function parseCodexResetCreditDetails(data: Record<string, unknown>): {
484
+ availableCount: number | null;
485
+ availableCredits: CodexRateLimitResetCredit[];
486
+ } {
487
+ const availableCount = Number(data.available_count);
488
+ const credits = Array.isArray(data.credits) ? data.credits : [];
489
+ return {
490
+ availableCount: Number.isFinite(availableCount) ? Math.max(0, Math.trunc(availableCount)) : null,
491
+ availableCredits: credits.flatMap((raw) => {
492
+ if (!raw || typeof raw !== "object") return [];
493
+ const credit = raw as { status?: unknown; granted_at?: unknown; expires_at?: unknown };
494
+ if (credit.status !== "available") return [];
495
+ if (typeof credit.expires_at !== "string" || !credit.expires_at.trim()) return [];
496
+ return [{
497
+ grantedAt: typeof credit.granted_at === "string" && credit.granted_at.trim() ? credit.granted_at : null,
498
+ expiresAt: credit.expires_at,
499
+ }];
500
+ }),
501
+ };
502
+ }
503
+
504
+ async function fetchCodexRateLimitResetCredits(auth: CodexAuth): Promise<{
505
+ availableCount: number | null;
506
+ availableCredits: CodexRateLimitResetCredit[];
507
+ } | null> {
508
+ try {
509
+ const resp = await fetch(CODEX_RESET_CREDITS_URL, {
510
+ headers: codexAuthHeaders(auth),
511
+ });
512
+ const text = await resp.text();
513
+ if (!resp.ok) throw new Error(`HTTP ${resp.status}: ${text.slice(0, 160)}`);
514
+ return parseCodexResetCreditDetails(JSON.parse(text) as Record<string, unknown>);
515
+ } catch (err) {
516
+ console.warn(`[Codex] reset credits lookup failed: ${(err as Error).message}`);
517
+ return null;
518
+ }
519
+ }
520
+
435
521
  export async function getCodexUsageSummary(): Promise<CodexUsageSummary> {
436
- const token = await getCodexAccessToken();
437
- if (!token) throw new Error("missing ~/.codex/auth.json access token");
522
+ const auth = await getCodexAuth();
523
+ if (!auth) throw new Error("missing ~/.codex/auth.json access token");
524
+
525
+ const resetCreditsPromise = fetchCodexRateLimitResetCredits(auth);
438
526
 
439
527
  const resp = await fetch(CODEX_USAGE_URL, {
440
- headers: { Authorization: `Bearer ${token}` },
528
+ headers: { Authorization: `Bearer ${auth.accessToken}` },
441
529
  });
442
530
  const text = await resp.text();
443
531
  if (!resp.ok) throw new Error(`HTTP ${resp.status}: ${text.slice(0, 160)}`);
444
532
 
445
533
  const data = JSON.parse(text) as {
446
534
  rate_limit?: Record<string, unknown>;
535
+ rate_limit_reset_credits?: unknown;
447
536
  };
448
537
  const rateLimit = data.rate_limit;
449
538
  if (!rateLimit || typeof rateLimit !== "object") throw new Error("missing rate_limit");
450
539
 
451
540
  const fiveHour = parseOptionalUsageWindow(rateLimit, ["primary_window"]);
452
541
  if (!fiveHour) throw new Error("missing rate_limit.primary_window.used_percent");
542
+ const resetCredits = await resetCreditsPromise;
453
543
 
454
544
  return {
455
545
  fiveHour,
@@ -459,6 +549,41 @@ export async function getCodexUsageSummary(): Promise<CodexUsageSummary> {
459
549
  "week_window",
460
550
  "long_window",
461
551
  ]),
552
+ rateLimitResetCreditsAvailable: resetCredits?.availableCount ?? parseRateLimitResetCredits(data),
553
+ rateLimitResetCredits: resetCredits?.availableCredits ?? null,
554
+ };
555
+ }
556
+
557
+ export async function consumeCodexRateLimitResetCredit(redeemRequestId: string): Promise<CodexResetConsumeResult> {
558
+ if (!redeemRequestId.trim()) throw new Error("missing redeem_request_id");
559
+ const token = await getCodexAccessToken();
560
+ if (!token) throw new Error("missing ~/.codex/auth.json access token");
561
+
562
+ const resp = await fetch(CODEX_RESET_CONSUME_URL, {
563
+ method: "POST",
564
+ headers: {
565
+ Authorization: `Bearer ${token}`,
566
+ "Content-Type": "application/json",
567
+ },
568
+ body: JSON.stringify({ redeem_request_id: redeemRequestId }),
569
+ });
570
+ const text = await resp.text();
571
+ if (!resp.ok) throw new Error(`HTTP ${resp.status}: ${text.slice(0, 160)}`);
572
+
573
+ const data = JSON.parse(text) as { code?: unknown; windows_reset?: unknown };
574
+ const code = data.code;
575
+ if (
576
+ code !== "reset"
577
+ && code !== "nothing_to_reset"
578
+ && code !== "no_credit"
579
+ && code !== "already_redeemed"
580
+ ) {
581
+ throw new Error("missing or unknown reset result code");
582
+ }
583
+ const windowsReset = Number(data.windows_reset);
584
+ return {
585
+ code,
586
+ windowsReset: Number.isFinite(windowsReset) ? Math.max(0, Math.trunc(windowsReset)) : 0,
462
587
  };
463
588
  }
464
589
 
@@ -29,9 +29,10 @@ export interface FeishuPlatform {
29
29
  updateChatInfo: typeof realApi.updateChatInfo;
30
30
  getChatInfo: typeof realApi.getChatInfo;
31
31
  disbandChat: typeof realApi.disbandChat;
32
- setChatAvatar: typeof realApi.setChatAvatar;
33
- getCodexUsageSummary: typeof realApi.getCodexUsageSummary;
34
- getOrDownloadImage: typeof realApi.getOrDownloadImage;
32
+ setChatAvatar: typeof realApi.setChatAvatar;
33
+ getCodexUsageSummary: typeof realApi.getCodexUsageSummary;
34
+ consumeCodexRateLimitResetCredit: typeof realApi.consumeCodexRateLimitResetCredit;
35
+ getOrDownloadImage: typeof realApi.getOrDownloadImage;
35
36
  verifyAllPermissions: typeof realApi.verifyAllPermissions;
36
37
  reportPermissionResults: typeof realApi.reportPermissionResults;
37
38
  extractSessionInfo: typeof realApi.extractSessionInfo;
@@ -109,17 +110,21 @@ export function disbandChat(...args: Parameters<typeof realApi.disbandChat>): Re
109
110
  return _impl.disbandChat(...args);
110
111
  }
111
112
 
112
- export function setChatAvatar(...args: Parameters<typeof realApi.setChatAvatar>): ReturnType<typeof realApi.setChatAvatar> {
113
- return _impl.setChatAvatar(...args);
114
- }
115
-
116
- export function getCodexUsageSummary(...args: Parameters<typeof realApi.getCodexUsageSummary>): ReturnType<typeof realApi.getCodexUsageSummary> {
117
- return _impl.getCodexUsageSummary(...args);
118
- }
119
-
120
- export function getOrDownloadImage(...args: Parameters<typeof realApi.getOrDownloadImage>): ReturnType<typeof realApi.getOrDownloadImage> {
121
- return _impl.getOrDownloadImage(...args);
122
- }
113
+ export function setChatAvatar(...args: Parameters<typeof realApi.setChatAvatar>): ReturnType<typeof realApi.setChatAvatar> {
114
+ return _impl.setChatAvatar(...args);
115
+ }
116
+
117
+ export function getCodexUsageSummary(...args: Parameters<typeof realApi.getCodexUsageSummary>): ReturnType<typeof realApi.getCodexUsageSummary> {
118
+ return _impl.getCodexUsageSummary(...args);
119
+ }
120
+
121
+ export function consumeCodexRateLimitResetCredit(...args: Parameters<typeof realApi.consumeCodexRateLimitResetCredit>): ReturnType<typeof realApi.consumeCodexRateLimitResetCredit> {
122
+ return _impl.consumeCodexRateLimitResetCredit(...args);
123
+ }
124
+
125
+ export function getOrDownloadImage(...args: Parameters<typeof realApi.getOrDownloadImage>): ReturnType<typeof realApi.getOrDownloadImage> {
126
+ return _impl.getOrDownloadImage(...args);
127
+ }
123
128
 
124
129
  export function verifyAllPermissions(...args: Parameters<typeof realApi.verifyAllPermissions>): ReturnType<typeof realApi.verifyAllPermissions> {
125
130
  return _impl.verifyAllPermissions(...args);
@@ -151,4 +156,4 @@ export function sendRestartCard(...args: Parameters<typeof realApi.sendRestartCa
151
156
 
152
157
  export function getMergeForwardMessages(...args: Parameters<typeof realApi.getMergeForwardMessages>): ReturnType<typeof realApi.getMergeForwardMessages> {
153
158
  return _impl.getMergeForwardMessages(...args);
154
- }
159
+ }
package/src/index.ts CHANGED
@@ -74,6 +74,7 @@ import {
74
74
  verifyAllPermissions,
75
75
  reportPermissionResults,
76
76
  setPlatform,
77
+ consumeCodexRateLimitResetCredit,
77
78
  } from "./feishu-platform.ts";
78
79
  import { SimulatedPlatform, SIM_DEFAULT_CHAT_ID } from "./sim-platform.ts";
79
80
  import { setMessageHandler } from "./sim-store.ts";
@@ -103,6 +104,7 @@ import {
103
104
  import { fixStaleStreamStates } from "./stream-state.ts";
104
105
  import { handleCommand, type PlatformAdapter } from "./orchestrator.ts";
105
106
  import { createWechatAdapter, startWechatPlatform } from "./wechat-platform.ts";
107
+ import { handleCodexResetCardAction } from "./codex-reset-actions.ts";
106
108
 
107
109
  // ---------------------------------------------------------------------------
108
110
  // Feishu 平台适配器
@@ -464,6 +466,15 @@ async function startBotServiceCore(): Promise<void> {
464
466
  return;
465
467
  }
466
468
 
469
+ const handledCodexReset = await handleCodexResetCardAction(data, {
470
+ getTenantAccessToken,
471
+ sendRawCard,
472
+ sendCardReply,
473
+ updateCardMessage,
474
+ consumeCodexRateLimitResetCredit,
475
+ });
476
+ if (handledCodexReset) return;
477
+
467
478
  const result = parseCardAction(data);
468
479
  if (!result) return;
469
480
  console.log(`[BTN] chat=${result.chatId} text="${result.text}"`);
@@ -44,6 +44,7 @@ import {
44
44
  buildSessionsCard,
45
45
  buildQueuedCard,
46
46
  buildQueueFullCard,
47
+ buildCodexUsageCard,
47
48
  } from "./cards.ts";
48
49
  import {
49
50
  formatGitResult,
@@ -78,10 +79,10 @@ import {
78
79
  enqueueMessage,
79
80
  cancelQueuedMessage,
80
81
  } from "./session-chat-binding.ts";
81
- import { getCodexUsageSummary, getTenantAccessToken, sendPostMessage } from "./feishu-platform.ts";
82
- import { getCursorUsageSummary, type CursorUsageSummary } from "./cursor-usage.ts";
83
- import { applySharedPrefix } from "./shared-prefix.ts";
84
- export { type PlatformAdapter } from "./platform-adapter.ts";
82
+ import { getCodexUsageSummary, getTenantAccessToken, sendPostMessage } from "./feishu-platform.ts";
83
+ import { getCursorUsageSummary, type CursorUsageSummary } from "./cursor-usage.ts";
84
+ import { applySharedPrefix } from "./shared-prefix.ts";
85
+ export { type PlatformAdapter } from "./platform-adapter.ts";
85
86
  import type { PlatformAdapter } from "./platform-adapter.ts";
86
87
  import type { CodexUsageSummary } from "./feishu-api.ts";
87
88
 
@@ -160,9 +161,42 @@ function formatCodexUsageSummary(usage: CodexUsageSummary): string {
160
161
  ].join("\n");
161
162
  };
162
163
 
164
+ const formatResetCredits = () => {
165
+ if (usage.rateLimitResetCreditsAvailable === null) return "**主动重置:** 暂无数据";
166
+ const lines = [`**主动重置:** 剩余 ${usage.rateLimitResetCreditsAvailable} 次`];
167
+ const credits = usage.rateLimitResetCredits ?? [];
168
+ if (credits.length > 0) {
169
+ const pad = (value: number) => String(value).padStart(2, "0");
170
+ const formatExpiresAt = (value: string) => {
171
+ const date = new Date(value);
172
+ if (!Number.isFinite(date.getTime())) return value;
173
+ return [
174
+ date.getFullYear(),
175
+ "-",
176
+ pad(date.getMonth() + 1),
177
+ "-",
178
+ pad(date.getDate()),
179
+ " ",
180
+ pad(date.getHours()),
181
+ ":",
182
+ pad(date.getMinutes()),
183
+ ":",
184
+ pad(date.getSeconds()),
185
+ ].join("");
186
+ };
187
+ lines.push("**过期时间:**");
188
+ for (const credit of credits) {
189
+ lines.push(`- ${formatExpiresAt(credit.expiresAt)}`);
190
+ }
191
+ }
192
+ return lines.join("\n");
193
+ };
194
+
163
195
  return [
164
196
  "Codex 用量:",
165
197
  "",
198
+ formatResetCredits(),
199
+ "",
166
200
  formatWindow("5h", usage.fiveHour),
167
201
  formatWindow("周", usage.weekly),
168
202
  ].join("\n");
@@ -225,7 +259,7 @@ function formatCursorUsageSummary(usage: CursorUsageSummary): string {
225
259
  }
226
260
 
227
261
  function usageHelpLine(tool: string): string {
228
- if (tool === "codex") return "\n发送 **/usage** 查看 Codex 5h 和周用量。";
262
+ if (tool === "codex") return "\n发送 **/usage** 查看 Codex 5h/周用量,以及查询/使用主动重置卡。";
229
263
  if (tool === "cursor") return "\n发送 **/usage** 查看 Cursor 用量。";
230
264
  return "";
231
265
  }
@@ -250,9 +284,12 @@ async function sendUsageSummary(platform: PlatformAdapter, chatId: string, tool:
250
284
  return;
251
285
  }
252
286
 
253
- const content = formatCodexUsageSummary(await getCodexUsageSummary());
287
+ const usage = await getCodexUsageSummary();
288
+ const content = formatCodexUsageSummary(usage);
254
289
  if (platform.kind === "wechat") {
255
290
  await platform.sendText(chatId, content).catch(() => {});
291
+ } else if (platform.kind === "feishu") {
292
+ await platform.sendRawCard(chatId, buildCodexUsageCard(content, usage.rateLimitResetCreditsAvailable));
256
293
  } else {
257
294
  await platform.sendCard(chatId, "Codex Usage", content, "blue");
258
295
  }
@@ -272,13 +309,13 @@ function isUntitledSessionChatName(name: string): boolean {
272
309
  return name === "新会话" || name.startsWith("新会话-");
273
310
  }
274
311
 
275
- function shouldSendWechatProcessingAck(
276
- platform: PlatformAdapter,
277
- isCommandText: boolean,
278
- chatType: string,
279
- ): boolean {
280
- return platform.kind === "wechat" && chatType === "p2p" && !isCommandText;
281
- }
312
+ function shouldSendWechatProcessingAck(
313
+ platform: PlatformAdapter,
314
+ isCommandText: boolean,
315
+ chatType: string,
316
+ ): boolean {
317
+ return platform.kind === "wechat" && chatType === "p2p" && !isCommandText;
318
+ }
282
319
 
283
320
  function isNonWechatP2p(platform: PlatformAdapter, chatType: string): boolean {
284
321
  return chatType === "p2p" && platform.kind !== "wechat";
@@ -408,17 +445,17 @@ export async function handleCommand(
408
445
  msgTimestamp: number,
409
446
  chatType = "group",
410
447
  traceId?: string,
411
- ): Promise<void> {
412
- const tid = traceId ?? makeTraceId();
413
- const sharedPrefix = applySharedPrefix(text);
414
- const promptText = sharedPrefix.text;
415
- text = sharedPrefix.body;
416
- const textLower = text.toLowerCase();
417
- const isCommandText = !sharedPrefix.matched && textLower.startsWith("/");
418
- recordChatPlatform(chatId, platform);
448
+ ): Promise<void> {
449
+ const tid = traceId ?? makeTraceId();
450
+ const sharedPrefix = applySharedPrefix(text);
451
+ const promptText = sharedPrefix.text;
452
+ text = sharedPrefix.body;
453
+ const textLower = text.toLowerCase();
454
+ const isCommandText = !sharedPrefix.matched && textLower.startsWith("/");
455
+ recordChatPlatform(chatId, platform);
419
456
  await cleanupNonWechatP2pBinding(platform, chatId, chatType, tid);
420
457
 
421
- if (isCommandText && textLower === "/restart") {
458
+ if (isCommandText && textLower === "/restart") {
422
459
  logTrace(tid, "BRANCH", { cmd: "/restart" });
423
460
  await platform.sendText(chatId, "重启中...请几秒后发消息唤醒我").catch(() => {});
424
461
  logTrace(tid, "DONE", { outcome: "restart" });
@@ -453,7 +490,7 @@ export async function handleCommand(
453
490
  return;
454
491
  }
455
492
 
456
- if (isCommandText && textLower === "/update") {
493
+ if (isCommandText && textLower === "/update") {
457
494
  logTrace(tid, "BRANCH", { cmd: "/update" });
458
495
  const isGlobal = isRunningFromGlobalNpm();
459
496
  appendStartupTrace("update: command received", { isGlobal, chatId });
@@ -470,7 +507,7 @@ export async function handleCommand(
470
507
  return;
471
508
  }
472
509
 
473
- if (isCommandText && textLower === "/usage") {
510
+ if (isCommandText && textLower === "/usage") {
474
511
  const usageTool = await resolveUsageTool(chatId);
475
512
  logTrace(tid, "BRANCH", { cmd: "/usage", tool: usageTool });
476
513
  try {
@@ -483,7 +520,7 @@ export async function handleCommand(
483
520
  return;
484
521
  }
485
522
 
486
- if (isCommandText && (textLower === "/cd" || textLower.startsWith("/cd "))) {
523
+ if (isCommandText && (textLower === "/cd" || textLower.startsWith("/cd "))) {
487
524
  logTrace(tid, "BRANCH", {
488
525
  cmd: "/cd",
489
526
  arg: text.slice(3).trim() || "(none)",
@@ -606,7 +643,7 @@ export async function handleCommand(
606
643
  return;
607
644
  }
608
645
 
609
- if (isCommandText && (textLower === "/new" || textLower.startsWith("/new "))) {
646
+ if (isCommandText && (textLower === "/new" || textLower.startsWith("/new "))) {
610
647
  const toolArg = text.slice(5).trim().toLowerCase();
611
648
  const tool = toolArg || resolveDefaultAgentTool();
612
649
  logTrace(tid, "BRANCH", { cmd: "/new", tool });
@@ -867,7 +904,7 @@ export async function handleCommand(
867
904
  if (sessionId && descriptionTool && toolLabel) {
868
905
  // 有会话上下文 — 路由到命令处理或 prompt
869
906
  logTrace(tid, "BRANCH", { sessionId, tool: descriptionTool });
870
- const routeKind = isCommandText ? "command" : "prompt";
907
+ const routeKind = isCommandText ? "command" : "prompt";
871
908
  const chatKind = chatType === "p2p" ? "p2p chat" : "session group";
872
909
  console.log(
873
910
  `[${ts()}] [ROUTE] ${toolLabel} ${chatKind} ${routeKind} detected, session=${sessionId} tool=${descriptionTool}`,
@@ -876,7 +913,7 @@ export async function handleCommand(
876
913
  if (
877
914
  chatType !== "p2p" &&
878
915
  isUntitledSessionChatName(chatInfo!.name) &&
879
- !isCommandText
916
+ !isCommandText
880
917
  ) {
881
918
  const MAX_PREFIX = 10;
882
919
  const prefix = text.slice(0, MAX_PREFIX);
@@ -911,7 +948,7 @@ export async function handleCommand(
911
948
  if (
912
949
  chatType === "p2p" &&
913
950
  platform.kind === "wechat" &&
914
- !isCommandText
951
+ !isCommandText
915
952
  ) {
916
953
  try {
917
954
  const reg = await loadSessionRegistryForBinding();
@@ -950,7 +987,7 @@ export async function handleCommand(
950
987
  }
951
988
  }
952
989
 
953
- if (isCommandText && textLower === "/stop") {
990
+ if (isCommandText && textLower === "/stop") {
954
991
  logTrace(tid, "BRANCH", { cmd: "/stop" });
955
992
  if (stopSession(sessionId)) {
956
993
  console.log(`[${ts()}] [STOP] User sent /stop, session=${sessionId}`);
@@ -964,7 +1001,7 @@ export async function handleCommand(
964
1001
  return;
965
1002
  }
966
1003
 
967
- if (isCommandText && textLower === "/cancel") {
1004
+ if (isCommandText && textLower === "/cancel") {
968
1005
  logTrace(tid, "BRANCH", { cmd: "/cancel" });
969
1006
  if (cancelQueuedMessage(sessionId)) {
970
1007
  console.log(`[${ts()}] [CANCEL] Queue cancelled for session=${sessionId}`);
@@ -977,7 +1014,7 @@ export async function handleCommand(
977
1014
  return;
978
1015
  }
979
1016
 
980
- if (isCommandText && textLower === "/test") {
1017
+ if (isCommandText && textLower === "/test") {
981
1018
  logTrace(tid, "BRANCH", { cmd: "/test" });
982
1019
  const tableHeaders = ["名称", "版本", "状态"];
983
1020
  const tableRows = [
@@ -1014,7 +1051,7 @@ export async function handleCommand(
1014
1051
  return;
1015
1052
  }
1016
1053
 
1017
- if (isCommandText && textLower === "/state") {
1054
+ if (isCommandText && textLower === "/state") {
1018
1055
  logTrace(tid, "BRANCH", { cmd: "/state" });
1019
1056
  const status = await getSessionStatus(chatId);
1020
1057
  const isActive = isSessionRunning(sessionId);
@@ -1053,7 +1090,7 @@ export async function handleCommand(
1053
1090
  return;
1054
1091
  }
1055
1092
 
1056
- if (isCommandText && textLower === "/sessions") {
1093
+ if (isCommandText && textLower === "/sessions") {
1057
1094
  logTrace(tid, "BRANCH", { cmd: "/sessions" });
1058
1095
  const allSessions = await getAllSessionsStatus();
1059
1096
  const now = Date.now();
@@ -1078,7 +1115,7 @@ export async function handleCommand(
1078
1115
  return;
1079
1116
  }
1080
1117
 
1081
- if (isCommandText && textLower === "/newh") {
1118
+ if (isCommandText && textLower === "/newh") {
1082
1119
  logTrace(tid, "BRANCH", { cmd: "/newh" });
1083
1120
  const adapter = getAdapterForTool(descriptionTool, sessionId);
1084
1121
  let cwd: string;
@@ -1165,7 +1202,7 @@ export async function handleCommand(
1165
1202
  return;
1166
1203
  }
1167
1204
 
1168
- if (isCommandText && textLower === "/deleteg") {
1205
+ if (isCommandText && textLower === "/deleteg") {
1169
1206
  logTrace(tid, "BRANCH", { cmd: "/deleteg" });
1170
1207
  if (chatType === "p2p") {
1171
1208
  await platform
@@ -1203,7 +1240,7 @@ export async function handleCommand(
1203
1240
  }
1204
1241
 
1205
1242
  // /session <number>:切换到 /sessions 列表中的指定会话
1206
- const sessionMatch = isCommandText ? textLower.match(/^\/session\s+(\d+)$/) : null;
1243
+ const sessionMatch = isCommandText ? textLower.match(/^\/session\s+(\d+)$/) : null;
1207
1244
  if (sessionMatch) {
1208
1245
  const index = parseInt(sessionMatch[1], 10) - 1;
1209
1246
  logTrace(tid, "BRANCH", { cmd: "/session", index: index + 1 });
@@ -1329,7 +1366,7 @@ export async function handleCommand(
1329
1366
  }
1330
1367
 
1331
1368
  // /model clear — 清除当前 session 的模型覆盖
1332
- if (isCommandText && textLower === "/model clear") {
1369
+ if (isCommandText && textLower === "/model clear") {
1333
1370
  logTrace(tid, "BRANCH", { cmd: "/model clear", sessionId });
1334
1371
  clearSessionModelOverride(sessionId);
1335
1372
  const defaultModel = getEffectiveModelForTool(descriptionTool);
@@ -1344,7 +1381,7 @@ export async function handleCommand(
1344
1381
  }
1345
1382
 
1346
1383
  // /model <name> — 切换当前 session 的模型(支持所有 agent,模糊匹配)
1347
- if (isCommandText && textLower.startsWith("/model ")) {
1384
+ if (isCommandText && textLower.startsWith("/model ")) {
1348
1385
  const modelArg = text.slice(7).trim();
1349
1386
  if (!modelArg) return; // 纯 "/model " 不处理,交给上面的 /model 分支
1350
1387
  logTrace(tid, "BRANCH", { cmd: "/model", arg: modelArg, sessionId, tool: descriptionTool });
@@ -1378,7 +1415,7 @@ export async function handleCommand(
1378
1415
  }
1379
1416
 
1380
1417
  // /model — 查看当前会话的可用模型(根据会话 Agent 类型)
1381
- if (isCommandText && textLower === "/model") {
1418
+ if (isCommandText && textLower === "/model") {
1382
1419
  logTrace(tid, "BRANCH", { cmd: "/model", sessionId, tool: descriptionTool });
1383
1420
  const models = getAllModelsForTool(descriptionTool as AgentTool);
1384
1421
  const currentModel = getEffectiveModelForTool(descriptionTool, sessionId);
@@ -1402,7 +1439,7 @@ export async function handleCommand(
1402
1439
  }
1403
1440
 
1404
1441
  // /git <args>:在「当前会话工作目录」执行 git 命令
1405
- if (isCommandText && (textLower.startsWith("/git ") || textLower === "/git")) {
1442
+ if (isCommandText && (textLower.startsWith("/git ") || textLower === "/git")) {
1406
1443
  const args = text === "/git" ? "" : text.slice(5).trim();
1407
1444
  logTrace(tid, "BRANCH", { cmd: "/git", args: args || "(none)" });
1408
1445
  if (!args) {
@@ -1471,9 +1508,9 @@ export async function handleCommand(
1471
1508
 
1472
1509
  // 并发检查:同一 session 只能有一个活跃 prompt,多余消息进入队列
1473
1510
  if (isSessionRunning(sessionId)) {
1474
- const queued = enqueueMessage(sessionId, {
1475
- text: promptText, chatId, openId, msgTimestamp, chatType, traceId: tid,
1476
- });
1511
+ const queued = enqueueMessage(sessionId, {
1512
+ text: promptText, chatId, openId, msgTimestamp, chatType, traceId: tid,
1513
+ });
1477
1514
  if (queued) {
1478
1515
  logTrace(tid, "QUEUED", { sessionId });
1479
1516
  console.log(
@@ -1498,16 +1535,16 @@ export async function handleCommand(
1498
1535
  return;
1499
1536
  }
1500
1537
 
1501
- if (shouldSendWechatProcessingAck(platform, isCommandText, chatType)) {
1538
+ if (shouldSendWechatProcessingAck(platform, isCommandText, chatType)) {
1502
1539
  await platform.sendText(chatId, "生成中...").catch(() => {});
1503
1540
  }
1504
1541
 
1505
1542
  try {
1506
1543
  logTrace(tid, "RESUME", { sessionId, tool: descriptionTool });
1507
1544
  await resumeAndPrompt(
1508
- sessionId,
1509
- promptText,
1510
- platform,
1545
+ sessionId,
1546
+ promptText,
1547
+ platform,
1511
1548
  chatId,
1512
1549
  msgTimestamp,
1513
1550
  descriptionTool,
@@ -1533,7 +1570,7 @@ export async function handleCommand(
1533
1570
  }
1534
1571
 
1535
1572
  // 无会话上下文 → 检查是否是 /model 查询
1536
- if (isCommandText && textLower === "/model") {
1573
+ if (isCommandText && textLower === "/model") {
1537
1574
  const defaultTool = resolveDefaultAgentTool();
1538
1575
  const models = getAllModelsForTool(defaultTool);
1539
1576
  let currentModel = "";
@@ -1560,7 +1597,7 @@ export async function handleCommand(
1560
1597
  }
1561
1598
 
1562
1599
  // 无会话上下文 → /sessions 仍是有效指令,不触发飞书私聊自动建群。
1563
- if (isCommandText && textLower === "/sessions") {
1600
+ if (isCommandText && textLower === "/sessions") {
1564
1601
  logTrace(tid, "BRANCH", { cmd: "/sessions", scope: "global" });
1565
1602
  const allSessions = await getAllSessionsStatus();
1566
1603
  const now = Date.now();
@@ -1586,7 +1623,7 @@ export async function handleCommand(
1586
1623
  }
1587
1624
 
1588
1625
  // 飞书私聊普通消息:不再绑定私聊本身,而是自动创建会话群并把私聊内容作为首轮 prompt。
1589
- if (isNonWechatP2p(platform, chatType) && !isCommandText) {
1626
+ if (isNonWechatP2p(platform, chatType) && !isCommandText) {
1590
1627
  const tool = resolveDefaultAgentTool();
1591
1628
  const toolLabel = toolDisplayName(tool);
1592
1629
  logTrace(tid, "BRANCH", { cmd: "auto_new_from_p2p", tool });
@@ -1704,9 +1741,9 @@ export async function handleCommand(
1704
1741
  try {
1705
1742
  logTrace(tid, "RESUME", { sessionId, tool, trigger: "auto_new_from_p2p" });
1706
1743
  await resumeAndPrompt(
1707
- sessionId,
1708
- promptText,
1709
- platform,
1744
+ sessionId,
1745
+ promptText,
1746
+ platform,
1710
1747
  newChatId,
1711
1748
  msgTimestamp,
1712
1749
  tool,