deepccc 0.2.14 → 0.2.16

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
@@ -1,75 +1,75 @@
1
- # DeepCCC
2
-
3
- DeepCCC 是一个本地优先的开源 Coding Agent,同时提供浏览器多会话界面、终端 CLI 和适合自动化集成的 JSONL 流。它针对 DeepSeek 做了缓存和上下文优化,也支持任意 OpenAI-compatible 服务以及 Anthropic Messages 协议。
4
-
5
- - 项目主页:https://github.com/wzj998/deepccc-agent
6
- - npm 包:https://www.npmjs.com/package/deepccc
7
- - 运行要求:Node.js >= 20,以及一个兼容模型服务的 API Key
8
-
9
- ## 安装与快速开始
10
-
11
- 模型原生流未收到完成事件、返回错误结束原因或达到输出长度限制时,会报告异常并保留已经输出的内容,不将不完整回复判为成功。没有回复且没有工具执行的空结果也会明确报错。用户主动取消仍按中断处理。
12
-
13
- 全局安装:
1
+ # DeepCCC
2
+
3
+ DeepCCC 是一个本地优先的开源 Coding Agent,同时提供浏览器多会话界面、终端 CLI 和适合自动化集成的 JSONL 流。它针对 DeepSeek 做了缓存和上下文优化,也支持任意 OpenAI-compatible 服务以及 Anthropic Messages 协议。
4
+
5
+ - 项目主页:https://github.com/wzj998/deepccc-agent
6
+ - npm 包:https://www.npmjs.com/package/deepccc
7
+ - 运行要求:Node.js >= 20,以及一个兼容模型服务的 API Key
8
+
9
+ ## 安装与快速开始
10
+
11
+ 模型原生流未收到完成事件、返回错误结束原因或达到输出长度限制时,会报告异常并保留已经输出的内容,不将不完整回复判为成功。没有回复且没有工具执行的空结果也会明确报错。用户主动取消仍按中断处理。
12
+
13
+ 全局安装:
14
+
15
+ ```bash
16
+ npm install -g deepccc
17
+ ```
18
+
19
+ 配置 `~/.deepccc/config.json` 或 `DEEPCCC_*` 环境变量后,运行:
14
20
 
15
21
  ```bash
16
- npm install -g deepccc
17
- ```
18
-
19
- 配置 `~/.deepccc/config.json` 或 `DEEPCCC_*` 环境变量后,运行:
20
-
21
- ```bash
22
- deepccc
23
- ```
24
-
25
- 浏览器会自动打开 `http://127.0.0.1:28080/`。终端模式使用:
26
-
27
- ```bash
28
- deepccc-cli
29
- ```
30
-
31
- 从源码运行:
22
+ deepccc
23
+ ```
24
+
25
+ 浏览器会自动打开 `http://127.0.0.1:28080/`。终端模式使用:
26
+
27
+ ```bash
28
+ deepccc-cli
29
+ ```
30
+
31
+ 从源码运行:
32
32
 
33
33
  ```bash
34
34
  git clone https://github.com/wzj998/deepccc-agent.git
35
35
  cd deepccc-agent
36
- npm install
37
- npm run build
38
- npm run dev
39
- ```
40
-
41
- ## Web UI 预览
42
-
43
- 以下画面来自 DeepCCC 对本项目真实开发需求的 Agent 调用。截图仅将用户名、组织名、
44
- 内部域名和绝对路径替换为公开示例,任务内容、模型配置、Agent 回复和审批流程均来自
45
- 实际运行结果。
46
-
47
- 多会话可以并行处理不同任务,每个会话分别选择 model、subModel 和 effort。下图来自
48
- `deepccc-agent/` 工作目录中的真实提问“这个项目妙在哪?”:
49
-
50
- ![DeepCCC Web UI 图片附件与真实 Agent 回复](docs/deepccc-web-ui.png)
51
-
52
- 命中需要确认的命令时,审批卡会直接出现在当前会话时间线中,不打断到弹窗:
53
-
54
- ![DeepCCC 会话内操作审批](docs/deepccc-inline-approval.png)
55
-
56
- 单一 API 配置作为新会话默认值,模型与 effort 仍可在每个会话中单独覆盖:
57
-
58
- ![DeepCCC API 与 Web 设置](docs/deepccc-api-settings.png)
59
-
60
- ## 核心能力
61
-
62
- - Web-first:多会话、持久化历史、实时流式过程、停止和恢复
63
- - 工具时间线:ChatCCC 风格 emoji 摘要、参数/结果折叠、省略行展开和状态记忆
64
- - 会话配置:每个会话独立选择 model、subModel 和 effort
65
- - 图片附件:Web 支持选择、粘贴和拖拽 PNG/JPEG/WebP;Agent 可用 `present_file` 回传图片
66
- - 本地工具:代码搜索、文件读写、补丁、命令执行、Git、网页搜索和抓取
67
- - 权限审批:危险命令在会话时间线中暂停,支持拒绝、允许一次、会话允许和永久允许
68
- - 上下文管理:自动压缩、原始流日志和跨会话历史检索
69
- - 长会话校准:当前状态覆盖旧建议,摘要区分历史/事实/推断/局限;轮内工具结果有独立预算,避免多步调查持续膨胀
70
- - 项目约定:自动加载 AGENTS.md、CLAUDE.md、系统提示和目录式 Skills
71
- - 项目理解:可控搜索范围、按需本地项目地图、源文件变化即失效的证据笔记;不针对特定业务仓库,详见 [项目理解与搜索](docs/workspace-understanding.md)
72
- - 自动化:`deepccc-cli --stream-json` 提供稳定 JSONL 事件接口
36
+ npm install
37
+ npm run build
38
+ npm run dev
39
+ ```
40
+
41
+ ## Web UI 预览
42
+
43
+ 以下画面来自 DeepCCC 对本项目真实开发需求的 Agent 调用。截图仅将用户名、组织名、
44
+ 内部域名和绝对路径替换为公开示例,任务内容、模型配置、Agent 回复和审批流程均来自
45
+ 实际运行结果。
46
+
47
+ 多会话可以并行处理不同任务,每个会话分别选择 model、subModel 和 effort。下图来自
48
+ `deepccc-agent/` 工作目录中的真实提问“这个项目妙在哪?”:
49
+
50
+ ![DeepCCC Web UI 图片附件与真实 Agent 回复](docs/deepccc-web-ui.png)
51
+
52
+ 命中需要确认的命令时,审批卡会直接出现在当前会话时间线中,不打断到弹窗:
53
+
54
+ ![DeepCCC 会话内操作审批](docs/deepccc-inline-approval.png)
55
+
56
+ 单一 API 配置作为新会话默认值,模型与 effort 仍可在每个会话中单独覆盖:
57
+
58
+ ![DeepCCC API 与 Web 设置](docs/deepccc-api-settings.png)
59
+
60
+ ## 核心能力
61
+
62
+ - Web-first:多会话、持久化历史、实时流式过程、停止和恢复
63
+ - 工具时间线:ChatCCC 风格 emoji 摘要、参数/结果折叠、省略行展开和状态记忆
64
+ - 会话配置:每个会话独立选择 model、subModel 和 effort
65
+ - 图片附件:Web 支持选择、粘贴和拖拽 PNG/JPEG/WebP;Agent 可用 `present_file` 回传图片
66
+ - 本地工具:代码搜索、文件读写、补丁、命令执行、Git、网页搜索和抓取
67
+ - 权限审批:危险命令在会话时间线中暂停,支持拒绝、允许一次、会话允许和永久允许
68
+ - 上下文管理:自动压缩、原始流日志和跨会话历史检索
69
+ - 长会话校准:当前状态覆盖旧建议,摘要区分历史/事实/推断/局限;轮内工具结果有独立预算,避免多步调查持续膨胀
70
+ - 项目约定:自动加载 AGENTS.md、CLAUDE.md、系统提示和目录式 Skills
71
+ - 项目理解:可控搜索范围、按需本地项目地图、源文件变化即失效的证据笔记;不针对特定业务仓库,详见 [项目理解与搜索](docs/workspace-understanding.md)
72
+ - 自动化:`deepccc-cli --stream-json` 提供稳定 JSONL 事件接口
73
73
 
74
74
  ## 缓存命中率
75
75
 
@@ -86,10 +86,10 @@ deepccc 的本地缓存优化实测命中率 **96.7%**,有效降低重复请
86
86
  ```bash
