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 +21 -17
- package/bin/chatccc.mjs +23 -23
- package/config.sample.json +29 -26
- package/images/avatars/badges/badge_claude.png +0 -0
- package/images/avatars/badges/badge_codex.png +0 -0
- package/images/avatars/badges/badge_cursor.png +0 -0
- package/images/avatars/brand-sources/claude_code_app_icon.png +0 -0
- package/images/avatars/brand-sources/codex_app_icon.png +0 -0
- package/images/avatars/brand-sources/cursor_icon_512.png +0 -0
- package/package.json +1 -1
- package/src/__tests__/config-reload.test.ts +41 -19
- package/src/adapters/codex-adapter.ts +294 -294
- package/src/cards.ts +23 -18
- package/src/config.ts +651 -616
- package/src/feishu-api.ts +115 -16
- package/src/index.ts +18 -11
- package/src/session.ts +3 -3
- package/src/web-ui.ts +1708 -1591
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
|
|
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
|
|
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
|
-
"
|
|
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
|
-
"
|
|
226
|
+
"cursor": {
|
|
227
|
+
"enabled": false,
|
|
228
|
+
"defaultAgent": false,
|
|
229
|
+
"path": "",
|
|
228
230
|
"model": ""
|
|
229
231
|
},
|
|
230
|
-
"codex": {
|
|
231
|
-
"enabled": false,
|
|
232
|
-
"
|
|
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.
|
|
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
|
|
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` |
|
|
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);
|
package/config.sample.json
CHANGED
|
@@ -1,26 +1,29 @@
|
|
|
1
|
-
{
|
|
2
|
-
"feishu": {
|
|
3
|
-
"appId": "",
|
|
4
|
-
"appSecret": ""
|
|
5
|
-
},
|
|
6
|
-
"port": 18080,
|
|
7
|
-
"gitTimeoutSeconds": 180,
|
|
8
|
-
"claude": {
|
|
9
|
-
"enabled": false,
|
|
10
|
-
"
|
|
11
|
-
"
|
|
12
|
-
"
|
|
13
|
-
"
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
"
|
|
18
|
-
"
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
"
|
|
24
|
-
"
|
|
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
|
+
}
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
package/package.json
CHANGED
|
@@ -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
|
-
|
|
22
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
+
});
|