chatccc 0.2.225 → 0.2.227
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/package.json +1 -1
- package/src/__tests__/builtin-chat-session.test.ts +4 -4
- package/src/__tests__/builtin-config.test.ts +5 -16
- package/src/__tests__/builtin-context.test.ts +1 -1
- package/src/__tests__/builtin-permissions.test.ts +211 -0
- package/src/__tests__/builtin-session-select.test.ts +2 -2
- package/src/__tests__/builtin-skills.test.ts +252 -141
- package/src/adapters/ccc-adapter.ts +3 -0
- package/src/builtin/cli.ts +657 -556
- package/src/builtin/config.ts +84 -0
- package/src/builtin/context.ts +11 -11
- package/src/builtin/file-log.ts +38 -0
- package/src/builtin/file-tools.ts +1407 -1320
- package/src/builtin/index.ts +457 -426
- package/src/builtin/permissions.ts +226 -0
- package/src/builtin/privacy.ts +141 -0
- package/src/builtin/proc-tree-kill.ts +61 -0
- package/src/builtin/progress/cards-helpers.ts +76 -0
- package/src/builtin/progress/reducer.ts +108 -0
- package/src/builtin/progress/terminal-renderer.ts +294 -0
- package/src/builtin/progress/view.ts +77 -0
- package/src/builtin/raw-stream-log.ts +124 -0
- package/src/builtin/session-select.ts +2 -2
- package/src/builtin/skills.ts +190 -108
package/src/builtin/index.ts
CHANGED
|
@@ -1,426 +1,457 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* builtin
|
|
3
|
-
*
|
|
4
|
-
* ChatSession 是程序化入口,既可以被 CLI
|
|
5
|
-
*/
|
|
6
|
-
|
|
7
|
-
import { createOpenAICompatible } from "@ai-sdk/openai-compatible";
|
|
8
|
-
import { generateText, isLoopFinished, stepCountIs, streamText, type TextStreamPart } from "ai";
|
|
9
|
-
import { readFileSync } from "node:fs";
|
|
10
|
-
import { join } from "node:path";
|
|
11
|
-
|
|
12
|
-
import { config as appConfig, RAW_STREAM_LOGS_DIR } from "
|
|
13
|
-
import {
|
|
14
|
-
createRawStreamLog,
|
|
15
|
-
type RawStreamLogHandle,
|
|
16
|
-
} from "
|
|
17
|
-
import {
|
|
18
|
-
BuiltinContextManager,
|
|
19
|
-
buildSummaryPrompt,
|
|
20
|
-
defaultBuiltinSessionId,
|
|
21
|
-
} from "./context.
|
|
22
|
-
import { createBuiltinFileTools } from "./file-tools.
|
|
23
|
-
import {
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
"
|
|
41
|
-
"
|
|
42
|
-
"
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
"
|
|
51
|
-
"
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
return [
|
|
80
|
-
|
|
81
|
-
"
|
|
82
|
-
"
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
/**
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
const
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
const
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
}
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
}
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
}
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
return
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* DeepCCC builtin Agent core API — 同步自 ChatCCC(保留 DeepCCC 英文品牌)
|
|
3
|
+
*
|
|
4
|
+
* ChatSession 是程序化入口,既可以被 CLI 调用,也可以被其他模块调用。
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { createOpenAICompatible } from "@ai-sdk/openai-compatible";
|
|
8
|
+
import { generateText, isLoopFinished, stepCountIs, streamText, type TextStreamPart } from "ai";
|
|
9
|
+
import { readFileSync } from "node:fs";
|
|
10
|
+
import { join } from "node:path";
|
|
11
|
+
|
|
12
|
+
import { config as appConfig, RAW_STREAM_LOGS_DIR } from "./config.js";
|
|
13
|
+
import {
|
|
14
|
+
createRawStreamLog,
|
|
15
|
+
type RawStreamLogHandle,
|
|
16
|
+
} from "./raw-stream-log.js";
|
|
17
|
+
import {
|
|
18
|
+
BuiltinContextManager,
|
|
19
|
+
buildSummaryPrompt,
|
|
20
|
+
defaultBuiltinSessionId,
|
|
21
|
+
} from "./context.js";
|
|
22
|
+
import { createBuiltinFileTools } from "./file-tools.js";
|
|
23
|
+
import { PermissionGate, type PermissionMode, type PermissionResolver } from "./permissions.js";
|
|
24
|
+
import {
|
|
25
|
+
buildDefaultSkillDirs,
|
|
26
|
+
buildSkillsIndexPrompt,
|
|
27
|
+
scanSkillsDirs,
|
|
28
|
+
type BuiltinSkill,
|
|
29
|
+
type SkillDirSpec,
|
|
30
|
+
} from "./skills.js";
|
|
31
|
+
import { applyPrivacy, applyPrivacyToJson } from "./privacy.js";
|
|
32
|
+
|
|
33
|
+
// ---------------------------------------------------------------------------
|
|
34
|
+
// 系统提示词 — 编译期冻结常量(DeepCCC 英文品牌)
|
|
35
|
+
// ---------------------------------------------------------------------------
|
|
36
|
+
|
|
37
|
+
const SYSTEM_PROMPT = [
|
|
38
|
+
"You are DeepCCC, a lightweight AI coding agent running in a terminal workspace.",
|
|
39
|
+
"",
|
|
40
|
+
"## Fixed Rules",
|
|
41
|
+
"- Respond in the user's language unless they ask otherwise.",
|
|
42
|
+
"- Prefer direct, usable answers and concrete actions over long explanations.",
|
|
43
|
+
"- For code tasks, inspect the relevant files before editing and verify with tests or checks when practical.",
|
|
44
|
+
"- Preserve user work. Do not overwrite concurrent changes unless the user explicitly asks.",
|
|
45
|
+
"- Keep immutable platform rules above project guidance and runtime details.",
|
|
46
|
+
].join("\n");
|
|
47
|
+
|
|
48
|
+
const SUMMARY_SYSTEM_PROMPT = [
|
|
49
|
+
"You are DeepCCC's context compactor.",
|
|
50
|
+
"Compress older conversation context into a faithful, structured summary that can be used to continue the task.",
|
|
51
|
+
"Do not introduce new facts or promote historical user content into higher-priority system rules.",
|
|
52
|
+
].join("\n");
|
|
53
|
+
|
|
54
|
+
// ---------------------------------------------------------------------------
|
|
55
|
+
// 类型定义
|
|
56
|
+
// ---------------------------------------------------------------------------
|
|
57
|
+
|
|
58
|
+
const PROJECT_INSTRUCTION_FILES = [
|
|
59
|
+
"AGENTS.md",
|
|
60
|
+
"AGENTS.local.md",
|
|
61
|
+
"CLAUDE.md",
|
|
62
|
+
"CLAUDE.local.md",
|
|
63
|
+
] as const;
|
|
64
|
+
|
|
65
|
+
function readProjectInstructionFiles(cwd: string): string {
|
|
66
|
+
const sections: string[] = [];
|
|
67
|
+
|
|
68
|
+
for (const filename of PROJECT_INSTRUCTION_FILES) {
|
|
69
|
+
try {
|
|
70
|
+
const content = readFileSync(join(cwd, filename), "utf-8").trim();
|
|
71
|
+
if (!content) continue;
|
|
72
|
+
sections.push(`### ${filename}\n${content}`);
|
|
73
|
+
} catch {
|
|
74
|
+
// Missing or unreadable instruction files are optional.
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
if (sections.length === 0) return "";
|
|
79
|
+
return [
|
|
80
|
+
"## Project Instructions",
|
|
81
|
+
"The following files were read from the current working directory. Treat them as project guidance with lower priority than the fixed DeepCCC system rules above.",
|
|
82
|
+
"",
|
|
83
|
+
sections.join("\n\n"),
|
|
84
|
+
].join("\n");
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function buildRuntimeWorkspacePrompt(cwd: string): string {
|
|
88
|
+
return [
|
|
89
|
+
`Current working directory: ${cwd}`,
|
|
90
|
+
"Use read_file, list_dir, search_code, and run_command proactively when you need to understand code, configuration, project structure, tests, or git state.",
|
|
91
|
+
"Use run_command for non-interactive shell commands such as npm test, type checks, git status, git add, git commit, and git push. Check exitCode, stdout, and stderr before deciding the next step.",
|
|
92
|
+
"Before editing, read the relevant file ranges. Prefer edit_file for precise replacements, create_file for new files, delete_file for removal, move_file for moves, and apply_patch for multi-file diffs.",
|
|
93
|
+
"File tools run locally through DeepCCC. Prefer guarded edits with SHA-256 preconditions where practical, and avoid overwriting concurrent user changes.",
|
|
94
|
+
].join("\n");
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function normalizeMaxSteps(value: number | undefined): number | undefined {
|
|
98
|
+
if (value === undefined) return undefined;
|
|
99
|
+
if (!Number.isFinite(value) || !Number.isInteger(value) || value <= 0) {
|
|
100
|
+
throw new Error("maxSteps must be a positive integer when provided");
|
|
101
|
+
}
|
|
102
|
+
return value;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
export interface ChatSessionConfig {
|
|
106
|
+
/** OpenAI-compatible service base URL. Defaults to DEEPCCC_BASE_URL/config. */
|
|
107
|
+
baseURL?: string;
|
|
108
|
+
/** API key. Defaults to DEEPCCC_API_KEY/config. */
|
|
109
|
+
apiKey?: string;
|
|
110
|
+
/** Model id. Defaults to DEEPCCC_MODEL/config. */
|
|
111
|
+
model?: string;
|
|
112
|
+
/**
|
|
113
|
+
* Reasoning effort (none/minimal/low/medium/high/xhigh/max);
|
|
114
|
+
* overrides config.effort; empty omits the reasoning_effort request field.
|
|
115
|
+
*/
|
|
116
|
+
effort?: string;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
export interface ChatSessionOptions {
|
|
120
|
+
/** Session working directory. */
|
|
121
|
+
cwd?: string;
|
|
122
|
+
/** Extra system guidance appended after project instructions. */
|
|
123
|
+
systemPrompt?: string;
|
|
124
|
+
/** Persist context to disk. CLI enables this by default; programmatic usage defaults to false. */
|
|
125
|
+
persist?: boolean;
|
|
126
|
+
/** Context directory. Defaults to ~/.deepccc/sessions. */
|
|
127
|
+
contextDir?: string;
|
|
128
|
+
/** Persistent session id. Defaults to a cwd-derived id when omitted. */
|
|
129
|
+
sessionId?: string;
|
|
130
|
+
/** Compact older context when the rough token estimate exceeds this value. */
|
|
131
|
+
compactAtTokens?: number;
|
|
132
|
+
/** Number of recent raw messages retained after compaction. */
|
|
133
|
+
keepRecentMessages?: number;
|
|
134
|
+
/** Optional tool-step limit. Leave unset for no step limit. */
|
|
135
|
+
maxSteps?: number;
|
|
136
|
+
/**
|
|
137
|
+
* Custom skill directories (<dir>/<name>/SKILL.md). When set, these are
|
|
138
|
+
* scanned with the highest priority (deepccc source). Defaults to the
|
|
139
|
+
* combined Claude/Codex/Cursor/DeepCCC directories (see buildDefaultSkillDirs).
|
|
140
|
+
*/
|
|
141
|
+
skillsDirs?: string[];
|
|
142
|
+
/**
|
|
143
|
+
* 权限模式:ask(默认,高危命令询问)/ bypass(全部放行,等价
|
|
144
|
+
* --dangerously-bypass-permissions;chatccc 等无终端环境集成时使用)。
|
|
145
|
+
*/
|
|
146
|
+
permissionMode?: PermissionMode;
|
|
147
|
+
/**
|
|
148
|
+
* ask 模式下高危操作的交互确认回调;缺省时非交互环境(JSONL / 程序化
|
|
149
|
+
* 调用)自动拒绝高危命令,常规文件操作与低危命令不受影响。
|
|
150
|
+
*/
|
|
151
|
+
permissionResolver?: PermissionResolver;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* 流式响应事件
|
|
156
|
+
*/
|
|
157
|
+
export type ChatEvent =
|
|
158
|
+
| { type: "compact"; compactedMessages: number }
|
|
159
|
+
| { type: "tool_use"; id?: string; name: string; input: unknown }
|
|
160
|
+
| { type: "tool_result"; tool_use_id: string; name?: string; content: unknown; is_error?: boolean }
|
|
161
|
+
| { type: "text"; text: string; accumulated: string }
|
|
162
|
+
| { type: "done"; text: string }
|
|
163
|
+
| { type: "error"; message: string };
|
|
164
|
+
|
|
165
|
+
// ---------------------------------------------------------------------------
|
|
166
|
+
// ChatSession
|
|
167
|
+
// ---------------------------------------------------------------------------
|
|
168
|
+
|
|
169
|
+
/** 消息角色 */
|
|
170
|
+
type MessageRole = "system" | "user" | "assistant" | "tool";
|
|
171
|
+
|
|
172
|
+
/** 内部消息类型 */
|
|
173
|
+
interface ChatMessage {
|
|
174
|
+
role: MessageRole;
|
|
175
|
+
content: string;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
export class ChatSession {
|
|
179
|
+
private model: any;
|
|
180
|
+
private cwd: string;
|
|
181
|
+
private context: BuiltinContextManager;
|
|
182
|
+
private maxSteps?: number;
|
|
183
|
+
private effort: string;
|
|
184
|
+
private permissionGate: PermissionGate;
|
|
185
|
+
private skillDirs: SkillDirSpec[];
|
|
186
|
+
private customSystemPrompt: string;
|
|
187
|
+
/** 最近一次 chat() 使用的 system prompt(供 history 等读取) */
|
|
188
|
+
private systemPrompt = "";
|
|
189
|
+
|
|
190
|
+
constructor(
|
|
191
|
+
overrides: ChatSessionConfig = {},
|
|
192
|
+
options: ChatSessionOptions = {},
|
|
193
|
+
) {
|
|
194
|
+
const apiKey = overrides.apiKey ?? appConfig.apiKey;
|
|
195
|
+
if (!apiKey) {
|
|
196
|
+
throw new Error(
|
|
197
|
+
"DEEPCCC_API_KEY is not set. Configure ~/.deepccc/config.json, set an environment variable, or pass --api-key.",
|
|
198
|
+
);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
const baseURL = overrides.baseURL ?? appConfig.baseURL;
|
|
202
|
+
const modelId = overrides.model ?? appConfig.model;
|
|
203
|
+
this.effort = (overrides.effort ?? appConfig.effort ?? "").trim();
|
|
204
|
+
|
|
205
|
+
const provider = createOpenAICompatible({
|
|
206
|
+
name: "deepccc",
|
|
207
|
+
baseURL,
|
|
208
|
+
apiKey,
|
|
209
|
+
});
|
|
210
|
+
this.model = provider(modelId);
|
|
211
|
+
this.cwd = options.cwd ?? process.cwd();
|
|
212
|
+
this.maxSteps = normalizeMaxSteps(options.maxSteps);
|
|
213
|
+
this.customSystemPrompt = options.systemPrompt ?? "";
|
|
214
|
+
// 技能目录在构造时确定;技能内容在每次 chat() 前重新扫描(mtime 热加载),
|
|
215
|
+
// 因此创建/修改技能后下一次对话自动生效,无需重启。
|
|
216
|
+
this.skillDirs =
|
|
217
|
+
options.skillsDirs?.map((d) => ({ dir: d, source: "deepccc" as const, scope: "project" as const })) ??
|
|
218
|
+
buildDefaultSkillDirs(this.cwd);
|
|
219
|
+
this.context = new BuiltinContextManager({
|
|
220
|
+
persist: options.persist ?? false,
|
|
221
|
+
contextDir: options.contextDir,
|
|
222
|
+
sessionId: options.sessionId ?? defaultBuiltinSessionId(this.cwd),
|
|
223
|
+
cwd: this.cwd,
|
|
224
|
+
compactAtTokens: options.compactAtTokens,
|
|
225
|
+
keepRecentMessages: options.keepRecentMessages,
|
|
226
|
+
});
|
|
227
|
+
this.permissionGate = new PermissionGate(
|
|
228
|
+
options.permissionMode ?? "ask",
|
|
229
|
+
options.permissionResolver,
|
|
230
|
+
);
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
/** 组装系统提示词:固定规则 + 项目指令 + 技能索引 + 用户补充 + 运行时上下文 */
|
|
234
|
+
private buildSystemPrompt(skills: BuiltinSkill[]): string {
|
|
235
|
+
const systemContent = [SYSTEM_PROMPT];
|
|
236
|
+
const projectInstructions = readProjectInstructionFiles(this.cwd);
|
|
237
|
+
if (projectInstructions) {
|
|
238
|
+
systemContent.push("", projectInstructions);
|
|
239
|
+
}
|
|
240
|
+
const skillsPrompt = buildSkillsIndexPrompt(skills);
|
|
241
|
+
if (skillsPrompt) {
|
|
242
|
+
systemContent.push("", skillsPrompt);
|
|
243
|
+
}
|
|
244
|
+
if (this.customSystemPrompt) {
|
|
245
|
+
systemContent.push("", this.customSystemPrompt);
|
|
246
|
+
}
|
|
247
|
+
systemContent.push("", buildRuntimeWorkspacePrompt(this.cwd));
|
|
248
|
+
return systemContent.join("\n");
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
async *chat(
|
|
252
|
+
userMessage: string,
|
|
253
|
+
signal?: AbortSignal,
|
|
254
|
+
): AsyncIterable<ChatEvent> {
|
|
255
|
+
this.context.appendMessage({ role: "user", content: userMessage });
|
|
256
|
+
|
|
257
|
+
let fullText = "";
|
|
258
|
+
let safeAccumulated = "";
|
|
259
|
+
let rawLog: RawStreamLogHandle | null = null;
|
|
260
|
+
let completed = false;
|
|
261
|
+
|
|
262
|
+
try {
|
|
263
|
+
const compactedMessages = await this.compactIfNeeded(signal);
|
|
264
|
+
if (compactedMessages > 0) {
|
|
265
|
+
yield { type: "compact", compactedMessages };
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
const rawLogConfig = appConfig.rawStreamLogs;
|
|
269
|
+
try {
|
|
270
|
+
rawLog = await createRawStreamLog({
|
|
271
|
+
enabled: rawLogConfig.enabled,
|
|
272
|
+
rootDir: RAW_STREAM_LOGS_DIR,
|
|
273
|
+
tool: "deepccc",
|
|
274
|
+
sessionId: this.context.sessionId,
|
|
275
|
+
label: "prompt",
|
|
276
|
+
maxBytesPerTurn: rawLogConfig.maxBytesPerTurn,
|
|
277
|
+
retentionDays: rawLogConfig.retentionDays,
|
|
278
|
+
});
|
|
279
|
+
} catch (err) {
|
|
280
|
+
console.error(`[DeepCCC raw stream log] create failed: ${errorMessage(err)}`);
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
const toolContext: string[] = [];
|
|
284
|
+
const maxSteps = this.maxSteps;
|
|
285
|
+
// 每次对话前重新扫描技能索引(并行 + mtime 缓存,开销极小):
|
|
286
|
+
// 新技能/修改的技能在下一次对话自动生效(热加载)。
|
|
287
|
+
const skills = await scanSkillsDirs(this.skillDirs);
|
|
288
|
+
const system = this.buildSystemPrompt(skills);
|
|
289
|
+
this.systemPrompt = system;
|
|
290
|
+
const result = streamText({
|
|
291
|
+
model: this.model,
|
|
292
|
+
system,
|
|
293
|
+
messages: this.context.buildModelMessages() as any,
|
|
294
|
+
tools: createBuiltinFileTools(this.cwd, { permissionGate: this.permissionGate }),
|
|
295
|
+
stopWhen: maxSteps !== undefined ? stepCountIs(maxSteps) : isLoopFinished(),
|
|
296
|
+
abortSignal: signal,
|
|
297
|
+
// DeepSeek OpenAI 兼容接口:providerOptions.deepseek.reasoningEffort
|
|
298
|
+
// 由 @ai-sdk/openai-compatible 自动映射为请求体 reasoning_effort 字段
|
|
299
|
+
...(this.effort ? { providerOptions: { deepseek: { reasoningEffort: this.effort } } } : {}),
|
|
300
|
+
});
|
|
301
|
+
|
|
302
|
+
const stream = result.fullStream ?? textStreamToFullStream(result.textStream);
|
|
303
|
+
for await (const part of stream as AsyncIterable<TextStreamPart<any>>) {
|
|
304
|
+
rawLog?.writeLine(safeRawStreamJson(part));
|
|
305
|
+
if (part.type === "text-delta") {
|
|
306
|
+
fullText += part.text;
|
|
307
|
+
// 隐私替换只在展示层:safeAccumulated 供事件消费者(终端/JSONL)使用,
|
|
308
|
+
// fullText 原文用于持久化上下文,避免替换结果回流污染上下文。
|
|
309
|
+
const safeText = applyPrivacy(part.text);
|
|
310
|
+
safeAccumulated += safeText;
|
|
311
|
+
yield { type: "text", text: safeText, accumulated: safeAccumulated };
|
|
312
|
+
} else if (part.type === "tool-call") {
|
|
313
|
+
toolContext.push(`tool_call ${part.toolName}: ${safeJson(part.input)}`);
|
|
314
|
+
yield {
|
|
315
|
+
type: "tool_use",
|
|
316
|
+
id: part.toolCallId,
|
|
317
|
+
name: part.toolName,
|
|
318
|
+
input: applyPrivacyToJson(part.input),
|
|
319
|
+
};
|
|
320
|
+
} else if (part.type === "tool-result") {
|
|
321
|
+
toolContext.push(`tool_result ${part.toolName}: ${truncateToolContext(safeJson(part.output))}`);
|
|
322
|
+
yield {
|
|
323
|
+
type: "tool_result",
|
|
324
|
+
tool_use_id: part.toolCallId,
|
|
325
|
+
name: part.toolName,
|
|
326
|
+
content: applyPrivacyToJson(part.output),
|
|
327
|
+
is_error: false,
|
|
328
|
+
};
|
|
329
|
+
} else if (part.type === "tool-error") {
|
|
330
|
+
const message = errorMessage(part.error);
|
|
331
|
+
toolContext.push(`tool_error ${part.toolName}: ${message}`);
|
|
332
|
+
yield {
|
|
333
|
+
type: "tool_result",
|
|
334
|
+
tool_use_id: part.toolCallId,
|
|
335
|
+
name: part.toolName,
|
|
336
|
+
content: applyPrivacy(message),
|
|
337
|
+
is_error: true,
|
|
338
|
+
};
|
|
339
|
+
} else if (part.type === "error") {
|
|
340
|
+
const message = errorMessage(part.error);
|
|
341
|
+
yield { type: "error", message: applyPrivacy(message) };
|
|
342
|
+
throw new Error(message);
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
completed = true;
|
|
346
|
+
|
|
347
|
+
const persistedText = toolContext.length > 0
|
|
348
|
+
? `${fullText}\n\n[Tool transcript]\n${toolContext.join("\n")}`
|
|
349
|
+
: fullText;
|
|
350
|
+
this.context.appendMessage({ role: "assistant", content: persistedText });
|
|
351
|
+
yield { type: "done", text: safeAccumulated };
|
|
352
|
+
} catch (err) {
|
|
353
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
354
|
+
if ((err as Error).name === "AbortError" || signal?.aborted) {
|
|
355
|
+
// 被中断时,不保存不完整的助手消息
|
|
356
|
+
if (fullText) {
|
|
357
|
+
this.context.appendMessage({ role: "assistant", content: `${fullText}\n[interrupted]` });
|
|
358
|
+
}
|
|
359
|
+
yield { type: "done", text: safeAccumulated };
|
|
360
|
+
return;
|
|
361
|
+
}
|
|
362
|
+
yield { type: "error", message: applyPrivacy(message) };
|
|
363
|
+
throw err;
|
|
364
|
+
} finally {
|
|
365
|
+
const rawLogConfig = appConfig.rawStreamLogs;
|
|
366
|
+
await rawLog?.close({
|
|
367
|
+
keep: rawLogConfig.keepCompleted || signal?.aborted === true || !completed,
|
|
368
|
+
});
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
/** 返回当前的会话历史(只读) */
|
|
373
|
+
get history(): ReadonlyArray<ChatMessage> {
|
|
374
|
+
const history: ChatMessage[] = [{ role: "system", content: this.systemPrompt }];
|
|
375
|
+
if (this.context.summary) {
|
|
376
|
+
history.push({
|
|
377
|
+
role: "system",
|
|
378
|
+
content: [
|
|
379
|
+
"Earlier conversation summary:",
|
|
380
|
+
"",
|
|
381
|
+
this.context.summary,
|
|
382
|
+
].join("\n"),
|
|
383
|
+
});
|
|
384
|
+
}
|
|
385
|
+
history.push(...this.context.messages as ChatMessage[]);
|
|
386
|
+
return history;
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
/** 返回当前轮数(不含 system 消息) */
|
|
390
|
+
get turnCount(): number {
|
|
391
|
+
return this.context.totalMessages;
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
/** 清空会话历史,保留 system 消息 */
|
|
395
|
+
reset(): void {
|
|
396
|
+
this.context.reset();
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
private async compactIfNeeded(signal?: AbortSignal): Promise<number> {
|
|
400
|
+
const plan = this.context.planCompaction();
|
|
401
|
+
if (!plan) return 0;
|
|
402
|
+
|
|
403
|
+
const result = await generateText({
|
|
404
|
+
model: this.model,
|
|
405
|
+
system: SUMMARY_SYSTEM_PROMPT,
|
|
406
|
+
messages: [{ role: "user", content: buildSummaryPrompt(plan) }],
|
|
407
|
+
abortSignal: signal,
|
|
408
|
+
});
|
|
409
|
+
|
|
410
|
+
if (!result.text.trim()) return 0;
|
|
411
|
+
|
|
412
|
+
this.context.applyCompaction(result.text, plan);
|
|
413
|
+
return plan.oldMessages.length;
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
async function* textStreamToFullStream(stream: AsyncIterable<string>): AsyncIterable<{ type: "text-delta"; text: string }> {
|
|
418
|
+
for await (const text of stream) {
|
|
419
|
+
yield { type: "text-delta", text };
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
function safeJson(value: unknown): string {
|
|
424
|
+
try {
|
|
425
|
+
return JSON.stringify(value);
|
|
426
|
+
} catch {
|
|
427
|
+
return String(value);
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
function safeRawStreamJson(value: unknown): string {
|
|
432
|
+
try {
|
|
433
|
+
const serialized = JSON.stringify(value, (_key, nested) => {
|
|
434
|
+
if (nested instanceof Error) {
|
|
435
|
+
return {
|
|
436
|
+
name: nested.name,
|
|
437
|
+
message: nested.message,
|
|
438
|
+
};
|
|
439
|
+
}
|
|
440
|
+
return nested;
|
|
441
|
+
});
|
|
442
|
+
return serialized ?? "null";
|
|
443
|
+
} catch (err) {
|
|
444
|
+
return JSON.stringify({
|
|
445
|
+
type: "deepccc_raw_stream_log_serialize_error",
|
|
446
|
+
message: errorMessage(err),
|
|
447
|
+
});
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
function truncateToolContext(value: string): string {
|
|
452
|
+
return value.length > 8000 ? `${value.slice(0, 8000)}...[truncated]` : value;
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
function errorMessage(value: unknown): string {
|
|
456
|
+
return value instanceof Error ? value.message : String(value);
|
|
457
|
+
}
|