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/README.md +69 -69
- package/package.json +1 -1
- package/src/__tests__/card-plain-text.test.ts +5 -5
- package/src/__tests__/cards.test.ts +68 -22
- package/src/__tests__/codex-reset-actions.test.ts +113 -0
- package/src/__tests__/feishu-avatar.test.ts +90 -9
- package/src/__tests__/feishu-platform.test.ts +22 -16
- package/src/__tests__/orchestrator.test.ts +104 -118
- package/src/__tests__/shared-prefix.test.ts +36 -36
- package/src/cards.ts +78 -12
- package/src/codex-reset-actions.ts +171 -0
- package/src/feishu-api.ts +131 -6
- package/src/feishu-platform.ts +20 -15
- package/src/index.ts +11 -0
- package/src/orchestrator.ts +91 -54
- package/src/shared-prefix.ts +29 -29
- package/src/sim-platform.ts +20 -14
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
|
-
|
|
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
|
-
|
|
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
|
|
437
|
-
if (!
|
|
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 ${
|
|
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
|
|
package/src/feishu-platform.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
|
121
|
-
return _impl.
|
|
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}"`);
|
package/src/orchestrator.ts
CHANGED
|
@@ -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
|
|
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,
|