chatccc 0.2.179 → 0.2.181

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 CHANGED
@@ -144,7 +144,12 @@ npm install -g chatccc
144
144
  chatccc
145
145
  ```
146
146
 
147
- 要求 Node.js >= 20。安装完成后,在任意目录执行 `chatccc` 即可启动。配置、日志和状态文件会保存在包目录下的 `config.json`、`logs/`、`state/`。
147
+ 要求 Node.js >= 20。安装完成后,在任意目录执行 `chatccc` 即可启动。配置、日志和状态文件会保存在用户目录的 `.chatccc` 下:
148
+
149
+ - Windows:`C:\Users\<用户名>\.chatccc\`
150
+ - macOS / Linux:`~/.chatccc/`
151
+
152
+ 旧版本留在仓库或包目录下的 `config.json`、`logs/`、`state/` 会在首次启动时自动迁移到用户目录。
148
153
 
149
154
  首次启动时,如果还没有有效配置,ChatCCC 会自动打开本地 Web 配置向导(默认 `http://127.0.0.1:18080`)。
150
155
 
@@ -197,7 +202,7 @@ chatccc
197
202
  # 在微信里找到机器人,发送 /new 开始对话
198
203
  ```
199
204
 
200
- 微信登录信息会保存到 `state/ilink-auth.json`。token 过期后重新扫码即可。
205
+ 微信登录信息会保存到 `~/.chatccc/state/ilink-auth.json`。token 过期后重新扫码即可。
201
206
 
202
207
  ### 3. AI 工具配置
203
208
 
@@ -234,7 +239,7 @@ Codex 的默认模型和推理强度可继续由 `~/.codex/config.toml` 管理
234
239
 
235
240
  ### 4. `config.json`
236
241
 
237
- `config.json` 不存在时,ChatCCC 会从 `config.sample.json` 复制一份。常用结构如下:
242
+ `~/.chatccc/config.json` 不存在时,ChatCCC 会从 `config.sample.json` 复制一份。常用结构如下:
238
243
 
239
244
  ```json
240
245
  {
@@ -243,11 +248,12 @@ Codex 的默认模型和推理强度可继续由 `~/.codex/config.toml` 管理
243
248
  "appSecret": "xxxxxxxxxxxxxxxxxxxxxxxxxx"
244
249
  },
245
250
  "platforms": {
246
- "feishu": { "enabled": true },
247
- "ilink": { "enabled": true }
251
+ "feishu": { "enabled": true, "platformType": "feishu" },
252
+ "ilink": { "enabled": true, "reuseTokenOnStart": true }
248
253
  },
249
254
  "port": 18080,
250
255
  "gitTimeoutSeconds": 180,
256
+ "allowInterrupt": false,
251
257
  "claude": {
252
258
  "enabled": false,
253
259
  "defaultAgent": true,
@@ -255,13 +261,16 @@ Codex 的默认模型和推理强度可继续由 `~/.codex/config.toml` 管理
255
261
  "subagentModel": "",
256
262
  "effort": "",
257
263
  "apiKey": "",
258
- "baseUrl": ""
264
+ "baseUrl": "",
265
+ "maxTurn": 0
259
266
  },
260
267
  "cursor": {
261
268
  "enabled": false,
262
269
  "defaultAgent": false,
263
270
  "path": "",
264
- "model": ""
271
+ "model": "",
272
+ "avatarBatteryMode": "apiPercent",
273
+ "onDemandMonthlyBudget": 1000
265
274
  },
