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.
@@ -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(options.cwd ?? process.cwd()),
129
- cwd: options.cwd ?? process.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
- yield { type: "compact", compactedMessages };
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
- for await (const chunk of result.textStream) {
169
- fullText += chunk;
170
- yield { type: "text", text: chunk, accumulated: fullText };
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
- this.context.appendMessage({ role: "assistant", content: fullText });
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 CODEX_AVATAR_USAGE_STYLE_VERSION = "usage-ring-gray-consumed-v13";
326
- const CURSOR_AVATAR_USAGE_STYLE_VERSION = "usage-battery-v1";
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 : "claude";
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
- const codexWeeklyUsage = normalizedTool === "codex" ? codexUsage?.weekly : null;
765
- const useDynamicCodexAvatar = codexUsage && codexWeeklyUsage;
766
- const useDynamicCursorAvatar = normalizedTool === "cursor" && cursorBatteryPercent !== null;
767
- const basePath = useDynamicCodexAvatar || useDynamicCursorAvatar
768
- ? AVATAR_SOURCES[normalizedStatus]
769
- : avatarCombinationPath(normalizedTool, normalizedStatus);
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,