@skylandnpm/octopus-proxy 0.1.32 → 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
@@ -11664,6 +11664,10 @@ async function loadConfig(path, options = {}) {
11664
11664
  }
11665
11665
  }
11666
11666
 
11667
+ // src/version.ts
11668
+ import { readFileSync } from "node:fs";
11669
+ var proxyVersion = JSON.parse(readFileSync(new URL("../package.json", import.meta.url), "utf8")).version;
11670
+
11667
11671
  // src/host-identity.ts
11668
11672
  import { execFile } from "node:child_process";
11669
11673
  import { createHash, randomUUID, X509Certificate } from "node:crypto";
@@ -11710,8 +11714,8 @@ async function ensureHostIdentity(stateDirectory) {
11710
11714
  }
11711
11715
 
11712
11716
  // src/daemon.ts
11713
- import { mkdir as mkdir9 } from "node:fs/promises";
11714
- import { join as join12 } from "node:path";
11717
+ import { mkdir as mkdir10 } from "node:fs/promises";
11718
+ import { join as join13 } from "node:path";
11715
11719
 
11716
11720
  // src/workspace-diff.ts
11717
11721
  import { execFile as execFile2 } from "node:child_process";
@@ -12243,6 +12247,9 @@ var Operations = [
12243
12247
  "terminal.remove",
12244
12248
  "terminal.input",
12245
12249
  "terminal.attach",
12250
+ "desktop.list",
12251
+ "desktop.capture",
12252
+ "desktop.read",
12246
12253
  "artifacts.list",
12247
12254
  "artifacts.history",
12248
12255
  "artifacts.read",
@@ -12262,7 +12269,7 @@ var CommandSchema = z.object({
12262
12269
  sessionId: z.string().optional(),
12263
12270
  payload: z.record(z.unknown()).default({})
12264
12271
  }).strict();
12265
- 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);
12266
12273
 
12267
12274
  // src/journal.ts
12268
12275
  var Journal = class {
@@ -12271,8 +12278,9 @@ var Journal = class {
12271
12278
  }
12272
12279
  active = /* @__PURE__ */ new Map();
12273
12280
  queues = /* @__PURE__ */ new Map();
12274
- execute(command, action) {
12275
- 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);
12276
12284
  if (active) {
12277
12285
  if (active.hash !== hash) return Promise.resolve(this.failure(command, "Request id reused"));
12278
12286
  return active.result;
@@ -12308,8 +12316,8 @@ var Journal = class {
12308
12316
  }).catch(() => {
12309
12317
  });
12310
12318
  }
