chatccc 0.2.184 → 0.2.186

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,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
 
@@ -237,9 +237,20 @@ codex --version
237
237
 
238
238
  Codex 的默认模型和推理强度可继续由 `~/.codex/config.toml` 管理,也可以在 `config.json` 中覆盖。
239
239
 
240
+ #### 可选:Chrome CDP
241
+
242
+ 常驻 Chrome CDP 开关用于维护本机 Chrome DevTools Protocol 端口;Codex `/usage` 可通过它读取 ChatGPT 订阅到期时间和剩余天数。
243
+
244
+ 依赖:
245
+
246
+ - 本机已安装 **Google Chrome**。
247
+ - 查询 ChatGPT 订阅到期时间时,需要在这个 CDP 专用 Chrome 窗口里登录 ChatGPT。
248
+
249
+ 默认端口是 `15166`,健康检查地址是 `http://127.0.0.1:15166/json/version`。Windows 下留空 `chromeDevtools.chromePath` 时会自动查找 Google Chrome。
250
+
240
251
  ### 4. `config.json`
241
252
 
242
- `~/.chatccc/config.json` 不存在时,ChatCCC 会从 `config.sample.json` 复制一份。常用结构如下:
253
+ `~/.chatccc/config.json` 不存在时,ChatCCC 会从 `config.sample.json` 复制一份。常用结构如下:
243
254
 
244
255
  ```json
245
256
  {
@@ -247,32 +258,37 @@ Codex 的默认模型和推理强度可继续由 `~/.codex/config.toml` 管理
247
258
  "appId": "cli_xxxxxxxxxxxx",
248
259
  "appSecret": "xxxxxxxxxxxxxxxxxxxxxxxxxx"
249
260
  },
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": {
261
+ "platforms": {
262
+ "feishu": { "enabled": true, "platformType": "feishu" },
263
+ "ilink": { "enabled": true, "reuseTokenOnStart": true }
264
+ },
265
+ "chromeDevtools": {
266
+ "enabled": false,
267
+ "port": 15166,
268
+ "chromePath": ""
269
+ },
270
+ "port": 18080,
271
+ "gitTimeoutSeconds": 180,
272
+ "allowInterrupt": false,
273
+ "claude": {
274
+ "enabled": false,
275
+ "defaultAgent": true,
276
+ "model": "claude-sonnet-4-6",
277
+ "subagentModel": "",
278
+ "effort": "",
279
+ "apiKey": "",
280
+ "baseUrl": "",
281
+ "maxTurn": 0
282
+ },
283
+ "cursor": {
284
+ "enabled": false,
285
+ "defaultAgent": false,
286
+ "path": "",
287
+ "model": "",
288
+ "avatarBatteryMode": "apiPercent",
289
+ "onDemandMonthlyBudget": 1000
290
+ },
291
+ "codex": {
276
292
  "enabled": false,
277
293
  "defaultAgent": false,
278
294
  "path": "",
@@ -285,21 +301,24 @@ Codex 的默认模型和推理强度可继续由 `~/.codex/config.toml` 管理
285
301
  | 字段 | 说明 |
286
302
  | --- | --- |
287
303
  | `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 编辑 |
304
+ | `platforms.feishu.enabled` | 是否启用飞书 |
305
+ | `platforms.feishu.platformType` | 飞书平台类型,默认 `feishu` |
306
+ | `platforms.ilink.enabled` | 是否启用微信 iLink |
307
+ | `platforms.ilink.reuseTokenOnStart` | 启动时是否复用已有微信登录 token |
308
+ | `chromeDevtools.enabled` | 是否启用常驻 Chrome CDP;默认 false |
309
+ | `chromeDevtools.port` | Chrome CDP 端口;默认 15166 |
310
+ | `chromeDevtools.chromePath` | Chrome 可执行文件路径;留空时自动探测 |
311
+ | `port` | 本地 Web 配置面板和中继服务端口 |
312
+ | `gitTimeoutSeconds` | `/git` 命令超时时间,默认 180 |
313
+ | `allowInterrupt` | 是否允许新消息中断正在运行的任务;默认 false |
314
+ | `*.enabled` | 是否启用对应 AI Agent |
315
+ | `*.defaultAgent` | `/new` 未指定 Agent 时使用哪个工具 |
316
+ | `cursor.path` / `codex.path` | CLI 可执行文件路径;留空时自动探测或使用 PATH |
317
+ | `cursor.avatarBatteryMode` | Cursor 头像电量显示来源:`apiPercent` `onDemandUse` |
318
+ | `cursor.onDemandMonthlyBudget` | `avatarBatteryMode=onDemandUse` 时用于计算电量的月预算 |
319
+ | `claude.model` / `claude.subagentModel` / `claude.effort` | 选填;设置后传给 Claude Agent SDK,留空以 `~/.claude/settings.json` 为准 |
320
+ | `claude.apiKey` / `claude.baseUrl` | 选填;设置后传给 Claude Agent SDK,留空以 `~/.claude/settings.json` 为准 |
321
+ | `claude.maxTurn` | 选填;Claude 最大对话轮数,默认 0(无限制),可在 Web UI 编辑 |
303
322
 