266
275
  "codex": {
267
276
  "enabled": false,
@@ -277,12 +286,17 @@ Codex 的默认模型和推理强度可继续由 `~/.codex/config.toml` 管理
277
286
  | --- | --- |
278
287
  | `feishu.appId` / `feishu.appSecret` | 飞书应用凭证 |
279
288
  | `platforms.feishu.enabled` | 是否启用飞书 |
289
+ | `platforms.feishu.platformType` | 飞书平台类型,默认 `feishu` |
280
290
  | `platforms.ilink.enabled` | 是否启用微信 iLink |
291
+ | `platforms.ilink.reuseTokenOnStart` | 启动时是否复用已有微信登录 token |
281
292
  | `port` | 本地 Web 配置面板和中继服务端口 |
282
293
  | `gitTimeoutSeconds` | `/git` 命令超时时间,默认 180 秒 |
294
+ | `allowInterrupt` | 是否允许新消息中断正在运行的任务;默认 false |
283
295
  | `*.enabled` | 是否启用对应 AI Agent |
284
296
  | `*.defaultAgent` | `/new` 未指定 Agent 时使用哪个工具 |
285
297
  | `cursor.path` / `codex.path` | CLI 可执行文件路径;留空时自动探测或使用 PATH |
298
+ | `cursor.avatarBatteryMode` | Cursor 头像电量显示来源:`apiPercent` 或 `onDemandUse` |
299
+ | `cursor.onDemandMonthlyBudget` | `avatarBatteryMode=onDemandUse` 时用于计算电量的月预算 |
286
300
  | `claude.model` / `claude.subagentModel` / `claude.effort` | 选填;设置后传给 Claude Agent SDK,留空以 `~/.claude/settings.json` 为准 |
287
301
  | `claude.apiKey` / `claude.baseUrl` | 选填;设置后传给 Claude Agent SDK,留空以 `~/.claude/settings.json` 为准 |
288
302
  | `claude.maxTurn` | 选填;Claude 最大对话轮数,默认 0(无限制),可在 Web UI 编辑 |
@@ -306,17 +320,21 @@ Codex 的默认模型和推理强度可继续由 `~/.codex/config.toml` 管理
306
320
  | `/newh` | 重置当前会话,保留工作目录 |
307
321
  | `/model` | 查看或切换当前会话的模型 |
308
322
  | `/stop` | 停止当前回复 |
323
+ | `/cancel` | 取消当前会话里排队等待处理的消息 |
309
324
  | `/state` | 查看当前会话状态 |
310
325
  | `/cd` | 查看或设置当前会话工作目录 |
311
326
  | `/sessions` | 查看所有会话状态 |
312
- | `/usage` | 查看 Codex 5h 用量和周用量 |
327
+ | `/session <数字>` | 将当前群聊切换到 `/sessions` 列表中的指定会话 |
328
+ | `/usage` | 查看当前会话对应 Agent 的用量;Codex 显示 5h/周用量,Cursor 显示当前周期用量 |
313
329
  | `/git <子命令>` | 在当前会话工作目录执行 `git ...` 并回传输出 |
330
+ | `/abd<内容>` | 去掉 `/abd` 前缀后把内容发给 Agent,并在消息末尾追加第一性原理需求澄清提示 |
314
331
  | `/plan <内容>` | 只读计划模式:仅允许读文件和 stop-stuck-loop 请求,不执行任何写操作 |
315
332
  | `/ask <内容>` | 只读问答模式:与 /plan 相同,仅允许读文件和 stop-stuck-loop 请求 |
316
333
  | `/restart` | 重启机器人进程 |
317
334
  | `/update` | 更新 npm 全局包并重启(仅限 `npm install -g chatccc` 安装的全局进程) |
335
+ | `/deleteg` | 解散当前飞书会话群;Agent 会话记录保留 |
318
336
 
319
- > **模型切换**:`/model` 查看可选模型清单,`/model <名称>` 模糊匹配切换,`/model clear` 恢复默认。当前仅 Claude Code 支持模型切换(需同时填写 `claude.model` `claude.subagentModel`),Cursor / Codex 切换正在开发中。
337
+ > **模型切换**:`/model` 查看当前会话 Agent 的可选模型清单,`/model <名称>` 模糊匹配切换,`/model clear` 恢复默认。可选模型来自当前 Agent 的配置:Claude 使用 `claude.model` / `claude.subagentModel`,Cursor 使用 `cursor.model`,Codex 使用 `codex.model`。
320
338
 
321
339
  ---
322
340
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "chatccc",
3
- "version": "0.2.179",
3
+ "version": "0.2.181",
4
4
  "description": "Feishu bot bridge for Claude Code",
5
5
  "license": "Apache-2.0",
6
6
  "type": "module",
@@ -19,6 +19,7 @@ describe("cardJsonToPlainText", () => {
19
19
  expect(text).toContain("/restart");
20
20
  expect(text).toContain("/update");
21
21
  expect(text).toContain("/cd");
22
+ expect(text).toContain("/abd");
22
23
  });
23
24
 
24
25
  it("converts status cards from v1 card format", () => {
@@ -6,10 +6,13 @@ import {
6
6
  buildCdCard,
7
7
  buildSessionsCard,
8
8
  buildStatusCard,
9
+ buildCodexUsageCard,
10
+ buildCodexResetConfirmCard,
9
11
  buildButtons,
10
12
  truncateContent,
11
13
  getToolEmoji,
12
14
  } from "../cards.ts";
15
+ import { ABD_HELP_LINE } from "../shared-prefix.ts";
13
16
 
14
17
  // ---------------------------------------------------------------------------
15
18
  // truncateContent
@@ -158,6 +161,15 @@ describe("buildHelpCard", () => {
158
161
  expect(action.tag).toBe("action");
159
162
  expect(action.actions).toHaveLength(7);
160
163
  });
164
+
165
+ it("adds ABD prefix help as the final help line", () => {
166
+ const card = buildHelpCard("test");
167
+ const parsed = JSON.parse(card);
168
+ const lines = parsed.elements[1].text.content.split("\n");
169
+
170
+ expect(lines).toContain("发送 **/usage** 查看 Codex 5h/周用量,以及查询/使用主动重置卡");
171
+ expect(lines.at(-1)).toBe(ABD_HELP_LINE);
172
+ });
161
173
  });
162
174
 
163
175
  // ---------------------------------------------------------------------------
@@ -435,6 +447,49 @@ describe("buildStatusCard", () => {
435
447
  });
436
448
  });