12311
- this.active.set(command.id, { hash, result });
12312
- result.finally(() => this.active.delete(command.id)).catch(() => {
12319
+ this.active.set(activeKey, { hash, result });
12320
+ result.finally(() => this.active.delete(activeKey)).catch(() => {
12313
12321
  });
12314
12322
  return result;
12315
12323
  }
@@ -12683,7 +12691,11 @@ var SecureChat = class {
12683
12691
  if (this.stopping) return;
12684
12692
  const url = new URL("/ws/proxy-chat", this.config.hubUrl);
12685
12693
  url.protocol = url.protocol === "https:" ? "wss:" : "ws:";
12686
- const socket = new WebSocket(url, { headers: { Authorization: `SecretKey ${this.config.secretKey}`, "X-Octopus-Bot-Id": this.config.botId }, maxPayload: 1024 * 1024 });
12694
+ const socket = new WebSocket(url, { headers: {
12695
+ Authorization: `SecretKey ${this.config.secretKey}`,
12696
+ "X-Octopus-Bot-Id": this.config.botId,
12697
+ "X-Octopus-Proxy-Version": proxyVersion
12698
+ }, maxPayload: 1024 * 1024 });
12687
12699
  this.outer = socket;
12688
12700
  let lastPong = Date.now();
12689
12701
  const heartbeat = setInterval(() => {
@@ -12833,7 +12845,7 @@ var SecureChat = class {
12833
12845
  }
12834
12846
  session.lastAction = Date.now();
12835
12847
  sendJson(session.tls, { type: "response", id: command.id, botId: command.botId, status: "accepted" });
12836
- const response = await this.journal.execute(command, () => this.dispatch(command, id));
12848
+ const response = await this.journal.execute(command, () => this.dispatch(command, id), id);
12837
12849
  if (this.sessions.get(id) === session && session.authenticated) sendJson(session.tls, response);
12838
12850
  }
12839
12851
  closeSession(id) {
@@ -15737,7 +15749,10 @@ var DAEMON_OPERATIONS = [
15737
15749
  "terminal.resize",
15738
15750
  "terminal.remove",
15739
15751
  "terminal.input",
15740
- "terminal.attach"
15752
+ "terminal.attach",
15753
+ "desktop.list",
15754
+ "desktop.capture",
15755
+ "desktop.read"
15741
15756
  ];
15742
15757
  function unsupportedOperation(kind, operation) {
15743
15758
  throw new Error(`Unsupported ${kind} operation: ${operation}`);
@@ -17230,19 +17245,277 @@ var TerminalManager = class {
17230
17245
  }
17231
17246
  };
17232
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
+
17233
17504
  // src/daemon.ts
17234
17505
  async function runDaemon(configPath, authenticate = verifyHostPassword) {
17235
17506
  const config = await loadConfig(configPath);
17236
- await mkdir9(config.stateDirectory, { recursive: true, mode: 448 });
17237
- 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"));
17238
17509
  let secureChat;
17239
17510
  function emit(projectId, event, data) {
17240
17511
  secureChat?.publish(projectId, event, data);
17241
17512
  }
17242
17513
  const terminals = new TerminalManager(workspace, (owner, project, event, data) => secureChat?.sendTo(owner, project, event, data));
17243
- 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);
17244
17516
  const projectRegistry = new ProjectRegistry(configPath, config, runtime, adapter);
17245
17517
  async function dispatch(c, secureSessionId) {
17518
+ if (c.operation.startsWith("desktop.")) return desktop.dispatch(c, secureSessionId);
17246
17519
  if (c.operation.startsWith("terminal.")) return terminals.dispatch(c, secureSessionId);
17247
17520
  const p = c.payload;
17248
17521
  if (c.operation === "runtime.status") return { ...await runtime.status(), capabilities: adapter.supportedOperations() };
@@ -17301,7 +17574,10 @@ async function runDaemon(configPath, authenticate = verifyHostPassword) {
17301
17574
  }
17302
17575
  const identity = await ensureHostIdentity(config.stateDirectory);
17303
17576
  await ensureHostAuth(config.stateDirectory);
17304
- 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);
17305
17581
  await runtime.init().catch((error) => console.error(`${runtime.kind} startup:`, error.message));
17306
17582
  secureChat.start();
17307
17583
  const scan = setInterval(async () => {
@@ -17318,6 +17594,7 @@ async function runDaemon(configPath, authenticate = verifyHostPassword) {
17318
17594
  clearInterval(scan);
17319
17595
  secureChat?.stop();
17320
17596
  terminals.close();
17597
+ desktop.close();
17321
17598
  runtime.detach();
17322
17599
  adapter.close();
17323
17600
  }
@@ -17351,8 +17628,12 @@ export {
17351
17628
  atomicJson,
17352
17629
  Uploads,
17353
17630
  ensureHostAuth,
17631
+ proxyVersion,
17354
17632
  ensureHostIdentity,
17355
17633
  createRuntime,
17356
17634
  createAdapter,
17635
+ DESKTOP_PERMISSION_GUIDANCE,
17636
+ requestDesktopPermission,
17637
+ checkDesktop,
17357
17638
  runDaemon
17358
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,
@@ -19,11 +21,13 @@ import {
19
21
  launchdLogPath,
20
22
  launchdPlistPath,
21
23
  loadConfig,
24
+ proxyVersion,
25
+ requestDesktopPermission,
22
26
  resolveConfigPath,
23
27
  runDaemon,
24
28
  supportedPlatform,
25
29
  systemdUnitPath
26
- } from "./chunk-CYJNEMLS.js";
30
+ } from "./chunk-BIL5ROSQ.js";
27
31
 
28
32
  // src/cli.ts
29
33
  import { spawn } from "node:child_process";
@@ -356,6 +360,7 @@ async function runDoctor(opts) {
356
360
  record("\u914D\u7F6E\u6821\u9A8C", "fail", error instanceof Error ? error.message : String(error));
357
361
  }
358
362
  if (config) {
363
+ results.push(...await checkDesktop(config.stateDirectory));
359
364
  try {
360
365
  await mkdir3(config.stateDirectory, { recursive: true, mode: 448 });
361
366
  await access2(config.stateDirectory, constants.W_OK);
@@ -382,6 +387,10 @@ async function runDoctor(opts) {
382
387
  }
383
388
  }
384
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
+ }
385
394
  if (config) {
386
395
  const probeConfig = { ...config, tool: { ...config.tool, autoStart: false } };
387
396
  const runtime = createRuntime(probeConfig), workspace = new Workspace(config.projects);
@@ -635,6 +644,14 @@ async function runOnboard(opts) {
635
644
  console.log(`\u5DF2\u5199\u5165\u914D\u7F6E\uFF1A${opts.configPath}\uFF080600\uFF09`);
636
645
  stateDirectory = expandHome(stateDirectory);
637
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
+ }
638
655
  try {
639
656
  const response = await fetch(hubUrl, { redirect: "manual", signal: AbortSignal.timeout(5e3) });
640
657
  console.log(`Hub \u8FDE\u901A\u6027\uFF1AHTTP ${response.status}`);
@@ -699,10 +716,6 @@ async function manageProjects(configPath2, remove) {
699
716
  }
700
717
  }
701
718
 
702
- // src/version.ts
703
- import { readFileSync } from "node:fs";
704
- var proxyVersion = JSON.parse(readFileSync(new URL("../package.json", import.meta.url), "utf8")).version;
705
-
706
719
  // src/cli.ts
707
720
  var USAGE = `octopus-proxy \u2014 Octopus \u4E3B\u673A\u4EE3\u7406
708
721
 
package/dist/main.js CHANGED
@@ -2,7 +2,7 @@
2
2
  import {
3
3
  ConfigError,
4
4
  runDaemon
5
- } from "./chunk-CYJNEMLS.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.32",
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": {