chatccc 0.2.18 → 0.2.20

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
@@ -12,7 +12,7 @@ ChatCCC 把 Claude Code、Cursor Agent、Codex (OpenAI) 接入了飞书群聊:
12
12
 
13
13
  - **手机上也能用 Claude Code / Cursor / Codex** —— 在飞书群里发消息就等于在终端输入指令,AI 的思考和回复会流式展示在群卡片里
14
14
  - **多会话并行** —— 一个群就是一个 AI 会话,完全隔离、互不干扰,并行工作效率更高
15
- - **多工具切换** —— `/new claude` 创建 Claude Code 会话,`/new cursor` 创建 Cursor 会话,`/new codex` 创建 Codex 会话,各取所长
15
+ - **多工具切换** —— `/new` 使用默认 Agent 创建会话,也可用 `/new claude`、`/new cursor`、`/new codex` 指定工具,各取所长
16
16
 
17
17
  一句话:**在任何设备上打开飞书,就能让 Claude Code / Cursor / Codex 帮你写代码、排查问题、分析项目。**
18
18
 
@@ -118,7 +118,7 @@ ChatCCC 启动时会按以下顺序确定 Cursor Agent CLI:
118
118
 
119
119
  > 旧版字段 `cursor.command` 仍可读取(启动时会打印一次 warning 提示改名),新配置请统一使用 `cursor.path`。
120
120
 
121
- > **说明**:只使用 Claude Code(`/new claude` `/new`)的用户无需安装 Cursor CLI。
121
+ > **说明**:只使用 Claude Code(`/new claude`,或把 Claude Code 设为默认 Agent 后使用 `/new`)的用户无需安装 Cursor CLI。
122
122
 
123
123
  #### Codex CLI(使用 Codex 会话时需要)
124
124
 
@@ -215,21 +215,24 @@ ChatCCC 的所有运行参数都集中在包根目录的 `config.json`。
215
215
  },
216
216
  "port": 18080,
217
217
  "gitTimeoutSeconds": 180,
