chatccc 0.2.187 → 0.2.189
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/agent-prompts/claude_specific.md +45 -45
- package/agent-prompts/codex_specific.md +2 -2
- package/agent-prompts/cursor_specific.md +13 -13
- package/config.sample.json +5 -0
- package/im-skills/feishu-skill/receive-send-file.md +63 -63
- package/im-skills/feishu-skill/receive-send-image.md +24 -24
- package/im-skills/feishu-skill/skill.md +3 -3
- package/im-skills/wechat-file-skill/receive-send-file.md +38 -38
- package/im-skills/wechat-file-skill/send-file.mjs +83 -83
- package/im-skills/wechat-file-skill/skill.md +10 -10
- package/im-skills/wechat-image-skill/skill.md +10 -10
- package/im-skills/wechat-video-skill/receive-send-video.md +38 -38
- package/im-skills/wechat-video-skill/send-video.mjs +79 -79
- package/im-skills/wechat-video-skill/skill.md +10 -10
- package/package.json +1 -1
- package/scripts/postinstall-sharp-check.mjs +58 -58
- package/src/__tests__/agent-delegate-task-rpc.test.ts +165 -165
- package/src/__tests__/builtin-config.test.ts +33 -0
- package/src/__tests__/card-plain-text.test.ts +5 -5
- package/src/__tests__/cardkit.test.ts +60 -60
- package/src/__tests__/cards.test.ts +77 -77
- package/src/__tests__/chatgpt-subscription-rpc.test.ts +89 -89
- package/src/__tests__/chatgpt-subscription.test.ts +135 -135
- package/src/__tests__/chrome-devtools-guard.test.ts +165 -125
- package/src/__tests__/claude-adapter.test.ts +592 -592
- package/src/__tests__/codex-reset-actions.test.ts +146 -146
- package/src/__tests__/config-reload.test.ts +1 -0
- package/src/__tests__/config-sample.test.ts +11 -0
- package/src/__tests__/feishu-api.test.ts +60 -60
- package/src/__tests__/feishu-avatar.test.ts +180 -115
- package/src/__tests__/feishu-platform.test.ts +22 -22
- package/src/__tests__/format-message.test.ts +47 -47
- package/src/__tests__/orchestrator.test.ts +150 -2
- package/src/__tests__/privacy.test.ts +198 -198
- package/src/__tests__/raw-stream-log.test.ts +106 -106
- package/src/__tests__/session.test.ts +10 -0
- package/src/__tests__/shared-prefix.test.ts +36 -36
- package/src/__tests__/stream-state.test.ts +42 -42
- package/src/__tests__/web-ui.test.ts +209 -130
- package/src/adapters/claude-adapter.ts +566 -566
- package/src/adapters/claude-session-meta-store.ts +120 -120
- package/src/adapters/codex-adapter.ts +10 -6
- package/src/adapters/cursor-adapter.ts +46 -46
- package/src/adapters/raw-stream-log.ts +124 -124
- package/src/adapters/resource-monitor.ts +140 -140
- package/src/agent-delegate-task-rpc.ts +153 -153
- package/src/agent-delegate-task.ts +81 -81
- package/src/agent-stop-stuck.ts +129 -129
- package/src/builtin/cli.ts +189 -197
- package/src/builtin/index.ts +168 -167
- package/src/cards.ts +130 -89
- package/src/chatgpt-subscription-rpc.ts +27 -27
- package/src/chatgpt-subscription.ts +299 -299
- package/src/chrome-devtools-guard.ts +318 -242
- package/src/codex-reset-actions.ts +184 -184
- package/src/config.ts +38 -0
- package/src/feishu-api.ts +219 -190
- package/src/feishu-platform.ts +20 -20
- package/src/format-message.ts +293 -293
- package/src/index.ts +2 -2
- package/src/litellm-proxy.ts +374 -374
- package/src/orchestrator.ts +201 -9
- package/src/platform-adapter.ts +9 -1
- package/src/privacy.ts +118 -118
- package/src/session-chat-binding.ts +6 -6
- package/src/session-name.ts +8 -8
- package/src/session.ts +44 -16
- package/src/shared-prefix.ts +29 -29
- package/src/sim-platform.ts +20 -20
- package/src/turn-cards.ts +117 -117
- package/src/web-ui.ts +142 -24
|
@@ -39,23 +39,23 @@ async function loadFeishuApiWithHome(homeDir: string, userDataDir: string) {
|
|
|
39
39
|
async function writeCodexAuth(homeDir: string): Promise<void> {
|
|
40
40
|
const codexDir = join(homeDir, ".codex");
|
|
41
41
|
await mkdir(codexDir, { recursive: true });
|
|
42
|
-
await writeFile(
|
|
43
|
-
join(codexDir, "auth.json"),
|
|
44
|
-
JSON.stringify({ tokens: { access_token: "codex-access-token", account_id: "codex-account-id" } }),
|
|
45
|
-
"utf-8",
|
|
46
|
-
);
|
|
47
|
-
}
|
|
42
|
+
await writeFile(
|
|
43
|
+
join(codexDir, "auth.json"),
|
|
44
|
+
JSON.stringify({ tokens: { access_token: "codex-access-token", account_id: "codex-account-id" } }),
|
|
45
|
+
"utf-8",
|
|
46
|
+
);
|
|
47
|
+
}
|
|
48
48
|
|
|
49
49
|
function mockAvatarFetch(uploadedNames: string[], usageResponse: Response): void {
|
|
50
50
|
vi.stubGlobal("fetch", vi.fn(async (url: string | URL | Request, init?: RequestInit) => {
|
|
51
51
|
const urlText = String(url);
|
|
52
|
-
if (urlText === "https://chatgpt.com/backend-api/wham/usage") {
|
|
53
|
-
return usageResponse.clone();
|
|
54
|
-
}
|
|
55
|
-
if (urlText === "https://chatgpt.com/backend-api/wham/rate-limit-reset-credits") {
|
|
56
|
-
return new Response(JSON.stringify({ credits: [] }), { status: 200 });
|
|
57
|
-
}
|
|
58
|
-
if (urlText === "https://open.feishu.test/im/v1/images") {
|
|
52
|
+
if (urlText === "https://chatgpt.com/backend-api/wham/usage") {
|
|
53
|
+
return usageResponse.clone();
|
|
54
|
+
}
|
|
55
|
+
if (urlText === "https://chatgpt.com/backend-api/wham/rate-limit-reset-credits") {
|
|
56
|
+
return new Response(JSON.stringify({ credits: [] }), { status: 200 });
|
|
57
|
+
}
|
|
58
|
+
if (urlText === "https://open.feishu.test/im/v1/images") {
|
|
59
59
|
const form = init?.body as FormData;
|
|
60
60
|
const image = form.get("image") as File;
|
|
61
61
|
uploadedNames.push(image.name);
|
|
@@ -68,6 +68,24 @@ function mockAvatarFetch(uploadedNames: string[], usageResponse: Response): void
|
|
|
68
68
|
}));
|
|
69
69
|
}
|
|
70
70
|
|
|
71
|
+
function mockAvatarUploadOnlyFetch(uploadedNames: string[]): ReturnType<typeof vi.fn> {
|
|
72
|
+
const fetchMock = vi.fn(async (url: string | URL | Request, init?: RequestInit) => {
|
|
73
|
+
const urlText = String(url);
|
|
74
|
+
if (urlText === "https://open.feishu.test/im/v1/images") {
|
|
75
|
+
const form = init?.body as FormData;
|
|
76
|
+
const image = form.get("image") as File;
|
|
77
|
+
uploadedNames.push(image.name);
|
|
78
|
+
return new Response(JSON.stringify({ code: 0, data: { image_key: "img_test" } }), { status: 200 });
|
|
79
|
+
}
|
|
80
|
+
if (urlText === "https://open.feishu.test/im/v1/chats/chat_1") {
|
|
81
|
+
return new Response(JSON.stringify({ code: 0 }), { status: 200 });
|
|
82
|
+
}
|
|
83
|
+
throw new Error(`unexpected fetch: ${urlText}`);
|
|
84
|
+
});
|
|
85
|
+
vi.stubGlobal("fetch", fetchMock);
|
|
86
|
+
return fetchMock;
|
|
87
|
+
}
|
|
88
|
+
|
|
71
89
|
describe("Codex avatar usage battery", () => {
|
|
72
90
|
afterEach(() => {
|
|
73
91
|
vi.unstubAllGlobals();
|
|
@@ -103,109 +121,134 @@ describe("Codex avatar usage battery", () => {
|
|
|
103
121
|
}
|
|
104
122
|
});
|
|
105
123
|
|
|
106
|
-
it("
|
|
107
|
-
const homeDir = await mkdtemp(join(tmpdir(), "chatccc-avatar-home-"));
|
|
108
|
-
const userDataDir = await mkdtemp(join(tmpdir(), "chatccc-avatar-data-"));
|
|
109
|
-
|
|
110
|
-
const fetchMock =
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
},
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
);
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
|
|
124
|
+
it("uses provided Codex usage without fetching usage again", async () => {
|
|
125
|
+
const homeDir = await mkdtemp(join(tmpdir(), "chatccc-avatar-home-"));
|
|
126
|
+
const userDataDir = await mkdtemp(join(tmpdir(), "chatccc-avatar-data-"));
|
|
127
|
+
const uploadedNames: string[] = [];
|
|
128
|
+
const fetchMock = mockAvatarUploadOnlyFetch(uploadedNames);
|
|
129
|
+
|
|
130
|
+
try {
|
|
131
|
+
const { setChatAvatar } = await loadFeishuApiWithHome(homeDir, userDataDir);
|
|
132
|
+
await setChatAvatar("tenant-token", "chat_1", "codex", "busy", {
|
|
133
|
+
codexUsage: {
|
|
134
|
+
fiveHour: { usedPercent: 37, remainingPercent: 63, resetAtEpochSeconds: null, resetAfterSeconds: null },
|
|
135
|
+
weekly: { usedPercent: 12, remainingPercent: 88, resetAtEpochSeconds: null, resetAfterSeconds: null },
|
|
136
|
+
rateLimitResetCreditsAvailable: null,
|
|
137
|
+
rateLimitResetCredits: null,
|
|
138
|
+
},
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
expect(uploadedNames).toEqual(["avatar_codex_busy_week_88_5h_63.jpg"]);
|
|
142
|
+
expect(fetchMock).not.toHaveBeenCalledWith("https://chatgpt.com/backend-api/wham/usage", expect.anything());
|
|
143
|
+
} finally {
|
|
144
|
+
await rm(homeDir, { recursive: true, force: true });
|
|
145
|
+
await rm(userDataDir, { recursive: true, force: true });
|
|
146
|
+
}
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
it("returns both usage windows and available reset credit expiries", async () => {
|
|
150
|
+
const homeDir = await mkdtemp(join(tmpdir(), "chatccc-avatar-home-"));
|
|
151
|
+
const userDataDir = await mkdtemp(join(tmpdir(), "chatccc-avatar-data-"));
|
|
152
|
+
await writeCodexAuth(homeDir);
|
|
153
|
+
const fetchMock = vi.fn(async (url: string | URL | Request) => {
|
|
154
|
+
const urlText = String(url);
|
|
155
|
+
if (urlText === "https://chatgpt.com/backend-api/wham/usage") {
|
|
156
|
+
return new Response(JSON.stringify({
|
|
157
|
+
rate_limit: {
|
|
158
|
+
primary_window: { used_percent: 37, reset_after_seconds: 10349, reset_at: 1781528212 },
|
|
159
|
+
secondary_window: { used_percent: 12, reset_after_seconds: 325063, reset_at: 1781842926 },
|
|
160
|
+
},
|
|
161
|
+
rate_limit_reset_credits: { available_count: 1 },
|
|
162
|
+
}), { status: 200 });
|
|
163
|
+
}
|
|
164
|
+
if (urlText === "https://chatgpt.com/backend-api/wham/rate-limit-reset-credits") {
|
|
165
|
+
return new Response(JSON.stringify({
|
|
166
|
+
available_count: 2,
|
|
167
|
+
credits: [
|
|
168
|
+
{
|
|
169
|
+
status: "available",
|
|
170
|
+
granted_at: "2026-06-12T04:01:47.770016Z",
|
|
171
|
+
expires_at: "2026-07-12T04:01:47.770016Z",
|
|
172
|
+
},
|
|
173
|
+
{
|
|
174
|
+
status: "redeemed",
|
|
175
|
+
granted_at: "2026-06-13T04:01:47.770016Z",
|
|
176
|
+
expires_at: "2026-07-13T04:01:47.770016Z",
|
|
177
|
+
},
|
|
178
|
+
{
|
|
179
|
+
status: "available",
|
|
180
|
+
granted_at: "2026-06-18T00:44:23.904386Z",
|
|
181
|
+
expires_at: "2026-07-18T00:44:23.904386Z",
|
|
182
|
+
},
|
|
183
|
+
],
|
|
184
|
+
}), { status: 200 });
|
|
185
|
+
}
|
|
186
|
+
throw new Error(`unexpected fetch: ${urlText}`);
|
|
187
|
+
});
|
|
188
|
+
vi.stubGlobal("fetch", fetchMock);
|
|
189
|
+
|
|
190
|
+
try {
|
|
191
|
+
const { getCodexUsageSummary } = await loadFeishuApiWithHome(homeDir, userDataDir);
|
|
192
|
+
await expect(getCodexUsageSummary()).resolves.toEqual({
|
|
193
|
+
fiveHour: { usedPercent: 37, remainingPercent: 63, resetAfterSeconds: 10349, resetAtEpochSeconds: 1781528212 },
|
|
194
|
+
weekly: { usedPercent: 12, remainingPercent: 88, resetAfterSeconds: 325063, resetAtEpochSeconds: 1781842926 },
|
|
195
|
+
rateLimitResetCreditsAvailable: 2,
|
|
196
|
+
rateLimitResetCredits: [
|
|
197
|
+
{ grantedAt: "2026-06-12T04:01:47.770016Z", expiresAt: "2026-07-12T04:01:47.770016Z" },
|
|
198
|
+
{ grantedAt: "2026-06-18T00:44:23.904386Z", expiresAt: "2026-07-18T00:44:23.904386Z" },
|
|
199
|
+
],
|
|
200
|
+
});
|
|
201
|
+
expect(fetchMock).toHaveBeenCalledWith(
|
|
202
|
+
"https://chatgpt.com/backend-api/wham/rate-limit-reset-credits",
|
|
203
|
+
expect.objectContaining({
|
|
204
|
+
headers: expect.objectContaining({
|
|
205
|
+
Authorization: "Bearer codex-access-token",
|
|
206
|
+
"ChatGPT-Account-ID": "codex-account-id",
|
|
207
|
+
"OpenAI-Beta": "codex-1",
|
|
208
|
+
originator: "Codex Desktop",
|
|
209
|
+
}),
|
|
210
|
+
}),
|
|
211
|
+
);
|
|
212
|
+
} finally {
|
|
213
|
+
await rm(homeDir, { recursive: true, force: true });
|
|
214
|
+
await rm(userDataDir, { recursive: true, force: true });
|
|
215
|
+
}
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
it("consumes a Codex rate-limit reset credit through the WHAM endpoint", async () => {
|
|
219
|
+
const homeDir = await mkdtemp(join(tmpdir(), "chatccc-avatar-home-"));
|
|
220
|
+
const userDataDir = await mkdtemp(join(tmpdir(), "chatccc-avatar-data-"));
|
|
221
|
+
await writeCodexAuth(homeDir);
|
|
222
|
+
const fetchMock = vi.fn(async () => new Response(JSON.stringify({
|
|
223
|
+
code: "reset",
|
|
224
|
+
windows_reset: 2,
|
|
225
|
+
}), { status: 200 }));
|
|
226
|
+
vi.stubGlobal("fetch", fetchMock);
|
|
227
|
+
|
|
228
|
+
try {
|
|
229
|
+
const { consumeCodexRateLimitResetCredit } = await loadFeishuApiWithHome(homeDir, userDataDir);
|
|
230
|
+
await expect(consumeCodexRateLimitResetCredit("request-1")).resolves.toEqual({
|
|
231
|
+
code: "reset",
|
|
232
|
+
windowsReset: 2,
|
|
233
|
+
});
|
|
234
|
+
expect(fetchMock).toHaveBeenCalledWith(
|
|
235
|
+
"https://chatgpt.com/backend-api/wham/rate-limit-reset-credits/consume",
|
|
236
|
+
expect.objectContaining({
|
|
237
|
+
method: "POST",
|
|
238
|
+
headers: expect.objectContaining({
|
|
239
|
+
Authorization: "Bearer codex-access-token",
|
|
240
|
+
"Content-Type": "application/json",
|
|
241
|
+
}),
|
|
242
|
+
body: JSON.stringify({ redeem_request_id: "request-1" }),
|
|
243
|
+
}),
|
|
244
|
+
);
|
|
245
|
+
} finally {
|
|
246
|
+
await rm(homeDir, { recursive: true, force: true });
|
|
247
|
+
await rm(userDataDir, { recursive: true, force: true });
|
|
172
248
|
}
|
|
173
|
-
});
|
|
174
|
-
|
|
175
|
-
it("
|
|
176
|
-
const homeDir = await mkdtemp(join(tmpdir(), "chatccc-avatar-home-"));
|
|
177
|
-
const userDataDir = await mkdtemp(join(tmpdir(), "chatccc-avatar-data-"));
|
|
178
|
-
await writeCodexAuth(homeDir);
|
|
179
|
-
const fetchMock = vi.fn(async () => new Response(JSON.stringify({
|
|
180
|
-
code: "reset",
|
|
181
|
-
windows_reset: 2,
|
|
182
|
-
}), { status: 200 }));
|
|
183
|
-
vi.stubGlobal("fetch", fetchMock);
|
|
184
|
-
|
|
185
|
-
try {
|
|
186
|
-
const { consumeCodexRateLimitResetCredit } = await loadFeishuApiWithHome(homeDir, userDataDir);
|
|
187
|
-
await expect(consumeCodexRateLimitResetCredit("request-1")).resolves.toEqual({
|
|
188
|
-
code: "reset",
|
|
189
|
-
windowsReset: 2,
|
|
190
|
-
});
|
|
191
|
-
expect(fetchMock).toHaveBeenCalledWith(
|
|
192
|
-
"https://chatgpt.com/backend-api/wham/rate-limit-reset-credits/consume",
|
|
193
|
-
expect.objectContaining({
|
|
194
|
-
method: "POST",
|
|
195
|
-
headers: expect.objectContaining({
|
|
196
|
-
Authorization: "Bearer codex-access-token",
|
|
197
|
-
"Content-Type": "application/json",
|
|
198
|
-
}),
|
|
199
|
-
body: JSON.stringify({ redeem_request_id: "request-1" }),
|
|
200
|
-
}),
|
|
201
|
-
);
|
|
202
|
-
} finally {
|
|
203
|
-
await rm(homeDir, { recursive: true, force: true });
|
|
204
|
-
await rm(userDataDir, { recursive: true, force: true });
|
|
205
|
-
}
|
|
206
|
-
});
|
|
207
|
-
|
|
208
|
-
it("falls back to the pre-combined Codex avatar when usage lookup fails", async () => {
|
|
249
|
+
});
|
|
250
|
+
|
|
251
|
+
it("falls back to the pre-combined Codex avatar when usage lookup fails", async () => {
|
|
209
252
|
const homeDir = await mkdtemp(join(tmpdir(), "chatccc-avatar-home-"));
|
|
210
253
|
const userDataDir = await mkdtemp(join(tmpdir(), "chatccc-avatar-data-"));
|
|
211
254
|
const uploadedNames: string[] = [];
|
|
@@ -257,6 +300,28 @@ describe("Cursor avatar usage battery", () => {
|
|
|
257
300
|
}
|
|
258
301
|
});
|
|
259
302
|
|
|
303
|
+
it("uses provided Cursor usage without fetching usage again", async () => {
|
|
304
|
+
const homeDir = await mkdtemp(join(tmpdir(), "chatccc-avatar-home-"));
|
|
305
|
+
const userDataDir = await mkdtemp(join(tmpdir(), "chatccc-avatar-data-"));
|
|
306
|
+
const uploadedNames: string[] = [];
|
|
307
|
+
mockAvatarUploadOnlyFetch(uploadedNames);
|
|
308
|
+
|
|
309
|
+
try {
|
|
310
|
+
const { setChatAvatar } = await loadFeishuApiWithHome(homeDir, userDataDir);
|
|
311
|
+
await setChatAvatar("tenant-token", "chat_1", "cursor", "idle", {
|
|
312
|
+
cursorUsage: {
|
|
313
|
+
planUsage: { apiPercentUsed: 30 },
|
|
314
|
+
},
|
|
315
|
+
});
|
|
316
|
+
|
|
317
|
+
expect(uploadedNames).toEqual(["avatar_cursor_idle_battery_70.jpg"]);
|
|
318
|
+
expect(getCursorUsageSummaryMock).not.toHaveBeenCalled();
|
|
319
|
+
} finally {
|
|
320
|
+
await rm(homeDir, { recursive: true, force: true });
|
|
321
|
+
await rm(userDataDir, { recursive: true, force: true });
|
|
322
|
+
}
|
|
323
|
+
});
|
|
324
|
+
|
|
260
325
|
it("uses On-Demand budget for Cursor avatar battery when configured", async () => {
|
|
261
326
|
const homeDir = await mkdtemp(join(tmpdir(), "chatccc-avatar-home-"));
|
|
262
327
|
const userDataDir = await mkdtemp(join(tmpdir(), "chatccc-avatar-data-"));
|
|
@@ -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, consumeCodexRateLimitResetCredit, 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, consumeCodexRateLimitResetCredit, disbandChat, sendRestartCard, getMergeForwardMessages } from "../feishu-platform.ts";
|
|
3
3
|
import type { FeishuPlatform } from "../feishu-platform.ts";
|
|
4
4
|
|
|
5
5
|
const realPlatform = getPlatform();
|
|
@@ -26,16 +26,16 @@ 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
|
-
rateLimitResetCreditsAvailable: 1,
|
|
35
|
-
rateLimitResetCredits: [{ grantedAt: null, expiresAt: "2026-07-12T04:01:47.770016Z" }],
|
|
36
|
-
}),
|
|
37
|
-
consumeCodexRateLimitResetCredit: async () => ({ code: "reset", windowsReset: 2 }),
|
|
38
|
-
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
|
+
rateLimitResetCreditsAvailable: 1,
|
|
35
|
+
rateLimitResetCredits: [{ grantedAt: null, expiresAt: "2026-07-12T04:01:47.770016Z" }],
|
|
36
|
+
}),
|
|
37
|
+
consumeCodexRateLimitResetCredit: async () => ({ code: "reset", windowsReset: 2 }),
|
|
38
|
+
getOrDownloadImage: async () => "/tmp/img.png",
|
|
39
39
|
verifyAllPermissions: async () => [],
|
|
40
40
|
reportPermissionResults: realPlatform.reportPermissionResults,
|
|
41
41
|
extractSessionInfo: realPlatform.extractSessionInfo,
|
|
@@ -53,16 +53,16 @@ describe("feishu-platform", () => {
|
|
|
53
53
|
expect(await getChatInfo("t", "mock_chat")).toEqual({ name: "x", description: "y" });
|
|
54
54
|
const perms = await verifyAllPermissions("t");
|
|
55
55
|
expect(perms).toEqual([]);
|
|
56
|
-
const mergeMsgs = await getMergeForwardMessages("t", "om_test");
|
|
57
|
-
expect(mergeMsgs).toEqual([]);
|
|
58
|
-
expect(await getCodexUsageSummary()).toEqual({
|
|
59
|
-
fiveHour: { usedPercent: 10, remainingPercent: 90, resetAtEpochSeconds: 1781528212, resetAfterSeconds: 10349 },
|
|
60
|
-
weekly: { usedPercent: 20, remainingPercent: 80, resetAtEpochSeconds: 1781842926, resetAfterSeconds: 325063 },
|
|
61
|
-
rateLimitResetCreditsAvailable: 1,
|
|
62
|
-
rateLimitResetCredits: [{ grantedAt: null, expiresAt: "2026-07-12T04:01:47.770016Z" }],
|
|
63
|
-
});
|
|
64
|
-
expect(await consumeCodexRateLimitResetCredit("request-1")).toEqual({ code: "reset", windowsReset: 2 });
|
|
65
|
-
} finally {
|
|
56
|
+
const mergeMsgs = await getMergeForwardMessages("t", "om_test");
|
|
57
|
+
expect(mergeMsgs).toEqual([]);
|
|
58
|
+
expect(await getCodexUsageSummary()).toEqual({
|
|
59
|
+
fiveHour: { usedPercent: 10, remainingPercent: 90, resetAtEpochSeconds: 1781528212, resetAfterSeconds: 10349 },
|
|
60
|
+
weekly: { usedPercent: 20, remainingPercent: 80, resetAtEpochSeconds: 1781842926, resetAfterSeconds: 325063 },
|
|
61
|
+
rateLimitResetCreditsAvailable: 1,
|
|
62
|
+
rateLimitResetCredits: [{ grantedAt: null, expiresAt: "2026-07-12T04:01:47.770016Z" }],
|
|
63
|
+
});
|
|
64
|
+
expect(await consumeCodexRateLimitResetCredit("request-1")).toEqual({ code: "reset", windowsReset: 2 });
|
|
65
|
+
} finally {
|
|
66
66
|
// 恢复到真实实现,避免影响后续测试
|
|
67
67
|
setPlatform(realPlatform);
|
|
68
68
|
}
|
|
@@ -72,4 +72,4 @@ describe("feishu-platform", () => {
|
|
|
72
72
|
setPlatform(realPlatform);
|
|
73
73
|
expect(getPlatform()).toBe(realPlatform);
|
|
74
74
|
});
|
|
75
|
-
});
|
|
75
|
+
});
|
|
@@ -11,52 +11,52 @@ vi.mock("../feishu-platform.ts", () => ({
|
|
|
11
11
|
getMergeForwardMessages: (...args: unknown[]) => mockGetMergeForwardMessages(...args),
|
|
12
12
|
}));
|
|
13
13
|
|
|
14
|
-
import { formatMessageContent, formatPostContent, formatMergeForward } from "../format-message.ts";
|
|
15
|
-
|
|
16
|
-
describe("formatMessageContent mixed post images", () => {
|
|
17
|
-
beforeEach(() => {
|
|
18
|
-
vi.clearAllMocks();
|
|
19
|
-
mockGetTenantAccessToken.mockResolvedValue("mock_token");
|
|
20
|
-
});
|
|
21
|
-
|
|
22
|
-
it("includes downloaded images from mixed post messages", async () => {
|
|
23
|
-
mockGetOrDownloadImage.mockResolvedValue("C:\\tmp\\img_001.png");
|
|
24
|
-
|
|
25
|
-
const result = await formatMessageContent({
|
|
26
|
-
message_id: "om_post",
|
|
27
|
-
message_type: "post",
|
|
28
|
-
content: JSON.stringify({
|
|
29
|
-
content: [[
|
|
30
|
-
{ tag: "text", text: "use this image" },
|
|
31
|
-
{ tag: "img", image_key: "img_001" },
|
|
32
|
-
]],
|
|
33
|
-
}),
|
|
34
|
-
});
|
|
35
|
-
|
|
36
|
-
expect(result).toBe("use this image\n[图片] C:\\tmp\\img_001.png");
|
|
37
|
-
expect(mockGetTenantAccessToken).toHaveBeenCalled();
|
|
38
|
-
expect(mockGetOrDownloadImage).toHaveBeenCalledWith("mock_token", "om_post", "img_001");
|
|
39
|
-
});
|
|
40
|
-
|
|
41
|
-
it("keeps post image key when mixed post image download fails", async () => {
|
|
42
|
-
mockGetOrDownloadImage.mockRejectedValue(new Error("download failed"));
|
|
43
|
-
|
|
44
|
-
const result = await formatMessageContent({
|
|
45
|
-
message_id: "om_post",
|
|
46
|
-
message_type: "post",
|
|
47
|
-
content: JSON.stringify({
|
|
48
|
-
content: [[
|
|
49
|
-
{ tag: "text", text: "caption" },
|
|
50
|
-
{ tag: "img", image_key: "img_002" },
|
|
51
|
-
]],
|
|
52
|
-
}),
|
|
53
|
-
});
|
|
54
|
-
|
|
55
|
-
expect(result).toBe("caption\n[图片: img_002]");
|
|
56
|
-
});
|
|
57
|
-
});
|
|
58
|
-
|
|
59
|
-
describe("formatMessageContent", () => {
|
|
14
|
+
import { formatMessageContent, formatPostContent, formatMergeForward } from "../format-message.ts";
|
|
15
|
+
|
|
16
|
+
describe("formatMessageContent mixed post images", () => {
|
|
17
|
+
beforeEach(() => {
|
|
18
|
+
vi.clearAllMocks();
|
|
19
|
+
mockGetTenantAccessToken.mockResolvedValue("mock_token");
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
it("includes downloaded images from mixed post messages", async () => {
|
|
23
|
+
mockGetOrDownloadImage.mockResolvedValue("C:\\tmp\\img_001.png");
|
|
24
|
+
|
|
25
|
+
const result = await formatMessageContent({
|
|
26
|
+
message_id: "om_post",
|
|
27
|
+
message_type: "post",
|
|
28
|
+
content: JSON.stringify({
|
|
29
|
+
content: [[
|
|
30
|
+
{ tag: "text", text: "use this image" },
|
|
31
|
+
{ tag: "img", image_key: "img_001" },
|
|
32
|
+
]],
|
|
33
|
+
}),
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
expect(result).toBe("use this image\n[图片] C:\\tmp\\img_001.png");
|
|
37
|
+
expect(mockGetTenantAccessToken).toHaveBeenCalled();
|
|
38
|
+
expect(mockGetOrDownloadImage).toHaveBeenCalledWith("mock_token", "om_post", "img_001");
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
it("keeps post image key when mixed post image download fails", async () => {
|
|
42
|
+
mockGetOrDownloadImage.mockRejectedValue(new Error("download failed"));
|
|
43
|
+
|
|
44
|
+
const result = await formatMessageContent({
|
|
45
|
+
message_id: "om_post",
|
|
46
|
+
message_type: "post",
|
|
47
|
+
content: JSON.stringify({
|
|
48
|
+
content: [[
|
|
49
|
+
{ tag: "text", text: "caption" },
|
|
50
|
+
{ tag: "img", image_key: "img_002" },
|
|
51
|
+
]],
|
|
52
|
+
}),
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
expect(result).toBe("caption\n[图片: img_002]");
|
|
56
|
+
});
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
describe("formatMessageContent", () => {
|
|
60
60
|
beforeEach(() => {
|
|
61
61
|
vi.clearAllMocks();
|
|
62
62
|
mockGetTenantAccessToken.mockResolvedValue("mock_token");
|
|
@@ -313,4 +313,4 @@ describe("formatPostContent", () => {
|
|
|
313
313
|
const result = formatPostContent({ content: [null, undefined, "string"] });
|
|
314
314
|
expect(result).toBe("");
|
|
315
315
|
});
|
|
316
|
-
});
|
|
316
|
+
});
|