@skylandnpm/octopus-proxy 0.1.34 → 0.1.38
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 +1 -1
- package/dist/{chunk-BIL5ROSQ.js → chunk-6DTK4JVV.js} +382 -180
- package/dist/cli.js +7 -4
- package/dist/main.js +1 -1
- package/native/desktop-macos.m +1 -1
- package/native/screen-unlock-macos.m +127 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -18,7 +18,7 @@ octopus-proxy identity
|
|
|
18
18
|
|
|
19
19
|
Proxy 主动连接 Hub 的 `/ws/proxy-chat`,无需开放入站端口或安装 sshd。Claw 经 `/ws/claw-chat` 建立到 Proxy 的内层 TLS 1.3 会话;Hub 只转发加密字节。首次打开工作区显示主机公钥指纹,点击 Trust 后按 Hub 地址和机器人 ID 固定;之后指纹变化会拒绝连接。每次连接都要输入运行 Proxy 服务账号的主机密码,由主机上的 PAM(Linux)或 Open Directory(macOS)验证。断线或连续 60 分钟没有用户操作后需要重新认证。服务安装时生成主机身份,`octopus-proxy identity` 可在主机上查看指纹。安装主机需提供 OpenSSL 和 C 编译器;Linux 还需 PAM 开发头文件。机器人 Secret 仅用于 Hub 路由,不能单独授权工作区。
|
|
20
20
|
|
|
21
|
-
macOS 12.3 及以上运行 `octopus-proxy onboard` 时,配置保存后、服务启动前会准备截屏辅助程序并申请系统屏幕录制权限;已授权时不会重复弹窗。交互模式和 `--non-interactive` 均执行此步骤,`--no-install-service`
|
|
21
|
+
macOS 12.3 及以上运行 `octopus-proxy onboard` 时,配置保存后、服务启动前会准备截屏辅助程序并申请系统屏幕录制权限;已授权时不会重复弹窗。交互模式和 `--non-interactive` 均执行此步骤,`--no-install-service` 也会申请。请在系统弹窗中允许访问;拒绝、超时或辅助程序编译失败时,向导提示原因与授权路径,并继续初始化。在“系统设置 → 隐私与安全性 → 屏幕录制(或屏幕与系统音频录制)”中应授权弹窗显示的实际程序,可能是 Terminal、iTerm、编辑器或 node,不保证出现 `octopus-desktop-capture`。onboard 和 doctor 会显示辅助程序路径,文件名不等于系统授权名称。终端授权不代表后台服务已授权,需在 Claw 点击截屏,让实际服务申请权限,按实际名称授权后重启 Proxy。Linux 没有通用的截屏授权弹窗,需使用桌面用户的图形会话环境;平台依赖与设置见[桌面快照说明](../../docs/claw-desktop-snapshots.md)。
|
|
22
22
|
|
|
23
23
|
添加授权项目时,填写项目 ID 后会询问是否继续添加;直接回车即可结束添加并继续保存配置。
|
|
24
24
|
|
|
@@ -11714,8 +11714,8 @@ async function ensureHostIdentity(stateDirectory) {
|
|
|
11714
11714
|
}
|
|
11715
11715
|
|
|
11716
11716
|
// src/daemon.ts
|
|
11717
|
-
import { mkdir as
|
|
11718
|
-
import { join as
|
|
11717
|
+
import { mkdir as mkdir11 } from "node:fs/promises";
|
|
11718
|
+
import { join as join14 } from "node:path";
|
|
11719
11719
|
|
|
11720
11720
|
// src/workspace-diff.ts
|
|
11721
11721
|
import { execFile as execFile2 } from "node:child_process";
|
|
@@ -12186,7 +12186,7 @@ function isAuthentication(operation) {
|
|
|
12186
12186
|
}
|
|
12187
12187
|
|
|
12188
12188
|
// src/journal.ts
|
|
12189
|
-
import { join as
|
|
12189
|
+
import { join as join6 } from "node:path";
|
|
12190
12190
|
import { createHash as createHash4 } from "node:crypto";
|
|
12191
12191
|
|
|
12192
12192
|
// ../../packages/protocol/src/index.ts
|
|
@@ -12250,6 +12250,8 @@ var Operations = [
|
|
|
12250
12250
|
"desktop.list",
|
|
12251
12251
|
"desktop.capture",
|
|
12252
12252
|
"desktop.read",
|
|
12253
|
+
"desktop.lockStatus",
|
|
12254
|
+
"desktop.unlock",
|
|
12253
12255
|
"artifacts.list",
|
|
12254
12256
|
"artifacts.history",
|
|
12255
12257
|
"artifacts.read",
|
|
@@ -12271,6 +12273,252 @@ var CommandSchema = z.object({
|
|
|
12271
12273
|
}).strict();
|
|
12272
12274
|
var isReadOperation = (operation) => operation === "desktop.capture" || /\.(status|snapshot|list|get|messages|diff|todos|children|logs|projects|read|search|history|models|agents|commands|providers|mcp)$/.test(operation);
|
|
12273
12275
|
|
|
12276
|
+
// src/screen-lock.ts
|
|
12277
|
+
import { spawn as spawn2 } from "node:child_process";
|
|
12278
|
+
import { chmod as chmod3, mkdir as mkdir5, rename as rename4, rm, stat as stat6 } from "node:fs/promises";
|
|
12279
|
+
import { randomUUID as randomUUID7 } from "node:crypto";
|
|
12280
|
+
import { userInfo as userInfo2 } from "node:os";
|
|
12281
|
+
import { join as join5 } from "node:path";
|
|
12282
|
+
import { fileURLToPath as fileURLToPath3 } from "node:url";
|
|
12283
|
+
import { setTimeout as delay } from "node:timers/promises";
|
|
12284
|
+
|
|
12285
|
+
// src/host-auth.ts
|
|
12286
|
+
import { spawn } from "node:child_process";
|
|
12287
|
+
import { chmod as chmod2, mkdir as mkdir4, rename as rename3, stat as stat5, unlink as unlink4 } from "node:fs/promises";
|
|
12288
|
+
import { userInfo, platform as platform2 } from "node:os";
|
|
12289
|
+
import { randomUUID as randomUUID6 } from "node:crypto";
|
|
12290
|
+
import { join as join4 } from "node:path";
|
|
12291
|
+
import { fileURLToPath as fileURLToPath2 } from "node:url";
|
|
12292
|
+
function helperPath(stateDirectory) {
|
|
12293
|
+
return join4(stateDirectory, "octopus-host-auth");
|
|
12294
|
+
}
|
|
12295
|
+
async function ensureHostAuth(stateDirectory) {
|
|
12296
|
+
await mkdir4(stateDirectory, { recursive: true, mode: 448 });
|
|
12297
|
+
const target = helperPath(stateDirectory);
|
|
12298
|
+
const system = platform2();
|
|
12299
|
+
if (system !== "darwin" && system !== "linux") throw new Error("Host authentication is supported only on macOS and Linux");
|
|
12300
|
+
const mac = system === "darwin";
|
|
12301
|
+
const source = fileURLToPath2(new URL(mac ? "../native/auth-macos.m" : "../native/auth-linux.c", import.meta.url));
|
|
12302
|
+
const sourceInfo = await stat5(source);
|
|
12303
|
+
const existing = await stat5(target).catch(() => null);
|
|
12304
|
+
if (existing?.isFile() && existing.mtimeMs >= sourceInfo.mtimeMs && (existing.mode & 63) === 0) return target;
|
|
12305
|
+
const temporary = `${target}.${randomUUID6()}`;
|
|
12306
|
+
const args = mac ? ["-O2", "-framework", "Foundation", "-framework", "OpenDirectory", source, "-o", target] : ["-O2", source, "-o", temporary, "-lpam"];
|
|
12307
|
+
if (mac) args[args.length - 1] = temporary;
|
|
12308
|
+
const compiler = spawn(mac ? "clang" : "cc", args, { stdio: ["ignore", "ignore", "pipe"] });
|
|
12309
|
+
let diagnostics = "";
|
|
12310
|
+
compiler.stderr.on("data", (chunk) => {
|
|
12311
|
+
diagnostics += String(chunk).slice(0, 4096);
|
|
12312
|
+
});
|
|
12313
|
+
const exit = await new Promise((resolve4, reject) => {
|
|
12314
|
+
compiler.on("error", reject);
|
|
12315
|
+
compiler.on("exit", (code) => resolve4(code ?? 1));
|
|
12316
|
+
});
|
|
12317
|
+
if (exit !== 0) {
|
|
12318
|
+
await unlink4(temporary).catch(() => {
|
|
12319
|
+
});
|
|
12320
|
+
throw new Error(`Host authentication helper build failed: ${diagnostics.trim()}`);
|
|
12321
|
+
}
|
|
12322
|
+
try {
|
|
12323
|
+
await chmod2(temporary, 448);
|
|
12324
|
+
await rename3(temporary, target);
|
|
12325
|
+
return target;
|
|
12326
|
+
} finally {
|
|
12327
|
+
await unlink4(temporary).catch(() => {
|
|
12328
|
+
});
|
|
12329
|
+
}
|
|
12330
|
+
}
|
|
12331
|
+
async function verifyHostPassword(stateDirectory, username, password) {
|
|
12332
|
+
if (username !== userInfo().username || !password || Buffer.byteLength(password) > 1024) return false;
|
|
12333
|
+
const path = await ensureHostAuth(stateDirectory);
|
|
12334
|
+
const child = spawn(path, [], { stdio: ["pipe", "pipe", "ignore"] });
|
|
12335
|
+
const bytes = Buffer.from(password, "utf8");
|
|
12336
|
+
const header2 = Buffer.alloc(2);
|
|
12337
|
+
header2.writeUInt16BE(bytes.length);
|
|
12338
|
+
child.stdin.end(Buffer.concat([header2, bytes]));
|
|
12339
|
+
bytes.fill(0);
|
|
12340
|
+
let output = "";
|
|
12341
|
+
child.stdout.on("data", (chunk) => {
|
|
12342
|
+
output += String(chunk).slice(0, 16);
|
|
12343
|
+
});
|
|
12344
|
+
const timer = setTimeout(() => child.kill("SIGKILL"), 1e4);
|
|
12345
|
+
try {
|
|
12346
|
+
const code = await new Promise((resolve4, reject) => {
|
|
12347
|
+
child.on("error", reject);
|
|
12348
|
+
child.on("exit", (value) => resolve4(value ?? 1));
|
|
12349
|
+
});
|
|
12350
|
+
return code === 0 && output.trim() === "OK";
|
|
12351
|
+
} finally {
|
|
12352
|
+
clearTimeout(timer);
|
|
12353
|
+
}
|
|
12354
|
+
}
|
|
12355
|
+
|
|
12356
|
+
// src/screen-lock.ts
|
|
12357
|
+
var ScreenLockError = class extends Error {
|
|
12358
|
+
};
|
|
12359
|
+
var nativeErrors = {
|
|
12360
|
+
permission: "\u8BF7\u5728 Proxy \u4E3B\u673A\u201C\u7CFB\u7EDF\u8BBE\u7F6E \u2192 \u9690\u79C1\u4E0E\u5B89\u5168\u6027 \u2192 \u8F85\u52A9\u529F\u80FD\u201D\u4E2D\u6388\u6743\u5B9E\u9645\u8FD0\u884C Proxy \u7684\u7A0B\u5E8F\uFF0C\u5E76\u91CD\u542F Proxy\u3002",
|
|
12361
|
+
session: "\u65E0\u6CD5\u8BBF\u95EE Proxy \u7528\u6237\u7684\u6D3B\u52A8\u56FE\u5F62\u4F1A\u8BDD\uFF1B\u4E0D\u652F\u6301\u767B\u5F55\u524D\u3001FileVault \u542F\u52A8\u89E3\u9501\u6216\u5207\u6362\u5230\u5176\u4ED6\u7528\u6237\u7684\u5C4F\u5E55\u3002",
|
|
12362
|
+
focus: "\u672A\u627E\u5230 loginwindow \u7684\u5BC6\u7801\u8F93\u5165\u6846\uFF0C\u8BF7\u5728\u4E3B\u673A\u663E\u793A\u5F53\u524D\u7528\u6237\u7684\u5BC6\u7801\u89E3\u9501\u754C\u9762\u540E\u91CD\u8BD5\u3002",
|
|
12363
|
+
changed: "\u4E3B\u673A\u9501\u5C4F\u72B6\u6001\u6216\u5BC6\u7801\u8F93\u5165\u7126\u70B9\u5DF2\u53D8\u5316\uFF0C\u5DF2\u505C\u6B62\u8F93\u5165\uFF0C\u8BF7\u91CD\u8BD5\u3002",
|
|
12364
|
+
timeout: "\u4E3B\u673A\u5C4F\u5E55\u4ECD\u9501\u5B9A\uFF0C\u7CFB\u7EDF\u672A\u63A5\u53D7\u89E3\u9501\u8F93\u5165\uFF1B\u8BF7\u68C0\u67E5\u8F85\u52A9\u529F\u80FD\u6743\u9650\u548C\u9501\u5C4F\u754C\u9762\u540E\u91CD\u8BD5\u3002",
|
|
12365
|
+
password: "\u4E3B\u673A\u5BC6\u7801\u683C\u5F0F\u65E0\u6548\u3002"
|
|
12366
|
+
};
|
|
12367
|
+
var executeScreenLock = (file, args, input, signal) => new Promise((resolve4, reject) => {
|
|
12368
|
+
if (signal?.aborted) {
|
|
12369
|
+
reject(new ScreenLockError("\u8FDE\u63A5\u5DF2\u5173\u95ED\uFF0C\u5C4F\u5E55\u89E3\u9501\u5DF2\u53D6\u6D88\u3002"));
|
|
12370
|
+
return;
|
|
12371
|
+
}
|
|
12372
|
+
const child = spawn2(file, args, { stdio: ["pipe", "pipe", "pipe"], signal });
|
|
12373
|
+
let output = "", diagnostic = "", timedOut = false;
|
|
12374
|
+
const timer = setTimeout(() => {
|
|
12375
|
+
timedOut = true;
|
|
12376
|
+
child.kill("SIGKILL");
|
|
12377
|
+
}, args[0] === "unlock" ? 45e3 : 15e3);
|
|
12378
|
+
child.stdout.on("data", (chunk) => {
|
|
12379
|
+
output += String(chunk);
|
|
12380
|
+
if (output.length > 4096) child.kill("SIGKILL");
|
|
12381
|
+
});
|
|
12382
|
+
child.stderr.on("data", (chunk) => {
|
|
12383
|
+
diagnostic = (diagnostic + String(chunk)).slice(0, 4096);
|
|
12384
|
+
});
|
|
12385
|
+
child.stdin.on("error", () => {
|
|
12386
|
+
});
|
|
12387
|
+
child.on("error", () => {
|
|
12388
|
+
clearTimeout(timer);
|
|
12389
|
+
reject(new ScreenLockError(signal?.aborted ? "\u8FDE\u63A5\u5DF2\u5173\u95ED\uFF0C\u5C4F\u5E55\u89E3\u9501\u5DF2\u53D6\u6D88\u3002" : "\u5C4F\u5E55\u89E3\u9501\u4F9D\u8D56\u4E0D\u53EF\u7528\uFF1BmacOS \u9700\u8981 Xcode Command Line Tools\uFF0CLinux \u9700\u8981 systemd loginctl\u3002"));
|
|
12390
|
+
});
|
|
12391
|
+
child.on("close", (code) => {
|
|
12392
|
+
clearTimeout(timer);
|
|
12393
|
+
if (signal?.aborted) reject(new ScreenLockError("\u8FDE\u63A5\u5DF2\u5173\u95ED\uFF0C\u5C4F\u5E55\u89E3\u9501\u5DF2\u53D6\u6D88\u3002"));
|
|
12394
|
+
else if (code !== 0 || output.length > 4096) reject(new ScreenLockError(timedOut ? "\u5C4F\u5E55\u89E3\u9501\u64CD\u4F5C\u8D85\u65F6\uFF0C\u8BF7\u68C0\u67E5\u4E3B\u673A\u72B6\u6001\u540E\u91CD\u8BD5\u3002" : nativeErrors[diagnostic.trim()] ?? "\u5C4F\u5E55\u89E3\u9501\u64CD\u4F5C\u5931\u8D25\uFF0C\u8BF7\u68C0\u67E5\u4E3B\u673A\u7684\u56FE\u5F62\u4F1A\u8BDD\u3001\u7CFB\u7EDF\u6743\u9650\u53CA\u4F9D\u8D56\u3002"));
|
|
12395
|
+
else resolve4(output);
|
|
12396
|
+
});
|
|
12397
|
+
child.stdin.end(input);
|
|
12398
|
+
});
|
|
12399
|
+
var PlatformScreenLock = class {
|
|
12400
|
+
constructor(stateDirectory, system = process.platform, environment = process.env, execute3 = executeScreenLock, authenticate = verifyHostPassword, account = userInfo2()) {
|
|
12401
|
+
this.stateDirectory = stateDirectory;
|
|
12402
|
+
this.system = system;
|
|
12403
|
+
this.environment = environment;
|
|
12404
|
+
this.execute = execute3;
|
|
12405
|
+
this.authenticate = authenticate;
|
|
12406
|
+
this.account = account;
|
|
12407
|
+
}
|
|
12408
|
+
helper;
|
|
12409
|
+
failures = 0;
|
|
12410
|
+
blockedUntil = 0;
|
|
12411
|
+
macHelper() {
|
|
12412
|
+
if (this.helper) return this.helper;
|
|
12413
|
+
this.helper = (async () => {
|
|
12414
|
+
await mkdir5(this.stateDirectory, { recursive: true, mode: 448 });
|
|
12415
|
+
const target = join5(this.stateDirectory, "octopus-screen-unlock");
|
|
12416
|
+
const source = fileURLToPath3(new URL("../native/screen-unlock-macos.m", import.meta.url));
|
|
12417
|
+
const sourceInfo = await stat6(source), existing = await stat6(target).catch(() => null);
|
|
12418
|
+
if (existing?.isFile() && existing.mtimeMs >= sourceInfo.mtimeMs && (existing.mode & 63) === 0) return target;
|
|
12419
|
+
const temporary = `${target}.${randomUUID7()}`;
|
|
12420
|
+
try {
|
|
12421
|
+
await this.execute("/usr/bin/clang", ["-O2", "-fobjc-arc", "-mmacosx-version-min=12.3", "-framework", "Foundation", "-framework", "AppKit", "-framework", "ApplicationServices", source, "-o", temporary]);
|
|
12422
|
+
await chmod3(temporary, 448);
|
|
12423
|
+
await rename4(temporary, target);
|
|
12424
|
+
return target;
|
|
12425
|
+
} finally {
|
|
12426
|
+
await rm(temporary, { force: true });
|
|
12427
|
+
}
|
|
12428
|
+
})();
|
|
12429
|
+
this.helper.catch(() => {
|
|
12430
|
+
this.helper = void 0;
|
|
12431
|
+
});
|
|
12432
|
+
return this.helper;
|
|
12433
|
+
}
|
|
12434
|
+
async linuxSession(signal) {
|
|
12435
|
+
let ids;
|
|
12436
|
+
if (this.environment.XDG_SESSION_ID) ids = [this.environment.XDG_SESSION_ID];
|
|
12437
|
+
else ids = (await this.execute("loginctl", ["--no-ask-password", "list-sessions", "--no-legend"], void 0, signal)).trim().split("\n").map((line) => line.trim().split(/\s+/)[0]).filter(Boolean);
|
|
12438
|
+
const sessions = [];
|
|
12439
|
+
for (const id of ids) {
|
|
12440
|
+
if (!/^[a-zA-Z0-9_-]+$/.test(id)) continue;
|
|
12441
|
+
const text = await this.execute("loginctl", ["--no-ask-password", "show-session", id, "-p", "User", "-p", "Type", "-p", "Active", "-p", "LockedHint"], void 0, signal);
|
|
12442
|
+
const properties = Object.fromEntries(text.trim().split("\n").map((line) => line.split("=")));
|
|
12443
|
+
if (properties.User !== String(this.account.uid) || !["x11", "wayland"].includes(properties.Type) || properties.Active !== "yes") continue;
|
|
12444
|
+
if (!["yes", "no"].includes(properties.LockedHint)) throw new ScreenLockError("\u684C\u9762\u672A\u62A5\u544A\u6709\u6548\u7684\u9501\u5C4F\u72B6\u6001\uFF0C\u65E0\u6CD5\u786E\u8BA4\u89E3\u9501\u3002");
|
|
12445
|
+
sessions.push({ id, locked: properties.LockedHint === "yes" });
|
|
12446
|
+
}
|
|
12447
|
+
if (sessions.length !== 1) throw new ScreenLockError("\u65E0\u6CD5\u786E\u5B9A Proxy \u7528\u6237\u7684\u552F\u4E00\u6D3B\u52A8\u56FE\u5F62\u4F1A\u8BDD\uFF0C\u8BF7\u8BBE\u7F6E\u6B63\u786E\u7684 XDG_SESSION_ID\u3002");
|
|
12448
|
+
return sessions[0];
|
|
12449
|
+
}
|
|
12450
|
+
parseStatus(text) {
|
|
12451
|
+
let data;
|
|
12452
|
+
try {
|
|
12453
|
+
data = JSON.parse(text);
|
|
12454
|
+
} catch {
|
|
12455
|
+
throw new ScreenLockError("\u4E3B\u673A\u8FD4\u56DE\u7684\u9501\u5C4F\u72B6\u6001\u65E0\u6548\u3002");
|
|
12456
|
+
}
|
|
12457
|
+
if (!data || typeof data.locked !== "boolean" || typeof data.canUnlock !== "boolean") throw new ScreenLockError("\u4E3B\u673A\u8FD4\u56DE\u7684\u9501\u5C4F\u72B6\u6001\u65E0\u6548\u3002");
|
|
12458
|
+
return { locked: data.locked, canUnlock: data.canUnlock, account: this.account.username, ...!data.canUnlock ? { message: nativeErrors.permission } : {} };
|
|
12459
|
+
}
|
|
12460
|
+
async status() {
|
|
12461
|
+
if (this.system === "darwin") return this.parseStatus(await this.execute(await this.macHelper(), ["status"]));
|
|
12462
|
+
if (this.system === "linux") return { locked: (await this.linuxSession()).locked, canUnlock: true, account: this.account.username };
|
|
12463
|
+
throw new ScreenLockError("\u5C4F\u5E55\u89E3\u9501\u4EC5\u652F\u6301 macOS 12.3 \u53CA\u4EE5\u4E0A\u548C\u4F7F\u7528 systemd \u7684 Linux \u56FE\u5F62\u4F1A\u8BDD\u3002");
|
|
12464
|
+
}
|
|
12465
|
+
async unlock(password, signal) {
|
|
12466
|
+
const check = () => {
|
|
12467
|
+
if (signal.aborted) throw new ScreenLockError("\u8FDE\u63A5\u5DF2\u5173\u95ED\uFF0C\u5C4F\u5E55\u89E3\u9501\u5DF2\u53D6\u6D88\u3002");
|
|
12468
|
+
};
|
|
12469
|
+
check();
|
|
12470
|
+
if (!password || Buffer.byteLength(password) > 1024 || /[\x00-\x1f\x7f]/.test(password)) throw new ScreenLockError("\u4E3B\u673A\u5BC6\u7801\u683C\u5F0F\u65E0\u6548\u3002");
|
|
12471
|
+
if (Date.now() < this.blockedUntil) throw new ScreenLockError("\u5BC6\u7801\u9A8C\u8BC1\u5931\u8D25\u6B21\u6570\u8FC7\u591A\uFF0C\u8BF7\u7B49\u5F85 30 \u79D2\u540E\u91CD\u8BD5\u3002");
|
|
12472
|
+
const status = await this.status();
|
|
12473
|
+
check();
|
|
12474
|
+
if (!status.locked) return status;
|
|
12475
|
+
if (!status.canUnlock) throw new ScreenLockError(status.message ?? "\u4E3B\u673A\u4E0D\u652F\u6301\u5C4F\u5E55\u89E3\u9501\u3002");
|
|
12476
|
+
let valid;
|
|
12477
|
+
try {
|
|
12478
|
+
valid = await this.authenticate(this.stateDirectory, this.account.username, password);
|
|
12479
|
+
} catch {
|
|
12480
|
+
throw new ScreenLockError("\u65E0\u6CD5\u9A8C\u8BC1\u4E3B\u673A\u5BC6\u7801\uFF0C\u8BF7\u68C0\u67E5\u4E3B\u673A\u8BA4\u8BC1\u8F85\u52A9\u7A0B\u5E8F\u3002");
|
|
12481
|
+
}
|
|
12482
|
+
check();
|
|
12483
|
+
if (!valid) {
|
|
12484
|
+
if (++this.failures >= 3) {
|
|
12485
|
+
this.blockedUntil = Date.now() + 3e4;
|
|
12486
|
+
this.failures = 0;
|
|
12487
|
+
}
|
|
12488
|
+
throw new ScreenLockError("\u4E3B\u673A\u767B\u5F55\u5BC6\u7801\u9519\u8BEF\uFF0C\u8BF7\u91CD\u65B0\u8F93\u5165\u3002");
|
|
12489
|
+
}
|
|
12490
|
+
this.failures = 0;
|
|
12491
|
+
if (this.system === "darwin") {
|
|
12492
|
+
const helper = await this.macHelper();
|
|
12493
|
+
check();
|
|
12494
|
+
const bytes = Buffer.from(password, "utf8"), input = Buffer.alloc(bytes.length + 2);
|
|
12495
|
+
input.writeUInt16BE(bytes.length);
|
|
12496
|
+
bytes.copy(input, 2);
|
|
12497
|
+
bytes.fill(0);
|
|
12498
|
+
let result;
|
|
12499
|
+
try {
|
|
12500
|
+
result = this.parseStatus(await this.execute(helper, ["unlock"], input, signal));
|
|
12501
|
+
} finally {
|
|
12502
|
+
input.fill(0);
|
|
12503
|
+
}
|
|
12504
|
+
check();
|
|
12505
|
+
if (result.locked) throw new ScreenLockError(nativeErrors.timeout);
|
|
12506
|
+
return result;
|
|
12507
|
+
}
|
|
12508
|
+
const session = await this.linuxSession(signal);
|
|
12509
|
+
check();
|
|
12510
|
+
if (session.locked) await this.execute("loginctl", ["--no-ask-password", "unlock-session", session.id], void 0, signal);
|
|
12511
|
+
for (let i = 0; i < 20; i++) {
|
|
12512
|
+
check();
|
|
12513
|
+
const current = await this.linuxSession(signal);
|
|
12514
|
+
if (current.id !== session.id) throw new ScreenLockError("\u4E3B\u673A\u6D3B\u52A8\u4F1A\u8BDD\u5DF2\u53D8\u5316\uFF0C\u5DF2\u505C\u6B62\u89E3\u9501\u3002");
|
|
12515
|
+
if (!current.locked) return { ...status, locked: false };
|
|
12516
|
+
await delay(250, void 0, { signal }).catch(() => check());
|
|
12517
|
+
}
|
|
12518
|
+
throw new ScreenLockError("\u684C\u9762\u9501\u5C4F\u7A0B\u5E8F\u672A\u54CD\u5E94\u89E3\u9501\u8BF7\u6C42\uFF0C\u8BF7\u786E\u8BA4\u5B83\u652F\u6301 systemd-logind \u7684 Unlock \u4FE1\u53F7\u3002");
|
|
12519
|
+
}
|
|
12520
|
+
};
|
|
12521
|
+
|
|
12274
12522
|
// src/journal.ts
|
|
12275
12523
|
var Journal = class {
|
|
12276
12524
|
constructor(directory) {
|
|
@@ -12286,8 +12534,8 @@ var Journal = class {
|
|
|
12286
12534
|
return active.result;
|
|
12287
12535
|
}
|
|
12288
12536
|
const run4 = async () => {
|
|
12289
|
-
const path =
|
|
12290
|
-
const readOnly = isReadOperation(command.operation) || isAuthentication(command.operation);
|
|
12537
|
+
const path = join6(this.directory, `${command.id}.json`);
|
|
12538
|
+
const readOnly = command.operation.startsWith("desktop.") || isReadOperation(command.operation) || isAuthentication(command.operation);
|
|
12291
12539
|
if (!readOnly) {
|
|
12292
12540
|
const old = await readJson(path);
|
|
12293
12541
|
if (old) {
|
|
@@ -12300,7 +12548,7 @@ var Journal = class {
|
|
|
12300
12548
|
try {
|
|
12301
12549
|
response = { type: "response", id: command.id, botId: command.botId, status: "completed", data: await action() };
|
|
12302
12550
|
} catch (error) {
|
|
12303
|
-
response = this.failure(command, isAuthentication(command.operation) ? "\u8BA4\u8BC1\u64CD\u4F5C\u5931\u8D25\uFF0C\u8BF7\u68C0\u67E5\u7F16\u7801\u5DE5\u5177\u7684\u767B\u5F55\u72B6\u6001\u540E\u91CD\u8BD5" : error instanceof Error ? error.message : "Operation failed");
|
|
12551
|
+
response = this.failure(command, command.operation === "desktop.unlock" ? error instanceof ScreenLockError ? error.message : "\u5C4F\u5E55\u89E3\u9501\u5931\u8D25\uFF0C\u8BF7\u68C0\u67E5\u4E3B\u673A\u72B6\u6001\u4E0E\u6743\u9650\u540E\u91CD\u8BD5\u3002" : isAuthentication(command.operation) ? "\u8BA4\u8BC1\u64CD\u4F5C\u5931\u8D25\uFF0C\u8BF7\u68C0\u67E5\u7F16\u7801\u5DE5\u5177\u7684\u767B\u5F55\u72B6\u6001\u540E\u91CD\u8BD5" : error instanceof Error ? error.message : "Operation failed");
|
|
12304
12552
|
if (!readOnly && error instanceof Error && ["TimeoutError", "AbortError", "UnknownResultError"].includes(error.name)) response.status = "unknown";
|
|
12305
12553
|
}
|
|
12306
12554
|
if (!readOnly) await atomicJson(path, { hash, response, finishedAt: (/* @__PURE__ */ new Date()).toISOString() });
|
|
@@ -12322,6 +12570,7 @@ var Journal = class {
|
|
|
12322
12570
|
return result;
|
|
12323
12571
|
}
|
|
12324
12572
|
queueKey(c) {
|
|
12573
|
+
if (c.operation.startsWith("desktop.")) return void 0;
|
|
12325
12574
|
if (isReadOperation(c.operation) || isAuthentication(c.operation)) return void 0;
|
|
12326
12575
|
const [group] = c.operation.split(".");
|
|
12327
12576
|
if (c.operation === "sessions.abort") return JSON.stringify(["abort", c.projectId, c.sessionId]);
|
|
@@ -12406,8 +12655,8 @@ function pageHistory(input, payload) {
|
|
|
12406
12655
|
|
|
12407
12656
|
// src/artifacts.ts
|
|
12408
12657
|
import { createHash as createHash5 } from "node:crypto";
|
|
12409
|
-
import { join as
|
|
12410
|
-
import { readFile as readFile4, writeFile as writeFile2, mkdir as
|
|
12658
|
+
import { join as join7 } from "node:path";
|
|
12659
|
+
import { readFile as readFile4, writeFile as writeFile2, mkdir as mkdir6, stat as stat7 } from "node:fs/promises";
|
|
12411
12660
|
var Artifacts = class {
|
|
12412
12661
|
constructor(workspace, directory, emit) {
|
|
12413
12662
|
this.workspace = workspace;
|
|
@@ -12423,7 +12672,7 @@ var Artifacts = class {
|
|
|
12423
12672
|
}
|
|
12424
12673
|
index(project) {
|
|
12425
12674
|
this.workspace.project(project);
|
|
12426
|
-
return
|
|
12675
|
+
return join7(this.directory, project, "index.json");
|
|
12427
12676
|
}
|
|
12428
12677
|
async list(project) {
|
|
12429
12678
|
return Object.values(await readJson(this.index(project)) ?? {});
|
|
@@ -12461,9 +12710,9 @@ var Artifacts = class {
|
|
|
12461
12710
|
changed = true;
|
|
12462
12711
|
}
|
|
12463
12712
|
if (previous?.hash !== hash || previous.deleted) {
|
|
12464
|
-
const dir =
|
|
12465
|
-
await
|
|
12466
|
-
await writeFile2(
|
|
12713
|
+
const dir = join7(this.directory, project, "blobs");
|
|
12714
|
+
await mkdir6(dir, { recursive: true, mode: 448 });
|
|
12715
|
+
await writeFile2(join7(dir, hash), data, { mode: 384 });
|
|
12467
12716
|
record2.revisions.push({ hash, timestamp: (/* @__PURE__ */ new Date()).toISOString() });
|
|
12468
12717
|
changed = true;
|
|
12469
12718
|
}
|
|
@@ -12479,7 +12728,7 @@ var Artifacts = class {
|
|
|
12479
12728
|
}
|
|
12480
12729
|
for (const item of Object.values(index)) if (!seen.has(item.path) && !item.revisions.at(-1)?.deleted) {
|
|
12481
12730
|
try {
|
|
12482
|
-
await
|
|
12731
|
+
await stat7(await this.workspace.path(project, item.path));
|
|
12483
12732
|
} catch (e) {
|
|
12484
12733
|
if (e.code === "ENOENT") {
|
|
12485
12734
|
item.revisions.push({ hash: "", timestamp: (/* @__PURE__ */ new Date()).toISOString(), deleted: true });
|
|
@@ -12499,7 +12748,7 @@ var Artifacts = class {
|
|
|
12499
12748
|
}
|
|
12500
12749
|
async read(project, path, hash) {
|
|
12501
12750
|
if (!/^[a-f0-9]{64}$/.test(hash) || !(await this.history(project, path)).some((r) => r.hash === hash)) throw new Error("Unknown revision");
|
|
12502
|
-
return readFile4(
|
|
12751
|
+
return readFile4(join7(this.directory, project, "blobs", hash), "utf8");
|
|
12503
12752
|
}
|
|
12504
12753
|
associate(project, path, sessionId) {
|
|
12505
12754
|
return this.serial(project, () => this.associateUnlocked(project, path, sessionId));
|
|
@@ -12516,82 +12765,9 @@ var Artifacts = class {
|
|
|
12516
12765
|
// src/secure-chat.ts
|
|
12517
12766
|
import WebSocket from "ws";
|
|
12518
12767
|
import { Duplex } from "node:stream";
|
|
12519
|
-
import { createPublicKey, randomUUID as
|
|
12520
|
-
import { userInfo as
|
|
12768
|
+
import { createPublicKey, randomUUID as randomUUID8, verify } from "node:crypto";
|
|
12769
|
+
import { userInfo as userInfo3 } from "node:os";
|
|
12521
12770
|
import { TLSSocket, createSecureContext } from "node:tls";
|
|
12522
|
-
|
|
12523
|
-
// src/host-auth.ts
|
|
12524
|
-
import { spawn } from "node:child_process";
|
|
12525
|
-
import { chmod as chmod2, mkdir as mkdir5, rename as rename3, stat as stat6, unlink as unlink4 } from "node:fs/promises";
|
|
12526
|
-
import { userInfo, platform as platform2 } from "node:os";
|
|
12527
|
-
import { randomUUID as randomUUID6 } from "node:crypto";
|
|
12528
|
-
import { join as join6 } from "node:path";
|
|
12529
|
-
import { fileURLToPath as fileURLToPath2 } from "node:url";
|
|
12530
|
-
function helperPath(stateDirectory) {
|
|
12531
|
-
return join6(stateDirectory, "octopus-host-auth");
|
|
12532
|
-
}
|
|
12533
|
-
async function ensureHostAuth(stateDirectory) {
|
|
12534
|
-
await mkdir5(stateDirectory, { recursive: true, mode: 448 });
|
|
12535
|
-
const target = helperPath(stateDirectory);
|
|
12536
|
-
const system = platform2();
|
|
12537
|
-
if (system !== "darwin" && system !== "linux") throw new Error("Host authentication is supported only on macOS and Linux");
|
|
12538
|
-
const mac = system === "darwin";
|
|
12539
|
-
const source = fileURLToPath2(new URL(mac ? "../native/auth-macos.m" : "../native/auth-linux.c", import.meta.url));
|
|
12540
|
-
const sourceInfo = await stat6(source);
|
|
12541
|
-
const existing = await stat6(target).catch(() => null);
|
|
12542
|
-
if (existing?.isFile() && existing.mtimeMs >= sourceInfo.mtimeMs && (existing.mode & 63) === 0) return target;
|
|
12543
|
-
const temporary = `${target}.${randomUUID6()}`;
|
|
12544
|
-
const args = mac ? ["-O2", "-framework", "Foundation", "-framework", "OpenDirectory", source, "-o", target] : ["-O2", source, "-o", temporary, "-lpam"];
|
|
12545
|
-
if (mac) args[args.length - 1] = temporary;
|
|
12546
|
-
const compiler = spawn(mac ? "clang" : "cc", args, { stdio: ["ignore", "ignore", "pipe"] });
|
|
12547
|
-
let diagnostics = "";
|
|
12548
|
-
compiler.stderr.on("data", (chunk) => {
|
|
12549
|
-
diagnostics += String(chunk).slice(0, 4096);
|
|
12550
|
-
});
|
|
12551
|
-
const exit = await new Promise((resolve4, reject) => {
|
|
12552
|
-
compiler.on("error", reject);
|
|
12553
|
-
compiler.on("exit", (code) => resolve4(code ?? 1));
|
|
12554
|
-
});
|
|
12555
|
-
if (exit !== 0) {
|
|
12556
|
-
await unlink4(temporary).catch(() => {
|
|
12557
|
-
});
|
|
12558
|
-
throw new Error(`Host authentication helper build failed: ${diagnostics.trim()}`);
|
|
12559
|
-
}
|
|
12560
|
-
try {
|
|
12561
|
-
await chmod2(temporary, 448);
|
|
12562
|
-
await rename3(temporary, target);
|
|
12563
|
-
return target;
|
|
12564
|
-
} finally {
|
|
12565
|
-
await unlink4(temporary).catch(() => {
|
|
12566
|
-
});
|
|
12567
|
-
}
|
|
12568
|
-
}
|
|
12569
|
-
async function verifyHostPassword(stateDirectory, username, password) {
|
|
12570
|
-
if (username !== userInfo().username || !password || Buffer.byteLength(password) > 1024) return false;
|
|
12571
|
-
const path = await ensureHostAuth(stateDirectory);
|
|
12572
|
-
const child = spawn(path, [], { stdio: ["pipe", "pipe", "ignore"] });
|
|
12573
|
-
const bytes = Buffer.from(password, "utf8");
|
|
12574
|
-
const header2 = Buffer.alloc(2);
|
|
12575
|
-
header2.writeUInt16BE(bytes.length);
|
|
12576
|
-
child.stdin.end(Buffer.concat([header2, bytes]));
|
|
12577
|
-
bytes.fill(0);
|
|
12578
|
-
let output = "";
|
|
12579
|
-
child.stdout.on("data", (chunk) => {
|
|
12580
|
-
output += String(chunk).slice(0, 16);
|
|
12581
|
-
});
|
|
12582
|
-
const timer = setTimeout(() => child.kill("SIGKILL"), 1e4);
|
|
12583
|
-
try {
|
|
12584
|
-
const code = await new Promise((resolve4, reject) => {
|
|
12585
|
-
child.on("error", reject);
|
|
12586
|
-
child.on("exit", (value) => resolve4(value ?? 1));
|
|
12587
|
-
});
|
|
12588
|
-
return code === 0 && output.trim() === "OK";
|
|
12589
|
-
} finally {
|
|
12590
|
-
clearTimeout(timer);
|
|
12591
|
-
}
|
|
12592
|
-
}
|
|
12593
|
-
|
|
12594
|
-
// src/secure-chat.ts
|
|
12595
12771
|
var OPEN = 1;
|
|
12596
12772
|
var DATA = 2;
|
|
12597
12773
|
var CLOSE = 3;
|
|
@@ -12649,13 +12825,13 @@ var SecureChat = class {
|
|
|
12649
12825
|
}
|
|
12650
12826
|
outer;
|
|
12651
12827
|
sessions = /* @__PURE__ */ new Map();
|
|
12652
|
-
epoch =
|
|
12828
|
+
epoch = randomUUID8();
|
|
12653
12829
|
sequence = 0;
|
|
12654
12830
|
events = [];
|
|
12655
12831
|
eventBytes = 0;
|
|
12656
12832
|
stopping = false;
|
|
12657
12833
|
context;
|
|
12658
|
-
serviceAccount =
|
|
12834
|
+
serviceAccount = userInfo3().username;
|
|
12659
12835
|
timer;
|
|
12660
12836
|
start() {
|
|
12661
12837
|
this.connect();
|
|
@@ -12861,7 +13037,7 @@ var SecureChat = class {
|
|
|
12861
13037
|
};
|
|
12862
13038
|
|
|
12863
13039
|
// src/proxy-snapshot.ts
|
|
12864
|
-
import { randomUUID as
|
|
13040
|
+
import { randomUUID as randomUUID9 } from "node:crypto";
|
|
12865
13041
|
import { execFile as execFile3 } from "node:child_process";
|
|
12866
13042
|
import { promisify as promisify3 } from "node:util";
|
|
12867
13043
|
var execute2 = promisify3(execFile3);
|
|
@@ -12915,7 +13091,7 @@ async function buildProxySnapshot(config, runtime, adapter, requestedProjectId)
|
|
|
12915
13091
|
let modelCatalogStatus = capabilities.includes("configuration.models") ? "unavailable" : "unsupported";
|
|
12916
13092
|
if (status.healthy === true && projectId && capabilities.includes("configuration.models")) {
|
|
12917
13093
|
try {
|
|
12918
|
-
const command = { type: "command", id:
|
|
13094
|
+
const command = { type: "command", id: randomUUID9(), botId: config.botId, operation: "configuration.models", projectId, payload: {} };
|
|
12919
13095
|
models.push(...publicModels(await adapter.dispatch(command)));
|
|
12920
13096
|
modelCatalogStatus = models.length ? "available" : "unavailable";
|
|
12921
13097
|
} catch {
|
|
@@ -12942,11 +13118,11 @@ async function buildProxySnapshot(config, runtime, adapter, requestedProjectId)
|
|
|
12942
13118
|
}
|
|
12943
13119
|
|
|
12944
13120
|
// src/media.ts
|
|
12945
|
-
import { createHash as createHash6, randomUUID as
|
|
13121
|
+
import { createHash as createHash6, randomUUID as randomUUID10 } from "node:crypto";
|
|
12946
13122
|
import { constants as constants3 } from "node:fs";
|
|
12947
13123
|
import { open as open4, unlink as unlink5, realpath as realpath3 } from "node:fs/promises";
|
|
12948
13124
|
import { basename as basename3, isAbsolute as isAbsolute3, relative as relative3, resolve as resolve3, sep as sep2 } from "node:path";
|
|
12949
|
-
import { fileURLToPath as
|
|
13125
|
+
import { fileURLToPath as fileURLToPath4 } from "node:url";
|
|
12950
13126
|
|
|
12951
13127
|
// src/screenshot-delivery.ts
|
|
12952
13128
|
var instructions = "\n\n[Octopus screenshot delivery]\nFor requested UI tests/screenshots, run the real application with the available test tools. Save each requested key-step or failure screenshot under the authorized project (prefer test-results/screenshots/ with a unique filename). After saving, immediately report it as  in your progress or final response. Octopus Proxy will attach the actual file to Claw. Never claim a screenshot or test succeeded without producing it; explain environment/tool blockers. Do not substitute generated mockups for test screenshots.\n[/Octopus screenshot delivery]";
|
|
@@ -13063,7 +13239,7 @@ var RemoteMedia = class {
|
|
|
13063
13239
|
} else {
|
|
13064
13240
|
let path = value.path ?? value.localPath ?? value.artifactUri ?? source.path ?? source.localPath ?? url;
|
|
13065
13241
|
if (typeof path !== "string") throw new Error("Image has no readable data or project path");
|
|
13066
|
-
if (path.startsWith("file:")) path =
|
|
13242
|
+
if (path.startsWith("file:")) path = fileURLToPath4(path);
|
|
13067
13243
|
if (/^[a-z][a-z0-9+.-]*:/i.test(path)) throw new Error("Only images inside the authorized project can be returned");
|
|
13068
13244
|
if (isAbsolute3(path)) {
|
|
13069
13245
|
const root = await this.workspace.root(project);
|
|
@@ -13159,7 +13335,7 @@ var RemoteMedia = class {
|
|
|
13159
13335
|
if (bytes.length === 0 || bytes.length > this.imageLimit()) throw new Error("Image is empty or exceeds size limit");
|
|
13160
13336
|
const extension = { "image/png": "png", "image/jpeg": "jpg", "image/gif": "gif", "image/webp": "webp", "application/pdf": "pdf" }[mime] ?? "bin";
|
|
13161
13337
|
const directory = await this.workspace.attachmentDirectory(project, "media", sha256), path = directory + "/asset." + extension;
|
|
13162
|
-
const temp = await this.workspace.path(project, directory + "/.octopus-media-" +
|
|
13338
|
+
const temp = await this.workspace.path(project, directory + "/.octopus-media-" + randomUUID10(), true);
|
|
13163
13339
|
const file = await open4(temp, constants3.O_WRONLY | constants3.O_CREAT | constants3.O_EXCL | constants3.O_NOFOLLOW, 384);
|
|
13164
13340
|
try {
|
|
13165
13341
|
await file.writeFile(bytes);
|
|
@@ -14577,7 +14753,7 @@ function applyEdits(text, edits) {
|
|
|
14577
14753
|
|
|
14578
14754
|
// src/project-config.ts
|
|
14579
14755
|
import { readFile as readFile5, writeFile as writeFile3, unlink as unlink6 } from "node:fs/promises";
|
|
14580
|
-
import { createHash as createHash7, randomUUID as
|
|
14756
|
+
import { createHash as createHash7, randomUUID as randomUUID11 } from "node:crypto";
|
|
14581
14757
|
var ProjectConfig = class {
|
|
14582
14758
|
constructor(workspace, request) {
|
|
14583
14759
|
this.workspace = workspace;
|
|
@@ -14628,7 +14804,7 @@ var ProjectConfig = class {
|
|
|
14628
14804
|
if (!this.validator(parse2(updated))) throw new Error("Merged OpenCode configuration is invalid: " + JSON.stringify(this.validator.errors));
|
|
14629
14805
|
if (exists) await this.workspace.write(project, name, updated, createHash7("sha256").update(original).digest("hex"));
|
|
14630
14806
|
else {
|
|
14631
|
-
const temporary = await this.workspace.path(project, `.octopus-config-${
|
|
14807
|
+
const temporary = await this.workspace.path(project, `.octopus-config-${randomUUID11()}.tmp`, true);
|
|
14632
14808
|
try {
|
|
14633
14809
|
await writeFile3(temporary, updated, { mode: 384, flag: "wx" });
|
|
14634
14810
|
await this.workspace.publish(project, temporary, name);
|
|
@@ -14799,13 +14975,16 @@ var OpenCodeAdapter = class {
|
|
|
14799
14975
|
}
|
|
14800
14976
|
async models(project) {
|
|
14801
14977
|
const models = await this.data(project, "GET", "/api/model");
|
|
14978
|
+
const config = await this.data(project, "GET", "/api/config").catch(() => void 0);
|
|
14979
|
+
const layers = Array.isArray(config) ? config : [config];
|
|
14980
|
+
const configured = layers.map((layer) => layer?.config?.model ?? layer?.model).filter((value) => typeof value === "string").at(-1);
|
|
14802
14981
|
const providers = /* @__PURE__ */ new Map();
|
|
14803
14982
|
for (const model of Array.isArray(models) ? models : []) {
|
|
14804
14983
|
if (model.enabled === false || model.status === "deprecated") continue;
|
|
14805
14984
|
const modelId = model.id ?? model.modelID;
|
|
14806
14985
|
if (typeof model.providerID !== "string" || typeof modelId !== "string") continue;
|
|
14807
14986
|
const provider = providers.get(model.providerID) ?? { id: model.providerID, models: {} };
|
|
14808
|
-
provider.models[modelId] = { ...model, id: modelId };
|
|
14987
|
+
provider.models[modelId] = { ...model, id: modelId, isDefault: configured ? configured === `${model.providerID}/${modelId}` : model.isDefault === true };
|
|
14809
14988
|
providers.set(model.providerID, provider);
|
|
14810
14989
|
}
|
|
14811
14990
|
return redact({ providers: [...providers.values()] });
|
|
@@ -15083,11 +15262,11 @@ function redact(value) {
|
|
|
15083
15262
|
}
|
|
15084
15263
|
|
|
15085
15264
|
// src/runtime.ts
|
|
15086
|
-
import { spawn as
|
|
15265
|
+
import { spawn as spawn3 } from "node:child_process";
|
|
15087
15266
|
import { createHash as createHash8 } from "node:crypto";
|
|
15088
|
-
import { mkdir as
|
|
15267
|
+
import { mkdir as mkdir7, open as open5, readFile as readFile6, rm as rm2, writeFile as writeFile4 } from "node:fs/promises";
|
|
15089
15268
|
import { createServer } from "node:net";
|
|
15090
|
-
import { join as
|
|
15269
|
+
import { join as join8 } from "node:path";
|
|
15091
15270
|
var Runtime = class {
|
|
15092
15271
|
constructor(config) {
|
|
15093
15272
|
this.config = config;
|
|
@@ -15103,7 +15282,7 @@ var Runtime = class {
|
|
|
15103
15282
|
return this.endpoint;
|
|
15104
15283
|
}
|
|
15105
15284
|
get endpointStatePath() {
|
|
15106
|
-
return
|
|
15285
|
+
return join8(this.config.stateDirectory, "opencode-endpoint.json");
|
|
15107
15286
|
}
|
|
15108
15287
|
get passwordHash() {
|
|
15109
15288
|
return createHash8("sha256").update(this.config.tool.password).digest("hex");
|
|
@@ -15165,13 +15344,13 @@ var Runtime = class {
|
|
|
15165
15344
|
throw new Error(`OpenCode \u5730\u5740 ${url.origin} \u5DF2\u6709 HTTP \u670D\u52A1\uFF0C\u4F46\u5065\u5EB7\u68C0\u67E5\u5931\u8D25\uFF08HTTP ${response.status}\uFF09\uFF1B\u8BF7\u68C0\u67E5\u670D\u52A1\u5BC6\u7801\u6216\u7AEF\u53E3`);
|
|
15166
15345
|
}
|
|
15167
15346
|
}
|
|
15168
|
-
await
|
|
15169
|
-
const logPath =
|
|
15347
|
+
await mkdir7(this.config.stateDirectory, { recursive: true, mode: 448 });
|
|
15348
|
+
const logPath = join8(this.config.stateDirectory, "opencode-serve.log");
|
|
15170
15349
|
const log = await open5(logPath, "a", 384);
|
|
15171
15350
|
const env = { ...process.env, OPENCODE_SERVER_PASSWORD: this.config.tool.password, OPENCODE_SERVER_USERNAME: "opencode" };
|
|
15172
15351
|
let child;
|
|
15173
15352
|
try {
|
|
15174
|
-
child =
|
|
15353
|
+
child = spawn3(this.config.tool.command, ["serve", "--hostname", url.hostname === "[::1]" ? "::1" : url.hostname, "--port", url.port || "80"], {
|
|
15175
15354
|
cwd: this.config.projects[0].directory,
|
|
15176
15355
|
env,
|
|
15177
15356
|
detached: true,
|
|
@@ -15209,7 +15388,7 @@ var Runtime = class {
|
|
|
15209
15388
|
await new Promise((resolve4) => setTimeout(resolve4, 250));
|
|
15210
15389
|
}
|
|
15211
15390
|
if (child.exitCode === null && child.signalCode === null) child.kill("SIGTERM");
|
|
15212
|
-
await
|
|
15391
|
+
await rm2(this.endpointStatePath, { force: true }).catch(() => {
|
|
15213
15392
|
});
|
|
15214
15393
|
this.endpoint = this.config.tool.baseUrl;
|
|
15215
15394
|
throw new Error(`OpenCode \u542F\u52A8\u5931\u8D25\u6216\u5065\u5EB7\u68C0\u67E5\u8D85\u65F6\uFF1B\u8BF7\u68C0\u67E5 ${logPath}\u3001\u670D\u52A1\u5730\u5740\u548C\u5BC6\u7801`);
|
|
@@ -15258,24 +15437,24 @@ var Runtime = class {
|
|
|
15258
15437
|
|
|
15259
15438
|
// src/tools/zcode-env.ts
|
|
15260
15439
|
import { existsSync } from "node:fs";
|
|
15261
|
-
import { dirname as dirname4, join as
|
|
15440
|
+
import { dirname as dirname4, join as join9 } from "node:path";
|
|
15262
15441
|
function zcodeEnvironment(command) {
|
|
15263
15442
|
const env = { ...process.env };
|
|
15264
15443
|
if (!env.ZCODE_BUILTIN_PROVIDER_CONFIG_FILE) {
|
|
15265
|
-
const bundled =
|
|
15444
|
+
const bundled = join9(dirname4(command), "..", "config", "provider", "zcode-builtin.json");
|
|
15266
15445
|
if (existsSync(bundled)) env.ZCODE_BUILTIN_PROVIDER_CONFIG_FILE = bundled;
|
|
15267
15446
|
}
|
|
15268
15447
|
return env;
|
|
15269
15448
|
}
|
|
15270
15449
|
|
|
15271
15450
|
// src/tools/stdio-runtime.ts
|
|
15272
|
-
import { spawn as
|
|
15451
|
+
import { spawn as spawn4, execFile as execFile4 } from "node:child_process";
|
|
15273
15452
|
import { promisify as promisify4 } from "node:util";
|
|
15274
|
-
import { mkdir as
|
|
15275
|
-
import { join as
|
|
15453
|
+
import { mkdir as mkdir8, open as open6 } from "node:fs/promises";
|
|
15454
|
+
import { join as join10 } from "node:path";
|
|
15276
15455
|
|
|
15277
15456
|
// src/tools/line-rpc.ts
|
|
15278
|
-
import { randomUUID as
|
|
15457
|
+
import { randomUUID as randomUUID12 } from "node:crypto";
|
|
15279
15458
|
import { crc32 } from "node:zlib";
|
|
15280
15459
|
import { StringDecoder } from "node:string_decoder";
|
|
15281
15460
|
function interrupted(message) {
|
|
@@ -15460,7 +15639,7 @@ var LineRpc = class {
|
|
|
15460
15639
|
}
|
|
15461
15640
|
};
|
|
15462
15641
|
function wrapEnvelope(frame) {
|
|
15463
|
-
return { wireVersion: 3, kind: "complete", deliveryKind: "desktop-continuous", logicalFrameId:
|
|
15642
|
+
return { wireVersion: 3, kind: "complete", deliveryKind: "desktop-continuous", logicalFrameId: randomUUID12(), logicalFrameOrdinal: 1, topic: "outbound", subscriptionId: "outbound", frame };
|
|
15464
15643
|
}
|
|
15465
15644
|
|
|
15466
15645
|
// src/tools/stdio-runtime.ts
|
|
@@ -15636,12 +15815,12 @@ ${bytes.toString("utf8")}`);
|
|
|
15636
15815
|
if (inFlight) return inFlight;
|
|
15637
15816
|
if (this.children.has(projectId)) return;
|
|
15638
15817
|
const spawnOnce = (async () => {
|
|
15639
|
-
const logPath =
|
|
15640
|
-
await
|
|
15818
|
+
const logPath = join10(this.config.stateDirectory, `${this.kind}-${projectId}.log`);
|
|
15819
|
+
await mkdir8(this.config.stateDirectory, { recursive: true, mode: 448 });
|
|
15641
15820
|
const log = await open6(logPath, "a", 384);
|
|
15642
15821
|
let child;
|
|
15643
15822
|
try {
|
|
15644
|
-
child =
|
|
15823
|
+
child = spawn4(this.config.tool.command, [...this.descriptor.serverArgs], { cwd: directory, env: this.kind === "zcode" ? zcodeEnvironment(this.config.tool.command) : { ...process.env }, stdio: ["pipe", "pipe", log.fd] });
|
|
15645
15824
|
} catch (error) {
|
|
15646
15825
|
await log.close();
|
|
15647
15826
|
throw error;
|
|
@@ -15690,14 +15869,14 @@ ${bytes.toString("utf8")}`);
|
|
|
15690
15869
|
return;
|
|
15691
15870
|
}
|
|
15692
15871
|
this.attempts.set(projectId, attempts);
|
|
15693
|
-
const
|
|
15872
|
+
const delay2 = Math.min(3e4, 1e3 * 2 ** attempts);
|
|
15694
15873
|
this.timers.set(projectId, setTimeout(() => {
|
|
15695
15874
|
this.timers.delete(projectId);
|
|
15696
15875
|
if (!this.wanted || this.detached) return;
|
|
15697
15876
|
const project = this.config.projects.find((entry) => entry.id === projectId);
|
|
15698
15877
|
if (project) void this.track(projectId, project.directory).catch(() => {
|
|
15699
15878
|
});
|
|
15700
|
-
},
|
|
15879
|
+
}, delay2));
|
|
15701
15880
|
}
|
|
15702
15881
|
kill(projectId) {
|
|
15703
15882
|
clearTimeout(this.timers.get(projectId));
|
|
@@ -15716,7 +15895,7 @@ ${bytes.toString("utf8")}`);
|
|
|
15716
15895
|
};
|
|
15717
15896
|
|
|
15718
15897
|
// src/tools/codex.ts
|
|
15719
|
-
import { join as
|
|
15898
|
+
import { join as join11 } from "node:path";
|
|
15720
15899
|
|
|
15721
15900
|
// src/adapter.ts
|
|
15722
15901
|
var DAEMON_OPERATIONS = [
|
|
@@ -15752,14 +15931,16 @@ var DAEMON_OPERATIONS = [
|
|
|
15752
15931
|
"terminal.attach",
|
|
15753
15932
|
"desktop.list",
|
|
15754
15933
|
"desktop.capture",
|
|
15755
|
-
"desktop.read"
|
|
15934
|
+
"desktop.read",
|
|
15935
|
+
"desktop.lockStatus",
|
|
15936
|
+
"desktop.unlock"
|
|
15756
15937
|
];
|
|
15757
15938
|
function unsupportedOperation(kind, operation) {
|
|
15758
15939
|
throw new Error(`Unsupported ${kind} operation: ${operation}`);
|
|
15759
15940
|
}
|
|
15760
15941
|
|
|
15761
15942
|
// src/tools/codex.ts
|
|
15762
|
-
import { randomUUID as
|
|
15943
|
+
import { randomUUID as randomUUID13 } from "node:crypto";
|
|
15763
15944
|
var OPERATIONS = [
|
|
15764
15945
|
...DAEMON_OPERATIONS,
|
|
15765
15946
|
...AUTH_OPERATIONS,
|
|
@@ -15854,7 +16035,7 @@ var CodexAdapter = class {
|
|
|
15854
16035
|
}
|
|
15855
16036
|
onServerRequest(project, id, method, params, respond) {
|
|
15856
16037
|
const detail = params ?? {};
|
|
15857
|
-
const requestId =
|
|
16038
|
+
const requestId = randomUUID13();
|
|
15858
16039
|
const threadId = String(detail.threadId ?? "");
|
|
15859
16040
|
if (["item/permissions/requestApproval", "item/commandExecution/requestApproval", "item/fileChange/requestApproval"].includes(method)) {
|
|
15860
16041
|
this.approvals.set(requestId, { project, method, requestId, threadId, detail, respond });
|
|
@@ -15869,21 +16050,21 @@ var CodexAdapter = class {
|
|
|
15869
16050
|
respond({ decision: "decline" });
|
|
15870
16051
|
}
|
|
15871
16052
|
async index(project) {
|
|
15872
|
-
const stored = await readJson(
|
|
16053
|
+
const stored = await readJson(join11(this.runtime.config.stateDirectory, `codex-${project}.json`));
|
|
15873
16054
|
return { threadIds: stored?.threadIds ?? [] };
|
|
15874
16055
|
}
|
|
15875
16056
|
async remember(project, threadId) {
|
|
15876
16057
|
const index = await this.index(project);
|
|
15877
16058
|
if (!index.threadIds.includes(threadId)) {
|
|
15878
16059
|
index.threadIds.push(threadId);
|
|
15879
|
-
await atomicJson(
|
|
16060
|
+
await atomicJson(join11(this.runtime.config.stateDirectory, `codex-${project}.json`), index);
|
|
15880
16061
|
}
|
|
15881
16062
|
}
|
|
15882
16063
|
async forget(project, threadId) {
|
|
15883
16064
|
const index = await this.index(project);
|
|
15884
16065
|
if (index.threadIds.includes(threadId)) {
|
|
15885
16066
|
index.threadIds = index.threadIds.filter((entry) => entry !== threadId);
|
|
15886
|
-
await atomicJson(
|
|
16067
|
+
await atomicJson(join11(this.runtime.config.stateDirectory, `codex-${project}.json`), index);
|
|
15887
16068
|
}
|
|
15888
16069
|
}
|
|
15889
16070
|
async resume(project, threadId, rpc) {
|
|
@@ -16094,7 +16275,10 @@ var CodexAdapter = class {
|
|
|
16094
16275
|
if (cursor && seenCursors.has(cursor)) break;
|
|
16095
16276
|
if (cursor) seenCursors.add(cursor);
|
|
16096
16277
|
} while (cursor && models.length < 500);
|
|
16097
|
-
|
|
16278
|
+
const config = await rpc.request("config/read", { cwd: await this.workspace.root(project), includeLayers: false }).catch(() => void 0);
|
|
16279
|
+
const configured = typeof config?.config?.model === "string" ? config.config.model.trim() : void 0;
|
|
16280
|
+
if (configured && !models.some((entry) => (entry.model ?? entry.id) === configured)) models.push({ id: configured, model: configured, displayName: configured });
|
|
16281
|
+
return { providers: [{ id: "codex", models: Object.fromEntries(models.map((entry) => [entry.model ?? entry.id, { id: entry.model ?? entry.id, name: entry.displayName ?? entry.id, isDefault: configured ? (entry.model ?? entry.id) === configured : entry.isDefault === true }])) }] };
|
|
16098
16282
|
}
|
|
16099
16283
|
if (op === "sessions.list") {
|
|
16100
16284
|
const index = await this.index(project);
|
|
@@ -16223,8 +16407,8 @@ var CodexAdapter = class {
|
|
|
16223
16407
|
};
|
|
16224
16408
|
|
|
16225
16409
|
// src/tools/zcode.ts
|
|
16226
|
-
import { spawn as
|
|
16227
|
-
import { randomUUID as
|
|
16410
|
+
import { spawn as spawn5 } from "node:child_process";
|
|
16411
|
+
import { randomUUID as randomUUID14 } from "node:crypto";
|
|
16228
16412
|
import { realpath as realpath5 } from "node:fs/promises";
|
|
16229
16413
|
var OPERATIONS2 = [
|
|
16230
16414
|
...DAEMON_OPERATIONS,
|
|
@@ -16279,7 +16463,7 @@ var ZcodeAdapter = class {
|
|
|
16279
16463
|
}
|
|
16280
16464
|
onServerRequest(project, id, method, params, respond, respondError) {
|
|
16281
16465
|
const detail = params ?? {};
|
|
16282
|
-
const requestId =
|
|
16466
|
+
const requestId = randomUUID14();
|
|
16283
16467
|
const sessionId = String(detail.sessionId ?? "");
|
|
16284
16468
|
if (method === "session/requestRuntimePreferences") {
|
|
16285
16469
|
respond({ nativeSearchEnhancementsEnabled: false });
|
|
@@ -16416,7 +16600,7 @@ var ZcodeAdapter = class {
|
|
|
16416
16600
|
const flow = this.auth.create(project, () => {
|
|
16417
16601
|
child?.kill("SIGTERM");
|
|
16418
16602
|
});
|
|
16419
|
-
child =
|
|
16603
|
+
child = spawn5(this.runtime.config.tool.command, ["login", "--no-browser", "--json"], { cwd: await this.workspace.root(project), env: zcodeEnvironment(this.runtime.config.tool.command), stdio: ["ignore", "pipe", "pipe"] });
|
|
16420
16604
|
let output = "", diagnostic = "";
|
|
16421
16605
|
child.stdout?.on("data", (data) => {
|
|
16422
16606
|
output = (output + data.toString()).slice(-65536);
|
|
@@ -16554,8 +16738,8 @@ var ZcodeAdapter = class {
|
|
|
16554
16738
|
};
|
|
16555
16739
|
|
|
16556
16740
|
// src/tools/pi.ts
|
|
16557
|
-
import { randomUUID as
|
|
16558
|
-
import { readFile as readFile7, readdir as readdir3, rm as
|
|
16741
|
+
import { randomUUID as randomUUID15 } from "node:crypto";
|
|
16742
|
+
import { readFile as readFile7, readdir as readdir3, rm as rm3 } from "node:fs/promises";
|
|
16559
16743
|
import { dirname as dirname5 } from "node:path";
|
|
16560
16744
|
var OPERATIONS3 = [
|
|
16561
16745
|
...DAEMON_OPERATIONS,
|
|
@@ -16649,7 +16833,7 @@ var PiAdapter = class {
|
|
|
16649
16833
|
}
|
|
16650
16834
|
onQuestion(project, sessionId, detail) {
|
|
16651
16835
|
if (!["select", "confirm", "input", "editor"].includes(String(detail.method))) return;
|
|
16652
|
-
const id =
|
|
16836
|
+
const id = randomUUID15();
|
|
16653
16837
|
void this.rpc(project, sessionId).then((rpc) => {
|
|
16654
16838
|
if (rpc.exited) return;
|
|
16655
16839
|
const question = { id, project, sessionId, detail, rpc };
|
|
@@ -16736,10 +16920,11 @@ var PiAdapter = class {
|
|
|
16736
16920
|
if (op === "configuration.models") {
|
|
16737
16921
|
const rpc2 = await this.runtime.connect(project);
|
|
16738
16922
|
const { models } = await rpc2.request("get_available_models");
|
|
16923
|
+
const state = await rpc2.request("get_state").catch(() => void 0);
|
|
16739
16924
|
const providers = /* @__PURE__ */ new Map();
|
|
16740
16925
|
for (const model of models) {
|
|
16741
16926
|
const provider = providers.get(model.provider) ?? { id: model.provider, models: /* @__PURE__ */ Object.create(null) };
|
|
16742
|
-
provider.models[model.id] = { id: model.id, name: model.name ?? model.id };
|
|
16927
|
+
provider.models[model.id] = { id: model.id, name: model.name ?? model.id, isDefault: state?.model?.provider === model.provider && state.model.id === model.id };
|
|
16743
16928
|
providers.set(model.provider, provider);
|
|
16744
16929
|
}
|
|
16745
16930
|
return { providers: [...providers.values()] };
|
|
@@ -16754,7 +16939,7 @@ var PiAdapter = class {
|
|
|
16754
16939
|
return sessions.sort((a, b) => b.time.updated - a.time.updated);
|
|
16755
16940
|
}
|
|
16756
16941
|
if (op === "sessions.create") {
|
|
16757
|
-
const session2 = { id:
|
|
16942
|
+
const session2 = { id: randomUUID15(), title: typeof p.title === "string" && p.title.trim() ? p.title : "\u65B0\u4EFB\u52A1", directory: await this.workspace.root(project), time: { created: Date.now(), updated: Date.now() } };
|
|
16758
16943
|
try {
|
|
16759
16944
|
const rpc2 = await this.rpc(project, session2.id);
|
|
16760
16945
|
await rpc2.request("set_session_name", { name: session2.title });
|
|
@@ -16801,8 +16986,8 @@ var PiAdapter = class {
|
|
|
16801
16986
|
this.deleting.add(this.key(project, id));
|
|
16802
16987
|
try {
|
|
16803
16988
|
await this.runtime.closeSession(project, id);
|
|
16804
|
-
await
|
|
16805
|
-
await
|
|
16989
|
+
await rm3(this.runtime.sessionFile(project, id), { force: true });
|
|
16990
|
+
await rm3(this.metadataPath(project, id));
|
|
16806
16991
|
this.clearQuestions(project, id);
|
|
16807
16992
|
} finally {
|
|
16808
16993
|
this.deleting.delete(this.key(project, id));
|
|
@@ -16852,9 +17037,9 @@ var PiAdapter = class {
|
|
|
16852
17037
|
};
|
|
16853
17038
|
|
|
16854
17039
|
// src/tools/pi-runtime.ts
|
|
16855
|
-
import { execFile as execFile5, spawn as
|
|
16856
|
-
import { mkdir as
|
|
16857
|
-
import { join as
|
|
17040
|
+
import { execFile as execFile5, spawn as spawn6 } from "node:child_process";
|
|
17041
|
+
import { mkdir as mkdir9, open as open7 } from "node:fs/promises";
|
|
17042
|
+
import { join as join12 } from "node:path";
|
|
16858
17043
|
import { promisify as promisify5 } from "node:util";
|
|
16859
17044
|
var run3 = promisify5(execFile5);
|
|
16860
17045
|
var PiRuntime = class {
|
|
@@ -16940,7 +17125,7 @@ ${bytes.toString("utf8")}`);
|
|
|
16940
17125
|
sessionFile(projectId, sessionId) {
|
|
16941
17126
|
this.project(projectId);
|
|
16942
17127
|
if (!describeTool("pi").sessionPattern.test(sessionId)) throw new Error("Invalid session id");
|
|
16943
|
-
return
|
|
17128
|
+
return join12(this.config.stateDirectory, "pi-sessions", projectId, `${sessionId}.jsonl`);
|
|
16944
17129
|
}
|
|
16945
17130
|
project(id) {
|
|
16946
17131
|
const project = this.config.projects.find((entry) => entry.id === id);
|
|
@@ -16971,15 +17156,15 @@ ${bytes.toString("utf8")}`);
|
|
|
16971
17156
|
async launch(key, projectId, sessionId, handlers, generation) {
|
|
16972
17157
|
await this.kill(key);
|
|
16973
17158
|
const directory = this.project(projectId).directory;
|
|
16974
|
-
await
|
|
16975
|
-
const logPath =
|
|
17159
|
+
await mkdir9(join12(this.config.stateDirectory, "pi-sessions", projectId), { recursive: true, mode: 448 });
|
|
17160
|
+
const logPath = join12(this.config.stateDirectory, `pi-${key}.log`);
|
|
16976
17161
|
const log = await open7(logPath, "a", 384);
|
|
16977
17162
|
if (generation !== this.generation) {
|
|
16978
17163
|
await log.close();
|
|
16979
17164
|
throw new Error("Pi runtime stopped during startup");
|
|
16980
17165
|
}
|
|
16981
17166
|
const args = [...describeTool("pi").serverArgs, ...sessionId ? ["--session", this.sessionFile(projectId, sessionId)] : ["--no-session"]];
|
|
16982
|
-
const child =
|
|
17167
|
+
const child = spawn6(this.config.tool.command, args, { cwd: directory, env: { ...process.env }, stdio: ["pipe", "pipe", log.fd] });
|
|
16983
17168
|
child.once("close", () => {
|
|
16984
17169
|
void log.close();
|
|
16985
17170
|
});
|
|
@@ -17034,7 +17219,7 @@ function createAdapter(runtime, workspace, uploads, emit) {
|
|
|
17034
17219
|
}
|
|
17035
17220
|
|
|
17036
17221
|
// src/project-registry.ts
|
|
17037
|
-
import { realpath as realpath6, readFile as readFile8, stat as
|
|
17222
|
+
import { realpath as realpath6, readFile as readFile8, stat as stat8 } from "node:fs/promises";
|
|
17038
17223
|
import { isAbsolute as isAbsolute4 } from "node:path";
|
|
17039
17224
|
var ProjectMutationSchema = ProjectSchema.extend({
|
|
17040
17225
|
name: ProjectSchema.shape.name.trim().max(120),
|
|
@@ -17094,7 +17279,7 @@ var ProjectRegistry = class {
|
|
|
17094
17279
|
const parsed = ProjectMutationSchema.parse(payload);
|
|
17095
17280
|
const directory = expandHome(parsed.directory);
|
|
17096
17281
|
if (!isAbsolute4(directory)) throw new Error("\u9879\u76EE\u76EE\u5F55\u5FC5\u987B\u662F Proxy \u4E3B\u673A\u4E0A\u7684\u7EDD\u5BF9\u8DEF\u5F84");
|
|
17097
|
-
const info = await
|
|
17282
|
+
const info = await stat8(directory).catch(() => void 0);
|
|
17098
17283
|
if (!info?.isDirectory()) throw new Error(`\u9879\u76EE\u76EE\u5F55\u4E0D\u5B58\u5728\u6216\u4E0D\u662F\u76EE\u5F55\uFF1A${directory}`);
|
|
17099
17284
|
const canonical = await realpath6(directory);
|
|
17100
17285
|
if (this.config.projects.some((project2) => project2.id === parsed.id)) throw new Error(`\u9879\u76EE ID \u5DF2\u5B58\u5728\uFF1A${parsed.id}`);
|
|
@@ -17114,7 +17299,7 @@ var ProjectRegistry = class {
|
|
|
17114
17299
|
if (index < 0) throw new Error(`\u672A\u77E5\u6388\u6743\u9879\u76EE\uFF1A${parsed.id}`);
|
|
17115
17300
|
const directory = expandHome(parsed.directory);
|
|
17116
17301
|
if (!isAbsolute4(directory)) throw new Error("\u9879\u76EE\u76EE\u5F55\u5FC5\u987B\u662F Proxy \u4E3B\u673A\u4E0A\u7684\u7EDD\u5BF9\u8DEF\u5F84");
|
|
17117
|
-
const info = await
|
|
17302
|
+
const info = await stat8(directory).catch(() => void 0);
|
|
17118
17303
|
if (!info?.isDirectory()) throw new Error(`\u9879\u76EE\u76EE\u5F55\u4E0D\u5B58\u5728\u6216\u4E0D\u662F\u76EE\u5F55\uFF1A${directory}`);
|
|
17119
17304
|
const canonical = await realpath6(directory);
|
|
17120
17305
|
for (const project of this.config.projects) {
|
|
@@ -17161,10 +17346,10 @@ var ProjectRegistry = class {
|
|
|
17161
17346
|
};
|
|
17162
17347
|
|
|
17163
17348
|
// src/terminal.ts
|
|
17164
|
-
import { randomUUID as
|
|
17349
|
+
import { randomUUID as randomUUID16 } from "node:crypto";
|
|
17165
17350
|
import { access } from "node:fs/promises";
|
|
17166
17351
|
import { constants as constants4 } from "node:fs";
|
|
17167
|
-
import { platform as platform3, userInfo as
|
|
17352
|
+
import { platform as platform3, userInfo as userInfo4 } from "node:os";
|
|
17168
17353
|
import * as pty from "node-pty";
|
|
17169
17354
|
var TerminalManager = class {
|
|
17170
17355
|
constructor(workspace, emit) {
|
|
@@ -17189,13 +17374,13 @@ var TerminalManager = class {
|
|
|
17189
17374
|
const cwd = await this.workspace.path(project, typeof p.cwd === "string" ? p.cwd : ".");
|
|
17190
17375
|
const shell = process.env.SHELL || (platform3() === "darwin" ? "/bin/zsh" : "/bin/sh");
|
|
17191
17376
|
await access(shell, constants4.X_OK);
|
|
17192
|
-
const id =
|
|
17377
|
+
const id = randomUUID16();
|
|
17193
17378
|
const terminalProcess = pty.spawn(shell, [], {
|
|
17194
17379
|
name: "xterm-256color",
|
|
17195
17380
|
cols: 80,
|
|
17196
17381
|
rows: 24,
|
|
17197
17382
|
cwd,
|
|
17198
|
-
env: { PATH: process.env.PATH || "/usr/bin:/bin", HOME:
|
|
17383
|
+
env: { PATH: process.env.PATH || "/usr/bin:/bin", HOME: userInfo4().homedir, TERM: "xterm-256color", USER: userInfo4().username }
|
|
17199
17384
|
});
|
|
17200
17385
|
const terminal2 = { id, project, owner, process: terminalProcess, history: "" };
|
|
17201
17386
|
this.terminals.set(id, terminal2);
|
|
@@ -17248,10 +17433,10 @@ var TerminalManager = class {
|
|
|
17248
17433
|
// src/desktop.ts
|
|
17249
17434
|
import { execFile as execFile6 } from "node:child_process";
|
|
17250
17435
|
import { promisify as promisify6 } from "node:util";
|
|
17251
|
-
import { createHash as createHash9, randomUUID as
|
|
17252
|
-
import { chmod as
|
|
17253
|
-
import { join as
|
|
17254
|
-
import { fileURLToPath as
|
|
17436
|
+
import { createHash as createHash9, randomUUID as randomUUID17 } from "node:crypto";
|
|
17437
|
+
import { chmod as chmod4, mkdir as mkdir10, mkdtemp, readFile as readFile9, rename as rename5, rm as rm4, stat as stat9 } from "node:fs/promises";
|
|
17438
|
+
import { join as join13 } from "node:path";
|
|
17439
|
+
import { fileURLToPath as fileURLToPath5 } from "node:url";
|
|
17255
17440
|
var DESKTOP_IMAGE_LIMIT = 20 * 1024 * 1024;
|
|
17256
17441
|
var DESKTOP_CHUNK_LIMIT = 512 * 1024;
|
|
17257
17442
|
var PNG_SIGNATURE = Buffer.from("89504e470d0a1a0a", "hex");
|
|
@@ -17266,7 +17451,7 @@ var executeDesktop = async (file, args) => {
|
|
|
17266
17451
|
throw new Error(`\u684C\u9762\u622A\u5C4F\u5931\u8D25\uFF1A${detail.stderr?.trim().slice(0, 2048) || detail.message}`);
|
|
17267
17452
|
}
|
|
17268
17453
|
};
|
|
17269
|
-
var DESKTOP_PERMISSION_GUIDANCE = "\u8BF7\u5728\u201C\u7CFB\u7EDF\u8BBE\u7F6E \u2192 \u9690\u79C1\u4E0E\u5B89\u5168\u6027 \u2192 \u5C4F\u5E55\u5F55\u5236\uFF08\u6216\u5C4F\u5E55\u4E0E\u7CFB\u7EDF\u97F3\u9891\u5F55\u5236\uFF09\u201D\u4E2D\u6388\u6743 octopus-desktop-capture \
|
|
17454
|
+
var DESKTOP_PERMISSION_GUIDANCE = "\u8BF7\u5728\u201C\u7CFB\u7EDF\u8BBE\u7F6E \u2192 \u9690\u79C1\u4E0E\u5B89\u5168\u6027 \u2192 \u5C4F\u5E55\u5F55\u5236\uFF08\u6216\u5C4F\u5E55\u4E0E\u7CFB\u7EDF\u97F3\u9891\u5F55\u5236\uFF09\u201D\u4E2D\u542F\u7528\u7CFB\u7EDF\u6388\u6743\u5F39\u7A97\u663E\u793A\u7684\u5B9E\u9645\u7A0B\u5E8F\u3002\u7EC8\u7AEF\u8FD0\u884C\u65F6\u53EF\u80FD\u663E\u793A Terminal\u3001iTerm \u6216\u7F16\u8F91\u5668\uFF0C\u540E\u53F0\u670D\u52A1\u53EF\u80FD\u663E\u793A node\uFF1B\u4E0D\u4E00\u5B9A\u4F1A\u51FA\u73B0 octopus-desktop-capture\u3002\u7EC8\u7AEF\u6388\u6743\u4E0D\u4EE3\u8868\u540E\u53F0\u670D\u52A1\u5DF2\u6388\u6743\uFF1B\u8BF7\u5728 Claw \u70B9\u51FB\u201C\u622A\u5C4F\u201D\u89E6\u53D1\u670D\u52A1\u7684\u6743\u9650\u7533\u8BF7\uFF0C\u6309\u5B9E\u9645\u540D\u79F0\u6388\u6743\u540E\u91CD\u542F Proxy\u3002";
|
|
17270
17455
|
async function requestDesktopPermission(stateDirectory) {
|
|
17271
17456
|
return new PlatformDesktop(stateDirectory).requestScreenCapturePermission();
|
|
17272
17457
|
}
|
|
@@ -17316,19 +17501,19 @@ var PlatformDesktop = class {
|
|
|
17316
17501
|
macHelper() {
|
|
17317
17502
|
if (this.helper) return this.helper;
|
|
17318
17503
|
this.helper = (async () => {
|
|
17319
|
-
await
|
|
17320
|
-
const target =
|
|
17321
|
-
const source =
|
|
17322
|
-
const sourceInfo = await
|
|
17504
|
+
await mkdir10(this.stateDirectory, { recursive: true, mode: 448 });
|
|
17505
|
+
const target = join13(this.stateDirectory, "octopus-desktop-capture");
|
|
17506
|
+
const source = fileURLToPath5(new URL("../native/desktop-macos.m", import.meta.url));
|
|
17507
|
+
const sourceInfo = await stat9(source), existing = await stat9(target).catch(() => null);
|
|
17323
17508
|
if (existing?.isFile() && existing.mtimeMs >= sourceInfo.mtimeMs && (existing.mode & 63) === 0) return target;
|
|
17324
|
-
const temporary = `${target}.${
|
|
17509
|
+
const temporary = `${target}.${randomUUID17()}`;
|
|
17325
17510
|
try {
|
|
17326
17511
|
await this.execute("/usr/bin/clang", ["-O2", "-fobjc-arc", "-mmacosx-version-min=12.3", "-framework", "Foundation", "-framework", "AppKit", "-framework", "ScreenCaptureKit", "-framework", "CoreMedia", "-framework", "CoreVideo", "-framework", "VideoToolbox", "-framework", "ImageIO", source, "-o", temporary]);
|
|
17327
|
-
await
|
|
17328
|
-
await
|
|
17512
|
+
await chmod4(temporary, 448);
|
|
17513
|
+
await rename5(temporary, target);
|
|
17329
17514
|
return target;
|
|
17330
17515
|
} finally {
|
|
17331
|
-
await
|
|
17516
|
+
await rm4(temporary, { force: true });
|
|
17332
17517
|
}
|
|
17333
17518
|
})();
|
|
17334
17519
|
this.helper.catch(() => {
|
|
@@ -17364,14 +17549,14 @@ var PlatformDesktop = class {
|
|
|
17364
17549
|
const helper = await this.macHelper();
|
|
17365
17550
|
try {
|
|
17366
17551
|
const displays = await this.list();
|
|
17367
|
-
record2("\u684C\u9762\u622A\u5C4F\u652F\u6301", "ok", `macOS ${version} \u652F\u6301 ScreenCaptureKit\uFF1B${displays.length} \u4E2A\u6D3B\u52A8\u663E\u793A\u5668\uFF1B\u8F85\u52A9\u7A0B\u5E8F\
|
|
17552
|
+
record2("\u684C\u9762\u622A\u5C4F\u652F\u6301", "ok", `macOS ${version} \u652F\u6301 ScreenCaptureKit\uFF1B${displays.length} \u4E2A\u6D3B\u52A8\u663E\u793A\u5668\uFF1B\u8F85\u52A9\u7A0B\u5E8F\uFF1A${helper}\uFF08\u6587\u4EF6\u540D\u4E0D\u4E00\u5B9A\u662F\u7CFB\u7EDF\u6388\u6743\u540D\u79F0\uFF09`);
|
|
17368
17553
|
} catch (error) {
|
|
17369
|
-
record2("\u684C\u9762\u622A\u5C4F\u652F\u6301", "warn", error instanceof Error ? error.message :
|
|
17554
|
+
record2("\u684C\u9762\u622A\u5C4F\u652F\u6301", "warn", `${error instanceof Error ? error.message : error} \u8F85\u52A9\u7A0B\u5E8F\uFF1A${helper}\uFF08\u6587\u4EF6\u540D\u4E0D\u4E00\u5B9A\u662F\u7CFB\u7EDF\u6388\u6743\u540D\u79F0\uFF09`);
|
|
17370
17555
|
}
|
|
17371
17556
|
try {
|
|
17372
17557
|
const permission = (await this.execute(helper, ["permission-status"])).trim();
|
|
17373
17558
|
if (permission !== "granted" && permission !== "denied") throw new Error("\u5C4F\u5E55\u5F55\u5236\u6388\u6743\u68C0\u67E5\u8FD4\u56DE\u4E86\u65E0\u6548\u7ED3\u679C\u3002");
|
|
17374
|
-
record2("\u5C4F\u5E55\u5F55\u5236\u6743\u9650", permission === "granted" ? "ok" : "warn", permission === "granted" ? "\
|
|
17559
|
+
record2("\u5C4F\u5E55\u5F55\u5236\u6743\u9650", permission === "granted" ? "ok" : "warn", permission === "granted" ? "\u5F53\u524D\u542F\u52A8\u7A0B\u5E8F\u5DF2\u6388\u6743\uFF1B\u540E\u53F0\u670D\u52A1\u7684\u6388\u6743\u9700\u5728 Claw \u5B9E\u9645\u622A\u5C4F\u9A8C\u8BC1\u3002" : `\u672A\u6388\u6743\u6216\u6388\u6743\u5C1A\u672A\u751F\u6548\uFF1B\u8FD0\u884C octopus-proxy onboard \u7533\u8BF7\u3002${DESKTOP_PERMISSION_GUIDANCE}`);
|
|
17375
17560
|
} catch (error) {
|
|
17376
17561
|
record2("\u5C4F\u5E55\u5F55\u5236\u6743\u9650", "warn", `\u65E0\u6CD5\u68C0\u67E5\uFF1A${error instanceof Error ? error.message : error} ${DESKTOP_PERMISSION_GUIDANCE}`);
|
|
17377
17562
|
}
|
|
@@ -17412,9 +17597,9 @@ var PlatformDesktop = class {
|
|
|
17412
17597
|
async capture(displayId) {
|
|
17413
17598
|
const display = (await this.list()).find((item) => item.id === displayId);
|
|
17414
17599
|
if (!display) throw new Error("\u6240\u9009\u663E\u793A\u5668\u5DF2\u65AD\u5F00\uFF0C\u8BF7\u5237\u65B0\u663E\u793A\u5668\u5217\u8868\u540E\u91CD\u8BD5\u3002");
|
|
17415
|
-
await
|
|
17416
|
-
const directory = await mkdtemp(
|
|
17417
|
-
const output =
|
|
17600
|
+
await mkdir10(this.stateDirectory, { recursive: true, mode: 448 });
|
|
17601
|
+
const directory = await mkdtemp(join13(this.stateDirectory, "desktop-capture-"));
|
|
17602
|
+
const output = join13(directory, "screen.png");
|
|
17418
17603
|
try {
|
|
17419
17604
|
if (this.system === "darwin") {
|
|
17420
17605
|
await this.execute(await this.macHelper(), ["capture", display.id, output]);
|
|
@@ -17432,24 +17617,39 @@ var PlatformDesktop = class {
|
|
|
17432
17617
|
await this.execute("import", args);
|
|
17433
17618
|
}
|
|
17434
17619
|
}
|
|
17435
|
-
const info = await
|
|
17620
|
+
const info = await stat9(output);
|
|
17436
17621
|
if (!info.isFile() || !info.size || info.size > DESKTOP_IMAGE_LIMIT) throw new Error("\u684C\u9762\u622A\u56FE\u4E3A\u7A7A\u6216\u8D85\u8FC7 20 MiB \u4E0A\u9650\u3002");
|
|
17437
17622
|
return { display, bytes: await readFile9(output) };
|
|
17438
17623
|
} finally {
|
|
17439
|
-
await
|
|
17624
|
+
await rm4(directory, { recursive: true, force: true });
|
|
17440
17625
|
}
|
|
17441
17626
|
}
|
|
17442
17627
|
};
|
|
17443
17628
|
var DesktopManager = class {
|
|
17444
|
-
constructor(backend) {
|
|
17629
|
+
constructor(backend, screenLock) {
|
|
17445
17630
|
this.backend = backend;
|
|
17631
|
+
this.screenLock = screenLock;
|
|
17446
17632
|
}
|
|
17447
17633
|
captures = /* @__PURE__ */ new Map();
|
|
17448
17634
|
pending = /* @__PURE__ */ new Map();
|
|
17635
|
+
unlocking;
|
|
17449
17636
|
closed = false;
|
|
17450
17637
|
async dispatch(command, owner) {
|
|
17451
17638
|
if (this.closed) throw new Error("\u684C\u9762\u622A\u5C4F\u670D\u52A1\u5DF2\u505C\u6B62\u3002");
|
|
17452
17639
|
const p = command.payload;
|
|
17640
|
+
if (command.operation === "desktop.lockStatus" || command.operation === "desktop.unlock") {
|
|
17641
|
+
if (!this.screenLock) throw new ScreenLockError("\u6B64 Proxy \u4E0D\u652F\u6301\u5C4F\u5E55\u89E3\u9501\uFF0C\u8BF7\u5347\u7EA7 Proxy\u3002");
|
|
17642
|
+
if (command.operation === "desktop.lockStatus") return this.screenLock.status();
|
|
17643
|
+
if (typeof p.password !== "string" || !p.password || Buffer.byteLength(p.password) > 1024 || /[\x00-\x1f\x7f]/.test(p.password)) throw new ScreenLockError("\u8BF7\u8F93\u5165\u6709\u6548\u7684\u4E3B\u673A\u767B\u5F55\u5BC6\u7801\uFF08\u6700\u591A 1024 \u5B57\u8282\uFF0C\u4E0D\u80FD\u5305\u542B\u63A7\u5236\u5B57\u7B26\uFF09\u3002");
|
|
17644
|
+
if (this.unlocking) throw new ScreenLockError("\u6B63\u5728\u89E3\u9501\u4E3B\u673A\u5C4F\u5E55\uFF0C\u8BF7\u7A0D\u540E\u91CD\u8BD5\u3002");
|
|
17645
|
+
const pending = { owner, abort: new AbortController() };
|
|
17646
|
+
this.unlocking = pending;
|
|
17647
|
+
try {
|
|
17648
|
+
return await this.screenLock.unlock(p.password, pending.abort.signal);
|
|
17649
|
+
} finally {
|
|
17650
|
+
if (this.unlocking === pending) this.unlocking = void 0;
|
|
17651
|
+
}
|
|
17652
|
+
}
|
|
17453
17653
|
if (command.operation === "desktop.list") return this.backend.list();
|
|
17454
17654
|
if (command.operation === "desktop.capture") {
|
|
17455
17655
|
if (typeof p.displayId !== "string" || !p.displayId) throw new Error("\u8BF7\u9009\u62E9\u663E\u793A\u5668\u3002");
|
|
@@ -17463,7 +17663,7 @@ var DesktopManager = class {
|
|
|
17463
17663
|
if (bytes.length < 24 || !bytes.subarray(0, 8).equals(PNG_SIGNATURE) || bytes.toString("ascii", 12, 16) !== "IHDR") throw new Error("\u622A\u5C4F\u672A\u8FD4\u56DE\u6709\u6548\u7684 PNG \u56FE\u7247\u3002");
|
|
17464
17664
|
const width = bytes.readUInt32BE(16), height = bytes.readUInt32BE(20);
|
|
17465
17665
|
if (!width || !height) throw new Error("\u684C\u9762\u622A\u56FE\u5C3A\u5BF8\u65E0\u6548\u3002");
|
|
17466
|
-
const info = { id:
|
|
17666
|
+
const info = { id: randomUUID17(), display: publicDisplay(display), capturedAt: (/* @__PURE__ */ new Date()).toISOString(), mime: "image/png", width, height, size: bytes.length, sha256: createHash9("sha256").update(bytes).digest("hex") };
|
|
17467
17667
|
this.clearCapture(owner);
|
|
17468
17668
|
const timer = setTimeout(() => this.clearCapture(owner), 6e4);
|
|
17469
17669
|
timer.unref();
|
|
@@ -17493,9 +17693,11 @@ var DesktopManager = class {
|
|
|
17493
17693
|
closeOwned(owner) {
|
|
17494
17694
|
this.pending.delete(owner);
|
|
17495
17695
|
this.clearCapture(owner);
|
|
17696
|
+
if (this.unlocking?.owner === owner) this.unlocking.abort.abort();
|
|
17496
17697
|
}
|
|
17497
17698
|
close() {
|
|
17498
17699
|
this.closed = true;
|
|
17700
|
+
this.unlocking?.abort.abort();
|
|
17499
17701
|
this.pending.clear();
|
|
17500
17702
|
for (const owner of this.captures.keys()) this.clearCapture(owner);
|
|
17501
17703
|
}
|
|
@@ -17504,15 +17706,15 @@ var DesktopManager = class {
|
|
|
17504
17706
|
// src/daemon.ts
|
|
17505
17707
|
async function runDaemon(configPath, authenticate = verifyHostPassword) {
|
|
17506
17708
|
const config = await loadConfig(configPath);
|
|
17507
|
-
await
|
|
17508
|
-
const workspace = new Workspace(config.projects), runtime = createRuntime(config), uploads = new Uploads(workspace,
|
|
17709
|
+
await mkdir11(config.stateDirectory, { recursive: true, mode: 448 });
|
|
17710
|
+
const workspace = new Workspace(config.projects), runtime = createRuntime(config), uploads = new Uploads(workspace, join14(config.stateDirectory, "uploads")), journal = new Journal(join14(config.stateDirectory, "journal"));
|
|
17509
17711
|
let secureChat;
|
|
17510
17712
|
function emit(projectId, event, data) {
|
|
17511
17713
|
secureChat?.publish(projectId, event, data);
|
|
17512
17714
|
}
|
|
17513
17715
|
const terminals = new TerminalManager(workspace, (owner, project, event, data) => secureChat?.sendTo(owner, project, event, data));
|
|
17514
|
-
const desktop = new DesktopManager(new PlatformDesktop(config.stateDirectory));
|
|
17515
|
-
const adapter = createAdapter(runtime, workspace, uploads, emit), artifacts = new Artifacts(workspace,
|
|
17716
|
+
const desktop = new DesktopManager(new PlatformDesktop(config.stateDirectory), new PlatformScreenLock(config.stateDirectory));
|
|
17717
|
+
const adapter = createAdapter(runtime, workspace, uploads, emit), artifacts = new Artifacts(workspace, join14(config.stateDirectory, "artifacts"), emit);
|
|
17516
17718
|
const projectRegistry = new ProjectRegistry(configPath, config, runtime, adapter);
|
|
17517
17719
|
async function dispatch(c, secureSessionId) {
|
|
17518
17720
|
if (c.operation.startsWith("desktop.")) return desktop.dispatch(c, secureSessionId);
|
package/dist/cli.js
CHANGED
|
@@ -27,7 +27,7 @@ import {
|
|
|
27
27
|
runDaemon,
|
|
28
28
|
supportedPlatform,
|
|
29
29
|
systemdUnitPath
|
|
30
|
-
} from "./chunk-
|
|
30
|
+
} from "./chunk-6DTK4JVV.js";
|
|
31
31
|
|
|
32
32
|
// src/cli.ts
|
|
33
33
|
import { spawn } from "node:child_process";
|
|
@@ -456,7 +456,7 @@ import { stdin, stdout } from "node:process";
|
|
|
456
456
|
import { access as access3, constants as constants2, mkdir as mkdir4, stat as stat3 } from "node:fs/promises";
|
|
457
457
|
import { execFile as execFile5 } from "node:child_process";
|
|
458
458
|
import { platform as platform2 } from "node:os";
|
|
459
|
-
import { isAbsolute as isAbsolute3 } from "node:path";
|
|
459
|
+
import { isAbsolute as isAbsolute3, join as join3 } from "node:path";
|
|
460
460
|
import { promisify as promisify5 } from "node:util";
|
|
461
461
|
var DEFAULT_HUB_URL = "https://octopus.wuyehao.com";
|
|
462
462
|
async function ask(rl, question, options = {}) {
|
|
@@ -645,9 +645,12 @@ async function runOnboard(opts) {
|
|
|
645
645
|
stateDirectory = expandHome(stateDirectory);
|
|
646
646
|
await mkdir4(stateDirectory, { recursive: true, mode: 448 });
|
|
647
647
|
try {
|
|
648
|
-
if (platform2() === "darwin")
|
|
648
|
+
if (platform2() === "darwin") {
|
|
649
|
+
console.log(`\u622A\u5C4F\u8F85\u52A9\u7A0B\u5E8F\u8DEF\u5F84\uFF1A${join3(stateDirectory, "octopus-desktop-capture")}\uFF08\u6587\u4EF6\u540D\u4E0D\u4E00\u5B9A\u662F\u7CFB\u7EDF\u6388\u6743\u540D\u79F0\uFF09`);
|
|
650
|
+
console.log("\u6B63\u5728\u68C0\u67E5\u5C4F\u5E55\u5F55\u5236\u6743\u9650\uFF1B\u5982\u51FA\u73B0\u7CFB\u7EDF\u6388\u6743\u63D0\u793A\uFF0C\u8BF7\u6309\u5F39\u7A97\u4E2D\u7684\u5B9E\u9645\u7A0B\u5E8F\u540D\u79F0\u6388\u6743\uFF0C\u53EF\u80FD\u663E\u793A Terminal\u3001iTerm\u3001\u7F16\u8F91\u5668\u6216 node\u3002");
|
|
651
|
+
}
|
|
649
652
|
const permission = await requestDesktopPermission(stateDirectory);
|
|
650
|
-
if (permission === "granted") console.log("\u5C4F\u5E55\u5F55\u5236\u6743\u9650\uFF1A\u5DF2\u6388\u6743\
|
|
653
|
+
if (permission === "granted") console.log("\u5C4F\u5E55\u5F55\u5236\u6743\u9650\uFF1A\u5F53\u524D\u542F\u52A8\u7A0B\u5E8F\u5DF2\u6388\u6743\uFF1B\u540E\u53F0\u670D\u52A1\u7684\u6388\u6743\u9700\u5728 Claw \u7684\u201C\u5FEB\u7167\u201D\u4E2D\u5B9E\u9645\u622A\u5C4F\u9A8C\u8BC1\u3002");
|
|
651
654
|
else if (permission === "denied") console.log(`\u8B66\u544A\uFF1A\u5C1A\u672A\u83B7\u5F97\u5C4F\u5E55\u5F55\u5236\u6743\u9650\uFF0C\u521D\u59CB\u5316\u5C06\u7EE7\u7EED\u3002${DESKTOP_PERMISSION_GUIDANCE}`);
|
|
652
655
|
} catch (error) {
|
|
653
656
|
console.log(`\u8B66\u544A\uFF1A\u65E0\u6CD5\u7533\u8BF7\u5C4F\u5E55\u5F55\u5236\u6743\u9650\uFF0C\u521D\u59CB\u5316\u5C06\u7EE7\u7EED\uFF1A${error instanceof Error ? error.message : error} ${DESKTOP_PERMISSION_GUIDANCE}`);
|
package/dist/main.js
CHANGED
package/native/desktop-macos.m
CHANGED
|
@@ -13,7 +13,7 @@ static void fail(NSString *message) {
|
|
|
13
13
|
}
|
|
14
14
|
|
|
15
15
|
static NSString *permissionMessage(void) {
|
|
16
|
-
return @"无法读取桌面。请在 Proxy 主机的“系统设置 → 隐私与安全性 →
|
|
16
|
+
return @"无法读取桌面。请在 Proxy 主机的“系统设置 → 隐私与安全性 → 屏幕录制(或屏幕与系统音频录制)”中启用系统弹窗显示的实际程序。终端运行时可能显示 Terminal、iTerm 或编辑器,后台服务可能显示 node;不一定会出现 octopus-desktop-capture。请按此次截屏请求的实际程序名称授权,重启 Proxy 后重试。";
|
|
17
17
|
}
|
|
18
18
|
|
|
19
19
|
static NSDictionary *displayInfo(CGDirectDisplayID displayId) {
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
#import <AppKit/AppKit.h>
|
|
2
|
+
#import <ApplicationServices/ApplicationServices.h>
|
|
3
|
+
#include <unistd.h>
|
|
4
|
+
#include <string.h>
|
|
5
|
+
|
|
6
|
+
static void wipe(void *buffer, size_t length) {
|
|
7
|
+
volatile unsigned char *bytes = buffer;
|
|
8
|
+
while (length--) *bytes++ = 0;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
static int fail(const char *code) { fprintf(stderr, "%s\n", code); return 1; }
|
|
12
|
+
|
|
13
|
+
// This WindowServer property is not a documented public lock-state API. Fail
|
|
14
|
+
// closed when the session is unavailable or is not this user's console desktop.
|
|
15
|
+
static BOOL lockState(BOOL *locked) {
|
|
16
|
+
NSDictionary *session = CFBridgingRelease(CGSessionCopyCurrentDictionary());
|
|
17
|
+
if (!session || ![session[(__bridge NSString *)kCGSessionOnConsoleKey] boolValue] ||
|
|
18
|
+
![session[(__bridge NSString *)kCGSessionLoginDoneKey] boolValue] ||
|
|
19
|
+
![session[(__bridge NSString *)kCGSessionUserIDKey] isKindOfClass:NSNumber.class] ||
|
|
20
|
+
[session[(__bridge NSString *)kCGSessionUserIDKey] unsignedIntValue] != getuid()) return NO;
|
|
21
|
+
id value = session[@"CGSSessionScreenIsLocked"];
|
|
22
|
+
if (value && ![value isKindOfClass:NSNumber.class]) return NO;
|
|
23
|
+
*locked = [value boolValue];
|
|
24
|
+
return YES;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
static void printStatus(BOOL locked) {
|
|
28
|
+
printf("{\"locked\":%s,\"canUnlock\":%s}", locked ? "true" : "false",
|
|
29
|
+
AXIsProcessTrusted() && CGPreflightPostEventAccess() ? "true" : "false");
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
static AXUIElementRef passwordFocus(void) {
|
|
33
|
+
AXUIElementRef system = AXUIElementCreateSystemWide();
|
|
34
|
+
AXUIElementSetMessagingTimeout(system, 0.5);
|
|
35
|
+
CFTypeRef focused = NULL;
|
|
36
|
+
AXError result = AXUIElementCopyAttributeValue(system, kAXFocusedUIElementAttribute, &focused);
|
|
37
|
+
CFRelease(system);
|
|
38
|
+
if (result != kAXErrorSuccess || !focused || CFGetTypeID(focused) != AXUIElementGetTypeID()) {
|
|
39
|
+
if (focused) CFRelease(focused);
|
|
40
|
+
return NULL;
|
|
41
|
+
}
|
|
42
|
+
pid_t pid = 0;
|
|
43
|
+
AXUIElementRef field = (AXUIElementRef)focused;
|
|
44
|
+
AXUIElementSetMessagingTimeout(field, 0.5);
|
|
45
|
+
NSRunningApplication *app = nil;
|
|
46
|
+
if (AXUIElementGetPid(field, &pid) == kAXErrorSuccess) app = [NSRunningApplication runningApplicationWithProcessIdentifier:pid];
|
|
47
|
+
CFTypeRef subrole = NULL;
|
|
48
|
+
AXUIElementCopyAttributeValue(field, kAXSubroleAttribute, &subrole);
|
|
49
|
+
BOOL secure = subrole && CFEqual(subrole, kAXSecureTextFieldSubrole);
|
|
50
|
+
if (subrole) CFRelease(subrole);
|
|
51
|
+
if (!secure || ![app.bundleIdentifier isEqualToString:@"com.apple.loginwindow"] ||
|
|
52
|
+
![app.executableURL.path isEqualToString:@"/System/Library/CoreServices/loginwindow.app/Contents/MacOS/loginwindow"]) {
|
|
53
|
+
CFRelease(field); return NULL;
|
|
54
|
+
}
|
|
55
|
+
return field;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
static BOOL stillFocused(AXUIElementRef field) {
|
|
59
|
+
BOOL locked = NO;
|
|
60
|
+
if (!lockState(&locked) || !locked) return NO;
|
|
61
|
+
AXUIElementRef current = passwordFocus();
|
|
62
|
+
BOOL same = current && CFEqual(current, field);
|
|
63
|
+
if (current) CFRelease(current);
|
|
64
|
+
return same;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
static BOOL key(CGKeyCode code, CGEventFlags flags, const UniChar *text, size_t length) {
|
|
68
|
+
CGEventRef down = CGEventCreateKeyboardEvent(NULL, code, true);
|
|
69
|
+
CGEventRef up = CGEventCreateKeyboardEvent(NULL, code, false);
|
|
70
|
+
if (!down || !up) { if (down) CFRelease(down); if (up) CFRelease(up); return NO; }
|
|
71
|
+
CGEventSetFlags(down, flags); CGEventSetFlags(up, flags);
|
|
72
|
+
if (text) { CGEventKeyboardSetUnicodeString(down, length, text); CGEventKeyboardSetUnicodeString(up, length, text); }
|
|
73
|
+
CGEventPost(kCGHIDEventTap, down); CGEventPost(kCGHIDEventTap, up);
|
|
74
|
+
CFRelease(down); CFRelease(up);
|
|
75
|
+
usleep(20000);
|
|
76
|
+
return YES;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
int main(int argc, const char *argv[]) {
|
|
80
|
+
@autoreleasepool {
|
|
81
|
+
if (getuid() == 0 || argc != 2) return fail("session");
|
|
82
|
+
BOOL locked = NO;
|
|
83
|
+
if (!lockState(&locked)) return fail("session");
|
|
84
|
+
if (strcmp(argv[1], "status") == 0) { printStatus(locked); return 0; }
|
|
85
|
+
if (strcmp(argv[1], "unlock") != 0) return fail("session");
|
|
86
|
+
if (!locked) { printStatus(NO); return 0; }
|
|
87
|
+
if (!AXIsProcessTrusted() || !CGPreflightPostEventAccess()) return fail("permission");
|
|
88
|
+
unsigned char header[2], bytes[1025] = {0};
|
|
89
|
+
if (fread(header, 1, 2, stdin) != 2) return fail("password");
|
|
90
|
+
size_t count = ((size_t)header[0] << 8) | header[1];
|
|
91
|
+
if (!count || count > 1024 || fread(bytes, 1, count, stdin) != count) { wipe(bytes, sizeof(bytes)); return fail("password"); }
|
|
92
|
+
NSString *password = [[NSString alloc] initWithBytes:bytes length:count encoding:NSUTF8StringEncoding];
|
|
93
|
+
wipe(bytes, sizeof(bytes));
|
|
94
|
+
if (!password || [password rangeOfCharacterFromSet:NSCharacterSet.controlCharacterSet].location != NSNotFound) return fail("password");
|
|
95
|
+
// A non-text key wakes the password prompt. Never type a secret into an
|
|
96
|
+
// arbitrary app, username field, clipboard, or a blind screen coordinate.
|
|
97
|
+
if (!lockState(&locked) || !locked) return fail("changed");
|
|
98
|
+
if (!key(56, 0, NULL, 0)) return fail("focus");
|
|
99
|
+
AXUIElementRef field = NULL;
|
|
100
|
+
for (int i = 0; i < 20 && !field; i++) { usleep(100000); field = passwordFocus(); }
|
|
101
|
+
if (!field) return fail("focus");
|
|
102
|
+
if (!stillFocused(field) || !key(0, kCGEventFlagMaskCommand, NULL, 0) ||
|
|
103
|
+
!stillFocused(field) || !key(51, 0, NULL, 0)) { CFRelease(field); return fail("changed"); }
|
|
104
|
+
UniChar characters[1024] = {0};
|
|
105
|
+
NSUInteger length = password.length;
|
|
106
|
+
if (length > 1024) { CFRelease(field); return fail("password"); }
|
|
107
|
+
[password getCharacters:characters range:NSMakeRange(0, length)];
|
|
108
|
+
password = nil;
|
|
109
|
+
BOOL sent = YES;
|
|
110
|
+
// One Unicode scalar per event avoids event-string truncation. Check the
|
|
111
|
+
// secure loginwindow focus and console lock immediately before each event.
|
|
112
|
+
for (NSUInteger i = 0; i < length;) {
|
|
113
|
+
NSUInteger units = CFStringIsSurrogateHighCharacter(characters[i]) && i + 1 < length && CFStringIsSurrogateLowCharacter(characters[i + 1]) ? 2 : 1;
|
|
114
|
+
if (!stillFocused(field) || !key(0, 0, &characters[i], units)) { sent = NO; break; }
|
|
115
|
+
i += units;
|
|
116
|
+
}
|
|
117
|
+
wipe(characters, sizeof(characters));
|
|
118
|
+
if (!sent || !stillFocused(field) || !key(36, 0, NULL, 0)) { CFRelease(field); return fail("changed"); }
|
|
119
|
+
CFRelease(field);
|
|
120
|
+
for (int i = 0; i < 50; i++) {
|
|
121
|
+
usleep(100000);
|
|
122
|
+
if (!lockState(&locked)) return fail("session");
|
|
123
|
+
if (!locked) { printStatus(NO); return 0; }
|
|
124
|
+
}
|
|
125
|
+
return fail("timeout");
|
|
126
|
+
}
|
|
127
|
+
}
|