chatccc 0.2.149 → 0.2.151

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.
@@ -19,12 +19,21 @@
19
19
 
20
20
  ## 卡住自检
21
21
 
22
- 如果你感觉任务已经完成,但发现自己陷入了循环(反复调用相同工具、输出 "stuck in a loop" 等),**立即**调用以下接口:
22
+ 如果你感觉任务已经完成,但发现自己陷入了循环(反复调用相同工具、输出 "stuck in a loop" 等),**立即**调用以下接口。
23
23
 
24
- POST {{stop_stuck_url}}
25
- Content-Type: application/json
24
+ **重要:必须用 heredoc 写临时文件再 curl,禁止在命令行参数中直接写中文 JSON。** Windows 下命令行参数传中文会变成 `?` 乱码。
26
25
 
26
+ ```bash
27
+ cat > /tmp/stop_ccc.json << 'STOPJSON'
27
28
  {"session_id": "{{session_id}}", "final_reply": "<你给用户的最终回复,总结已完成的工作和结果>"}
29
+ STOPJSON
30
+ curl -s -X POST {{stop_stuck_url}} -H "Content-Type: application/json" -d @/tmp/stop_ccc.json
31
+ ```
32
+
33
+ final_reply 注意事项:
34
+ - 把 `<...>` 占位符替换为你的实际回复,不要包含双引号 `"` 和反斜杠 `\`
35
+ - 不要包含换行,用空格代替
36
+ - 保持简短(1-2 句话)
28
37
 
29
38
  调用后停止所有工具调用,直接输出最终回复即可。
30
39
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "chatccc",
3
- "version": "0.2.149",
3
+ "version": "0.2.151",
4
4
  "description": "Feishu bot bridge for Claude Code",
5
5
  "license": "Apache-2.0",
6
6
  "type": "module",
@@ -19,6 +19,7 @@
19
19
  "images/img_readme_*.png",
20
20
  "images/avatars/status_*.png",
21
21
  "images/avatars/badges/",
22
+ "images/avatars/combinations/",
22
23
  "package.json",
23
24
  "README.md",
24
25
  "config.sample.json"
@@ -106,9 +106,11 @@ describe("buildProgressCard", () => {
106
106
  const card = buildProgressCard("hello");
107
107
  const parsed = JSON.parse(card);
108
108
  const buttons = parsed.body.elements.filter((e: any) => e.tag === "button");
109
- expect(buttons).toHaveLength(2);
110
- expect(buttons[0].text.content).toBe("查看状态(/state)");
111
- expect(buttons[1].text.content).toBe("停止生成(/stop)");
109
+ expect(buttons).toHaveLength(2);
110
+ expect(buttons[0].text.content).toBe("查看状态(/state)");
111
+ expect(buttons[0].value).toEqual({ action: "state" });
112
+ expect(buttons[0].element_id).toBe("action_state");
113
+ expect(buttons[1].text.content).toBe("停止生成(/stop)");
112
114
  });
113
115
 
114
116
  it("hides stop button when showStop is false", () => {
@@ -0,0 +1,102 @@
1
+ import { mkdir, mkdtemp, rm, writeFile } from "node:fs/promises";
2
+ import { tmpdir } from "node:os";
3
+ import { join } from "node:path";
4
+
5
+ import { afterEach, describe, expect, it, vi } from "vitest";
6
+
7
+ async function loadFeishuApiWithHome(homeDir: string, userDataDir: string) {
8
+ vi.resetModules();
9
+ vi.doMock("node:os", () => ({ homedir: () => homeDir }));
10
+ vi.doMock("../config.ts", () => ({
11
+ APP_ID: "app_id",
12
+ APP_SECRET: "app_secret",
13
+ BASE_URL: "https://open.feishu.test",
14
+ CHAT_LOGS_DIR: join(userDataDir, "logs"),
15
+ PROJECT_ROOT: process.cwd(),
16
+ USER_DATA_DIR: userDataDir,
17
+ CLAUDE_SESSION_PREFIX: "Claude Code Session:",
18
+ CURSOR_SESSION_PREFIX: "Cursor Session:",
19
+ CODEX_SESSION_PREFIX: "Codex Session:",
20
+ ts: () => "test-ts",
21
+ resolveDefaultAgentTool: () => "claude",
22
+ toolDisplayName: (tool: string) => tool,
23
+ }));
24
+ return import("../feishu-api.ts");
25
+ }
26
+
27
+ async function writeCodexAuth(homeDir: string): Promise<void> {
28
+ const codexDir = join(homeDir, ".codex");
29
+ await mkdir(codexDir, { recursive: true });
30
+ await writeFile(
31
+ join(codexDir, "auth.json"),
32
+ JSON.stringify({ tokens: { access_token: "codex-access-token" } }),
33
+ "utf-8",
34
+ );
35
+ }
36
+
37
+ function mockAvatarFetch(uploadedNames: string[], usageResponse: Response): void {
38
+ vi.stubGlobal("fetch", vi.fn(async (url: string | URL | Request, init?: RequestInit) => {
39
+ const urlText = String(url);
40
+ if (urlText === "https://chatgpt.com/backend-api/wham/usage") {
41
+ return usageResponse.clone();
42
+ }
43
+ if (urlText === "https://open.feishu.test/im/v1/images") {
44
+ const form = init?.body as FormData;
45
+ const image = form.get("image") as File;
46
+ uploadedNames.push(image.name);
47
+ return new Response(JSON.stringify({ code: 0, data: { image_key: "img_test" } }), { status: 200 });
48
+ }
49
+ if (urlText === "https://open.feishu.test/im/v1/chats/chat_1") {
50
+ return new Response(JSON.stringify({ code: 0 }), { status: 200 });
51
+ }
52
+ throw new Error(`unexpected fetch: ${urlText}`);
53
+ }));
54
+ }
55
+
56
+ describe("Codex avatar usage battery", () => {
57
+ afterEach(() => {
58
+ vi.unstubAllGlobals();
59
+ vi.doUnmock("node:os");
60
+ vi.doUnmock("../config.ts");
61
+ vi.restoreAllMocks();
62
+ });
63
+
64
+ it("adds the 5h remaining percent to Codex avatar uploads when usage lookup succeeds", async () => {
65
+ const homeDir = await mkdtemp(join(tmpdir(), "chatccc-avatar-home-"));
66
+ const userDataDir = await mkdtemp(join(tmpdir(), "chatccc-avatar-data-"));
67
+ const uploadedNames: string[] = [];
68
+ await writeCodexAuth(homeDir);
69
+ mockAvatarFetch(uploadedNames, new Response(JSON.stringify({
70
+ rate_limit: { primary_window: { used_percent: 37 } },
71
+ }), { status: 200 }));
72
+
73
+ try {
74
+ const { setChatAvatar } = await loadFeishuApiWithHome(homeDir, userDataDir);
75
+ await setChatAvatar("tenant-token", "chat_1", "codex", "busy");
76
+
77
+ expect(uploadedNames).toEqual(["avatar_codex_busy_usage_63.jpg"]);
78
+ } finally {
79
+ await rm(homeDir, { recursive: true, force: true });
80
+ await rm(userDataDir, { recursive: true, force: true });
81
+ }
82
+ });
83
+
84
+ it("falls back to the pre-combined Codex avatar when usage lookup fails", async () => {
85
+ const homeDir = await mkdtemp(join(tmpdir(), "chatccc-avatar-home-"));
86
+ const userDataDir = await mkdtemp(join(tmpdir(), "chatccc-avatar-data-"));
87
+ const uploadedNames: string[] = [];
88
+ await writeCodexAuth(homeDir);
89
+ vi.spyOn(console, "warn").mockImplementation(() => {});
90
+ mockAvatarFetch(uploadedNames, new Response("fail", { status: 500 }));
91
+
92
+ try {
93
+ const { setChatAvatar } = await loadFeishuApiWithHome(homeDir, userDataDir);
94
+ await setChatAvatar("tenant-token", "chat_1", "codex", "idle");
95
+
96
+ expect(uploadedNames).toEqual(["avatar_codex_idle.jpg"]);
97
+ } finally {
98
+ await rm(homeDir, { recursive: true, force: true });
99
+ await rm(userDataDir, { recursive: true, force: true });
100
+ }
101
+ });
102
+ });
@@ -4,6 +4,21 @@ import { TextDecoder } from "node:util";
4
4
  // Windows 下 curl 传中文可能用 GBK 编码而非 UTF-8,按顺序尝试解码
5
5
  const FALLBACK_ENCODINGS = ["gbk", "gb2312", "latin1"];
6
6
 
7
+ /**
8
+ * 检测 UTF-8 解码结果是否看起来像乱码(GBK 字节被误当 UTF-8 解析)。
9
+ * 典型特征:有 Latin-1 补充字符(U+0080-U+00FF)但没有 CJK 字符。
10
+ */
11
+ function hasGarbledPattern(text: string): boolean {
12
+ let latin1Count = 0;
13
+ let cjkCount = 0;
14
+ for (const ch of text) {
15
+ const code = ch.codePointAt(0) ?? 0;
16
+ if (code >= 0x4E00 && code <= 0x9FFF) cjkCount++;
17
+ if (code >= 0x0080 && code <= 0x00FF) latin1Count++;
18
+ }
19
+ return latin1Count > 0 && cjkCount === 0;
20
+ }
21
+
7
22
  function normalizeHeaderValue(value: string | string[] | undefined): string {
8
23
  if (Array.isArray(value)) return value.join("; ");
9
24
  return value ?? "";
@@ -43,7 +58,27 @@ export async function readUtf8JsonBody<T>(req: IncomingMessage, maxBytes: number
43
58
  const body = await readLimitedBodyBuffer(req, maxBytes);
44
59
 
45
60
  // 首选 UTF-8;若 JSON 解析失败则尝试常见中文编码
46
- for (const enc of ["utf-8", ...FALLBACK_ENCODINGS]) {
61
+ // 注意:UTF-8 解码可能"成功"但实际是 GBK 字节被误当 UTF-8 解析,
62
+ // 此时 JSON 也能解析通过但中文变成乱码。通过 hasGarbledPattern 检测
63
+ // 这种"假成功"并回退到 GBK。
64
+ try {
65
+ const text = new TextDecoder("utf-8", { fatal: true }).decode(body);
66
+ const parsed = JSON.parse(text) as T;
67
+ if (hasGarbledPattern(text)) {
68
+ // UTF-8 解码出的文本看起来像乱码,尝试 GBK
69
+ try {
70
+ const gbkText = new TextDecoder("gbk", { fatal: true }).decode(body);
71
+ return JSON.parse(gbkText) as T;
72
+ } catch {
73
+ // GBK 也不行,用 UTF-8 结果
74
+ }
75
+ }
76
+ return parsed;
77
+ } catch {
78
+ // UTF-8 解码失败,尝试其他编码
79
+ }
80
+
81
+ for (const enc of FALLBACK_ENCODINGS) {
47
82
  try {
48
83
  const text = new TextDecoder(enc, { fatal: true }).decode(body);
49
84
  return JSON.parse(text) as T;
package/src/cards.ts CHANGED
@@ -91,8 +91,8 @@ export function buildProgressCard(
91
91
  tag: "button",
92
92
  text: { tag: "plain_text", content: "查看状态(/state)" },
93
93
  type: "default",
94
- value: { action: "status" },
95
- element_id: "action_status",
94
+ value: { action: "state" },
95
+ element_id: "action_state",
96
96
  });
97
97
  elements.push({
98
98
  tag: "button",
package/src/feishu-api.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  import { readdir, stat, readFile, mkdir, writeFile } from "node:fs/promises";
2
+ import { homedir } from "node:os";
2
3
  import { extname, resolve as resolvePath } from "node:path";
3
4
  import { dirname, join } from "node:path";
4
5
  import sharp from "sharp";
@@ -297,7 +298,10 @@ export function extractSessionId(description: string): string | null {
297
298
 
298
299
  const AVATAR_DIR = resolvePath(PROJECT_ROOT, "images", "avatars");
299
300
  const AVATAR_BADGE_DIR = resolvePath(AVATAR_DIR, "badges");
301
+ const AVATAR_COMBINATIONS_DIR = resolvePath(AVATAR_DIR, "combinations");
300
302
  const AVATAR_KEY_CACHE_FILE = resolvePath(USER_DATA_DIR, "state", "avatar-image-keys.json");
303
+ const CODEX_AUTH_FILE = resolvePath(homedir(), ".codex", "auth.json");
304
+ const CODEX_USAGE_URL = "https://chatgpt.com/backend-api/wham/usage";
301
305
  const AVATAR_SOURCES: Record<string, string> = {
302
306
  new: resolvePath(AVATAR_DIR, "status_new.png"),
303
307
  busy: resolvePath(AVATAR_DIR, "status_busy.png"),
@@ -309,8 +313,11 @@ const AVATAR_BADGES: Record<string, string> = {
309
313
  codex: resolvePath(AVATAR_BADGE_DIR, "badge_codex.png"),
310
314
  };
311
315
  const AVATAR_SIZE = 256;
312
- const BADGE_SIZE = 92;
313
- const BADGE_MARGIN = 10;
316
+
317
+ interface CodexUsageBalance {
318
+ usedPercent: number;
319
+ remainingPercent: number;
320
+ }
314
321
 
315
322
  const avatarKeyCache = new Map<string, string>();
316
323
  let avatarKeyCacheLoaded = false;
@@ -323,8 +330,19 @@ function normalizeAvatarStatus(status: string): string {
323
330
  return AVATAR_SOURCES[status] ? status : "idle";
324
331
  }
325
332
 
326
- function avatarCacheKey(tool: string, status: string): string {
327
- return `${normalizeAvatarTool(tool)}:${normalizeAvatarStatus(status)}`;
333
+ function avatarCombinationPath(tool: string, status: string): string {
334
+ return resolvePath(AVATAR_COMBINATIONS_DIR, `avatar_${normalizeAvatarTool(tool)}_${normalizeAvatarStatus(status)}.png`);
335
+ }
336
+
337
+ function avatarCacheKey(tool: string, status: string, codexUsage: CodexUsageBalance | null = null): string {
338
+ const normalizedTool = normalizeAvatarTool(tool);
339
+ const normalizedStatus = normalizeAvatarStatus(status);
340
+ if (normalizedTool === "codex") {
341
+ return codexUsage
342
+ ? `${normalizedTool}:${normalizedStatus}:battery:${codexUsage.remainingPercent}`
343
+ : `${normalizedTool}:${normalizedStatus}:plain`;
344
+ }
345
+ return `${normalizedTool}:${normalizedStatus}`;
328
346
  }
329
347
 
330
348
  async function loadAvatarKeyCache(): Promise<void> {
@@ -350,25 +368,119 @@ async function persistAvatarKeyCache(): Promise<void> {
350
368
  );
351
369
  }
352
370
 
353
- async function renderAvatar(tool: string, status: string): Promise<{ buffer: Buffer; contentType: string; filename: string }> {
371
+ function clampPercent(value: number): number {
372
+ if (!Number.isFinite(value)) return 0;
373
+ return Math.max(0, Math.min(100, Math.round(value)));
374
+ }
375
+
376
+ async function getCodexAccessToken(): Promise<string | null> {
377
+ try {
378
+ const raw = await readFile(CODEX_AUTH_FILE, "utf-8");
379
+ const parsed = JSON.parse(raw) as { tokens?: { access_token?: unknown } };
380
+ const token = parsed.tokens?.access_token;
381
+ return typeof token === "string" && token.trim() ? token : null;
382
+ } catch {
383
+ return null;
384
+ }
385
+ }
386
+
387
+ async function fetchCodexUsageBalance(): Promise<CodexUsageBalance | null> {
388
+ try {
389
+ const token = await getCodexAccessToken();
390
+ if (!token) throw new Error("missing ~/.codex/auth.json access token");
391
+
392
+ const resp = await fetch(CODEX_USAGE_URL, {
393
+ headers: { Authorization: `Bearer ${token}` },
394
+ });
395
+ const text = await resp.text();
396
+ if (!resp.ok) throw new Error(`HTTP ${resp.status}: ${text.slice(0, 160)}`);
397
+
398
+ const data = JSON.parse(text) as {
399
+ rate_limit?: { primary_window?: { used_percent?: unknown } };
400
+ };
401
+ const usedPercent = Number(data.rate_limit?.primary_window?.used_percent);
402
+ if (!Number.isFinite(usedPercent)) throw new Error("missing rate_limit.primary_window.used_percent");
403
+
404
+ const used = clampPercent(usedPercent);
405
+ return {
406
+ usedPercent: used,
407
+ remainingPercent: clampPercent(100 - used),
408
+ };
409
+ } catch (err) {
410
+ console.warn(`[${ts()}] [AVATAR] Codex usage unavailable, using plain avatar: ${(err as Error).message}`);
411
+ return null;
412
+ }
413
+ }
414
+
415
+ function codexUsagePalette(remainingPercent: number): { start: string; end: string; glow: string } {
416
+ if (remainingPercent <= 25) return { start: "#ef4444", end: "#fb923c", glow: "#fed7aa" };
417
+ if (remainingPercent <= 60) return { start: "#eab308", end: "#facc15", glow: "#fef3c7" };
418
+ return { start: "#16a34a", end: "#34d399", glow: "#bbf7d0" };
419
+ }
420
+
421
+ function buildCodexUsageBatterySvg(remainingPercent: number): Buffer {
422
+ const remaining = clampPercent(remainingPercent);
423
+ const label = `${remaining}%`;
424
+ const palette = codexUsagePalette(remaining);
425
+
426
+ const bodyX = 28;
427
+ const bodyY = 38;
428
+ const bodyW = 91;
429
+ const bodyH = 47;
430
+ const capW = 10;
431
+ const capH = 18;
432
+ const capX = bodyX + bodyW;
433
+ const capY = bodyY + Math.round((bodyH - capH) / 2);
434
+ const pad = 5;
435
+ const fillMaxW = bodyW - pad * 2;
436
+ const fillWidth = Math.round((fillMaxW * remaining) / 100);
437
+
438
+ return Buffer.from(`
439
+ <svg width="256" height="256" xmlns="http://www.w3.org/2000/svg">
440
+ <defs>
441
+ <filter id="shadow" x="-35%" y="-35%" width="170%" height="170%">
442
+ <feDropShadow dx="0" dy="4" stdDeviation="4" flood-color="#4a2712" flood-opacity="0.30"/>
443
+ </filter>
444
+ <linearGradient id="fill" x1="0" y1="0" x2="1" y2="0">
445
+ <stop offset="0" stop-color="${palette.start}"/>
446
+ <stop offset="1" stop-color="${palette.end}"/>
447
+ </linearGradient>
448
+ <linearGradient id="well" x1="0" y1="0" x2="1" y2="1">
449
+ <stop offset="0" stop-color="#334155"/>
450
+ <stop offset="1" stop-color="#111827"/>
451
+ </linearGradient>
452
+ <clipPath id="batteryInnerClip">
453
+ <rect x="${bodyX + pad}" y="${bodyY + pad}" width="${fillMaxW}" height="${bodyH - pad * 2}" rx="10"/>
454
+ </clipPath>
455
+ </defs>
456
+ <g filter="url(#shadow)">
457
+ <rect x="${bodyX}" y="${bodyY}" width="${bodyW}" height="${bodyH}" rx="15" fill="#0f172a"/>
458
+ <rect x="${bodyX + pad}" y="${bodyY + pad}" width="${fillMaxW}" height="${bodyH - pad * 2}" rx="10" fill="url(#well)"/>
459
+ <rect x="${bodyX + pad}" y="${bodyY + pad}" width="${fillWidth}" height="${bodyH - pad * 2}" fill="url(#fill)" clip-path="url(#batteryInnerClip)"/>
460
+ <rect x="${bodyX + pad + 3}" y="${bodyY + pad + 4}" width="${Math.max(0, fillWidth - 6)}" height="7" rx="3.5" fill="${palette.glow}" fill-opacity="0.42" clip-path="url(#batteryInnerClip)"/>
461
+ <rect x="${capX}" y="${capY}" width="${capW}" height="${capH}" rx="5" fill="#0f172a"/>
462
+ <rect x="${bodyX}" y="${bodyY}" width="${bodyW}" height="${bodyH}" rx="15" fill="none" stroke="#f8fafc" stroke-opacity="0.82" stroke-width="3.2"/>
463
+ <text x="${bodyX + bodyW / 2}" y="${bodyY + 32}" text-anchor="middle" font-family="Arial, Helvetica, sans-serif" font-size="27" font-weight="900" letter-spacing="0" stroke="#0b1220" stroke-width="5" paint-order="stroke" stroke-linejoin="round" fill="#ffffff">${label}</text>
464
+ </g>
465
+ </svg>`);
466
+ }
467
+
468
+ async function renderAvatar(tool: string, status: string, codexUsage: CodexUsageBalance | null = null): Promise<{ buffer: Buffer; contentType: string; filename: string }> {
354
469
  const normalizedTool = normalizeAvatarTool(tool);
355
470
  const normalizedStatus = normalizeAvatarStatus(status);
356
- const badge = await sharp(await readFile(AVATAR_BADGES[normalizedTool]))
357
- .resize(BADGE_SIZE, BADGE_SIZE, {
358
- fit: "contain",
359
- kernel: sharp.kernel.lanczos3,
360
- background: { r: 0, g: 0, b: 0, alpha: 0 },
361
- })
362
- .png()
363
- .toBuffer();
471
+ const composites: sharp.OverlayOptions[] = [];
472
+
473
+ if (normalizedTool === "codex" && codexUsage) {
474
+ composites.push({ input: buildCodexUsageBatterySvg(codexUsage.remainingPercent), left: 0, top: 0 });
475
+ }
364
476
 
365
- const jpeg = await sharp(await readFile(AVATAR_SOURCES[normalizedStatus]))
366
- .resize(AVATAR_SIZE, AVATAR_SIZE, { fit: "cover", position: "center" })
367
- .composite([{
368
- input: badge,
369
- left: AVATAR_SIZE - BADGE_SIZE - BADGE_MARGIN,
370
- top: AVATAR_SIZE - BADGE_SIZE - BADGE_MARGIN,
371
- }])
477
+ let pipeline = sharp(await readFile(avatarCombinationPath(normalizedTool, normalizedStatus)))
478
+ .resize(AVATAR_SIZE, AVATAR_SIZE, { fit: "cover", position: "center" });
479
+ if (composites.length > 0) {
480
+ pipeline = pipeline.composite(composites);
481
+ }
482
+
483
+ const jpeg = await pipeline
372
484
  .flatten({ background: "#ffffff" })
373
485
  .removeAlpha()
374
486
  .jpeg({ quality: 95, progressive: false })
@@ -377,12 +489,14 @@ async function renderAvatar(tool: string, status: string): Promise<{ buffer: Buf
377
489
  return {
378
490
  buffer: jpeg,
379
491
  contentType: "image/jpeg",
380
- filename: `avatar_${normalizedTool}_${normalizedStatus}.jpg`,
492
+ filename: codexUsage
493
+ ? `avatar_${normalizedTool}_${normalizedStatus}_usage_${codexUsage.remainingPercent}.jpg`
494
+ : `avatar_${normalizedTool}_${normalizedStatus}.jpg`,
381
495
  };
382
496
  }
383
497
 
384
- async function uploadImage(token: string, tool: string, status: string): Promise<string> {
385
- const image = await renderAvatar(tool, status);
498
+ async function uploadImage(token: string, tool: string, status: string, codexUsage: CodexUsageBalance | null = null): Promise<string> {
499
+ const image = await renderAvatar(tool, status, codexUsage);
386
500
  const blob = new Blob([new Uint8Array(image.buffer)], { type: image.contentType });
387
501
  const form = new FormData();
388
502
  form.append("image_type", "avatar");
@@ -405,15 +519,18 @@ async function uploadImage(token: string, tool: string, status: string): Promise
405
519
 
406
520
  async function getOrUploadAvatarKey(token: string, tool: string, status: string): Promise<string> {
407
521
  await loadAvatarKeyCache();
408
- const keyName = avatarCacheKey(tool, status);
522
+ const normalizedTool = normalizeAvatarTool(tool);
523
+ const normalizedStatus = normalizeAvatarStatus(status);
524
+ const codexUsage = normalizedTool === "codex" ? await fetchCodexUsageBalance() : null;
525
+ const keyName = avatarCacheKey(normalizedTool, normalizedStatus, codexUsage);
409
526
  const cached = avatarKeyCache.get(keyName);
410
527
  if (cached) return cached;
411
- const key = await uploadImage(token, tool, status);
528
+ const key = await uploadImage(token, normalizedTool, normalizedStatus, codexUsage);
412
529
  avatarKeyCache.set(keyName, key);
413
530
  await persistAvatarKeyCache().catch((err) => {
414
531
  console.error(`[${ts()}] [AVATAR] persist cache FAIL: ${(err as Error).message}`);
415
532
  });
416
- console.log(`[${ts()}] [AVATAR] Uploaded "${status}" → image_key=${key}`);
533
+ console.log(`[${ts()}] [AVATAR] Uploaded "${keyName}" → image_key=${key}`);
417
534
  return key;
418
535
  }
419
536
 
package/src/index.ts CHANGED
@@ -212,7 +212,7 @@ function parseCardAction(data: unknown): CardActionResult | null {
212
212
  }
213
213
  if (!cmd) return null;
214
214
 
215
- const CMD_MAP: Record<string, string> = { stop: "/stop", cancel: "/cancel", new: "/new", "new claude": "/new claude", "new cursor": "/new cursor", "new codex": "/new codex", restart: "/restart", status: "/state", cd: "/cd", sessions: "/sessions", newh: "/newh" };
215
+ const CMD_MAP: Record<string, string> = { stop: "/stop", cancel: "/cancel", new: "/new", "new claude": "/new claude", "new cursor": "/new cursor", "new codex": "/new codex", restart: "/restart", state: "/state", cd: "/cd", sessions: "/sessions", newh: "/newh" };
216
216
  let text = CMD_MAP[cmd] ?? "";
217
217
  if (cmd === "cd" && typeof action.value === "object" && action.value !== null) {
218
218
  const path = (action.value as Record<string, string>).path;