chatccc 0.2.150 → 0.2.152
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/combinations/avatar_claude_busy.png +0 -0
- package/images/avatars/combinations/avatar_claude_idle.png +0 -0
- package/images/avatars/combinations/avatar_claude_new.png +0 -0
- package/images/avatars/combinations/avatar_codex_busy.png +0 -0
- package/images/avatars/combinations/avatar_codex_idle.png +0 -0
- package/images/avatars/combinations/avatar_codex_new.png +0 -0
- package/images/avatars/combinations/avatar_cursor_busy.png +0 -0
- package/images/avatars/combinations/avatar_cursor_idle.png +0 -0
- package/images/avatars/combinations/avatar_cursor_new.png +0 -0
- package/package.json +2 -1
- package/src/__tests__/cards.test.ts +5 -3
- package/src/__tests__/feishu-avatar.test.ts +102 -0
- package/src/adapters/adapter-interface.ts +4 -0
- package/src/adapters/claude-adapter.ts +2 -0
- package/src/agent-stop-stuck.ts +2 -0
- package/src/cards.ts +2 -2
- package/src/feishu-api.ts +143 -26
- package/src/index.ts +1 -1
- package/src/session-chat-binding.ts +4 -0
- package/src/session.ts +4 -0
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "chatccc",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.152",
|
|
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[
|
|
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
|
+
});
|
|
@@ -128,6 +128,10 @@ export interface ToolPromptOptions {
|
|
|
128
128
|
onProcessStart?: (info: ToolProcessInfo) => void;
|
|
129
129
|
/** Called when the adapter leaves the prompt process scope normally or by abort. */
|
|
130
130
|
onProcessExit?: (info: ToolProcessInfo) => void;
|
|
131
|
+
/** Called when the adapter creates an SDK session internally.
|
|
132
|
+
* The callback receives a close function that terminates the underlying
|
|
133
|
+
* subprocess. Used by stop-stuck-loop to kill the CLI process immediately. */
|
|
134
|
+
onSessionCreated?: (closeSession: () => void) => void;
|
|
131
135
|
}
|
|
132
136
|
|
|
133
137
|
// ---------------------------------------------------------------------------
|
|
@@ -490,6 +490,8 @@ class ClaudeAdapter implements ToolAdapter {
|
|
|
490
490
|
})),
|
|
491
491
|
);
|
|
492
492
|
|
|
493
|
+
options?.onSessionCreated?.(() => session.close());
|
|
494
|
+
|
|
493
495
|
try {
|
|
494
496
|
await session.send(buildClaudePromptText(userText, undefined, sessionId));
|
|
495
497
|
for await (const raw of session.stream()) {
|
package/src/agent-stop-stuck.ts
CHANGED
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: "
|
|
95
|
-
element_id: "
|
|
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
|
-
|
|
313
|
-
|
|
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
|
|
327
|
-
return
|
|
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
|
-
|
|
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
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
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
|
-
|
|
366
|
-
.resize(AVATAR_SIZE, AVATAR_SIZE, { fit: "cover", position: "center" })
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
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:
|
|
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
|
|
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,
|
|
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 "${
|
|
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",
|
|
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;
|
|
@@ -73,6 +73,10 @@ export interface ActivePrompt {
|
|
|
73
73
|
abnormalExitNotified?: boolean;
|
|
74
74
|
/** Set when the resource monitor detects CPU + memory unchanged for 3 minutes. */
|
|
75
75
|
resourceStuck?: boolean;
|
|
76
|
+
/** Adapter-provided callback to close the underlying SDK session / subprocess.
|
|
77
|
+
* Called by stop-stuck-loop before controller.abort() to terminate the CLI
|
|
78
|
+
* process immediately, rather than waiting for the async generator to unblock. */
|
|
79
|
+
closeSession?: () => void;
|
|
76
80
|
}
|
|
77
81
|
|
|
78
82
|
export const activePrompts = new Map<string, ActivePrompt>();
|
package/src/session.ts
CHANGED
|
@@ -1110,6 +1110,10 @@ export async function runAgentSession(
|
|
|
1110
1110
|
clearPromptProcessMonitor(sessionId);
|
|
1111
1111
|
if (exitInfo.pid !== undefined) unregisterProcess(exitInfo.pid);
|
|
1112
1112
|
},
|
|
1113
|
+
onSessionCreated: (closeSession) => {
|
|
1114
|
+
const prompt = activePrompts.get(sessionId);
|
|
1115
|
+
if (prompt) prompt.closeSession = closeSession;
|
|
1116
|
+
},
|
|
1113
1117
|
})) {
|
|
1114
1118
|
for (const block of unifiedMsg.blocks) {
|
|
1115
1119
|
accumulateBlockContent(block, state, toolCallMap);
|