chatccc 0.2.213 → 0.2.215

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.
@@ -126,9 +126,12 @@ import {
126
126
  _setFinalResponseCloseTimeoutForTest,
127
127
  _resetFinalResponseCloseTimeoutForTest,
128
128
  setSessionEffortOverride,
129
+ getEffectiveFastModeForTool,
130
+ setSessionFastModeOverride,
129
131
  RESPONSE_STALL_RECOVERY_PROMPT,
130
132
  RESPONSE_STALL_RECOVERY_EXHAUSTED_NOTICE,
131
133
  } from "../session.ts";
134
+ import { config } from "../config.ts";
132
135
  import {
133
136
  activePrompts,
134
137
  bindChatToSession,
@@ -145,7 +148,26 @@ import {
145
148
  } from "../session-chat-binding.ts";
146
149
  import type { AccumulatorState } from "../session.ts";
147
150
  import type { ToolAdapter, ToolPromptOptions, UnifiedBlock, SessionInfo } from "../adapters/adapter-interface.ts";
148
- import type { PlatformAdapter } from "../platform-adapter.ts";
151
+ import type { PlatformAdapter } from "../platform-adapter.ts";
152
+
153
+ describe("Codex Fast mode overrides", () => {
154
+ it("uses the global default until the current session overrides it", () => {
155
+ const previousFastMode = config.codex.fastMode;
156
+ config.codex.fastMode = false;
157
+ resetState();
158
+
159
+ expect(getEffectiveFastModeForTool("codex", "sid-fast")).toBe(false);
160
+ setSessionFastModeOverride("sid-fast", true);
161
+ expect(getEffectiveFastModeForTool("codex", "sid-fast")).toBe(true);
162
+ setSessionFastModeOverride("sid-fast", false);
163
+ expect(getEffectiveFastModeForTool("codex", "sid-fast")).toBe(false);
164
+ expect(getEffectiveFastModeForTool("claude", "sid-fast")).toBe(false);
165
+
166
+ resetState();
167
+ expect(getEffectiveFastModeForTool("codex", "sid-fast")).toBe(false);
168
+ config.codex.fastMode = previousFastMode;
169
+ });
170
+ });
149
171
 
150
172
  // Helper to create a mock active session entry
151
173
  function mockActiveSession(chatId: string, overrides: Partial<{
@@ -1274,7 +1296,7 @@ describe("runAgentSession response stall watchdog", () => {
1274
1296
  expect(consumeQueued).not.toHaveBeenCalled();
1275
1297
 
1276
1298
  await vi.advanceTimersByTimeAsync(200);
1277
- expect(consumeQueued).toHaveBeenCalledTimes(1);
1299
+ await vi.waitFor(() => expect(consumeQueued).toHaveBeenCalledTimes(1));
1278
1300
  expect(consumeQueued).toHaveBeenCalledWith(
1279
1301
  platform,
1280
1302
  expect.objectContaining({ text: "queued user prompt" }),
@@ -68,11 +68,12 @@ describe("unflattenConfig", () => {
68
68
  });
69
69
  });
70
70
 
71
- it("maps Cursor and Codex alternative models into agent config", () => {
71
+ it("maps Cursor and Codex runtime settings into agent config", () => {
72
72
  expect(
73
73
  unflattenConfig({
74
74
  CHATCCC_CURSOR_ALTERNATIVE_MODEL: "gpt-5.5-high",
75
75
  CHATCCC_CODEX_ALTERNATIVE_MODEL: "gpt-5.3-codex",
76
+ CHATCCC_CODEX_FAST_MODE: true,
76
77
  }),
77
78
  ).toEqual({
78
79
  cursor: {
@@ -80,6 +81,7 @@ describe("unflattenConfig", () => {
80
81
  },
81
82
  codex: {
82
83
  alternativeModel: "gpt-5.3-codex",
84
+ fastMode: true,
83
85
  },
84
86
  });
85
87
  });
@@ -149,6 +151,7 @@ describe("getRestartRequiredReasons", () => {
149
151
  model: "",
150
152
  alternativeModel: "",
151
153
  effort: "",
154
+ fastMode: false,
152
155
  },
153
156
  };
154
157
 
@@ -159,7 +162,13 @@ describe("getRestartRequiredReasons", () => {
159
162
  chromeDevtools: { enabled: true, port: 15167, chromePath: "C:/Chrome/chrome.exe" },
160
163
  claude: { ...baseConfig.claude, model: "claude-sonnet", apiKey: "sk-test", maxTurn: 8 },
161
164
  cursor: { ...baseConfig.cursor, path: "C:/cursor-agent.cmd", model: "cursor-model" },
162
- codex: { ...baseConfig.codex, path: "C:/codex.cmd", model: "gpt-5.3-codex", effort: "high" },
165
+ codex: {
166
+ ...baseConfig.codex,
167
+ path: "C:/codex.cmd",
168
+ model: "gpt-5.3-codex",
169
+ effort: "high",
170
+ fastMode: true,
171
+ },
163
172
  }),
164
173
  ).toEqual([]);
165
174
  });
@@ -211,6 +220,12 @@ describe("dashboard edit modal", () => {
211
220
  expect(PAGE_HTML).toContain("生效范围:飞书开关、App ID、App Secret 或平台类型变更需要重启 ChatCCC");
212
221
  });
213
222
 
223
+ it("shows the Codex Fast mode switch in both the wizard and dashboard", () => {
224
+ expect(PAGE_HTML).toContain('id="field-CHATCCC_CODEX_FAST_MODE"');
225
+ expect(PAGE_HTML).toContain('id="cfg-CODEX_FAST_MODE"');
226
+ expect(PAGE_HTML).toContain("Fast 模式");
227
+ });
228
+
214
229
  it("shows the Web UI startup switch in both the wizard and dashboard", () => {
215
230
  expect(PAGE_HTML).toContain('id="field-CHATCCC_WEB_UI_OPEN_ON_START"');
216
231
  expect(PAGE_HTML).toContain('id="cfg-WEB_UI_OPEN_ON_START"');
@@ -199,13 +199,12 @@ export function normalizeCodexMessage(
199
199
  // 子进程辅助函数
200
200
  // ---------------------------------------------------------------------------
201
201
 
202
- function spawnCodex(
202
+ export function buildCodexInvocationArgs(
203
203
  args: string[],
204
- cwd?: string,
205
- stdinText?: string,
206
204
  modelOverride?: string,
207
205
  effortOverride?: string,
208
- ): ChildProcess {
206
+ fastModeOverride?: boolean,
207
+ ): string[] {
209
208
  const allArgs = [...args];
210
209
  const model = modelOverride ?? resolveCodexModel();
211
210
  if (model) {
@@ -217,6 +216,25 @@ function spawnCodex(
217
216
  if (effort) {
218
217
  allArgs.push("-c", `model_reasoning_effort="${effort}"`);
219
218
  }
219
+ const fastMode = fastModeOverride ?? config.codex.fastMode;
220
+ allArgs.push("-c", `service_tier="${fastMode ? "fast" : "default"}"`);
221
+ return allArgs;
222
+ }
223
+
224
+ function spawnCodex(
225
+ args: string[],
226
+ cwd?: string,
227
+ stdinText?: string,
228
+ modelOverride?: string,
229
+ effortOverride?: string,
230
+ fastModeOverride?: boolean,
231
+ ): ChildProcess {
232
+ const allArgs = buildCodexInvocationArgs(
233
+ args,
234
+ modelOverride,
235
+ effortOverride,
236
+ fastModeOverride,
237
+ );
220
238
 
221
239
  const proc = spawn(detectCodexCommand(), allArgs, {
222
240
  cwd,
@@ -269,11 +287,18 @@ class CodexAdapter implements ToolAdapter {
269
287
  private metaStore: CodexSessionMetaStore;
270
288
  private modelOverride: string | undefined;
271
289
  private effortOverride: string | undefined;
290
+ private fastModeOverride: boolean | undefined;
272
291
 
273
- constructor(metaStore: CodexSessionMetaStore, modelOverride?: string, effortOverride?: string) {
292
+ constructor(
293
+ metaStore: CodexSessionMetaStore,
294
+ modelOverride?: string,
295
+ effortOverride?: string,
296
+ fastModeOverride?: boolean,
297
+ ) {
274
298
  this.metaStore = metaStore;
275
299
  this.modelOverride = modelOverride;
276
300
  this.effortOverride = effortOverride;
301
+ this.fastModeOverride = fastModeOverride;
277
302
  }
278
303
 
279
304
  // createSession: 生成 sessionId,记录 cwd,不创建 Codex 线程(延迟到首次 prompt)
@@ -304,7 +329,14 @@ class CodexAdapter implements ToolAdapter {
304
329
  ? [...baseArgs, "-C", cwd, "-"]
305
330
  : [...baseArgs, "resume", threadId, "-"];
306
331
 
307
- const proc = spawnCodex(args, cwd, buildCodexPromptText(userText), this.modelOverride, this.effortOverride);
332
+ const proc = spawnCodex(
333
+ args,
334
+ cwd,
335
+ buildCodexPromptText(userText),
336
+ this.modelOverride,
337
+ this.effortOverride,
338
+ this.fastModeOverride,
339
+ );
308
340
  if (proc.pid !== undefined) options?.onProcessStart?.({ pid: proc.pid });
309
341
 
310
342
  const rawLogConfig = config.rawStreamLogs.codex;
@@ -379,6 +411,7 @@ export interface CreateCodexAdapterOptions {
379
411
  /** per-session 模型覆盖(/model 命令);传了就用,不传走全局 codex.model */
380
412
  model?: string;
381
413
  effort?: string;
414
+ fastMode?: boolean;
382
415
  }
383
416
 
384
417
  export function createCodexAdapter(
@@ -388,5 +421,6 @@ export function createCodexAdapter(
388
421
  options.metaStore ?? defaultCodexSessionMetaStore,
389
422
  options.model,
390
423
  options.effort,
424
+ options.fastMode,
391
425
  );
392
426
  }
package/src/cards.ts CHANGED
@@ -553,8 +553,12 @@ export function buildModelCard(
553
553
  lines.push("", "没有可切换的模型。请在 config.json 中配置模型字段。");
554
554
  }
555
555
 
556
- const buttons: ButtonDef[] = [];
557
- for (const m of models.slice(0, 20)) {
556
+ if (tool === "codex") {
557
+ lines.push("输入 `/fast` 查看或切换当前会话的 Fast 模式");
558
+ }
559
+
560
+ const buttons: ButtonDef[] = [];
561
+ for (const m of models.slice(0, 20)) {
558
562
  const shortName = m.includes("/") ? m.slice(m.lastIndexOf("/") + 1) : m;
559
563
  buttons.push({
560
564
  text: `/model ${shortName}`,
@@ -574,6 +578,43 @@ export function buildModelCard(
574
578
  });
575
579
  }
576
580
 
581
+ export function buildFastModeCard(enabled: boolean): string {
582
+ const mode = enabled ? "ON (Fast)" : "OFF (Standard)";
583
+ return JSON.stringify({
584
+ config: { wide_screen_mode: true },
585
+ header: {
586
+ template: enabled ? "green" : "blue",
587
+ title: { content: "Codex Fast 模式", tag: "plain_text" },
588
+ },
589
+ elements: [
590
+ {
591
+ tag: "div",
592
+ text: {
593
+ tag: "lark_md",
594
+ content: [
595
+ `**当前模式:** ${mode}`,
596
+ "",
597
+ "切换将在下一条消息生效,当前生成不中断。",
598
+ ].join("\n"),
599
+ },
600
+ },
601
+ { tag: "hr" },
602
+ buildButtons([
603
+ {
604
+ text: "ON",
605
+ value: JSON.stringify({ cmd: "/fast on" }),
606
+ type: enabled ? "primary" : "default",
607
+ },
608
+ {
609
+ text: "OFF",
610
+ value: JSON.stringify({ cmd: "/fast off" }),
611
+ type: enabled ? "default" : "primary",
612
+ },
613
+ ]),
614
+ ],
615
+ });
616
+ }
617
+
577
618
  export function buildEffortCard(
578
619
  currentEffort: string,
579
620
  efforts: string[],
package/src/config.ts CHANGED
@@ -97,8 +97,10 @@ export interface CodexConfig {
97
97
  path: string;
98
98
  model: string;
99
99
  /** /model 可切换的单个备选模型;留空则不加入候选列表 */
100
- alternativeModel: string;
100
+ alternativeModel: string;
101
101
  effort: string;
102
+ /** Codex Priority service tier. False explicitly forces the standard tier. */
103
+ fastMode: boolean;
102
104
  }
103
105
 
104
106
  export interface CccConfig {
@@ -442,7 +444,7 @@ function loadConfig(): AppConfig {
442
444
  avatarBatteryMode: "apiPercent",
443
445
  onDemandMonthlyBudget: 1000,
444
446
  },
445
- codex: { enabled: false, defaultAgent: false, path: "", model: "", alternativeModel: "", effort: "" },
447
+ codex: { enabled: false, defaultAgent: false, path: "", model: "", alternativeModel: "", effort: "", fastMode: false },
446
448
  ccc: { DEEPSEEK_API_KEY: "", DEEPSEEK_BASE_URL: DEFAULT_CCC_DEEPSEEK_BASE_URL, model: DEFAULT_CCC_MODEL },
447
449
  };
448
450
 
@@ -495,7 +497,7 @@ function loadConfig(): AppConfig {
495
497
  avatarBatteryMode?: unknown;
496
498
  onDemandMonthlyBudget?: unknown;
497
499
  };
498
- codex?: { enabled?: unknown; defaultAgent?: unknown; path?: unknown; command?: unknown; model?: unknown; alternativeModel?: unknown; effort?: unknown };
500
+ codex?: { enabled?: unknown; defaultAgent?: unknown; path?: unknown; command?: unknown; model?: unknown; alternativeModel?: unknown; effort?: unknown; fastMode?: unknown };
499
501
  ccc?: { DEEPSEEK_API_KEY?: unknown; DEEPSEEK_BASE_URL?: unknown; model?: unknown };
500
502
  webUi?: { openOnStart?: unknown };
501
503
  chromeDevtools?: { enabled?: unknown; port?: unknown; chromePath?: unknown };
@@ -555,9 +557,10 @@ function loadConfig(): AppConfig {
555
557
  Boolean(
556
558
  (typeof codexRaw.path === "string" && codexRaw.path.trim()) ||
557
559
  (typeof codexRaw.command === "string" && (codexRaw.command as string).trim()) ||
558
- (typeof codexRaw.model === "string" && (codexRaw.model as string).trim()) ||
559
- (typeof codexRaw.alternativeModel === "string" && (codexRaw.alternativeModel as string).trim()) ||
560
- (typeof codexRaw.effort === "string" && (codexRaw.effort as string).trim()),
560
+ (typeof codexRaw.model === "string" && (codexRaw.model as string).trim()) ||
561
+ (typeof codexRaw.alternativeModel === "string" && (codexRaw.alternativeModel as string).trim()) ||
562
+ (typeof codexRaw.effort === "string" && (codexRaw.effort as string).trim()) ||
563
+ codexRaw.fastMode === true,
561
564
  );
562
565
 
563
566
  const claudeEnabled = resolveEnabled(claude.enabled, claudeNonEmpty);
@@ -649,6 +652,7 @@ function loadConfig(): AppConfig {
649
652
  model: normalizeOptionalConfigField(codexRaw.model, { label: "codex.model" }),
650
653
  alternativeModel: normalizeOptionalConfigField(codexRaw.alternativeModel, { label: "codex.alternativeModel" }),
651
654
  effort: normalizeOptionalConfigField(codexRaw.effort, { label: "codex.effort" }),
655
+ fastMode: codexRaw.fastMode === true,
652
656
  },
653
657
  ccc: {
654
658
  DEEPSEEK_API_KEY: normalizeOptionalConfigField(cccRaw.DEEPSEEK_API_KEY, { label: "ccc.DEEPSEEK_API_KEY" }),
@@ -39,6 +39,7 @@ import {
39
39
  import {
40
40
  buildHelpCard,
41
41
  buildEffortCard,
42
+ buildFastModeCard,
42
43
  buildModelCard,
43
44
  buildStatusCard,
44
45
  buildCdContent,
@@ -69,6 +70,8 @@ import {
69
70
  getAdapterForTool,
70
71
  getEffectiveModelForTool,
71
72
  getEffectiveEffortForTool,
73
+ getEffectiveFastModeForTool,
74
+ setSessionFastModeOverride,
72
75
  stopSession,
73
76
  loadSessionRegistryForBinding,
74
77
  removeSessionRegistryRecord,
@@ -325,11 +328,33 @@ function formatCursorUsageSummary(usage: CursorUsageSummary): string {
325
328
  ].filter(Boolean).join("\n");
326
329
  }
327
330
 
328
- function usageHelpLine(tool: string): string {
331
+ function usageHelpLine(tool: string): string {
329
332
  if (tool === "codex") return "\n发送 **/usage** 查看 Codex 实际存在的 5h/7天用量窗口,以及查询/使用主动重置卡。";
330
333
  if (tool === "cursor") return "\n发送 **/usage** 查看 Cursor 用量。";
331
- return "";
332
- }
334
+ return "";
335
+ }
336
+
337
+ function fastHelpAfterModel(tool: string): string {
338
+ return tool === "codex"
339
+ ? "\n发送 **/fast** 查看或切换当前会话的 Fast 模式。"
340
+ : "";
341
+ }
342
+
343
+ async function sendFastModeStatus(
344
+ platform: PlatformAdapter,
345
+ chatId: string,
346
+ enabled: boolean,
347
+ ): Promise<void> {
348
+ if (platform.kind === "wechat") {
349
+ const mode = enabled ? "ON (Fast)" : "OFF (Standard)";
350
+ await platform.sendText(
351
+ chatId,
352
+ `Codex Fast 模式: ${mode}\n输入 /fast on 或 /fast off 切换。切换将在下一条消息生效,当前生成不中断。`,
353
+ );
354
+ return;
355
+ }
356
+ await platform.sendRawCard(chatId, buildFastModeCard(enabled));
357
+ }
333
358
 
334
359
  async function resolveUsageTarget(chatId: string): Promise<{ tool: "codex" | "cursor"; sessionId?: string }> {
335
360
  try {
@@ -1021,7 +1046,7 @@ export async function handleCommand(
1021
1046
  `**工作目录:** \`${cwd}\`\n\n` +
1022
1047
  `直接在这里发消息即可与 ${toolLabel} 对话。\n\n` +
1023
1048
  `发送 **/cd** 切换新建会话的默认目录。\n` +
1024
- `发送 **/model** 查看或切换当前会话的模型。\n` +
1049
+ `发送 **/model** 查看或切换当前会话的模型。${fastHelpAfterModel(tool)}\n` +
1025
1050
  `发送 **/new** 创建新会话,**/newh** 重置当前会话(沿用工作目录)。\n` +
1026
1051
  `发送 **/sessions** 查看所有会话状态。\n` +
1027
1052
  `发送 \`/git <子命令>\` 在本会话工作目录执行 git,例如 \`/git status\`、\`/git log --oneline -n 5\`。` +
@@ -1109,7 +1134,7 @@ export async function handleCommand(
1109
1134
  `**工作目录:** \`${cwd}\`\n\n` +
1110
1135
  `直接在这里发消息即可与 ${toolLabel} 对话。\n\n` +
1111
1136
  `发送 **/cd** 切换新建会话的默认目录。\n` +
1112
- `发送 **/model** 查看或切换当前会话的模型。\n` +
1137
+ `发送 **/model** 查看或切换当前会话的模型。${fastHelpAfterModel(tool)}\n` +
1113
1138
  `发送 **/new** 创建新会话,**/newh** 重置当前会话(沿用工作目录)。\n` +
1114
1139
  `发送 **/sessions** 查看所有会话状态。\n` +
1115
1140
  `发送 \`/git <子命令>\` 在本会话工作目录执行 git,例如 \`/git status\`、\`/git log --oneline -n 5\`。` +
@@ -1531,7 +1556,7 @@ export async function handleCommand(
1531
1556
  `**工作目录:** \`${cwd}\`${isFeishuP2p(platform, chatType) ? "(飞书私聊固定使用系统用户目录)" : "(沿用当前会话目录)"}\n\n` +
1532
1557
  `直接在这里发消息即可继续对话。\n` +
1533
1558
  `发送 **/cd** 可切换新建会话的默认目录。\n` +
1534
- `发送 **/model** 查看或切换当前会话的模型。`,
1559
+ `发送 **/model** 查看或切换当前会话的模型。${fastHelpAfterModel(descriptionTool)}`,
1535
1560
  "green",
1536
1561
  );
1537
1562
 
@@ -1705,7 +1730,7 @@ export async function handleCommand(
1705
1730
  `**Session ID:** ${target.sessionId}\n` +
1706
1731
  `**工作目录:** \`${cwd2}\`\n\n` +
1707
1732
  `直接在这里发消息即可继续对话。\n` +
1708
- `发送 **/model** 查看或切换当前会话的模型。${busyNote}`,
1733
+ `发送 **/model** 查看或切换当前会话的模型。${fastHelpAfterModel(descriptionTool)}${busyNote}`,
1709
1734
  "green",
1710
1735
  );
1711
1736
 
@@ -1718,7 +1743,44 @@ export async function handleCommand(
1718
1743
  return;
1719
1744
  }
1720
1745
 
1721
- // /model clear 清除当前 session 的模型覆盖
1746
+ if (isCommandText && (textLower === "/fast" || textLower.startsWith("/fast "))) {
1747
+ const fastArg = text.slice(5).trim().toLowerCase();
1748
+ logTrace(tid, "BRANCH", { cmd: "/fast", arg: fastArg, sessionId, tool: descriptionTool });
1749
+
1750
+ if (descriptionTool !== "codex") {
1751
+ const msg = `当前 ${toolLabel} 会话不支持 Fast 模式;/fast 仅适用于 Codex。`;
1752
+ await (platform.kind === "wechat"
1753
+ ? platform.sendText(chatId, msg)
1754
+ : platform.sendCard(chatId, "Codex Fast 模式", msg, "yellow")
1755
+ ).catch(() => {});
1756
+ logTrace(tid, "DONE", { outcome: "fast_unsupported", tool: descriptionTool });
1757
+ return;
1758
+ }
1759
+
1760
+ if (fastArg && fastArg !== "on" && fastArg !== "off") {
1761
+ const msg = "用法: /fast、/fast on 或 /fast off";
1762
+ await (platform.kind === "wechat"
1763
+ ? platform.sendText(chatId, msg)
1764
+ : platform.sendCard(chatId, "Codex Fast 模式", msg, "yellow")
1765
+ ).catch(() => {});
1766
+ logTrace(tid, "DONE", { outcome: "fast_invalid", arg: fastArg });
1767
+ return;
1768
+ }
1769
+
1770
+ if (fastArg) {
1771
+ setSessionFastModeOverride(sessionId, fastArg === "on");
1772
+ }
1773
+ const enabled = getEffectiveFastModeForTool("codex", sessionId);
1774
+ await sendFastModeStatus(platform, chatId, enabled).catch(() => {});
1775
+ logTrace(tid, "DONE", {
1776
+ outcome: fastArg ? "fast_switched" : "fast_query",
1777
+ enabled,
1778
+ sessionId,
1779
+ });
1780
+ return;
1781
+ }
1782
+
1783
+ // /model clear — 清除当前 session 的模型覆盖
1722
1784
  if (isCommandText && textLower === "/model clear") {
1723
1785
  logTrace(tid, "BRANCH", { cmd: "/model clear", sessionId });
1724
1786
  clearSessionModelOverride(sessionId);
@@ -1776,12 +1838,15 @@ export async function handleCommand(
1776
1838
  if (platform.kind === "wechat") {
1777
1839
  const lines = [currentModel ? `当前模型 (${toolLabel}): ${currentModel}` : `当前模型 (${toolLabel}): 未指定`];
1778
1840
  if (models.length > 0) {
1779
- lines.push("", "可切换模型:");
1780
- for (const m of models) lines.push(` ${m}`);
1781
- lines.push("", "输入 /model <模型名> 切换模型");
1782
- } else {
1783
- lines.push("", "没有可切换的模型。请在 config.json 中配置模型字段。");
1784
- }
1841
+ lines.push("", "可切换模型:");
1842
+ for (const m of models) lines.push(` ${m}`);
1843
+ lines.push("", "输入 /model <模型名> 切换模型");
1844
+ } else {
1845
+ lines.push("", "没有可切换的模型。请在 config.json 中配置模型字段。");
1846
+ }
1847
+ if (descriptionTool === "codex") {
1848
+ lines.push("输入 /fast 查看或切换当前会话的 Fast 模式");
1849
+ }
1785
1850
  await platform.sendText(chatId, lines.join("\n")).catch(() => {});
1786
1851
  } else {
1787
1852
  const card = buildModelCard(currentModel, models, descriptionTool);
@@ -2022,7 +2087,34 @@ export async function handleCommand(
2022
2087
  return;
2023
2088
  }
2024
2089
 
2025
- // 无会话上下文 检查是否是 /model 查询
2090
+ if (isCommandText && (textLower === "/fast" || textLower.startsWith("/fast "))) {
2091
+ const defaultTool = resolveDefaultAgentTool();
2092
+ const fastArg = text.slice(5).trim().toLowerCase();
2093
+ if (defaultTool !== "codex") {
2094
+ const msg = `当前默认 Agent (${toolDisplayName(defaultTool)}) 不支持 Fast 模式;/fast 仅适用于 Codex。`;
2095
+ await (platform.kind === "wechat"
2096
+ ? platform.sendText(chatId, msg)
2097
+ : platform.sendCard(chatId, "Codex Fast 模式", msg, "yellow")
2098
+ ).catch(() => {});
2099
+ logTrace(tid, "DONE", { outcome: "fast_unsupported", defaultTool });
2100
+ return;
2101
+ }
2102
+ if (fastArg) {
2103
+ const msg = "当前没有绑定 Codex 会话,无法设置会话覆盖。请先创建或进入 Codex 会话;全局默认值可在 Web UI 中设置。";
2104
+ await (platform.kind === "wechat"
2105
+ ? platform.sendText(chatId, msg)
2106
+ : platform.sendCard(chatId, "Codex Fast 模式", msg, "yellow")
2107
+ ).catch(() => {});
2108
+ logTrace(tid, "DONE", { outcome: "fast_no_session", arg: fastArg });
2109
+ return;
2110
+ }
2111
+ const enabled = getEffectiveFastModeForTool("codex");
2112
+ await sendFastModeStatus(platform, chatId, enabled).catch(() => {});
2113
+ logTrace(tid, "DONE", { outcome: "fast_query", enabled, defaultTool });
2114
+ return;
2115
+ }
2116
+
2117
+ // 无会话上下文 → 检查是否是 /model 查询
2026
2118
  if (isCommandText && textLower === "/model") {
2027
2119
  const defaultTool = resolveDefaultAgentTool();
2028
2120
  const models = getAllModelsForTool(defaultTool);
@@ -2034,12 +2126,15 @@ export async function handleCommand(
2034
2126
  if (platform.kind === "wechat") {
2035
2127
  const lines = [currentModel ? `当前模型 (${defaultTool}): ${currentModel}` : `当前模型 (${defaultTool}): 未指定`];
2036
2128
  if (models.length > 0) {
2037
- lines.push("", "可切换模型:");
2038
- for (const m of models) lines.push(` ${m}`);
2039
- lines.push("", "在会话中输入 /model <模型名> 切换模型");
2040
- } else {
2041
- lines.push("", "没有可切换的模型。请在 config.json 中配置模型字段。");
2042
- }
2129
+ lines.push("", "可切换模型:");
2130
+ for (const m of models) lines.push(` ${m}`);
2131
+ lines.push("", "在会话中输入 /model <模型名> 切换模型");
2132
+ } else {
2133
+ lines.push("", "没有可切换的模型。请在 config.json 中配置模型字段。");
2134
+ }
2135
+ if (defaultTool === "codex") {
2136
+ lines.push("输入 /fast 查看当前 Codex Fast 模式");
2137
+ }
2043
2138
  await platform.sendText(chatId, lines.join("\n")).catch(() => {});
2044
2139
  } else {
2045
2140
  const card = buildModelCard(currentModel, models, defaultTool);
package/src/session.ts CHANGED
@@ -475,6 +475,7 @@ export function resetState(): void {
475
475
  displayCards.clear();
476
476
  sessionModelOverrides.clear();
477
477
  sessionEffortOverrides.clear();
478
+ sessionFastModeOverrides.clear();
478
479
  adapterCache.clear();
479
480
  stopUnifiedDisplayLoop();
480
481
  console.log(`[${ts()}] [RESET] State cleared (dedup + active sessions + bindings)`);
@@ -492,6 +493,7 @@ const adapterCache = new Map<string, ToolAdapter>();
492
493
  // Per-session 模型覆盖(/model 命令设置,不持久化)
493
494
  const sessionModelOverrides = new Map<string, string>();
494
495
  const sessionEffortOverrides = new Map<string, string>();
496
+ const sessionFastModeOverrides = new Map<string, boolean>();
495
497
 
496
498
  /** 返回 session 的生效模型:优先 per-session 覆盖,其次全局配置(Claude) */
497
499
  function getModelForSession(sessionId?: string): string {
@@ -524,6 +526,14 @@ export function getEffectiveEffortForTool(tool: string, sessionId?: string): str
524
526
  }
525
527
  return "";
526
528
  }
529
+
530
+ export function getEffectiveFastModeForTool(tool: string, sessionId?: string): boolean {
531
+ if (tool !== "codex") return false;
532
+ if (sessionId && sessionFastModeOverrides.has(sessionId)) {
533
+ return sessionFastModeOverrides.get(sessionId) === true;
534
+ }
535
+ return config.codex.fastMode;
536
+ }
527
537
 
528
538
  /** 为指定 session 设置模型覆盖(/model <name>) */
529
539
  export function setSessionModelOverride(sessionId: string, model: string): void {
@@ -547,10 +557,16 @@ export function clearSessionEffortOverride(sessionId: string): void {
547
557
  adapterCache.clear();
548
558
  }
549
559
 
560
+ export function setSessionFastModeOverride(sessionId: string, fastMode: boolean): void {
561
+ sessionFastModeOverrides.set(sessionId, fastMode);
562
+ adapterCache.clear();
563
+ }
564
+
550
565
  export function getAdapterForTool(tool: string, sessionId?: string): ToolAdapter {
551
566
  const effectiveModel = getEffectiveModelForTool(tool, sessionId);
552
567
  const effectiveEffort = getEffectiveEffortForTool(tool, sessionId);
553
- const cacheKey = `${tool}:${effectiveModel || ""}:${effectiveEffort || ""}`;
568
+ const effectiveFastMode = getEffectiveFastModeForTool(tool, sessionId);
569
+ const cacheKey = `${tool}:${effectiveModel || ""}:${effectiveEffort || ""}:${effectiveFastMode ? "fast" : "default"}`;
554
570
  const cached = adapterCache.get(cacheKey);
555
571
  if (cached) return cached;
556
572
 
@@ -558,7 +574,11 @@ export function getAdapterForTool(tool: string, sessionId?: string): ToolAdapter
558
574
  if (tool === "cursor") {
559
575
  adapter = createCursorAdapter({ model: effectiveModel || undefined });
560
576
  } else if (tool === "codex") {
561
- adapter = createCodexAdapter({ model: effectiveModel || undefined, effort: effectiveEffort || undefined });
577
+ adapter = createCodexAdapter({
578
+ model: effectiveModel || undefined,
579
+ effort: effectiveEffort || undefined,
580
+ fastMode: effectiveFastMode,
581
+ });
562
582
  } else if (tool === "ccc") {
563
583
  adapter = createCccAdapter({ model: effectiveModel || undefined });
564
584
  } else {
@@ -1009,7 +1029,7 @@ function formatToolConfigForLog(tool: string, sessionModel?: string, sessionId?:
1009
1029
  const effortStr = e.trim() !== ""
1010
1030
  ? `effort=${e}`
1011
1031
  : "effort=(由 codex config.toml 决定)";
1012
- return `model=${modelStr}, ${effortStr}`;
1032
+ return `model=${modelStr}, ${effortStr}, fast=${getEffectiveFastModeForTool(tool, sessionId) ? "on" : "off"}`;
1013
1033
  }
1014
1034
  if (tool === "ccc") {
1015
1035
  const m = getEffectiveModelForTool(tool, sessionId);
@@ -2406,9 +2426,10 @@ export async function getAllSessionsStatus(): Promise<SessionsListEntry[]> {
2406
2426
  export function _setAdapterForToolForTest(tool: string, adapter: ToolAdapter): void {
2407
2427
  adapterCache.set(tool, adapter);
2408
2428
  // 同时设置当前配置模型对应的 key(getAdapterForTool 会优先 lookup 含 model 的 key)
2409
- const effective = getEffectiveModelForTool(tool);
2429
+ const effective = getEffectiveModelForTool(tool);
2410
2430
  const effort = getEffectiveEffortForTool(tool);
2411
- adapterCache.set(`${tool}:${effective || ""}:${effort || ""}`, adapter);
2431
+ const fastMode = getEffectiveFastModeForTool(tool);
2432
+ adapterCache.set(`${tool}:${effective || ""}:${effort || ""}:${fastMode ? "fast" : "default"}`, adapter);
2412
2433
  if (effective) adapterCache.set(`${tool}:${effective}`, adapter);
2413
2434
  }
2414
2435