chatccc 0.2.229 → 0.2.231

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 (120) hide show
  1. package/.agents/skills/create-chatccc-feishu-app/SKILL.md +85 -85
  2. package/.claude/skills/create-chatccc-feishu-app/SKILL.md +85 -85
  3. package/.cursor/skills/create-chatccc-feishu-app/SKILL.md +85 -85
  4. package/README.md +90 -90
  5. package/agent-prompts/claude_specific.md +45 -45
  6. package/agent-prompts/codex_specific.md +2 -2
  7. package/agent-prompts/cursor_specific.md +13 -13
  8. package/bin/cccagent.mjs +17 -17
  9. package/im-skills/feishu-skill/receive-send-file.md +63 -63
  10. package/im-skills/feishu-skill/receive-send-image.md +24 -24
  11. package/im-skills/feishu-skill/skill.md +3 -3
  12. package/im-skills/wechat-file-skill/receive-send-file.md +38 -38
  13. package/im-skills/wechat-file-skill/send-file.mjs +83 -83
  14. package/im-skills/wechat-file-skill/skill.md +10 -10
  15. package/im-skills/wechat-image-skill/skill.md +10 -10
  16. package/im-skills/wechat-video-skill/receive-send-video.md +38 -38
  17. package/im-skills/wechat-video-skill/send-video.mjs +79 -79
  18. package/im-skills/wechat-video-skill/skill.md +10 -10
  19. package/package.json +1 -1
  20. package/scripts/postinstall-sharp-check.mjs +58 -58
  21. package/src/__tests__/agent-activity.test.ts +86 -76
  22. package/src/__tests__/agent-delegate-task-rpc.test.ts +165 -165
  23. package/src/__tests__/agent-reload-config-rpc.test.ts +99 -99
  24. package/src/__tests__/builtin-chat-session.test.ts +65 -1
  25. package/src/__tests__/builtin-cli-json.test.ts +39 -39
  26. package/src/__tests__/builtin-config.test.ts +26 -26
  27. package/src/__tests__/builtin-context.test.ts +202 -163
  28. package/src/__tests__/builtin-permissions.test.ts +211 -211
  29. package/src/__tests__/builtin-session-select.test.ts +116 -116
  30. package/src/__tests__/builtin-sigint.test.ts +56 -56
  31. package/src/__tests__/builtin-web-tools.test.ts +220 -220
  32. package/src/__tests__/card-action-routing.test.ts +18 -18
  33. package/src/__tests__/cardkit.test.ts +60 -60
  34. package/src/__tests__/ccc-adapter.test.ts +170 -136
  35. package/src/__tests__/chatgpt-subscription-rpc.test.ts +89 -89
  36. package/src/__tests__/chatgpt-subscription.test.ts +135 -135
  37. package/src/__tests__/chrome-devtools-guard.test.ts +165 -165
  38. package/src/__tests__/claude-adapter.test.ts +614 -614
  39. package/src/__tests__/claude-raw-stream-log.test.ts +87 -87
  40. package/src/__tests__/codex-adapter.test.ts +58 -58
  41. package/src/__tests__/codex-raw-stream-log.test.ts +170 -170
  42. package/src/__tests__/codex-reset-actions.test.ts +146 -146
  43. package/src/__tests__/cursor-adapter.test.ts +268 -268
  44. package/src/__tests__/feishu-api.test.ts +60 -60
  45. package/src/__tests__/feishu-avatar.test.ts +164 -164
  46. package/src/__tests__/feishu-message-ingress.test.ts +138 -138
  47. package/src/__tests__/feishu-platform.test.ts +22 -22
  48. package/src/__tests__/format-message.test.ts +47 -47
  49. package/src/__tests__/jsonl-stream.test.ts +79 -79
  50. package/src/__tests__/package-files.test.ts +24 -24
  51. package/src/__tests__/privacy.test.ts +198 -198
  52. package/src/__tests__/progress-reducer.test.ts +121 -110
  53. package/src/__tests__/raw-stream-log.test.ts +106 -106
  54. package/src/__tests__/response-stall.test.ts +49 -49
  55. package/src/__tests__/session.test.ts +174 -16
  56. package/src/__tests__/shared-prefix.test.ts +36 -36
  57. package/src/__tests__/sim-platform.test.ts +16 -16
  58. package/src/__tests__/startup-lifecycle.test.ts +231 -231
  59. package/src/__tests__/stop-session.test.ts +34 -34
  60. package/src/__tests__/stream-state.test.ts +42 -42
  61. package/src/__tests__/terminal-error.test.ts +54 -0
  62. package/src/__tests__/terminal-renderer.test.ts +247 -247
  63. package/src/__tests__/update-command-guard.test.ts +144 -144
  64. package/src/__tests__/web-ui.test.ts +326 -326
  65. package/src/adapters/adapter-interface.ts +24 -18
  66. package/src/adapters/ccc-adapter.ts +141 -131
  67. package/src/adapters/claude-adapter.ts +620 -620
  68. package/src/adapters/claude-session-meta-store.ts +120 -120
  69. package/src/adapters/codex-adapter.ts +426 -426
  70. package/src/adapters/cursor-adapter.ts +681 -681
  71. package/src/adapters/jsonl-stream.ts +157 -157
  72. package/src/adapters/raw-stream-log.ts +124 -124
  73. package/src/adapters/resource-monitor.ts +140 -140
  74. package/src/agent-activity.ts +175 -170
  75. package/src/agent-delegate-task-rpc.ts +153 -153
  76. package/src/agent-delegate-task.ts +91 -91
  77. package/src/agent-reload-config-rpc.ts +34 -34
  78. package/src/agent-stop-stuck.ts +129 -129
  79. package/src/builtin/cli.ts +9 -2
  80. package/src/builtin/config.ts +84 -84
  81. package/src/builtin/context.ts +62 -4
  82. package/src/builtin/file-log.ts +38 -38
  83. package/src/builtin/index.ts +83 -19
  84. package/src/builtin/proc-tree-kill.ts +61 -61
  85. package/src/builtin/progress/cards-helpers.ts +76 -76
  86. package/src/builtin/progress/reducer.ts +113 -108
  87. package/src/builtin/progress/terminal-renderer.ts +294 -294
  88. package/src/builtin/progress/view.ts +77 -77
  89. package/src/builtin/raw-stream-log.ts +124 -124
  90. package/src/builtin/session-select.ts +48 -48
  91. package/src/builtin/sigint.ts +50 -50
  92. package/src/builtin/web-tools.ts +313 -313
  93. package/src/card-action-routing.ts +14 -14
  94. package/src/chatgpt-subscription-rpc.ts +27 -27
  95. package/src/chatgpt-subscription.ts +299 -299
  96. package/src/chrome-devtools-guard.ts +318 -318
  97. package/src/codex-reset-actions.ts +184 -184
  98. package/src/feishu-api.ts +193 -193
  99. package/src/feishu-message-ingress.ts +195 -195
  100. package/src/feishu-platform.ts +20 -20
  101. package/src/format-message.ts +293 -293
  102. package/src/litellm-proxy.ts +374 -374
  103. package/src/orchestrator.ts +8 -3
  104. package/src/platform-adapter.ts +6 -6
  105. package/src/privacy.ts +118 -118
  106. package/src/progress/reducer.ts +113 -108
  107. package/src/progress/terminal-renderer.ts +294 -294
  108. package/src/progress/view.ts +77 -77
  109. package/src/response-stall.ts +28 -28
  110. package/src/runtime-reload.ts +34 -34
  111. package/src/session-chat-binding.ts +82 -82
  112. package/src/session-name.ts +8 -8
  113. package/src/session.ts +149 -26
  114. package/src/shared-prefix.ts +29 -29
  115. package/src/sim-platform.ts +20 -20
  116. package/src/startup-lifecycle.ts +250 -250
  117. package/src/stream-state.ts +21 -18
  118. package/src/terminal-error.ts +129 -0
  119. package/src/turn-cards.ts +117 -117
  120. package/src/update-command-guard.ts +165 -165
