chatccc 0.2.183 → 0.2.185
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 +88 -69
- package/config.sample.json +5 -0
- package/im-skills/feishu-skill/skill.md +3 -2
- package/package.json +1 -1
- package/src/__tests__/agent-delegate-task-rpc.test.ts +165 -0
- package/src/__tests__/chatgpt-subscription-rpc.test.ts +89 -0
- package/src/__tests__/chatgpt-subscription.test.ts +135 -0
- package/src/__tests__/chrome-devtools-guard.test.ts +125 -0
- package/src/__tests__/codex-reset-actions.test.ts +146 -146
- package/src/__tests__/config-reload.test.ts +13 -0
- package/src/__tests__/config-sample.test.ts +11 -0
- package/src/__tests__/format-message.test.ts +47 -47
- package/src/__tests__/orchestrator.test.ts +162 -117
- package/src/__tests__/web-ui.test.ts +16 -0
- package/src/agent-delegate-task-rpc.ts +153 -0
- package/src/agent-delegate-task.ts +81 -0
- package/src/chatgpt-subscription-rpc.ts +27 -0
- package/src/chatgpt-subscription.ts +299 -0
- package/src/chrome-devtools-guard.ts +242 -0
- package/src/codex-reset-actions.ts +184 -184
- package/src/config.ts +21 -0
- package/src/format-message.ts +293 -293
- package/src/index.ts +19 -4
- package/src/orchestrator.ts +173 -237
- package/src/session-name.ts +8 -0
- package/src/session.ts +6 -5
- package/src/web-ui.ts +136 -6
package/src/orchestrator.ts
CHANGED
|
@@ -38,14 +38,14 @@ import {
|
|
|
38
38
|
import {
|
|
39
39
|
buildHelpCard,
|
|
40
40
|
buildModelCard,
|
|
41
|
-
buildStatusCard,
|
|
42
|
-
buildCdContent,
|
|
43
|
-
buildCdCard,
|
|
44
|
-
buildSessionsCard,
|
|
45
|
-
buildQueuedCard,
|
|
46
|
-
buildQueueFullCard,
|
|
47
|
-
buildCodexUsageCard,
|
|
48
|
-
} from "./cards.ts";
|
|
41
|
+
buildStatusCard,
|
|
42
|
+
buildCdContent,
|
|
43
|
+
buildCdCard,
|
|
44
|
+
buildSessionsCard,
|
|
45
|
+
buildQueuedCard,
|
|
46
|
+
buildQueueFullCard,
|
|
47
|
+
buildCodexUsageCard,
|
|
48
|
+
} from "./cards.ts";
|
|
49
49
|
import {
|
|
50
50
|
formatGitResult,
|
|
51
51
|
gitResultHeaderTemplate,
|
|
@@ -79,10 +79,13 @@ import {
|
|
|
79
79
|
enqueueMessage,
|
|
80
80
|
cancelQueuedMessage,
|
|
81
81
|
} from "./session-chat-binding.ts";
|
|
82
|
-
import { getCodexUsageSummary, getTenantAccessToken, sendPostMessage } from "./feishu-platform.ts";
|
|
83
|
-
import { getCursorUsageSummary, type CursorUsageSummary } from "./cursor-usage.ts";
|
|
84
|
-
import {
|
|
85
|
-
|
|
82
|
+
import { getCodexUsageSummary, getTenantAccessToken, sendPostMessage } from "./feishu-platform.ts";
|
|
83
|
+
import { getCursorUsageSummary, type CursorUsageSummary } from "./cursor-usage.ts";
|
|
84
|
+
import { getChatGptSubscriptionStatus, type ChatGptSubscriptionResult } from "./chatgpt-subscription.ts";
|
|
85
|
+
import { delegateAgentTask } from "./agent-delegate-task.ts";
|
|
86
|
+
import { applySharedPrefix } from "./shared-prefix.ts";
|
|
87
|
+
import { cwdDisplayName, sessionChatName } from "./session-name.ts";
|
|
88
|
+
export { type PlatformAdapter } from "./platform-adapter.ts";
|
|
86
89
|
import type { PlatformAdapter } from "./platform-adapter.ts";
|
|
87
90
|
import type { CodexUsageSummary } from "./feishu-api.ts";
|
|
88
91
|
|
|
@@ -90,15 +93,6 @@ import type { CodexUsageSummary } from "./feishu-api.ts";
|
|
|
90
93
|
// 辅助函数
|
|
91
94
|
// ---------------------------------------------------------------------------
|
|
92
95
|
|
|
93
|
-
export function cwdDisplayName(cwd: string): string {
|
|
94
|
-
const trimmed = cwd.trim().replace(/[\\/]+$/, "");
|
|
95
|
-
return trimmed.split(/[\\/]/).filter(Boolean).pop() || trimmed || "cwd";
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
export function sessionChatName(left: string, cwd: string): string {
|
|
99
|
-
return `${left}-${cwdDisplayName(cwd)}`;
|
|
100
|
-
}
|
|
101
|
-
|
|
102
96
|
/** 模型模糊匹配:精确匹配优先,否则找子串匹配(模型名越短越优先) */
|
|
103
97
|
function findModelMatch(input: string, models: string[]): string | null {
|
|
104
98
|
if (models.length === 0) return null;
|
|
@@ -114,7 +108,7 @@ function findModelMatch(input: string, models: string[]): string | null {
|
|
|
114
108
|
return candidates[0] ?? null;
|
|
115
109
|
}
|
|
116
110
|
|
|
117
|
-
function formatCodexUsageSummary(usage: CodexUsageSummary): string {
|
|
111
|
+
function formatCodexUsageSummary(usage: CodexUsageSummary, chatGptSubscription: ChatGptSubscriptionResult | null = null): string {
|
|
118
112
|
const progressBar = (usedPercent: number) => {
|
|
119
113
|
const width = 20;
|
|
120
114
|
const usedBlocks = Math.max(0, Math.min(width, Math.round((usedPercent / 100) * width)));
|
|
@@ -153,54 +147,87 @@ function formatCodexUsageSummary(usage: CodexUsageSummary): string {
|
|
|
153
147
|
return `${absolute}${formatDuration(balance.resetAfterSeconds)}`;
|
|
154
148
|
};
|
|
155
149
|
|
|
156
|
-
const formatWindow = (label: string, balance: CodexUsageSummary["fiveHour"] | null) => {
|
|
157
|
-
if (!balance) return `**${label}:** 暂无数据`;
|
|
158
|
-
return [
|
|
159
|
-
`**${label}:** 已用 ${balance.usedPercent}%,剩余 ${balance.remainingPercent}%,重置: ${formatResetTime(balance)}`,
|
|
160
|
-
progressBar(balance.usedPercent),
|
|
161
|
-
].join("\n");
|
|
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
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
150
|
+
const formatWindow = (label: string, balance: CodexUsageSummary["fiveHour"] | null) => {
|
|
151
|
+
if (!balance) return `**${label}:** 暂无数据`;
|
|
152
|
+
return [
|
|
153
|
+
`**${label}:** 已用 ${balance.usedPercent}%,剩余 ${balance.remainingPercent}%,重置: ${formatResetTime(balance)}`,
|
|
154
|
+
progressBar(balance.usedPercent),
|
|
155
|
+
].join("\n");
|
|
156
|
+
};
|
|
157
|
+
|
|
158
|
+
const formatResetCredits = () => {
|
|
159
|
+
if (usage.rateLimitResetCreditsAvailable === null) return "**主动重置:** 暂无数据";
|
|
160
|
+
const lines = [`**主动重置:** 剩余 ${usage.rateLimitResetCreditsAvailable} 次`];
|
|
161
|
+
const credits = usage.rateLimitResetCredits ?? [];
|
|
162
|
+
if (credits.length > 0) {
|
|
163
|
+
const pad = (value: number) => String(value).padStart(2, "0");
|
|
164
|
+
const formatExpiresAt = (value: string) => {
|
|
165
|
+
const date = new Date(value);
|
|
166
|
+
if (!Number.isFinite(date.getTime())) return value;
|
|
167
|
+
return [
|
|
168
|
+
date.getFullYear(),
|
|
169
|
+
"-",
|
|
170
|
+
pad(date.getMonth() + 1),
|
|
171
|
+
"-",
|
|
172
|
+
pad(date.getDate()),
|
|
173
|
+
" ",
|
|
174
|
+
pad(date.getHours()),
|
|
175
|
+
":",
|
|
176
|
+
pad(date.getMinutes()),
|
|
177
|
+
":",
|
|
178
|
+
pad(date.getSeconds()),
|
|
179
|
+
].join("");
|
|
180
|
+
};
|
|
181
|
+
lines.push("**过期时间:**");
|
|
182
|
+
for (const credit of credits) {
|
|
183
|
+
lines.push(`- ${formatExpiresAt(credit.expiresAt)}`);
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
return lines.join("\n");
|
|
187
|
+
};
|
|
188
|
+
|
|
189
|
+
const formatChatGptSubscription = () => {
|
|
190
|
+
if (!chatGptSubscription?.ok || !chatGptSubscription.subscription) return "";
|
|
191
|
+
const subscription = chatGptSubscription.subscription;
|
|
192
|
+
const pad = (value: number) => String(value).padStart(2, "0");
|
|
193
|
+
const formatExpiresAt = (value: string | null) => {
|
|
194
|
+
if (!value) return "暂无数据";
|
|
195
|
+
const date = new Date(value);
|
|
196
|
+
if (!Number.isFinite(date.getTime())) return value;
|
|
197
|
+
return [
|
|
198
|
+
date.getFullYear(),
|
|
199
|
+
"-",
|
|
200
|
+
pad(date.getMonth() + 1),
|
|
201
|
+
"-",
|
|
202
|
+
pad(date.getDate()),
|
|
203
|
+
" ",
|
|
204
|
+
pad(date.getHours()),
|
|
205
|
+
":",
|
|
206
|
+
pad(date.getMinutes()),
|
|
207
|
+
].join("");
|
|
208
|
+
};
|
|
209
|
+
const remaining = typeof subscription.remainingDays === "number"
|
|
210
|
+
? `(剩余 ${subscription.remainingDays} 天)`
|
|
211
|
+
: "";
|
|
212
|
+
return [
|
|
213
|
+
"**ChatGPT 订阅:**",
|
|
214
|
+
`- 套餐: ${subscription.plan ?? "暂无数据"}`,
|
|
215
|
+
`- 到期: ${formatExpiresAt(subscription.expiresAt)}${remaining}`,
|
|
216
|
+
`- 自动续费: ${subscription.willRenew === null ? "暂无数据" : subscription.willRenew ? "是" : "否"}`,
|
|
217
|
+
].join("\n");
|
|
218
|
+
};
|
|
219
|
+
|
|
220
|
+
return [
|
|
221
|
+
"Codex 用量:",
|
|
222
|
+
"",
|
|
223
|
+
formatChatGptSubscription(),
|
|
224
|
+
chatGptSubscription?.ok ? "" : "",
|
|
225
|
+
formatResetCredits(),
|
|
226
|
+
"",
|
|
227
|
+
formatWindow("5h", usage.fiveHour),
|
|
228
|
+
formatWindow("周", usage.weekly),
|
|
229
|
+
].filter((line, index, arr) => line !== "" || (index > 0 && arr[index - 1] !== "")).join("\n");
|
|
230
|
+
}
|
|
204
231
|
|
|
205
232
|
function formatCursorUsageSummary(usage: CursorUsageSummary): string {
|
|
206
233
|
const timeZone = Intl.DateTimeFormat().resolvedOptions().timeZone;
|
|
@@ -259,7 +286,7 @@ function formatCursorUsageSummary(usage: CursorUsageSummary): string {
|
|
|
259
286
|
}
|
|
260
287
|
|
|
261
288
|
function usageHelpLine(tool: string): string {
|
|
262
|
-
if (tool === "codex") return "\n发送 **/usage** 查看 Codex 5h/周用量,以及查询/使用主动重置卡。";
|
|
289
|
+
if (tool === "codex") return "\n发送 **/usage** 查看 Codex 5h/周用量,以及查询/使用主动重置卡。";
|
|
263
290
|
if (tool === "cursor") return "\n发送 **/usage** 查看 Cursor 用量。";
|
|
264
291
|
return "";
|
|
265
292
|
}
|
|
@@ -284,16 +311,19 @@ async function sendUsageSummary(platform: PlatformAdapter, chatId: string, tool:
|
|
|
284
311
|
return;
|
|
285
312
|
}
|
|
286
313
|
|
|
287
|
-
const usage = await
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
}
|
|
314
|
+
const [usage, chatGptSubscription] = await Promise.all([
|
|
315
|
+
getCodexUsageSummary(),
|
|
316
|
+
getChatGptSubscriptionStatus().catch(() => null),
|
|
317
|
+
]);
|
|
318
|
+
const content = formatCodexUsageSummary(usage, chatGptSubscription);
|
|
319
|
+
if (platform.kind === "wechat") {
|
|
320
|
+
await platform.sendText(chatId, content).catch(() => {});
|
|
321
|
+
} else if (platform.kind === "feishu") {
|
|
322
|
+
await platform.sendRawCard(chatId, buildCodexUsageCard(content, usage.rateLimitResetCreditsAvailable));
|
|
323
|
+
} else {
|
|
324
|
+
await platform.sendCard(chatId, "Codex Usage", content, "blue");
|
|
325
|
+
}
|
|
326
|
+
}
|
|
297
327
|
|
|
298
328
|
async function sendUsageError(platform: PlatformAdapter, chatId: string, tool: "codex" | "cursor", err: unknown): Promise<void> {
|
|
299
329
|
const toolLabel = tool === "cursor" ? "Cursor" : "Codex";
|
|
@@ -309,13 +339,13 @@ function isUntitledSessionChatName(name: string): boolean {
|
|
|
309
339
|
return name === "新会话" || name.startsWith("新会话-");
|
|
310
340
|
}
|
|
311
341
|
|
|
312
|
-
function shouldSendWechatProcessingAck(
|
|
313
|
-
platform: PlatformAdapter,
|
|
314
|
-
isCommandText: boolean,
|
|
315
|
-
chatType: string,
|
|
316
|
-
): boolean {
|
|
317
|
-
return platform.kind === "wechat" && chatType === "p2p" && !isCommandText;
|
|
318
|
-
}
|
|
342
|
+
function shouldSendWechatProcessingAck(
|
|
343
|
+
platform: PlatformAdapter,
|
|
344
|
+
isCommandText: boolean,
|
|
345
|
+
chatType: string,
|
|
346
|
+
): boolean {
|
|
347
|
+
return platform.kind === "wechat" && chatType === "p2p" && !isCommandText;
|
|
348
|
+
}
|
|
319
349
|
|
|
320
350
|
function isNonWechatP2p(platform: PlatformAdapter, chatType: string): boolean {
|
|
321
351
|
return chatType === "p2p" && platform.kind !== "wechat";
|
|
@@ -445,17 +475,17 @@ export async function handleCommand(
|
|
|
445
475
|
msgTimestamp: number,
|
|
446
476
|
chatType = "group",
|
|
447
477
|
traceId?: string,
|
|
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);
|
|
478
|
+
): Promise<void> {
|
|
479
|
+
const tid = traceId ?? makeTraceId();
|
|
480
|
+
const sharedPrefix = applySharedPrefix(text);
|
|
481
|
+
const promptText = sharedPrefix.text;
|
|
482
|
+
text = sharedPrefix.body;
|
|
483
|
+
const textLower = text.toLowerCase();
|
|
484
|
+
const isCommandText = !sharedPrefix.matched && textLower.startsWith("/");
|
|
485
|
+
recordChatPlatform(chatId, platform);
|
|
456
486
|
await cleanupNonWechatP2pBinding(platform, chatId, chatType, tid);
|
|
457
487
|
|
|
458
|
-
if (isCommandText && textLower === "/restart") {
|
|
488
|
+
if (isCommandText && textLower === "/restart") {
|
|
459
489
|
logTrace(tid, "BRANCH", { cmd: "/restart" });
|
|
460
490
|
await platform.sendText(chatId, "重启中...请几秒后发消息唤醒我").catch(() => {});
|
|
461
491
|
logTrace(tid, "DONE", { outcome: "restart" });
|
|
@@ -490,7 +520,7 @@ export async function handleCommand(
|
|
|
490
520
|
return;
|
|
491
521
|
}
|
|
492
522
|
|
|
493
|
-
if (isCommandText && textLower === "/update") {
|
|
523
|
+
if (isCommandText && textLower === "/update") {
|
|
494
524
|
logTrace(tid, "BRANCH", { cmd: "/update" });
|
|
495
525
|
const isGlobal = isRunningFromGlobalNpm();
|
|
496
526
|
appendStartupTrace("update: command received", { isGlobal, chatId });
|
|
@@ -507,7 +537,7 @@ export async function handleCommand(
|
|
|
507
537
|
return;
|
|
508
538
|
}
|
|
509
539
|
|
|
510
|
-
if (isCommandText && textLower === "/usage") {
|
|
540
|
+
if (isCommandText && textLower === "/usage") {
|
|
511
541
|
const usageTool = await resolveUsageTool(chatId);
|
|
512
542
|
logTrace(tid, "BRANCH", { cmd: "/usage", tool: usageTool });
|
|
513
543
|
try {
|
|
@@ -520,7 +550,7 @@ export async function handleCommand(
|
|
|
520
550
|
return;
|
|
521
551
|
}
|
|
522
552
|
|
|
523
|
-
if (isCommandText && (textLower === "/cd" || textLower.startsWith("/cd "))) {
|
|
553
|
+
if (isCommandText && (textLower === "/cd" || textLower.startsWith("/cd "))) {
|
|
524
554
|
logTrace(tid, "BRANCH", {
|
|
525
555
|
cmd: "/cd",
|
|
526
556
|
arg: text.slice(3).trim() || "(none)",
|
|
@@ -643,7 +673,7 @@ export async function handleCommand(
|
|
|
643
673
|
return;
|
|
644
674
|
}
|
|
645
675
|
|
|
646
|
-
if (isCommandText && (textLower === "/new" || textLower.startsWith("/new "))) {
|
|
676
|
+
if (isCommandText && (textLower === "/new" || textLower.startsWith("/new "))) {
|
|
647
677
|
const toolArg = text.slice(5).trim().toLowerCase();
|
|
648
678
|
const tool = toolArg || resolveDefaultAgentTool();
|
|
649
679
|
logTrace(tid, "BRANCH", { cmd: "/new", tool });
|
|
@@ -904,7 +934,7 @@ export async function handleCommand(
|
|
|
904
934
|
if (sessionId && descriptionTool && toolLabel) {
|
|
905
935
|
// 有会话上下文 — 路由到命令处理或 prompt
|
|
906
936
|
logTrace(tid, "BRANCH", { sessionId, tool: descriptionTool });
|
|
907
|
-
const routeKind = isCommandText ? "command" : "prompt";
|
|
937
|
+
const routeKind = isCommandText ? "command" : "prompt";
|
|
908
938
|
const chatKind = chatType === "p2p" ? "p2p chat" : "session group";
|
|
909
939
|
console.log(
|
|
910
940
|
`[${ts()}] [ROUTE] ${toolLabel} ${chatKind} ${routeKind} detected, session=${sessionId} tool=${descriptionTool}`,
|
|
@@ -913,7 +943,7 @@ export async function handleCommand(
|
|
|
913
943
|
if (
|
|
914
944
|
chatType !== "p2p" &&
|
|
915
945
|
isUntitledSessionChatName(chatInfo!.name) &&
|
|
916
|
-
!isCommandText
|
|
946
|
+
!isCommandText
|
|
917
947
|
) {
|
|
918
948
|
const MAX_PREFIX = 10;
|
|
919
949
|
const prefix = text.slice(0, MAX_PREFIX);
|
|
@@ -948,7 +978,7 @@ export async function handleCommand(
|
|
|
948
978
|
if (
|
|
949
979
|
chatType === "p2p" &&
|
|
950
980
|
platform.kind === "wechat" &&
|
|
951
|
-
!isCommandText
|
|
981
|
+
!isCommandText
|
|
952
982
|
) {
|
|
953
983
|
try {
|
|
954
984
|
const reg = await loadSessionRegistryForBinding();
|
|
@@ -987,7 +1017,7 @@ export async function handleCommand(
|
|
|
987
1017
|
}
|
|
988
1018
|
}
|
|
989
1019
|
|
|
990
|
-
if (isCommandText && textLower === "/stop") {
|
|
1020
|
+
if (isCommandText && textLower === "/stop") {
|
|
991
1021
|
logTrace(tid, "BRANCH", { cmd: "/stop" });
|
|
992
1022
|
if (stopSession(sessionId)) {
|
|
993
1023
|
console.log(`[${ts()}] [STOP] User sent /stop, session=${sessionId}`);
|
|
@@ -1001,7 +1031,7 @@ export async function handleCommand(
|
|
|
1001
1031
|
return;
|
|
1002
1032
|
}
|
|
1003
1033
|
|
|
1004
|
-
if (isCommandText && textLower === "/cancel") {
|
|
1034
|
+
if (isCommandText && textLower === "/cancel") {
|
|
1005
1035
|
logTrace(tid, "BRANCH", { cmd: "/cancel" });
|
|
1006
1036
|
if (cancelQueuedMessage(sessionId)) {
|
|
1007
1037
|
console.log(`[${ts()}] [CANCEL] Queue cancelled for session=${sessionId}`);
|
|
@@ -1014,7 +1044,7 @@ export async function handleCommand(
|
|
|
1014
1044
|
return;
|
|
1015
1045
|
}
|
|
1016
1046
|
|
|
1017
|
-
if (isCommandText && textLower === "/test") {
|
|
1047
|
+
if (isCommandText && textLower === "/test") {
|
|
1018
1048
|
logTrace(tid, "BRANCH", { cmd: "/test" });
|
|
1019
1049
|
const tableHeaders = ["名称", "版本", "状态"];
|
|
1020
1050
|
const tableRows = [
|
|
@@ -1051,7 +1081,7 @@ export async function handleCommand(
|
|
|
1051
1081
|
return;
|
|
1052
1082
|
}
|
|
1053
1083
|
|
|
1054
|
-
if (isCommandText && textLower === "/state") {
|
|
1084
|
+
if (isCommandText && textLower === "/state") {
|
|
1055
1085
|
logTrace(tid, "BRANCH", { cmd: "/state" });
|
|
1056
1086
|
const status = await getSessionStatus(chatId);
|
|
1057
1087
|
const isActive = isSessionRunning(sessionId);
|
|
@@ -1090,7 +1120,7 @@ export async function handleCommand(
|
|
|
1090
1120
|
return;
|
|
1091
1121
|
}
|
|
1092
1122
|
|
|
1093
|
-
if (isCommandText && textLower === "/sessions") {
|
|
1123
|
+
if (isCommandText && textLower === "/sessions") {
|
|
1094
1124
|
logTrace(tid, "BRANCH", { cmd: "/sessions" });
|
|
1095
1125
|
const allSessions = await getAllSessionsStatus();
|
|
1096
1126
|
const now = Date.now();
|
|
@@ -1115,7 +1145,7 @@ export async function handleCommand(
|
|
|
1115
1145
|
return;
|
|
1116
1146
|
}
|
|
1117
1147
|
|
|
1118
|
-
if (isCommandText && textLower === "/newh") {
|
|
1148
|
+
if (isCommandText && textLower === "/newh") {
|
|
1119
1149
|
logTrace(tid, "BRANCH", { cmd: "/newh" });
|
|
1120
1150
|
const adapter = getAdapterForTool(descriptionTool, sessionId);
|
|
1121
1151
|
let cwd: string;
|
|
@@ -1202,7 +1232,7 @@ export async function handleCommand(
|
|
|
1202
1232
|
return;
|
|
1203
1233
|
}
|
|
1204
1234
|
|
|
1205
|
-
if (isCommandText && textLower === "/deleteg") {
|
|
1235
|
+
if (isCommandText && textLower === "/deleteg") {
|
|
1206
1236
|
logTrace(tid, "BRANCH", { cmd: "/deleteg" });
|
|
1207
1237
|
if (chatType === "p2p") {
|
|
1208
1238
|
await platform
|
|
@@ -1240,7 +1270,7 @@ export async function handleCommand(
|
|
|
1240
1270
|
}
|
|
1241
1271
|
|
|
1242
1272
|
// /session <number>:切换到 /sessions 列表中的指定会话
|
|
1243
|
-
const sessionMatch = isCommandText ? textLower.match(/^\/session\s+(\d+)$/) : null;
|
|
1273
|
+
const sessionMatch = isCommandText ? textLower.match(/^\/session\s+(\d+)$/) : null;
|
|
1244
1274
|
if (sessionMatch) {
|
|
1245
1275
|
const index = parseInt(sessionMatch[1], 10) - 1;
|
|
1246
1276
|
logTrace(tid, "BRANCH", { cmd: "/session", index: index + 1 });
|
|
@@ -1366,7 +1396,7 @@ export async function handleCommand(
|
|
|
1366
1396
|
}
|
|
1367
1397
|
|
|
1368
1398
|
// /model clear — 清除当前 session 的模型覆盖
|
|
1369
|
-
if (isCommandText && textLower === "/model clear") {
|
|
1399
|
+
if (isCommandText && textLower === "/model clear") {
|
|
1370
1400
|
logTrace(tid, "BRANCH", { cmd: "/model clear", sessionId });
|
|
1371
1401
|
clearSessionModelOverride(sessionId);
|
|
1372
1402
|
const defaultModel = getEffectiveModelForTool(descriptionTool);
|
|
@@ -1381,7 +1411,7 @@ export async function handleCommand(
|
|
|
1381
1411
|
}
|
|
1382
1412
|
|
|
1383
1413
|
// /model <name> — 切换当前 session 的模型(支持所有 agent,模糊匹配)
|
|
1384
|
-
if (isCommandText && textLower.startsWith("/model ")) {
|
|
1414
|
+
if (isCommandText && textLower.startsWith("/model ")) {
|
|
1385
1415
|
const modelArg = text.slice(7).trim();
|
|
1386
1416
|
if (!modelArg) return; // 纯 "/model " 不处理,交给上面的 /model 分支
|
|
1387
1417
|
logTrace(tid, "BRANCH", { cmd: "/model", arg: modelArg, sessionId, tool: descriptionTool });
|
|
@@ -1415,7 +1445,7 @@ export async function handleCommand(
|
|
|
1415
1445
|
}
|
|
1416
1446
|
|
|
1417
1447
|
// /model — 查看当前会话的可用模型(根据会话 Agent 类型)
|
|
1418
|
-
if (isCommandText && textLower === "/model") {
|
|
1448
|
+
if (isCommandText && textLower === "/model") {
|
|
1419
1449
|
logTrace(tid, "BRANCH", { cmd: "/model", sessionId, tool: descriptionTool });
|
|
1420
1450
|
const models = getAllModelsForTool(descriptionTool as AgentTool);
|
|
1421
1451
|
const currentModel = getEffectiveModelForTool(descriptionTool, sessionId);
|
|
@@ -1439,7 +1469,7 @@ export async function handleCommand(
|
|
|
1439
1469
|
}
|
|
1440
1470
|
|
|
1441
1471
|
// /git <args>:在「当前会话工作目录」执行 git 命令
|
|
1442
|
-
if (isCommandText && (textLower.startsWith("/git ") || textLower === "/git")) {
|
|
1472
|
+
if (isCommandText && (textLower.startsWith("/git ") || textLower === "/git")) {
|
|
1443
1473
|
const args = text === "/git" ? "" : text.slice(5).trim();
|
|
1444
1474
|
logTrace(tid, "BRANCH", { cmd: "/git", args: args || "(none)" });
|
|
1445
1475
|
if (!args) {
|
|
@@ -1508,9 +1538,9 @@ export async function handleCommand(
|
|
|
1508
1538
|
|
|
1509
1539
|
// 并发检查:同一 session 只能有一个活跃 prompt,多余消息进入队列
|
|
1510
1540
|
if (isSessionRunning(sessionId)) {
|
|
1511
|
-
const queued = enqueueMessage(sessionId, {
|
|
1512
|
-
text: promptText, chatId, openId, msgTimestamp, chatType, traceId: tid,
|
|
1513
|
-
});
|
|
1541
|
+
const queued = enqueueMessage(sessionId, {
|
|
1542
|
+
text: promptText, chatId, openId, msgTimestamp, chatType, traceId: tid,
|
|
1543
|
+
});
|
|
1514
1544
|
if (queued) {
|
|
1515
1545
|
logTrace(tid, "QUEUED", { sessionId });
|
|
1516
1546
|
console.log(
|
|
@@ -1535,16 +1565,16 @@ export async function handleCommand(
|
|
|
1535
1565
|
return;
|
|
1536
1566
|
}
|
|
1537
1567
|
|
|
1538
|
-
if (shouldSendWechatProcessingAck(platform, isCommandText, chatType)) {
|
|
1568
|
+
if (shouldSendWechatProcessingAck(platform, isCommandText, chatType)) {
|
|
1539
1569
|
await platform.sendText(chatId, "生成中...").catch(() => {});
|
|
1540
1570
|
}
|
|
1541
1571
|
|
|
1542
1572
|
try {
|
|
1543
1573
|
logTrace(tid, "RESUME", { sessionId, tool: descriptionTool });
|
|
1544
1574
|
await resumeAndPrompt(
|
|
1545
|
-
sessionId,
|
|
1546
|
-
promptText,
|
|
1547
|
-
platform,
|
|
1575
|
+
sessionId,
|
|
1576
|
+
promptText,
|
|
1577
|
+
platform,
|
|
1548
1578
|
chatId,
|
|
1549
1579
|
msgTimestamp,
|
|
1550
1580
|
descriptionTool,
|
|
@@ -1570,7 +1600,7 @@ export async function handleCommand(
|
|
|
1570
1600
|
}
|
|
1571
1601
|
|
|
1572
1602
|
// 无会话上下文 → 检查是否是 /model 查询
|
|
1573
|
-
if (isCommandText && textLower === "/model") {
|
|
1603
|
+
if (isCommandText && textLower === "/model") {
|
|
1574
1604
|
const defaultTool = resolveDefaultAgentTool();
|
|
1575
1605
|
const models = getAllModelsForTool(defaultTool);
|
|
1576
1606
|
let currentModel = "";
|
|
@@ -1597,7 +1627,7 @@ export async function handleCommand(
|
|
|
1597
1627
|
}
|
|
1598
1628
|
|
|
1599
1629
|
// 无会话上下文 → /sessions 仍是有效指令,不触发飞书私聊自动建群。
|
|
1600
|
-
if (isCommandText && textLower === "/sessions") {
|
|
1630
|
+
if (isCommandText && textLower === "/sessions") {
|
|
1601
1631
|
logTrace(tid, "BRANCH", { cmd: "/sessions", scope: "global" });
|
|
1602
1632
|
const allSessions = await getAllSessionsStatus();
|
|
1603
1633
|
const now = Date.now();
|
|
@@ -1623,7 +1653,7 @@ export async function handleCommand(
|
|
|
1623
1653
|
}
|
|
1624
1654
|
|
|
1625
1655
|
// 飞书私聊普通消息:不再绑定私聊本身,而是自动创建会话群并把私聊内容作为首轮 prompt。
|
|
1626
|
-
if (isNonWechatP2p(platform, chatType) && !isCommandText) {
|
|
1656
|
+
if (isNonWechatP2p(platform, chatType) && !isCommandText) {
|
|
1627
1657
|
const tool = resolveDefaultAgentTool();
|
|
1628
1658
|
const toolLabel = toolDisplayName(tool);
|
|
1629
1659
|
logTrace(tid, "BRANCH", { cmd: "auto_new_from_p2p", tool });
|
|
@@ -1639,128 +1669,34 @@ export async function handleCommand(
|
|
|
1639
1669
|
return;
|
|
1640
1670
|
}
|
|
1641
1671
|
|
|
1642
|
-
let sessionId: string;
|
|
1643
|
-
let sessionCwd: string;
|
|
1644
1672
|
try {
|
|
1645
|
-
const
|
|
1646
|
-
|
|
1647
|
-
|
|
1648
|
-
|
|
1649
|
-
|
|
1650
|
-
|
|
1651
|
-
|
|
1652
|
-
|
|
1653
|
-
|
|
1654
|
-
|
|
1655
|
-
error: (err as Error).message,
|
|
1673
|
+
const cwd = await getDefaultCwd(chatId);
|
|
1674
|
+
const result = await delegateAgentTask({
|
|
1675
|
+
platform,
|
|
1676
|
+
tool,
|
|
1677
|
+
cwd,
|
|
1678
|
+
promptText,
|
|
1679
|
+
openIds: [openId],
|
|
1680
|
+
chatNamePrefix: text.slice(0, 10) || "新会话",
|
|
1681
|
+
msgTimestamp,
|
|
1682
|
+
traceId: tid,
|
|
1656
1683
|
});
|
|
1657
|
-
await platform.sendCard(
|
|
1658
|
-
chatId,
|
|
1659
|
-
"Error",
|
|
1660
|
-
`Failed to initialize ${toolLabel} session:\n${(err as Error).message}`,
|
|
1661
|
-
"red",
|
|
1662
|
-
);
|
|
1663
|
-
return;
|
|
1664
|
-
}
|
|
1665
|
-
|
|
1666
|
-
const cwd = sessionCwd;
|
|
1667
|
-
const initialName = sessionChatName(text.slice(0, 10) || "新会话", cwd);
|
|
1668
|
-
let newChatId: string;
|
|
1669
|
-
try {
|
|
1670
|
-
newChatId = await platform.createGroup(initialName, [openId]);
|
|
1671
|
-
console.log(
|
|
1672
|
-
`[${ts()}] [AUTO-P2P 2/5] Created Feishu group: ${newChatId} → OK`,
|
|
1673
|
-
);
|
|
1674
|
-
} catch (err) {
|
|
1675
|
-
console.error(`[${ts()}] [AUTO-P2P 2/5] FAIL: ${(err as Error).message}`);
|
|
1676
1684
|
logTrace(tid, "DONE", {
|
|
1677
|
-
outcome: "
|
|
1678
|
-
|
|
1685
|
+
outcome: "auto_new_p2p_prompt_done",
|
|
1686
|
+
newChatId: result.chatId,
|
|
1687
|
+
sessionId: result.sessionId,
|
|
1688
|
+
tool,
|
|
1679
1689
|
});
|
|
1680
|
-
await platform.sendCard(
|
|
1681
|
-
chatId,
|
|
1682
|
-
"Error",
|
|
1683
|
-
`Failed to create group:\n${(err as Error).message}`,
|
|
1684
|
-
"red",
|
|
1685
|
-
);
|
|
1686
|
-
return;
|
|
1687
|
-
}
|
|
1688
|
-
|
|
1689
|
-
try {
|
|
1690
|
-
const descPrefix = sessionPrefixForTool(tool);
|
|
1691
|
-
await platform.updateChatInfo(
|
|
1692
|
-
newChatId,
|
|
1693
|
-
initialName,
|
|
1694
|
-
`${descPrefix} ${sessionId}`,
|
|
1695
|
-
);
|
|
1696
|
-
console.log(
|
|
1697
|
-
`[${ts()}] [AUTO-P2P 3/5] Renamed group → name="${initialName}" (${toolLabel}) → OK`,
|
|
1698
|
-
);
|
|
1699
1690
|
} catch (err) {
|
|
1700
|
-
console.error(`[${ts()}] [AUTO-P2P
|
|
1691
|
+
console.error(`[${ts()}] [AUTO-P2P] FAIL: ${(err as Error).message}`);
|
|
1701
1692
|
logTrace(tid, "DONE", {
|
|
1702
|
-
outcome: "
|
|
1693
|
+
outcome: "auto_new_p2p_delegate_fail",
|
|
1703
1694
|
error: (err as Error).message,
|
|
1704
1695
|
});
|
|
1705
1696
|
await platform.sendCard(
|
|
1706
1697
|
chatId,
|
|
1707
1698
|
"Error",
|
|
1708
|
-
`
|
|
1709
|
-
"yellow",
|
|
1710
|
-
);
|
|
1711
|
-
return;
|
|
1712
|
-
}
|
|
1713
|
-
|
|
1714
|
-
await setDefaultCwd(cwd, newChatId);
|
|
1715
|
-
bindChatToSession(sessionId, newChatId);
|
|
1716
|
-
await recordSessionRegistry({
|
|
1717
|
-
chatId: newChatId,
|
|
1718
|
-
sessionId,
|
|
1719
|
-
tool,
|
|
1720
|
-
chatName: initialName,
|
|
1721
|
-
turnCount: 0,
|
|
1722
|
-
startTime: Date.now(),
|
|
1723
|
-
running: false,
|
|
1724
|
-
});
|
|
1725
|
-
await saveSessionTool(sessionId, tool, initialName);
|
|
1726
|
-
|
|
1727
|
-
await platform.sendCard(
|
|
1728
|
-
newChatId,
|
|
1729
|
-
`${toolLabel} Session Ready`,
|
|
1730
|
-
`已根据私聊消息创建 **${toolLabel}** 会话群。\n\n` +
|
|
1731
|
-
`**Session ID:** ${sessionId}\n` +
|
|
1732
|
-
`**工作目录:** \`${cwd}\`\n\n` +
|
|
1733
|
-
`下面会自动把你的私聊消息作为第一句话发送给 ${toolLabel}。\n\n` +
|
|
1734
|
-
`发送 **/model** 查看或切换当前会话的模型。\n` +
|
|
1735
|
-
`发送 **/new** 创建新会话,**/newh** 重置当前会话(沿用工作目录)。`,
|
|
1736
|
-
"green",
|
|
1737
|
-
);
|
|
1738
|
-
platform.setChatAvatar(newChatId, tool, "new").catch(() => {});
|
|
1739
|
-
console.log(`[${ts()}] [AUTO-P2P 4/5] Replied to new group → OK`);
|
|
1740
|
-
|
|
1741
|
-
try {
|
|
1742
|
-
logTrace(tid, "RESUME", { sessionId, tool, trigger: "auto_new_from_p2p" });
|
|
1743
|
-
await resumeAndPrompt(
|
|
1744
|
-
sessionId,
|
|
1745
|
-
promptText,
|
|
1746
|
-
platform,
|
|
1747
|
-
newChatId,
|
|
1748
|
-
msgTimestamp,
|
|
1749
|
-
tool,
|
|
1750
|
-
tid,
|
|
1751
|
-
);
|
|
1752
|
-
console.log(`[${ts()}] [AUTO-P2P 5/5] First prompt sent → OK`);
|
|
1753
|
-
logTrace(tid, "DONE", { outcome: "auto_new_p2p_prompt_done", newChatId, sessionId, tool });
|
|
1754
|
-
} catch (err) {
|
|
1755
|
-
console.error(`[${ts()}] [AUTO-P2P 5/5] FAIL: ${(err as Error).message}`);
|
|
1756
|
-
logTrace(tid, "DONE", {
|
|
1757
|
-
outcome: "auto_new_p2p_prompt_fail",
|
|
1758
|
-
error: (err as Error).message,
|
|
1759
|
-
});
|
|
1760
|
-
await platform.sendCard(
|
|
1761
|
-
newChatId,
|
|
1762
|
-
"Error",
|
|
1763
|
-
`Failed to send first prompt:\n${(err as Error).message}`,
|
|
1699
|
+
`Failed to create ${toolLabel} delegated task:\n${(err as Error).message}`,
|
|
1764
1700
|
"red",
|
|
1765
1701
|
);
|
|
1766
1702
|
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export function cwdDisplayName(cwd: string): string {
|
|
2
|
+
const trimmed = cwd.trim().replace(/[\\/]+$/, "");
|
|
3
|
+
return trimmed.split(/[\\/]/).filter(Boolean).pop() || trimmed || "cwd";
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
export function sessionChatName(left: string, cwd: string): string {
|
|
7
|
+
return `${left}-${cwdDisplayName(cwd)}`;
|
|
8
|
+
}
|