304
323
  > **权限控制**:普通消息以 `bypassPermissions` 模式运行,跳过 Agent 操作确认。使用 `/plan` 或 `/ask` 前缀时,ChatCCC 自动切换为只读模式:Claude SDK 仅放行 Read + stop-stuck-loop 网络请求,Codex 使用 `--sandbox read-only`,Cursor 使用 `--mode plan/ask`。请只在可信环境中使用。
305
324
 
@@ -315,26 +334,26 @@ Codex 的默认模型和推理强度可继续由 `~/.codex/config.toml` 管理
315
334
  | --- | --- |
316
335
  | `/new` | 使用默认 Agent 创建新会话 |
317
336
  | `/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`。
337
+ | `/new cursor` | 创建 Cursor 会话 |
338
+ | `/new codex` | 创建 Codex 会话 |
339
+ | `/newh` | 重置当前会话,保留工作目录 |
340
+ | `/model` | 查看或切换当前会话的模型 |
341
+ | `/stop` | 停止当前回复 |
342
+ | `/cancel` | 取消当前会话里排队等待处理的消息 |
343
+ | `/state` | 查看当前会话状态 |
344
+ | `/cd` | 查看或设置当前会话工作目录 |
345
+ | `/sessions` | 查看所有会话状态 |
346
+ | `/session <数字>` | 将当前群聊切换到 `/sessions` 列表中的指定会话 |
347
+ | `/usage` | 查看当前会话对应 Agent 的用量;Codex 显示 5h/周用量,若 Chrome CDP 可用且 ChatGPT 已登录,会额外显示订阅到期时间;Cursor 显示当前周期用量 |
348
+ | `/git <子命令>` | 在当前会话工作目录执行 `git ...` 并回传输出 |
349
+ | `/abd<内容>` | 去掉 `/abd` 前缀后把内容发给 Agent,并在消息末尾追加第一性原理需求澄清提示 |
350
+ | `/plan <内容>` | 只读计划模式:仅允许读文件和 stop-stuck-loop 请求,不执行任何写操作 |
351
+ | `/ask <内容>` | 只读问答模式:与 /plan 相同,仅允许读文件和 stop-stuck-loop 请求 |
352
+ | `/restart` | 重启机器人进程 |
353
+ | `/update` | 更新 npm 全局包并重启(仅限 `npm install -g chatccc` 安装的全局进程) |
354
+ | `/deleteg` | 解散当前飞书会话群;Agent 会话记录保留 |
355
+
356
+ > **模型切换**:`/model` 查看当前会话 Agent 的可选模型清单,`/model <名称>` 模糊匹配切换,`/model clear` 恢复默认。可选模型来自当前 Agent 的配置:Claude 使用 `claude.model` / `claude.subagentModel`,Cursor 使用 `cursor.model`,Codex 使用 `codex.model`。
338
357
 
339
358
  ---
340
359
 
@@ -1,3 +1,14 @@
1
1
  ## 计划/问答模式权限规则
2
2
 
