chatccc 0.2.193 → 0.2.194

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "chatccc",
3
- "version": "0.2.193",
3
+ "version": "0.2.194",
4
4
  "description": "Feishu bot bridge for Claude Code",
5
5
  "license": "Apache-2.0",
6
6
  "type": "module",
@@ -1,4 +1,4 @@
1
- import { mkdir, mkdtemp, rm, writeFile } from "node:fs/promises";
1
+ import { mkdir, mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
2
2
  import { tmpdir } from "node:os";
3
3
  import { join } from "node:path";
4
4
 
@@ -68,7 +68,7 @@ function mockAvatarFetch(uploadedNames: string[], usageResponse: Response): void
68
68
  }));
69
69
  }
70
70
 
71
- function mockAvatarUploadOnlyFetch(uploadedNames: string[]): ReturnType<typeof vi.fn> {
71
+ function mockAvatarUploadOnlyFetch(uploadedNames: string[]): ReturnType<typeof vi.fn> {
72
72
  const fetchMock = vi.fn(async (url: string | URL | Request, init?: RequestInit) => {
73
73
  const urlText = String(url);
74
74
  if (urlText === "https://open.feishu.test/im/v1/images") {
@@ -83,10 +83,44 @@ function mockAvatarUploadOnlyFetch(uploadedNames: string[]): ReturnType<typeof v
83
83
  throw new Error(`unexpected fetch: ${urlText}`);
84
84
  });
85
85
  vi.stubGlobal("fetch", fetchMock);
86
- return fetchMock;
87
- }
88
-
89
- describe("Codex avatar usage battery", () => {
86
+ return fetchMock;
87
+ }
88
+
89
+ describe("Plain avatar fallback", () => {
90
+ afterEach(() => {
91
+ vi.unstubAllGlobals();
92
+ vi.doUnmock("node:os");
93
+ vi.doUnmock("../config.ts");
94
+ vi.doUnmock("../cursor-usage.ts");
95
+ vi.restoreAllMocks();
96
+ getCursorUsageSummaryMock.mockReset();
97
+ mockConfig.cursor.avatarBatteryMode = "apiPercent";
98
+ mockConfig.cursor.onDemandMonthlyBudget = 1000;
99
+ });
100
+
101
+ it("uses a status-only avatar for unknown tools instead of falling back to Claude", async () => {
102
+ const homeDir = await mkdtemp(join(tmpdir(), "chatccc-avatar-home-"));
103
+ const userDataDir = await mkdtemp(join(tmpdir(), "chatccc-avatar-data-"));
104
+ const uploadedNames: string[] = [];
105
+ mockAvatarUploadOnlyFetch(uploadedNames);
106
+
107
+ try {
108
+ const { setChatAvatar } = await loadFeishuApiWithHome(homeDir, userDataDir);
109
+ await setChatAvatar("tenant-token", "chat_1", "ccc", "new");
110
+
111
+ expect(uploadedNames).toEqual(["avatar_plain_new.jpg"]);
112
+ const cacheRaw = await readFile(join(userDataDir, "state", "avatar-image-keys.json"), "utf-8");
113
+ const cache = JSON.parse(cacheRaw) as Record<string, string>;
114
+ expect(cache["plain:new"]).toBe("img_test");
115
+ expect(cache["claude:new"]).toBeUndefined();
116
+ } finally {
117
+ await rm(homeDir, { recursive: true, force: true });
118
+ await rm(userDataDir, { recursive: true, force: true });
119
+ }
120
+ });
121
+ });
122
+
123
+ describe("Codex avatar usage battery", () => {
90
124
  afterEach(() => {
91
125
  vi.unstubAllGlobals();
92
126
  vi.doUnmock("node:os");
package/src/feishu-api.ts CHANGED
@@ -319,11 +319,12 @@ 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 CODEX_AVATAR_USAGE_STYLE_VERSION = "usage-ring-gray-consumed-v13";
326
- const CURSOR_AVATAR_USAGE_STYLE_VERSION = "usage-battery-v1";
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-ring-gray-consumed-v13";
327
+ const CURSOR_AVATAR_USAGE_STYLE_VERSION = "usage-battery-v1";
327
328
 
328
329
  export interface CodexUsageBalance {
329
330
  usedPercent: number;
@@ -354,17 +355,17 @@ export interface CodexResetConsumeResult {
354
355
  const avatarKeyCache = new Map<string, string>();
355
356
  let avatarKeyCacheLoaded = false;
356
357
 
357
- function normalizeAvatarTool(tool: string): string {
358
- return AVATAR_BADGES[tool] ? tool : "claude";
359
- }
358
+ function normalizeAvatarTool(tool: string): string {
359
+ return AVATAR_BADGES[tool] ? tool : PLAIN_AVATAR_TOOL;
360
+ }
360
361
 
361
362
  function normalizeAvatarStatus(status: string): string {
362
363
  return AVATAR_SOURCES[status] ? status : "idle";
363
364
  }
364
365
 
365
- function avatarCombinationPath(tool: string, status: string): string {
366
- return resolvePath(AVATAR_COMBINATIONS_DIR, `avatar_${normalizeAvatarTool(tool)}_${normalizeAvatarStatus(status)}.png`);
367
- }
366
+ function avatarCombinationPath(tool: string, status: string): string {
367
+ return resolvePath(AVATAR_COMBINATIONS_DIR, `avatar_${normalizeAvatarTool(tool)}_${normalizeAvatarStatus(status)}.png`);
368
+ }
368
369
 
369
370
  function avatarCacheKey(
370
371
  tool: string,
@@ -757,16 +758,19 @@ async function renderAvatar(
757
758
  codexUsage: CodexUsageSummary | null = null,
758
759
  cursorBatteryPercent: number | null = null,
759
760
  ): Promise<{ buffer: Buffer; contentType: string; filename: string }> {
760
- const normalizedTool = normalizeAvatarTool(tool);
761
- const normalizedStatus = normalizeAvatarStatus(status);
762
- const composites: sharp.OverlayOptions[] = [];
763
-
764
- const codexWeeklyUsage = normalizedTool === "codex" ? codexUsage?.weekly : null;
765
- const useDynamicCodexAvatar = codexUsage && codexWeeklyUsage;
766
- const useDynamicCursorAvatar = normalizedTool === "cursor" && cursorBatteryPercent !== null;
767
- const basePath = useDynamicCodexAvatar || useDynamicCursorAvatar
768
- ? AVATAR_SOURCES[normalizedStatus]
769
- : avatarCombinationPath(normalizedTool, normalizedStatus);
761
+ const normalizedTool = normalizeAvatarTool(tool);
762
+ const normalizedStatus = normalizeAvatarStatus(status);
763
+ const composites: sharp.OverlayOptions[] = [];
764
+ const hasAgentBadge = normalizedTool !== PLAIN_AVATAR_TOOL;
765
+
766
+ const codexWeeklyUsage = normalizedTool === "codex" ? codexUsage?.weekly : null;
767
+ const useDynamicCodexAvatar = codexUsage && codexWeeklyUsage;
768
+ const useDynamicCursorAvatar = normalizedTool === "cursor" && cursorBatteryPercent !== null;
769
+ const basePath = useDynamicCodexAvatar || useDynamicCursorAvatar
770
+ ? AVATAR_SOURCES[normalizedStatus]
771
+ : hasAgentBadge
772
+ ? avatarCombinationPath(normalizedTool, normalizedStatus)
773
+ : AVATAR_SOURCES[normalizedStatus];
770
774
 
771
775
  if (useDynamicCodexAvatar) {
772
776
  composites.push(
@@ -793,16 +797,16 @@ async function renderAvatar(
793
797
  .jpeg({ quality: 95, progressive: false })
794
798
  .toBuffer();
795
799
 
796
- return {
797
- buffer: jpeg,
798
- contentType: "image/jpeg",
799
- filename: codexUsage?.weekly
800
- ? `avatar_${normalizedTool}_${normalizedStatus}_week_${codexUsage.weekly.remainingPercent}_5h_${codexUsage.fiveHour.remainingPercent}.jpg`
801
- : cursorBatteryPercent !== null
802
- ? `avatar_${normalizedTool}_${normalizedStatus}_battery_${cursorBatteryPercent}.jpg`
803
- : `avatar_${normalizedTool}_${normalizedStatus}.jpg`,
804
- };
805
- }
800
+ return {
801
+ buffer: jpeg,
802
+ contentType: "image/jpeg",
803
+ filename: normalizedTool === "codex" && codexUsage?.weekly
804
+ ? `avatar_${normalizedTool}_${normalizedStatus}_week_${codexUsage.weekly.remainingPercent}_5h_${codexUsage.fiveHour.remainingPercent}.jpg`
805
+ : normalizedTool === "cursor" && cursorBatteryPercent !== null
806
+ ? `avatar_${normalizedTool}_${normalizedStatus}_battery_${cursorBatteryPercent}.jpg`
807
+ : `avatar_${normalizedTool}_${normalizedStatus}.jpg`,
808
+ };
809
+ }
806
810
 
807
811
  async function uploadImage(
808
812
  token: string,