deepccc 0.2.25 → 0.2.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 +22 -7
- package/bin/deepccc-cli.mjs +17 -17
- package/bin/deepccc.mjs +32 -32
- package/dist/attachments.d.ts +1 -1
- package/dist/attachments.js +7 -5
- package/dist/cli.js +52 -7
- package/dist/completion-session.js +5 -3
- package/dist/config.d.ts +3 -1
- package/dist/config.js +4 -0
- package/dist/context.js +4 -1
- package/dist/decision-session.js +5 -3
- package/dist/file-tools.d.ts +3 -1
- package/dist/file-tools.js +5 -0
- package/dist/index.d.ts +12 -1
- package/dist/index.js +39 -4
- package/dist/json-mode-fetch.d.ts +2 -0
- package/dist/json-mode-fetch.js +12 -0
- package/dist/multimodal.d.ts +51 -0
- package/dist/multimodal.js +291 -0
- package/dist/progress/reducer.js +2 -0
- package/dist/web-page.js +411 -411
- package/dist/web-runtime.d.ts +1 -1
- package/dist/web-runtime.js +3 -0
- package/dist/web-server.d.ts +1 -0
- package/dist/web-server.js +2 -0
- package/docs/completion-session.md +40 -36
- package/docs/decision-session.md +196 -192
- package/docs/workspace-understanding.md +41 -41
- package/os-prompts/darwin.md +8 -8
- package/os-prompts/linux.md +8 -8
- package/os-prompts/win32.md +11 -11
- package/package.json +68 -68
package/README.md
CHANGED
|
@@ -57,9 +57,9 @@ npm run dev
|
|
|
57
57
|
|
|
58
58
|

