chatccc 0.2.296 → 0.2.298
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 +2 -2
- package/deepccc-agent/README.md +8 -4
- package/deepccc-agent/os-prompts/win32.md +8 -8
- package/deepccc-agent/package.json +1 -1
- package/dist/deepccc-agent/src/file-tools.js +244 -15
- package/dist/deepccc-agent/src/index.js +3 -3
- package/dist/deepccc-agent/src/permissions.js +1 -1
- package/dist/deepccc-agent/src/progress/cards-helpers.js +2 -0
- package/dist/deepccc-agent/src/web-tool-presentation.js +2 -0
- package/dist/src/cards.js +4 -2
- package/dist/src/session.js +125 -9
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -229,7 +229,7 @@ Claude Code、Cursor 和 Codex 需要对应的本地工具;CCC Agent 内置于
|
|
|
229
229
|
|
|
230
230
|
CCC Agent 是 ChatCCC 内置的编程 Agent,不需要额外安装 CLI,开箱即用。在首次配置向导或 Web 管理页中启用后,填写 API Key、Base URL 和模型即可使用;它可以设为 `/new` 的默认 Agent,也可以通过 `/new ccc` 显式创建会话。
|
|
231
231
|
|
|
232
|
-
CCC Agent 支持**协作式让位**:任务运行期间你可以继续在群里发消息,ChatCCC 会在 Agent 的每个模型步骤边界把新消息注入当前任务,不打断正在执行的工具步骤、不新开一轮对话。已完成的中间工作会保留,Agent 收到插话后会调整后续方向。运行中的消息进入独立的注入队列(深度 50),与整轮排队(深度 1
|
|
232
|
+
CCC Agent 支持**协作式让位**:任务运行期间你可以继续在群里发消息,ChatCCC 会在 Agent 的每个模型步骤边界把新消息注入当前任务,不打断正在执行的工具步骤、不新开一轮对话。已完成的中间工作会保留,Agent 收到插话后会调整后续方向。运行中的消息进入独立的注入队列(深度 50),与整轮排队(深度 1)分离。飞书中每条消息实际注入后,机器人会发送一条最多 200 字的回执,冻结注入前的过程卡片,并用新的“生成中”卡片展示后续过程。
|
|
233
233
|
|
|
234
234
|
DeepCCC 同时提供独立端口的本地 Web UI。ChatCCC 管理页顶部点击 **DeepCCC Web** 会复用或按需启动该服务;只全局安装 `deepccc` 的用户直接运行 `deepccc` 即可启动并打开网页版,终端模式改用 `deepccc-cli`。默认地址为 `http://127.0.0.1:28080/`,端口可通过 `~/.deepccc/config.json` 的 `web.port` 修改。网页版支持多会话并发、持久化历史、会话级 model/effort、API 设置和会话内高风险操作审批。
|
|
235
235
|
|
|
@@ -290,7 +290,7 @@ codex --version
|
|
|
290
290
|
|
|
291
291
|
Codex 的默认模型和推理强度可继续由 `~/.codex/config.toml` 管理,也可以在 `config.json` 中覆盖。
|
|
292
292
|
|
|
293
|
-
ChatCCC 通过 Codex 的 **app-server 模式**(一个常驻进程 + WebSocket JSON-RPC)驱动 Codex,而不是一次性 `codex exec` 子进程。因此 Codex 与 CCC Agent 一样支持**协作式让位**:任务运行期间继续发消息,ChatCCC 会在 Codex 的工具步骤边界把消息注入当前 turn(`turn/steer
|
|
293
|
+
ChatCCC 通过 Codex 的 **app-server 模式**(一个常驻进程 + WebSocket JSON-RPC)驱动 Codex,而不是一次性 `codex exec` 子进程。因此 Codex 与 CCC Agent 一样支持**协作式让位**:任务运行期间继续发消息,ChatCCC 会在 Codex 的工具步骤边界把消息注入当前 turn(`turn/steer`),不打断正在执行的命令、不丢已完成的工作;飞书会在 `turn/steer` 成功后发送注入回执,并从新的过程卡片继续展示。`/stop` 通过 `turn/interrupt` 只打断当前 turn,不影响同进程内的其他会话。权限策略与旧 exec 的 `--dangerously-bypass-approvals-and-sandbox` 等价(零审批、完整沙箱权限),`/plan`、`/ask` 退化为只读沙箱。要求 Codex CLI >= 0.60.0(建议保持最新版)。
|
|
294
294
|
|
|
295
295
|
#### 可选:Chrome CDP
|
|
296
296
|
|
package/deepccc-agent/README.md
CHANGED
|
@@ -165,7 +165,7 @@ Web UI 支持新建、恢复、重命名和删除多会话,多个会话可以
|
|
|
165
165
|
|
|
166
166
|
API 设置采用单一 Provider 配置,支持 OpenAI-compatible 与 Anthropic Messages。完整 API Key 只保存在本机 `~/.deepccc/config.json`,浏览器读取设置时仅返回掩码。危险命令会在会话中暂停并请求“拒绝、允许一次、本会话允许、永久允许”,浏览器断开或审批超时默认拒绝。
|
|
167
167
|
|
|
168
|
-
`git.coAuthor.enabled` 默认开启。DeepCCC 通过 `run_command` 创建 Git 提交时会保留用户为
|
|
168
|
+
`git.coAuthor.enabled` 默认开启。DeepCCC 通过 `run_process` 或 `run_command` 创建 Git 提交时会保留用户为
|
|
169
169
|
主 Author,并追加 `Co-authored-by: DeepCCC <20184052+wzj998@users.noreply.github.com>`。
|
|
170
170
|
可设为 `false` 或用 `DEEPCCC_GIT_COAUTHOR=false` 全局关闭。ChatCCC 的
|
|
171
171
|
`ccc.gitCoAuthor` 是三态 override:`null`/缺失跟随这里,`true` 强制开启,`false` 强制关闭。
|
|
@@ -276,7 +276,7 @@ deepccc-cli --max-output-tokens 8192
|
|
|
276
276
|
|
|
277
277
|
## 权限机制
|
|
278
278
|
|
|
279
|
-
`deepccc` 内置轻量权限机制,对标主流 agent 的审批体验:**只拦截有副作用的操作**(`run_command` 与文件写操作),只读工具(`read_file` / `list_dir` / `search_code`)永不拦截,常规文件编辑默认放行不打断工作流。
|
|
279
|
+
`deepccc` 内置轻量权限机制,对标主流 agent 的审批体验:**只拦截有副作用的操作**(`run_process` / `run_script` / `run_command` 与文件写操作),只读工具(`read_file` / `list_dir` / `search_code`)永不拦截,常规文件编辑默认放行不打断工作流。
|
|
280
280
|
|
|
281
281
|
默认模式(`ask`)下,只有**命中内置危险命令库**的高危命令才会询问,例如:
|
|
282
282
|
|
|
@@ -307,11 +307,13 @@ deepccc-cli --max-output-tokens 8192
|
|
|
307
307
|
```json
|
|
308
308
|
{
|
|
309
309
|
"allow": [
|
|
310
|
+
"run_process:git status*",
|
|
310
311
|
"run_command:git status*",
|
|
311
312
|
"run_command:git push --force origin release*"
|
|
312
313
|
],
|
|
313
314
|
"deny": [
|
|
314
315
|
"edit_file:node_modules/**",
|
|
316
|
+
"run_process:npm publish*",
|
|
315
317
|
"run_command:npm publish*"
|
|
316
318
|
]
|
|
317
319
|
}
|
|
@@ -433,11 +435,13 @@ deepccc 的内核主战场在 ChatCCC 仓库的 `deepccc-agent/` 子目录;本
|
|
|
433
435
|
- 用 ripgrep 搜索代码
|
|
434
436
|
- 编辑、创建、删除、移动文件
|
|
435
437
|
- 应用 unified diff patch
|
|
436
|
-
-
|
|
438
|
+
- 用 `run_process` 以结构化 argv、`shell:false` 运行单个程序,避免参数被 shell 二次解析
|
|
439
|
+
- 用 `run_script` 通过 stdin 运行多行 Python/Node 代码,避免复杂 `python -c` / `node -e` 引号
|
|
440
|
+
- 仅在需要 `&&`、管道或重定向时用 `run_command` 运行非交互式 shell 命令
|
|
437
441
|
- 联网搜索(`websearch`:DuckDuckGo,免 API key,返回标题 + URL + 摘要)
|
|
438
442
|
- 抓取网页并转纯文本(`webfetch`:仅 http/https,自动去 HTML 标签、控制长度与超时)
|
|
439
443
|
|
|
440
|
-
|
|
444
|
+
三个命令工具都支持独立 `cwd`、超时、取消、输出限长和运行中消息让位;返回非零退出码时不会直接被当成工具异常,模型可以读取结构化结果继续判断。Windows 下 `run_command` 会拒绝多行或嵌套同类引号的复杂 Python/Node 内联代码,并提示改用 `run_script`。
|
|
441
445
|
|
|
442
446
|
## License
|
|
443
447
|
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
## Windows 命令行提示
|
|
2
2
|
|
|
3
|
-
你运行在 Windows
|
|
3
|
+
你运行在 Windows 上。优先绕过 shell,只有需要 shell 运算符时才进入 cmd.exe:
|
|
4
4
|
|
|
5
|
-
-
|
|
6
|
-
-
|
|
7
|
-
-
|
|
8
|
-
-
|
|
9
|
-
-
|
|
10
|
-
-
|
|
11
|
-
- npm 11.x 在 Windows 上忽略 `--prefix` 对 `npm publish`
|
|
5
|
+
- 单个程序优先用 `run_process`,把可执行文件和每个参数分别放进 `executable` / `args`。它使用 `shell:false`,不会让 cmd.exe 再解释引号、反斜杠、`&`、`%VAR%` 等内容。
|
|
6
|
+
- 多行或引号密集的 Python/Node 代码必须用 `run_script`。不要塞进 `python -c` / `node -e`;Windows 下复杂内联代码会被 `run_command` 拒绝,避免命令返回 0 却实际执行了不同代码。
|
|
7
|
+
- `run_command` 仅用于 `&&`、管道、重定向等必须由 shell 解析的场景;它通过 cmd.exe 执行,不是 bash,也不是 PowerShell。
|
|
8
|
+
- 双引号在 cmd.exe、Node 的 shell 封装和目标程序的 argv 解析之间会经历多层处理;是否保留或剥离取决于具体调用链,不能假设会“原样到达”。单引号在 cmd.exe 中不是可靠的引用字符。
|
|
9
|
+
- 使用各命令工具的 `cwd` 参数切换工作目录,不要前置 `cd /d ... &&`。只有手工编写纯 cmd 命令时,跨盘 `cd` 才需要 `/d`。
|
|
10
|
+
- PowerShell 专用语法(Get-Item、2>$null、Select-Object)不能直接写进 `run_command`;确需使用时,用 `run_process` 显式执行 `powershell.exe` 并逐项传参。
|
|
11
|
+
- npm 11.x 在 Windows 上忽略 `--prefix` 对 `npm publish` 的作用。发布时把工具的 `cwd` 参数设为目标包目录,再执行 `npm publish`。
|
|
@@ -4,7 +4,7 @@ import { createReadStream } from "node:fs";
|
|
|
4
4
|
import { copyFile, mkdir, open, readFile, readdir, rename, stat, unlink, writeFile } from "node:fs/promises";
|
|
5
5
|
import { createRequire } from "node:module";
|
|
6
6
|
import { homedir } from "node:os";
|
|
7
|
-
import { basename, dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
|
|
7
|
+
import { basename, delimiter, dirname, extname, isAbsolute, join, relative, resolve, sep } from "node:path";
|
|
8
8
|
import { createInterface } from "node:readline";
|
|
9
9
|
import { jsonSchema, tool } from "ai";
|
|
10
10
|
import { isDangerousCommand } from "./permissions.js";
|
|
@@ -25,6 +25,7 @@ const SEARCH_TIMEOUT_MS = 15_000;
|
|
|
25
25
|
const MAX_COMMAND_OUTPUT_BYTES = 256 * 1024;
|
|
26
26
|
const DEFAULT_COMMAND_TIMEOUT_MS = 120_000;
|
|
27
27
|
const MAX_COMMAND_TIMEOUT_MS = 900_000;
|
|
28
|
+
const MAX_SCRIPT_BYTES = 2 * 1024 * 1024;
|
|
28
29
|
/**
|
|
29
30
|
* 让位注入的轮询间隔:run_command 执行期以此频率检查是否有待注入的用户消息。
|
|
30
31
|
* 取 1s 而非更小值:轮询本身不花 token(只读内存队列,无模型调用),但更密的
|
|
@@ -54,6 +55,22 @@ export function withGitCoAuthor(command, coAuthor) {
|
|
|
54
55
|
const trailer = ` --trailer "Co-authored-by: ${coAuthor.name} <${coAuthor.email}>"`;
|
|
55
56
|
return command.replace(/(^|(?:&&|\|\||;|\|)\s*)(git\s+commit)\b/g, (_match, prefix, gitCommit) => `${prefix}${gitCommit}${trailer}`);
|
|
56
57
|
}
|
|
58
|
+
/** Structured equivalent of withGitCoAuthor for run_process({ executable: "git", args: [...] }). */
|
|
59
|
+
export function withGitCoAuthorArgs(executable, args, coAuthor) {
|
|
60
|
+
const executableName = basename(executable).replace(/\.(?:exe|cmd|bat)$/i, "").toLowerCase();
|
|
61
|
+
if (!coAuthor?.enabled
|
|
62
|
+
|| executableName !== "git"
|
|
63
|
+
|| args[0]?.toLowerCase() !== "commit"
|
|
64
|
+
|| args.some((arg) => arg.includes(coAuthor.email))) {
|
|
65
|
+
return [...args];
|
|
66
|
+
}
|
|
67
|
+
return [
|
|
68
|
+
args[0],
|
|
69
|
+
"--trailer",
|
|
70
|
+
`Co-authored-by: ${coAuthor.name} <${coAuthor.email}>`,
|
|
71
|
+
...args.slice(1),
|
|
72
|
+
];
|
|
73
|
+
}
|
|
57
74
|
/**
|
|
58
75
|
* 把 `~` / `~/x`(含反斜杠 `~\\x`)展开为用户主目录绝对路径。
|
|
59
76
|
* `~user/x` 形式不展开(Node 无内置支持),保持原样交给后续解析。
|
|
@@ -775,10 +792,91 @@ function pruneBackgroundCommands() {
|
|
|
775
792
|
return;
|
|
776
793
|
}
|
|
777
794
|
}
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
795
|
+
const WINDOWS_INLINE_SCRIPT = /\b(?:python(?:\d+(?:\.\d+)?)?|py|node)(?:\.exe)?\s+(?:-c|-e|--eval)\s+/gi;
|
|
796
|
+
/**
|
|
797
|
+
* cmd.exe cannot reliably preserve nested quotes/backslashes in complex inline
|
|
798
|
+
* Python/Node programs. Keep simple one-line snippets working, but reject the
|
|
799
|
+
* cases for which a zero exit code can still mean that different code ran.
|
|
800
|
+
*/
|
|
801
|
+
export function isUnsafeWindowsInlineScript(command) {
|
|
802
|
+
WINDOWS_INLINE_SCRIPT.lastIndex = 0;
|
|
803
|
+
let match;
|
|
804
|
+
while ((match = WINDOWS_INLINE_SCRIPT.exec(command)) !== null) {
|
|
805
|
+
const tail = command.slice(WINDOWS_INLINE_SCRIPT.lastIndex);
|
|
806
|
+
if (/[\r\n]/.test(tail))
|
|
807
|
+
return true;
|
|
808
|
+
const shellBoundary = tail.search(/\s(?:&&|\|\||[|>])/);
|
|
809
|
+
const argument = (shellBoundary >= 0 ? tail.slice(0, shellBoundary) : tail).trim();
|
|
810
|
+
const quote = argument[0];
|
|
811
|
+
if (quote !== "\"" && quote !== "'")
|
|
812
|
+
continue;
|
|
813
|
+
// cmd.exe does not treat single quotes as argument delimiters.
|
|
814
|
+
if (quote === "'")
|
|
815
|
+
return true;
|
|
816
|
+
const closingQuote = argument.lastIndexOf(quote);
|
|
817
|
+
if (closingQuote <= 0)
|
|
818
|
+
return true;
|
|
819
|
+
const body = argument.slice(1, closingQuote);
|
|
820
|
+
if (body.includes(quote))
|
|
821
|
+
return true;
|
|
822
|
+
}
|
|
823
|
+
return false;
|
|
824
|
+
}
|
|
825
|
+
function assertStringArgs(args) {
|
|
826
|
+
if (args === undefined)
|
|
827
|
+
return [];
|
|
828
|
+
if (!Array.isArray(args) || args.some((arg) => typeof arg !== "string")) {
|
|
829
|
+
throw new Error("args must be an array of strings");
|
|
830
|
+
}
|
|
831
|
+
return [...args];
|
|
832
|
+
}
|
|
833
|
+
function resolveProcessExecutable(commandCwd, value) {
|
|
834
|
+
const executable = value.trim();
|
|
835
|
+
if (!executable)
|
|
836
|
+
throw new Error("executable is required");
|
|
837
|
+
const expanded = expandHomePath(executable);
|
|
838
|
+
if (isAbsolute(expanded))
|
|
839
|
+
return resolve(expanded);
|
|
840
|
+
if (expanded.includes("/") || expanded.includes("\\") || expanded.startsWith(".")) {
|
|
841
|
+
return resolve(commandCwd, expanded);
|
|
842
|
+
}
|
|
843
|
+
return executable;
|
|
844
|
+
}
|
|
845
|
+
function formatProcessCommand(executable, args) {
|
|
846
|
+
return [executable, ...args].map((part) => JSON.stringify(part)).join(" ");
|
|
847
|
+
}
|
|
848
|
+
async function resolveWindowsNodeShim(executable, args) {
|
|
849
|
+
if (process.platform !== "win32" || extname(executable).toLowerCase() === ".exe") {
|
|
850
|
+
return { executable, args };
|
|
851
|
+
}
|
|
852
|
+
const toolName = basename(executable).replace(/\.(?:cmd|bat)$/i, "").toLowerCase();
|
|
853
|
+
if (toolName !== "npm" && toolName !== "npx")
|
|
854
|
+
return { executable, args };
|
|
855
|
+
const cliName = toolName === "npm" ? "npm-cli.js" : "npx-cli.js";
|
|
856
|
+
const candidates = new Set();
|
|
857
|
+
const npmExecPath = process.env.npm_execpath?.trim();
|
|
858
|
+
if (npmExecPath) {
|
|
859
|
+
candidates.add(toolName === "npm" ? npmExecPath : join(dirname(npmExecPath), cliName));
|
|
860
|
+
}
|
|
861
|
+
const addInstallRoot = (root) => {
|
|
862
|
+
if (root)
|
|
863
|
+
candidates.add(join(root, "node_modules", "npm", "bin", cliName));
|
|
864
|
+
};
|
|
865
|
+
addInstallRoot(dirname(process.execPath));
|
|
866
|
+
if (executable.includes("/") || executable.includes("\\"))
|
|
867
|
+
addInstallRoot(dirname(executable));
|
|
868
|
+
for (const pathEntry of (process.env.PATH ?? "").split(delimiter)) {
|
|
869
|
+
addInstallRoot(pathEntry.replace(/^"(.*)"$/, "$1"));
|
|
870
|
+
}
|
|
871
|
+
for (const candidate of candidates) {
|
|
872
|
+
if (await pathExists(candidate)) {
|
|
873
|
+
return { executable: process.execPath, args: [candidate, ...args] };
|
|
874
|
+
}
|
|
875
|
+
}
|
|
876
|
+
throw new Error(`无法在 shell:false 下定位 Windows ${toolName} CLI;请用 run_command 执行 ${toolName},`
|
|
877
|
+
+ "或用 run_process 显式调用 node 并把 npm CLI JS 路径放入 args。");
|
|
878
|
+
}
|
|
879
|
+
async function spawnForTool(cwd, input, abortSignal, shouldYield) {
|
|
782
880
|
const commandCwd = resolveToolPath(cwd, input.cwd);
|
|
783
881
|
const cwdInfo = await stat(commandCwd);
|
|
784
882
|
if (!cwdInfo.isDirectory()) {
|
|
@@ -788,6 +886,7 @@ export async function runCommandForTool(cwd, input, abortSignal, coAuthor, shoul
|
|
|
788
886
|
const startedAt = Date.now();
|
|
789
887
|
const stdout = { chunks: [], bytes: 0, truncated: false };
|
|
790
888
|
const stderr = { chunks: [], bytes: 0, truncated: false };
|
|
889
|
+
const command = input.command;
|
|
791
890
|
return new Promise((resolvePromise, reject) => {
|
|
792
891
|
let settled = false;
|
|
793
892
|
let timedOut = false;
|
|
@@ -795,11 +894,11 @@ export async function runCommandForTool(cwd, input, abortSignal, coAuthor, shoul
|
|
|
795
894
|
let fallbackTimer;
|
|
796
895
|
let yieldPoll;
|
|
797
896
|
let backgroundEntry;
|
|
798
|
-
const child = spawn(
|
|
897
|
+
const child = spawn(input.executable, input.args, {
|
|
799
898
|
cwd: commandCwd,
|
|
800
|
-
shell:
|
|
899
|
+
shell: input.shell,
|
|
801
900
|
windowsHide: true,
|
|
802
|
-
stdio: ["ignore", "pipe", "pipe"],
|
|
901
|
+
stdio: [input.stdin === undefined ? "ignore" : "pipe", "pipe", "pipe"],
|
|
803
902
|
detached: process.platform !== "win32",
|
|
804
903
|
});
|
|
805
904
|
const cleanup = () => {
|
|
@@ -934,6 +1033,10 @@ export async function runCommandForTool(cwd, input, abortSignal, coAuthor, shoul
|
|
|
934
1033
|
child.stderr?.on("data", (chunk) => {
|
|
935
1034
|
appendLimitedOutput(stderr, chunk);
|
|
936
1035
|
});
|
|
1036
|
+
child.stdin?.on("error", () => {
|
|
1037
|
+
// The child error/close event is authoritative; ignore a secondary EPIPE
|
|
1038
|
+
// when an interpreter exits before consuming all script input.
|
|
1039
|
+
});
|
|
937
1040
|
child.once("error", (err) => {
|
|
938
1041
|
if (backgroundEntry) {
|
|
939
1042
|
settle(null, "error");
|
|
@@ -948,8 +1051,76 @@ export async function runCommandForTool(cwd, input, abortSignal, coAuthor, shoul
|
|
|
948
1051
|
child.once("close", (code, signal) => {
|
|
949
1052
|
settle(code, signal);
|
|
950
1053
|
});
|
|
1054
|
+
if (input.stdin !== undefined) {
|
|
1055
|
+
child.stdin?.end(input.stdin, "utf8");
|
|
1056
|
+
}
|
|
951
1057
|
});
|
|
952
1058
|
}
|
|
1059
|
+
export async function runCommandForTool(cwd, input, abortSignal, coAuthor, shouldYield) {
|
|
1060
|
+
const command = withGitCoAuthor(input.command?.trim(), coAuthor);
|
|
1061
|
+
if (!command)
|
|
1062
|
+
throw new Error("command is required");
|
|
1063
|
+
if (process.platform === "win32" && isUnsafeWindowsInlineScript(command)) {
|
|
1064
|
+
throw new Error("Windows CMD 无法可靠传递复杂的 python -c/node -e 内联代码;请改用 run_script,"
|
|
1065
|
+
+ "单程序参数请用 run_process。run_command 仅保留给 &&、管道和重定向等 shell 操作。");
|
|
1066
|
+
}
|
|
1067
|
+
return spawnForTool(cwd, {
|
|
1068
|
+
executable: command,
|
|
1069
|
+
args: [],
|
|
1070
|
+
command,
|
|
1071
|
+
cwd: input.cwd,
|
|
1072
|
+
timeoutMs: input.timeoutMs,
|
|
1073
|
+
shell: true,
|
|
1074
|
+
}, abortSignal, shouldYield);
|
|
1075
|
+
}
|
|
1076
|
+
export async function runProcessForTool(cwd, input, abortSignal, coAuthor, shouldYield) {
|
|
1077
|
+
const commandCwd = resolveToolPath(cwd, input.cwd);
|
|
1078
|
+
const requestedExecutable = resolveProcessExecutable(commandCwd, input.executable ?? "");
|
|
1079
|
+
const requestedArgs = withGitCoAuthorArgs(requestedExecutable, assertStringArgs(input.args), coAuthor);
|
|
1080
|
+
const invocation = await resolveWindowsNodeShim(requestedExecutable, requestedArgs);
|
|
1081
|
+
return spawnForTool(cwd, {
|
|
1082
|
+
executable: invocation.executable,
|
|
1083
|
+
args: invocation.args,
|
|
1084
|
+
command: formatProcessCommand(requestedExecutable, requestedArgs),
|
|
1085
|
+
cwd: commandCwd,
|
|
1086
|
+
timeoutMs: input.timeoutMs,
|
|
1087
|
+
shell: false,
|
|
1088
|
+
}, abortSignal, shouldYield);
|
|
1089
|
+
}
|
|
1090
|
+
async function defaultScriptExecutable(commandCwd, language) {
|
|
1091
|
+
if (language === "node")
|
|
1092
|
+
return process.execPath;
|
|
1093
|
+
const venvPython = process.platform === "win32"
|
|
1094
|
+
? join(commandCwd, ".venv", "Scripts", "python.exe")
|
|
1095
|
+
: join(commandCwd, ".venv", "bin", "python");
|
|
1096
|
+
if (await pathExists(venvPython))
|
|
1097
|
+
return venvPython;
|
|
1098
|
+
return process.platform === "win32" ? "python" : "python3";
|
|
1099
|
+
}
|
|
1100
|
+
export async function runScriptForTool(cwd, input, abortSignal, shouldYield) {
|
|
1101
|
+
if (input.language !== "node" && input.language !== "python") {
|
|
1102
|
+
throw new Error("language must be node or python");
|
|
1103
|
+
}
|
|
1104
|
+
if (typeof input.code !== "string" || !input.code.trim()) {
|
|
1105
|
+
throw new Error("code is required");
|
|
1106
|
+
}
|
|
1107
|
+
if (Buffer.byteLength(input.code, "utf8") > MAX_SCRIPT_BYTES) {
|
|
1108
|
+
throw new Error(`script exceeds ${MAX_SCRIPT_BYTES} bytes`);
|
|
1109
|
+
}
|
|
1110
|
+
const commandCwd = resolveToolPath(cwd, input.cwd);
|
|
1111
|
+
const configuredExecutable = input.executable?.trim();
|
|
1112
|
+
const executable = resolveProcessExecutable(commandCwd, configuredExecutable || await defaultScriptExecutable(commandCwd, input.language));
|
|
1113
|
+
const args = ["-", ...assertStringArgs(input.args)];
|
|
1114
|
+
return spawnForTool(cwd, {
|
|
1115
|
+
executable,
|
|
1116
|
+
args,
|
|
1117
|
+
command: `${formatProcessCommand(executable, args)} <${input.language} script via stdin>`,
|
|
1118
|
+
cwd: commandCwd,
|
|
1119
|
+
timeoutMs: input.timeoutMs,
|
|
1120
|
+
shell: false,
|
|
1121
|
+
stdin: input.code,
|
|
1122
|
+
}, abortSignal, shouldYield);
|
|
1123
|
+
}
|
|
953
1124
|
/** 读取后台命令的最新输出;waitMs > 0 时最多等待该时长(等待期间仍可让位)。 */
|
|
954
1125
|
export async function commandOutputForTool(taskId, input, shouldYield) {
|
|
955
1126
|
const entry = backgroundCommands.get(taskId);
|
|
@@ -1407,14 +1578,72 @@ export function createBuiltinFileTools(cwd, options = {}) {
|
|
|
1407
1578
|
}),
|
|
1408
1579
|
execute: (input) => presentFileForTool(cwd, input),
|
|
1409
1580
|
}),
|
|
1581
|
+
run_process: tool({
|
|
1582
|
+
description: "优先使用:以结构化 executable + args 直接运行单个非交互式程序(shell:false),参数不会经过平台 shell 展开,适合测试、git、npm 和含空格/引号的参数。需要 &&、管道或重定向时才用 run_command。",
|
|
1583
|
+
inputSchema: jsonSchema({
|
|
1584
|
+
type: "object",
|
|
1585
|
+
additionalProperties: false,
|
|
1586
|
+
properties: {
|
|
1587
|
+
executable: { type: "string", description: "可执行文件名或路径,例如 git、npm、node、.venv/Scripts/python.exe。" },
|
|
1588
|
+
args: {
|
|
1589
|
+
type: "array",
|
|
1590
|
+
items: { type: "string" },
|
|
1591
|
+
description: "逐项传递给程序的参数;不要自行拼接或 shell 转义。",
|
|
1592
|
+
},
|
|
1593
|
+
cwd: { type: "string", description: "可选工作目录。请用此字段切换目录,不要在命令前拼接 cd /d。" },
|
|
1594
|
+
timeoutMs: { type: "number", description: `可选超时(毫秒),上限为 ${MAX_COMMAND_TIMEOUT_MS}。` },
|
|
1595
|
+
},
|
|
1596
|
+
required: ["executable"],
|
|
1597
|
+
}),
|
|
1598
|
+
execute: async (input, execOptions) => {
|
|
1599
|
+
const action = [input.executable, ...(input.args ?? [])].join(" ");
|
|
1600
|
+
await guard({
|
|
1601
|
+
tool: "run_process",
|
|
1602
|
+
action,
|
|
1603
|
+
reason: isDangerousCommand(action) ? "high-risk" : "rule",
|
|
1604
|
+
detail: `运行程序: ${action}`,
|
|
1605
|
+
});
|
|
1606
|
+
return runProcessForTool(cwd, input, execOptions.abortSignal, options.gitCoAuthor, options.shouldYieldToInjection);
|
|
1607
|
+
},
|
|
1608
|
+
}),
|
|
1609
|
+
run_script: tool({
|
|
1610
|
+
description: "运行多行 Node.js 或 Python 脚本,代码通过 stdin 传入解释器(shell:false),避免 python -c/node -e 的多层引号问题。Python 默认优先使用 cwd 下的 .venv,也可显式指定 executable。",
|
|
1611
|
+
inputSchema: jsonSchema({
|
|
1612
|
+
type: "object",
|
|
1613
|
+
additionalProperties: false,
|
|
1614
|
+
properties: {
|
|
1615
|
+
language: { type: "string", enum: ["node", "python"], description: "脚本语言。" },
|
|
1616
|
+
code: { type: "string", description: "要执行的完整多行代码,不需要 shell 引号。" },
|
|
1617
|
+
args: {
|
|
1618
|
+
type: "array",
|
|
1619
|
+
items: { type: "string" },
|
|
1620
|
+
description: "可选脚本参数,逐项原样传递。",
|
|
1621
|
+
},
|
|
1622
|
+
executable: { type: "string", description: "可选解释器路径,例如 .venv/Scripts/python.exe。" },
|
|
1623
|
+
cwd: { type: "string", description: "可选工作目录。请用此字段切换目录,不要在命令前拼接 cd /d。" },
|
|
1624
|
+
timeoutMs: { type: "number", description: `可选超时(毫秒),上限为 ${MAX_COMMAND_TIMEOUT_MS}。` },
|
|
1625
|
+
},
|
|
1626
|
+
required: ["language", "code"],
|
|
1627
|
+
}),
|
|
1628
|
+
execute: async (input, execOptions) => {
|
|
1629
|
+
const runtime = input.executable?.trim() || input.language;
|
|
1630
|
+
await guard({
|
|
1631
|
+
tool: "run_script",
|
|
1632
|
+
action: `${runtime} script`,
|
|
1633
|
+
reason: "rule",
|
|
1634
|
+
detail: `运行 ${input.language} 脚本`,
|
|
1635
|
+
});
|
|
1636
|
+
return runScriptForTool(cwd, input, execOptions.abortSignal, options.shouldYieldToInjection);
|
|
1637
|
+
},
|
|
1638
|
+
}),
|
|
1410
1639
|
run_command: tool({
|
|
1411
|
-
description: "
|
|
1640
|
+
description: "仅在需要 &&、管道、重定向或其他 shell 运算符时运行非交互式 shell 命令;单程序优先用 run_process,多行 Python/Node 优先用 run_script。请用 cwd 字段切换目录,不要前置 cd /d。返回 stdout/stderr 和 exitCode;非零退出码不是工具错误。运行期间若有新消息会转入后台并返回 taskId。",
|
|
1412
1641
|
inputSchema: jsonSchema({
|
|
1413
1642
|
type: "object",
|
|
1414
1643
|
additionalProperties: false,
|
|
1415
1644
|
properties: {
|
|
1416
|
-
command: { type: "string", description: "
|
|
1417
|
-
cwd: { type: "string", description: "
|
|
1645
|
+
command: { type: "string", description: "确实需要 shell 解析的命令行。Windows 下复杂 python -c/node -e 会被拒绝。" },
|
|
1646
|
+
cwd: { type: "string", description: "可选工作目录。请用此字段切换目录,不要在命令前拼接 cd /d。" },
|
|
1418
1647
|
timeoutMs: { type: "number", description: `可选超时(毫秒),上限为 ${MAX_COMMAND_TIMEOUT_MS}。` },
|
|
1419
1648
|
},
|
|
1420
1649
|
required: ["command"],
|
|
@@ -1430,12 +1659,12 @@ export function createBuiltinFileTools(cwd, options = {}) {
|
|
|
1430
1659
|
},
|
|
1431
1660
|
}),
|
|
1432
1661
|
command_output: tool({
|
|
1433
|
-
description: "
|
|
1662
|
+
description: "读取 run_process、run_script 或 run_command 让位到后台后的最新输出。立即返回当前状态;waitMs 可最多等待 30 秒。",
|
|
1434
1663
|
inputSchema: jsonSchema({
|
|
1435
1664
|
type: "object",
|
|
1436
1665
|
additionalProperties: false,
|
|
1437
1666
|
properties: {
|
|
1438
|
-
taskId: { type: "string", description: "
|
|
1667
|
+
taskId: { type: "string", description: "命令工具返回的 taskId(backgrounded:true 时)。" },
|
|
1439
1668
|
waitMs: { type: "number", description: `可选等待毫秒数;0 或省略立即返回,上限 ${MAX_COMMAND_OUTPUT_WAIT_MS}。` },
|
|
1440
1669
|
},
|
|
1441
1670
|
required: ["taskId"],
|
|
@@ -1443,12 +1672,12 @@ export function createBuiltinFileTools(cwd, options = {}) {
|
|
|
1443
1672
|
execute: (input) => commandOutputForTool(input.taskId, input, options.shouldYieldToInjection),
|
|
1444
1673
|
}),
|
|
1445
1674
|
command_kill: tool({
|
|
1446
|
-
description: "
|
|
1675
|
+
description: "终止一个由 run_process、run_script 或 run_command 转入后台的进程。",
|
|
1447
1676
|
inputSchema: jsonSchema({
|
|
1448
1677
|
type: "object",
|
|
1449
1678
|
additionalProperties: false,
|
|
1450
1679
|
properties: {
|
|
1451
|
-
taskId: { type: "string", description: "
|
|
1680
|
+
taskId: { type: "string", description: "命令工具返回的 taskId。" },
|
|
1452
1681
|
},
|
|
1453
1682
|
required: ["taskId"],
|
|
1454
1683
|
}),
|
|
@@ -153,8 +153,8 @@ function readProjectInstructionFiles(cwd) {
|
|
|
153
153
|
function buildRuntimeWorkspacePrompt(cwd) {
|
|
154
154
|
return [
|
|
155
155
|
`当前工作目录:${cwd}`,
|
|
156
|
-
"需要理解代码、配置、项目结构、测试或 git 状态时,主动使用 read_file、list_dir、search_code
|
|
157
|
-
"
|
|
156
|
+
"需要理解代码、配置、项目结构、测试或 git 状态时,主动使用 read_file、list_dir、search_code 和命令执行工具。",
|
|
157
|
+
"单个程序优先使用 run_process 的结构化 executable/args;多行 Python/Node 使用 run_script;只有 &&、管道、重定向等 shell 场景才使用 run_command。用 cwd 参数切换目录,不要前置 cd。先检查 exitCode、stdout 和 stderr 再决定下一步。",
|
|
158
158
|
"编辑前先阅读相关文件范围。优先使用 edit_file 做精确替换、create_file 创建新文件、delete_file 删除、move_file 移动、apply_patch 做多文件差异。",
|
|
159
159
|
"文件工具通过 DeepCCC 在本地执行。可行时优先使用带 SHA-256 前置条件的受保护编辑,避免覆盖并发用户修改。",
|
|
160
160
|
].join("\n");
|
|
@@ -288,7 +288,7 @@ export class ChatSession {
|
|
|
288
288
|
context;
|
|
289
289
|
compactionTimeoutMs;
|
|
290
290
|
maxSteps;
|
|
291
|
-
/**
|
|
291
|
+
/** 透传给命令工具的让位注入判定(见 ChatSessionOptions.shouldYieldToInjection)。 */
|
|
292
292
|
shouldYieldToInjection;
|
|
293
293
|
effort;
|
|
294
294
|
maxOutputTokens;
|
|
@@ -146,7 +146,7 @@ export class PermissionGate {
|
|
|
146
146
|
return "deny";
|
|
147
147
|
if (rules.allow.some((r) => keys.some((k) => matchRule(r, k))))
|
|
148
148
|
return "allow";
|
|
149
|
-
//
|
|
149
|
+
// 只有命令执行工具标记为 high-risk 才询问;文件编辑等常规操作默认放行
|
|
150
150
|
if (request.reason !== "high-risk")
|
|
151
151
|
return "allow";
|
|
152
152
|
// 高危且无交互能力(非 TTY / JSONL / 程序化调用)→ 安全默认拒绝
|
|
@@ -50,6 +50,8 @@ const TOOL_EMOJI_MAP = {
|
|
|
50
50
|
read_file: "\u{1F4D6}", // 📖
|
|
51
51
|
list_dir: "\u{1F4C2}", // 📂
|
|
52
52
|
search_code: "\u{1F50E}", // 🔎
|
|
53
|
+
run_process: "\u{1F5A5}\u{FE0F}", // 🖥️
|
|
54
|
+
run_script: "\u{1F5A5}\u{FE0F}", // 🖥️
|
|
53
55
|
run_command: "\u{1F5A5}\u{FE0F}", // 🖥️
|
|
54
56
|
edit_file: "\u{270F}\u{FE0F}", // ✏️
|
|
55
57
|
create_file: "\u{270D}\u{FE0F}", // ✍️
|
|
@@ -2,6 +2,8 @@ export const WEB_TOOL_SUMMARY_RULES = {
|
|
|
2
2
|
read_file: { emoji: "📖", inputFields: ["path"] },
|
|
3
3
|
list_dir: { emoji: "📂", inputFields: ["path"] },
|
|
4
4
|
search_code: { emoji: "🔎", inputFields: ["query", "path"] },
|
|
5
|
+
run_process: { emoji: "🖥️", inputFields: ["executable", "args", "cwd"] },
|
|
6
|
+
run_script: { emoji: "🖥️", inputFields: ["language", "cwd"] },
|
|
5
7
|
run_command: { emoji: "🖥️", inputFields: ["command"] },
|
|
6
8
|
edit_file: { emoji: "✏️", inputFields: ["path"] },
|
|
7
9
|
create_file: { emoji: "✍️", inputFields: ["path"] },
|
package/dist/src/cards.js
CHANGED
|
@@ -61,6 +61,8 @@ const TOOL_EMOJI_MAP = {
|
|
|
61
61
|
read_file: "\u{1F4D6}", // 📖
|
|
62
62
|
list_dir: "\u{1F4C2}", // 📂
|
|
63
63
|
search_code: "\u{1F50E}", // 🔎
|
|
64
|
+
run_process: "\u{1F5A5}\u{FE0F}", // 🖥️
|
|
65
|
+
run_script: "\u{1F5A5}\u{FE0F}", // 🖥️
|
|
64
66
|
run_command: "\u{1F5A5}\u{FE0F}", // 🖥️
|
|
65
67
|
edit_file: "\u{270F}\u{FE0F}", // ✏️
|
|
66
68
|
create_file: "\u{270D}\u{FE0F}", // ✍️
|
|
@@ -397,7 +399,7 @@ export function buildQueueFullCard() {
|
|
|
397
399
|
],
|
|
398
400
|
});
|
|
399
401
|
}
|
|
400
|
-
//
|
|
402
|
+
// 协作式让位卡片(ccc/codex):消息进入注入队列,将在 step/item 边界吸收进本轮。
|
|
401
403
|
// 与上面的整轮队列卡片区分,强调“无需等待整轮结束”。
|
|
402
404
|
export function buildInjectionQueuedCard(text) {
|
|
403
405
|
const preview = text.length > 100 ? text.slice(0, 100) + "…" : text;
|
|
@@ -417,7 +419,7 @@ export function buildInjectionQueuedCard(text) {
|
|
|
417
419
|
],
|
|
418
420
|
});
|
|
419
421
|
}
|
|
420
|
-
//
|
|
422
|
+
// 注入队列满卡片(ccc/codex)
|
|
421
423
|
export function buildInjectionQueueFullCard() {
|
|
422
424
|
return JSON.stringify({
|
|
423
425
|
config: { wide_screen_mode: true },
|
package/dist/src/session.js
CHANGED
|
@@ -267,6 +267,28 @@ function formatTerminalCardContent(state) {
|
|
|
267
267
|
? `${content}\n\n${reason}\n以上内容可能不完整。`
|
|
268
268
|
: reason;
|
|
269
269
|
}
|
|
270
|
+
function formatTerminalCardContentForDisplay(state, display) {
|
|
271
|
+
if (display.rotationAccLen === undefined)
|
|
272
|
+
return formatTerminalCardContent(state);
|
|
273
|
+
const accumulatedDelta = state.accumulatedContent.slice(display.rotationAccLen);
|
|
274
|
+
const previousFinalReply = display.rotationFinalReply ?? "";
|
|
275
|
+
const finalReplyDelta = previousFinalReply && state.finalReply.startsWith(previousFinalReply)
|
|
276
|
+
? state.finalReply.slice(previousFinalReply.length)
|
|
277
|
+
: state.finalReply;
|
|
278
|
+
const content = accumulatedDelta + finalReplyDelta;
|
|
279
|
+
if (state.status !== "error")
|
|
280
|
+
return content;
|
|
281
|
+
const error = state.terminalError ?? {
|
|
282
|
+
kind: "unknown",
|
|
283
|
+
title: "原因未记录",
|
|
284
|
+
message: "当前状态中没有可用的错误详情,请查看运行日志。",
|
|
285
|
+
occurredAt: Date.now(),
|
|
286
|
+
};
|
|
287
|
+
const reason = formatTerminalErrorReason(error);
|
|
288
|
+
return content.trim()
|
|
289
|
+
? `${content}\n\n${reason}\n以上内容可能不完整。`
|
|
290
|
+
: reason;
|
|
291
|
+
}
|
|
270
292
|
function isCardKitSequenceConflict(err) {
|
|
271
293
|
return err instanceof Error && err.message.includes("300317");
|
|
272
294
|
}
|
|
@@ -729,6 +751,85 @@ export function _resetSessionRegistryFileForTest() {
|
|
|
729
751
|
export function pickFinalReply(state) {
|
|
730
752
|
return state.finalCompleteText || state.finalText;
|
|
731
753
|
}
|
|
754
|
+
const APPLIED_INJECTION_PREVIEW_CHARS = 200;
|
|
755
|
+
export function unwrapInjectedUserMessage(text) {
|
|
756
|
+
const match = text.match(/^\[User message\]\r?\n([\s\S]*)\r?\n\[\/User message\]$/);
|
|
757
|
+
return match?.[1] ?? text;
|
|
758
|
+
}
|
|
759
|
+
export function formatAppliedInjectionReceipt(text) {
|
|
760
|
+
const normalized = text.trim() || "(空消息)";
|
|
761
|
+
const chars = Array.from(normalized);
|
|
762
|
+
const preview = chars.length > APPLIED_INJECTION_PREVIEW_CHARS
|
|
763
|
+
? `${chars.slice(0, APPLIED_INJECTION_PREVIEW_CHARS).join("")}…`
|
|
764
|
+
: normalized;
|
|
765
|
+
return `已收到注入信息:${preview}`;
|
|
766
|
+
}
|
|
767
|
+
async function rotateFeishuCardForAppliedInjection(sessionId, turnCount, tool, injectedText, state, nextHeaderTitle) {
|
|
768
|
+
if (tool !== "codex" && tool !== "ccc")
|
|
769
|
+
return;
|
|
770
|
+
const chatId = pickDisplayChat(sessionId);
|
|
771
|
+
if (!chatId)
|
|
772
|
+
return;
|
|
773
|
+
const platform = platformForChat(chatId);
|
|
774
|
+
if (platform?.kind !== "feishu")
|
|
775
|
+
return;
|
|
776
|
+
let previousDisplay = displayCards.get(chatId);
|
|
777
|
+
if (previousDisplay
|
|
778
|
+
&& (previousDisplay.sessionId !== sessionId || previousDisplay.turnCount !== turnCount)) {
|
|
779
|
+
previousDisplay = undefined;
|
|
780
|
+
}
|
|
781
|
+
if (previousDisplay) {
|
|
782
|
+
while (previousDisplay.cardBusy)
|
|
783
|
+
await new Promise((resolve) => setTimeout(resolve, 20));
|
|
784
|
+
if (displayCards.get(chatId) !== previousDisplay)
|
|
785
|
+
previousDisplay = undefined;
|
|
786
|
+
}
|
|
787
|
+
if (previousDisplay) {
|
|
788
|
+
previousDisplay.cardBusy = true;
|
|
789
|
+
const nextSequence = previousDisplay.sequence + 1;
|
|
790
|
+
const oldContent = state.accumulatedContent + pickFinalReply(state);
|
|
791
|
+
const oldCard = buildProgressCard(progressView({
|
|
792
|
+
text: truncateContent(oldContent) || "等待 Agent 输出...",
|
|
793
|
+
status: "done",
|
|
794
|
+
showStop: false,
|
|
795
|
+
headerTitle: "已收到注入,后续转入新卡片",
|
|
796
|
+
headerTemplate: "blue",
|
|
797
|
+
}));
|
|
798
|
+
await platform.cardUpdate(previousDisplay.cardId, oldCard, nextSequence).then(() => {
|
|
799
|
+
previousDisplay.sequence = nextSequence;
|
|
800
|
+
}).catch((err) => {
|
|
801
|
+
console.error(`[${ts()}] [INJECT] freeze old card failed: ${err.message}`);
|
|
802
|
+
if (isCardKitSequenceConflict(err))
|
|
803
|
+
previousDisplay.sequence = nextSequence;
|
|
804
|
+
});
|
|
805
|
+
}
|
|
806
|
+
await platform.sendText(chatId, formatAppliedInjectionReceipt(injectedText)).catch((err) => {
|
|
807
|
+
console.error(`[${ts()}] [INJECT] receipt send failed: ${err.message}`);
|
|
808
|
+
});
|
|
809
|
+
const newCardId = await createVisibleProgressCard(platform, chatId, sessionId, turnCount, "新生成卡片发送失败,结果将继续更新在上一张卡片中。", nextHeaderTitle);
|
|
810
|
+
if (!newCardId) {
|
|
811
|
+
if (previousDisplay)
|
|
812
|
+
previousDisplay.cardBusy = false;
|
|
813
|
+
return;
|
|
814
|
+
}
|
|
815
|
+
if (previousDisplay) {
|
|
816
|
+
await markCardDone(sessionId, turnCount, previousDisplay.cardId);
|
|
817
|
+
}
|
|
818
|
+
displayCards.set(chatId, {
|
|
819
|
+
cardId: newCardId,
|
|
820
|
+
sequence: 1,
|
|
821
|
+
cardBusy: false,
|
|
822
|
+
cardCreatedAt: Date.now(),
|
|
823
|
+
lastSentContent: "",
|
|
824
|
+
lastSentHeaderTitle: nextHeaderTitle,
|
|
825
|
+
streamErrorNotified: false,
|
|
826
|
+
sessionId,
|
|
827
|
+
turnCount,
|
|
828
|
+
rotationAccLen: state.accumulatedContent.length,
|
|
829
|
+
rotationFinalReply: pickFinalReply(state),
|
|
830
|
+
dotCount: 0,
|
|
831
|
+
});
|
|
832
|
+
}
|
|
732
833
|
export function accumulateBlockContent(block, state, toolCallMap) {
|
|
733
834
|
if (state.transcript !== undefined || (block.type !== "agent_progress" && block.type !== "text_reset")) {
|
|
734
835
|
const transcriptState = { transcript: state.transcript ?? [] };
|
|
@@ -1123,7 +1224,7 @@ export async function runAgentSession(sessionId, userText, platform, _chatId, ms
|
|
|
1123
1224
|
else {
|
|
1124
1225
|
const nextSeq = display.sequence + 1;
|
|
1125
1226
|
const { title: headerTitle, template: headerTemplate } = formatTerminalHeader(prevState.status, prevState.terminalError);
|
|
1126
|
-
const cardContent = truncateContent(
|
|
1227
|
+
const cardContent = truncateContent(formatTerminalCardContentForDisplay(prevState, display)) || " ";
|
|
1127
1228
|
const doneCard = buildProgressCard(progressView({ text: cardContent, status: "done", showStop: false, headerTitle, headerTemplate }));
|
|
1128
1229
|
await pp.cardUpdate(display.cardId, doneCard, nextSeq).catch(err => {
|
|
1129
1230
|
console.error(`[${ts()}] [DISPLAY] prevState final cardUpdate failed: ${err.message}`);
|
|
@@ -1289,7 +1390,7 @@ export async function runAgentSession(sessionId, userText, platform, _chatId, ms
|
|
|
1289
1390
|
}
|
|
1290
1391
|
try {
|
|
1291
1392
|
// 供 onInjectionRejected 退回的“最后一条已取出但未成功注入”的消息
|
|
1292
|
-
|
|
1393
|
+
const injectionState = { lastShifted: null };
|
|
1293
1394
|
for await (const unifiedMsg of adapter.prompt(sessionId, userTextWithCapabilities, cwd, controller.signal, {
|
|
1294
1395
|
onProcessStart: (processInfo) => {
|
|
1295
1396
|
startPromptProcessMonitor(sessionId, processInfo);
|
|
@@ -1312,16 +1413,16 @@ export async function runAgentSession(sessionId, userText, platform, _chatId, ms
|
|
|
1312
1413
|
const injected = shiftInjection(sessionId);
|
|
1313
1414
|
if (!injected)
|
|
1314
1415
|
return undefined;
|
|
1315
|
-
|
|
1416
|
+
injectionState.lastShifted = injected;
|
|
1316
1417
|
// 与首次消息保持一致的结构化包装;imSkillsPrompt 已在首次消息中,
|
|
1317
1418
|
// 此处不重复注入,避免多轮注入导致上下文膨胀。
|
|
1318
1419
|
return `[User message]\n${injected.text}\n[/User message]`;
|
|
1319
1420
|
} : undefined,
|
|
1320
1421
|
// 注入未被当前 turn 吸收(如 turn 恰好已结束)时,退回队列头部,避免丢消息。
|
|
1321
1422
|
onInjectionRejected: tool === "ccc" || tool === "codex" ? () => {
|
|
1322
|
-
if (
|
|
1323
|
-
unshiftInjection(sessionId,
|
|
1324
|
-
|
|
1423
|
+
if (injectionState.lastShifted) {
|
|
1424
|
+
unshiftInjection(sessionId, injectionState.lastShifted);
|
|
1425
|
+
injectionState.lastShifted = null;
|
|
1325
1426
|
}
|
|
1326
1427
|
} : undefined,
|
|
1327
1428
|
})) {
|
|
@@ -1339,7 +1440,15 @@ export async function runAgentSession(sessionId, userText, platform, _chatId, ms
|
|
|
1339
1440
|
let activityChanged = false;
|
|
1340
1441
|
let progressHeartbeat = false;
|
|
1341
1442
|
let outputReset = false;
|
|
1342
|
-
|
|
1443
|
+
let injectionBoundaryObserved = false;
|
|
1444
|
+
for (const originalBlock of unifiedMsg.blocks) {
|
|
1445
|
+
let block = originalBlock;
|
|
1446
|
+
if (originalBlock.type === "input_injected") {
|
|
1447
|
+
const injectedText = injectionState.lastShifted?.text
|
|
1448
|
+
?? unwrapInjectedUserMessage(originalBlock.text);
|
|
1449
|
+
injectionState.lastShifted = null;
|
|
1450
|
+
block = { ...originalBlock, text: injectedText };
|
|
1451
|
+
}
|
|
1343
1452
|
if (block.type === "agent_progress")
|
|
1344
1453
|
progressHeartbeat = true;
|
|
1345
1454
|
if (block.type === "text_reset") {
|
|
@@ -1349,6 +1458,10 @@ export async function runAgentSession(sessionId, userText, platform, _chatId, ms
|
|
|
1349
1458
|
if (updateAgentActivity(activityTracker, block))
|
|
1350
1459
|
activityChanged = true;
|
|
1351
1460
|
accumulateBlockContent(block, state, toolCallMap);
|
|
1461
|
+
if (block.type === "input_injected") {
|
|
1462
|
+
injectionBoundaryObserved = true;
|
|
1463
|
+
await rotateFeishuCardForAppliedInjection(sessionId, nextTurnCount, tool, block.text, state, formatAgentActivityTitle(activityTracker.activity));
|
|
1464
|
+
}
|
|
1352
1465
|
if (block.type === "compact_boundary" && block.post_tokens) {
|
|
1353
1466
|
for (const cid of getChatsForSession(sessionId)) {
|
|
1354
1467
|
const sinfo = sessionInfoMap.get(cid);
|
|
@@ -1374,7 +1487,10 @@ export async function runAgentSession(sessionId, userText, platform, _chatId, ms
|
|
|
1374
1487
|
}
|
|
1375
1488
|
// 定时写入文件
|
|
1376
1489
|
const now2 = Date.now();
|
|
1377
|
-
if (activityChanged
|
|
1490
|
+
if (activityChanged
|
|
1491
|
+
|| outputReset
|
|
1492
|
+
|| injectionBoundaryObserved
|
|
1493
|
+
|| now2 - lastFileWrite >= FILE_WRITE_INTERVAL_MS) {
|
|
1378
1494
|
lastFileWrite = now2;
|
|
1379
1495
|
await writeStreamState({
|
|
1380
1496
|
sessionId,
|
|
@@ -1814,7 +1930,7 @@ export function startUnifiedDisplayLoop() {
|
|
|
1814
1930
|
if (!terminalCardAlreadyUpdated) {
|
|
1815
1931
|
const nextSeq = display.sequence + 1;
|
|
1816
1932
|
const { title: headerTitle, template: headerTemplate } = formatTerminalHeader(state.status, state.terminalError);
|
|
1817
|
-
const cardContent = truncateContent(
|
|
1933
|
+
const cardContent = truncateContent(formatTerminalCardContentForDisplay(state, display)) || " ";
|
|
1818
1934
|
const doneCard = buildProgressCard(progressView({ text: cardContent, status: "done", showStop: false, headerTitle, headerTemplate }));
|
|
1819
1935
|
await p.cardUpdate(display.cardId, doneCard, nextSeq).then(() => {
|
|
1820
1936
|
display.sequence = nextSeq;
|