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.
@@ -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
- "effort": ""
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "chatccc",
3
- "version": "0.2.186",
3
+ "version": "0.2.188",
4
4
  "description": "Feishu bot bridge for Claude Code",
5
5
  "license": "Apache-2.0",
6
6
  "type": "module",
@@ -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("keeps Chrome CDP guard disabled by default with port 15166", () => {
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
- chromeDevtools?: { enabled?: unknown; port?: unknown; chromePath?: unknown };
34
+ cursor?: { alternativeModel?: unknown };
35
+ codex?: { alternativeModel?: unknown };
35
36
  };
36
37
 
37
- expect(sample.chromeDevtools?.enabled).toBe(false);
38
- expect(sample.chromeDevtools?.port).toBe(15166);
39
- expect(sample.chromeDevtools?.chromePath).toBe("");
38
+ expect(sample.cursor?.alternativeModel).toBe("");
39
+ expect(sample.codex?.alternativeModel).toBe("");
40
40
  });
41
41
 
42
- it("keeps raw stream logs disabled by default for every agent", () => {
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
- rawStreamLogs?: Record<string, {
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
- for (const tool of ["claude", "cursor", "codex"]) {
54
- expect(sample.rawStreamLogs?.[tool]?.enabled).toBe(false);
55
- expect(sample.rawStreamLogs?.[tool]?.maxBytesPerTurn).toBe(52_428_800);
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
+ });