chatccc 0.2.18 → 0.2.19

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/src/feishu-api.ts CHANGED
@@ -1,6 +1,6 @@
1
- import { readdir, stat, readFile } from "node:fs/promises";
2
- import { resolve as resolvePath } from "node:path";
3
- import { join } from "node:path";
1
+ import { readdir, stat, readFile, mkdir, writeFile } from "node:fs/promises";
2
+ import { resolve as resolvePath } from "node:path";
3
+ import { dirname, join } from "node:path";
4
4
  import sharp from "sharp";
5
5
 
6
6
  import {
@@ -260,34 +260,94 @@ export function extractSessionId(description: string): string | null {
260
260
  // Avatar
261
261
  // ---------------------------------------------------------------------------
262
262
 
263
- const AVATAR_DIR = resolvePath(PROJECT_ROOT, "images", "avatars");
264
- const AVATAR_SOURCES: Record<string, string> = {
265
- new: resolvePath(AVATAR_DIR, "status_new.png"),
266
- busy: resolvePath(AVATAR_DIR, "status_busy.png"),
267
- idle: resolvePath(AVATAR_DIR, "status_idle.png"),
268
- };
269
-
270
- const avatarKeyCache = new Map<string, string>();
271
-
272
- async function loadAvatarSource(source: string): Promise<{ buffer: Buffer; contentType: string; filename: string }> {
273
- const input = await readFile(source);
274
-
275
- const jpeg = await sharp(input)
276
- .resize(256, 256, { fit: "cover", position: "center" })
277
- .flatten({ background: "#ffffff" })
278
- .removeAlpha()
279
- .jpeg({ quality: 95, progressive: false })
263
+ const AVATAR_DIR = resolvePath(PROJECT_ROOT, "images", "avatars");
264
+ const AVATAR_BADGE_DIR = resolvePath(AVATAR_DIR, "badges");
265
+ const AVATAR_KEY_CACHE_FILE = resolvePath(PROJECT_ROOT, "state", "avatar-image-keys.json");
266
+ const AVATAR_SOURCES: Record<string, string> = {
267
+ new: resolvePath(AVATAR_DIR, "status_new.png"),
268
+ busy: resolvePath(AVATAR_DIR, "status_busy.png"),
269
+ idle: resolvePath(AVATAR_DIR, "status_idle.png"),
270
+ };
271
+ const AVATAR_BADGES: Record<string, string> = {
272
+ claude: resolvePath(AVATAR_BADGE_DIR, "badge_claude.png"),
273
+ cursor: resolvePath(AVATAR_BADGE_DIR, "badge_cursor.png"),
274
+ codex: resolvePath(AVATAR_BADGE_DIR, "badge_codex.png"),
275
+ };
276
+ const AVATAR_SIZE = 256;
277
+ const BADGE_SIZE = 92;
278
+ const BADGE_MARGIN = 10;
279
+
280
+ const avatarKeyCache = new Map<string, string>();
281
+ let avatarKeyCacheLoaded = false;
282
+
283
+ function normalizeAvatarTool(tool: string): string {
284
+ return AVATAR_BADGES[tool] ? tool : "claude";
285
+ }
286
+
287
+ function normalizeAvatarStatus(status: string): string {
288
+ return AVATAR_SOURCES[status] ? status : "idle";
289
+ }
290
+
291
+ function avatarCacheKey(tool: string, status: string): string {
292
+ return `${normalizeAvatarTool(tool)}:${normalizeAvatarStatus(status)}`;
293
+ }
294
+
295
+ async function loadAvatarKeyCache(): Promise<void> {
296
+ if (avatarKeyCacheLoaded) return;
297
+ avatarKeyCacheLoaded = true;
298
+ try {
299
+ const raw = await readFile(AVATAR_KEY_CACHE_FILE, "utf-8");
300
+ const parsed = JSON.parse(raw) as Record<string, unknown>;
301
+ for (const [key, value] of Object.entries(parsed)) {
302
+ if (typeof value === "string" && value.trim()) avatarKeyCache.set(key, value);
303
+ }
304
+ } catch {
305
+ // Missing or malformed cache should not block avatar updates.
306
+ }
307
+ }
308
+
309
+ async function persistAvatarKeyCache(): Promise<void> {
310
+ await mkdir(dirname(AVATAR_KEY_CACHE_FILE), { recursive: true });
311
+ await writeFile(
312
+ AVATAR_KEY_CACHE_FILE,
313
+ JSON.stringify(Object.fromEntries(avatarKeyCache.entries()), null, 2),
314
+ "utf-8",
315
+ );
316
+ }
317
+
318
+ async function renderAvatar(tool: string, status: string): Promise<{ buffer: Buffer; contentType: string; filename: string }> {
319
+ const normalizedTool = normalizeAvatarTool(tool);
320
+ const normalizedStatus = normalizeAvatarStatus(status);
321
+ const badge = await sharp(await readFile(AVATAR_BADGES[normalizedTool]))
322
+ .resize(BADGE_SIZE, BADGE_SIZE, {
323
+ fit: "contain",
324
+ kernel: sharp.kernel.lanczos3,
325
+ background: { r: 0, g: 0, b: 0, alpha: 0 },
326
+ })
327
+ .png()
328
+ .toBuffer();
329
+
330
+ const jpeg = await sharp(await readFile(AVATAR_SOURCES[normalizedStatus]))
331
+ .resize(AVATAR_SIZE, AVATAR_SIZE, { fit: "cover", position: "center" })
332
+ .composite([{
333
+ input: badge,
334
+ left: AVATAR_SIZE - BADGE_SIZE - BADGE_MARGIN,
335
+ top: AVATAR_SIZE - BADGE_SIZE - BADGE_MARGIN,
336
+ }])
337
+ .flatten({ background: "#ffffff" })
338
+ .removeAlpha()
339
+ .jpeg({ quality: 95, progressive: false })
280
340
  .toBuffer();
281
341
 
282
- return {
283
- buffer: jpeg,
284
- contentType: "image/jpeg",
285
- filename: "avatar.jpg",
286
- };
287
- }
288
-
289
- async function uploadImage(token: string, source: string): Promise<string> {
290
- const image = await loadAvatarSource(source);
342
+ return {
343
+ buffer: jpeg,
344
+ contentType: "image/jpeg",
345
+ filename: `avatar_${normalizedTool}_${normalizedStatus}.jpg`,
346
+ };
347
+ }
348
+
349
+ async function uploadImage(token: string, tool: string, status: string): Promise<string> {
350
+ const image = await renderAvatar(tool, status);
291
351
  const blob = new Blob([new Uint8Array(image.buffer)], { type: image.contentType });
292
352
  const form = new FormData();
293
353
  form.append("image_type", "avatar");
@@ -308,18 +368,23 @@ async function uploadImage(token: string, source: string): Promise<string> {
308
368
  return data.data!.image_key!;
309
369
  }
310
370
 
311
- async function getOrUploadAvatarKey(token: string, status: string): Promise<string> {
312
- const cached = avatarKeyCache.get(status);
371
+ async function getOrUploadAvatarKey(token: string, tool: string, status: string): Promise<string> {
372
+ await loadAvatarKeyCache();
373
+ const keyName = avatarCacheKey(tool, status);
374
+ const cached = avatarKeyCache.get(keyName);
313
375
  if (cached) return cached;
314
- const key = await uploadImage(token, AVATAR_SOURCES[status]);
315
- avatarKeyCache.set(status, key);
376
+ const key = await uploadImage(token, tool, status);
377
+ avatarKeyCache.set(keyName, key);
378
+ await persistAvatarKeyCache().catch((err) => {
379
+ console.error(`[${ts()}] [AVATAR] persist cache FAIL: ${(err as Error).message}`);
380
+ });
316
381
  console.log(`[${ts()}] [AVATAR] Uploaded "${status}" → image_key=${key}`);
317
382
  return key;
318
383
  }
319
384
 
320
- export async function setChatAvatar(token: string, chatId: string, status: string): Promise<void> {
321
- try {
322
- const avatarKey = await getOrUploadAvatarKey(token, status);
385
+ export async function setChatAvatar(token: string, chatId: string, tool: string, status: string): Promise<void> {
386
+ try {
387
+ const avatarKey = await getOrUploadAvatarKey(token, tool, status);
323
388
  const resp = await fetch(`${BASE_URL}/im/v1/chats/${chatId}`, {
324
389
  method: "PUT",
325
390
  headers: {
@@ -336,9 +401,9 @@ export async function setChatAvatar(token: string, chatId: string, status: strin
336
401
  }
337
402
  if (data.code !== 0) throw new Error(`[${data.code}] ${data.msg}`);
338
403
  } catch (err) {
339
- console.error(`[${ts()}] [AVATAR] setChatAvatar FAIL: chatId=${chatId} status=${status} ${(err as Error).message}`);
340
- }
341
- }
404
+ console.error(`[${ts()}] [AVATAR] setChatAvatar FAIL: chatId=${chatId} tool=${tool} status=${status} ${(err as Error).message}`);
405
+ }
406
+ }
342
407
 
343
408
  // ---------------------------------------------------------------------------
344
409
  // Messaging
package/src/index.ts CHANGED
@@ -405,7 +405,7 @@ async function handleCommand(text: string, chatId: string, openId: string, msgTi
405
405
  );
406
406
 
407
407
  console.log(`[${ts()}] [STEP 4/4] Replied to new group → OK`);
408
- setChatAvatar(freshToken, newChatId, "new").catch(() => {});
408
+ setChatAvatar(freshToken, newChatId, tool, "new").catch(() => {});
409
409
  console.log(`${"=".repeat(60)}`);
410
410
  return;
411
411
  }
@@ -558,7 +558,7 @@ async function handleCommand(text: string, chatId: string, openId: string, msgTi
558
558
  tool: descriptionTool,
559
559
  });
560
560
 
561
- setChatAvatar(freshToken, chatId, "new").catch(() => {});
561
+ setChatAvatar(freshToken, chatId, descriptionTool, "new").catch(() => {});
562
562
 
563
563
  await sendCardReply(
564
564
  freshToken, chatId, `${toolLabel} Session Reset`,
package/src/session.ts CHANGED
@@ -324,7 +324,7 @@ export async function resumeAndPrompt(
324
324
  });
325
325
  const myGen = sessionGen;
326
326
 
327
- setChatAvatar(token, chatId, "busy").catch(() => {});
327
+ setChatAvatar(token, chatId, tool, "busy").catch(() => {});
328
328
 
329
329
  const now = Date.now();
330
330
  const existingInfo = sessionInfoMap.get(chatId);
@@ -466,7 +466,7 @@ export async function resumeAndPrompt(
466
466
  if (!cEntry || cEntry.gen !== myGen) return;
467
467
  const wasStopped = cEntry.stopped;
468
468
  chatSessionMap.delete(chatId);
469
- setChatAvatar(token, chatId, "idle").catch(() => {});
469
+ setChatAvatar(token, chatId, tool, "idle").catch(() => {});
470
470
 
471
471
  const finalCardContent = state.accumulatedContent || " ";
472
472
  if (cardId) {