chatccc 0.2.17 → 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/images/avatars/badges/badge_claude.png +0 -0
- package/images/avatars/badges/badge_codex.png +0 -0
- package/images/avatars/badges/badge_cursor.png +0 -0
- package/images/avatars/brand-sources/claude_code_app_icon.png +0 -0
- package/images/avatars/brand-sources/codex_app_icon.png +0 -0
- package/images/avatars/brand-sources/cursor_icon_512.png +0 -0
- package/images/avatars/status_busy.png +0 -0
- package/images/avatars/status_idle.png +0 -0
- package/images/avatars/status_new.png +0 -0
- package/package.json +1 -1
- package/src/feishu-api.ts +104 -39
- package/src/index.ts +2 -2
- package/src/session.ts +2 -2
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
package/package.json
CHANGED
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
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
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:
|
|
286
|
-
};
|
|
287
|
-
}
|
|
288
|
-
|
|
289
|
-
async function uploadImage(token: string,
|
|
290
|
-
const image = await
|
|
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
|
-
|
|
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,
|
|
315
|
-
avatarKeyCache.set(
|
|
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) {
|