437
449
 
450
+ // ---------------------------------------------------------------------------
451
+ // buildCodexUsageCard / buildCodexResetConfirmCard
452
+ // ---------------------------------------------------------------------------
453
+
454
+ describe("Codex usage reset cards", () => {
455
+ it("shows the reset button only when reset credits are available", () => {
456
+ const card = buildCodexUsageCard("Codex 用量", 2);
457
+ const parsed = JSON.parse(card);
458
+ const action = parsed.elements.find((element: any) => element.tag === "action");
459
+ expect(action.actions[0].text.content).toBe("发起重置");
460
+ expect(action.actions[0].value).toEqual({ action: "codex_reset_request", availableCount: 2 });
461
+
462
+ const noCreditCard = buildCodexUsageCard("Codex 用量", 0);
463
+ const noCreditParsed = JSON.parse(noCreditCard);
464
+ expect(noCreditParsed.elements.some((element: any) => element.tag === "action")).toBe(false);
465
+ });
466
+
467
+ it("builds yes/no confirmation buttons tied to the parent usage card", () => {
468
+ const card = buildCodexResetConfirmCard({
469
+ availableCount: 2,
470
+ parentMessageId: "usage-message",
471
+ requestId: "request-1",
472
+ });
473
+ const parsed = JSON.parse(card);
474
+ const action = parsed.elements.find((element: any) => element.tag === "action");
475
+
476
+ expect(action.actions[0].text.content).toBe("是,发起重置");
477
+ expect(action.actions[0].value).toEqual({
478
+ action: "codex_reset_confirm",
479
+ decision: "yes",
480
+ parentMessageId: "usage-message",
481
+ requestId: "request-1",
482
+ });
483
+ expect(action.actions[1].text.content).toBe("否");
484
+ expect(action.actions[1].value).toEqual({
485
+ action: "codex_reset_confirm",
486
+ decision: "no",
487
+ parentMessageId: "usage-message",
488
+ requestId: "request-1",
489
+ });
490
+ });
491
+ });
492
+
438
493
  // ---------------------------------------------------------------------------
439
494
  // buildButtons
440
495
  // ---------------------------------------------------------------------------
