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/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) {
@@ -22,9 +22,11 @@ import {
22
22
  PROJECT_ROOT,
23
23
  anthropicConfigDisplay,
24
24
  config,
25
- fileLog,
26
- getAllModelsForTool,
27
- getDefaultCwd,
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
- buildModelCard,
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
- chatGptSubscription?.ok ? "" : "",
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 resolveUsageTool(chatId: string): Promise<"codex" | "cursor"> {
333
+ async function resolveUsageTarget(chatId: string): Promise<{ tool: "codex" | "cursor"; sessionId?: string }> {
295
334
  try {
296
335
  const registry = await loadSessionRegistryForBinding();
297
- return registry[chatId]?.tool === "cursor" ? "cursor" : "codex";
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
- async function sendUsageSummary(platform: PlatformAdapter, chatId: string, tool: "codex" | "cursor"): Promise<void> {
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 content = formatCursorUsageSummary(await getCursorUsageSummary());
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 usageTool = await resolveUsageTool(chatId);
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 && (textLower.startsWith("/git ") || textLower === "/git")) {
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 === "/sessions") {
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();
@@ -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
- CLAUDE_EFFORT,
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
- isAnthropicConfigEmpty,
21
- toolDisplayName,
22
- ts,
23
- } from "./config.ts";
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
- stopUnifiedDisplayLoop();
339
- console.log(`[${ts()}] [RESET] State cleared (dedup + active sessions + bindings)`);
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 getAdapterForTool(tool: string, sessionId?: string): ToolAdapter {
387
- const effectiveModel = getEffectiveModelForTool(tool, sessionId);
388
- const cacheKey = effectiveModel ? `${tool}:${effectiveModel}` : tool;
389
- const cached = adapterCache.get(cacheKey);
390
- if (cached) return cached;
391
-
392
- let adapter: ToolAdapter;
393
- if (tool === "cursor") {
394
- adapter = createCursorAdapter({ model: effectiveModel || undefined });
395
- } else if (tool === "codex") {
396
- adapter = createCodexAdapter({ model: effectiveModel || undefined });
397
- } else {
398
- adapter = createClaudeAdapter({
399
- model: effectiveModel,
400
- subagentModel: CLAUDE_SUBAGENT_MODEL,
401
- effort: CLAUDE_EFFORT,
402
- apiKey: CLAUDE_API_KEY,
403
- baseUrl: CLAUDE_BASE_URL,
404
- isEmpty: isAnthropicConfigEmpty,
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 = config.codex.model;
835
- const e = config.codex.effort;
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(CLAUDE_EFFORT)}`;
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 = config.codex.model;
1760
- const e = config.codex.effort;
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(CLAUDE_EFFORT),
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
- if (effective) adapterCache.set(`${tool}:${effective}`, adapter);
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 {