chatccc 0.2.216 → 0.2.218
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 +1 -1
- package/src/__tests__/feishu-avatar.test.ts +74 -11
- package/src/__tests__/orchestrator.test.ts +12 -0
- package/src/agent-delegate-task.ts +12 -2
- package/src/feishu-api.ts +81 -48
- package/src/orchestrator.ts +45 -23
- package/src/platform-adapter.ts +6 -5
- package/src/session.ts +20 -8
package/package.json
CHANGED
|
@@ -2,7 +2,8 @@ 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
|
|
|
5
|
-
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
5
|
+
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
6
|
+
import sharp from "sharp";
|
|
6
7
|
|
|
7
8
|
const mockConfig = {
|
|
8
9
|
cursor: {
|
|
@@ -68,14 +69,18 @@ function mockAvatarFetch(uploadedNames: string[], usageResponse: Response): void
|
|
|
68
69
|
}));
|
|
69
70
|
}
|
|
70
71
|
|
|
71
|
-
function mockAvatarUploadOnlyFetch(
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
72
|
+
function mockAvatarUploadOnlyFetch(
|
|
73
|
+
uploadedNames: string[],
|
|
74
|
+
uploadedImages: Buffer[] = [],
|
|
75
|
+
): ReturnType<typeof vi.fn> {
|
|
76
|
+
const fetchMock = vi.fn(async (url: string | URL | Request, init?: RequestInit) => {
|
|
77
|
+
const urlText = String(url);
|
|
78
|
+
if (urlText === "https://open.feishu.test/im/v1/images") {
|
|
79
|
+
const form = init?.body as FormData;
|
|
80
|
+
const image = form.get("image") as File;
|
|
81
|
+
uploadedNames.push(image.name);
|
|
82
|
+
uploadedImages.push(Buffer.from(await image.arrayBuffer()));
|
|
83
|
+
return new Response(JSON.stringify({ code: 0, data: { image_key: "img_test" } }), { status: 200 });
|
|
79
84
|
}
|
|
80
85
|
if (urlText === "https://open.feishu.test/im/v1/chats/chat_1") {
|
|
81
86
|
return new Response(JSON.stringify({ code: 0 }), { status: 200 });
|
|
@@ -197,7 +202,7 @@ describe("Codex avatar usage battery", () => {
|
|
|
197
202
|
}
|
|
198
203
|
});
|
|
199
204
|
|
|
200
|
-
it("uses provided Codex usage without fetching usage again", async () => {
|
|
205
|
+
it("uses provided Codex usage without fetching usage again", async () => {
|
|
201
206
|
const homeDir = await mkdtemp(join(tmpdir(), "chatccc-avatar-home-"));
|
|
202
207
|
const userDataDir = await mkdtemp(join(tmpdir(), "chatccc-avatar-data-"));
|
|
203
208
|
const uploadedNames: string[] = [];
|
|
@@ -220,7 +225,65 @@ describe("Codex avatar usage battery", () => {
|
|
|
220
225
|
await rm(homeDir, { recursive: true, force: true });
|
|
221
226
|
await rm(userDataDir, { recursive: true, force: true });
|
|
222
227
|
}
|
|
223
|
-
});
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
it("isolates Fast and standard Codex avatars in the upload cache", async () => {
|
|
231
|
+
const homeDir = await mkdtemp(join(tmpdir(), "chatccc-avatar-home-"));
|
|
232
|
+
const userDataDir = await mkdtemp(join(tmpdir(), "chatccc-avatar-data-"));
|
|
233
|
+
const uploadedNames: string[] = [];
|
|
234
|
+
mockAvatarUploadOnlyFetch(uploadedNames);
|
|
235
|
+
|
|
236
|
+
try {
|
|
237
|
+
const { setChatAvatar } = await loadFeishuApiWithHome(homeDir, userDataDir);
|
|
238
|
+
await setChatAvatar("tenant-token", "chat_1", "codex", "idle", {
|
|
239
|
+
codexUsage: null,
|
|
240
|
+
fastMode: false,
|
|
241
|
+
});
|
|
242
|
+
await setChatAvatar("tenant-token", "chat_1", "codex", "idle", {
|
|
243
|
+
codexUsage: null,
|
|
244
|
+
fastMode: true,
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
expect(uploadedNames).toEqual([
|
|
248
|
+
"avatar_codex_idle.jpg",
|
|
249
|
+
"avatar_codex_idle_fast.jpg",
|
|
250
|
+
]);
|
|
251
|
+
const cacheRaw = await readFile(join(userDataDir, "state", "avatar-image-keys.json"), "utf-8");
|
|
252
|
+
const cache = JSON.parse(cacheRaw) as Record<string, string>;
|
|
253
|
+
expect(cache["codex:idle:plain"]).toBe("img_test");
|
|
254
|
+
expect(cache["codex:idle:plain:fast-champagne-frame-v2"]).toBe("img_test");
|
|
255
|
+
} finally {
|
|
256
|
+
await rm(homeDir, { recursive: true, force: true });
|
|
257
|
+
await rm(userDataDir, { recursive: true, force: true });
|
|
258
|
+
}
|
|
259
|
+
});
|
|
260
|
+
|
|
261
|
+
it("renders an extra-thick champagne frame above the Codex badge in Fast mode", async () => {
|
|
262
|
+
const homeDir = await mkdtemp(join(tmpdir(), "chatccc-avatar-home-"));
|
|
263
|
+
const userDataDir = await mkdtemp(join(tmpdir(), "chatccc-avatar-data-"));
|
|
264
|
+
const uploadedNames: string[] = [];
|
|
265
|
+
const uploadedImages: Buffer[] = [];
|
|
266
|
+
mockAvatarUploadOnlyFetch(uploadedNames, uploadedImages);
|
|
267
|
+
|
|
268
|
+
try {
|
|
269
|
+
const { setChatAvatar } = await loadFeishuApiWithHome(homeDir, userDataDir);
|
|
270
|
+
await setChatAvatar("tenant-token", "chat_1", "codex", "idle", {
|
|
271
|
+
codexUsage: null,
|
|
272
|
+
fastMode: true,
|
|
273
|
+
});
|
|
274
|
+
|
|
275
|
+
expect(uploadedNames).toEqual(["avatar_codex_idle_fast.jpg"]);
|
|
276
|
+
const { data, info } = await sharp(uploadedImages[0]).raw().toBuffer({ resolveWithObject: true });
|
|
277
|
+
const pixelOffset = (144 * info.width + 200) * info.channels;
|
|
278
|
+
const [red, green, blue] = data.subarray(pixelOffset, pixelOffset + 3);
|
|
279
|
+
expect(red).toBeGreaterThan(220);
|
|
280
|
+
expect(green).toBeGreaterThan(150);
|
|
281
|
+
expect(blue).toBeLessThan(180);
|
|
282
|
+
} finally {
|
|
283
|
+
await rm(homeDir, { recursive: true, force: true });
|
|
284
|
+
await rm(userDataDir, { recursive: true, force: true });
|
|
285
|
+
}
|
|
286
|
+
});
|
|
224
287
|
|
|
225
288
|
it("returns both usage windows and available reset credit expiries", async () => {
|
|
226
289
|
const homeDir = await mkdtemp(join(tmpdir(), "chatccc-avatar-home-"));
|
|
@@ -681,9 +681,21 @@ describe("handleCommand WeChat processing ack", () => {
|
|
|
681
681
|
|
|
682
682
|
await handleCommand(platform, "/fast on", "feishu-codex", "ou-user", Date.now(), "p2p");
|
|
683
683
|
expect(getEffectiveFastModeForTool("codex", "sid-codex-fast")).toBe(true);
|
|
684
|
+
expect(platform.setChatAvatar).toHaveBeenLastCalledWith(
|
|
685
|
+
"feishu-codex",
|
|
686
|
+
"codex",
|
|
687
|
+
"idle",
|
|
688
|
+
{ fastMode: true },
|
|
689
|
+
);
|
|
684
690
|
|
|
685
691
|
await handleCommand(platform, "/fast off", "feishu-codex", "ou-user", Date.now(), "p2p");
|
|
686
692
|
expect(getEffectiveFastModeForTool("codex", "sid-codex-fast")).toBe(false);
|
|
693
|
+
expect(platform.setChatAvatar).toHaveBeenLastCalledWith(
|
|
694
|
+
"feishu-codex",
|
|
695
|
+
"codex",
|
|
696
|
+
"idle",
|
|
697
|
+
{ fastMode: false },
|
|
698
|
+
);
|
|
687
699
|
card = JSON.parse(
|
|
688
700
|
vi.mocked(platform.sendRawCard).mock.calls.at(-1)?.[1] ?? "{}",
|
|
689
701
|
) as { elements?: Array<{ tag: string; text?: { content: string } }> };
|
|
@@ -3,7 +3,13 @@ import { resolve } from "node:path";
|
|
|
3
3
|
import { sessionPrefixForTool, toolDisplayName, ts } from "./config.ts";
|
|
4
4
|
import { setDefaultCwd } from "./config.ts";
|
|
5
5
|
import type { PlatformAdapter } from "./platform-adapter.ts";
|
|
6
|
-
import {
|
|
6
|
+
import {
|
|
7
|
+
getEffectiveFastModeForTool,
|
|
8
|
+
initClaudeSession,
|
|
9
|
+
recordSessionRegistry,
|
|
10
|
+
resumeAndPrompt,
|
|
11
|
+
saveSessionTool,
|
|
12
|
+
} from "./session.ts";
|
|
7
13
|
import { bindChatToSession } from "./session-chat-binding.ts";
|
|
8
14
|
import { sessionChatName } from "./session-name.ts";
|
|
9
15
|
|
|
@@ -64,7 +70,11 @@ export async function delegateAgentTask(input: DelegateAgentTaskInput): Promise<
|
|
|
64
70
|
`下面会自动把任务作为第一句话发送给 ${toolLabel}。`,
|
|
65
71
|
"green",
|
|
66
72
|
).catch(() => {});
|
|
67
|
-
|
|
73
|
+
const fastMode = getEffectiveFastModeForTool(input.tool, sessionId);
|
|
74
|
+
const avatarUpdate = fastMode
|
|
75
|
+
? input.platform.setChatAvatar(chatId, input.tool, "new", { fastMode: true })
|
|
76
|
+
: input.platform.setChatAvatar(chatId, input.tool, "new");
|
|
77
|
+
avatarUpdate.catch(() => {});
|
|
68
78
|
|
|
69
79
|
await resumeAndPrompt(
|
|
70
80
|
sessionId,
|
package/src/feishu-api.ts
CHANGED
|
@@ -324,6 +324,7 @@ const AVATAR_BADGE_SIZE = 92;
|
|
|
324
324
|
const AVATAR_BADGE_MARGIN = 10;
|
|
325
325
|
const PLAIN_AVATAR_TOOL = "plain";
|
|
326
326
|
const CODEX_AVATAR_USAGE_STYLE_VERSION = "usage-window-aware-v14";
|
|
327
|
+
const CODEX_FAST_FRAME_STYLE_VERSION = "fast-champagne-frame-v2";
|
|
327
328
|
const CURSOR_AVATAR_USAGE_STYLE_VERSION = "usage-battery-v1";
|
|
328
329
|
|
|
329
330
|
export interface CodexUsageBalance {
|
|
@@ -371,18 +372,20 @@ function avatarCombinationPath(tool: string, status: string): string {
|
|
|
371
372
|
return resolvePath(AVATAR_COMBINATIONS_DIR, `avatar_${normalizeAvatarTool(tool)}_${normalizeAvatarStatus(status)}.png`);
|
|
372
373
|
}
|
|
373
374
|
|
|
374
|
-
function avatarCacheKey(
|
|
375
|
-
tool: string,
|
|
376
|
-
status: string,
|
|
377
|
-
codexUsage: CodexUsageSummary | null = null,
|
|
378
|
-
cursorBatteryPercent: number | null = null,
|
|
379
|
-
|
|
375
|
+
function avatarCacheKey(
|
|
376
|
+
tool: string,
|
|
377
|
+
status: string,
|
|
378
|
+
codexUsage: CodexUsageSummary | null = null,
|
|
379
|
+
cursorBatteryPercent: number | null = null,
|
|
380
|
+
fastMode = false,
|
|
381
|
+
): string {
|
|
380
382
|
const normalizedTool = normalizeAvatarTool(tool);
|
|
381
383
|
const normalizedStatus = normalizeAvatarStatus(status);
|
|
382
384
|
if (normalizedTool === "codex") {
|
|
383
|
-
|
|
385
|
+
const fastKey = fastMode ? `:${CODEX_FAST_FRAME_STYLE_VERSION}` : "";
|
|
386
|
+
if (!codexUsage?.weekly) return `${normalizedTool}:${normalizedStatus}:plain${fastKey}`;
|
|
384
387
|
const ringKey = codexUsage.fiveHour ? `:5h-ring:${codexUsage.fiveHour.remainingPercent}` : "";
|
|
385
|
-
return `${normalizedTool}:${normalizedStatus}:${CODEX_AVATAR_USAGE_STYLE_VERSION}:7d-battery:${codexUsage.weekly.remainingPercent}${ringKey}`;
|
|
388
|
+
return `${normalizedTool}:${normalizedStatus}:${CODEX_AVATAR_USAGE_STYLE_VERSION}:7d-battery:${codexUsage.weekly.remainingPercent}${ringKey}${fastKey}`;
|
|
386
389
|
}
|
|
387
390
|
if (normalizedTool === "cursor") {
|
|
388
391
|
return cursorBatteryPercent !== null
|
|
@@ -757,7 +760,10 @@ function buildCodexUsageRingSvg(remainingPercent: number): Buffer {
|
|
|
757
760
|
</svg>`);
|
|
758
761
|
}
|
|
759
762
|
|
|
760
|
-
async function buildAgentBadgeOverlay(
|
|
763
|
+
async function buildAgentBadgeOverlay(
|
|
764
|
+
tool: string,
|
|
765
|
+
margin = AVATAR_BADGE_MARGIN,
|
|
766
|
+
): Promise<sharp.OverlayOptions> {
|
|
761
767
|
const badge = await sharp(AVATAR_BADGES[tool])
|
|
762
768
|
.resize(AVATAR_BADGE_SIZE, AVATAR_BADGE_SIZE, {
|
|
763
769
|
fit: "contain",
|
|
@@ -768,26 +774,52 @@ async function buildAgentBadgeOverlay(tool: string): Promise<sharp.OverlayOption
|
|
|
768
774
|
.toBuffer();
|
|
769
775
|
return {
|
|
770
776
|
input: badge,
|
|
771
|
-
left: AVATAR_SIZE - AVATAR_BADGE_SIZE -
|
|
772
|
-
top: AVATAR_SIZE - AVATAR_BADGE_SIZE -
|
|
773
|
-
};
|
|
774
|
-
}
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
777
|
+
left: AVATAR_SIZE - AVATAR_BADGE_SIZE - margin,
|
|
778
|
+
top: AVATAR_SIZE - AVATAR_BADGE_SIZE - margin,
|
|
779
|
+
};
|
|
780
|
+
}
|
|
781
|
+
|
|
782
|
+
function buildCodexFastFrameOverlay(): sharp.OverlayOptions {
|
|
783
|
+
const size = AVATAR_BADGE_SIZE + 20;
|
|
784
|
+
const innerOffset = 9;
|
|
785
|
+
const innerSize = size - innerOffset * 2;
|
|
786
|
+
const frame = Buffer.from(`
|
|
787
|
+
<svg width="${size}" height="${size}" xmlns="http://www.w3.org/2000/svg">
|
|
788
|
+
<defs>
|
|
789
|
+
<linearGradient id="champagne" x1="0" y1="0" x2="${size}" y2="${size}" gradientUnits="userSpaceOnUse">
|
|
790
|
+
<stop offset="0" stop-color="#FFF0BE"/>
|
|
791
|
+
<stop offset="0.55" stop-color="#FFD56A"/>
|
|
792
|
+
<stop offset="1" stop-color="#EFA923"/>
|
|
793
|
+
</linearGradient>
|
|
794
|
+
</defs>
|
|
795
|
+
<rect x="0.5" y="0.5" width="${size - 1}" height="${size - 1}" rx="25" fill="url(#champagne)"/>
|
|
796
|
+
<rect x="${innerOffset}" y="${innerOffset}" width="${innerSize}" height="${innerSize}" rx="21" fill="#fffdf4"/>
|
|
797
|
+
</svg>`);
|
|
798
|
+
return {
|
|
799
|
+
input: frame,
|
|
800
|
+
left: AVATAR_SIZE - size - 1,
|
|
801
|
+
top: AVATAR_SIZE - size - 1,
|
|
802
|
+
};
|
|
803
|
+
}
|
|
804
|
+
|
|
805
|
+
async function renderAvatar(
|
|
806
|
+
tool: string,
|
|
807
|
+
status: string,
|
|
808
|
+
codexUsage: CodexUsageSummary | null = null,
|
|
809
|
+
cursorBatteryPercent: number | null = null,
|
|
810
|
+
fastMode = false,
|
|
811
|
+
): Promise<{ buffer: Buffer; contentType: string; filename: string }> {
|
|
782
812
|
const normalizedTool = normalizeAvatarTool(tool);
|
|
783
813
|
const normalizedStatus = normalizeAvatarStatus(status);
|
|
784
814
|
const composites: sharp.OverlayOptions[] = [];
|
|
785
815
|
const hasAgentBadge = normalizedTool !== PLAIN_AVATAR_TOOL;
|
|
816
|
+
const useFastCodexAvatar = normalizedTool === "codex" && fastMode;
|
|
786
817
|
|
|
787
818
|
const codexWeeklyUsage = normalizedTool === "codex" ? codexUsage?.weekly ?? null : null;
|
|
788
819
|
const useDynamicCodexAvatar = normalizedTool === "codex" && codexUsage !== null && codexWeeklyUsage !== null;
|
|
789
820
|
const useDynamicCursorAvatar = normalizedTool === "cursor" && cursorBatteryPercent !== null;
|
|
790
|
-
const
|
|
821
|
+
const useDynamicBadgeAvatar = useDynamicCodexAvatar || useDynamicCursorAvatar || useFastCodexAvatar;
|
|
822
|
+
const basePath = useDynamicBadgeAvatar
|
|
791
823
|
? AVATAR_SOURCES[normalizedStatus]
|
|
792
824
|
: hasAgentBadge
|
|
793
825
|
? avatarCombinationPath(normalizedTool, normalizedStatus)
|
|
@@ -797,16 +829,15 @@ async function renderAvatar(
|
|
|
797
829
|
if (codexUsage.fiveHour) {
|
|
798
830
|
composites.push({ input: buildCodexUsageRingSvg(codexUsage.fiveHour.remainingPercent), left: 0, top: 0 });
|
|
799
831
|
}
|
|
800
|
-
composites.push(
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
}
|
|
832
|
+
composites.push({ input: buildCodexUsageBatterySvg(codexWeeklyUsage.remainingPercent), left: 0, top: 0 });
|
|
833
|
+
} else if (useDynamicCursorAvatar) {
|
|
834
|
+
composites.push({ input: buildCodexUsageBatterySvg(cursorBatteryPercent), left: 0, top: 0 });
|
|
835
|
+
}
|
|
836
|
+
|
|
837
|
+
if (useDynamicBadgeAvatar) {
|
|
838
|
+
if (useFastCodexAvatar) composites.push(buildCodexFastFrameOverlay());
|
|
839
|
+
composites.push(await buildAgentBadgeOverlay(normalizedTool, useFastCodexAvatar ? 11 : AVATAR_BADGE_MARGIN));
|
|
840
|
+
}
|
|
810
841
|
|
|
811
842
|
let pipeline = sharp(await readFile(basePath))
|
|
812
843
|
.resize(AVATAR_SIZE, AVATAR_SIZE, { fit: "cover", position: "center" });
|
|
@@ -824,21 +855,22 @@ async function renderAvatar(
|
|
|
824
855
|
buffer: jpeg,
|
|
825
856
|
contentType: "image/jpeg",
|
|
826
857
|
filename: normalizedTool === "codex" && codexUsage?.weekly
|
|
827
|
-
? `avatar_${normalizedTool}_${normalizedStatus}_7d_${codexUsage.weekly.remainingPercent}${codexUsage.fiveHour ? `_5h_${codexUsage.fiveHour.remainingPercent}` : ""}.jpg`
|
|
858
|
+
? `avatar_${normalizedTool}_${normalizedStatus}${useFastCodexAvatar ? "_fast" : ""}_7d_${codexUsage.weekly.remainingPercent}${codexUsage.fiveHour ? `_5h_${codexUsage.fiveHour.remainingPercent}` : ""}.jpg`
|
|
828
859
|
: normalizedTool === "cursor" && cursorBatteryPercent !== null
|
|
829
860
|
? `avatar_${normalizedTool}_${normalizedStatus}_battery_${cursorBatteryPercent}.jpg`
|
|
830
|
-
: `avatar_${normalizedTool}_${normalizedStatus}.jpg`,
|
|
861
|
+
: `avatar_${normalizedTool}_${normalizedStatus}${useFastCodexAvatar ? "_fast" : ""}.jpg`,
|
|
831
862
|
};
|
|
832
863
|
}
|
|
833
864
|
|
|
834
|
-
async function uploadImage(
|
|
835
|
-
token: string,
|
|
836
|
-
tool: string,
|
|
837
|
-
status: string,
|
|
838
|
-
codexUsage: CodexUsageSummary | null = null,
|
|
839
|
-
cursorBatteryPercent: number | null = null,
|
|
840
|
-
|
|
841
|
-
|
|
865
|
+
async function uploadImage(
|
|
866
|
+
token: string,
|
|
867
|
+
tool: string,
|
|
868
|
+
status: string,
|
|
869
|
+
codexUsage: CodexUsageSummary | null = null,
|
|
870
|
+
cursorBatteryPercent: number | null = null,
|
|
871
|
+
fastMode = false,
|
|
872
|
+
): Promise<string> {
|
|
873
|
+
const image = await renderAvatar(tool, status, codexUsage, cursorBatteryPercent, fastMode);
|
|
842
874
|
const blob = new Blob([new Uint8Array(image.buffer)], { type: image.contentType });
|
|
843
875
|
const form = new FormData();
|
|
844
876
|
form.append("image_type", "avatar");
|
|
@@ -869,13 +901,14 @@ async function getOrUploadAvatarKey(
|
|
|
869
901
|
const normalizedTool = normalizeAvatarTool(tool);
|
|
870
902
|
const normalizedStatus = normalizeAvatarStatus(status);
|
|
871
903
|
const codexUsage = normalizedTool === "codex" ? await resolveCodexAvatarUsage(usageHints.codexUsage) : null;
|
|
872
|
-
const cursorBatteryPercent = normalizedTool === "cursor"
|
|
873
|
-
? await resolveCursorAvatarBatteryPercent(usageHints.cursorUsage)
|
|
874
|
-
: null;
|
|
875
|
-
const
|
|
876
|
-
const
|
|
877
|
-
|
|
878
|
-
|
|
904
|
+
const cursorBatteryPercent = normalizedTool === "cursor"
|
|
905
|
+
? await resolveCursorAvatarBatteryPercent(usageHints.cursorUsage)
|
|
906
|
+
: null;
|
|
907
|
+
const fastMode = normalizedTool === "codex" && usageHints.fastMode === true;
|
|
908
|
+
const keyName = avatarCacheKey(normalizedTool, normalizedStatus, codexUsage, cursorBatteryPercent, fastMode);
|
|
909
|
+
const cached = avatarKeyCache.get(keyName);
|
|
910
|
+
if (cached) return cached;
|
|
911
|
+
const key = await uploadImage(token, normalizedTool, normalizedStatus, codexUsage, cursorBatteryPercent, fastMode);
|
|
879
912
|
avatarKeyCache.set(keyName, key);
|
|
880
913
|
await persistAvatarKeyCache().catch((err) => {
|
|
881
914
|
console.error(`[${ts()}] [AVATAR] persist cache FAIL: ${(err as Error).message}`);
|
package/src/orchestrator.ts
CHANGED
|
@@ -340,6 +340,22 @@ function fastHelpAfterModel(tool: string): string {
|
|
|
340
340
|
: "";
|
|
341
341
|
}
|
|
342
342
|
|
|
343
|
+
function setChatAvatarForSession(
|
|
344
|
+
platform: PlatformAdapter,
|
|
345
|
+
chatId: string,
|
|
346
|
+
tool: string,
|
|
347
|
+
status: string,
|
|
348
|
+
sessionId?: string,
|
|
349
|
+
usageHints?: ChatAvatarUsageHints,
|
|
350
|
+
): Promise<void> {
|
|
351
|
+
const fastMode = getEffectiveFastModeForTool(tool, sessionId);
|
|
352
|
+
if (!usageHints && !fastMode) return platform.setChatAvatar(chatId, tool, status);
|
|
353
|
+
return platform.setChatAvatar(chatId, tool, status, {
|
|
354
|
+
...usageHints,
|
|
355
|
+
...(fastMode ? { fastMode: true } : {}),
|
|
356
|
+
});
|
|
357
|
+
}
|
|
358
|
+
|
|
343
359
|
async function sendFastModeStatus(
|
|
344
360
|
platform: PlatformAdapter,
|
|
345
361
|
chatId: string,
|
|
@@ -369,24 +385,26 @@ async function resolveUsageTarget(chatId: string): Promise<{ tool: "codex" | "cu
|
|
|
369
385
|
}
|
|
370
386
|
}
|
|
371
387
|
|
|
372
|
-
function refreshUsageAvatar(
|
|
373
|
-
platform: PlatformAdapter,
|
|
374
|
-
chatId: string,
|
|
375
|
-
tool: "codex" | "cursor",
|
|
376
|
-
status: "busy" | "idle",
|
|
377
|
-
usageHints: ChatAvatarUsageHints,
|
|
378
|
-
|
|
379
|
-
|
|
388
|
+
function refreshUsageAvatar(
|
|
389
|
+
platform: PlatformAdapter,
|
|
390
|
+
chatId: string,
|
|
391
|
+
tool: "codex" | "cursor",
|
|
392
|
+
status: "busy" | "idle",
|
|
393
|
+
usageHints: ChatAvatarUsageHints,
|
|
394
|
+
sessionId?: string,
|
|
395
|
+
): void {
|
|
396
|
+
setChatAvatarForSession(platform, chatId, tool, status, sessionId, usageHints).catch((err) => {
|
|
380
397
|
console.warn(`[${ts()}] [AVATAR] usage refresh failed: chatId=${chatId} tool=${tool} ${(err as Error).message}`);
|
|
381
398
|
});
|
|
382
399
|
}
|
|
383
400
|
|
|
384
|
-
async function sendUsageSummary(
|
|
385
|
-
platform: PlatformAdapter,
|
|
386
|
-
chatId: string,
|
|
387
|
-
tool: "codex" | "cursor",
|
|
388
|
-
avatarStatus: "busy" | "idle" = "idle",
|
|
389
|
-
|
|
401
|
+
async function sendUsageSummary(
|
|
402
|
+
platform: PlatformAdapter,
|
|
403
|
+
chatId: string,
|
|
404
|
+
tool: "codex" | "cursor",
|
|
405
|
+
avatarStatus: "busy" | "idle" = "idle",
|
|
406
|
+
sessionId?: string,
|
|
407
|
+
): Promise<void> {
|
|
390
408
|
if (tool === "cursor") {
|
|
391
409
|
const usage = await getCursorUsageSummary();
|
|
392
410
|
const content = formatCursorUsageSummary(usage);
|
|
@@ -395,7 +413,7 @@ async function sendUsageSummary(
|
|
|
395
413
|
} else {
|
|
396
414
|
await platform.sendCard(chatId, "Cursor Usage", content, "blue");
|
|
397
415
|
}
|
|
398
|
-
refreshUsageAvatar(platform, chatId, tool, avatarStatus, { cursorUsage: usage });
|
|
416
|
+
refreshUsageAvatar(platform, chatId, tool, avatarStatus, { cursorUsage: usage }, sessionId);
|
|
399
417
|
return;
|
|
400
418
|
}
|
|
401
419
|
|
|
@@ -411,7 +429,7 @@ async function sendUsageSummary(
|
|
|
411
429
|
} else {
|
|
412
430
|
await platform.sendCard(chatId, "Codex Usage", content, "blue");
|
|
413
431
|
}
|
|
414
|
-
refreshUsageAvatar(platform, chatId, tool, avatarStatus, { codexUsage: usage });
|
|
432
|
+
refreshUsageAvatar(platform, chatId, tool, avatarStatus, { codexUsage: usage }, sessionId);
|
|
415
433
|
}
|
|
416
434
|
|
|
417
435
|
async function sendUsageError(platform: PlatformAdapter, chatId: string, tool: "codex" | "cursor", err: unknown): Promise<void> {
|
|
@@ -585,7 +603,7 @@ async function resolveFeishuP2pAgent(
|
|
|
585
603
|
`检测到默认 Agent 已变化:**${previousLabel} → ${desiredLabel}**。\n\n已创建新的空白 ${desiredLabel} 私聊会话,并从本条消息开始使用。`,
|
|
586
604
|
"green",
|
|
587
605
|
).catch(() => {});
|
|
588
|
-
platform
|
|
606
|
+
setChatAvatarForSession(platform, chatId, desiredTool, "new", init.sessionId).catch(() => {});
|
|
589
607
|
return { kind: "ready", sessionId: init.sessionId, tool: desiredTool };
|
|
590
608
|
} catch (err) {
|
|
591
609
|
return {
|
|
@@ -817,7 +835,7 @@ export async function handleCommand(
|
|
|
817
835
|
const avatarStatus = usageTarget.sessionId && isSessionRunning(usageTarget.sessionId) ? "busy" : "idle";
|
|
818
836
|
logTrace(tid, "BRANCH", { cmd: "/usage", tool: usageTool });
|
|
819
837
|
try {
|
|
820
|
-
await sendUsageSummary(platform, chatId, usageTool, avatarStatus);
|
|
838
|
+
await sendUsageSummary(platform, chatId, usageTool, avatarStatus, usageTarget.sessionId);
|
|
821
839
|
logTrace(tid, "DONE", { outcome: "usage", tool: usageTool });
|
|
822
840
|
} catch (err) {
|
|
823
841
|
await sendUsageError(platform, chatId, usageTool, err);
|
|
@@ -1149,7 +1167,7 @@ export async function handleCommand(
|
|
|
1149
1167
|
sessionId,
|
|
1150
1168
|
tool,
|
|
1151
1169
|
});
|
|
1152
|
-
platform
|
|
1170
|
+
setChatAvatarForSession(platform, newChatId, tool, "new", sessionId).catch(() => {});
|
|
1153
1171
|
console.log(`${"=".repeat(60)}`);
|
|
1154
1172
|
return;
|
|
1155
1173
|
}
|
|
@@ -1544,9 +1562,7 @@ export async function handleCommand(
|
|
|
1544
1562
|
);
|
|
1545
1563
|
}
|
|
1546
1564
|
|
|
1547
|
-
platform
|
|
1548
|
-
.setChatAvatar(chatId, descriptionTool, "new")
|
|
1549
|
-
.catch(() => {});
|
|
1565
|
+
setChatAvatarForSession(platform, chatId, descriptionTool, "new", newSessionId).catch(() => {});
|
|
1550
1566
|
|
|
1551
1567
|
await platform.sendCard(
|
|
1552
1568
|
chatId,
|
|
@@ -1716,7 +1732,7 @@ export async function handleCommand(
|
|
|
1716
1732
|
);
|
|
1717
1733
|
}
|
|
1718
1734
|
|
|
1719
|
-
platform
|
|
1735
|
+
setChatAvatarForSession(platform, chatId, target.tool, "new", target.sessionId).catch(() => {});
|
|
1720
1736
|
|
|
1721
1737
|
const targetToolLabel = toolDisplayName(target.tool);
|
|
1722
1738
|
const busyNote = isSessionRunning(target.sessionId)
|
|
@@ -1772,6 +1788,12 @@ export async function handleCommand(
|
|
|
1772
1788
|
}
|
|
1773
1789
|
const enabled = getEffectiveFastModeForTool("codex", sessionId);
|
|
1774
1790
|
await sendFastModeStatus(platform, chatId, enabled).catch(() => {});
|
|
1791
|
+
if (fastArg) {
|
|
1792
|
+
const avatarStatus = isSessionRunning(sessionId) ? "busy" : "idle";
|
|
1793
|
+
await platform.setChatAvatar(chatId, "codex", avatarStatus, { fastMode: enabled }).catch((err) => {
|
|
1794
|
+
console.warn(`[${ts()}] [AVATAR] Fast mode refresh failed: chatId=${chatId} ${(err as Error).message}`);
|
|
1795
|
+
});
|
|
1796
|
+
}
|
|
1775
1797
|
logTrace(tid, "DONE", {
|
|
1776
1798
|
outcome: fastArg ? "fast_switched" : "fast_query",
|
|
1777
1799
|
enabled,
|
package/src/platform-adapter.ts
CHANGED
|
@@ -8,10 +8,11 @@
|
|
|
8
8
|
import type { CursorUsageSummary } from "./cursor-usage.ts";
|
|
9
9
|
import type { CodexUsageSummary } from "./feishu-api.ts";
|
|
10
10
|
|
|
11
|
-
export interface ChatAvatarUsageHints {
|
|
12
|
-
codexUsage?: CodexUsageSummary | null;
|
|
13
|
-
cursorUsage?: CursorUsageSummary | null;
|
|
14
|
-
|
|
11
|
+
export interface ChatAvatarUsageHints {
|
|
12
|
+
codexUsage?: CodexUsageSummary | null;
|
|
13
|
+
cursorUsage?: CursorUsageSummary | null;
|
|
14
|
+
fastMode?: boolean;
|
|
15
|
+
}
|
|
15
16
|
|
|
16
17
|
export interface PlatformAdapter {
|
|
17
18
|
/** 平台标识,用于区分不同平台的行为(如 wechat、feishu 等) */
|
|
@@ -66,4 +67,4 @@ export interface PlatformAdapter {
|
|
|
66
67
|
|
|
67
68
|
/** 更新已发送的进度展示,sequence 保证有序 */
|
|
68
69
|
cardUpdate(cardId: string, cardJson: string, sequence: number): Promise<void>;
|
|
69
|
-
}
|
|
70
|
+
}
|
package/src/session.ts
CHANGED
|
@@ -538,6 +538,18 @@ export function getEffectiveFastModeForTool(tool: string, sessionId?: string): b
|
|
|
538
538
|
}
|
|
539
539
|
return config.codex.fastMode;
|
|
540
540
|
}
|
|
541
|
+
|
|
542
|
+
function setSessionChatAvatar(
|
|
543
|
+
platform: PlatformAdapter,
|
|
544
|
+
chatId: string,
|
|
545
|
+
tool: string,
|
|
546
|
+
status: string,
|
|
547
|
+
sessionId: string,
|
|
548
|
+
): Promise<void> {
|
|
549
|
+
return getEffectiveFastModeForTool(tool, sessionId)
|
|
550
|
+
? platform.setChatAvatar(chatId, tool, status, { fastMode: true })
|
|
551
|
+
: platform.setChatAvatar(chatId, tool, status);
|
|
552
|
+
}
|
|
541
553
|
|
|
542
554
|
/** 为指定 session 设置模型覆盖(/model <name>) */
|
|
543
555
|
export function setSessionModelOverride(sessionId: string, model: string): void {
|
|
@@ -1290,7 +1302,7 @@ export async function runAgentSession(
|
|
|
1290
1302
|
if (displayCards.get(displayChatId) !== display) {
|
|
1291
1303
|
const finalStatus = turnFinalStatus(prevState.status);
|
|
1292
1304
|
finalizeTurnCards(sessionId, prevState.turnCount, finalStatus).catch(() => {});
|
|
1293
|
-
pp
|
|
1305
|
+
setSessionChatAvatar(pp, displayChatId, prevState.tool, "idle", sessionId).catch(() => {});
|
|
1294
1306
|
} else {
|
|
1295
1307
|
const nextSeq = display.sequence + 1;
|
|
1296
1308
|
const { title: headerTitle, template: headerTemplate } = formatTerminalHeader(prevState.status);
|
|
@@ -1310,7 +1322,7 @@ export async function runAgentSession(
|
|
|
1310
1322
|
if (prevTerminalReply && stillOursAfterUpdate && !isFinalReplySentForTurn(prevState)) {
|
|
1311
1323
|
await sendFinalReplyTextOnce(pp, displayChatId, sessionId, prevState.turnCount, prevTerminalReply);
|
|
1312
1324
|
}
|
|
1313
|
-
pp
|
|
1325
|
+
setSessionChatAvatar(pp, displayChatId, prevState.tool, "idle", sessionId).catch(() => {});
|
|
1314
1326
|
}
|
|
1315
1327
|
} else if (pp && prevTerminalReply && !isFinalReplySentForTurn(prevState)) {
|
|
1316
1328
|
// 无 display 记录但上一轮有 finalReply(极快轮次),至少发送
|
|
@@ -1375,7 +1387,7 @@ export async function runAgentSession(
|
|
|
1375
1387
|
// 设置最后活跃群头像为 busy
|
|
1376
1388
|
const activeCid = getLastActiveChat(sessionId) ?? getChatsForSession(sessionId)[0];
|
|
1377
1389
|
if (activeCid) {
|
|
1378
|
-
platform
|
|
1390
|
+
setSessionChatAvatar(platform, activeCid, tool, "busy", sessionId).catch(() => {});
|
|
1379
1391
|
}
|
|
1380
1392
|
|
|
1381
1393
|
const state: AccumulatorState = {
|
|
@@ -1665,7 +1677,7 @@ export async function runAgentSession(
|
|
|
1665
1677
|
const active1 = getLastActiveChat(sessionId) ?? finalizationChatIds[0];
|
|
1666
1678
|
if (active1) {
|
|
1667
1679
|
await platform.sendText(active1, "会话已停止。").catch(() => {});
|
|
1668
|
-
platform
|
|
1680
|
+
setSessionChatAvatar(platform, active1, tool, "idle", sessionId).catch(() => {});
|
|
1669
1681
|
}
|
|
1670
1682
|
console.log(`[${ts()}] Session ${sessionId} stopped (content chunks: ${state.chunkCount})`);
|
|
1671
1683
|
if (tid) logTrace(tid, "SESSION_END", { sessionId, outcome: "stopped", chunks: state.chunkCount });
|
|
@@ -1695,7 +1707,7 @@ export async function runAgentSession(
|
|
|
1695
1707
|
formatAutoEndedReply(finalReplyToWrite),
|
|
1696
1708
|
);
|
|
1697
1709
|
}
|
|
1698
|
-
pp
|
|
1710
|
+
setSessionChatAvatar(pp, activeAutoEnded, tool, "idle", sessionId).catch(() => {});
|
|
1699
1711
|
|
|
1700
1712
|
if (wasAutoRecovery) {
|
|
1701
1713
|
// 这是紧接第一次停滞而启动的恢复轮;再次发生相同停滞即终止
|
|
@@ -1730,7 +1742,7 @@ export async function runAgentSession(
|
|
|
1730
1742
|
});
|
|
1731
1743
|
}
|
|
1732
1744
|
const activeErr = getLastActiveChat(sessionId) ?? finalizationChatIds[0];
|
|
1733
|
-
if (activeErr) platform
|
|
1745
|
+
if (activeErr) setSessionChatAvatar(platform, activeErr, tool, "idle", sessionId).catch(() => {});
|
|
1734
1746
|
console.log(`[${ts()}] Session ${sessionId} process exited unexpectedly (content chunks: ${state.chunkCount})`);
|
|
1735
1747
|
if (tid) logTrace(tid, "SESSION_END", { sessionId, outcome: "process_missing", chunks: state.chunkCount });
|
|
1736
1748
|
} else {
|
|
@@ -1753,7 +1765,7 @@ export async function runAgentSession(
|
|
|
1753
1765
|
const pp = platformForChat(active2) ?? platform;
|
|
1754
1766
|
await sendFinalReplyTextOnce(pp, active2, sessionId, nextTurnCount, finalReply);
|
|
1755
1767
|
}
|
|
1756
|
-
platform
|
|
1768
|
+
setSessionChatAvatar(platform, active2, tool, "idle", sessionId).catch(() => {});
|
|
1757
1769
|
}
|
|
1758
1770
|
console.log(`[${ts()}] Session ${sessionId} stream complete (content chunks: ${state.chunkCount})`);
|
|
1759
1771
|
if (tid) logTrace(tid, "SESSION_END", { sessionId, chunks: state.chunkCount, finalTextLen: finalReply.length });
|
|
@@ -1996,7 +2008,7 @@ export function startUnifiedDisplayLoop(): void {
|
|
|
1996
2008
|
finalizeTurnCards(sessionId, state.turnCount, finalSt).catch(() => {});
|
|
1997
2009
|
displayCards.delete(chatId);
|
|
1998
2010
|
}
|
|
1999
|
-
p
|
|
2011
|
+
setSessionChatAvatar(p, chatId, state.tool, "idle", sessionId).catch(() => {});
|
|
2000
2012
|
console.log(`[${ts()}] [DISPLAY] unified loop deleted display for ${chatId} (terminal: ${state.status})`);
|
|
2001
2013
|
} else {
|
|
2002
2014
|
// running: 创建或更新展示
|