chatccc 0.2.187 → 0.2.188
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/config.sample.json +14 -9
- package/package.json +1 -1
- package/src/__tests__/builtin-config.test.ts +33 -0
- package/src/__tests__/chrome-devtools-guard.test.ts +40 -0
- package/src/__tests__/config-reload.test.ts +4 -3
- package/src/__tests__/config-sample.test.ts +17 -6
- package/src/__tests__/feishu-avatar.test.ts +180 -115
- package/src/__tests__/orchestrator.test.ts +165 -17
- package/src/__tests__/session.test.ts +17 -7
- package/src/adapters/codex-adapter.ts +26 -21
- package/src/builtin/cli.ts +8 -16
- package/src/builtin/index.ts +12 -11
- package/src/cards.ts +48 -7
- package/src/chrome-devtools-guard.ts +78 -2
- package/src/config.ts +86 -48
- package/src/feishu-api.ts +219 -190
- package/src/index.ts +2 -2
- package/src/orchestrator.ts +211 -19
- package/src/platform-adapter.ts +9 -1
- package/src/session.ts +92 -64
package/src/index.ts
CHANGED
|
@@ -142,8 +142,8 @@ function createFeishuAdapter(): PlatformAdapter {
|
|
|
142
142
|
async disbandChat(chatId) {
|
|
143
143
|
return disbandChat(await auth(), chatId);
|
|
144
144
|
},
|
|
145
|
-
async setChatAvatar(chatId, tool, status) {
|
|
146
|
-
return setChatAvatar(await auth(), chatId, tool, status);
|
|
145
|
+
async setChatAvatar(chatId, tool, status, usageHints) {
|
|
146
|
+
return setChatAvatar(await auth(), chatId, tool, status, usageHints);
|
|
147
147
|
},
|
|
148
148
|
|
|
149
149
|
extractSessionInfo(description) {
|
package/src/orchestrator.ts
CHANGED
|
@@ -22,9 +22,11 @@ import {
|
|
|
22
22
|
PROJECT_ROOT,
|
|
23
23
|
anthropicConfigDisplay,
|
|
24
24
|
config,
|
|
25
|
-
fileLog,
|
|
26
|
-
|
|
27
|
-
|
|
25
|
+
fileLog,
|
|
26
|
+
getAllEffortsForTool,
|
|
27
|
+
getAllModelsForTool,
|
|
28
|
+
getDefaultEffortForTool,
|
|
29
|
+
getDefaultCwd,
|
|
28
30
|
setDefaultCwd,
|
|
29
31
|
getRecentDirs,
|
|
30
32
|
addRecentDir,
|
|
@@ -36,8 +38,9 @@ import {
|
|
|
36
38
|
type AgentTool,
|
|
37
39
|
} from "./config.ts";
|
|
38
40
|
import {
|
|
39
|
-
buildHelpCard,
|
|
40
|
-
|
|
41
|
+
buildHelpCard,
|
|
42
|
+
buildEffortCard,
|
|
43
|
+
buildModelCard,
|
|
41
44
|
buildStatusCard,
|
|
42
45
|
buildCdContent,
|
|
43
46
|
buildCdCard,
|
|
@@ -52,18 +55,21 @@ import {
|
|
|
52
55
|
runGitCommand,
|
|
53
56
|
} from "./git-command.ts";
|
|
54
57
|
import {
|
|
55
|
-
clearSessionModelOverride,
|
|
58
|
+
clearSessionModelOverride,
|
|
59
|
+
clearSessionEffortOverride,
|
|
56
60
|
getSessionStatus,
|
|
57
61
|
getAllSessionsStatus,
|
|
58
62
|
initClaudeSession,
|
|
59
63
|
lastMsgTimestamps,
|
|
60
64
|
resumeAndPrompt,
|
|
61
65
|
sessionInfoMap,
|
|
62
|
-
setSessionModelOverride,
|
|
66
|
+
setSessionModelOverride,
|
|
67
|
+
setSessionEffortOverride,
|
|
63
68
|
switchChatBinding,
|
|
64
69
|
recordSessionRegistry,
|
|
65
70
|
getAdapterForTool,
|
|
66
|
-
getEffectiveModelForTool,
|
|
71
|
+
getEffectiveModelForTool,
|
|
72
|
+
getEffectiveEffortForTool,
|
|
67
73
|
stopSession,
|
|
68
74
|
loadSessionRegistryForBinding,
|
|
69
75
|
removeSessionRegistryRecord,
|
|
@@ -86,7 +92,7 @@ import { delegateAgentTask } from "./agent-delegate-task.ts";
|
|
|
86
92
|
import { applySharedPrefix } from "./shared-prefix.ts";
|
|
87
93
|
import { cwdDisplayName, sessionChatName } from "./session-name.ts";
|
|
88
94
|
export { type PlatformAdapter } from "./platform-adapter.ts";
|
|
89
|
-
import type { PlatformAdapter } from "./platform-adapter.ts";
|
|
95
|
+
import type { ChatAvatarUsageHints, PlatformAdapter } from "./platform-adapter.ts";
|
|
90
96
|
import type { CodexUsageSummary } from "./feishu-api.ts";
|
|
91
97
|
|
|
92
98
|
// ---------------------------------------------------------------------------
|
|
@@ -186,6 +192,39 @@ function formatCodexUsageSummary(usage: CodexUsageSummary, chatGptSubscription:
|
|
|
186
192
|
return lines.join("\n");
|
|
187
193
|
};
|
|
188
194
|
|
|
195
|
+
const formatSubscriptionFailureReason = (result: ChatGptSubscriptionResult) => {
|
|
196
|
+
const port = result.chromeCdp.port;
|
|
197
|
+
switch (result.code) {
|
|
198
|
+
case "chrome_cdp_unreachable":
|
|
199
|
+
return `Chrome CDP 端口 ${port} 不可访问。请确认常驻 Chrome 已启动,或重启 ChatCCC。`;
|
|
200
|
+
case "chrome_cdp_occupied":
|
|
201
|
+
return `${port} 端口可访问,但不是健康的 Chrome CDP。请释放该端口或修改 chromeDevtools.port。`;
|
|
202
|
+
case "chatgpt_page_missing":
|
|
203
|
+
return `没有可用的 ChatGPT 页面。请在 ${port} 端口对应的 Chrome 浏览器中打开 https://chatgpt.com/ 并登录。`;
|
|
204
|
+
case "chatgpt_session_missing":
|
|
205
|
+
return `请在 ${port} 端口对应的 Chrome 浏览器中登录 ChatGPT。`;
|
|
206
|
+
case "chatgpt_subscription_failed":
|
|
207
|
+
return "ChatGPT 订阅接口探测失败。可能是页面未加载完成、ChatGPT 接口变更或网络异常。";
|
|
208
|
+
case "chrome_cdp_disabled":
|
|
209
|
+
return "";
|
|
210
|
+
case "ok":
|
|
211
|
+
return "";
|
|
212
|
+
}
|
|
213
|
+
};
|
|
214
|
+
|
|
215
|
+
const formatChatGptSubscriptionFailure = () => {
|
|
216
|
+
if (!chatGptSubscription || chatGptSubscription.ok || !chatGptSubscription.chromeCdp.enabled) return "";
|
|
217
|
+
const lines = [
|
|
218
|
+
"**ChatGPT 订阅查询失败:**",
|
|
219
|
+
`- 原因: ${formatSubscriptionFailureReason(chatGptSubscription) || "暂无数据"}`,
|
|
220
|
+
];
|
|
221
|
+
const detail = chatGptSubscription.reason?.replace(/\s+/g, " ").trim();
|
|
222
|
+
if (detail) {
|
|
223
|
+
lines.push(`- 详情: ${detail.length > 240 ? `${detail.slice(0, 240)}...` : detail}`);
|
|
224
|
+
}
|
|
225
|
+
return lines.join("\n");
|
|
226
|
+
};
|
|
227
|
+
|
|
189
228
|
const formatChatGptSubscription = () => {
|
|
190
229
|
if (!chatGptSubscription?.ok || !chatGptSubscription.subscription) return "";
|
|
191
230
|
const subscription = chatGptSubscription.subscription;
|
|
@@ -221,7 +260,7 @@ function formatCodexUsageSummary(usage: CodexUsageSummary, chatGptSubscription:
|
|
|
221
260
|
"Codex 用量:",
|
|
222
261
|
"",
|
|
223
262
|
formatChatGptSubscription(),
|
|
224
|
-
|
|
263
|
+
formatChatGptSubscriptionFailure(),
|
|
225
264
|
formatResetCredits(),
|
|
226
265
|
"",
|
|
227
266
|
formatWindow("5h", usage.fiveHour),
|
|
@@ -291,23 +330,46 @@ function usageHelpLine(tool: string): string {
|
|
|
291
330
|
return "";
|
|
292
331
|
}
|
|
293
332
|
|
|
294
|
-
async function
|
|
333
|
+
async function resolveUsageTarget(chatId: string): Promise<{ tool: "codex" | "cursor"; sessionId?: string }> {
|
|
295
334
|
try {
|
|
296
335
|
const registry = await loadSessionRegistryForBinding();
|
|
297
|
-
|
|
336
|
+
const record = registry[chatId];
|
|
337
|
+
return {
|
|
338
|
+
tool: record?.tool === "cursor" ? "cursor" : "codex",
|
|
339
|
+
sessionId: record?.sessionId,
|
|
340
|
+
};
|
|
298
341
|
} catch {
|
|
299
|
-
return "codex";
|
|
342
|
+
return { tool: "codex" };
|
|
300
343
|
}
|
|
301
344
|
}
|
|
302
345
|
|
|
303
|
-
|
|
346
|
+
function refreshUsageAvatar(
|
|
347
|
+
platform: PlatformAdapter,
|
|
348
|
+
chatId: string,
|
|
349
|
+
tool: "codex" | "cursor",
|
|
350
|
+
status: "busy" | "idle",
|
|
351
|
+
usageHints: ChatAvatarUsageHints,
|
|
352
|
+
): void {
|
|
353
|
+
platform.setChatAvatar(chatId, tool, status, usageHints).catch((err) => {
|
|
354
|
+
console.warn(`[${ts()}] [AVATAR] usage refresh failed: chatId=${chatId} tool=${tool} ${(err as Error).message}`);
|
|
355
|
+
});
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
async function sendUsageSummary(
|
|
359
|
+
platform: PlatformAdapter,
|
|
360
|
+
chatId: string,
|
|
361
|
+
tool: "codex" | "cursor",
|
|
362
|
+
avatarStatus: "busy" | "idle" = "idle",
|
|
363
|
+
): Promise<void> {
|
|
304
364
|
if (tool === "cursor") {
|
|
305
|
-
const
|
|
365
|
+
const usage = await getCursorUsageSummary();
|
|
366
|
+
const content = formatCursorUsageSummary(usage);
|
|
306
367
|
if (platform.kind === "wechat") {
|
|
307
368
|
await platform.sendText(chatId, content).catch(() => {});
|
|
308
369
|
} else {
|
|
309
370
|
await platform.sendCard(chatId, "Cursor Usage", content, "blue");
|
|
310
371
|
}
|
|
372
|
+
refreshUsageAvatar(platform, chatId, tool, avatarStatus, { cursorUsage: usage });
|
|
311
373
|
return;
|
|
312
374
|
}
|
|
313
375
|
|
|
@@ -323,6 +385,7 @@ async function sendUsageSummary(platform: PlatformAdapter, chatId: string, tool:
|
|
|
323
385
|
} else {
|
|
324
386
|
await platform.sendCard(chatId, "Codex Usage", content, "blue");
|
|
325
387
|
}
|
|
388
|
+
refreshUsageAvatar(platform, chatId, tool, avatarStatus, { codexUsage: usage });
|
|
326
389
|
}
|
|
327
390
|
|
|
328
391
|
async function sendUsageError(platform: PlatformAdapter, chatId: string, tool: "codex" | "cursor", err: unknown): Promise<void> {
|
|
@@ -538,10 +601,12 @@ export async function handleCommand(
|
|
|
538
601
|
}
|
|
539
602
|
|
|
540
603
|
if (isCommandText && textLower === "/usage") {
|
|
541
|
-
const
|
|
604
|
+
const usageTarget = await resolveUsageTarget(chatId);
|
|
605
|
+
const usageTool = usageTarget.tool;
|
|
606
|
+
const avatarStatus = usageTarget.sessionId && isSessionRunning(usageTarget.sessionId) ? "busy" : "idle";
|
|
542
607
|
logTrace(tid, "BRANCH", { cmd: "/usage", tool: usageTool });
|
|
543
608
|
try {
|
|
544
|
-
await sendUsageSummary(platform, chatId, usageTool);
|
|
609
|
+
await sendUsageSummary(platform, chatId, usageTool, avatarStatus);
|
|
545
610
|
logTrace(tid, "DONE", { outcome: "usage", tool: usageTool });
|
|
546
611
|
} catch (err) {
|
|
547
612
|
await sendUsageError(platform, chatId, usageTool, err);
|
|
@@ -1469,7 +1534,96 @@ export async function handleCommand(
|
|
|
1469
1534
|
}
|
|
1470
1535
|
|
|
1471
1536
|
// /git <args>:在「当前会话工作目录」执行 git 命令
|
|
1472
|
-
if (isCommandText &&
|
|
1537
|
+
if (isCommandText && textLower === "/effort clear") {
|
|
1538
|
+
logTrace(tid, "BRANCH", { cmd: "/effort clear", sessionId, tool: descriptionTool });
|
|
1539
|
+
const efforts = getAllEffortsForTool(descriptionTool as AgentTool);
|
|
1540
|
+
const toolLabel = toolDisplayName(descriptionTool);
|
|
1541
|
+
if (efforts.length === 0) {
|
|
1542
|
+
const msg = `当前 ${toolLabel} 不支持 effort 切换。`;
|
|
1543
|
+
await (platform.kind === "wechat"
|
|
1544
|
+
? platform.sendText(chatId, msg)
|
|
1545
|
+
: platform.sendCard(chatId, "Effort 切换", msg, "red")
|
|
1546
|
+
).catch(() => {});
|
|
1547
|
+
logTrace(tid, "DONE", { outcome: "effort_unsupported", tool: descriptionTool });
|
|
1548
|
+
return;
|
|
1549
|
+
}
|
|
1550
|
+
|
|
1551
|
+
clearSessionEffortOverride(sessionId);
|
|
1552
|
+
const defaultEffort = getDefaultEffortForTool(descriptionTool as AgentTool);
|
|
1553
|
+
const msg = `已清除当前 ${toolLabel} 会话的 effort 覆盖,恢复使用: \`${defaultEffort || "(未指定)"}\``;
|
|
1554
|
+
await (platform.kind === "wechat"
|
|
1555
|
+
? platform.sendText(chatId, msg)
|
|
1556
|
+
: platform.sendCard(chatId, "Effort 切换", msg, "green")
|
|
1557
|
+
).catch(() => {});
|
|
1558
|
+
logTrace(tid, "DONE", { outcome: "effort_cleared", sessionId, tool: descriptionTool });
|
|
1559
|
+
return;
|
|
1560
|
+
}
|
|
1561
|
+
|
|
1562
|
+
if (isCommandText && textLower.startsWith("/effort ")) {
|
|
1563
|
+
const effortArg = text.slice(8).trim();
|
|
1564
|
+
if (!effortArg) return;
|
|
1565
|
+
logTrace(tid, "BRANCH", { cmd: "/effort", arg: effortArg, sessionId, tool: descriptionTool });
|
|
1566
|
+
|
|
1567
|
+
const efforts = getAllEffortsForTool(descriptionTool as AgentTool);
|
|
1568
|
+
const toolLabel = toolDisplayName(descriptionTool);
|
|
1569
|
+
if (efforts.length === 0) {
|
|
1570
|
+
const msg = `当前 ${toolLabel} 不支持 effort 切换。`;
|
|
1571
|
+
await (platform.kind === "wechat"
|
|
1572
|
+
? platform.sendText(chatId, msg)
|
|
1573
|
+
: platform.sendCard(chatId, "Effort 切换", msg, "red")
|
|
1574
|
+
).catch(() => {});
|
|
1575
|
+
logTrace(tid, "DONE", { outcome: "effort_unsupported", tool: descriptionTool });
|
|
1576
|
+
return;
|
|
1577
|
+
}
|
|
1578
|
+
|
|
1579
|
+
const target = findModelMatch(effortArg, efforts);
|
|
1580
|
+
if (!target) {
|
|
1581
|
+
const msg = `未找到匹配 "${effortArg}" 的 effort。当前 ${toolLabel} 可选 effort:\n${efforts.map(e => ` \`${e}\``).join("\n")}`;
|
|
1582
|
+
await (platform.kind === "wechat"
|
|
1583
|
+
? platform.sendText(chatId, msg)
|
|
1584
|
+
: platform.sendCard(chatId, "Effort 切换", msg, "red")
|
|
1585
|
+
).catch(() => {});
|
|
1586
|
+
logTrace(tid, "DONE", { outcome: "effort_not_found", arg: effortArg, tool: descriptionTool });
|
|
1587
|
+
return;
|
|
1588
|
+
}
|
|
1589
|
+
|
|
1590
|
+
setSessionEffortOverride(sessionId, target);
|
|
1591
|
+
const msg = `已切换当前 ${toolLabel} 会话 effort 为: \`${target}\``;
|
|
1592
|
+
await (platform.kind === "wechat"
|
|
1593
|
+
? platform.sendText(chatId, msg)
|
|
1594
|
+
: platform.sendCard(chatId, "Effort 切换", msg, "green")
|
|
1595
|
+
).catch(() => {});
|
|
1596
|
+
logTrace(tid, "DONE", { outcome: "effort_switched", arg: effortArg, target, sessionId, tool: descriptionTool });
|
|
1597
|
+
return;
|
|
1598
|
+
}
|
|
1599
|
+
|
|
1600
|
+
if (isCommandText && textLower === "/effort") {
|
|
1601
|
+
logTrace(tid, "BRANCH", { cmd: "/effort", sessionId, tool: descriptionTool });
|
|
1602
|
+
const efforts = getAllEffortsForTool(descriptionTool as AgentTool);
|
|
1603
|
+
const currentEffort = getEffectiveEffortForTool(descriptionTool, sessionId);
|
|
1604
|
+
const toolLabel = toolDisplayName(descriptionTool);
|
|
1605
|
+
|
|
1606
|
+
if (efforts.length === 0) {
|
|
1607
|
+
const msg = `当前 ${toolLabel} 不支持 effort 切换。`;
|
|
1608
|
+
await (platform.kind === "wechat"
|
|
1609
|
+
? platform.sendText(chatId, msg)
|
|
1610
|
+
: platform.sendCard(chatId, "Effort 切换", msg, "red")
|
|
1611
|
+
).catch(() => {});
|
|
1612
|
+
} else if (platform.kind === "wechat") {
|
|
1613
|
+
const lines = [currentEffort ? `当前 effort (${toolLabel}): ${currentEffort}` : `当前 effort (${toolLabel}): 未指定`];
|
|
1614
|
+
lines.push("", "可切换 effort:");
|
|
1615
|
+
for (const e of efforts) lines.push(` ${e}`);
|
|
1616
|
+
lines.push("", "输入 /effort <effort> 切换 effort");
|
|
1617
|
+
await platform.sendText(chatId, lines.join("\n")).catch(() => {});
|
|
1618
|
+
} else {
|
|
1619
|
+
const card = buildEffortCard(currentEffort, efforts, descriptionTool);
|
|
1620
|
+
await platform.sendRawCard(chatId, card);
|
|
1621
|
+
}
|
|
1622
|
+
logTrace(tid, "DONE", { outcome: "effort_query", tool: descriptionTool });
|
|
1623
|
+
return;
|
|
1624
|
+
}
|
|
1625
|
+
|
|
1626
|
+
if (isCommandText && (textLower.startsWith("/git ") || textLower === "/git")) {
|
|
1473
1627
|
const args = text === "/git" ? "" : text.slice(5).trim();
|
|
1474
1628
|
logTrace(tid, "BRANCH", { cmd: "/git", args: args || "(none)" });
|
|
1475
1629
|
if (!args) {
|
|
@@ -1627,7 +1781,45 @@ export async function handleCommand(
|
|
|
1627
1781
|
}
|
|
1628
1782
|
|
|
1629
1783
|
// 无会话上下文 → /sessions 仍是有效指令,不触发飞书私聊自动建群。
|
|
1630
|
-
if (isCommandText && textLower === "/
|
|
1784
|
+
if (isCommandText && textLower === "/effort") {
|
|
1785
|
+
const defaultTool = resolveDefaultAgentTool();
|
|
1786
|
+
const efforts = getAllEffortsForTool(defaultTool);
|
|
1787
|
+
const currentEffort = getDefaultEffortForTool(defaultTool);
|
|
1788
|
+
const toolLabel = toolDisplayName(defaultTool);
|
|
1789
|
+
|
|
1790
|
+
if (efforts.length === 0) {
|
|
1791
|
+
const msg = `当前默认 agent (${toolLabel}) 不支持 effort 切换。`;
|
|
1792
|
+
await (platform.kind === "wechat"
|
|
1793
|
+
? platform.sendText(chatId, msg)
|
|
1794
|
+
: platform.sendCard(chatId, "Effort 切换", msg, "red")
|
|
1795
|
+
).catch(() => {});
|
|
1796
|
+
} else if (platform.kind === "wechat") {
|
|
1797
|
+
const lines = [currentEffort ? `当前默认 effort (${toolLabel}): ${currentEffort}` : `当前默认 effort (${toolLabel}): 未指定`];
|
|
1798
|
+
lines.push("", "可切换 effort:");
|
|
1799
|
+
for (const e of efforts) lines.push(` ${e}`);
|
|
1800
|
+
lines.push("", "在会话中输入 /effort <effort> 切换 effort");
|
|
1801
|
+
await platform.sendText(chatId, lines.join("\n")).catch(() => {});
|
|
1802
|
+
} else {
|
|
1803
|
+
const card = buildEffortCard(currentEffort, efforts, defaultTool);
|
|
1804
|
+
await platform.sendRawCard(chatId, card);
|
|
1805
|
+
}
|
|
1806
|
+
logTrace(tid, "DONE", { outcome: "effort_query", defaultTool });
|
|
1807
|
+
return;
|
|
1808
|
+
}
|
|
1809
|
+
|
|
1810
|
+
if (isCommandText && textLower.startsWith("/effort ")) {
|
|
1811
|
+
const defaultTool = resolveDefaultAgentTool();
|
|
1812
|
+
const toolLabel = toolDisplayName(defaultTool);
|
|
1813
|
+
const msg = `当前没有绑定会话。请先进入 Claude/Codex 会话,再输入 /effort <effort> 切换当前会话的 effort。当前默认 agent: ${toolLabel}`;
|
|
1814
|
+
await (platform.kind === "wechat"
|
|
1815
|
+
? platform.sendText(chatId, msg)
|
|
1816
|
+
: platform.sendCard(chatId, "Effort 切换", msg, "yellow")
|
|
1817
|
+
).catch(() => {});
|
|
1818
|
+
logTrace(tid, "DONE", { outcome: "effort_no_session", defaultTool });
|
|
1819
|
+
return;
|
|
1820
|
+
}
|
|
1821
|
+
|
|
1822
|
+
if (isCommandText && textLower === "/sessions") {
|
|
1631
1823
|
logTrace(tid, "BRANCH", { cmd: "/sessions", scope: "global" });
|
|
1632
1824
|
const allSessions = await getAllSessionsStatus();
|
|
1633
1825
|
const now = Date.now();
|
package/src/platform-adapter.ts
CHANGED
|
@@ -5,6 +5,14 @@
|
|
|
5
5
|
* 每个方法内部自行管理认证,消费者不感知 token 等认证细节。
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
+
import type { CursorUsageSummary } from "./cursor-usage.ts";
|
|
9
|
+
import type { CodexUsageSummary } from "./feishu-api.ts";
|
|
10
|
+
|
|
11
|
+
export interface ChatAvatarUsageHints {
|
|
12
|
+
codexUsage?: CodexUsageSummary | null;
|
|
13
|
+
cursorUsage?: CursorUsageSummary | null;
|
|
14
|
+
}
|
|
15
|
+
|
|
8
16
|
export interface PlatformAdapter {
|
|
9
17
|
/** 平台标识,用于区分不同平台的行为(如 wechat、feishu 等) */
|
|
10
18
|
kind?: string;
|
|
@@ -40,7 +48,7 @@ export interface PlatformAdapter {
|
|
|
40
48
|
disbandChat(chatId: string): Promise<void>;
|
|
41
49
|
|
|
42
50
|
/** 设置群头像 */
|
|
43
|
-
setChatAvatar(chatId: string, tool: string, status: string): Promise<void>;
|
|
51
|
+
setChatAvatar(chatId: string, tool: string, status: string, usageHints?: ChatAvatarUsageHints): Promise<void>;
|
|
44
52
|
|
|
45
53
|
/** 从群描述中提取 session 信息 */
|
|
46
54
|
extractSessionInfo(
|
package/src/session.ts
CHANGED
|
@@ -2,10 +2,9 @@ import { readFile, writeFile, mkdir } from "node:fs/promises";
|
|
|
2
2
|
import { dirname, join } from "node:path";
|
|
3
3
|
|
|
4
4
|
import {
|
|
5
|
-
CLAUDE_API_KEY,
|
|
6
|
-
CLAUDE_BASE_URL,
|
|
7
|
-
|
|
8
|
-
CLAUDE_MAX_TURN,
|
|
5
|
+
CLAUDE_API_KEY,
|
|
6
|
+
CLAUDE_BASE_URL,
|
|
7
|
+
CLAUDE_MAX_TURN,
|
|
9
8
|
CLAUDE_MODEL,
|
|
10
9
|
CLAUDE_SUBAGENT_MODEL,
|
|
11
10
|
CHATCCC_PORT,
|
|
@@ -15,12 +14,13 @@ import {
|
|
|
15
14
|
addRecentDir,
|
|
16
15
|
anthropicConfigDisplay,
|
|
17
16
|
config,
|
|
18
|
-
fileLog,
|
|
19
|
-
getDefaultCwd,
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
17
|
+
fileLog,
|
|
18
|
+
getDefaultCwd,
|
|
19
|
+
getDefaultEffortForTool,
|
|
20
|
+
isAnthropicConfigEmpty,
|
|
21
|
+
toolDisplayName,
|
|
22
|
+
ts,
|
|
23
|
+
} from "./config.ts";
|
|
24
24
|
import { buildProgressCard, getToolEmoji, isCodeBlockOpen, truncateContent } from "./cards.ts";
|
|
25
25
|
import { simplifyToolUse, simplifyToolResult } from "./simplify.ts";
|
|
26
26
|
import { logTrace } from "./trace.ts";
|
|
@@ -333,11 +333,14 @@ export function resetState(): void {
|
|
|
333
333
|
for (const prompt of activePrompts.values()) {
|
|
334
334
|
if (prompt.processMonitor) clearInterval(prompt.processMonitor);
|
|
335
335
|
}
|
|
336
|
-
activePrompts.clear();
|
|
337
|
-
displayCards.clear();
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
336
|
+
activePrompts.clear();
|
|
337
|
+
displayCards.clear();
|
|
338
|
+
sessionModelOverrides.clear();
|
|
339
|
+
sessionEffortOverrides.clear();
|
|
340
|
+
adapterCache.clear();
|
|
341
|
+
stopUnifiedDisplayLoop();
|
|
342
|
+
console.log(`[${ts()}] [RESET] State cleared (dedup + active sessions + bindings)`);
|
|
343
|
+
}
|
|
341
344
|
|
|
342
345
|
// 注:`rebuildBindingsFromRegistry` 定义在下方与 loadSessionRegistry 同区域,
|
|
343
346
|
// 是 onReady/onReconnected 取代 resetState 的正确入口。
|
|
@@ -349,7 +352,8 @@ export function resetState(): void {
|
|
|
349
352
|
const adapterCache = new Map<string, ToolAdapter>();
|
|
350
353
|
|
|
351
354
|
// Per-session 模型覆盖(/model 命令设置,不持久化)
|
|
352
|
-
const sessionModelOverrides = new Map<string, string>();
|
|
355
|
+
const sessionModelOverrides = new Map<string, string>();
|
|
356
|
+
const sessionEffortOverrides = new Map<string, string>();
|
|
353
357
|
|
|
354
358
|
/** 返回 session 的生效模型:优先 per-session 覆盖,其次全局配置(Claude) */
|
|
355
359
|
function getModelForSession(sessionId?: string): string {
|
|
@@ -361,15 +365,26 @@ function getModelForSession(sessionId?: string): string {
|
|
|
361
365
|
}
|
|
362
366
|
|
|
363
367
|
/** 返回指定 tool 的生效模型:优先 per-session 覆盖,其次 tool 默认配置 */
|
|
364
|
-
export function getEffectiveModelForTool(tool: string, sessionId?: string): string {
|
|
365
|
-
if (sessionId) {
|
|
366
|
-
const override = sessionModelOverrides.get(sessionId);
|
|
367
|
-
if (override) return override;
|
|
368
|
-
}
|
|
368
|
+
export function getEffectiveModelForTool(tool: string, sessionId?: string): string {
|
|
369
|
+
if (sessionId) {
|
|
370
|
+
const override = sessionModelOverrides.get(sessionId);
|
|
371
|
+
if (override) return override;
|
|
372
|
+
}
|
|
369
373
|
if (tool === "cursor") return config.cursor.model;
|
|
370
374
|
if (tool === "codex") return config.codex.model;
|
|
371
|
-
return CLAUDE_MODEL;
|
|
372
|
-
}
|
|
375
|
+
return CLAUDE_MODEL;
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
export function getEffectiveEffortForTool(tool: string, sessionId?: string): string {
|
|
379
|
+
if (sessionId) {
|
|
380
|
+
const override = sessionEffortOverrides.get(sessionId);
|
|
381
|
+
if (override) return override;
|
|
382
|
+
}
|
|
383
|
+
if (tool === "claude" || tool === "codex") {
|
|
384
|
+
return getDefaultEffortForTool(tool);
|
|
385
|
+
}
|
|
386
|
+
return "";
|
|
387
|
+
}
|
|
373
388
|
|
|
374
389
|
/** 为指定 session 设置模型覆盖(/model <name>) */
|
|
375
390
|
export function setSessionModelOverride(sessionId: string, model: string): void {
|
|
@@ -378,30 +393,41 @@ export function setSessionModelOverride(sessionId: string, model: string): void
|
|
|
378
393
|
}
|
|
379
394
|
|
|
380
395
|
/** 清除指定 session 的模型覆盖(/model clear) */
|
|
381
|
-
export function clearSessionModelOverride(sessionId: string): void {
|
|
382
|
-
sessionModelOverrides.delete(sessionId);
|
|
383
|
-
adapterCache.clear();
|
|
384
|
-
}
|
|
385
|
-
|
|
386
|
-
export function
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
396
|
+
export function clearSessionModelOverride(sessionId: string): void {
|
|
397
|
+
sessionModelOverrides.delete(sessionId);
|
|
398
|
+
adapterCache.clear();
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
export function setSessionEffortOverride(sessionId: string, effort: string): void {
|
|
402
|
+
sessionEffortOverrides.set(sessionId, effort);
|
|
403
|
+
adapterCache.clear();
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
export function clearSessionEffortOverride(sessionId: string): void {
|
|
407
|
+
sessionEffortOverrides.delete(sessionId);
|
|
408
|
+
adapterCache.clear();
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
export function getAdapterForTool(tool: string, sessionId?: string): ToolAdapter {
|
|
412
|
+
const effectiveModel = getEffectiveModelForTool(tool, sessionId);
|
|
413
|
+
const effectiveEffort = getEffectiveEffortForTool(tool, sessionId);
|
|
414
|
+
const cacheKey = `${tool}:${effectiveModel || ""}:${effectiveEffort || ""}`;
|
|
415
|
+
const cached = adapterCache.get(cacheKey);
|
|
416
|
+
if (cached) return cached;
|
|
417
|
+
|
|
418
|
+
let adapter: ToolAdapter;
|
|
419
|
+
if (tool === "cursor") {
|
|
420
|
+
adapter = createCursorAdapter({ model: effectiveModel || undefined });
|
|
421
|
+
} else if (tool === "codex") {
|
|
422
|
+
adapter = createCodexAdapter({ model: effectiveModel || undefined, effort: effectiveEffort || undefined });
|
|
423
|
+
} else {
|
|
424
|
+
adapter = createClaudeAdapter({
|
|
425
|
+
model: effectiveModel,
|
|
426
|
+
subagentModel: CLAUDE_SUBAGENT_MODEL,
|
|
427
|
+
effort: effectiveEffort,
|
|
428
|
+
apiKey: CLAUDE_API_KEY,
|
|
429
|
+
baseUrl: CLAUDE_BASE_URL,
|
|
430
|
+
isEmpty: isAnthropicConfigEmpty,
|
|
405
431
|
maxTurn: CLAUDE_MAX_TURN,
|
|
406
432
|
});
|
|
407
433
|
}
|
|
@@ -829,18 +855,18 @@ export async function switchChatBinding(args: SwitchChatBindingArgs): Promise<Sw
|
|
|
829
855
|
function formatToolConfigForLog(tool: string, sessionModel?: string, sessionId?: string): string {
|
|
830
856
|
if (tool === "cursor") {
|
|
831
857
|
return `model=${sessionModel ?? "(由 cursor-agent 决定,init 事件后学习)"}`;
|
|
832
|
-
}
|
|
833
|
-
if (tool === "codex") {
|
|
834
|
-
const m =
|
|
835
|
-
const e =
|
|
858
|
+
}
|
|
859
|
+
if (tool === "codex") {
|
|
860
|
+
const m = getEffectiveModelForTool(tool, sessionId);
|
|
861
|
+
const e = getEffectiveEffortForTool(tool, sessionId);
|
|
836
862
|
const modelStr = m.trim() !== "" ? m : "(由 codex config.toml 决定)";
|
|
837
863
|
const effortStr = e.trim() !== ""
|
|
838
864
|
? `effort=${e}`
|
|
839
865
|
: "effort=(由 codex config.toml 决定)";
|
|
840
866
|
return `model=${modelStr}, ${effortStr}`;
|
|
841
867
|
}
|
|
842
|
-
return `model=${anthropicConfigDisplay(getModelForSession(sessionId))}, subagentModel=${anthropicConfigDisplay(CLAUDE_SUBAGENT_MODEL)}, effort=${anthropicConfigDisplay(
|
|
843
|
-
}
|
|
868
|
+
return `model=${anthropicConfigDisplay(getModelForSession(sessionId))}, subagentModel=${anthropicConfigDisplay(CLAUDE_SUBAGENT_MODEL)}, effort=${anthropicConfigDisplay(getEffectiveEffortForTool(tool, sessionId))}`;
|
|
869
|
+
}
|
|
844
870
|
|
|
845
871
|
export async function initClaudeSession(tool: string, overrideCwd?: string, chatId?: string): Promise<{ sessionId: string; cwd: string }> {
|
|
846
872
|
const cwd = overrideCwd ?? (await getDefaultCwd(chatId));
|
|
@@ -1754,20 +1780,20 @@ async function resolveModelEffort(
|
|
|
1754
1780
|
// adapter 异常时降级为占位符(不阻塞 /state 卡片)
|
|
1755
1781
|
}
|
|
1756
1782
|
return { model, effort: null };
|
|
1757
|
-
}
|
|
1758
|
-
if (tool === "codex") {
|
|
1759
|
-
const m =
|
|
1760
|
-
const e =
|
|
1783
|
+
}
|
|
1784
|
+
if (tool === "codex") {
|
|
1785
|
+
const m = getEffectiveModelForTool(tool, sessionId);
|
|
1786
|
+
const e = getEffectiveEffortForTool(tool, sessionId);
|
|
1761
1787
|
return {
|
|
1762
1788
|
model: m.trim() !== "" ? m : UNKNOWN_MODEL_PLACEHOLDER,
|
|
1763
1789
|
effort: e.trim() !== "" ? e : UNKNOWN_MODEL_PLACEHOLDER,
|
|
1764
1790
|
};
|
|
1765
|
-
}
|
|
1766
|
-
return {
|
|
1767
|
-
model: anthropicConfigDisplay(getModelForSession(sessionId)),
|
|
1768
|
-
effort: anthropicConfigDisplay(
|
|
1769
|
-
};
|
|
1770
|
-
}
|
|
1791
|
+
}
|
|
1792
|
+
return {
|
|
1793
|
+
model: anthropicConfigDisplay(getModelForSession(sessionId)),
|
|
1794
|
+
effort: anthropicConfigDisplay(getEffectiveEffortForTool(tool, sessionId)),
|
|
1795
|
+
};
|
|
1796
|
+
}
|
|
1771
1797
|
|
|
1772
1798
|
export async function getSessionStatus(chatId: string): Promise<SessionStatus | null> {
|
|
1773
1799
|
const info = sessionInfoMap.get(chatId);
|
|
@@ -1873,7 +1899,9 @@ export function _setAdapterForToolForTest(tool: string, adapter: ToolAdapter): v
|
|
|
1873
1899
|
adapterCache.set(tool, adapter);
|
|
1874
1900
|
// 同时设置当前配置模型对应的 key(getAdapterForTool 会优先 lookup 含 model 的 key)
|
|
1875
1901
|
const effective = getEffectiveModelForTool(tool);
|
|
1876
|
-
|
|
1902
|
+
const effort = getEffectiveEffortForTool(tool);
|
|
1903
|
+
adapterCache.set(`${tool}:${effective || ""}:${effort || ""}`, adapter);
|
|
1904
|
+
if (effective) adapterCache.set(`${tool}:${effective}`, adapter);
|
|
1877
1905
|
}
|
|
1878
1906
|
|
|
1879
1907
|
export function clearAdapterCache(): void {
|