deepccc 0.1.24 → 0.1.26
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 +19 -1
- package/dist/cli.js +5 -0
- package/dist/config.d.ts +10 -0
- package/dist/config.js +4 -0
- package/dist/context.d.ts +9 -1
- package/dist/context.js +8 -2
- 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 +89 -16
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -75,8 +75,10 @@ $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,
|
|
81
|
+
"contextWindow": 1048576,
|
|
80
82
|
"rawStreamLogs": {
|
|
81
83
|
"enabled": true,
|
|
82
84
|
"maxBytesPerTurn": 1048576,
|
|
@@ -90,11 +92,27 @@ $env:DEEPCCC_STREAMING="true"
|
|
|
90
92
|
`DEEPCCC_PROVIDER` 或命令行 `--provider` 覆盖:
|
|
91
93
|
|
|
92
94
|
- `openai` 使用 OpenAI-compatible Chat Completions 协议,兼容 DeepSeek、OpenAI、LiteLLM、vLLM 等服务。
|
|
93
|
-
- `anthropic` 使用 Anthropic Messages 协议。配置中的 `baseURL`
|
|
95
|
+
- `anthropic` 使用 Anthropic Messages 协议。配置中的 `baseURL` **完全按填写值使用,不自动补 `/v1`**,请填写到完整版本化地址,例如 DeepSeek 官方 Anthropic 端点为 `https://api.deepseek.com/anthropic/v1`(官方 Anthropic SDK 使用的 `https://api.deepseek.com/anthropic` 基址会拼接为 `.../anthropic/v1/messages`)。`effort` 是 OpenAI/DeepSeek 扩展参数,在 Anthropic 模式下不会发送。
|
|
94
96
|
|
|
95
97
|
`streaming` 控制主对话是否使用流式请求,默认 `true`;也可以通过
|
|
96
98
|
`DEEPCCC_STREAMING=true|false` 覆盖。关闭后,终端会在整条模型响应完成后一次性显示结果。
|
|
97
99
|
|
|
100
|
+
`contextWindow` 是模型上下文窗口(token),默认 `1048576`(1M,DeepSeek V4 Pro/Flash
|
|
101
|
+
原生规格);上下文压缩阈值自动 = `contextWindow × 0.8`(超出即把较早消息压缩为摘要)。
|
|
102
|
+
可通过 `DEEPCCC_CONTEXT_WINDOW` 环境变量覆盖。⚠️ 超过模型/服务端实际上限时请求会被
|
|
103
|
+
API 拒绝(context length exceeded),实际窗口以模型与所用服务端为准(如 litellm 的
|
|
104
|
+
`max_input_tokens`)。
|
|
105
|
+
|
|
106
|
+
`subModel` 是子模型(选填),默认 `""`(留空跟随主模型)。配置后,DeepCCC 内部的轻量
|
|
107
|
+
环节——上下文压缩摘要生成、`task` 子代理任务——使用子模型执行,主对话仍用主模型。
|
|
108
|
+
典型用法:主模型用 pro 承担复杂推理,子模型用 flash 做高频廉价的摘要与子任务。
|
|
109
|
+
可通过 `DEEPCCC_SUB_MODEL` 环境变量或命令行 `--sub-model` 覆盖。
|
|
110
|
+
|
|
111
|
+
`task` 子代理工具:主模型可把边界清晰的独立子任务(仓库调研、长文档阅读、独立模块生成)
|
|
112
|
+
委派给子代理执行——子代理使用子模型、独立上下文,不污染主对话上下文;结果截断回传。
|
|
113
|
+
子代理**不能再次委派**(禁止嵌套),单轮最多 20 个工具步,超时与主会话压缩超时一致。
|
|
114
|
+
仅在配置了子模型时建议使用(未配置时子代理跟随主模型,节省有限)。
|
|
115
|
+
|
|
98
116
|
`rawStreamLogs.enabled` 默认 `true`,通过 `DEEPCCC_RAW_STREAM_LOGS` 环境变量或配置 JSON 关闭。
|
|
99
117
|
开启时,每次对话的原始流按 gzip JSONL 落到 `~/.deepccc/raw-stream-logs/`,供
|
|
100
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,10 +5,20 @@ 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
|
/** 主对话是否使用流式请求;默认开启 */
|
|
11
16
|
streaming: boolean;
|
|
17
|
+
/**
|
|
18
|
+
* 模型上下文窗口(token),默认 1048576(1M,DeepSeek V4 Pro/Flash 原生规格)。
|
|
19
|
+
* 上下文压缩阈值自动 = contextWindow × 0.8;超过模型/服务端实际上限会被 API 拒绝。
|
|
20
|
+
*/
|
|
21
|
+
contextWindow: number;
|
|
12
22
|
rawStreamLogs: {
|
|
13
23
|
enabled: boolean;
|
|
14
24
|
maxBytesPerTurn: number;
|
package/dist/config.js
CHANGED
|
@@ -12,8 +12,10 @@ 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,
|
|
18
|
+
contextWindow: 1_048_576,
|
|
17
19
|
rawStreamLogs: {
|
|
18
20
|
// 默认开启:压缩后可通过 session_search(include_raw_logs=true)找回原文。
|
|
19
21
|
// 如需关闭,在 ~/.deepccc/config.json 中设置 rawStreamLogs.enabled=false。
|
|
@@ -65,8 +67,10 @@ function loadConfig() {
|
|
|
65
67
|
apiKey: env("DEEPCCC_API_KEY") ?? env("DEEPSEEK_API_KEY") ?? file.apiKey ?? DEFAULT_CONFIG.apiKey,
|
|
66
68
|
baseURL: env("DEEPCCC_BASE_URL") ?? env("DEEPSEEK_BASE_URL") ?? file.baseURL ?? DEFAULT_CONFIG.baseURL,
|
|
67
69
|
model: env("DEEPCCC_MODEL") ?? env("DEEPSEEK_MODEL") ?? file.model ?? DEFAULT_CONFIG.model,
|
|
70
|
+
subModel: env("DEEPCCC_SUB_MODEL") ?? file.subModel ?? DEFAULT_CONFIG.subModel,
|
|
68
71
|
effort: env("DEEPCCC_EFFORT") ?? env("DEEPSEEK_EFFORT") ?? file.effort ?? DEFAULT_CONFIG.effort,
|
|
69
72
|
streaming: boolEnv("DEEPCCC_STREAMING") ?? file.streaming ?? DEFAULT_CONFIG.streaming,
|
|
73
|
+
contextWindow: numberEnv("DEEPCCC_CONTEXT_WINDOW") ?? file.contextWindow ?? DEFAULT_CONFIG.contextWindow,
|
|
70
74
|
rawStreamLogs: {
|
|
71
75
|
enabled: boolEnv("DEEPCCC_RAW_STREAM_LOGS") ?? rawLogs.enabled ?? DEFAULT_CONFIG.rawStreamLogs.enabled,
|
|
72
76
|
maxBytesPerTurn: numberEnv("DEEPCCC_RAW_STREAM_MAX_BYTES") ?? rawLogs.maxBytesPerTurn ?? DEFAULT_CONFIG.rawStreamLogs.maxBytesPerTurn,
|
package/dist/context.d.ts
CHANGED
|
@@ -48,11 +48,19 @@ export interface BuiltinContextOptions {
|
|
|
48
48
|
contextDir?: string;
|
|
49
49
|
sessionId?: string;
|
|
50
50
|
cwd?: string;
|
|
51
|
+
/** 模型上下文窗口(token)。缺省时压缩阈值 = contextWindow × 0.8。 */
|
|
52
|
+
contextWindow?: number;
|
|
53
|
+
/** 直接指定压缩阈值(token),优先于 contextWindow 的 80% 派生。 */
|
|
51
54
|
compactAtTokens?: number;
|
|
52
55
|
keepRecentMessages?: number;
|
|
53
56
|
}
|
|
54
57
|
export declare const DEFAULT_BUILTIN_CONTEXT_DIR: string;
|
|
55
|
-
|
|
58
|
+
/** 默认模型上下文窗口:1M(DeepSeek V4 Pro/Flash 原生规格)。 */
|
|
59
|
+
export declare const DEFAULT_CONTEXT_WINDOW_TOKENS = 1048576;
|
|
60
|
+
/** 压缩阈值 = 窗口 × 0.8(业界惯例:在窗口接近上限前主动压缩,留出安全余量)。 */
|
|
61
|
+
export declare const COMPACTION_THRESHOLD_RATIO = 0.8;
|
|
62
|
+
/** 默认压缩阈值 = 1M × 0.8。 */
|
|
63
|
+
export declare const DEFAULT_COMPACT_AT_TOKENS: number;
|
|
56
64
|
export declare const DEFAULT_KEEP_RECENT_MESSAGES = 16;
|
|
57
65
|
export declare function normalizeBuiltinSessionId(value: string): string;
|
|
58
66
|
export declare function defaultBuiltinSessionId(cwd?: string): string;
|
package/dist/context.js
CHANGED
|
@@ -3,7 +3,12 @@ import { existsSync, mkdirSync, readdirSync, readFileSync, renameSync, writeFile
|
|
|
3
3
|
import { homedir } from "node:os";
|
|
4
4
|
import { join } from "node:path";
|
|
5
5
|
export const DEFAULT_BUILTIN_CONTEXT_DIR = join(homedir(), ".deepccc", "sessions");
|
|
6
|
-
|
|
6
|
+
/** 默认模型上下文窗口:1M(DeepSeek V4 Pro/Flash 原生规格)。 */
|
|
7
|
+
export const DEFAULT_CONTEXT_WINDOW_TOKENS = 1_048_576;
|
|
8
|
+
/** 压缩阈值 = 窗口 × 0.8(业界惯例:在窗口接近上限前主动压缩,留出安全余量)。 */
|
|
9
|
+
export const COMPACTION_THRESHOLD_RATIO = 0.8;
|
|
10
|
+
/** 默认压缩阈值 = 1M × 0.8。 */
|
|
11
|
+
export const DEFAULT_COMPACT_AT_TOKENS = Math.floor(DEFAULT_CONTEXT_WINDOW_TOKENS * COMPACTION_THRESHOLD_RATIO);
|
|
7
12
|
export const DEFAULT_KEEP_RECENT_MESSAGES = 16;
|
|
8
13
|
const RECENT_CONTEXT_BUDGET_RATIO = 0.6;
|
|
9
14
|
const MAX_COMPACTION_SUMMARY_CHARS = 8_000;
|
|
@@ -247,7 +252,8 @@ export class BuiltinContextManager {
|
|
|
247
252
|
this.contextDir = options.contextDir ?? DEFAULT_BUILTIN_CONTEXT_DIR;
|
|
248
253
|
this.sessionId = normalizeBuiltinSessionId(options.sessionId ?? defaultBuiltinSessionId());
|
|
249
254
|
this.cwd = options.cwd;
|
|
250
|
-
this.compactAtTokens = options.compactAtTokens
|
|
255
|
+
this.compactAtTokens = options.compactAtTokens
|
|
256
|
+
?? Math.floor((options.contextWindow ?? DEFAULT_CONTEXT_WINDOW_TOKENS) * COMPACTION_THRESHOLD_RATIO);
|
|
251
257
|
this.keepRecentMessages = Math.max(1, options.keepRecentMessages ?? DEFAULT_KEEP_RECENT_MESSAGES);
|
|
252
258
|
this.state = this.load();
|
|
253
259
|
}
|
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.
|
|
@@ -47,6 +51,11 @@ export interface ChatSessionOptions {
|
|
|
47
51
|
sessionId?: string;
|
|
48
52
|
/** Compact older context when the rough token estimate exceeds this value. */
|
|
49
53
|
compactAtTokens?: number;
|
|
54
|
+
/**
|
|
55
|
+
* 模型上下文窗口(token),默认 1048576(1M)。压缩阈值自动 = contextWindow × 0.8;
|
|
56
|
+
* 显式 compactAtTokens 优先于该派生值。
|
|
57
|
+
*/
|
|
58
|
+
contextWindow?: number;
|
|
50
59
|
/** Number of recent raw messages retained after compaction. */
|
|
51
60
|
keepRecentMessages?: number;
|
|
52
61
|
/** Hard deadline for all context-compaction passes in one turn. */
|
|
@@ -110,17 +119,30 @@ interface ChatMessage {
|
|
|
110
119
|
}
|
|
111
120
|
export declare class ChatSession {
|
|
112
121
|
private model;
|
|
122
|
+
/** 子模型实例;未配置 subModel 时与主模型同一实例 */
|
|
123
|
+
private subModel;
|
|
113
124
|
private provider;
|
|
125
|
+
private apiKey;
|
|
126
|
+
private baseURL;
|
|
127
|
+
private modelId;
|
|
128
|
+
private subModelId;
|
|
114
129
|
private cwd;
|
|
115
130
|
private context;
|
|
116
131
|
private compactionTimeoutMs;
|
|
117
132
|
private maxSteps?;
|
|
118
133
|
private effort;
|
|
134
|
+
private permissionMode;
|
|
135
|
+
private permissionResolver?;
|
|
119
136
|
private permissionGate;
|
|
120
137
|
private skillDirs;
|
|
121
138
|
private customSystemPrompt;
|
|
122
139
|
/** 最近一次 chat() 使用的 system prompt(供 history 等读取) */
|
|
123
140
|
private systemPrompt;
|
|
141
|
+
/**
|
|
142
|
+
* task 子代理工具执行器:用子模型开独立子会话(独立上下文、独立 cwd)执行子任务,
|
|
143
|
+
* 回传最终文本(截断 + 隐私替换)。单层:子会话的工具集不包含 runTask,天然禁止嵌套。
|
|
144
|
+
*/
|
|
145
|
+
private runTask;
|
|
124
146
|
constructor(overrides?: ChatSessionConfig, options?: ChatSessionOptions);
|
|
125
147
|
/**
|
|
126
148
|
* 组装系统提示词。顺序遵循“稳定性优先”原则(缓存命中友好):
|
package/dist/index.js
CHANGED
|
@@ -8,12 +8,12 @@ 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
18
|
import { buildDefaultSkillDirs, buildSkillsIndexPrompt, scanSkillsDirs, } from "./skills.js";
|
|
19
19
|
import { applyPrivacy, applyPrivacyToJson } from "./privacy.js";
|
|
@@ -76,6 +76,15 @@ const COMPACTION_RECOVERY_HINT_DISABLED = [
|
|
|
76
76
|
].join("\n");
|
|
77
77
|
export const DEFAULT_COMPACTION_TIMEOUT_MS = 5 * 60 * 1000;
|
|
78
78
|
const MAX_COMPACTION_OUTPUT_TOKENS = 16_384;
|
|
79
|
+
/** task 子代理工具:子代理单轮对话的最大工具步数(防失控循环,结果收敛后即结束) */
|
|
80
|
+
const TASK_MAX_STEPS = 20;
|
|
81
|
+
/** 子代理最终输出截断:保留前 MAX_TASK_OUTPUT_CHARS 字符,尾部注明截断信息 */
|
|
82
|
+
function truncateTaskOutput(text) {
|
|
83
|
+
if (text.length <= MAX_TASK_OUTPUT_CHARS)
|
|
84
|
+
return text;
|
|
85
|
+
return (text.slice(0, MAX_TASK_OUTPUT_CHARS) +
|
|
86
|
+
`\n…[子代理输出已截断,共 ${text.length} 字符,仅保留前 ${MAX_TASK_OUTPUT_CHARS} 字符]`);
|
|
87
|
+
}
|
|
79
88
|
const ANTHROPIC_TOOL_JSON_COMPATIBILITY_NOTE = [
|
|
80
89
|
"[Protocol compatibility note]",
|
|
81
90
|
"tool-call arguments use JSON encoding; the final reply does not need to be JSON unless the user requests it.",
|
|
@@ -210,22 +219,78 @@ function normalizeMaxSteps(value) {
|
|
|
210
219
|
return value;
|
|
211
220
|
}
|
|
212
221
|
function normalizeAnthropicBaseURL(baseURL) {
|
|
213
|
-
|
|
214
|
-
|
|
222
|
+
// 完全按用户填写的地址使用,不自动补 /v1(AI SDK 仅对官方 api.anthropic.com
|
|
223
|
+
// 特判补一次 /v1,其他地址原样拼接 /messages)。
|
|
224
|
+
// DeepSeek Anthropic 端点示例:https://api.deepseek.com/anthropic/v1。
|
|
225
|
+
return baseURL.trim().replace(/\/+$/, "");
|
|
215
226
|
}
|
|
216
227
|
export class ChatSession {
|
|
217
228
|
model;
|
|
229
|
+
/** 子模型实例;未配置 subModel 时与主模型同一实例 */
|
|
230
|
+
subModel;
|
|
218
231
|
provider;
|
|
232
|
+
apiKey;
|
|
233
|
+
baseURL;
|
|
234
|
+
modelId;
|
|
235
|
+
subModelId;
|
|
219
236
|
cwd;
|
|
220
237
|
context;
|
|
221
238
|
compactionTimeoutMs;
|
|
222
239
|
maxSteps;
|
|
223
240
|
effort;
|
|
241
|
+
permissionMode;
|
|
242
|
+
permissionResolver;
|
|
224
243
|
permissionGate;
|
|
225
244
|
skillDirs;
|
|
226
245
|
customSystemPrompt;
|
|
227
246
|
/** 最近一次 chat() 使用的 system prompt(供 history 等读取) */
|
|
228
247
|
systemPrompt = "";
|
|
248
|
+
/**
|
|
249
|
+
* task 子代理工具执行器:用子模型开独立子会话(独立上下文、独立 cwd)执行子任务,
|
|
250
|
+
* 回传最终文本(截断 + 隐私替换)。单层:子会话的工具集不包含 runTask,天然禁止嵌套。
|
|
251
|
+
*/
|
|
252
|
+
runTask = async (input, signal) => {
|
|
253
|
+
const rawCwd = input.cwd?.trim();
|
|
254
|
+
const taskCwd = rawCwd ? (isAbsolute(rawCwd) ? rawCwd : resolve(this.cwd, rawCwd)) : this.cwd;
|
|
255
|
+
const timeoutController = new AbortController();
|
|
256
|
+
const timeout = setTimeout(() => timeoutController.abort(), this.compactionTimeoutMs);
|
|
257
|
+
timeout.unref?.();
|
|
258
|
+
const taskSignal = signal
|
|
259
|
+
? AbortSignal.any([signal, timeoutController.signal])
|
|
260
|
+
: timeoutController.signal;
|
|
261
|
+
try {
|
|
262
|
+
const child = new ChatSession({
|
|
263
|
+
provider: this.provider,
|
|
264
|
+
apiKey: this.apiKey,
|
|
265
|
+
baseURL: this.baseURL,
|
|
266
|
+
model: this.subModelId || this.modelId,
|
|
267
|
+
effort: this.effort,
|
|
268
|
+
}, {
|
|
269
|
+
cwd: taskCwd,
|
|
270
|
+
persist: false,
|
|
271
|
+
permissionMode: this.permissionMode,
|
|
272
|
+
permissionResolver: this.permissionResolver,
|
|
273
|
+
maxSteps: TASK_MAX_STEPS,
|
|
274
|
+
compactionTimeoutMs: this.compactionTimeoutMs,
|
|
275
|
+
skillsDirs: this.skillDirs.map((s) => s.dir),
|
|
276
|
+
});
|
|
277
|
+
let full = "";
|
|
278
|
+
for await (const event of child.chat(input.description, taskSignal)) {
|
|
279
|
+
if (event.type === "text") {
|
|
280
|
+
full += event.text;
|
|
281
|
+
}
|
|
282
|
+
else if (event.type === "error") {
|
|
283
|
+
throw new Error(`task 子代理执行失败: ${event.message}`);
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
if (!full.trim())
|
|
287
|
+
return "(子代理未返回文本内容)";
|
|
288
|
+
return applyPrivacy(truncateTaskOutput(full));
|
|
289
|
+
}
|
|
290
|
+
finally {
|
|
291
|
+
clearTimeout(timeout);
|
|
292
|
+
}
|
|
293
|
+
};
|
|
229
294
|
constructor(overrides = {}, options = {}) {
|
|
230
295
|
const apiKey = overrides.apiKey ?? appConfig.apiKey;
|
|
231
296
|
if (!apiKey) {
|
|
@@ -233,28 +298,32 @@ export class ChatSession {
|
|
|
233
298
|
}
|
|
234
299
|
const baseURL = overrides.baseURL ?? appConfig.baseURL;
|
|
235
300
|
const modelId = overrides.model ?? appConfig.model;
|
|
301
|
+
this.modelId = modelId;
|
|
302
|
+
this.subModelId = (overrides.subModel ?? appConfig.subModel ?? "").trim();
|
|
236
303
|
this.provider = normalizeDeepCccProvider(overrides.provider ?? appConfig.provider);
|
|
237
304
|
this.effort = (overrides.effort ?? appConfig.effort ?? "").trim();
|
|
238
|
-
|
|
239
|
-
|
|
305
|
+
this.apiKey = apiKey;
|
|
306
|
+
this.baseURL = baseURL;
|
|
307
|
+
const provider = this.provider === "anthropic"
|
|
308
|
+
? createAnthropic({
|
|
240
309
|
baseURL: normalizeAnthropicBaseURL(baseURL),
|
|
241
310
|
apiKey,
|
|
242
|
-
})
|
|
243
|
-
|
|
244
|
-
}
|
|
245
|
-
else {
|
|
246
|
-
const provider = createOpenAICompatible({
|
|
311
|
+
})
|
|
312
|
+
: createOpenAICompatible({
|
|
247
313
|
name: "deepccc",
|
|
248
314
|
baseURL,
|
|
249
315
|
apiKey,
|
|
250
316
|
includeUsage: true,
|
|
251
317
|
});
|
|
252
|
-
|
|
253
|
-
|
|
318
|
+
this.model = provider(modelId);
|
|
319
|
+
// 子模型:留空时与主模型共用同一实例(行为与旧版完全一致,零开销)
|
|
320
|
+
this.subModel = this.subModelId ? provider(this.subModelId) : this.model;
|
|
254
321
|
this.cwd = options.cwd ?? process.cwd();
|
|
255
322
|
this.maxSteps = normalizeMaxSteps(options.maxSteps);
|
|
256
323
|
this.compactionTimeoutMs = Math.max(1, options.compactionTimeoutMs ?? DEFAULT_COMPACTION_TIMEOUT_MS);
|
|
257
324
|
this.customSystemPrompt = options.systemPrompt ?? "";
|
|
325
|
+
this.permissionMode = options.permissionMode ?? "ask";
|
|
326
|
+
this.permissionResolver = options.permissionResolver;
|
|
258
327
|
// 技能目录在构造时确定;技能内容在每次 chat() 前重新扫描(mtime 热加载),
|
|
259
328
|
// 因此创建/修改技能后下一次对话自动生效,无需重启。
|
|
260
329
|
this.skillDirs =
|
|
@@ -265,10 +334,11 @@ export class ChatSession {
|
|
|
265
334
|
contextDir: options.contextDir,
|
|
266
335
|
sessionId: options.sessionId ?? defaultBuiltinSessionId(this.cwd),
|
|
267
336
|
cwd: this.cwd,
|
|
337
|
+
contextWindow: options.contextWindow ?? appConfig.contextWindow,
|
|
268
338
|
compactAtTokens: options.compactAtTokens,
|
|
269
339
|
keepRecentMessages: options.keepRecentMessages,
|
|
270
340
|
});
|
|
271
|
-
this.permissionGate = new PermissionGate(
|
|
341
|
+
this.permissionGate = new PermissionGate(this.permissionMode, this.permissionResolver);
|
|
272
342
|
}
|
|
273
343
|
/**
|
|
274
344
|
* 组装系统提示词。顺序遵循“稳定性优先”原则(缓存命中友好):
|
|
@@ -357,7 +427,10 @@ export class ChatSession {
|
|
|
357
427
|
model: this.model,
|
|
358
428
|
system,
|
|
359
429
|
messages: modelMessages,
|
|
360
|
-
tools: createBuiltinFileTools(this.cwd, {
|
|
430
|
+
tools: createBuiltinFileTools(this.cwd, {
|
|
431
|
+
permissionGate: this.permissionGate,
|
|
432
|
+
runTask: this.runTask,
|
|
433
|
+
}),
|
|
361
434
|
stopWhen: maxSteps !== undefined ? stepCountIs(maxSteps) : isLoopFinished(),
|
|
362
435
|
abortSignal: signal,
|
|
363
436
|
...(effortProviderOptions ? { providerOptions: effortProviderOptions } : {}),
|
|
@@ -497,7 +570,7 @@ export class ChatSession {
|
|
|
497
570
|
if (!plan)
|
|
498
571
|
return 0;
|
|
499
572
|
const result = await generateText({
|
|
500
|
-
model: this.
|
|
573
|
+
model: this.subModel,
|
|
501
574
|
system: SUMMARY_SYSTEM_PROMPT,
|
|
502
575
|
messages: [{ role: "user", content: buildSummaryPrompt(plan) }],
|
|
503
576
|
abortSignal: compactionSignal,
|