87
87
  export DEEPCCC_API_KEY="sk-..."
88
88
  export DEEPCCC_BASE_URL="https://api.deepseek.com/v1"
89
- export DEEPCCC_MODEL="deepseek-v4-pro"
90
- export DEEPCCC_EFFORT="high"
91
- export DEEPCCC_MAX_OUTPUT_TOKENS="32768"
92
- export DEEPCCC_STREAMING="true"
89
+ export DEEPCCC_MODEL="deepseek-v4-pro"
90
+ export DEEPCCC_EFFORT="high"
91
+ export DEEPCCC_MAX_OUTPUT_TOKENS="32768"
92
+ export DEEPCCC_STREAMING="true"
93
93
  ```
94
94
 
95
95
  Windows PowerShell:
@@ -98,10 +98,10 @@ Windows PowerShell:
98
98
  $env:DEEPCCC_API_KEY="sk-..."
99
99
  $env:DEEPCCC_PROVIDER="openai"
100
100
  $env:DEEPCCC_BASE_URL="https://api.deepseek.com/v1"
101
- $env:DEEPCCC_MODEL="deepseek-v4-pro"
102
- $env:DEEPCCC_EFFORT="high"
103
- $env:DEEPCCC_MAX_OUTPUT_TOKENS="32768"
104
- $env:DEEPCCC_STREAMING="true"
101
+ $env:DEEPCCC_MODEL="deepseek-v4-pro"
102
+ $env:DEEPCCC_EFFORT="high"
103
+ $env:DEEPCCC_MAX_OUTPUT_TOKENS="32768"
104
+ $env:DEEPCCC_STREAMING="true"
105
105
  ```
106
106
 
107
107
  也兼容这些 DeepSeek 别名:
@@ -119,10 +119,10 @@ $env:DEEPCCC_STREAMING="true"
119
119
  "apiKey": "sk-...",
120
120
  "baseURL": "https://api.deepseek.com/v1",
121
121
  "model": "deepseek-v4-pro",
122
- "subModel": "",
123
- "effort": "",
124
- "maxOutputTokens": null,
125
- "streaming": true,
122
+ "subModel": "",
123
+ "effort": "",
124
+ "maxOutputTokens": null,
125
+ "streaming": true,
126
126
  "contextWindow": 1048576,
127
127
  "git": {
128
128
  "coAuthor": {
@@ -131,39 +131,39 @@ $env:DEEPCCC_STREAMING="true"
131
131
  "email": "20184052+wzj998@users.noreply.github.com"
132
132
  }
133
133
  },
