chatccc 0.2.50 → 0.2.52
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 +51 -4
- package/config.sample.json +34 -30
- package/package.json +1 -1
- package/src/__tests__/card-plain-text.test.ts +42 -0
- package/src/__tests__/claude-adapter.test.ts +54 -1
- package/src/__tests__/config-reload.test.ts +64 -47
- package/src/__tests__/config-sample.test.ts +19 -0
- package/src/__tests__/session.test.ts +346 -1
- package/src/__tests__/stream-state.test.ts +134 -0
- package/src/__tests__/wechat-platform.test.ts +32 -0
- package/src/adapters/claude-adapter.ts +23 -2
- package/src/card-plain-text.ts +108 -0
- package/src/cards.ts +4 -4
- package/src/config.ts +775 -742
- package/src/feishu-api.ts +6 -0
- package/src/index.ts +172 -781
- package/src/orchestrator.ts +1032 -0
- package/src/platform-adapter.ts +61 -0
- package/src/session-chat-binding.ts +2 -0
- package/src/session.ts +431 -114
- package/src/sim-agent.ts +42 -31
- package/src/stream-state.ts +140 -93
- package/src/web-ui.ts +1891 -1718
- package/src/wechat-platform.ts +517 -0
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# ChatCCC
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
**飞书(推荐,体验丝滑)/ 微信 聊天控制 Claude Code / Cursor / Codex**
|
|
4
4
|
|
|
5
5
|
---
|
|
6
6
|
|
|
@@ -8,9 +8,10 @@
|
|
|
8
8
|
|
|
9
9
|
传统的 Claude Code(尤其是使用第三方 API 的,如 DeepSeek),需要坐在电脑桌前才能用。**离开电脑就没法用了。**
|
|
10
10
|
|
|
11
|
-
ChatCCC 把 Claude Code、Cursor Agent、Codex (OpenAI)
|
|
11
|
+
ChatCCC 把 Claude Code、Cursor Agent、Codex (OpenAI) 接入了飞书群聊和微信私聊:
|
|
12
12
|
|
|
13
|
-
- **手机上也能用 Claude Code / Cursor / Codex** ——
|
|
13
|
+
- **手机上也能用 Claude Code / Cursor / Codex** —— 在飞书群或微信里发消息就等于在终端输入指令,AI 的思考和回复会流式展示在群卡片里(微信为文本模式)
|
|
14
|
+
- **双平台支持** —— 飞书(群聊 + 卡片)和微信 iLink(私聊 + 文本),一套代码同时服务两个平台
|
|
14
15
|
- **多会话并行** —— 一个群就是一个 AI 会话,完全隔离、互不干扰,并行工作效率更高
|
|
15
16
|
- **多工具切换** —— `/new` 使用默认 Agent 创建会话,也可用 `/new claude`、`/new cursor`、`/new codex` 指定工具,各取所长
|
|
16
17
|
|
|
@@ -32,6 +33,45 @@ ChatCCC 把 Claude Code、Cursor Agent、Codex (OpenAI) 接入了飞书群聊:
|
|
|
32
33
|
- **零配置成本** —— 单一 `config.json`,没有 TOML 也没有零散环境变量。`npm i -g chatccc` 后**任意目录**直接 `chatccc` 就跑,首次启动自动弹出本地 Web 配置向导
|
|
33
34
|
- **群里能跑 git** —— `/git status`、`/git pull`、`/git log` 在飞书群里直接执行 stdout/stderr 回发,不用回电脑
|
|
34
35
|
- **代码极简易改** —— 纯 TypeScript 实现,核心只有 20 多个文件,统一 `ToolAdapter` 接口屏蔽 Claude / Cursor / Codex 差异,看得懂、改得动
|
|
36
|
+
- **微信 iLink 支持** —— 扫码登录微信个人号,即可在微信中与 AI 对话。微信端为纯文本模式(无卡片),生成过程中的内容以增量方式推送,避免重复刷屏。
|
|
37
|
+
|
|
38
|
+
---
|
|
39
|
+
|
|
40
|
+
## 微信 iLink 平台
|
|
41
|
+
|
|
42
|
+
除了飞书,ChatCCC 也支持通过 [微信 iLink](https://github.com/openilink/openilink-sdk-node) 接入微信个人号。**微信模式无需任何配置,启动后扫码即可使用。**
|
|
43
|
+
|
|
44
|
+
### 快速开始(微信)
|
|
45
|
+
|
|
46
|
+
```bash
|
|
47
|
+
chatccc # 启动 ChatCCC
|
|
48
|
+
# → 控制台打印出微信扫码二维码
|
|
49
|
+
# → 打开微信扫一扫 → 关注机器人
|
|
50
|
+
# → 发送 /new 开始对话
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
就这么简单。**不用创建飞书应用、不用配置权限、不用订阅回调。** 扫一下控制台上的二维码就能用。
|
|
54
|
+
|
|
55
|
+
### 微信与飞书的差异
|
|
56
|
+
|
|
57
|
+
| 特性 | 飞书 | 微信 |
|
|
58
|
+
| --- | --- | --- |
|
|
59
|
+
| 会话类型 | 群聊(一群一会话) | 私聊(一对一) |
|
|
60
|
+
| 消息展示 | CardKit 卡片(流式更新) | 纯文本(增量推送,结尾标记"━━━ 回答结束 ━━━") |
|
|
61
|
+
| `/new` 行为 | 创建新群聊 | 在当前私聊中创建新会话 |
|
|
62
|
+
| `/newh` 行为 | 群内重置会话 | 私聊内重置会话 |
|
|
63
|
+
| 非指令消息 | 走卡片进度展示 | 立即回复"生成中...",增量推送结果 |
|
|
64
|
+
| 群管理 | 创建/重命名/解散/设置头像 | 不支持 |
|
|
65
|
+
| 启动方式 | 飞书应用凭证 + WebSocket | 扫码登录(QR 码) |
|
|
66
|
+
| 连接保活 | WebSocket 长连接 | HTTP 长轮询 |
|
|
67
|
+
|
|
68
|
+
### 启用微信
|
|
69
|
+
|
|
70
|
+
`config.json` 中 `platforms.ilink.enabled` 设为 `true`(默认已开启)。启动 ChatCCC 后,终端会打印微信扫码登录二维码,使用微信扫描即可登录。
|
|
71
|
+
|
|
72
|
+
> 微信 iLink 登录的 token 和 context 会持久化到 `state/ilink-auth.json`,下次启动时会自动尝试复用。token 过期后重新扫码即可。
|
|
73
|
+
|
|
74
|
+
> 微信模式下无须配置飞书应用、无须订阅事件回调。只需启动 ChatCCC → 扫码 → 在微信中找到机器人即可对话。
|
|
35
75
|
|
|
36
76
|
---
|
|
37
77
|
|
|
@@ -229,6 +269,10 @@ ChatCCC 的所有运行参数都集中在包根目录的 `config.json`。
|
|
|
229
269
|
"appId": "cli_xxxxxxxxxxxx",
|
|
230
270
|
"appSecret": "xxxxxxxxxxxxxxxxxxxxxxxxxx"
|
|
231
271
|
},
|
|
272
|
+
"platforms": {
|
|
273
|
+
"feishu": { "enabled": true },
|
|
274
|
+
"ilink": { "enabled": true }
|
|
275
|
+
},
|
|
232
276
|
"port": 18080,
|
|
233
277
|
"gitTimeoutSeconds": 180,
|
|
234
278
|
"claude": {
|
|
@@ -260,6 +304,7 @@ ChatCCC 的所有运行参数都集中在包根目录的 `config.json`。
|
|
|
260
304
|
| 字段 | 必填 | 说明 |
|
|
261
305
|
| --- | --- | --- |
|
|
262
306
|
| `feishu.appId` / `feishu.appSecret` | 是 | 飞书应用「凭证与基础信息」中的 App ID / App Secret |
|
|
307
|
+
| `platforms.feishu.enabled` / `platforms.ilink.enabled` | 否 | IM 平台开关;默认同时开启飞书和微信 iLink。开启微信后启动时会显示扫码登录二维码 |
|
|
263
308
|
| `port` | 否 | 本地 WebSocket 中继 + Web 向导监听端口,默认 `18080`;同机多实例时改成不同值即可 |
|
|
264
309
|
| `gitTimeoutSeconds` | 否 | `/git` 命令在会话工作目录执行时的单次超时秒数,默认 `180`,允许范围 `1–3600`,超时会被 `SIGKILL` 强制终止 |
|
|
265
310
|
| `claude.enabled` / `cursor.enabled` / `codex.enabled` | 否 | 是否启用对应 AI Agent;Web 向导/管理页只展示 `enabled: true` 的 agent 卡片。字段缺省时按「任一配置字段非空」自动判定(向后兼容) |
|
|
@@ -309,7 +354,9 @@ ChatCCC 的所有运行参数都集中在包根目录的 `config.json`。
|
|
|
309
354
|
|
|
310
355
|
### 5. 开始使用
|
|
311
356
|
|
|
312
|
-
在飞书中找到你的机器人,发送 `/new
|
|
357
|
+
**飞书:** 在飞书中找到你的机器人,发送 `/new`(使用默认 Agent)或 `/new claude` / `/new cursor` / `/new codex`,机器人会自动创建一个群聊并把 AI 会话绑定到该群。之后直接在群里发消息就能对话。
|
|
358
|
+
|
|
359
|
+
**微信:** 启动 `chatccc` 后,控制台会打印微信扫码二维码。打开微信扫一扫关注机器人,直接发送 `/new` 或 `/new claude` 等指令即可在私聊中开始对话。所有命令与飞书端完全一致。
|
|
313
360
|
|
|
314
361
|
### 可用指令
|
|
315
362
|
|
package/config.sample.json
CHANGED
|
@@ -1,30 +1,34 @@
|
|
|
1
|
-
{
|
|
2
|
-
"feishu": {
|
|
3
|
-
"appId": "",
|
|
4
|
-
"appSecret": ""
|
|
5
|
-
},
|
|
6
|
-
"
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
"
|
|
15
|
-
"
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
"
|
|
19
|
-
"
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
"
|
|
25
|
-
"
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
"
|
|
29
|
-
|
|
30
|
-
|
|
1
|
+
{
|
|
2
|
+
"feishu": {
|
|
3
|
+
"appId": "",
|
|
4
|
+
"appSecret": ""
|
|
5
|
+
},
|
|
6
|
+
"platforms": {
|
|
7
|
+
"feishu": { "enabled": true },
|
|
8
|
+
"ilink": { "enabled": true, "reuseTokenOnStart": true }
|
|
9
|
+
},
|
|
10
|
+
"port": 18080,
|
|
11
|
+
"gitTimeoutSeconds": 180,
|
|
12
|
+
"allowInterrupt": false,
|
|
13
|
+
"claude": {
|
|
14
|
+
"enabled": false,
|
|
15
|
+
"defaultAgent": true,
|
|
16
|
+
"model": "",
|
|
17
|
+
"effort": "",
|
|
18
|
+
"apiKey": "",
|
|
19
|
+
"baseUrl": ""
|
|
20
|
+
},
|
|
21
|
+
"cursor": {
|
|
22
|
+
"enabled": false,
|
|
23
|
+
"defaultAgent": false,
|
|
24
|
+
"path": "",
|
|
25
|
+
"model": ""
|
|
26
|
+
},
|
|
27
|
+
"codex": {
|
|
28
|
+
"enabled": false,
|
|
29
|
+
"defaultAgent": false,
|
|
30
|
+
"path": "",
|
|
31
|
+
"model": "",
|
|
32
|
+
"effort": ""
|
|
33
|
+
}
|
|
34
|
+
}
|
package/package.json
CHANGED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
|
|
3
|
+
import { cardJsonToPlainText } from "../card-plain-text.ts";
|
|
4
|
+
import {
|
|
5
|
+
buildHelpCard,
|
|
6
|
+
buildProgressCard,
|
|
7
|
+
buildStatusCard,
|
|
8
|
+
} from "../cards.ts";
|
|
9
|
+
|
|
10
|
+
describe("cardJsonToPlainText", () => {
|
|
11
|
+
it("converts help cards to readable text with commands", () => {
|
|
12
|
+
const text = cardJsonToPlainText(buildHelpCard("hello"));
|
|
13
|
+
|
|
14
|
+
expect(text).toContain("# ChatCCC");
|
|
15
|
+
expect(text).toContain("hello");
|
|
16
|
+
expect(text).toContain("/new");
|
|
17
|
+
expect(text).toContain("/new cursor");
|
|
18
|
+
expect(text).toContain("/new codex");
|
|
19
|
+
expect(text).toContain("/restart");
|
|
20
|
+
expect(text).toContain("/cd");
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
it("converts status cards from v1 card format", () => {
|
|
24
|
+
const text = cardJsonToPlainText(buildStatusCard("status body", "green"));
|
|
25
|
+
|
|
26
|
+
expect(text).toContain("# 会话状态");
|
|
27
|
+
expect(text).toContain("status body");
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
it("converts schema 2.0 progress cards", () => {
|
|
31
|
+
const text = cardJsonToPlainText(buildProgressCard("stream body"));
|
|
32
|
+
|
|
33
|
+
expect(text).toContain("# 生成中...");
|
|
34
|
+
expect(text).toContain("stream body");
|
|
35
|
+
expect(text).toContain("/status");
|
|
36
|
+
expect(text).toContain("/stop");
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it("returns null for invalid card json", () => {
|
|
40
|
+
expect(cardJsonToPlainText("{bad json")).toBeNull();
|
|
41
|
+
});
|
|
42
|
+
});
|
|
@@ -685,6 +685,14 @@ describe("createClaudeAdapter — env 注入", () => {
|
|
|
685
685
|
// 确保每个用例从干净的 process.env 起步
|
|
686
686
|
delete process.env.ANTHROPIC_API_KEY;
|
|
687
687
|
delete process.env.ANTHROPIC_BASE_URL;
|
|
688
|
+
delete process.env.ANTHROPIC_AUTH_TOKEN;
|
|
689
|
+
delete process.env.CLAUDE_CODE_OAUTH_TOKEN;
|
|
690
|
+
delete process.env.ANTHROPIC_MODEL;
|
|
691
|
+
delete process.env.ANTHROPIC_DEFAULT_OPUS_MODEL;
|
|
692
|
+
delete process.env.ANTHROPIC_DEFAULT_SONNET_MODEL;
|
|
693
|
+
delete process.env.ANTHROPIC_DEFAULT_HAIKU_MODEL;
|
|
694
|
+
delete process.env.CLAUDE_CODE_SUBAGENT_MODEL;
|
|
695
|
+
delete process.env.CLAUDE_CODE_EFFORT_LEVEL;
|
|
688
696
|
});
|
|
689
697
|
|
|
690
698
|
// 用例之间清掉我们写入的 env,避免互相污染
|
|
@@ -780,6 +788,51 @@ describe("createClaudeAdapter — env 注入", () => {
|
|
|
780
788
|
expect(opts.env.ANTHROPIC_BASE_URL).toBe("https://gateway.example/anthropic");
|
|
781
789
|
});
|
|
782
790
|
|
|
791
|
+
it("ChatCCC API config is isolated from Claude settings auth/model env", async () => {
|
|
792
|
+
setupMockCreateSession();
|
|
793
|
+
process.env.ANTHROPIC_AUTH_TOKEN = "token-from-claude-settings";
|
|
794
|
+
process.env.CLAUDE_CODE_OAUTH_TOKEN = "oauth-from-claude-settings";
|
|
795
|
+
process.env.ANTHROPIC_MODEL = "model-from-claude-settings";
|
|
796
|
+
process.env.ANTHROPIC_DEFAULT_SONNET_MODEL = "sonnet-from-claude-settings";
|
|
797
|
+
process.env.CLAUDE_CODE_SUBAGENT_MODEL = "subagent-from-claude-settings";
|
|
798
|
+
process.env.CLAUDE_CODE_EFFORT_LEVEL = "max";
|
|
799
|
+
const adapter = createClaudeAdapter({
|
|
800
|
+
model: "chatccc-model",
|
|
801
|
+
effort: "high",
|
|
802
|
+
isEmpty: (v) => v.trim() === "",
|
|
803
|
+
apiKey: "sk-chatccc",
|
|
804
|
+
baseUrl: "https://chatccc-gateway.example/anthropic",
|
|
805
|
+
});
|
|
806
|
+
|
|
807
|
+
await adapter.createSession("/cwd");
|
|
808
|
+
|
|
809
|
+
const opts = sdk.unstable_v2_createSession.mock.calls[0][0];
|
|
810
|
+
expect(opts.env.ANTHROPIC_API_KEY).toBe("sk-chatccc");
|
|
811
|
+
expect(opts.env.ANTHROPIC_BASE_URL).toBe("https://chatccc-gateway.example/anthropic");
|
|
812
|
+
expect(opts.env.ANTHROPIC_AUTH_TOKEN).toBeUndefined();
|
|
813
|
+
expect(opts.env.CLAUDE_CODE_OAUTH_TOKEN).toBeUndefined();
|
|
814
|
+
expect(opts.env.ANTHROPIC_MODEL).toBeUndefined();
|
|
815
|
+
expect(opts.env.ANTHROPIC_DEFAULT_SONNET_MODEL).toBeUndefined();
|
|
816
|
+
expect(opts.env.CLAUDE_CODE_SUBAGENT_MODEL).toBeUndefined();
|
|
817
|
+
expect(opts.env.CLAUDE_CODE_EFFORT_LEVEL).toBeUndefined();
|
|
818
|
+
});
|
|
819
|
+
|
|
820
|
+
it("第三方 API 配置时仍然加载 CLAUDE.md(settingSources 始终为 project+local)", async () => {
|
|
821
|
+
setupMockCreateSession();
|
|
822
|
+
const adapter = createClaudeAdapter({
|
|
823
|
+
model: "",
|
|
824
|
+
effort: "",
|
|
825
|
+
isEmpty: (v) => v.trim() === "",
|
|
826
|
+
apiKey: "sk-chatccc",
|
|
827
|
+
baseUrl: "https://chatccc-gateway.example/anthropic",
|
|
828
|
+
});
|
|
829
|
+
|
|
830
|
+
await adapter.createSession("/cwd");
|
|
831
|
+
|
|
832
|
+
const opts = sdk.unstable_v2_createSession.mock.calls[0][0];
|
|
833
|
+
expect(opts.settingSources).toEqual(["project", "local"]);
|
|
834
|
+
});
|
|
835
|
+
|
|
783
836
|
it("不修改主进程 process.env(永不污染)", async () => {
|
|
784
837
|
setupMockCreateSession();
|
|
785
838
|
const before = { ...process.env };
|
|
@@ -817,4 +870,4 @@ describe("createClaudeAdapter — env 注入", () => {
|
|
|
817
870
|
expect(opts.env.ANTHROPIC_API_KEY).toBe("from-system-env");
|
|
818
871
|
expect(opts.env.ANTHROPIC_BASE_URL).toBe("https://override.example/anthropic");
|
|
819
872
|
});
|
|
820
|
-
});
|
|
873
|
+
});
|
|
@@ -12,15 +12,17 @@ import {
|
|
|
12
12
|
CLAUDE_BASE_URL,
|
|
13
13
|
CLAUDE_EFFORT,
|
|
14
14
|
CLAUDE_MODEL,
|
|
15
|
-
CURSOR_AGENT_ARGS,
|
|
16
|
-
CURSOR_AGENT_COMMAND,
|
|
17
|
-
|
|
18
|
-
|
|
15
|
+
CURSOR_AGENT_ARGS,
|
|
16
|
+
CURSOR_AGENT_COMMAND,
|
|
17
|
+
FEISHU_ENABLED,
|
|
18
|
+
GIT_TIMEOUT_MS,
|
|
19
|
+
GIT_TIMEOUT_SECONDS,
|
|
20
|
+
ILINK_ENABLED,
|
|
19
21
|
applyLoadedConfig,
|
|
20
22
|
config,
|
|
21
|
-
resolveDefaultAgentTool,
|
|
22
|
-
type AppConfig,
|
|
23
|
-
} from "../config.ts";
|
|
23
|
+
resolveDefaultAgentTool,
|
|
24
|
+
type AppConfig,
|
|
25
|
+
} from "../config.ts";
|
|
24
26
|
|
|
25
27
|
// ---------------------------------------------------------------------------
|
|
26
28
|
// applyLoadedConfig — setup → service「在线切换」时刷新进程内 config 的核心机制
|
|
@@ -33,20 +35,22 @@ import {
|
|
|
33
35
|
// 原地切换复用同一 server,重新读端口只会引入混乱)
|
|
34
36
|
// ---------------------------------------------------------------------------
|
|
35
37
|
|
|
36
|
-
const baseAppConfig: AppConfig = {
|
|
37
|
-
feishu: { appId: "INITIAL_APP", appSecret: "INITIAL_SECRET" },
|
|
38
|
+
const baseAppConfig: AppConfig = {
|
|
39
|
+
feishu: { appId: "INITIAL_APP", appSecret: "INITIAL_SECRET" },
|
|
40
|
+
platforms: { feishu: { enabled: true }, ilink: { enabled: true } },
|
|
38
41
|
port: 18080,
|
|
39
42
|
gitTimeoutSeconds: 180,
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
43
|
+
allowInterrupt: false,
|
|
44
|
+
claude: {
|
|
45
|
+
enabled: true,
|
|
46
|
+
defaultAgent: true,
|
|
47
|
+
model: "initial-model",
|
|
44
48
|
effort: "initial-effort",
|
|
45
49
|
apiKey: "sk-initial",
|
|
46
50
|
baseUrl: "https://initial.gw/anthropic",
|
|
47
51
|
},
|
|
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" },
|
|
52
|
+
cursor: { enabled: true, defaultAgent: false, path: "/initial/cursor", model: "initial-cursor-model" },
|
|
53
|
+
codex: { enabled: true, defaultAgent: false, path: "/initial/codex", model: "initial-codex-model", effort: "initial-codex-effort" },
|
|
50
54
|
};
|
|
51
55
|
|
|
52
56
|
// 把 module 状态抢救快照:每个 it 跑前重置回这个状态,避免污染相邻测试。
|
|
@@ -77,10 +81,10 @@ describe("applyLoadedConfig — 刷新 export let 常量", () => {
|
|
|
77
81
|
it("更新 Claude 配置(model / effort / apiKey / baseUrl)", () => {
|
|
78
82
|
applyLoadedConfig({
|
|
79
83
|
...structuredClone(baseAppConfig),
|
|
80
|
-
claude: {
|
|
81
|
-
enabled: true,
|
|
82
|
-
defaultAgent: true,
|
|
83
|
-
model: "deepseek-v4-pro",
|
|
84
|
+
claude: {
|
|
85
|
+
enabled: true,
|
|
86
|
+
defaultAgent: true,
|
|
87
|
+
model: "deepseek-v4-pro",
|
|
84
88
|
effort: "high",
|
|
85
89
|
apiKey: "sk-newkey",
|
|
86
90
|
baseUrl: "https://gw2.example/anthropic",
|
|
@@ -93,7 +97,7 @@ describe("applyLoadedConfig — 刷新 export let 常量", () => {
|
|
|
93
97
|
expect(CLAUDE_BASE_URL).toBe("https://gw2.example/anthropic");
|
|
94
98
|
});
|
|
95
99
|
|
|
96
|
-
it("更新 GIT_TIMEOUT_SECONDS 与 GIT_TIMEOUT_MS(毫秒派生值同步刷新)", () => {
|
|
100
|
+
it("更新 GIT_TIMEOUT_SECONDS 与 GIT_TIMEOUT_MS(毫秒派生值同步刷新)", () => {
|
|
97
101
|
applyLoadedConfig({
|
|
98
102
|
...structuredClone(baseAppConfig),
|
|
99
103
|
gitTimeoutSeconds: 240,
|
|
@@ -102,12 +106,25 @@ describe("applyLoadedConfig — 刷新 export let 常量", () => {
|
|
|
102
106
|
expect(GIT_TIMEOUT_SECONDS).toBe(240);
|
|
103
107
|
// 派生值必须跟着更新,否则 /git 仍然按旧 timeout 运行
|
|
104
108
|
expect(GIT_TIMEOUT_MS).toBe(240 * 1000);
|
|
105
|
-
});
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
it("更新平台开关(默认飞书和微信都开启)", () => {
|
|
112
|
+
expect(FEISHU_ENABLED).toBe(true);
|
|
113
|
+
expect(ILINK_ENABLED).toBe(true);
|
|
114
|
+
|
|
115
|
+
applyLoadedConfig({
|
|
116
|
+
...structuredClone(baseAppConfig),
|
|
117
|
+
platforms: { feishu: { enabled: false }, ilink: { enabled: true } },
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
expect(FEISHU_ENABLED).toBe(false);
|
|
121
|
+
expect(ILINK_ENABLED).toBe(true);
|
|
122
|
+
});
|
|
106
123
|
|
|
107
124
|
it("CURSOR_AGENT_ARGS 跟随 cursor.model 重新解析", () => {
|
|
108
125
|
applyLoadedConfig({
|
|
109
126
|
...structuredClone(baseAppConfig),
|
|
110
|
-
cursor: { enabled: true, defaultAgent: false, path: "/x/cursor", model: "claude-3.7-sonnet" },
|
|
127
|
+
cursor: { enabled: true, defaultAgent: false, path: "/x/cursor", model: "claude-3.7-sonnet" },
|
|
111
128
|
});
|
|
112
129
|
|
|
113
130
|
// CURSOR_AGENT_ARGS 是 ['-p', '--force', ..., '--model', 'claude-3.7-sonnet']
|
|
@@ -118,7 +135,7 @@ describe("applyLoadedConfig — 刷新 export let 常量", () => {
|
|
|
118
135
|
it("cursor.model 留空时 CURSOR_AGENT_ARGS 不含 --model", () => {
|
|
119
136
|
applyLoadedConfig({
|
|
120
137
|
...structuredClone(baseAppConfig),
|
|
121
|
-
cursor: { enabled: true, defaultAgent: false, path: "/x/cursor", model: "" },
|
|
138
|
+
cursor: { enabled: true, defaultAgent: false, path: "/x/cursor", model: "" },
|
|
122
139
|
});
|
|
123
140
|
|
|
124
141
|
expect(CURSOR_AGENT_ARGS).not.toContain("--model");
|
|
@@ -127,7 +144,7 @@ describe("applyLoadedConfig — 刷新 export let 常量", () => {
|
|
|
127
144
|
it("CURSOR_AGENT_COMMAND 优先取 config.cursor.path", () => {
|
|
128
145
|
applyLoadedConfig({
|
|
129
146
|
...structuredClone(baseAppConfig),
|
|
130
|
-
cursor: { enabled: true, defaultAgent: false, path: "C:/custom/cursor.exe", model: "" },
|
|
147
|
+
cursor: { enabled: true, defaultAgent: false, path: "C:/custom/cursor.exe", model: "" },
|
|
131
148
|
});
|
|
132
149
|
|
|
133
150
|
expect(CURSOR_AGENT_COMMAND).toBe("C:/custom/cursor.exe");
|
|
@@ -145,14 +162,14 @@ describe("applyLoadedConfig — 刷新 export let 常量", () => {
|
|
|
145
162
|
});
|
|
146
163
|
});
|
|
147
164
|
|
|
148
|
-
describe("applyLoadedConfig — config 对象引用契约", () => {
|
|
165
|
+
describe("applyLoadedConfig — config 对象引用契约", () => {
|
|
149
166
|
it("config 引用保持不变(就地更新),但字段被刷新", () => {
|
|
150
167
|
const refBefore = config;
|
|
151
168
|
|
|
152
169
|
applyLoadedConfig({
|
|
153
170
|
...structuredClone(baseAppConfig),
|
|
154
171
|
feishu: { appId: "REF_TEST_APP", appSecret: "REF_TEST_SECRET" },
|
|
155
|
-
codex: { enabled: true, defaultAgent: false, path: "/refresh/codex", model: "fresh-model", effort: "low" },
|
|
172
|
+
codex: { enabled: true, defaultAgent: false, path: "/refresh/codex", model: "fresh-model", effort: "low" },
|
|
156
173
|
});
|
|
157
174
|
|
|
158
175
|
// 必须是同一个引用:codex-adapter 等下游模块"直接 import config",
|
|
@@ -170,24 +187,24 @@ describe("applyLoadedConfig — config 对象引用契约", () => {
|
|
|
170
187
|
expect(APP_ID).toBe("");
|
|
171
188
|
expect(APP_SECRET).toBe("");
|
|
172
189
|
expect(config.feishu.appId).toBe("");
|
|
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
|
-
});
|
|
190
|
+
});
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
describe("resolveDefaultAgentTool", () => {
|
|
194
|
+
it("优先使用显式 defaultAgent 且已启用的 Agent", () => {
|
|
195
|
+
const cfg = structuredClone(baseAppConfig);
|
|
196
|
+
cfg.claude.defaultAgent = false;
|
|
197
|
+
cfg.cursor.defaultAgent = true;
|
|
198
|
+
|
|
199
|
+
expect(resolveDefaultAgentTool(cfg)).toBe("cursor");
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
it("defaultAgent 指向未启用 Agent 时回退到第一个已启用 Agent", () => {
|
|
203
|
+
const cfg = structuredClone(baseAppConfig);
|
|
204
|
+
cfg.claude.enabled = false;
|
|
205
|
+
cfg.claude.defaultAgent = true;
|
|
206
|
+
cfg.cursor.defaultAgent = false;
|
|
207
|
+
|
|
208
|
+
expect(resolveDefaultAgentTool(cfg)).toBe("cursor");
|
|
209
|
+
});
|
|
210
|
+
});
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { readFileSync } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
|
|
4
|
+
import { describe, expect, it } from "vitest";
|
|
5
|
+
|
|
6
|
+
describe("config.sample.json", () => {
|
|
7
|
+
it("enables Feishu and WeChat iLink by default", () => {
|
|
8
|
+
const configSamplePath = join(process.cwd(), "config.sample.json");
|
|
9
|
+
const sample = JSON.parse(readFileSync(configSamplePath, "utf8")) as {
|
|
10
|
+
platforms?: {
|
|
11
|
+
feishu?: { enabled?: unknown };
|
|
12
|
+
ilink?: { enabled?: unknown };
|
|
13
|
+
};
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
expect(sample.platforms?.feishu?.enabled).toBe(true);
|
|
17
|
+
expect(sample.platforms?.ilink?.enabled).toBe(true);
|
|
18
|
+
});
|
|
19
|
+
});
|