chatccc 0.2.258 → 0.2.260
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 +20 -11
- package/config.sample.json +23 -8
- package/deepccc-agent/package.json +1 -1
- package/dist/deepccc-agent/src/context.js +12 -3
- package/dist/deepccc-agent/src/index.js +116 -73
- package/dist/deepccc-agent/src/progress/reducer.js +4 -0
- package/dist/deepccc-agent/src/tool-protocol.js +17 -0
- package/dist/src/adapters/ccc-adapter.js +12 -0
- package/dist/src/adapters/claude-adapter.js +3 -2
- package/dist/src/adapters/dsh-adapter.js +230 -0
- package/dist/src/agent-activity.js +4 -0
- package/dist/src/agent-tool.js +2 -1
- package/dist/src/card-action-parser.js +1 -0
- package/dist/src/cards.js +11 -3
- package/dist/src/config.js +34 -2
- package/dist/src/engines/engine-manager.js +408 -0
- package/dist/src/engines/engine-specs.js +157 -0
- package/dist/src/feishu-api.js +2 -1
- package/dist/src/orchestrator.js +21 -10
- package/dist/src/progress/reducer.js +4 -0
- package/dist/src/response-stall.js +2 -2
- package/dist/src/session.js +43 -4
- package/dist/src/web-ui.js +286 -200
- package/package.json +1 -1
- package/dist/src/claude-sdk-installer.js +0 -249
package/README.md
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
# ChatCCC
|
|
2
2
|
|
|
3
|
-
**用飞书或微信聊天控制 Claude Code / Cursor / Codex / CCC Agent。**
|
|
3
|
+
**用飞书或微信聊天控制 Claude Code / Cursor / Codex / CCC Agent / DeepSeek Harness。**
|
|
4
4
|
|
|
5
|
-
ChatCCC 把本地 AI 编程工具接入即时通讯软件。你可以在手机上发消息,让 Claude Code、Cursor Agent、Codex
|
|
5
|
+
ChatCCC 把本地 AI 编程工具接入即时通讯软件。你可以在手机上发消息,让 Claude Code、Cursor Agent、Codex、内置 CCC Agent 或 DeepSeek Harness 继续写代码、查问题、跑命令;不用一直守在电脑前。
|
|
6
6
|
|
|
7
7
|
飞书是推荐入口:直接私聊机器人即可持续使用同一个专属会话,需要并行任务时再用 `/new` 创建独立会话群;卡片能流式更新,体验完整。微信 iLink 更适合快速试用或临时使用:扫码即可接入,但只能走私聊文本模式。
|
|
8
8
|
|
|
@@ -21,7 +21,7 @@ ChatCCC 把本地 AI 编程工具接入即时通讯软件。你可以在手机
|
|
|
21
21
|
- **手机上也能用 AI 编程工具**:在飞书或微信发消息,就像在终端给 Agent 下指令。
|
|
22
22
|
- **飞书体验更完整**:私聊可持续对话,`/new` 创建的一群一会话支持多任务并行,CardKit 卡片可流式更新。
|
|
23
23
|
- **微信接入更轻**:不用创建飞书应用,启动后扫码即可在微信私聊里使用。
|
|
24
|
-
- **多 Agent 切换**:`/new` 使用默认 Agent,也可以用 `/new claude`、`/new cursor`、`/new codex`、`/new ccc` 指定工具。
|
|
24
|
+
- **多 Agent 切换**:`/new` 使用默认 Agent,也可以用 `/new claude`、`/new cursor`、`/new codex`、`/new ccc`、`/new dsh` 指定工具。
|
|
25
25
|
- **群里能跑 git**:`/git status`、`/git pull`、`/git log` 会在当前会话工作目录执行,并把输出发回聊天窗口。
|
|
26
26
|
|
|
27
27
|
## 飞书和微信的差异
|
|
@@ -216,7 +216,7 @@ chatccc
|
|
|
216
216
|
|
|
217
217
|
### 3. AI 工具配置
|
|
218
218
|
|
|
219
|
-
Claude Code、Cursor 和 Codex 需要对应的本地工具;CCC Agent 内置于 ChatCCC,开箱即用,**模型接入不限于 DeepSeek**——它支持 OpenAI-compatible 和 Anthropic Messages 两种协议(详见下文 CCC Agent)。
|
|
219
|
+
Claude Code、Cursor 和 Codex 需要对应的本地工具;CCC Agent 内置于 ChatCCC,开箱即用,**模型接入不限于 DeepSeek**——它支持 OpenAI-compatible 和 Anthropic Messages 两种协议(详见下文 CCC Agent)。DeepSeek Harness(DSH)采用按需安装,不计入 ChatCCC 主包生产依赖。
|
|
220
220
|
|
|
221
221
|
#### CCC Agent
|
|
222
222
|
|
|
@@ -242,9 +242,15 @@ ChatCCC 会把 `ccc.DEEPSEEK_API_KEY`、`ccc.DEEPSEEK_BASE_URL`、模型和 effo
|
|
|
242
242
|
|
|
243
243
|
#### Claude Code
|
|
244
244
|
|
|
245
|
-
ChatCCC 通过 Anthropic Claude Agent SDK 调用 Claude Code 能力。SDK 引擎(含 Claude Code CLI 原生二进制,约 220MB)**按需下载**:在首次配置向导或 Web
|
|
245
|
+
ChatCCC 通过 Anthropic Claude Agent SDK 调用 Claude Code 能力。SDK 引擎(含 Claude Code CLI 原生二进制,约 220MB)**按需下载**:在首次配置向导或 Web 管理页打开 Claude Code 开关,或点击一次安装按钮,页面会连续显示环境检查、下载、校验、Runtime 验证、原子切换和清理进度,不需要逐步确认。**只有启用 Claude Code 的用户才需要下载**,不使用 Claude Code 的安装不包含该引擎,chatccc 主包体积保持精简。
|
|
246
246
|
|
|
247
|
-
SDK 始终使用其内置的 Claude Code CLI(档位 b),不依赖用户自行安装的 `claude` 命令。本机完成 Claude Code 登录后即可使用(SDK 复用 `~/.claude` 登录态);同时支持官方和第三方 Anthropic 兼容 API:使用官方服务无需额外配置,使用第三方 API 则填写 `claude.apiKey` 和 `claude.baseUrl`。`claude.model`、`claude.subagentModel`、`claude.effort`、`claude.apiKey`、`claude.baseUrl` 均为选填;`claude.maxTurn` 控制每次对话的最大轮数(默认 0,即无限制)。填写后会把对应配置传给 Claude Agent SDK;留空则以 `~/.claude/settings.json` 为准。
|
|
247
|
+
SDK 始终使用其内置的 Claude Code CLI(档位 b),不依赖用户自行安装的 `claude` 命令。本机完成 Claude Code 登录后即可使用(SDK 复用 `~/.claude` 登录态);同时支持官方和第三方 Anthropic 兼容 API:使用官方服务无需额外配置,使用第三方 API 则填写 `claude.apiKey` 和 `claude.baseUrl`。`claude.model`、`claude.subagentModel`、`claude.effort`、`claude.apiKey`、`claude.baseUrl` 均为选填;`claude.maxTurn` 控制每次对话的最大轮数(默认 0,即无限制)。填写后会把对应配置传给 Claude Agent SDK;留空则以 `~/.claude/settings.json` 为准。
|
|
248
|
+
|
|
249
|
+
#### DeepSeek Harness
|
|
250
|
+
|
|
251
|
+
DeepSeek Harness 是可选 Agent。在首次配置向导或 Web 管理页打开 DSH 开关,ChatCCC 会一次点击完成环境检查、临时目录准备、精确版本下载、包校验、Runtime 握手、原子切换和旧版本清理,并在页面逐步显示进度。安装失败时不会替换当前可用版本;刷新页面后仍可恢复安装进度。当前固定安装 `0.1.0-rc.6`,要求 Node.js >= 22.19.0。
|
|
252
|
+
|
|
253
|
+
DSH 引擎安装在 `~/.chatccc/engines/dsh/`,不加入 ChatCCC 的 `dependencies`。配置 `dsh.apiKey`、`dsh.baseUrl`、`dsh.model`、`dsh.provider` 和 `dsh.maxTokens` 后,可设为默认 Agent 或使用 `/new dsh` 新建会话。安装验证只做本地 JSON-RPC Runtime 握手,不会调用付费模型。
|
|
248
254
|
|
|
249
255
|
#### Cursor Agent CLI
|
|
250
256
|
|
|
@@ -374,13 +380,15 @@ Codex 的默认模型和推理强度可继续由 `~/.codex/config.toml` 管理
|
|
|
374
380
|
| `ccc.model` | CCC Agent 默认模型 |
|
|
375
381
|
| `ccc.subModel` | CCC Agent 子模型(选填):用于内部轻量环节(压缩摘要生成、task 子代理任务);留空跟随主模型 |
|
|
376
382
|
| `ccc.compactionTimeoutMs` | CCC Agent 上下文压缩单轮超时(毫秒),默认 300000(5 分钟);压缩超时会让整轮对话失败,建议保持默认或调大 |
|
|
377
|
-
| `ccc.contextWindow` | CCC Agent 模型上下文窗口(token),默认 1048576(1M,DeepSeek V4 Pro/Flash 原生规格);压缩阈值自动 = 窗口 × 80%;超过模型/服务端实际上限会被 API 拒绝,可在 Web UI 下拉选择或自定义(单位 k) |
|
|
383
|
+
| `ccc.contextWindow` | CCC Agent 模型上下文窗口(token),默认 1048576(1M,DeepSeek V4 Pro/Flash 原生规格);压缩阈值自动 = 窗口 × 80%;超过模型/服务端实际上限会被 API 拒绝,可在 Web UI 下拉选择或自定义(单位 k) |
|
|
384
|
+
| `dsh.apiKey` / `dsh.baseUrl` | DeepSeek Harness 的 API Key 和官方 DeepSeek 服务地址 |
|
|
385
|
+
| `dsh.model` / `dsh.provider` / `dsh.maxTokens` | DSH 默认模型、Runtime provider 和最大 token 数 |
|
|
378
386
|
|
|
379
387
|
> **权限控制**:普通消息以 `bypassPermissions` 模式运行,跳过 Agent 操作确认。使用 `/plan` 或 `/ask` 前缀时,ChatCCC 自动切换为只读模式:Claude SDK 仅放行 Read + stop-stuck-loop 网络请求,Codex 使用 `--sandbox read-only`,Cursor 使用 `--mode plan/ask`。请只在可信环境中使用。
|
|
380
388
|
|
|
381
389
|
### 5. 开始使用
|
|
382
390
|
|
|
383
|
-
**飞书:** 找到机器人后直接发送普通消息,即可在当前私聊中创建并持续使用专属 AI 会话;私聊工作目录固定为运行 ChatCCC 的系统账号用户目录。默认 Agent 发生变化后,下一条私聊普通消息会触发切换并创建新的空会话;若旧 Agent 正在生成,该消息会先排队,待当前回复完成后再切换。命令不会触发自动切换。需要独立任务时,发送 `/new`、`/new claude`、`/new cursor`、`/new codex` 或 `/new
|
|
391
|
+
**飞书:** 找到机器人后直接发送普通消息,即可在当前私聊中创建并持续使用专属 AI 会话;私聊工作目录固定为运行 ChatCCC 的系统账号用户目录。默认 Agent 发生变化后,下一条私聊普通消息会触发切换并创建新的空会话;若旧 Agent 正在生成,该消息会先排队,待当前回复完成后再切换。命令不会触发自动切换。需要独立任务时,发送 `/new`、`/new claude`、`/new cursor`、`/new codex`、`/new ccc` 或 `/new dsh`,机器人会另外创建会话群。
|
|
384
392
|
|
|
385
393
|
**微信:** 扫码登录后,在机器人私聊里发送 `/new` 或指定 Agent 的 `/new ...` 命令即可开始。功能与飞书基本一致,但展示为纯文本。
|
|
386
394
|
|
|
@@ -396,7 +404,8 @@ Codex 的默认模型和推理强度可继续由 `~/.codex/config.toml` 管理
|
|
|
396
404
|
| `/new claude` | 创建 Claude Code 会话;飞书中会创建新群 |
|
|
397
405
|
| `/new cursor` | 创建 Cursor 会话;飞书中会创建新群 |
|
|
398
406
|
| `/new codex` | 创建 Codex 会话;飞书中会创建新群 |
|
|
399
|
-
| `/new ccc` | 创建内置 CCC Agent 会话;飞书中会创建新群 |
|
|
407
|
+
| `/new ccc` | 创建内置 CCC Agent 会话;飞书中会创建新群 |
|
|
408
|
+
| `/new dsh` | 创建 DeepSeek Harness 会话;飞书中会创建新群 |
|
|
400
409
|
| `/forget` | 在当前聊天原地重置会话(忘掉上下文);群聊保留工作目录,飞书私聊固定使用系统用户目录 |
|
|
401
410
|
| `/model` | 查看或切换当前会话的模型 |
|
|
402
411
|
| `/fast` | 查看当前 Codex 会话的 Fast 模式;使用 `/fast on` 或 `/fast off` 切换 |
|
|
@@ -406,7 +415,7 @@ Codex 的默认模型和推理强度可继续由 `~/.codex/config.toml` 管理
|
|
|
406
415
|
| `/cd` | 查看或设置后续新建会话的默认工作目录,不改变当前会话;飞书私聊自身始终使用系统用户目录 |
|
|
407
416
|
| `/sessions` | 查看所有会话状态 |
|
|
408
417
|
| `/session <数字>` | 将当前群聊切换到 `/sessions` 列表中的指定会话;飞书私聊不支持切换 |
|
|
409
|
-
| `/usage` | 查看当前会话对应 Agent 的用量;Codex 显示 5h/7天窗口,Cursor 显示当前周期用量,CCC Agent 仅在官方 DeepSeek 端点时显示账户余额(其他兼容端点自动跳过) |
|
|
418
|
+
| `/usage` | 查看当前会话对应 Agent 的用量;Codex 显示 5h/7天窗口,Cursor 显示当前周期用量,CCC Agent 和 DSH 仅在官方 DeepSeek 端点时显示账户余额(其他兼容端点自动跳过) |
|
|
410
419
|
| `/git <子命令>` | 在当前会话工作目录执行 `git ...` 并回传输出 |
|
|
411
420
|
| `/abd<内容>` | 去掉 `/abd` 前缀后把内容发给 Agent,并在消息末尾追加第一性原理需求澄清提示 |
|
|
412
421
|
| `/plan <内容>` | 只读计划模式:仅允许读文件和 stop-stuck-loop 请求,不执行任何写操作 |
|
|
@@ -425,4 +434,4 @@ Codex 的默认模型和推理强度可继续由 `~/.codex/config.toml` 管理
|
|
|
425
434
|
|
|
426
435
|
## 技术栈
|
|
427
436
|
|
|
428
|
-
TypeScript(发布前编译为 JavaScript,tsx 仅用于开发)/ Node.js >= 20 / AI SDK / Anthropic Claude Agent SDK
|
|
437
|
+
TypeScript(发布前编译为 JavaScript,tsx 仅用于开发)/ Node.js >= 20 / AI SDK / Anthropic Claude Agent SDK(按需下载)/ DeepSeek Harness SDK + JSON-RPC Runtime(按需下载,要求 Node.js >= 22.19.0)/ Cursor Agent CLI / Codex CLI / 飞书 WebSocket API / CardKit / 微信 iLink
|
package/config.sample.json
CHANGED
|
@@ -37,14 +37,20 @@
|
|
|
37
37
|
"retentionDays": 7,
|
|
38
38
|
"keepCompleted": false
|
|
39
39
|
},
|
|
40
|
-
"ccc": {
|
|
41
|
-
"enabled": false,
|
|
42
|
-
"maxBytesPerTurn": 52428800,
|
|
43
|
-
"retentionDays": 7,
|
|
44
|
-
"keepCompleted": false
|
|
40
|
+
"ccc": {
|
|
41
|
+
"enabled": false,
|
|
42
|
+
"maxBytesPerTurn": 52428800,
|
|
43
|
+
"retentionDays": 7,
|
|
44
|
+
"keepCompleted": false
|
|
45
|
+
},
|
|
46
|
+
"dsh": {
|
|
47
|
+
"enabled": false,
|
|
48
|
+
"maxBytesPerTurn": 52428800,
|
|
49
|
+
"retentionDays": 7,
|
|
50
|
+
"keepCompleted": false
|
|
45
51
|
}
|
|
46
52
|
},
|
|
47
|
-
"ccc": {
|
|
53
|
+
"ccc": {
|
|
48
54
|
"enabled": false,
|
|
49
55
|
"defaultAgent": false,
|
|
50
56
|
"DEEPSEEK_API_KEY": "",
|
|
@@ -55,8 +61,17 @@
|
|
|
55
61
|
"effort": "",
|
|
56
62
|
"provider": "",
|
|
57
63
|
"compactionTimeoutMs": 300000,
|
|
58
|
-
"contextWindow": 1048576
|
|
59
|
-
},
|
|
64
|
+
"contextWindow": 1048576
|
|
65
|
+
},
|
|
66
|
+
"dsh": {
|
|
67
|
+
"enabled": false,
|
|
68
|
+
"defaultAgent": false,
|
|
69
|
+
"apiKey": "",
|
|
70
|
+
"baseUrl": "https://api.deepseek.com/v1",
|
|
71
|
+
"model": "deepseek-v4-flash",
|
|
72
|
+
"provider": "deepseek-official",
|
|
73
|
+
"maxTokens": 49152
|
|
74
|
+
},
|
|
60
75
|
"claude": {
|
|
61
76
|
"enabled": false,
|
|
62
77
|
"defaultAgent": true,
|
|
@@ -2,6 +2,7 @@ import { createHash, randomBytes } from "node:crypto";
|
|
|
2
2
|
import { existsSync, mkdirSync, readdirSync, readFileSync, renameSync, writeFileSync } from "node:fs";
|
|
3
3
|
import { homedir } from "node:os";
|
|
4
4
|
import { join } from "node:path";
|
|
5
|
+
import { hasMalformedToolProtocolText } from "./tool-protocol.js";
|
|
5
6
|
export const DEFAULT_BUILTIN_CONTEXT_DIR = join(homedir(), ".deepccc", "sessions");
|
|
6
7
|
/** 默认模型上下文窗口:1M(DeepSeek V4 Pro/Flash 原生规格)。 */
|
|
7
8
|
export const DEFAULT_CONTEXT_WINDOW_TOKENS = 1_048_576;
|
|
@@ -290,14 +291,19 @@ export class BuiltinContextManager {
|
|
|
290
291
|
].join("\n"),
|
|
291
292
|
});
|
|
292
293
|
}
|
|
293
|
-
|
|
294
|
+
// Keep malformed provider output on disk for diagnosis, but quarantine it
|
|
295
|
+
// from future prompts so one protocol failure cannot teach the model to
|
|
296
|
+
// repeat the same invalid tool syntax on every later turn.
|
|
297
|
+
messages.push(...this.modelSafeMessages());
|
|
294
298
|
return messages;
|
|
295
299
|
}
|
|
296
300
|
planCompaction() {
|
|
297
|
-
|
|
301
|
+
// The compaction model is still a model input: exclude quarantined protocol
|
|
302
|
+
// leaks here too, otherwise a later summary could reintroduce the bad syntax.
|
|
303
|
+
const messages = this.modelSafeMessages();
|
|
304
|
+
const estimated = estimateBuiltinContextTokens(this.state.summary, messages);
|
|
298
305
|
if (estimated <= this.compactAtTokens)
|
|
299
306
|
return null;
|
|
300
|
-
const messages = this.state.messages;
|
|
301
307
|
if (messages.length <= 1)
|
|
302
308
|
return null;
|
|
303
309
|
const earliestAllowed = Math.max(0, messages.length - this.keepRecentMessages);
|
|
@@ -340,6 +346,9 @@ export class BuiltinContextManager {
|
|
|
340
346
|
writeFileSync(tmp, content, "utf8");
|
|
341
347
|
renameSync(tmp, this.contextFilePath);
|
|
342
348
|
}
|
|
349
|
+
modelSafeMessages() {
|
|
350
|
+
return this.state.messages.filter((message) => message.role !== "assistant" || !hasMalformedToolProtocolText(message.content));
|
|
351
|
+
}
|
|
343
352
|
load() {
|
|
344
353
|
if (!this.persist || !existsSync(this.contextFilePath))
|
|
345
354
|
return emptyState(this.sessionId, this.cwd);
|
|
@@ -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
|
// ---------------------------------------------------------------------------
|
|
@@ -400,7 +401,6 @@ export class ChatSession {
|
|
|
400
401
|
catch (err) {
|
|
401
402
|
console.error(`[DeepCCC raw stream log] create failed: ${errorMessage(err)}`);
|
|
402
403
|
}
|
|
403
|
-
const toolContext = [];
|
|
404
404
|
const maxSteps = this.maxSteps;
|
|
405
405
|
// 每次对话前重新扫描技能索引(并行 + mtime 缓存,开销极小):
|
|
406
406
|
// 新技能/修改的技能在下一次对话自动生效(热加载)。
|
|
@@ -423,10 +423,9 @@ export class ChatSession {
|
|
|
423
423
|
? { deepseek: { reasoningEffort: this.effort } }
|
|
424
424
|
: { anthropic: { effort: this.effort } };
|
|
425
425
|
}
|
|
426
|
-
const
|
|
426
|
+
const baseGenerationOptions = {
|
|
427
427
|
model: this.model,
|
|
428
428
|
system,
|
|
429
|
-
messages: modelMessages,
|
|
430
429
|
tools: createBuiltinFileTools(this.cwd, {
|
|
431
430
|
permissionGate: this.permissionGate,
|
|
432
431
|
runTask: this.runTask,
|
|
@@ -435,88 +434,132 @@ export class ChatSession {
|
|
|
435
434
|
abortSignal: signal,
|
|
436
435
|
...(effortProviderOptions ? { providerOptions: effortProviderOptions } : {}),
|
|
437
436
|
};
|
|
438
|
-
let
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
437
|
+
for (let attempt = 0; attempt < 2; attempt += 1) {
|
|
438
|
+
fullText = "";
|
|
439
|
+
safeAccumulated = "";
|
|
440
|
+
const toolContext = [];
|
|
441
|
+
toolCallsById.clear();
|
|
442
|
+
toolCallOrder.length = 0;
|
|
443
|
+
let lastReasoningProgressAt;
|
|
444
|
+
const attemptMessages = attempt === 0
|
|
445
|
+
? modelMessages
|
|
446
|
+
: [...modelMessages, { role: "user", content: TOOL_PROTOCOL_RECOVERY_PROMPT }];
|
|
447
|
+
const generationOptions = {
|
|
448
|
+
...baseGenerationOptions,
|
|
449
|
+
messages: attemptMessages,
|
|
450
|
+
};
|
|
451
|
+
let stream;
|
|
452
|
+
if (appConfig.streaming) {
|
|
453
|
+
const result = streamText(generationOptions);
|
|
454
|
+
stream = result.fullStream ?? textStreamToFullStream(result.textStream);
|
|
456
455
|
}
|
|
457
|
-
else
|
|
458
|
-
|
|
459
|
-
|
|
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
|
-
};
|
|
456
|
+
else {
|
|
457
|
+
const result = await generateText(generationOptions);
|
|
458
|
+
stream = generateResultToFullStream(result);
|
|
467
459
|
}
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
460
|
+
for await (const part of stream) {
|
|
461
|
+
rawLog?.writeLine(safeRawStreamJson(part));
|
|
462
|
+
if (part.type === "reasoning-start" || part.type === "reasoning-delta") {
|
|
463
|
+
// Reasoning content remains private. A throttled heartbeat is enough
|
|
464
|
+
// for ChatCCC to distinguish active inference from a stalled stream.
|
|
465
|
+
const now = Date.now();
|
|
466
|
+
if (lastReasoningProgressAt === undefined || now - lastReasoningProgressAt >= 1_000) {
|
|
467
|
+
lastReasoningProgressAt = now;
|
|
468
|
+
yield { type: "progress", phase: "reasoning" };
|
|
469
|
+
}
|
|
473
470
|
}
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
471
|
+
else if (part.type === "text-delta") {
|
|
472
|
+
fullText += part.text;
|
|
473
|
+
// 隐私替换只在展示层:safeAccumulated 供事件消费者(终端/JSONL)使用,
|
|
474
|
+
// fullText 原文用于持久化上下文,避免替换结果回流污染上下文。
|
|
475
|
+
const safeText = applyPrivacy(part.text);
|
|
476
|
+
safeAccumulated += safeText;
|
|
477
|
+
yield { type: "text", text: safeText, accumulated: safeAccumulated };
|
|
478
|
+
}
|
|
479
|
+
else if (part.type === "tool-call") {
|
|
480
|
+
toolContext.push(`tool_call ${part.toolName}: ${safeJson(part.input)}`);
|
|
481
|
+
toolCallsById.set(part.toolCallId, { name: part.toolName, input: safeJson(part.input) });
|
|
482
|
+
toolCallOrder.push(part.toolCallId);
|
|
483
|
+
yield {
|
|
484
|
+
type: "tool_use",
|
|
485
|
+
id: part.toolCallId,
|
|
486
|
+
name: part.toolName,
|
|
487
|
+
input: applyPrivacyToJson(part.input),
|
|
488
|
+
};
|
|
489
|
+
}
|
|
490
|
+
else if (part.type === "tool-result") {
|
|
491
|
+
toolContext.push(`tool_result ${part.toolName}: ${truncateToolContext(safeJson(part.output))}`);
|
|
492
|
+
const call = toolCallsById.get(part.toolCallId);
|
|
493
|
+
if (call)
|
|
494
|
+
call.output = truncateToolContext(safeJson(part.output));
|
|
495
|
+
yield {
|
|
496
|
+
type: "tool_result",
|
|
497
|
+
tool_use_id: part.toolCallId,
|
|
498
|
+
name: part.toolName,
|
|
499
|
+
content: applyPrivacyToJson(part.output),
|
|
500
|
+
is_error: false,
|
|
501
|
+
};
|
|
502
|
+
}
|
|
503
|
+
else if (part.type === "tool-error") {
|
|
504
|
+
const message = errorMessage(part.error);
|
|
505
|
+
toolContext.push(`tool_error ${part.toolName}: ${message}`);
|
|
506
|
+
const call = toolCallsById.get(part.toolCallId);
|
|
507
|
+
if (call) {
|
|
508
|
+
call.output = message;
|
|
509
|
+
call.is_error = true;
|
|
510
|
+
}
|
|
511
|
+
yield {
|
|
512
|
+
type: "tool_result",
|
|
513
|
+
tool_use_id: part.toolCallId,
|
|
514
|
+
name: part.toolName,
|
|
515
|
+
content: applyPrivacy(message),
|
|
516
|
+
is_error: true,
|
|
517
|
+
};
|
|
518
|
+
}
|
|
519
|
+
else if (part.type === "error") {
|
|
520
|
+
const message = errorMessage(part.error);
|
|
521
|
+
yield { type: "error", message: applyPrivacy(message) };
|
|
522
|
+
throw new Error(message);
|
|
489
523
|
}
|
|
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
524
|
}
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
525
|
+
if (hasMalformedToolProtocolText(fullText)) {
|
|
526
|
+
console.warn(`[DeepCCC] malformed DSML tool output detected for ${this.context.sessionId} `
|
|
527
|
+
+ `(attempt ${attempt + 1}/2, structuredToolCalls=${toolCallOrder.length})`);
|
|
528
|
+
rawLog?.writeLine(safeRawStreamJson({
|
|
529
|
+
type: "deepccc_tool_protocol_recovery",
|
|
530
|
+
attempt: attempt + 1,
|
|
531
|
+
structuredToolCalls: toolCallOrder.length,
|
|
532
|
+
}));
|
|
533
|
+
yield { type: "text_reset" };
|
|
534
|
+
if (attempt === 0 && toolCallOrder.length === 0) {
|
|
535
|
+
yield { type: "status", phase: "generating" };
|
|
536
|
+
continue;
|
|
537
|
+
}
|
|
538
|
+
throw new Error(toolCallOrder.length > 0
|
|
539
|
+
? "工具调用协议异常:检测到混合的结构化与 DSML 文本调用,为避免重复执行工具,本轮已安全终止"
|
|
540
|
+
: "工具调用协议异常:模型重试后仍输出了无效的 DSML 工具调用");
|
|
502
541
|
}
|
|
542
|
+
completed = true;
|
|
543
|
+
const collectedToolCalls = toolCallOrder
|
|
544
|
+
.map((id) => toolCallsById.get(id))
|
|
545
|
+
.filter((call) => call !== undefined);
|
|
546
|
+
this.context.appendMessage(buildPersistedAssistantMessage({
|
|
547
|
+
fullText,
|
|
548
|
+
transcriptLines: toolContext,
|
|
549
|
+
toolCalls: collectedToolCalls,
|
|
550
|
+
}));
|
|
551
|
+
yield { type: "done", text: safeAccumulated };
|
|
552
|
+
return;
|
|
503
553
|
}
|
|
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
554
|
}
|
|
515
555
|
catch (err) {
|
|
516
556
|
const message = err instanceof Error ? err.message : String(err);
|
|
557
|
+
const malformedProtocolOutput = hasMalformedToolProtocolText(fullText);
|
|
558
|
+
if (malformedProtocolOutput)
|
|
559
|
+
yield { type: "text_reset" };
|
|
517
560
|
if (err.name === "AbortError" || signal?.aborted) {
|
|
518
561
|
// 被中断时,不保存不完整的助手消息
|
|
519
|
-
if (fullText) {
|
|
562
|
+
if (fullText && !malformedProtocolOutput) {
|
|
520
563
|
this.context.appendMessage({ role: "assistant", content: `${fullText}\n[interrupted]` });
|
|
521
564
|
}
|
|
522
565
|
yield { type: "done", text: safeAccumulated };
|
|
@@ -57,6 +57,10 @@ export function reduceProgress(prev, event) {
|
|
|
57
57
|
case "text":
|
|
58
58
|
// accumulated 是全文累积,直接全量替换,天然幂等
|
|
59
59
|
return withProgressView(prev, { text: event.accumulated });
|
|
60
|
+
case "progress":
|
|
61
|
+
return withProgressView(prev, { headerTitle: "思考中..." });
|
|
62
|
+
case "text_reset":
|
|
63
|
+
return withProgressView(prev, { text: "", tools: [] });
|
|
60
64
|
case "tool_use": {
|
|
61
65
|
const tool = {
|
|
62
66
|
id: event.id ?? `tool-${prev.tools.length + 1}`,
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Detects provider output that leaked DeepSeek's internal DSML tool-call syntax
|
|
3
|
+
* into normal assistant text. A trailing closing tag is intentionally required:
|
|
4
|
+
* mentioning DSML in prose or a code sample must not trigger recovery.
|
|
5
|
+
*/
|
|
6
|
+
const MALFORMED_DSML_TRAILER = /<\/[||]{2}DSML[||]{2}(?:parameter|invoke)>\s*$/iu;
|
|
7
|
+
const RENDERED_TOOL_CALL = /\[调用\s+[A-Za-z_][\w.-]*\]/u;
|
|
8
|
+
const DSML_INVOKE_TAG = /<[||]{2}DSML[||]{2}invoke\b/iu;
|
|
9
|
+
export function hasMalformedToolProtocolText(text) {
|
|
10
|
+
return MALFORMED_DSML_TRAILER.test(text)
|
|
11
|
+
&& (RENDERED_TOOL_CALL.test(text) || DSML_INVOKE_TAG.test(text));
|
|
12
|
+
}
|
|
13
|
+
export const TOOL_PROTOCOL_RECOVERY_PROMPT = [
|
|
14
|
+
"[系统恢复提示] 上一次响应泄漏了内部工具调用协议,因此已被丢弃。",
|
|
15
|
+
"请重新完成当前用户请求。需要调用工具时,只能使用 API 提供的结构化工具调用;不要把 DSML 或工具参数作为普通文本输出。",
|
|
16
|
+
"不要提及本恢复提示,也不要假装工具已经执行。",
|
|
17
|
+
].join("\n");
|
|
@@ -55,6 +55,18 @@ export function createCccAdapter(options = {}) {
|
|
|
55
55
|
}],
|
|
56
56
|
};
|
|
57
57
|
}
|
|
58
|
+
else if (event.type === "progress") {
|
|
59
|
+
yield {
|
|
60
|
+
type: "assistant",
|
|
61
|
+
blocks: [{ type: "agent_progress", phase: event.phase }],
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
else if (event.type === "text_reset") {
|
|
65
|
+
yield {
|
|
66
|
+
type: "assistant",
|
|
67
|
+
blocks: [{ type: "text_reset" }],
|
|
68
|
+
};
|
|
69
|
+
}
|
|
58
70
|
else if (event.type === "text") {
|
|
59
71
|
yield {
|
|
60
72
|
type: "assistant",
|
|
@@ -14,7 +14,7 @@ import { parseUserCommand } from "./adapter-interface.js";
|
|
|
14
14
|
import { CHATCCC_PORT, config, PROJECT_ROOT, RAW_STREAM_LOGS_DIR } from "../config.js";
|
|
15
15
|
import { defaultClaudeSessionMetaStore, } from "./claude-session-meta-store.js";
|
|
16
16
|
import { createRawStreamLog, } from "./raw-stream-log.js";
|
|
17
|
-
import {
|
|
17
|
+
import { engineManager } from "../engines/engine-specs.js";
|
|
18
18
|
const CLAUDE_SPECIFIC_PROMPT_PATH = join(PROJECT_ROOT, "agent-prompts", "claude_specific.md");
|
|
19
19
|
export function buildSdkEnv(subagentModel, apiKey, baseUrl) {
|
|
20
20
|
const subagentModelTrim = (subagentModel ?? "").trim();
|
|
@@ -230,7 +230,8 @@ let sdkModulePromise = null;
|
|
|
230
230
|
*/
|
|
231
231
|
export function loadClaudeSdkModule() {
|
|
232
232
|
if (!sdkModulePromise) {
|
|
233
|
-
sdkModulePromise =
|
|
233
|
+
sdkModulePromise = engineManager.getEntryPath("claude")
|
|
234
|
+
.then((entryPath) => import(__rewriteRelativeImportExtension(pathToFileURL(entryPath).href)))
|
|
234
235
|
.then((m) => m)
|
|
235
236
|
.catch((err) => {
|
|
236
237
|
sdkModulePromise = null;
|