chatccc 0.2.197 → 0.2.199

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.
Files changed (52) hide show
  1. package/agent-prompts/cursor_specific.md +13 -13
  2. package/bin/cccagent.mjs +17 -17
  3. package/config.sample.json +27 -27
  4. package/package.json +2 -1
  5. package/src/__tests__/agent-reload-config-rpc.test.ts +99 -99
  6. package/src/__tests__/builtin-chat-session.test.ts +277 -277
  7. package/src/__tests__/builtin-cli-json.test.ts +39 -39
  8. package/src/__tests__/builtin-config.test.ts +33 -33
  9. package/src/__tests__/builtin-context.test.ts +163 -163
  10. package/src/__tests__/builtin-file-tools.test.ts +224 -224
  11. package/src/__tests__/builtin-session-select.test.ts +116 -116
  12. package/src/__tests__/builtin-sigint.test.ts +56 -56
  13. package/src/__tests__/cards.test.ts +109 -109
  14. package/src/__tests__/ccc-adapter.test.ts +114 -114
  15. package/src/__tests__/chatgpt-subscription-rpc.test.ts +89 -89
  16. package/src/__tests__/chatgpt-subscription.test.ts +135 -135
  17. package/src/__tests__/chrome-devtools-guard.test.ts +165 -165
  18. package/src/__tests__/claude-raw-stream-log.test.ts +87 -87
  19. package/src/__tests__/codex-raw-stream-log.test.ts +163 -120
  20. package/src/__tests__/config-reload.test.ts +10 -10
  21. package/src/__tests__/config-sample.test.ts +18 -18
  22. package/src/__tests__/cursor-adapter.test.ts +114 -1
  23. package/src/__tests__/feishu-avatar.test.ts +40 -40
  24. package/src/__tests__/jsonl-stream.test.ts +79 -0
  25. package/src/__tests__/orchestrator.test.ts +200 -200
  26. package/src/__tests__/raw-stream-log.test.ts +106 -106
  27. package/src/__tests__/session.test.ts +40 -40
  28. package/src/__tests__/sim-platform.test.ts +12 -12
  29. package/src/__tests__/web-ui.test.ts +209 -209
  30. package/src/adapters/ccc-adapter.ts +121 -121
  31. package/src/adapters/claude-adapter.ts +603 -603
  32. package/src/adapters/codex-adapter.ts +380 -392
  33. package/src/adapters/cursor-adapter.ts +56 -30
  34. package/src/adapters/jsonl-stream.ts +157 -0
  35. package/src/adapters/raw-stream-log.ts +124 -124
  36. package/src/agent-reload-config-rpc.ts +34 -34
  37. package/src/builtin/cli.ts +473 -473
  38. package/src/builtin/context.ts +323 -323
  39. package/src/builtin/file-tools.ts +1072 -1072
  40. package/src/builtin/index.ts +404 -404
  41. package/src/builtin/session-select.ts +48 -48
  42. package/src/builtin/sigint.ts +50 -50
  43. package/src/cards.ts +195 -195
  44. package/src/chatgpt-subscription-rpc.ts +27 -27
  45. package/src/chatgpt-subscription.ts +299 -299
  46. package/src/chrome-devtools-guard.ts +318 -318
  47. package/src/config.ts +125 -125
  48. package/src/feishu-api.ts +49 -49
  49. package/src/orchestrator.ts +179 -179
  50. package/src/runtime-reload.ts +34 -34
  51. package/src/session.ts +141 -141
  52. package/src/web-ui.ts +205 -205
