chatccc 0.2.231 → 0.2.233

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 (130) 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 -86
  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 +449 -393
  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 +319 -202
  28. package/src/__tests__/builtin-file-tools.test.ts +240 -240
  29. package/src/__tests__/builtin-permissions.test.ts +211 -211
  30. package/src/__tests__/builtin-session-search.test.ts +228 -0
  31. package/src/__tests__/builtin-session-select.test.ts +116 -116
  32. package/src/__tests__/builtin-sigint.test.ts +56 -56
  33. package/src/__tests__/builtin-skills.test.ts +284 -284
  34. package/src/__tests__/builtin-web-tools.test.ts +220 -220
  35. package/src/__tests__/card-action-routing.test.ts +18 -18
  36. package/src/__tests__/cardkit.test.ts +60 -60
  37. package/src/__tests__/ccc-adapter.test.ts +170 -170
  38. package/src/__tests__/chatgpt-subscription-rpc.test.ts +89 -89
  39. package/src/__tests__/chatgpt-subscription.test.ts +135 -135
  40. package/src/__tests__/chrome-devtools-guard.test.ts +165 -165
  41. package/src/__tests__/claude-adapter.test.ts +614 -614
  42. package/src/__tests__/claude-raw-stream-log.test.ts +87 -87
  43. package/src/__tests__/codex-adapter.test.ts +58 -58
  44. package/src/__tests__/codex-raw-stream-log.test.ts +170 -170
  45. package/src/__tests__/codex-reset-actions.test.ts +146 -146
  46. package/src/__tests__/crash-logging.test.ts +95 -1
  47. package/src/__tests__/cursor-adapter.test.ts +268 -268
  48. package/src/__tests__/feishu-api.test.ts +60 -60
  49. package/src/__tests__/feishu-avatar.test.ts +164 -164
  50. package/src/__tests__/feishu-message-ingress.test.ts +138 -138
  51. package/src/__tests__/feishu-platform.test.ts +22 -22
  52. package/src/__tests__/format-message.test.ts +47 -47
  53. package/src/__tests__/jsonl-stream.test.ts +79 -79
  54. package/src/__tests__/package-files.test.ts +24 -24
  55. package/src/__tests__/privacy.test.ts +198 -198
  56. package/src/__tests__/progress-reducer.test.ts +121 -121
  57. package/src/__tests__/raw-stream-log.test.ts +106 -106
  58. package/src/__tests__/response-stall.test.ts +49 -49
  59. package/src/__tests__/restart.test.ts +55 -10
  60. package/src/__tests__/session.test.ts +199 -199
  61. package/src/__tests__/shared-prefix.test.ts +36 -36
  62. package/src/__tests__/sim-platform.test.ts +16 -16
  63. package/src/__tests__/startup-lifecycle.test.ts +231 -231
  64. package/src/__tests__/stop-session.test.ts +34 -34
  65. package/src/__tests__/stream-state.test.ts +42 -42
  66. package/src/__tests__/terminal-error.test.ts +54 -54
  67. package/src/__tests__/terminal-renderer.test.ts +247 -247
  68. package/src/__tests__/update-command-guard.test.ts +144 -144
  69. package/src/__tests__/web-ui.test.ts +326 -326
  70. package/src/adapters/adapter-interface.ts +33 -33
  71. package/src/adapters/ccc-adapter.ts +141 -141
  72. package/src/adapters/claude-adapter.ts +620 -620
  73. package/src/adapters/claude-session-meta-store.ts +120 -120
  74. package/src/adapters/codex-adapter.ts +426 -426
  75. package/src/adapters/cursor-adapter.ts +681 -681
  76. package/src/adapters/jsonl-stream.ts +157 -157
  77. package/src/adapters/raw-stream-log.ts +124 -124
  78. package/src/adapters/resource-monitor.ts +140 -140
  79. package/src/agent-activity.ts +175 -175
  80. package/src/agent-delegate-task-rpc.ts +153 -153
  81. package/src/agent-delegate-task.ts +91 -91
  82. package/src/agent-reload-config-rpc.ts +34 -34
  83. package/src/agent-stop-stuck.ts +129 -129
  84. package/src/builtin/cli.ts +6 -0
  85. package/src/builtin/config.ts +84 -84
  86. package/src/builtin/context.ts +465 -391
  87. package/src/builtin/file-log.ts +38 -38
  88. package/src/builtin/file-tools.ts +34 -0
  89. package/src/builtin/index.ts +24 -23
  90. package/src/builtin/proc-tree-kill.ts +61 -61
  91. package/src/builtin/progress/cards-helpers.ts +76 -76
  92. package/src/builtin/progress/reducer.ts +113 -113
  93. package/src/builtin/progress/terminal-renderer.ts +294 -294
  94. package/src/builtin/progress/view.ts +77 -77
  95. package/src/builtin/raw-stream-log.ts +124 -124
  96. package/src/builtin/session-search.ts +363 -0
  97. package/src/builtin/session-select.ts +48 -48
  98. package/src/builtin/sigint.ts +50 -50
  99. package/src/builtin/skills.ts +205 -205
  100. package/src/builtin/web-tools.ts +313 -313
  101. package/src/card-action-routing.ts +14 -14
  102. package/src/chatgpt-subscription-rpc.ts +27 -27
  103. package/src/chatgpt-subscription.ts +299 -299
  104. package/src/chrome-devtools-guard.ts +318 -318
  105. package/src/codex-reset-actions.ts +184 -184
  106. package/src/feishu-api.ts +193 -193
  107. package/src/feishu-message-ingress.ts +195 -195
  108. package/src/feishu-platform.ts +20 -20
  109. package/src/format-message.ts +293 -293
  110. package/src/index.ts +6 -1
  111. package/src/litellm-proxy.ts +374 -374
  112. package/src/orchestrator.ts +36 -13
  113. package/src/platform-adapter.ts +6 -6
  114. package/src/privacy.ts +118 -118
  115. package/src/progress/reducer.ts +113 -113
  116. package/src/progress/terminal-renderer.ts +294 -294
  117. package/src/progress/view.ts +77 -77
  118. package/src/response-stall.ts +28 -28
  119. package/src/runtime-reload.ts +34 -34
  120. package/src/session-chat-binding.ts +82 -82
  121. package/src/session-name.ts +8 -8
  122. package/src/session.ts +201 -201
  123. package/src/shared-prefix.ts +29 -29
  124. package/src/shared.ts +44 -0
  125. package/src/sim-platform.ts +20 -20
  126. package/src/startup-lifecycle.ts +250 -250
  127. package/src/stream-state.ts +21 -21
  128. package/src/terminal-error.ts +129 -129
  129. package/src/turn-cards.ts +117 -117
  130. 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
  /**