chatccc 0.2.185 → 0.2.187
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/agent-prompts/cursor_specific.md +12 -1
- package/config.sample.json +22 -0
- package/package.json +1 -1
- package/src/__tests__/config-reload.test.ts +23 -5
- package/src/__tests__/config-sample.test.ts +30 -0
- package/src/__tests__/raw-stream-log.test.ts +106 -0
- package/src/__tests__/web-ui.test.ts +22 -0
- package/src/adapters/cursor-adapter.ts +46 -21
- package/src/adapters/raw-stream-log.ts +124 -0
- package/src/config.ts +61 -3
- package/src/web-ui.ts +32 -7
|
@@ -1,3 +1,14 @@
|
|
|
1
1
|
## 计划/问答模式权限规则
|
|
2
2
|
|
|
3
|
-
如果用户消息以 `/plan` 或 `/ask` 开头,说明当前处于只读模式。**遇到任何需要用户同意的权限请求时,不要向用户申请权限,立即调用 stop-stuck-loop 接口提前结束本轮会话。**
|
|
3
|
+
如果用户消息以 `/plan` 或 `/ask` 开头,说明当前处于只读模式。**遇到任何需要用户同意的权限请求时,不要向用户申请权限,立即调用 stop-stuck-loop 接口提前结束本轮会话。**
|
|
4
|
+
|
|
5
|
+
## 用户问题澄清规则
|
|
6
|
+
|
|
7
|
+
当需要在开始实现前向用户确认问题、让用户做选择,或用户要求“从第一性原理出发挖掘真实需求 / 判断需求是否合理 / 开始实现前有什么问题要问我”时,**不要调用 `AskQuestion`、多选表单或任何结构化选择工具**。
|
|
8
|
+
|
|
9
|
+
必须直接用普通文本回复用户:
|
|
10
|
+
|
|
11
|
+
- 先简要说明你理解到的真实需求和你对合理性的判断。
|
|
12
|
+
- 再列出需要用户确认的问题;如果有推荐选项,就用文本说明“我建议 X,原因是 Y”。
|
|
13
|
+
- 问题数量尽量少,只问会影响实现边界、风险或不可逆决策的问题。
|
|
14
|
+
- 不要把这些问题放进工具调用参数里,也不要只在卡片/工具结果中呈现。
|
package/config.sample.json
CHANGED
|
@@ -15,6 +15,26 @@
|
|
|
15
15
|
"port": 18080,
|
|
16
16
|
"gitTimeoutSeconds": 180,
|
|
17
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
|
+
},
|
|
18
38
|
"claude": {
|
|
19
39
|
"enabled": false,
|
|
20
40
|
"defaultAgent": true,
|
|
@@ -30,6 +50,7 @@
|
|
|
30
50
|
"defaultAgent": false,
|
|
31
51
|
"path": "",
|
|
32
52
|
"model": "",
|
|
53
|
+
"alternativeModel": "",
|
|
33
54
|
"avatarBatteryMode": "apiPercent",
|
|
34
55
|
"onDemandMonthlyBudget": 1000
|
|
35
56
|
},
|
|
@@ -38,6 +59,7 @@
|
|
|
38
59
|
"defaultAgent": false,
|
|
39
60
|
"path": "",
|
|
40
61
|
"model": "",
|
|
62
|
+
"alternativeModel": "",
|
|
41
63
|
"effort": ""
|
|
42
64
|
}
|
|
43
65
|
}
|
package/package.json
CHANGED
|
@@ -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";
|
|
@@ -44,6 +45,11 @@ const baseAppConfig: AppConfig = {
|
|
|
44
45
|
port: 18080,
|
|
45
46
|
gitTimeoutSeconds: 180,
|
|
46
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
|
+
},
|
|
47
53
|
claude: {
|
|
48
54
|
enabled: true,
|
|
49
55
|
defaultAgent: true,
|
|
@@ -59,10 +65,11 @@ const baseAppConfig: AppConfig = {
|
|
|
59
65
|
defaultAgent: false,
|
|
60
66
|
path: "/initial/cursor",
|
|
61
67
|
model: "initial-cursor-model",
|
|
68
|
+
alternativeModel: "initial-cursor-alt-model",
|
|
62
69
|
avatarBatteryMode: "apiPercent",
|
|
63
70
|
onDemandMonthlyBudget: 1000,
|
|
64
71
|
},
|
|
65
|
-
codex: { enabled: true, defaultAgent: false, path: "/initial/codex", model: "initial-codex-model", effort: "initial-codex-effort" },
|
|
72
|
+
codex: { enabled: true, defaultAgent: false, path: "/initial/codex", model: "initial-codex-model", alternativeModel: "initial-codex-alt-model", effort: "initial-codex-effort" },
|
|
66
73
|
};
|
|
67
74
|
|
|
68
75
|
// 把 module 状态抢救快照:每个 it 跑前重置回这个状态,避免污染相邻测试。
|
|
@@ -140,7 +147,7 @@ describe("applyLoadedConfig — 刷新 export let 常量", () => {
|
|
|
140
147
|
it("CURSOR_AGENT_ARGS 跟随 cursor.model 重新解析", () => {
|
|
141
148
|
applyLoadedConfig({
|
|
142
149
|
...structuredClone(baseAppConfig),
|
|
143
|
-
cursor: { enabled: true, defaultAgent: false, path: "/x/cursor", model: "claude-3.7-sonnet", avatarBatteryMode: "apiPercent", onDemandMonthlyBudget: 1000 },
|
|
150
|
+
cursor: { enabled: true, defaultAgent: false, path: "/x/cursor", model: "claude-3.7-sonnet", alternativeModel: "", avatarBatteryMode: "apiPercent", onDemandMonthlyBudget: 1000 },
|
|
144
151
|
});
|
|
145
152
|
|
|
146
153
|
// CURSOR_AGENT_ARGS 是 ['-p', '--force', '--approve-mcps', ..., '--model', 'claude-3.7-sonnet']
|
|
@@ -152,7 +159,7 @@ describe("applyLoadedConfig — 刷新 export let 常量", () => {
|
|
|
152
159
|
it("cursor.model 留空时 CURSOR_AGENT_ARGS 不含 --model", () => {
|
|
153
160
|
applyLoadedConfig({
|
|
154
161
|
...structuredClone(baseAppConfig),
|
|
155
|
-
cursor: { enabled: true, defaultAgent: false, path: "/x/cursor", model: "", avatarBatteryMode: "apiPercent", onDemandMonthlyBudget: 1000 },
|
|
162
|
+
cursor: { enabled: true, defaultAgent: false, path: "/x/cursor", model: "", alternativeModel: "", avatarBatteryMode: "apiPercent", onDemandMonthlyBudget: 1000 },
|
|
156
163
|
});
|
|
157
164
|
|
|
158
165
|
expect(CURSOR_AGENT_ARGS).not.toContain("--model");
|
|
@@ -161,12 +168,23 @@ describe("applyLoadedConfig — 刷新 export let 常量", () => {
|
|
|
161
168
|
it("CURSOR_AGENT_COMMAND 优先取 config.cursor.path", () => {
|
|
162
169
|
applyLoadedConfig({
|
|
163
170
|
...structuredClone(baseAppConfig),
|
|
164
|
-
cursor: { enabled: true, defaultAgent: false, path: "C:/custom/cursor.exe", model: "", avatarBatteryMode: "apiPercent", onDemandMonthlyBudget: 1000 },
|
|
171
|
+
cursor: { enabled: true, defaultAgent: false, path: "C:/custom/cursor.exe", model: "", alternativeModel: "", avatarBatteryMode: "apiPercent", onDemandMonthlyBudget: 1000 },
|
|
165
172
|
});
|
|
166
173
|
|
|
167
174
|
expect(CURSOR_AGENT_COMMAND).toBe("C:/custom/cursor.exe");
|
|
168
175
|
});
|
|
169
176
|
|
|
177
|
+
it("/model 候选列表包含 Cursor/Codex 的单个备选模型并保持主模型优先", () => {
|
|
178
|
+
const cfg = structuredClone(baseAppConfig);
|
|
179
|
+
cfg.cursor.model = "cursor-main";
|
|
180
|
+
cfg.cursor.alternativeModel = "cursor-alt";
|
|
181
|
+
cfg.codex.model = "codex-main";
|
|
182
|
+
cfg.codex.alternativeModel = "codex-main";
|
|
183
|
+
|
|
184
|
+
expect(getAllModelsForTool("cursor", cfg)).toEqual(["cursor-main", "cursor-alt"]);
|
|
185
|
+
expect(getAllModelsForTool("codex", cfg)).toEqual(["codex-main"]);
|
|
186
|
+
});
|
|
187
|
+
|
|
170
188
|
it("不修改 CHATCCC_PORT(端口在 setup 切换时必须保持不变)", () => {
|
|
171
189
|
const portBefore = CHATCCC_PORT;
|
|
172
190
|
applyLoadedConfig({
|
|
@@ -186,7 +204,7 @@ describe("applyLoadedConfig — config 对象引用契约", () => {
|
|
|
186
204
|
applyLoadedConfig({
|
|
187
205
|
...structuredClone(baseAppConfig),
|
|
188
206
|
feishu: { appId: "REF_TEST_APP", appSecret: "REF_TEST_SECRET" },
|
|
189
|
-
codex: { enabled: true, defaultAgent: false, path: "/refresh/codex", model: "fresh-model", effort: "low" },
|
|
207
|
+
codex: { enabled: true, defaultAgent: false, path: "/refresh/codex", model: "fresh-model", alternativeModel: "", effort: "low" },
|
|
190
208
|
});
|
|
191
209
|
|
|
192
210
|
// 必须是同一个引用:codex-adapter 等下游模块"直接 import config",
|
|
@@ -28,6 +28,17 @@ describe("config.sample.json", () => {
|
|
|
28
28
|
expect(sample.claude?.subagentModel).toBe("");
|
|
29
29
|
});
|
|
30
30
|
|
|
31
|
+
it("leaves Cursor and Codex alternative models empty by default", () => {
|
|
32
|
+
const configSamplePath = join(process.cwd(), "config.sample.json");
|
|
33
|
+
const sample = JSON.parse(readFileSync(configSamplePath, "utf8")) as {
|
|
34
|
+
cursor?: { alternativeModel?: unknown };
|
|
35
|
+
codex?: { alternativeModel?: unknown };
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
expect(sample.cursor?.alternativeModel).toBe("");
|
|
39
|
+
expect(sample.codex?.alternativeModel).toBe("");
|
|
40
|
+
});
|
|
41
|
+
|
|
31
42
|
it("keeps Chrome CDP guard disabled by default with port 15166", () => {
|
|
32
43
|
const configSamplePath = join(process.cwd(), "config.sample.json");
|
|
33
44
|
const sample = JSON.parse(readFileSync(configSamplePath, "utf8")) as {
|
|
@@ -38,4 +49,23 @@ describe("config.sample.json", () => {
|
|
|
38
49
|
expect(sample.chromeDevtools?.port).toBe(15166);
|
|
39
50
|
expect(sample.chromeDevtools?.chromePath).toBe("");
|
|
40
51
|
});
|
|
52
|
+
|
|
53
|
+
it("keeps raw stream logs disabled by default for every agent", () => {
|
|
54
|
+
const configSamplePath = join(process.cwd(), "config.sample.json");
|
|
55
|
+
const sample = JSON.parse(readFileSync(configSamplePath, "utf8")) as {
|
|
56
|
+
rawStreamLogs?: Record<string, {
|
|
57
|
+
enabled?: unknown;
|
|
58
|
+
maxBytesPerTurn?: unknown;
|
|
59
|
+
retentionDays?: unknown;
|
|
60
|
+
keepCompleted?: unknown;
|
|
61
|
+
}>;
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
for (const tool of ["claude", "cursor", "codex"]) {
|
|
65
|
+
expect(sample.rawStreamLogs?.[tool]?.enabled).toBe(false);
|
|
66
|
+
expect(sample.rawStreamLogs?.[tool]?.maxBytesPerTurn).toBe(52_428_800);
|
|
67
|
+
expect(sample.rawStreamLogs?.[tool]?.retentionDays).toBe(7);
|
|
68
|
+
expect(sample.rawStreamLogs?.[tool]?.keepCompleted).toBe(false);
|
|
69
|
+
}
|
|
70
|
+
});
|
|
41
71
|
});
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import { gunzipSync } from "node:zlib";
|
|
2
|
+
import { mkdtemp, readFile, rm, stat, writeFile } from "node:fs/promises";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import { tmpdir } from "node:os";
|
|
5
|
+
|
|
6
|
+
import { describe, expect, it } from "vitest";
|
|
7
|
+
|
|
8
|
+
import {
|
|
9
|
+
createRawStreamLog,
|
|
10
|
+
sanitizeLogPathSegment,
|
|
11
|
+
} from "../adapters/raw-stream-log.ts";
|
|
12
|
+
|
|
13
|
+
describe("raw stream log", () => {
|
|
14
|
+
it("does nothing when disabled", async () => {
|
|
15
|
+
const root = await mkdtemp(join(tmpdir(), "chatccc-raw-log-"));
|
|
16
|
+
try {
|
|
17
|
+
const log = await createRawStreamLog({
|
|
18
|
+
enabled: false,
|
|
19
|
+
rootDir: root,
|
|
20
|
+
tool: "cursor",
|
|
21
|
+
sessionId: "sid",
|
|
22
|
+
label: "turn",
|
|
23
|
+
maxBytesPerTurn: 1024,
|
|
24
|
+
retentionDays: 7,
|
|
25
|
+
});
|
|
26
|
+
expect(log).toBeNull();
|
|
27
|
+
} finally {
|
|
28
|
+
await rm(root, { recursive: true, force: true });
|
|
29
|
+
}
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
it("writes gzipped JSONL and can keep the file", async () => {
|
|
33
|
+
const root = await mkdtemp(join(tmpdir(), "chatccc-raw-log-"));
|
|
34
|
+
try {
|
|
35
|
+
const log = await createRawStreamLog({
|
|
36
|
+
enabled: true,
|
|
37
|
+
rootDir: root,
|
|
38
|
+
tool: "cursor",
|
|
39
|
+
sessionId: "sid/unsafe",
|
|
40
|
+
label: "turn:1",
|
|
41
|
+
maxBytesPerTurn: 1024,
|
|
42
|
+
retentionDays: 7,
|
|
43
|
+
});
|
|
44
|
+
expect(log).not.toBeNull();
|
|
45
|
+
log!.writeLine('{"type":"assistant","text":"hello"}');
|
|
46
|
+
await log!.close({ keep: true });
|
|
47
|
+
|
|
48
|
+
const raw = gunzipSync(await readFile(log!.filePath)).toString("utf-8");
|
|
49
|
+
expect(raw).toBe('{"type":"assistant","text":"hello"}\n');
|
|
50
|
+
expect(await stat(log!.filePath)).toBeTruthy();
|
|
51
|
+
} finally {
|
|
52
|
+
await rm(root, { recursive: true, force: true });
|
|
53
|
+
}
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
it("writes a truncation marker once maxBytesPerTurn is exceeded", async () => {
|
|
57
|
+
const root = await mkdtemp(join(tmpdir(), "chatccc-raw-log-"));
|
|
58
|
+
try {
|
|
59
|
+
const log = await createRawStreamLog({
|
|
60
|
+
enabled: true,
|
|
61
|
+
rootDir: root,
|
|
62
|
+
tool: "cursor",
|
|
63
|
+
sessionId: "sid",
|
|
64
|
+
label: "turn",
|
|
65
|
+
maxBytesPerTurn: 12,
|
|
66
|
+
retentionDays: 7,
|
|
67
|
+
});
|
|
68
|
+
log!.writeLine('{"a":1}');
|
|
69
|
+
log!.writeLine('{"too":"large"}');
|
|
70
|
+
log!.writeLine('{"ignored":true}');
|
|
71
|
+
await log!.close({ keep: true });
|
|
72
|
+
|
|
73
|
+
const raw = gunzipSync(await readFile(log!.filePath)).toString("utf-8");
|
|
74
|
+
expect(raw).toContain('{"a":1}');
|
|
75
|
+
expect(raw).toContain("chatccc_raw_stream_log_truncated");
|
|
76
|
+
expect(raw).not.toContain("ignored");
|
|
77
|
+
} finally {
|
|
78
|
+
await rm(root, { recursive: true, force: true });
|
|
79
|
+
}
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
it("removes the file when keep is false", async () => {
|
|
83
|
+
const root = await mkdtemp(join(tmpdir(), "chatccc-raw-log-"));
|
|
84
|
+
try {
|
|
85
|
+
const log = await createRawStreamLog({
|
|
86
|
+
enabled: true,
|
|
87
|
+
rootDir: root,
|
|
88
|
+
tool: "cursor",
|
|
89
|
+
sessionId: "sid",
|
|
90
|
+
label: "turn",
|
|
91
|
+
maxBytesPerTurn: 1024,
|
|
92
|
+
retentionDays: 7,
|
|
93
|
+
});
|
|
94
|
+
log!.writeLine('{"type":"result"}');
|
|
95
|
+
await log!.close({ keep: false });
|
|
96
|
+
await expect(stat(log!.filePath)).rejects.toThrow();
|
|
97
|
+
} finally {
|
|
98
|
+
await rm(root, { recursive: true, force: true });
|
|
99
|
+
}
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
it("sanitizes path segments", () => {
|
|
103
|
+
expect(sanitizeLogPathSegment("../sid:1/2")).toBe("sid_1_2");
|
|
104
|
+
expect(sanitizeLogPathSegment("")).toBe("unknown");
|
|
105
|
+
});
|
|
106
|
+
});
|
|
@@ -34,6 +34,22 @@ describe("unflattenConfig", () => {
|
|
|
34
34
|
});
|
|
35
35
|
});
|
|
36
36
|
|
|
37
|
+
it("maps Cursor and Codex alternative models into agent config", () => {
|
|
38
|
+
expect(
|
|
39
|
+
unflattenConfig({
|
|
40
|
+
CHATCCC_CURSOR_ALTERNATIVE_MODEL: "gpt-5.5-high",
|
|
41
|
+
CHATCCC_CODEX_ALTERNATIVE_MODEL: "gpt-5.3-codex",
|
|
42
|
+
}),
|
|
43
|
+
).toEqual({
|
|
44
|
+
cursor: {
|
|
45
|
+
alternativeModel: "gpt-5.5-high",
|
|
46
|
+
},
|
|
47
|
+
codex: {
|
|
48
|
+
alternativeModel: "gpt-5.3-codex",
|
|
49
|
+
},
|
|
50
|
+
});
|
|
51
|
+
});
|
|
52
|
+
|
|
37
53
|
it("maps Chrome CDP guard fields into chromeDevtools config", () => {
|
|
38
54
|
expect(
|
|
39
55
|
unflattenConfig({
|
|
@@ -57,6 +73,12 @@ describe("dashboard edit modal", () => {
|
|
|
57
73
|
expect(PAGE_HTML).toContain("document.getElementById('edit-modal').classList.remove('hidden');");
|
|
58
74
|
expect(PAGE_HTML).toContain("document.getElementById('edit-overlay').classList.remove('hidden');");
|
|
59
75
|
});
|
|
76
|
+
|
|
77
|
+
it("uses plain alternative model labels for Cursor and Codex", () => {
|
|
78
|
+
expect(PAGE_HTML).toContain("field-CHATCCC_CURSOR_ALTERNATIVE_MODEL");
|
|
79
|
+
expect(PAGE_HTML).toContain("field-CHATCCC_CODEX_ALTERNATIVE_MODEL");
|
|
80
|
+
expect(PAGE_HTML).toContain("备选模型");
|
|
81
|
+
});
|
|
60
82
|
});
|
|
61
83
|
|
|
62
84
|
// ---------------------------------------------------------------------------
|
|
@@ -20,12 +20,16 @@ import type {
|
|
|
20
20
|
SessionInfo,
|
|
21
21
|
} from "./adapter-interface.ts";
|
|
22
22
|
import { parseUserCommand } from "./adapter-interface.ts";
|
|
23
|
-
import { CURSOR_AGENT_COMMAND, CURSOR_AGENT_ARGS } from "../config.ts";
|
|
23
|
+
import { config, CURSOR_AGENT_COMMAND, CURSOR_AGENT_ARGS, RAW_STREAM_LOGS_DIR } from "../config.ts";
|
|
24
24
|
import {
|
|
25
25
|
defaultCursorSessionMetaStore,
|
|
26
26
|
type CursorSessionMetaStore,
|
|
27
27
|
} from "./cursor-session-meta-store.ts";
|
|
28
|
-
import { killProcessTree } from "./proc-tree-kill.ts";
|
|
28
|
+
import { killProcessTree } from "./proc-tree-kill.ts";
|
|
29
|
+
import {
|
|
30
|
+
createRawStreamLog,
|
|
31
|
+
type RawStreamLogHandle,
|
|
32
|
+
} from "./raw-stream-log.ts";
|
|
29
33
|
|
|
30
34
|
// ---------------------------------------------------------------------------
|
|
31
35
|
// 特殊注入提示
|
|
@@ -344,11 +348,12 @@ function spawnAgent(
|
|
|
344
348
|
return proc;
|
|
345
349
|
}
|
|
346
350
|
|
|
347
|
-
async function* readJsonLines(
|
|
348
|
-
proc: ChildProcess,
|
|
349
|
-
signal?: AbortSignal,
|
|
350
|
-
debugTag?: string,
|
|
351
|
-
|
|
351
|
+
async function* readJsonLines(
|
|
352
|
+
proc: ChildProcess,
|
|
353
|
+
signal?: AbortSignal,
|
|
354
|
+
debugTag?: string,
|
|
355
|
+
rawLog?: RawStreamLogHandle | null,
|
|
356
|
+
): AsyncGenerator<CursorMessageLine> {
|
|
352
357
|
const tag = debugTag ?? "cursor";
|
|
353
358
|
const rl = createInterface({ input: proc.stdout!, crlfDelay: Infinity });
|
|
354
359
|
// abort 时主动 close readline,避免等待 Windows 管道自然关闭(可能延迟数分钟)
|
|
@@ -359,9 +364,10 @@ async function* readJsonLines(
|
|
|
359
364
|
for await (const line of rl) {
|
|
360
365
|
if (signal?.aborted) break;
|
|
361
366
|
lineCount++;
|
|
362
|
-
const trimmed = line.trim();
|
|
363
|
-
if (!trimmed) continue;
|
|
364
|
-
|
|
367
|
+
const trimmed = line.trim();
|
|
368
|
+
if (!trimmed) continue;
|
|
369
|
+
rawLog?.writeLine(trimmed);
|
|
370
|
+
try {
|
|
365
371
|
yield JSON.parse(trimmed) as CursorMessageLine;
|
|
366
372
|
} catch { /* 非 JSON 行静默跳过 */ }
|
|
367
373
|
}
|
|
@@ -426,15 +432,32 @@ class CursorAdapter implements ToolAdapter {
|
|
|
426
432
|
cmd.mode ?? undefined,
|
|
427
433
|
);
|
|
428
434
|
this.activeProcs.add(proc);
|
|
429
|
-
if (proc.pid !== undefined) options?.onProcessStart?.({ pid: proc.pid });
|
|
435
|
+
if (proc.pid !== undefined) options?.onProcessStart?.({ pid: proc.pid });
|
|
436
|
+
|
|
437
|
+
const rawLogConfig = config.rawStreamLogs.cursor;
|
|
438
|
+
let rawLog: RawStreamLogHandle | null = null;
|
|
439
|
+
try {
|
|
440
|
+
rawLog = await createRawStreamLog({
|
|
441
|
+
enabled: rawLogConfig.enabled,
|
|
442
|
+
rootDir: RAW_STREAM_LOGS_DIR,
|
|
443
|
+
tool: "cursor",
|
|
444
|
+
sessionId,
|
|
445
|
+
label: "prompt",
|
|
446
|
+
maxBytesPerTurn: rawLogConfig.maxBytesPerTurn,
|
|
447
|
+
retentionDays: rawLogConfig.retentionDays,
|
|
448
|
+
});
|
|
449
|
+
} catch (err) {
|
|
450
|
+
console.error(`[Cursor raw stream log] create failed: ${(err as Error).message}`);
|
|
451
|
+
}
|
|
430
452
|
|
|
431
453
|
// 见 codex-adapter.ts 同位置注释:spawn 用了 shell:true,必须杀整棵树,
|
|
432
454
|
// 否则 abort 后真正在跑的孙进程 cursor-agent 还会继续输出 & 占用资源。
|
|
433
|
-
const onAbort = () => { void killProcessTree(proc.pid); };
|
|
434
|
-
signal?.addEventListener("abort", onAbort, { once: true });
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
455
|
+
const onAbort = () => { void killProcessTree(proc.pid); };
|
|
456
|
+
signal?.addEventListener("abort", onAbort, { once: true });
|
|
457
|
+
let sawResult = false;
|
|
458
|
+
|
|
459
|
+
try {
|
|
460
|
+
for await (const raw of readJsonLines(proc, signal, sessionId, rawLog)) {
|
|
438
461
|
if (signal?.aborted) break;
|
|
439
462
|
if (
|
|
440
463
|
raw.type === "system" &&
|
|
@@ -450,15 +473,17 @@ class CursorAdapter implements ToolAdapter {
|
|
|
450
473
|
if (normalized) yield normalized;
|
|
451
474
|
|
|
452
475
|
// result 是流末事件,收到后立即结束进程,防止 CLI 僵死导致 readline 挂起。
|
|
453
|
-
if (raw.type === "result") {
|
|
454
|
-
|
|
476
|
+
if (raw.type === "result") {
|
|
477
|
+
sawResult = true;
|
|
478
|
+
void killProcessTree(proc.pid);
|
|
455
479
|
break;
|
|
456
480
|
}
|
|
457
481
|
}
|
|
458
482
|
} finally {
|
|
459
483
|
signal?.removeEventListener("abort", onAbort);
|
|
460
|
-
await killProcessTree(proc.pid);
|
|
461
|
-
|
|
484
|
+
await killProcessTree(proc.pid);
|
|
485
|
+
await rawLog?.close({ keep: rawLogConfig.keepCompleted || signal?.aborted === true || !sawResult });
|
|
486
|
+
this.activeProcs.delete(proc);
|
|
462
487
|
if (proc.pid !== undefined) options?.onProcessExit?.({ pid: proc.pid });
|
|
463
488
|
console.log(`[Cursor debug] prompt end: sessionId=${sessionId}, signalAborted=${signal?.aborted ?? false}`);
|
|
464
489
|
}
|
|
@@ -494,4 +519,4 @@ export function createCursorAdapter(
|
|
|
494
519
|
options: CreateCursorAdapterOptions = {},
|
|
495
520
|
): ToolAdapter {
|
|
496
521
|
return new CursorAdapter(options.metaStore ?? defaultCursorSessionMetaStore, options.model);
|
|
497
|
-
}
|
|
522
|
+
}
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import { createWriteStream, type Dirent } from "node:fs";
|
|
2
|
+
import { mkdir, readdir, rm, stat, unlink } from "node:fs/promises";
|
|
3
|
+
import { once } from "node:events";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
import { createGzip } from "node:zlib";
|
|
6
|
+
|
|
7
|
+
export interface RawStreamLogOptions {
|
|
8
|
+
enabled: boolean;
|
|
9
|
+
rootDir: string;
|
|
10
|
+
tool: string;
|
|
11
|
+
sessionId: string;
|
|
12
|
+
label: string;
|
|
13
|
+
maxBytesPerTurn: number;
|
|
14
|
+
retentionDays: number;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export interface RawStreamLogHandle {
|
|
18
|
+
filePath: string;
|
|
19
|
+
writeLine(line: string): void;
|
|
20
|
+
close(options: { keep: boolean }): Promise<void>;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function sanitizeLogPathSegment(value: string): string {
|
|
24
|
+
const safe = value.replace(/[^a-zA-Z0-9._-]+/g, "_").replace(/^[._]+|_+$/g, "");
|
|
25
|
+
return safe || "unknown";
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
async function cleanupOldRawStreamLogs(rootDir: string, retentionDays: number): Promise<void> {
|
|
29
|
+
if (!Number.isFinite(retentionDays) || retentionDays <= 0) return;
|
|
30
|
+
const cutoff = Date.now() - retentionDays * 24 * 60 * 60 * 1000;
|
|
31
|
+
|
|
32
|
+
const visit = async (dir: string): Promise<void> => {
|
|
33
|
+
let entries: Dirent[];
|
|
34
|
+
try {
|
|
35
|
+
entries = await readdir(dir, { withFileTypes: true });
|
|
36
|
+
} catch {
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
await Promise.all(entries.map(async (entry) => {
|
|
41
|
+
const path = join(dir, entry.name);
|
|
42
|
+
if (entry.isDirectory()) {
|
|
43
|
+
await visit(path);
|
|
44
|
+
try {
|
|
45
|
+
await rm(path, { recursive: false });
|
|
46
|
+
} catch {
|
|
47
|
+
// Directory is not empty or already gone.
|
|
48
|
+
}
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
if (!entry.isFile()) return;
|
|
53
|
+
try {
|
|
54
|
+
const info = await stat(path);
|
|
55
|
+
if (info.mtimeMs < cutoff) await rm(path, { force: true });
|
|
56
|
+
} catch {
|
|
57
|
+
// Best-effort cleanup only.
|
|
58
|
+
}
|
|
59
|
+
}));
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
await visit(rootDir);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export async function createRawStreamLog(options: RawStreamLogOptions): Promise<RawStreamLogHandle | null> {
|
|
66
|
+
if (!options.enabled) return null;
|
|
67
|
+
|
|
68
|
+
const maxBytes = Math.max(0, Math.floor(options.maxBytesPerTurn));
|
|
69
|
+
const tool = sanitizeLogPathSegment(options.tool);
|
|
70
|
+
const session = sanitizeLogPathSegment(options.sessionId);
|
|
71
|
+
const label = sanitizeLogPathSegment(options.label);
|
|
72
|
+
const dir = join(options.rootDir, tool, session);
|
|
73
|
+
await mkdir(dir, { recursive: true });
|
|
74
|
+
void cleanupOldRawStreamLogs(join(options.rootDir, tool), options.retentionDays);
|
|
75
|
+
|
|
76
|
+
const timestamp = new Date().toISOString().replace(/[:.]/g, "-");
|
|
77
|
+
const filePath = join(dir, `${timestamp}-${label}.jsonl.gz`);
|
|
78
|
+
const output = createWriteStream(filePath);
|
|
79
|
+
const gzip = createGzip();
|
|
80
|
+
gzip.pipe(output);
|
|
81
|
+
|
|
82
|
+
let bytes = 0;
|
|
83
|
+
let truncated = false;
|
|
84
|
+
let ended = false;
|
|
85
|
+
|
|
86
|
+
const writeLine = (line: string): void => {
|
|
87
|
+
if (ended || truncated) return;
|
|
88
|
+
const payload = `${line}\n`;
|
|
89
|
+
const payloadBytes = Buffer.byteLength(payload, "utf-8");
|
|
90
|
+
if (maxBytes > 0 && bytes + payloadBytes > maxBytes) {
|
|
91
|
+
const marker = JSON.stringify({
|
|
92
|
+
type: "chatccc_raw_stream_log_truncated",
|
|
93
|
+
reason: "max_bytes_per_turn_exceeded",
|
|
94
|
+
maxBytesPerTurn: maxBytes,
|
|
95
|
+
writtenBytes: bytes,
|
|
96
|
+
});
|
|
97
|
+
gzip.write(`${marker}\n`);
|
|
98
|
+
truncated = true;
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
bytes += payloadBytes;
|
|
102
|
+
gzip.write(payload);
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
const close = async ({ keep }: { keep: boolean }): Promise<void> => {
|
|
106
|
+
if (ended) return;
|
|
107
|
+
ended = true;
|
|
108
|
+
gzip.end();
|
|
109
|
+
try {
|
|
110
|
+
await once(output, "finish");
|
|
111
|
+
} catch {
|
|
112
|
+
// Ignore close errors; caller cannot recover from debug log failures.
|
|
113
|
+
}
|
|
114
|
+
if (!keep) {
|
|
115
|
+
try {
|
|
116
|
+
await unlink(filePath);
|
|
117
|
+
} catch {
|
|
118
|
+
// Best-effort cleanup only.
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
return { filePath, writeLine, close };
|
|
124
|
+
}
|
package/src/config.ts
CHANGED
|
@@ -42,6 +42,7 @@ export const LOG_DIR = join(USER_DATA_DIR, "logs");
|
|
|
42
42
|
export const fileLog = setupFileLogging(LOG_DIR, "index");
|
|
43
43
|
|
|
44
44
|
export const CHAT_LOGS_DIR = join(USER_DATA_DIR, "state", "chat_logs");
|
|
45
|
+
export const RAW_STREAM_LOGS_DIR = join(LOG_DIR, "raw-streams");
|
|
45
46
|
|
|
46
47
|
export async function appendChatLog(chatId: string, sender: string, text: string): Promise<void> {
|
|
47
48
|
try {
|
|
@@ -81,6 +82,8 @@ export interface CursorConfig {
|
|
|
81
82
|
/** Cursor Agent CLI 可执行文件绝对路径;留空时由运行时按 LocalAppData / PATH 兜底 */
|
|
82
83
|
path: string;
|
|
83
84
|
model: string;
|
|
85
|
+
/** /model 可切换的单个备选模型;留空则不加入候选列表 */
|
|
86
|
+
alternativeModel: string;
|
|
84
87
|
avatarBatteryMode: CursorAvatarBatteryMode;
|
|
85
88
|
onDemandMonthlyBudget: number;
|
|
86
89
|
}
|
|
@@ -93,6 +96,8 @@ export interface CodexConfig {
|
|
|
93
96
|
/** Codex CLI 可执行文件绝对路径;留空时退回到 PATH 中的 `codex` */
|
|
94
97
|
path: string;
|
|
95
98
|
model: string;
|
|
99
|
+
/** /model 可切换的单个备选模型;留空则不加入候选列表 */
|
|
100
|
+
alternativeModel: string;
|
|
96
101
|
effort: string;
|
|
97
102
|
}
|
|
98
103
|
|
|
@@ -122,6 +127,19 @@ export interface ChromeDevtoolsConfig {
|
|
|
122
127
|
chromePath: string;
|
|
123
128
|
}
|
|
124
129
|
|
|
130
|
+
export interface RawStreamAgentLogConfig {
|
|
131
|
+
enabled: boolean;
|
|
132
|
+
maxBytesPerTurn: number;
|
|
133
|
+
retentionDays: number;
|
|
134
|
+
keepCompleted: boolean;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
export interface RawStreamLogsConfig {
|
|
138
|
+
claude: RawStreamAgentLogConfig;
|
|
139
|
+
cursor: RawStreamAgentLogConfig;
|
|
140
|
+
codex: RawStreamAgentLogConfig;
|
|
141
|
+
}
|
|
142
|
+
|
|
125
143
|
export interface AppConfig {
|
|
126
144
|
feishu: FeishuConfig;
|
|
127
145
|
platforms: PlatformsConfig;
|
|
@@ -130,6 +148,7 @@ export interface AppConfig {
|
|
|
130
148
|
gitTimeoutSeconds: number;
|
|
131
149
|
/** 若为 false,AI 生成过程中用户发送消息不会打断,须先点「停止」再发送新消息 */
|
|
132
150
|
allowInterrupt: boolean;
|
|
151
|
+
rawStreamLogs: RawStreamLogsConfig;
|
|
133
152
|
claude: ClaudeConfig;
|
|
134
153
|
cursor: CursorConfig;
|
|
135
154
|
codex: CodexConfig;
|
|
@@ -151,8 +170,10 @@ export function getAllModelsForTool(tool: AgentTool, cfg: AppConfig = config): s
|
|
|
151
170
|
collect(cfg.claude.subagentModel);
|
|
152
171
|
} else if (tool === "cursor") {
|
|
153
172
|
collect(cfg.cursor.model);
|
|
173
|
+
collect(cfg.cursor.alternativeModel);
|
|
154
174
|
} else if (tool === "codex") {
|
|
155
175
|
collect(cfg.codex.model);
|
|
176
|
+
collect(cfg.codex.alternativeModel);
|
|
156
177
|
}
|
|
157
178
|
|
|
158
179
|
return Array.from(seen).slice(0, 100);
|
|
@@ -348,6 +369,23 @@ function normalizeCursorOnDemandMonthlyBudget(raw: unknown): number {
|
|
|
348
369
|
return Number.isFinite(value) && value > 0 ? value : 1000;
|
|
349
370
|
}
|
|
350
371
|
|
|
372
|
+
function normalizePositiveInteger(raw: unknown, fallback: number): number {
|
|
373
|
+
const value = typeof raw === "string" ? Number(raw.trim()) : Number(raw);
|
|
374
|
+
return Number.isInteger(value) && value > 0 ? value : fallback;
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
function normalizeRawStreamAgentLogConfig(raw: unknown): RawStreamAgentLogConfig {
|
|
378
|
+
const obj = typeof raw === "object" && raw !== null
|
|
379
|
+
? raw as Record<string, unknown>
|
|
380
|
+
: {};
|
|
381
|
+
return {
|
|
382
|
+
enabled: typeof obj.enabled === "boolean" ? obj.enabled : false,
|
|
383
|
+
maxBytesPerTurn: normalizePositiveInteger(obj.maxBytesPerTurn, 50 * 1024 * 1024),
|
|
384
|
+
retentionDays: normalizePositiveInteger(obj.retentionDays, 7),
|
|
385
|
+
keepCompleted: typeof obj.keepCompleted === "boolean" ? obj.keepCompleted : false,
|
|
386
|
+
};
|
|
387
|
+
}
|
|
388
|
+
|
|
351
389
|
function loadConfig(): AppConfig {
|
|
352
390
|
const defaults: AppConfig = {
|
|
353
391
|
feishu: { appId: "", appSecret: "" },
|
|
@@ -356,16 +394,22 @@ function loadConfig(): AppConfig {
|
|
|
356
394
|
port: 18080,
|
|
357
395
|
gitTimeoutSeconds: 180,
|
|
358
396
|
allowInterrupt: false,
|
|
397
|
+
rawStreamLogs: {
|
|
398
|
+
claude: { enabled: false, maxBytesPerTurn: 50 * 1024 * 1024, retentionDays: 7, keepCompleted: false },
|
|
399
|
+
cursor: { enabled: false, maxBytesPerTurn: 50 * 1024 * 1024, retentionDays: 7, keepCompleted: false },
|
|
400
|
+
codex: { enabled: false, maxBytesPerTurn: 50 * 1024 * 1024, retentionDays: 7, keepCompleted: false },
|
|
401
|
+
},
|
|
359
402
|
claude: { enabled: false, defaultAgent: true, model: "", subagentModel: "", effort: "", apiKey: "", baseUrl: "", maxTurn: 0 },
|
|
360
403
|
cursor: {
|
|
361
404
|
enabled: false,
|
|
362
405
|
defaultAgent: false,
|
|
363
406
|
path: "",
|
|
364
407
|
model: "claude-opus-4-7-max",
|
|
408
|
+
alternativeModel: "",
|
|
365
409
|
avatarBatteryMode: "apiPercent",
|
|
366
410
|
onDemandMonthlyBudget: 1000,
|
|
367
411
|
},
|
|
368
|
-
codex: { enabled: false, defaultAgent: false, path: "", model: "", effort: "" },
|
|
412
|
+
codex: { enabled: false, defaultAgent: false, path: "", model: "", alternativeModel: "", effort: "" },
|
|
369
413
|
};
|
|
370
414
|
|
|
371
415
|
if (!IS_TEST_ENV) {
|
|
@@ -413,11 +457,13 @@ function loadConfig(): AppConfig {
|
|
|
413
457
|
path?: unknown;
|
|
414
458
|
command?: unknown;
|
|
415
459
|
model?: unknown;
|
|
460
|
+
alternativeModel?: unknown;
|
|
416
461
|
avatarBatteryMode?: unknown;
|
|
417
462
|
onDemandMonthlyBudget?: unknown;
|
|
418
463
|
};
|
|
419
|
-
codex?: { enabled?: unknown; defaultAgent?: unknown; path?: unknown; command?: unknown; model?: unknown; effort?: unknown };
|
|
464
|
+
codex?: { enabled?: unknown; defaultAgent?: unknown; path?: unknown; command?: unknown; model?: unknown; alternativeModel?: unknown; effort?: unknown };
|
|
420
465
|
chromeDevtools?: { enabled?: unknown; port?: unknown; chromePath?: unknown };
|
|
466
|
+
rawStreamLogs?: unknown;
|
|
421
467
|
};
|
|
422
468
|
try {
|
|
423
469
|
parsed = JSON.parse(raw);
|
|
@@ -431,6 +477,9 @@ function loadConfig(): AppConfig {
|
|
|
431
477
|
const cursorRaw = (parsed.cursor ?? {}) as NonNullable<typeof parsed.cursor>;
|
|
432
478
|
const codexRaw = (parsed.codex ?? {}) as NonNullable<typeof parsed.codex>;
|
|
433
479
|
const chromeDevtoolsRaw = (parsed.chromeDevtools ?? {}) as NonNullable<typeof parsed.chromeDevtools>;
|
|
480
|
+
const rawStreamLogsRaw = typeof parsed.rawStreamLogs === "object" && parsed.rawStreamLogs !== null
|
|
481
|
+
? parsed.rawStreamLogs as unknown as Record<string, unknown>
|
|
482
|
+
: {};
|
|
434
483
|
|
|
435
484
|
// 兼容旧字段 `command`:命中时打印一次性 warning 提示用户改名
|
|
436
485
|
const onLegacyField = (label: string, value: string): void => {
|
|
@@ -461,13 +510,15 @@ function loadConfig(): AppConfig {
|
|
|
461
510
|
Boolean(
|
|
462
511
|
(typeof cursorRaw.path === "string" && cursorRaw.path.trim()) ||
|
|
463
512
|
(typeof cursorRaw.command === "string" && (cursorRaw.command as string).trim()) ||
|
|
464
|
-
(typeof cursorRaw.model === "string" && (cursorRaw.model as string).trim())
|
|
513
|
+
(typeof cursorRaw.model === "string" && (cursorRaw.model as string).trim()) ||
|
|
514
|
+
(typeof cursorRaw.alternativeModel === "string" && (cursorRaw.alternativeModel as string).trim()),
|
|
465
515
|
);
|
|
466
516
|
const codexNonEmpty = (): boolean =>
|
|
467
517
|
Boolean(
|
|
468
518
|
(typeof codexRaw.path === "string" && codexRaw.path.trim()) ||
|
|
469
519
|
(typeof codexRaw.command === "string" && (codexRaw.command as string).trim()) ||
|
|
470
520
|
(typeof codexRaw.model === "string" && (codexRaw.model as string).trim()) ||
|
|
521
|
+
(typeof codexRaw.alternativeModel === "string" && (codexRaw.alternativeModel as string).trim()) ||
|
|
471
522
|
(typeof codexRaw.effort === "string" && (codexRaw.effort as string).trim()),
|
|
472
523
|
);
|
|
473
524
|
|
|
@@ -522,6 +573,11 @@ function loadConfig(): AppConfig {
|
|
|
522
573
|
port: typeof parsed.port === "number" ? parsed.port : 18080,
|
|
523
574
|
gitTimeoutSeconds: typeof parsed.gitTimeoutSeconds === "number" ? parsed.gitTimeoutSeconds : 180,
|
|
524
575
|
allowInterrupt: typeof parsed.allowInterrupt === "boolean" ? parsed.allowInterrupt : false,
|
|
576
|
+
rawStreamLogs: {
|
|
577
|
+
claude: normalizeRawStreamAgentLogConfig(rawStreamLogsRaw.claude),
|
|
578
|
+
cursor: normalizeRawStreamAgentLogConfig(rawStreamLogsRaw.cursor),
|
|
579
|
+
codex: normalizeRawStreamAgentLogConfig(rawStreamLogsRaw.codex),
|
|
580
|
+
},
|
|
525
581
|
claude: {
|
|
526
582
|
enabled: claudeEnabled,
|
|
527
583
|
defaultAgent: defaultTool === "claude",
|
|
@@ -539,6 +595,7 @@ function loadConfig(): AppConfig {
|
|
|
539
595
|
defaultAgent: defaultTool === "cursor",
|
|
540
596
|
path: readToolCliPath(cursorRaw, { label: "cursor", onLegacyField }),
|
|
541
597
|
model: normalizeOptionalConfigField(cursorRaw.model, { label: "cursor.model", fallback: "claude-opus-4-7-max" }),
|
|
598
|
+
alternativeModel: normalizeOptionalConfigField(cursorRaw.alternativeModel, { label: "cursor.alternativeModel" }),
|
|
542
599
|
avatarBatteryMode: normalizeCursorAvatarBatteryMode(cursorRaw.avatarBatteryMode),
|
|
543
600
|
onDemandMonthlyBudget: normalizeCursorOnDemandMonthlyBudget(cursorRaw.onDemandMonthlyBudget),
|
|
544
601
|
},
|
|
@@ -547,6 +604,7 @@ function loadConfig(): AppConfig {
|
|
|
547
604
|
defaultAgent: defaultTool === "codex",
|
|
548
605
|
path: readToolCliPath(codexRaw, { label: "codex", onLegacyField }),
|
|
549
606
|
model: normalizeOptionalConfigField(codexRaw.model, { label: "codex.model" }),
|
|
607
|
+
alternativeModel: normalizeOptionalConfigField(codexRaw.alternativeModel, { label: "codex.alternativeModel" }),
|
|
550
608
|
effort: normalizeOptionalConfigField(codexRaw.effort, { label: "codex.effort" }),
|
|
551
609
|
},
|
|
552
610
|
};
|
package/src/web-ui.ts
CHANGED
|
@@ -40,10 +40,11 @@ interface AppConfig {
|
|
|
40
40
|
path?: string;
|
|
41
41
|
command?: string;
|
|
42
42
|
model?: string;
|
|
43
|
+
alternativeModel?: string;
|
|
43
44
|
avatarBatteryMode?: string;
|
|
44
45
|
onDemandMonthlyBudget?: number;
|
|
45
46
|
};
|
|
46
|
-
codex?: { enabled?: boolean; defaultAgent?: boolean; path?: string; command?: string; model?: string; effort?: string };
|
|
47
|
+
codex?: { enabled?: boolean; defaultAgent?: boolean; path?: string; command?: string; model?: string; alternativeModel?: string; effort?: string };
|
|
47
48
|
}
|
|
48
49
|
|
|
49
50
|
// ---------------------------------------------------------------------------
|
|
@@ -380,6 +381,9 @@ export function unflattenConfig(flat: Record<string, unknown>): Record<string, u
|
|
|
380
381
|
} else if (key === "CHATCCC_CURSOR_MODEL") {
|
|
381
382
|
result.cursor = result.cursor || {};
|
|
382
383
|
(result.cursor as Record<string, unknown>).model = val;
|
|
384
|
+
} else if (key === "CHATCCC_CURSOR_ALTERNATIVE_MODEL") {
|
|
385
|
+
result.cursor = result.cursor || {};
|
|
386
|
+
(result.cursor as Record<string, unknown>).alternativeModel = val;
|
|
383
387
|
} else if (key === "CHATCCC_CURSOR_AVATAR_BATTERY_MODE") {
|
|
384
388
|
result.cursor = result.cursor || {};
|
|
385
389
|
(result.cursor as Record<string, unknown>).avatarBatteryMode = val;
|
|
@@ -398,6 +402,9 @@ export function unflattenConfig(flat: Record<string, unknown>): Record<string, u
|
|
|
398
402
|
} else if (key === "CHATCCC_CODEX_MODEL") {
|
|
399
403
|
result.codex = result.codex || {};
|
|
400
404
|
(result.codex as Record<string, unknown>).model = val;
|
|
405
|
+
} else if (key === "CHATCCC_CODEX_ALTERNATIVE_MODEL") {
|
|
406
|
+
result.codex = result.codex || {};
|
|
407
|
+
(result.codex as Record<string, unknown>).alternativeModel = val;
|
|
401
408
|
} else if (key === "CHATCCC_CODEX_EFFORT") {
|
|
402
409
|
result.codex = result.codex || {};
|
|
403
410
|
(result.codex as Record<string, unknown>).effort = val;
|
|
@@ -747,6 +754,10 @@ header .badge{font-size:13px;padding:4px 12px;border-radius:12px;font-weight:500
|
|
|
747
754
|
<label>模型</label>
|
|
748
755
|
<input type="text" id="field-CHATCCC_CURSOR_MODEL" placeholder="留空表示不传 --model">
|
|
749
756
|
</div>
|
|
757
|
+
<div class="form-group">
|
|
758
|
+
<label>备选模型(选填)</label>
|
|
759
|
+
<input type="text" id="field-CHATCCC_CURSOR_ALTERNATIVE_MODEL" placeholder="加入 /model 列表,便于会话内切换">
|
|
760
|
+
</div>
|
|
750
761
|
<div class="form-group">
|
|
751
762
|
<label>头像电池电量</label>
|
|
752
763
|
<select id="field-CHATCCC_CURSOR_AVATAR_BATTERY_MODE" onchange="onCursorBatteryModeChange('field-', this.value)" style="width:100%;padding:8px 12px;border:1px solid #cbd5e1;border-radius:8px;font-size:14px;outline:none">
|
|
@@ -785,6 +796,10 @@ header .badge{font-size:13px;padding:4px 12px;border-radius:12px;font-weight:500
|
|
|
785
796
|
<label>模型</label>
|
|
786
797
|
<input type="text" id="field-CHATCCC_CODEX_MODEL" placeholder="留空由 codex config.toml 决定">
|
|
787
798
|
</div>
|
|
799
|
+
<div class="form-group">
|
|
800
|
+
<label>备选模型(选填)</label>
|
|
801
|
+
<input type="text" id="field-CHATCCC_CODEX_ALTERNATIVE_MODEL" placeholder="加入 /model 列表,便于会话内切换">
|
|
802
|
+
</div>
|
|
788
803
|
<div class="form-group">
|
|
789
804
|
<label>努力程度 (Effort)</label>
|
|
790
805
|
<input type="text" id="field-CHATCCC_CODEX_EFFORT" placeholder="留空由 codex config.toml 决定">
|
|
@@ -891,6 +906,7 @@ header .badge{font-size:13px;padding:4px 12px;border-radius:12px;font-weight:500
|
|
|
891
906
|
<div class="section-detail">
|
|
892
907
|
<div class="config-row"><span class="key">CLI 路径</span><span class="val" id="cfg-CURSOR_PATH">-</span></div>
|
|
893
908
|
<div class="config-row"><span class="key">模型</span><span class="val" id="cfg-CURSOR_MODEL">-</span></div>
|
|
909
|
+
<div class="config-row"><span class="key">备选模型</span><span class="val" id="cfg-CURSOR_ALTERNATIVE_MODEL">-</span></div>
|
|
894
910
|
<div class="config-row"><span class="key">头像电池电量</span><span class="val" id="cfg-CURSOR_AVATAR_BATTERY_MODE">-</span></div>
|
|
895
911
|
<div class="config-row" id="cfg-CURSOR_ON_DEMAND_MONTHLY_BUDGET_ROW"><span class="key">每月On demand use预算</span><span class="val" id="cfg-CURSOR_ON_DEMAND_MONTHLY_BUDGET">-</span></div>
|
|
896
912
|
<label class="agent-default-row" style="margin-top:10px"><input type="checkbox" id="dash-default-cursor" onchange="setDashboardDefaultAgent('cursor', this.checked)"> 设为默认 Agent</label>
|
|
@@ -903,6 +919,7 @@ header .badge{font-size:13px;padding:4px 12px;border-radius:12px;font-weight:500
|
|
|
903
919
|
<div class="section-detail">
|
|
904
920
|
<div class="config-row"><span class="key">CLI 路径</span><span class="val" id="cfg-CODEX_PATH">-</span></div>
|
|
905
921
|
<div class="config-row"><span class="key">模型</span><span class="val" id="cfg-CODEX_MODEL">-</span></div>
|
|
922
|
+
<div class="config-row"><span class="key">备选模型</span><span class="val" id="cfg-CODEX_ALTERNATIVE_MODEL">-</span></div>
|
|
906
923
|
<div class="config-row"><span class="key">Effort</span><span class="val" id="cfg-CODEX_EFFORT">-</span></div>
|
|
907
924
|
<label class="agent-default-row" style="margin-top:10px"><input type="checkbox" id="dash-default-codex" onchange="setDashboardDefaultAgent('codex', this.checked)"> 设为默认 Agent</label>
|
|
908
925
|
<button class="btn btn-outline" style="margin-top:8px" onclick="editSection('codex')">编辑</button>
|
|
@@ -950,8 +967,8 @@ var step2InputBound = false;
|
|
|
950
967
|
|
|
951
968
|
const AGENT_FIELDS = {
|
|
952
969
|
claude: ['CHATCCC_ANTHROPIC_MODEL','CHATCCC_ANTHROPIC_SUBAGENT_MODEL','CHATCCC_ANTHROPIC_EFFORT','CHATCCC_ANTHROPIC_API_KEY','CHATCCC_ANTHROPIC_BASE_URL','CHATCCC_ANTHROPIC_MAX_TURN'],
|
|
953
|
-
cursor: ['CHATCCC_CURSOR_PATH','CHATCCC_CURSOR_MODEL','CHATCCC_CURSOR_AVATAR_BATTERY_MODE','CHATCCC_CURSOR_ON_DEMAND_MONTHLY_BUDGET'],
|
|
954
|
-
codex: ['CHATCCC_CODEX_PATH','CHATCCC_CODEX_MODEL','CHATCCC_CODEX_EFFORT']
|
|
970
|
+
cursor: ['CHATCCC_CURSOR_PATH','CHATCCC_CURSOR_MODEL','CHATCCC_CURSOR_ALTERNATIVE_MODEL','CHATCCC_CURSOR_AVATAR_BATTERY_MODE','CHATCCC_CURSOR_ON_DEMAND_MONTHLY_BUDGET'],
|
|
971
|
+
codex: ['CHATCCC_CODEX_PATH','CHATCCC_CODEX_MODEL','CHATCCC_CODEX_ALTERNATIVE_MODEL','CHATCCC_CODEX_EFFORT']
|
|
955
972
|
};
|
|
956
973
|
const FEISHU_FIELDS = ['CHATCCC_APP_ID','CHATCCC_APP_SECRET'];
|
|
957
974
|
const CHROME_DEVTOOLS_FIELDS = ['CHATCCC_CHROME_DEVTOOLS_ENABLED','CHATCCC_CHROME_DEVTOOLS_PORT','CHATCCC_CHROME_DEVTOOLS_PATH'];
|
|
@@ -1217,8 +1234,8 @@ function isAgentEnabled(node, keys) {
|
|
|
1217
1234
|
}
|
|
1218
1235
|
|
|
1219
1236
|
var CLAUDE_FALLBACK_KEYS = ['model','subagentModel','effort','maxTurn'];
|
|
1220
|
-
var CURSOR_FALLBACK_KEYS = ['path','command','model'];
|
|
1221
|
-
var CODEX_FALLBACK_KEYS = ['path','command','model','effort'];
|
|
1237
|
+
var CURSOR_FALLBACK_KEYS = ['path','command','model','alternativeModel'];
|
|
1238
|
+
var CODEX_FALLBACK_KEYS = ['path','command','model','alternativeModel','effort'];
|
|
1222
1239
|
|
|
1223
1240
|
function renderStep2() {
|
|
1224
1241
|
var c = state.config || {};
|
|
@@ -1230,6 +1247,7 @@ function renderStep2() {
|
|
|
1230
1247
|
if (c.cursor) {
|
|
1231
1248
|
prefillNested('field-CHATCCC_CURSOR_PATH', c.cursor.path || c.cursor.command);
|
|
1232
1249
|
prefillNested('field-CHATCCC_CURSOR_MODEL', c.cursor.model);
|
|
1250
|
+
prefillNested('field-CHATCCC_CURSOR_ALTERNATIVE_MODEL', c.cursor.alternativeModel);
|
|
1233
1251
|
var cursorMode = c.cursor.avatarBatteryMode || 'apiPercent';
|
|
1234
1252
|
var cursorModeInput = document.getElementById('field-CHATCCC_CURSOR_AVATAR_BATTERY_MODE');
|
|
1235
1253
|
if (cursorModeInput) cursorModeInput.value = cursorMode;
|
|
@@ -1245,6 +1263,7 @@ function renderStep2() {
|
|
|
1245
1263
|
if (c.codex) {
|
|
1246
1264
|
prefillNested('field-CHATCCC_CODEX_PATH', c.codex.path || c.codex.command);
|
|
1247
1265
|
prefillNested('field-CHATCCC_CODEX_MODEL', c.codex.model);
|
|
1266
|
+
prefillNested('field-CHATCCC_CODEX_ALTERNATIVE_MODEL', c.codex.alternativeModel);
|
|
1248
1267
|
prefillNested('field-CHATCCC_CODEX_EFFORT', c.codex.effort);
|
|
1249
1268
|
}
|
|
1250
1269
|
|
|
@@ -1385,6 +1404,7 @@ function renderStep3() {
|
|
|
1385
1404
|
lines.push('<h4 style="margin:10px 0 4px;color:#334155">Cursor</h4>');
|
|
1386
1405
|
if (vars.CHATCCC_CURSOR_PATH) lines.push('<div class="config-row"><span class="key">CLI 路径</span><span class="val">' + vars.CHATCCC_CURSOR_PATH + '</span></div>');
|
|
1387
1406
|
lines.push('<div class="config-row"><span class="key">模型</span><span class="val">' + (vars.CHATCCC_CURSOR_MODEL || '(留空)') + '</span></div>');
|
|
1407
|
+
lines.push('<div class="config-row"><span class="key">备选模型</span><span class="val">' + (vars.CHATCCC_CURSOR_ALTERNATIVE_MODEL || '(留空)') + '</span></div>');
|
|
1388
1408
|
lines.push('<div class="config-row"><span class="key">头像电池电量</span><span class="val">' + cursorBatteryModeLabel(vars.CHATCCC_CURSOR_AVATAR_BATTERY_MODE) + '</span></div>');
|
|
1389
1409
|
if (vars.CHATCCC_CURSOR_AVATAR_BATTERY_MODE === 'onDemandUse') {
|
|
1390
1410
|
lines.push('<div class="config-row"><span class="key">每月On demand use预算</span><span class="val">' + (vars.CHATCCC_CURSOR_ON_DEMAND_MONTHLY_BUDGET || '1000') + '</span></div>');
|
|
@@ -1393,6 +1413,7 @@ function renderStep3() {
|
|
|
1393
1413
|
lines.push('<h4 style="margin:10px 0 4px;color:#334155">Codex</h4>');
|
|
1394
1414
|
if (vars.CHATCCC_CODEX_PATH) lines.push('<div class="config-row"><span class="key">CLI 路径</span><span class="val">' + vars.CHATCCC_CODEX_PATH + '</span></div>');
|
|
1395
1415
|
lines.push('<div class="config-row"><span class="key">模型</span><span class="val">' + (vars.CHATCCC_CODEX_MODEL || '(留空)') + '</span></div>');
|
|
1416
|
+
lines.push('<div class="config-row"><span class="key">备选模型</span><span class="val">' + (vars.CHATCCC_CODEX_ALTERNATIVE_MODEL || '(留空)') + '</span></div>');
|
|
1396
1417
|
lines.push('<div class="config-row"><span class="key">Effort</span><span class="val">' + (vars.CHATCCC_CODEX_EFFORT || '(留空)') + '</span></div>');
|
|
1397
1418
|
}
|
|
1398
1419
|
});
|
|
@@ -1578,6 +1599,7 @@ function updateDashboardUI() {
|
|
|
1578
1599
|
document.getElementById('cfg-ANTHROPIC_MAX_TURN').textContent = (c.claude && c.claude.maxTurn != null) ? String(c.claude.maxTurn) : '0';
|
|
1579
1600
|
document.getElementById('cfg-CURSOR_PATH').textContent = (c.cursor && (c.cursor.path || c.cursor.command)) || '-';
|
|
1580
1601
|
document.getElementById('cfg-CURSOR_MODEL').textContent = (c.cursor && c.cursor.model) || '(留空)';
|
|
1602
|
+
document.getElementById('cfg-CURSOR_ALTERNATIVE_MODEL').textContent = (c.cursor && c.cursor.alternativeModel) || '(留空)';
|
|
1581
1603
|
var cursorBatteryMode = (c.cursor && c.cursor.avatarBatteryMode) || 'apiPercent';
|
|
1582
1604
|
document.getElementById('cfg-CURSOR_AVATAR_BATTERY_MODE').textContent = cursorBatteryModeLabel(cursorBatteryMode);
|
|
1583
1605
|
var cursorBudgetRow = document.getElementById('cfg-CURSOR_ON_DEMAND_MONTHLY_BUDGET_ROW');
|
|
@@ -1585,6 +1607,7 @@ function updateDashboardUI() {
|
|
|
1585
1607
|
document.getElementById('cfg-CURSOR_ON_DEMAND_MONTHLY_BUDGET').textContent = String((c.cursor && c.cursor.onDemandMonthlyBudget) || 1000);
|
|
1586
1608
|
document.getElementById('cfg-CODEX_PATH').textContent = (c.codex && (c.codex.path || c.codex.command)) || 'codex';
|
|
1587
1609
|
document.getElementById('cfg-CODEX_MODEL').textContent = (c.codex && c.codex.model) || '(留空)';
|
|
1610
|
+
document.getElementById('cfg-CODEX_ALTERNATIVE_MODEL').textContent = (c.codex && c.codex.alternativeModel) || '(留空)';
|
|
1588
1611
|
document.getElementById('cfg-CODEX_EFFORT').textContent = (c.codex && c.codex.effort) || '(留空)';
|
|
1589
1612
|
}
|
|
1590
1613
|
|
|
@@ -1651,10 +1674,10 @@ function editSection(section) {
|
|
|
1651
1674
|
'CHATCCC_CHROME_DEVTOOLS_PATH': 'Chrome 路径(选填)',
|
|
1652
1675
|
'CHATCCC_ANTHROPIC_MODEL': '模型', 'CHATCCC_ANTHROPIC_SUBAGENT_MODEL': 'Subagent 模型', 'CHATCCC_ANTHROPIC_EFFORT': 'Effort',
|
|
1653
1676
|
'CHATCCC_ANTHROPIC_API_KEY': 'API Key', 'CHATCCC_ANTHROPIC_BASE_URL': 'Base URL', 'CHATCCC_ANTHROPIC_MAX_TURN': 'Max Turns (0=无限制)',
|
|
1654
|
-
'CHATCCC_CURSOR_PATH': 'CLI 路径', 'CHATCCC_CURSOR_MODEL': '模型',
|
|
1677
|
+
'CHATCCC_CURSOR_PATH': 'CLI 路径', 'CHATCCC_CURSOR_MODEL': '模型', 'CHATCCC_CURSOR_ALTERNATIVE_MODEL': '备选模型',
|
|
1655
1678
|
'CHATCCC_CURSOR_AVATAR_BATTERY_MODE': '头像电池电量',
|
|
1656
1679
|
'CHATCCC_CURSOR_ON_DEMAND_MONTHLY_BUDGET': '每月On demand use预算',
|
|
1657
|
-
'CHATCCC_CODEX_PATH': 'CLI 路径', 'CHATCCC_CODEX_MODEL': '模型', 'CHATCCC_CODEX_EFFORT': 'Effort'
|
|
1680
|
+
'CHATCCC_CODEX_PATH': 'CLI 路径', 'CHATCCC_CODEX_MODEL': '模型', 'CHATCCC_CODEX_ALTERNATIVE_MODEL': '备选模型', 'CHATCCC_CODEX_EFFORT': 'Effort'
|
|
1658
1681
|
};
|
|
1659
1682
|
var hintMap = {
|
|
1660
1683
|
'CHATCCC_CHROME_DEVTOOLS_ENABLED': '依赖:本机 Google Chrome;ChatGPT 订阅到期查询需要在该 CDP Chrome 中登录 ChatGPT。',
|
|
@@ -1687,11 +1710,13 @@ function editSection(section) {
|
|
|
1687
1710
|
} else if (section === 'cursor' && state.config.cursor) {
|
|
1688
1711
|
if (key === 'CHATCCC_CURSOR_PATH') val = state.config.cursor.path || state.config.cursor.command || '';
|
|
1689
1712
|
else if (key === 'CHATCCC_CURSOR_MODEL') val = state.config.cursor.model || '';
|
|
1713
|
+
else if (key === 'CHATCCC_CURSOR_ALTERNATIVE_MODEL') val = state.config.cursor.alternativeModel || '';
|
|
1690
1714
|
else if (key === 'CHATCCC_CURSOR_AVATAR_BATTERY_MODE') val = state.config.cursor.avatarBatteryMode || 'apiPercent';
|
|
1691
1715
|
else if (key === 'CHATCCC_CURSOR_ON_DEMAND_MONTHLY_BUDGET') val = (state.config.cursor.onDemandMonthlyBudget != null) ? String(state.config.cursor.onDemandMonthlyBudget) : '1000';
|
|
1692
1716
|
} else if (section === 'codex' && state.config.codex) {
|
|
1693
1717
|
if (key === 'CHATCCC_CODEX_PATH') val = state.config.codex.path || state.config.codex.command || '';
|
|
1694
1718
|
else if (key === 'CHATCCC_CODEX_MODEL') val = state.config.codex.model || '';
|
|
1719
|
+
else if (key === 'CHATCCC_CODEX_ALTERNATIVE_MODEL') val = state.config.codex.alternativeModel || '';
|
|
1695
1720
|
else if (key === 'CHATCCC_CODEX_EFFORT') val = state.config.codex.effort || '';
|
|
1696
1721
|
}
|
|
1697
1722
|
}
|