3
- 如果用户消息以 `/plan` 或 `/ask` 开头,说明当前处于只读模式。**遇到任何需要用户同意的权限请求时,不要向用户申请权限,立即调用 stop-stuck-loop 接口提前结束本轮会话。**
3
+ 如果用户消息以 `/plan` 或 `/ask` 开头,说明当前处于只读模式。**遇到任何需要用户同意的权限请求时,不要向用户申请权限,立即调用 stop-stuck-loop 接口提前结束本轮会话。**
4
+
5
+ ## 用户问题澄清规则
6
+
7
+ 当需要在开始实现前向用户确认问题、让用户做选择,或用户要求“从第一性原理出发挖掘真实需求 / 判断需求是否合理 / 开始实现前有什么问题要问我”时,**不要调用 `AskQuestion`、多选表单或任何结构化选择工具**。
8
+
9
+ 必须直接用普通文本回复用户:
10
+
11
+ - 先简要说明你理解到的真实需求和你对合理性的判断。
12
+ - 再列出需要用户确认的问题;如果有推荐选项,就用文本说明“我建议 X,原因是 Y”。
13
+ - 问题数量尽量少,只问会影响实现边界、风险或不可逆决策的问题。
14
+ - 不要把这些问题放进工具调用参数里,也不要只在卡片/工具结果中呈现。
@@ -7,10 +7,35 @@
7
7
  "feishu": { "enabled": true, "platformType": "feishu" },
8
8
  "ilink": { "enabled": true, "reuseTokenOnStart": true }
9
9
  },
