@skylandnpm/octopus-proxy 0.1.33 → 0.1.34

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -18,8 +18,12 @@ 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` 也会申请。请在系统弹窗中允许访问;拒绝、超时或辅助程序编译失败时,向导提示原因与授权路径,并继续初始化。之后可在“系统设置 → 隐私与安全性 → 屏幕录制(或屏幕与系统音频录制)”中授权 `octopus-desktop-capture` 或系统列出的启动程序,再重启 Proxy。Linux 没有通用的截屏授权弹窗,需使用桌面用户的图形会话环境;平台依赖与设置见[桌面快照说明](../../docs/claw-desktop-snapshots.md)。
22
+
21
23
  添加授权项目时,填写项目 ID 后会询问是否继续添加;直接回车即可结束添加并继续保存配置。
22
24
 
25
+ `octopus-proxy doctor` 会显示“桌面截屏支持”和“屏幕录制权限”:macOS 检查系统版本、辅助程序、活动显示器和当前授权状态;未授权时给出 onboard 申请及系统设置指引。Linux 检查图形会话、显示器和截屏依赖,缺少依赖、无桌面或不支持的 Wayland 环境会显示具体提示;Linux 无统一的授权查询接口,因此会明确显示权限尚无法确认,需在 Claw 实际截屏验证。诊断不触发授权弹窗、不截屏,`doctor --fix` 也不会自动申请截屏权限。这些检查使用运行 doctor 的用户与环境;服务以其他启动程序或图形会话运行时,其权限与环境需要单独确认。
26
+
23
27
  如果旧配置中的项目 ID 重复或有错误项,先列出项目,再按序号删除指定项。此命令直接读取原始配置,因此配置校验失败时也能使用;删除前会生成仅当前用户可读的备份。清理完成后检查配置并重启服务:
24
28
 
25
29
  ```sh
@@ -11714,8 +11714,8 @@ async function ensureHostIdentity(stateDirectory) {
11714
11714
  }
11715
11715
 
11716
11716
  // src/daemon.ts
11717
- import { mkdir as mkdir9 } from "node:fs/promises";
11718
- import { join as join12 } from "node:path";
11717
+ import { mkdir as mkdir10 } from "node:fs/promises";
11718
+ import { join as join13 } from "node:path";
11719
11719
 
11720
11720
  // src/workspace-diff.ts
11721
11721
  import { execFile as execFile2 } from "node:child_process";
@@ -12247,6 +12247,9 @@ var Operations = [
12247
12247
  "terminal.remove",
12248
12248
  "terminal.input",
12249
12249
  "terminal.attach",
12250
+ "desktop.list",
12251
+ "desktop.capture",
12252
+ "desktop.read",
12250
12253
  "artifacts.list",
12251
12254
  "artifacts.history",
12252
12255
  "artifacts.read",
@@ -12266,7 +12269,7 @@ var CommandSchema = z.object({
12266
12269
  sessionId: z.string().optional(),
12267
12270
  payload: z.record(z.unknown()).default({})
12268
12271
  }).strict();
12269
- var isReadOperation = (operation) => /\.(status|snapshot|list|get|messages|diff|todos|children|logs|projects|read|search|history|models|agents|commands|providers|mcp)$/.test(operation);
12272
+ 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);
12270
12273
 
12271
12274
  // src/journal.ts
