deepccc 0.1.27 → 0.2.0
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 +66 -17
- package/dist/cli.d.ts +1 -0
- package/dist/cli.js +22 -0
- package/dist/config.d.ts +21 -0
- package/dist/config.js +73 -4
- package/dist/context.d.ts +1 -0
- package/dist/context.js +12 -0
- package/dist/file-tools.d.ts +10 -1
- package/dist/file-tools.js +11 -4
- package/dist/index.d.ts +9 -0
- package/dist/index.js +29 -6
- package/dist/web-page.d.ts +1 -0
- package/dist/web-page.js +74 -0
- package/dist/web-runtime.d.ts +99 -0
- package/dist/web-runtime.js +191 -0
- package/dist/web-server.d.ts +40 -0
- package/dist/web-server.js +243 -0
- package/dist/web-session-store.d.ts +43 -0
- package/dist/web-session-store.js +130 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
`deepccc` 是一个轻量级本地编程 Agent,针对 DeepSeek 使用体验做了优化,同时支持其他 OpenAI-compatible 模型接口。
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
它提供本地 Web UI、交互式命令行、JSONL 流式输出、本地文件工具、命令执行、项目提示词自动注入和持久化上下文。
|
|
6
6
|
|
|
7
7
|
## 当前状态
|
|
8
8
|
|
|
@@ -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
|
|
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:
|
|
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,27 +79,64 @@ $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
|
-
"
|
|
82
|
+
"subModel": "",
|
|
83
|
+
"effort": "",
|
|
84
|
+
"maxOutputTokens": null,
|
|
85
|
+
"streaming": true,
|
|
81
86
|
"contextWindow": 1048576,
|
|
82
|
-
"
|
|
87
|
+
"git": {
|
|
88
|
+
"coAuthor": {
|
|
89
|
+
"enabled": true,
|
|
90
|
+
"name": "DeepCCC",
|
|
91
|
+
"email": "20184052+wzj998@users.noreply.github.com"
|
|
92
|
+
}
|
|
93
|
+
},
|
|
94
|
+
"rawStreamLogs": {
|
|
83
95
|
"enabled": true,
|
|
84
96
|
"maxBytesPerTurn": 1048576,
|
|
85
97
|
"retentionDays": 7,
|
|
86
|
-
"keepCompleted": false
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
|
|
98
|
+
"keepCompleted": false
|
|
99
|
+
},
|
|
100
|
+
"web": {
|
|
101
|
+
"port": 28080,
|
|
102
|
+
"openOnStart": true
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
## Web UI
|
|
108
|
+
|
|
109
|
+
全局安装后可以直接启动本地网页版:
|
|
110
|
+
|
|
111
|
+
```bash
|
|
112
|
+
deepccc web
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
默认只监听 `http://127.0.0.1:28080/`,不会暴露到局域网。可在
|
|
116
|
+
`~/.deepccc/config.json` 的 `web.port` 修改端口,`web.openOnStart` 控制启动时是否自动打开浏览器;也可以临时使用 `deepccc web --port 28081 --no-open`。
|
|
117
|
+
|
|
118
|
+
Web UI 支持新建、恢复、重命名和删除多会话,多个会话可以同时运行,即使它们指向同一个工作目录。每个会话可独立选择 model、subModel 和 effort,并持续复用 CLI 已保存的历史。注意:当前版本不自动创建 Git worktree;同目录的多个运行中 Agent 直接修改同一组文件,页面会提示覆盖与冲突风险。
|
|
119
|
+
|
|
120
|
+
API 设置采用单一 Provider 配置,支持 OpenAI-compatible 与 Anthropic Messages。完整 API Key 只保存在本机 `~/.deepccc/config.json`,浏览器读取设置时仅返回掩码。高风险命令会在网页中暂停并请求“拒绝、允许一次、本会话允许、永久允许”,浏览器断开或审批超时默认拒绝。
|
|
121
|
+
|
|
122
|
+
`git.coAuthor.enabled` 默认开启。DeepCCC 通过 `run_command` 创建 Git 提交时会保留用户为
|
|
123
|
+
主 Author,并追加 `Co-authored-by: DeepCCC <20184052+wzj998@users.noreply.github.com>`。
|
|
124
|
+
可设为 `false` 或用 `DEEPCCC_GIT_COAUTHOR=false` 全局关闭。ChatCCC 的
|
|
125
|
+
`ccc.gitCoAuthor` 是三态 override:`null`/缺失跟随这里,`true` 强制开启,`false` 强制关闭。
|
|
90
126
|
|
|
91
127
|
`provider` 可选 `openai` 或 `anthropic`,默认 `openai`,也可以通过
|
|
92
128
|
`DEEPCCC_PROVIDER` 或命令行 `--provider` 覆盖:
|
|
93
129
|
|
|
94
130
|
- `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`
|
|
131
|
+
- `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
132
|
|
|
97
|
-
`streaming` 控制主对话是否使用流式请求,默认 `true`;也可以通过
|
|
98
|
-
`DEEPCCC_STREAMING=true|false` 覆盖。关闭后,终端会在整条模型响应完成后一次性显示结果。
|
|
133
|
+
`streaming` 控制主对话是否使用流式请求,默认 `true`;也可以通过
|
|
134
|
+
`DEEPCCC_STREAMING=true|false` 覆盖。关闭后,终端会在整条模型响应完成后一次性显示结果。
|
|
135
|
+
|
|
136
|
+
`maxOutputTokens` 限制主对话单次最大输出 token,默认不配置(`null`/缺失),此时不向
|
|
137
|
+
Provider 发送 `max_tokens`,使用模型服务端默认值。可通过 `DEEPCCC_MAX_OUTPUT_TOKENS`
|
|
138
|
+
或命令行 `--max-output-tokens` 覆盖;只接受正整数。该限制会同时覆盖模型思考内容、
|
|
139
|
+
工具参数和最终回答,设置过小可能导致工具调用或长回复被截断。
|
|
99
140
|
|
|
100
141
|
`contextWindow` 是模型上下文窗口(token),默认 `1048576`(1M,DeepSeek V4 Pro/Flash
|
|
101
142
|
原生规格);上下文压缩阈值自动 = `contextWindow × 0.8`(超出即把较早消息压缩为摘要)。
|
|
@@ -166,7 +207,15 @@ deepccc --max-steps 20
|
|
|
166
207
|
deepccc --effort high
|
|
167
208
|
```
|
|
168
209
|
|
|
169
|
-
可选值:`none` / `minimal` / `low` / `medium` / `high` / `xhigh` / `max`(留空则不传 `reasoning_effort` 请求字段)。
|
|
210
|
+
可选值:`none` / `minimal` / `low` / `medium` / `high` / `xhigh` / `max`(留空则不传 `reasoning_effort` 请求字段)。
|
|
211
|
+
|
|
212
|
+
限制主对话最大输出 token:
|
|
213
|
+
|
|
214
|
+
```bash
|
|
215
|
+
deepccc --max-output-tokens 8192
|
|
216
|
+
```
|
|
217
|
+
|
|
218
|
+
不设置时使用 Provider 默认值。
|
|
170
219
|
|
|
171
220
|
## 权限机制
|
|
172
221
|
|
package/dist/cli.d.ts
CHANGED
package/dist/cli.js
CHANGED
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
* node bin/deepccc.mjs
|
|
6
6
|
* node bin/deepccc.mjs --model deepseek-v4-pro
|
|
7
7
|
* node bin/deepccc.mjs --stream-json --prompt "hello"
|
|
8
|
+
* node bin/deepccc.mjs web
|
|
8
9
|
*
|
|
9
10
|
* 交互模式(TTY)下,单轮回复渲染为固定"过程区块":状态行 + 折叠工具行 +
|
|
10
11
|
* 原地更新正文,不再滚屏刷 JSON;完成/停止/异常后定型留在屏幕上。
|
|
@@ -65,6 +66,10 @@ function parseArgs(argv = process.argv.slice(2)) {
|
|
|
65
66
|
config.effort = next;
|
|
66
67
|
i++;
|
|
67
68
|
}
|
|
69
|
+
else if (arg === "--max-output-tokens" && next !== undefined) {
|
|
70
|
+
config.maxOutputTokens = parsePositiveIntegerOption("--max-output-tokens", next);
|
|
71
|
+
i++;
|
|
72
|
+
}
|
|
68
73
|
else if (arg === "--base-url" && next !== undefined) {
|
|
69
74
|
config.baseURL = next;
|
|
70
75
|
i++;
|
|
@@ -131,6 +136,7 @@ function printHelp(appConfig) {
|
|
|
131
136
|
` --model <name> Model name (current default ${appConfig.model})`,
|
|
132
137
|
` --sub-model <name> Sub-model for lightweight steps (compaction/task; empty = follow main model)`,
|
|
133
138
|
` --effort <level> Reasoning effort: none/minimal/low/medium/high/xhigh/max (overrides config.effort)`,
|
|
139
|
+
` --max-output-tokens <n> Maximum output tokens (unset = Provider default)`,
|
|
134
140
|
` --base-url <url> Provider API base URL (current default ${appConfig.baseURL})`,
|
|
135
141
|
" --api-key <key> API key",
|
|
136
142
|
" --cwd <path> Working directory",
|
|
@@ -140,6 +146,7 @@ function printHelp(appConfig) {
|
|
|
140
146
|
" --stream-json One-shot mode: write JSONL events to stdout",
|
|
141
147
|
" --prompt <text> Prompt text for --stream-json",
|
|
142
148
|
" --plain Force plain streaming output (no progress block renderer)",
|
|
149
|
+
" web Start the local Web UI (default http://127.0.0.1:28080)",
|
|
143
150
|
" --dangerously-bypass-permissions Skip all permission prompts (aligns with chatccc's bypass mode)",
|
|
144
151
|
" --help, -h Show help",
|
|
145
152
|
"",
|
|
@@ -603,6 +610,21 @@ function runSkillCreate(argv) {
|
|
|
603
610
|
console.log("hot reload: 下一次对话自动生效,无需重启");
|
|
604
611
|
}
|
|
605
612
|
async function main() {
|
|
613
|
+
if (process.argv[2] === "web") {
|
|
614
|
+
const webArgs = process.argv.slice(3);
|
|
615
|
+
const portIndex = webArgs.indexOf("--port");
|
|
616
|
+
const port = portIndex >= 0 && webArgs[portIndex + 1] ? Number(webArgs[portIndex + 1]) : undefined;
|
|
617
|
+
if (port !== undefined && (!Number.isInteger(port) || port < 1 || port > 65_535)) {
|
|
618
|
+
throw new Error("--port must be an integer between 1 and 65535");
|
|
619
|
+
}
|
|
620
|
+
const { startDeepCccWebServer } = await import("./web-server.js");
|
|
621
|
+
const handle = await startDeepCccWebServer({
|
|
622
|
+
...(port ? { port } : {}),
|
|
623
|
+
...(webArgs.includes("--no-open") ? { openBrowser: false } : {}),
|
|
624
|
+
});
|
|
625
|
+
console.log(`DeepCCC Web UI: ${handle.url}${handle.reused ? " (reused existing server)" : ""}`);
|
|
626
|
+
return;
|
|
627
|
+
}
|
|
606
628
|
// skill create 子命令:deepccc skill create <name> [--scope global|project] [--description "..."]
|
|
607
629
|
// 默认创建在全局 ~/.deepccc/skills(Codex 目录结构),--scope project 创建到 <cwd>/.deepccc/skills。
|
|
608
630
|
if (process.argv[2] === "skill" && process.argv[3] === "create") {
|
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,19 +21,38 @@ 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;
|
|
25
34
|
retentionDays: number;
|
|
26
35
|
keepCompleted: boolean;
|
|
27
36
|
};
|
|
37
|
+
web: {
|
|
38
|
+
port: number;
|
|
39
|
+
openOnStart: boolean;
|
|
40
|
+
};
|
|
28
41
|
}
|
|
29
42
|
export declare const DEEPCCC_HOME: string;
|
|
30
43
|
export declare const RAW_STREAM_LOGS_DIR: string;
|
|
44
|
+
export declare const CONFIG_PATH: string;
|
|
31
45
|
/**
|
|
32
46
|
* 默认配置(不读环境/文件)。导出供测试断言默认值;运行时请用 loadConfig 结果。
|
|
33
47
|
*/
|
|
34
48
|
export declare const DEFAULT_CONFIG: DeepCccConfig;
|
|
49
|
+
export declare function readConfigFile(): Partial<DeepCccConfig>;
|
|
35
50
|
export declare function normalizeDeepCccProvider(value: unknown): DeepCccProvider;
|
|
51
|
+
export declare function loadConfig(): DeepCccConfig;
|
|
36
52
|
export declare function ensureConfigDir(): void;
|
|
53
|
+
export type DeepCccConfigPatch = Partial<Pick<DeepCccConfig, "provider" | "apiKey" | "baseURL" | "model" | "subModel" | "effort" | "maxOutputTokens" | "streaming" | "contextWindow">> & {
|
|
54
|
+
web?: Partial<DeepCccConfig["web"]>;
|
|
55
|
+
};
|
|
56
|
+
/** Persist only user-editable Web fields while preserving advanced configuration. */
|
|
57
|
+
export declare function saveConfigPatch(patch: DeepCccConfigPatch): DeepCccConfig;
|
|
37
58
|
export declare const config: DeepCccConfig;
|
package/dist/config.js
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
import { existsSync, mkdirSync, readFileSync } from "node:fs";
|
|
1
|
+
import { existsSync, mkdirSync, readFileSync, renameSync, writeFileSync } from "node:fs";
|
|
2
2
|
import { homedir } from "node:os";
|
|
3
3
|
import { dirname, join } from "node:path";
|
|
4
4
|
export const DEEPCCC_HOME = join(homedir(), ".deepccc");
|
|
5
5
|
export const RAW_STREAM_LOGS_DIR = join(DEEPCCC_HOME, "raw-stream-logs");
|
|
6
|
-
const CONFIG_PATH = join(DEEPCCC_HOME, "config.json");
|
|
6
|
+
export const CONFIG_PATH = join(DEEPCCC_HOME, "config.json");
|
|
7
7
|
/**
|
|
8
8
|
* 默认配置(不读环境/文件)。导出供测试断言默认值;运行时请用 loadConfig 结果。
|
|
9
9
|
*/
|
|
@@ -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。
|
|
@@ -24,8 +31,12 @@ export const DEFAULT_CONFIG = {
|
|
|
24
31
|
retentionDays: 7,
|
|
25
32
|
keepCompleted: false,
|
|
26
33
|
},
|
|
34
|
+
web: {
|
|
35
|
+
port: 28_080,
|
|
36
|
+
openOnStart: true,
|
|
37
|
+
},
|
|
27
38
|
};
|
|
28
|
-
function readConfigFile() {
|
|
39
|
+
export function readConfigFile() {
|
|
29
40
|
if (!existsSync(CONFIG_PATH))
|
|
30
41
|
return {};
|
|
31
42
|
const raw = JSON.parse(readFileSync(CONFIG_PATH, "utf8"));
|
|
@@ -49,6 +60,12 @@ function numberEnv(name) {
|
|
|
49
60
|
const value = Number(env(name));
|
|
50
61
|
return Number.isFinite(value) && value >= 0 ? value : undefined;
|
|
51
62
|
}
|
|
63
|
+
function optionalPositiveInteger(value) {
|
|
64
|
+
if (value === undefined || value === null || value === "")
|
|
65
|
+
return undefined;
|
|
66
|
+
const parsed = Number(value);
|
|
67
|
+
return Number.isInteger(parsed) && parsed > 0 ? parsed : undefined;
|
|
68
|
+
}
|
|
52
69
|
export function normalizeDeepCccProvider(value) {
|
|
53
70
|
if (value === undefined || value === null || String(value).trim() === "")
|
|
54
71
|
return "openai";
|
|
@@ -57,11 +74,14 @@ export function normalizeDeepCccProvider(value) {
|
|
|
57
74
|
return normalized;
|
|
58
75
|
throw new Error(`DEEPCCC_PROVIDER/provider must be "openai" or "anthropic", received: ${String(value)}`);
|
|
59
76
|
}
|
|
60
|
-
function loadConfig() {
|
|
77
|
+
export function loadConfig() {
|
|
61
78
|
const file = readConfigFile();
|
|
62
79
|
const rawLogs = file.rawStreamLogs && typeof file.rawStreamLogs === "object"
|
|
63
80
|
? file.rawStreamLogs
|
|
64
81
|
: {};
|
|
82
|
+
const git = file.git && typeof file.git === "object" ? file.git : {};
|
|
83
|
+
const coAuthor = git.coAuthor && typeof git.coAuthor === "object" ? git.coAuthor : {};
|
|
84
|
+
const web = file.web && typeof file.web === "object" ? file.web : {};
|
|
65
85
|
return {
|
|
66
86
|
provider: normalizeDeepCccProvider(env("DEEPCCC_PROVIDER") ?? file.provider ?? DEFAULT_CONFIG.provider),
|
|
67
87
|
apiKey: env("DEEPCCC_API_KEY") ?? env("DEEPSEEK_API_KEY") ?? file.apiKey ?? DEFAULT_CONFIG.apiKey,
|
|
@@ -69,17 +89,66 @@ function loadConfig() {
|
|
|
69
89
|
model: env("DEEPCCC_MODEL") ?? env("DEEPSEEK_MODEL") ?? file.model ?? DEFAULT_CONFIG.model,
|
|
70
90
|
subModel: env("DEEPCCC_SUB_MODEL") ?? file.subModel ?? DEFAULT_CONFIG.subModel,
|
|
71
91
|
effort: env("DEEPCCC_EFFORT") ?? env("DEEPSEEK_EFFORT") ?? file.effort ?? DEFAULT_CONFIG.effort,
|
|
92
|
+
maxOutputTokens: optionalPositiveInteger(env("DEEPCCC_MAX_OUTPUT_TOKENS"))
|
|
93
|
+
?? optionalPositiveInteger(file.maxOutputTokens),
|
|
72
94
|
streaming: boolEnv("DEEPCCC_STREAMING") ?? file.streaming ?? DEFAULT_CONFIG.streaming,
|
|
73
95
|
contextWindow: numberEnv("DEEPCCC_CONTEXT_WINDOW") ?? file.contextWindow ?? DEFAULT_CONFIG.contextWindow,
|
|
96
|
+
git: {
|
|
97
|
+
coAuthor: {
|
|
98
|
+
enabled: boolEnv("DEEPCCC_GIT_COAUTHOR") ?? coAuthor.enabled ?? DEFAULT_CONFIG.git.coAuthor.enabled,
|
|
99
|
+
name: coAuthor.name?.trim() || DEFAULT_CONFIG.git.coAuthor.name,
|
|
100
|
+
email: coAuthor.email?.trim() || DEFAULT_CONFIG.git.coAuthor.email,
|
|
101
|
+
},
|
|
102
|
+
},
|
|
74
103
|
rawStreamLogs: {
|
|
75
104
|
enabled: boolEnv("DEEPCCC_RAW_STREAM_LOGS") ?? rawLogs.enabled ?? DEFAULT_CONFIG.rawStreamLogs.enabled,
|
|
76
105
|
maxBytesPerTurn: numberEnv("DEEPCCC_RAW_STREAM_MAX_BYTES") ?? rawLogs.maxBytesPerTurn ?? DEFAULT_CONFIG.rawStreamLogs.maxBytesPerTurn,
|
|
77
106
|
retentionDays: numberEnv("DEEPCCC_RAW_STREAM_RETENTION_DAYS") ?? rawLogs.retentionDays ?? DEFAULT_CONFIG.rawStreamLogs.retentionDays,
|
|
78
107
|
keepCompleted: boolEnv("DEEPCCC_RAW_STREAM_KEEP_COMPLETED") ?? rawLogs.keepCompleted ?? DEFAULT_CONFIG.rawStreamLogs.keepCompleted,
|
|
79
108
|
},
|
|
109
|
+
web: {
|
|
110
|
+
port: normalizeWebPort(web.port),
|
|
111
|
+
openOnStart: web.openOnStart ?? DEFAULT_CONFIG.web.openOnStart,
|
|
112
|
+
},
|
|
80
113
|
};
|
|
81
114
|
}
|
|
82
115
|
export function ensureConfigDir() {
|
|
83
116
|
mkdirSync(dirname(CONFIG_PATH), { recursive: true });
|
|
84
117
|
}
|
|
118
|
+
/** Persist only user-editable Web fields while preserving advanced configuration. */
|
|
119
|
+
export function saveConfigPatch(patch) {
|
|
120
|
+
const existing = readConfigFile();
|
|
121
|
+
const { web: webPatch, ...flatPatch } = patch;
|
|
122
|
+
const next = {
|
|
123
|
+
...existing,
|
|
124
|
+
...flatPatch,
|
|
125
|
+
...(webPatch ? { web: { ...DEFAULT_CONFIG.web, ...(existing.web ?? {}), ...webPatch } } : {}),
|
|
126
|
+
};
|
|
127
|
+
if (patch.provider !== undefined)
|
|
128
|
+
next.provider = normalizeDeepCccProvider(patch.provider);
|
|
129
|
+
if (patch.baseURL !== undefined && !patch.baseURL.trim())
|
|
130
|
+
throw new Error("baseURL must not be empty");
|
|
131
|
+
if (patch.model !== undefined && !patch.model.trim())
|
|
132
|
+
throw new Error("model must not be empty");
|
|
133
|
+
if (patch.effort !== undefined && !["", "none", "minimal", "low", "medium", "high", "xhigh", "max"].includes(patch.effort)) {
|
|
134
|
+
throw new Error(`Unsupported effort: ${patch.effort}`);
|
|
135
|
+
}
|
|
136
|
+
if (patch.contextWindow !== undefined && (!Number.isInteger(patch.contextWindow) || patch.contextWindow <= 0)) {
|
|
137
|
+
throw new Error("contextWindow must be a positive integer");
|
|
138
|
+
}
|
|
139
|
+
if (webPatch?.port !== undefined) {
|
|
140
|
+
next.web = { ...(next.web ?? DEFAULT_CONFIG.web), port: normalizeWebPort(webPatch.port) };
|
|
141
|
+
}
|
|
142
|
+
ensureConfigDir();
|
|
143
|
+
const tempPath = `${CONFIG_PATH}.${process.pid}.tmp`;
|
|
144
|
+
writeFileSync(tempPath, `${JSON.stringify(next, null, 2)}\n`, "utf8");
|
|
145
|
+
renameSync(tempPath, CONFIG_PATH);
|
|
146
|
+
return loadConfig();
|
|
147
|
+
}
|
|
148
|
+
function normalizeWebPort(value) {
|
|
149
|
+
const parsed = Number(value ?? DEFAULT_CONFIG.web.port);
|
|
150
|
+
if (!Number.isInteger(parsed) || parsed < 1 || parsed > 65_535)
|
|
151
|
+
return DEFAULT_CONFIG.web.port;
|
|
152
|
+
return parsed;
|
|
153
|
+
}
|
|
85
154
|
export const config = loadConfig();
|
package/dist/context.d.ts
CHANGED
|
@@ -66,6 +66,7 @@ export declare function normalizeBuiltinSessionId(value: string): string;
|
|
|
66
66
|
export declare function defaultBuiltinSessionId(cwd?: string): string;
|
|
67
67
|
export declare function newBuiltinSessionId(now?: Date, suffix?: string): string;
|
|
68
68
|
export declare function getBuiltinContextSession(sessionId: string, contextDir?: string): BuiltinContextSessionInfo | null;
|
|
69
|
+
export declare function readBuiltinContextState(sessionId: string, contextDir?: string): BuiltinContextState | null;
|
|
69
70
|
export declare function listBuiltinContextSessions(contextDir?: string): BuiltinContextSessionInfo[];
|
|
70
71
|
export declare function latestBuiltinSessionForCwd(cwd: string, contextDir?: string): BuiltinContextSessionInfo | null;
|
|
71
72
|
/**
|
package/dist/context.js
CHANGED
|
@@ -135,6 +135,18 @@ function readSessionInfo(contextDir, sessionId) {
|
|
|
135
135
|
export function getBuiltinContextSession(sessionId, contextDir = DEFAULT_BUILTIN_CONTEXT_DIR) {
|
|
136
136
|
return readSessionInfo(contextDir, sessionId);
|
|
137
137
|
}
|
|
138
|
+
export function readBuiltinContextState(sessionId, contextDir = DEFAULT_BUILTIN_CONTEXT_DIR) {
|
|
139
|
+
const normalizedSessionId = normalizeBuiltinSessionId(sessionId);
|
|
140
|
+
const filePath = contextFilePath(contextDir, normalizedSessionId);
|
|
141
|
+
if (!existsSync(filePath))
|
|
142
|
+
return null;
|
|
143
|
+
try {
|
|
144
|
+
return normalizeState(JSON.parse(readFileSync(filePath, "utf8")), normalizedSessionId);
|
|
145
|
+
}
|
|
146
|
+
catch {
|
|
147
|
+
return null;
|
|
148
|
+
}
|
|
149
|
+
}
|
|
138
150
|
export function listBuiltinContextSessions(contextDir = DEFAULT_BUILTIN_CONTEXT_DIR) {
|
|
139
151
|
if (!existsSync(contextDir))
|
|
140
152
|
return [];
|
package/dist/file-tools.d.ts
CHANGED
|
@@ -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;
|
package/dist/file-tools.js
CHANGED
|
@@ -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,
|
|
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.
|
|
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,10 @@ 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;
|
|
42
|
+
/** Override whether the main conversation uses streaming requests. */
|
|
43
|
+
streaming?: boolean;
|
|
40
44
|
}
|
|
41
45
|
export interface ChatSessionOptions {
|
|
42
46
|
/** Session working directory. */
|
|
@@ -78,6 +82,8 @@ export interface ChatSessionOptions {
|
|
|
78
82
|
* 调用)自动拒绝高危命令,常规文件操作与低危命令不受影响。
|
|
79
83
|
*/
|
|
80
84
|
permissionResolver?: PermissionResolver;
|
|
85
|
+
/** Overrides ~/.deepccc/config.json git.coAuthor.enabled for this integration. */
|
|
86
|
+
gitCoAuthor?: boolean;
|
|
81
87
|
}
|
|
82
88
|
/**
|
|
83
89
|
* 流式响应事件
|
|
@@ -136,9 +142,12 @@ export declare class ChatSession {
|
|
|
136
142
|
private compactionTimeoutMs;
|
|
137
143
|
private maxSteps?;
|
|
138
144
|
private effort;
|
|
145
|
+
private maxOutputTokens?;
|
|
146
|
+
private streaming;
|
|
139
147
|
private permissionMode;
|
|
140
148
|
private permissionResolver?;
|
|
141
149
|
private permissionGate;
|
|
150
|
+
private gitCoAuthorEnabled;
|
|
142
151
|
private skillDirs;
|
|
143
152
|
private customSystemPrompt;
|
|
144
153
|
/** 最近一次 chat() 使用的 system prompt(供 history 等读取) */
|
package/dist/index.js
CHANGED
|
@@ -77,6 +77,7 @@ const COMPACTION_RECOVERY_HINT_DISABLED = [
|
|
|
77
77
|
].join("\n");
|
|
78
78
|
export const DEFAULT_COMPACTION_TIMEOUT_MS = 5 * 60 * 1000;
|
|
79
79
|
const MAX_COMPACTION_OUTPUT_TOKENS = 16_384;
|
|
80
|
+
const OPENAI_COMPATIBLE_PROVIDER_NAME = "deepccc";
|
|
80
81
|
/** task 子代理工具:子代理单轮对话的最大工具步数(防失控循环,结果收敛后即结束) */
|
|
81
82
|
const TASK_MAX_STEPS = 20;
|
|
82
83
|
/** 子代理最终输出截断:保留前 MAX_TASK_OUTPUT_CHARS 字符,尾部注明截断信息 */
|
|
@@ -219,6 +220,14 @@ function normalizeMaxSteps(value) {
|
|
|
219
220
|
}
|
|
220
221
|
return value;
|
|
221
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
|
+
}
|
|
222
231
|
function normalizeAnthropicBaseURL(baseURL) {
|
|
223
232
|
// 完全按用户填写的地址使用,不自动补 /v1(AI SDK 仅对官方 api.anthropic.com
|
|
224
233
|
// 特判补一次 /v1,其他地址原样拼接 /messages)。
|
|
@@ -239,9 +248,12 @@ export class ChatSession {
|
|
|
239
248
|
compactionTimeoutMs;
|
|
240
249
|
maxSteps;
|
|
241
250
|
effort;
|
|
251
|
+
maxOutputTokens;
|
|
252
|
+
streaming;
|
|
242
253
|
permissionMode;
|
|
243
254
|
permissionResolver;
|
|
244
255
|
permissionGate;
|
|
256
|
+
gitCoAuthorEnabled;
|
|
245
257
|
skillDirs;
|
|
246
258
|
customSystemPrompt;
|
|
247
259
|
/** 最近一次 chat() 使用的 system prompt(供 history 等读取) */
|
|
@@ -266,6 +278,7 @@ export class ChatSession {
|
|
|
266
278
|
baseURL: this.baseURL,
|
|
267
279
|
model: this.subModelId || this.modelId,
|
|
268
280
|
effort: this.effort,
|
|
281
|
+
maxOutputTokens: this.maxOutputTokens,
|
|
269
282
|
}, {
|
|
270
283
|
cwd: taskCwd,
|
|
271
284
|
persist: false,
|
|
@@ -303,6 +316,8 @@ export class ChatSession {
|
|
|
303
316
|
this.subModelId = (overrides.subModel ?? appConfig.subModel ?? "").trim();
|
|
304
317
|
this.provider = normalizeDeepCccProvider(overrides.provider ?? appConfig.provider);
|
|
305
318
|
this.effort = (overrides.effort ?? appConfig.effort ?? "").trim();
|
|
319
|
+
this.maxOutputTokens = normalizeMaxOutputTokens(overrides.maxOutputTokens ?? appConfig.maxOutputTokens);
|
|
320
|
+
this.streaming = overrides.streaming ?? appConfig.streaming;
|
|
306
321
|
this.apiKey = apiKey;
|
|
307
322
|
this.baseURL = baseURL;
|
|
308
323
|
const provider = this.provider === "anthropic"
|
|
@@ -311,7 +326,7 @@ export class ChatSession {
|
|
|
311
326
|
apiKey,
|
|
312
327
|
})
|
|
313
328
|
: createOpenAICompatible({
|
|
314
|
-
name:
|
|
329
|
+
name: OPENAI_COMPATIBLE_PROVIDER_NAME,
|
|
315
330
|
baseURL,
|
|
316
331
|
apiKey,
|
|
317
332
|
includeUsage: true,
|
|
@@ -325,6 +340,7 @@ export class ChatSession {
|
|
|
325
340
|
this.customSystemPrompt = options.systemPrompt ?? "";
|
|
326
341
|
this.permissionMode = options.permissionMode ?? "ask";
|
|
327
342
|
this.permissionResolver = options.permissionResolver;
|
|
343
|
+
this.gitCoAuthorEnabled = options.gitCoAuthor ?? appConfig.git.coAuthor.enabled;
|
|
328
344
|
// 技能目录在构造时确定;技能内容在每次 chat() 前重新扫描(mtime 热加载),
|
|
329
345
|
// 因此创建/修改技能后下一次对话自动生效,无需重启。
|
|
330
346
|
this.skillDirs =
|
|
@@ -413,14 +429,14 @@ export class ChatSession {
|
|
|
413
429
|
? addAnthropicToolJsonCompatibilityNote(hintedMessages)
|
|
414
430
|
: hintedMessages;
|
|
415
431
|
// effort 按协议映射:
|
|
416
|
-
// - OpenAI 兼容:providerOptions.
|
|
417
|
-
//
|
|
432
|
+
// - OpenAI 兼容:providerOptions 的 key 必须与 createOpenAICompatible.name 一致;
|
|
433
|
+
// reasoningEffort 由 SDK 映射为请求体 reasoning_effort 字段;
|
|
418
434
|
// - Anthropic:providerOptions.anthropic.effort 由 @ai-sdk/anthropic 组装为请求体
|
|
419
435
|
// output_config.effort(官方 Effort API,见 platform.claude.com/docs/en/build-with-claude/effort)
|
|
420
436
|
let effortProviderOptions;
|
|
421
437
|
if (this.effort) {
|
|
422
438
|
effortProviderOptions = this.provider === "openai"
|
|
423
|
-
? {
|
|
439
|
+
? { [OPENAI_COMPATIBLE_PROVIDER_NAME]: { reasoningEffort: this.effort } }
|
|
424
440
|
: { anthropic: { effort: this.effort } };
|
|
425
441
|
}
|
|
426
442
|
const baseGenerationOptions = {
|
|
@@ -429,9 +445,16 @@ export class ChatSession {
|
|
|
429
445
|
tools: createBuiltinFileTools(this.cwd, {
|
|
430
446
|
permissionGate: this.permissionGate,
|
|
431
447
|
runTask: this.runTask,
|
|
448
|
+
gitCoAuthor: {
|
|
449
|
+
...appConfig.git.coAuthor,
|
|
450
|
+
enabled: this.gitCoAuthorEnabled,
|
|
451
|
+
},
|
|
432
452
|
}),
|
|
433
453
|
stopWhen: maxSteps !== undefined ? stepCountIs(maxSteps) : isLoopFinished(),
|
|
434
454
|
abortSignal: signal,
|
|
455
|
+
...(this.maxOutputTokens !== undefined
|
|
456
|
+
? { maxOutputTokens: this.maxOutputTokens }
|
|
457
|
+
: {}),
|
|
435
458
|
...(effortProviderOptions ? { providerOptions: effortProviderOptions } : {}),
|
|
436
459
|
};
|
|
437
460
|
for (let attempt = 0; attempt < 2; attempt += 1) {
|
|
@@ -449,7 +472,7 @@ export class ChatSession {
|
|
|
449
472
|
messages: attemptMessages,
|
|
450
473
|
};
|
|
451
474
|
let stream;
|
|
452
|
-
if (
|
|
475
|
+
if (this.streaming) {
|
|
453
476
|
const result = streamText(generationOptions);
|
|
454
477
|
stream = result.fullStream ?? textStreamToFullStream(result.textStream);
|
|
455
478
|
}
|
|
@@ -624,7 +647,7 @@ export class ChatSession {
|
|
|
624
647
|
// output_config.effort=low),避免继承主对话的高 effort 拖慢"压缩上下文中"阶段。
|
|
625
648
|
maxOutputTokens: MAX_COMPACTION_OUTPUT_TOKENS,
|
|
626
649
|
providerOptions: this.provider === "openai"
|
|
627
|
-
? {
|
|
650
|
+
? { [OPENAI_COMPATIBLE_PROVIDER_NAME]: { reasoningEffort: "none" } }
|
|
628
651
|
: { anthropic: { effort: "low" } },
|
|
629
652
|
});
|
|
630
653
|
if (!result.text.trim()) {
|