|
|
59
59
|
|
|
60
|
-
## 核心能力
|
|
61
|
-
|
|
62
|
-
- 有界结构化决策:程序化 `DecisionSession` 支持外部管理上下文、显式工具白名单、运行时结果校验和整轮截止时间;不改变默认聊天行为。详见 [接入文档](docs/decision-session.md)。
|
|
60
|
+
## 核心能力
|
|
61
|
+
|
|
62
|
+
- 有界结构化决策:程序化 `DecisionSession` 支持外部管理上下文、显式工具白名单、运行时结果校验和整轮截止时间;不改变默认聊天行为。详见 [接入文档](docs/decision-session.md)。
|
|
63
63
|
|
|
64
64
|
- Web-first:多会话、持久化历史、实时流式过程、停止和恢复
|
|
65
65
|
- 工具时间线:ChatCCC 风格 emoji 摘要、参数/结果折叠、省略行展开和状态记忆
|
|
@@ -163,7 +163,11 @@ Web UI 支持新建、恢复、重命名和删除多会话,多个会话可以
|
|
|
163
163
|
|
|
164
164
|
模型文本、reasoning 心跳和工具事件通过 SSE 实时更新。每轮消息按真实发生顺序持久化和回放,因此刷新后仍会保持“阶段说明 → 工具调用 → 后续结论”的交错时间线;旧版会话没有顺序数据时,会兼容显示为“工具调用 → 最终回答”。每次工具调用与对应结果合并成一张卡片,折叠态显示 emoji、工具名、状态和关键参数;展开后调用参数默认保留前 8/后 4 行,工具结果保留前 12/后 6 行,省略内容可继续展开。工具卡及省略行的展开状态保存在当前浏览器标签页的 `sessionStorage`,持续生成、切换会话和刷新页面均不会自动收起。消息正文支持标题、表格、列表、引用、链接和代码块等常用 Markdown。
|
|
165
165
|
|
|
166
|
-
|
|
166
|
+
图片会转换成模型可见的原生视觉输入。Web 支持选择、粘贴和拖拽 PNG/JPEG/WebP/GIF;CLI 可用 `--image` 或交互命令 `/image`。每条消息最多 10 张、单图最大 20 MiB、图片合计最大 24 MiB,单边最多 8192 像素;超限时提示分图或缩小,保留原图细节。附件复制到 `~/.deepccc/attachments/<session-id>/`,聊天历史只保存引用,raw 日志过滤图片编码。删除 Web 会话时附件一并清理。
|
|
167
|
+
|
|
168
|
+
默认由当前模型直接看图。若主模型不支持视觉,可在 API 设置中指定 `visionModel`(如 `deepseek-flash`),或设置 `DEEPCCC_VISION_MODEL` / CLI `--vision-model`;该模型使用同一 API 协议、Base URL 和 Key,先分析图片,再由主模型结合观察继续任务。界面及 JSONL 会显示实际图片模型和模式。未配置图片模型的 `deepseek-v4-pro` 收图会明确报错;其他兼容模型需服务端支持图像,接口错误不会降级成“假装看过”。官方 DeepSeek 端点的旧 Flash 名称仅在视觉请求中映射到当前 `deepseek-flash`,自定义网关保留用户配置的名称。
|
|
169
|
+
|
|
170
|
+
Agent 可用 `read_image` 查看本地截图、图表或工具产物,并通过 `question` 指定需要观察的细节;`present_file` 用于把图片展示给用户。后续追问会按预算重新加载近期图片;较早图片保留路径供 `read_image` 重新查看。独立图片模型的观察按原文问题、模型、端点与图片内容哈希缓存;细节追问可重新调用 `read_image`。压缩后的摘要应保留重要图片引用,缺失或损坏图片会明确报错。本版本支持图片输入,不包含音频、视频或 PDF 原生输入。
|
|
167
171
|
|
|
168
172
|
API 设置采用单一 Provider 配置,支持 OpenAI-compatible 与 Anthropic Messages。完整 API Key 只保存在本机 `~/.deepccc/config.json`,浏览器读取设置时仅返回掩码。危险命令会在会话中暂停并请求“拒绝、允许一次、本会话允许、永久允许”,浏览器断开或审批超时默认拒绝。
|
|
169
173
|
|
|
@@ -240,11 +244,22 @@ deepccc-cli --provider anthropic --base-url https://api.example.com --api-key "$
|
|
|
240
244
|
deepccc-cli --cwd /path/to/project
|
|
241
245
|
```
|
|
242
246
|
|
|
243
|
-
|
|
247
|
+
附加一张或多张本地图片(重复传入 `--image`;路径含空格时加引号):
|
|
244
248
|
|
|
245
249
|
```bash
|
|
246
|
-
deepccc-cli --image ./error.png --image ./expected.webp
|
|
247
|
-
```
|
|
250
|
+
deepccc-cli --image ./error.png --image ./expected.webp
|
|
251
|
+
```
|
|
252
|
+
|
|
253
|
+
启动后再加图:输入 `/image "C:\截图\报错截图.png"`,看到“图片已加入下一条消息”后输入问题并回车。多图重复 `/image`;相对路径按会话工作目录(`--cwd`)解析。该命令在当前回复结束后使用。启动时的 `--image` 也只加入队列,随后输入问题发送。
|
|
254
|
+
|
|
255
|
+
脚本或纯命令模式可直接发送图片并获取 JSONL:
|
|
256
|
+
|
|
257
|
+
```bash
|
|
258
|
+
deepccc-cli --model deepseek-flash --stream-json --prompt "解释截图里的错误" --image "./error screenshot.png"
|
|
259
|
+
deepccc-cli --model deepseek-v4-pro --vision-model deepseek-flash --stream-json --prompt "对比两张图" --image ./before.png --image ./after.png
|
|
260
|
+
```
|
|
261
|
+
|
|
262
|
+
安装 ChatCCC 后提供的 `cccagent` 使用同一 CLI:将上述 `deepccc-cli` 换成 `cccagent` 即可。它读取 `~/.deepccc/config.json`、`DEEPCCC_*` 环境变量或命令行参数;ChatCCC 机器人里的 API 配置不会自动改写独立 CLI 配置。聊天窗口的图片粘贴/拖拽属于 Web 功能,纯终端请使用文件路径。
|
|
248
263
|
|
|
249
264
|
恢复当前工作目录最近一次会话:
|
|
250
265
|
|
package/bin/deepccc-cli.mjs
CHANGED
|
@@ -1,17 +1,17 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
import { existsSync } from "node:fs";
|
|
3
|
-
import { spawnSync } from "node:child_process";
|
|
4
|
-
import { createRequire } from "node:module";
|
|
5
|
-
import { dirname, join } from "node:path";
|
|
6
|
-
|
|
7
|
-
const require = createRequire(import.meta.url);
|
|
8
|
-
const pkgRoot = dirname(require.resolve("../package.json"));
|
|
9
|
-
const distCli = join(pkgRoot, "dist", "cli.js");
|
|
10
|
-
let args;
|
|
11
|
-
if (existsSync(distCli)) {
|
|
12
|
-
args = [distCli, ...process.argv.slice(2)];
|
|
13
|
-
} else {
|
|
14
|
-
args = [require.resolve("tsx/cli"), join(pkgRoot, "src", "cli.ts"), ...process.argv.slice(2)];
|
|
15
|
-
}
|
|
16
|
-
const result = spawnSync(process.execPath, args, { stdio: "inherit", cwd: process.cwd(), env: process.env });
|
|
17
|
-
process.exit(result.status === null ? 1 : result.status);
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { existsSync } from "node:fs";
|
|
3
|
+
import { spawnSync } from "node:child_process";
|
|
4
|
+
import { createRequire } from "node:module";
|
|
5
|
+
import { dirname, join } from "node:path";
|
|
6
|
+
|
|
7
|
+
const require = createRequire(import.meta.url);
|
|
8
|
+
const pkgRoot = dirname(require.resolve("../package.json"));
|
|
9
|
+
const distCli = join(pkgRoot, "dist", "cli.js");
|
|
10
|
+
let args;
|
|
11
|
+
if (existsSync(distCli)) {
|
|
12
|
+
args = [distCli, ...process.argv.slice(2)];
|
|
13
|
+
} else {
|
|
14
|
+
args = [require.resolve("tsx/cli"), join(pkgRoot, "src", "cli.ts"), ...process.argv.slice(2)];
|
|
15
|
+
}
|
|
16
|
+
const result = spawnSync(process.execPath, args, { stdio: "inherit", cwd: process.cwd(), env: process.env });
|
|
17
|
+
process.exit(result.status === null ? 1 : result.status);
|
package/bin/deepccc.mjs
CHANGED
|
@@ -1,32 +1,32 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
import { existsSync } from "node:fs";
|
|
3
|
-
import { spawnSync } from "node:child_process";
|
|
4
|
-
import { createRequire } from "node:module";
|
|
5
|
-
import { dirname, join } from "node:path";
|
|
6
|
-
|
|
7
|
-
const require = createRequire(import.meta.url);
|
|
8
|
-
const pkgRoot = dirname(require.resolve("../package.json"));
|
|
9
|
-
const rawArgs = process.argv.slice(2);
|
|
10
|
-
const distWebEntry = join(pkgRoot, "dist", "web-entry.js");
|
|
11
|
-
const distCli = join(pkgRoot, "dist", "cli.js");
|
|
12
|
-
const sourceMode = !existsSync(distWebEntry);
|
|
13
|
-
const isLegacy = rawArgs.length > 0 && rawArgs[0] !== "web" && !["--reuse-existing", "--port", "--no-open", "--help", "-h"].includes(rawArgs[0]);
|
|
14
|
-
|
|
15
|
-
if (isLegacy) {
|
|
16
|
-
console.error("[DeepCCC] CLI commands have moved to deepccc-cli; forwarding this invocation for compatibility.");
|
|
17
|
-
const args = sourceMode
|
|
18
|
-
? [require.resolve("tsx/cli"), join(pkgRoot, "src", "cli.ts"), ...rawArgs]
|
|
19
|
-
: [distCli, ...rawArgs];
|
|
20
|
-
const result = spawnSync(process.execPath, args, { stdio: "inherit", cwd: process.cwd(), env: process.env });
|
|
21
|
-
process.exit(result.status === null ? 1 : result.status);
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
if (sourceMode) {
|
|
25
|
-
const result = spawnSync(process.execPath, [require.resolve("tsx/cli"), join(pkgRoot, "src", "web-entry.ts"), ...rawArgs], {
|
|
26
|
-
stdio: "inherit", cwd: process.cwd(), env: process.env,
|
|
27
|
-
});
|
|
28
|
-
process.exit(result.status === null ? 1 : result.status);
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
const { runWebEntry } = await import(new URL("../dist/web-entry.js", import.meta.url));
|
|
32
|
-
await runWebEntry(rawArgs);
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { existsSync } from "node:fs";
|
|
3
|
+
import { spawnSync } from "node:child_process";
|
|
4
|
+
import { createRequire } from "node:module";
|
|
5
|
+
import { dirname, join } from "node:path";
|
|
6
|
+
|
|
7
|
+
const require = createRequire(import.meta.url);
|
|
8
|
+
const pkgRoot = dirname(require.resolve("../package.json"));
|
|
9
|
+
const rawArgs = process.argv.slice(2);
|
|
10
|
+
const distWebEntry = join(pkgRoot, "dist", "web-entry.js");
|
|
11
|
+
const distCli = join(pkgRoot, "dist", "cli.js");
|
|
12
|
+
const sourceMode = !existsSync(distWebEntry);
|
|
13
|
+
const isLegacy = rawArgs.length > 0 && rawArgs[0] !== "web" && !["--reuse-existing", "--port", "--no-open", "--help", "-h"].includes(rawArgs[0]);
|
|
14
|
+
|
|
15
|
+
if (isLegacy) {
|
|
16
|
+
console.error("[DeepCCC] CLI commands have moved to deepccc-cli; forwarding this invocation for compatibility.");
|
|
17
|
+
const args = sourceMode
|
|
18
|
+
? [require.resolve("tsx/cli"), join(pkgRoot, "src", "cli.ts"), ...rawArgs]
|
|
19
|
+
: [distCli, ...rawArgs];
|
|
20
|
+
const result = spawnSync(process.execPath, args, { stdio: "inherit", cwd: process.cwd(), env: process.env });
|
|
21
|
+
process.exit(result.status === null ? 1 : result.status);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
if (sourceMode) {
|
|
25
|
+
const result = spawnSync(process.execPath, [require.resolve("tsx/cli"), join(pkgRoot, "src", "web-entry.ts"), ...rawArgs], {
|
|
26
|
+
stdio: "inherit", cwd: process.cwd(), env: process.env,
|
|
27
|
+
});
|
|
28
|
+
process.exit(result.status === null ? 1 : result.status);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const { runWebEntry } = await import(new URL("../dist/web-entry.js", import.meta.url));
|
|
32
|
+
await runWebEntry(rawArgs);
|
package/dist/attachments.d.ts
CHANGED
|
@@ -5,7 +5,7 @@ export declare const ATTACHMENT_PROMPT_END = "</deepccc-attachments>";
|
|
|
5
5
|
export interface AttachmentMeta {
|
|
6
6
|
attachmentId: string;
|
|
7
7
|
originalName: string;
|
|
8
|
-
mimeType: "image/png" | "image/jpeg" | "image/webp";
|
|
8
|
+
mimeType: "image/png" | "image/jpeg" | "image/webp" | "image/gif";
|
|
9
9
|
size: number;
|
|
10
10
|
absolutePath: string;
|
|
11
11
|
}
|
package/dist/attachments.js
CHANGED
|
@@ -22,7 +22,7 @@ export class AttachmentStore {
|
|
|
22
22
|
throw new Error("Image attachment exceeds the 20 MB limit");
|
|
23
23
|
const mimeType = detectImageMime(bytes);
|
|
24
24
|
if (!mimeType)
|
|
25
|
-
throw new Error("Only PNG, JPEG, or WebP image attachments are supported");
|
|
25
|
+
throw new Error("Only PNG, JPEG, or WebP image attachments (also GIF) are supported");
|
|
26
26
|
const normalizedSessionId = normalizeBuiltinSessionId(sessionId);
|
|
27
27
|
const attachmentId = normalizeAttachmentId(this.idFactory());
|
|
28
28
|
const extension = extensionForMime(mimeType);
|
|
@@ -98,7 +98,7 @@ export class AttachmentStore {
|
|
|
98
98
|
return null;
|
|
99
99
|
if (typeof value.originalName !== "string" || typeof value.size !== "number")
|
|
100
100
|
return null;
|
|
101
|
-
if (value.mimeType !== "image/png" && value.mimeType !== "image/jpeg" && value.mimeType !== "image/webp")
|
|
101
|
+
if (value.mimeType !== "image/png" && value.mimeType !== "image/jpeg" && value.mimeType !== "image/webp" && value.mimeType !== "image/gif")
|
|
102
102
|
return null;
|
|
103
103
|
if (value.fileName !== `${normalizedId}${extensionForMime(value.mimeType)}`)
|
|
104
104
|
return null;
|
|
@@ -133,7 +133,7 @@ export function buildAttachmentPrompt(text, attachments) {
|
|
|
133
133
|
ATTACHMENT_PROMPT_START,
|
|
134
134
|
JSON.stringify(manifest),
|
|
135
135
|
ATTACHMENT_PROMPT_END,
|
|
136
|
-
"
|
|
136
|
+
"图片由内核转换为视觉输入;历史中只保存附件引用,需要重新查看时使用 read_image。",
|
|
137
137
|
].join("\n");
|
|
138
138
|
}
|
|
139
139
|
export function parseAttachmentPrompt(content) {
|
|
@@ -155,6 +155,8 @@ export function parseAttachmentPrompt(content) {
|
|
|
155
155
|
}
|
|
156
156
|
export function detectImageMime(bytes) {
|
|
157
157
|
const buffer = Buffer.from(bytes);
|
|
158
|
+
if (buffer.length >= 10 && /GIF8[79]a/.test(buffer.subarray(0, 6).toString("ascii")))
|
|
159
|
+
return "image/gif";
|
|
158
160
|
if (buffer.length >= 8 && buffer.subarray(0, 8).equals(Buffer.from([137, 80, 78, 71, 13, 10, 26, 10])))
|
|
159
161
|
return "image/png";
|
|
160
162
|
if (buffer.length >= 3 && buffer[0] === 0xff && buffer[1] === 0xd8 && buffer[2] === 0xff)
|
|
@@ -164,7 +166,7 @@ export function detectImageMime(bytes) {
|
|
|
164
166
|
return null;
|
|
165
167
|
}
|
|
166
168
|
function extensionForMime(mimeType) {
|
|
167
|
-
return mimeType === "image/png" ? ".png" : mimeType === "image/jpeg" ? ".jpg" : ".webp";
|
|
169
|
+
return mimeType === "image/png" ? ".png" : mimeType === "image/jpeg" ? ".jpg" : mimeType === "image/gif" ? ".gif" : ".webp";
|
|
168
170
|
}
|
|
169
171
|
function cleanOriginalName(value, extension) {
|
|
170
172
|
const cleaned = basename(value || `image${extension}`).replace(/[\u0000-\u001f<>:"/\\|?*]+/g, "_").slice(0, 180);
|
|
@@ -188,5 +190,5 @@ function isAttachmentMeta(value) {
|
|
|
188
190
|
&& typeof attachment.originalName === "string"
|
|
189
191
|
&& typeof attachment.absolutePath === "string"
|
|
190
192
|
&& typeof attachment.size === "number"
|
|
191
|
-
&& (attachment.mimeType === "image/png" || attachment.mimeType === "image/jpeg" || attachment.mimeType === "image/webp");
|
|
193
|
+
&& (attachment.mimeType === "image/png" || attachment.mimeType === "image/jpeg" || attachment.mimeType === "image/webp" || attachment.mimeType === "image/gif");
|
|
192
194
|
}
|
package/dist/cli.js
CHANGED
|
@@ -59,6 +59,10 @@ function parseArgs(argv = process.argv.slice(2)) {
|
|
|
59
59
|
config.model = next;
|
|
60
60
|
i++;
|
|
61
61
|
}
|
|
62
|
+
else if (arg === "--vision-model" && next !== undefined) {
|
|
63
|
+
config.visionModel = next;
|
|
64
|
+
i++;
|
|
65
|
+
}
|
|
62
66
|
else if (arg === "--sub-model" && next !== undefined) {
|
|
63
67
|
config.subModel = next;
|
|
64
68
|
i++;
|
|
@@ -140,6 +144,7 @@ function printHelp(appConfig) {
|
|
|
140
144
|
` --provider <name> API protocol: openai or anthropic (current default ${appConfig.provider})`,
|
|
141
145
|
` --model <name> Model name (current default ${appConfig.model})`,
|
|
142
146
|
` --sub-model <name> Sub-model for lightweight steps (compaction/task; empty = follow main model)`,
|
|
147
|
+
" --vision-model <name> Optional image analysis model; empty uses native images in the main model",
|
|
143
148
|
` --effort <level> Reasoning effort: none/minimal/low/medium/high/xhigh/max (overrides config.effort)`,
|
|
144
149
|
` --max-output-tokens <n> Maximum output tokens (unset = Provider default)`,
|
|
145
150
|
` --base-url <url> Provider API base URL (current default ${appConfig.baseURL})`,
|
|
@@ -150,7 +155,8 @@ function printHelp(appConfig) {
|
|
|
150
155
|
" --list-sessions List saved sessions and exit",
|
|
151
156
|
" --stream-json One-shot mode: write JSONL events to stdout",
|
|
152
157
|
" --prompt <text> Prompt text for --stream-json",
|
|
153
|
-
" --image <path> Attach a PNG/JPEG/WebP file (repeatable;
|
|
158
|
+
" --image <path> Attach a PNG/JPEG/WebP/GIF file (repeatable; quote paths with spaces)",
|
|
159
|
+
" Interactive: /image <path> queues one image for your next message; repeat for multiple images",
|
|
154
160
|
" --plain Force plain streaming output (no progress block renderer)",
|
|
155
161
|
" --dangerously-bypass-permissions Skip all permission prompts (aligns with chatccc's bypass mode)",
|
|
156
162
|
" --help, -h Show help",
|
|
@@ -220,7 +226,10 @@ function writeJsonLine(event) {
|
|
|
220
226
|
process.stdout.write(`${JSON.stringify(event)}\n`);
|
|
221
227
|
}
|
|
222
228
|
function streamJsonEvent(event) {
|
|
223
|
-
if (event.type === "
|
|
229
|
+
if (event.type === "image_input") {
|
|
230
|
+
writeJsonLine({ ...event });
|
|
231
|
+
}
|
|
232
|
+
else if (event.type === "status") {
|
|
224
233
|
writeJsonLine({
|
|
225
234
|
type: "status",
|
|
226
235
|
phase: event.phase,
|
|
@@ -319,7 +328,7 @@ async function runStreamJson(args) {
|
|
|
319
328
|
}
|
|
320
329
|
let attachments;
|
|
321
330
|
try {
|
|
322
|
-
attachments = await importCliAttachments(resolvedSession.sessionId, args.images);
|
|
331
|
+
attachments = await importCliAttachments(resolvedSession.sessionId, args.images, cwd);
|
|
323
332
|
}
|
|
324
333
|
catch (err) {
|
|
325
334
|
writeJsonLine({ type: "error", message: err.message });
|
|
@@ -448,7 +457,7 @@ async function runRepl(args) {
|
|
|
448
457
|
}
|
|
449
458
|
let pendingAttachments;
|
|
450
459
|
try {
|
|
451
|
-
pendingAttachments = await importCliAttachments(resolvedSession.sessionId, args.images);
|
|
460
|
+
pendingAttachments = await importCliAttachments(resolvedSession.sessionId, args.images, cwd);
|
|
452
461
|
}
|
|
453
462
|
catch (err) {
|
|
454
463
|
console.error(`${C.yellow}${err.message}${C.reset}`);
|
|
@@ -463,10 +472,41 @@ async function runRepl(args) {
|
|
|
463
472
|
// model step 边界 drain 后吸收到当前 turn(而非等待整轮结束)。
|
|
464
473
|
let chatRunning = false;
|
|
465
474
|
const pendingInjections = [];
|
|
475
|
+
let attachmentImport = Promise.resolve();
|
|
466
476
|
rl.prompt();
|
|
467
477
|
rl.on("line", async (line) => {
|
|
468
478
|
ctrlCState.reset();
|
|
469
479
|
const input = line.trim();
|
|
480
|
+
if (/^\/image(?:\s|$)/.test(input)) {
|
|
481
|
+
if (chatRunning) {
|
|
482
|
+
console.log("请等待当前回复结束后用 /image 添加图片。");
|
|
483
|
+
return;
|
|
484
|
+
}
|
|
485
|
+
let path = input.slice(6).trim();
|
|
486
|
+
if ((path.startsWith('"') && path.endsWith('"')) || (path.startsWith("'") && path.endsWith("'")))
|
|
487
|
+
path = path.slice(1, -1);
|
|
488
|
+
if (!path) {
|
|
489
|
+
console.log('用法:/image "C:\\path with spaces\\image.png"');
|
|
490
|
+
rl.prompt();
|
|
491
|
+
return;
|
|
492
|
+
}
|
|
493
|
+
attachmentImport = attachmentImport.then(async () => {
|
|
494
|
+
try {
|
|
495
|
+
if (pendingAttachments.length >= 10)
|
|
496
|
+
throw new Error("每条消息最多 10 张图片");
|
|
497
|
+
const added = await importCliAttachments(resolvedSession.sessionId, [path], cwd);
|
|
498
|
+
pendingAttachments.push(...added);
|
|
499
|
+
console.log(`图片已加入下一条消息:${added.map(a => a.originalName).join(", ")}。输入问题后回车发送。`);
|
|
500
|
+
}
|
|
501
|
+
catch (error) {
|
|
502
|
+
console.log(`添加图片失败:${error.message}`);
|
|
503
|
+
}
|
|
504
|
+
});
|
|
505
|
+
await attachmentImport;
|
|
506
|
+
rl.prompt();
|
|
507
|
+
return;
|
|
508
|
+
}
|
|
509
|
+
await attachmentImport;
|
|
470
510
|
if (!input && !pendingAttachments.length) {
|
|
471
511
|
rl.prompt();
|
|
472
512
|
return;
|
|
@@ -529,13 +569,16 @@ async function runRepl(args) {
|
|
|
529
569
|
for await (const event of session.chat(chatInput, signal, () => pendingInjections.shift())) {
|
|
530
570
|
if (renderer && view) {
|
|
531
571
|
view = reduceProgress(view, event);
|
|
532
|
-
if (event.type === "text" || event.type === "compact" || event.type === "status" || event.type === "input_injected") {
|
|
572
|
+
if (event.type === "text" || event.type === "compact" || event.type === "status" || event.type === "input_injected" || event.type === "image_input") {
|
|
533
573
|
renderer.render(view);
|
|
534
574
|
}
|
|
535
575
|
else {
|
|
536
576
|
renderer.flush();
|
|
537
577
|
}
|
|
538
578
|
}
|
|
579
|
+
else if (event.type === "image_input") {
|
|
580
|
+
console.log(`[图片理解:${event.model},${event.count} 张,${event.mode}]`);
|
|
581
|
+
}
|
|
539
582
|
else if (event.type === "status") {
|
|
540
583
|
console.log(`${C.dim}[${event.phase === "compacting" ? "compacting context" : "generating reply"}]${C.reset}`);
|
|
541
584
|
}
|
|
@@ -618,11 +661,13 @@ async function runRepl(args) {
|
|
|
618
661
|
process.exit(0);
|
|
619
662
|
});
|
|
620
663
|
}
|
|
621
|
-
async function importCliAttachments(sessionId, paths) {
|
|
664
|
+
async function importCliAttachments(sessionId, paths, cwd = process.cwd()) {
|
|
665
|
+
if (paths.length > 10)
|
|
666
|
+
throw new Error("每条消息最多 10 张图片");
|
|
622
667
|
const store = new AttachmentStore();
|
|
623
668
|
const attachments = [];
|
|
624
669
|
for (const path of paths)
|
|
625
|
-
attachments.push(await store.importFile(sessionId, path));
|
|
670
|
+
attachments.push(await store.importFile(sessionId, resolvePath(cwd, path)));
|
|
626
671
|
return attachments;
|
|
627
672
|
}
|
|
628
673
|
/**
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
/** Opt-in stateless completion transport for integrations owning their complete prompts. */
|
|
2
2
|
import { createOpenAICompatible } from '@ai-sdk/openai-compatible';
|
|
3
|
-
import {
|
|
3
|
+
import { streamText } from 'ai';
|
|
4
|
+
import { jsonObjectFetch } from './json-mode-fetch.js';
|
|
4
5
|
const protectedKeys = new Set(['messages', 'model', 'stream', 'tools', 'tool_choice', 'response_format', 'temperature', 'max_tokens', 'max_completion_tokens']);
|
|
5
6
|
function positive(n) { if (!Number.isSafeInteger(n) || n < 1)
|
|
6
7
|
throw new Error('Positive integer budget required'); return n; }
|
|
@@ -45,11 +46,12 @@ export class CompletionSession {
|
|
|
45
46
|
try {
|
|
46
47
|
if (controller.signal.aborted)
|
|
47
48
|
throw new Error('Interrupted');
|
|
49
|
+
const transport = request.json ? jsonObjectFetch() : globalThis.fetch;
|
|
48
50
|
const client = createOpenAICompatible({ name: 'completion', ...this.config, includeUsage: true,
|
|
49
|
-
fetch: async (url, init) =>
|
|
51
|
+
fetch: async (url, init) => transport(url, { ...init, body: JSON.stringify({ ...JSON.parse(String(init?.body)), ...extensions }) }) });
|
|
50
52
|
calls++;
|
|
51
53
|
const result = streamText({ model: client(this.config.model), messages, allowSystemInMessages: true, temperature: request.temperature, maxOutputTokens: request.maxOutputTokens,
|
|
52
|
-
|
|
54
|
+
maxRetries: 0, abortSignal: controller.signal, onError: () => { } });
|
|
53
55
|
const iterator = result.fullStream[Symbol.asyncIterator]();
|
|
54
56
|
let content = '', reasoning = '', finish;
|
|
55
57
|
try {
|
package/dist/config.d.ts
CHANGED
|
@@ -10,6 +10,8 @@ export interface DeepCccConfig {
|
|
|
10
10
|
* 留空("")时跟随主模型,行为与旧版完全一致。
|
|
11
11
|
*/
|
|
12
12
|
subModel: string;
|
|
13
|
+
/** Optional dedicated image analysis model, using the same provider credentials. */
|
|
14
|
+
visionModel: string;
|
|
13
15
|
/** Reasoning effort(none/minimal/low/medium/high/xhigh/max),留空不传 reasoning_effort */
|
|
14
16
|
effort: string;
|
|
15
17
|
/** 主对话最大输出 token;未配置时不传,使用 Provider 默认值。 */
|
|
@@ -50,7 +52,7 @@ export declare function readConfigFile(): Partial<DeepCccConfig>;
|
|
|
50
52
|
export declare function normalizeDeepCccProvider(value: unknown): DeepCccProvider;
|
|
51
53
|
export declare function loadConfig(): DeepCccConfig;
|
|
52
54
|
export declare function ensureConfigDir(): void;
|
|
53
|
-
export type DeepCccConfigPatch = Partial<Pick<DeepCccConfig, "provider" | "apiKey" | "baseURL" | "model" | "subModel" | "effort" | "maxOutputTokens" | "streaming" | "contextWindow">> & {
|
|
55
|
+
export type DeepCccConfigPatch = Partial<Pick<DeepCccConfig, "provider" | "apiKey" | "baseURL" | "model" | "subModel" | "visionModel" | "effort" | "maxOutputTokens" | "streaming" | "contextWindow">> & {
|
|
54
56
|
web?: Partial<DeepCccConfig["web"]>;
|
|
55
57
|
};
|
|
56
58
|
/** Persist only user-editable Web fields while preserving advanced configuration. */
|
package/dist/config.js
CHANGED
|
@@ -13,6 +13,7 @@ export const DEFAULT_CONFIG = {
|
|
|
13
13
|
baseURL: "https://api.deepseek.com/v1",
|
|
14
14
|
model: "deepseek-v4-pro",
|
|
15
15
|
subModel: "",
|
|
16
|
+
visionModel: "",
|
|
16
17
|
effort: "",
|
|
17
18
|
streaming: true,
|
|
18
19
|
contextWindow: 1_048_576,
|
|
@@ -88,6 +89,7 @@ export function loadConfig() {
|
|
|
88
89
|
baseURL: env("DEEPCCC_BASE_URL") ?? env("DEEPSEEK_BASE_URL") ?? file.baseURL ?? DEFAULT_CONFIG.baseURL,
|
|
89
90
|
model: env("DEEPCCC_MODEL") ?? env("DEEPSEEK_MODEL") ?? file.model ?? DEFAULT_CONFIG.model,
|
|
90
91
|
subModel: env("DEEPCCC_SUB_MODEL") ?? file.subModel ?? DEFAULT_CONFIG.subModel,
|
|
92
|
+
visionModel: env("DEEPCCC_VISION_MODEL") ?? file.visionModel ?? DEFAULT_CONFIG.visionModel,
|
|
91
93
|
effort: env("DEEPCCC_EFFORT") ?? env("DEEPSEEK_EFFORT") ?? file.effort ?? DEFAULT_CONFIG.effort,
|
|
92
94
|
maxOutputTokens: optionalPositiveInteger(env("DEEPCCC_MAX_OUTPUT_TOKENS"))
|
|
93
95
|
?? optionalPositiveInteger(file.maxOutputTokens),
|
|
@@ -130,6 +132,8 @@ export function saveConfigPatch(patch) {
|
|
|
130
132
|
throw new Error("baseURL must not be empty");
|
|
131
133
|
if (patch.model !== undefined && !patch.model.trim())
|
|
132
134
|
throw new Error("model must not be empty");
|
|
135
|
+
if (patch.visionModel !== undefined && typeof patch.visionModel !== "string")
|
|
136
|
+
throw new Error("visionModel must be a string");
|
|
133
137
|
if (patch.effort !== undefined && !["", "none", "minimal", "low", "medium", "high", "xhigh", "max"].includes(patch.effort)) {
|
|
134
138
|
throw new Error(`Unsupported effort: ${patch.effort}`);
|
|
135
139
|
}
|
package/dist/context.js
CHANGED
|
@@ -285,7 +285,9 @@ export function estimateBuiltinToolContextTokens(messages) {
|
|
|
285
285
|
}
|
|
286
286
|
export function estimateBuiltinContextTokens(summary, messages) {
|
|
287
287
|
const text = summary + messages.reduce((sum, message) => sum + `${message.role}\n${stripStoredToolTranscript(message.content)}\n`, "");
|
|
288
|
-
|
|
288
|
+
// Reserve a conservative visual allowance without counting binary/base64 as text.
|
|
289
|
+
const imageCount = messages.reduce((n, m) => n + (m.role === "user" && m.content.includes("<deepccc-attachments>") ? (m.content.match(/"attachmentId"\s*:/g)?.length ?? 0) : 0), 0);
|
|
290
|
+
return estimateTextTokens(text) + estimateBuiltinToolContextTokens(messages) + Math.min(imageCount, 10) * 2048;
|
|
289
291
|
}
|
|
290
292
|
export function serializeMessagesForSummary(messages) {
|
|
291
293
|
return messages
|
|
@@ -531,6 +533,7 @@ export function buildSummaryPrompt(plan) {
|
|
|
531
533
|
"- 将会话进度与已核实项目事实分节:项目事实保留能力、实现入口、证据路径/符号及验证范围;推测单列,后来的纠正覆盖此前错误判断。",
|
|
532
534
|
"- 不把搜索失败/截断/无命中概括为实现不存在,不把辅助模块概括为整个架构。不保留密钥、授权令牌或临时 capability grant。",
|
|
533
535
|
"- 项目事实是历史证据,不是当前实现保证;提示继续时可用 workspace_map 找到有效证据笔记,再读取源码核验。",
|
|
536
|
+
"- 保留与当前任务相关的图片附件路径、原始问题和已确认观察,标明未确定细节;只保留引用,不包含图片编码。后续需要查看原图可调用 read_image。",
|
|
534
537
|
"",
|
|
535
538
|
];
|
|
536
539
|
if (plan.previousSummary.trim()) {
|
package/dist/decision-session.js
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
/** Bounded, text-only decisions with caller-owned context and explicit capabilities. */
|
|
2
2
|
import { createOpenAICompatible } from "@ai-sdk/openai-compatible";
|
|
3
3
|
import { createAnthropic } from "@ai-sdk/anthropic";
|
|
4
|
-
import { asSchema,
|
|
4
|
+
import { asSchema, streamText, tool } from "ai";
|
|
5
|
+
import { jsonObjectFetch } from "./json-mode-fetch.js";
|
|
5
6
|
import { parseJsonResponse } from "./json-response.js";
|
|
6
7
|
import { performance } from "node:perf_hooks";
|
|
7
8
|
import { config, normalizeDeepCccProvider } from "./config.js";
|
|
@@ -92,7 +93,8 @@ export class DecisionSession {
|
|
|
92
93
|
const baseURL = overrides.baseURL ?? config.baseURL;
|
|
93
94
|
const client = provider === "anthropic"
|
|
94
95
|
? createAnthropic({ apiKey, baseURL: baseURL.trim().replace(/\/+$/, "") })
|
|
95
|
-
: createOpenAICompatible({ name: "deepccc", apiKey, baseURL, includeUsage: true
|
|
96
|
+
: createOpenAICompatible({ name: "deepccc", apiKey, baseURL, includeUsage: true,
|
|
97
|
+
...(this.outputMode === "json" ? { fetch: jsonObjectFetch() } : {}) });
|
|
96
98
|
this.model = client(overrides.model ?? config.model);
|
|
97
99
|
const effort = (overrides.effort ?? config.effort ?? "").trim();
|
|
98
100
|
this.providerOptions = effort ? provider === "anthropic"
|
|
@@ -240,7 +242,7 @@ export class DecisionSession {
|
|
|
240
242
|
const began = performance.now();
|
|
241
243
|
metrics.modelCalls++;
|
|
242
244
|
const result = streamText({ model: this.model, system: this.system, messages: [...messages],
|
|
243
|
-
...(this.outputMode === "json" ? {
|
|
245
|
+
...(this.outputMode === "json" ? {} : { tools: this.modelTools, toolChoice: "required" }),
|
|
244
246
|
maxRetries: 0, abortSignal: signal, maxOutputTokens: this.maxOutputTokens,
|
|
245
247
|
// The SDK default logs raw errors. Return allowlisted diagnostics instead.
|
|
246
248
|
onError: () => { },
|
package/dist/file-tools.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { type ToolSet } from "ai";
|
|
2
2
|
import { type PermissionGate } from "./permissions.js";
|
|
3
|
+
import type { ImageReference, ReadImageInput } from "./multimodal.js";
|
|
3
4
|
import { type SearchScope } from "./workspace-policy.js";
|
|
4
5
|
/** task 子代理工具:子任务结果回传主会话前的最大字符数(防止子代理长输出撑爆主上下文) */
|
|
5
6
|
export declare const MAX_TASK_OUTPUT_CHARS = 32000;
|
|
@@ -209,7 +210,7 @@ export interface PresentFileInput {
|
|
|
209
210
|
export interface PresentFileOutput {
|
|
210
211
|
path: string;
|
|
211
212
|
name: string;
|
|
212
|
-
mimeType: "image/png" | "image/jpeg" | "image/webp";
|
|
213
|
+
mimeType: "image/png" | "image/jpeg" | "image/webp" | "image/gif";
|
|
213
214
|
size: number;
|
|
214
215
|
caption?: string;
|
|
215
216
|
}
|
|
@@ -244,6 +245,7 @@ export declare function deleteFileForTool(cwd: string, input: DeleteFileInput):
|
|
|
244
245
|
export declare function moveFileForTool(cwd: string, input: MoveFileInput): Promise<MoveFileOutput>;
|
|
245
246
|
export declare function applyPatchForTool(cwd: string, input: ApplyPatchInput): Promise<ApplyPatchOutput>;
|
|
246
247
|
export interface BuiltinFileToolsOptions {
|
|
248
|
+
readImage?: (input: ReadImageInput) => Promise<ImageReference>;
|
|
247
249
|
/** 权限门控:副作用工具(进程/脚本/命令执行与文件写操作)执行前会先经过 gate.check */
|
|
248
250
|
permissionGate?: PermissionGate;
|
|
249
251
|
/** Git commits created by DeepCCC receive this Co-authored-by trailer when enabled. */
|
package/dist/file-tools.js
CHANGED
|
@@ -1513,6 +1513,11 @@ export function createBuiltinFileTools(cwd, options = {}) {
|
|
|
1513
1513
|
}),
|
|
1514
1514
|
execute: (input) => readFileForTool(cwd, input),
|
|
1515
1515
|
}),
|
|
1516
|
+
...(options.readImage ? { read_image: tool({
|
|
1517
|
+
description: "查看本地 PNG/JPEG/WebP/GIF 图片。图片作为视觉输入提供给模型,返回的是附件引用而非图片文字。可指定 question 聚焦细节;用于历史图片、报错截图、图表和工具生成的图片。",
|
|
1518
|
+
inputSchema: jsonSchema({ type: "object", additionalProperties: false, properties: { path: { type: "string", description: "图片绝对路径或相对于工作目录的路径" }, question: { type: "string", description: "希望观察的细节或问题" } }, required: ["path"] }),
|
|
1519
|
+
execute: input => options.readImage(input),
|
|
1520
|
+
}) } : {}),
|
|
1516
1521
|
list_dir: tool({
|
|
1517
1522
|
description: "列出本地目录中的文件。",
|
|
1518
1523
|
inputSchema: jsonSchema({
|
package/dist/index.d.ts
CHANGED
|
@@ -5,6 +5,8 @@
|
|
|
5
5
|
*/
|
|
6
6
|
import { type DeepCccProvider } from "./config.js";
|
|
7
7
|
import { type PermissionMode, type PermissionResolver } from "./permissions.js";
|
|
8
|
+
import { type ImageUseNotice } from "./multimodal.js";
|
|
9
|
+
import type { AttachmentStore } from "./attachments.js";
|
|
8
10
|
export { DecisionSession } from "./decision-session.js";
|
|
9
11
|
export { CompletionSession } from "./completion-session.js";
|
|
10
12
|
export type { CompletionConfig, CompletionRequest, CompletionResult } from "./completion-session.js";
|
|
@@ -36,6 +38,8 @@ export interface ChatSessionConfig {
|
|
|
36
38
|
* 子模型 id(可选):用于压缩摘要生成与 task 子代理任务。留空跟随主模型。
|
|
37
39
|
*/
|
|
38
40
|
subModel?: string;
|
|
41
|
+
/** Optional dedicated image analysis model. Empty = native image input to the main model. */
|
|
42
|
+
visionModel?: string;
|
|
39
43
|
/**
|
|
40
44
|
* Reasoning effort (none/minimal/low/medium/high/xhigh/max);
|
|
41
45
|
* overrides config.effort; empty omits the reasoning_effort request field.
|
|
@@ -47,6 +51,7 @@ export interface ChatSessionConfig {
|
|
|
47
51
|
streaming?: boolean;
|
|
48
52
|
}
|
|
49
53
|
export interface ChatSessionOptions {
|
|
54
|
+
attachmentStore?: AttachmentStore;
|
|
50
55
|
/** Session working directory. */
|
|
51
56
|
cwd?: string;
|
|
52
57
|
/** Extra system guidance appended after project instructions. */
|
|
@@ -100,7 +105,9 @@ export interface ChatSessionOptions {
|
|
|
100
105
|
/**
|
|
101
106
|
* 流式响应事件
|
|
102
107
|
*/
|
|
103
|
-
export type ChatEvent = {
|
|
108
|
+
export type ChatEvent = ({
|
|
109
|
+
type: "image_input";
|
|
110
|
+
} & ImageUseNotice) | {
|
|
104
111
|
type: "status";
|
|
105
112
|
phase: "compacting" | "generating";
|
|
106
113
|
} | {
|
|
@@ -152,6 +159,10 @@ export declare class ChatSession {
|
|
|
152
159
|
private baseURL;
|
|
153
160
|
private modelId;
|
|
154
161
|
private subModelId;
|
|
162
|
+
private visionModelId;
|
|
163
|
+
private imageRuntime;
|
|
164
|
+
private providerFactory;
|
|
165
|
+
private imageNotices;
|
|
155
166
|
private cwd;
|
|
156
167
|
private context;
|
|
157
168
|
private compactionTimeoutMs;
|