chatccc 0.2.243 → 0.2.245

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.
Files changed (65) hide show
  1. package/README.md +6 -6
  2. package/bin/cccagent.mjs +17 -17
  3. package/deepccc-agent/README.md +14 -8
  4. package/deepccc-agent/bin/deepccc.mjs +26 -26
  5. package/deepccc-agent/os-prompts/darwin.md +8 -0
  6. package/deepccc-agent/os-prompts/linux.md +8 -0
  7. package/deepccc-agent/os-prompts/win32.md +9 -0
  8. package/deepccc-agent/package-lock.json +2 -2
  9. package/deepccc-agent/package.json +63 -62
  10. package/deepccc-agent/src/__tests__/chat-session.test.ts +682 -578
  11. package/deepccc-agent/src/__tests__/cli-json.test.ts +49 -49
  12. package/deepccc-agent/src/__tests__/config.test.ts +26 -26
  13. package/deepccc-agent/src/__tests__/context.test.ts +319 -319
  14. package/deepccc-agent/src/__tests__/file-tools.test.ts +240 -240
  15. package/deepccc-agent/src/__tests__/permissions.test.ts +195 -195
  16. package/deepccc-agent/src/__tests__/privacy.test.ts +8 -5
  17. package/deepccc-agent/src/__tests__/progress-reducer.test.ts +121 -121
  18. package/deepccc-agent/src/__tests__/session-search.test.ts +262 -262
  19. package/deepccc-agent/src/__tests__/session-select.test.ts +116 -116
  20. package/deepccc-agent/src/__tests__/sigint.test.ts +56 -56
  21. package/deepccc-agent/src/__tests__/skills.test.ts +284 -284
  22. package/deepccc-agent/src/__tests__/terminal-renderer.test.ts +247 -247
  23. package/deepccc-agent/src/__tests__/web-tools.test.ts +220 -220
  24. package/deepccc-agent/src/cli.ts +7 -6
  25. package/deepccc-agent/src/config.ts +88 -84
  26. package/deepccc-agent/src/context.ts +465 -465
  27. package/deepccc-agent/src/file-log.ts +38 -38
  28. package/deepccc-agent/src/index.ts +103 -36
  29. package/deepccc-agent/src/proc-tree-kill.ts +61 -61
  30. package/deepccc-agent/src/progress/cards-helpers.ts +76 -76
  31. package/deepccc-agent/src/progress/reducer.ts +113 -113
  32. package/deepccc-agent/src/progress/terminal-renderer.ts +294 -294
  33. package/deepccc-agent/src/progress/view.ts +77 -77
  34. package/deepccc-agent/src/raw-stream-log.ts +124 -124
  35. package/deepccc-agent/src/session-search.ts +370 -370
  36. package/deepccc-agent/src/session-select.ts +48 -48
  37. package/deepccc-agent/src/sigint.ts +50 -50
  38. package/deepccc-agent/src/skills.ts +205 -205
  39. package/deepccc-agent/src/web-tools.ts +313 -313
  40. package/deepccc-agent/tsconfig.build.json +13 -13
  41. package/deepccc-agent/tsconfig.json +13 -13
  42. package/deepccc-agent/vitest.config.ts +7 -7
  43. package/package.json +1 -1
  44. package/src/__tests__/builtin-chat-session.test.ts +522 -522
  45. package/src/__tests__/builtin-config.test.ts +26 -26
  46. package/src/__tests__/builtin-context.test.ts +319 -319
  47. package/src/__tests__/builtin-file-tools.test.ts +240 -240
  48. package/src/__tests__/builtin-permissions.test.ts +211 -211
  49. package/src/__tests__/builtin-session-search.test.ts +262 -262
  50. package/src/__tests__/builtin-session-select.test.ts +116 -116
  51. package/src/__tests__/builtin-sigint.test.ts +56 -56
  52. package/src/__tests__/builtin-skills.test.ts +284 -284
  53. package/src/__tests__/builtin-web-tools.test.ts +220 -220
  54. package/src/__tests__/ccc-adapter.test.ts +15 -0
  55. package/src/__tests__/config.test.ts +17 -17
  56. package/src/__tests__/progress-reducer.test.ts +121 -121
  57. package/src/__tests__/session-ccc-config.test.ts +45 -45
  58. package/src/__tests__/session.test.ts +369 -306
  59. package/src/adapters/adapter-interface.ts +8 -2
  60. package/src/adapters/ccc-adapter.ts +149 -145
  61. package/src/config-utils.ts +13 -13
  62. package/src/config.ts +13 -13
  63. package/src/progress/reducer.ts +113 -113
  64. package/src/session-chat-binding.ts +83 -83
  65. package/src/session.ts +323 -320
