toc-ocra 0.2.8 → 0.2.10
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 +14 -0
- package/dist/service/agent.mjs +211 -48
- package/dist/src/index.js +29 -9
- package/dist/src/index.js.map +1 -1
- package/dist/src/runs/run-supervisor.d.ts +5 -0
- package/dist/src/runs/run-supervisor.js +54 -9
- 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/runtime/agent-service.js +1 -1
- package/dist/src/runtime/agent-service.js.map +1 -1
- package/dist/src/session/terminal-line-sanitizer.d.ts +2 -0
- package/dist/src/session/terminal-line-sanitizer.js +10 -0
- package/dist/src/session/terminal-line-sanitizer.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,20 @@ 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
|
+
|
|
84
|
+
`toc-ocra code` 会同时显示正在运行的后台服务版本与当前安装版本。显示连接码只说明可连接本机插件服务;更新 npm 安装后,旧后台进程不会自动加载新代码。请在任务结束后执行 `toc-ocra service:install` 重启,或使用 `toc-ocra update` 完成升级与重启。重启会停止本机正在运行的任务。
|
|
85
|
+
|
|
86
|
+
`toc-ocra update` 跟随当前命令实际所在的 npm 全局安装目录,避免 nvm 与 Homebrew 的 npm 前缀不同导致更新了另一份安装。源码目录运行时请更新源码、构建并重新安装服务。
|
|
87
|
+
|
|
88
|
+
CLI 聊天的流式文字保留空格、缩进和换行;内容清理使用独立于终端日志行的处理,避免把仅含空白的片段误变成空数据或破坏 Markdown 排版。
|
|
89
|
+
|
|
76
90
|
详见 [docs/architecture.md](docs/architecture.md)。
|
|
77
91
|
|
|
78
92
|
## 开发
|
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,17 +16717,21 @@ 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
|
|
16725
16725
|
var TERMINAL_ESCAPE = /\u001b(?:\][^\u0007]*?(?:\u0007|\u001b\\)|\[[0-?]*[ -/]*[@-~]|[@-Z\\-_])/g;
|
|
16726
16726
|
var CONTROL_CHARACTER = /[\u0000-\u0008\u000b-\u001f\u007f]/g;
|
|
16727
|
+
var CHAT_CONTROL_CHARACTER = /[\u0000-\u0008\u000b\u000c\u000e-\u001f\u007f]/g;
|
|
16727
16728
|
var SENSITIVE_VALUE = /\b(?:sk|rk|pk|ghp|github_pat)_[A-Za-z0-9._-]+|\bBearer\s+\S+|\b(?:api[_-]?key|token|authorization|x-mcp-token)\s*[:=]\s*\S[^\r\n]*/gi;
|
|
16728
16729
|
function sanitizeTerminalLine(line) {
|
|
16729
16730
|
return line.replace(TERMINAL_ESCAPE, "").replace(CONTROL_CHARACTER, "").replace(SENSITIVE_VALUE, "[redacted]").replace(/\s+$/, "");
|
|
16730
16731
|
}
|
|
16732
|
+
function sanitizeChatText(text) {
|
|
16733
|
+
return text.replace(TERMINAL_ESCAPE, "").replace(CHAT_CONTROL_CHARACTER, "").replace(SENSITIVE_VALUE, "[redacted]");
|
|
16734
|
+
}
|
|
16731
16735
|
|
|
16732
16736
|
// src/session/terminal-tail-buffer.ts
|
|
16733
16737
|
var MAX_TAIL_LINES = 2e3;
|
|
@@ -16837,6 +16841,88 @@ function clipToCharacterBudget(rows, budget) {
|
|
|
16837
16841
|
return { lines: rows, limited: false };
|
|
16838
16842
|
}
|
|
16839
16843
|
|
|
16844
|
+
// src/runs/task-attachments.ts
|
|
16845
|
+
import { createHash as createHash2, randomUUID as randomUUID2 } from "node:crypto";
|
|
16846
|
+
import { constants as constants2 } from "node:fs";
|
|
16847
|
+
import { lstat, mkdir as mkdir4, open, realpath, rename as rename4, rm as rm2, writeFile as writeFile4 } from "node:fs/promises";
|
|
16848
|
+
import { basename as basename2, isAbsolute as isAbsolute2, join as join6, relative } from "node:path";
|
|
16849
|
+
function within(root, path) {
|
|
16850
|
+
const segment = relative(root, path);
|
|
16851
|
+
return segment === "" || !segment.startsWith("..") && !isAbsolute2(segment);
|
|
16852
|
+
}
|
|
16853
|
+
async function attachmentDirectory(workdir, messageId) {
|
|
16854
|
+
let directory = workdir;
|
|
16855
|
+
for (const segment of [".echo", "attachments", `message-${encodeURIComponent(messageId).slice(0, 180)}`]) {
|
|
16856
|
+
directory = join6(directory, segment);
|
|
16857
|
+
await mkdir4(directory, { mode: 448 }).catch((error51) => {
|
|
16858
|
+
if (error51.code !== "EEXIST") throw error51;
|
|
16859
|
+
});
|
|
16860
|
+
const metadata = await lstat(directory);
|
|
16861
|
+
if (metadata.isSymbolicLink() || !metadata.isDirectory() || !within(workdir, await realpath(directory))) {
|
|
16862
|
+
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");
|
|
16863
|
+
}
|
|
16864
|
+
if (segment === ".echo") await writeFile4(join6(directory, ".gitignore"), "*\n", { flag: "wx", mode: 384 }).catch((error51) => {
|
|
16865
|
+
if (error51.code !== "EEXIST") throw error51;
|
|
16866
|
+
});
|
|
16867
|
+
}
|
|
16868
|
+
return directory;
|
|
16869
|
+
}
|
|
16870
|
+
async function matches(path, size, sha256, signal2) {
|
|
16871
|
+
const metadata = await lstat(path).catch(() => void 0);
|
|
16872
|
+
if (!metadata?.isFile() || metadata.isSymbolicLink() || metadata.size !== size) return false;
|
|
16873
|
+
const file2 = await open(path, constants2.O_RDONLY | (constants2.O_NOFOLLOW ?? 0));
|
|
16874
|
+
try {
|
|
16875
|
+
const hash2 = createHash2("sha256");
|
|
16876
|
+
let read = 0;
|
|
16877
|
+
for await (const chunk of file2.createReadStream({ autoClose: false })) {
|
|
16878
|
+
signal2.throwIfAborted();
|
|
16879
|
+
read += chunk.length;
|
|
16880
|
+
if (read > size) return false;
|
|
16881
|
+
hash2.update(chunk);
|
|
16882
|
+
}
|
|
16883
|
+
return hash2.digest("hex") === sha256;
|
|
16884
|
+
} finally {
|
|
16885
|
+
await file2.close();
|
|
16886
|
+
}
|
|
16887
|
+
}
|
|
16888
|
+
async function prepareTaskAttachments(payload, workdir, download, signal2) {
|
|
16889
|
+
const files = payload.task_attachments ?? [];
|
|
16890
|
+
if (!files.length) return payload.prompt;
|
|
16891
|
+
const directory = await attachmentDirectory(workdir, payload.message_id || payload.run_id);
|
|
16892
|
+
let prompt = payload.prompt;
|
|
16893
|
+
const paths = [];
|
|
16894
|
+
for (const file2 of files) {
|
|
16895
|
+
signal2.throwIfAborted();
|
|
16896
|
+
const token = `{{echo-file:${file2.file_id}}}`;
|
|
16897
|
+
if (file2.prompt_token !== token) throw new Error("\u9644\u4EF6\u8DEF\u5F84\u5360\u4F4D\u7B26\u65E0\u6548\uFF0C\u8BF7\u66F4\u65B0\u670D\u52A1\u5668");
|
|
16898
|
+
const characters = Array.from(basename2(file2.name.replace(/\\/g, "/")).replace(/[^\p{L}\p{N}._-]/gu, "_").replace(/\.+$/u, ""));
|
|
16899
|
+
while (Buffer.byteLength(characters.join("")) > 160) characters.shift();
|
|
16900
|
+
const name = characters.join("") || "attachment";
|
|
16901
|
+
const path = join6(directory, `${file2.file_id}-${name}`);
|
|
16902
|
+
if (!await matches(path, file2.size, file2.sha256, signal2)) {
|
|
16903
|
+
const temporary = join6(directory, `.download-${file2.file_id}-${randomUUID2()}.part`);
|
|
16904
|
+
try {
|
|
16905
|
+
await download(payload.run_id, file2.file_id, temporary, file2.size, file2.sha256, signal2);
|
|
16906
|
+
signal2.throwIfAborted();
|
|
16907
|
+
if (!within(workdir, await realpath(directory))) throw new Error("\u9644\u4EF6\u76EE\u5F55\u5DF2\u6539\u53D8\uFF0C\u8BF7\u91CD\u8BD5");
|
|
16908
|
+
await rename4(temporary, path);
|
|
16909
|
+
} finally {
|
|
16910
|
+
await rm2(temporary, { force: true });
|
|
16911
|
+
}
|
|
16912
|
+
}
|
|
16913
|
+
if (!within(workdir, await realpath(path))) throw new Error("\u9644\u4EF6\u8DEF\u5F84\u4E0D\u5728 CLI \u5DE5\u4F5C\u76EE\u5F55\u5185");
|
|
16914
|
+
prompt = prompt.split(token).join(path);
|
|
16915
|
+
paths.push(`- ${JSON.stringify(path)}\uFF08${file2.mime}\uFF0C${file2.size} \u5B57\u8282\uFF09`);
|
|
16916
|
+
}
|
|
16917
|
+
if (prompt.includes("{{echo-file:")) throw new Error("\u9644\u4EF6\u8DEF\u5F84\u672A\u5B8C\u6574\u89E3\u6790");
|
|
16918
|
+
prompt += `
|
|
16919
|
+
|
|
16920
|
+
\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
|
|
16921
|
+
${paths.join("\n")}`;
|
|
16922
|
+
if (Buffer.byteLength(prompt) > 512 * 1024) throw new Error("\u5305\u542B\u9644\u4EF6\u8DEF\u5F84\u7684\u6D88\u606F\u8FC7\u957F");
|
|
16923
|
+
return prompt;
|
|
16924
|
+
}
|
|
16925
|
+
|
|
16840
16926
|
// src/runs/run-supervisor.ts
|
|
16841
16927
|
var COMPLETION_EXIT_GRACE_MS = 15e3;
|
|
16842
16928
|
var RECENT_RUN_TTL_MS = 10 * 6e4;
|
|
@@ -16858,12 +16944,13 @@ var RunSupervisor = class {
|
|
|
16858
16944
|
/** Run ids whose finish report has not been acknowledged yet. */
|
|
16859
16945
|
unreported = /* @__PURE__ */ new Set();
|
|
16860
16946
|
chatRunIds = /* @__PURE__ */ new Set();
|
|
16947
|
+
preparing = /* @__PURE__ */ new Map();
|
|
16861
16948
|
get activeCount() {
|
|
16862
|
-
return this.active.size;
|
|
16949
|
+
return this.active.size + this.preparing.size;
|
|
16863
16950
|
}
|
|
16864
16951
|
/** Runs to list in the heartbeat: still running, or finished but not yet reported. */
|
|
16865
16952
|
activeRunIds() {
|
|
16866
|
-
return [.../* @__PURE__ */ new Set([...this.active.keys(), ...this.unreported])];
|
|
16953
|
+
return [.../* @__PURE__ */ new Set([...this.active.keys(), ...this.preparing.keys(), ...this.unreported])];
|
|
16867
16954
|
}
|
|
16868
16955
|
activeChatRunIds() {
|
|
16869
16956
|
return this.activeRunIds().filter((id) => this.chatRunIds.has(id));
|
|
@@ -16885,6 +16972,16 @@ var RunSupervisor = class {
|
|
|
16885
16972
|
* directory, unknown CLI) are reported as a failed run, never thrown.
|
|
16886
16973
|
*/
|
|
16887
16974
|
async start(payload) {
|
|
16975
|
+
if (this.preparing.has(payload.run_id)) return;
|
|
16976
|
+
const controller = new AbortController();
|
|
16977
|
+
this.preparing.set(payload.run_id, { workstationId: payload.workstation_id, controller });
|
|
16978
|
+
try {
|
|
16979
|
+
await this.startPrepared(payload, controller.signal);
|
|
16980
|
+
} finally {
|
|
16981
|
+
this.preparing.delete(payload.run_id);
|
|
16982
|
+
}
|
|
16983
|
+
}
|
|
16984
|
+
async startPrepared(payload, signal2) {
|
|
16888
16985
|
if (this.active.has(payload.run_id) || this.recent.has(payload.run_id)) {
|
|
16889
16986
|
this.logger.info("run-duplicate-ignored", { runId: payload.run_id });
|
|
16890
16987
|
return;
|
|
@@ -16895,20 +16992,32 @@ var RunSupervisor = class {
|
|
|
16895
16992
|
this.remember({ runId: payload.run_id, workstationId: payload.workstation_id, tail: new TerminalTailBuffer(), finishedAt: Date.now(), status: "failed", chat: !!payload.chat_session_id });
|
|
16896
16993
|
await this.report(payload.run_id, { status: "failed", error: error51, duration_ms: 0 });
|
|
16897
16994
|
};
|
|
16898
|
-
if ([...this.active.values()].some((run2) => run2.workstationId === payload.workstation_id)) {
|
|
16995
|
+
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
16996
|
return refuse("\u672C\u673A\u8BE5\u5DE5\u4F4D\u5DF2\u6709\u4EFB\u52A1\u5728\u8FD0\u884C");
|
|
16900
16997
|
}
|
|
16901
|
-
if (this.active.size
|
|
16998
|
+
if (this.active.size + this.preparing.size > this.config.maxConcurrentRuns) {
|
|
16902
16999
|
return refuse(`\u672C\u673A\u540C\u65F6\u8FD0\u884C\u7684\u4EFB\u52A1\u5DF2\u8FBE\u4E0A\u9650\uFF08${this.config.maxConcurrentRuns}\uFF09`);
|
|
16903
17000
|
}
|
|
16904
17001
|
let provider;
|
|
16905
17002
|
let workdir;
|
|
16906
17003
|
try {
|
|
16907
17004
|
provider = resolveHeadlessProvider(payload.agent_type);
|
|
17005
|
+
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
17006
|
workdir = payload.chat_session_id && !payload.workdir ? await this.defaultChatWorkdir() : await this.validateWorkdir(payload.workdir || this.config.allowedRoots[0]);
|
|
16909
17007
|
} catch (error51) {
|
|
16910
17008
|
return refuse(errorMessage(error51));
|
|
16911
17009
|
}
|
|
17010
|
+
try {
|
|
17011
|
+
if (payload.task_attachments?.length) payload = { ...payload, prompt: await this.prepareTaskFiles(payload, signal2, workdir) };
|
|
17012
|
+
signal2.throwIfAborted();
|
|
17013
|
+
} catch (error51) {
|
|
17014
|
+
if (signal2.aborted) {
|
|
17015
|
+
this.remember({ runId: payload.run_id, workstationId: payload.workstation_id, tail: new TerminalTailBuffer(), finishedAt: Date.now(), status: "cancelled", chat: !!payload.chat_session_id });
|
|
17016
|
+
await this.report(payload.run_id, { status: "cancelled", error: "\u9644\u4EF6\u51C6\u5907\u5DF2\u53D6\u6D88", duration_ms: 0 });
|
|
17017
|
+
return;
|
|
17018
|
+
}
|
|
17019
|
+
return refuse(`\u9644\u4EF6\u4E0B\u8F7D\u5931\u8D25\uFF1A${errorMessage(error51)}`);
|
|
17020
|
+
}
|
|
16912
17021
|
const request = {
|
|
16913
17022
|
prompt: payload.prompt,
|
|
16914
17023
|
sessionId: payload.session_id,
|
|
@@ -16927,7 +17036,7 @@ var RunSupervisor = class {
|
|
|
16927
17036
|
}
|
|
16928
17037
|
if (!payload.prompt && !payload.attachments?.length) return refuse("\u6D88\u606F\u5185\u5BB9\u4E3A\u7A7A");
|
|
16929
17038
|
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
|
|
17039
|
+
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
17040
|
const launch = provider.buildLaunch({
|
|
16932
17041
|
prompt: payload.prompt,
|
|
16933
17042
|
sessionId: payload.session_id,
|
|
@@ -16976,6 +17085,7 @@ var RunSupervisor = class {
|
|
|
16976
17085
|
timers: []
|
|
16977
17086
|
};
|
|
16978
17087
|
this.active.set(run.runId, run);
|
|
17088
|
+
this.preparing.delete(run.runId);
|
|
16979
17089
|
this.unreported.add(run.runId);
|
|
16980
17090
|
if (run.pid > 1) this.orphans?.add({ runId: run.runId, pid: run.pid, executable: launch.executable, startedAt: run.startedAt });
|
|
16981
17091
|
run.tail.append(`\u25CF ${provider.id} \u542F\u52A8\u4E8E ${workdir}${payload.session_id ? `\uFF08\u7EE7\u7EED\u4F1A\u8BDD ${payload.session_id}\uFF09` : ""}
|
|
@@ -16995,7 +17105,7 @@ var RunSupervisor = class {
|
|
|
16995
17105
|
run.tail.append(`${clean}
|
|
16996
17106
|
`);
|
|
16997
17107
|
});
|
|
16998
|
-
child.once("close", (code,
|
|
17108
|
+
child.once("close", (code, signal3) => void this.onExit(run, code, signal3, spawnError));
|
|
16999
17109
|
if (provider.startInput) {
|
|
17000
17110
|
child.once("spawn", () => {
|
|
17001
17111
|
try {
|
|
@@ -17130,16 +17240,16 @@ var RunSupervisor = class {
|
|
|
17130
17240
|
async prepareChatAttachments(payload) {
|
|
17131
17241
|
if (!payload.attachments?.length) return [];
|
|
17132
17242
|
if (!this.reporter.chatMedia) throw new Error("\u9644\u4EF6\u8F6C\u53D1\u672A\u914D\u7F6E");
|
|
17133
|
-
const directory =
|
|
17134
|
-
await
|
|
17243
|
+
const directory = join7(this.config.dataDir, "chat-media", encodeURIComponent(payload.chat_session_id ?? payload.run_id));
|
|
17244
|
+
await mkdir5(directory, { recursive: true, mode: 448 });
|
|
17135
17245
|
const files = [];
|
|
17136
17246
|
for (const attachment of payload.attachments) {
|
|
17137
|
-
const name =
|
|
17138
|
-
const path =
|
|
17247
|
+
const name = basename3(attachment.name).replace(/[^\p{L}\p{N}._-]/gu, "_").slice(-120) || "attachment";
|
|
17248
|
+
const path = join7(directory, `${encodeURIComponent(attachment.media_id)}-${name}`);
|
|
17139
17249
|
const existing = await stat2(path).catch(() => void 0);
|
|
17140
17250
|
if (!existing?.isFile()) {
|
|
17141
17251
|
const data = await this.reporter.chatMedia(payload.run_id, attachment.media_id);
|
|
17142
|
-
await
|
|
17252
|
+
await writeFile5(path, data, { mode: 384 });
|
|
17143
17253
|
}
|
|
17144
17254
|
files.push({ path, name: attachment.name, kind: attachment.kind });
|
|
17145
17255
|
}
|
|
@@ -17152,7 +17262,14 @@ var RunSupervisor = class {
|
|
|
17152
17262
|
/** Stops a run: the whole process tree, then a `cancelled` report. */
|
|
17153
17263
|
async cancel(runId) {
|
|
17154
17264
|
const run = this.active.get(runId);
|
|
17155
|
-
if (!run)
|
|
17265
|
+
if (!run) {
|
|
17266
|
+
const preparing = this.preparing.get(runId);
|
|
17267
|
+
if (preparing) {
|
|
17268
|
+
preparing.controller.abort();
|
|
17269
|
+
return true;
|
|
17270
|
+
}
|
|
17271
|
+
return false;
|
|
17272
|
+
}
|
|
17156
17273
|
run.cancelRequested = true;
|
|
17157
17274
|
run.tail.append("\u25CF \u6536\u5230\u505C\u6B62\u6307\u4EE4\uFF0C\u6B63\u5728\u7ED3\u675F\u8FDB\u7A0B\u6811\n");
|
|
17158
17275
|
this.logger.info("run-cancel", { runId, pid: run.pid });
|
|
@@ -17175,6 +17292,7 @@ var RunSupervisor = class {
|
|
|
17175
17292
|
return { ...read, status: this.active.has(target.runId) ? "running" : "finished", run_id: target.runId };
|
|
17176
17293
|
}
|
|
17177
17294
|
async disposeAll() {
|
|
17295
|
+
for (const entry of this.preparing.values()) entry.controller.abort();
|
|
17178
17296
|
await Promise.all([...this.active.values()].map((run) => {
|
|
17179
17297
|
run.cancelRequested = true;
|
|
17180
17298
|
return killProcessTree(run.pid, 1e3);
|
|
@@ -17200,24 +17318,29 @@ var RunSupervisor = class {
|
|
|
17200
17318
|
if (!input) throw new Error("\u6CA1\u6709\u914D\u7F6E\u5141\u8BB8\u7684\u5DE5\u4F5C\u76EE\u5F55");
|
|
17201
17319
|
let candidate;
|
|
17202
17320
|
try {
|
|
17203
|
-
candidate = await
|
|
17321
|
+
candidate = await realpath2(isAbsolute3(input) ? input : resolve3(input));
|
|
17204
17322
|
} catch {
|
|
17205
17323
|
throw new Error(`\u5DE5\u4F5C\u76EE\u5F55\u4E0D\u5B58\u5728\uFF1A${input}`);
|
|
17206
17324
|
}
|
|
17207
17325
|
for (const root of this.config.allowedRoots) {
|
|
17208
|
-
const allowed = await
|
|
17209
|
-
const segment =
|
|
17210
|
-
if (segment === "" || !segment.startsWith("..") && !
|
|
17326
|
+
const allowed = await realpath2(root).catch(() => resolve3(root));
|
|
17327
|
+
const segment = relative2(allowed, candidate);
|
|
17328
|
+
if (segment === "" || !segment.startsWith("..") && !isAbsolute3(segment)) return candidate;
|
|
17211
17329
|
}
|
|
17212
17330
|
throw new Error(`\u5DE5\u4F5C\u76EE\u5F55\u4E0D\u5728\u5141\u8BB8\u8303\u56F4\u5185\uFF1A${input}`);
|
|
17213
17331
|
}
|
|
17332
|
+
async prepareTaskFiles(payload, signal2 = AbortSignal.timeout(15 * 6e4), workdir) {
|
|
17333
|
+
if (!payload.workdir) throw new Error("\u8BF7\u5148\u914D\u7F6E\u8BBE\u5907\u9ED8\u8BA4\u76EE\u5F55\u6216\u5DE5\u4F4D\u5DE5\u4F5C\u76EE\u5F55");
|
|
17334
|
+
if (!this.reporter.taskFile) throw new Error("\u4EFB\u52A1\u9644\u4EF6\u4E0B\u8F7D\u672A\u914D\u7F6E");
|
|
17335
|
+
return prepareTaskAttachments(payload, workdir ?? await this.validateWorkdir(payload.workdir), this.reporter.taskFile, AbortSignal.any([signal2, AbortSignal.timeout(15 * 6e4)]));
|
|
17336
|
+
}
|
|
17214
17337
|
};
|
|
17215
17338
|
function errorMessage(error51) {
|
|
17216
17339
|
return error51 instanceof Error ? error51.message : String(error51);
|
|
17217
17340
|
}
|
|
17218
17341
|
function sanitizeChatEvent(event) {
|
|
17219
17342
|
const sanitize = (value) => {
|
|
17220
|
-
if (typeof value === "string") return
|
|
17343
|
+
if (typeof value === "string") return sanitizeChatText(value);
|
|
17221
17344
|
if (Array.isArray(value)) return value.map(sanitize);
|
|
17222
17345
|
if (value && typeof value === "object") {
|
|
17223
17346
|
return Object.fromEntries(Object.entries(value).map(([key, item]) => [key, sanitize(item)]));
|
|
@@ -17226,12 +17349,16 @@ function sanitizeChatEvent(event) {
|
|
|
17226
17349
|
};
|
|
17227
17350
|
return {
|
|
17228
17351
|
...event,
|
|
17229
|
-
...event.text !== void 0 ? { text:
|
|
17352
|
+
...event.text !== void 0 ? { text: sanitizeChatText(event.text) } : {},
|
|
17230
17353
|
...event.item ? { item: sanitize(event.item) } : {}
|
|
17231
17354
|
};
|
|
17232
17355
|
}
|
|
17233
17356
|
|
|
17234
17357
|
// src/transport/gateway-client.ts
|
|
17358
|
+
import { createHash as createHash3 } from "node:crypto";
|
|
17359
|
+
import { createWriteStream } from "node:fs";
|
|
17360
|
+
import { Readable, Transform } from "node:stream";
|
|
17361
|
+
import { pipeline } from "node:stream/promises";
|
|
17235
17362
|
var GatewayError = class extends Error {
|
|
17236
17363
|
constructor(message, status) {
|
|
17237
17364
|
super(message);
|
|
@@ -17313,19 +17440,46 @@ var GatewayClient = class {
|
|
|
17313
17440
|
runStarted(runId, body) {
|
|
17314
17441
|
return this.request("POST", `/runs/${encodeURIComponent(runId)}/started`, body, 15e3);
|
|
17315
17442
|
}
|
|
17443
|
+
async taskFile(runId, fileId, destination, size, sha256, signal2) {
|
|
17444
|
+
const response = await fetch(`${this.config.serverUrl}/api/agent/runs/${encodeURIComponent(runId)}/files/${encodeURIComponent(fileId)}`, {
|
|
17445
|
+
headers: { authorization: `Bearer ${this.config.token}` },
|
|
17446
|
+
redirect: "error",
|
|
17447
|
+
signal: AbortSignal.any([signal2, AbortSignal.timeout(15 * 6e4)])
|
|
17448
|
+
});
|
|
17449
|
+
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);
|
|
17450
|
+
const declared = response.headers.get("content-length");
|
|
17451
|
+
if (declared !== null && Number(declared) !== size) {
|
|
17452
|
+
await response.body.cancel();
|
|
17453
|
+
throw new Error("\u9644\u4EF6\u5927\u5C0F\u4E0E\u4FDD\u5B58\u7684\u8BB0\u5F55\u4E0D\u4E00\u81F4");
|
|
17454
|
+
}
|
|
17455
|
+
const hash2 = createHash3("sha256");
|
|
17456
|
+
let received = 0;
|
|
17457
|
+
const verifier = new Transform({ transform(chunk, _encoding, callback) {
|
|
17458
|
+
received += chunk.length;
|
|
17459
|
+
if (received > size) {
|
|
17460
|
+
callback(new Error("\u9644\u4EF6\u4E0B\u8F7D\u8D85\u51FA\u8BB0\u5F55\u5927\u5C0F"));
|
|
17461
|
+
return;
|
|
17462
|
+
}
|
|
17463
|
+
hash2.update(chunk);
|
|
17464
|
+
callback(null, chunk);
|
|
17465
|
+
} });
|
|
17466
|
+
const target = createWriteStream(destination, { flags: "wx", mode: 384 });
|
|
17467
|
+
await pipeline(Readable.fromWeb(response.body), verifier, target, { signal: signal2 });
|
|
17468
|
+
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");
|
|
17469
|
+
}
|
|
17316
17470
|
runFinished(runId, body) {
|
|
17317
17471
|
return this.request("POST", `/runs/${encodeURIComponent(runId)}/finished`, body, 15e3);
|
|
17318
17472
|
}
|
|
17319
17473
|
};
|
|
17320
17474
|
|
|
17321
17475
|
// src/workspace/directory-browser.ts
|
|
17322
|
-
import { readdir, realpath as
|
|
17476
|
+
import { readdir, realpath as realpath3, stat as stat3 } from "node:fs/promises";
|
|
17323
17477
|
import { homedir as homedir5 } from "node:os";
|
|
17324
17478
|
import { dirname as dirname6, parse as parse3, resolve as resolve4 } from "node:path";
|
|
17325
17479
|
var MAX_ENTRIES = 1e3;
|
|
17326
17480
|
async function listDirectory(requested, allowedRoots) {
|
|
17327
17481
|
const raw = requested.trim().replace(/^~(?=$|[\\/])/, homedir5()) || homedir5();
|
|
17328
|
-
const directory = await
|
|
17482
|
+
const directory = await realpath3(resolve4(raw)).catch(() => {
|
|
17329
17483
|
throw new Error(`\u76EE\u5F55\u4E0D\u5B58\u5728\uFF1A${requested}`);
|
|
17330
17484
|
});
|
|
17331
17485
|
if (!(await stat3(directory)).isDirectory()) throw new Error("\u8FD9\u4E0D\u662F\u4E00\u4E2A\u76EE\u5F55");
|
|
@@ -17357,12 +17511,13 @@ async function listDirectory(requested, allowedRoots) {
|
|
|
17357
17511
|
|
|
17358
17512
|
// src/workspace/workspace-manager.ts
|
|
17359
17513
|
import { spawn as spawn8 } from "node:child_process";
|
|
17360
|
-
import { chmod as chmod3, mkdir as
|
|
17514
|
+
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
17515
|
import { homedir as homedir6, tmpdir } from "node:os";
|
|
17362
|
-
import { isAbsolute as
|
|
17516
|
+
import { isAbsolute as isAbsolute4, join as join8, relative as relative3, resolve as resolve5 } from "node:path";
|
|
17363
17517
|
var MAX_NODES = 2e3;
|
|
17364
17518
|
var MAX_DIRECTORY_ENTRIES = 12e3;
|
|
17365
17519
|
var IGNORED_DIRECTORIES = /* @__PURE__ */ new Set([
|
|
17520
|
+
".echo",
|
|
17366
17521
|
".git",
|
|
17367
17522
|
"node_modules",
|
|
17368
17523
|
".next",
|
|
@@ -17408,12 +17563,12 @@ var WorkspaceManager = class {
|
|
|
17408
17563
|
validateRootPath(value) {
|
|
17409
17564
|
const raw = value.trim().replace(/^~(?=$|[\\/])/, homedir6());
|
|
17410
17565
|
const root = resolve5(raw);
|
|
17411
|
-
if (!
|
|
17412
|
-
if (
|
|
17566
|
+
if (!isAbsolute4(raw)) throw new Error("\u6839\u76EE\u5F55\u5FC5\u987B\u662F\u7EDD\u5BF9\u8DEF\u5F84");
|
|
17567
|
+
if (relative3(root, resolve5(root, "..")) === "") throw new Error("\u4E0D\u80FD\u628A\u6587\u4EF6\u7CFB\u7EDF\u6839\u76EE\u5F55\u4F5C\u4E3A\u76EE\u5F55\u6839");
|
|
17413
17568
|
return root;
|
|
17414
17569
|
}
|
|
17415
17570
|
async syncTree(rootPath, request) {
|
|
17416
|
-
await
|
|
17571
|
+
await mkdir6(rootPath, { recursive: true });
|
|
17417
17572
|
await addAllowedRoot(this.config, rootPath);
|
|
17418
17573
|
const results = [];
|
|
17419
17574
|
const requestedPaths = /* @__PURE__ */ new Set();
|
|
@@ -17437,7 +17592,7 @@ var WorkspaceManager = class {
|
|
|
17437
17592
|
});
|
|
17438
17593
|
continue;
|
|
17439
17594
|
}
|
|
17440
|
-
await
|
|
17595
|
+
await mkdir6(target, { recursive: true });
|
|
17441
17596
|
results.push({
|
|
17442
17597
|
relative_path: relativePath,
|
|
17443
17598
|
status: "created",
|
|
@@ -17486,7 +17641,7 @@ var WorkspaceManager = class {
|
|
|
17486
17641
|
}
|
|
17487
17642
|
if (!metadata) {
|
|
17488
17643
|
if (!gitUrl) throw new Error("A missing repository needs a Git remote URL");
|
|
17489
|
-
await
|
|
17644
|
+
await mkdir6(resolve5(target, ".."), { recursive: true });
|
|
17490
17645
|
const cloneArgs = branch ? ["clone", "--branch", branch, "--single-branch", gitUrl, target] : ["clone", gitUrl, target];
|
|
17491
17646
|
await this.runGit(cloneArgs, rootPath, timeoutSeconds, false, gitEnvironment);
|
|
17492
17647
|
created = true;
|
|
@@ -17580,13 +17735,13 @@ var WorkspaceManager = class {
|
|
|
17580
17735
|
};
|
|
17581
17736
|
}
|
|
17582
17737
|
async scanTree(rootPath, maxDepth, scanRoots) {
|
|
17583
|
-
const resolvedRoot = await
|
|
17738
|
+
const resolvedRoot = await realpath4(rootPath);
|
|
17584
17739
|
const rootMetadata = await stat4(resolvedRoot);
|
|
17585
17740
|
if (!rootMetadata.isDirectory()) throw new Error("The workspace root is not a directory");
|
|
17586
17741
|
const nodes = [];
|
|
17587
17742
|
let entriesRead = 0;
|
|
17588
17743
|
const appendNode = async (target) => {
|
|
17589
|
-
const relativePath =
|
|
17744
|
+
const relativePath = relative3(resolvedRoot, target).replaceAll("\\", "/");
|
|
17590
17745
|
if (!relativePath || !this.isSafeRelativePath(relativePath)) return void 0;
|
|
17591
17746
|
const git = await this.readGitMetadata(target);
|
|
17592
17747
|
if (git) {
|
|
@@ -17670,8 +17825,8 @@ var WorkspaceManager = class {
|
|
|
17670
17825
|
if (result.exitCode !== 0 || !topLevel) return false;
|
|
17671
17826
|
try {
|
|
17672
17827
|
const [resolvedDirectory, resolvedTopLevel] = await Promise.all([
|
|
17673
|
-
|
|
17674
|
-
|
|
17828
|
+
realpath4(directory),
|
|
17829
|
+
realpath4(topLevel)
|
|
17675
17830
|
]);
|
|
17676
17831
|
return resolvedDirectory === resolvedTopLevel;
|
|
17677
17832
|
} catch {
|
|
@@ -17691,8 +17846,8 @@ var WorkspaceManager = class {
|
|
|
17691
17846
|
}
|
|
17692
17847
|
resolveWithinRoot(rootPath, relativePath) {
|
|
17693
17848
|
const target = resolve5(rootPath, relativePath);
|
|
17694
|
-
const relativeToRoot =
|
|
17695
|
-
if (relativeToRoot.startsWith("..") ||
|
|
17849
|
+
const relativeToRoot = relative3(rootPath, target);
|
|
17850
|
+
if (relativeToRoot.startsWith("..") || isAbsolute4(relativeToRoot)) {
|
|
17696
17851
|
throw new Error("A workspace path must remain under the configured workspace root");
|
|
17697
17852
|
}
|
|
17698
17853
|
return target;
|
|
@@ -17733,13 +17888,13 @@ var WorkspaceManager = class {
|
|
|
17733
17888
|
}
|
|
17734
17889
|
async createGitAuthEnvironment(auth) {
|
|
17735
17890
|
if (!auth) return void 0;
|
|
17736
|
-
const directory = await mkdtemp(
|
|
17891
|
+
const directory = await mkdtemp(join8(tmpdir(), "toc-ocra-git-auth-"));
|
|
17737
17892
|
const isWindows = process.platform === "win32";
|
|
17738
|
-
const scriptPath =
|
|
17893
|
+
const scriptPath = join8(directory, isWindows ? "askpass.cmd" : "askpass.sh");
|
|
17739
17894
|
let script;
|
|
17740
17895
|
if (isWindows) {
|
|
17741
|
-
const helperPath =
|
|
17742
|
-
await
|
|
17896
|
+
const helperPath = join8(directory, "askpass.cjs");
|
|
17897
|
+
await writeFile6(
|
|
17743
17898
|
helperPath,
|
|
17744
17899
|
'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
17900
|
{ mode: 448 }
|
|
@@ -17748,7 +17903,7 @@ var WorkspaceManager = class {
|
|
|
17748
17903
|
} else {
|
|
17749
17904
|
script = '#!/bin/sh\ncase "$1" in *Username*|*username*) printf "%s\\n" "$ECHO_GIT_USERNAME" ;; *) printf "%s\\n" "$ECHO_GIT_PASSWORD" ;; esac\n';
|
|
17750
17905
|
}
|
|
17751
|
-
await
|
|
17906
|
+
await writeFile6(scriptPath, script, { mode: 448 });
|
|
17752
17907
|
if (!isWindows) await chmod3(scriptPath, 448);
|
|
17753
17908
|
return {
|
|
17754
17909
|
env: {
|
|
@@ -17759,7 +17914,7 @@ var WorkspaceManager = class {
|
|
|
17759
17914
|
ECHO_GIT_USERNAME: auth.username,
|
|
17760
17915
|
ECHO_GIT_PASSWORD: auth.password
|
|
17761
17916
|
},
|
|
17762
|
-
cleanup: () =>
|
|
17917
|
+
cleanup: () => rm3(directory, { recursive: true, force: true })
|
|
17763
17918
|
};
|
|
17764
17919
|
}
|
|
17765
17920
|
message(error51) {
|
|
@@ -17784,6 +17939,7 @@ var RunStartPayloadSchema = external_exports.object({
|
|
|
17784
17939
|
prompt: external_exports.string().max(512 * 1024),
|
|
17785
17940
|
chat_session_id: external_exports.string().max(64).optional(),
|
|
17786
17941
|
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(),
|
|
17942
|
+
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
17943
|
session_id: external_exports.string().max(200).optional().default(""),
|
|
17788
17944
|
model: external_exports.string().max(100).optional().default(""),
|
|
17789
17945
|
reasoning_effort: external_exports.string().max(40).optional().default(""),
|
|
@@ -17854,13 +18010,14 @@ var AgentRuntime = class {
|
|
|
17854
18010
|
await this.retry(() => this.client.chatEvents(runId, events), 5);
|
|
17855
18011
|
},
|
|
17856
18012
|
chatMedia: (runId, mediaId) => this.client.chatMedia(runId, mediaId),
|
|
18013
|
+
taskFile: (runId, fileId, destination, size, sha256, signal2) => this.client.taskFile(runId, fileId, destination, size, sha256, signal2),
|
|
17857
18014
|
finished: async (runId, report) => {
|
|
17858
18015
|
this.pendingReports.set(runId, report);
|
|
17859
18016
|
await this.flushReport(runId);
|
|
17860
18017
|
}
|
|
17861
18018
|
};
|
|
17862
18019
|
this.workspaces = new WorkspaceManager(config2, this.logger);
|
|
17863
|
-
this.orphans = new OrphanRegistry(
|
|
18020
|
+
this.orphans = new OrphanRegistry(join9(config2.dataDir, chatOnly ? "chat-active-runs.json" : "active-runs.json"));
|
|
17864
18021
|
this.runs = new RunSupervisor(config2, this.logger, reporter, this.orphans);
|
|
17865
18022
|
}
|
|
17866
18023
|
config;
|
|
@@ -17873,7 +18030,7 @@ var AgentRuntime = class {
|
|
|
17873
18030
|
workspaces;
|
|
17874
18031
|
stopController = new AbortController();
|
|
17875
18032
|
startedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
17876
|
-
chatRunnerId =
|
|
18033
|
+
chatRunnerId = randomUUID3();
|
|
17877
18034
|
chatReady = false;
|
|
17878
18035
|
chatLeaseInitialized = false;
|
|
17879
18036
|
chatError;
|
|
@@ -18014,6 +18171,7 @@ var AgentRuntime = class {
|
|
|
18014
18171
|
return {
|
|
18015
18172
|
providers: snapshot.providers.map((p) => ({ id: p.id, installed: p.installed, runnable: p.available, version: p.version ?? "", path: p.resolvedPath ?? "" })),
|
|
18016
18173
|
cli_chat: { version: 1, providers: CHAT_PROVIDERS.filter((id) => snapshot.detectedIds.includes(id)) },
|
|
18174
|
+
task_attachments: { version: 1 },
|
|
18017
18175
|
detected_ids: snapshot.detectedIds,
|
|
18018
18176
|
refreshed_at: snapshot.refreshedAt
|
|
18019
18177
|
};
|
|
@@ -18067,6 +18225,11 @@ var AgentRuntime = class {
|
|
|
18067
18225
|
}
|
|
18068
18226
|
async execute(command) {
|
|
18069
18227
|
switch (command.type) {
|
|
18228
|
+
case "run.files.prepare": {
|
|
18229
|
+
const payload = RunStartPayloadSchema.parse(command.payload);
|
|
18230
|
+
const prompt = await this.runs.prepareTaskFiles(payload);
|
|
18231
|
+
return { success: true, result: { prompt } };
|
|
18232
|
+
}
|
|
18070
18233
|
case "chat.run.start":
|
|
18071
18234
|
case "run.start": {
|
|
18072
18235
|
const payload = RunStartPayloadSchema.parse(command.payload);
|
|
@@ -18298,7 +18461,7 @@ var AgentService = class {
|
|
|
18298
18461
|
}
|
|
18299
18462
|
controlApi() {
|
|
18300
18463
|
return {
|
|
18301
|
-
info: () => ({ name: "toc-ocra", version: this.version, protocol: CONTROL_PROTOCOL_VERSION, paired: Boolean(this.config.token) }),
|
|
18464
|
+
info: () => ({ name: "toc-ocra", version: this.version, protocol: CONTROL_PROTOCOL_VERSION, paired: Boolean(this.config.token), capabilities: { task_attachments: { version: 1 } } }),
|
|
18302
18465
|
pair: (code) => this.pairing.pair(code),
|
|
18303
18466
|
authorize: (header) => this.pairing.authorize(header),
|
|
18304
18467
|
status: () => this.status(),
|
|
@@ -18401,7 +18564,7 @@ function preferSystemNode() {
|
|
|
18401
18564
|
const names = process.platform === "win32" ? ["node.exe"] : ["node"];
|
|
18402
18565
|
for (const directory of (process.env.PATH ?? "").split(delimiter5)) {
|
|
18403
18566
|
for (const name of names) {
|
|
18404
|
-
const candidate = directory &&
|
|
18567
|
+
const candidate = directory && join10(directory, name);
|
|
18405
18568
|
if (candidate && existsSync6(candidate)) {
|
|
18406
18569
|
process.env.TOC_OCRA_NODE_PATH = candidate;
|
|
18407
18570
|
return;
|
package/dist/src/index.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { spawnSync } from 'node:child_process';
|
|
3
3
|
import { existsSync } from 'node:fs';
|
|
4
|
-
import { dirname, resolve } from 'node:path';
|
|
4
|
+
import { basename, dirname, resolve } from 'node:path';
|
|
5
5
|
import { fileURLToPath } from 'node:url';
|
|
6
6
|
import { defaultConfigPath, enrollDevice, loadAgentConfig, loadBaseConfig, redactedConfig } from './config.js';
|
|
7
7
|
import { readConnectionCode } from './control/pairing.js';
|
|
@@ -28,7 +28,7 @@ async function doctor() {
|
|
|
28
28
|
process.exitCode = 1;
|
|
29
29
|
}
|
|
30
30
|
/** Prints the connection code of the running service, for the plugin. */
|
|
31
|
-
function code() {
|
|
31
|
+
async function code() {
|
|
32
32
|
const config = loadBaseConfig();
|
|
33
33
|
const current = readConnectionCode(config.dataDir);
|
|
34
34
|
if (!current) {
|
|
@@ -37,6 +37,23 @@ function code() {
|
|
|
37
37
|
return;
|
|
38
38
|
}
|
|
39
39
|
process.stdout.write(`插件连接码:${current}\n本机服务:http://${config.healthHost}:${config.healthPort}\n`);
|
|
40
|
+
try {
|
|
41
|
+
const response = await fetch(`http://${config.healthHost}:${config.healthPort}/control/info`, { signal: AbortSignal.timeout(3_000), redirect: 'error' });
|
|
42
|
+
if (!response.ok)
|
|
43
|
+
throw new Error(`HTTP ${response.status}`);
|
|
44
|
+
const info = await response.json();
|
|
45
|
+
if (info.name !== PACKAGE_NAME || typeof info.version !== 'string')
|
|
46
|
+
throw new Error('本机服务信息无效');
|
|
47
|
+
const installed = agentVersion();
|
|
48
|
+
process.stdout.write(`当前运行服务版本:${info.version}\n命令行安装版本:${installed}\n`);
|
|
49
|
+
if (info.version !== installed)
|
|
50
|
+
process.stdout.write('后台服务仍在运行另一版本。请在本机任务结束后执行 toc-ocra service:install 重启服务。\n');
|
|
51
|
+
if ((info.capabilities?.task_attachments?.version ?? 0) < 1)
|
|
52
|
+
process.stdout.write('当前服务未声明任务附件下载能力;若发送附件被拒绝,请执行 toc-ocra update 升级并重启服务。\n');
|
|
53
|
+
}
|
|
54
|
+
catch {
|
|
55
|
+
process.stderr.write('未能读取当前运行服务版本;连接码不代表设备已连接到后端,请检查本机服务状态。\n');
|
|
56
|
+
}
|
|
40
57
|
}
|
|
41
58
|
async function detect() {
|
|
42
59
|
const hydration = await hydrateShellPath();
|
|
@@ -89,8 +106,16 @@ function manageService(action, args) {
|
|
|
89
106
|
return true;
|
|
90
107
|
}
|
|
91
108
|
function update(args) {
|
|
109
|
+
const packageRoot = resolve(dirname(fileURLToPath(import.meta.url)), '../..');
|
|
110
|
+
const modules = dirname(packageRoot);
|
|
111
|
+
if (basename(modules) !== 'node_modules' || (process.platform !== 'win32' && basename(dirname(modules)) !== 'lib')) {
|
|
112
|
+
throw new Error('当前从源码或非 npm 全局目录运行,请先更新这份源码并构建,再重新安装对应后台服务');
|
|
113
|
+
}
|
|
114
|
+
// npm on PATH can belong to a different Node installation (e.g. Homebrew
|
|
115
|
+
// versus nvm). Update the package that this command actually loaded.
|
|
116
|
+
const prefix = process.platform === 'win32' ? dirname(modules) : dirname(dirname(modules));
|
|
92
117
|
const npmCommand = process.platform === 'win32' ? 'npm.cmd' : 'npm';
|
|
93
|
-
const install = spawnSync(npmCommand, ['install', '-g', `${PACKAGE_NAME}@latest`], { stdio: 'inherit', shell: false });
|
|
118
|
+
const install = spawnSync(npmCommand, ['install', '-g', '--prefix', prefix, `${PACKAGE_NAME}@latest`], { stdio: 'inherit', shell: false });
|
|
94
119
|
if (install.error)
|
|
95
120
|
throw install.error;
|
|
96
121
|
if (install.status !== 0) {
|
|
@@ -132,12 +157,7 @@ else if (command === 'enroll')
|
|
|
132
157
|
else if (command === 'setup')
|
|
133
158
|
void setup(rest).catch(fail('安装失败'));
|
|
134
159
|
else if (command === 'code') {
|
|
135
|
-
|
|
136
|
-
code();
|
|
137
|
-
}
|
|
138
|
-
catch (error) {
|
|
139
|
-
fail('code failed')(error);
|
|
140
|
-
}
|
|
160
|
+
void code().catch(fail('code failed'));
|
|
141
161
|
}
|
|
142
162
|
else if (command === 'service:install' || command === 'service:uninstall') {
|
|
143
163
|
try {
|