218
- "claude": {
219
- "enabled": false,
220
- "model": "",
218
+ "claude": {
219
+ "enabled": false,
220
+ "defaultAgent": true,
221
+ "model": "",
221
222
  "effort": "",
222
223
  "apiKey": "",
223
224
  "baseUrl": ""
224
225
  },
225
- "cursor": {
226
- "enabled": false,
227
- "path": "",
226
+ "cursor": {
227
+ "enabled": false,
228
+ "defaultAgent": false,
229
+ "path": "",
228
230
  "model": ""
229
231
  },
230
- "codex": {
231
- "enabled": false,
232
- "path": "",
232
+ "codex": {
233
+ "enabled": false,
234
+ "defaultAgent": false,
235
+ "path": "",
233
236
  "model": "",
234
237
  "effort": ""
235
238
  }
@@ -242,9 +245,10 @@ ChatCCC 的所有运行参数都集中在包根目录的 `config.json`。
242
245
  | --- | --- | --- |
243
246
  | `feishu.appId` / `feishu.appSecret` | 是 | 飞书应用「凭证与基础信息」中的 App ID / App Secret |
244
247
  | `port` | 否 | 本地 WebSocket 中继 + Web 向导监听端口,默认 `18080`;同机多实例时改成不同值即可 |
245
- | `gitTimeoutSeconds` | 否 | `/git` 命令在会话工作目录执行时的单次超时秒数,默认 `180`,允许范围 `1–3600`,超时会被 `SIGKILL` 强制终止 |
246
- | `claude.enabled` / `cursor.enabled` / `codex.enabled` | 否 | 是否启用对应 AI Agent;Web 向导/管理页只展示 `enabled: true` 的 agent 卡片。字段缺省时按「任一配置字段非空」自动判定(向后兼容) |
247
- | `claude.model` | 否 | Claude Code 会话使用的模型;留空(`""` / 全空白)→ 不向 SDK `model`,由 SDK / 服务商默认决定 |
248
+ | `gitTimeoutSeconds` | 否 | `/git` 命令在会话工作目录执行时的单次超时秒数,默认 `180`,允许范围 `1–3600`,超时会被 `SIGKILL` 强制终止 |
249
+ | `claude.enabled` / `cursor.enabled` / `codex.enabled` | 否 | 是否启用对应 AI Agent;Web 向导/管理页只展示 `enabled: true` 的 agent 卡片。字段缺省时按「任一配置字段非空」自动判定(向后兼容) |
250
+ | `claude.defaultAgent` / `cursor.defaultAgent` / `codex.defaultAgent` | 否 | `/new` 未指定具体 Agent 时使用哪个默认 Agent;同一时间应只有一个为 `true`。Web 配置页切换某个 Agent 为默认时会自动关闭其它 Agent 的默认开关 |
251
+ | `claude.model` | 否 | Claude Code 会话使用的模型;留空(`""` / 全空白)→ 不向 SDK 传 `model`,由 SDK / 服务商默认决定 |
248
252
  | `claude.effort` | 否 | Claude 思考深度(如 `low` / `medium` / `high` / `max`);留空 → 不向 SDK 传 `effort` |
249
253
  | `claude.apiKey` | 否 | 第三方 Anthropic 兼容网关的 API 密钥;**官方 Claude 用户保持 `""` 即可**,详见下文「第三方 API」一节 |
250
254
  | `claude.baseUrl` | 否 | 第三方 Anthropic 兼容网关的 base URL(例如 `https://api.deepseek.com/anthropic`);**官方 Claude 用户保持 `""` 即可** |
@@ -289,14 +293,14 @@ ChatCCC 的所有运行参数都集中在包根目录的 `config.json`。
289
293
 
290
294
  ### 5. 开始使用
291
295
 
292
- 在飞书中找到你的机器人,发送 `/new`(默认 Claude Code)或 `/new claude` / `/new cursor` / `/new codex`,机器人会自动创建一个群聊并把 AI 会话绑定到该群。之后直接在群里发消息就能对话。
296
+ 在飞书中找到你的机器人,发送 `/new`(使用 `config.json` 中 `defaultAgent: true` 的默认 Agent)或 `/new claude` / `/new cursor` / `/new codex`,机器人会自动创建一个群聊并把 AI 会话绑定到该群。之后直接在群里发消息就能对话。
293
297
 
294
298
  ### 可用指令
295
299
 
296
300
 
297
301
  | 指令 | 作用 |
298
302
  | --------------- | ---------------------------- |
299
- | `/new` | 创建新的 Claude Code 会话(默认) |
303
+ | `/new` | 使用默认 Agent 创建新会话 |
300
304
  | `/new claude` | 创建新的 Claude Code 会话 |
301
305
  | `/new cursor` | 创建新的 Cursor 会话 |
302
306
  | `/new codex` | 创建新的 Codex 会话(OpenAI) |
@@ -313,4 +317,4 @@ ChatCCC 的所有运行参数都集中在包根目录的 `config.json`。
313
317
 
314
318
  ## 技术栈
315
319
 
316
- TypeScript / Node.js >= 20 / tsx / Anthropic Claude Agent SDK / Cursor Agent CLI / Codex CLI (OpenAI) / 飞书 WebSocket API / CardKit
320
+ TypeScript / Node.js >= 20 / tsx / Anthropic Claude Agent SDK / Cursor Agent CLI / Codex CLI (OpenAI) / 飞书 WebSocket API / CardKit
package/bin/chatccc.mjs CHANGED
@@ -1,24 +1,24 @@
1
- #!/usr/bin/env node
2
- import { spawnSync } from "node:child_process";
3
- import { createRequire } from "node:module";
4
- import { dirname, join } from "node:path";
5
-
6
- const require = createRequire(import.meta.url);
7
- // 相对 bin/chatccc.mjs 解析 package.json,包根目录不依赖 cwd,也不会多退一级到 node_modules
8
- const pkgRoot = dirname(require.resolve("../package.json"));
9
- const indexTs = join(pkgRoot, "src", "index.ts");
10
- const tsxCli = require.resolve("tsx/cli");
11
-
12
- const result = spawnSync(process.execPath, [tsxCli, indexTs, ...process.argv.slice(2)], {
13
- stdio: "inherit",
14
- cwd: process.cwd(),
15
- env: process.env,
16
- });
17
-
18
- const code = result.status === null ? 1 : result.status;
19
- if (code !== 0) {
20
- console.error("[ 未启动 ]");
21
- console.error("chatccc 子进程已结束,服务没有在后台运行。");
22
- }
23
-
1
+ #!/usr/bin/env node
2
+ import { spawnSync } from "node:child_process";
3
+ import { createRequire } from "node:module";
4
+ import { dirname, join } from "node:path";
5
+
6
+ const require = createRequire(import.meta.url);
7
+ // 相对 bin/chatccc.mjs 解析 package.json,包根目录不依赖 cwd,也不会多退一级到 node_modules
8
+ const pkgRoot = dirname(require.resolve("../package.json"));
9
+ const indexTs = join(pkgRoot, "src", "index.ts");
10
+ const tsxCli = require.resolve("tsx/cli");
11
+
12
+ const result = spawnSync(process.execPath, [tsxCli, indexTs, ...process.argv.slice(2)], {
13
+ stdio: "inherit",
14
+ cwd: process.cwd(),
15
+ env: process.env,
16
+ });
17
+
18
+ const code = result.status === null ? 1 : result.status;
19
+ if (code !== 0) {
20
+ console.error("[ 未启动 ]");
21
+ console.error("chatccc 子进程已结束,服务没有在后台运行。");
22
+ }
23
+
24
24
  process.exit(code);
@@ -1,26 +1,29 @@
1
- {
2
- "feishu": {
3
- "appId": "",
4
- "appSecret": ""
5
- },
6
- "port": 18080,
7
- "gitTimeoutSeconds": 180,
8
- "claude": {
9
- "enabled": false,
10
- "model": "",
11
- "effort": "",
12
- "apiKey": "",
13
- "baseUrl": ""
14
- },
15
- "cursor": {
16
- "enabled": false,
17
- "path": "",
18
- "model": ""
19
- },
20
- "codex": {
21
- "enabled": false,
22
- "path": "",
23
- "model": "",
24
- "effort": ""
25
- }
26
- }
1
+ {
2
+ "feishu": {
3
+ "appId": "",
4
+ "appSecret": ""
5
+ },
6
+ "port": 18080,
7
+ "gitTimeoutSeconds": 180,
8
+ "claude": {
9
+ "enabled": false,
10
+ "defaultAgent": true,
11
+ "model": "",
12
+ "effort": "",
13
+ "apiKey": "",
14
+ "baseUrl": ""
15
+ },
16
+ "cursor": {
17
+ "enabled": false,
18
+ "defaultAgent": false,
19
+ "path": "",
20
+ "model": ""
21
+ },
22
+ "codex": {
23
+ "enabled": false,
24
+ "defaultAgent": false,
25
+ "path": "",
26
+ "model": "",
27
+ "effort": ""
28
+ }
29
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "chatccc",
3
- "version": "0.2.18",
3
+ "version": "0.2.20",
4
4
  "description": "Feishu bot bridge for Claude Code",
5
5
  "license": "Apache-2.0",
6
6
  "type": "module",
@@ -16,10 +16,11 @@ import {
16
16
  CURSOR_AGENT_COMMAND,
17
17
  GIT_TIMEOUT_MS,
18
18
  GIT_TIMEOUT_SECONDS,
19
- applyLoadedConfig,
20
- config,
21
- type AppConfig,
22
- } from "../config.ts";
19
+ applyLoadedConfig,
20
+ config,
21
+ resolveDefaultAgentTool,
22
+ type AppConfig,
23
+ } from "../config.ts";
23
24
 
24
25
  // ---------------------------------------------------------------------------
25
26
  // applyLoadedConfig — setup → service「在线切换」时刷新进程内 config 的核心机制
@@ -36,15 +37,16 @@ const baseAppConfig: AppConfig = {
36
37
  feishu: { appId: "INITIAL_APP", appSecret: "INITIAL_SECRET" },
37
38
  port: 18080,
38
39
  gitTimeoutSeconds: 180,
39
- claude: {
40
- enabled: true,
41
- model: "initial-model",
40
+ claude: {
41
+ enabled: true,
42
+ defaultAgent: true,
43
+ model: "initial-model",
42
44
  effort: "initial-effort",
43
45
  apiKey: "sk-initial",
44
46
  baseUrl: "https://initial.gw/anthropic",
45
47
  },
46
- cursor: { enabled: true, path: "/initial/cursor", model: "initial-cursor-model" },
47
- codex: { enabled: true, path: "/initial/codex", model: "initial-codex-model", effort: "initial-codex-effort" },
48
+ cursor: { enabled: true, defaultAgent: false, path: "/initial/cursor", model: "initial-cursor-model" },
49
+ codex: { enabled: true, defaultAgent: false, path: "/initial/codex", model: "initial-codex-model", effort: "initial-codex-effort" },
48
50
  };
49
51
 
50
52
  // 把 module 状态抢救快照:每个 it 跑前重置回这个状态,避免污染相邻测试。
@@ -75,9 +77,10 @@ describe("applyLoadedConfig — 刷新 export let 常量", () => {
75
77
  it("更新 Claude 配置(model / effort / apiKey / baseUrl)", () => {
76
78
  applyLoadedConfig({
77
79
  ...structuredClone(baseAppConfig),
78
- claude: {
79
- enabled: true,
80
- model: "deepseek-v4-pro",
80
+ claude: {
81
+ enabled: true,
82
+ defaultAgent: true,
83
+ model: "deepseek-v4-pro",
81
84
  effort: "high",
82
85
  apiKey: "sk-newkey",
83
86
  baseUrl: "https://gw2.example/anthropic",
@@ -104,7 +107,7 @@ describe("applyLoadedConfig — 刷新 export let 常量", () => {
104
107
  it("CURSOR_AGENT_ARGS 跟随 cursor.model 重新解析", () => {
105
108
  applyLoadedConfig({
106
109
  ...structuredClone(baseAppConfig),
107
- cursor: { enabled: true, path: "/x/cursor", model: "claude-3.7-sonnet" },
110
+ cursor: { enabled: true, defaultAgent: false, path: "/x/cursor", model: "claude-3.7-sonnet" },
108
111
  });
109
112
 
110
113
  // CURSOR_AGENT_ARGS 是 ['-p', '--force', ..., '--model', 'claude-3.7-sonnet']
@@ -115,7 +118,7 @@ describe("applyLoadedConfig — 刷新 export let 常量", () => {
115
118
  it("cursor.model 留空时 CURSOR_AGENT_ARGS 不含 --model", () => {
116
119
  applyLoadedConfig({
117
120
  ...structuredClone(baseAppConfig),
118
- cursor: { enabled: true, path: "/x/cursor", model: "" },
121
+ cursor: { enabled: true, defaultAgent: false, path: "/x/cursor", model: "" },
119
122
  });
120
123
 
121
124
  expect(CURSOR_AGENT_ARGS).not.toContain("--model");
@@ -124,7 +127,7 @@ describe("applyLoadedConfig — 刷新 export let 常量", () => {
124
127
  it("CURSOR_AGENT_COMMAND 优先取 config.cursor.path", () => {
125
128
  applyLoadedConfig({
126
129
  ...structuredClone(baseAppConfig),
127
- cursor: { enabled: true, path: "C:/custom/cursor.exe", model: "" },
130
+ cursor: { enabled: true, defaultAgent: false, path: "C:/custom/cursor.exe", model: "" },
128
131
  });
129
132
 
130
133
  expect(CURSOR_AGENT_COMMAND).toBe("C:/custom/cursor.exe");
@@ -142,14 +145,14 @@ describe("applyLoadedConfig — 刷新 export let 常量", () => {
142
145
  });
143
146
  });
144
147
 
145
- describe("applyLoadedConfig — config 对象引用契约", () => {
148
+ describe("applyLoadedConfig — config 对象引用契约", () => {
146
149
  it("config 引用保持不变(就地更新),但字段被刷新", () => {
147
150
  const refBefore = config;
148
151
 
149
152
  applyLoadedConfig({
150
153
  ...structuredClone(baseAppConfig),
151
154
  feishu: { appId: "REF_TEST_APP", appSecret: "REF_TEST_SECRET" },
152
- codex: { enabled: true, path: "/refresh/codex", model: "fresh-model", effort: "low" },
155
+ codex: { enabled: true, defaultAgent: false, path: "/refresh/codex", model: "fresh-model", effort: "low" },
153
156
  });
154
157
 
155
158
  // 必须是同一个引用:codex-adapter 等下游模块"直接 import config",
@@ -167,5 +170,24 @@ describe("applyLoadedConfig — config 对象引用契约", () => {
167
170
  expect(APP_ID).toBe("");
168
171
  expect(APP_SECRET).toBe("");
169
172
  expect(config.feishu.appId).toBe("");
170
- });
171
- });
173
+ });
174
+ });
175
+
176
+ describe("resolveDefaultAgentTool", () => {
177
+ it("优先使用显式 defaultAgent 且已启用的 Agent", () => {
178
+ const cfg = structuredClone(baseAppConfig);
179
+ cfg.claude.defaultAgent = false;
180
+ cfg.cursor.defaultAgent = true;
181
+
182
+ expect(resolveDefaultAgentTool(cfg)).toBe("cursor");
183
+ });
184
+
185
+ it("defaultAgent 指向未启用 Agent 时回退到第一个已启用 Agent", () => {
186
+ const cfg = structuredClone(baseAppConfig);
187
+ cfg.claude.enabled = false;
188
+ cfg.claude.defaultAgent = true;
189
+ cfg.cursor.defaultAgent = false;
190
+
191
+ expect(resolveDefaultAgentTool(cfg)).toBe("cursor");
192
+ });
193
+ });