12272
12275
  var Journal = class {
@@ -12275,8 +12278,9 @@ var Journal = class {
12275
12278
  }
12276
12279
  active = /* @__PURE__ */ new Map();
12277
12280
  queues = /* @__PURE__ */ new Map();
12278
- execute(command, action) {
12279
- const hash = createHash4("sha256").update(JSON.stringify(command)).digest("hex"), active = this.active.get(command.id);
12281
+ execute(command, action, owner) {
12282
+ const activeKey = command.operation.startsWith("desktop.") ? JSON.stringify([owner, command.id]) : command.id;
12283
+ const hash = createHash4("sha256").update(JSON.stringify(command)).digest("hex"), active = this.active.get(activeKey);
12280
12284
  if (active) {
12281
12285
  if (active.hash !== hash) return Promise.resolve(this.failure(command, "Request id reused"));
12282
12286
  return active.result;
@@ -12312,8 +12316,8 @@ var Journal = class {
12312
12316
  }).catch(() => {
12313
12317
  });
12314
12318
  }
12315
- this.active.set(command.id, { hash, result });
12316
- result.finally(() => this.active.delete(command.id)).catch(() => {
12319
+ this.active.set(activeKey, { hash, result });
12320
+ result.finally(() => this.active.delete(activeKey)).catch(() => {
12317
12321
  });
12318
12322
  return result;
12319
12323
  }
@@ -12841,7 +12845,7 @@ var SecureChat = class {
12841
12845
  }
12842
12846
  session.lastAction = Date.now();
12843
12847
  sendJson(session.tls, { type: "response", id: command.id, botId: command.botId, status: "accepted" });
12844
- const response = await this.journal.execute(command, () => this.dispatch(command, id));
12848
+ const response = await this.journal.execute(command, () => this.dispatch(command, id), id);
12845
12849
  if (this.sessions.get(id) === session && session.authenticated) sendJson(session.tls, response);
12846
12850
  }
12847
12851
  closeSession(id) {
@@ -15745,7 +15749,10 @@ var DAEMON_OPERATIONS = [
15745
15749
  "terminal.resize",
15746
15750
  "terminal.remove",
15747
15751
  "terminal.input",
15748
- "terminal.attach"
15752
+ "terminal.attach",
15753
+ "desktop.list",
15754
+ "desktop.capture",
15755
+ "desktop.read"
15749
15756
  ];
15750
15757
  function unsupportedOperation(kind, operation) {
15751
15758
  throw new Error(`Unsupported ${kind} operation: ${operation}`);
@@ -17238,19 +17245,277 @@ var TerminalManager = class {
17238
17245
  }
17239
17246
  };
17240
17247
 
17248
+ // src/desktop.ts
17249
+ import { execFile as execFile6 } from "node:child_process";
17250
+ import { promisify as promisify6 } from "node:util";
17251
+ import { createHash as createHash9, randomUUID as randomUUID16 } from "node:crypto";
17252
+ import { chmod as chmod3, mkdir as mkdir9, mkdtemp, readFile as readFile9, rename as rename4, rm as rm3, stat as stat8 } from "node:fs/promises";
17253
+ import { join as join12 } from "node:path";
17254
+ import { fileURLToPath as fileURLToPath4 } from "node:url";
17255
+ var DESKTOP_IMAGE_LIMIT = 20 * 1024 * 1024;
17256
+ var DESKTOP_CHUNK_LIMIT = 512 * 1024;
17257
+ var PNG_SIGNATURE = Buffer.from("89504e470d0a1a0a", "hex");
17258
+ var executeFile = promisify6(execFile6);
17259
+ var executeDesktop = async (file, args) => {
17260
+ try {
17261
+ return (await executeFile(file, args, { timeout: 15e3, killSignal: "SIGKILL", maxBuffer: 256 * 1024, encoding: "utf8" })).stdout;
17262
+ } catch (error) {
17263
+ const detail = error;
17264
+ if (detail.code === "ENOENT") throw new Error(`\u684C\u9762\u622A\u5C4F\u7F3A\u5C11\u4F9D\u8D56\uFF1A${file}\u3002\u8BF7\u5728 Proxy \u4E3B\u673A\u5B89\u88C5\u540E\u91CD\u8BD5\u3002`);
17265
+ if (detail.killed) throw new Error("\u684C\u9762\u622A\u5C4F\u8D85\u65F6\uFF0C\u8BF7\u68C0\u67E5 Proxy \u4E3B\u673A\u7684\u684C\u9762\u4F1A\u8BDD\u540E\u91CD\u8BD5\u3002");
17266
+ throw new Error(`\u684C\u9762\u622A\u5C4F\u5931\u8D25\uFF1A${detail.stderr?.trim().slice(0, 2048) || detail.message}`);
17267
+ }
17268
+ };
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 \u6216\u7CFB\u7EDF\u5217\u51FA\u7684\u542F\u52A8\u7A0B\u5E8F\uFF0C\u7136\u540E\u91CD\u542F Proxy\u3002";
17270
+ async function requestDesktopPermission(stateDirectory) {
17271
+ return new PlatformDesktop(stateDirectory).requestScreenCapturePermission();
17272
+ }
17273
+ async function checkDesktop(stateDirectory) {
17274
+ return new PlatformDesktop(stateDirectory).diagnose();
17275
+ }
17276
+ function parseXrandr(text) {
17277
+ const displays = [];
17278
+ for (const line of text.split("\n")) {
17279
+ const match = /^\s*\d+:\s+([+*]*)(\S+)\s+(\d+)\/\d+x(\d+)\/\d+([+-]\d+)([+-]\d+)/.exec(line);
17280
+ if (!match) continue;
17281
+ const [, flags, id, width, height, x, y] = match;
17282
+ displays.push({ id, name: id, primary: flags.includes("*"), builtIn: /^(eDP|LVDS|DSI)/i.test(id), width: Number(width), height: Number(height), x: Number(x), y: Number(y) });
17283
+ }
17284
+ return displays;
17285
+ }
17286
+ function parseWlrRandr(text) {
17287
+ const displays = [];
17288
+ for (const block of text.split(/(?=^\S)/m)) {
17289
+ const name = /^(\S+)(?:\s+"(.*)")?/.exec(block);
17290
+ const mode = /^\s+(\d+)x(\d+)\s+px[^\n]*\([^\n]*current[^\n]*\)/m.exec(block);
17291
+ if (!name || !mode || !/^\s+Enabled:\s+yes\s*$/m.test(block)) continue;
17292
+ const rotated = /^\s+Transform:\s+(?:90|270|flipped-90|flipped-270)\s*$/m.test(block);
17293
+ displays.push({ id: name[1], name: name[2] || name[1], primary: false, builtIn: /^(eDP|LVDS|DSI)/i.test(name[1]), width: Number(mode[rotated ? 2 : 1]), height: Number(mode[rotated ? 1 : 2]) });
17294
+ }
17295
+ return displays;
17296
+ }
17297
+ function publicDisplay(display) {
17298
+ return { id: display.id, name: display.name, primary: display.primary, builtIn: display.builtIn, width: display.width, height: display.height };
17299
+ }
17300
+ var PlatformDesktop = class {
17301
+ constructor(stateDirectory, system = process.platform, environment = process.env, execute3 = executeDesktop) {
17302
+ this.stateDirectory = stateDirectory;
17303
+ this.system = system;
17304
+ this.environment = environment;
17305
+ this.execute = execute3;
17306
+ }
17307
+ helper;
17308
+ linuxMode() {
17309
+ if (this.environment.WAYLAND_DISPLAY || this.environment.XDG_SESSION_TYPE === "wayland") {
17310
+ if (!this.environment.WAYLAND_DISPLAY || !this.environment.XDG_RUNTIME_DIR) throw new Error("Proxy \u672A\u8FDE\u63A5 Wayland \u684C\u9762\u4F1A\u8BDD\uFF0C\u8BF7\u4F7F\u7528\u684C\u9762\u7528\u6237\u7684 WAYLAND_DISPLAY \u548C XDG_RUNTIME_DIR\u3002");
17311
+ return "wayland";
17312
+ }
17313
+ if (!this.environment.DISPLAY) throw new Error("Proxy \u4E3B\u673A\u6CA1\u6709\u53EF\u7528\u7684\u56FE\u5F62\u684C\u9762\u4F1A\u8BDD\uFF0C\u8BF7\u8BBE\u7F6E\u684C\u9762\u7528\u6237\u7684 DISPLAY \u548C XAUTHORITY\u3002");
17314
+ return "x11";
17315
+ }
17316
+ macHelper() {
17317
+ if (this.helper) return this.helper;
17318
+ this.helper = (async () => {
17319
+ await mkdir9(this.stateDirectory, { recursive: true, mode: 448 });
17320
+ const target = join12(this.stateDirectory, "octopus-desktop-capture");
17321
+ const source = fileURLToPath4(new URL("../native/desktop-macos.m", import.meta.url));
17322
+ const sourceInfo = await stat8(source), existing = await stat8(target).catch(() => null);
17323
+ if (existing?.isFile() && existing.mtimeMs >= sourceInfo.mtimeMs && (existing.mode & 63) === 0) return target;
17324
+ const temporary = `${target}.${randomUUID16()}`;
17325
+ try {
17326
+ 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 chmod3(temporary, 448);
17328
+ await rename4(temporary, target);
17329
+ return target;
17330
+ } finally {
17331
+ await rm3(temporary, { force: true });
17332
+ }
17333
+ })();
17334
+ this.helper.catch(() => {
17335
+ this.helper = void 0;
17336
+ });
17337
+ return this.helper;
17338
+ }
17339
+ async linuxDisplays() {
17340
+ const wayland = this.linuxMode() === "wayland";
17341
+ try {
17342
+ const text = await this.execute(wayland ? "wlr-randr" : "xrandr", wayland ? [] : ["--listactivemonitors"]);
17343
+ return wayland ? parseWlrRandr(text) : parseXrandr(text);
17344
+ } catch (error) {
17345
+ if (wayland) throw new Error(`${error instanceof Error ? error.message : error} \u9996\u7248 Wayland \u9700\u8981\u652F\u6301 wlr-output-management \u548C grim \u622A\u5C4F\u7684\u684C\u9762\uFF1BGNOME/KDE Wayland \u6682\u4E0D\u652F\u6301\u3002`);
17346
+ throw error;
17347
+ }
17348
+ }
17349
+ async requestScreenCapturePermission() {
17350
+ if (this.system !== "darwin") return "not-required";
17351
+ const result = (await this.execute(await this.macHelper(), ["request-permission"])).trim();
17352
+ if (result !== "granted" && result !== "denied") throw new Error("\u5C4F\u5E55\u5F55\u5236\u6388\u6743\u68C0\u67E5\u8FD4\u56DE\u4E86\u65E0\u6548\u7ED3\u679C\u3002");
17353
+ return result;
17354
+ }
17355
+ async diagnose() {
17356
+ const results = [];
17357
+ const record2 = (name, status, detail) => results.push({ name, status, detail });
17358
+ try {
17359
+ if (this.system === "darwin") {
17360
+ const version = (await this.execute("/usr/bin/sw_vers", ["-productVersion"])).trim();
17361
+ const match = /^(\d+)\.(\d+)(?:\.\d+)?$/.exec(version);
17362
+ if (!match) throw new Error("\u65E0\u6CD5\u8BC6\u522B macOS \u7248\u672C\uFF0C\u65E0\u6CD5\u786E\u8BA4\u622A\u5C4F\u652F\u6301\u3002");
17363
+ if (Number(match[1]) < 12 || Number(match[1]) === 12 && Number(match[2]) < 3) throw new Error(`macOS ${version} \u4E0D\u652F\u6301\u684C\u9762\u622A\u5C4F\uFF0C\u9700\u8981 macOS 12.3 \u53CA\u4EE5\u4E0A\u3002`);
17364
+ const helper = await this.macHelper();
17365
+ try {
17366
+ 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\u53EF\u7528`);
17368
+ } catch (error) {
17369
+ record2("\u684C\u9762\u622A\u5C4F\u652F\u6301", "warn", error instanceof Error ? error.message : String(error));
17370
+ }
17371
+ try {
17372
+ const permission = (await this.execute(helper, ["permission-status"])).trim();
17373
+ 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" ? "\u5DF2\u6388\u6743\uFF08\u5F53\u524D\u7528\u6237\u53CA\u542F\u52A8\u7A0B\u5E8F\uFF09\uFF1B\u68C0\u67E5\u672A\u8BFB\u53D6\u684C\u9762\u56FE\u7247\u3002" : `\u672A\u6388\u6743\u6216\u6388\u6743\u5C1A\u672A\u751F\u6548\uFF1B\u8FD0\u884C octopus-proxy onboard \u7533\u8BF7\u3002${DESKTOP_PERMISSION_GUIDANCE}`);
17375
+ } catch (error) {
17376
+ record2("\u5C4F\u5E55\u5F55\u5236\u6743\u9650", "warn", `\u65E0\u6CD5\u68C0\u67E5\uFF1A${error instanceof Error ? error.message : error} ${DESKTOP_PERMISSION_GUIDANCE}`);
17377
+ }
17378
+ } else if (this.system === "linux") {
17379
+ const mode = this.linuxMode();
17380
+ if (mode === "wayland" && /(?:^|[:;])(GNOME|KDE|PLASMA)(?:$|[:;])/i.test(this.environment.XDG_CURRENT_DESKTOP ?? "")) throw new Error("GNOME/KDE Wayland \u6682\u4E0D\u652F\u6301\u684C\u9762\u622A\u5C4F\uFF1B\u8BF7\u4F7F\u7528 X11 \u6216\u517C\u5BB9 wlr-output-management \u4E0E grim \u7684 Wayland \u684C\u9762\u3002");
17381
+ const displays = await this.list();
17382
+ if (mode === "wayland") await this.execute("grim", ["-h"]);
17383
+ else {
17384
+ let version;
17385
+ try {
17386
+ version = await this.execute("magick", ["import", "-version"]);
17387
+ } catch (error) {
17388
+ if (!(error instanceof Error) || !error.message.startsWith("\u684C\u9762\u622A\u5C4F\u7F3A\u5C11\u4F9D\u8D56\uFF1Amagick\u3002")) throw error;
17389
+ version = await this.execute("import", ["-version"]);
17390
+ }
17391
+ if (!/^(?:Features|Delegates[^:\r\n]*):[^\r\n]*\b(?:x|X11)\b/im.test(version)) throw new Error("ImageMagick \u672A\u542F\u7528 X11 \u652F\u6301\uFF0C\u8BF7\u5B89\u88C5\u652F\u6301 X11 \u7684 ImageMagick\u3002");
17392
+ }
17393
+ record2("\u684C\u9762\u622A\u5C4F\u652F\u6301", "ok", `${mode === "x11" ? "X11" : "Wayland"} \u56FE\u5F62\u4F1A\u8BDD\u53EF\u8BBF\u95EE\uFF1B${displays.length} \u4E2A\u6D3B\u52A8\u663E\u793A\u5668\uFF1B\u622A\u5C4F\u4F9D\u8D56\u53EF\u7528${mode === "wayland" ? "\uFF1B\u622A\u5C4F\u534F\u8BAE\u9700\u5B9E\u9645\u622A\u5C4F\u9A8C\u8BC1" : ""}`);
17394
+ record2("\u5C4F\u5E55\u5F55\u5236\u6743\u9650", "warn", "Linux \u65E0\u7EDF\u4E00\u7684\u622A\u5C4F\u6388\u6743\u72B6\u6001\u63A5\u53E3\uFF1B\u5F53\u524D\u56FE\u5F62\u4F1A\u8BDD\u53EF\u8BBF\u95EE\uFF0C\u5B9E\u9645\u622A\u5C4F\u6743\u9650\u9700\u5728 Claw \u70B9\u51FB\u201C\u622A\u5C4F\u201D\u9A8C\u8BC1\u3002");
17395
+ } else throw new Error(`\u5F53\u524D\u5E73\u53F0 ${this.system} \u4E0D\u652F\u6301\u684C\u9762\u622A\u5C4F\uFF0C\u4EC5\u652F\u6301 macOS 12.3 \u53CA\u4EE5\u4E0A\u548C Linux\u3002`);
17396
+ } catch (error) {
17397
+ record2("\u684C\u9762\u622A\u5C4F\u652F\u6301", "warn", error instanceof Error ? error.message : String(error));
17398
+ record2("\u5C4F\u5E55\u5F55\u5236\u6743\u9650", "warn", "\u672A\u68C0\u67E5\uFF1A\u622A\u5C4F\u73AF\u5883\u6216\u4F9D\u8D56\u4E0D\u53EF\u7528\uFF1B\u8BF7\u5148\u5904\u7406\u201C\u684C\u9762\u622A\u5C4F\u652F\u6301\u201D\u7684\u63D0\u793A\u3002");
17399
+ }
17400
+ return results;
17401
+ }
17402
+ async list() {
17403
+ let displays;
17404
+ if (this.system === "darwin") {
17405
+ displays = JSON.parse(await this.execute(await this.macHelper(), ["list"]));
17406
+ } else if (this.system === "linux") {
17407
+ displays = (await this.linuxDisplays()).map(publicDisplay);
17408
+ } else throw new Error("\u684C\u9762\u622A\u5C4F\u4EC5\u652F\u6301 macOS 12.3 \u53CA\u4EE5\u4E0A\u548C Linux\u3002");
17409
+ if (!displays.length) throw new Error("\u6CA1\u6709\u53EF\u622A\u5C4F\u7684\u6D3B\u52A8\u663E\u793A\u5668\uFF0C\u8BF7\u68C0\u67E5 Proxy \u4E3B\u673A\u7684\u56FE\u5F62\u4F1A\u8BDD\u3002");
17410
+ return displays;
17411
+ }
17412
+ async capture(displayId) {
17413
+ const display = (await this.list()).find((item) => item.id === displayId);
17414
+ 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 mkdir9(this.stateDirectory, { recursive: true, mode: 448 });
17416
+ const directory = await mkdtemp(join12(this.stateDirectory, "desktop-capture-"));
17417
+ const output = join12(directory, "screen.png");
17418
+ try {
17419
+ if (this.system === "darwin") {
17420
+ await this.execute(await this.macHelper(), ["capture", display.id, output]);
17421
+ } else if (this.linuxMode() === "wayland") {
17422
+ await this.execute("grim", ["-o", display.id, "-t", "png", output]);
17423
+ } else {
17424
+ const region = (await this.linuxDisplays()).find((item) => item.id === display.id);
17425
+ if (!region) throw new Error("\u6240\u9009\u663E\u793A\u5668\u5DF2\u65AD\u5F00\uFF0C\u8BF7\u5237\u65B0\u663E\u793A\u5668\u5217\u8868\u540E\u91CD\u8BD5\u3002");
17426
+ if (region.x < 0 || region.y < 0) throw new Error("X11 \u663E\u793A\u5668\u5750\u6807\u8D85\u51FA\u6839\u7A97\u53E3\uFF0C\u8BF7\u68C0\u67E5\u663E\u793A\u5668\u5E03\u5C40\u3002");
17427
+ const args = ["-silent", "-window", "root", "-crop", `${region.width}x${region.height}+${region.x}+${region.y}`, "+repage", `png:${output}`];
17428
+ try {
17429
+ await this.execute("magick", ["import", ...args]);
17430
+ } catch (error) {
17431
+ if (!(error instanceof Error) || !error.message.startsWith("\u684C\u9762\u622A\u5C4F\u7F3A\u5C11\u4F9D\u8D56\uFF1Amagick\u3002")) throw error;
17432
+ await this.execute("import", args);
17433
+ }
17434
+ }
17435
+ const info = await stat8(output);
17436
+ 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
+ return { display, bytes: await readFile9(output) };
17438
+ } finally {
17439
+ await rm3(directory, { recursive: true, force: true });
17440
+ }
17441
+ }
17442
+ };
17443
+ var DesktopManager = class {
17444
+ constructor(backend) {
17445
+ this.backend = backend;
17446
+ }
17447
+ captures = /* @__PURE__ */ new Map();
17448
+ pending = /* @__PURE__ */ new Map();
17449
+ closed = false;
17450
+ async dispatch(command, owner) {
17451
+ if (this.closed) throw new Error("\u684C\u9762\u622A\u5C4F\u670D\u52A1\u5DF2\u505C\u6B62\u3002");
17452
+ const p = command.payload;
17453
+ if (command.operation === "desktop.list") return this.backend.list();
17454
+ if (command.operation === "desktop.capture") {
17455
+ if (typeof p.displayId !== "string" || !p.displayId) throw new Error("\u8BF7\u9009\u62E9\u663E\u793A\u5668\u3002");
17456
+ if (this.pending.has(owner)) throw new Error("\u6B63\u5728\u622A\u5C4F\uFF0C\u8BF7\u7B49\u5F85\u5F53\u524D\u622A\u5C4F\u5B8C\u6210\u3002");
17457
+ const token = {};
17458
+ this.pending.set(owner, token);
17459
+ try {
17460
+ const { display, bytes } = await this.backend.capture(p.displayId);
17461
+ if (this.closed || this.pending.get(owner) !== token) throw new Error("\u8FDE\u63A5\u5DF2\u9501\u5B9A\uFF0C\u622A\u5C4F\u7ED3\u679C\u5DF2\u4E22\u5F03\u3002");
17462
+ if (!bytes.length || bytes.length > DESKTOP_IMAGE_LIMIT) throw new Error("\u684C\u9762\u622A\u56FE\u4E3A\u7A7A\u6216\u8D85\u8FC7 20 MiB \u4E0A\u9650\u3002");
17463
+ 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
+ const width = bytes.readUInt32BE(16), height = bytes.readUInt32BE(20);
17465
+ if (!width || !height) throw new Error("\u684C\u9762\u622A\u56FE\u5C3A\u5BF8\u65E0\u6548\u3002");
17466
+ const info = { id: randomUUID16(), display: publicDisplay(display), capturedAt: (/* @__PURE__ */ new Date()).toISOString(), mime: "image/png", width, height, size: bytes.length, sha256: createHash9("sha256").update(bytes).digest("hex") };
17467
+ this.clearCapture(owner);
17468
+ const timer = setTimeout(() => this.clearCapture(owner), 6e4);
17469
+ timer.unref();
17470
+ this.captures.set(owner, { info, bytes, timer });
17471
+ return info;
17472
+ } finally {
17473
+ if (this.pending.get(owner) === token) this.pending.delete(owner);
17474
+ }
17475
+ }
17476
+ if (command.operation === "desktop.read") {
17477
+ const capture = this.captures.get(owner);
17478
+ if (!capture || p.id !== capture.info.id) throw new Error("\u5FEB\u7167\u5DF2\u8FC7\u671F\u6216\u4E0D\u53EF\u8BFB\u53D6\uFF0C\u8BF7\u91CD\u65B0\u622A\u5C4F\u3002");
17479
+ const offset = p.offset ?? 0, length = p.length ?? DESKTOP_CHUNK_LIMIT;
17480
+ if (typeof offset !== "number" || !Number.isSafeInteger(offset) || offset < 0 || offset > capture.bytes.length || typeof length !== "number" || !Number.isSafeInteger(length) || length < 1 || length > DESKTOP_CHUNK_LIMIT) throw new Error("\u5FEB\u7167\u8BFB\u53D6\u8303\u56F4\u65E0\u6548\u3002");
17481
+ return { id: capture.info.id, offset, size: capture.info.size, mime: "image/png", sha256: capture.info.sha256, data: capture.bytes.subarray(offset, offset + length).toString("base64") };
17482
+ }
17483
+ throw new Error(`Unsupported desktop operation: ${command.operation}`);
17484
+ }
17485
+ clearCapture(owner) {
17486
+ const capture = this.captures.get(owner);
17487
+ if (capture) {
17488
+ clearTimeout(capture.timer);
17489
+ capture.bytes.fill(0);
17490
+ this.captures.delete(owner);
17491
+ }
17492
+ }
17493
+ closeOwned(owner) {
17494
+ this.pending.delete(owner);
17495
+ this.clearCapture(owner);
17496
+ }
17497
+ close() {
17498
+ this.closed = true;
17499
+ this.pending.clear();
17500
+ for (const owner of this.captures.keys()) this.clearCapture(owner);
17501
+ }
17502
+ };
17503
+
17241
17504
  // src/daemon.ts
17242
17505
  async function runDaemon(configPath, authenticate = verifyHostPassword) {
17243
17506
  const config = await loadConfig(configPath);
17244
- await mkdir9(config.stateDirectory, { recursive: true, mode: 448 });
17245
- const workspace = new Workspace(config.projects), runtime = createRuntime(config), uploads = new Uploads(workspace, join12(config.stateDirectory, "uploads")), journal = new Journal(join12(config.stateDirectory, "journal"));
17507
+ await mkdir10(config.stateDirectory, { recursive: true, mode: 448 });
17508
+ const workspace = new Workspace(config.projects), runtime = createRuntime(config), uploads = new Uploads(workspace, join13(config.stateDirectory, "uploads")), journal = new Journal(join13(config.stateDirectory, "journal"));
17246
17509
  let secureChat;
17247
17510
  function emit(projectId, event, data) {
17248
17511
  secureChat?.publish(projectId, event, data);
17249
17512
  }
17250
17513
  const terminals = new TerminalManager(workspace, (owner, project, event, data) => secureChat?.sendTo(owner, project, event, data));
17251
- const adapter = createAdapter(runtime, workspace, uploads, emit), artifacts = new Artifacts(workspace, join12(config.stateDirectory, "artifacts"), emit);
17514
+ const desktop = new DesktopManager(new PlatformDesktop(config.stateDirectory));
17515
+ const adapter = createAdapter(runtime, workspace, uploads, emit), artifacts = new Artifacts(workspace, join13(config.stateDirectory, "artifacts"), emit);
17252
17516
  const projectRegistry = new ProjectRegistry(configPath, config, runtime, adapter);
17253
17517
  async function dispatch(c, secureSessionId) {
17518
+ if (c.operation.startsWith("desktop.")) return desktop.dispatch(c, secureSessionId);
17254
17519
  if (c.operation.startsWith("terminal.")) return terminals.dispatch(c, secureSessionId);
17255
17520
  const p = c.payload;
17256
17521
  if (c.operation === "runtime.status") return { ...await runtime.status(), capabilities: adapter.supportedOperations() };
@@ -17309,7 +17574,10 @@ async function runDaemon(configPath, authenticate = verifyHostPassword) {
17309
17574
  }
17310
17575
  const identity = await ensureHostIdentity(config.stateDirectory);
17311
17576
  await ensureHostAuth(config.stateDirectory);
17312
- secureChat = new SecureChat(config, identity, journal, dispatch, (owner) => terminals.closeOwned(owner), authenticate);
17577
+ secureChat = new SecureChat(config, identity, journal, dispatch, (owner) => {
17578
+ terminals.closeOwned(owner);
17579
+ desktop.closeOwned(owner);
17580
+ }, authenticate);
17313
17581
  await runtime.init().catch((error) => console.error(`${runtime.kind} startup:`, error.message));
17314
17582
  secureChat.start();
17315
17583
  const scan = setInterval(async () => {
@@ -17326,6 +17594,7 @@ async function runDaemon(configPath, authenticate = verifyHostPassword) {
17326
17594
  clearInterval(scan);
17327
17595
  secureChat?.stop();
17328
17596
  terminals.close();
17597
+ desktop.close();
17329
17598
  runtime.detach();
17330
17599
  adapter.close();
17331
17600
  }
@@ -17363,5 +17632,8 @@ export {
17363
17632
  ensureHostIdentity,
17364
17633
  createRuntime,
17365
17634
  createAdapter,
17635
+ DESKTOP_PERMISSION_GUIDANCE,
17636
+ requestDesktopPermission,
17637
+ checkDesktop,
17366
17638
  runDaemon
17367
17639
  };
package/dist/cli.js CHANGED
@@ -1,12 +1,14 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  ConfigError,
4
+ DESKTOP_PERMISSION_GUIDANCE,
4
5
  SERVICE_LABEL,
5
6
  SYSTEMD_UNIT,
6
7
  TOOL_KINDS,
7
8
  Uploads,
8
9
  Workspace,
9
10
  atomicJson,
11
+ checkDesktop,
10
12
  createAdapter,
11
13
  createRuntime,
12
14
  daemonEntryPath,
@@ -20,11 +22,12 @@ import {
20
22
  launchdPlistPath,
21
23
  loadConfig,
22
24
  proxyVersion,
25
+ requestDesktopPermission,
23
26
  resolveConfigPath,
24
27
  runDaemon,
25
28
  supportedPlatform,
26
29
  systemdUnitPath
27
- } from "./chunk-2RX5VSBS.js";
30
+ } from "./chunk-BIL5ROSQ.js";
28
31
 
29
32
  // src/cli.ts
30
33
  import { spawn } from "node:child_process";
@@ -357,6 +360,7 @@ async function runDoctor(opts) {
357
360
  record("\u914D\u7F6E\u6821\u9A8C", "fail", error instanceof Error ? error.message : String(error));
358
361
  }
359
362
  if (config) {
363
+ results.push(...await checkDesktop(config.stateDirectory));
360
364
  try {
361
365
  await mkdir3(config.stateDirectory, { recursive: true, mode: 448 });
362
366
  await access2(config.stateDirectory, constants.W_OK);
@@ -383,6 +387,10 @@ async function runDoctor(opts) {
383
387
  }
384
388
  }
385
389
  }
390
+ if (!config) {
391
+ record("\u684C\u9762\u622A\u5C4F\u652F\u6301", "warn", "\u672A\u68C0\u67E5\uFF1A\u914D\u7F6E\u4E0D\u53EF\u7528\uFF1B\u5148\u8FD0\u884C octopus-proxy onboard \u6216\u4FEE\u590D\u914D\u7F6E\u3002");
392
+ record("\u5C4F\u5E55\u5F55\u5236\u6743\u9650", "warn", "\u672A\u68C0\u67E5\uFF1A\u65E0\u6CD5\u786E\u5B9A\u622A\u5C4F\u8F85\u52A9\u7A0B\u5E8F\u7684\u72B6\u6001\u76EE\u5F55\u3002");
393
+ }
386
394
  if (config) {
387
395
  const probeConfig = { ...config, tool: { ...config.tool, autoStart: false } };
388
396
  const runtime = createRuntime(probeConfig), workspace = new Workspace(config.projects);
@@ -636,6 +644,14 @@ async function runOnboard(opts) {
636
644
  console.log(`\u5DF2\u5199\u5165\u914D\u7F6E\uFF1A${opts.configPath}\uFF080600\uFF09`);
637
645
  stateDirectory = expandHome(stateDirectory);
638
646
  await mkdir4(stateDirectory, { recursive: true, mode: 448 });
647
+ try {
648
+ if (platform2() === "darwin") console.log("\u6B63\u5728\u68C0\u67E5\u5C4F\u5E55\u5F55\u5236\u6743\u9650\uFF1B\u5982\u51FA\u73B0\u7CFB\u7EDF\u6388\u6743\u63D0\u793A\uFF0C\u8BF7\u5141\u8BB8 Proxy \u8BFB\u53D6\u684C\u9762\u3002");
649
+ const permission = await requestDesktopPermission(stateDirectory);
650
+ if (permission === "granted") console.log("\u5C4F\u5E55\u5F55\u5236\u6743\u9650\uFF1A\u5DF2\u6388\u6743\uFF0C\u53EF\u5728 Claw \u7684\u201C\u5FEB\u7167\u201D\u4E2D\u622A\u5C4F\u3002");
651
+ 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
+ } catch (error) {
653
+ 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}`);
654
+ }
639
655
  try {
640
656
  const response = await fetch(hubUrl, { redirect: "manual", signal: AbortSignal.timeout(5e3) });
641
657
  console.log(`Hub \u8FDE\u901A\u6027\uFF1AHTTP ${response.status}`);
package/dist/main.js CHANGED
@@ -2,7 +2,7 @@
2
2
  import {
3
3
  ConfigError,
4
4
  runDaemon
5
- } from "./chunk-2RX5VSBS.js";
5
+ } from "./chunk-BIL5ROSQ.js";
6
6
 
7
7
  // src/main.ts
8
8
  runDaemon(process.env.OCTOPUS_PROXY_CONFIG ?? "proxy.json").catch((error) => {
@@ -0,0 +1,141 @@
1
+ #import <AppKit/AppKit.h>
2
+ #import <ScreenCaptureKit/ScreenCaptureKit.h>
3
+ #import <ImageIO/ImageIO.h>
4
+ #import <VideoToolbox/VideoToolbox.h>
5
+
6
+ static BOOL finished = NO;
7
+ static int resultCode = 1;
8
+
9
+ static void fail(NSString *message) {
10
+ fprintf(stderr, "%s\n", message.UTF8String);
11
+ resultCode = 1;
12
+ finished = YES;
13
+ }
14
+
15
+ static NSString *permissionMessage(void) {
16
+ return @"无法读取桌面。请在 Proxy 主机的“系统设置 → 隐私与安全性 → 屏幕录制(或屏幕与系统音频录制)”中授权 octopus-desktop-capture 或其启动程序,并重启 Proxy 后重试。";
17
+ }
18
+
19
+ static NSDictionary *displayInfo(CGDirectDisplayID displayId) {
20
+ NSString *name = [NSString stringWithFormat:@"显示器 %u", displayId];
21
+ for (NSScreen *screen in NSScreen.screens) {
22
+ if ([screen.deviceDescription[@"NSScreenNumber"] unsignedIntValue] == displayId) {
23
+ name = screen.localizedName;
24
+ break;
25
+ }
26
+ }
27
+ return @{@"id": [NSString stringWithFormat:@"%u", displayId], @"name": name,
28
+ @"primary": @((BOOL)(displayId == CGMainDisplayID())), @"builtIn": @((BOOL)CGDisplayIsBuiltin(displayId)),
29
+ @"width": @(CGDisplayPixelsWide(displayId)), @"height": @(CGDisplayPixelsHigh(displayId))};
30
+ }
31
+
32
+ @interface DesktopFrame : NSObject <SCStreamOutput, SCStreamDelegate>
33
+ @property(nonatomic, strong) SCStream *stream;
34
+ @property(nonatomic, copy) NSString *path;
35
+ @property(nonatomic) BOOL received;
36
+ @end
37
+
38
+ @implementation DesktopFrame
39
+ - (void)stream:(SCStream *)stream didStopWithError:(NSError *)error {
40
+ dispatch_async(dispatch_get_main_queue(), ^{ if (!finished) fail(permissionMessage()); });
41
+ }
42
+ - (void)stream:(SCStream *)stream didOutputSampleBuffer:(CMSampleBufferRef)sampleBuffer ofType:(SCStreamOutputType)type {
43
+ if (self.received || type != SCStreamOutputTypeScreen || !CMSampleBufferIsValid(sampleBuffer)) return;
44
+ NSArray *attachments = (__bridge NSArray *)CMSampleBufferGetSampleAttachmentsArray(sampleBuffer, NO);
45
+ NSNumber *status = attachments.firstObject[SCStreamFrameInfoStatus];
46
+ if (!status || status.integerValue != SCFrameStatusComplete) return;
47
+ CVPixelBufferRef pixel = CMSampleBufferGetImageBuffer(sampleBuffer);
48
+ if (!pixel) return;
49
+ self.received = YES;
50
+ CGImageRef image = NULL;
51
+ OSStatus conversion = VTCreateCGImageFromCVPixelBuffer(pixel, NULL, &image);
52
+ BOOL saved = NO;
53
+ if (conversion == noErr && image) {
54
+ CGImageDestinationRef destination = CGImageDestinationCreateWithURL((__bridge CFURLRef)[NSURL fileURLWithPath:self.path], CFSTR("public.png"), 1, NULL);
55
+ if (destination) {
56
+ CGImageDestinationAddImage(destination, image, NULL);
57
+ saved = CGImageDestinationFinalize(destination);
58
+ CFRelease(destination);
59
+ }
60
+ CGImageRelease(image);
61
+ }
62
+ [stream stopCaptureWithCompletionHandler:^(NSError *error) {
63
+ dispatch_async(dispatch_get_main_queue(), ^{
64
+ if (finished) return;
65
+ if (saved) { resultCode = 0; finished = YES; }
66
+ else fail(@"无法保存桌面 PNG 截图,请检查 Proxy 状态目录。" );
67
+ });
68
+ }];
69
+ }
70
+ @end
71
+
72
+ int main(int argc, const char *argv[]) {
73
+ @autoreleasepool {
74
+ if (@available(macOS 12.3, *)) {
75
+ if (argc == 2 && strcmp(argv[1], "permission-status") == 0) {
76
+ puts(CGPreflightScreenCaptureAccess() ? "granted" : "denied");
77
+ return 0;
78
+ }
79
+ if (argc == 2 && strcmp(argv[1], "request-permission") == 0) {
80
+ BOOL granted = CGPreflightScreenCaptureAccess();
81
+ if (!granted) granted = CGRequestScreenCaptureAccess();
82
+ puts(granted ? "granted" : "denied");
83
+ return 0;
84
+ }
85
+ if (argc == 2 && strcmp(argv[1], "list") == 0) {
86
+ CGDirectDisplayID ids[64];
87
+ uint32_t count = 0;
88
+ if (CGGetActiveDisplayList(64, ids, &count) != kCGErrorSuccess) {
89
+ fail(@"无法读取活动显示器,请检查 Proxy 是否运行在已登录的图形桌面会话中。" );
90
+ return 1;
91
+ }
92
+ NSMutableArray *displays = [NSMutableArray array];
93
+ for (uint32_t i = 0; i < count; i++) {
94
+ if (CGDisplayMirrorsDisplay(ids[i]) == kCGNullDirectDisplay) [displays addObject:displayInfo(ids[i])];
95
+ }
96
+ NSData *data = [NSJSONSerialization dataWithJSONObject:displays options:0 error:NULL];
97
+ fwrite(data.bytes, 1, data.length, stdout);
98
+ return 0;
99
+ }
100
+ if (argc != 4 || strcmp(argv[1], "capture") != 0) { fail(@"截屏参数无效。" ); return 1; }
101
+ char *end = NULL;
102
+ unsigned long requested = strtoul(argv[2], &end, 10);
103
+ if (!requested || *end || requested > UINT32_MAX) { fail(@"显示器 ID 无效。" ); return 1; }
104
+ if (!CGPreflightScreenCaptureAccess()) {
105
+ CGRequestScreenCaptureAccess();
106
+ fail(permissionMessage());
107
+ return 1;
108
+ }
109
+ DesktopFrame *frame = [DesktopFrame new];
110
+ frame.path = [NSString stringWithUTF8String:argv[3]];
111
+ [SCShareableContent getShareableContentExcludingDesktopWindows:NO onScreenWindowsOnly:YES completionHandler:^(SCShareableContent *content, NSError *error) {
112
+ if (error || !content) { dispatch_async(dispatch_get_main_queue(), ^{ fail(permissionMessage()); }); return; }
113
+ SCDisplay *display = nil;
114
+ for (SCDisplay *candidate in content.displays) if (candidate.displayID == (CGDirectDisplayID)requested) { display = candidate; break; }
115
+ if (!display) { dispatch_async(dispatch_get_main_queue(), ^{ fail(@"所选显示器已断开,请刷新显示器列表后重试。" ); }); return; }
116
+ SCContentFilter *filter = [[SCContentFilter alloc] initWithDisplay:display excludingWindows:@[]];
117
+ SCStreamConfiguration *configuration = [SCStreamConfiguration new];
118
+ configuration.width = CGDisplayPixelsWide(display.displayID);
119
+ configuration.height = CGDisplayPixelsHigh(display.displayID);
120
+ configuration.pixelFormat = kCVPixelFormatType_32BGRA;
121
+ configuration.showsCursor = YES;
122
+ configuration.queueDepth = 3;
123
+ configuration.minimumFrameInterval = CMTimeMake(1, 30);
124
+ frame.stream = [[SCStream alloc] initWithFilter:filter configuration:configuration delegate:frame];
125
+ NSError *outputError = nil;
126
+ if (![frame.stream addStreamOutput:frame type:SCStreamOutputTypeScreen sampleHandlerQueue:dispatch_queue_create("octopus.desktop.frame", DISPATCH_QUEUE_SERIAL) error:&outputError]) {
127
+ dispatch_async(dispatch_get_main_queue(), ^{ fail(@"无法启动桌面截屏。" ); }); return;
128
+ }
129
+ [frame.stream startCaptureWithCompletionHandler:^(NSError *startError) {
130
+ if (startError) dispatch_async(dispatch_get_main_queue(), ^{ fail(permissionMessage()); });
131
+ }];
132
+ }];
133
+ NSDate *deadline = [NSDate dateWithTimeIntervalSinceNow:12];
134
+ while (!finished && deadline.timeIntervalSinceNow > 0) [[NSRunLoop mainRunLoop] runUntilDate:[NSDate dateWithTimeIntervalSinceNow:0.05]];
135
+ if (!finished) fail(@"桌面截屏超时,请检查主机的图形会话与屏幕录制权限。" );
136
+ return resultCode;
137
+ }
138
+ fail(@"桌面截屏需要 macOS 12.3 及以上。" );
139
+ return 1;
140
+ }
141
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@skylandnpm/octopus-proxy",
3
- "version": "0.1.33",
3
+ "version": "0.1.34",
4
4
  "type": "module",
5
5
  "description": "Octopus host proxy: OpenCode, Codex, ZCode and Pi adapters with service installer CLI",
6
6
  "bin": {