chatccc 0.2.174 → 0.2.176
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/README.md +3 -3
- package/agent-prompts/claude_specific.md +45 -45
- package/agent-prompts/codex_specific.md +2 -2
- package/agent-prompts/cursor_specific.md +2 -2
- package/im-skills/feishu-skill/receive-send-file.md +63 -63
- package/im-skills/feishu-skill/receive-send-image.md +24 -24
- package/package.json +1 -1
- package/src/__tests__/cardkit.test.ts +60 -60
- package/src/__tests__/claude-adapter.test.ts +592 -592
- package/src/__tests__/config-reload.test.ts +18 -18
- package/src/__tests__/feishu-api.test.ts +60 -60
- package/src/__tests__/feishu-avatar.test.ts +129 -129
- package/src/__tests__/feishu-platform.test.ts +16 -16
- package/src/__tests__/format-message.test.ts +272 -272
- package/src/__tests__/orchestrator.test.ts +93 -93
- package/src/__tests__/privacy.test.ts +67 -12
- package/src/__tests__/web-ui.test.ts +92 -92
- package/src/adapters/claude-adapter.ts +566 -566
- package/src/adapters/claude-session-meta-store.ts +120 -120
- package/src/agent-stop-stuck.ts +129 -129
- package/src/cards.ts +4 -4
- package/src/feishu-api.ts +220 -219
- package/src/feishu-platform.ts +15 -15
- package/src/format-message.ts +213 -213
- package/src/litellm-proxy.ts +374 -374
- package/src/orchestrator.ts +112 -112
- package/src/privacy.ts +89 -39
- package/src/session-chat-binding.ts +6 -6
- package/src/sim-platform.ts +14 -14
- package/src/web-ui.ts +6 -6
|
@@ -8,11 +8,11 @@ import {
|
|
|
8
8
|
APP_ID,
|
|
9
9
|
APP_SECRET,
|
|
10
10
|
CHATCCC_PORT,
|
|
11
|
-
CLAUDE_API_KEY,
|
|
12
|
-
CLAUDE_BASE_URL,
|
|
13
|
-
CLAUDE_EFFORT,
|
|
14
|
-
CLAUDE_MAX_TURN,
|
|
15
|
-
CLAUDE_MODEL,
|
|
11
|
+
CLAUDE_API_KEY,
|
|
12
|
+
CLAUDE_BASE_URL,
|
|
13
|
+
CLAUDE_EFFORT,
|
|
14
|
+
CLAUDE_MAX_TURN,
|
|
15
|
+
CLAUDE_MODEL,
|
|
16
16
|
CLAUDE_SUBAGENT_MODEL,
|
|
17
17
|
CURSOR_AGENT_ARGS,
|
|
18
18
|
CURSOR_AGENT_COMMAND,
|
|
@@ -82,7 +82,7 @@ describe("applyLoadedConfig — 刷新 export let 常量", () => {
|
|
|
82
82
|
expect(APP_SECRET).toBe("NEW_APP_SECRET");
|
|
83
83
|
});
|
|
84
84
|
|
|
85
|
-
it("更新 Claude 配置(model / subagentModel / effort / maxTurn / apiKey / baseUrl)", () => {
|
|
85
|
+
it("更新 Claude 配置(model / subagentModel / effort / maxTurn / apiKey / baseUrl)", () => {
|
|
86
86
|
applyLoadedConfig({
|
|
87
87
|
...structuredClone(baseAppConfig),
|
|
88
88
|
claude: {
|
|
@@ -90,18 +90,18 @@ describe("applyLoadedConfig — 刷新 export let 常量", () => {
|
|
|
90
90
|
defaultAgent: true,
|
|
91
91
|
model: "claude-sonnet-4-6",
|
|
92
92
|
subagentModel: "claude-haiku-4-5-20251001",
|
|
93
|
-
effort: "high",
|
|
94
|
-
apiKey: "sk-test",
|
|
95
|
-
baseUrl: "https://api.example.com",
|
|
96
|
-
maxTurn: 7,
|
|
97
|
-
},
|
|
98
|
-
});
|
|
99
|
-
|
|
100
|
-
expect(CLAUDE_MODEL).toBe("claude-sonnet-4-6");
|
|
101
|
-
expect(CLAUDE_SUBAGENT_MODEL).toBe("claude-haiku-4-5-20251001");
|
|
102
|
-
expect(CLAUDE_EFFORT).toBe("high");
|
|
103
|
-
expect(CLAUDE_MAX_TURN).toBe(7);
|
|
104
|
-
expect(CLAUDE_API_KEY).toBe("sk-test");
|
|
93
|
+
effort: "high",
|
|
94
|
+
apiKey: "sk-test",
|
|
95
|
+
baseUrl: "https://api.example.com",
|
|
96
|
+
maxTurn: 7,
|
|
97
|
+
},
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
expect(CLAUDE_MODEL).toBe("claude-sonnet-4-6");
|
|
101
|
+
expect(CLAUDE_SUBAGENT_MODEL).toBe("claude-haiku-4-5-20251001");
|
|
102
|
+
expect(CLAUDE_EFFORT).toBe("high");
|
|
103
|
+
expect(CLAUDE_MAX_TURN).toBe(7);
|
|
104
|
+
expect(CLAUDE_API_KEY).toBe("sk-test");
|
|
105
105
|
expect(CLAUDE_BASE_URL).toBe("https://api.example.com");
|
|
106
106
|
});
|
|
107
107
|
|
|
@@ -1,60 +1,60 @@
|
|
|
1
|
-
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
2
|
-
|
|
3
|
-
import { sendTextReply } from "../feishu-api.ts";
|
|
4
|
-
|
|
5
|
-
function abortError(): Error {
|
|
6
|
-
const err = new Error("aborted");
|
|
7
|
-
err.name = "AbortError";
|
|
8
|
-
return err;
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
describe("sendTextReply timeout", () => {
|
|
12
|
-
afterEach(() => {
|
|
13
|
-
vi.useRealTimers();
|
|
14
|
-
vi.restoreAllMocks();
|
|
15
|
-
vi.unstubAllGlobals();
|
|
16
|
-
});
|
|
17
|
-
|
|
18
|
-
it("aborts a hung text send request", async () => {
|
|
19
|
-
vi.useFakeTimers();
|
|
20
|
-
vi.spyOn(console, "error").mockImplementation(() => {});
|
|
21
|
-
|
|
22
|
-
const fetchMock = vi.fn((_url: Parameters<typeof fetch>[0], init?: Parameters<typeof fetch>[1]) => (
|
|
23
|
-
new Promise<Response>((_resolve, reject) => {
|
|
24
|
-
init?.signal?.addEventListener("abort", () => reject(abortError()));
|
|
25
|
-
})
|
|
26
|
-
));
|
|
27
|
-
vi.stubGlobal("fetch", fetchMock);
|
|
28
|
-
|
|
29
|
-
const send = expect(sendTextReply("token", "chat-1", "hello"))
|
|
30
|
-
.resolves.toBe(false);
|
|
31
|
-
await vi.advanceTimersByTimeAsync(15_000);
|
|
32
|
-
|
|
33
|
-
await send;
|
|
34
|
-
expect(fetchMock).toHaveBeenCalledWith(
|
|
35
|
-
expect.stringContaining("/im/v1/messages?receive_id_type=chat_id"),
|
|
36
|
-
expect.objectContaining({ signal: expect.any(AbortSignal) }),
|
|
37
|
-
);
|
|
38
|
-
});
|
|
39
|
-
|
|
40
|
-
it("also aborts when response body reading hangs", async () => {
|
|
41
|
-
vi.useFakeTimers();
|
|
42
|
-
vi.spyOn(console, "error").mockImplementation(() => {});
|
|
43
|
-
|
|
44
|
-
const fetchMock = vi.fn((_url: Parameters<typeof fetch>[0], init?: Parameters<typeof fetch>[1]) => (
|
|
45
|
-
Promise.resolve({
|
|
46
|
-
status: 200,
|
|
47
|
-
text: () => new Promise<string>((_resolve, reject) => {
|
|
48
|
-
init?.signal?.addEventListener("abort", () => reject(abortError()));
|
|
49
|
-
}),
|
|
50
|
-
} as Response)
|
|
51
|
-
));
|
|
52
|
-
vi.stubGlobal("fetch", fetchMock);
|
|
53
|
-
|
|
54
|
-
const send = expect(sendTextReply("token", "chat-2", "hello"))
|
|
55
|
-
.resolves.toBe(false);
|
|
56
|
-
await vi.advanceTimersByTimeAsync(15_000);
|
|
57
|
-
|
|
58
|
-
await send;
|
|
59
|
-
});
|
|
60
|
-
});
|
|
1
|
+
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
2
|
+
|
|
3
|
+
import { sendTextReply } from "../feishu-api.ts";
|
|
4
|
+
|
|
5
|
+
function abortError(): Error {
|
|
6
|
+
const err = new Error("aborted");
|
|
7
|
+
err.name = "AbortError";
|
|
8
|
+
return err;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
describe("sendTextReply timeout", () => {
|
|
12
|
+
afterEach(() => {
|
|
13
|
+
vi.useRealTimers();
|
|
14
|
+
vi.restoreAllMocks();
|
|
15
|
+
vi.unstubAllGlobals();
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
it("aborts a hung text send request", async () => {
|
|
19
|
+
vi.useFakeTimers();
|
|
20
|
+
vi.spyOn(console, "error").mockImplementation(() => {});
|
|
21
|
+
|
|
22
|
+
const fetchMock = vi.fn((_url: Parameters<typeof fetch>[0], init?: Parameters<typeof fetch>[1]) => (
|
|
23
|
+
new Promise<Response>((_resolve, reject) => {
|
|
24
|
+
init?.signal?.addEventListener("abort", () => reject(abortError()));
|
|
25
|
+
})
|
|
26
|
+
));
|
|
27
|
+
vi.stubGlobal("fetch", fetchMock);
|
|
28
|
+
|
|
29
|
+
const send = expect(sendTextReply("token", "chat-1", "hello"))
|
|
30
|
+
.resolves.toBe(false);
|
|
31
|
+
await vi.advanceTimersByTimeAsync(15_000);
|
|
32
|
+
|
|
33
|
+
await send;
|
|
34
|
+
expect(fetchMock).toHaveBeenCalledWith(
|
|
35
|
+
expect.stringContaining("/im/v1/messages?receive_id_type=chat_id"),
|
|
36
|
+
expect.objectContaining({ signal: expect.any(AbortSignal) }),
|
|
37
|
+
);
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it("also aborts when response body reading hangs", async () => {
|
|
41
|
+
vi.useFakeTimers();
|
|
42
|
+
vi.spyOn(console, "error").mockImplementation(() => {});
|
|
43
|
+
|
|
44
|
+
const fetchMock = vi.fn((_url: Parameters<typeof fetch>[0], init?: Parameters<typeof fetch>[1]) => (
|
|
45
|
+
Promise.resolve({
|
|
46
|
+
status: 200,
|
|
47
|
+
text: () => new Promise<string>((_resolve, reject) => {
|
|
48
|
+
init?.signal?.addEventListener("abort", () => reject(abortError()));
|
|
49
|
+
}),
|
|
50
|
+
} as Response)
|
|
51
|
+
));
|
|
52
|
+
vi.stubGlobal("fetch", fetchMock);
|
|
53
|
+
|
|
54
|
+
const send = expect(sendTextReply("token", "chat-2", "hello"))
|
|
55
|
+
.resolves.toBe(false);
|
|
56
|
+
await vi.advanceTimersByTimeAsync(15_000);
|
|
57
|
+
|
|
58
|
+
await send;
|
|
59
|
+
});
|
|
60
|
+
});
|
|
@@ -1,129 +1,129 @@
|
|
|
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 weekly battery and 5h ring percentages 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: {
|
|
71
|
-
primary_window: { used_percent: 37 },
|
|
72
|
-
secondary_window: { used_percent: 12 },
|
|
73
|
-
},
|
|
74
|
-
}), { status: 200 }));
|
|
75
|
-
|
|
76
|
-
try {
|
|
77
|
-
const { setChatAvatar } = await loadFeishuApiWithHome(homeDir, userDataDir);
|
|
78
|
-
await setChatAvatar("tenant-token", "chat_1", "codex", "busy");
|
|
79
|
-
|
|
80
|
-
expect(uploadedNames).toEqual(["avatar_codex_busy_week_88_5h_63.jpg"]);
|
|
81
|
-
} finally {
|
|
82
|
-
await rm(homeDir, { recursive: true, force: true });
|
|
83
|
-
await rm(userDataDir, { recursive: true, force: true });
|
|
84
|
-
}
|
|
85
|
-
});
|
|
86
|
-
|
|
87
|
-
it("returns both 5h and weekly Codex usage windows", async () => {
|
|
88
|
-
const homeDir = await mkdtemp(join(tmpdir(), "chatccc-avatar-home-"));
|
|
89
|
-
const userDataDir = await mkdtemp(join(tmpdir(), "chatccc-avatar-data-"));
|
|
90
|
-
const uploadedNames: string[] = [];
|
|
91
|
-
await writeCodexAuth(homeDir);
|
|
92
|
-
mockAvatarFetch(uploadedNames, new Response(JSON.stringify({
|
|
93
|
-
rate_limit: {
|
|
94
|
-
primary_window: { used_percent: 37, reset_after_seconds: 10349, reset_at: 1781528212 },
|
|
95
|
-
secondary_window: { used_percent: 12, reset_after_seconds: 325063, reset_at: 1781842926 },
|
|
96
|
-
},
|
|
97
|
-
}), { status: 200 }));
|
|
98
|
-
|
|
99
|
-
try {
|
|
100
|
-
const { getCodexUsageSummary } = await loadFeishuApiWithHome(homeDir, userDataDir);
|
|
101
|
-
await expect(getCodexUsageSummary()).resolves.toEqual({
|
|
102
|
-
fiveHour: { usedPercent: 37, remainingPercent: 63, resetAfterSeconds: 10349, resetAtEpochSeconds: 1781528212 },
|
|
103
|
-
weekly: { usedPercent: 12, remainingPercent: 88, resetAfterSeconds: 325063, resetAtEpochSeconds: 1781842926 },
|
|
104
|
-
});
|
|
105
|
-
} finally {
|
|
106
|
-
await rm(homeDir, { recursive: true, force: true });
|
|
107
|
-
await rm(userDataDir, { recursive: true, force: true });
|
|
108
|
-
}
|
|
109
|
-
});
|
|
110
|
-
|
|
111
|
-
it("falls back to the pre-combined Codex avatar when usage lookup fails", async () => {
|
|
112
|
-
const homeDir = await mkdtemp(join(tmpdir(), "chatccc-avatar-home-"));
|
|
113
|
-
const userDataDir = await mkdtemp(join(tmpdir(), "chatccc-avatar-data-"));
|
|
114
|
-
const uploadedNames: string[] = [];
|
|
115
|
-
await writeCodexAuth(homeDir);
|
|
116
|
-
vi.spyOn(console, "warn").mockImplementation(() => {});
|
|
117
|
-
mockAvatarFetch(uploadedNames, new Response("fail", { status: 500 }));
|
|
118
|
-
|
|
119
|
-
try {
|
|
120
|
-
const { setChatAvatar } = await loadFeishuApiWithHome(homeDir, userDataDir);
|
|
121
|
-
await setChatAvatar("tenant-token", "chat_1", "codex", "idle");
|
|
122
|
-
|
|
123
|
-
expect(uploadedNames).toEqual(["avatar_codex_idle.jpg"]);
|
|
124
|
-
} finally {
|
|
125
|
-
await rm(homeDir, { recursive: true, force: true });
|
|
126
|
-
await rm(userDataDir, { recursive: true, force: true });
|
|
127
|
-
}
|
|
128
|
-
});
|
|
129
|
-
});
|
|
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 weekly battery and 5h ring percentages 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: {
|
|
71
|
+
primary_window: { used_percent: 37 },
|
|
72
|
+
secondary_window: { used_percent: 12 },
|
|
73
|
+
},
|
|
74
|
+
}), { status: 200 }));
|
|
75
|
+
|
|
76
|
+
try {
|
|
77
|
+
const { setChatAvatar } = await loadFeishuApiWithHome(homeDir, userDataDir);
|
|
78
|
+
await setChatAvatar("tenant-token", "chat_1", "codex", "busy");
|
|
79
|
+
|
|
80
|
+
expect(uploadedNames).toEqual(["avatar_codex_busy_week_88_5h_63.jpg"]);
|
|
81
|
+
} finally {
|
|
82
|
+
await rm(homeDir, { recursive: true, force: true });
|
|
83
|
+
await rm(userDataDir, { recursive: true, force: true });
|
|
84
|
+
}
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
it("returns both 5h and weekly Codex usage windows", async () => {
|
|
88
|
+
const homeDir = await mkdtemp(join(tmpdir(), "chatccc-avatar-home-"));
|
|
89
|
+
const userDataDir = await mkdtemp(join(tmpdir(), "chatccc-avatar-data-"));
|
|
90
|
+
const uploadedNames: string[] = [];
|
|
91
|
+
await writeCodexAuth(homeDir);
|
|
92
|
+
mockAvatarFetch(uploadedNames, new Response(JSON.stringify({
|
|
93
|
+
rate_limit: {
|
|
94
|
+
primary_window: { used_percent: 37, reset_after_seconds: 10349, reset_at: 1781528212 },
|
|
95
|
+
secondary_window: { used_percent: 12, reset_after_seconds: 325063, reset_at: 1781842926 },
|
|
96
|
+
},
|
|
97
|
+
}), { status: 200 }));
|
|
98
|
+
|
|
99
|
+
try {
|
|
100
|
+
const { getCodexUsageSummary } = await loadFeishuApiWithHome(homeDir, userDataDir);
|
|
101
|
+
await expect(getCodexUsageSummary()).resolves.toEqual({
|
|
102
|
+
fiveHour: { usedPercent: 37, remainingPercent: 63, resetAfterSeconds: 10349, resetAtEpochSeconds: 1781528212 },
|
|
103
|
+
weekly: { usedPercent: 12, remainingPercent: 88, resetAfterSeconds: 325063, resetAtEpochSeconds: 1781842926 },
|
|
104
|
+
});
|
|
105
|
+
} finally {
|
|
106
|
+
await rm(homeDir, { recursive: true, force: true });
|
|
107
|
+
await rm(userDataDir, { recursive: true, force: true });
|
|
108
|
+
}
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
it("falls back to the pre-combined Codex avatar when usage lookup fails", async () => {
|
|
112
|
+
const homeDir = await mkdtemp(join(tmpdir(), "chatccc-avatar-home-"));
|
|
113
|
+
const userDataDir = await mkdtemp(join(tmpdir(), "chatccc-avatar-data-"));
|
|
114
|
+
const uploadedNames: string[] = [];
|
|
115
|
+
await writeCodexAuth(homeDir);
|
|
116
|
+
vi.spyOn(console, "warn").mockImplementation(() => {});
|
|
117
|
+
mockAvatarFetch(uploadedNames, new Response("fail", { status: 500 }));
|
|
118
|
+
|
|
119
|
+
try {
|
|
120
|
+
const { setChatAvatar } = await loadFeishuApiWithHome(homeDir, userDataDir);
|
|
121
|
+
await setChatAvatar("tenant-token", "chat_1", "codex", "idle");
|
|
122
|
+
|
|
123
|
+
expect(uploadedNames).toEqual(["avatar_codex_idle.jpg"]);
|
|
124
|
+
} finally {
|
|
125
|
+
await rm(homeDir, { recursive: true, force: true });
|
|
126
|
+
await rm(userDataDir, { recursive: true, force: true });
|
|
127
|
+
}
|
|
128
|
+
});
|
|
129
|
+
});
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { describe, it, expect, beforeAll, afterAll } from "vitest";
|
|
2
|
-
import { getPlatform, setPlatform, getTenantAccessToken, getChatInfo, createGroupChat, updateChatInfo, sendTextReply, sendCardReply, sendRawCard, sendPostMessage, extractSessionInfo, formatDelayNotice, reportPermissionResults, verifyAllPermissions, addReaction, recallMessage, updateCardMessage, setChatAvatar, getCodexUsageSummary, disbandChat, sendRestartCard, getMergeForwardMessages } from "../feishu-platform.ts";
|
|
2
|
+
import { getPlatform, setPlatform, getTenantAccessToken, getChatInfo, createGroupChat, updateChatInfo, sendTextReply, sendCardReply, sendRawCard, sendPostMessage, extractSessionInfo, formatDelayNotice, reportPermissionResults, verifyAllPermissions, addReaction, recallMessage, updateCardMessage, setChatAvatar, getCodexUsageSummary, disbandChat, sendRestartCard, getMergeForwardMessages } from "../feishu-platform.ts";
|
|
3
3
|
import type { FeishuPlatform } from "../feishu-platform.ts";
|
|
4
4
|
|
|
5
5
|
const realPlatform = getPlatform();
|
|
@@ -26,13 +26,13 @@ describe("feishu-platform", () => {
|
|
|
26
26
|
createGroupChat: async () => "mock_chat",
|
|
27
27
|
updateChatInfo: async () => {},
|
|
28
28
|
getChatInfo: async () => ({ name: "x", description: "y" }),
|
|
29
|
-
disbandChat: async () => {},
|
|
30
|
-
setChatAvatar: async () => {},
|
|
31
|
-
getCodexUsageSummary: async () => ({
|
|
32
|
-
fiveHour: { usedPercent: 10, remainingPercent: 90, resetAtEpochSeconds: 1781528212, resetAfterSeconds: 10349 },
|
|
33
|
-
weekly: { usedPercent: 20, remainingPercent: 80, resetAtEpochSeconds: 1781842926, resetAfterSeconds: 325063 },
|
|
34
|
-
}),
|
|
35
|
-
getOrDownloadImage: async () => "/tmp/img.png",
|
|
29
|
+
disbandChat: async () => {},
|
|
30
|
+
setChatAvatar: async () => {},
|
|
31
|
+
getCodexUsageSummary: async () => ({
|
|
32
|
+
fiveHour: { usedPercent: 10, remainingPercent: 90, resetAtEpochSeconds: 1781528212, resetAfterSeconds: 10349 },
|
|
33
|
+
weekly: { usedPercent: 20, remainingPercent: 80, resetAtEpochSeconds: 1781842926, resetAfterSeconds: 325063 },
|
|
34
|
+
}),
|
|
35
|
+
getOrDownloadImage: async () => "/tmp/img.png",
|
|
36
36
|
verifyAllPermissions: async () => [],
|
|
37
37
|
reportPermissionResults: realPlatform.reportPermissionResults,
|
|
38
38
|
extractSessionInfo: realPlatform.extractSessionInfo,
|
|
@@ -50,13 +50,13 @@ describe("feishu-platform", () => {
|
|
|
50
50
|
expect(await getChatInfo("t", "mock_chat")).toEqual({ name: "x", description: "y" });
|
|
51
51
|
const perms = await verifyAllPermissions("t");
|
|
52
52
|
expect(perms).toEqual([]);
|
|
53
|
-
const mergeMsgs = await getMergeForwardMessages("t", "om_test");
|
|
54
|
-
expect(mergeMsgs).toEqual([]);
|
|
55
|
-
expect(await getCodexUsageSummary()).toEqual({
|
|
56
|
-
fiveHour: { usedPercent: 10, remainingPercent: 90, resetAtEpochSeconds: 1781528212, resetAfterSeconds: 10349 },
|
|
57
|
-
weekly: { usedPercent: 20, remainingPercent: 80, resetAtEpochSeconds: 1781842926, resetAfterSeconds: 325063 },
|
|
58
|
-
});
|
|
59
|
-
} finally {
|
|
53
|
+
const mergeMsgs = await getMergeForwardMessages("t", "om_test");
|
|
54
|
+
expect(mergeMsgs).toEqual([]);
|
|
55
|
+
expect(await getCodexUsageSummary()).toEqual({
|
|
56
|
+
fiveHour: { usedPercent: 10, remainingPercent: 90, resetAtEpochSeconds: 1781528212, resetAfterSeconds: 10349 },
|
|
57
|
+
weekly: { usedPercent: 20, remainingPercent: 80, resetAtEpochSeconds: 1781842926, resetAfterSeconds: 325063 },
|
|
58
|
+
});
|
|
59
|
+
} finally {
|
|
60
60
|
// 恢复到真实实现,避免影响后续测试
|
|
61
61
|
setPlatform(realPlatform);
|
|
62
62
|
}
|
|
@@ -66,4 +66,4 @@ describe("feishu-platform", () => {
|
|
|
66
66
|
setPlatform(realPlatform);
|
|
67
67
|
expect(getPlatform()).toBe(realPlatform);
|
|
68
68
|
});
|
|
69
|
-
});
|
|
69
|
+
});
|