10
- "port": 18080,
11
- "gitTimeoutSeconds": 180,
12
- "allowInterrupt": false,
13
- "claude": {
10
+ "chromeDevtools": {
11
+ "enabled": false,
12
+ "port": 15166,
13
+ "chromePath": ""
14
+ },
15
+ "port": 18080,
16
+ "gitTimeoutSeconds": 180,
17
+ "allowInterrupt": false,
18
+ "rawStreamLogs": {
19
+ "claude": {
20
+ "enabled": false,
21
+ "maxBytesPerTurn": 52428800,
22
+ "retentionDays": 7,
23
+ "keepCompleted": false
24
+ },
25
+ "cursor": {
26
+ "enabled": false,
27
+ "maxBytesPerTurn": 52428800,
28
+ "retentionDays": 7,
29
+ "keepCompleted": false
30
+ },
31
+ "codex": {
32
+ "enabled": false,
33
+ "maxBytesPerTurn": 52428800,
34
+ "retentionDays": 7,
35
+ "keepCompleted": false
36
+ }
37
+ },
38
+ "claude": {
14
39
  "enabled": false,
15
40
  "defaultAgent": true,
16
41
  "model": "",
@@ -7,6 +7,6 @@ Current working directory: {{cwd}}
7
7
 
8
8
  Use local endpoints instead of calling Feishu Open Platform directly.
9
9
 
10
- - **Send images**: POST `{{send_image_url}}` with `{"session_id":"{{session_id}}","path":"<absolute path>","caption":"<optional>"}` — read `{{im_skills_cache_dir}}/feishu-skill/receive-send-image.md`
11
- - **Send files/videos**: POST `{{send_file_url}}` with `{"session_id":"{{session_id}}","path":"<absolute path>","caption":"<optional>"}` — read `{{im_skills_cache_dir}}/feishu-skill/receive-send-file.md`
12
- - **Delegate a task to a new agent conversation**: POST `{{delegate_task_url}}` with `{"tool":"codex|claude|cursor","cwd":"<absolute working directory>","open_id":"<Feishu user open_id>","prompt":"<first task>"}`. Use `open_ids` for multiple users. This uses the normal ChatCCC prompt flow, so project prompt injection and IM skills still apply.
10
+ - **Send images**: POST `{{send_image_url}}` with `{"session_id":"{{session_id}}","path":"<absolute path>","caption":"<optional>"}` — read `{{im_skills_cache_dir}}/feishu-skill/receive-send-image.md`
11
+ - **Send files/videos**: POST `{{send_file_url}}` with `{"session_id":"{{session_id}}","path":"<absolute path>","caption":"<optional>"}` — read `{{im_skills_cache_dir}}/feishu-skill/receive-send-file.md`
12
+ - **Delegate a task to a new agent conversation**: POST `{{delegate_task_url}}` with `{"tool":"codex|claude|cursor","cwd":"<absolute working directory>","open_id":"<Feishu user open_id>","prompt":"<first task>"}`. Use `open_ids` for multiple users. This uses the normal ChatCCC prompt flow, so project prompt injection and IM skills still apply.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "chatccc",
3
- "version": "0.2.184",
3
+ "version": "0.2.186",
4
4
  "description": "Feishu bot bridge for Claude Code",
5
5
  "license": "Apache-2.0",
6
6
  "type": "module",
@@ -1,165 +1,165 @@
1
- import { Readable } from "node:stream";
2
- import { describe, expect, it, vi, beforeEach } from "vitest";
3
-
4
- import type { PlatformAdapter } from "../platform-adapter.ts";
5
-
6
- const mockDelegateAgentTask = vi.hoisted(() => vi.fn());
7
-
8
- vi.mock("../agent-delegate-task.ts", () => ({
9
- delegateAgentTask: mockDelegateAgentTask,
10
- }));
11
-
12
- import {
13
- AGENT_DELEGATE_TASK_PATH,
14
- buildAgentDelegateTaskCapabilityPrompt,
15
- handleAgentDelegateTaskRequest,
16
- } from "../agent-delegate-task-rpc.ts";
17
- import { ABD_APPEND_PROMPT } from "../shared-prefix.ts";
18
-
19
- function request(body: unknown, path = AGENT_DELEGATE_TASK_PATH, method = "POST"): Readable & {
20
- url?: string;
21
- method?: string;
22
- headers: Record<string, string>;
23
- } {
24
- const req = Readable.from([Buffer.from(JSON.stringify(body), "utf8")]) as Readable & {
25
- url?: string;
26
- method?: string;
27
- headers: Record<string, string>;
28
- };
29
- req.url = path;
30
- req.method = method;
31
- req.headers = { "content-type": "application/json; charset=utf-8" };
32
- return req;
33
- }
34
-
35
- function response() {
36
- const res = {
37
- statusCode: 0,
38
- headers: {} as Record<string, string>,
39
- body: "",
40
- writeHead(status: number, headers: Record<string, string>) {
41
- this.statusCode = status;
42
- this.headers = headers;
43
- return this;
44
- },
45
- end(chunk?: string) {
46
- this.body += chunk ?? "";
47
- return this;
48
- },
49
- };
50
- return res;
51
- }
52
-
53
- function platform(kind: "feishu" | "wechat" = "feishu"): PlatformAdapter {
54
- return {
55
- kind,
56
- sendText: vi.fn(async () => true),
57
- sendCard: vi.fn(async () => true),
58
- sendRawCard: vi.fn(async () => true),
59
- createGroup: vi.fn(async () => "chat-id"),
60
- updateChatInfo: vi.fn(async () => {}),
61
- getChatInfo: vi.fn(async () => ({ name: "chat", description: "" })),
62
- disbandChat: vi.fn(async () => {}),
63
- setChatAvatar: vi.fn(async () => {}),
64
- extractSessionInfo: vi.fn(() => null),
65
- cardCreate: vi.fn(async () => "card-id"),
66
- cardSend: vi.fn(async () => "message-id"),
67
- cardUpdate: vi.fn(async () => {}),
68
- };
69
- }
70
-
71
- describe("agent delegate task RPC", () => {
72
- beforeEach(() => {
73
- mockDelegateAgentTask.mockReset();
74
- mockDelegateAgentTask.mockResolvedValue({
75
- chatId: "chat-new",
76
- sessionId: "sid-new",
77
- tool: "codex",
78
- cwd: "F:\\repo",
79
- });
80
- });
81
-
82
- it("delegates a task through the local API", async () => {
83
- const req = request({
84
- tool: "codex",
85
- cwd: "F:\\repo",
86
- open_id: "ou-user",
87
- prompt: "帮我分析日志",
88
- });
89
- const res = response();
90
-
91
- await handleAgentDelegateTaskRequest(req as never, res as never, platform());
92
-
93
- expect(res.statusCode).toBe(200);
94
- expect(JSON.parse(res.body)).toMatchObject({
95
- ok: true,
96
- chat_id: "chat-new",
97
- session_id: "sid-new",
98
- tool: "codex",
99
- });
100
- expect(mockDelegateAgentTask).toHaveBeenCalledWith(expect.objectContaining({
101
- tool: "codex",
102
- cwd: expect.stringContaining("repo"),
103
- promptText: "帮我分析日志",
104
- openIds: ["ou-user"],
105
- chatNamePrefix: "帮我分析日志",
106
- }));
107
- });
108
-
109
- it("applies the shared ABD prefix to API prompts", async () => {
110
- const req = request({
111
- tool: "claude",
112
- cwd: "F:\\repo",
113
- openIds: ["ou-user"],
114
- prompt: "/abd帮我分析",
115
- });
116
- const res = response();
117
-
118
- await handleAgentDelegateTaskRequest(req as never, res as never, platform());
119
-
120
- expect(res.statusCode).toBe(200);
121
- expect(mockDelegateAgentTask).toHaveBeenCalledWith(expect.objectContaining({
122
- promptText: `帮我分析\n\n---\n${ABD_APPEND_PROMPT}`,
123
- chatNamePrefix: "帮我分析",
124
- }));
125
- });
126
-
127
- it("rejects non-Feishu platforms", async () => {
128
- const req = request({
129
- tool: "codex",
130
- cwd: "F:\\repo",
131
- open_id: "ou-user",
132
- prompt: "task",
133
- });
134
- const res = response();
135
-
136
- await handleAgentDelegateTaskRequest(req as never, res as never, platform("wechat"));
137
-
138
- expect(res.statusCode).toBe(409);
139
- expect(mockDelegateAgentTask).not.toHaveBeenCalled();
140
- });
141
-
142
- it("rejects missing required parameters", async () => {
143
- const req = request({ tool: "codex", cwd: "F:\\repo", prompt: "task" });
144
- const res = response();
145
-
146
- await handleAgentDelegateTaskRequest(req as never, res as never, platform());
147
-
148
- expect(res.statusCode).toBe(400);
149
- expect(JSON.parse(res.body).error).toContain("open_id");
150
- expect(mockDelegateAgentTask).not.toHaveBeenCalled();
151
- });
152
-
153
- it("builds prompt instructions for the delegate endpoint", () => {
154
- const prompt = buildAgentDelegateTaskCapabilityPrompt({
155
- url: `http://127.0.0.1:18080${AGENT_DELEGATE_TASK_PATH}`,
156
- cwd: "F:/repo",
157
- });
158
-
159
- expect(prompt).toContain("POST http://127.0.0.1:18080/api/agent/delegate-task");
160
- expect(prompt).toContain('"tool":"codex|claude|cursor"');
161
- expect(prompt).toContain('"cwd":"absolute working directory"');
162
- expect(prompt).toContain("project prompt injection and IM skills still apply");
163
- expect(prompt).toContain("Current working directory: F:/repo");
164
- });
165
- });
1
+ import { Readable } from "node:stream";
2
+ import { describe, expect, it, vi, beforeEach } from "vitest";
3
+
4
+ import type { PlatformAdapter } from "../platform-adapter.ts";
5
+
6
+ const mockDelegateAgentTask = vi.hoisted(() => vi.fn());
7
+
8
+ vi.mock("../agent-delegate-task.ts", () => ({
9
+ delegateAgentTask: mockDelegateAgentTask,
10
+ }));
11
+
12
+ import {
13
+ AGENT_DELEGATE_TASK_PATH,
14
+ buildAgentDelegateTaskCapabilityPrompt,
15
+ handleAgentDelegateTaskRequest,
16
+ } from "../agent-delegate-task-rpc.ts";
17
+ import { ABD_APPEND_PROMPT } from "../shared-prefix.ts";
18
+
19
+ function request(body: unknown, path = AGENT_DELEGATE_TASK_PATH, method = "POST"): Readable & {
20
+ url?: string;
21
+ method?: string;
22
+ headers: Record<string, string>;
23
+ } {
24
+ const req = Readable.from([Buffer.from(JSON.stringify(body), "utf8")]) as Readable & {
25
+ url?: string;
26
+ method?: string;
27
+ headers: Record<string, string>;
28
+ };
29
+ req.url = path;
30
+ req.method = method;
31
+ req.headers = { "content-type": "application/json; charset=utf-8" };
32
+ return req;
33
+ }
34
+
35
+ function response() {
36
+ const res = {
37
+ statusCode: 0,
38
+ headers: {} as Record<string, string>,
39
+ body: "",
40
+ writeHead(status: number, headers: Record<string, string>) {
41
+ this.statusCode = status;
42
+ this.headers = headers;
43
+ return this;
44
+ },
45
+ end(chunk?: string) {
46
+ this.body += chunk ?? "";
47
+ return this;
48
+ },
49
+ };
50
+ return res;
51
+ }
52
+
53
+ function platform(kind: "feishu" | "wechat" = "feishu"): PlatformAdapter {
54
+ return {
55
+ kind,
56
+ sendText: vi.fn(async () => true),
57
+ sendCard: vi.fn(async () => true),
58
+ sendRawCard: vi.fn(async () => true),
59
+ createGroup: vi.fn(async () => "chat-id"),
60
+ updateChatInfo: vi.fn(async () => {}),
61
+ getChatInfo: vi.fn(async () => ({ name: "chat", description: "" })),
62
+ disbandChat: vi.fn(async () => {}),
63
+ setChatAvatar: vi.fn(async () => {}),
64
+ extractSessionInfo: vi.fn(() => null),
65
+ cardCreate: vi.fn(async () => "card-id"),
66
+ cardSend: vi.fn(async () => "message-id"),
67
+ cardUpdate: vi.fn(async () => {}),
68
+ };
69
+ }
70
+
71
+ describe("agent delegate task RPC", () => {
72
+ beforeEach(() => {
73
+ mockDelegateAgentTask.mockReset();
74
+ mockDelegateAgentTask.mockResolvedValue({
75
+ chatId: "chat-new",
76
+ sessionId: "sid-new",
77
+ tool: "codex",
78
+ cwd: "F:\\repo",
79
+ });
80
+ });
81
+
82
+ it("delegates a task through the local API", async () => {
83
+ const req = request({
84
+ tool: "codex",
85
+ cwd: "F:\\repo",
86
+ open_id: "ou-user",
87
+ prompt: "帮我分析日志",
88
+ });
89
+ const res = response();
90
+
91
+ await handleAgentDelegateTaskRequest(req as never, res as never, platform());
92
+
93
+ expect(res.statusCode).toBe(200);
94
+ expect(JSON.parse(res.body)).toMatchObject({
95
+ ok: true,
96
+ chat_id: "chat-new",
97
+ session_id: "sid-new",
98
+ tool: "codex",
99
+ });
100
+ expect(mockDelegateAgentTask).toHaveBeenCalledWith(expect.objectContaining({
101
+ tool: "codex",
102
+ cwd: expect.stringContaining("repo"),
103
+ promptText: "帮我分析日志",
104
+ openIds: ["ou-user"],
105
+ chatNamePrefix: "帮我分析日志",
106
+ }));
107
+ });
108
+
109
+ it("applies the shared ABD prefix to API prompts", async () => {
110
+ const req = request({
111
+ tool: "claude",
112
+ cwd: "F:\\repo",
113
+ openIds: ["ou-user"],
114
+ prompt: "/abd帮我分析",
115
+ });
116
+ const res = response();
117
+
118
+ await handleAgentDelegateTaskRequest(req as never, res as never, platform());
119
+
120
+ expect(res.statusCode).toBe(200);
121
+ expect(mockDelegateAgentTask).toHaveBeenCalledWith(expect.objectContaining({
122
+ promptText: `帮我分析\n\n---\n${ABD_APPEND_PROMPT}`,
123
+ chatNamePrefix: "帮我分析",
124
+ }));
125
+ });
126
+
127
+ it("rejects non-Feishu platforms", async () => {
128
+ const req = request({
129
+ tool: "codex",
130
+ cwd: "F:\\repo",
131
+ open_id: "ou-user",
132
+ prompt: "task",
133
+ });
134
+ const res = response();
135
+
136
+ await handleAgentDelegateTaskRequest(req as never, res as never, platform("wechat"));
137
+
138
+ expect(res.statusCode).toBe(409);
139
+ expect(mockDelegateAgentTask).not.toHaveBeenCalled();
140
+ });
141
+
142
+ it("rejects missing required parameters", async () => {
143
+ const req = request({ tool: "codex", cwd: "F:\\repo", prompt: "task" });
144
+ const res = response();
145
+
146
+ await handleAgentDelegateTaskRequest(req as never, res as never, platform());
147
+
148
+ expect(res.statusCode).toBe(400);
149
+ expect(JSON.parse(res.body).error).toContain("open_id");
150
+ expect(mockDelegateAgentTask).not.toHaveBeenCalled();
151
+ });
152
+
153
+ it("builds prompt instructions for the delegate endpoint", () => {
154
+ const prompt = buildAgentDelegateTaskCapabilityPrompt({
155
+ url: `http://127.0.0.1:18080${AGENT_DELEGATE_TASK_PATH}`,
156
+ cwd: "F:/repo",
157
+ });
158
+
159
+ expect(prompt).toContain("POST http://127.0.0.1:18080/api/agent/delegate-task");
160
+ expect(prompt).toContain('"tool":"codex|claude|cursor"');
161
+ expect(prompt).toContain('"cwd":"absolute working directory"');
162
+ expect(prompt).toContain("project prompt injection and IM skills still apply");
163
+ expect(prompt).toContain("Current working directory: F:/repo");
164
+ });
165
+ });