chatccc 0.2.193 → 0.2.195
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/config.sample.json +13 -7
- package/package.json +1 -1
- package/src/__tests__/builtin-chat-session.test.ts +105 -0
- package/src/__tests__/builtin-file-tools.test.ts +196 -0
- package/src/__tests__/ccc-adapter.test.ts +40 -0
- package/src/__tests__/config-reload.test.ts +6 -5
- package/src/__tests__/config-sample.test.ts +1 -1
- package/src/__tests__/feishu-avatar.test.ts +40 -6
- package/src/adapters/ccc-adapter.ts +20 -0
- package/src/builtin/cli.ts +20 -0
- package/src/builtin/file-tools.ts +915 -0
- package/src/builtin/index.ts +129 -9
- package/src/config.ts +17 -14
- package/src/feishu-api.ts +35 -31
package/src/builtin/index.ts
CHANGED
|
@@ -5,14 +5,19 @@
|
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
7
|
import { createOpenAICompatible } from "@ai-sdk/openai-compatible";
|
|
8
|
-
import { generateText, streamText } from "ai";
|
|
8
|
+
import { generateText, stepCountIs, streamText, type TextStreamPart } from "ai";
|
|
9
9
|
|
|
10
|
-
import { config as appConfig } from "../config.ts";
|
|
10
|
+
import { config as appConfig, RAW_STREAM_LOGS_DIR } from "../config.ts";
|
|
11
|
+
import {
|
|
12
|
+
createRawStreamLog,
|
|
13
|
+
type RawStreamLogHandle,
|
|
14
|
+
} from "../adapters/raw-stream-log.ts";
|
|
11
15
|
import {
|
|
12
16
|
BuiltinContextManager,
|
|
13
17
|
buildSummaryPrompt,
|
|
14
18
|
defaultBuiltinSessionId,
|
|
15
19
|
} from "./context.ts";
|
|
20
|
+
import { createBuiltinFileTools } from "./file-tools.ts";
|
|
16
21
|
|
|
17
22
|
// ---------------------------------------------------------------------------
|
|
18
23
|
// 系统提示词 — 编译期冻结常量
|
|
@@ -69,6 +74,8 @@ export interface ChatSessionOptions {
|
|
|
69
74
|
*/
|
|
70
75
|
export type ChatEvent =
|
|
71
76
|
| { type: "compact"; compactedMessages: number }
|
|
77
|
+
| { type: "tool_use"; id?: string; name: string; input: unknown }
|
|
78
|
+
| { type: "tool_result"; tool_use_id: string; name?: string; content: unknown; is_error?: boolean }
|
|
72
79
|
| { type: "text"; text: string; accumulated: string }
|
|
73
80
|
| { type: "done"; text: string }
|
|
74
81
|
| { type: "error"; message: string };
|
|
@@ -89,6 +96,7 @@ interface ChatMessage {
|
|
|
89
96
|
export class ChatSession {
|
|
90
97
|
private model: any;
|
|
91
98
|
private systemPrompt: string;
|
|
99
|
+
private cwd: string;
|
|
92
100
|
private context: BuiltinContextManager;
|
|
93
101
|
|
|
94
102
|
constructor(
|
|
@@ -111,6 +119,7 @@ export class ChatSession {
|
|
|
111
119
|
apiKey,
|
|
112
120
|
});
|
|
113
121
|
this.model = provider(modelId);
|
|
122
|
+
this.cwd = options.cwd ?? process.cwd();
|
|
114
123
|
|
|
115
124
|
// 构建系统提示词
|
|
116
125
|
const systemContent = [SYSTEM_PROMPT];
|
|
@@ -119,14 +128,19 @@ export class ChatSession {
|
|
|
119
128
|
}
|
|
120
129
|
if (options?.cwd) {
|
|
121
130
|
systemContent.push("", `当前工作目录: ${options.cwd}`);
|
|
131
|
+
systemContent.push(
|
|
132
|
+
"你可以在需要理解代码、配置或项目结构时主动使用 read_file、list_dir、search_code 工具读取本地文件。",
|
|
133
|
+
"需要修改文件时,优先使用 edit_file 进行精确替换;新建用 create_file,删除用 delete_file,移动用 move_file,多文件 diff 可使用 apply_patch。",
|
|
134
|
+
"文件工具由 ChatCCC 在本机执行;编辑前先读取相关片段,尽量使用 SHA-256 前置条件,避免覆盖用户并发改动。",
|
|
135
|
+
);
|
|
122
136
|
}
|
|
123
137
|
|
|
124
138
|
this.systemPrompt = systemContent.join("\n");
|
|
125
139
|
this.context = new BuiltinContextManager({
|
|
126
140
|
persist: options.persist ?? false,
|
|
127
141
|
contextDir: options.contextDir,
|
|
128
|
-
sessionId: options.sessionId ?? defaultBuiltinSessionId(
|
|
129
|
-
cwd:
|
|
142
|
+
sessionId: options.sessionId ?? defaultBuiltinSessionId(this.cwd),
|
|
143
|
+
cwd: this.cwd,
|
|
130
144
|
compactAtTokens: options.compactAtTokens,
|
|
131
145
|
keepRecentMessages: options.keepRecentMessages,
|
|
132
146
|
});
|
|
@@ -151,26 +165,85 @@ export class ChatSession {
|
|
|
151
165
|
this.context.appendMessage({ role: "user", content: userMessage });
|
|
152
166
|
|
|
153
167
|
let fullText = "";
|
|
168
|
+
let rawLog: RawStreamLogHandle | null = null;
|
|
169
|
+
let completed = false;
|
|
154
170
|
|
|
155
171
|
try {
|
|
156
172
|
const compactedMessages = await this.compactIfNeeded(signal);
|
|
157
173
|
if (compactedMessages > 0) {
|
|
158
|
-
|
|
174
|
+
yield { type: "compact", compactedMessages };
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
const rawLogConfig = appConfig.rawStreamLogs.ccc;
|
|
178
|
+
try {
|
|
179
|
+
rawLog = await createRawStreamLog({
|
|
180
|
+
enabled: rawLogConfig.enabled,
|
|
181
|
+
rootDir: RAW_STREAM_LOGS_DIR,
|
|
182
|
+
tool: "ccc",
|
|
183
|
+
sessionId: this.context.sessionId,
|
|
184
|
+
label: "prompt",
|
|
185
|
+
maxBytesPerTurn: rawLogConfig.maxBytesPerTurn,
|
|
186
|
+
retentionDays: rawLogConfig.retentionDays,
|
|
187
|
+
});
|
|
188
|
+
} catch (err) {
|
|
189
|
+
console.error(`[CCC raw stream log] create failed: ${errorMessage(err)}`);
|
|
159
190
|
}
|
|
160
191
|
|
|
192
|
+
const toolContext: string[] = [];
|
|
161
193
|
const result = streamText({
|
|
162
194
|
model: this.model,
|
|
163
195
|
system: this.systemPrompt,
|
|
164
196
|
messages: this.context.buildModelMessages() as any,
|
|
197
|
+
tools: createBuiltinFileTools(this.cwd),
|
|
198
|
+
stopWhen: stepCountIs(8),
|
|
165
199
|
abortSignal: signal,
|
|
166
200
|
});
|
|
167
201
|
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
202
|
+
const stream = result.fullStream ?? textStreamToFullStream(result.textStream);
|
|
203
|
+
for await (const part of stream as AsyncIterable<TextStreamPart<any>>) {
|
|
204
|
+
rawLog?.writeLine(safeRawStreamJson(part));
|
|
205
|
+
if (part.type === "text-delta") {
|
|
206
|
+
fullText += part.text;
|
|
207
|
+
yield { type: "text", text: part.text, accumulated: fullText };
|
|
208
|
+
} else if (part.type === "tool-call") {
|
|
209
|
+
toolContext.push(`tool_call ${part.toolName}: ${safeJson(part.input)}`);
|
|
210
|
+
yield {
|
|
211
|
+
type: "tool_use",
|
|
212
|
+
id: part.toolCallId,
|
|
213
|
+
name: part.toolName,
|
|
214
|
+
input: part.input,
|
|
215
|
+
};
|
|
216
|
+
} else if (part.type === "tool-result") {
|
|
217
|
+
toolContext.push(`tool_result ${part.toolName}: ${truncateToolContext(safeJson(part.output))}`);
|
|
218
|
+
yield {
|
|
219
|
+
type: "tool_result",
|
|
220
|
+
tool_use_id: part.toolCallId,
|
|
221
|
+
name: part.toolName,
|
|
222
|
+
content: part.output,
|
|
223
|
+
is_error: false,
|
|
224
|
+
};
|
|
225
|
+
} else if (part.type === "tool-error") {
|
|
226
|
+
const message = errorMessage(part.error);
|
|
227
|
+
toolContext.push(`tool_error ${part.toolName}: ${message}`);
|
|
228
|
+
yield {
|
|
229
|
+
type: "tool_result",
|
|
230
|
+
tool_use_id: part.toolCallId,
|
|
231
|
+
name: part.toolName,
|
|
232
|
+
content: message,
|
|
233
|
+
is_error: true,
|
|
234
|
+
};
|
|
235
|
+
} else if (part.type === "error") {
|
|
236
|
+
const message = errorMessage(part.error);
|
|
237
|
+
yield { type: "error", message };
|
|
238
|
+
throw new Error(message);
|
|
239
|
+
}
|
|
171
240
|
}
|
|
241
|
+
completed = true;
|
|
172
242
|
|
|
173
|
-
|
|
243
|
+
const persistedText = toolContext.length > 0
|
|
244
|
+
? `${fullText}\n\n[Tool transcript]\n${toolContext.join("\n")}`
|
|
245
|
+
: fullText;
|
|
246
|
+
this.context.appendMessage({ role: "assistant", content: persistedText });
|
|
174
247
|
yield { type: "done", text: fullText };
|
|
175
248
|
} catch (err) {
|
|
176
249
|
const message = err instanceof Error ? err.message : String(err);
|
|
@@ -184,6 +257,11 @@ export class ChatSession {
|
|
|
184
257
|
}
|
|
185
258
|
yield { type: "error", message };
|
|
186
259
|
throw err;
|
|
260
|
+
} finally {
|
|
261
|
+
const rawLogConfig = appConfig.rawStreamLogs.ccc;
|
|
262
|
+
await rawLog?.close({
|
|
263
|
+
keep: rawLogConfig.keepCompleted || signal?.aborted === true || !completed,
|
|
264
|
+
});
|
|
187
265
|
}
|
|
188
266
|
}
|
|
189
267
|
|
|
@@ -231,3 +309,45 @@ export class ChatSession {
|
|
|
231
309
|
return plan.oldMessages.length;
|
|
232
310
|
}
|
|
233
311
|
}
|
|
312
|
+
|
|
313
|
+
async function* textStreamToFullStream(stream: AsyncIterable<string>): AsyncIterable<{ type: "text-delta"; text: string }> {
|
|
314
|
+
for await (const text of stream) {
|
|
315
|
+
yield { type: "text-delta", text };
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
function safeJson(value: unknown): string {
|
|
320
|
+
try {
|
|
321
|
+
return JSON.stringify(value);
|
|
322
|
+
} catch {
|
|
323
|
+
return String(value);
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
function safeRawStreamJson(value: unknown): string {
|
|
328
|
+
try {
|
|
329
|
+
const serialized = JSON.stringify(value, (_key, nested) => {
|
|
330
|
+
if (nested instanceof Error) {
|
|
331
|
+
return {
|
|
332
|
+
name: nested.name,
|
|
333
|
+
message: nested.message,
|
|
334
|
+
};
|
|
335
|
+
}
|
|
336
|
+
return nested;
|
|
337
|
+
});
|
|
338
|
+
return serialized ?? "null";
|
|
339
|
+
} catch (err) {
|
|
340
|
+
return JSON.stringify({
|
|
341
|
+
type: "chatccc_raw_stream_log_serialize_error",
|
|
342
|
+
message: errorMessage(err),
|
|
343
|
+
});
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
function truncateToolContext(value: string): string {
|
|
348
|
+
return value.length > 8000 ? `${value.slice(0, 8000)}...[truncated]` : value;
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
function errorMessage(value: unknown): string {
|
|
352
|
+
return value instanceof Error ? value.message : String(value);
|
|
353
|
+
}
|
package/src/config.ts
CHANGED
|
@@ -143,11 +143,12 @@ export interface RawStreamAgentLogConfig {
|
|
|
143
143
|
keepCompleted: boolean;
|
|
144
144
|
}
|
|
145
145
|
|
|
146
|
-
export interface RawStreamLogsConfig {
|
|
147
|
-
claude: RawStreamAgentLogConfig;
|
|
148
|
-
cursor: RawStreamAgentLogConfig;
|
|
149
|
-
codex: RawStreamAgentLogConfig;
|
|
150
|
-
|
|
146
|
+
export interface RawStreamLogsConfig {
|
|
147
|
+
claude: RawStreamAgentLogConfig;
|
|
148
|
+
cursor: RawStreamAgentLogConfig;
|
|
149
|
+
codex: RawStreamAgentLogConfig;
|
|
150
|
+
ccc: RawStreamAgentLogConfig;
|
|
151
|
+
}
|
|
151
152
|
|
|
152
153
|
export interface AppConfig {
|
|
153
154
|
feishu: FeishuConfig;
|
|
@@ -423,10 +424,11 @@ function loadConfig(): AppConfig {
|
|
|
423
424
|
port: 18080,
|
|
424
425
|
gitTimeoutSeconds: 180,
|
|
425
426
|
allowInterrupt: false,
|
|
426
|
-
rawStreamLogs: {
|
|
427
|
-
claude: { enabled: false, maxBytesPerTurn: 50 * 1024 * 1024, retentionDays: 7, keepCompleted: false },
|
|
428
|
-
cursor: { enabled: false, maxBytesPerTurn: 50 * 1024 * 1024, retentionDays: 7, keepCompleted: false },
|
|
429
|
-
codex: { enabled: false, maxBytesPerTurn: 50 * 1024 * 1024, retentionDays: 7, keepCompleted: false },
|
|
427
|
+
rawStreamLogs: {
|
|
428
|
+
claude: { enabled: false, maxBytesPerTurn: 50 * 1024 * 1024, retentionDays: 7, keepCompleted: false },
|
|
429
|
+
cursor: { enabled: false, maxBytesPerTurn: 50 * 1024 * 1024, retentionDays: 7, keepCompleted: false },
|
|
430
|
+
codex: { enabled: false, maxBytesPerTurn: 50 * 1024 * 1024, retentionDays: 7, keepCompleted: false },
|
|
431
|
+
ccc: { enabled: false, maxBytesPerTurn: 50 * 1024 * 1024, retentionDays: 7, keepCompleted: false },
|
|
430
432
|
},
|
|
431
433
|
claude: { enabled: false, defaultAgent: true, model: "", subagentModel: "", effort: "", apiKey: "", baseUrl: "", maxTurn: 0 },
|
|
432
434
|
cursor: {
|
|
@@ -605,11 +607,12 @@ function loadConfig(): AppConfig {
|
|
|
605
607
|
port: typeof parsed.port === "number" ? parsed.port : 18080,
|
|
606
608
|
gitTimeoutSeconds: typeof parsed.gitTimeoutSeconds === "number" ? parsed.gitTimeoutSeconds : 180,
|
|
607
609
|
allowInterrupt: typeof parsed.allowInterrupt === "boolean" ? parsed.allowInterrupt : false,
|
|
608
|
-
rawStreamLogs: {
|
|
609
|
-
claude: normalizeRawStreamAgentLogConfig(rawStreamLogsRaw.claude),
|
|
610
|
-
cursor: normalizeRawStreamAgentLogConfig(rawStreamLogsRaw.cursor),
|
|
611
|
-
codex: normalizeRawStreamAgentLogConfig(rawStreamLogsRaw.codex),
|
|
612
|
-
|
|
610
|
+
rawStreamLogs: {
|
|
611
|
+
claude: normalizeRawStreamAgentLogConfig(rawStreamLogsRaw.claude),
|
|
612
|
+
cursor: normalizeRawStreamAgentLogConfig(rawStreamLogsRaw.cursor),
|
|
613
|
+
codex: normalizeRawStreamAgentLogConfig(rawStreamLogsRaw.codex),
|
|
614
|
+
ccc: normalizeRawStreamAgentLogConfig(rawStreamLogsRaw.ccc),
|
|
615
|
+
},
|
|
613
616
|
claude: {
|
|
614
617
|
enabled: claudeEnabled,
|
|
615
618
|
defaultAgent: defaultTool === "claude",
|
package/src/feishu-api.ts
CHANGED
|
@@ -319,11 +319,12 @@ const AVATAR_BADGES: Record<string, string> = {
|
|
|
319
319
|
cursor: resolvePath(AVATAR_BADGE_DIR, "badge_cursor.png"),
|
|
320
320
|
codex: resolvePath(AVATAR_BADGE_DIR, "badge_codex.png"),
|
|
321
321
|
};
|
|
322
|
-
const AVATAR_SIZE = 256;
|
|
323
|
-
const AVATAR_BADGE_SIZE = 92;
|
|
324
|
-
const AVATAR_BADGE_MARGIN = 10;
|
|
325
|
-
const
|
|
326
|
-
const
|
|
322
|
+
const AVATAR_SIZE = 256;
|
|
323
|
+
const AVATAR_BADGE_SIZE = 92;
|
|
324
|
+
const AVATAR_BADGE_MARGIN = 10;
|
|
325
|
+
const PLAIN_AVATAR_TOOL = "plain";
|
|
326
|
+
const CODEX_AVATAR_USAGE_STYLE_VERSION = "usage-ring-gray-consumed-v13";
|
|
327
|
+
const CURSOR_AVATAR_USAGE_STYLE_VERSION = "usage-battery-v1";
|
|
327
328
|
|
|
328
329
|
export interface CodexUsageBalance {
|
|
329
330
|
usedPercent: number;
|
|
@@ -354,17 +355,17 @@ export interface CodexResetConsumeResult {
|
|
|
354
355
|
const avatarKeyCache = new Map<string, string>();
|
|
355
356
|
let avatarKeyCacheLoaded = false;
|
|
356
357
|
|
|
357
|
-
function normalizeAvatarTool(tool: string): string {
|
|
358
|
-
return AVATAR_BADGES[tool] ? tool :
|
|
359
|
-
}
|
|
358
|
+
function normalizeAvatarTool(tool: string): string {
|
|
359
|
+
return AVATAR_BADGES[tool] ? tool : PLAIN_AVATAR_TOOL;
|
|
360
|
+
}
|
|
360
361
|
|
|
361
362
|
function normalizeAvatarStatus(status: string): string {
|
|
362
363
|
return AVATAR_SOURCES[status] ? status : "idle";
|
|
363
364
|
}
|
|
364
365
|
|
|
365
|
-
function avatarCombinationPath(tool: string, status: string): string {
|
|
366
|
-
return resolvePath(AVATAR_COMBINATIONS_DIR, `avatar_${normalizeAvatarTool(tool)}_${normalizeAvatarStatus(status)}.png`);
|
|
367
|
-
}
|
|
366
|
+
function avatarCombinationPath(tool: string, status: string): string {
|
|
367
|
+
return resolvePath(AVATAR_COMBINATIONS_DIR, `avatar_${normalizeAvatarTool(tool)}_${normalizeAvatarStatus(status)}.png`);
|
|
368
|
+
}
|
|
368
369
|
|
|
369
370
|
function avatarCacheKey(
|
|
370
371
|
tool: string,
|
|
@@ -757,16 +758,19 @@ async function renderAvatar(
|
|
|
757
758
|
codexUsage: CodexUsageSummary | null = null,
|
|
758
759
|
cursorBatteryPercent: number | null = null,
|
|
759
760
|
): Promise<{ buffer: Buffer; contentType: string; filename: string }> {
|
|
760
|
-
const normalizedTool = normalizeAvatarTool(tool);
|
|
761
|
-
const normalizedStatus = normalizeAvatarStatus(status);
|
|
762
|
-
const composites: sharp.OverlayOptions[] = [];
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
const
|
|
766
|
-
const
|
|
767
|
-
const
|
|
768
|
-
|
|
769
|
-
|
|
761
|
+
const normalizedTool = normalizeAvatarTool(tool);
|
|
762
|
+
const normalizedStatus = normalizeAvatarStatus(status);
|
|
763
|
+
const composites: sharp.OverlayOptions[] = [];
|
|
764
|
+
const hasAgentBadge = normalizedTool !== PLAIN_AVATAR_TOOL;
|
|
765
|
+
|
|
766
|
+
const codexWeeklyUsage = normalizedTool === "codex" ? codexUsage?.weekly : null;
|
|
767
|
+
const useDynamicCodexAvatar = codexUsage && codexWeeklyUsage;
|
|
768
|
+
const useDynamicCursorAvatar = normalizedTool === "cursor" && cursorBatteryPercent !== null;
|
|
769
|
+
const basePath = useDynamicCodexAvatar || useDynamicCursorAvatar
|
|
770
|
+
? AVATAR_SOURCES[normalizedStatus]
|
|
771
|
+
: hasAgentBadge
|
|
772
|
+
? avatarCombinationPath(normalizedTool, normalizedStatus)
|
|
773
|
+
: AVATAR_SOURCES[normalizedStatus];
|
|
770
774
|
|
|
771
775
|
if (useDynamicCodexAvatar) {
|
|
772
776
|
composites.push(
|
|
@@ -793,16 +797,16 @@ async function renderAvatar(
|
|
|
793
797
|
.jpeg({ quality: 95, progressive: false })
|
|
794
798
|
.toBuffer();
|
|
795
799
|
|
|
796
|
-
return {
|
|
797
|
-
buffer: jpeg,
|
|
798
|
-
contentType: "image/jpeg",
|
|
799
|
-
filename: codexUsage?.weekly
|
|
800
|
-
? `avatar_${normalizedTool}_${normalizedStatus}_week_${codexUsage.weekly.remainingPercent}_5h_${codexUsage.fiveHour.remainingPercent}.jpg`
|
|
801
|
-
: cursorBatteryPercent !== null
|
|
802
|
-
? `avatar_${normalizedTool}_${normalizedStatus}_battery_${cursorBatteryPercent}.jpg`
|
|
803
|
-
: `avatar_${normalizedTool}_${normalizedStatus}.jpg`,
|
|
804
|
-
};
|
|
805
|
-
}
|
|
800
|
+
return {
|
|
801
|
+
buffer: jpeg,
|
|
802
|
+
contentType: "image/jpeg",
|
|
803
|
+
filename: normalizedTool === "codex" && codexUsage?.weekly
|
|
804
|
+
? `avatar_${normalizedTool}_${normalizedStatus}_week_${codexUsage.weekly.remainingPercent}_5h_${codexUsage.fiveHour.remainingPercent}.jpg`
|
|
805
|
+
: normalizedTool === "cursor" && cursorBatteryPercent !== null
|
|
806
|
+
? `avatar_${normalizedTool}_${normalizedStatus}_battery_${cursorBatteryPercent}.jpg`
|
|
807
|
+
: `avatar_${normalizedTool}_${normalizedStatus}.jpg`,
|
|
808
|
+
};
|
|
809
|
+
}
|
|
806
810
|
|
|
807
811
|
async function uploadImage(
|
|
808
812
|
token: string,
|