134
- "rawStreamLogs": {
134
+ "rawStreamLogs": {
135
135
  "enabled": true,
136
136
  "maxBytesPerTurn": 1048576,
137
137
  "retentionDays": 7,
138
- "keepCompleted": false
139
- },
140
- "web": {
141
- "port": 28080,
142
- "openOnStart": true
143
- }
144
- }
145
- ```
146
-
147
- ## Web UI
148
-
149
- 全局安装后可以直接启动本地网页版:
150
-
151
- ```bash
152
- deepccc
153
- ```
154
-
155
- 默认只监听 `http://127.0.0.1:28080/`,不会暴露到局域网。可在
156
- `~/.deepccc/config.json` 的 `web.port` 修改端口,`web.openOnStart` 控制启动时是否自动打开浏览器;也可以临时使用 `deepccc --port 28081 --no-open`。默认启动会安全替换经过实例身份验证的旧 DeepCCC Web;传入 `--reuse-existing` 时复用已有实例。`deepccc web` 保留为兼容别名。
157
-
158
- 从源码开发时,`npm run dev` 启动 Web Server 并打开页面(不启用 watch);`npm run dev:cli` 启动终端模式。
159
-
160
- Web UI 支持新建、恢复、重命名和删除多会话,多个会话可以同时运行,即使它们指向同一个工作目录。每个会话可独立选择 model、subModel 和 effort,并持续复用 CLI 已保存的历史。注意:当前版本不自动创建 Git worktree;同目录的多个运行中 Agent 直接修改同一组文件,页面会提示覆盖与冲突风险。
161
-
162
- 模型文本、reasoning 心跳和工具事件通过 SSE 实时更新。每轮消息按真实发生顺序持久化和回放,因此刷新后仍会保持“阶段说明 → 工具调用 → 后续结论”的交错时间线;旧版会话没有顺序数据时,会兼容显示为“工具调用 → 最终回答”。每次工具调用与对应结果合并成一张卡片,折叠态显示 emoji、工具名、状态和关键参数;展开后调用参数默认保留前 8/后 4 行,工具结果保留前 12/后 6 行,省略内容可继续展开。工具卡及省略行的展开状态保存在当前浏览器标签页的 `sessionStorage`,持续生成、切换会话和刷新页面均不会自动收起。消息正文支持标题、表格、列表、引用、链接和代码块等常用 Markdown。
163
-
164
- 图片始终按本地附件处理,不转换为 Provider 原生多模态消息。Web 支持文件选择、剪贴板粘贴和拖拽,每条消息最多 10 张 PNG/JPEG/WebP、单张最大 20 MB;附件复制到 `~/.deepccc/attachments/<session-id>/`,Agent 收到本地绝对路径后使用可用工具自行处理。Agent 可调用 `present_file` 把当前工作目录或会话附件目录中的图片直接展示在会话中;删除会话时对应附件一并清理。
165
-
166
- API 设置采用单一 Provider 配置,支持 OpenAI-compatible 与 Anthropic Messages。完整 API Key 只保存在本机 `~/.deepccc/config.json`,浏览器读取设置时仅返回掩码。危险命令会在会话中暂停并请求“拒绝、允许一次、本会话允许、永久允许”,浏览器断开或审批超时默认拒绝。
138
+ "keepCompleted": false
139
+ },
140
+ "web": {
141
+ "port": 28080,
142
+ "openOnStart": true
143
+ }
144
+ }
145
+ ```
146
+
147
+ ## Web UI
148
+
149
+ 全局安装后可以直接启动本地网页版:
150
+
151
+ ```bash
152
+ deepccc
153
+ ```
154
+
155
+ 默认只监听 `http://127.0.0.1:28080/`,不会暴露到局域网。可在
156
+ `~/.deepccc/config.json` 的 `web.port` 修改端口,`web.openOnStart` 控制启动时是否自动打开浏览器;也可以临时使用 `deepccc --port 28081 --no-open`。默认启动会安全替换经过实例身份验证的旧 DeepCCC Web;传入 `--reuse-existing` 时复用已有实例。`deepccc web` 保留为兼容别名。
157
+
158
+ 从源码开发时,`npm run dev` 启动 Web Server 并打开页面(不启用 watch);`npm run dev:cli` 启动终端模式。
159
+
160
+ Web UI 支持新建、恢复、重命名和删除多会话,多个会话可以同时运行,即使它们指向同一个工作目录。每个会话可独立选择 model、subModel 和 effort,并持续复用 CLI 已保存的历史。注意:当前版本不自动创建 Git worktree;同目录的多个运行中 Agent 直接修改同一组文件,页面会提示覆盖与冲突风险。
161
+
162
+ 模型文本、reasoning 心跳和工具事件通过 SSE 实时更新。每轮消息按真实发生顺序持久化和回放,因此刷新后仍会保持“阶段说明 → 工具调用 → 后续结论”的交错时间线;旧版会话没有顺序数据时,会兼容显示为“工具调用 → 最终回答”。每次工具调用与对应结果合并成一张卡片,折叠态显示 emoji、工具名、状态和关键参数;展开后调用参数默认保留前 8/后 4 行,工具结果保留前 12/后 6 行,省略内容可继续展开。工具卡及省略行的展开状态保存在当前浏览器标签页的 `sessionStorage`,持续生成、切换会话和刷新页面均不会自动收起。消息正文支持标题、表格、列表、引用、链接和代码块等常用 Markdown。
163
+
164
+ 图片始终按本地附件处理,不转换为 Provider 原生多模态消息。Web 支持文件选择、剪贴板粘贴和拖拽,每条消息最多 10 张 PNG/JPEG/WebP、单张最大 20 MB;附件复制到 `~/.deepccc/attachments/<session-id>/`,Agent 收到本地绝对路径后使用可用工具自行处理。Agent 可调用 `present_file` 把当前工作目录或会话附件目录中的图片直接展示在会话中;删除会话时对应附件一并清理。
165
+
166
+ API 设置采用单一 Provider 配置,支持 OpenAI-compatible 与 Anthropic Messages。完整 API Key 只保存在本机 `~/.deepccc/config.json`,浏览器读取设置时仅返回掩码。危险命令会在会话中暂停并请求“拒绝、允许一次、本会话允许、永久允许”,浏览器断开或审批超时默认拒绝。
167
167
 
168
168
  `git.coAuthor.enabled` 默认开启。DeepCCC 通过 `run_command` 创建 Git 提交时会保留用户为
169
169
  主 Author,并追加 `Co-authored-by: DeepCCC <20184052+wzj998@users.noreply.github.com>`。
@@ -174,19 +174,19 @@ API 设置采用单一 Provider 配置,支持 OpenAI-compatible 与 Anthropic
174
174
  `DEEPCCC_PROVIDER` 或命令行 `--provider` 覆盖:
175
175
 
176
176
  - `openai` 使用 OpenAI-compatible Chat Completions 协议,兼容 DeepSeek、OpenAI、LiteLLM、vLLM 等服务。
177
- - `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`;目标服务不支持时应留空。
178
-
179
- `streaming` 控制主对话是否使用流式请求,默认 `true`;也可以通过
180
- `DEEPCCC_STREAMING=true|false` 覆盖。关闭后,终端会在整条模型响应完成后一次性显示结果。
181
-
182
- `maxOutputTokens` 限制主对话单次最大输出 token,默认不配置(`null`/缺失),此时不向
183
- Provider 发送 `max_tokens`,使用模型服务端默认值。可通过 `DEEPCCC_MAX_OUTPUT_TOKENS`
184
- 或命令行 `--max-output-tokens` 覆盖;只接受正整数。该限制会同时覆盖模型思考内容、
185
- 工具参数和最终回答,设置过小可能导致工具调用或长回复被截断。
186
-
187
- `contextWindow` 是模型上下文窗口(token),默认 `1048576`(1M,DeepSeek V4 Pro/Flash
188
- 原生规格);常规上下文压缩阈值自动 = `contextWindow × 0.8`。工具输入与结果另有独立预算:
189
- 默认取 `min(64000, 常规压缩阈值 × 25%)`,超过后会提前压缩,避免长会话积累大量低信号工具输出。
177
+ - `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`;目标服务不支持时应留空。
178
+
179
+ `streaming` 控制主对话是否使用流式请求,默认 `true`;也可以通过
180
+ `DEEPCCC_STREAMING=true|false` 覆盖。关闭后,终端会在整条模型响应完成后一次性显示结果。
181
+
182
+ `maxOutputTokens` 限制主对话单次最大输出 token,默认不配置(`null`/缺失),此时不向
183
+ Provider 发送 `max_tokens`,使用模型服务端默认值。可通过 `DEEPCCC_MAX_OUTPUT_TOKENS`
184
+ 或命令行 `--max-output-tokens` 覆盖;只接受正整数。该限制会同时覆盖模型思考内容、
185
+ 工具参数和最终回答,设置过小可能导致工具调用或长回复被截断。
186
+
187
+ `contextWindow` 是模型上下文窗口(token),默认 `1048576`(1M,DeepSeek V4 Pro/Flash
188
+ 原生规格);常规上下文压缩阈值自动 = `contextWindow × 0.8`。工具输入与结果另有独立预算:
189
+ 默认取 `min(64000, 常规压缩阈值 × 25%)`,超过后会提前压缩,避免长会话积累大量低信号工具输出。
190
190
  可通过 `DEEPCCC_CONTEXT_WINDOW` 环境变量覆盖。⚠️ 超过模型/服务端实际上限时请求会被