@@ -0,0 +1,113 @@
1
+ import { beforeEach, describe, expect, it, vi } from "vitest";
2
+
3
+ import {
4
+ _resetCodexResetRequestsForTest,
5
+ handleCodexResetCardAction,
6
+ type CodexResetActionDeps,
7
+ } from "../codex-reset-actions.ts";
8
+
9
+ function deps(): CodexResetActionDeps & {
10
+ getTenantAccessToken: ReturnType<typeof vi.fn>;
11
+ sendRawCard: ReturnType<typeof vi.fn>;
12
+ sendCardReply: ReturnType<typeof vi.fn>;
13
+ updateCardMessage: ReturnType<typeof vi.fn>;
14
+ consumeCodexRateLimitResetCredit: ReturnType<typeof vi.fn>;
15
+ createRequestId: ReturnType<typeof vi.fn>;
16
+ } {
17
+ return {
18
+ getTenantAccessToken: vi.fn(async () => "tenant-token"),
19
+ sendRawCard: vi.fn(async () => true),
20
+ sendCardReply: vi.fn(async () => true),
21
+ updateCardMessage: vi.fn(async () => true),
22
+ consumeCodexRateLimitResetCredit: vi.fn(async () => ({ code: "reset" as const, windowsReset: 2 })),
23
+ createRequestId: vi.fn(() => "request-1"),
24
+ };
25
+ }
26
+
27
+ function event(value: Record<string, unknown>, messageId: string) {
28
+ return {
29
+ event: {
30
+ open_message_id: messageId,
31
+ open_chat_id: "chat-1",
32
+ operator: { open_id: "ou-user" },
33
+ action: { value },
34
+ },
35
+ };
36
+ }
37
+
38
+ describe("Codex reset card actions", () => {
39
+ beforeEach(() => {
40
+ _resetCodexResetRequestsForTest();
41
+ });
42
+
43
+ it("sends a confirmation card tied to the clicked usage card", async () => {
44
+ const d = deps();
45
+
46
+ await expect(handleCodexResetCardAction(
47
+ event({ action: "codex_reset_request" }, "usage-message"),
48
+ d,
49
+ )).resolves.toBe(true);
50
+
51
+ expect(d.sendRawCard).toHaveBeenCalledTimes(1);
52
+ expect(d.sendRawCard.mock.calls[0][1]).toBe("chat-1");
53
+ const confirmCard = JSON.parse(d.sendRawCard.mock.calls[0][2]);
54
+ const action = confirmCard.elements.find((element: any) => element.tag === "action");
55
+ expect(action.actions[0].value).toEqual({
56
+ action: "codex_reset_confirm",
57
+ decision: "yes",
58
+ parentMessageId: "usage-message",
59
+ requestId: "request-1",
60
+ });
61
+ });
62
+
63
+ it("consumes a reset credit, sends the result, and collapses the confirmation card when user confirms", async () => {
64
+ const d = deps();
65
+ await handleCodexResetCardAction(event({ action: "codex_reset_request" }, "usage-message"), d);
66
+
67
+ await expect(handleCodexResetCardAction(
68
+ event({
69
+ action: "codex_reset_confirm",
70
+ decision: "yes",
71
+ parentMessageId: "usage-message",
72
+ requestId: "request-1",
73
+ }, "confirm-message"),
74
+ d,
75
+ )).resolves.toBe(true);
76
+
77
+ expect(d.consumeCodexRateLimitResetCredit).toHaveBeenCalledWith("request-1");
78
+ expect(d.updateCardMessage).toHaveBeenCalledTimes(1);
79
+ expect(d.updateCardMessage.mock.calls[0][1]).toBe("confirm-message");
80
+ expect(d.sendCardReply).toHaveBeenCalledWith(
81
+ "tenant-token",
82
+ "chat-1",
83
+ "Codex 主动重置",
84
+ expect.stringContaining("重置成功"),
85
+ "green",
86
+ );
87
+ });
88
+
89
+ it("does not consume a reset credit and sends a cancellation message when user declines", async () => {
90
+ const d = deps();
91
+ await handleCodexResetCardAction(event({ action: "codex_reset_request" }, "usage-message"), d);
92
+
93
+ await expect(handleCodexResetCardAction(
94
+ event({
95
+ action: "codex_reset_confirm",
96
+ decision: "no",
97
+ parentMessageId: "usage-message",
98
+ requestId: "request-1",
99
+ }, "confirm-message"),
100
+ d,
101
+ )).resolves.toBe(true);
102
+
103
+ expect(d.consumeCodexRateLimitResetCredit).not.toHaveBeenCalled();
104
+ expect(d.updateCardMessage).toHaveBeenCalledTimes(1);
105
+ expect(d.sendCardReply).toHaveBeenCalledWith(
106
+ "tenant-token",
107
+ "chat-1",
108
+ "Codex 主动重置",
109
+ "用户取消了重置。",
110
+ "grey",
111
+ );
112
+ });
113
+ });
@@ -41,7 +41,7 @@ async function writeCodexAuth(homeDir: string): Promise<void> {
41
41
  await mkdir(codexDir, { recursive: true });
42
42
  await writeFile(
43
43
  join(codexDir, "auth.json"),
44
- JSON.stringify({ tokens: { access_token: "codex-access-token" } }),
44
+ JSON.stringify({ tokens: { access_token: "codex-access-token", account_id: "codex-account-id" } }),
45
45
  "utf-8",
46
46
  );
47
47
  }
