chatccc 0.2.230 → 0.2.232
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 +1 -1
- package/package.json +1 -1
- package/src/__tests__/agent-activity.test.ts +10 -0
- package/src/__tests__/builtin-chat-session.test.ts +449 -329
- package/src/__tests__/builtin-context.test.ts +158 -2
- package/src/__tests__/builtin-file-tools.test.ts +240 -240
- package/src/__tests__/builtin-session-search.test.ts +228 -0
- package/src/__tests__/builtin-skills.test.ts +284 -284
- package/src/__tests__/ccc-adapter.test.ts +34 -0
- package/src/__tests__/progress-reducer.test.ts +11 -0
- package/src/__tests__/restart.test.ts +132 -132
- package/src/__tests__/session.test.ts +199 -204
- package/src/__tests__/terminal-error.test.ts +54 -54
- package/src/adapters/adapter-interface.ts +15 -9
- package/src/adapters/ccc-adapter.ts +11 -1
- package/src/agent-activity.ts +5 -0
- package/src/builtin/cli.ts +15 -2
- package/src/builtin/context.ts +465 -333
- package/src/builtin/file-tools.ts +34 -0
- package/src/builtin/index.ts +87 -22
- package/src/builtin/progress/reducer.ts +5 -0
- package/src/builtin/session-search.ts +363 -0
- package/src/builtin/skills.ts +205 -205
- package/src/orchestrator.ts +2494 -2494
- package/src/progress/reducer.ts +5 -0
- package/src/session.ts +201 -200
- package/src/stream-state.ts +21 -21
- package/src/terminal-error.ts +129 -129
package/src/stream-state.ts
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import { readFile, writeFile, mkdir, rename, unlink } from "node:fs/promises";
|
|
2
2
|
import { dirname, join } from "node:path";
|
|
3
3
|
|
|
4
|
-
import { USER_DATA_DIR, ts } from "./config.ts";
|
|
5
|
-
import { createAgentActivityTracker } from "./agent-activity.ts";
|
|
6
|
-
import type { AgentActivity } from "./agent-activity.ts";
|
|
7
|
-
import type { TerminalErrorInfo } from "./terminal-error.ts";
|
|
4
|
+
import { USER_DATA_DIR, ts } from "./config.ts";
|
|
5
|
+
import { createAgentActivityTracker } from "./agent-activity.ts";
|
|
6
|
+
import type { AgentActivity } from "./agent-activity.ts";
|
|
7
|
+
import type { TerminalErrorInfo } from "./terminal-error.ts";
|
|
8
8
|
|
|
9
9
|
// ---------------------------------------------------------------------------
|
|
10
10
|
// stream-state.json — 每个 session 的流式输出持久化文件
|
|
@@ -14,14 +14,14 @@ export const STREAMS_DIR = join(USER_DATA_DIR, "state", "streams");
|
|
|
14
14
|
|
|
15
15
|
export interface StreamState {
|
|
16
16
|
sessionId: string;
|
|
17
|
-
status: "running" | "done" | "stopped" | "error" | "auto_ended";
|
|
17
|
+
status: "running" | "done" | "stopped" | "error" | "auto_ended";
|
|
18
18
|
accumulatedContent: string;
|
|
19
19
|
/** 本轮会话中 LLM 输出的全部文本内容(所有 text block 的累加)。
|
|
20
20
|
* 命名含 "final" 但实为"全部累积文本",并非仅"最终一段回复"。
|
|
21
21
|
* 参见 session.ts 的 AccumulatorState 注释。 */
|
|
22
|
-
finalReply: string;
|
|
23
|
-
/** Current user-visible work phase for running progress cards. */
|
|
24
|
-
activity?: AgentActivity;
|
|
22
|
+
finalReply: string;
|
|
23
|
+
/** Current user-visible work phase for running progress cards. */
|
|
24
|
+
activity?: AgentActivity;
|
|
25
25
|
/** The turn whose terminal text reply has already been delivered to IM. */
|
|
26
26
|
finalReplySentTurn?: number;
|
|
27
27
|
finalReplySentAt?: number;
|
|
@@ -33,12 +33,12 @@ export interface StreamState {
|
|
|
33
33
|
tool: string;
|
|
34
34
|
/** Set by stop-stuck-loop to prevent the session from being resumed.
|
|
35
35
|
* The orchestrator checks this before resuming and creates a new session instead. */
|
|
36
|
-
stuckAt?: number;
|
|
37
|
-
/** Set when the shared response watchdog ends a turn after three minutes without new characters. */
|
|
38
|
-
autoEndedAt?: number;
|
|
39
|
-
/** 脱敏后的用户可见根因;完整原始异常只保留在运行日志中。 */
|
|
40
|
-
terminalError?: TerminalErrorInfo;
|
|
41
|
-
}
|
|
36
|
+
stuckAt?: number;
|
|
37
|
+
/** Set when the shared response watchdog ends a turn after three minutes without new characters. */
|
|
38
|
+
autoEndedAt?: number;
|
|
39
|
+
/** 脱敏后的用户可见根因;完整原始异常只保留在运行日志中。 */
|
|
40
|
+
terminalError?: TerminalErrorInfo;
|
|
41
|
+
}
|
|
42
42
|
|
|
43
43
|
function getStreamStatePath(sessionId: string): string {
|
|
44
44
|
return join(STREAMS_DIR, `${sessionId}.json`);
|
|
@@ -131,18 +131,18 @@ export async function markFinalReplySent(sessionId: string, turnCount: number, s
|
|
|
131
131
|
await writeStreamState(state);
|
|
132
132
|
}
|
|
133
133
|
|
|
134
|
-
export function createEmptyStreamState(sessionId: string, cwd: string, tool: string, turnCount: number): StreamState {
|
|
135
|
-
const now = Date.now();
|
|
136
|
-
return {
|
|
134
|
+
export function createEmptyStreamState(sessionId: string, cwd: string, tool: string, turnCount: number): StreamState {
|
|
135
|
+
const now = Date.now();
|
|
136
|
+
return {
|
|
137
137
|
sessionId,
|
|
138
138
|
status: "running",
|
|
139
|
-
accumulatedContent: "",
|
|
140
|
-
finalReply: "",
|
|
141
|
-
activity: createAgentActivityTracker(now).activity,
|
|
139
|
+
accumulatedContent: "",
|
|
140
|
+
finalReply: "",
|
|
141
|
+
activity: createAgentActivityTracker(now).activity,
|
|
142
142
|
chunkCount: 0,
|
|
143
143
|
turnCount,
|
|
144
144
|
contextTokens: 0,
|
|
145
|
-
updatedAt: now,
|
|
145
|
+
updatedAt: now,
|
|
146
146
|
cwd,
|
|
147
147
|
tool,
|
|
148
148
|
};
|
package/src/terminal-error.ts
CHANGED
|
@@ -1,129 +1,129 @@
|
|
|
1
|
-
/** 用户可见的终态错误。这里只保存脱敏摘要;完整原始异常继续只写运行日志。 */
|
|
2
|
-
export type TerminalErrorKind =
|
|
3
|
-
| "network_timeout"
|
|
4
|
-
| "network"
|
|
5
|
-
| "authentication"
|
|
6
|
-
| "rate_limit"
|
|
7
|
-
| "provider"
|
|
8
|
-
| "process"
|
|
9
|
-
| "resource"
|
|
10
|
-
| "unknown";
|
|
11
|
-
|
|
12
|
-
export interface TerminalErrorInfo {
|
|
13
|
-
kind: TerminalErrorKind;
|
|
14
|
-
title: string;
|
|
15
|
-
message: string;
|
|
16
|
-
occurredAt: number;
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
const MAX_UNKNOWN_ERROR_LENGTH = 300;
|
|
20
|
-
|
|
21
|
-
function errorMessage(error: unknown): string {
|
|
22
|
-
if (error instanceof Error) return error.message;
|
|
23
|
-
if (typeof error === "string") return error;
|
|
24
|
-
try {
|
|
25
|
-
return JSON.stringify(error);
|
|
26
|
-
} catch {
|
|
27
|
-
return String(error);
|
|
28
|
-
}
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
/**
|
|
32
|
-
* 未分类错误仍应给出可诊断线索,但不能把常见凭据带进群聊或持久化状态。
|
|
33
|
-
* 已分类错误使用固定文案,不复述地址、请求体或 provider 原始响应。
|
|
34
|
-
*/
|
|
35
|
-
export function sanitizeTerminalErrorDetail(raw: string): string {
|
|
36
|
-
return raw
|
|
37
|
-
.replace(/\bBearer\s+[^\s,;]+/gi, "Bearer [已脱敏]")
|
|
38
|
-
.replace(/\b(api[_-]?key|access[_-]?token|token|secret)\s*[:=]\s*[^\s,;]+/gi, "$1=[已脱敏]")
|
|
39
|
-
.replace(/\bsk-[A-Za-z0-9._-]{6,}\b/g, "sk-[已脱敏]")
|
|
40
|
-
.replace(/([?&](?:api[_-]?key|access[_-]?token|token|secret)=)[^&\s]+/gi, "$1[已脱敏]")
|
|
41
|
-
.slice(0, MAX_UNKNOWN_ERROR_LENGTH);
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
function parsePositiveInt(message: string, pattern: RegExp): number | undefined {
|
|
45
|
-
const value = Number.parseInt(message.match(pattern)?.[1] ?? "", 10);
|
|
46
|
-
return Number.isFinite(value) && value > 0 ? value : undefined;
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
function formatSeconds(milliseconds: number): string {
|
|
50
|
-
if (milliseconds % 1000 === 0) return `${milliseconds / 1000} 秒`;
|
|
51
|
-
return `${milliseconds} 毫秒`;
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
export function classifyTerminalError(error: unknown, occurredAt = Date.now()): TerminalErrorInfo {
|
|
55
|
-
const raw = errorMessage(error);
|
|
56
|
-
const lower = raw.toLowerCase();
|
|
57
|
-
const attempts = parsePositiveInt(raw, /\bafter\s+(\d+)\s+attempts?\b/i);
|
|
58
|
-
const timeoutMs = parsePositiveInt(raw, /\btimeout\s*:\s*(\d+)\s*ms\b/i);
|
|
59
|
-
|
|
60
|
-
if (/\b429\b|rate[ _-]?limit|too many requests/.test(lower)) {
|
|
61
|
-
return {
|
|
62
|
-
kind: "rate_limit",
|
|
63
|
-
title: "请求受到限流",
|
|
64
|
-
message: "模型服务请求受到限流,请稍后重试。",
|
|
65
|
-
occurredAt,
|
|
66
|
-
};
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
if (/\b401\b|\b403\b|unauthori[sz]ed|forbidden|invalid api key|authentication/.test(lower)) {
|
|
70
|
-
return {
|
|
71
|
-
kind: "authentication",
|
|
72
|
-
title: "模型服务鉴权失败",
|
|
73
|
-
message: "模型服务拒绝了当前凭据,请检查 API Key、账号权限或凭据是否过期。",
|
|
74
|
-
occurredAt,
|
|
75
|
-
};
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
if (
|
|
79
|
-
/connect timeout|connection timed out|etimedout|und_err_connect_timeout/.test(lower)
|
|
80
|
-
|| (lower.includes("cannot connect") && lower.includes("timeout"))
|
|
81
|
-
) {
|
|
82
|
-
const retryText = attempts ? `,已重试 ${attempts} 次` : "";
|
|
83
|
-
const timeoutText = timeoutMs ? `,单次连接等待 ${formatSeconds(timeoutMs)}` : "";
|
|
84
|
-
return {
|
|
85
|
-
kind: "network_timeout",
|
|
86
|
-
title: "网络连接超时",
|
|
87
|
-
message: `连接模型服务失败${retryText}${timeoutText}。请检查网络、VPN或模型服务状态后重试。`,
|
|
88
|
-
occurredAt,
|
|
89
|
-
};
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
if (/econnrefused|econnreset|enotfound|eai_again|socket hang up|network error|cannot connect/.test(lower)) {
|
|
93
|
-
return {
|
|
94
|
-
kind: "network",
|
|
95
|
-
title: "无法连接模型服务",
|
|
96
|
-
message: "与模型服务的网络连接失败,请检查网络、VPN、DNS或服务状态后重试。",
|
|
97
|
-
occurredAt,
|
|
98
|
-
};
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
const httpStatus = raw.match(/\b(?:HTTP\s*)?(5\d\d)\b/i)?.[1];
|
|
102
|
-
if (httpStatus || /service unavailable|bad gateway|gateway timeout|provider error/.test(lower)) {
|
|
103
|
-
return {
|
|
104
|
-
kind: "provider",
|
|
105
|
-
title: "模型服务暂时不可用",
|
|
106
|
-
message: `模型服务返回异常${httpStatus ? `(HTTP ${httpStatus})` : ""},请稍后重试。`,
|
|
107
|
-
occurredAt,
|
|
108
|
-
};
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
const safeDetail = sanitizeTerminalErrorDetail(raw).trim() || "未提供错误详情";
|
|
112
|
-
return {
|
|
113
|
-
kind: "unknown",
|
|
114
|
-
title: "Agent 执行错误",
|
|
115
|
-
message: `发生未分类错误:${safeDetail}`,
|
|
116
|
-
occurredAt,
|
|
117
|
-
};
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
export function formatTerminalErrorReason(error: TerminalErrorInfo): string {
|
|
121
|
-
return `⚠️ 异常结束:${error.title}\n${error.message}`;
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
export function formatTerminalErrorNotice(error: TerminalErrorInfo, finalReply = ""): string {
|
|
125
|
-
const reason = formatTerminalErrorReason(error);
|
|
126
|
-
return finalReply.trim()
|
|
127
|
-
? `${reason}\n\n以下回复可能不完整:\n\n${finalReply.trim()}`
|
|
128
|
-
: reason;
|
|
129
|
-
}
|
|
1
|
+
/** 用户可见的终态错误。这里只保存脱敏摘要;完整原始异常继续只写运行日志。 */
|
|
2
|
+
export type TerminalErrorKind =
|
|
3
|
+
| "network_timeout"
|
|
4
|
+
| "network"
|
|
5
|
+
| "authentication"
|
|
6
|
+
| "rate_limit"
|
|
7
|
+
| "provider"
|
|
8
|
+
| "process"
|
|
9
|
+
| "resource"
|
|
10
|
+
| "unknown";
|
|
11
|
+
|
|
12
|
+
export interface TerminalErrorInfo {
|
|
13
|
+
kind: TerminalErrorKind;
|
|
14
|
+
title: string;
|
|
15
|
+
message: string;
|
|
16
|
+
occurredAt: number;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const MAX_UNKNOWN_ERROR_LENGTH = 300;
|
|
20
|
+
|
|
21
|
+
function errorMessage(error: unknown): string {
|
|
22
|
+
if (error instanceof Error) return error.message;
|
|
23
|
+
if (typeof error === "string") return error;
|
|
24
|
+
try {
|
|
25
|
+
return JSON.stringify(error);
|
|
26
|
+
} catch {
|
|
27
|
+
return String(error);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* 未分类错误仍应给出可诊断线索,但不能把常见凭据带进群聊或持久化状态。
|
|
33
|
+
* 已分类错误使用固定文案,不复述地址、请求体或 provider 原始响应。
|
|
34
|
+
*/
|
|
35
|
+
export function sanitizeTerminalErrorDetail(raw: string): string {
|
|
36
|
+
return raw
|
|
37
|
+
.replace(/\bBearer\s+[^\s,;]+/gi, "Bearer [已脱敏]")
|
|
38
|
+
.replace(/\b(api[_-]?key|access[_-]?token|token|secret)\s*[:=]\s*[^\s,;]+/gi, "$1=[已脱敏]")
|
|
39
|
+
.replace(/\bsk-[A-Za-z0-9._-]{6,}\b/g, "sk-[已脱敏]")
|
|
40
|
+
.replace(/([?&](?:api[_-]?key|access[_-]?token|token|secret)=)[^&\s]+/gi, "$1[已脱敏]")
|
|
41
|
+
.slice(0, MAX_UNKNOWN_ERROR_LENGTH);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function parsePositiveInt(message: string, pattern: RegExp): number | undefined {
|
|
45
|
+
const value = Number.parseInt(message.match(pattern)?.[1] ?? "", 10);
|
|
46
|
+
return Number.isFinite(value) && value > 0 ? value : undefined;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function formatSeconds(milliseconds: number): string {
|
|
50
|
+
if (milliseconds % 1000 === 0) return `${milliseconds / 1000} 秒`;
|
|
51
|
+
return `${milliseconds} 毫秒`;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export function classifyTerminalError(error: unknown, occurredAt = Date.now()): TerminalErrorInfo {
|
|
55
|
+
const raw = errorMessage(error);
|
|
56
|
+
const lower = raw.toLowerCase();
|
|
57
|
+
const attempts = parsePositiveInt(raw, /\bafter\s+(\d+)\s+attempts?\b/i);
|
|
58
|
+
const timeoutMs = parsePositiveInt(raw, /\btimeout\s*:\s*(\d+)\s*ms\b/i);
|
|
59
|
+
|
|
60
|
+
if (/\b429\b|rate[ _-]?limit|too many requests/.test(lower)) {
|
|
61
|
+
return {
|
|
62
|
+
kind: "rate_limit",
|
|
63
|
+
title: "请求受到限流",
|
|
64
|
+
message: "模型服务请求受到限流,请稍后重试。",
|
|
65
|
+
occurredAt,
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
if (/\b401\b|\b403\b|unauthori[sz]ed|forbidden|invalid api key|authentication/.test(lower)) {
|
|
70
|
+
return {
|
|
71
|
+
kind: "authentication",
|
|
72
|
+
title: "模型服务鉴权失败",
|
|
73
|
+
message: "模型服务拒绝了当前凭据,请检查 API Key、账号权限或凭据是否过期。",
|
|
74
|
+
occurredAt,
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
if (
|
|
79
|
+
/connect timeout|connection timed out|etimedout|und_err_connect_timeout/.test(lower)
|
|
80
|
+
|| (lower.includes("cannot connect") && lower.includes("timeout"))
|
|
81
|
+
) {
|
|
82
|
+
const retryText = attempts ? `,已重试 ${attempts} 次` : "";
|
|
83
|
+
const timeoutText = timeoutMs ? `,单次连接等待 ${formatSeconds(timeoutMs)}` : "";
|
|
84
|
+
return {
|
|
85
|
+
kind: "network_timeout",
|
|
86
|
+
title: "网络连接超时",
|
|
87
|
+
message: `连接模型服务失败${retryText}${timeoutText}。请检查网络、VPN或模型服务状态后重试。`,
|
|
88
|
+
occurredAt,
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
if (/econnrefused|econnreset|enotfound|eai_again|socket hang up|network error|cannot connect/.test(lower)) {
|
|
93
|
+
return {
|
|
94
|
+
kind: "network",
|
|
95
|
+
title: "无法连接模型服务",
|
|
96
|
+
message: "与模型服务的网络连接失败,请检查网络、VPN、DNS或服务状态后重试。",
|
|
97
|
+
occurredAt,
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const httpStatus = raw.match(/\b(?:HTTP\s*)?(5\d\d)\b/i)?.[1];
|
|
102
|
+
if (httpStatus || /service unavailable|bad gateway|gateway timeout|provider error/.test(lower)) {
|
|
103
|
+
return {
|
|
104
|
+
kind: "provider",
|
|
105
|
+
title: "模型服务暂时不可用",
|
|
106
|
+
message: `模型服务返回异常${httpStatus ? `(HTTP ${httpStatus})` : ""},请稍后重试。`,
|
|
107
|
+
occurredAt,
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const safeDetail = sanitizeTerminalErrorDetail(raw).trim() || "未提供错误详情";
|
|
112
|
+
return {
|
|
113
|
+
kind: "unknown",
|
|
114
|
+
title: "Agent 执行错误",
|
|
115
|
+
message: `发生未分类错误:${safeDetail}`,
|
|
116
|
+
occurredAt,
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
export function formatTerminalErrorReason(error: TerminalErrorInfo): string {
|
|
121
|
+
return `⚠️ 异常结束:${error.title}\n${error.message}`;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
export function formatTerminalErrorNotice(error: TerminalErrorInfo, finalReply = ""): string {
|
|
125
|
+
const reason = formatTerminalErrorReason(error);
|
|
126
|
+
return finalReply.trim()
|
|
127
|
+
? `${reason}\n\n以下回复可能不完整:\n\n${finalReply.trim()}`
|
|
128
|
+
: reason;
|
|
129
|
+
}
|