191
191
  API 拒绝(context length exceeded),实际窗口以模型与所用服务端为准(如 litellm 的
192
192
  `max_input_tokens`)。
@@ -213,62 +213,62 @@ API 拒绝(context length exceeded),实际窗口以模型与所用服务
213
213
  在当前目录启动一个交互式 Agent:
214
214
 
215
215
  ```bash
216
- deepccc-cli
216
+ deepccc-cli
217
217
  ```
218
218
 
219
219
  指定其他模型或 OpenAI-compatible 接口:
220
220
 
221
221
  ```bash
222
- deepccc-cli --base-url https://api.openai.com/v1 --api-key "$OPENAI_API_KEY" --model gpt-4.1
222
+ deepccc-cli --base-url https://api.openai.com/v1 --api-key "$OPENAI_API_KEY" --model gpt-4.1
223
223
  ```
224
224
 
225
225
  使用 Anthropic Messages 协议(同样支持流式输出):
226
226
 
227
227
  ```bash
228
- deepccc-cli --provider anthropic --base-url https://api.example.com --api-key "$API_KEY" --model claude-sonnet-4-6
228
+ deepccc-cli --provider anthropic --base-url https://api.example.com --api-key "$API_KEY" --model claude-sonnet-4-6
229
229
  ```
230
230
 
231
- 指定工作目录:
231
+ 指定工作目录:
232
232
 
233
233
  ```bash
234
- deepccc-cli --cwd /path/to/project
235
- ```
236
-
237
- 附加一张或多张本地图片(可重复传入 `--image`,仍采用本地附件路径,不发送原生多模态内容):
238
-
239
- ```bash
240
- deepccc-cli --image ./error.png --image ./expected.webp
241
- ```
234
+ deepccc-cli --cwd /path/to/project
235
+ ```
236
+
237
+ 附加一张或多张本地图片(可重复传入 `--image`,仍采用本地附件路径,不发送原生多模态内容):
238
+
239
+ ```bash
240
+ deepccc-cli --image ./error.png --image ./expected.webp
241
+ ```
242
242
 
243
243
  恢复当前工作目录最近一次会话:
244
244
 
245
245
  ```bash
246
- deepccc-cli --resume
246
+ deepccc-cli --resume
247
247
  ```
248
248
 
249
249
  设置工具调用步数上限:
250
250
 
251
251
  ```bash
252
- deepccc-cli --max-steps 20
252
+ deepccc-cli --max-steps 20
253
253
  ```
254
254
 
255
- 默认情况下,`deepccc-cli` 不设置固定步数上限,会让模型自然完成工具循环。
255
+ 默认情况下,`deepccc-cli` 不设置固定步数上限,会让模型自然完成工具循环。
256
256
 
257
257
  设置推理强度(reasoning effort):
258
258
 
259
259
  ```bash
260
- deepccc-cli --effort high
260
+ deepccc-cli --effort high
261
261
  ```
262
262
 
263
- 可选值:`none` / `minimal` / `low` / `medium` / `high` / `xhigh` / `max`(留空则不传 `reasoning_effort` 请求字段)。
264
-
265
- 限制主对话最大输出 token:
266
-
267
- ```bash
268
- deepccc-cli --max-output-tokens 8192
269
- ```
270
-
271
- 不设置时使用 Provider 默认值。
263
+ 可选值:`none` / `minimal` / `low` / `medium` / `high` / `xhigh` / `max`(留空则不传 `reasoning_effort` 请求字段)。
264
+
265
+ 限制主对话最大输出 token:
266
+
267
+ ```bash
268
+ deepccc-cli --max-output-tokens 8192
269
+ ```
270
+
271
+ 不设置时使用 Provider 默认值。
272
272
 
273
273
  ## 权限机制
274
274
 
@@ -320,7 +320,7 @@ deepccc-cli --max-output-tokens 8192
320
320
  `--stream-json` 或程序化调用(无终端可交互)时,高危命令**安全默认拒绝**。需要全自动场景可显式传入:
321
321
 
322
322
  ```bash
323
- deepccc-cli --dangerously-bypass-permissions
323
+ deepccc-cli --dangerously-bypass-permissions
324
324
  ```
325
325
 
326
326
  该参数与 `ChatSession` 的 `permissionMode: "bypass"` 等价,也是 chatccc 集成 deepccc 时使用的模式(对齐 chatccc 调用 Claude Code / Codex 的 bypass 方式)。
@@ -329,12 +329,12 @@ deepccc-cli --dangerously-bypass-permissions
329
329
 
330
330
  交互模式下,每轮回复渲染为固定"过程区块":状态行(压缩上下文中/生成回复中/完成/已停止/异常结束)+ 折叠工具行 + 原地更新正文,不滚屏刷 JSON。活动状态有心跳点号动画;完成/停止/异常后区块定型留在屏幕上。
331
331
 
332
- 持久化上下文达到常规 token 阈值或独立工具预算时,deepccc 会先按预算保留最近消息,再压缩较早内容;工具历史使用标准结构化 tool-call/tool-result 消息重放,不会把内部 `[工具记录]` 文本重复送回模型。若模型仍输出伪工具记录,系统会丢弃并重试一次;重复失败时安全终止,避免把未执行命令当成真实结果。超长历史消息、工具记录和压缩输入会被限长,一次压缩最多等待 5 分钟。
332
+ 持久化上下文达到常规 token 阈值或独立工具预算时,deepccc 会先按预算保留最近消息,再压缩较早内容;工具历史使用标准结构化 tool-call/tool-result 消息重放,不会把内部 `[工具记录]` 文本重复送回模型。若模型仍输出伪工具记录,系统会丢弃并重试一次;重复失败时安全终止,避免把未执行命令当成真实结果。超长历史消息、工具记录和压缩输入会被限长,一次压缩最多等待 5 分钟。
333
333
 
334
334
  如果终端渲染出现异常,可以强制回退为纯文本流式输出:
335
335
 
336
336
  ```bash
