toc-ocra 0.2.8 → 0.2.9
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 +8 -0
- package/dist/service/agent.mjs +204 -45
- package/dist/src/runs/run-supervisor.d.ts +5 -0
- package/dist/src/runs/run-supervisor.js +51 -6
- package/dist/src/runs/run-supervisor.js.map +1 -1
- package/dist/src/runs/task-attachments.d.ts +4 -0
- package/dist/src/runs/task-attachments.js +90 -0
- package/dist/src/runs/task-attachments.js.map +1 -0
- package/dist/src/runtime/agent-runtime.js +7 -0
- package/dist/src/runtime/agent-runtime.js.map +1 -1
- package/dist/src/transport/gateway-client.d.ts +1 -0
- package/dist/src/transport/gateway-client.js +31 -0
- package/dist/src/transport/gateway-client.js.map +1 -1
- package/dist/src/types/protocol.d.ts +8 -0
- package/dist/src/types/protocol.js +1 -0
- package/dist/src/types/protocol.js.map +1 -1
- package/dist/src/workspace/workspace-manager.js +1 -0
- package/dist/src/workspace/workspace-manager.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -73,6 +73,14 @@ node dist/src/index.js run
|
|
|
73
73
|
- **服务端对账兜底**:心跳会上报本机正在运行的任务,服务端据此释放本机已经不存在的运行;Agent 崩溃重启时,会清理上次遗留的 CLI 进程。
|
|
74
74
|
- **加一个 CLI**:在 `src/providers/cli/` 新增一个文件,实现 `HeadlessProvider`(启动参数 + 解析每一行输出),再在 `src/providers/registry.ts` 加一行。
|
|
75
75
|
|
|
76
|
+
### 看板附件
|
|
77
|
+
|
|
78
|
+
任务链附件在 CLI 启动前由本机代理下载到本次实际工作目录的 `.echo/attachments/message-{message_id}/`,完成大小与 SHA-256 校验后,提示词中的附件引用会替换为本机绝对路径。下载目录必须属于已允许的 CLI 工作目录,不能通过符号链接跳出;未配置设备默认目录或工位工作目录时会拒绝附件任务。每条消息最多 16 个附件,准备过程可取消、可复用已校验文件,最长等待 15 分钟。
|
|
79
|
+
|
|
80
|
+
窗口工位使用同机代理的 `run.files.prepare` 先下载文件,然后由服务端转发路径;同机设备代理必须在线。更新后的代理通过心跳声明 `task_attachments.version=1`,服务端会拒绝把附件任务发给不支持此协议的旧代理。请更新并重启设备代理与对应后端后使用。
|
|
81
|
+
|
|
82
|
+
下载文件暂时保留,可手工清理;`.echo` 不进入设备目录扫描,并带有仅作用于该内部目录的 Git 忽略规则。视频也可下载为文件,实际解析取决于 CLI 的能力。
|
|
83
|
+
|
|
76
84
|
详见 [docs/architecture.md](docs/architecture.md)。
|
|
77
85
|
|
|
78
86
|
## 开发
|
package/dist/service/agent.mjs
CHANGED
|
@@ -7,7 +7,7 @@ var __export = (target, all) => {
|
|
|
7
7
|
|
|
8
8
|
// src/plugin-service.ts
|
|
9
9
|
import { existsSync as existsSync6 } from "node:fs";
|
|
10
|
-
import { delimiter as delimiter5, join as
|
|
10
|
+
import { delimiter as delimiter5, join as join10 } from "node:path";
|
|
11
11
|
|
|
12
12
|
// src/config.ts
|
|
13
13
|
import { randomUUID } from "node:crypto";
|
|
@@ -15731,8 +15731,8 @@ var CliDetectionService = class {
|
|
|
15731
15731
|
var cliDetection = new CliDetectionService();
|
|
15732
15732
|
|
|
15733
15733
|
// src/runtime/agent-runtime.ts
|
|
15734
|
-
import { randomUUID as
|
|
15735
|
-
import { join as
|
|
15734
|
+
import { randomUUID as randomUUID3 } from "node:crypto";
|
|
15735
|
+
import { join as join9 } from "node:path";
|
|
15736
15736
|
|
|
15737
15737
|
// src/exec/script-runner.ts
|
|
15738
15738
|
import { spawn as spawn3 } from "node:child_process";
|
|
@@ -16717,8 +16717,8 @@ function commandOf(pid) {
|
|
|
16717
16717
|
|
|
16718
16718
|
// src/runs/run-supervisor.ts
|
|
16719
16719
|
import { spawn as spawn7 } from "node:child_process";
|
|
16720
|
-
import { mkdir as
|
|
16721
|
-
import { basename as
|
|
16720
|
+
import { mkdir as mkdir5, realpath as realpath2, stat as stat2, writeFile as writeFile5 } from "node:fs/promises";
|
|
16721
|
+
import { basename as basename3, isAbsolute as isAbsolute3, join as join7, relative as relative2, resolve as resolve3 } from "node:path";
|
|
16722
16722
|
import { createInterface as createInterface2 } from "node:readline";
|
|
16723
16723
|
|
|
16724
16724
|
// src/session/terminal-line-sanitizer.ts
|
|
@@ -16837,6 +16837,88 @@ function clipToCharacterBudget(rows, budget) {
|
|
|
16837
16837
|
return { lines: rows, limited: false };
|
|
16838
16838
|
}
|
|
16839
16839
|
|
|
16840
|
+
// src/runs/task-attachments.ts
|
|
16841
|
+
import { createHash as createHash2, randomUUID as randomUUID2 } from "node:crypto";
|
|
16842
|
+
import { constants as constants2 } from "node:fs";
|
|
16843
|
+
import { lstat, mkdir as mkdir4, open, realpath, rename as rename4, rm as rm2, writeFile as writeFile4 } from "node:fs/promises";
|
|
16844
|
+
import { basename as basename2, isAbsolute as isAbsolute2, join as join6, relative } from "node:path";
|
|
16845
|
+
function within(root, path) {
|
|
16846
|
+
const segment = relative(root, path);
|
|
16847
|
+
return segment === "" || !segment.startsWith("..") && !isAbsolute2(segment);
|
|
16848
|
+
}
|
|
16849
|
+
async function attachmentDirectory(workdir, messageId) {
|
|
16850
|
+
let directory = workdir;
|
|
16851
|
+
for (const segment of [".echo", "attachments", `message-${encodeURIComponent(messageId).slice(0, 180)}`]) {
|
|
16852
|
+
directory = join6(directory, segment);
|
|
16853
|
+
await mkdir4(directory, { mode: 448 }).catch((error51) => {
|
|
16854
|
+
if (error51.code !== "EEXIST") throw error51;
|
|
16855
|
+
});
|
|
16856
|
+
const metadata = await lstat(directory);
|
|
16857
|
+
if (metadata.isSymbolicLink() || !metadata.isDirectory() || !within(workdir, await realpath(directory))) {
|
|
16858
|
+
throw new Error("\u9644\u4EF6\u76EE\u5F55\u5FC5\u987B\u4F4D\u4E8E CLI \u5DE5\u4F5C\u76EE\u5F55\u5185\uFF0C\u4E14\u4E0D\u80FD\u662F\u7B26\u53F7\u94FE\u63A5");
|
|
16859
|
+
}
|
|
16860
|
+
if (segment === ".echo") await writeFile4(join6(directory, ".gitignore"), "*\n", { flag: "wx", mode: 384 }).catch((error51) => {
|
|
16861
|
+
if (error51.code !== "EEXIST") throw error51;
|
|
16862
|
+
});
|
|
16863
|
+
}
|
|
16864
|
+
return directory;
|
|
16865
|
+
}
|
|
16866
|
+
async function matches(path, size, sha256, signal2) {
|
|
16867
|
+
const metadata = await lstat(path).catch(() => void 0);
|
|
16868
|
+
if (!metadata?.isFile() || metadata.isSymbolicLink() || metadata.size !== size) return false;
|
|
16869
|
+
const file2 = await open(path, constants2.O_RDONLY | (constants2.O_NOFOLLOW ?? 0));
|
|
16870
|
+
try {
|
|
16871
|
+
const hash2 = createHash2("sha256");
|
|
16872
|
+
let read = 0;
|
|
16873
|
+
for await (const chunk of file2.createReadStream({ autoClose: false })) {
|
|
16874
|
+
signal2.throwIfAborted();
|
|
16875
|
+
read += chunk.length;
|
|
16876
|
+
if (read > size) return false;
|
|
16877
|
+
hash2.update(chunk);
|
|
16878
|
+
}
|
|
16879
|
+
return hash2.digest("hex") === sha256;
|
|
16880
|
+
} finally {
|
|
16881
|
+
await file2.close();
|
|
16882
|
+
}
|
|
16883
|
+
}
|
|
16884
|
+
async function prepareTaskAttachments(payload, workdir, download, signal2) {
|
|
16885
|
+
const files = payload.task_attachments ?? [];
|
|
16886
|
+
if (!files.length) return payload.prompt;
|
|
16887
|
+
const directory = await attachmentDirectory(workdir, payload.message_id || payload.run_id);
|
|
16888
|
+
let prompt = payload.prompt;
|
|
16889
|
+
const paths = [];
|
|
16890
|
+
for (const file2 of files) {
|
|
16891
|
+
signal2.throwIfAborted();
|
|
16892
|
+
const token = `{{echo-file:${file2.file_id}}}`;
|
|
16893
|
+
if (file2.prompt_token !== token) throw new Error("\u9644\u4EF6\u8DEF\u5F84\u5360\u4F4D\u7B26\u65E0\u6548\uFF0C\u8BF7\u66F4\u65B0\u670D\u52A1\u5668");
|
|
16894
|
+
const characters = Array.from(basename2(file2.name.replace(/\\/g, "/")).replace(/[^\p{L}\p{N}._-]/gu, "_").replace(/\.+$/u, ""));
|
|
16895
|
+
while (Buffer.byteLength(characters.join("")) > 160) characters.shift();
|
|
16896
|
+
const name = characters.join("") || "attachment";
|
|
16897
|
+
const path = join6(directory, `${file2.file_id}-${name}`);
|
|
16898
|
+
if (!await matches(path, file2.size, file2.sha256, signal2)) {
|
|
16899
|
+
const temporary = join6(directory, `.download-${file2.file_id}-${randomUUID2()}.part`);
|
|
16900
|
+
try {
|
|
16901
|
+
await download(payload.run_id, file2.file_id, temporary, file2.size, file2.sha256, signal2);
|
|
16902
|
+
signal2.throwIfAborted();
|
|
16903
|
+
if (!within(workdir, await realpath(directory))) throw new Error("\u9644\u4EF6\u76EE\u5F55\u5DF2\u6539\u53D8\uFF0C\u8BF7\u91CD\u8BD5");
|
|
16904
|
+
await rename4(temporary, path);
|
|
16905
|
+
} finally {
|
|
16906
|
+
await rm2(temporary, { force: true });
|
|
16907
|
+
}
|
|
16908
|
+
}
|
|
16909
|
+
if (!within(workdir, await realpath(path))) throw new Error("\u9644\u4EF6\u8DEF\u5F84\u4E0D\u5728 CLI \u5DE5\u4F5C\u76EE\u5F55\u5185");
|
|
16910
|
+
prompt = prompt.split(token).join(path);
|
|
16911
|
+
paths.push(`- ${JSON.stringify(path)}\uFF08${file2.mime}\uFF0C${file2.size} \u5B57\u8282\uFF09`);
|
|
16912
|
+
}
|
|
16913
|
+
if (prompt.includes("{{echo-file:")) throw new Error("\u9644\u4EF6\u8DEF\u5F84\u672A\u5B8C\u6574\u89E3\u6790");
|
|
16914
|
+
prompt += `
|
|
16915
|
+
|
|
16916
|
+
\u9644\u4EF6\u5DF2\u7531\u8BBE\u5907\u4EE3\u7406\u4E0B\u8F7D\u5230\u5F53\u524D\u5DE5\u4F5C\u76EE\u5F55\u3002\u8BF7\u76F4\u63A5\u8BFB\u53D6\u4EE5\u4E0B\u672C\u673A\u6587\u4EF6\uFF0C\u65E0\u9700\u4E0B\u8F7D\uFF1A
|
|
16917
|
+
${paths.join("\n")}`;
|
|
16918
|
+
if (Buffer.byteLength(prompt) > 512 * 1024) throw new Error("\u5305\u542B\u9644\u4EF6\u8DEF\u5F84\u7684\u6D88\u606F\u8FC7\u957F");
|
|
16919
|
+
return prompt;
|
|
16920
|
+
}
|
|
16921
|
+
|
|
16840
16922
|
// src/runs/run-supervisor.ts
|
|
16841
16923
|
var COMPLETION_EXIT_GRACE_MS = 15e3;
|
|
16842
16924
|
var RECENT_RUN_TTL_MS = 10 * 6e4;
|
|
@@ -16858,12 +16940,13 @@ var RunSupervisor = class {
|
|
|
16858
16940
|
/** Run ids whose finish report has not been acknowledged yet. */
|
|
16859
16941
|
unreported = /* @__PURE__ */ new Set();
|
|
16860
16942
|
chatRunIds = /* @__PURE__ */ new Set();
|
|
16943
|
+
preparing = /* @__PURE__ */ new Map();
|
|
16861
16944
|
get activeCount() {
|
|
16862
|
-
return this.active.size;
|
|
16945
|
+
return this.active.size + this.preparing.size;
|
|
16863
16946
|
}
|
|
16864
16947
|
/** Runs to list in the heartbeat: still running, or finished but not yet reported. */
|
|
16865
16948
|
activeRunIds() {
|
|
16866
|
-
return [.../* @__PURE__ */ new Set([...this.active.keys(), ...this.unreported])];
|
|
16949
|
+
return [.../* @__PURE__ */ new Set([...this.active.keys(), ...this.preparing.keys(), ...this.unreported])];
|
|
16867
16950
|
}
|
|
16868
16951
|
activeChatRunIds() {
|
|
16869
16952
|
return this.activeRunIds().filter((id) => this.chatRunIds.has(id));
|
|
@@ -16885,6 +16968,16 @@ var RunSupervisor = class {
|
|
|
16885
16968
|
* directory, unknown CLI) are reported as a failed run, never thrown.
|
|
16886
16969
|
*/
|
|
16887
16970
|
async start(payload) {
|
|
16971
|
+
if (this.preparing.has(payload.run_id)) return;
|
|
16972
|
+
const controller = new AbortController();
|
|
16973
|
+
this.preparing.set(payload.run_id, { workstationId: payload.workstation_id, controller });
|
|
16974
|
+
try {
|
|
16975
|
+
await this.startPrepared(payload, controller.signal);
|
|
16976
|
+
} finally {
|
|
16977
|
+
this.preparing.delete(payload.run_id);
|
|
16978
|
+
}
|
|
16979
|
+
}
|
|
16980
|
+
async startPrepared(payload, signal2) {
|
|
16888
16981
|
if (this.active.has(payload.run_id) || this.recent.has(payload.run_id)) {
|
|
16889
16982
|
this.logger.info("run-duplicate-ignored", { runId: payload.run_id });
|
|
16890
16983
|
return;
|
|
@@ -16895,20 +16988,32 @@ var RunSupervisor = class {
|
|
|
16895
16988
|
this.remember({ runId: payload.run_id, workstationId: payload.workstation_id, tail: new TerminalTailBuffer(), finishedAt: Date.now(), status: "failed", chat: !!payload.chat_session_id });
|
|
16896
16989
|
await this.report(payload.run_id, { status: "failed", error: error51, duration_ms: 0 });
|
|
16897
16990
|
};
|
|
16898
|
-
if ([...this.active.values()].some((run2) => run2.workstationId === payload.workstation_id)) {
|
|
16991
|
+
if ([...this.active.values()].some((run2) => run2.workstationId === payload.workstation_id) || [...this.preparing].some(([id, run2]) => id !== payload.run_id && run2.workstationId === payload.workstation_id)) {
|
|
16899
16992
|
return refuse("\u672C\u673A\u8BE5\u5DE5\u4F4D\u5DF2\u6709\u4EFB\u52A1\u5728\u8FD0\u884C");
|
|
16900
16993
|
}
|
|
16901
|
-
if (this.active.size
|
|
16994
|
+
if (this.active.size + this.preparing.size > this.config.maxConcurrentRuns) {
|
|
16902
16995
|
return refuse(`\u672C\u673A\u540C\u65F6\u8FD0\u884C\u7684\u4EFB\u52A1\u5DF2\u8FBE\u4E0A\u9650\uFF08${this.config.maxConcurrentRuns}\uFF09`);
|
|
16903
16996
|
}
|
|
16904
16997
|
let provider;
|
|
16905
16998
|
let workdir;
|
|
16906
16999
|
try {
|
|
16907
17000
|
provider = resolveHeadlessProvider(payload.agent_type);
|
|
17001
|
+
if (payload.task_attachments?.length && !payload.workdir) throw new Error("\u9644\u4EF6\u9700\u8981\u914D\u7F6E\u8BBE\u5907\u9ED8\u8BA4\u76EE\u5F55\u6216\u5DE5\u4F4D\u5DE5\u4F5C\u76EE\u5F55");
|
|
16908
17002
|
workdir = payload.chat_session_id && !payload.workdir ? await this.defaultChatWorkdir() : await this.validateWorkdir(payload.workdir || this.config.allowedRoots[0]);
|
|
16909
17003
|
} catch (error51) {
|
|
16910
17004
|
return refuse(errorMessage(error51));
|
|
16911
17005
|
}
|
|
17006
|
+
try {
|
|
17007
|
+
if (payload.task_attachments?.length) payload = { ...payload, prompt: await this.prepareTaskFiles(payload, signal2, workdir) };
|
|
17008
|
+
signal2.throwIfAborted();
|
|
17009
|
+
} catch (error51) {
|
|
17010
|
+
if (signal2.aborted) {
|
|
17011
|
+
this.remember({ runId: payload.run_id, workstationId: payload.workstation_id, tail: new TerminalTailBuffer(), finishedAt: Date.now(), status: "cancelled", chat: !!payload.chat_session_id });
|
|
17012
|
+
await this.report(payload.run_id, { status: "cancelled", error: "\u9644\u4EF6\u51C6\u5907\u5DF2\u53D6\u6D88", duration_ms: 0 });
|
|
17013
|
+
return;
|
|
17014
|
+
}
|
|
17015
|
+
return refuse(`\u9644\u4EF6\u4E0B\u8F7D\u5931\u8D25\uFF1A${errorMessage(error51)}`);
|
|
17016
|
+
}
|
|
16912
17017
|
const request = {
|
|
16913
17018
|
prompt: payload.prompt,
|
|
16914
17019
|
sessionId: payload.session_id,
|
|
@@ -16927,7 +17032,7 @@ var RunSupervisor = class {
|
|
|
16927
17032
|
}
|
|
16928
17033
|
if (!payload.prompt && !payload.attachments?.length) return refuse("\u6D88\u606F\u5185\u5BB9\u4E3A\u7A7A");
|
|
16929
17034
|
if ([...this.active.values()].some((run2) => run2.workstationId === payload.workstation_id)) return refuse("\u672C\u673A\u8BE5\u5DE5\u4F4D\u5DF2\u6709\u4EFB\u52A1\u5728\u8FD0\u884C");
|
|
16930
|
-
if (this.active.size
|
|
17035
|
+
if (this.active.size + this.preparing.size > this.config.maxConcurrentRuns) return refuse("\u672C\u673A\u540C\u65F6\u8FD0\u884C\u7684\u4EFB\u52A1\u5DF2\u8FBE\u4E0A\u9650");
|
|
16931
17036
|
const launch = provider.buildLaunch({
|
|
16932
17037
|
prompt: payload.prompt,
|
|
16933
17038
|
sessionId: payload.session_id,
|
|
@@ -16976,6 +17081,7 @@ var RunSupervisor = class {
|
|
|
16976
17081
|
timers: []
|
|
16977
17082
|
};
|
|
16978
17083
|
this.active.set(run.runId, run);
|
|
17084
|
+
this.preparing.delete(run.runId);
|
|
16979
17085
|
this.unreported.add(run.runId);
|
|
16980
17086
|
if (run.pid > 1) this.orphans?.add({ runId: run.runId, pid: run.pid, executable: launch.executable, startedAt: run.startedAt });
|
|
16981
17087
|
run.tail.append(`\u25CF ${provider.id} \u542F\u52A8\u4E8E ${workdir}${payload.session_id ? `\uFF08\u7EE7\u7EED\u4F1A\u8BDD ${payload.session_id}\uFF09` : ""}
|
|
@@ -16995,7 +17101,7 @@ var RunSupervisor = class {
|
|
|
16995
17101
|
run.tail.append(`${clean}
|
|
16996
17102
|
`);
|
|
16997
17103
|
});
|
|
16998
|
-
child.once("close", (code,
|
|
17104
|
+
child.once("close", (code, signal3) => void this.onExit(run, code, signal3, spawnError));
|
|
16999
17105
|
if (provider.startInput) {
|
|
17000
17106
|
child.once("spawn", () => {
|
|
17001
17107
|
try {
|
|
@@ -17130,16 +17236,16 @@ var RunSupervisor = class {
|
|
|
17130
17236
|
async prepareChatAttachments(payload) {
|
|
17131
17237
|
if (!payload.attachments?.length) return [];
|
|
17132
17238
|
if (!this.reporter.chatMedia) throw new Error("\u9644\u4EF6\u8F6C\u53D1\u672A\u914D\u7F6E");
|
|
17133
|
-
const directory =
|
|
17134
|
-
await
|
|
17239
|
+
const directory = join7(this.config.dataDir, "chat-media", encodeURIComponent(payload.chat_session_id ?? payload.run_id));
|
|
17240
|
+
await mkdir5(directory, { recursive: true, mode: 448 });
|
|
17135
17241
|
const files = [];
|
|
17136
17242
|
for (const attachment of payload.attachments) {
|
|
17137
|
-
const name =
|
|
17138
|
-
const path =
|
|
17243
|
+
const name = basename3(attachment.name).replace(/[^\p{L}\p{N}._-]/gu, "_").slice(-120) || "attachment";
|
|
17244
|
+
const path = join7(directory, `${encodeURIComponent(attachment.media_id)}-${name}`);
|
|
17139
17245
|
const existing = await stat2(path).catch(() => void 0);
|
|
17140
17246
|
if (!existing?.isFile()) {
|
|
17141
17247
|
const data = await this.reporter.chatMedia(payload.run_id, attachment.media_id);
|
|
17142
|
-
await
|
|
17248
|
+
await writeFile5(path, data, { mode: 384 });
|
|
17143
17249
|
}
|
|
17144
17250
|
files.push({ path, name: attachment.name, kind: attachment.kind });
|
|
17145
17251
|
}
|
|
@@ -17152,7 +17258,14 @@ var RunSupervisor = class {
|
|
|
17152
17258
|
/** Stops a run: the whole process tree, then a `cancelled` report. */
|
|
17153
17259
|
async cancel(runId) {
|
|
17154
17260
|
const run = this.active.get(runId);
|
|
17155
|
-
if (!run)
|
|
17261
|
+
if (!run) {
|
|
17262
|
+
const preparing = this.preparing.get(runId);
|
|
17263
|
+
if (preparing) {
|
|
17264
|
+
preparing.controller.abort();
|
|
17265
|
+
return true;
|
|
17266
|
+
}
|
|
17267
|
+
return false;
|
|
17268
|
+
}
|
|
17156
17269
|
run.cancelRequested = true;
|
|
17157
17270
|
run.tail.append("\u25CF \u6536\u5230\u505C\u6B62\u6307\u4EE4\uFF0C\u6B63\u5728\u7ED3\u675F\u8FDB\u7A0B\u6811\n");
|
|
17158
17271
|
this.logger.info("run-cancel", { runId, pid: run.pid });
|
|
@@ -17175,6 +17288,7 @@ var RunSupervisor = class {
|
|
|
17175
17288
|
return { ...read, status: this.active.has(target.runId) ? "running" : "finished", run_id: target.runId };
|
|
17176
17289
|
}
|
|
17177
17290
|
async disposeAll() {
|
|
17291
|
+
for (const entry of this.preparing.values()) entry.controller.abort();
|
|
17178
17292
|
await Promise.all([...this.active.values()].map((run) => {
|
|
17179
17293
|
run.cancelRequested = true;
|
|
17180
17294
|
return killProcessTree(run.pid, 1e3);
|
|
@@ -17200,17 +17314,22 @@ var RunSupervisor = class {
|
|
|
17200
17314
|
if (!input) throw new Error("\u6CA1\u6709\u914D\u7F6E\u5141\u8BB8\u7684\u5DE5\u4F5C\u76EE\u5F55");
|
|
17201
17315
|
let candidate;
|
|
17202
17316
|
try {
|
|
17203
|
-
candidate = await
|
|
17317
|
+
candidate = await realpath2(isAbsolute3(input) ? input : resolve3(input));
|
|
17204
17318
|
} catch {
|
|
17205
17319
|
throw new Error(`\u5DE5\u4F5C\u76EE\u5F55\u4E0D\u5B58\u5728\uFF1A${input}`);
|
|
17206
17320
|
}
|
|
17207
17321
|
for (const root of this.config.allowedRoots) {
|
|
17208
|
-
const allowed = await
|
|
17209
|
-
const segment =
|
|
17210
|
-
if (segment === "" || !segment.startsWith("..") && !
|
|
17322
|
+
const allowed = await realpath2(root).catch(() => resolve3(root));
|
|
17323
|
+
const segment = relative2(allowed, candidate);
|
|
17324
|
+
if (segment === "" || !segment.startsWith("..") && !isAbsolute3(segment)) return candidate;
|
|
17211
17325
|
}
|
|
17212
17326
|
throw new Error(`\u5DE5\u4F5C\u76EE\u5F55\u4E0D\u5728\u5141\u8BB8\u8303\u56F4\u5185\uFF1A${input}`);
|
|
17213
17327
|
}
|
|
17328
|
+
async prepareTaskFiles(payload, signal2 = AbortSignal.timeout(15 * 6e4), workdir) {
|
|
17329
|
+
if (!payload.workdir) throw new Error("\u8BF7\u5148\u914D\u7F6E\u8BBE\u5907\u9ED8\u8BA4\u76EE\u5F55\u6216\u5DE5\u4F4D\u5DE5\u4F5C\u76EE\u5F55");
|
|
17330
|
+
if (!this.reporter.taskFile) throw new Error("\u4EFB\u52A1\u9644\u4EF6\u4E0B\u8F7D\u672A\u914D\u7F6E");
|
|
17331
|
+
return prepareTaskAttachments(payload, workdir ?? await this.validateWorkdir(payload.workdir), this.reporter.taskFile, AbortSignal.any([signal2, AbortSignal.timeout(15 * 6e4)]));
|
|
17332
|
+
}
|
|
17214
17333
|
};
|
|
17215
17334
|
function errorMessage(error51) {
|
|
17216
17335
|
return error51 instanceof Error ? error51.message : String(error51);
|
|
@@ -17232,6 +17351,10 @@ function sanitizeChatEvent(event) {
|
|
|
17232
17351
|
}
|
|
17233
17352
|
|
|
17234
17353
|
// src/transport/gateway-client.ts
|
|
17354
|
+
import { createHash as createHash3 } from "node:crypto";
|
|
17355
|
+
import { createWriteStream } from "node:fs";
|
|
17356
|
+
import { Readable, Transform } from "node:stream";
|
|
17357
|
+
import { pipeline } from "node:stream/promises";
|
|
17235
17358
|
var GatewayError = class extends Error {
|
|
17236
17359
|
constructor(message, status) {
|
|
17237
17360
|
super(message);
|
|
@@ -17313,19 +17436,46 @@ var GatewayClient = class {
|
|
|
17313
17436
|
runStarted(runId, body) {
|
|
17314
17437
|
return this.request("POST", `/runs/${encodeURIComponent(runId)}/started`, body, 15e3);
|
|
17315
17438
|
}
|
|
17439
|
+
async taskFile(runId, fileId, destination, size, sha256, signal2) {
|
|
17440
|
+
const response = await fetch(`${this.config.serverUrl}/api/agent/runs/${encodeURIComponent(runId)}/files/${encodeURIComponent(fileId)}`, {
|
|
17441
|
+
headers: { authorization: `Bearer ${this.config.token}` },
|
|
17442
|
+
redirect: "error",
|
|
17443
|
+
signal: AbortSignal.any([signal2, AbortSignal.timeout(15 * 6e4)])
|
|
17444
|
+
});
|
|
17445
|
+
if (!response.ok || !response.body) throw new GatewayError("\u65E0\u6CD5\u8BFB\u53D6\u4EFB\u52A1\u9644\u4EF6\uFF0C\u8BF7\u68C0\u67E5\u6587\u4EF6\u662F\u5426\u5DF2\u5220\u9664\u53CA\u8BBE\u5907\u51ED\u636E", response.status);
|
|
17446
|
+
const declared = response.headers.get("content-length");
|
|
17447
|
+
if (declared !== null && Number(declared) !== size) {
|
|
17448
|
+
await response.body.cancel();
|
|
17449
|
+
throw new Error("\u9644\u4EF6\u5927\u5C0F\u4E0E\u4FDD\u5B58\u7684\u8BB0\u5F55\u4E0D\u4E00\u81F4");
|
|
17450
|
+
}
|
|
17451
|
+
const hash2 = createHash3("sha256");
|
|
17452
|
+
let received = 0;
|
|
17453
|
+
const verifier = new Transform({ transform(chunk, _encoding, callback) {
|
|
17454
|
+
received += chunk.length;
|
|
17455
|
+
if (received > size) {
|
|
17456
|
+
callback(new Error("\u9644\u4EF6\u4E0B\u8F7D\u8D85\u51FA\u8BB0\u5F55\u5927\u5C0F"));
|
|
17457
|
+
return;
|
|
17458
|
+
}
|
|
17459
|
+
hash2.update(chunk);
|
|
17460
|
+
callback(null, chunk);
|
|
17461
|
+
} });
|
|
17462
|
+
const target = createWriteStream(destination, { flags: "wx", mode: 384 });
|
|
17463
|
+
await pipeline(Readable.fromWeb(response.body), verifier, target, { signal: signal2 });
|
|
17464
|
+
if (received !== size || hash2.digest("hex") !== sha256) throw new Error("\u9644\u4EF6\u4E0B\u8F7D\u4E0D\u5B8C\u6574\u6216\u6821\u9A8C\u5931\u8D25\uFF0C\u8BF7\u91CD\u8BD5");
|
|
17465
|
+
}
|
|
17316
17466
|
runFinished(runId, body) {
|
|
17317
17467
|
return this.request("POST", `/runs/${encodeURIComponent(runId)}/finished`, body, 15e3);
|
|
17318
17468
|
}
|
|
17319
17469
|
};
|
|
17320
17470
|
|
|
17321
17471
|
// src/workspace/directory-browser.ts
|
|
17322
|
-
import { readdir, realpath as
|
|
17472
|
+
import { readdir, realpath as realpath3, stat as stat3 } from "node:fs/promises";
|
|
17323
17473
|
import { homedir as homedir5 } from "node:os";
|
|
17324
17474
|
import { dirname as dirname6, parse as parse3, resolve as resolve4 } from "node:path";
|
|
17325
17475
|
var MAX_ENTRIES = 1e3;
|
|
17326
17476
|
async function listDirectory(requested, allowedRoots) {
|
|
17327
17477
|
const raw = requested.trim().replace(/^~(?=$|[\\/])/, homedir5()) || homedir5();
|
|
17328
|
-
const directory = await
|
|
17478
|
+
const directory = await realpath3(resolve4(raw)).catch(() => {
|
|
17329
17479
|
throw new Error(`\u76EE\u5F55\u4E0D\u5B58\u5728\uFF1A${requested}`);
|
|
17330
17480
|
});
|
|
17331
17481
|
if (!(await stat3(directory)).isDirectory()) throw new Error("\u8FD9\u4E0D\u662F\u4E00\u4E2A\u76EE\u5F55");
|
|
@@ -17357,12 +17507,13 @@ async function listDirectory(requested, allowedRoots) {
|
|
|
17357
17507
|
|
|
17358
17508
|
// src/workspace/workspace-manager.ts
|
|
17359
17509
|
import { spawn as spawn8 } from "node:child_process";
|
|
17360
|
-
import { chmod as chmod3, mkdir as
|
|
17510
|
+
import { chmod as chmod3, mkdir as mkdir6, mkdtemp, opendir, realpath as realpath4, rm as rm3, stat as stat4, writeFile as writeFile6 } from "node:fs/promises";
|
|
17361
17511
|
import { homedir as homedir6, tmpdir } from "node:os";
|
|
17362
|
-
import { isAbsolute as
|
|
17512
|
+
import { isAbsolute as isAbsolute4, join as join8, relative as relative3, resolve as resolve5 } from "node:path";
|
|
17363
17513
|
var MAX_NODES = 2e3;
|
|
17364
17514
|
var MAX_DIRECTORY_ENTRIES = 12e3;
|
|
17365
17515
|
var IGNORED_DIRECTORIES = /* @__PURE__ */ new Set([
|
|
17516
|
+
".echo",
|
|
17366
17517
|
".git",
|
|
17367
17518
|
"node_modules",
|
|
17368
17519
|
".next",
|
|
@@ -17408,12 +17559,12 @@ var WorkspaceManager = class {
|
|
|
17408
17559
|
validateRootPath(value) {
|
|
17409
17560
|
const raw = value.trim().replace(/^~(?=$|[\\/])/, homedir6());
|
|
17410
17561
|
const root = resolve5(raw);
|
|
17411
|
-
if (!
|
|
17412
|
-
if (
|
|
17562
|
+
if (!isAbsolute4(raw)) throw new Error("\u6839\u76EE\u5F55\u5FC5\u987B\u662F\u7EDD\u5BF9\u8DEF\u5F84");
|
|
17563
|
+
if (relative3(root, resolve5(root, "..")) === "") throw new Error("\u4E0D\u80FD\u628A\u6587\u4EF6\u7CFB\u7EDF\u6839\u76EE\u5F55\u4F5C\u4E3A\u76EE\u5F55\u6839");
|
|
17413
17564
|
return root;
|
|
17414
17565
|
}
|
|
17415
17566
|
async syncTree(rootPath, request) {
|
|
17416
|
-
await
|
|
17567
|
+
await mkdir6(rootPath, { recursive: true });
|
|
17417
17568
|
await addAllowedRoot(this.config, rootPath);
|
|
17418
17569
|
const results = [];
|
|
17419
17570
|
const requestedPaths = /* @__PURE__ */ new Set();
|
|
@@ -17437,7 +17588,7 @@ var WorkspaceManager = class {
|
|
|
17437
17588
|
});
|
|
17438
17589
|
continue;
|
|
17439
17590
|
}
|
|
17440
|
-
await
|
|
17591
|
+
await mkdir6(target, { recursive: true });
|
|
17441
17592
|
results.push({
|
|
17442
17593
|
relative_path: relativePath,
|
|
17443
17594
|
status: "created",
|
|
@@ -17486,7 +17637,7 @@ var WorkspaceManager = class {
|
|
|
17486
17637
|
}
|
|
17487
17638
|
if (!metadata) {
|
|
17488
17639
|
if (!gitUrl) throw new Error("A missing repository needs a Git remote URL");
|
|
17489
|
-
await
|
|
17640
|
+
await mkdir6(resolve5(target, ".."), { recursive: true });
|
|
17490
17641
|
const cloneArgs = branch ? ["clone", "--branch", branch, "--single-branch", gitUrl, target] : ["clone", gitUrl, target];
|
|
17491
17642
|
await this.runGit(cloneArgs, rootPath, timeoutSeconds, false, gitEnvironment);
|
|
17492
17643
|
created = true;
|
|
@@ -17580,13 +17731,13 @@ var WorkspaceManager = class {
|
|
|
17580
17731
|
};
|
|
17581
17732
|
}
|
|
17582
17733
|
async scanTree(rootPath, maxDepth, scanRoots) {
|
|
17583
|
-
const resolvedRoot = await
|
|
17734
|
+
const resolvedRoot = await realpath4(rootPath);
|
|
17584
17735
|
const rootMetadata = await stat4(resolvedRoot);
|
|
17585
17736
|
if (!rootMetadata.isDirectory()) throw new Error("The workspace root is not a directory");
|
|
17586
17737
|
const nodes = [];
|
|
17587
17738
|
let entriesRead = 0;
|
|
17588
17739
|
const appendNode = async (target) => {
|
|
17589
|
-
const relativePath =
|
|
17740
|
+
const relativePath = relative3(resolvedRoot, target).replaceAll("\\", "/");
|
|
17590
17741
|
if (!relativePath || !this.isSafeRelativePath(relativePath)) return void 0;
|
|
17591
17742
|
const git = await this.readGitMetadata(target);
|
|
17592
17743
|
if (git) {
|
|
@@ -17670,8 +17821,8 @@ var WorkspaceManager = class {
|
|
|
17670
17821
|
if (result.exitCode !== 0 || !topLevel) return false;
|
|
17671
17822
|
try {
|
|
17672
17823
|
const [resolvedDirectory, resolvedTopLevel] = await Promise.all([
|
|
17673
|
-
|
|
17674
|
-
|
|
17824
|
+
realpath4(directory),
|
|
17825
|
+
realpath4(topLevel)
|
|
17675
17826
|
]);
|
|
17676
17827
|
return resolvedDirectory === resolvedTopLevel;
|
|
17677
17828
|
} catch {
|
|
@@ -17691,8 +17842,8 @@ var WorkspaceManager = class {
|
|
|
17691
17842
|
}
|
|
17692
17843
|
resolveWithinRoot(rootPath, relativePath) {
|
|
17693
17844
|
const target = resolve5(rootPath, relativePath);
|
|
17694
|
-
const relativeToRoot =
|
|
17695
|
-
if (relativeToRoot.startsWith("..") ||
|
|
17845
|
+
const relativeToRoot = relative3(rootPath, target);
|
|
17846
|
+
if (relativeToRoot.startsWith("..") || isAbsolute4(relativeToRoot)) {
|
|
17696
17847
|
throw new Error("A workspace path must remain under the configured workspace root");
|
|
17697
17848
|
}
|
|
17698
17849
|
return target;
|
|
@@ -17733,13 +17884,13 @@ var WorkspaceManager = class {
|
|
|
17733
17884
|
}
|
|
17734
17885
|
async createGitAuthEnvironment(auth) {
|
|
17735
17886
|
if (!auth) return void 0;
|
|
17736
|
-
const directory = await mkdtemp(
|
|
17887
|
+
const directory = await mkdtemp(join8(tmpdir(), "toc-ocra-git-auth-"));
|
|
17737
17888
|
const isWindows = process.platform === "win32";
|
|
17738
|
-
const scriptPath =
|
|
17889
|
+
const scriptPath = join8(directory, isWindows ? "askpass.cmd" : "askpass.sh");
|
|
17739
17890
|
let script;
|
|
17740
17891
|
if (isWindows) {
|
|
17741
|
-
const helperPath =
|
|
17742
|
-
await
|
|
17892
|
+
const helperPath = join8(directory, "askpass.cjs");
|
|
17893
|
+
await writeFile6(
|
|
17743
17894
|
helperPath,
|
|
17744
17895
|
'const prompt = process.argv.slice(2).join(" ");\nprocess.stdout.write(/username/i.test(prompt) ? process.env.ECHO_GIT_USERNAME ?? "" : process.env.ECHO_GIT_PASSWORD ?? "");\n',
|
|
17745
17896
|
{ mode: 448 }
|
|
@@ -17748,7 +17899,7 @@ var WorkspaceManager = class {
|
|
|
17748
17899
|
} else {
|
|
17749
17900
|
script = '#!/bin/sh\ncase "$1" in *Username*|*username*) printf "%s\\n" "$ECHO_GIT_USERNAME" ;; *) printf "%s\\n" "$ECHO_GIT_PASSWORD" ;; esac\n';
|
|
17750
17901
|
}
|
|
17751
|
-
await
|
|
17902
|
+
await writeFile6(scriptPath, script, { mode: 448 });
|
|
17752
17903
|
if (!isWindows) await chmod3(scriptPath, 448);
|
|
17753
17904
|
return {
|
|
17754
17905
|
env: {
|
|
@@ -17759,7 +17910,7 @@ var WorkspaceManager = class {
|
|
|
17759
17910
|
ECHO_GIT_USERNAME: auth.username,
|
|
17760
17911
|
ECHO_GIT_PASSWORD: auth.password
|
|
17761
17912
|
},
|
|
17762
|
-
cleanup: () =>
|
|
17913
|
+
cleanup: () => rm3(directory, { recursive: true, force: true })
|
|
17763
17914
|
};
|
|
17764
17915
|
}
|
|
17765
17916
|
message(error51) {
|
|
@@ -17784,6 +17935,7 @@ var RunStartPayloadSchema = external_exports.object({
|
|
|
17784
17935
|
prompt: external_exports.string().max(512 * 1024),
|
|
17785
17936
|
chat_session_id: external_exports.string().max(64).optional(),
|
|
17786
17937
|
attachments: external_exports.array(external_exports.object({ media_id: external_exports.string().min(1).max(64), name: external_exports.string().max(200), kind: external_exports.enum(["image", "file"]) })).max(16).optional(),
|
|
17938
|
+
task_attachments: external_exports.array(external_exports.object({ file_id: external_exports.uuid(), name: external_exports.string().min(1).max(255), mime: external_exports.string().max(255), size: external_exports.number().int().positive(), sha256: external_exports.string().regex(/^[a-f0-9]{64}$/), prompt_token: external_exports.string().max(100) })).max(16).nullish(),
|
|
17787
17939
|
session_id: external_exports.string().max(200).optional().default(""),
|
|
17788
17940
|
model: external_exports.string().max(100).optional().default(""),
|
|
17789
17941
|
reasoning_effort: external_exports.string().max(40).optional().default(""),
|
|
@@ -17854,13 +18006,14 @@ var AgentRuntime = class {
|
|
|
17854
18006
|
await this.retry(() => this.client.chatEvents(runId, events), 5);
|
|
17855
18007
|
},
|
|
17856
18008
|
chatMedia: (runId, mediaId) => this.client.chatMedia(runId, mediaId),
|
|
18009
|
+
taskFile: (runId, fileId, destination, size, sha256, signal2) => this.client.taskFile(runId, fileId, destination, size, sha256, signal2),
|
|
17857
18010
|
finished: async (runId, report) => {
|
|
17858
18011
|
this.pendingReports.set(runId, report);
|
|
17859
18012
|
await this.flushReport(runId);
|
|
17860
18013
|
}
|
|
17861
18014
|
};
|
|
17862
18015
|
this.workspaces = new WorkspaceManager(config2, this.logger);
|
|
17863
|
-
this.orphans = new OrphanRegistry(
|
|
18016
|
+
this.orphans = new OrphanRegistry(join9(config2.dataDir, chatOnly ? "chat-active-runs.json" : "active-runs.json"));
|
|
17864
18017
|
this.runs = new RunSupervisor(config2, this.logger, reporter, this.orphans);
|
|
17865
18018
|
}
|
|
17866
18019
|
config;
|
|
@@ -17873,7 +18026,7 @@ var AgentRuntime = class {
|
|
|
17873
18026
|
workspaces;
|
|
17874
18027
|
stopController = new AbortController();
|
|
17875
18028
|
startedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
17876
|
-
chatRunnerId =
|
|
18029
|
+
chatRunnerId = randomUUID3();
|
|
17877
18030
|
chatReady = false;
|
|
17878
18031
|
chatLeaseInitialized = false;
|
|
17879
18032
|
chatError;
|
|
@@ -18014,6 +18167,7 @@ var AgentRuntime = class {
|
|
|
18014
18167
|
return {
|
|
18015
18168
|
providers: snapshot.providers.map((p) => ({ id: p.id, installed: p.installed, runnable: p.available, version: p.version ?? "", path: p.resolvedPath ?? "" })),
|
|
18016
18169
|
cli_chat: { version: 1, providers: CHAT_PROVIDERS.filter((id) => snapshot.detectedIds.includes(id)) },
|
|
18170
|
+
task_attachments: { version: 1 },
|
|
18017
18171
|
detected_ids: snapshot.detectedIds,
|
|
18018
18172
|
refreshed_at: snapshot.refreshedAt
|
|
18019
18173
|
};
|
|
@@ -18067,6 +18221,11 @@ var AgentRuntime = class {
|
|
|
18067
18221
|
}
|
|
18068
18222
|
async execute(command) {
|
|
18069
18223
|
switch (command.type) {
|
|
18224
|
+
case "run.files.prepare": {
|
|
18225
|
+
const payload = RunStartPayloadSchema.parse(command.payload);
|
|
18226
|
+
const prompt = await this.runs.prepareTaskFiles(payload);
|
|
18227
|
+
return { success: true, result: { prompt } };
|
|
18228
|
+
}
|
|
18070
18229
|
case "chat.run.start":
|
|
18071
18230
|
case "run.start": {
|
|
18072
18231
|
const payload = RunStartPayloadSchema.parse(command.payload);
|
|
@@ -18401,7 +18560,7 @@ function preferSystemNode() {
|
|
|
18401
18560
|
const names = process.platform === "win32" ? ["node.exe"] : ["node"];
|
|
18402
18561
|
for (const directory of (process.env.PATH ?? "").split(delimiter5)) {
|
|
18403
18562
|
for (const name of names) {
|
|
18404
|
-
const candidate = directory &&
|
|
18563
|
+
const candidate = directory && join10(directory, name);
|
|
18405
18564
|
if (candidate && existsSync6(candidate)) {
|
|
18406
18565
|
process.env.TOC_OCRA_NODE_PATH = candidate;
|
|
18407
18566
|
return;
|
|
@@ -4,6 +4,7 @@ import type { ChatContentEvent } from '../providers/provider.js';
|
|
|
4
4
|
import { type TerminalTailRead } from '../session/terminal-tail-buffer.js';
|
|
5
5
|
import type { RunFinishedReport, RunStartPayload, RunStartedReport } from '../types/protocol.js';
|
|
6
6
|
import { OrphanRegistry } from './orphan-registry.js';
|
|
7
|
+
import { type TaskFileDownload } from './task-attachments.js';
|
|
7
8
|
/**
|
|
8
9
|
* Supervises headless CLI turns. One process per turn:
|
|
9
10
|
*
|
|
@@ -17,6 +18,7 @@ import { OrphanRegistry } from './orphan-registry.js';
|
|
|
17
18
|
*/
|
|
18
19
|
export declare const COMPLETION_EXIT_GRACE_MS = 15000;
|
|
19
20
|
export type RunReporter = {
|
|
21
|
+
taskFile?: TaskFileDownload;
|
|
20
22
|
chatEvents?(runId: string, events: Array<ChatContentEvent & {
|
|
21
23
|
sequence: number;
|
|
22
24
|
}>): Promise<void>;
|
|
@@ -38,6 +40,7 @@ export declare class RunSupervisor {
|
|
|
38
40
|
/** Run ids whose finish report has not been acknowledged yet. */
|
|
39
41
|
private readonly unreported;
|
|
40
42
|
private readonly chatRunIds;
|
|
43
|
+
private readonly preparing;
|
|
41
44
|
constructor(config: AgentConfig, logger: DiagnosticLogger, reporter: RunReporter, orphans?: OrphanRegistry | undefined);
|
|
42
45
|
get activeCount(): number;
|
|
43
46
|
/** Runs to list in the heartbeat: still running, or finished but not yet reported. */
|
|
@@ -56,6 +59,7 @@ export declare class RunSupervisor {
|
|
|
56
59
|
* directory, unknown CLI) are reported as a failed run, never thrown.
|
|
57
60
|
*/
|
|
58
61
|
start(payload: RunStartPayload): Promise<void>;
|
|
62
|
+
private startPrepared;
|
|
59
63
|
private onStdout;
|
|
60
64
|
private onExit;
|
|
61
65
|
private queueChatEvent;
|
|
@@ -72,4 +76,5 @@ export declare class RunSupervisor {
|
|
|
72
76
|
private remember;
|
|
73
77
|
private defaultChatWorkdir;
|
|
74
78
|
validateWorkdir(input: string | undefined): Promise<string>;
|
|
79
|
+
prepareTaskFiles(payload: RunStartPayload, signal?: AbortSignal, workdir?: string): Promise<string>;
|
|
75
80
|
}
|