deepccc 0.1.26 → 0.1.28

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 CHANGED
@@ -46,8 +46,10 @@ deepccc 的本地缓存优化实测命中率 **96.7%**,有效降低重复请
46
46
  ```bash
47
47
  export DEEPCCC_API_KEY="sk-..."
48
48
  export DEEPCCC_BASE_URL="https://api.deepseek.com/v1"
49
- export DEEPCCC_MODEL="deepseek-v4-pro"
50
- export DEEPCCC_STREAMING="true"
49
+ export DEEPCCC_MODEL="deepseek-v4-pro"
50
+ export DEEPCCC_EFFORT="high"
51
+ export DEEPCCC_MAX_OUTPUT_TOKENS="32768"
52
+ export DEEPCCC_STREAMING="true"
51
53
  ```
52
54
 
53
55
  Windows PowerShell:
@@ -56,8 +58,10 @@ Windows PowerShell:
56
58
  $env:DEEPCCC_API_KEY="sk-..."
57
59
  $env:DEEPCCC_PROVIDER="openai"
58
60
  $env:DEEPCCC_BASE_URL="https://api.deepseek.com/v1"
59
- $env:DEEPCCC_MODEL="deepseek-v4-pro"
60
- $env:DEEPCCC_STREAMING="true"
61
+ $env:DEEPCCC_MODEL="deepseek-v4-pro"
62
+ $env:DEEPCCC_EFFORT="high"
63
+ $env:DEEPCCC_MAX_OUTPUT_TOKENS="32768"
64
+ $env:DEEPCCC_STREAMING="true"
61
65
  ```
62
66
 
63
67
  也兼容这些 DeepSeek 别名:
@@ -75,10 +79,18 @@ $env:DEEPCCC_STREAMING="true"
75
79
  "apiKey": "sk-...",
76
80
  "baseURL": "https://api.deepseek.com/v1",
77
81
  "model": "deepseek-v4-pro",
78
- "subModel": "",
79
- "effort": "",
80
- "streaming": true,
82
+ "subModel": "",
83
+ "effort": "",
84
+ "maxOutputTokens": null,
85
+ "streaming": true,
81
86
  "contextWindow": 1048576,
87
+ "git": {
88
+ "coAuthor": {
89
+ "enabled": true,
90
+ "name": "DeepCCC",
91
+ "email": "20184052+wzj998@users.noreply.github.com"
92
+ }
93
+ },
82
94
  "rawStreamLogs": {
83
95
  "enabled": true,
84
96
  "maxBytesPerTurn": 1048576,
@@ -88,14 +100,24 @@ $env:DEEPCCC_STREAMING="true"
88
100
  }
89
101
  ```
90
102
 
103
+ `git.coAuthor.enabled` 默认开启。DeepCCC 通过 `run_command` 创建 Git 提交时会保留用户为
104
+ 主 Author,并追加 `Co-authored-by: DeepCCC <20184052+wzj998@users.noreply.github.com>`。
105
+ 可设为 `false` 或用 `DEEPCCC_GIT_COAUTHOR=false` 全局关闭。ChatCCC 的
106
+ `ccc.gitCoAuthor` 是三态 override:`null`/缺失跟随这里,`true` 强制开启,`false` 强制关闭。
107
+
91
108
  `provider` 可选 `openai` 或 `anthropic`,默认 `openai`,也可以通过
92
109
  `DEEPCCC_PROVIDER` 或命令行 `--provider` 覆盖:
93
110
 
94
111
  - `openai` 使用 OpenAI-compatible Chat Completions 协议,兼容 DeepSeek、OpenAI、LiteLLM、vLLM 等服务。
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 模式下不会发送。
112
+ - `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-compatible 模式映射为 `reasoning_effort`,在 Anthropic 模式映射为 `output_config.effort`;目标服务不支持时应留空。
96
113
 
97
- `streaming` 控制主对话是否使用流式请求,默认 `true`;也可以通过
98
- `DEEPCCC_STREAMING=true|false` 覆盖。关闭后,终端会在整条模型响应完成后一次性显示结果。
114
+ `streaming` 控制主对话是否使用流式请求,默认 `true`;也可以通过
115
+ `DEEPCCC_STREAMING=true|false` 覆盖。关闭后,终端会在整条模型响应完成后一次性显示结果。
116
+
117
+ `maxOutputTokens` 限制主对话单次最大输出 token,默认不配置(`null`/缺失),此时不向
118
+ Provider 发送 `max_tokens`,使用模型服务端默认值。可通过 `DEEPCCC_MAX_OUTPUT_TOKENS`
119
+ 或命令行 `--max-output-tokens` 覆盖;只接受正整数。该限制会同时覆盖模型思考内容、
120
+ 工具参数和最终回答,设置过小可能导致工具调用或长回复被截断。
99
121
 
100
122
  `contextWindow` 是模型上下文窗口(token),默认 `1048576`(1M,DeepSeek V4 Pro/Flash
101
123
  原生规格);上下文压缩阈值自动 = `contextWindow × 0.8`(超出即把较早消息压缩为摘要)。
@@ -166,7 +188,15 @@ deepccc --max-steps 20
166
188
  deepccc --effort high
167
189
  ```