@@ -1,48 +1,48 @@
1
- import {
2
- DEFAULT_BUILTIN_CONTEXT_DIR,
3
- getBuiltinContextSession,
4
- latestBuiltinSessionForCwd,
5
- newBuiltinSessionId,
6
- normalizeBuiltinSessionId,
7
- } from "./context.js";
8
-
9
- export type BuiltinResumeRequest = true | string | undefined;
10
-
11
- export interface ResolveBuiltinSessionOptions {
12
- cwd: string;
13
- contextDir?: string;
14
- resume?: BuiltinResumeRequest;
15
- now?: Date;
16
- randomSuffix?: string;
17
- }
18
-
19
- export interface ResolvedBuiltinSession {
20
- mode: "new" | "resumed";
21
- sessionId: string;
22
- }
23
-
24
- export function resolveBuiltinSession(options: ResolveBuiltinSessionOptions): ResolvedBuiltinSession {
25
- const contextDir = options.contextDir ?? DEFAULT_BUILTIN_CONTEXT_DIR;
26
-
27
- if (options.resume === true) {
28
- const latest = latestBuiltinSessionForCwd(options.cwd, contextDir);
29
- if (!latest) {
30
- throw new Error(`未找到当前目录可恢复的 ccc 会话: ${options.cwd}`);
31
- }
32
- return { mode: "resumed", sessionId: latest.sessionId };
33
- }
34
-
35
- if (typeof options.resume === "string") {
36
- const sessionId = normalizeBuiltinSessionId(options.resume);
37
- const existing = getBuiltinContextSession(sessionId, contextDir);
38
- if (!existing) {
39
- throw new Error(`未找到 ccc 会话: ${sessionId}`);
40
- }
41
- return { mode: "resumed", sessionId };
42
- }
43
-
44
- return {
45
- mode: "new",
46
- sessionId: newBuiltinSessionId(options.now, options.randomSuffix),
47
- };
48
- }
1
+ import {
2
+ DEFAULT_BUILTIN_CONTEXT_DIR,
3
+ getBuiltinContextSession,
4
+ latestBuiltinSessionForCwd,
5
+ newBuiltinSessionId,
6
+ normalizeBuiltinSessionId,
7
+ } from "./context.js";
8
+
9
+ export type BuiltinResumeRequest = true | string | undefined;
10
+
11
+ export interface ResolveBuiltinSessionOptions {
12
+ cwd: string;
13
+ contextDir?: string;
14
+ resume?: BuiltinResumeRequest;
15
+ now?: Date;
16
+ randomSuffix?: string;
17
+ }
18
+
19
+ export interface ResolvedBuiltinSession {
20
+ mode: "new" | "resumed";
21
+ sessionId: string;
22
+ }
23
+
24
+ export function resolveBuiltinSession(options: ResolveBuiltinSessionOptions): ResolvedBuiltinSession {
25
+ const contextDir = options.contextDir ?? DEFAULT_BUILTIN_CONTEXT_DIR;
26
+
27
+ if (options.resume === true) {
28
+ const latest = latestBuiltinSessionForCwd(options.cwd, contextDir);
29
+ if (!latest) {
30
+ throw new Error(`未找到当前目录可恢复的 ccc 会话: ${options.cwd}`);
31
+ }
32
+ return { mode: "resumed", sessionId: latest.sessionId };
33
+ }
34
+
35
+ if (typeof options.resume === "string") {
36
+ const sessionId = normalizeBuiltinSessionId(options.resume);
37
+ const existing = getBuiltinContextSession(sessionId, contextDir);
38
+ if (!existing) {
39
+ throw new Error(`未找到 ccc 会话: ${sessionId}`);
40
+ }
41
+ return { mode: "resumed", sessionId };
42
+ }
43
+
44
+ return {
45
+ mode: "new",
46
+ sessionId: newBuiltinSessionId(options.now, options.randomSuffix),
47
+ };
48
+ }
@@ -1,50 +1,50 @@
1
- export const CTRL_C_CONFIRM_WINDOW_MS = 2000;
2
-
3
- export type CtrlCAction =
4
- | "arm-interrupt"
5
- | "interrupt"
6
- | "arm-exit"
7
- | "exit";
8
-
9
- type PendingCtrlCAction = "interrupt" | "exit";
10
-
11
- export interface CtrlCStateOptions {
12
- windowMs?: number;
13
- now?: () => number;
14
- }
15
-
16
- export interface CtrlCState {
17
- press(isGenerating: boolean): CtrlCAction;
18
- reset(): void;
19
- }
20
-
21
- export function createCtrlCState(options: CtrlCStateOptions = {}): CtrlCState {
22
- const windowMs = options.windowMs ?? CTRL_C_CONFIRM_WINDOW_MS;
23
- const now = options.now ?? Date.now;
24
-
25
- let pending: PendingCtrlCAction | null = null;
26
- let lastPressAt = 0;
27
-
28
- return {
29
- press(isGenerating: boolean): CtrlCAction {
30
- const action: PendingCtrlCAction = isGenerating ? "interrupt" : "exit";
31
- const current = now();
32
- const isConfirmed = pending === action && current - lastPressAt <= windowMs;
33
-
34
- if (isConfirmed) {
35
- pending = null;
36
- lastPressAt = 0;
37
- return action;
38
- }
39
-
40
- pending = action;
41
- lastPressAt = current;
42
- return isGenerating ? "arm-interrupt" : "arm-exit";
43
- },
44
-
45
- reset(): void {
46
- pending = null;
47
- lastPressAt = 0;
48
- },
49
- };
50
- }
1
+ export const CTRL_C_CONFIRM_WINDOW_MS = 2000;
2
+
3
+ export type CtrlCAction =
4
+ | "arm-interrupt"
5
+ | "interrupt"
6
+ | "arm-exit"
7
+ | "exit";
8
+
9
+ type PendingCtrlCAction = "interrupt" | "exit";
10
+
11
+ export interface CtrlCStateOptions {
12
+ windowMs?: number;
13
+ now?: () => number;
14
+ }
15
+
16
+ export interface CtrlCState {
17
+ press(isGenerating: boolean): CtrlCAction;
18
+ reset(): void;
19
+ }
20
+
21
+ export function createCtrlCState(options: CtrlCStateOptions = {}): CtrlCState {
22
+ const windowMs = options.windowMs ?? CTRL_C_CONFIRM_WINDOW_MS;
23
+ const now = options.now ?? Date.now;
24
+
25
+ let pending: PendingCtrlCAction | null = null;
26
+ let lastPressAt = 0;
27
+
28
+ return {
29
+ press(isGenerating: boolean): CtrlCAction {
30
+ const action: PendingCtrlCAction = isGenerating ? "interrupt" : "exit";
31
+ const current = now();
32
+ const isConfirmed = pending === action && current - lastPressAt <= windowMs;
33
+
34
+ if (isConfirmed) {
35
+ pending = null;
36
+ lastPressAt = 0;
37
+ return action;
38
+ }
39
+
40
+ pending = action;
41
+ lastPressAt = current;
42
+ return isGenerating ? "arm-interrupt" : "arm-exit";
43
+ },
44
+
45
+ reset(): void {
46
+ pending = null;
47
+ lastPressAt = 0;
48
+ },
49
+ };
50
+ }
package/src/cards.ts CHANGED
@@ -1,10 +1,10 @@
1
- // ---------------------------------------------------------------------------
2
- // Button helpers
3
- // ---------------------------------------------------------------------------
4
-
5
- import { ABD_HELP_LINE } from "./shared-prefix.ts";
6
-
7
- export interface ButtonDef {
1
+ // ---------------------------------------------------------------------------
2
+ // Button helpers
3
+ // ---------------------------------------------------------------------------
4
+
5
+ import { ABD_HELP_LINE } from "./shared-prefix.ts";
6
+
7
+ export interface ButtonDef {
8
8
  text: string;
9
9
  value: string;
10
10
  type?: "primary" | "default" | "danger";
@@ -137,11 +137,11 @@ export function buildHelpCard(
137
137
  "发送 **/newh** 重置当前会话(沿用当前工作目录,不切换)",
138
138
  "发送 **/plan** 以规划模式提问(只读,不执行写操作)",
139
139
  "发送 **/ask** 以问答模式提问(只读,不执行写操作)",
140
- "发送 **/usage** 查看 Codex 5h/周用量,以及查询/使用主动重置卡",
141
- "发送 **/restart** 重启 ChatCCC 进程",
142
- "发送 **/update** 更新并重启(仅 npm 全局安装可用)",
143
- ABD_HELP_LINE,
144
- ].join("\n");
140
+ "发送 **/usage** 查看 Codex 5h/周用量,以及查询/使用主动重置卡",
141
+ "发送 **/restart** 重启 ChatCCC 进程",
142
+ "发送 **/update** 更新并重启(仅 npm 全局安装可用)",
143
+ ABD_HELP_LINE,
144
+ ].join("\n");
145
145
  return JSON.stringify({
146
146
  config: { wide_screen_mode: true },
147
147
  header: { template: "blue", title: { content: "ChatCCC", tag: "plain_text" } },
@@ -274,48 +274,48 @@ export function buildCdCard(
274
274
  });
275
275
  }
276
276
 
277
- function sessionToolLabel(tool: string): string {
278
- if (tool === "cursor") return "Cursor";
279
- if (tool === "codex") return "Codex";
280
- if (tool === "ccc") return "CCC Agent";
281
- return "Claude Code";
282
- }
283
-
284
- function pushSessionGroup(
285
- lines: string[],
286
- title: string,
287
- sessions: Array<{
288
- sessionId: string;
289
- chatName: string;
290
- chatId: string;
291
- active: boolean;
292
- turnCount: number;
293
- elapsedSeconds: number | null;
294
- model: string;
295
- tool: string;
296
- }>,
297
- formatSession: (session: {
298
- sessionId: string;
299
- chatName: string;
300
- chatId: string;
301
- active: boolean;
302
- turnCount: number;
303
- elapsedSeconds: number | null;
304
- model: string;
305
- tool: string;
306
- }, index: number) => string,
307
- index: { value: number },
308
- ): void {
309
- if (sessions.length === 0) return;
310
- if (index.value > 0) lines.push("", `**${title}:**`, "");
311
- else lines.push(`**${title}:**`, "");
312
- for (const session of sessions) {
313
- lines.push(formatSession(session, index.value++));
314
- }
315
- }
316
-
317
- // 所有会话列表卡片(Claude Code 优先,然后 Cursor)
318
- export function buildSessionsCard(sessions: Array<{
277
+ function sessionToolLabel(tool: string): string {
278
+ if (tool === "cursor") return "Cursor";
279
+ if (tool === "codex") return "Codex";
280
+ if (tool === "ccc") return "CCC Agent";
281
+ return "Claude Code";
282
+ }
283
+
284
+ function pushSessionGroup(
285
+ lines: string[],
286
+ title: string,
287
+ sessions: Array<{
288
+ sessionId: string;
289
+ chatName: string;
290
+ chatId: string;
291
+ active: boolean;
292
+ turnCount: number;
293
+ elapsedSeconds: number | null;
294
+ model: string;
295
+ tool: string;
296
+ }>,
297
+ formatSession: (session: {
298
+ sessionId: string;
299
+ chatName: string;
300
+ chatId: string;
301
+ active: boolean;
302
+ turnCount: number;
303
+ elapsedSeconds: number | null;
304
+ model: string;
305
+ tool: string;
306
+ }, index: number) => string,
307
+ index: { value: number },
308
+ ): void {
309
+ if (sessions.length === 0) return;
310
+ if (index.value > 0) lines.push("", `**${title}:**`, "");
311
+ else lines.push(`**${title}:**`, "");
312
+ for (const session of sessions) {
313
+ lines.push(formatSession(session, index.value++));
314
+ }
315
+ }
316
+
317
+ // 所有会话列表卡片(Claude Code 优先,然后 Cursor)
318
+ export function buildSessionsCard(sessions: Array<{
319
319
  sessionId: string;
320
320
  chatName: string;
321
321
  chatId: string;
@@ -325,16 +325,16 @@ export function buildSessionsCard(sessions: Array<{
325
325
  model: string;
326
326
  tool: string;
327
327
  }>, opts: { defaultToolLabel?: string } = {}): string {
328
- const defaultToolLabel = opts.defaultToolLabel ?? "Claude Code";
329
- // 按 tool 分组排序:Claude Code 在前,Cursor 其次,Codex 最后
330
- const claudeCodeSessions = sessions.filter(s => s.tool !== "cursor" && s.tool !== "codex" && s.tool !== "ccc");
331
- const cursorSessions = sessions.filter(s => s.tool === "cursor");
332
- const codexSessions = sessions.filter(s => s.tool === "codex");
333
- const cccSessions = sessions.filter(s => s.tool === "ccc");
334
- const hasClaudeCode = claudeCodeSessions.length > 0;
335
- const hasCursor = cursorSessions.length > 0;
336
- const hasCodex = codexSessions.length > 0;
337
- const hasCcc = cccSessions.length > 0;
328
+ const defaultToolLabel = opts.defaultToolLabel ?? "Claude Code";
329
+ // 按 tool 分组排序:Claude Code 在前,Cursor 其次,Codex 最后
330
+ const claudeCodeSessions = sessions.filter(s => s.tool !== "cursor" && s.tool !== "codex" && s.tool !== "ccc");
331
+ const cursorSessions = sessions.filter(s => s.tool === "cursor");
332
+ const codexSessions = sessions.filter(s => s.tool === "codex");
333
+ const cccSessions = sessions.filter(s => s.tool === "ccc");
334
+ const hasClaudeCode = claudeCodeSessions.length > 0;
335
+ const hasCursor = cursorSessions.length > 0;
336
+ const hasCodex = codexSessions.length > 0;
337
+ const hasCcc = cccSessions.length > 0;
338
338
 
339
339
  if (sessions.length === 0) {
340
340
  return JSON.stringify({
@@ -357,18 +357,18 @@ export function buildSessionsCard(sessions: Array<{
357
357
  const secs = s.elapsedSeconds % 60;
358
358
  extra = ` | 本轮: ${mins}分${secs}秒`;
359
359
  }
360
- const toolLabel = sessionToolLabel(s.tool);
360
+ const toolLabel = sessionToolLabel(s.tool);
361
361
  const namePart = s.chatName ? `**${s.chatName}** ` : "";
362
362
  const chatTag = !s.chatId ? " (chat id缺失)" : s.chatId.startsWith("oc_") ? " (群聊)" : "";
363
363
  return `**${i + 1}.** ${namePart}${chatTag} \`${shortId}\` ${status} | 工具: ${toolLabel} | 轮数: ${s.turnCount} | ${s.model}${extra}`;
364
364
  };
365
365
 
366
366
  const lines: string[] = [`共 **${sessions.length}** 个会话:`, ""];
367
- const idx = { value: 0 };
368
- if (hasClaudeCode) pushSessionGroup(lines, "Claude Code 会话", claudeCodeSessions, formatSession, idx);
369
- if (hasCursor) pushSessionGroup(lines, "Cursor 会话", cursorSessions, formatSession, idx);
370
- if (hasCodex) pushSessionGroup(lines, "Codex 会话", codexSessions, formatSession, idx);
371
- if (hasCcc) pushSessionGroup(lines, "CCC Agent 会话", cccSessions, formatSession, idx);
367
+ const idx = { value: 0 };
368
+ if (hasClaudeCode) pushSessionGroup(lines, "Claude Code 会话", claudeCodeSessions, formatSession, idx);
369
+ if (hasCursor) pushSessionGroup(lines, "Cursor 会话", cursorSessions, formatSession, idx);
370
+ if (hasCodex) pushSessionGroup(lines, "Codex 会话", codexSessions, formatSession, idx);
371
+ if (hasCcc) pushSessionGroup(lines, "CCC Agent 会话", cccSessions, formatSession, idx);
372
372
 
373
373
  return JSON.stringify({
374
374
  config: { wide_screen_mode: true },
@@ -433,11 +433,11 @@ export function buildQueueFullCard(): string {
433
433
  }
434
434
 
435
435
  // 状态卡片(带关闭按钮)
436
- export function buildStatusCard(statusText: string, template = "blue"): string {
437
- return JSON.stringify({
438
- config: { wide_screen_mode: true },
439
- header: { template, title: { content: "会话状态", tag: "plain_text" } },
440
- elements: [
436
+ export function buildStatusCard(statusText: string, template = "blue"): string {
437
+ return JSON.stringify({
438
+ config: { wide_screen_mode: true },
439
+ header: { template, title: { content: "会话状态", tag: "plain_text" } },
440
+ elements: [
441
441
  { tag: "div", text: { tag: "lark_md", content: statusText } },
442
442
  { tag: "hr" },
443
443
  {
@@ -449,82 +449,82 @@ export function buildStatusCard(statusText: string, template = "blue"): string {
449
449
  value: { action: "close" },
450
450
  }],
451
451
  },
452
- ],
453
- });
454
- }
455
-
456
- export function buildCodexUsageCard(content: string, resetCreditsAvailable: number | null): string {
457
- const elements: object[] = [
458
- { tag: "div", text: { tag: "lark_md", content } },
459
- ];
460
- if (resetCreditsAvailable !== null && resetCreditsAvailable > 0) {
461
- elements.push({ tag: "hr" });
462
- elements.push({
463
- tag: "action",
464
- actions: [{
465
- tag: "button",
466
- text: { tag: "plain_text", content: "发起重置" },
467
- type: "primary",
468
- value: { action: "codex_reset_request", availableCount: resetCreditsAvailable },
469
- }],
470
- });
471
- }
472
-
473
- return JSON.stringify({
474
- config: { wide_screen_mode: true },
475
- header: { template: "blue", title: { content: "Codex Usage", tag: "plain_text" } },
476
- elements,
477
- });
478
- }
479
-
480
- export function buildCodexResetConfirmCard(params: {
481
- availableCount: number;
482
- parentMessageId: string;
483
- requestId: string;
484
- }): string {
485
- const valueBase = {
486
- parentMessageId: params.parentMessageId,
487
- requestId: params.requestId,
488
- };
489
- return JSON.stringify({
490
- config: { wide_screen_mode: true },
491
- header: { template: "yellow", title: { content: "确认 Codex 主动重置", tag: "plain_text" } },
492
- elements: [
493
- {
494
- tag: "div",
495
- text: {
496
- tag: "lark_md",
497
- content: `当前可用主动重置次数:**${params.availableCount}**。\n\n确认后会消耗 1 次主动重置,并重置当前可重置的 Codex 用量窗口。`,
498
- },
499
- },
500
- { tag: "hr" },
501
- {
502
- tag: "action",
503
- actions: [
504
- {
505
- tag: "button",
506
- text: { tag: "plain_text", content: "是,发起重置" },
507
- type: "danger",
508
- value: { action: "codex_reset_confirm", decision: "yes", ...valueBase },
509
- },
510
- {
511
- tag: "button",
512
- text: { tag: "plain_text", content: "否" },
513
- type: "default",
514
- value: { action: "codex_reset_confirm", decision: "no", ...valueBase },
515
- },
516
- ],
517
- },
518
- ],
519
- });
520
- }
521
-
522
- /** 模型切换卡片(/model 命令,飞书专用,含切换按钮) */
523
- export function buildModelCard(
524
- currentModel: string,
525
- models: string[],
526
- tool?: string,
527
- ): string {
452
+ ],
453
+ });
454
+ }
455
+
456
+ export function buildCodexUsageCard(content: string, resetCreditsAvailable: number | null): string {
457
+ const elements: object[] = [
458
+ { tag: "div", text: { tag: "lark_md", content } },
459
+ ];
460
+ if (resetCreditsAvailable !== null && resetCreditsAvailable > 0) {
461
+ elements.push({ tag: "hr" });
462
+ elements.push({
463
+ tag: "action",
464
+ actions: [{
465
+ tag: "button",
466
+ text: { tag: "plain_text", content: "发起重置" },
467
+ type: "primary",
468
+ value: { action: "codex_reset_request", availableCount: resetCreditsAvailable },
469
+ }],
470
+ });
471
+ }
472
+
473
+ return JSON.stringify({
474
+ config: { wide_screen_mode: true },
475
+ header: { template: "blue", title: { content: "Codex Usage", tag: "plain_text" } },
476
+ elements,
477
+ });
478
+ }
479
+
480
+ export function buildCodexResetConfirmCard(params: {
481
+ availableCount: number;
482
+ parentMessageId: string;
483
+ requestId: string;
484
+ }): string {
485
+ const valueBase = {
486
+ parentMessageId: params.parentMessageId,
487
+ requestId: params.requestId,
488
+ };
489
+ return JSON.stringify({
490
+ config: { wide_screen_mode: true },
491
+ header: { template: "yellow", title: { content: "确认 Codex 主动重置", tag: "plain_text" } },
492
+ elements: [
493
+ {
494
+ tag: "div",
495
+ text: {
496
+ tag: "lark_md",
497
+ content: `当前可用主动重置次数:**${params.availableCount}**。\n\n确认后会消耗 1 次主动重置,并重置当前可重置的 Codex 用量窗口。`,
498
+ },
499
+ },
500
+ { tag: "hr" },
501
+ {
502
+ tag: "action",
503
+ actions: [
504
+ {
505
+ tag: "button",
506
+ text: { tag: "plain_text", content: "是,发起重置" },
507
+ type: "danger",
508
+ value: { action: "codex_reset_confirm", decision: "yes", ...valueBase },
509
+ },
510
+ {
511
+ tag: "button",
512
+ text: { tag: "plain_text", content: "否" },
513
+ type: "default",
514
+ value: { action: "codex_reset_confirm", decision: "no", ...valueBase },
515
+ },
516
+ ],
517
+ },
518
+ ],
519
+ });
520
+ }
521
+
522
+ /** 模型切换卡片(/model 命令,飞书专用,含切换按钮) */
523
+ export function buildModelCard(
524
+ currentModel: string,
525
+ models: string[],
526
+ tool?: string,
527
+ ): string {
528
528
  const toolLabel = tool ? ` (${tool})` : "";
529
529
  const currentLine = currentModel
530
530
  ? `**当前模型:** \`${currentModel}\``
@@ -558,48 +558,48 @@ export function buildModelCard(
558
558
  { tag: "div", text: { tag: "lark_md", content: lines.join("\n") } },
559
559
  { tag: "hr" },
560
560
  buildButtons(buttons),
561
- ],
562
- });
563
- }
564
-
565
- export function buildEffortCard(
566
- currentEffort: string,
567
- efforts: string[],
568
- tool?: string,
569
- ): string {
570
- const toolLabel = tool ? ` (${tool})` : "";
571
- const currentLine = currentEffort
572
- ? `**当前 effort:** \`${currentEffort}\``
573
- : "**当前 effort:** 未指定";
574
-
575
- const lines: string[] = [currentLine];
576
- if (efforts.length > 0) {
577
- lines.push("", "**可切换 effort**");
578
- for (const effort of efforts) {
579
- lines.push(`- \`${effort}\``);
580
- }
581
- lines.push("", "点击按钮切换 effort,或输入 `/effort clear` 恢复默认");
582
- } else {
583
- lines.push("", "当前 agent 不支持 effort 切换。");
584
- }
585
-
586
- const buttons: ButtonDef[] = [];
587
- for (const effort of efforts.slice(0, 20)) {
588
- buttons.push({
589
- text: `/effort ${effort}`,
590
- value: JSON.stringify({ cmd: `/effort ${effort}` }),
591
- type: "primary",
592
- });
593
- }
594
-
595
- return JSON.stringify({
596
- config: { wide_screen_mode: true },
597
- header: { template: "blue", title: { content: `Effort 切换${toolLabel}`, tag: "plain_text" } },
598
- elements: [
599
- { tag: "div", text: { tag: "lark_md", content: lines.join("\n") } },
600
- { tag: "hr" },
601
- buildButtons(buttons),
602
- ],
603
- });
604
- }
561
+ ],
562
+ });
563
+ }
564
+
565
+ export function buildEffortCard(
566
+ currentEffort: string,
567
+ efforts: string[],
568
+ tool?: string,
569
+ ): string {
570
+ const toolLabel = tool ? ` (${tool})` : "";
571
+ const currentLine = currentEffort
572
+ ? `**当前 effort:** \`${currentEffort}\``
573
+ : "**当前 effort:** 未指定";
574
+
575
+ const lines: string[] = [currentLine];
576
+ if (efforts.length > 0) {
577
+ lines.push("", "**可切换 effort**");
578
+ for (const effort of efforts) {
579
+ lines.push(`- \`${effort}\``);
580
+ }
581
+ lines.push("", "点击按钮切换 effort,或输入 `/effort clear` 恢复默认");
582
+ } else {
583
+ lines.push("", "当前 agent 不支持 effort 切换。");
584
+ }
585
+
586
+ const buttons: ButtonDef[] = [];
587
+ for (const effort of efforts.slice(0, 20)) {
588
+ buttons.push({
589
+ text: `/effort ${effort}`,
590
+ value: JSON.stringify({ cmd: `/effort ${effort}` }),
591
+ type: "primary",
592
+ });
593
+ }
594
+
595
+ return JSON.stringify({
596
+ config: { wide_screen_mode: true },
597
+ header: { template: "blue", title: { content: `Effort 切换${toolLabel}`, tag: "plain_text" } },
598
+ elements: [
599
+ { tag: "div", text: { tag: "lark_md", content: lines.join("\n") } },
600
+ { tag: "hr" },
601
+ buildButtons(buttons),
602
+ ],
603
+ });
604
+ }
605
605