chatccc 0.2.242 → 0.2.243
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +5 -5
- package/bin/cccagent.mjs +17 -17
- package/deepccc-agent/bin/deepccc.mjs +26 -26
- package/deepccc-agent/package.json +62 -62
- package/deepccc-agent/src/__tests__/chat-session.test.ts +578 -522
- package/deepccc-agent/src/__tests__/cli-json.test.ts +49 -49
- package/deepccc-agent/src/__tests__/config.test.ts +26 -26
- package/deepccc-agent/src/__tests__/context.test.ts +319 -319
- package/deepccc-agent/src/__tests__/file-tools.test.ts +240 -240
- package/deepccc-agent/src/__tests__/permissions.test.ts +195 -195
- package/deepccc-agent/src/__tests__/progress-reducer.test.ts +121 -121
- package/deepccc-agent/src/__tests__/session-search.test.ts +262 -262
- package/deepccc-agent/src/__tests__/session-select.test.ts +116 -116
- package/deepccc-agent/src/__tests__/sigint.test.ts +56 -56
- package/deepccc-agent/src/__tests__/skills.test.ts +284 -284
- package/deepccc-agent/src/__tests__/terminal-renderer.test.ts +247 -247
- package/deepccc-agent/src/__tests__/web-tools.test.ts +220 -220
- package/deepccc-agent/src/config.ts +84 -84
- package/deepccc-agent/src/context.ts +465 -465
- package/deepccc-agent/src/file-log.ts +38 -38
- package/deepccc-agent/src/index.ts +22 -0
- package/deepccc-agent/src/proc-tree-kill.ts +61 -61
- package/deepccc-agent/src/progress/cards-helpers.ts +76 -76
- package/deepccc-agent/src/progress/reducer.ts +113 -113
- package/deepccc-agent/src/progress/terminal-renderer.ts +294 -294
- package/deepccc-agent/src/progress/view.ts +77 -77
- package/deepccc-agent/src/raw-stream-log.ts +124 -124
- package/deepccc-agent/src/session-search.ts +370 -370
- package/deepccc-agent/src/session-select.ts +48 -48
- package/deepccc-agent/src/sigint.ts +50 -50
- package/deepccc-agent/src/skills.ts +205 -205
- package/deepccc-agent/src/web-tools.ts +313 -313
- package/deepccc-agent/tsconfig.build.json +13 -13
- package/deepccc-agent/tsconfig.json +13 -13
- package/deepccc-agent/vitest.config.ts +7 -7
- package/package.json +1 -1
- package/src/__tests__/builtin-chat-session.test.ts +522 -522
- package/src/__tests__/builtin-config.test.ts +26 -26
- package/src/__tests__/builtin-context.test.ts +319 -319
- package/src/__tests__/builtin-file-tools.test.ts +240 -240
- package/src/__tests__/builtin-permissions.test.ts +211 -211
- package/src/__tests__/builtin-session-search.test.ts +262 -262
- package/src/__tests__/builtin-session-select.test.ts +116 -116
- package/src/__tests__/builtin-sigint.test.ts +56 -56
- package/src/__tests__/builtin-skills.test.ts +284 -284
- package/src/__tests__/builtin-web-tools.test.ts +220 -220
- package/src/__tests__/config.test.ts +17 -17
- package/src/__tests__/progress-reducer.test.ts +121 -121
- package/src/__tests__/session-ccc-config.test.ts +45 -45
- package/src/__tests__/session.test.ts +298 -298
- package/src/adapters/ccc-adapter.ts +145 -145
- package/src/config-utils.ts +13 -13
- package/src/config.ts +13 -13
- package/src/progress/reducer.ts +113 -113
- package/src/session-chat-binding.ts +83 -83
- package/src/session.ts +311 -311
|
@@ -1,38 +1,38 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* file-log.ts — DeepCCC 文件日志(写入 ~/.deepccc/logs/)
|
|
3
|
-
*
|
|
4
|
-
* 交互渲染模式下渲染器独占终端 stdout:普通 console 日志只写文件、不回显
|
|
5
|
-
* 到终端,避免生成过程中任何 console 输出混入过程区块、破坏行数计数
|
|
6
|
-
* (导致重绘上移不足、把上方历史内容"吃掉")。
|
|
7
|
-
*/
|
|
8
|
-
|
|
9
|
-
import { appendFileSync, mkdirSync } from "node:fs";
|
|
10
|
-
import { homedir } from "node:os";
|
|
11
|
-
import { join } from "node:path";
|
|
12
|
-
|
|
13
|
-
export function setupFileLogging(logDir: string, prefix: string): { logPath: string } {
|
|
14
|
-
mkdirSync(logDir, { recursive: true });
|
|
15
|
-
const ts = new Date().toISOString().replace(/[:.]/g, "-").slice(0, 19);
|
|
16
|
-
const logPath = join(logDir, `${prefix}-${ts}.log`);
|
|
17
|
-
appendFileSync(logPath, "", { flag: "a", encoding: "utf8" });
|
|
18
|
-
return { logPath };
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
/** 默认日志目录:~/.deepccc/logs */
|
|
22
|
-
export function defaultLogDir(): string {
|
|
23
|
-
return join(homedir(), ".deepccc", "logs");
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
export function writeLogLine(logPath: string, level: string, args: unknown[]): void {
|
|
27
|
-
try {
|
|
28
|
-
const text = args
|
|
29
|
-
.map((a) =>
|
|
30
|
-
typeof a === "string" ? a
|
|
31
|
-
: a instanceof Error ? (a.stack ?? a.message)
|
|
32
|
-
: JSON.stringify(a))
|
|
33
|
-
.join(" ");
|
|
34
|
-
appendFileSync(logPath, `[${new Date().toISOString()}] [${level}] ${text}\n`, "utf8");
|
|
35
|
-
} catch {
|
|
36
|
-
// 日志系统自身失败不影响主流程
|
|
37
|
-
}
|
|
38
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* file-log.ts — DeepCCC 文件日志(写入 ~/.deepccc/logs/)
|
|
3
|
+
*
|
|
4
|
+
* 交互渲染模式下渲染器独占终端 stdout:普通 console 日志只写文件、不回显
|
|
5
|
+
* 到终端,避免生成过程中任何 console 输出混入过程区块、破坏行数计数
|
|
6
|
+
* (导致重绘上移不足、把上方历史内容"吃掉")。
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { appendFileSync, mkdirSync } from "node:fs";
|
|
10
|
+
import { homedir } from "node:os";
|
|
11
|
+
import { join } from "node:path";
|
|
12
|
+
|
|
13
|
+
export function setupFileLogging(logDir: string, prefix: string): { logPath: string } {
|
|
14
|
+
mkdirSync(logDir, { recursive: true });
|
|
15
|
+
const ts = new Date().toISOString().replace(/[:.]/g, "-").slice(0, 19);
|
|
16
|
+
const logPath = join(logDir, `${prefix}-${ts}.log`);
|
|
17
|
+
appendFileSync(logPath, "", { flag: "a", encoding: "utf8" });
|
|
18
|
+
return { logPath };
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/** 默认日志目录:~/.deepccc/logs */
|
|
22
|
+
export function defaultLogDir(): string {
|
|
23
|
+
return join(homedir(), ".deepccc", "logs");
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function writeLogLine(logPath: string, level: string, args: unknown[]): void {
|
|
27
|
+
try {
|
|
28
|
+
const text = args
|
|
29
|
+
.map((a) =>
|
|
30
|
+
typeof a === "string" ? a
|
|
31
|
+
: a instanceof Error ? (a.stack ?? a.message)
|
|
32
|
+
: JSON.stringify(a))
|
|
33
|
+
.join(" ");
|
|
34
|
+
appendFileSync(logPath, `[${new Date().toISOString()}] [${level}] ${text}\n`, "utf8");
|
|
35
|
+
} catch {
|
|
36
|
+
// 日志系统自身失败不影响主流程
|
|
37
|
+
}
|
|
38
|
+
}
|
|
@@ -120,6 +120,24 @@ function buildRuntimeWorkspacePrompt(cwd: string): string {
|
|
|
120
120
|
].join("\n");
|
|
121
121
|
}
|
|
122
122
|
|
|
123
|
+
/**
|
|
124
|
+
* Windows 专属命令行指引(仅 win32 注入):cmd.exe 的引号语义与 bash 不同,
|
|
125
|
+
* 模型按 bash 习惯写命令时会被 cmd 拆坏(引号保留为字面量、单引号不生效、
|
|
126
|
+
* 多行/嵌套引号脚本崩坏)。这段提示放在固定规则区(项目指令之前)。
|
|
127
|
+
*/
|
|
128
|
+
function buildPlatformCommandPrompt(): string {
|
|
129
|
+
if (process.platform !== "win32") return "";
|
|
130
|
+
return [
|
|
131
|
+
"## Windows Command-Line Notes",
|
|
132
|
+
"You are running on Windows. run_command executes through cmd.exe, not bash. cmd quoting differs from bash and breaks common habits:",
|
|
133
|
+
"- Double quotes are NOT stripped: `echo \"hello world\"` prints `\"hello world\"` (quotes included), and `\"a b\" \"c\"` passes the literal arguments `\"a b\"` and `\"c\"` (quotes included) to the program.",
|
|
134
|
+
"- Single quotes are NOT quoting characters in cmd.exe: `'a b'` is parsed as two arguments (`'a` and `b'`).",
|
|
135
|
+
"- Multi-line or quote-heavy inline scripts (python -c \"...\\n...\", ssh host \"bash -c '...'\") frequently break under cmd quoting; write the script to a temporary file and execute that file instead.",
|
|
136
|
+
"- PowerShell-only syntax (Get-Item, 2>$null, Select-Object) is unavailable; the shell is cmd.exe unless you explicitly invoke powershell.",
|
|
137
|
+
"- To pass an argument containing spaces, use double quotes and expect the quotes to reach the program literally; when the target accepts file input, prefer writing the value to a file.",
|
|
138
|
+
].join("\n");
|
|
139
|
+
}
|
|
140
|
+
|
|
123
141
|
function normalizeMaxSteps(value: number | undefined): number | undefined {
|
|
124
142
|
if (value === undefined) return undefined;
|
|
125
143
|
if (!Number.isFinite(value) || !Number.isInteger(value) || value <= 0) {
|
|
@@ -269,6 +287,10 @@ export class ChatSession {
|
|
|
269
287
|
*/
|
|
270
288
|
private buildSystemPrompt(skills: BuiltinSkill[]): string {
|
|
271
289
|
const systemContent = [SYSTEM_PROMPT];
|
|
290
|
+
const platformPrompt = buildPlatformCommandPrompt();
|
|
291
|
+
if (platformPrompt) {
|
|
292
|
+
systemContent.push("", platformPrompt);
|
|
293
|
+
}
|
|
272
294
|
const projectInstructions = readProjectInstructionFiles(this.cwd);
|
|
273
295
|
if (projectInstructions) {
|
|
274
296
|
systemContent.push("", projectInstructions);
|
|
@@ -1,61 +1,61 @@
|
|
|
1
|
-
import { spawn } from "node:child_process";
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* Best-effort process-tree termination.
|
|
5
|
-
*
|
|
6
|
-
* Commands are spawned through a platform shell, so the pid we get is often
|
|
7
|
-
* the outer shell process. Killing only that process can leave the real child
|
|
8
|
-
* command running. This helper targets the whole process tree on Windows and
|
|
9
|
-
* the process group on POSIX when possible.
|
|
10
|
-
*/
|
|
11
|
-
export async function killProcessTree(pid: number | undefined): Promise<void> {
|
|
12
|
-
if (pid == null || !Number.isFinite(pid) || pid <= 0) return;
|
|
13
|
-
if (process.platform === "win32") {
|
|
14
|
-
await killWindowsTree(pid);
|
|
15
|
-
return;
|
|
16
|
-
}
|
|
17
|
-
await killPosixTree(pid);
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
function killWindowsTree(pid: number): Promise<void> {
|
|
21
|
-
return new Promise<void>((resolve) => {
|
|
22
|
-
let resolved = false;
|
|
23
|
-
const done = () => {
|
|
24
|
-
if (resolved) return;
|
|
25
|
-
resolved = true;
|
|
26
|
-
resolve();
|
|
27
|
-
};
|
|
28
|
-
|
|
29
|
-
try {
|
|
30
|
-
const proc = spawn("taskkill", ["/pid", String(pid), "/T", "/F"], {
|
|
31
|
-
stdio: "ignore",
|
|
32
|
-
windowsHide: true,
|
|
33
|
-
});
|
|
34
|
-
proc.once("error", (err) => {
|
|
35
|
-
console.warn(`[killProcessTree] taskkill spawn error for pid=${pid}: ${(err as Error).message}`);
|
|
36
|
-
done();
|
|
37
|
-
});
|
|
38
|
-
proc.once("close", () => { done(); });
|
|
39
|
-
setTimeout(done, 3000).unref();
|
|
40
|
-
} catch (err) {
|
|
41
|
-
console.warn(`[killProcessTree] taskkill failed for pid=${pid}: ${(err as Error).message}`);
|
|
42
|
-
done();
|
|
43
|
-
}
|
|
44
|
-
});
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
async function killPosixTree(pid: number): Promise<void> {
|
|
48
|
-
trySignal(-pid, "SIGTERM");
|
|
49
|
-
trySignal(pid, "SIGTERM");
|
|
50
|
-
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
51
|
-
trySignal(-pid, "SIGKILL");
|
|
52
|
-
trySignal(pid, "SIGKILL");
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
function trySignal(target: number, signal: NodeJS.Signals): void {
|
|
56
|
-
try {
|
|
57
|
-
process.kill(target, signal);
|
|
58
|
-
} catch {
|
|
59
|
-
// Process is already gone or cannot be signaled.
|
|
60
|
-
}
|
|
61
|
-
}
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Best-effort process-tree termination.
|
|
5
|
+
*
|
|
6
|
+
* Commands are spawned through a platform shell, so the pid we get is often
|
|
7
|
+
* the outer shell process. Killing only that process can leave the real child
|
|
8
|
+
* command running. This helper targets the whole process tree on Windows and
|
|
9
|
+
* the process group on POSIX when possible.
|
|
10
|
+
*/
|
|
11
|
+
export async function killProcessTree(pid: number | undefined): Promise<void> {
|
|
12
|
+
if (pid == null || !Number.isFinite(pid) || pid <= 0) return;
|
|
13
|
+
if (process.platform === "win32") {
|
|
14
|
+
await killWindowsTree(pid);
|
|
15
|
+
return;
|
|
16
|
+
}
|
|
17
|
+
await killPosixTree(pid);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function killWindowsTree(pid: number): Promise<void> {
|
|
21
|
+
return new Promise<void>((resolve) => {
|
|
22
|
+
let resolved = false;
|
|
23
|
+
const done = () => {
|
|
24
|
+
if (resolved) return;
|
|
25
|
+
resolved = true;
|
|
26
|
+
resolve();
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
try {
|
|
30
|
+
const proc = spawn("taskkill", ["/pid", String(pid), "/T", "/F"], {
|
|
31
|
+
stdio: "ignore",
|
|
32
|
+
windowsHide: true,
|
|
33
|
+
});
|
|
34
|
+
proc.once("error", (err) => {
|
|
35
|
+
console.warn(`[killProcessTree] taskkill spawn error for pid=${pid}: ${(err as Error).message}`);
|
|
36
|
+
done();
|
|
37
|
+
});
|
|
38
|
+
proc.once("close", () => { done(); });
|
|
39
|
+
setTimeout(done, 3000).unref();
|
|
40
|
+
} catch (err) {
|
|
41
|
+
console.warn(`[killProcessTree] taskkill failed for pid=${pid}: ${(err as Error).message}`);
|
|
42
|
+
done();
|
|
43
|
+
}
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
async function killPosixTree(pid: number): Promise<void> {
|
|
48
|
+
trySignal(-pid, "SIGTERM");
|
|
49
|
+
trySignal(pid, "SIGTERM");
|
|
50
|
+
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
51
|
+
trySignal(-pid, "SIGKILL");
|
|
52
|
+
trySignal(pid, "SIGKILL");
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function trySignal(target: number, signal: NodeJS.Signals): void {
|
|
56
|
+
try {
|
|
57
|
+
process.kill(target, signal);
|
|
58
|
+
} catch {
|
|
59
|
+
// Process is already gone or cannot be signaled.
|
|
60
|
+
}
|
|
61
|
+
}
|
|
@@ -1,76 +1,76 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* progress/cards-helpers.ts — 从 ChatCCC cards.ts 提取的纯函数(DeepCCC 独立版)
|
|
3
|
-
*
|
|
4
|
-
* terminal-renderer 需要 getToolEmoji(工具 emoji 映射)和 truncateContent
|
|
5
|
-
* (正文按行/字符截断)。DeepCCC 没有飞书卡片模块,这两个函数在此独立存放,
|
|
6
|
-
* 与 ChatCCC 保持一致实现,避免引入整张卡片模块。
|
|
7
|
-
*/
|
|
8
|
-
|
|
9
|
-
// 检测 markdown 代码块是否未闭合(``` 出现奇数次)
|
|
10
|
-
export function isCodeBlockOpen(text: string): boolean {
|
|
11
|
-
const matches = text.match(/```/g);
|
|
12
|
-
return matches ? matches.length % 2 !== 0 : false;
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
export function truncateContent(text: string, maxLines = 20, maxChars = 8000): string {
|
|
16
|
-
const lines = text.split("\n");
|
|
17
|
-
// 跳过开头空行
|
|
18
|
-
let startIdx = 0;
|
|
19
|
-
while (startIdx < lines.length && lines[startIdx].trim() === "") {
|
|
20
|
-
startIdx++;
|
|
21
|
-
}
|
|
22
|
-
const effectiveLines = lines.slice(startIdx);
|
|
23
|
-
let displayText: string;
|
|
24
|
-
if (effectiveLines.length > maxLines) {
|
|
25
|
-
const firstLine = effectiveLines[0];
|
|
26
|
-
const lastLines = effectiveLines.slice(-(maxLines - 1)).join("\n");
|
|
27
|
-
displayText = firstLine + "\n...\n" + lastLines;
|
|
28
|
-
} else {
|
|
29
|
-
displayText = text;
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
// 截断后如果代码块未闭合,补上闭合标记,避免后续追加内容时误入代码块
|
|
33
|
-
if (isCodeBlockOpen(displayText)) {
|
|
34
|
-
displayText += "\n```";
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
return displayText;
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
const TOOL_EMOJI_MAP: Record<string, string> = {
|
|
41
|
-
Read: "\u{1F4D6}", // 📖
|
|
42
|
-
Write: "\u{270D}\u{FE0F}", // ✍️
|
|
43
|
-
Edit: "\u{270F}\u{FE0F}", // ✏️
|
|
44
|
-
Grep: "\u{1F50E}", // 🔎
|
|
45
|
-
Glob: "\u{1F4C2}", // 📂
|
|
46
|
-
Bash: "\u{1F5A5}\u{FE0F}", // 🖥️
|
|
47
|
-
WebSearch: "\u{1F310}", // 🌐
|
|
48
|
-
WebFetch: "\u{1F4E5}", // 📥
|
|
49
|
-
TodoWrite: "\u{2705}", // ✅
|
|
50
|
-
Agent: "\u{1F916}", // 🤖
|
|
51
|
-
NotebookEdit: "\u{1F4D3}", // 📓
|
|
52
|
-
AskUserQuestion: "\u{2753}",// ❓
|
|
53
|
-
// CCC 内置 Agent 下划线命名(getToolEmoji 会先把下划线转驼峰再查表,这里保留蛇形条目以便直接命中)
|
|
54
|
-
read_file: "\u{1F4D6}", // 📖
|
|
55
|
-
list_dir: "\u{1F4C2}", // 📂
|
|
56
|
-
search_code: "\u{1F50E}", // 🔎
|
|
57
|
-
run_command: "\u{1F5A5}\u{FE0F}", // 🖥️
|
|
58
|
-
edit_file: "\u{270F}\u{FE0F}", // ✏️
|
|
59
|
-
create_file: "\u{270D}\u{FE0F}", // ✍️
|
|
60
|
-
delete_file: "\u{1F5D1}\u{FE0F}", // 🗑️
|
|
61
|
-
move_file: "\u{1F4E6}", // 📦
|
|
62
|
-
apply_patch: "\u{1F4CB}", // 📋
|
|
63
|
-
};
|
|
64
|
-
|
|
65
|
-
export function getToolEmoji(name: string): string {
|
|
66
|
-
return TOOL_EMOJI_MAP[name] ?? TOOL_EMOJI_MAP[normalizeToolName(name)] ?? "\u{1F527}"; // 🔧
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
/** 把下划线命名转成驼峰(read_file → ReadFile),用于兼容两种命名风格 */
|
|
70
|
-
export function normalizeToolName(name: string): string {
|
|
71
|
-
return name
|
|
72
|
-
.split("_")
|
|
73
|
-
.filter((part) => part.length > 0)
|
|
74
|
-
.map((part) => part.charAt(0).toUpperCase() + part.slice(1))
|
|
75
|
-
.join("");
|
|
76
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* progress/cards-helpers.ts — 从 ChatCCC cards.ts 提取的纯函数(DeepCCC 独立版)
|
|
3
|
+
*
|
|
4
|
+
* terminal-renderer 需要 getToolEmoji(工具 emoji 映射)和 truncateContent
|
|
5
|
+
* (正文按行/字符截断)。DeepCCC 没有飞书卡片模块,这两个函数在此独立存放,
|
|
6
|
+
* 与 ChatCCC 保持一致实现,避免引入整张卡片模块。
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
// 检测 markdown 代码块是否未闭合(``` 出现奇数次)
|
|
10
|
+
export function isCodeBlockOpen(text: string): boolean {
|
|
11
|
+
const matches = text.match(/```/g);
|
|
12
|
+
return matches ? matches.length % 2 !== 0 : false;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function truncateContent(text: string, maxLines = 20, maxChars = 8000): string {
|
|
16
|
+
const lines = text.split("\n");
|
|
17
|
+
// 跳过开头空行
|
|
18
|
+
let startIdx = 0;
|
|
19
|
+
while (startIdx < lines.length && lines[startIdx].trim() === "") {
|
|
20
|
+
startIdx++;
|
|
21
|
+
}
|
|
22
|
+
const effectiveLines = lines.slice(startIdx);
|
|
23
|
+
let displayText: string;
|
|
24
|
+
if (effectiveLines.length > maxLines) {
|
|
25
|
+
const firstLine = effectiveLines[0];
|
|
26
|
+
const lastLines = effectiveLines.slice(-(maxLines - 1)).join("\n");
|
|
27
|
+
displayText = firstLine + "\n...\n" + lastLines;
|
|
28
|
+
} else {
|
|
29
|
+
displayText = text;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// 截断后如果代码块未闭合,补上闭合标记,避免后续追加内容时误入代码块
|
|
33
|
+
if (isCodeBlockOpen(displayText)) {
|
|
34
|
+
displayText += "\n```";
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
return displayText;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const TOOL_EMOJI_MAP: Record<string, string> = {
|
|
41
|
+
Read: "\u{1F4D6}", // 📖
|
|
42
|
+
Write: "\u{270D}\u{FE0F}", // ✍️
|
|
43
|
+
Edit: "\u{270F}\u{FE0F}", // ✏️
|
|
44
|
+
Grep: "\u{1F50E}", // 🔎
|
|
45
|
+
Glob: "\u{1F4C2}", // 📂
|
|
46
|
+
Bash: "\u{1F5A5}\u{FE0F}", // 🖥️
|
|
47
|
+
WebSearch: "\u{1F310}", // 🌐
|
|
48
|
+
WebFetch: "\u{1F4E5}", // 📥
|
|
49
|
+
TodoWrite: "\u{2705}", // ✅
|
|
50
|
+
Agent: "\u{1F916}", // 🤖
|
|
51
|
+
NotebookEdit: "\u{1F4D3}", // 📓
|
|
52
|
+
AskUserQuestion: "\u{2753}",// ❓
|
|
53
|
+
// CCC 内置 Agent 下划线命名(getToolEmoji 会先把下划线转驼峰再查表,这里保留蛇形条目以便直接命中)
|
|
54
|
+
read_file: "\u{1F4D6}", // 📖
|
|
55
|
+
list_dir: "\u{1F4C2}", // 📂
|
|
56
|
+
search_code: "\u{1F50E}", // 🔎
|
|
57
|
+
run_command: "\u{1F5A5}\u{FE0F}", // 🖥️
|
|
58
|
+
edit_file: "\u{270F}\u{FE0F}", // ✏️
|
|
59
|
+
create_file: "\u{270D}\u{FE0F}", // ✍️
|
|
60
|
+
delete_file: "\u{1F5D1}\u{FE0F}", // 🗑️
|
|
61
|
+
move_file: "\u{1F4E6}", // 📦
|
|
62
|
+
apply_patch: "\u{1F4CB}", // 📋
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
export function getToolEmoji(name: string): string {
|
|
66
|
+
return TOOL_EMOJI_MAP[name] ?? TOOL_EMOJI_MAP[normalizeToolName(name)] ?? "\u{1F527}"; // 🔧
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/** 把下划线命名转成驼峰(read_file → ReadFile),用于兼容两种命名风格 */
|
|
70
|
+
export function normalizeToolName(name: string): string {
|
|
71
|
+
return name
|
|
72
|
+
.split("_")
|
|
73
|
+
.filter((part) => part.length > 0)
|
|
74
|
+
.map((part) => part.charAt(0).toUpperCase() + part.slice(1))
|
|
75
|
+
.join("");
|
|
76
|
+
}
|
|
@@ -1,113 +1,113 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* progress/reducer.ts — ChatEvent 事件流 → ProgressView 的增量合并
|
|
3
|
-
*
|
|
4
|
-
* 这是飞书与终端共享的唯一一份"事件 → 过程视图"公共逻辑:
|
|
5
|
-
* 终端 REPL 直接消费 ChatSession.chat() 的事件流,逐条 reduce;
|
|
6
|
-
* 飞书侧 display loop 若未来接入事件流,同样使用本 reducer,保证两端
|
|
7
|
-
* 看到的过程状态(正文累积、工具调用、终态判定)完全一致。
|
|
8
|
-
*/
|
|
9
|
-
|
|
10
|
-
import type { ChatEvent } from "../index.js";
|
|
11
|
-
import {
|
|
12
|
-
withProgressView,
|
|
13
|
-
type ProgressToolCall,
|
|
14
|
-
type ProgressView,
|
|
15
|
-
} from "./view.js";
|
|
16
|
-
|
|
17
|
-
/** 工具调用 input 单行摘要(终端折叠行展示用) */
|
|
18
|
-
export function summarizeToolInput(input: unknown, maxChars = 120): string {
|
|
19
|
-
let raw: string;
|
|
20
|
-
if (typeof input === "string") {
|
|
21
|
-
raw = input;
|
|
22
|
-
} else {
|
|
23
|
-
try {
|
|
24
|
-
raw = JSON.stringify(input);
|
|
25
|
-
} catch {
|
|
26
|
-
raw = String(input);
|
|
27
|
-
}
|
|
28
|
-
}
|
|
29
|
-
const oneLine = raw.replace(/\s+/g, " ").trim();
|
|
30
|
-
return oneLine.length > maxChars ? oneLine.slice(0, maxChars) + "…" : oneLine;
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
/** 工具结果单行摘要(成功/失败各取首行) */
|
|
34
|
-
export function summarizeToolResult(content: unknown, maxChars = 120): string {
|
|
35
|
-
let raw: string;
|
|
36
|
-
if (typeof content === "string") {
|
|
37
|
-
raw = content;
|
|
38
|
-
} else if (content instanceof Error) {
|
|
39
|
-
raw = content.message;
|
|
40
|
-
} else {
|
|
41
|
-
try {
|
|
42
|
-
raw = JSON.stringify(content);
|
|
43
|
-
} catch {
|
|
44
|
-
raw = String(content);
|
|
45
|
-
}
|
|
46
|
-
}
|
|
47
|
-
const firstLine = raw.split("\n")[0] ?? "";
|
|
48
|
-
return firstLine.length > maxChars ? firstLine.slice(0, maxChars) + "…" : firstLine;
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
/**
|
|
52
|
-
* 把一条 ChatEvent 合并进 ProgressView,返回新视图(不可变更新)。
|
|
53
|
-
* 未识别的/不影响展示的事件(如 compact)返回原视图。
|
|
54
|
-
*/
|
|
55
|
-
export function reduceProgress(prev: ProgressView, event: ChatEvent): ProgressView {
|
|
56
|
-
switch (event.type) {
|
|
57
|
-
case "status":
|
|
58
|
-
return withProgressView(prev, {
|
|
59
|
-
headerTitle: event.phase === "compacting" ? "压缩上下文中..." : "生成回复中...",
|
|
60
|
-
});
|
|
61
|
-
|
|
62
|
-
case "text":
|
|
63
|
-
// accumulated 是全文累积,直接全量替换,天然幂等
|
|
64
|
-
return withProgressView(prev, { text: event.accumulated });
|
|
65
|
-
|
|
66
|
-
case "tool_use": {
|
|
67
|
-
const tool: ProgressToolCall = {
|
|
68
|
-
id: event.id ?? `tool-${prev.tools.length + 1}`,
|
|
69
|
-
name: event.name,
|
|
70
|
-
status: "running",
|
|
71
|
-
detail: summarizeToolInput(event.input),
|
|
72
|
-
};
|
|
73
|
-
return withProgressView(prev, { tools: [...prev.tools, tool] });
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
case "tool_result": {
|
|
77
|
-
const nextStatus = event.is_error ? ("error" as const) : ("ok" as const);
|
|
78
|
-
const summary = summarizeToolResult(event.content);
|
|
79
|
-
let matched = false;
|
|
80
|
-
const tools = prev.tools.map((t) => {
|
|
81
|
-
if (matched || t.id !== event.tool_use_id) return t;
|
|
82
|
-
matched = true;
|
|
83
|
-
return { ...t, status: nextStatus, summary };
|
|
84
|
-
});
|
|
85
|
-
if (!matched) {
|
|
86
|
-
// id 缺失或失配:兜底更新最后一个 running 工具,避免状态悬挂
|
|
87
|
-
const lastRunning = [...prev.tools].reverse().findIndex((t) => t.status === "running");
|
|
88
|
-
if (lastRunning >= 0) {
|
|
89
|
-
const idx = prev.tools.length - 1 - lastRunning;
|
|
90
|
-
const updated = prev.tools.map((t, i) =>
|
|
91
|
-
i === idx ? { ...t, status: nextStatus, summary } : t,
|
|
92
|
-
);
|
|
93
|
-
return withProgressView(prev, { tools: updated });
|
|
94
|
-
}
|
|
95
|
-
}
|
|
96
|
-
return withProgressView(prev, { tools });
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
case "done":
|
|
100
|
-
return withProgressView(prev, {
|
|
101
|
-
status: "done",
|
|
102
|
-
showStop: false,
|
|
103
|
-
text: event.text,
|
|
104
|
-
});
|
|
105
|
-
|
|
106
|
-
case "error":
|
|
107
|
-
return withProgressView(prev, { status: "error", showStop: false });
|
|
108
|
-
|
|
109
|
-
case "compact":
|
|
110
|
-
// 旧上下文压缩不影响当前过程展示
|
|
111
|
-
return prev;
|
|
112
|
-
}
|
|
113
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* progress/reducer.ts — ChatEvent 事件流 → ProgressView 的增量合并
|
|
3
|
+
*
|
|
4
|
+
* 这是飞书与终端共享的唯一一份"事件 → 过程视图"公共逻辑:
|
|
5
|
+
* 终端 REPL 直接消费 ChatSession.chat() 的事件流,逐条 reduce;
|
|
6
|
+
* 飞书侧 display loop 若未来接入事件流,同样使用本 reducer,保证两端
|
|
7
|
+
* 看到的过程状态(正文累积、工具调用、终态判定)完全一致。
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import type { ChatEvent } from "../index.js";
|
|
11
|
+
import {
|
|
12
|
+
withProgressView,
|
|
13
|
+
type ProgressToolCall,
|
|
14
|
+
type ProgressView,
|
|
15
|
+
} from "./view.js";
|
|
16
|
+
|
|
17
|
+
/** 工具调用 input 单行摘要(终端折叠行展示用) */
|
|
18
|
+
export function summarizeToolInput(input: unknown, maxChars = 120): string {
|
|
19
|
+
let raw: string;
|
|
20
|
+
if (typeof input === "string") {
|
|
21
|
+
raw = input;
|
|
22
|
+
} else {
|
|
23
|
+
try {
|
|
24
|
+
raw = JSON.stringify(input);
|
|
25
|
+
} catch {
|
|
26
|
+
raw = String(input);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
const oneLine = raw.replace(/\s+/g, " ").trim();
|
|
30
|
+
return oneLine.length > maxChars ? oneLine.slice(0, maxChars) + "…" : oneLine;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** 工具结果单行摘要(成功/失败各取首行) */
|
|
34
|
+
export function summarizeToolResult(content: unknown, maxChars = 120): string {
|
|
35
|
+
let raw: string;
|
|
36
|
+
if (typeof content === "string") {
|
|
37
|
+
raw = content;
|
|
38
|
+
} else if (content instanceof Error) {
|
|
39
|
+
raw = content.message;
|
|
40
|
+
} else {
|
|
41
|
+
try {
|
|
42
|
+
raw = JSON.stringify(content);
|
|
43
|
+
} catch {
|
|
44
|
+
raw = String(content);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
const firstLine = raw.split("\n")[0] ?? "";
|
|
48
|
+
return firstLine.length > maxChars ? firstLine.slice(0, maxChars) + "…" : firstLine;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* 把一条 ChatEvent 合并进 ProgressView,返回新视图(不可变更新)。
|
|
53
|
+
* 未识别的/不影响展示的事件(如 compact)返回原视图。
|
|
54
|
+
*/
|
|
55
|
+
export function reduceProgress(prev: ProgressView, event: ChatEvent): ProgressView {
|
|
56
|
+
switch (event.type) {
|
|
57
|
+
case "status":
|
|
58
|
+
return withProgressView(prev, {
|
|
59
|
+
headerTitle: event.phase === "compacting" ? "压缩上下文中..." : "生成回复中...",
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
case "text":
|
|
63
|
+
// accumulated 是全文累积,直接全量替换,天然幂等
|
|
64
|
+
return withProgressView(prev, { text: event.accumulated });
|
|
65
|
+
|
|
66
|
+
case "tool_use": {
|
|
67
|
+
const tool: ProgressToolCall = {
|
|
68
|
+
id: event.id ?? `tool-${prev.tools.length + 1}`,
|
|
69
|
+
name: event.name,
|
|
70
|
+
status: "running",
|
|
71
|
+
detail: summarizeToolInput(event.input),
|
|
72
|
+
};
|
|
73
|
+
return withProgressView(prev, { tools: [...prev.tools, tool] });
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
case "tool_result": {
|
|
77
|
+
const nextStatus = event.is_error ? ("error" as const) : ("ok" as const);
|
|
78
|
+
const summary = summarizeToolResult(event.content);
|
|
79
|
+
let matched = false;
|
|
80
|
+
const tools = prev.tools.map((t) => {
|
|
81
|
+
if (matched || t.id !== event.tool_use_id) return t;
|
|
82
|
+
matched = true;
|
|
83
|
+
return { ...t, status: nextStatus, summary };
|
|
84
|
+
});
|
|
85
|
+
if (!matched) {
|
|
86
|
+
// id 缺失或失配:兜底更新最后一个 running 工具,避免状态悬挂
|
|
87
|
+
const lastRunning = [...prev.tools].reverse().findIndex((t) => t.status === "running");
|
|
88
|
+
if (lastRunning >= 0) {
|
|
89
|
+
const idx = prev.tools.length - 1 - lastRunning;
|
|
90
|
+
const updated = prev.tools.map((t, i) =>
|
|
91
|
+
i === idx ? { ...t, status: nextStatus, summary } : t,
|
|
92
|
+
);
|
|
93
|
+
return withProgressView(prev, { tools: updated });
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
return withProgressView(prev, { tools });
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
case "done":
|
|
100
|
+
return withProgressView(prev, {
|
|
101
|
+
status: "done",
|
|
102
|
+
showStop: false,
|
|
103
|
+
text: event.text,
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
case "error":
|
|
107
|
+
return withProgressView(prev, { status: "error", showStop: false });
|
|
108
|
+
|
|
109
|
+
case "compact":
|
|
110
|
+
// 旧上下文压缩不影响当前过程展示
|
|
111
|
+
return prev;
|
|
112
|
+
}
|
|
113
|
+
}
|