chatccc 0.2.223 → 0.2.225
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 +40 -39
- package/package.json +1 -1
- package/src/__tests__/builtin-chat-session.test.ts +33 -0
- package/src/__tests__/builtin-config.test.ts +1 -0
- package/src/__tests__/ccc-adapter.test.ts +20 -0
- package/src/__tests__/config-reload.test.ts +52 -52
- package/src/__tests__/config-sample.test.ts +41 -40
- package/src/__tests__/orchestrator.test.ts +836 -803
- package/src/__tests__/session.test.ts +1183 -1173
- package/src/__tests__/terminal-renderer.test.ts +104 -0
- package/src/__tests__/web-ui.test.ts +3 -0
- package/src/adapters/ccc-adapter.ts +1 -0
- package/src/builtin/cli.ts +63 -17
- package/src/builtin/index.ts +10 -0
- package/src/config.ts +203 -192
- package/src/progress/terminal-renderer.ts +112 -8
- package/src/session.ts +5 -2
- package/src/web-ui.ts +626 -607
|
@@ -2,6 +2,8 @@ import { afterEach, describe, expect, it, vi } from "vitest";
|
|
|
2
2
|
|
|
3
3
|
import {
|
|
4
4
|
buildBlockLines,
|
|
5
|
+
charWidth,
|
|
6
|
+
clipToWidth,
|
|
5
7
|
TerminalProgressRenderer,
|
|
6
8
|
} from "../progress/terminal-renderer.ts";
|
|
7
9
|
import { progressView } from "../progress/view.ts";
|
|
@@ -41,6 +43,7 @@ describe("buildBlockLines", () => {
|
|
|
41
43
|
it("renders done / stopped / error status lines", () => {
|
|
42
44
|
const done = buildBlockLines(progressView({ status: "done", text: "ok" }), 100);
|
|
43
45
|
expect(done[0]).toContain("✅ 完成");
|
|
46
|
+
expect(done[0]).toContain("\x1b[1m"); // 完成状态行加粗,不用浅色
|
|
44
47
|
const stopped = buildBlockLines(progressView({ status: "stopped", text: "x" }), 100);
|
|
45
48
|
expect(stopped[0]).toContain("⏹ 已停止");
|
|
46
49
|
const error = buildBlockLines(progressView({ status: "error", text: "x" }), 100);
|
|
@@ -66,6 +69,10 @@ describe("buildBlockLines", () => {
|
|
|
66
69
|
expect(lines[2]).toContain("npm test passed");
|
|
67
70
|
expect(lines[3]).toContain("✗");
|
|
68
71
|
expect(lines[3]).toContain("regex error");
|
|
72
|
+
// 工具行摘要不用浅色字体(回复内容可读性优先)
|
|
73
|
+
for (const line of [lines[1], lines[2], lines[3]]) {
|
|
74
|
+
expect(line).not.toContain("\x1b[2m");
|
|
75
|
+
}
|
|
69
76
|
});
|
|
70
77
|
|
|
71
78
|
it("truncates long body to maxBodyLines", () => {
|
|
@@ -84,6 +91,46 @@ describe("buildBlockLines", () => {
|
|
|
84
91
|
expect(line.length).toBeLessThanOrEqual(30);
|
|
85
92
|
}
|
|
86
93
|
});
|
|
94
|
+
|
|
95
|
+
it("charWidth counts CJK/emoji as 2 columns and control/ZWJ as 0", () => {
|
|
96
|
+
expect(charWidth("a")).toBe(1);
|
|
97
|
+
expect(charWidth("中")).toBe(2);
|
|
98
|
+
expect(charWidth("📂")).toBe(2);
|
|
99
|
+
expect(charWidth("\u200d")).toBe(0); // ZWJ
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
it("clipToWidth truncates by display width, not string length", () => {
|
|
103
|
+
expect(clipToWidth("中文abc", 4)).toBe("中文");
|
|
104
|
+
expect(clipToWidth("中文abc", 5)).toBe("中文a");
|
|
105
|
+
expect(clipToWidth("📂 x", 2)).toBe("📂");
|
|
106
|
+
expect(clipToWidth("📂 x", 3)).toBe("📂 ");
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
it("clipToWidth keeps ANSI tokens whole and closes colors at the cut", () => {
|
|
110
|
+
const ansi = "\x1b[32m📂\x1b[0m abc";
|
|
111
|
+
expect(clipToWidth(ansi, 6)).toBe("\x1b[32m📂\x1b[0m abc");
|
|
112
|
+
expect(clipToWidth(ansi, 5)).toBe("\x1b[32m📂\x1b[0m ab");
|
|
113
|
+
// 截断发生在未闭合颜色内时自动补 reset,避免颜色泄漏
|
|
114
|
+
expect(clipToWidth("\x1b[1m\x1b[32m✅ 完成\x1b[0m", 6)).toBe("\x1b[1m\x1b[32m✅ 完\x1b[0m");
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
it("emoji tool lines are clipped by display width so the block never wraps", () => {
|
|
118
|
+
// 模拟用户示例:emoji + 超长 JSON 工具行,宽 40 的终端必须整行不折行
|
|
119
|
+
const lines = buildBlockLines(
|
|
120
|
+
progressView({
|
|
121
|
+
text: "",
|
|
122
|
+
tools: [
|
|
123
|
+
{ id: "t1", name: "list_dir", status: "ok", summary: `{"path":"C:\\Users\\weizhangjian\\.chatccc","entries":[{"name":"builtin","path":"C:\\Users${'x'.repeat(300)}"}]}` },
|
|
124
|
+
],
|
|
125
|
+
}),
|
|
126
|
+
40,
|
|
127
|
+
);
|
|
128
|
+
const toolLine = lines.find((l) => l.includes("list_dir"))!;
|
|
129
|
+
// 显示宽度(剥离 ANSI 后按 charWidth 计算)不超过终端列宽,永不折行
|
|
130
|
+
const visible = [...toolLine.replace(/\x1b\[[0-9;]*m/g, "")].reduce((w, ch) => w + charWidth(ch), 0);
|
|
131
|
+
expect(visible).toBeLessThanOrEqual(40);
|
|
132
|
+
expect(toolLine).toContain("\x1b[0m"); // ANSI 完整闭合
|
|
133
|
+
});
|
|
87
134
|
});
|
|
88
135
|
|
|
89
136
|
describe("TerminalProgressRenderer", () => {
|
|
@@ -93,6 +140,7 @@ describe("TerminalProgressRenderer", () => {
|
|
|
93
140
|
r.begin(progressView({ text: "hello" }));
|
|
94
141
|
expect(out.output.startsWith("\x1b[?25l")).toBe(true);
|
|
95
142
|
expect(out.output).toContain("hello");
|
|
143
|
+
r.dispose();
|
|
96
144
|
});
|
|
97
145
|
|
|
98
146
|
it("end finalizes the block and restores cursor, keeping block on screen", () => {
|
|
@@ -140,4 +188,60 @@ describe("TerminalProgressRenderer", () => {
|
|
|
140
188
|
expect(out.output).toContain("\x1b[2K\n");
|
|
141
189
|
expect(out.output).toContain("\x1b[J");
|
|
142
190
|
});
|
|
191
|
+
|
|
192
|
+
it("heartbeat animates generating dots so the block never freezes", () => {
|
|
193
|
+
vi.useFakeTimers();
|
|
194
|
+
const out = new FakeOut();
|
|
195
|
+
const r = new TerminalProgressRenderer({ out: asOut(out), animMs: 100 });
|
|
196
|
+
r.begin(progressView({ headerTitle: "生成中...", text: "" }));
|
|
197
|
+
const frame0 = out.output;
|
|
198
|
+
expect(frame0).toContain("·"); // 首帧即带动画点
|
|
199
|
+
vi.advanceTimersByTime(100);
|
|
200
|
+
const frame1 = out.output;
|
|
201
|
+
expect(frame1).not.toBe(frame0); // 无新事件也持续重绘
|
|
202
|
+
expect(frame1).toContain("··"); // 第二帧点号增长
|
|
203
|
+
r.dispose();
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
it("stops the heartbeat after end so the final block stays static", () => {
|
|
207
|
+
vi.useFakeTimers();
|
|
208
|
+
const out = new FakeOut();
|
|
209
|
+
const r = new TerminalProgressRenderer({ out: asOut(out), animMs: 100 });
|
|
210
|
+
r.begin(progressView({ text: "" }));
|
|
211
|
+
r.end(progressView({ status: "done", text: "final" }));
|
|
212
|
+
const afterEnd = out.output.length;
|
|
213
|
+
vi.advanceTimersByTime(500);
|
|
214
|
+
expect(out.output.length).toBe(afterEnd);
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
it("redraw moves up exactly blockLines-1 rows, never eating history above", () => {
|
|
218
|
+
// 回归测试:光标在区块最后一行时,回到第一行只需上移 N-1 行。
|
|
219
|
+
// 上移 N 行会每帧多上移 1 行,把上方历史内容逐行吃掉。
|
|
220
|
+
const out = new FakeOut();
|
|
221
|
+
const r = new TerminalProgressRenderer({ out: asOut(out), animMs: 0 });
|
|
222
|
+
// 状态行 1 行 + 正文 4 行 = 5 行区块
|
|
223
|
+
r.begin(progressView({ text: "a\nb\nc\nd" }));
|
|
224
|
+
const afterBegin = out.output;
|
|
225
|
+
r.flush(); // 第二帧强制重绘
|
|
226
|
+
const delta = out.output.slice(afterBegin.length);
|
|
227
|
+
expect(delta).toContain("\x1b[4A"); // 上移 5-1=4 行
|
|
228
|
+
expect(delta).not.toContain("\x1b[5A"); // 绝不出现上移 N 行
|
|
229
|
+
r.dispose();
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
it("multi-frame redraws keep moving up N-1 rows each time (no drift)", () => {
|
|
233
|
+
const out = new FakeOut();
|
|
234
|
+
const r = new TerminalProgressRenderer({ out: asOut(out), animMs: 0 });
|
|
235
|
+
r.begin(progressView({ text: "a\nb\nc\nd" }));
|
|
236
|
+
for (let i = 0; i < 3; i++) {
|
|
237
|
+
r.flush();
|
|
238
|
+
}
|
|
239
|
+
// 连续多帧重绘,每帧上移都是 4 行(5 行区块),不是 5/6/7... 逐帧递增
|
|
240
|
+
const moves = out.output.match(/\x1b\[(\d+)A/g) ?? [];
|
|
241
|
+
expect(moves.length).toBe(3);
|
|
242
|
+
for (const move of moves) {
|
|
243
|
+
expect(move).toBe("\x1b[4A");
|
|
244
|
+
}
|
|
245
|
+
r.dispose();
|
|
246
|
+
});
|
|
143
247
|
});
|
|
@@ -95,6 +95,7 @@ describe("unflattenConfig", () => {
|
|
|
95
95
|
CHATCCC_CCC_BASE_URL: "https://api.deepseek.com/v1",
|
|
96
96
|
CHATCCC_CCC_MODEL: "deepseek-v4-flash",
|
|
97
97
|
CHATCCC_CCC_ALTERNATIVE_MODEL: "deepseek-v4-pro",
|
|
98
|
+
CHATCCC_CCC_EFFORT: "max",
|
|
98
99
|
}),
|
|
99
100
|
).toEqual({
|
|
100
101
|
ccc: {
|
|
@@ -104,6 +105,7 @@ describe("unflattenConfig", () => {
|
|
|
104
105
|
DEEPSEEK_BASE_URL: "https://api.deepseek.com/v1",
|
|
105
106
|
model: "deepseek-v4-flash",
|
|
106
107
|
alternativeModel: "deepseek-v4-pro",
|
|
108
|
+
effort: "max",
|
|
107
109
|
},
|
|
108
110
|
});
|
|
109
111
|
});
|
|
@@ -251,6 +253,7 @@ describe("dashboard edit modal", () => {
|
|
|
251
253
|
expect(PAGE_HTML).toContain('id="agent-default-ccc"');
|
|
252
254
|
expect(PAGE_HTML).toContain('id="field-CHATCCC_CCC_MODEL"');
|
|
253
255
|
expect(PAGE_HTML).toContain('id="field-CHATCCC_CCC_ALTERNATIVE_MODEL"');
|
|
256
|
+
expect(PAGE_HTML).toContain('id="field-CHATCCC_CCC_EFFORT"');
|
|
254
257
|
expect(PAGE_HTML).toContain('id="dash-ccc"');
|
|
255
258
|
expect(PAGE_HTML).toContain("editSection('ccc')");
|
|
256
259
|
});
|
|
@@ -41,6 +41,7 @@ export function createCccAdapter(options: CccAdapterOptions = {}): ToolAdapter {
|
|
|
41
41
|
...(options.apiKey !== undefined ? { apiKey: options.apiKey } : {}),
|
|
42
42
|
...(options.baseURL !== undefined ? { baseURL: options.baseURL } : {}),
|
|
43
43
|
...(options.model !== undefined ? { model: options.model } : {}),
|
|
44
|
+
...(options.effort !== undefined ? { effort: options.effort } : {}),
|
|
44
45
|
};
|
|
45
46
|
|
|
46
47
|
return {
|
package/src/builtin/cli.ts
CHANGED
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
|
|
14
14
|
import * as readline from "node:readline";
|
|
15
15
|
import * as process from "node:process";
|
|
16
|
+
import { appendFileSync } from "node:fs";
|
|
16
17
|
import { resolve as resolvePath } from "node:path";
|
|
17
18
|
import { fileURLToPath } from "node:url";
|
|
18
19
|
|
|
@@ -32,11 +33,14 @@ interface ParsedArgs {
|
|
|
32
33
|
help: boolean;
|
|
33
34
|
streamJson: boolean;
|
|
34
35
|
prompt: string | null;
|
|
36
|
+
/** 强制纯文本流式输出(不用过程区块渲染器),渲染异常时的兜底通道 */
|
|
37
|
+
plain: boolean;
|
|
35
38
|
}
|
|
36
39
|
|
|
37
40
|
interface RuntimeDeps {
|
|
38
41
|
ChatSession: typeof import("./index.js").ChatSession;
|
|
39
42
|
appConfig: typeof import("../config.ts").config;
|
|
43
|
+
fileLog: typeof import("../config.ts").fileLog;
|
|
40
44
|
}
|
|
41
45
|
|
|
42
46
|
interface JsonLine {
|
|
@@ -60,6 +64,7 @@ function parseArgs(argv = process.argv.slice(2)): ParsedArgs {
|
|
|
60
64
|
let help = false;
|
|
61
65
|
let streamJson = false;
|
|
62
66
|
let prompt: string | null = null;
|
|
67
|
+
let plain = false;
|
|
63
68
|
|
|
64
69
|
for (let i = 0; i < argv.length; i++) {
|
|
65
70
|
const arg = argv[i];
|
|
@@ -67,6 +72,9 @@ function parseArgs(argv = process.argv.slice(2)): ParsedArgs {
|
|
|
67
72
|
if (arg === "--model" && next !== undefined) {
|
|
68
73
|
config.model = next;
|
|
69
74
|
i++;
|
|
75
|
+
} else if (arg === "--effort" && next !== undefined) {
|
|
76
|
+
config.effort = next;
|
|
77
|
+
i++;
|
|
70
78
|
} else if (arg === "--base-url" && next !== undefined) {
|
|
71
79
|
config.baseURL = next;
|
|
72
80
|
i++;
|
|
@@ -93,20 +101,22 @@ function parseArgs(argv = process.argv.slice(2)): ParsedArgs {
|
|
|
93
101
|
} else if (arg === "--prompt" && next !== undefined) {
|
|
94
102
|
prompt = next;
|
|
95
103
|
i++;
|
|
104
|
+
} else if (arg === "--plain") {
|
|
105
|
+
plain = true;
|
|
96
106
|
} else if (arg === "--help" || arg === "-h") {
|
|
97
107
|
help = true;
|
|
98
108
|
}
|
|
99
109
|
}
|
|
100
110
|
|
|
101
|
-
return { config, options, listSessions, resume, help, streamJson, prompt };
|
|
111
|
+
return { config, options, listSessions, resume, help, streamJson, prompt, plain };
|
|
102
112
|
}
|
|
103
113
|
|
|
104
114
|
async function loadRuntime(): Promise<RuntimeDeps> {
|
|
105
|
-
const [{ ChatSession }, { config: appConfig }] = await Promise.all([
|
|
115
|
+
const [{ ChatSession }, { config: appConfig, fileLog }] = await Promise.all([
|
|
106
116
|
import("./index.js"),
|
|
107
117
|
import("../config.ts"),
|
|
108
118
|
]);
|
|
109
|
-
return { ChatSession, appConfig };
|
|
119
|
+
return { ChatSession, appConfig, fileLog };
|
|
110
120
|
}
|
|
111
121
|
|
|
112
122
|
function printHelp(appConfig: RuntimeDeps["appConfig"]): void {
|
|
@@ -117,6 +127,7 @@ function printHelp(appConfig: RuntimeDeps["appConfig"]): void {
|
|
|
117
127
|
"",
|
|
118
128
|
"Options:",
|
|
119
129
|
` --model <name> Model name (overrides config.ccc.model, current default ${appConfig.ccc.model})`,
|
|
130
|
+
` --effort <level> Reasoning effort: none/minimal/low/medium/high/xhigh/max (overrides config.ccc.effort)`,
|
|
120
131
|
` --base-url <url> API base URL (current default ${appConfig.ccc.DEEPSEEK_BASE_URL})`,
|
|
121
132
|
" --api-key <key> API key (overrides config.ccc.DEEPSEEK_API_KEY)",
|
|
122
133
|
" --cwd <path> Working directory",
|
|
@@ -125,6 +136,7 @@ function printHelp(appConfig: RuntimeDeps["appConfig"]): void {
|
|
|
125
136
|
" --list-sessions List saved ccc sessions and exit",
|
|
126
137
|
" --stream-json One-shot mode: write JSONL events to stdout",
|
|
127
138
|
" --prompt <text> Prompt text for --stream-json",
|
|
139
|
+
" --plain Force plain streaming output (no progress block renderer)",
|
|
128
140
|
" --help, -h Show help",
|
|
129
141
|
"",
|
|
130
142
|
"Default config source:",
|
|
@@ -307,8 +319,32 @@ const C = {
|
|
|
307
319
|
yellow: "\x1b[33m",
|
|
308
320
|
};
|
|
309
321
|
|
|
322
|
+
/**
|
|
323
|
+
* 交互模式下渲染器独占终端 stdout:普通日志只写日志文件、不回显到终端,
|
|
324
|
+
* 避免生成过程中任何 console 输出混入过程区块、破坏行数计数(导致重绘
|
|
325
|
+
* 上移不足、把上方历史内容"吃掉")。错误提示(console.error)保留回显。
|
|
326
|
+
*/
|
|
327
|
+
function muteConsoleLogToFile(logPath: string): void {
|
|
328
|
+
const writeFile = (level: string, args: unknown[]): void => {
|
|
329
|
+
try {
|
|
330
|
+
const text = args
|
|
331
|
+
.map((a) =>
|
|
332
|
+
typeof a === "string" ? a
|
|
333
|
+
: a instanceof Error ? (a.stack ?? a.message)
|
|
334
|
+
: JSON.stringify(a))
|
|
335
|
+
.join(" ");
|
|
336
|
+
appendFileSync(logPath, `[${new Date().toISOString()}] [${level}] ${text}\n`, "utf8");
|
|
337
|
+
} catch {
|
|
338
|
+
// 日志系统自身失败不影响主流程
|
|
339
|
+
}
|
|
340
|
+
};
|
|
341
|
+
console.log = (...args: unknown[]) => writeFile("LOG", args);
|
|
342
|
+
console.info = (...args: unknown[]) => writeFile("INFO", args);
|
|
343
|
+
console.warn = (...args: unknown[]) => writeFile("WARN", args);
|
|
344
|
+
}
|
|
345
|
+
|
|
310
346
|
async function runRepl(args: ParsedArgs): Promise<void> {
|
|
311
|
-
const { ChatSession, appConfig } = await loadRuntime();
|
|
347
|
+
const { ChatSession, appConfig, fileLog } = await loadRuntime();
|
|
312
348
|
|
|
313
349
|
const cwd = resolvePath(args.options.cwd ?? process.cwd());
|
|
314
350
|
let resolvedSession;
|
|
@@ -326,6 +362,12 @@ async function runRepl(args: ParsedArgs): Promise<void> {
|
|
|
326
362
|
console.log(`${C.dim}Type a message to chat. Double Ctrl+C interrupts generation or exits. Type exit to quit.${C.reset}`);
|
|
327
363
|
console.log("");
|
|
328
364
|
|
|
365
|
+
// 交互渲染模式下 console 输出只写日志文件、不回显到终端(渲染器独占 stdout,
|
|
366
|
+
// 避免生成中日志混入区块破坏行数)。--plain 无渲染器,不需要静音。
|
|
367
|
+
if (process.stdout.isTTY === true && !args.plain) {
|
|
368
|
+
muteConsoleLogToFile(fileLog.logPath);
|
|
369
|
+
}
|
|
370
|
+
|
|
329
371
|
let session: InstanceType<typeof ChatSession>;
|
|
330
372
|
try {
|
|
331
373
|
session = new ChatSession(args.config, {
|
|
@@ -359,20 +401,20 @@ async function runRepl(args: ParsedArgs): Promise<void> {
|
|
|
359
401
|
}
|
|
360
402
|
|
|
361
403
|
if (input === "exit") {
|
|
362
|
-
|
|
404
|
+
process.stdout.write(`${C.dim}bye${C.reset}\n`);
|
|
363
405
|
rl.close();
|
|
364
406
|
return;
|
|
365
407
|
}
|
|
366
408
|
|
|
367
409
|
if (input === "/clear") {
|
|
368
410
|
session.reset();
|
|
369
|
-
|
|
411
|
+
process.stdout.write(`${C.dim}session cleared${C.reset}\n`);
|
|
370
412
|
rl.prompt();
|
|
371
413
|
return;
|
|
372
414
|
}
|
|
373
415
|
|
|
374
416
|
if (input === "/history") {
|
|
375
|
-
|
|
417
|
+
process.stdout.write(`${C.dim}${session.turnCount} conversation turns${C.reset}\n`);
|
|
376
418
|
rl.prompt();
|
|
377
419
|
return;
|
|
378
420
|
}
|
|
@@ -381,12 +423,16 @@ async function runRepl(args: ParsedArgs): Promise<void> {
|
|
|
381
423
|
const signal = currentAbort.signal;
|
|
382
424
|
|
|
383
425
|
// TTY 下用"过程区块"(飞书过程卡片的终端形态):固定区域原地更新、
|
|
384
|
-
// 工具调用折叠为单行;非 TTY(管道/CI
|
|
385
|
-
const useTerminalBlock = process.stdout.isTTY === true;
|
|
426
|
+
// 工具调用折叠为单行;非 TTY(管道/CI)或 --plain 回退为纯文本流式输出。
|
|
427
|
+
const useTerminalBlock = process.stdout.isTTY === true && !args.plain;
|
|
386
428
|
const renderer = useTerminalBlock ? new TerminalProgressRenderer() : null;
|
|
387
429
|
let view: ProgressView | null = null;
|
|
388
430
|
if (renderer) {
|
|
389
431
|
view = progressView({ headerTitle: "生成中..." });
|
|
432
|
+
// 先回行首换行再 begin:让过程区块从输入行下方开始,避免首帧 \r\x1b[2K
|
|
433
|
+
// 清掉用户刚输入的问题行(历史文本不被刷掉)。\r\n 兼容 readline
|
|
434
|
+
// 行提交后光标仍停在输入行行尾的情况。
|
|
435
|
+
process.stdout.write("\r\n");
|
|
390
436
|
renderer.begin(view);
|
|
391
437
|
}
|
|
392
438
|
let rendererEnded = false;
|
|
@@ -425,14 +471,14 @@ async function runRepl(args: ParsedArgs): Promise<void> {
|
|
|
425
471
|
view = progressView({ ...view, status: aborted ? "stopped" : "error", showStop: false });
|
|
426
472
|
renderer.end(view);
|
|
427
473
|
rendererEnded = true;
|
|
428
|
-
|
|
474
|
+
process.stdout.write("\n");
|
|
429
475
|
}
|
|
430
|
-
console.
|
|
476
|
+
console.error(`\n${C.yellow}[error] ${(err as Error).message}${C.reset}`);
|
|
431
477
|
} finally {
|
|
432
478
|
if (renderer && view && !rendererEnded) {
|
|
433
479
|
// 定型终态区块(完成/已停止/异常结束)留在屏幕上,恢复光标
|
|
434
480
|
renderer.end(view);
|
|
435
|
-
|
|
481
|
+
process.stdout.write("\n");
|
|
436
482
|
}
|
|
437
483
|
currentAbort = null;
|
|
438
484
|
ctrlCState.reset();
|
|
@@ -445,31 +491,31 @@ async function runRepl(args: ParsedArgs): Promise<void> {
|
|
|
445
491
|
const action = ctrlCState.press(currentAbort !== null);
|
|
446
492
|
|
|
447
493
|
if (action === "exit") {
|
|
448
|
-
console.
|
|
494
|
+
console.error(`\n${C.dim}bye${C.reset}`);
|
|
449
495
|
rl.close();
|
|
450
496
|
return;
|
|
451
497
|
}
|
|
452
498
|
|
|
453
499
|
if (action === "interrupt") {
|
|
454
|
-
console.
|
|
500
|
+
console.error(`\n${C.yellow}[interrupting...]${C.reset}`);
|
|
455
501
|
currentAbort?.abort();
|
|
456
502
|
currentAbort = null;
|
|
457
503
|
return;
|
|
458
504
|
}
|
|
459
505
|
|
|
460
506
|
if (action === "arm-interrupt") {
|
|
461
|
-
console.
|
|
507
|
+
console.error(`\n${C.dim}Press Ctrl+C again to interrupt current response${C.reset}`);
|
|
462
508
|
return;
|
|
463
509
|
}
|
|
464
510
|
|
|
465
511
|
if (action === "arm-exit") {
|
|
466
|
-
console.
|
|
512
|
+
console.error(`\n${C.dim}Press Ctrl+C again to exit, or type exit${C.reset}`);
|
|
467
513
|
rl.prompt();
|
|
468
514
|
}
|
|
469
515
|
});
|
|
470
516
|
|
|
471
517
|
rl.on("close", () => {
|
|
472
|
-
|
|
518
|
+
process.stdout.write("\n");
|
|
473
519
|
process.exit(0);
|
|
474
520
|
});
|
|
475
521
|
}
|
package/src/builtin/index.ts
CHANGED
|
@@ -100,6 +100,11 @@ export interface ChatSessionConfig {
|
|
|
100
100
|
apiKey?: string;
|
|
101
101
|
/** 模型名称;传入时覆盖 config.ccc.model */
|
|
102
102
|
model?: string;
|
|
103
|
+
/**
|
|
104
|
+
* Reasoning effort(none/minimal/low/medium/high/xhigh/max);
|
|
105
|
+
* 传入时覆盖 config.ccc.effort,留空不传 reasoning_effort 请求字段。
|
|
106
|
+
*/
|
|
107
|
+
effort?: string;
|
|
103
108
|
}
|
|
104
109
|
|
|
105
110
|
export interface ChatSessionOptions {
|
|
@@ -156,6 +161,7 @@ export class ChatSession {
|
|
|
156
161
|
private cwd: string;
|
|
157
162
|
private context: BuiltinContextManager;
|
|
158
163
|
private maxSteps?: number;
|
|
164
|
+
private effort: string;
|
|
159
165
|
|
|
160
166
|
constructor(
|
|
161
167
|
overrides: ChatSessionConfig = {},
|
|
@@ -170,6 +176,7 @@ export class ChatSession {
|
|
|
170
176
|
|
|
171
177
|
const baseURL = overrides.baseURL ?? appConfig.ccc.DEEPSEEK_BASE_URL;
|
|
172
178
|
const modelId = overrides.model ?? appConfig.ccc.model;
|
|
179
|
+
this.effort = (overrides.effort ?? appConfig.ccc.effort ?? "").trim();
|
|
173
180
|
|
|
174
181
|
const provider = createOpenAICompatible({
|
|
175
182
|
name: "deepseek",
|
|
@@ -260,6 +267,9 @@ export class ChatSession {
|
|
|
260
267
|
tools: createBuiltinFileTools(this.cwd),
|
|
261
268
|
stopWhen: maxSteps !== undefined ? stepCountIs(maxSteps) : isLoopFinished(),
|
|
262
269
|
abortSignal: signal,
|
|
270
|
+
// DeepSeek OpenAI 兼容接口:providerOptions.deepseek.reasoningEffort
|
|
271
|
+
// 由 @ai-sdk/openai-compatible 自动映射为请求体 reasoning_effort 字段
|
|
272
|
+
...(this.effort ? { providerOptions: { deepseek: { reasoningEffort: this.effort } } } : {}),
|
|
263
273
|
});
|
|
264
274
|
|
|
265
275
|
const stream = result.fullStream ?? textStreamToFullStream(result.textStream);
|