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.
Files changed (71) hide show
  1. package/agent-prompts/claude_specific.md +45 -45
  2. package/agent-prompts/codex_specific.md +2 -2
  3. package/agent-prompts/cursor_specific.md +13 -13
  4. package/config.sample.json +5 -0
  5. package/im-skills/feishu-skill/receive-send-file.md +63 -63
  6. package/im-skills/feishu-skill/receive-send-image.md +24 -24
  7. package/im-skills/feishu-skill/skill.md +3 -3
  8. package/im-skills/wechat-file-skill/receive-send-file.md +38 -38
  9. package/im-skills/wechat-file-skill/send-file.mjs +83 -83
  10. package/im-skills/wechat-file-skill/skill.md +10 -10
  11. package/im-skills/wechat-image-skill/skill.md +10 -10
  12. package/im-skills/wechat-video-skill/receive-send-video.md +38 -38
  13. package/im-skills/wechat-video-skill/send-video.mjs +79 -79
  14. package/im-skills/wechat-video-skill/skill.md +10 -10
  15. package/package.json +1 -1
  16. package/scripts/postinstall-sharp-check.mjs +58 -58
  17. package/src/__tests__/agent-delegate-task-rpc.test.ts +165 -165
  18. package/src/__tests__/builtin-config.test.ts +33 -0
  19. package/src/__tests__/card-plain-text.test.ts +5 -5
  20. package/src/__tests__/cardkit.test.ts +60 -60
  21. package/src/__tests__/cards.test.ts +77 -77
  22. package/src/__tests__/chatgpt-subscription-rpc.test.ts +89 -89
  23. package/src/__tests__/chatgpt-subscription.test.ts +135 -135
  24. package/src/__tests__/chrome-devtools-guard.test.ts +165 -125
  25. package/src/__tests__/claude-adapter.test.ts +592 -592
  26. package/src/__tests__/codex-reset-actions.test.ts +146 -146
  27. package/src/__tests__/config-reload.test.ts +1 -0
  28. package/src/__tests__/config-sample.test.ts +11 -0
  29. package/src/__tests__/feishu-api.test.ts +60 -60
  30. package/src/__tests__/feishu-avatar.test.ts +180 -115
  31. package/src/__tests__/feishu-platform.test.ts +22 -22
  32. package/src/__tests__/format-message.test.ts +47 -47
  33. package/src/__tests__/orchestrator.test.ts +150 -2
  34. package/src/__tests__/privacy.test.ts +198 -198
  35. package/src/__tests__/raw-stream-log.test.ts +106 -106
  36. package/src/__tests__/session.test.ts +10 -0
  37. package/src/__tests__/shared-prefix.test.ts +36 -36
  38. package/src/__tests__/stream-state.test.ts +42 -42
  39. package/src/__tests__/web-ui.test.ts +209 -130
  40. package/src/adapters/claude-adapter.ts +566 -566
  41. package/src/adapters/claude-session-meta-store.ts +120 -120
  42. package/src/adapters/codex-adapter.ts +10 -6
  43. package/src/adapters/cursor-adapter.ts +46 -46
  44. package/src/adapters/raw-stream-log.ts +124 -124
  45. package/src/adapters/resource-monitor.ts +140 -140
  46. package/src/agent-delegate-task-rpc.ts +153 -153
  47. package/src/agent-delegate-task.ts +81 -81
  48. package/src/agent-stop-stuck.ts +129 -129
  49. package/src/builtin/cli.ts +189 -197
  50. package/src/builtin/index.ts +168 -167
  51. package/src/cards.ts +130 -89
  52. package/src/chatgpt-subscription-rpc.ts +27 -27
  53. package/src/chatgpt-subscription.ts +299 -299
  54. package/src/chrome-devtools-guard.ts +318 -242
  55. package/src/codex-reset-actions.ts +184 -184
  56. package/src/config.ts +38 -0
  57. package/src/feishu-api.ts +219 -190
  58. package/src/feishu-platform.ts +20 -20
  59. package/src/format-message.ts +293 -293
  60. package/src/index.ts +2 -2
  61. package/src/litellm-proxy.ts +374 -374
  62. package/src/orchestrator.ts +201 -9
  63. package/src/platform-adapter.ts +9 -1
  64. package/src/privacy.ts +118 -118
  65. package/src/session-chat-binding.ts +6 -6
  66. package/src/session-name.ts +8 -8
  67. package/src/session.ts +44 -16
  68. package/src/shared-prefix.ts +29 -29
  69. package/src/sim-platform.ts +20 -20
  70. package/src/turn-cards.ts +117 -117
  71. 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("returns both usage windows and available reset credit expiries", async () => {
107
- const homeDir = await mkdtemp(join(tmpdir(), "chatccc-avatar-home-"));
108
- const userDataDir = await mkdtemp(join(tmpdir(), "chatccc-avatar-data-"));
109
- await writeCodexAuth(homeDir);
110
- const fetchMock = vi.fn(async (url: string | URL | Request) => {
111
- const urlText = String(url);
112
- if (urlText === "https://chatgpt.com/backend-api/wham/usage") {
113
- return new Response(JSON.stringify({
114
- rate_limit: {
115
- primary_window: { used_percent: 37, reset_after_seconds: 10349, reset_at: 1781528212 },
116
- secondary_window: { used_percent: 12, reset_after_seconds: 325063, reset_at: 1781842926 },
117
- },
118
- rate_limit_reset_credits: { available_count: 1 },
119
- }), { status: 200 });
120
- }
121
- if (urlText === "https://chatgpt.com/backend-api/wham/rate-limit-reset-credits") {
122
- return new Response(JSON.stringify({
123
- available_count: 2,
124
- credits: [
125
- {
126
- status: "available",
127
- granted_at: "2026-06-12T04:01:47.770016Z",
128
- expires_at: "2026-07-12T04:01:47.770016Z",
129
- },
130
- {
131
- status: "redeemed",
132
- granted_at: "2026-06-13T04:01:47.770016Z",
133
- expires_at: "2026-07-13T04:01:47.770016Z",
134
- },
135
- {
136
- status: "available",
137
- granted_at: "2026-06-18T00:44:23.904386Z",
138
- expires_at: "2026-07-18T00:44:23.904386Z",
139
- },
140
- ],
141
- }), { status: 200 });
142
- }
143
- throw new Error(`unexpected fetch: ${urlText}`);
144
- });
145
- vi.stubGlobal("fetch", fetchMock);
146
-
147
- try {
148
- const { getCodexUsageSummary } = await loadFeishuApiWithHome(homeDir, userDataDir);
149
- await expect(getCodexUsageSummary()).resolves.toEqual({
150
- fiveHour: { usedPercent: 37, remainingPercent: 63, resetAfterSeconds: 10349, resetAtEpochSeconds: 1781528212 },
151
- weekly: { usedPercent: 12, remainingPercent: 88, resetAfterSeconds: 325063, resetAtEpochSeconds: 1781842926 },
152
- rateLimitResetCreditsAvailable: 2,
153
- rateLimitResetCredits: [
154
- { grantedAt: "2026-06-12T04:01:47.770016Z", expiresAt: "2026-07-12T04:01:47.770016Z" },
155
- { grantedAt: "2026-06-18T00:44:23.904386Z", expiresAt: "2026-07-18T00:44:23.904386Z" },
156
- ],
157
- });
158
- expect(fetchMock).toHaveBeenCalledWith(
159
- "https://chatgpt.com/backend-api/wham/rate-limit-reset-credits",
160
- expect.objectContaining({
161
- headers: expect.objectContaining({
162
- Authorization: "Bearer codex-access-token",
163
- "ChatGPT-Account-ID": "codex-account-id",
164
- "OpenAI-Beta": "codex-1",
165
- originator: "Codex Desktop",
166
- }),
167
- }),
168
- );
169
- } finally {
170
- await rm(homeDir, { recursive: true, force: true });
171
- await rm(userDataDir, { recursive: true, force: true });
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("consumes a Codex rate-limit reset credit through the WHAM endpoint", async () => {
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
+ });