chatccc 0.2.186 → 0.2.188
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/config.sample.json +39 -32
- package/package.json +1 -1
- package/src/__tests__/builtin-config.test.ts +33 -0
- package/src/__tests__/chrome-devtools-guard.test.ts +40 -0
- package/src/__tests__/config-reload.test.ts +30 -16
- package/src/__tests__/config-sample.test.ts +41 -19
- package/src/__tests__/feishu-avatar.test.ts +180 -115
- package/src/__tests__/orchestrator.test.ts +165 -17
- package/src/__tests__/session.test.ts +17 -7
- package/src/__tests__/web-ui.test.ts +22 -0
- package/src/adapters/codex-adapter.ts +26 -21
- package/src/builtin/cli.ts +8 -16
- package/src/builtin/index.ts +12 -11
- package/src/cards.ts +48 -7
- package/src/chrome-devtools-guard.ts +78 -2
- package/src/config.ts +158 -108
- package/src/feishu-api.ts +219 -190
- package/src/index.ts +2 -2
- package/src/orchestrator.ts +211 -19
- package/src/platform-adapter.ts +9 -1
- package/src/session.ts +92 -64
- package/src/web-ui.ts +32 -7
package/config.sample.json
CHANGED
|
@@ -12,30 +12,30 @@
|
|
|
12
12
|
"port": 15166,
|
|
13
13
|
"chromePath": ""
|
|
14
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": {
|
|
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": {
|
|
39
39
|
"enabled": false,
|
|
40
40
|
"defaultAgent": true,
|
|
41
41
|
"model": "",
|
|
@@ -50,14 +50,21 @@
|
|
|
50
50
|
"defaultAgent": false,
|
|
51
51
|
"path": "",
|
|
52
52
|
"model": "",
|
|
53
|
+
"alternativeModel": "",
|
|
53
54
|
"avatarBatteryMode": "apiPercent",
|
|
54
55
|
"onDemandMonthlyBudget": 1000
|
|
55
56
|
},
|
|
56
|
-
"codex": {
|
|
57
|
-
"enabled": false,
|
|
58
|
-
"defaultAgent": false,
|
|
59
|
-
"path": "",
|
|
60
|
-
"model": "",
|
|
61
|
-
"
|
|
62
|
-
|
|
63
|
-
}
|
|
57
|
+
"codex": {
|
|
58
|
+
"enabled": false,
|
|
59
|
+
"defaultAgent": false,
|
|
60
|
+
"path": "",
|
|
61
|
+
"model": "",
|
|
62
|
+
"alternativeModel": "",
|
|
63
|
+
"effort": ""
|
|
64
|
+
},
|
|
65
|
+
"ccc": {
|
|
66
|
+
"DEEPSEEK_API_KEY": "",
|
|
67
|
+
"DEEPSEEK_BASE_URL": "https://api.deepseek.com/v1",
|
|
68
|
+
"model": "deepseek-v4-pro[1m]"
|
|
69
|
+
}
|
|
70
|
+
}
|
package/package.json
CHANGED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { afterEach, describe, expect, it } from "vitest";
|
|
2
|
+
|
|
3
|
+
import { ChatSession } from "../builtin/index.ts";
|
|
4
|
+
import { config } from "../config.ts";
|
|
5
|
+
|
|
6
|
+
const originalDeepSeekApiKey = process.env.DEEPSEEK_API_KEY;
|
|
7
|
+
const originalCccConfig = structuredClone(config.ccc);
|
|
8
|
+
|
|
9
|
+
afterEach(() => {
|
|
10
|
+
if (originalDeepSeekApiKey === undefined) {
|
|
11
|
+
delete process.env.DEEPSEEK_API_KEY;
|
|
12
|
+
} else {
|
|
13
|
+
process.env.DEEPSEEK_API_KEY = originalDeepSeekApiKey;
|
|
14
|
+
}
|
|
15
|
+
config.ccc = structuredClone(originalCccConfig);
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
describe("builtin ChatSession config", () => {
|
|
19
|
+
it("does not fall back to DEEPSEEK_API_KEY environment variable", () => {
|
|
20
|
+
config.ccc = {
|
|
21
|
+
DEEPSEEK_API_KEY: "",
|
|
22
|
+
DEEPSEEK_BASE_URL: "https://api.deepseek.com/v1",
|
|
23
|
+
model: "deepseek-v4-pro[1m]",
|
|
24
|
+
};
|
|
25
|
+
process.env.DEEPSEEK_API_KEY = "sk-env-should-not-be-used";
|
|
26
|
+
|
|
27
|
+
expect(() => new ChatSession()).toThrow("ccc.DEEPSEEK_API_KEY 未设置");
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
it("allows constructor parameters to override config defaults", () => {
|
|
31
|
+
expect(() => new ChatSession({ apiKey: "sk-test" })).not.toThrow();
|
|
32
|
+
});
|
|
33
|
+
});
|
|
@@ -3,6 +3,7 @@ import { spawn } from "node:child_process";
|
|
|
3
3
|
|
|
4
4
|
import {
|
|
5
5
|
ensureChromeCdpRunning,
|
|
6
|
+
ensureChatcccPageOpen,
|
|
6
7
|
isChromeCdpHealthy,
|
|
7
8
|
probeChromeCdp,
|
|
8
9
|
resolveChromeExecutable,
|
|
@@ -122,4 +123,43 @@ describe("Chrome CDP guard", () => {
|
|
|
122
123
|
expect(resolveChromeUserDataDir(15166, { LOCALAPPDATA: "C:/Users/me/AppData/Local" }))
|
|
123
124
|
.toBe("C:\\Users\\me\\AppData\\Local\\chatccc\\chrome-cdp-15166");
|
|
124
125
|
});
|
|
126
|
+
|
|
127
|
+
it("opens the configured ChatCCC page when no localhost or loopback page is present", async () => {
|
|
128
|
+
const fetchImpl = vi.fn(async (input: string | URL | Request, init?: RequestInit) => {
|
|
129
|
+
const url = String(input);
|
|
130
|
+
if (url === "http://127.0.0.1:15166/json/list") {
|
|
131
|
+
return new Response(JSON.stringify([
|
|
132
|
+
{ id: "chatgpt", type: "page", url: "https://chatgpt.com/", webSocketDebuggerUrl: "ws://page" },
|
|
133
|
+
]), { status: 200 });
|
|
134
|
+
}
|
|
135
|
+
if (url === "http://127.0.0.1:15166/json/new?http%3A%2F%2Flocalhost%3A18081%2F") {
|
|
136
|
+
expect(init?.method).toBe("PUT");
|
|
137
|
+
return new Response(JSON.stringify({ id: "chatccc" }), { status: 200 });
|
|
138
|
+
}
|
|
139
|
+
throw new Error(`unexpected fetch ${url}`);
|
|
140
|
+
}) as unknown as typeof fetch;
|
|
141
|
+
|
|
142
|
+
await expect(ensureChatcccPageOpen(15166, 18081, { fetchImpl })).resolves.toEqual({
|
|
143
|
+
ok: true,
|
|
144
|
+
opened: true,
|
|
145
|
+
});
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
it("does not open another ChatCCC page when localhost or 127.0.0.1 is already present", async () => {
|
|
149
|
+
const fetchImpl = vi.fn(async (input: string | URL | Request) => {
|
|
150
|
+
const url = String(input);
|
|
151
|
+
if (url === "http://127.0.0.1:15166/json/list") {
|
|
152
|
+
return new Response(JSON.stringify([
|
|
153
|
+
{ id: "chatccc", type: "page", url: "http://127.0.0.1:18080/", webSocketDebuggerUrl: "ws://page" },
|
|
154
|
+
]), { status: 200 });
|
|
155
|
+
}
|
|
156
|
+
throw new Error(`unexpected fetch ${url}`);
|
|
157
|
+
}) as unknown as typeof fetch;
|
|
158
|
+
|
|
159
|
+
await expect(ensureChatcccPageOpen(15166, 18080, { fetchImpl })).resolves.toEqual({
|
|
160
|
+
ok: true,
|
|
161
|
+
opened: false,
|
|
162
|
+
});
|
|
163
|
+
expect(fetchImpl).toHaveBeenCalledTimes(1);
|
|
164
|
+
});
|
|
125
165
|
});
|
|
@@ -22,6 +22,7 @@ import {
|
|
|
22
22
|
ILINK_ENABLED,
|
|
23
23
|
applyLoadedConfig,
|
|
24
24
|
config,
|
|
25
|
+
getAllModelsForTool,
|
|
25
26
|
resolveDefaultAgentTool,
|
|
26
27
|
type AppConfig,
|
|
27
28
|
} from "../config.ts";
|
|
@@ -41,15 +42,15 @@ const baseAppConfig: AppConfig = {
|
|
|
41
42
|
feishu: { appId: "INITIAL_APP", appSecret: "INITIAL_SECRET" },
|
|
42
43
|
platforms: { feishu: { enabled: true }, ilink: { enabled: true } },
|
|
43
44
|
chromeDevtools: { enabled: false, port: 15166, chromePath: "" },
|
|
44
|
-
port: 18080,
|
|
45
|
-
gitTimeoutSeconds: 180,
|
|
46
|
-
allowInterrupt: false,
|
|
47
|
-
rawStreamLogs: {
|
|
48
|
-
claude: { enabled: false, maxBytesPerTurn: 52_428_800, retentionDays: 7, keepCompleted: false },
|
|
49
|
-
cursor: { enabled: false, maxBytesPerTurn: 52_428_800, retentionDays: 7, keepCompleted: false },
|
|
50
|
-
codex: { enabled: false, maxBytesPerTurn: 52_428_800, retentionDays: 7, keepCompleted: false },
|
|
51
|
-
},
|
|
52
|
-
claude: {
|
|
45
|
+
port: 18080,
|
|
46
|
+
gitTimeoutSeconds: 180,
|
|
47
|
+
allowInterrupt: false,
|
|
48
|
+
rawStreamLogs: {
|
|
49
|
+
claude: { enabled: false, maxBytesPerTurn: 52_428_800, retentionDays: 7, keepCompleted: false },
|
|
50
|
+
cursor: { enabled: false, maxBytesPerTurn: 52_428_800, retentionDays: 7, keepCompleted: false },
|
|
51
|
+
codex: { enabled: false, maxBytesPerTurn: 52_428_800, retentionDays: 7, keepCompleted: false },
|
|
52
|
+
},
|
|
53
|
+
claude: {
|
|
53
54
|
enabled: true,
|
|
54
55
|
defaultAgent: true,
|
|
55
56
|
model: "initial-model",
|
|
@@ -64,11 +65,13 @@ const baseAppConfig: AppConfig = {
|
|
|
64
65
|
defaultAgent: false,
|
|
65
66
|
path: "/initial/cursor",
|
|
66
67
|
model: "initial-cursor-model",
|
|
68
|
+
alternativeModel: "initial-cursor-alt-model",
|
|
67
69
|
avatarBatteryMode: "apiPercent",
|
|
68
70
|
onDemandMonthlyBudget: 1000,
|
|
69
|
-
},
|
|
70
|
-
codex: { enabled: true, defaultAgent: false, path: "/initial/codex", model: "initial-codex-model", effort: "initial-codex-effort" },
|
|
71
|
-
}
|
|
71
|
+
},
|
|
72
|
+
codex: { enabled: true, defaultAgent: false, path: "/initial/codex", model: "initial-codex-model", alternativeModel: "initial-codex-alt-model", effort: "initial-codex-effort" },
|
|
73
|
+
ccc: { DEEPSEEK_API_KEY: "initial-ccc-key", DEEPSEEK_BASE_URL: "https://initial.deepseek.test/v1", model: "initial-ccc-model" },
|
|
74
|
+
};
|
|
72
75
|
|
|
73
76
|
// 把 module 状态抢救快照:每个 it 跑前重置回这个状态,避免污染相邻测试。
|
|
74
77
|
// 不直接用启动时的 config 引用做 snapshot——它可能已经被前一个 it 改写。
|
|
@@ -145,7 +148,7 @@ describe("applyLoadedConfig — 刷新 export let 常量", () => {
|
|
|
145
148
|
it("CURSOR_AGENT_ARGS 跟随 cursor.model 重新解析", () => {
|
|
146
149
|
applyLoadedConfig({
|
|
147
150
|
...structuredClone(baseAppConfig),
|
|
148
|
-
cursor: { enabled: true, defaultAgent: false, path: "/x/cursor", model: "claude-3.7-sonnet", avatarBatteryMode: "apiPercent", onDemandMonthlyBudget: 1000 },
|
|
151
|
+
cursor: { enabled: true, defaultAgent: false, path: "/x/cursor", model: "claude-3.7-sonnet", alternativeModel: "", avatarBatteryMode: "apiPercent", onDemandMonthlyBudget: 1000 },
|
|
149
152
|
});
|
|
150
153
|
|
|
151
154
|
// CURSOR_AGENT_ARGS 是 ['-p', '--force', '--approve-mcps', ..., '--model', 'claude-3.7-sonnet']
|
|
@@ -157,7 +160,7 @@ describe("applyLoadedConfig — 刷新 export let 常量", () => {
|
|
|
157
160
|
it("cursor.model 留空时 CURSOR_AGENT_ARGS 不含 --model", () => {
|
|
158
161
|
applyLoadedConfig({
|
|
159
162
|
...structuredClone(baseAppConfig),
|
|
160
|
-
cursor: { enabled: true, defaultAgent: false, path: "/x/cursor", model: "", avatarBatteryMode: "apiPercent", onDemandMonthlyBudget: 1000 },
|
|
163
|
+
cursor: { enabled: true, defaultAgent: false, path: "/x/cursor", model: "", alternativeModel: "", avatarBatteryMode: "apiPercent", onDemandMonthlyBudget: 1000 },
|
|
161
164
|
});
|
|
162
165
|
|
|
163
166
|
expect(CURSOR_AGENT_ARGS).not.toContain("--model");
|
|
@@ -166,12 +169,23 @@ describe("applyLoadedConfig — 刷新 export let 常量", () => {
|
|
|
166
169
|
it("CURSOR_AGENT_COMMAND 优先取 config.cursor.path", () => {
|
|
167
170
|
applyLoadedConfig({
|
|
168
171
|
...structuredClone(baseAppConfig),
|
|
169
|
-
cursor: { enabled: true, defaultAgent: false, path: "C:/custom/cursor.exe", model: "", avatarBatteryMode: "apiPercent", onDemandMonthlyBudget: 1000 },
|
|
172
|
+
cursor: { enabled: true, defaultAgent: false, path: "C:/custom/cursor.exe", model: "", alternativeModel: "", avatarBatteryMode: "apiPercent", onDemandMonthlyBudget: 1000 },
|
|
170
173
|
});
|
|
171
174
|
|
|
172
175
|
expect(CURSOR_AGENT_COMMAND).toBe("C:/custom/cursor.exe");
|
|
173
176
|
});
|
|
174
177
|
|
|
178
|
+
it("/model 候选列表包含 Cursor/Codex 的单个备选模型并保持主模型优先", () => {
|
|
179
|
+
const cfg = structuredClone(baseAppConfig);
|
|
180
|
+
cfg.cursor.model = "cursor-main";
|
|
181
|
+
cfg.cursor.alternativeModel = "cursor-alt";
|
|
182
|
+
cfg.codex.model = "codex-main";
|
|
183
|
+
cfg.codex.alternativeModel = "codex-main";
|
|
184
|
+
|
|
185
|
+
expect(getAllModelsForTool("cursor", cfg)).toEqual(["cursor-main", "cursor-alt"]);
|
|
186
|
+
expect(getAllModelsForTool("codex", cfg)).toEqual(["codex-main"]);
|
|
187
|
+
});
|
|
188
|
+
|
|
175
189
|
it("不修改 CHATCCC_PORT(端口在 setup 切换时必须保持不变)", () => {
|
|
176
190
|
const portBefore = CHATCCC_PORT;
|
|
177
191
|
applyLoadedConfig({
|
|
@@ -191,7 +205,7 @@ describe("applyLoadedConfig — config 对象引用契约", () => {
|
|
|
191
205
|
applyLoadedConfig({
|
|
192
206
|
...structuredClone(baseAppConfig),
|
|
193
207
|
feishu: { appId: "REF_TEST_APP", appSecret: "REF_TEST_SECRET" },
|
|
194
|
-
codex: { enabled: true, defaultAgent: false, path: "/refresh/codex", model: "fresh-model", effort: "low" },
|
|
208
|
+
codex: { enabled: true, defaultAgent: false, path: "/refresh/codex", model: "fresh-model", alternativeModel: "", effort: "low" },
|
|
195
209
|
});
|
|
196
210
|
|
|
197
211
|
// 必须是同一个引用:codex-adapter 等下游模块"直接 import config",
|
|
@@ -28,33 +28,55 @@ describe("config.sample.json", () => {
|
|
|
28
28
|
expect(sample.claude?.subagentModel).toBe("");
|
|
29
29
|
});
|
|
30
30
|
|
|
31
|
-
it("
|
|
31
|
+
it("leaves Cursor and Codex alternative models empty by default", () => {
|
|
32
32
|
const configSamplePath = join(process.cwd(), "config.sample.json");
|
|
33
33
|
const sample = JSON.parse(readFileSync(configSamplePath, "utf8")) as {
|
|
34
|
-
|
|
34
|
+
cursor?: { alternativeModel?: unknown };
|
|
35
|
+
codex?: { alternativeModel?: unknown };
|
|
35
36
|
};
|
|
36
37
|
|
|
37
|
-
expect(sample.
|
|
38
|
-
expect(sample.
|
|
39
|
-
expect(sample.chromeDevtools?.chromePath).toBe("");
|
|
38
|
+
expect(sample.cursor?.alternativeModel).toBe("");
|
|
39
|
+
expect(sample.codex?.alternativeModel).toBe("");
|
|
40
40
|
});
|
|
41
41
|
|
|
42
|
-
it("
|
|
42
|
+
it("sets ccc agent DeepSeek defaults in the sample config", () => {
|
|
43
43
|
const configSamplePath = join(process.cwd(), "config.sample.json");
|
|
44
44
|
const sample = JSON.parse(readFileSync(configSamplePath, "utf8")) as {
|
|
45
|
-
|
|
46
|
-
enabled?: unknown;
|
|
47
|
-
maxBytesPerTurn?: unknown;
|
|
48
|
-
retentionDays?: unknown;
|
|
49
|
-
keepCompleted?: unknown;
|
|
50
|
-
}>;
|
|
45
|
+
ccc?: { DEEPSEEK_API_KEY?: unknown; DEEPSEEK_BASE_URL?: unknown; model?: unknown };
|
|
51
46
|
};
|
|
52
47
|
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
expect(sample.rawStreamLogs?.[tool]?.retentionDays).toBe(7);
|
|
57
|
-
expect(sample.rawStreamLogs?.[tool]?.keepCompleted).toBe(false);
|
|
58
|
-
}
|
|
48
|
+
expect(sample.ccc?.DEEPSEEK_API_KEY).toBe("");
|
|
49
|
+
expect(sample.ccc?.DEEPSEEK_BASE_URL).toBe("https://api.deepseek.com/v1");
|
|
50
|
+
expect(sample.ccc?.model).toBe("deepseek-v4-pro[1m]");
|
|
59
51
|
});
|
|
60
|
-
|
|
52
|
+
|
|
53
|
+
it("keeps Chrome CDP guard disabled by default with port 15166", () => {
|
|
54
|
+
const configSamplePath = join(process.cwd(), "config.sample.json");
|
|
55
|
+
const sample = JSON.parse(readFileSync(configSamplePath, "utf8")) as {
|
|
56
|
+
chromeDevtools?: { enabled?: unknown; port?: unknown; chromePath?: unknown };
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
expect(sample.chromeDevtools?.enabled).toBe(false);
|
|
60
|
+
expect(sample.chromeDevtools?.port).toBe(15166);
|
|
61
|
+
expect(sample.chromeDevtools?.chromePath).toBe("");
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it("keeps raw stream logs disabled by default for every agent", () => {
|
|
65
|
+
const configSamplePath = join(process.cwd(), "config.sample.json");
|
|
66
|
+
const sample = JSON.parse(readFileSync(configSamplePath, "utf8")) as {
|
|
67
|
+
rawStreamLogs?: Record<string, {
|
|
68
|
+
enabled?: unknown;
|
|
69
|
+
maxBytesPerTurn?: unknown;
|
|
70
|
+
retentionDays?: unknown;
|
|
71
|
+
keepCompleted?: unknown;
|
|
72
|
+
}>;
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
for (const tool of ["claude", "cursor", "codex"]) {
|
|
76
|
+
expect(sample.rawStreamLogs?.[tool]?.enabled).toBe(false);
|
|
77
|
+
expect(sample.rawStreamLogs?.[tool]?.maxBytesPerTurn).toBe(52_428_800);
|
|
78
|
+
expect(sample.rawStreamLogs?.[tool]?.retentionDays).toBe(7);
|
|
79
|
+
expect(sample.rawStreamLogs?.[tool]?.keepCompleted).toBe(false);
|
|
80
|
+
}
|
|
81
|
+
});
|
|
82
|
+
});
|