337
- deepccc-cli --plain
337
+ deepccc-cli --plain
338
338
  ```
339
339
 
340
340
  ## JSONL 流式输出
@@ -342,13 +342,13 @@ deepccc-cli --plain
342
342
  JSONL 模式适合脚本、服务端集成或其他上层系统调用:
343
343
 
344
344
  ```bash
345
- deepccc-cli --stream-json --prompt "检查这个仓库并总结测试命令"
345
+ deepccc-cli --stream-json --prompt "检查这个仓库并总结测试命令"
346
346
  ```
347
347
 
348
348
  也可以从 stdin 传入提示词:
349
349
 
350
350
  ```bash
351
- echo "运行测试并解释失败原因" | deepccc-cli --stream-json
351
+ echo "运行测试并解释失败原因" | deepccc-cli --stream-json
352
352
  ```
353
353
 
354
354
  输出是逐行 JSON:
@@ -381,6 +381,8 @@ ChatCCC 是一个把 Claude Code / Codex / Cursor / CCC Agent 聚合到飞书/
381
381
 
382
382
  这种方式适合已经在 ChatCCC 里协作的场景:ChatCCC 负责会话入口和消息通道,`deepccc` 负责本地编程 Agent 能力,包括读取项目提示词、运行命令、编辑文件和输出流式结果。
383
383
 
384
+ deepccc 内核支持**协作式让位**:任务运行期间用户继续发消息,ChatCCC 会把消息排入注入队列(深度 50),内核在每个模型步骤边界(`prepareStep`)吸收进当前 turn——不打断正在执行的工具步骤、不新开一轮对话,已完成的中间态持久化保留,收到插话后继续调整方向。`/stop` 打断当前 turn,`/cancel` 清空注入队列。该能力通过 `chat()` 的 `drainInput` 回调对外暴露,运行期新消息以 `input_injected` 事件反馈到调用方;仅流式(`streaming: true`)模式生效,non-streaming 下退化为整轮结束后消费。
385
+
384
386
  deepccc 的内核主战场在 ChatCCC 仓库的 `deepccc-agent/` 子目录;本仓库(deepccc-agent)是发布镜像,由 ChatCCC 仓库的 `sync-deepccc.mjs` 目录级同步(多的删、少的补、不同的改),之后 `npm run build && npm publish` 发布独立 `deepccc` 包。
385
387
 
386
388
  ## 项目提示词自动注入
package/dist/cli.js CHANGED
@@ -459,6 +459,10 @@ async function runRepl(args) {
459
459
  }
460
460
  let currentAbort = null;
461
461
  const ctrlCState = createCtrlCState();
462
+ // 协作式让位:agent 运行期间用户仍可输入,新行进入注入队列,由内核在每个
463
+ // model step 边界 drain 后吸收到当前 turn(而非等待整轮结束)。
464
+ let chatRunning = false;
465
+ const pendingInjections = [];
462
466
  rl.prompt();
463
467
  rl.on("line", async (line) => {
464
468
  ctrlCState.reset();
@@ -467,6 +471,17 @@ async function runRepl(args) {
467
471
  rl.prompt();
468
472
  return;
469
473
  }
474
+ if (chatRunning) {
475
+ // 运行期间不并发启动新 turn:把输入注入当前 turn 的 step 边界。
476
+ if (input === "exit" || input.startsWith("/")) {
477
+ process.stdout.write(`${C.dim}[running] 命令将在本轮结束后生效;或按两次 Ctrl+C 中断${C.reset}\n`);
478
+ return;
479
+ }
480
+ pendingInjections.push(buildAttachmentPrompt(input, pendingAttachments));
481
+ pendingAttachments = [];
482
+ process.stdout.write(`${C.dim}[queued] 消息将在当前步骤后注入本轮${C.reset}\n`);
483
+ return;
484
+ }
470
485
  if (input === "exit") {
471
486
  process.stdout.write(`${C.dim}bye${C.reset}\n`);
472
487
  rl.close();
@@ -510,10 +525,11 @@ async function runRepl(args) {
510
525
  const chatInput = buildAttachmentPrompt(input, pendingAttachments);
511
526
  pendingAttachments = [];
512
527
  let lastAccumulated = "";
513
- for await (const event of session.chat(chatInput, signal)) {
528
+ chatRunning = true;
529
+ for await (const event of session.chat(chatInput, signal, () => pendingInjections.shift())) {
514
530
  if (renderer && view) {
515
531
  view = reduceProgress(view, event);
516
- if (event.type === "text" || event.type === "compact" || event.type === "status") {
532
+ if (event.type === "text" || event.type === "compact" || event.type === "status" || event.type === "input_injected") {
517
533
  renderer.render(view);
518
534
  }
519
535
  else {
@@ -543,6 +559,9 @@ async function runRepl(args) {
543
559
  const status = event.is_error ? "error" : "ok";
544
560
  console.log(`${C.dim}[tool result] ${event.name ?? event.tool_use_id} ${status}${C.reset}`);
545
561
  }
562
+ else if (event.type === "input_injected") {
563
+ console.log(`\n${C.dim}[injected] ${event.text}${C.reset}`);
564
+ }
546
565
  else if (event.type === "error") {
547
566
  console.log(`\n${C.yellow}[error] ${event.message}${C.reset}`);
548
567
  }
@@ -561,6 +580,7 @@ async function runRepl(args) {
561
580
  finally {
562
581
  activeRenderer = null;
563
582
  activeView = null;
583
+ chatRunning = false;
564
584
  if (renderer && view && !rendererEnded) {
565
585
  // 定型终态区块(完成/已停止/异常结束)留在屏幕上,恢复光标
566
586
  renderer.end(view);
package/dist/index.d.ts CHANGED
@@ -116,6 +116,9 @@ export type ChatEvent = {
116
116
  accumulated: string;
117
117
  } | {
118
118
  type: "text_reset";
119
+ } | {
120
+ type: "input_injected";
121
+ text: string;
119
122
  } | {
120
123
  type: "done";
121
124
  text: string;
@@ -167,7 +170,7 @@ export declare class ChatSession {
167
170
  * 放最后可以让前面的稳定内容尽量命中缓存,只丢尾段。
168
171
  */
169
172
  private buildSystemPrompt;
170
- chat(userMessage: string, signal?: AbortSignal): AsyncIterable<ChatEvent>;
173
+ chat(userMessage: string, signal?: AbortSignal, drainInput?: () => string | undefined): AsyncIterable<ChatEvent>;
171
174
  /** 返回当前的会话历史(只读) */
172
175
  get history(): ReadonlyArray<ChatMessage>;
173
176
  /** 返回当前轮数(不含 system 消息) */
package/dist/index.js CHANGED
@@ -246,6 +246,17 @@ function normalizeAnthropicBaseURL(baseURL) {
246
246
  // DeepSeek Anthropic 端点示例:https://api.deepseek.com/anthropic/v1。
247
247
  return baseURL.trim().replace(/\/+$/, "");
248
248
  }
249
+ /**
250
+ * 协作式让位的内部中断信号:在 prepareStep(每个模型 step 前)检测到外部
251
+ * 注入消息时抛出,触发 chat() 持久化当前中间态 → 注入 user 消息 → 以新
252
+ * messages 重启 streamText,让 agent 在当前 turn 内吸收新指令而非结束整轮。
253
+ */
254
+ class InputInjectionInterrupt extends Error {
255
+ constructor() {
256
+ super("input injection requested at step boundary");
257
+ this.name = "InputInjectionInterrupt";
258
+ }
259
+ }
249
260
  export class ChatSession {
250
261
  model;
251
262
  /** 子模型实例;未配置 subModel 时与主模型同一实例 */
@@ -396,7 +407,7 @@ export class ChatSession {
396
407
  }
397
408
  return systemContent.join("\n");
398
409
  }
399
- async *chat(userMessage, signal) {
410
+ async *chat(userMessage, signal, drainInput) {
400
411
  this.context.appendMessage({ role: "user", content: userMessage });
401
412
  let fullText = "";
402
413
  let safeAccumulated = "";
@@ -469,6 +480,13 @@ export class ChatSession {
469
480
  ? { [OPENAI_COMPATIBLE_PROVIDER_NAME]: { reasoningEffort: this.effort } }
470
481
  : { anthropic: { effort: this.effort } };
471
482
  }
483
+ // 协作式让位(仅 streaming 模式):外部可在每个 model step 边界 drain 一条
484
+ // 新消息注入当前 turn。non-streaming 下 generateText 是原子请求,无法在 step
485
+ // 边界取回中间工具结果,因此忽略 drainInput(消息留在上层队列,整轮结束后消费)。
486
+ const canInject = this.streaming && typeof drainInput === "function";
487
+ // pendingInjection 是 prepareStep 与 catch 之间的注入信号槽。用标志而非
488
+ // instanceof 判断,因为 provider SDK 可能包装抛出的错误。
489
+ let pendingInjection = null;
472
490
  const baseGenerationOptions = {
473
491
  model: this.model,
474
492
  system,
@@ -483,6 +501,13 @@ export class ChatSession {
483
501
  stopWhen: maxSteps !== undefined ? stepCountIs(maxSteps) : isLoopFinished(),
484
502
  abortSignal: signal,
485
503
  prepareStep: ({ messages, stepNumber }) => {
504
+ if (canInject) {
505
+ const injected = drainInput();
506
+ if (injected) {
507
+ pendingInjection = injected;
508
+ throw new InputInjectionInterrupt();
509
+ }
510
+ }
486
511
  const compacted = compactToolLoopMessages(messages);
487
512
  if (compacted.compactedResults > 0) {
488
513
  rawLog?.writeLine(safeRawStreamJson({
@@ -500,151 +525,198 @@ export class ChatSession {
500
525
  : {}),
501
526
  ...(effortProviderOptions ? { providerOptions: effortProviderOptions } : {}),
502
527
  };
503
- for (let attempt = 0; attempt < 2; attempt += 1) {
504
- fullText = "";
505
- safeAccumulated = "";
506
- toolContext = [];
507
- toolCallsById.clear();
508
- toolCallOrder.length = 0;
509
- timeline.length = 0;
510
- let lastReasoningProgressAt;
511
- const attemptMessages = attempt === 0
512
- ? modelMessages
513
- : [...modelMessages, { role: "user", content: TOOL_PROTOCOL_RECOVERY_PROMPT }];
514
- const generationOptions = {
515
- ...baseGenerationOptions,
516
- messages: attemptMessages,
517
- };
518
- let stream;
519
- let requiresFinish = false;
520
- let receivedFinish = false;
521
- let finishReason;
522
- if (this.streaming) {
523
- const result = streamText(generationOptions);
524
- requiresFinish = result.fullStream != null;
525
- stream = result.fullStream ?? textStreamToFullStream(result.textStream);
526
- }
527
- else {
528
- const result = await generateText(generationOptions);
529
- finishReason = result.finishReason;
530
- stream = generateResultToFullStream(result);
531
- }
532
- for await (const part of stream) {
533
- rawLog?.writeLine(safeRawStreamJson(part));
534
- if (part.type === "finish") {
535
- receivedFinish = true;
536
- finishReason = part.finishReason;
537
- }
538
- if (part.type === "reasoning-start" || part.type === "reasoning-delta") {
539
- // Reasoning content remains private. A throttled heartbeat is enough
540
- // for ChatCCC to distinguish active inference from a stalled stream.
541
- const now = Date.now();
542
- if (lastReasoningProgressAt === undefined || now - lastReasoningProgressAt >= 1_000) {
543
- lastReasoningProgressAt = now;
544
- yield { type: "progress", phase: "reasoning" };
545
- }
546
- }
547
- else if (part.type === "text-delta") {
548
- fullText += part.text;
549
- const previous = timeline[timeline.length - 1];
550
- if (previous?.type === "text")
551
- previous.text += part.text;
552
- else
553
- timeline.push({ type: "text", text: part.text });
554
- // 隐私替换只在展示层:safeAccumulated 供事件消费者(终端/JSONL)使用,
555
- // fullText 原文用于持久化上下文,避免替换结果回流污染上下文。
556
- const safeText = applyPrivacy(part.text);
557
- safeAccumulated += safeText;
558
- yield { type: "text", text: safeText, accumulated: safeAccumulated };
559
- }
560
- else if (part.type === "tool-call") {
561
- const input = safeJson(part.input);
562
- toolContext.push(`tool_call ${part.toolName}: ${input}`);
563
- toolCallsById.set(part.toolCallId, { id: part.toolCallId, name: part.toolName, input });
564
- toolCallOrder.push(part.toolCallId);
565
- timeline.push({ type: "tool_use", id: part.toolCallId, name: part.toolName, input });
566
- yield {
567
- type: "tool_use",
568
- id: part.toolCallId,
569
- name: part.toolName,
570
- input: applyPrivacyToJson(part.input),
571
- };
572
- }
573
- else if (part.type === "tool-result") {
574
- const output = truncateToolContext(safeJson(part.output));
575
- toolContext.push(`tool_result ${part.toolName}: ${output}`);
576
- const call = toolCallsById.get(part.toolCallId);
577
- if (call)
578
- call.output = output;
579
- timeline.push({ type: "tool_result", tool_use_id: part.toolCallId, name: part.toolName, output });
580
- yield {
581
- type: "tool_result",
582
- tool_use_id: part.toolCallId,
583
- name: part.toolName,
584
- content: applyPrivacyToJson(part.output),
585
- is_error: false,
528
+ // 注入重启后 context 已变(中间 assistant + 注入 user),需要重新构建并应用
529
+ // 与首次一致的 recovery-hint / anthropic 兼容提示。
530
+ const decorateMessages = (msgs) => {
531
+ const hinted = maybeAppendCompactionRecoveryHint(msgs, this.context.summary, appConfig.rawStreamLogs.enabled, this.context.sessionId);
532
+ return this.provider === "anthropic"
533
+ ? addAnthropicToolJsonCompatibilityNote(hinted)
534
+ : hinted;
535
+ };
536
+ let currentMessages = modelMessages;
537
+ while (true) {
538
+ pendingInjection = null;
539
+ try {
540
+ for (let attempt = 0; attempt < 2; attempt += 1) {
541
+ fullText = "";
542
+ toolContext = [];
543
+ toolCallsById.clear();
544
+ toolCallOrder.length = 0;
545
+ timeline.length = 0;
546
+ // safeAccumulated 是整轮展示累积:注入延续时不重置,只有工具协议恢复
547
+ // (重新生成、已 yield text_reset)才重置。
548
+ if (attempt === 1)
549
+ safeAccumulated = "";
550
+ let lastReasoningProgressAt;
551
+ const attemptMessages = attempt === 0
552
+ ? currentMessages
553
+ : [...currentMessages, { role: "user", content: TOOL_PROTOCOL_RECOVERY_PROMPT }];
554
+ const generationOptions = {
555
+ ...baseGenerationOptions,
556
+ messages: attemptMessages,
586
557
  };
587
- }
588
- else if (part.type === "tool-error") {
589
- const message = errorMessage(part.error);
590
- toolContext.push(`tool_error ${part.toolName}: ${message}`);
591
- const call = toolCallsById.get(part.toolCallId);
592
- if (call) {
593
- call.output = message;
594
- call.is_error = true;
558
+ let stream;
559
+ let requiresFinish = false;
560
+ let receivedFinish = false;
561
+ let finishReason;
562
+ if (this.streaming) {
563
+ const result = streamText(generationOptions);
564
+ requiresFinish = result.fullStream != null;
565
+ stream = result.fullStream ?? textStreamToFullStream(result.textStream);
595
566
  }
596
- timeline.push({ type: "tool_result", tool_use_id: part.toolCallId, name: part.toolName, output: message, is_error: true });
597
- yield {
598
- type: "tool_result",
599
- tool_use_id: part.toolCallId,
600
- name: part.toolName,
601
- content: applyPrivacy(message),
602
- is_error: true,
603
- };
604
- }
605
- else if (part.type === "error") {
606
- const message = errorMessage(part.error);
607
- yield { type: "error", message: applyPrivacy(message) };
608
- throw new Error(message);
567
+ else {
568
+ const result = await generateText(generationOptions);
569
+ finishReason = result.finishReason;
570
+ stream = generateResultToFullStream(result);
571
+ }
572
+ for await (const part of stream) {
573
+ rawLog?.writeLine(safeRawStreamJson(part));
574
+ if (part.type === "finish") {
575
+ receivedFinish = true;
576
+ finishReason = part.finishReason;
577
+ }
578
+ if (part.type === "reasoning-start" || part.type === "reasoning-delta") {
579
+ // Reasoning content remains private. A throttled heartbeat is enough
580
+ // for ChatCCC to distinguish active inference from a stalled stream.
581
+ const now = Date.now();
582
+ if (lastReasoningProgressAt === undefined || now - lastReasoningProgressAt >= 1_000) {
583
+ lastReasoningProgressAt = now;
584
+ yield { type: "progress", phase: "reasoning" };
585
+ }
586
+ }
587
+ else if (part.type === "text-delta") {
588
+ fullText += part.text;
589
+ const previous = timeline[timeline.length - 1];
590
+ if (previous?.type === "text")
591
+ previous.text += part.text;
592
+ else
593
+ timeline.push({ type: "text", text: part.text });
594
+ // 隐私替换只在展示层:safeAccumulated 供事件消费者(终端/JSONL)使用,
595
+ // fullText 原文用于持久化上下文,避免替换结果回流污染上下文。
596
+ const safeText = applyPrivacy(part.text);
597
+ safeAccumulated += safeText;
598
+ yield { type: "text", text: safeText, accumulated: safeAccumulated };
599
+ }
600
+ else if (part.type === "tool-call") {
601
+ const input = safeJson(part.input);
602
+ toolContext.push(`tool_call ${part.toolName}: ${input}`);
603
+ toolCallsById.set(part.toolCallId, { id: part.toolCallId, name: part.toolName, input });
604
+ toolCallOrder.push(part.toolCallId);
605
+ timeline.push({ type: "tool_use", id: part.toolCallId, name: part.toolName, input });
606
+ yield {
607
+ type: "tool_use",
608
+ id: part.toolCallId,
609
+ name: part.toolName,
610
+ input: applyPrivacyToJson(part.input),
611
+ };
612
+ }
613
+ else if (part.type === "tool-result") {
614
+ const output = truncateToolContext(safeJson(part.output));
615
+ toolContext.push(`tool_result ${part.toolName}: ${output}`);
616
+ const call = toolCallsById.get(part.toolCallId);
617
+ if (call)
618
+ call.output = output;
619
+ timeline.push({ type: "tool_result", tool_use_id: part.toolCallId, name: part.toolName, output });
620
+ yield {
621
+ type: "tool_result",
622
+ tool_use_id: part.toolCallId,
623
+ name: part.toolName,
624
+ content: applyPrivacyToJson(part.output),
625
+ is_error: false,
626
+ };
627
+ }
628
+ else if (part.type === "tool-error") {
629
+ const message = errorMessage(part.error);
630
+ toolContext.push(`tool_error ${part.toolName}: ${message}`);
631
+ const call = toolCallsById.get(part.toolCallId);
632
+ if (call) {
633
+ call.output = message;
634
+ call.is_error = true;
635
+ }
636
+ timeline.push({ type: "tool_result", tool_use_id: part.toolCallId, name: part.toolName, output: message, is_error: true });
637
+ yield {
638
+ type: "tool_result",
639
+ tool_use_id: part.toolCallId,
640
+ name: part.toolName,
641
+ content: applyPrivacy(message),
642
+ is_error: true,
643
+ };
644
+ }
645
+ else if (part.type === "error") {
646
+ if (pendingInjection !== null) {
647
+ // prepareStep 里的注入中断被 provider 转成了 error part。
648
+ throw new InputInjectionInterrupt();
649
+ }
650
+ const message = errorMessage(part.error);
651
+ yield { type: "error", message: applyPrivacy(message) };
652
+ throw new Error(message);
653
+ }
654
+ }
655
+ if (!signal?.aborted) {
656
+ if (requiresFinish && !receivedFinish)
657
+ throw new Error("DeepCCC 输出流中断:未收到模型完成事件,回复可能不完整");
658
+ if (finishReason === "error" || finishReason === "length")
659
+ throw new Error(`DeepCCC 未正常完成:finishReason=${finishReason},回复可能不完整`);
660
+ if (!fullText.trim() && toolCallOrder.length === 0)
661
+ throw new Error("DeepCCC 本轮未产生有效回复");
662
+ }
663
+ if (hasMalformedToolProtocolText(fullText)) {
664
+ console.warn(`[DeepCCC] malformed tool protocol text detected for ${this.context.sessionId} `
665
+ + `(attempt ${attempt + 1}/2, structuredToolCalls=${toolCallOrder.length})`);
666
+ rawLog?.writeLine(safeRawStreamJson({
667
+ type: "deepccc_tool_protocol_recovery",
668
+ attempt: attempt + 1,
669
+ structuredToolCalls: toolCallOrder.length,
670
+ }));
671
+ yield { type: "text_reset" };
672
+ if (attempt === 0 && toolCallOrder.length === 0) {
673
+ yield { type: "status", phase: "generating" };
674
+ continue;
675
+ }
676
+ throw new Error(toolCallOrder.length > 0
677
+ ? "工具调用协议异常:检测到混合的结构化调用与伪造工具文本,为避免重复执行工具,本轮已安全终止"
678
+ : "工具调用协议异常:模型重试后仍输出了无效或伪造的工具调用文本");
679
+ }
680
+ completed = true;
681
+ const collectedToolCalls = toolCallOrder
682
+ .map((id) => toolCallsById.get(id))
683
+ .filter((call) => call !== undefined);
684
+ this.context.appendMessage(buildPersistedAssistantMessage({
685
+ fullText,
686
+ transcriptLines: toolContext,
687
+ toolCalls: collectedToolCalls,
688
+ timeline,
689
+ }));
690
+ yield { type: "done", text: safeAccumulated };
691
+ return;
609
692
  }
610
693
  }
611
- if (!signal?.aborted) {
612
- if (requiresFinish && !receivedFinish)
613
- throw new Error("DeepCCC 输出流中断:未收到模型完成事件,回复可能不完整");
614
- if (finishReason === "error" || finishReason === "length")
615
- throw new Error(`DeepCCC 未正常完成:finishReason=${finishReason},回复可能不完整`);
616
- if (!fullText.trim() && toolCallOrder.length === 0)
617
- throw new Error("DeepCCC 本轮未产生有效回复");
618
- }
619
- if (hasMalformedToolProtocolText(fullText)) {
620
- console.warn(`[DeepCCC] malformed tool protocol text detected for ${this.context.sessionId} `
621
- + `(attempt ${attempt + 1}/2, structuredToolCalls=${toolCallOrder.length})`);
622
- rawLog?.writeLine(safeRawStreamJson({
623
- type: "deepccc_tool_protocol_recovery",
624
- attempt: attempt + 1,
625
- structuredToolCalls: toolCallOrder.length,
626
- }));
627
- yield { type: "text_reset" };
628
- if (attempt === 0 && toolCallOrder.length === 0) {
629
- yield { type: "status", phase: "generating" };
694
+ catch (err) {
695
+ if (pendingInjection !== null) {
696
+ // 协作式让位:持久化本段中间态 → 注入 user → 重建 messages → 继续当前 turn。
697
+ if (fullText.trim() || toolCallOrder.length > 0) {
698
+ const collectedToolCalls = toolCallOrder
699
+ .map((id) => toolCallsById.get(id))
700
+ .filter((call) => call !== undefined);
701
+ this.context.appendMessage(buildPersistedAssistantMessage({
702
+ fullText,
703
+ transcriptLines: toolContext,
704
+ toolCalls: collectedToolCalls,
705
+ timeline: timeline.map((entry) => ({ ...entry })),
706
+ }));
707
+ }
708
+ const injectedText = pendingInjection;
709
+ this.context.appendMessage({ role: "user", content: injectedText });
710
+ rawLog?.writeLine(safeRawStreamJson({
711
+ type: "deepccc_input_injected",
712
+ text: injectedText,
713
+ }));
714
+ yield { type: "input_injected", text: injectedText };
715
+ currentMessages = decorateMessages(this.context.buildModelMessages());
630
716
  continue;
631
717
  }
632
- throw new Error(toolCallOrder.length > 0
633
- ? "工具调用协议异常:检测到混合的结构化调用与伪造工具文本,为避免重复执行工具,本轮已安全终止"
634
- : "工具调用协议异常:模型重试后仍输出了无效或伪造的工具调用文本");
718
+ throw err;
635
719
  }
636
- completed = true;
637
- const collectedToolCalls = toolCallOrder
638
- .map((id) => toolCallsById.get(id))
639
- .filter((call) => call !== undefined);
640
- this.context.appendMessage(buildPersistedAssistantMessage({
641
- fullText,
642
- transcriptLines: toolContext,
643
- toolCalls: collectedToolCalls,
644
- timeline,
645
- }));
646
- yield { type: "done", text: safeAccumulated };
647
- return;
648
720
  }
649
721
  }
650
722
  catch (err) {
@@ -102,5 +102,11 @@ export function reduceProgress(prev, event) {
102
102
  case "compact":
103
103
  // 旧上下文压缩不影响当前过程展示
104
104
  return prev;
105
+ case "input_injected": {
106
+ // 协作式让位:在当前 turn 的 step 边界注入了新消息,仅在头部提示,
107
+ // 不改动正文与工具状态。
108
+ const preview = event.text.length > 40 ? `${event.text.slice(0, 40)}…` : event.text;
109
+ return withProgressView(prev, { headerTitle: `已注入新消息:${preview}` });
110
+ }
105
111
  }
106
112
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "deepccc",
3
- "version": "0.2.14",
3
+ "version": "0.2.16",
4
4
  "description": "A lightweight coding agent with OpenAI-compatible and Anthropic Messages API support.",
5
5
  "license": "Apache-2.0",
6
6
  "keywords": [