package/src/feishu-api.ts CHANGED
@@ -11,10 +11,10 @@ import {
11
11
  CHAT_LOGS_DIR,
12
12
  PROJECT_ROOT,
13
13
  USER_DATA_DIR,
14
- CLAUDE_SESSION_PREFIX,
15
- CURSOR_SESSION_PREFIX,
16
- CODEX_SESSION_PREFIX,
17
- CCC_SESSION_PREFIX,
14
+ CLAUDE_SESSION_PREFIX,
15
+ CURSOR_SESSION_PREFIX,
16
+ CODEX_SESSION_PREFIX,
17
+ CCC_SESSION_PREFIX,
18
18
  ts,
19
19
  resolveDefaultAgentTool,
20
20
  toolDisplayName,
@@ -277,16 +277,16 @@ export async function disbandChat(
277
277
 
278
278
  export function extractSessionInfo(description: string): { sessionId: string; tool: string } | null {
279
279
  const PREFIXES: Array<{ prefix: string; tool: string }> = [
280
- { prefix: CLAUDE_SESSION_PREFIX, tool: "claude" },
281
- { prefix: CURSOR_SESSION_PREFIX, tool: "cursor" },
282
- { prefix: CODEX_SESSION_PREFIX, tool: "codex" },
283
- { prefix: CCC_SESSION_PREFIX, tool: "ccc" },
284
- ];
285
- for (const { prefix, tool } of PREFIXES) {
286
- const idx = description.indexOf(prefix);
287
- if (idx === -1) continue;
288
- const after = description.slice(idx + prefix.length).trim();
289
- const match = after.match(/^([a-zA-Z0-9_.-]+)/);
280
+ { prefix: CLAUDE_SESSION_PREFIX, tool: "claude" },
281
+ { prefix: CURSOR_SESSION_PREFIX, tool: "cursor" },
282
+ { prefix: CODEX_SESSION_PREFIX, tool: "codex" },
283
+ { prefix: CCC_SESSION_PREFIX, tool: "ccc" },
284
+ ];
285
+ for (const { prefix, tool } of PREFIXES) {
286
+ const idx = description.indexOf(prefix);
287
+ if (idx === -1) continue;
288
+ const after = description.slice(idx + prefix.length).trim();
289
+ const match = after.match(/^([a-zA-Z0-9_.-]+)/);
290
290
  if (match) return { sessionId: match[1], tool };
291
291
  }
292
292
  return null;
@@ -319,30 +319,30 @@ const AVATAR_BADGES: Record<string, string> = {
319
319
  cursor: resolvePath(AVATAR_BADGE_DIR, "badge_cursor.png"),
320
320
  codex: resolvePath(AVATAR_BADGE_DIR, "badge_codex.png"),
321
321
  };
322
- const AVATAR_SIZE = 256;
323
- const AVATAR_BADGE_SIZE = 92;
324
- const AVATAR_BADGE_MARGIN = 10;
325
- const PLAIN_AVATAR_TOOL = "plain";
326
- const CODEX_AVATAR_USAGE_STYLE_VERSION = "usage-window-aware-v14";
327
- const CODEX_FAST_FRAME_STYLE_VERSION = "fast-champagne-frame-v2";
328
- const CURSOR_AVATAR_USAGE_STYLE_VERSION = "usage-battery-v1";
329
-
330
- export interface CodexUsageBalance {
331
- usedPercent: number;
332
- remainingPercent: number;
333
- resetAtEpochSeconds: number | null;
334
- resetAfterSeconds: number | null;
335
- limitWindowSeconds?: number | null;
336
- }
322
+ const AVATAR_SIZE = 256;
323
+ const AVATAR_BADGE_SIZE = 92;
324
+ const AVATAR_BADGE_MARGIN = 10;
325
+ const PLAIN_AVATAR_TOOL = "plain";
326
+ const CODEX_AVATAR_USAGE_STYLE_VERSION = "usage-window-aware-v14";
327
+ const CODEX_FAST_FRAME_STYLE_VERSION = "fast-champagne-frame-v2";
328
+ const CURSOR_AVATAR_USAGE_STYLE_VERSION = "usage-battery-v1";
329
+
330
+ export interface CodexUsageBalance {
331
+ usedPercent: number;
332
+ remainingPercent: number;
333
+ resetAtEpochSeconds: number | null;
334
+ resetAfterSeconds: number | null;
335
+ limitWindowSeconds?: number | null;
336
+ }
337
337
 
338
338
  export interface CodexRateLimitResetCredit {
339
339
  grantedAt: string | null;
340
340
  expiresAt: string;
341
341
  }
342
342
 
343
- export interface CodexUsageSummary {
344
- fiveHour: CodexUsageBalance | null;
345
- weekly: CodexUsageBalance | null;
343
+ export interface CodexUsageSummary {
344
+ fiveHour: CodexUsageBalance | null;
345
+ weekly: CodexUsageBalance | null;
346
346
  rateLimitResetCreditsAvailable: number | null;
347
347
  rateLimitResetCredits: CodexRateLimitResetCredit[] | null;
348
348
  }
@@ -357,35 +357,35 @@ export interface CodexResetConsumeResult {
357
357
  const avatarKeyCache = new Map<string, string>();
358
358
  let avatarKeyCacheLoaded = false;
359
359
 
360
- function normalizeAvatarTool(tool: string): string {
361
- return AVATAR_BADGES[tool] ? tool : PLAIN_AVATAR_TOOL;
362
- }
363
-
364
- const FIVE_HOUR_WINDOW_SECONDS = 5 * 60 * 60;
365
- const SEVEN_DAY_WINDOW_SECONDS = 7 * 24 * 60 * 60;
360
+ function normalizeAvatarTool(tool: string): string {
361
+ return AVATAR_BADGES[tool] ? tool : PLAIN_AVATAR_TOOL;
362
+ }
363
+
364
+ const FIVE_HOUR_WINDOW_SECONDS = 5 * 60 * 60;
365
+ const SEVEN_DAY_WINDOW_SECONDS = 7 * 24 * 60 * 60;
366
366
 
367
367
  function normalizeAvatarStatus(status: string): string {
368
368
  return AVATAR_SOURCES[status] ? status : "idle";
369
369
  }
370
370
 
371
- function avatarCombinationPath(tool: string, status: string): string {
372
- return resolvePath(AVATAR_COMBINATIONS_DIR, `avatar_${normalizeAvatarTool(tool)}_${normalizeAvatarStatus(status)}.png`);
373
- }
374
-
375
- function avatarCacheKey(
376
- tool: string,
377
- status: string,
378
- codexUsage: CodexUsageSummary | null = null,
379
- cursorBatteryPercent: number | null = null,
380
- fastMode = false,
381
- ): string {
382
- const normalizedTool = normalizeAvatarTool(tool);
383
- const normalizedStatus = normalizeAvatarStatus(status);
384
- if (normalizedTool === "codex") {
385
- const fastKey = fastMode ? `:${CODEX_FAST_FRAME_STYLE_VERSION}` : "";
386
- if (!codexUsage?.weekly) return `${normalizedTool}:${normalizedStatus}:plain${fastKey}`;
387
- const ringKey = codexUsage.fiveHour ? `:5h-ring:${codexUsage.fiveHour.remainingPercent}` : "";
388
- return `${normalizedTool}:${normalizedStatus}:${CODEX_AVATAR_USAGE_STYLE_VERSION}:7d-battery:${codexUsage.weekly.remainingPercent}${ringKey}${fastKey}`;
371
+ function avatarCombinationPath(tool: string, status: string): string {
372
+ return resolvePath(AVATAR_COMBINATIONS_DIR, `avatar_${normalizeAvatarTool(tool)}_${normalizeAvatarStatus(status)}.png`);
373
+ }
374
+
375
+ function avatarCacheKey(
376
+ tool: string,
377
+ status: string,
378
+ codexUsage: CodexUsageSummary | null = null,
379
+ cursorBatteryPercent: number | null = null,
380
+ fastMode = false,
381
+ ): string {
382
+ const normalizedTool = normalizeAvatarTool(tool);
383
+ const normalizedStatus = normalizeAvatarStatus(status);
384
+ if (normalizedTool === "codex") {
385
+ const fastKey = fastMode ? `:${CODEX_FAST_FRAME_STYLE_VERSION}` : "";
386
+ if (!codexUsage?.weekly) return `${normalizedTool}:${normalizedStatus}:plain${fastKey}`;
387
+ const ringKey = codexUsage.fiveHour ? `:5h-ring:${codexUsage.fiveHour.remainingPercent}` : "";
388
+ return `${normalizedTool}:${normalizedStatus}:${CODEX_AVATAR_USAGE_STYLE_VERSION}:7d-battery:${codexUsage.weekly.remainingPercent}${ringKey}${fastKey}`;
389
389
  }
390
390
  if (normalizedTool === "cursor") {
391
391
  return cursorBatteryPercent !== null
@@ -423,30 +423,30 @@ function clampPercent(value: number): number {
423
423
  return Math.max(0, Math.min(100, Math.round(value)));
424
424
  }
425
425
 
426
- function usageBalanceFromWindow(raw: Record<string, unknown>, fieldName: string): CodexUsageBalance {
426
+ function usageBalanceFromWindow(raw: Record<string, unknown>, fieldName: string): CodexUsageBalance {
427
427
  const value = raw.used_percent;
428
428
  const usedPercent = Number(value);
429
429
  if (!Number.isFinite(usedPercent)) throw new Error(`missing ${fieldName}.used_percent`);
430
- const used = clampPercent(usedPercent);
431
- const resetAt = Number(raw.reset_at);
432
- const resetAfter = Number(raw.reset_after_seconds);
433
- const rawLimitWindow = raw.limit_window_seconds;
434
- const limitWindow = rawLimitWindow === null || rawLimitWindow === undefined || rawLimitWindow === ""
435
- ? Number.NaN
436
- : Number(rawLimitWindow);
437
- const balance: CodexUsageBalance = {
438
- usedPercent: used,
439
- remainingPercent: clampPercent(100 - used),
440
- resetAtEpochSeconds: Number.isFinite(resetAt) ? resetAt : null,
441
- resetAfterSeconds: Number.isFinite(resetAfter) ? resetAfter : null,
442
- };
443
- if (Number.isFinite(limitWindow)) balance.limitWindowSeconds = limitWindow;
444
- return balance;
445
- }
446
-
447
- function isUsageWindowDuration(balance: CodexUsageBalance | null, seconds: number): boolean {
448
- return balance?.limitWindowSeconds === seconds;
449
- }
430
+ const used = clampPercent(usedPercent);
431
+ const resetAt = Number(raw.reset_at);
432
+ const resetAfter = Number(raw.reset_after_seconds);
433
+ const rawLimitWindow = raw.limit_window_seconds;
434
+ const limitWindow = rawLimitWindow === null || rawLimitWindow === undefined || rawLimitWindow === ""
435
+ ? Number.NaN
436
+ : Number(rawLimitWindow);
437
+ const balance: CodexUsageBalance = {
438
+ usedPercent: used,
439
+ remainingPercent: clampPercent(100 - used),
440
+ resetAtEpochSeconds: Number.isFinite(resetAt) ? resetAt : null,
441
+ resetAfterSeconds: Number.isFinite(resetAfter) ? resetAfter : null,
442
+ };
443
+ if (Number.isFinite(limitWindow)) balance.limitWindowSeconds = limitWindow;
444
+ return balance;
445
+ }
446
+
447
+ function isUsageWindowDuration(balance: CodexUsageBalance | null, seconds: number): boolean {
448
+ return balance?.limitWindowSeconds === seconds;
449
+ }
450
450
 
451
451
  function parseOptionalUsageWindow(rateLimit: Record<string, unknown>, keys: string[]): CodexUsageBalance | null {
452
452
  for (const key of keys) {
@@ -558,25 +558,25 @@ export async function getCodexUsageSummary(): Promise<CodexUsageSummary> {
558
558
  const rateLimit = data.rate_limit;
559
559
  if (!rateLimit || typeof rateLimit !== "object") throw new Error("missing rate_limit");
560
560
 
561
- const primaryWindow = parseOptionalUsageWindow(rateLimit, ["primary_window"]);
562
- const secondaryWindow = parseOptionalUsageWindow(rateLimit, [
563
- "secondary_window",
564
- "weekly_window",
565
- "week_window",
566
- "long_window",
567
- ]);
568
- if (!primaryWindow && !secondaryWindow) throw new Error("missing supported rate_limit usage window");
569
-
570
- const windows = [primaryWindow, secondaryWindow];
571
- const fiveHour = windows.find((window) => isUsageWindowDuration(window, FIVE_HOUR_WINDOW_SECONDS))
572
- ?? (primaryWindow?.limitWindowSeconds === undefined ? primaryWindow : null);
573
- const weekly = windows.find((window) => isUsageWindowDuration(window, SEVEN_DAY_WINDOW_SECONDS))
574
- ?? (secondaryWindow?.limitWindowSeconds === undefined ? secondaryWindow : null);
575
- const resetCredits = await resetCreditsPromise;
576
-
577
- return {
578
- fiveHour,
579
- weekly,
561
+ const primaryWindow = parseOptionalUsageWindow(rateLimit, ["primary_window"]);
562
+ const secondaryWindow = parseOptionalUsageWindow(rateLimit, [
563
+ "secondary_window",
564
+ "weekly_window",
565
+ "week_window",
566
+ "long_window",
567
+ ]);
568
+ if (!primaryWindow && !secondaryWindow) throw new Error("missing supported rate_limit usage window");
569
+
570
+ const windows = [primaryWindow, secondaryWindow];
571
+ const fiveHour = windows.find((window) => isUsageWindowDuration(window, FIVE_HOUR_WINDOW_SECONDS))
572
+ ?? (primaryWindow?.limitWindowSeconds === undefined ? primaryWindow : null);
573
+ const weekly = windows.find((window) => isUsageWindowDuration(window, SEVEN_DAY_WINDOW_SECONDS))
574
+ ?? (secondaryWindow?.limitWindowSeconds === undefined ? secondaryWindow : null);
575
+ const resetCredits = await resetCreditsPromise;
576
+
577
+ return {
578
+ fiveHour,
579
+ weekly,
580
580
  rateLimitResetCreditsAvailable: resetCredits?.availableCount ?? parseRateLimitResetCredits(data),
581
581
  rateLimitResetCredits: resetCredits?.availableCredits ?? null,
582
582
  };
@@ -760,10 +760,10 @@ function buildCodexUsageRingSvg(remainingPercent: number): Buffer {
760
760
  </svg>`);
761
761
  }
762
762
 
763
- async function buildAgentBadgeOverlay(
764
- tool: string,
765
- margin = AVATAR_BADGE_MARGIN,
766
- ): Promise<sharp.OverlayOptions> {
763
+ async function buildAgentBadgeOverlay(
764
+ tool: string,
765
+ margin = AVATAR_BADGE_MARGIN,
766
+ ): Promise<sharp.OverlayOptions> {
767
767
  const badge = await sharp(AVATAR_BADGES[tool])
768
768
  .resize(AVATAR_BADGE_SIZE, AVATAR_BADGE_SIZE, {
769
769
  fit: "contain",
@@ -774,70 +774,70 @@ async function buildAgentBadgeOverlay(
774
774
  .toBuffer();
775
775
  return {
776
776
  input: badge,
777
- left: AVATAR_SIZE - AVATAR_BADGE_SIZE - margin,
778
- top: AVATAR_SIZE - AVATAR_BADGE_SIZE - margin,
779
- };
780
- }
781
-
782
- function buildCodexFastFrameOverlay(): sharp.OverlayOptions {
783
- const size = AVATAR_BADGE_SIZE + 20;
784
- const innerOffset = 9;
785
- const innerSize = size - innerOffset * 2;
786
- const frame = Buffer.from(`
787
- <svg width="${size}" height="${size}" xmlns="http://www.w3.org/2000/svg">
788
- <defs>
789
- <linearGradient id="champagne" x1="0" y1="0" x2="${size}" y2="${size}" gradientUnits="userSpaceOnUse">
790
- <stop offset="0" stop-color="#FFF0BE"/>
791
- <stop offset="0.55" stop-color="#FFD56A"/>
792
- <stop offset="1" stop-color="#EFA923"/>
793
- </linearGradient>
794
- </defs>
795
- <rect x="0.5" y="0.5" width="${size - 1}" height="${size - 1}" rx="25" fill="url(#champagne)"/>
796
- <rect x="${innerOffset}" y="${innerOffset}" width="${innerSize}" height="${innerSize}" rx="21" fill="#fffdf4"/>
797
- </svg>`);
798
- return {
799
- input: frame,
800
- left: AVATAR_SIZE - size - 1,
801
- top: AVATAR_SIZE - size - 1,
802
- };
803
- }
804
-
805
- async function renderAvatar(
806
- tool: string,
807
- status: string,
808
- codexUsage: CodexUsageSummary | null = null,
809
- cursorBatteryPercent: number | null = null,
810
- fastMode = false,
811
- ): Promise<{ buffer: Buffer; contentType: string; filename: string }> {
812
- const normalizedTool = normalizeAvatarTool(tool);
813
- const normalizedStatus = normalizeAvatarStatus(status);
814
- const composites: sharp.OverlayOptions[] = [];
815
- const hasAgentBadge = normalizedTool !== PLAIN_AVATAR_TOOL;
816
- const useFastCodexAvatar = normalizedTool === "codex" && fastMode;
817
-
818
- const codexWeeklyUsage = normalizedTool === "codex" ? codexUsage?.weekly ?? null : null;
819
- const useDynamicCodexAvatar = normalizedTool === "codex" && codexUsage !== null && codexWeeklyUsage !== null;
820
- const useDynamicCursorAvatar = normalizedTool === "cursor" && cursorBatteryPercent !== null;
821
- const useDynamicBadgeAvatar = useDynamicCodexAvatar || useDynamicCursorAvatar || useFastCodexAvatar;
822
- const basePath = useDynamicBadgeAvatar
823
- ? AVATAR_SOURCES[normalizedStatus]
824
- : hasAgentBadge
825
- ? avatarCombinationPath(normalizedTool, normalizedStatus)
826
- : AVATAR_SOURCES[normalizedStatus];
827
-
828
- if (useDynamicCodexAvatar) {
829
- if (codexUsage.fiveHour) {
830
- composites.push({ input: buildCodexUsageRingSvg(codexUsage.fiveHour.remainingPercent), left: 0, top: 0 });
831
- }
832
- composites.push({ input: buildCodexUsageBatterySvg(codexWeeklyUsage.remainingPercent), left: 0, top: 0 });
833
- } else if (useDynamicCursorAvatar) {
834
- composites.push({ input: buildCodexUsageBatterySvg(cursorBatteryPercent), left: 0, top: 0 });
835
- }
836
-
837
- if (useDynamicBadgeAvatar) {
838
- if (useFastCodexAvatar) composites.push(buildCodexFastFrameOverlay());
839
- composites.push(await buildAgentBadgeOverlay(normalizedTool, useFastCodexAvatar ? 11 : AVATAR_BADGE_MARGIN));
840
- }
777
+ left: AVATAR_SIZE - AVATAR_BADGE_SIZE - margin,
778
+ top: AVATAR_SIZE - AVATAR_BADGE_SIZE - margin,
779
+ };
780
+ }
781
+
782
+ function buildCodexFastFrameOverlay(): sharp.OverlayOptions {
783
+ const size = AVATAR_BADGE_SIZE + 20;
784
+ const innerOffset = 9;
785
+ const innerSize = size - innerOffset * 2;
786
+ const frame = Buffer.from(`
787
+ <svg width="${size}" height="${size}" xmlns="http://www.w3.org/2000/svg">
788
+ <defs>
789
+ <linearGradient id="champagne" x1="0" y1="0" x2="${size}" y2="${size}" gradientUnits="userSpaceOnUse">
790
+ <stop offset="0" stop-color="#FFF0BE"/>
791
+ <stop offset="0.55" stop-color="#FFD56A"/>
792
+ <stop offset="1" stop-color="#EFA923"/>
793
+ </linearGradient>
794
+ </defs>
795
+ <rect x="0.5" y="0.5" width="${size - 1}" height="${size - 1}" rx="25" fill="url(#champagne)"/>
796
+ <rect x="${innerOffset}" y="${innerOffset}" width="${innerSize}" height="${innerSize}" rx="21" fill="#fffdf4"/>
797
+ </svg>`);
798
+ return {
799
+ input: frame,
800
+ left: AVATAR_SIZE - size - 1,
801
+ top: AVATAR_SIZE - size - 1,
802
+ };
803
+ }
804
+
805
+ async function renderAvatar(
806
+ tool: string,
807
+ status: string,
808
+ codexUsage: CodexUsageSummary | null = null,
809
+ cursorBatteryPercent: number | null = null,
810
+ fastMode = false,
811
+ ): Promise<{ buffer: Buffer; contentType: string; filename: string }> {
812
+ const normalizedTool = normalizeAvatarTool(tool);
813
+ const normalizedStatus = normalizeAvatarStatus(status);
814
+ const composites: sharp.OverlayOptions[] = [];
815
+ const hasAgentBadge = normalizedTool !== PLAIN_AVATAR_TOOL;
816
+ const useFastCodexAvatar = normalizedTool === "codex" && fastMode;
817
+
818
+ const codexWeeklyUsage = normalizedTool === "codex" ? codexUsage?.weekly ?? null : null;
819
+ const useDynamicCodexAvatar = normalizedTool === "codex" && codexUsage !== null && codexWeeklyUsage !== null;
820
+ const useDynamicCursorAvatar = normalizedTool === "cursor" && cursorBatteryPercent !== null;
821
+ const useDynamicBadgeAvatar = useDynamicCodexAvatar || useDynamicCursorAvatar || useFastCodexAvatar;
822
+ const basePath = useDynamicBadgeAvatar
823
+ ? AVATAR_SOURCES[normalizedStatus]
824
+ : hasAgentBadge
825
+ ? avatarCombinationPath(normalizedTool, normalizedStatus)
826
+ : AVATAR_SOURCES[normalizedStatus];
827
+
828
+ if (useDynamicCodexAvatar) {
829
+ if (codexUsage.fiveHour) {
830
+ composites.push({ input: buildCodexUsageRingSvg(codexUsage.fiveHour.remainingPercent), left: 0, top: 0 });
831
+ }
832
+ composites.push({ input: buildCodexUsageBatterySvg(codexWeeklyUsage.remainingPercent), left: 0, top: 0 });
833
+ } else if (useDynamicCursorAvatar) {
834
+ composites.push({ input: buildCodexUsageBatterySvg(cursorBatteryPercent), left: 0, top: 0 });
835
+ }
836
+
837
+ if (useDynamicBadgeAvatar) {
838
+ if (useFastCodexAvatar) composites.push(buildCodexFastFrameOverlay());
839
+ composites.push(await buildAgentBadgeOverlay(normalizedTool, useFastCodexAvatar ? 11 : AVATAR_BADGE_MARGIN));
840
+ }
841
841
 
842
842
  let pipeline = sharp(await readFile(basePath))
843
843
  .resize(AVATAR_SIZE, AVATAR_SIZE, { fit: "cover", position: "center" });
@@ -851,26 +851,26 @@ async function renderAvatar(
851
851
  .jpeg({ quality: 95, progressive: false })
852
852
  .toBuffer();
853
853
 
854
- return {
855
- buffer: jpeg,
856
- contentType: "image/jpeg",
857
- filename: normalizedTool === "codex" && codexUsage?.weekly
858
- ? `avatar_${normalizedTool}_${normalizedStatus}${useFastCodexAvatar ? "_fast" : ""}_7d_${codexUsage.weekly.remainingPercent}${codexUsage.fiveHour ? `_5h_${codexUsage.fiveHour.remainingPercent}` : ""}.jpg`
859
- : normalizedTool === "cursor" && cursorBatteryPercent !== null
860
- ? `avatar_${normalizedTool}_${normalizedStatus}_battery_${cursorBatteryPercent}.jpg`
861
- : `avatar_${normalizedTool}_${normalizedStatus}${useFastCodexAvatar ? "_fast" : ""}.jpg`,
862
- };
863
- }
864
-
865
- async function uploadImage(
866
- token: string,
867
- tool: string,
868
- status: string,
869
- codexUsage: CodexUsageSummary | null = null,
870
- cursorBatteryPercent: number | null = null,
871
- fastMode = false,
872
- ): Promise<string> {
873
- const image = await renderAvatar(tool, status, codexUsage, cursorBatteryPercent, fastMode);
854
+ return {
855
+ buffer: jpeg,
856
+ contentType: "image/jpeg",
857
+ filename: normalizedTool === "codex" && codexUsage?.weekly
858
+ ? `avatar_${normalizedTool}_${normalizedStatus}${useFastCodexAvatar ? "_fast" : ""}_7d_${codexUsage.weekly.remainingPercent}${codexUsage.fiveHour ? `_5h_${codexUsage.fiveHour.remainingPercent}` : ""}.jpg`
859
+ : normalizedTool === "cursor" && cursorBatteryPercent !== null
860
+ ? `avatar_${normalizedTool}_${normalizedStatus}_battery_${cursorBatteryPercent}.jpg`
861
+ : `avatar_${normalizedTool}_${normalizedStatus}${useFastCodexAvatar ? "_fast" : ""}.jpg`,
862
+ };
863
+ }
864
+
865
+ async function uploadImage(
866
+ token: string,
867
+ tool: string,
868
+ status: string,
869
+ codexUsage: CodexUsageSummary | null = null,
870
+ cursorBatteryPercent: number | null = null,
871
+ fastMode = false,
872
+ ): Promise<string> {
873
+ const image = await renderAvatar(tool, status, codexUsage, cursorBatteryPercent, fastMode);
874
874
  const blob = new Blob([new Uint8Array(image.buffer)], { type: image.contentType });
875
875
  const form = new FormData();
876
876
  form.append("image_type", "avatar");
@@ -901,14 +901,14 @@ async function getOrUploadAvatarKey(
901
901
  const normalizedTool = normalizeAvatarTool(tool);
902
902
  const normalizedStatus = normalizeAvatarStatus(status);
903
903
  const codexUsage = normalizedTool === "codex" ? await resolveCodexAvatarUsage(usageHints.codexUsage) : null;
904
- const cursorBatteryPercent = normalizedTool === "cursor"
905
- ? await resolveCursorAvatarBatteryPercent(usageHints.cursorUsage)
906
- : null;
907
- const fastMode = normalizedTool === "codex" && usageHints.fastMode === true;
908
- const keyName = avatarCacheKey(normalizedTool, normalizedStatus, codexUsage, cursorBatteryPercent, fastMode);
909
- const cached = avatarKeyCache.get(keyName);
910
- if (cached) return cached;
911
- const key = await uploadImage(token, normalizedTool, normalizedStatus, codexUsage, cursorBatteryPercent, fastMode);
904
+ const cursorBatteryPercent = normalizedTool === "cursor"
905
+ ? await resolveCursorAvatarBatteryPercent(usageHints.cursorUsage)
906
+ : null;
907
+ const fastMode = normalizedTool === "codex" && usageHints.fastMode === true;
908
+ const keyName = avatarCacheKey(normalizedTool, normalizedStatus, codexUsage, cursorBatteryPercent, fastMode);
909
+ const cached = avatarKeyCache.get(keyName);
910
+ if (cached) return cached;
911
+ const key = await uploadImage(token, normalizedTool, normalizedStatus, codexUsage, cursorBatteryPercent, fastMode);
912
912
  avatarKeyCache.set(keyName, key);
913
913
  await persistAvatarKeyCache().catch((err) => {
914
914
  console.error(`[${ts()}] [AVATAR] persist cache FAIL: ${(err as Error).message}`);
@@ -1056,7 +1056,7 @@ export function formatDelayNotice(createTimeMs: number, messageText?: string, no
1056
1056
  }
1057
1057
 
1058
1058
  const contentLine = messageText ? `\n> 原始内容:${messageText.slice(0, 200)}` : "";
1059
- return `> ⚠️ 延迟送达提醒:此消息于 ${sendTimeStr} 发送,现延迟约 ${delayStr}后送达${contentLine}`;
1059
+ return `> ⚠️ 延迟送达提醒:此消息于 ${sendTimeStr} 发送,现延迟约 ${delayStr}后送达${contentLine}`;
1060
1060
  }
1061
1061
 
1062
1062
  /**