chatccc 0.2.181 → 0.2.183
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 +69 -69
- package/package.json +1 -1
- package/src/__tests__/card-plain-text.test.ts +5 -5
- package/src/__tests__/cards.test.ts +77 -77
- package/src/__tests__/codex-reset-actions.test.ts +40 -7
- package/src/__tests__/feishu-avatar.test.ts +115 -115
- package/src/__tests__/feishu-platform.test.ts +22 -22
- package/src/__tests__/format-message.test.ts +316 -273
- package/src/__tests__/orchestrator.test.ts +117 -117
- package/src/__tests__/shared-prefix.test.ts +36 -36
- package/src/cards.ts +89 -89
- package/src/codex-reset-actions.ts +19 -6
- package/src/feishu-api.ts +184 -184
- package/src/feishu-platform.ts +20 -20
- package/src/format-message.ts +293 -214
- package/src/index.ts +2 -0
- package/src/orchestrator.ts +120 -120
- package/src/shared-prefix.ts +29 -29
- package/src/sim-platform.ts +20 -20
package/README.md
CHANGED
|
@@ -144,12 +144,12 @@ npm install -g chatccc
|
|
|
144
144
|
chatccc
|
|
145
145
|
```
|
|
146
146
|
|
|
147
|
-
要求 Node.js >= 20。安装完成后,在任意目录执行 `chatccc` 即可启动。配置、日志和状态文件会保存在用户目录的 `.chatccc` 下:
|
|
148
|
-
|
|
149
|
-
- Windows:`C:\Users\<用户名>\.chatccc\`
|
|
150
|
-
- macOS / Linux:`~/.chatccc/`
|
|
151
|
-
|
|
152
|
-
旧版本留在仓库或包目录下的 `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/` 会在首次启动时自动迁移到用户目录。
|
|
153
153
|
|
|
154
154
|
首次启动时,如果还没有有效配置,ChatCCC 会自动打开本地 Web 配置向导(默认 `http://127.0.0.1:18080`)。
|
|
155
155
|
|
|
@@ -202,7 +202,7 @@ chatccc
|
|
|
202
202
|
# 在微信里找到机器人,发送 /new 开始对话
|
|
203
203
|
```
|
|
204
204
|
|
|
205
|
-
微信登录信息会保存到 `~/.chatccc/state/ilink-auth.json`。token 过期后重新扫码即可。
|
|
205
|
+
微信登录信息会保存到 `~/.chatccc/state/ilink-auth.json`。token 过期后重新扫码即可。
|
|
206
206
|
|
|
207
207
|
### 3. AI 工具配置
|
|
208
208
|
|
|
@@ -239,7 +239,7 @@ Codex 的默认模型和推理强度可继续由 `~/.codex/config.toml` 管理
|
|
|
239
239
|
|
|
240
240
|
### 4. `config.json`
|
|
241
241
|
|
|
242
|
-
`~/.chatccc/config.json` 不存在时,ChatCCC 会从 `config.sample.json` 复制一份。常用结构如下:
|
|
242
|
+
`~/.chatccc/config.json` 不存在时,ChatCCC 会从 `config.sample.json` 复制一份。常用结构如下:
|
|
243
243
|
|
|
244
244
|
```json
|
|
245
245
|
{
|
|
@@ -247,32 +247,32 @@ Codex 的默认模型和推理强度可继续由 `~/.codex/config.toml` 管理
|
|
|
247
247
|
"appId": "cli_xxxxxxxxxxxx",
|
|
248
248
|
"appSecret": "xxxxxxxxxxxxxxxxxxxxxxxxxx"
|
|
249
249
|
},
|
|
250
|
-
"platforms": {
|
|
251
|
-
"feishu": { "enabled": true, "platformType": "feishu" },
|
|
252
|
-
"ilink": { "enabled": true, "reuseTokenOnStart": true }
|
|
253
|
-
},
|
|
254
|
-
"port": 18080,
|
|
255
|
-
"gitTimeoutSeconds": 180,
|
|
256
|
-
"allowInterrupt": false,
|
|
257
|
-
"claude": {
|
|
258
|
-
"enabled": false,
|
|
259
|
-
"defaultAgent": true,
|
|
260
|
-
"model": "claude-sonnet-4-6",
|
|
261
|
-
"subagentModel": "",
|
|
262
|
-
"effort": "",
|
|
263
|
-
"apiKey": "",
|
|
264
|
-
"baseUrl": "",
|
|
265
|
-
"maxTurn": 0
|
|
266
|
-
},
|
|
267
|
-
"cursor": {
|
|
268
|
-
"enabled": false,
|
|
269
|
-
"defaultAgent": false,
|
|
270
|
-
"path": "",
|
|
271
|
-
"model": "",
|
|
272
|
-
"avatarBatteryMode": "apiPercent",
|
|
273
|
-
"onDemandMonthlyBudget": 1000
|
|
274
|
-
},
|
|
275
|
-
"codex": {
|
|
250
|
+
"platforms": {
|
|
251
|
+
"feishu": { "enabled": true, "platformType": "feishu" },
|
|
252
|
+
"ilink": { "enabled": true, "reuseTokenOnStart": true }
|
|
253
|
+
},
|
|
254
|
+
"port": 18080,
|
|
255
|
+
"gitTimeoutSeconds": 180,
|
|
256
|
+
"allowInterrupt": false,
|
|
257
|
+
"claude": {
|
|
258
|
+
"enabled": false,
|
|
259
|
+
"defaultAgent": true,
|
|
260
|
+
"model": "claude-sonnet-4-6",
|
|
261
|
+
"subagentModel": "",
|
|
262
|
+
"effort": "",
|
|
263
|
+
"apiKey": "",
|
|
264
|
+
"baseUrl": "",
|
|
265
|
+
"maxTurn": 0
|
|
266
|
+
},
|
|
267
|
+
"cursor": {
|
|
268
|
+
"enabled": false,
|
|
269
|
+
"defaultAgent": false,
|
|
270
|
+
"path": "",
|
|
271
|
+
"model": "",
|
|
272
|
+
"avatarBatteryMode": "apiPercent",
|
|
273
|
+
"onDemandMonthlyBudget": 1000
|
|
274
|
+
},
|
|
275
|
+
"codex": {
|
|
276
276
|
"enabled": false,
|
|
277
277
|
"defaultAgent": false,
|
|
278
278
|
"path": "",
|
|
@@ -285,21 +285,21 @@ Codex 的默认模型和推理强度可继续由 `~/.codex/config.toml` 管理
|
|
|
285
285
|
| 字段 | 说明 |
|
|
286
286
|
| --- | --- |
|
|
287
287
|
| `feishu.appId` / `feishu.appSecret` | 飞书应用凭证 |
|
|
288
|
-
| `platforms.feishu.enabled` | 是否启用飞书 |
|
|
289
|
-
| `platforms.feishu.platformType` | 飞书平台类型,默认 `feishu` |
|
|
290
|
-
| `platforms.ilink.enabled` | 是否启用微信 iLink |
|
|
291
|
-
| `platforms.ilink.reuseTokenOnStart` | 启动时是否复用已有微信登录 token |
|
|
292
|
-
| `port` | 本地 Web 配置面板和中继服务端口 |
|
|
293
|
-
| `gitTimeoutSeconds` | `/git` 命令超时时间,默认 180 秒 |
|
|
294
|
-
| `allowInterrupt` | 是否允许新消息中断正在运行的任务;默认 false |
|
|
295
|
-
| `*.enabled` | 是否启用对应 AI Agent |
|
|
296
|
-
| `*.defaultAgent` | `/new` 未指定 Agent 时使用哪个工具 |
|
|
297
|
-
| `cursor.path` / `codex.path` | CLI 可执行文件路径;留空时自动探测或使用 PATH |
|
|
298
|
-
| `cursor.avatarBatteryMode` | Cursor 头像电量显示来源:`apiPercent` 或 `onDemandUse` |
|
|
299
|
-
| `cursor.onDemandMonthlyBudget` | `avatarBatteryMode=onDemandUse` 时用于计算电量的月预算 |
|
|
300
|
-
| `claude.model` / `claude.subagentModel` / `claude.effort` | 选填;设置后传给 Claude Agent SDK,留空以 `~/.claude/settings.json` 为准 |
|
|
301
|
-
| `claude.apiKey` / `claude.baseUrl` | 选填;设置后传给 Claude Agent SDK,留空以 `~/.claude/settings.json` 为准 |
|
|
302
|
-
| `claude.maxTurn` | 选填;Claude 最大对话轮数,默认 0(无限制),可在 Web UI 编辑 |
|
|
288
|
+
| `platforms.feishu.enabled` | 是否启用飞书 |
|
|
289
|
+
| `platforms.feishu.platformType` | 飞书平台类型,默认 `feishu` |
|
|
290
|
+
| `platforms.ilink.enabled` | 是否启用微信 iLink |
|
|
291
|
+
| `platforms.ilink.reuseTokenOnStart` | 启动时是否复用已有微信登录 token |
|
|
292
|
+
| `port` | 本地 Web 配置面板和中继服务端口 |
|
|
293
|
+
| `gitTimeoutSeconds` | `/git` 命令超时时间,默认 180 秒 |
|
|
294
|
+
| `allowInterrupt` | 是否允许新消息中断正在运行的任务;默认 false |
|
|
295
|
+
| `*.enabled` | 是否启用对应 AI Agent |
|
|
296
|
+
| `*.defaultAgent` | `/new` 未指定 Agent 时使用哪个工具 |
|
|
297
|
+
| `cursor.path` / `codex.path` | CLI 可执行文件路径;留空时自动探测或使用 PATH |
|
|
298
|
+
| `cursor.avatarBatteryMode` | Cursor 头像电量显示来源:`apiPercent` 或 `onDemandUse` |
|
|
299
|
+
| `cursor.onDemandMonthlyBudget` | `avatarBatteryMode=onDemandUse` 时用于计算电量的月预算 |
|
|
300
|
+
| `claude.model` / `claude.subagentModel` / `claude.effort` | 选填;设置后传给 Claude Agent SDK,留空以 `~/.claude/settings.json` 为准 |
|
|
301
|
+
| `claude.apiKey` / `claude.baseUrl` | 选填;设置后传给 Claude Agent SDK,留空以 `~/.claude/settings.json` 为准 |
|
|
302
|
+
| `claude.maxTurn` | 选填;Claude 最大对话轮数,默认 0(无限制),可在 Web UI 编辑 |
|
|
303
303
|
|
|
304
304
|
> **权限控制**:普通消息以 `bypassPermissions` 模式运行,跳过 Agent 操作确认。使用 `/plan` 或 `/ask` 前缀时,ChatCCC 自动切换为只读模式:Claude SDK 仅放行 Read + stop-stuck-loop 网络请求,Codex 使用 `--sandbox read-only`,Cursor 使用 `--mode plan/ask`。请只在可信环境中使用。
|
|
305
305
|
|
|
@@ -315,26 +315,26 @@ Codex 的默认模型和推理强度可继续由 `~/.codex/config.toml` 管理
|
|
|
315
315
|
| --- | --- |
|
|
316
316
|
| `/new` | 使用默认 Agent 创建新会话 |
|
|
317
317
|
| `/new claude` | 创建 Claude Code 会话 |
|
|
318
|
-
| `/new cursor` | 创建 Cursor 会话 |
|
|
319
|
-
| `/new codex` | 创建 Codex 会话 |
|
|
320
|
-
| `/newh` | 重置当前会话,保留工作目录 |
|
|
321
|
-
| `/model` | 查看或切换当前会话的模型 |
|
|
322
|
-
| `/stop` | 停止当前回复 |
|
|
323
|
-
| `/cancel` | 取消当前会话里排队等待处理的消息 |
|
|
324
|
-
| `/state` | 查看当前会话状态 |
|
|
325
|
-
| `/cd` | 查看或设置当前会话工作目录 |
|
|
326
|
-
| `/sessions` | 查看所有会话状态 |
|
|
327
|
-
| `/session <数字>` | 将当前群聊切换到 `/sessions` 列表中的指定会话 |
|
|
328
|
-
| `/usage` | 查看当前会话对应 Agent 的用量;Codex 显示 5h/周用量,Cursor 显示当前周期用量 |
|
|
329
|
-
| `/git <子命令>` | 在当前会话工作目录执行 `git ...` 并回传输出 |
|
|
330
|
-
| `/abd<内容>` | 去掉 `/abd` 前缀后把内容发给 Agent,并在消息末尾追加第一性原理需求澄清提示 |
|
|
331
|
-
| `/plan <内容>` | 只读计划模式:仅允许读文件和 stop-stuck-loop 请求,不执行任何写操作 |
|
|
332
|
-
| `/ask <内容>` | 只读问答模式:与 /plan 相同,仅允许读文件和 stop-stuck-loop 请求 |
|
|
333
|
-
| `/restart` | 重启机器人进程 |
|
|
334
|
-
| `/update` | 更新 npm 全局包并重启(仅限 `npm install -g chatccc` 安装的全局进程) |
|
|
335
|
-
| `/deleteg` | 解散当前飞书会话群;Agent 会话记录保留 |
|
|
336
|
-
|
|
337
|
-
> **模型切换**:`/model` 查看当前会话 Agent 的可选模型清单,`/model <名称>` 模糊匹配切换,`/model clear` 恢复默认。可选模型来自当前 Agent 的配置:Claude 使用 `claude.model` / `claude.subagentModel`,Cursor 使用 `cursor.model`,Codex 使用 `codex.model`。
|
|
318
|
+
| `/new cursor` | 创建 Cursor 会话 |
|
|
319
|
+
| `/new codex` | 创建 Codex 会话 |
|
|
320
|
+
| `/newh` | 重置当前会话,保留工作目录 |
|
|
321
|
+
| `/model` | 查看或切换当前会话的模型 |
|
|
322
|
+
| `/stop` | 停止当前回复 |
|
|
323
|
+
| `/cancel` | 取消当前会话里排队等待处理的消息 |
|
|
324
|
+
| `/state` | 查看当前会话状态 |
|
|
325
|
+
| `/cd` | 查看或设置当前会话工作目录 |
|
|
326
|
+
| `/sessions` | 查看所有会话状态 |
|
|
327
|
+
| `/session <数字>` | 将当前群聊切换到 `/sessions` 列表中的指定会话 |
|
|
328
|
+
| `/usage` | 查看当前会话对应 Agent 的用量;Codex 显示 5h/周用量,Cursor 显示当前周期用量 |
|
|
329
|
+
| `/git <子命令>` | 在当前会话工作目录执行 `git ...` 并回传输出 |
|
|
330
|
+
| `/abd<内容>` | 去掉 `/abd` 前缀后把内容发给 Agent,并在消息末尾追加第一性原理需求澄清提示 |
|
|
331
|
+
| `/plan <内容>` | 只读计划模式:仅允许读文件和 stop-stuck-loop 请求,不执行任何写操作 |
|
|
332
|
+
| `/ask <内容>` | 只读问答模式:与 /plan 相同,仅允许读文件和 stop-stuck-loop 请求 |
|
|
333
|
+
| `/restart` | 重启机器人进程 |
|
|
334
|
+
| `/update` | 更新 npm 全局包并重启(仅限 `npm install -g chatccc` 安装的全局进程) |
|
|
335
|
+
| `/deleteg` | 解散当前飞书会话群;Agent 会话记录保留 |
|
|
336
|
+
|
|
337
|
+
> **模型切换**:`/model` 查看当前会话 Agent 的可选模型清单,`/model <名称>` 模糊匹配切换,`/model clear` 恢复默认。可选模型来自当前 Agent 的配置:Claude 使用 `claude.model` / `claude.subagentModel`,Cursor 使用 `cursor.model`,Codex 使用 `codex.model`。
|
|
338
338
|
|
|
339
339
|
---
|
|
340
340
|
|
package/package.json
CHANGED
|
@@ -16,11 +16,11 @@ describe("cardJsonToPlainText", () => {
|
|
|
16
16
|
expect(text).toContain("/new");
|
|
17
17
|
expect(text).toContain("/new cursor");
|
|
18
18
|
expect(text).toContain("/new codex");
|
|
19
|
-
expect(text).toContain("/restart");
|
|
20
|
-
expect(text).toContain("/update");
|
|
21
|
-
expect(text).toContain("/cd");
|
|
22
|
-
expect(text).toContain("/abd");
|
|
23
|
-
});
|
|
19
|
+
expect(text).toContain("/restart");
|
|
20
|
+
expect(text).toContain("/update");
|
|
21
|
+
expect(text).toContain("/cd");
|
|
22
|
+
expect(text).toContain("/abd");
|
|
23
|
+
});
|
|
24
24
|
|
|
25
25
|
it("converts status cards from v1 card format", () => {
|
|
26
26
|
const text = cardJsonToPlainText(buildStatusCard("status body", "green"));
|
|
@@ -1,18 +1,18 @@
|
|
|
1
1
|
import { describe, it, expect } from "vitest";
|
|
2
|
-
import {
|
|
3
|
-
buildProgressCard,
|
|
4
|
-
buildHelpCard,
|
|
2
|
+
import {
|
|
3
|
+
buildProgressCard,
|
|
4
|
+
buildHelpCard,
|
|
5
5
|
buildCdContent,
|
|
6
6
|
buildCdCard,
|
|
7
7
|
buildSessionsCard,
|
|
8
|
-
buildStatusCard,
|
|
9
|
-
buildCodexUsageCard,
|
|
10
|
-
buildCodexResetConfirmCard,
|
|
11
|
-
buildButtons,
|
|
12
|
-
truncateContent,
|
|
13
|
-
getToolEmoji,
|
|
14
|
-
} from "../cards.ts";
|
|
15
|
-
import { ABD_HELP_LINE } from "../shared-prefix.ts";
|
|
8
|
+
buildStatusCard,
|
|
9
|
+
buildCodexUsageCard,
|
|
10
|
+
buildCodexResetConfirmCard,
|
|
11
|
+
buildButtons,
|
|
12
|
+
truncateContent,
|
|
13
|
+
getToolEmoji,
|
|
14
|
+
} from "../cards.ts";
|
|
15
|
+
import { ABD_HELP_LINE } from "../shared-prefix.ts";
|
|
16
16
|
|
|
17
17
|
// ---------------------------------------------------------------------------
|
|
18
18
|
// truncateContent
|
|
@@ -154,23 +154,23 @@ describe("buildHelpCard", () => {
|
|
|
154
154
|
expect(parsed.elements[0].text.content).toContain("你好");
|
|
155
155
|
});
|
|
156
156
|
|
|
157
|
-
it("includes action buttons", () => {
|
|
158
|
-
const card = buildHelpCard("test");
|
|
159
|
-
const parsed = JSON.parse(card);
|
|
160
|
-
const action = parsed.elements[2];
|
|
161
|
-
expect(action.tag).toBe("action");
|
|
162
|
-
expect(action.actions).toHaveLength(7);
|
|
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
|
-
});
|
|
173
|
-
});
|
|
157
|
+
it("includes action buttons", () => {
|
|
158
|
+
const card = buildHelpCard("test");
|
|
159
|
+
const parsed = JSON.parse(card);
|
|
160
|
+
const action = parsed.elements[2];
|
|
161
|
+
expect(action.tag).toBe("action");
|
|
162
|
+
expect(action.actions).toHaveLength(7);
|
|
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
|
+
});
|
|
173
|
+
});
|
|
174
174
|
|
|
175
175
|
// ---------------------------------------------------------------------------
|
|
176
176
|
// buildCdContent
|
|
@@ -425,7 +425,7 @@ describe("buildSessionsCard", () => {
|
|
|
425
425
|
// buildStatusCard
|
|
426
426
|
// ---------------------------------------------------------------------------
|
|
427
427
|
|
|
428
|
-
describe("buildStatusCard", () => {
|
|
428
|
+
describe("buildStatusCard", () => {
|
|
429
429
|
it("returns valid JSON with status text", () => {
|
|
430
430
|
const card = buildStatusCard("一切正常");
|
|
431
431
|
const parsed = JSON.parse(card);
|
|
@@ -445,54 +445,54 @@ describe("buildStatusCard", () => {
|
|
|
445
445
|
const action = parsed.elements[2];
|
|
446
446
|
expect(action.actions[0].text.content).toBe("收起");
|
|
447
447
|
});
|
|
448
|
-
});
|
|
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
|
-
|
|
493
|
-
// ---------------------------------------------------------------------------
|
|
494
|
-
// buildButtons
|
|
495
|
-
// ---------------------------------------------------------------------------
|
|
448
|
+
});
|
|
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
|
+
|
|
493
|
+
// ---------------------------------------------------------------------------
|
|
494
|
+
// buildButtons
|
|
495
|
+
// ---------------------------------------------------------------------------
|
|
496
496
|
|
|
497
497
|
describe("buildButtons", () => {
|
|
498
498
|
it("returns action with buttons", () => {
|
|
@@ -9,16 +9,20 @@ import {
|
|
|
9
9
|
function deps(): CodexResetActionDeps & {
|
|
10
10
|
getTenantAccessToken: ReturnType<typeof vi.fn>;
|
|
11
11
|
sendRawCard: ReturnType<typeof vi.fn>;
|
|
12
|
+
sendTextReply: ReturnType<typeof vi.fn>;
|
|
12
13
|
sendCardReply: ReturnType<typeof vi.fn>;
|
|
13
14
|
updateCardMessage: ReturnType<typeof vi.fn>;
|
|
15
|
+
recallMessage: ReturnType<typeof vi.fn>;
|
|
14
16
|
consumeCodexRateLimitResetCredit: ReturnType<typeof vi.fn>;
|
|
15
17
|
createRequestId: ReturnType<typeof vi.fn>;
|
|
16
18
|
} {
|
|
17
19
|
return {
|
|
18
20
|
getTenantAccessToken: vi.fn(async () => "tenant-token"),
|
|
19
21
|
sendRawCard: vi.fn(async () => true),
|
|
22
|
+
sendTextReply: vi.fn(async () => true),
|
|
20
23
|
sendCardReply: vi.fn(async () => true),
|
|
21
24
|
updateCardMessage: vi.fn(async () => true),
|
|
25
|
+
recallMessage: vi.fn(async () => true),
|
|
22
26
|
consumeCodexRateLimitResetCredit: vi.fn(async () => ({ code: "reset" as const, windowsReset: 2 })),
|
|
23
27
|
createRequestId: vi.fn(() => "request-1"),
|
|
24
28
|
};
|
|
@@ -77,13 +81,16 @@ describe("Codex reset card actions", () => {
|
|
|
77
81
|
expect(d.consumeCodexRateLimitResetCredit).toHaveBeenCalledWith("request-1");
|
|
78
82
|
expect(d.updateCardMessage).toHaveBeenCalledTimes(1);
|
|
79
83
|
expect(d.updateCardMessage.mock.calls[0][1]).toBe("confirm-message");
|
|
80
|
-
expect(d.
|
|
84
|
+
expect(JSON.parse(d.updateCardMessage.mock.calls[0][2]).elements).toEqual([
|
|
85
|
+
{ tag: "markdown", content: " " },
|
|
86
|
+
]);
|
|
87
|
+
expect(d.recallMessage).toHaveBeenCalledWith("tenant-token", "confirm-message");
|
|
88
|
+
expect(d.sendTextReply).toHaveBeenCalledWith(
|
|
81
89
|
"tenant-token",
|
|
82
90
|
"chat-1",
|
|
83
|
-
"Codex 主动重置",
|
|
84
91
|
expect.stringContaining("重置成功"),
|
|
85
|
-
"green",
|
|
86
92
|
);
|
|
93
|
+
expect(d.sendCardReply).not.toHaveBeenCalled();
|
|
87
94
|
});
|
|
88
95
|
|
|
89
96
|
it("does not consume a reset credit and sends a cancellation message when user declines", async () => {
|
|
@@ -102,12 +109,38 @@ describe("Codex reset card actions", () => {
|
|
|
102
109
|
|
|
103
110
|
expect(d.consumeCodexRateLimitResetCredit).not.toHaveBeenCalled();
|
|
104
111
|
expect(d.updateCardMessage).toHaveBeenCalledTimes(1);
|
|
105
|
-
expect(d.
|
|
112
|
+
expect(d.recallMessage).toHaveBeenCalledWith("tenant-token", "confirm-message");
|
|
113
|
+
expect(d.sendTextReply).toHaveBeenCalledWith(
|
|
106
114
|
"tenant-token",
|
|
107
115
|
"chat-1",
|
|
108
|
-
"Codex
|
|
109
|
-
"用户取消了重置。",
|
|
110
|
-
"grey",
|
|
116
|
+
"Codex 主动重置:用户取消了重置。",
|
|
111
117
|
);
|
|
118
|
+
expect(d.sendCardReply).not.toHaveBeenCalled();
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
it("can collapse a confirmation card using context.message_id fallback", async () => {
|
|
122
|
+
const d = deps();
|
|
123
|
+
await handleCodexResetCardAction(event({ action: "codex_reset_request" }, "usage-message"), d);
|
|
124
|
+
|
|
125
|
+
await expect(handleCodexResetCardAction({
|
|
126
|
+
event: {
|
|
127
|
+
context: {
|
|
128
|
+
message_id: "confirm-message-from-context",
|
|
129
|
+
open_chat_id: "chat-1",
|
|
130
|
+
},
|
|
131
|
+
action: {
|
|
132
|
+
value: {
|
|
133
|
+
action: "codex_reset_confirm",
|
|
134
|
+
decision: "no",
|
|
135
|
+
parentMessageId: "usage-message",
|
|
136
|
+
requestId: "request-1",
|
|
137
|
+
},
|
|
138
|
+
},
|
|
139
|
+
},
|
|
140
|
+
}, d)).resolves.toBe(true);
|
|
141
|
+
|
|
142
|
+
expect(d.updateCardMessage).toHaveBeenCalledTimes(1);
|
|
143
|
+
expect(d.updateCardMessage.mock.calls[0][1]).toBe("confirm-message-from-context");
|
|
144
|
+
expect(d.recallMessage).toHaveBeenCalledWith("tenant-token", "confirm-message-from-context");
|
|
112
145
|
});
|
|
113
146
|
});
|