@@ -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
+ }
@@ -6,8 +6,10 @@
6
6
 
7
7
  import { createOpenAICompatible } from "@ai-sdk/openai-compatible";
8
8
  import { generateText, isLoopFinished, stepCountIs, streamText, type TextStreamPart } from "ai";
9
- import { readFileSync } from "node:fs";
9
+ import { existsSync, readFileSync } from "node:fs";
10
+ import { homedir } from "node:os";
10
11
  import { join } from "node:path";
12
+ import { fileURLToPath } from "node:url";
11
13
 
12
14
  import { config as appConfig, RAW_STREAM_LOGS_DIR } from "./config.js";
13
15
  import {
@@ -121,21 +123,49 @@ function buildRuntimeWorkspacePrompt(cwd: string): string {
121
123
  }
122
124
 
123
125
  /**
124
- * Windows 专属命令行指引(仅 win32 注入):cmd.exe 的引号语义与 bash 不同,
125
- * 模型按 bash 习惯写命令时会被 cmd 拆坏(引号保留为字面量、单引号不生效、
126
- * 多行/嵌套引号脚本崩坏)。这段提示放在固定规则区(项目指令之前)。
126
+ * 各操作系统特有的命令行指引(文本资产,维护在 os-prompts/ 目录,而非硬编码):
127
+ *
128
+ * - 内置文件:包内 os-prompts/<platform>.md(win32/darwin/linux),随 npm 包分发;
129
+ * - 用户覆盖:~/.deepccc/prompts/<platform>.md 存在时完全替代内置内容(可自定义)。
130
+ *
131
+ * 文件内容自带标题(如 "## Windows Command-Line Notes"),读取后 trim 直接作为
132
+ * 一个段落注入固定规则区(项目指令之前)。未知平台或文件缺失时返回空字符串。
127
133
  */
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");
134
+ export function loadPlatformCommandPrompt(
135
+ platform: string = process.platform,
136
+ dirs: { builtinDir?: string; userDir?: string } = {},
137
+ ): string {
138
+ const filename =
139
+ platform === "win32" ? "win32.md" :
140
+ platform === "darwin" ? "darwin.md" :
141
+ platform === "linux" ? "linux.md" :
142
+ null;
143
+ if (!filename) return "";
144
+
145
+ // import.meta.url 定位包根:chatccc 源码运行时指向 deepccc-agent/os-prompts/,
146
+ // deepccc dist 运行时指向包根 os-prompts/(dist/index.js 的 ../os-prompts/)。
147
+ const builtinDir =
148
+ dirs.builtinDir ?? fileURLToPath(new URL("../os-prompts/", import.meta.url));
149
+ const userDir = dirs.userDir ?? join(homedir(), ".deepccc", "prompts");
150
+
151
+ // 用户覆盖优先;读取失败时静默回退内置,内置也失败则返回空。
152
+ const userFile = join(userDir, filename);
153
+ if (existsSync(userFile)) {
154
+ try {
155
+ return readFileSync(userFile, "utf-8").trim();
156
+ } catch {
157
+ // fall through to builtin
158
+ }
159
+ }
160
+ const builtinFile = join(builtinDir, filename);
161
+ if (existsSync(builtinFile)) {
162
+ try {
163
+ return readFileSync(builtinFile, "utf-8").trim();
164
+ } catch {
165
+ return "";
166
+ }
167
+ }
168
+ return "";
139
169
  }
140
170
 
141
171
  function normalizeMaxSteps(value: number | undefined): number | undefined {
@@ -250,10 +280,11 @@ export class ChatSession {
250
280
  const modelId = overrides.model ?? appConfig.model;
251
281
  this.effort = (overrides.effort ?? appConfig.effort ?? "").trim();
252
282
 
253
- const provider = createOpenAICompatible({
254
- name: "deepccc",
255
- baseURL,
256
- apiKey,
283
+ const provider = createOpenAICompatible({
284
+ name: "deepccc",
285
+ baseURL,
286
+ apiKey,
287
+ includeUsage: true,
257
288
  });
258
289
  this.model = provider(modelId);
259
290
  this.cwd = options.cwd ?? process.cwd();
@@ -287,7 +318,7 @@ export class ChatSession {
287
318
  */
288
319
  private buildSystemPrompt(skills: BuiltinSkill[]): string {
289
320
  const systemContent = [SYSTEM_PROMPT];
290
- const platformPrompt = buildPlatformCommandPrompt();
321
+ const platformPrompt = loadPlatformCommandPrompt();
291
322
  if (platformPrompt) {
292
323
  systemContent.push("", platformPrompt);
293
324
  }
@@ -353,20 +384,27 @@ export class ChatSession {
353
384
  const skills = await scanSkillsDirs(this.skillDirs);
354
385
  const system = this.buildSystemPrompt(skills);
355
386
  this.systemPrompt = system;
356
- const result = streamText({
357
- model: this.model,
358
- system,
359
- messages: this.context.buildModelMessages() as any,
387
+ const generationOptions = {
388
+ model: this.model,
389
+ system,
390
+ messages: this.context.buildModelMessages() as any,
360
391
  tools: createBuiltinFileTools(this.cwd, { permissionGate: this.permissionGate }),
361
392
  stopWhen: maxSteps !== undefined ? stepCountIs(maxSteps) : isLoopFinished(),
362
393
  abortSignal: signal,
363
- // DeepSeek OpenAI 兼容接口:providerOptions.deepseek.reasoningEffort
364
- // 由 @ai-sdk/openai-compatible 自动映射为请求体 reasoning_effort 字段
365
- ...(this.effort ? { providerOptions: { deepseek: { reasoningEffort: this.effort } } } : {}),
366
- });
367
-
368
- const stream = result.fullStream ?? textStreamToFullStream(result.textStream);
369
- for await (const part of stream as AsyncIterable<TextStreamPart<any>>) {
394
+ // DeepSeek OpenAI 兼容接口:providerOptions.deepseek.reasoningEffort
395
+ // 由 @ai-sdk/openai-compatible 自动映射为请求体 reasoning_effort 字段
396
+ ...(this.effort ? { providerOptions: { deepseek: { reasoningEffort: this.effort } } } : {}),
397
+ };
398
+ let stream: AsyncIterable<TextStreamPart<any>>;
399
+ if (appConfig.streaming) {
400
+ const result = streamText(generationOptions);
401
+ stream = result.fullStream ?? textStreamToFullStream(result.textStream);
402
+ } else {
403
+ const result = await generateText(generationOptions);
404
+ stream = generateResultToFullStream(result);
405
+ }
406
+
407
+ for await (const part of stream as AsyncIterable<TextStreamPart<any>>) {
370
408
  rawLog?.writeLine(safeRawStreamJson(part));
371
409
  if (part.type === "text-delta") {
372
410
  fullText += part.text;
@@ -524,11 +562,40 @@ export class ChatSession {
524
562
  }
525
563
  }
526
564
 
527
- async function* textStreamToFullStream(stream: AsyncIterable<string>): AsyncIterable<{ type: "text-delta"; text: string }> {
528
- for await (const text of stream) {
529
- yield { type: "text-delta", text };
530
- }
531
- }
565
+ async function* textStreamToFullStream(stream: AsyncIterable<string>): AsyncIterable<{ type: "text-delta"; text: string }> {
566
+ for await (const text of stream) {
567
+ yield { type: "text-delta", text };
568
+ }
569
+ }
570
+
571
+ async function* generateResultToFullStream(result: any): AsyncIterable<TextStreamPart<any>> {
572
+ let emittedText = false;
573
+ for (const step of result.steps ?? []) {
574
+ for (const call of step.toolCalls ?? []) {
575
+ yield {
576
+ type: "tool-call",
577
+ toolCallId: call.toolCallId,
578
+ toolName: call.toolName,
579
+ input: call.input,
580
+ } as TextStreamPart<any>;
581
+ }
582
+ for (const toolResult of step.toolResults ?? []) {
583
+ yield {
584
+ type: "tool-result",
585
+ toolCallId: toolResult.toolCallId,
586
+ toolName: toolResult.toolName,
587
+ output: toolResult.output,
588
+ } as TextStreamPart<any>;
589
+ }
590
+ if (step.text) {
591
+ emittedText = true;
592
+ yield { type: "text-delta", text: step.text } as TextStreamPart<any>;
593
+ }
594
+ }
595
+ if (!emittedText && result.text) {
596
+ yield { type: "text-delta", text: result.text } as TextStreamPart<any>;
597
+ }
598
+ }
532
599
 
533
600
  function safeJson(value: unknown): string {
534
601
  try {
@@ -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
+ }