chatccc 0.2.231 → 0.2.233
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/.agents/skills/create-chatccc-feishu-app/SKILL.md +85 -85
- package/.claude/skills/create-chatccc-feishu-app/SKILL.md +85 -85
- package/.cursor/skills/create-chatccc-feishu-app/SKILL.md +85 -85
- package/README.md +90 -90
- package/agent-prompts/claude_specific.md +45 -45
- package/agent-prompts/codex_specific.md +2 -2
- package/agent-prompts/cursor_specific.md +13 -13
- package/bin/cccagent.mjs +17 -17
- package/im-skills/feishu-skill/receive-send-file.md +63 -63
- package/im-skills/feishu-skill/receive-send-image.md +24 -24
- package/im-skills/feishu-skill/skill.md +3 -3
- package/im-skills/wechat-file-skill/receive-send-file.md +38 -38
- package/im-skills/wechat-file-skill/send-file.mjs +83 -83
- package/im-skills/wechat-file-skill/skill.md +10 -10
- package/im-skills/wechat-image-skill/skill.md +10 -10
- package/im-skills/wechat-video-skill/receive-send-video.md +38 -38
- package/im-skills/wechat-video-skill/send-video.mjs +79 -79
- package/im-skills/wechat-video-skill/skill.md +10 -10
- package/package.json +1 -1
- package/scripts/postinstall-sharp-check.mjs +58 -58
- package/src/__tests__/agent-activity.test.ts +86 -86
- package/src/__tests__/agent-delegate-task-rpc.test.ts +165 -165
- package/src/__tests__/agent-reload-config-rpc.test.ts +99 -99
- package/src/__tests__/builtin-chat-session.test.ts +449 -393
- package/src/__tests__/builtin-cli-json.test.ts +39 -39
- package/src/__tests__/builtin-config.test.ts +26 -26
- package/src/__tests__/builtin-context.test.ts +319 -202
- 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 +228 -0
- 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__/card-action-routing.test.ts +18 -18
- package/src/__tests__/cardkit.test.ts +60 -60
- package/src/__tests__/ccc-adapter.test.ts +170 -170
- package/src/__tests__/chatgpt-subscription-rpc.test.ts +89 -89
- package/src/__tests__/chatgpt-subscription.test.ts +135 -135
- package/src/__tests__/chrome-devtools-guard.test.ts +165 -165
- package/src/__tests__/claude-adapter.test.ts +614 -614
- package/src/__tests__/claude-raw-stream-log.test.ts +87 -87
- package/src/__tests__/codex-adapter.test.ts +58 -58
- package/src/__tests__/codex-raw-stream-log.test.ts +170 -170
- package/src/__tests__/codex-reset-actions.test.ts +146 -146
- package/src/__tests__/crash-logging.test.ts +95 -1
- package/src/__tests__/cursor-adapter.test.ts +268 -268
- package/src/__tests__/feishu-api.test.ts +60 -60
- package/src/__tests__/feishu-avatar.test.ts +164 -164
- package/src/__tests__/feishu-message-ingress.test.ts +138 -138
- package/src/__tests__/feishu-platform.test.ts +22 -22
- package/src/__tests__/format-message.test.ts +47 -47
- package/src/__tests__/jsonl-stream.test.ts +79 -79
- package/src/__tests__/package-files.test.ts +24 -24
- package/src/__tests__/privacy.test.ts +198 -198
- package/src/__tests__/progress-reducer.test.ts +121 -121
- package/src/__tests__/raw-stream-log.test.ts +106 -106
- package/src/__tests__/response-stall.test.ts +49 -49
- package/src/__tests__/restart.test.ts +55 -10
- package/src/__tests__/session.test.ts +199 -199
- package/src/__tests__/shared-prefix.test.ts +36 -36
- package/src/__tests__/sim-platform.test.ts +16 -16
- package/src/__tests__/startup-lifecycle.test.ts +231 -231
- package/src/__tests__/stop-session.test.ts +34 -34
- package/src/__tests__/stream-state.test.ts +42 -42
- package/src/__tests__/terminal-error.test.ts +54 -54
- package/src/__tests__/terminal-renderer.test.ts +247 -247
- package/src/__tests__/update-command-guard.test.ts +144 -144
- package/src/__tests__/web-ui.test.ts +326 -326
- package/src/adapters/adapter-interface.ts +33 -33
- package/src/adapters/ccc-adapter.ts +141 -141
- package/src/adapters/claude-adapter.ts +620 -620
- package/src/adapters/claude-session-meta-store.ts +120 -120
- package/src/adapters/codex-adapter.ts +426 -426
- package/src/adapters/cursor-adapter.ts +681 -681
- package/src/adapters/jsonl-stream.ts +157 -157
- package/src/adapters/raw-stream-log.ts +124 -124
- package/src/adapters/resource-monitor.ts +140 -140
- package/src/agent-activity.ts +175 -175
- package/src/agent-delegate-task-rpc.ts +153 -153
- package/src/agent-delegate-task.ts +91 -91
- package/src/agent-reload-config-rpc.ts +34 -34
- package/src/agent-stop-stuck.ts +129 -129
- package/src/builtin/cli.ts +6 -0
- package/src/builtin/config.ts +84 -84
- package/src/builtin/context.ts +465 -391
- package/src/builtin/file-log.ts +38 -38
- package/src/builtin/file-tools.ts +34 -0
- package/src/builtin/index.ts +24 -23
- package/src/builtin/proc-tree-kill.ts +61 -61
- package/src/builtin/progress/cards-helpers.ts +76 -76
- package/src/builtin/progress/reducer.ts +113 -113
- package/src/builtin/progress/terminal-renderer.ts +294 -294
- package/src/builtin/progress/view.ts +77 -77
- package/src/builtin/raw-stream-log.ts +124 -124
- package/src/builtin/session-search.ts +363 -0
- package/src/builtin/session-select.ts +48 -48
- package/src/builtin/sigint.ts +50 -50
- package/src/builtin/skills.ts +205 -205
- package/src/builtin/web-tools.ts +313 -313
- package/src/card-action-routing.ts +14 -14
- package/src/chatgpt-subscription-rpc.ts +27 -27
- package/src/chatgpt-subscription.ts +299 -299
- package/src/chrome-devtools-guard.ts +318 -318
- package/src/codex-reset-actions.ts +184 -184
- package/src/feishu-api.ts +193 -193
- package/src/feishu-message-ingress.ts +195 -195
- package/src/feishu-platform.ts +20 -20
- package/src/format-message.ts +293 -293
- package/src/index.ts +6 -1
- package/src/litellm-proxy.ts +374 -374
- package/src/orchestrator.ts +36 -13
- package/src/platform-adapter.ts +6 -6
- package/src/privacy.ts +118 -118
- package/src/progress/reducer.ts +113 -113
- package/src/progress/terminal-renderer.ts +294 -294
- package/src/progress/view.ts +77 -77
- package/src/response-stall.ts +28 -28
- package/src/runtime-reload.ts +34 -34
- package/src/session-chat-binding.ts +82 -82
- package/src/session-name.ts +8 -8
- package/src/session.ts +201 -201
- package/src/shared-prefix.ts +29 -29
- package/src/shared.ts +44 -0
- package/src/sim-platform.ts +20 -20
- package/src/startup-lifecycle.ts +250 -250
- package/src/stream-state.ts +21 -21
- package/src/terminal-error.ts +129 -129
- package/src/turn-cards.ts +117 -117
- package/src/update-command-guard.ts +165 -165
package/src/builtin/file-log.ts
CHANGED
|
@@ -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
|
+
}
|
|
@@ -11,6 +11,11 @@ import { jsonSchema, tool, type ToolSet } from "ai";
|
|
|
11
11
|
|
|
12
12
|
import { isDangerousCommand, type PermissionGate, type PermissionRequest } from "./permissions.js";
|
|
13
13
|
import { killProcessTree } from "./proc-tree-kill.js";
|
|
14
|
+
import {
|
|
15
|
+
searchBuiltinSessions,
|
|
16
|
+
type SessionSearchInput,
|
|
17
|
+
type SessionSearchOutput,
|
|
18
|
+
} from "./session-search.js";
|
|
14
19
|
import {
|
|
15
20
|
webFetchForTool,
|
|
16
21
|
webSearchForTool,
|
|
@@ -1199,6 +1204,11 @@ export async function applyPatchForTool(cwd: string, input: ApplyPatchInput): Pr
|
|
|
1199
1204
|
export interface BuiltinFileToolsOptions {
|
|
1200
1205
|
/** 权限门控:副作用工具(run_command/文件写操作)执行前会先经过 gate.check */
|
|
1201
1206
|
permissionGate?: PermissionGate;
|
|
1207
|
+
/** session_search 工具的会话/原始日志目录(默认 ~/.deepccc/sessions 与 raw-stream-logs;测试可注入) */
|
|
1208
|
+
sessionSearch?: {
|
|
1209
|
+
contextDir?: string;
|
|
1210
|
+
rawLogsDir?: string;
|
|
1211
|
+
};
|
|
1202
1212
|
}
|
|
1203
1213
|
|
|
1204
1214
|
export function createBuiltinFileTools(
|
|
@@ -1455,5 +1465,29 @@ export function createBuiltinFileTools(
|
|
|
1455
1465
|
}),
|
|
1456
1466
|
execute: (input, options) => webFetchForTool(input, { abortSignal: options.abortSignal }),
|
|
1457
1467
|
}),
|
|
1468
|
+
// 会话历史检索:只读 ~/.deepccc 存档,无需权限询问
|
|
1469
|
+
session_search: tool<SessionSearchInput, SessionSearchOutput>({
|
|
1470
|
+
description:
|
|
1471
|
+
"Search DeepCCC session archives by keyword and return matching snippets. Use when you need to recall old user messages, assistant replies, or tool calls from previous sessions. Multiple terms must all appear in the same message (AND, case-insensitive). Set include_raw_logs to also scan gzipped raw stream logs (slower).",
|
|
1472
|
+
inputSchema: jsonSchema<SessionSearchInput>({
|
|
1473
|
+
type: "object",
|
|
1474
|
+
additionalProperties: false,
|
|
1475
|
+
properties: {
|
|
1476
|
+
query: { type: "string", description: "Keywords to search for (space-separated, all must match)." },
|
|
1477
|
+
session_id: { type: "string", description: "Optional session id to restrict the search to." },
|
|
1478
|
+
include_raw_logs: { type: "boolean", description: "Also scan gzipped raw stream logs under ~/.deepccc/raw-stream-logs. Default false." },
|
|
1479
|
+
max_results: { type: "number", description: "Maximum number of matches, capped at 50." },
|
|
1480
|
+
},
|
|
1481
|
+
required: ["query"],
|
|
1482
|
+
}),
|
|
1483
|
+
execute: (input) =>
|
|
1484
|
+
searchBuiltinSessions(input.query, {
|
|
1485
|
+
contextDir: options.sessionSearch?.contextDir,
|
|
1486
|
+
rawLogsDir: options.sessionSearch?.rawLogsDir,
|
|
1487
|
+
includeRawLogs: input.include_raw_logs ?? false,
|
|
1488
|
+
sessionId: input.session_id,
|
|
1489
|
+
maxResults: input.max_results,
|
|
1490
|
+
}),
|
|
1491
|
+
}),
|
|
1458
1492
|
};
|
|
1459
1493
|
}
|
package/src/builtin/index.ts
CHANGED
|
@@ -15,8 +15,9 @@ import {
|
|
|
15
15
|
type RawStreamLogHandle,
|
|
16
16
|
} from "./raw-stream-log.js";
|
|
17
17
|
import {
|
|
18
|
-
|
|
18
|
+
buildPersistedAssistantMessage,
|
|
19
19
|
buildSummaryPrompt,
|
|
20
|
+
BuiltinContextManager,
|
|
20
21
|
defaultBuiltinSessionId,
|
|
21
22
|
} from "./context.js";
|
|
22
23
|
import { createBuiltinFileTools } from "./file-tools.js";
|
|
@@ -53,8 +54,6 @@ const SUMMARY_SYSTEM_PROMPT = [
|
|
|
53
54
|
|
|
54
55
|
export const DEFAULT_COMPACTION_TIMEOUT_MS = 5 * 60 * 1000;
|
|
55
56
|
const MAX_COMPACTION_PASSES = 8;
|
|
56
|
-
const MAX_PERSISTED_ASSISTANT_TEXT_CHARS = 32_000;
|
|
57
|
-
const MAX_PERSISTED_TOOL_TRANSCRIPT_CHARS = 24_000;
|
|
58
57
|
|
|
59
58
|
// ---------------------------------------------------------------------------
|
|
60
59
|
// 类型定义
|
|
@@ -273,6 +272,10 @@ export class ChatSession {
|
|
|
273
272
|
let safeAccumulated = "";
|
|
274
273
|
let rawLog: RawStreamLogHandle | null = null;
|
|
275
274
|
let completed = false;
|
|
275
|
+
// 结构化工具调用存档:按 toolCallId 关联入参/出参/错误,落盘到 context.json 的
|
|
276
|
+
// assistant 消息 toolCalls 字段;[Tool transcript] 文本视图仍按原格式生成。
|
|
277
|
+
const toolCallsById = new Map<string, { name: string; input?: string; output?: string; is_error?: boolean }>();
|
|
278
|
+
const toolCallOrder: string[] = [];
|
|
276
279
|
|
|
277
280
|
try {
|
|
278
281
|
if (this.context.planCompaction()) {
|
|
@@ -330,6 +333,8 @@ export class ChatSession {
|
|
|
330
333
|
yield { type: "text", text: safeText, accumulated: safeAccumulated };
|
|
331
334
|
} else if (part.type === "tool-call") {
|
|
332
335
|
toolContext.push(`tool_call ${part.toolName}: ${safeJson(part.input)}`);
|
|
336
|
+
toolCallsById.set(part.toolCallId, { name: part.toolName, input: safeJson(part.input) });
|
|
337
|
+
toolCallOrder.push(part.toolCallId);
|
|
333
338
|
yield {
|
|
334
339
|
type: "tool_use",
|
|
335
340
|
id: part.toolCallId,
|
|
@@ -338,6 +343,10 @@ export class ChatSession {
|
|
|
338
343
|
};
|
|
339
344
|
} else if (part.type === "tool-result") {
|
|
340
345
|
toolContext.push(`tool_result ${part.toolName}: ${truncateToolContext(safeJson(part.output))}`);
|
|
346
|
+
const call = toolCallsById.get(part.toolCallId);
|
|
347
|
+
if (call) {
|
|
348
|
+
call.output = truncateToolContext(safeJson(part.output));
|
|
349
|
+
}
|
|
341
350
|
yield {
|
|
342
351
|
type: "tool_result",
|
|
343
352
|
tool_use_id: part.toolCallId,
|
|
@@ -348,6 +357,11 @@ export class ChatSession {
|
|
|
348
357
|
} else if (part.type === "tool-error") {
|
|
349
358
|
const message = errorMessage(part.error);
|
|
350
359
|
toolContext.push(`tool_error ${part.toolName}: ${message}`);
|
|
360
|
+
const call = toolCallsById.get(part.toolCallId);
|
|
361
|
+
if (call) {
|
|
362
|
+
call.output = message;
|
|
363
|
+
call.is_error = true;
|
|
364
|
+
}
|
|
351
365
|
yield {
|
|
352
366
|
type: "tool_result",
|
|
353
367
|
tool_use_id: part.toolCallId,
|
|
@@ -363,19 +377,14 @@ export class ChatSession {
|
|
|
363
377
|
}
|
|
364
378
|
completed = true;
|
|
365
379
|
|
|
366
|
-
const
|
|
380
|
+
const collectedToolCalls = toolCallOrder
|
|
381
|
+
.map((id) => toolCallsById.get(id))
|
|
382
|
+
.filter((call): call is { name: string; input?: string; output?: string; is_error?: boolean } => call !== undefined);
|
|
383
|
+
this.context.appendMessage(buildPersistedAssistantMessage({
|
|
367
384
|
fullText,
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
);
|
|
371
|
-
const persistedText = toolContext.length > 0
|
|
372
|
-
? `${persistedAssistantText}\n\n[Tool transcript]\n${truncateMiddle(
|
|
373
|
-
toolContext.join("\n"),
|
|
374
|
-
MAX_PERSISTED_TOOL_TRANSCRIPT_CHARS,
|
|
375
|
-
"...[tool transcript truncated]...",
|
|
376
|
-
)}`
|
|
377
|
-
: persistedAssistantText;
|
|
378
|
-
this.context.appendMessage({ role: "assistant", content: persistedText });
|
|
385
|
+
transcriptLines: toolContext,
|
|
386
|
+
toolCalls: collectedToolCalls,
|
|
387
|
+
}));
|
|
379
388
|
yield { type: "done", text: safeAccumulated };
|
|
380
389
|
} catch (err) {
|
|
381
390
|
const message = err instanceof Error ? err.message : String(err);
|
|
@@ -509,14 +518,6 @@ function truncateToolContext(value: string): string {
|
|
|
509
518
|
return value.length > 8000 ? `${value.slice(0, 8000)}...[truncated]` : value;
|
|
510
519
|
}
|
|
511
520
|
|
|
512
|
-
function truncateMiddle(value: string, maxChars: number, marker: string): string {
|
|
513
|
-
if (value.length <= maxChars) return value;
|
|
514
|
-
const available = Math.max(0, maxChars - marker.length - 2);
|
|
515
|
-
const headChars = Math.ceil(available / 2);
|
|
516
|
-
const tailChars = Math.floor(available / 2);
|
|
517
|
-
return `${value.slice(0, headChars)}\n${marker}\n${value.slice(-tailChars)}`;
|
|
518
|
-
}
|
|
519
|
-
|
|
520
521
|
function formatDuration(ms: number): string {
|
|
521
522
|
if (ms % 60_000 === 0) return `${ms / 60_000} minutes`;
|
|
522
523
|
if (ms % 1_000 === 0) return `${ms / 1_000} seconds`;
|
|
@@ -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
|
+
}
|