chatccc 0.2.219 → 0.2.220
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 +36 -16
- package/config.sample.json +4 -1
- package/package.json +1 -1
- package/src/__tests__/builtin-config.test.ts +3 -0
- package/src/__tests__/cards.test.ts +6 -6
- package/src/__tests__/config-reload.test.ts +19 -8
- package/src/__tests__/config-sample.test.ts +6 -2
- package/src/__tests__/orchestrator.test.ts +84 -5
- package/src/__tests__/sim-platform.test.ts +10 -0
- package/src/__tests__/web-ui.test.ts +40 -0
- package/src/cards.ts +7 -5
- package/src/config.ts +41 -10
- package/src/index.ts +13 -0
- package/src/orchestrator.ts +1553 -1488
- package/src/web-ui.ts +164 -46
package/README.md
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
# ChatCCC
|
|
2
2
|
|
|
3
|
-
**用飞书或微信聊天控制 Claude Code / Cursor / Codex。**
|
|
3
|
+
**用飞书或微信聊天控制 Claude Code / Cursor / Codex / CCC Agent。**
|
|
4
4
|
|
|
5
|
-
ChatCCC 把本地 AI 编程工具接入即时通讯软件。你可以在手机上发消息,让 Claude Code、Cursor Agent
|
|
5
|
+
ChatCCC 把本地 AI 编程工具接入即时通讯软件。你可以在手机上发消息,让 Claude Code、Cursor Agent、Codex 或内置 CCC Agent 继续写代码、查问题、跑命令;不用一直守在电脑前。
|
|
6
6
|
|
|
7
7
|
飞书是推荐入口:直接私聊机器人即可持续使用同一个专属会话,需要并行任务时再用 `/new` 创建独立会话群;卡片能流式更新,体验完整。微信 iLink 更适合快速试用或临时使用:扫码即可接入,但只能走私聊文本模式。
|
|
8
8
|
|
|
@@ -21,7 +21,7 @@ ChatCCC 把本地 AI 编程工具接入即时通讯软件。你可以在手机
|
|
|
21
21
|
- **手机上也能用 AI 编程工具**:在飞书或微信发消息,就像在终端给 Agent 下指令。
|
|
22
22
|
- **飞书体验更完整**:私聊可持续对话,`/new` 创建的一群一会话支持多任务并行,CardKit 卡片可流式更新。
|
|
23
23
|
- **微信接入更轻**:不用创建飞书应用,启动后扫码即可在微信私聊里使用。
|
|
24
|
-
- **多 Agent 切换**:`/new` 使用默认 Agent,也可以用 `/new claude`、`/new cursor`、`/new codex` 指定工具。
|
|
24
|
+
- **多 Agent 切换**:`/new` 使用默认 Agent,也可以用 `/new claude`、`/new cursor`、`/new codex`、`/new ccc` 指定工具。
|
|
25
25
|
- **群里能跑 git**:`/git status`、`/git pull`、`/git log` 会在当前会话工作目录执行,并把输出发回聊天窗口。
|
|
26
26
|
|
|
27
27
|
## 飞书和微信的差异
|
|
@@ -208,7 +208,7 @@ chatccc
|
|
|
208
208
|
|
|
209
209
|
### 3. AI 工具配置
|
|
210
210
|
|
|
211
|
-
|
|
211
|
+
Claude Code、Cursor 和 Codex 需要对应的本地工具;CCC Agent 内置于 ChatCCC,只需配置 DeepSeek 兼容 API。
|
|
212
212
|
|
|
213
213
|
#### Claude Code
|
|
214
214
|
|
|
@@ -237,7 +237,13 @@ codex login
|
|
|
237
237
|
codex --version
|
|
238
238
|
```
|
|
239
239
|
|
|
240
|
-
Codex 的默认模型和推理强度可继续由 `~/.codex/config.toml` 管理,也可以在 `config.json` 中覆盖。
|
|
240
|
+
Codex 的默认模型和推理强度可继续由 `~/.codex/config.toml` 管理,也可以在 `config.json` 中覆盖。
|
|
241
|
+
|
|
242
|
+
#### CCC Agent
|
|
243
|
+
|
|
244
|
+
CCC Agent 是 ChatCCC 内置的编程 Agent,不需要额外安装 CLI。在首次配置向导或 Web 管理页中启用后,填写 DeepSeek 兼容 API Key、Base URL 和模型即可使用;它可以设为 `/new` 的默认 Agent,也可以通过 `/new ccc` 显式创建会话。
|
|
245
|
+
|
|
246
|
+
`ccc.alternativeModel` 是单个备选模型,只会加入 `/model` 的人工切换列表,不会在请求失败时自动重试或切换,避免重复执行带副作用的工具调用。
|
|
241
247
|
|
|
242
248
|
#### 可选:Chrome CDP
|
|
243
249
|
|
|
@@ -285,22 +291,32 @@ Codex 的默认模型和推理强度可继续由 `~/.codex/config.toml` 管理
|
|
|
285
291
|
"baseUrl": "",
|
|
286
292
|
"maxTurn": 0
|
|
287
293
|
},
|
|
288
|
-
"cursor": {
|
|
294
|
+
"cursor": {
|
|
289
295
|
"enabled": false,
|
|
290
296
|
"defaultAgent": false,
|
|
291
|
-
"path": "",
|
|
292
|
-
"model": "",
|
|
293
|
-
"
|
|
297
|
+
"path": "",
|
|
298
|
+
"model": "",
|
|
299
|
+
"alternativeModel": "",
|
|
300
|
+
"avatarBatteryMode": "apiPercent",
|
|
294
301
|
"onDemandMonthlyBudget": 1000
|
|
295
302
|
},
|
|
296
|
-
"codex": {
|
|
303
|
+
"codex": {
|
|
297
304
|
"enabled": false,
|
|
298
305
|
"defaultAgent": false,
|
|
299
306
|
"path": "",
|
|
300
307
|
"model": "",
|
|
308
|
+
"alternativeModel": "",
|
|
301
309
|
"effort": "",
|
|
302
310
|
"fastMode": false
|
|
303
|
-
}
|
|
311
|
+
},
|
|
312
|
+
"ccc": {
|
|
313
|
+
"enabled": false,
|
|
314
|
+
"defaultAgent": false,
|
|
315
|
+
"DEEPSEEK_API_KEY": "",
|
|
316
|
+
"DEEPSEEK_BASE_URL": "https://api.deepseek.com/v1",
|
|
317
|
+
"model": "deepseek-v4-pro",
|
|
318
|
+
"alternativeModel": ""
|
|
319
|
+
}
|
|
304
320
|
}
|
|
305
321
|
```
|
|
306
322
|
|
|
@@ -326,13 +342,16 @@ Codex 的默认模型和推理强度可继续由 `~/.codex/config.toml` 管理
|
|
|
326
342
|
| `cursor.onDemandMonthlyBudget` | `avatarBatteryMode=onDemandUse` 时用于计算电量的月预算 |
|
|
327
343
|
| `claude.model` / `claude.subagentModel` / `claude.effort` | 选填;设置后传给 Claude Agent SDK,留空以 `~/.claude/settings.json` 为准 |
|
|
328
344
|
| `claude.apiKey` / `claude.baseUrl` | 选填;设置后传给 Claude Agent SDK,留空以 `~/.claude/settings.json` 为准 |
|
|
329
|
-
| `claude.maxTurn` | 选填;Claude 最大对话轮数,默认 0(无限制),可在 Web UI 编辑 |
|
|
345
|
+
| `claude.maxTurn` | 选填;Claude 最大对话轮数,默认 0(无限制),可在 Web UI 编辑 |
|
|
346
|
+
| `cursor.alternativeModel` / `codex.alternativeModel` / `ccc.alternativeModel` | 单个备选模型;加入 `/model` 人工切换列表,不会自动故障转移 |
|
|
347
|
+
| `ccc.DEEPSEEK_API_KEY` / `ccc.DEEPSEEK_BASE_URL` | CCC Agent 使用的 DeepSeek 兼容 API 凭证和服务地址 |
|
|
348
|
+
| `ccc.model` | CCC Agent 默认模型 |
|
|
330
349
|
|
|
331
350
|
> **权限控制**:普通消息以 `bypassPermissions` 模式运行,跳过 Agent 操作确认。使用 `/plan` 或 `/ask` 前缀时,ChatCCC 自动切换为只读模式:Claude SDK 仅放行 Read + stop-stuck-loop 网络请求,Codex 使用 `--sandbox read-only`,Cursor 使用 `--mode plan/ask`。请只在可信环境中使用。
|
|
332
351
|
|
|
333
352
|
### 5. 开始使用
|
|
334
353
|
|
|
335
|
-
**飞书:** 找到机器人后直接发送普通消息,即可在当前私聊中创建并持续使用专属 AI 会话;私聊工作目录固定为运行 ChatCCC 的系统账号用户目录。默认 Agent 发生变化后,下一条私聊普通消息会触发切换并创建新的空会话;若旧 Agent 正在生成,该消息会先排队,待当前回复完成后再切换。命令不会触发自动切换。需要独立任务时,发送 `/new`、`/new claude`、`/new cursor` 或 `/new
|
|
354
|
+
**飞书:** 找到机器人后直接发送普通消息,即可在当前私聊中创建并持续使用专属 AI 会话;私聊工作目录固定为运行 ChatCCC 的系统账号用户目录。默认 Agent 发生变化后,下一条私聊普通消息会触发切换并创建新的空会话;若旧 Agent 正在生成,该消息会先排队,待当前回复完成后再切换。命令不会触发自动切换。需要独立任务时,发送 `/new`、`/new claude`、`/new cursor`、`/new codex` 或 `/new ccc`,机器人会另外创建会话群。
|
|
336
355
|
|
|
337
356
|
**微信:** 扫码登录后,在机器人私聊里发送 `/new` 或指定 Agent 的 `/new ...` 命令即可开始。功能与飞书基本一致,但展示为纯文本。
|
|
338
357
|
|
|
@@ -348,6 +367,7 @@ Codex 的默认模型和推理强度可继续由 `~/.codex/config.toml` 管理
|
|
|
348
367
|
| `/new claude` | 创建 Claude Code 会话;飞书中会创建新群 |
|
|
349
368
|
| `/new cursor` | 创建 Cursor 会话;飞书中会创建新群 |
|
|
350
369
|
| `/new codex` | 创建 Codex 会话;飞书中会创建新群 |
|
|
370
|
+
| `/new ccc` | 创建内置 CCC Agent 会话;飞书中会创建新群 |
|
|
351
371
|
| `/newh` | 在当前聊天原地重置会话;群聊保留工作目录,飞书私聊固定使用系统用户目录 |
|
|
352
372
|
| `/model` | 查看或切换当前会话的模型 |
|
|
353
373
|
| `/fast` | 查看当前 Codex 会话的 Fast 模式;使用 `/fast on` 或 `/fast off` 切换 |
|
|
@@ -357,7 +377,7 @@ Codex 的默认模型和推理强度可继续由 `~/.codex/config.toml` 管理
|
|
|
357
377
|
| `/cd` | 查看或设置后续新建会话的默认工作目录,不改变当前会话;飞书私聊自身始终使用系统用户目录 |
|
|
358
378
|
| `/sessions` | 查看所有会话状态 |
|
|
359
379
|
| `/session <数字>` | 将当前群聊切换到 `/sessions` 列表中的指定会话;飞书私聊不支持切换 |
|
|
360
|
-
| `/usage` | 查看当前会话对应 Agent 的用量;Codex
|
|
380
|
+
| `/usage` | 查看当前会话对应 Agent 的用量;Codex 显示 5h/7天窗口,Cursor 显示当前周期用量,使用官方 DeepSeek API 的 CCC Agent 显示账户余额 |
|
|
361
381
|
| `/git <子命令>` | 在当前会话工作目录执行 `git ...` 并回传输出 |
|
|
362
382
|
| `/abd<内容>` | 去掉 `/abd` 前缀后把内容发给 Agent,并在消息末尾追加第一性原理需求澄清提示 |
|
|
363
383
|
| `/plan <内容>` | 只读计划模式:仅允许读文件和 stop-stuck-loop 请求,不执行任何写操作 |
|
|
@@ -368,7 +388,7 @@ Codex 的默认模型和推理强度可继续由 `~/.codex/config.toml` 管理
|
|
|
368
388
|
|
|
369
389
|
`/update` 会在执行 npm 更新前把飞书消息或按钮事件 ID 原子写入 `~/.chatccc/state/update-command-guard.json`。同一 ID 跨重启重投时会静默忽略;用户主动发送的新 `/update` 因事件 ID 不同,仍可立即执行。该保护仅作用于 `/update`,普通消息与 `/restart` 的处理不变。
|
|
370
390
|
|
|
371
|
-
> **模型切换**:`/model` 查看当前会话 Agent 的可选模型清单,`/model <名称>` 模糊匹配切换,`/model clear` 恢复默认。可选模型来自当前 Agent 的配置:Claude 使用 `claude.model` / `claude.subagentModel
|
|
391
|
+
> **模型切换**:`/model` 查看当前会话 Agent 的可选模型清单,`/model <名称>` 模糊匹配切换,`/model clear` 恢复默认。可选模型来自当前 Agent 的配置:Claude 使用 `claude.model` / `claude.subagentModel`;Cursor、Codex 和 CCC Agent 使用各自的 `model` / `alternativeModel`。
|
|
372
392
|
|
|
373
393
|
> **Codex Fast 模式**:Web UI 中的“Fast 模式”设置新 Codex 会话的全局默认值,默认关闭。进入 Codex 会话后,`/fast` 查询当前状态,`/fast on` 和 `/fast off` 只覆盖当前会话并从下一条消息生效。ChatCCC 会显式向 Codex CLI 传入 `service_tier="fast"` 或 `service_tier="default"`,因此关闭时不会继承用户 `config.toml` 中可能开启的 Fast。
|
|
374
394
|
|
|
@@ -376,4 +396,4 @@ Codex 的默认模型和推理强度可继续由 `~/.codex/config.toml` 管理
|
|
|
376
396
|
|
|
377
397
|
## 技术栈
|
|
378
398
|
|
|
379
|
-
TypeScript / Node.js >= 20 / tsx / Anthropic Claude Agent SDK / Cursor Agent CLI / Codex CLI / 飞书 WebSocket API / CardKit / 微信 iLink
|
|
399
|
+
TypeScript / Node.js >= 20 / tsx / AI SDK / Anthropic Claude Agent SDK / Cursor Agent CLI / Codex CLI / 飞书 WebSocket API / CardKit / 微信 iLink
|
package/config.sample.json
CHANGED
|
@@ -73,8 +73,11 @@
|
|
|
73
73
|
"fastMode": false
|
|
74
74
|
},
|
|
75
75
|
"ccc": {
|
|
76
|
+
"enabled": false,
|
|
77
|
+
"defaultAgent": false,
|
|
76
78
|
"DEEPSEEK_API_KEY": "",
|
|
77
79
|
"DEEPSEEK_BASE_URL": "https://api.deepseek.com/v1",
|
|
78
|
-
"model": "deepseek-v4-pro"
|
|
80
|
+
"model": "deepseek-v4-pro",
|
|
81
|
+
"alternativeModel": ""
|
|
79
82
|
}
|
|
80
83
|
}
|
package/package.json
CHANGED
|
@@ -18,9 +18,12 @@ afterEach(() => {
|
|
|
18
18
|
describe("builtin ChatSession config", () => {
|
|
19
19
|
it("does not fall back to DEEPSEEK_API_KEY environment variable", () => {
|
|
20
20
|
config.ccc = {
|
|
21
|
+
enabled: false,
|
|
22
|
+
defaultAgent: false,
|
|
21
23
|
DEEPSEEK_API_KEY: "",
|
|
22
24
|
DEEPSEEK_BASE_URL: "https://api.deepseek.com/v1",
|
|
23
25
|
model: "deepseek-v4-pro",
|
|
26
|
+
alternativeModel: "",
|
|
24
27
|
};
|
|
25
28
|
process.env.DEEPSEEK_API_KEY = "sk-env-should-not-be-used";
|
|
26
29
|
|
|
@@ -161,7 +161,7 @@ describe("buildHelpCard", () => {
|
|
|
161
161
|
const parsed = JSON.parse(card);
|
|
162
162
|
const action = parsed.elements[2];
|
|
163
163
|
expect(action.tag).toBe("action");
|
|
164
|
-
expect(action.actions).toHaveLength(
|
|
164
|
+
expect(action.actions).toHaveLength(8);
|
|
165
165
|
});
|
|
166
166
|
|
|
167
167
|
it("adds ABD prefix help as the final help line", () => {
|
|
@@ -169,17 +169,17 @@ describe("buildHelpCard", () => {
|
|
|
169
169
|
const parsed = JSON.parse(card);
|
|
170
170
|
const lines = parsed.elements[1].text.content.split("\n");
|
|
171
171
|
|
|
172
|
-
expect(lines).toContain("发送 **/usage**
|
|
172
|
+
expect(lines).toContain("发送 **/usage** 查看当前 Agent 的用量或余额");
|
|
173
173
|
expect(lines.at(-1)).toBe(ABD_HELP_LINE);
|
|
174
174
|
});
|
|
175
175
|
|
|
176
|
-
it("
|
|
176
|
+
it("advertises CCC Agent as a normal session option", () => {
|
|
177
177
|
const card = buildHelpCard("test");
|
|
178
178
|
const parsed = JSON.parse(card);
|
|
179
179
|
const text = JSON.stringify(parsed);
|
|
180
180
|
|
|
181
|
-
expect(text).
|
|
182
|
-
expect(text).
|
|
181
|
+
expect(text).toContain("/new ccc");
|
|
182
|
+
expect(text).toContain("CCC Agent");
|
|
183
183
|
});
|
|
184
184
|
});
|
|
185
185
|
|
|
@@ -419,7 +419,7 @@ describe("buildSessionsCard", () => {
|
|
|
419
419
|
expect(content).not.toContain("Cursor 会话");
|
|
420
420
|
});
|
|
421
421
|
|
|
422
|
-
it("labels
|
|
422
|
+
it("labels CCC sessions without grouping them as Claude Code", () => {
|
|
423
423
|
const card = buildSessionsCard([
|
|
424
424
|
{ sessionId: "session-20260702-121530-a1b2c3", chatName: "", chatId: "oc_ccc", active: false, turnCount: 1, elapsedSeconds: null, model: "deepseek-v4-pro", tool: "ccc" },
|
|
425
425
|
]);
|
|
@@ -72,7 +72,7 @@ const baseAppConfig: AppConfig = {
|
|
|
72
72
|
onDemandMonthlyBudget: 1000,
|
|
73
73
|
},
|
|
74
74
|
codex: { enabled: true, defaultAgent: false, path: "/initial/codex", model: "initial-codex-model", alternativeModel: "initial-codex-alt-model", effort: "initial-codex-effort", fastMode: false },
|
|
75
|
-
ccc: { DEEPSEEK_API_KEY: "initial-ccc-key", DEEPSEEK_BASE_URL: "https://initial.deepseek.test/v1", model: "initial-ccc-model" },
|
|
75
|
+
ccc: { enabled: true, defaultAgent: false, DEEPSEEK_API_KEY: "initial-ccc-key", DEEPSEEK_BASE_URL: "https://initial.deepseek.test/v1", model: "initial-ccc-model", alternativeModel: "initial-ccc-alt-model" },
|
|
76
76
|
};
|
|
77
77
|
|
|
78
78
|
// 把 module 状态抢救快照:每个 it 跑前重置回这个状态,避免污染相邻测试。
|
|
@@ -177,15 +177,18 @@ describe("applyLoadedConfig — 刷新 export let 常量", () => {
|
|
|
177
177
|
expect(CURSOR_AGENT_COMMAND).toBe("C:/custom/cursor.exe");
|
|
178
178
|
});
|
|
179
179
|
|
|
180
|
-
it("/model 候选列表包含 Cursor/Codex 的单个备选模型并保持主模型优先", () => {
|
|
180
|
+
it("/model 候选列表包含 Cursor/Codex/CCC 的单个备选模型并保持主模型优先", () => {
|
|
181
181
|
const cfg = structuredClone(baseAppConfig);
|
|
182
182
|
cfg.cursor.model = "cursor-main";
|
|
183
183
|
cfg.cursor.alternativeModel = "cursor-alt";
|
|
184
184
|
cfg.codex.model = "codex-main";
|
|
185
|
-
cfg.codex.alternativeModel = "codex-main";
|
|
185
|
+
cfg.codex.alternativeModel = "codex-main";
|
|
186
|
+
cfg.ccc.model = "ccc-main";
|
|
187
|
+
cfg.ccc.alternativeModel = "ccc-alt";
|
|
186
188
|
|
|
187
189
|
expect(getAllModelsForTool("cursor", cfg)).toEqual(["cursor-main", "cursor-alt"]);
|
|
188
|
-
expect(getAllModelsForTool("codex", cfg)).toEqual(["codex-main"]);
|
|
190
|
+
expect(getAllModelsForTool("codex", cfg)).toEqual(["codex-main"]);
|
|
191
|
+
expect(getAllModelsForTool("ccc", cfg)).toEqual(["ccc-main", "ccc-alt"]);
|
|
189
192
|
});
|
|
190
193
|
|
|
191
194
|
it("不修改 CHATCCC_PORT(端口在 setup 切换时必须保持不变)", () => {
|
|
@@ -262,12 +265,20 @@ describe("resolveDefaultAgentTool", () => {
|
|
|
262
265
|
expect(resolveDefaultAgentTool(cfg)).toBe("cursor");
|
|
263
266
|
});
|
|
264
267
|
|
|
265
|
-
it("defaultAgent 指向未启用 Agent 时回退到第一个已启用 Agent", () => {
|
|
268
|
+
it("defaultAgent 指向未启用 Agent 时回退到第一个已启用 Agent", () => {
|
|
266
269
|
const cfg = structuredClone(baseAppConfig);
|
|
267
270
|
cfg.claude.enabled = false;
|
|
268
271
|
cfg.claude.defaultAgent = true;
|
|
269
272
|
cfg.cursor.defaultAgent = false;
|
|
270
273
|
|
|
271
|
-
expect(resolveDefaultAgentTool(cfg)).toBe("cursor");
|
|
272
|
-
});
|
|
273
|
-
|
|
274
|
+
expect(resolveDefaultAgentTool(cfg)).toBe("cursor");
|
|
275
|
+
});
|
|
276
|
+
|
|
277
|
+
it("支持把已启用的 CCC Agent 设为默认 Agent", () => {
|
|
278
|
+
const cfg = structuredClone(baseAppConfig);
|
|
279
|
+
for (const tool of ["claude", "cursor", "codex"] as const) cfg[tool].defaultAgent = false;
|
|
280
|
+
cfg.ccc.defaultAgent = true;
|
|
281
|
+
|
|
282
|
+
expect(resolveDefaultAgentTool(cfg)).toBe("ccc");
|
|
283
|
+
});
|
|
284
|
+
});
|
|
@@ -33,19 +33,23 @@ describe("config.sample.json", () => {
|
|
|
33
33
|
const sample = JSON.parse(readFileSync(configSamplePath, "utf8")) as {
|
|
34
34
|
cursor?: { alternativeModel?: unknown };
|
|
35
35
|
codex?: { alternativeModel?: unknown; fastMode?: unknown };
|
|
36
|
+
ccc?: { alternativeModel?: unknown };
|
|
36
37
|
};
|
|
37
38
|
|
|
38
39
|
expect(sample.cursor?.alternativeModel).toBe("");
|
|
39
40
|
expect(sample.codex?.alternativeModel).toBe("");
|
|
41
|
+
expect(sample.ccc?.alternativeModel).toBe("");
|
|
40
42
|
expect(sample.codex?.fastMode).toBe(false);
|
|
41
43
|
});
|
|
42
44
|
|
|
43
45
|
it("sets ccc agent DeepSeek defaults in the sample config", () => {
|
|
44
46
|
const configSamplePath = join(process.cwd(), "config.sample.json");
|
|
45
47
|
const sample = JSON.parse(readFileSync(configSamplePath, "utf8")) as {
|
|
46
|
-
ccc?: { DEEPSEEK_API_KEY?: unknown; DEEPSEEK_BASE_URL?: unknown; model?: unknown };
|
|
48
|
+
ccc?: { enabled?: unknown; defaultAgent?: unknown; DEEPSEEK_API_KEY?: unknown; DEEPSEEK_BASE_URL?: unknown; model?: unknown };
|
|
47
49
|
};
|
|
48
50
|
|
|
51
|
+
expect(sample.ccc?.enabled).toBe(false);
|
|
52
|
+
expect(sample.ccc?.defaultAgent).toBe(false);
|
|
49
53
|
expect(sample.ccc?.DEEPSEEK_API_KEY).toBe("");
|
|
50
54
|
expect(sample.ccc?.DEEPSEEK_BASE_URL).toBe("https://api.deepseek.com/v1");
|
|
51
55
|
expect(sample.ccc?.model).toBe("deepseek-v4-pro");
|
|
@@ -89,4 +93,4 @@ describe("config.sample.json", () => {
|
|
|
89
93
|
expect(sample.rawStreamLogs?.[tool]?.keepCompleted).toBe(false);
|
|
90
94
|
}
|
|
91
95
|
});
|
|
92
|
-
});
|
|
96
|
+
});
|
|
@@ -148,6 +148,12 @@ describe("handleCommand WeChat processing ack", () => {
|
|
|
148
148
|
config.cursor.defaultAgent = false;
|
|
149
149
|
config.codex.defaultAgent = false;
|
|
150
150
|
config.codex.fastMode = false;
|
|
151
|
+
config.ccc.enabled = true;
|
|
152
|
+
config.ccc.defaultAgent = false;
|
|
153
|
+
config.ccc.DEEPSEEK_API_KEY = "sk-test-ccc";
|
|
154
|
+
config.ccc.DEEPSEEK_BASE_URL = "https://api.deepseek.com/v1";
|
|
155
|
+
config.ccc.model = "deepseek-v4-pro";
|
|
156
|
+
config.ccc.alternativeModel = "deepseek-v4-flash";
|
|
151
157
|
mockStreamStates.clear();
|
|
152
158
|
mockGetCodexUsageSummary.mockReset();
|
|
153
159
|
mockGetCursorUsageSummary.mockReset();
|
|
@@ -204,7 +210,8 @@ describe("handleCommand WeChat processing ack", () => {
|
|
|
204
210
|
_clearAdapterCacheForTest();
|
|
205
211
|
_resetSessionRegistryFileForTest();
|
|
206
212
|
_resetSessionToolsFileForTest();
|
|
207
|
-
vi.useRealTimers();
|
|
213
|
+
vi.useRealTimers();
|
|
214
|
+
vi.unstubAllGlobals();
|
|
208
215
|
await rm(tempDir, { recursive: true, force: true });
|
|
209
216
|
});
|
|
210
217
|
|
|
@@ -1013,7 +1020,7 @@ describe("handleCommand WeChat processing ack", () => {
|
|
|
1013
1020
|
expect(card.elements[0].text.content).toContain("ChatGPT browser session has no access token.");
|
|
1014
1021
|
});
|
|
1015
1022
|
|
|
1016
|
-
it("handles /usage as Cursor usage in Cursor chats", async () => {
|
|
1023
|
+
it("handles /usage as Cursor usage in Cursor chats", async () => {
|
|
1017
1024
|
const platform = mockPlatform("feishu");
|
|
1018
1025
|
await recordSessionRegistry({
|
|
1019
1026
|
chatId: "cursor-chat",
|
|
@@ -1113,6 +1120,64 @@ describe("handleCommand WeChat processing ack", () => {
|
|
|
1113
1120
|
expect(claudeReadyCall?.[2]).not.toContain("/usage");
|
|
1114
1121
|
});
|
|
1115
1122
|
|
|
1123
|
+
it("handles /usage as DeepSeek balance in CCC Agent chats", async () => {
|
|
1124
|
+
const platform = mockPlatform("feishu");
|
|
1125
|
+
const fetchMock = vi.fn(async () => new Response(JSON.stringify({
|
|
1126
|
+
is_available: true,
|
|
1127
|
+
balance_infos: [{
|
|
1128
|
+
currency: "CNY",
|
|
1129
|
+
total_balance: "12.34",
|
|
1130
|
+
topped_up_balance: "10.00",
|
|
1131
|
+
granted_balance: "2.34",
|
|
1132
|
+
}],
|
|
1133
|
+
}), { status: 200 }));
|
|
1134
|
+
vi.stubGlobal("fetch", fetchMock);
|
|
1135
|
+
await recordSessionRegistry({
|
|
1136
|
+
chatId: "ccc-chat",
|
|
1137
|
+
sessionId: "sid-ccc",
|
|
1138
|
+
tool: "ccc",
|
|
1139
|
+
chatName: "ccc-session",
|
|
1140
|
+
running: false,
|
|
1141
|
+
});
|
|
1142
|
+
|
|
1143
|
+
await handleCommand(platform, "/usage", "ccc-chat", "ou-user", Date.now(), "group");
|
|
1144
|
+
|
|
1145
|
+
expect(fetchMock).toHaveBeenCalledWith(
|
|
1146
|
+
"https://api.deepseek.com/user/balance",
|
|
1147
|
+
expect.objectContaining({ headers: { Authorization: "Bearer sk-test-ccc" } }),
|
|
1148
|
+
);
|
|
1149
|
+
expect(platform.sendCard).toHaveBeenCalledWith(
|
|
1150
|
+
"ccc-chat",
|
|
1151
|
+
"CCC Usage",
|
|
1152
|
+
expect.stringContaining("总余额: 12.34"),
|
|
1153
|
+
"blue",
|
|
1154
|
+
);
|
|
1155
|
+
});
|
|
1156
|
+
|
|
1157
|
+
it("does not query balance for a non-official CCC API endpoint", async () => {
|
|
1158
|
+
const platform = mockPlatform("feishu");
|
|
1159
|
+
const fetchMock = vi.fn();
|
|
1160
|
+
vi.stubGlobal("fetch", fetchMock);
|
|
1161
|
+
config.ccc.DEEPSEEK_BASE_URL = "https://deepseek-proxy.example.com/v1";
|
|
1162
|
+
await recordSessionRegistry({
|
|
1163
|
+
chatId: "ccc-proxy-chat",
|
|
1164
|
+
sessionId: "sid-ccc-proxy",
|
|
1165
|
+
tool: "ccc",
|
|
1166
|
+
chatName: "ccc-proxy-session",
|
|
1167
|
+
running: false,
|
|
1168
|
+
});
|
|
1169
|
+
|
|
1170
|
+
await handleCommand(platform, "/usage", "ccc-proxy-chat", "ou-user", Date.now(), "group");
|
|
1171
|
+
|
|
1172
|
+
expect(fetchMock).not.toHaveBeenCalled();
|
|
1173
|
+
expect(platform.sendCard).toHaveBeenCalledWith(
|
|
1174
|
+
"ccc-proxy-chat",
|
|
1175
|
+
"CCC Usage",
|
|
1176
|
+
expect.stringContaining("仅支持官方 DeepSeek API"),
|
|
1177
|
+
"blue",
|
|
1178
|
+
);
|
|
1179
|
+
});
|
|
1180
|
+
|
|
1116
1181
|
it("reloads runtime config for the exact /reload command", async () => {
|
|
1117
1182
|
const platform = mockPlatform("feishu");
|
|
1118
1183
|
|
|
@@ -1129,7 +1194,7 @@ describe("handleCommand WeChat processing ack", () => {
|
|
|
1129
1194
|
);
|
|
1130
1195
|
});
|
|
1131
1196
|
|
|
1132
|
-
it("allows the
|
|
1197
|
+
it("allows and advertises the /new ccc entry", async () => {
|
|
1133
1198
|
const platform = mockPlatform("feishu");
|
|
1134
1199
|
_setAdapterForToolForTest("ccc", mockAdapter("session-20260702-121530-a1b2c3"));
|
|
1135
1200
|
|
|
@@ -1150,7 +1215,21 @@ describe("handleCommand WeChat processing ack", () => {
|
|
|
1150
1215
|
const helpPlatform = mockPlatform("feishu");
|
|
1151
1216
|
await handleCommand(helpPlatform, "hello", "feishu-group-help", "ou-user", Date.now(), "group");
|
|
1152
1217
|
const helpCard = vi.mocked(helpPlatform.sendRawCard).mock.calls.at(-1)?.[1] as string;
|
|
1153
|
-
expect(helpCard).
|
|
1154
|
-
expect(helpCard).
|
|
1218
|
+
expect(helpCard).toContain("/new ccc");
|
|
1219
|
+
expect(helpCard).toContain("CCC Agent");
|
|
1220
|
+
});
|
|
1221
|
+
|
|
1222
|
+
it("shows the CCC model list when CCC Agent is the unbound default", async () => {
|
|
1223
|
+
const platform = mockPlatform("feishu");
|
|
1224
|
+
config.claude.defaultAgent = false;
|
|
1225
|
+
config.cursor.defaultAgent = false;
|
|
1226
|
+
config.codex.defaultAgent = false;
|
|
1227
|
+
config.ccc.defaultAgent = true;
|
|
1228
|
+
|
|
1229
|
+
await handleCommand(platform, "/model", "feishu-p2p-ccc-model", "ou-user", Date.now(), "p2p");
|
|
1230
|
+
|
|
1231
|
+
const card = vi.mocked(platform.sendRawCard).mock.calls.at(-1)?.[1] as string;
|
|
1232
|
+
expect(card).toContain("deepseek-v4-pro");
|
|
1233
|
+
expect(card).toContain("deepseek-v4-flash");
|
|
1155
1234
|
});
|
|
1156
1235
|
});
|
|
@@ -80,4 +80,14 @@ describe("SimulatedPlatform", () => {
|
|
|
80
80
|
// 近期消息不触发
|
|
81
81
|
expect(SimulatedPlatform.formatDelayNotice(Date.now(), "test")).toBeNull();
|
|
82
82
|
});
|
|
83
|
+
|
|
84
|
+
it("formatDelayNotice 边界:恰好 15 分钟触发,未来时间戳不触发", () => {
|
|
85
|
+
const now = Date.now();
|
|
86
|
+
// 恰好等于阈值(15 分钟)应触发提醒
|
|
87
|
+
expect(SimulatedPlatform.formatDelayNotice(now - 15 * 60 * 1000, "边界", now)).not.toBeNull();
|
|
88
|
+
// 14 分 59 秒不触发
|
|
89
|
+
expect(SimulatedPlatform.formatDelayNotice(now - (15 * 60 * 1000 - 1000), "边界", now)).toBeNull();
|
|
90
|
+
// 未来时间戳(时钟偏移)不触发
|
|
91
|
+
expect(SimulatedPlatform.formatDelayNotice(now + 60 * 1000, "边界", now)).toBeNull();
|
|
92
|
+
});
|
|
83
93
|
});
|
|
@@ -86,6 +86,28 @@ describe("unflattenConfig", () => {
|
|
|
86
86
|
});
|
|
87
87
|
});
|
|
88
88
|
|
|
89
|
+
it("maps CCC Agent settings into ccc config", () => {
|
|
90
|
+
expect(
|
|
91
|
+
unflattenConfig({
|
|
92
|
+
CHATCCC_CCC_ENABLED: true,
|
|
93
|
+
CHATCCC_CCC_DEFAULT_AGENT: true,
|
|
94
|
+
CHATCCC_CCC_API_KEY: "sk-test-ccc",
|
|
95
|
+
CHATCCC_CCC_BASE_URL: "https://api.deepseek.com/v1",
|
|
96
|
+
CHATCCC_CCC_MODEL: "deepseek-v4-flash",
|
|
97
|
+
CHATCCC_CCC_ALTERNATIVE_MODEL: "deepseek-v4-pro",
|
|
98
|
+
}),
|
|
99
|
+
).toEqual({
|
|
100
|
+
ccc: {
|
|
101
|
+
enabled: true,
|
|
102
|
+
defaultAgent: true,
|
|
103
|
+
DEEPSEEK_API_KEY: "sk-test-ccc",
|
|
104
|
+
DEEPSEEK_BASE_URL: "https://api.deepseek.com/v1",
|
|
105
|
+
model: "deepseek-v4-flash",
|
|
106
|
+
alternativeModel: "deepseek-v4-pro",
|
|
107
|
+
},
|
|
108
|
+
});
|
|
109
|
+
});
|
|
110
|
+
|
|
89
111
|
it("maps Chrome CDP guard fields into chromeDevtools config", () => {
|
|
90
112
|
expect(
|
|
91
113
|
unflattenConfig({
|
|
@@ -153,6 +175,14 @@ describe("getRestartRequiredReasons", () => {
|
|
|
153
175
|
effort: "",
|
|
154
176
|
fastMode: false,
|
|
155
177
|
},
|
|
178
|
+
ccc: {
|
|
179
|
+
enabled: true,
|
|
180
|
+
defaultAgent: false,
|
|
181
|
+
DEEPSEEK_API_KEY: "sk-test-ccc",
|
|
182
|
+
DEEPSEEK_BASE_URL: "https://api.deepseek.com/v1",
|
|
183
|
+
model: "deepseek-v4-pro",
|
|
184
|
+
alternativeModel: "",
|
|
185
|
+
},
|
|
156
186
|
};
|
|
157
187
|
|
|
158
188
|
it("does not require restart for agent and Chrome runtime settings", () => {
|
|
@@ -215,6 +245,16 @@ describe("dashboard edit modal", () => {
|
|
|
215
245
|
expect(PAGE_HTML).toContain("备选模型");
|
|
216
246
|
});
|
|
217
247
|
|
|
248
|
+
it("shows CCC Agent in the wizard and dashboard with model switching fields", () => {
|
|
249
|
+
expect(PAGE_HTML).toContain('id="agent-card-ccc"');
|
|
250
|
+
expect(PAGE_HTML).toContain('id="agent-enable-ccc"');
|
|
251
|
+
expect(PAGE_HTML).toContain('id="agent-default-ccc"');
|
|
252
|
+
expect(PAGE_HTML).toContain('id="field-CHATCCC_CCC_MODEL"');
|
|
253
|
+
expect(PAGE_HTML).toContain('id="field-CHATCCC_CCC_ALTERNATIVE_MODEL"');
|
|
254
|
+
expect(PAGE_HTML).toContain('id="dash-ccc"');
|
|
255
|
+
expect(PAGE_HTML).toContain("editSection('ccc')");
|
|
256
|
+
});
|
|
257
|
+
|
|
218
258
|
it("shows config effect scope hints", () => {
|
|
219
259
|
expect(PAGE_HTML).toContain("生效范围:保存后下一条消息或下个新会话生效");
|
|
220
260
|
expect(PAGE_HTML).toContain("生效范围:飞书开关、App ID、App Secret 或平台类型变更需要重启 ChatCCC");
|
package/src/cards.ts
CHANGED
|
@@ -133,11 +133,12 @@ export function buildHelpCard(
|
|
|
133
133
|
`发送 **/new** 创建新会话(使用 /cd 设置的目录,默认 ${defaultToolLabel})`,
|
|
134
134
|
"发送 **/new claude** 创建新 Claude 会话",
|
|
135
135
|
"发送 **/new cursor** 创建新 Cursor 会话",
|
|
136
|
-
"发送 **/new codex** 创建新 Codex 会话",
|
|
136
|
+
"发送 **/new codex** 创建新 Codex 会话",
|
|
137
|
+
"发送 **/new ccc** 创建新 CCC Agent 会话",
|
|
137
138
|
"发送 **/newh** 重置当前会话(沿用当前工作目录,不切换)",
|
|
138
139
|
"发送 **/plan** 以规划模式提问(只读,不执行写操作)",
|
|
139
140
|
"发送 **/ask** 以问答模式提问(只读,不执行写操作)",
|
|
140
|
-
"发送 **/usage**
|
|
141
|
+
"发送 **/usage** 查看当前 Agent 的用量或余额",
|
|
141
142
|
"发送 **/restart** 重启 ChatCCC 进程",
|
|
142
143
|
"发送 **/update** 更新并重启(仅 npm 全局安装可用)",
|
|
143
144
|
ABD_HELP_LINE,
|
|
@@ -152,7 +153,8 @@ export function buildHelpCard(
|
|
|
152
153
|
{ text: `新建默认会话(/new,${defaultToolLabel})`, value: JSON.stringify({ cmd: "new" }), type: "primary" },
|
|
153
154
|
{ text: "新建 Claude 会话(/new claude)", value: JSON.stringify({ cmd: "new claude" }), type: "primary" },
|
|
154
155
|
{ text: "新建 Cursor 会话(/new cursor)", value: JSON.stringify({ cmd: "new cursor" }), type: "primary" },
|
|
155
|
-
{ text: "新建 Codex 会话(/new codex)", value: JSON.stringify({ cmd: "new codex" }), type: "primary" },
|
|
156
|
+
{ text: "新建 Codex 会话(/new codex)", value: JSON.stringify({ cmd: "new codex" }), type: "primary" },
|
|
157
|
+
{ text: "新建 CCC Agent 会话(/new ccc)", value: JSON.stringify({ cmd: "new ccc" }), type: "primary" },
|
|
156
158
|
{ text: "重启 ChatCCC(/restart)", value: JSON.stringify({ cmd: "restart" }), type: "danger" },
|
|
157
159
|
{ text: "更新并重启(/update)", value: JSON.stringify({ cmd: "update" }), type: "danger" },
|
|
158
160
|
{ text: "切换工作路径(/cd)", value: JSON.stringify({ cmd: "cd" }), type: "default" },
|
|
@@ -344,8 +346,8 @@ export function buildSessionsCard(sessions: Array<{
|
|
|
344
346
|
header: { template: "blue", title: { content: "所有会话", tag: "plain_text" } },
|
|
345
347
|
elements: [
|
|
346
348
|
{ tag: "div", text: { tag: "lark_md", content: fixedPrivateSession
|
|
347
|
-
? `当前没有会话记录。\n\n直接发送普通消息即可创建飞书私聊专属 ${defaultToolLabel} 会话;默认 Agent 变化后,下一条普通消息会创建对应 Agent 的新空会话。发送 **/new**、**/new claude**、**/new cursor** 或 **/new
|
|
348
|
-
: `当前没有会话记录。\n\n使用 **/new**(默认 ${defaultToolLabel})、**/new claude**、**/new cursor** 或 **/new
|
|
349
|
+
? `当前没有会话记录。\n\n直接发送普通消息即可创建飞书私聊专属 ${defaultToolLabel} 会话;默认 Agent 变化后,下一条普通消息会创建对应 Agent 的新空会话。发送 **/new**、**/new claude**、**/new cursor**、**/new codex** 或 **/new ccc** 会另外创建会话群。`
|
|
350
|
+
: `当前没有会话记录。\n\n使用 **/new**(默认 ${defaultToolLabel})、**/new claude**、**/new cursor**、**/new codex** 或 **/new ccc** 创建新会话。\n创建后可在任意会话群内发送 **/sessions** 查看列表,用 **/session 数字** 切换会话。` } },
|
|
349
351
|
{ tag: "hr" },
|
|
350
352
|
{ tag: "action", actions: [{ tag: "button", text: { tag: "plain_text", content: "收起" }, type: "default", value: { action: "close" } }] },
|
|
351
353
|
],
|