deepccc 0.1.25 → 0.1.27
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 +11 -0
- package/dist/cli.js +5 -0
- package/dist/config.d.ts +5 -0
- package/dist/config.js +2 -0
- package/dist/context.d.ts +1 -0
- package/dist/context.js +12 -3
- package/dist/file-tools.d.ts +18 -0
- package/dist/file-tools.js +22 -0
- package/dist/index.d.ts +22 -0
- package/dist/index.js +200 -87
- package/dist/progress/reducer.js +4 -0
- package/dist/tool-protocol.d.ts +2 -0
- package/dist/tool-protocol.js +17 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -75,6 +75,7 @@ $env:DEEPCCC_STREAMING="true"
|
|
|
75
75
|
"apiKey": "sk-...",
|
|
76
76
|
"baseURL": "https://api.deepseek.com/v1",
|
|
77
77
|
"model": "deepseek-v4-pro",
|
|
78
|
+
"subModel": "",
|
|
78
79
|
"effort": "",
|
|
79
80
|
"streaming": true,
|
|
80
81
|
"contextWindow": 1048576,
|
|
@@ -102,6 +103,16 @@ $env:DEEPCCC_STREAMING="true"
|
|
|
102
103
|
API 拒绝(context length exceeded),实际窗口以模型与所用服务端为准(如 litellm 的
|
|
103
104
|
`max_input_tokens`)。
|
|
104
105
|
|
|
106
|
+
`subModel` 是子模型(选填),默认 `""`(留空跟随主模型)。配置后,DeepCCC 内部的轻量
|
|
107
|
+
环节——上下文压缩摘要生成、`task` 子代理任务——使用子模型执行,主对话仍用主模型。
|
|
108
|
+
典型用法:主模型用 pro 承担复杂推理,子模型用 flash 做高频廉价的摘要与子任务。
|
|
109
|
+
可通过 `DEEPCCC_SUB_MODEL` 环境变量或命令行 `--sub-model` 覆盖。
|
|
110
|
+
|
|
111
|
+
`task` 子代理工具:主模型可把边界清晰的独立子任务(仓库调研、长文档阅读、独立模块生成)
|
|
112
|
+
委派给子代理执行——子代理使用子模型、独立上下文,不污染主对话上下文;结果截断回传。
|
|
113
|
+
子代理**不能再次委派**(禁止嵌套),单轮最多 20 个工具步,超时与主会话压缩超时一致。
|
|
114
|
+
仅在配置了子模型时建议使用(未配置时子代理跟随主模型,节省有限)。
|
|
115
|
+
|
|
105
116
|
`rawStreamLogs.enabled` 默认 `true`,通过 `DEEPCCC_RAW_STREAM_LOGS` 环境变量或配置 JSON 关闭。
|
|
106
117
|
开启时,每次对话的原始流按 gzip JSONL 落到 `~/.deepccc/raw-stream-logs/`,供
|
|
107
118
|
`session_search` 工具在会话被压缩后找回被压缩消息的精确原文(检索时设置
|
package/dist/cli.js
CHANGED
|
@@ -57,6 +57,10 @@ function parseArgs(argv = process.argv.slice(2)) {
|
|
|
57
57
|
config.model = next;
|
|
58
58
|
i++;
|
|
59
59
|
}
|
|
60
|
+
else if (arg === "--sub-model" && next !== undefined) {
|
|
61
|
+
config.subModel = next;
|
|
62
|
+
i++;
|
|
63
|
+
}
|
|
60
64
|
else if (arg === "--effort" && next !== undefined) {
|
|
61
65
|
config.effort = next;
|
|
62
66
|
i++;
|
|
@@ -125,6 +129,7 @@ function printHelp(appConfig) {
|
|
|
125
129
|
"Options:",
|
|
126
130
|
` --provider <name> API protocol: openai or anthropic (current default ${appConfig.provider})`,
|
|
127
131
|
` --model <name> Model name (current default ${appConfig.model})`,
|
|
132
|
+
` --sub-model <name> Sub-model for lightweight steps (compaction/task; empty = follow main model)`,
|
|
128
133
|
` --effort <level> Reasoning effort: none/minimal/low/medium/high/xhigh/max (overrides config.effort)`,
|
|
129
134
|
` --base-url <url> Provider API base URL (current default ${appConfig.baseURL})`,
|
|
130
135
|
" --api-key <key> API key",
|
package/dist/config.d.ts
CHANGED
|
@@ -5,6 +5,11 @@ export interface DeepCccConfig {
|
|
|
5
5
|
apiKey: string;
|
|
6
6
|
baseURL: string;
|
|
7
7
|
model: string;
|
|
8
|
+
/**
|
|
9
|
+
* 子模型(可选):用于内部轻量环节(上下文压缩摘要生成、task 子代理任务)。
|
|
10
|
+
* 留空("")时跟随主模型,行为与旧版完全一致。
|
|
11
|
+
*/
|
|
12
|
+
subModel: string;
|
|
8
13
|
/** Reasoning effort(none/minimal/low/medium/high/xhigh/max),留空不传 reasoning_effort */
|
|
9
14
|
effort: string;
|
|
10
15
|
/** 主对话是否使用流式请求;默认开启 */
|
package/dist/config.js
CHANGED
|
@@ -12,6 +12,7 @@ export const DEFAULT_CONFIG = {
|
|
|
12
12
|
apiKey: "",
|
|
13
13
|
baseURL: "https://api.deepseek.com/v1",
|
|
14
14
|
model: "deepseek-v4-pro",
|
|
15
|
+
subModel: "",
|
|
15
16
|
effort: "",
|
|
16
17
|
streaming: true,
|
|
17
18
|
contextWindow: 1_048_576,
|
|
@@ -66,6 +67,7 @@ function loadConfig() {
|
|
|
66
67
|
apiKey: env("DEEPCCC_API_KEY") ?? env("DEEPSEEK_API_KEY") ?? file.apiKey ?? DEFAULT_CONFIG.apiKey,
|
|
67
68
|
baseURL: env("DEEPCCC_BASE_URL") ?? env("DEEPSEEK_BASE_URL") ?? file.baseURL ?? DEFAULT_CONFIG.baseURL,
|
|
68
69
|
model: env("DEEPCCC_MODEL") ?? env("DEEPSEEK_MODEL") ?? file.model ?? DEFAULT_CONFIG.model,
|
|
70
|
+
subModel: env("DEEPCCC_SUB_MODEL") ?? file.subModel ?? DEFAULT_CONFIG.subModel,
|
|
69
71
|
effort: env("DEEPCCC_EFFORT") ?? env("DEEPSEEK_EFFORT") ?? file.effort ?? DEFAULT_CONFIG.effort,
|
|
70
72
|
streaming: boolEnv("DEEPCCC_STREAMING") ?? file.streaming ?? DEFAULT_CONFIG.streaming,
|
|
71
73
|
contextWindow: numberEnv("DEEPCCC_CONTEXT_WINDOW") ?? file.contextWindow ?? DEFAULT_CONFIG.contextWindow,
|
package/dist/context.d.ts
CHANGED
package/dist/context.js
CHANGED
|
@@ -2,6 +2,7 @@ import { createHash, randomBytes } from "node:crypto";
|
|
|
2
2
|
import { existsSync, mkdirSync, readdirSync, readFileSync, renameSync, writeFileSync } from "node:fs";
|
|
3
3
|
import { homedir } from "node:os";
|
|
4
4
|
import { join } from "node:path";
|
|
5
|
+
import { hasMalformedToolProtocolText } from "./tool-protocol.js";
|
|
5
6
|
export const DEFAULT_BUILTIN_CONTEXT_DIR = join(homedir(), ".deepccc", "sessions");
|
|
6
7
|
/** 默认模型上下文窗口:1M(DeepSeek V4 Pro/Flash 原生规格)。 */
|
|
7
8
|
export const DEFAULT_CONTEXT_WINDOW_TOKENS = 1_048_576;
|
|
@@ -290,14 +291,19 @@ export class BuiltinContextManager {
|
|
|
290
291
|
].join("\n"),
|
|
291
292
|
});
|
|
292
293
|
}
|
|
293
|
-
|
|
294
|
+
// Keep malformed provider output on disk for diagnosis, but quarantine it
|
|
295
|
+
// from future prompts so one protocol failure cannot teach the model to
|
|
296
|
+
// repeat the same invalid tool syntax on every later turn.
|
|
297
|
+
messages.push(...this.modelSafeMessages());
|
|
294
298
|
return messages;
|
|
295
299
|
}
|
|
296
300
|
planCompaction() {
|
|
297
|
-
|
|
301
|
+
// The compaction model is still a model input: exclude quarantined protocol
|
|
302
|
+
// leaks here too, otherwise a later summary could reintroduce the bad syntax.
|
|
303
|
+
const messages = this.modelSafeMessages();
|
|
304
|
+
const estimated = estimateBuiltinContextTokens(this.state.summary, messages);
|
|
298
305
|
if (estimated <= this.compactAtTokens)
|
|
299
306
|
return null;
|
|
300
|
-
const messages = this.state.messages;
|
|
301
307
|
if (messages.length <= 1)
|
|
302
308
|
return null;
|
|
303
309
|
const earliestAllowed = Math.max(0, messages.length - this.keepRecentMessages);
|
|
@@ -340,6 +346,9 @@ export class BuiltinContextManager {
|
|
|
340
346
|
writeFileSync(tmp, content, "utf8");
|
|
341
347
|
renameSync(tmp, this.contextFilePath);
|
|
342
348
|
}
|
|
349
|
+
modelSafeMessages() {
|
|
350
|
+
return this.state.messages.filter((message) => message.role !== "assistant" || !hasMalformedToolProtocolText(message.content));
|
|
351
|
+
}
|
|
343
352
|
load() {
|
|
344
353
|
if (!this.persist || !existsSync(this.contextFilePath))
|
|
345
354
|
return emptyState(this.sessionId, this.cwd);
|
package/dist/file-tools.d.ts
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { type ToolSet } from "ai";
|
|
2
2
|
import { type PermissionGate } from "./permissions.js";
|
|
3
|
+
/** task 子代理工具:子任务结果回传主会话前的最大字符数(防止子代理长输出撑爆主上下文) */
|
|
4
|
+
export declare const MAX_TASK_OUTPUT_CHARS = 32000;
|
|
3
5
|
export interface ReadFileInput {
|
|
4
6
|
path: string;
|
|
5
7
|
startLine?: number;
|
|
@@ -155,5 +157,21 @@ export interface BuiltinFileToolsOptions {
|
|
|
155
157
|
contextDir?: string;
|
|
156
158
|
rawLogsDir?: string;
|
|
157
159
|
};
|
|
160
|
+
/**
|
|
161
|
+
* task 子代理工具的执行回调:把独立子任务委派给子会话(使用子模型、独立上下文)。
|
|
162
|
+
* 未提供时 task 工具不可用(抛出明确错误),主会话之外的工具集不会意外开启子代理。
|
|
163
|
+
*/
|
|
164
|
+
runTask?: TaskRunner;
|
|
165
|
+
}
|
|
166
|
+
export interface TaskRunnerInput {
|
|
167
|
+
/** 子任务描述:包含目标、约束与交付物,子代理将以此作为唯一指令。 */
|
|
168
|
+
description: string;
|
|
169
|
+
/** 可选子任务工作目录(相对主会话 cwd 或绝对路径),默认继承主会话工作目录。 */
|
|
170
|
+
cwd?: string;
|
|
171
|
+
}
|
|
172
|
+
export interface TaskRunnerOutput {
|
|
173
|
+
/** 子代理最终文本回复(已截断到 MAX_TASK_OUTPUT_CHARS,已过隐私替换)。 */
|
|
174
|
+
result: string;
|
|
158
175
|
}
|
|
176
|
+
export type TaskRunner = (input: TaskRunnerInput, signal?: AbortSignal) => Promise<string>;
|
|
159
177
|
export declare function createBuiltinFileTools(cwd: string, options?: BuiltinFileToolsOptions): ToolSet;
|
package/dist/file-tools.js
CHANGED
|
@@ -22,6 +22,8 @@ const SEARCH_TIMEOUT_MS = 15_000;
|
|
|
22
22
|
const MAX_COMMAND_OUTPUT_BYTES = 256 * 1024;
|
|
23
23
|
const DEFAULT_COMMAND_TIMEOUT_MS = 120_000;
|
|
24
24
|
const MAX_COMMAND_TIMEOUT_MS = 900_000;
|
|
25
|
+
/** task 子代理工具:子任务结果回传主会话前的最大字符数(防止子代理长输出撑爆主上下文) */
|
|
26
|
+
export const MAX_TASK_OUTPUT_CHARS = 32_000;
|
|
25
27
|
const requireFromHere = createRequire(import.meta.url);
|
|
26
28
|
const FALLBACK_SKIPPED_DIRECTORIES = new Set([".git", "node_modules"]);
|
|
27
29
|
/**
|
|
@@ -939,6 +941,7 @@ export async function applyPatchForTool(cwd, input) {
|
|
|
939
941
|
}
|
|
940
942
|
export function createBuiltinFileTools(cwd, options = {}) {
|
|
941
943
|
const gate = options.permissionGate;
|
|
944
|
+
const runTask = options.runTask;
|
|
942
945
|
/** 文件路径的候选匹配键:原始路径 + 绝对路径 + 相对 cwd 路径(正/反斜杠双版本,规则可任选其一) */
|
|
943
946
|
const pathKeys = (p) => {
|
|
944
947
|
const abs = resolveToolPath(cwd, p);
|
|
@@ -1023,6 +1026,25 @@ export function createBuiltinFileTools(cwd, options = {}) {
|
|
|
1023
1026
|
return runCommandForTool(cwd, input, options.abortSignal);
|
|
1024
1027
|
},
|
|
1025
1028
|
}),
|
|
1029
|
+
task: tool({
|
|
1030
|
+
description: "把独立子任务委派给子代理执行:子代理使用子模型、拥有独立上下文,不污染主对话上下文。适合边界清晰、可独立交付的调研/代码生成子任务(如扫描整个仓库、阅读长文档、生成独立模块)。子代理不能再次委派任务(禁止嵌套),结果会截断回传。",
|
|
1031
|
+
inputSchema: jsonSchema({
|
|
1032
|
+
type: "object",
|
|
1033
|
+
additionalProperties: false,
|
|
1034
|
+
properties: {
|
|
1035
|
+
description: { type: "string", description: "子任务描述:目标、约束与交付物。请写清楚子代理需要返回什么。" },
|
|
1036
|
+
cwd: { type: "string", description: "可选子任务工作目录(绝对路径或相对主会话 cwd),默认继承主会话工作目录。" },
|
|
1037
|
+
},
|
|
1038
|
+
required: ["description"],
|
|
1039
|
+
}),
|
|
1040
|
+
execute: async (input, execOptions) => {
|
|
1041
|
+
if (!runTask) {
|
|
1042
|
+
throw new Error("task 工具不可用:当前环境未启用子代理执行器");
|
|
1043
|
+
}
|
|
1044
|
+
const result = await runTask(input, execOptions.abortSignal);
|
|
1045
|
+
return { result };
|
|
1046
|
+
},
|
|
1047
|
+
}),
|
|
1026
1048
|
edit_file: tool({
|
|
1027
1049
|
description: "通过精确的 oldText -> newText 替换编辑现有 UTF-8 文本文件。可行时使用 SHA-256 前置条件以避免覆盖并发编辑。",
|
|
1028
1050
|
inputSchema: jsonSchema({
|
package/dist/index.d.ts
CHANGED
|
@@ -28,6 +28,10 @@ export interface ChatSessionConfig {
|
|
|
28
28
|
apiKey?: string;
|
|
29
29
|
/** Model id. Defaults to DEEPCCC_MODEL/config. */
|
|
30
30
|
model?: string;
|
|
31
|
+
/**
|
|
32
|
+
* 子模型 id(可选):用于压缩摘要生成与 task 子代理任务。留空跟随主模型。
|
|
33
|
+
*/
|
|
34
|
+
subModel?: string;
|
|
31
35
|
/**
|
|
32
36
|
* Reasoning effort (none/minimal/low/medium/high/xhigh/max);
|
|
33
37
|
* overrides config.effort; empty omits the reasoning_effort request field.
|
|
@@ -81,6 +85,9 @@ export interface ChatSessionOptions {
|
|
|
81
85
|
export type ChatEvent = {
|
|
82
86
|
type: "status";
|
|
83
87
|
phase: "compacting" | "generating";
|
|
88
|
+
} | {
|
|
89
|
+
type: "progress";
|
|
90
|
+
phase: "reasoning";
|
|
84
91
|
} | {
|
|
85
92
|
type: "compact";
|
|
86
93
|
compactedMessages: number;
|
|
@@ -99,6 +106,8 @@ export type ChatEvent = {
|
|
|
99
106
|
type: "text";
|
|
100
107
|
text: string;
|
|
101
108
|
accumulated: string;
|
|
109
|
+
} | {
|
|
110
|
+
type: "text_reset";
|
|
102
111
|
} | {
|
|
103
112
|
type: "done";
|
|
104
113
|
text: string;
|
|
@@ -115,17 +124,30 @@ interface ChatMessage {
|
|
|
115
124
|
}
|
|
116
125
|
export declare class ChatSession {
|
|
117
126
|
private model;
|
|
127
|
+
/** 子模型实例;未配置 subModel 时与主模型同一实例 */
|
|
128
|
+
private subModel;
|
|
118
129
|
private provider;
|
|
130
|
+
private apiKey;
|
|
131
|
+
private baseURL;
|
|
132
|
+
private modelId;
|
|
133
|
+
private subModelId;
|
|
119
134
|
private cwd;
|
|
120
135
|
private context;
|
|
121
136
|
private compactionTimeoutMs;
|
|
122
137
|
private maxSteps?;
|
|
123
138
|
private effort;
|
|
139
|
+
private permissionMode;
|
|
140
|
+
private permissionResolver?;
|
|
124
141
|
private permissionGate;
|
|
125
142
|
private skillDirs;
|
|
126
143
|
private customSystemPrompt;
|
|
127
144
|
/** 最近一次 chat() 使用的 system prompt(供 history 等读取) */
|
|
128
145
|
private systemPrompt;
|
|
146
|
+
/**
|
|
147
|
+
* task 子代理工具执行器:用子模型开独立子会话(独立上下文、独立 cwd)执行子任务,
|
|
148
|
+
* 回传最终文本(截断 + 隐私替换)。单层:子会话的工具集不包含 runTask,天然禁止嵌套。
|
|
149
|
+
*/
|
|
150
|
+
private runTask;
|
|
129
151
|
constructor(overrides?: ChatSessionConfig, options?: ChatSessionOptions);
|
|
130
152
|
/**
|
|
131
153
|
* 组装系统提示词。顺序遵循“稳定性优先”原则(缓存命中友好):
|
package/dist/index.js
CHANGED
|
@@ -8,13 +8,14 @@ import { createAnthropic } from "@ai-sdk/anthropic";
|
|
|
8
8
|
import { generateText, isLoopFinished, stepCountIs, streamText } from "ai";
|
|
9
9
|
import { existsSync, readFileSync } from "node:fs";
|
|
10
10
|
import { homedir } from "node:os";
|
|
11
|
-
import { join } from "node:path";
|
|
11
|
+
import { isAbsolute, join, resolve } from "node:path";
|
|
12
12
|
import { fileURLToPath } from "node:url";
|
|
13
13
|
import { config as appConfig, normalizeDeepCccProvider, RAW_STREAM_LOGS_DIR, } from "./config.js";
|
|
14
14
|
import { createRawStreamLog, } from "./raw-stream-log.js";
|
|
15
15
|
import { buildPersistedAssistantMessage, buildSummaryPrompt, BuiltinContextManager, defaultBuiltinSessionId, } from "./context.js";
|
|
16
|
-
import { createBuiltinFileTools } from "./file-tools.js";
|
|
16
|
+
import { createBuiltinFileTools, MAX_TASK_OUTPUT_CHARS } from "./file-tools.js";
|
|
17
17
|
import { PermissionGate } from "./permissions.js";
|
|
18
|
+
import { hasMalformedToolProtocolText, TOOL_PROTOCOL_RECOVERY_PROMPT, } from "./tool-protocol.js";
|
|
18
19
|
import { buildDefaultSkillDirs, buildSkillsIndexPrompt, scanSkillsDirs, } from "./skills.js";
|
|
19
20
|
import { applyPrivacy, applyPrivacyToJson } from "./privacy.js";
|
|
20
21
|
// ---------------------------------------------------------------------------
|
|
@@ -76,6 +77,15 @@ const COMPACTION_RECOVERY_HINT_DISABLED = [
|
|
|
76
77
|
].join("\n");
|
|
77
78
|
export const DEFAULT_COMPACTION_TIMEOUT_MS = 5 * 60 * 1000;
|
|
78
79
|
const MAX_COMPACTION_OUTPUT_TOKENS = 16_384;
|
|
80
|
+
/** task 子代理工具:子代理单轮对话的最大工具步数(防失控循环,结果收敛后即结束) */
|
|
81
|
+
const TASK_MAX_STEPS = 20;
|
|
82
|
+
/** 子代理最终输出截断:保留前 MAX_TASK_OUTPUT_CHARS 字符,尾部注明截断信息 */
|
|
83
|
+
function truncateTaskOutput(text) {
|
|
84
|
+
if (text.length <= MAX_TASK_OUTPUT_CHARS)
|
|
85
|
+
return text;
|
|
86
|
+
return (text.slice(0, MAX_TASK_OUTPUT_CHARS) +
|
|
87
|
+
`\n…[子代理输出已截断,共 ${text.length} 字符,仅保留前 ${MAX_TASK_OUTPUT_CHARS} 字符]`);
|
|
88
|
+
}
|
|
79
89
|
const ANTHROPIC_TOOL_JSON_COMPATIBILITY_NOTE = [
|
|
80
90
|
"[Protocol compatibility note]",
|
|
81
91
|
"tool-call arguments use JSON encoding; the final reply does not need to be JSON unless the user requests it.",
|
|
@@ -217,17 +227,71 @@ function normalizeAnthropicBaseURL(baseURL) {
|
|
|
217
227
|
}
|
|
218
228
|
export class ChatSession {
|
|
219
229
|
model;
|
|
230
|
+
/** 子模型实例;未配置 subModel 时与主模型同一实例 */
|
|
231
|
+
subModel;
|
|
220
232
|
provider;
|
|
233
|
+
apiKey;
|
|
234
|
+
baseURL;
|
|
235
|
+
modelId;
|
|
236
|
+
subModelId;
|
|
221
237
|
cwd;
|
|
222
238
|
context;
|
|
223
239
|
compactionTimeoutMs;
|
|
224
240
|
maxSteps;
|
|
225
241
|
effort;
|
|
242
|
+
permissionMode;
|
|
243
|
+
permissionResolver;
|
|
226
244
|
permissionGate;
|
|
227
245
|
skillDirs;
|
|
228
246
|
customSystemPrompt;
|
|
229
247
|
/** 最近一次 chat() 使用的 system prompt(供 history 等读取) */
|
|
230
248
|
systemPrompt = "";
|
|
249
|
+
/**
|
|
250
|
+
* task 子代理工具执行器:用子模型开独立子会话(独立上下文、独立 cwd)执行子任务,
|
|
251
|
+
* 回传最终文本(截断 + 隐私替换)。单层:子会话的工具集不包含 runTask,天然禁止嵌套。
|
|
252
|
+
*/
|
|
253
|
+
runTask = async (input, signal) => {
|
|
254
|
+
const rawCwd = input.cwd?.trim();
|
|
255
|
+
const taskCwd = rawCwd ? (isAbsolute(rawCwd) ? rawCwd : resolve(this.cwd, rawCwd)) : this.cwd;
|
|
256
|
+
const timeoutController = new AbortController();
|
|
257
|
+
const timeout = setTimeout(() => timeoutController.abort(), this.compactionTimeoutMs);
|
|
258
|
+
timeout.unref?.();
|
|
259
|
+
const taskSignal = signal
|
|
260
|
+
? AbortSignal.any([signal, timeoutController.signal])
|
|
261
|
+
: timeoutController.signal;
|
|
262
|
+
try {
|
|
263
|
+
const child = new ChatSession({
|
|
264
|
+
provider: this.provider,
|
|
265
|
+
apiKey: this.apiKey,
|
|
266
|
+
baseURL: this.baseURL,
|
|
267
|
+
model: this.subModelId || this.modelId,
|
|
268
|
+
effort: this.effort,
|
|
269
|
+
}, {
|
|
270
|
+
cwd: taskCwd,
|
|
271
|
+
persist: false,
|
|
272
|
+
permissionMode: this.permissionMode,
|
|
273
|
+
permissionResolver: this.permissionResolver,
|
|
274
|
+
maxSteps: TASK_MAX_STEPS,
|
|
275
|
+
compactionTimeoutMs: this.compactionTimeoutMs,
|
|
276
|
+
skillsDirs: this.skillDirs.map((s) => s.dir),
|
|
277
|
+
});
|
|
278
|
+
let full = "";
|
|
279
|
+
for await (const event of child.chat(input.description, taskSignal)) {
|
|
280
|
+
if (event.type === "text") {
|
|
281
|
+
full += event.text;
|
|
282
|
+
}
|
|
283
|
+
else if (event.type === "error") {
|
|
284
|
+
throw new Error(`task 子代理执行失败: ${event.message}`);
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
if (!full.trim())
|
|
288
|
+
return "(子代理未返回文本内容)";
|
|
289
|
+
return applyPrivacy(truncateTaskOutput(full));
|
|
290
|
+
}
|
|
291
|
+
finally {
|
|
292
|
+
clearTimeout(timeout);
|
|
293
|
+
}
|
|
294
|
+
};
|
|
231
295
|
constructor(overrides = {}, options = {}) {
|
|
232
296
|
const apiKey = overrides.apiKey ?? appConfig.apiKey;
|
|
233
297
|
if (!apiKey) {
|
|
@@ -235,28 +299,32 @@ export class ChatSession {
|
|
|
235
299
|
}
|
|
236
300
|
const baseURL = overrides.baseURL ?? appConfig.baseURL;
|
|
237
301
|
const modelId = overrides.model ?? appConfig.model;
|
|
302
|
+
this.modelId = modelId;
|
|
303
|
+
this.subModelId = (overrides.subModel ?? appConfig.subModel ?? "").trim();
|
|
238
304
|
this.provider = normalizeDeepCccProvider(overrides.provider ?? appConfig.provider);
|
|
239
305
|
this.effort = (overrides.effort ?? appConfig.effort ?? "").trim();
|
|
240
|
-
|
|
241
|
-
|
|
306
|
+
this.apiKey = apiKey;
|
|
307
|
+
this.baseURL = baseURL;
|
|
308
|
+
const provider = this.provider === "anthropic"
|
|
309
|
+
? createAnthropic({
|
|
242
310
|
baseURL: normalizeAnthropicBaseURL(baseURL),
|
|
243
311
|
apiKey,
|
|
244
|
-
})
|
|
245
|
-
|
|
246
|
-
}
|
|
247
|
-
else {
|
|
248
|
-
const provider = createOpenAICompatible({
|
|
312
|
+
})
|
|
313
|
+
: createOpenAICompatible({
|
|
249
314
|
name: "deepccc",
|
|
250
315
|
baseURL,
|
|
251
316
|
apiKey,
|
|
252
317
|
includeUsage: true,
|
|
253
318
|
});
|
|
254
|
-
|
|
255
|
-
|
|
319
|
+
this.model = provider(modelId);
|
|
320
|
+
// 子模型:留空时与主模型共用同一实例(行为与旧版完全一致,零开销)
|
|
321
|
+
this.subModel = this.subModelId ? provider(this.subModelId) : this.model;
|
|
256
322
|
this.cwd = options.cwd ?? process.cwd();
|
|
257
323
|
this.maxSteps = normalizeMaxSteps(options.maxSteps);
|
|
258
324
|
this.compactionTimeoutMs = Math.max(1, options.compactionTimeoutMs ?? DEFAULT_COMPACTION_TIMEOUT_MS);
|
|
259
325
|
this.customSystemPrompt = options.systemPrompt ?? "";
|
|
326
|
+
this.permissionMode = options.permissionMode ?? "ask";
|
|
327
|
+
this.permissionResolver = options.permissionResolver;
|
|
260
328
|
// 技能目录在构造时确定;技能内容在每次 chat() 前重新扫描(mtime 热加载),
|
|
261
329
|
// 因此创建/修改技能后下一次对话自动生效,无需重启。
|
|
262
330
|
this.skillDirs =
|
|
@@ -271,7 +339,7 @@ export class ChatSession {
|
|
|
271
339
|
compactAtTokens: options.compactAtTokens,
|
|
272
340
|
keepRecentMessages: options.keepRecentMessages,
|
|
273
341
|
});
|
|
274
|
-
this.permissionGate = new PermissionGate(
|
|
342
|
+
this.permissionGate = new PermissionGate(this.permissionMode, this.permissionResolver);
|
|
275
343
|
}
|
|
276
344
|
/**
|
|
277
345
|
* 组装系统提示词。顺序遵循“稳定性优先”原则(缓存命中友好):
|
|
@@ -333,7 +401,6 @@ export class ChatSession {
|
|
|
333
401
|
catch (err) {
|
|
334
402
|
console.error(`[DeepCCC raw stream log] create failed: ${errorMessage(err)}`);
|
|
335
403
|
}
|
|
336
|
-
const toolContext = [];
|
|
337
404
|
const maxSteps = this.maxSteps;
|
|
338
405
|
// 每次对话前重新扫描技能索引(并行 + mtime 缓存,开销极小):
|
|
339
406
|
// 新技能/修改的技能在下一次对话自动生效(热加载)。
|
|
@@ -356,97 +423,143 @@ export class ChatSession {
|
|
|
356
423
|
? { deepseek: { reasoningEffort: this.effort } }
|
|
357
424
|
: { anthropic: { effort: this.effort } };
|
|
358
425
|
}
|
|
359
|
-
const
|
|
426
|
+
const baseGenerationOptions = {
|
|
360
427
|
model: this.model,
|
|
361
428
|
system,
|
|
362
|
-
|
|
363
|
-
|
|
429
|
+
tools: createBuiltinFileTools(this.cwd, {
|
|
430
|
+
permissionGate: this.permissionGate,
|
|
431
|
+
runTask: this.runTask,
|
|
432
|
+
}),
|
|
364
433
|
stopWhen: maxSteps !== undefined ? stepCountIs(maxSteps) : isLoopFinished(),
|
|
365
434
|
abortSignal: signal,
|
|
366
435
|
...(effortProviderOptions ? { providerOptions: effortProviderOptions } : {}),
|
|
367
436
|
};
|
|
368
|
-
let
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
437
|
+
for (let attempt = 0; attempt < 2; attempt += 1) {
|
|
438
|
+
fullText = "";
|
|
439
|
+
safeAccumulated = "";
|
|
440
|
+
const toolContext = [];
|
|
441
|
+
toolCallsById.clear();
|
|
442
|
+
toolCallOrder.length = 0;
|
|
443
|
+
let lastReasoningProgressAt;
|
|
444
|
+
const attemptMessages = attempt === 0
|
|
445
|
+
? modelMessages
|
|
446
|
+
: [...modelMessages, { role: "user", content: TOOL_PROTOCOL_RECOVERY_PROMPT }];
|
|
447
|
+
const generationOptions = {
|
|
448
|
+
...baseGenerationOptions,
|
|
449
|
+
messages: attemptMessages,
|
|
450
|
+
};
|
|
451
|
+
let stream;
|
|
452
|
+
if (appConfig.streaming) {
|
|
453
|
+
const result = streamText(generationOptions);
|
|
454
|
+
stream = result.fullStream ?? textStreamToFullStream(result.textStream);
|
|
386
455
|
}
|
|
387
|
-
else
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
toolCallOrder.push(part.toolCallId);
|
|
391
|
-
yield {
|
|
392
|
-
type: "tool_use",
|
|
393
|
-
id: part.toolCallId,
|
|
394
|
-
name: part.toolName,
|
|
395
|
-
input: applyPrivacyToJson(part.input),
|
|
396
|
-
};
|
|
456
|
+
else {
|
|
457
|
+
const result = await generateText(generationOptions);
|
|
458
|
+
stream = generateResultToFullStream(result);
|
|
397
459
|
}
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
460
|
+
for await (const part of stream) {
|
|
461
|
+
rawLog?.writeLine(safeRawStreamJson(part));
|
|
462
|
+
if (part.type === "reasoning-start" || part.type === "reasoning-delta") {
|
|
463
|
+
// Reasoning content remains private. A throttled heartbeat is enough
|
|
464
|
+
// for ChatCCC to distinguish active inference from a stalled stream.
|
|
465
|
+
const now = Date.now();
|
|
466
|
+
if (lastReasoningProgressAt === undefined || now - lastReasoningProgressAt >= 1_000) {
|
|
467
|
+
lastReasoningProgressAt = now;
|
|
468
|
+
yield { type: "progress", phase: "reasoning" };
|
|
469
|
+
}
|
|
403
470
|
}
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
471
|
+
else if (part.type === "text-delta") {
|
|
472
|
+
fullText += part.text;
|
|
473
|
+
// 隐私替换只在展示层:safeAccumulated 供事件消费者(终端/JSONL)使用,
|
|
474
|
+
// fullText 原文用于持久化上下文,避免替换结果回流污染上下文。
|
|
475
|
+
const safeText = applyPrivacy(part.text);
|
|
476
|
+
safeAccumulated += safeText;
|
|
477
|
+
yield { type: "text", text: safeText, accumulated: safeAccumulated };
|
|
478
|
+
}
|
|
479
|
+
else if (part.type === "tool-call") {
|
|
480
|
+
toolContext.push(`tool_call ${part.toolName}: ${safeJson(part.input)}`);
|
|
481
|
+
toolCallsById.set(part.toolCallId, { name: part.toolName, input: safeJson(part.input) });
|
|
482
|
+
toolCallOrder.push(part.toolCallId);
|
|
483
|
+
yield {
|
|
484
|
+
type: "tool_use",
|
|
485
|
+
id: part.toolCallId,
|
|
486
|
+
name: part.toolName,
|
|
487
|
+
input: applyPrivacyToJson(part.input),
|
|
488
|
+
};
|
|
489
|
+
}
|
|
490
|
+
else if (part.type === "tool-result") {
|
|
491
|
+
toolContext.push(`tool_result ${part.toolName}: ${truncateToolContext(safeJson(part.output))}`);
|
|
492
|
+
const call = toolCallsById.get(part.toolCallId);
|
|
493
|
+
if (call)
|
|
494
|
+
call.output = truncateToolContext(safeJson(part.output));
|
|
495
|
+
yield {
|
|
496
|
+
type: "tool_result",
|
|
497
|
+
tool_use_id: part.toolCallId,
|
|
498
|
+
name: part.toolName,
|
|
499
|
+
content: applyPrivacyToJson(part.output),
|
|
500
|
+
is_error: false,
|
|
501
|
+
};
|
|
502
|
+
}
|
|
503
|
+
else if (part.type === "tool-error") {
|
|
504
|
+
const message = errorMessage(part.error);
|
|
505
|
+
toolContext.push(`tool_error ${part.toolName}: ${message}`);
|
|
506
|
+
const call = toolCallsById.get(part.toolCallId);
|
|
507
|
+
if (call) {
|
|
508
|
+
call.output = message;
|
|
509
|
+
call.is_error = true;
|
|
510
|
+
}
|
|
511
|
+
yield {
|
|
512
|
+
type: "tool_result",
|
|
513
|
+
tool_use_id: part.toolCallId,
|
|
514
|
+
name: part.toolName,
|
|
515
|
+
content: applyPrivacy(message),
|
|
516
|
+
is_error: true,
|
|
517
|
+
};
|
|
518
|
+
}
|
|
519
|
+
else if (part.type === "error") {
|
|
520
|
+
const message = errorMessage(part.error);
|
|
521
|
+
yield { type: "error", message: applyPrivacy(message) };
|
|
522
|
+
throw new Error(message);
|
|
419
523
|
}
|
|
420
|
-
yield {
|
|
421
|
-
type: "tool_result",
|
|
422
|
-
tool_use_id: part.toolCallId,
|
|
423
|
-
name: part.toolName,
|
|
424
|
-
content: applyPrivacy(message),
|
|
425
|
-
is_error: true,
|
|
426
|
-
};
|
|
427
524
|
}
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
525
|
+
if (hasMalformedToolProtocolText(fullText)) {
|
|
526
|
+
console.warn(`[DeepCCC] malformed DSML tool output detected for ${this.context.sessionId} `
|
|
527
|
+
+ `(attempt ${attempt + 1}/2, structuredToolCalls=${toolCallOrder.length})`);
|
|
528
|
+
rawLog?.writeLine(safeRawStreamJson({
|
|
529
|
+
type: "deepccc_tool_protocol_recovery",
|
|
530
|
+
attempt: attempt + 1,
|
|
531
|
+
structuredToolCalls: toolCallOrder.length,
|
|
532
|
+
}));
|
|
533
|
+
yield { type: "text_reset" };
|
|
534
|
+
if (attempt === 0 && toolCallOrder.length === 0) {
|
|
535
|
+
yield { type: "status", phase: "generating" };
|
|
536
|
+
continue;
|
|
537
|
+
}
|
|
538
|
+
throw new Error(toolCallOrder.length > 0
|
|
539
|
+
? "工具调用协议异常:检测到混合的结构化与 DSML 文本调用,为避免重复执行工具,本轮已安全终止"
|
|
540
|
+
: "工具调用协议异常:模型重试后仍输出了无效的 DSML 工具调用");
|
|
432
541
|
}
|
|
542
|
+
completed = true;
|
|
543
|
+
const collectedToolCalls = toolCallOrder
|
|
544
|
+
.map((id) => toolCallsById.get(id))
|
|
545
|
+
.filter((call) => call !== undefined);
|
|
546
|
+
this.context.appendMessage(buildPersistedAssistantMessage({
|
|
547
|
+
fullText,
|
|
548
|
+
transcriptLines: toolContext,
|
|
549
|
+
toolCalls: collectedToolCalls,
|
|
550
|
+
}));
|
|
551
|
+
yield { type: "done", text: safeAccumulated };
|
|
552
|
+
return;
|
|
433
553
|
}
|
|
434
|
-
completed = true;
|
|
435
|
-
const collectedToolCalls = toolCallOrder
|
|
436
|
-
.map((id) => toolCallsById.get(id))
|
|
437
|
-
.filter((call) => call !== undefined);
|
|
438
|
-
this.context.appendMessage(buildPersistedAssistantMessage({
|
|
439
|
-
fullText,
|
|
440
|
-
transcriptLines: toolContext,
|
|
441
|
-
toolCalls: collectedToolCalls,
|
|
442
|
-
}));
|
|
443
|
-
yield { type: "done", text: safeAccumulated };
|
|
444
554
|
}
|
|
445
555
|
catch (err) {
|
|
446
556
|
const message = err instanceof Error ? err.message : String(err);
|
|
557
|
+
const malformedProtocolOutput = hasMalformedToolProtocolText(fullText);
|
|
558
|
+
if (malformedProtocolOutput)
|
|
559
|
+
yield { type: "text_reset" };
|
|
447
560
|
if (err.name === "AbortError" || signal?.aborted) {
|
|
448
561
|
// 被中断时,不保存不完整的助手消息
|
|
449
|
-
if (fullText) {
|
|
562
|
+
if (fullText && !malformedProtocolOutput) {
|
|
450
563
|
this.context.appendMessage({ role: "assistant", content: `${fullText}\n[interrupted]` });
|
|
451
564
|
}
|
|
452
565
|
yield { type: "done", text: safeAccumulated };
|
|
@@ -500,7 +613,7 @@ export class ChatSession {
|
|
|
500
613
|
if (!plan)
|
|
501
614
|
return 0;
|
|
502
615
|
const result = await generateText({
|
|
503
|
-
model: this.
|
|
616
|
+
model: this.subModel,
|
|
504
617
|
system: SUMMARY_SYSTEM_PROMPT,
|
|
505
618
|
messages: [{ role: "user", content: buildSummaryPrompt(plan) }],
|
|
506
619
|
abortSignal: compactionSignal,
|
package/dist/progress/reducer.js
CHANGED
|
@@ -57,6 +57,10 @@ export function reduceProgress(prev, event) {
|
|
|
57
57
|
case "text":
|
|
58
58
|
// accumulated 是全文累积,直接全量替换,天然幂等
|
|
59
59
|
return withProgressView(prev, { text: event.accumulated });
|
|
60
|
+
case "progress":
|
|
61
|
+
return withProgressView(prev, { headerTitle: "思考中..." });
|
|
62
|
+
case "text_reset":
|
|
63
|
+
return withProgressView(prev, { text: "", tools: [] });
|
|
60
64
|
case "tool_use": {
|
|
61
65
|
const tool = {
|
|
62
66
|
id: event.id ?? `tool-${prev.tools.length + 1}`,
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Detects provider output that leaked DeepSeek's internal DSML tool-call syntax
|
|
3
|
+
* into normal assistant text. A trailing closing tag is intentionally required:
|
|
4
|
+
* mentioning DSML in prose or a code sample must not trigger recovery.
|
|
5
|
+
*/
|
|
6
|
+
const MALFORMED_DSML_TRAILER = /<\/[||]{2}DSML[||]{2}(?:parameter|invoke)>\s*$/iu;
|
|
7
|
+
const RENDERED_TOOL_CALL = /\[调用\s+[A-Za-z_][\w.-]*\]/u;
|
|
8
|
+
const DSML_INVOKE_TAG = /<[||]{2}DSML[||]{2}invoke\b/iu;
|
|
9
|
+
export function hasMalformedToolProtocolText(text) {
|
|
10
|
+
return MALFORMED_DSML_TRAILER.test(text)
|
|
11
|
+
&& (RENDERED_TOOL_CALL.test(text) || DSML_INVOKE_TAG.test(text));
|
|
12
|
+
}
|
|
13
|
+
export const TOOL_PROTOCOL_RECOVERY_PROMPT = [
|
|
14
|
+
"[系统恢复提示] 上一次响应泄漏了内部工具调用协议,因此已被丢弃。",
|
|
15
|
+
"请重新完成当前用户请求。需要调用工具时,只能使用 API 提供的结构化工具调用;不要把 DSML 或工具参数作为普通文本输出。",
|
|
16
|
+
"不要提及本恢复提示,也不要假装工具已经执行。",
|
|
17
|
+
].join("\n");
|