168
190
 
169
- 可选值:`none` / `minimal` / `low` / `medium` / `high` / `xhigh` / `max`(留空则不传 `reasoning_effort` 请求字段)。
191
+ 可选值:`none` / `minimal` / `low` / `medium` / `high` / `xhigh` / `max`(留空则不传 `reasoning_effort` 请求字段)。
192
+
193
+ 限制主对话最大输出 token:
194
+
195
+ ```bash
196
+ deepccc --max-output-tokens 8192
197
+ ```
198
+
199
+ 不设置时使用 Provider 默认值。
170
200
 
171
201
  ## 权限机制
172
202
 
package/dist/cli.js CHANGED
@@ -65,6 +65,10 @@ function parseArgs(argv = process.argv.slice(2)) {
65
65
  config.effort = next;
66
66
  i++;
67
67
  }
68
+ else if (arg === "--max-output-tokens" && next !== undefined) {
69
+ config.maxOutputTokens = parsePositiveIntegerOption("--max-output-tokens", next);
70
+ i++;
71
+ }
68
72
  else if (arg === "--base-url" && next !== undefined) {
69
73
  config.baseURL = next;
70
74
  i++;
@@ -131,6 +135,7 @@ function printHelp(appConfig) {
131
135
  ` --model <name> Model name (current default ${appConfig.model})`,
132
136
  ` --sub-model <name> Sub-model for lightweight steps (compaction/task; empty = follow main model)`,
133
137
  ` --effort <level> Reasoning effort: none/minimal/low/medium/high/xhigh/max (overrides config.effort)`,
138
+ ` --max-output-tokens <n> Maximum output tokens (unset = Provider default)`,
134
139
  ` --base-url <url> Provider API base URL (current default ${appConfig.baseURL})`,
135
140
  " --api-key <key> API key",
136
141
  " --cwd <path> Working directory",
package/dist/config.d.ts CHANGED
@@ -12,6 +12,8 @@ export interface DeepCccConfig {
12
12
  subModel: string;
13
13
  /** Reasoning effort(none/minimal/low/medium/high/xhigh/max),留空不传 reasoning_effort */
14
14
  effort: string;
15
+ /** 主对话最大输出 token;未配置时不传,使用 Provider 默认值。 */
16
+ maxOutputTokens?: number;
15
17
  /** 主对话是否使用流式请求;默认开启 */
16
18
  streaming: boolean;
17
19
  /**
@@ -19,6 +21,13 @@ export interface DeepCccConfig {
19
21
  * 上下文压缩阈值自动 = contextWindow × 0.8;超过模型/服务端实际上限会被 API 拒绝。
20
22
  */
21
23
  contextWindow: number;
24
+ git: {
25
+ coAuthor: {
26
+ enabled: boolean;
27
+ name: string;
28
+ email: string;
29
+ };
30
+ };
22
31
  rawStreamLogs: {
23
32
  enabled: boolean;
24
33
  maxBytesPerTurn: number;
package/dist/config.js CHANGED
@@ -16,6 +16,13 @@ export const DEFAULT_CONFIG = {
16
16
  effort: "",
17
17
  streaming: true,
18
18
  contextWindow: 1_048_576,
19
+ git: {
20
+ coAuthor: {
21
+ enabled: true,
22
+ name: "DeepCCC",
23
+ email: "20184052+wzj998@users.noreply.github.com",
24
+ },
25
+ },
19
26
  rawStreamLogs: {
20
27
  // 默认开启:压缩后可通过 session_search(include_raw_logs=true)找回原文。
21
28
  // 如需关闭,在 ~/.deepccc/config.json 中设置 rawStreamLogs.enabled=false。
@@ -49,6 +56,12 @@ function numberEnv(name) {
49
56
  const value = Number(env(name));
50
57
  return Number.isFinite(value) && value >= 0 ? value : undefined;
51
58
  }
59
+ function optionalPositiveInteger(value) {
60
+ if (value === undefined || value === null || value === "")
61
+ return undefined;
62
+ const parsed = Number(value);
63
+ return Number.isInteger(parsed) && parsed > 0 ? parsed : undefined;
64
+ }
52
65
  export function normalizeDeepCccProvider(value) {
53
66
  if (value === undefined || value === null || String(value).trim() === "")
54
67
  return "openai";
@@ -62,6 +75,8 @@ function loadConfig() {
62
75
  const rawLogs = file.rawStreamLogs && typeof file.rawStreamLogs === "object"
63
76
  ? file.rawStreamLogs
64
77
  : {};
78
+ const git = file.git && typeof file.git === "object" ? file.git : {};
79
+ const coAuthor = git.coAuthor && typeof git.coAuthor === "object" ? git.coAuthor : {};
65
80
  return {
66
81
  provider: normalizeDeepCccProvider(env("DEEPCCC_PROVIDER") ?? file.provider ?? DEFAULT_CONFIG.provider),
67
82
  apiKey: env("DEEPCCC_API_KEY") ?? env("DEEPSEEK_API_KEY") ?? file.apiKey ?? DEFAULT_CONFIG.apiKey,
@@ -69,8 +84,17 @@ function loadConfig() {
69
84
  model: env("DEEPCCC_MODEL") ?? env("DEEPSEEK_MODEL") ?? file.model ?? DEFAULT_CONFIG.model,
70
85
  subModel: env("DEEPCCC_SUB_MODEL") ?? file.subModel ?? DEFAULT_CONFIG.subModel,
71
86
  effort: env("DEEPCCC_EFFORT") ?? env("DEEPSEEK_EFFORT") ?? file.effort ?? DEFAULT_CONFIG.effort,
87
+ maxOutputTokens: optionalPositiveInteger(env("DEEPCCC_MAX_OUTPUT_TOKENS"))
88
+ ?? optionalPositiveInteger(file.maxOutputTokens),
72
89
  streaming: boolEnv("DEEPCCC_STREAMING") ?? file.streaming ?? DEFAULT_CONFIG.streaming,
73
90
  contextWindow: numberEnv("DEEPCCC_CONTEXT_WINDOW") ?? file.contextWindow ?? DEFAULT_CONFIG.contextWindow,
91
+ git: {
92
+ coAuthor: {
93
+ enabled: boolEnv("DEEPCCC_GIT_COAUTHOR") ?? coAuthor.enabled ?? DEFAULT_CONFIG.git.coAuthor.enabled,
94
+ name: coAuthor.name?.trim() || DEFAULT_CONFIG.git.coAuthor.name,
95
+ email: coAuthor.email?.trim() || DEFAULT_CONFIG.git.coAuthor.email,
96
+ },
97
+ },
74
98
  rawStreamLogs: {
75
99
  enabled: boolEnv("DEEPCCC_RAW_STREAM_LOGS") ?? rawLogs.enabled ?? DEFAULT_CONFIG.rawStreamLogs.enabled,
76
100
  maxBytesPerTurn: numberEnv("DEEPCCC_RAW_STREAM_MAX_BYTES") ?? rawLogs.maxBytesPerTurn ?? DEFAULT_CONFIG.rawStreamLogs.maxBytesPerTurn,
package/dist/context.d.ts CHANGED
@@ -111,5 +111,6 @@ export declare class BuiltinContextManager {
111
111
  applyCompaction(summary: string, plan: BuiltinCompactionPlan): void;
112
112
  reset(): void;
113
113
  save(): void;
114
+ private modelSafeMessages;
114
115
  private load;
115
116
  }
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
- messages.push(...this.state.messages);
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
- const estimated = estimateBuiltinContextTokens(this.state.summary, this.state.messages);
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);
@@ -71,6 +71,13 @@ export interface RunCommandOutput {
71
71
  truncated: boolean;
72
72
  durationMs: number;
73
73
  }
74
+ export interface GitCoAuthorOptions {
75
+ enabled: boolean;
76
+ name: string;
77
+ email: string;
78
+ }
79
+ /** Adds DeepCCC attribution to git commit commands without replacing the user's author. */
80
+ export declare function withGitCoAuthor(command: string, coAuthor?: GitCoAuthorOptions): string;
74
81
  export interface FileEdit {
75
82
  oldText: string;
76
83
  newText: string;
@@ -143,7 +150,7 @@ export declare function expandHomePath(value: string): string;
143
150
  export declare function readFileForTool(cwd: string, input: ReadFileInput): Promise<ReadFileOutput>;
144
151
  export declare function listDirForTool(cwd: string, input?: ListDirInput): Promise<ListDirOutput>;
145
152
  export declare function searchCodeForTool(cwd: string, input: SearchCodeInput, signal?: AbortSignal, runtimeOptions?: SearchCodeRuntimeOptions): Promise<SearchCodeOutput>;
146
- export declare function runCommandForTool(cwd: string, input: RunCommandInput, abortSignal?: AbortSignal): Promise<RunCommandOutput>;
153
+ export declare function runCommandForTool(cwd: string, input: RunCommandInput, abortSignal?: AbortSignal, coAuthor?: GitCoAuthorOptions): Promise<RunCommandOutput>;
147
154
  export declare function editFileForTool(cwd: string, input: EditFileInput): Promise<EditFileOutput>;
148
155
  export declare function createFileForTool(cwd: string, input: CreateFileInput): Promise<FileWriteOutput>;
149
156
  export declare function deleteFileForTool(cwd: string, input: DeleteFileInput): Promise<DeleteFileOutput>;
@@ -152,6 +159,8 @@ export declare function applyPatchForTool(cwd: string, input: ApplyPatchInput):
152
159
  export interface BuiltinFileToolsOptions {
153
160
  /** 权限门控:副作用工具(run_command/文件写操作)执行前会先经过 gate.check */
154
161
  permissionGate?: PermissionGate;
162
+ /** Git commits created by DeepCCC receive this Co-authored-by trailer when enabled. */
163
+ gitCoAuthor?: GitCoAuthorOptions;
155
164
  /** session_search 工具的会话/原始日志目录(默认 ~/.deepccc/sessions 与 raw-stream-logs;测试可注入) */
156
165
  sessionSearch?: {
157
166
  contextDir?: string;
@@ -26,6 +26,13 @@ const MAX_COMMAND_TIMEOUT_MS = 900_000;
26
26
  export const MAX_TASK_OUTPUT_CHARS = 32_000;
27
27
  const requireFromHere = createRequire(import.meta.url);
28
28
  const FALLBACK_SKIPPED_DIRECTORIES = new Set([".git", "node_modules"]);
29
+ /** Adds DeepCCC attribution to git commit commands without replacing the user's author. */
30
+ export function withGitCoAuthor(command, coAuthor) {
31
+ if (!coAuthor?.enabled || command.includes(coAuthor.email))
32
+ return command;
33
+ const trailer = ` --trailer "Co-authored-by: ${coAuthor.name} <${coAuthor.email}>"`;
34
+ return command.replace(/(^|(?:&&|\|\||;|\|)\s*)(git\s+commit)\b/g, (_match, prefix, gitCommit) => `${prefix}${gitCommit}${trailer}`);
35
+ }
29
36
  /**
30
37
  * 把 `~` / `~/x`(含反斜杠 `~\\x`)展开为用户主目录绝对路径。
31
38
  * `~user/x` 形式不展开(Node 无内置支持),保持原样交给后续解析。
@@ -676,8 +683,8 @@ export async function searchCodeForTool(cwd, input, signal, runtimeOptions = {})
676
683
  : fallback.truncated,
677
684
  };
678
685
  }
679
- export async function runCommandForTool(cwd, input, abortSignal) {
680
- const command = input.command?.trim();
686
+ export async function runCommandForTool(cwd, input, abortSignal, coAuthor) {
687
+ const command = withGitCoAuthor(input.command?.trim(), coAuthor);
681
688
  if (!command)
682
689
  throw new Error("command is required");
683
690
  const commandCwd = resolveToolPath(cwd, input.cwd);
@@ -1016,14 +1023,14 @@ export function createBuiltinFileTools(cwd, options = {}) {
1016
1023
  },
1017
1024
  required: ["command"],
1018
1025
  }),
1019
- execute: async (input, options) => {
1026
+ execute: async (input, execOptions) => {
1020
1027
  await guard({
1021
1028
  tool: "run_command",
1022
1029
  action: input.command,
1023
1030
  reason: isDangerousCommand(input.command) ? "high-risk" : "rule",
1024
1031
  detail: `运行命令: ${input.command}`,
1025
1032
  });
1026
- return runCommandForTool(cwd, input, options.abortSignal);
1033
+ return runCommandForTool(cwd, input, execOptions.abortSignal, options.gitCoAuthor);
1027
1034
  },
1028
1035
  }),
1029
1036
  task: tool({
package/dist/index.d.ts CHANGED
@@ -37,6 +37,8 @@ export interface ChatSessionConfig {
37
37
  * overrides config.effort; empty omits the reasoning_effort request field.
38
38
  */
39
39
  effort?: string;
40
+ /** Maximum output tokens for the main conversation; unset uses the Provider default. */
41
+ maxOutputTokens?: number;
40
42
  }
41
43
  export interface ChatSessionOptions {
42
44
  /** Session working directory. */
@@ -78,6 +80,8 @@ export interface ChatSessionOptions {
78
80
  * 调用)自动拒绝高危命令,常规文件操作与低危命令不受影响。
79
81
  */
80
82
  permissionResolver?: PermissionResolver;
83
+ /** Overrides ~/.deepccc/config.json git.coAuthor.enabled for this integration. */
84
+ gitCoAuthor?: boolean;
81
85
  }
82
86
  /**
83
87
  * 流式响应事件
@@ -85,6 +89,9 @@ export interface ChatSessionOptions {
85
89
  export type ChatEvent = {
86
90
  type: "status";
87
91
  phase: "compacting" | "generating";
92
+ } | {
93
+ type: "progress";
94
+ phase: "reasoning";
88
95
  } | {
89
96
  type: "compact";
90
97
  compactedMessages: number;
@@ -103,6 +110,8 @@ export type ChatEvent = {
103
110
  type: "text";
104
111
  text: string;
105
112
  accumulated: string;
113
+ } | {
114
+ type: "text_reset";
106
115
  } | {
107
116
  type: "done";
108
117
  text: string;
@@ -131,9 +140,11 @@ export declare class ChatSession {
131
140
  private compactionTimeoutMs;
132
141
  private maxSteps?;
133
142
  private effort;
143
+ private maxOutputTokens?;
134
144
  private permissionMode;
135
145
  private permissionResolver?;
136
146
  private permissionGate;
147
+ private gitCoAuthorEnabled;
137
148
  private skillDirs;
138
149
  private customSystemPrompt;
139
150
  /** 最近一次 chat() 使用的 system prompt(供 history 等读取) */
package/dist/index.js CHANGED
@@ -15,6 +15,7 @@ import { createRawStreamLog, } from "./raw-stream-log.js";
15
15
  import { buildPersistedAssistantMessage, buildSummaryPrompt, BuiltinContextManager, defaultBuiltinSessionId, } from "./context.js";
16
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,7 @@ 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
+ const OPENAI_COMPATIBLE_PROVIDER_NAME = "deepccc";
79
81
  /** task 子代理工具:子代理单轮对话的最大工具步数(防失控循环,结果收敛后即结束) */
80
82
  const TASK_MAX_STEPS = 20;
81
83
  /** 子代理最终输出截断:保留前 MAX_TASK_OUTPUT_CHARS 字符,尾部注明截断信息 */
@@ -218,6 +220,14 @@ function normalizeMaxSteps(value) {
218
220
  }
219
221
  return value;
220
222
  }
223
+ function normalizeMaxOutputTokens(value) {
224
+ if (value === undefined)
225
+ return undefined;
226
+ if (!Number.isFinite(value) || !Number.isInteger(value) || value <= 0) {
227
+ throw new Error("maxOutputTokens must be a positive integer when provided");
228
+ }
229
+ return value;
230
+ }
221
231
  function normalizeAnthropicBaseURL(baseURL) {
222
232
  // 完全按用户填写的地址使用,不自动补 /v1(AI SDK 仅对官方 api.anthropic.com
223
233
  // 特判补一次 /v1,其他地址原样拼接 /messages)。
@@ -238,9 +248,11 @@ export class ChatSession {
238
248
  compactionTimeoutMs;
239
249
  maxSteps;
240
250
  effort;
251
+ maxOutputTokens;
241
252
  permissionMode;
242
253
  permissionResolver;
243
254
  permissionGate;
255
+ gitCoAuthorEnabled;
244
256
  skillDirs;
245
257
  customSystemPrompt;
246
258
  /** 最近一次 chat() 使用的 system prompt(供 history 等读取) */
@@ -265,6 +277,7 @@ export class ChatSession {
265
277
  baseURL: this.baseURL,
266
278
  model: this.subModelId || this.modelId,
267
279
  effort: this.effort,
280
+ maxOutputTokens: this.maxOutputTokens,
268
281
  }, {
269
282
  cwd: taskCwd,
270
283
  persist: false,
@@ -302,6 +315,7 @@ export class ChatSession {
302
315
  this.subModelId = (overrides.subModel ?? appConfig.subModel ?? "").trim();
303
316
  this.provider = normalizeDeepCccProvider(overrides.provider ?? appConfig.provider);
304
317
  this.effort = (overrides.effort ?? appConfig.effort ?? "").trim();
318
+ this.maxOutputTokens = normalizeMaxOutputTokens(overrides.maxOutputTokens ?? appConfig.maxOutputTokens);
305
319
  this.apiKey = apiKey;
306
320
  this.baseURL = baseURL;
307
321
  const provider = this.provider === "anthropic"
@@ -310,7 +324,7 @@ export class ChatSession {
310
324
  apiKey,
311
325
  })
312
326
  : createOpenAICompatible({
313
- name: "deepccc",
327
+ name: OPENAI_COMPATIBLE_PROVIDER_NAME,
314
328
  baseURL,
315
329
  apiKey,
316
330
  includeUsage: true,
@@ -324,6 +338,7 @@ export class ChatSession {
324
338
  this.customSystemPrompt = options.systemPrompt ?? "";
325
339
  this.permissionMode = options.permissionMode ?? "ask";
326
340
  this.permissionResolver = options.permissionResolver;
341
+ this.gitCoAuthorEnabled = options.gitCoAuthor ?? appConfig.git.coAuthor.enabled;
327
342
  // 技能目录在构造时确定;技能内容在每次 chat() 前重新扫描(mtime 热加载),
328
343
  // 因此创建/修改技能后下一次对话自动生效,无需重启。
329
344
  this.skillDirs =
@@ -400,7 +415,6 @@ export class ChatSession {
400
415
  catch (err) {
401
416
  console.error(`[DeepCCC raw stream log] create failed: ${errorMessage(err)}`);
402
417
  }
403
- const toolContext = [];
404
418
  const maxSteps = this.maxSteps;
405
419
  // 每次对话前重新扫描技能索引(并行 + mtime 缓存,开销极小):
406
420
  // 新技能/修改的技能在下一次对话自动生效(热加载)。
@@ -413,110 +427,160 @@ export class ChatSession {
413
427
  ? addAnthropicToolJsonCompatibilityNote(hintedMessages)
414
428
  : hintedMessages;
415
429
  // effort 按协议映射:
416
- // - OpenAI 兼容:providerOptions.deepseek.reasoningEffort 由 @ai-sdk/openai-compatible
417
- // 自动映射为请求体 reasoning_effort 字段(DeepSeek 原生支持);
430
+ // - OpenAI 兼容:providerOptions 的 key 必须与 createOpenAICompatible.name 一致;
431
+ // reasoningEffort 由 SDK 映射为请求体 reasoning_effort 字段;
418
432
  // - Anthropic:providerOptions.anthropic.effort 由 @ai-sdk/anthropic 组装为请求体
419
433
  // output_config.effort(官方 Effort API,见 platform.claude.com/docs/en/build-with-claude/effort)
420
434
  let effortProviderOptions;
421
435
  if (this.effort) {
422
436
  effortProviderOptions = this.provider === "openai"
423
- ? { deepseek: { reasoningEffort: this.effort } }
437
+ ? { [OPENAI_COMPATIBLE_PROVIDER_NAME]: { reasoningEffort: this.effort } }
424
438
  : { anthropic: { effort: this.effort } };
425
439
  }
426
- const generationOptions = {
440
+ const baseGenerationOptions = {
427
441
  model: this.model,
428
442
  system,
429
- messages: modelMessages,
430
443
  tools: createBuiltinFileTools(this.cwd, {
431
444
  permissionGate: this.permissionGate,
432
445
  runTask: this.runTask,
446
+ gitCoAuthor: {
447
+ ...appConfig.git.coAuthor,
448
+ enabled: this.gitCoAuthorEnabled,
449
+ },
433
450
  }),
434
451
  stopWhen: maxSteps !== undefined ? stepCountIs(maxSteps) : isLoopFinished(),
435
452
  abortSignal: signal,
453
+ ...(this.maxOutputTokens !== undefined
454
+ ? { maxOutputTokens: this.maxOutputTokens }
455
+ : {}),
436
456
  ...(effortProviderOptions ? { providerOptions: effortProviderOptions } : {}),
437
457
  };
438
- let stream;
439
- if (appConfig.streaming) {
440
- const result = streamText(generationOptions);
441
- stream = result.fullStream ?? textStreamToFullStream(result.textStream);
442
- }
443
- else {
444
- const result = await generateText(generationOptions);
445
- stream = generateResultToFullStream(result);
446
- }
447
- for await (const part of stream) {
448
- rawLog?.writeLine(safeRawStreamJson(part));
449
- if (part.type === "text-delta") {
450
- fullText += part.text;
451
- // 隐私替换只在展示层:safeAccumulated 供事件消费者(终端/JSONL)使用,
452
- // fullText 原文用于持久化上下文,避免替换结果回流污染上下文。
453
- const safeText = applyPrivacy(part.text);
454
- safeAccumulated += safeText;
455
- yield { type: "text", text: safeText, accumulated: safeAccumulated };
458
+ for (let attempt = 0; attempt < 2; attempt += 1) {
459
+ fullText = "";
460
+ safeAccumulated = "";
461
+ const toolContext = [];
462
+ toolCallsById.clear();
463
+ toolCallOrder.length = 0;
464
+ let lastReasoningProgressAt;
465
+ const attemptMessages = attempt === 0
466
+ ? modelMessages
467
+ : [...modelMessages, { role: "user", content: TOOL_PROTOCOL_RECOVERY_PROMPT }];
468
+ const generationOptions = {
469
+ ...baseGenerationOptions,
470
+ messages: attemptMessages,
471
+ };
472
+ let stream;
473
+ if (appConfig.streaming) {
474
+ const result = streamText(generationOptions);
475
+ stream = result.fullStream ?? textStreamToFullStream(result.textStream);
456
476
  }
457
- else if (part.type === "tool-call") {
458
- toolContext.push(`tool_call ${part.toolName}: ${safeJson(part.input)}`);
459
- toolCallsById.set(part.toolCallId, { name: part.toolName, input: safeJson(part.input) });
460
- toolCallOrder.push(part.toolCallId);
461
- yield {
462
- type: "tool_use",
463
- id: part.toolCallId,
464
- name: part.toolName,
465
- input: applyPrivacyToJson(part.input),
466
- };
477
+ else {
478
+ const result = await generateText(generationOptions);
479
+ stream = generateResultToFullStream(result);
467
480
  }
468
- else if (part.type === "tool-result") {
469
- toolContext.push(`tool_result ${part.toolName}: ${truncateToolContext(safeJson(part.output))}`);
470
- const call = toolCallsById.get(part.toolCallId);
471
- if (call) {
472
- call.output = truncateToolContext(safeJson(part.output));
481
+ for await (const part of stream) {
482
+ rawLog?.writeLine(safeRawStreamJson(part));
483
+ if (part.type === "reasoning-start" || part.type === "reasoning-delta") {
484
+ // Reasoning content remains private. A throttled heartbeat is enough
485
+ // for ChatCCC to distinguish active inference from a stalled stream.
486
+ const now = Date.now();
487
+ if (lastReasoningProgressAt === undefined || now - lastReasoningProgressAt >= 1_000) {
488
+ lastReasoningProgressAt = now;
489
+ yield { type: "progress", phase: "reasoning" };
490
+ }
473
491
  }
474
- yield {
475
- type: "tool_result",
476
- tool_use_id: part.toolCallId,
477
- name: part.toolName,
478
- content: applyPrivacyToJson(part.output),
479
- is_error: false,
480
- };
481
- }
482
- else if (part.type === "tool-error") {
483
- const message = errorMessage(part.error);
484
- toolContext.push(`tool_error ${part.toolName}: ${message}`);
485
- const call = toolCallsById.get(part.toolCallId);
486
- if (call) {
487
- call.output = message;
488
- call.is_error = true;
492
+ else if (part.type === "text-delta") {
493
+ fullText += part.text;
494
+ // 隐私替换只在展示层:safeAccumulated 供事件消费者(终端/JSONL)使用,
495
+ // fullText 原文用于持久化上下文,避免替换结果回流污染上下文。
496
+ const safeText = applyPrivacy(part.text);
497
+ safeAccumulated += safeText;
498
+ yield { type: "text", text: safeText, accumulated: safeAccumulated };
499
+ }
500
+ else if (part.type === "tool-call") {
501
+ toolContext.push(`tool_call ${part.toolName}: ${safeJson(part.input)}`);
502
+ toolCallsById.set(part.toolCallId, { name: part.toolName, input: safeJson(part.input) });
503
+ toolCallOrder.push(part.toolCallId);
504
+ yield {
505
+ type: "tool_use",
506
+ id: part.toolCallId,
507
+ name: part.toolName,
508
+ input: applyPrivacyToJson(part.input),
509
+ };
510
+ }
511
+ else if (part.type === "tool-result") {
512
+ toolContext.push(`tool_result ${part.toolName}: ${truncateToolContext(safeJson(part.output))}`);
513
+ const call = toolCallsById.get(part.toolCallId);
514
+ if (call)
515
+ call.output = truncateToolContext(safeJson(part.output));
516
+ yield {
517
+ type: "tool_result",
518
+ tool_use_id: part.toolCallId,
519
+ name: part.toolName,
520
+ content: applyPrivacyToJson(part.output),
521
+ is_error: false,
522
+ };
523
+ }
524
+ else if (part.type === "tool-error") {
525
+ const message = errorMessage(part.error);
526
+ toolContext.push(`tool_error ${part.toolName}: ${message}`);
527
+ const call = toolCallsById.get(part.toolCallId);
528
+ if (call) {
529
+ call.output = message;
530
+ call.is_error = true;
531
+ }
532
+ yield {
533
+ type: "tool_result",
534
+ tool_use_id: part.toolCallId,
535
+ name: part.toolName,
536
+ content: applyPrivacy(message),
537
+ is_error: true,
538
+ };
539
+ }
540
+ else if (part.type === "error") {
541
+ const message = errorMessage(part.error);
542
+ yield { type: "error", message: applyPrivacy(message) };
543
+ throw new Error(message);
489
544
  }
490
- yield {
491
- type: "tool_result",
492
- tool_use_id: part.toolCallId,
493
- name: part.toolName,
494
- content: applyPrivacy(message),
495
- is_error: true,
496
- };
497
545
  }
498
- else if (part.type === "error") {
499
- const message = errorMessage(part.error);
500
- yield { type: "error", message: applyPrivacy(message) };
501
- throw new Error(message);
546
+ if (hasMalformedToolProtocolText(fullText)) {
547
+ console.warn(`[DeepCCC] malformed DSML tool output detected for ${this.context.sessionId} `
548
+ + `(attempt ${attempt + 1}/2, structuredToolCalls=${toolCallOrder.length})`);
549
+ rawLog?.writeLine(safeRawStreamJson({
550
+ type: "deepccc_tool_protocol_recovery",
551
+ attempt: attempt + 1,
552
+ structuredToolCalls: toolCallOrder.length,
553
+ }));
554
+ yield { type: "text_reset" };
555
+ if (attempt === 0 && toolCallOrder.length === 0) {
556
+ yield { type: "status", phase: "generating" };
557
+ continue;
558
+ }
559
+ throw new Error(toolCallOrder.length > 0
560
+ ? "工具调用协议异常:检测到混合的结构化与 DSML 文本调用,为避免重复执行工具,本轮已安全终止"
561
+ : "工具调用协议异常:模型重试后仍输出了无效的 DSML 工具调用");
502
562
  }
563
+ completed = true;
564
+ const collectedToolCalls = toolCallOrder
565
+ .map((id) => toolCallsById.get(id))
566
+ .filter((call) => call !== undefined);
567
+ this.context.appendMessage(buildPersistedAssistantMessage({
568
+ fullText,
569
+ transcriptLines: toolContext,
570
+ toolCalls: collectedToolCalls,
571
+ }));
572
+ yield { type: "done", text: safeAccumulated };
573
+ return;
503
574
  }
504
- completed = true;
505
- const collectedToolCalls = toolCallOrder
506
- .map((id) => toolCallsById.get(id))
507
- .filter((call) => call !== undefined);
508
- this.context.appendMessage(buildPersistedAssistantMessage({
509
- fullText,
510
- transcriptLines: toolContext,
511
- toolCalls: collectedToolCalls,
512
- }));
513
- yield { type: "done", text: safeAccumulated };
514
575
  }
515
576
  catch (err) {
516
577
  const message = err instanceof Error ? err.message : String(err);
578
+ const malformedProtocolOutput = hasMalformedToolProtocolText(fullText);
579
+ if (malformedProtocolOutput)
580
+ yield { type: "text_reset" };
517
581
  if (err.name === "AbortError" || signal?.aborted) {
518
582
  // 被中断时,不保存不完整的助手消息
519
- if (fullText) {
583
+ if (fullText && !malformedProtocolOutput) {
520
584
  this.context.appendMessage({ role: "assistant", content: `${fullText}\n[interrupted]` });
521
585
  }
522
586
  yield { type: "done", text: safeAccumulated };
@@ -581,7 +645,7 @@ export class ChatSession {
581
645
  // output_config.effort=low),避免继承主对话的高 effort 拖慢"压缩上下文中"阶段。
582
646
  maxOutputTokens: MAX_COMPACTION_OUTPUT_TOKENS,
583
647
  providerOptions: this.provider === "openai"
584
- ? { deepseek: { reasoningEffort: "none" } }
648
+ ? { [OPENAI_COMPATIBLE_PROVIDER_NAME]: { reasoningEffort: "none" } }
585
649
  : { anthropic: { effort: "low" } },
586
650
  });
587
651
  if (!result.text.trim()) {
@@ -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,2 @@
1
+ export declare function hasMalformedToolProtocolText(text: string): boolean;
2
+ export declare const TOOL_PROTOCOL_RECOVERY_PROMPT: string;
@@ -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");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "deepccc",
3
- "version": "0.1.26",
3
+ "version": "0.1.28",
4
4
  "description": "A lightweight coding agent with OpenAI-compatible and Anthropic Messages API support.",
5
5
  "license": "Apache-2.0",
6
6
  "keywords": [