@@ -52,6 +52,9 @@ function mockAvatarFetch(uploadedNames: string[], usageResponse: Response): void
52
52
  if (urlText === "https://chatgpt.com/backend-api/wham/usage") {
53
53
  return usageResponse.clone();
54
54
  }
55
+ if (urlText === "https://chatgpt.com/backend-api/wham/rate-limit-reset-credits") {
56
+ return new Response(JSON.stringify({ credits: [] }), { status: 200 });
57
+ }
55
58
  if (urlText === "https://open.feishu.test/im/v1/images") {
56
59
  const form = init?.body as FormData;
57
60
  const image = form.get("image") as File;
@@ -100,24 +103,102 @@ describe("Codex avatar usage battery", () => {
100
103
  }
101
104
  });
102
105
 
103
- it("returns both 5h and weekly Codex usage windows", async () => {
106
+ it("returns both usage windows and available reset credit expiries", async () => {
104
107
  const homeDir = await mkdtemp(join(tmpdir(), "chatccc-avatar-home-"));
105
108
  const userDataDir = await mkdtemp(join(tmpdir(), "chatccc-avatar-data-"));
106
- const uploadedNames: string[] = [];
107
109
  await writeCodexAuth(homeDir);
108
- mockAvatarFetch(uploadedNames, new Response(JSON.stringify({
109
- rate_limit: {
110
- primary_window: { used_percent: 37, reset_after_seconds: 10349, reset_at: 1781528212 },
111
- secondary_window: { used_percent: 12, reset_after_seconds: 325063, reset_at: 1781842926 },
112
- },
113
- }), { status: 200 }));
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);
114
146
 
115
147
  try {
116
148
  const { getCodexUsageSummary } = await loadFeishuApiWithHome(homeDir, userDataDir);
117
149
  await expect(getCodexUsageSummary()).resolves.toEqual({
118
150
  fiveHour: { usedPercent: 37, remainingPercent: 63, resetAfterSeconds: 10349, resetAtEpochSeconds: 1781528212 },
119
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 });
172
+ }
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,
120
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
+ );
121
202
  } finally {
122
203
  await rm(homeDir, { recursive: true, force: true });
123
204
  await rm(userDataDir, { recursive: true, force: true });
@@ -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, 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,13 +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
- }),
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
+ 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",
36
39
  verifyAllPermissions: async () => [],
37
40
  reportPermissionResults: realPlatform.reportPermissionResults,
38
41
  extractSessionInfo: realPlatform.extractSessionInfo,
@@ -50,13 +53,16 @@ describe("feishu-platform", () => {
50
53
  expect(await getChatInfo("t", "mock_chat")).toEqual({ name: "x", description: "y" });
51
54
  const perms = await verifyAllPermissions("t");
52
55
  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 {
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 {
60
66
  // 恢复到真实实现,避免影响后续测试
61
67
  setPlatform(realPlatform);
62
68
  }
@@ -66,4 +72,4 @@ describe("feishu-platform", () => {
66
72
  setPlatform(realPlatform);
67
73
  expect(getPlatform()).toBe(realPlatform);
68
74
  });
69
- });
75
+ });