@skylandnpm/octopus-proxy 0.1.66 → 0.1.68

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.
@@ -4,15 +4,16 @@ import {
4
4
  PlatformDesktop,
5
5
  REMOTE_DEFAULTS,
6
6
  RemoteConfigSchema,
7
+ RemotePlatformRegistry,
7
8
  RemoteQualitySchema,
8
9
  SignalSchema,
9
10
  __commonJS,
10
11
  __toESM,
11
12
  assertDirectSignal,
12
- probeRemote,
13
+ nativeDependencies,
13
14
  remoteSessionBudget,
14
15
  z
15
- } from "./chunk-UFLXQ4HQ.js";
16
+ } from "./chunk-OTKNRBZ7.js";
16
17
 
17
18
  // ../../node_modules/.pnpm/ajv@8.17.1/node_modules/ajv/dist/compile/codegen/code.js
18
19
  var require_code = __commonJS({
@@ -7812,18 +7813,22 @@ async function startDeviceControl(directory, devices, connections) {
7812
7813
  }
7813
7814
 
7814
7815
  // src/daemon.ts
7815
- import { mkdir as mkdir9 } from "node:fs/promises";
7816
- import { join as join15 } from "node:path";
7816
+ import { mkdir as mkdir10 } from "node:fs/promises";
7817
+ import { join as join16 } from "node:path";
7817
7818
 
7818
7819
  // src/workspace-diff.ts
7819
7820
  import { execFile as execFile2 } from "node:child_process";
7820
7821
  import { promisify as promisify2 } from "node:util";
7821
- import { realpath } from "node:fs/promises";
7822
+ import { realpath, stat as stat3 } from "node:fs/promises";
7822
7823
  import { relative, resolve } from "node:path";
7823
7824
  var execute = promisify2(execFile2);
7824
7825
  var MAX_PATCH = 256 * 1024;
7825
7826
  var MAX_TOTAL = 1024 * 1024;
7826
- async function workspaceDiff(workspace, project) {
7827
+ var MAX_LINES = 2e3;
7828
+ async function workspaceDiff(workspace, project, page = {}) {
7829
+ const offset = page.paged ? page.offset ?? 0 : 0;
7830
+ const limit = page.paged ? page.limit ?? 20 : 200;
7831
+ if (!Number.isSafeInteger(offset) || offset < 0 || !Number.isSafeInteger(limit) || limit < 1 || limit > 200) throw new Error("Invalid diff page");
7827
7832
  const root = await workspace.root(project);
7828
7833
  const git = async (...args) => (await execute("git", ["--no-optional-locks", "-c", "core.quotePath=false", ...args], {
7829
7834
  cwd: root,
@@ -7867,13 +7872,8 @@ async function workspaceDiff(workspace, project) {
7867
7872
  if (!base) throw new Error("\u65E0\u6CD5\u8BFB\u53D6 Git \u6BD4\u8F83\u57FA\u51C6");
7868
7873
  const files = [];
7869
7874
  let bytes = 0;
7870
- for (const entry of entries.slice(0, 200)) {
7875
+ for (const entry of entries.slice(offset, offset + limit)) {
7871
7876
  const change = { ...entry, patch: "", additions: 0, deletions: 0 };
7872
- if (bytes >= MAX_TOTAL) {
7873
- change.notice = "\u53D8\u66F4\u8F83\u591A\uFF0C\u672A\u52A0\u8F7D\u6B64\u6587\u4EF6\u5185\u5BB9";
7874
- files.push(change);
7875
- continue;
7876
- }
7877
7877
  try {
7878
7878
  if (entry.status === "??") {
7879
7879
  const read = await workspace.read(project, entry.file, 0, MAX_PATCH);
@@ -7890,16 +7890,30 @@ ${lines.map((line) => `+${line}`).join("\n")}
7890
7890
  `;
7891
7891
  }
7892
7892
  } else {
7893
- change.patch = await git("diff", "--no-ext-diff", "--no-textconv", "--no-renames", "--no-color", "--relative", "--unified=3", base, "--", entry.file);
7894
- change.binary = change.patch.includes("Binary files ");
7895
- if (Buffer.byteLength(change.patch) > MAX_PATCH) {
7896
- change.patch = "";
7897
- change.notice = "\u5DEE\u5F02\u8FC7\u5927\uFF0C\u8BF7\u5728\u672C\u5730 Git \u67E5\u770B";
7893
+ let currentSize = 0, baseSize = 0;
7894
+ try {
7895
+ currentSize = (await stat3(await workspace.path(project, entry.file))).size;
7896
+ } catch (error) {
7897
+ if (error.code !== "ENOENT") throw error;
7898
+ }
7899
+ try {
7900
+ baseSize = Number((await git("cat-file", "-s", `${base}:${prefix ? `${prefix}/` : ""}${entry.file}`)).trim());
7901
+ } catch {
7902
+ }
7903
+ if (Math.max(currentSize, baseSize) > MAX_PATCH) change.notice = "\u6587\u4EF6\u8D85\u8FC7 256 KiB\uFF0C\u672A\u52A0\u8F7D\u5DEE\u5F02\uFF0C\u8BF7\u5728\u6587\u4EF6\u9762\u677F\u6216\u672C\u5730 Git \u67E5\u770B";
7904
+ else {
7905
+ change.patch = await git("diff", "--no-ext-diff", "--no-textconv", "--no-renames", "--no-color", "--relative", "--unified=3", base, "--", entry.file);
7906
+ change.binary = change.patch.includes("Binary files ");
7898
7907
  }
7899
7908
  }
7900
7909
  } catch (error) {
7901
7910
  change.notice = error instanceof Error ? error.message.slice(0, 250) : "\u65E0\u6CD5\u52A0\u8F7D\u6587\u4EF6\u5DEE\u5F02";
7902
7911
  }
7912
+ if (Buffer.byteLength(change.patch) > MAX_PATCH || change.patch.split("\n").length > MAX_LINES) {
7913
+ change.patch = "";
7914
+ change.notice = "\u5DEE\u5F02\u8D85\u8FC7 256 KiB \u6216 2000 \u884C\uFF0C\u672A\u52A0\u8F7D\u5DEE\u5F02\uFF0C\u8BF7\u5728\u672C\u5730 Git \u67E5\u770B";
7915
+ }
7916
+ if (bytes + Buffer.byteLength(change.patch) > MAX_TOTAL) break;
7903
7917
  let inHunk = false;
7904
7918
  for (const line of change.patch.split("\n")) {
7905
7919
  if (line.startsWith("@@ ")) inHunk = true;
@@ -7909,12 +7923,13 @@ ${lines.map((line) => `+${line}`).join("\n")}
7909
7923
  bytes += Buffer.byteLength(change.patch);
7910
7924
  files.push(change);
7911
7925
  }
7912
- return { scope: "workspace", files, total: entries.length, truncated: entries.length > 200 || files.some((file) => file.notice), directory: prefix || ".", capturedAt: Date.now() };
7926
+ const nextOffset = offset + files.length < entries.length ? offset + files.length : void 0;
7927
+ return { scope: "workspace", files, total: entries.length, nextOffset, truncated: nextOffset !== void 0 || files.some((file) => file.notice), directory: prefix || ".", capturedAt: Date.now() };
7913
7928
  }
7914
7929
 
7915
7930
  // src/workspace.ts
7916
7931
  import { constants } from "node:fs";
7917
- import { mkdir as mkdir2, open as open2, readdir, realpath as realpath2, stat as stat3, link, unlink as unlink3 } from "node:fs/promises";
7932
+ import { mkdir as mkdir2, open as open2, readdir, realpath as realpath2, stat as stat4, link, unlink as unlink3 } from "node:fs/promises";
7918
7933
  import { basename, dirname, extname, isAbsolute as isAbsolute2, relative as relative2, resolve as resolve2, sep } from "node:path";
7919
7934
  import { createHash as createHash2 } from "node:crypto";
7920
7935
  var mimeOf = (name) => ({ ".png": "image/png", ".jpg": "image/jpeg", ".jpeg": "image/jpeg", ".gif": "image/gif", ".webp": "image/webp", ".svg": "image/svg+xml", ".md": "text/markdown", ".txt": "text/plain", ".json": "application/json", ".pdf": "application/pdf" })[extname(name).toLowerCase()] ?? "application/octet-stream";
@@ -7949,7 +7964,7 @@ var Workspace = class {
7949
7964
  const name = relative2(await this.root(id), resolve2(full, e.name));
7950
7965
  try {
7951
7966
  const safe = await this.path(id, name);
7952
- const info = await stat3(safe);
7967
+ const info = await stat4(safe);
7953
7968
  return { name: e.name, path: name, type: info.isDirectory() ? "directory" : "file", size: info.size };
7954
7969
  } catch {
7955
7970
  return null;
@@ -8046,7 +8061,7 @@ var Workspace = class {
8046
8061
 
8047
8062
  // src/uploads.ts
8048
8063
  import { constants as constants2 } from "node:fs";
8049
- import { open as open3, readdir as readdir2, unlink as unlink4, stat as stat4 } from "node:fs/promises";
8064
+ import { open as open3, readdir as readdir2, unlink as unlink4, stat as stat5 } from "node:fs/promises";
8050
8065
  import { basename as basename2, dirname as dirname3, join as join4 } from "node:path";
8051
8066
  import { createHash as createHash3, randomUUID as randomUUID4 } from "node:crypto";
8052
8067
 
@@ -8105,7 +8120,7 @@ var Uploads = class {
8105
8120
  if (!u || u.projectId !== projectId) throw new Error("Upload not found in this project");
8106
8121
  if (u.state === "pending" && u.offset === u.size) {
8107
8122
  try {
8108
- await stat4(u.temp);
8123
+ await stat5(u.temp);
8109
8124
  } catch (e) {
8110
8125
  if (e.code !== "ENOENT") throw e;
8111
8126
  const target = await this.workspace.path(projectId, u.destination);
@@ -8130,7 +8145,7 @@ var Uploads = class {
8130
8145
  } else destination = `${String(input.directory ?? ".")}/${name}`;
8131
8146
  const dest = await this.workspace.path(projectId, destination, true);
8132
8147
  try {
8133
- await stat4(dest);
8148
+ await stat5(dest);
8134
8149
  throw new Error("Destination exists; rename the file");
8135
8150
  } catch (e) {
8136
8151
  if (e.code !== "ENOENT") throw e;
@@ -8513,7 +8528,7 @@ function pageHistory(input, payload) {
8513
8528
  // src/artifacts.ts
8514
8529
  import { createHash as createHash5 } from "node:crypto";
8515
8530
  import { join as join6 } from "node:path";
8516
- import { readFile as readFile4, writeFile as writeFile3, mkdir as mkdir4, stat as stat5 } from "node:fs/promises";
8531
+ import { readFile as readFile4, writeFile as writeFile3, mkdir as mkdir4, stat as stat6 } from "node:fs/promises";
8517
8532
  var Artifacts = class {
8518
8533
  constructor(workspace, directory, emit) {
8519
8534
  this.workspace = workspace;
@@ -8537,6 +8552,12 @@ var Artifacts = class {
8537
8552
  scan(project) {
8538
8553
  return this.serial(project, () => this.scanUnlocked(project));
8539
8554
  }
8555
+ async scanPage(project, offset = 0, limit = 20) {
8556
+ if (!Number.isSafeInteger(offset) || offset < 0 || !Number.isSafeInteger(limit) || limit < 1 || limit > 200) throw new Error("Invalid artifact page");
8557
+ const all = (await this.scan(project)).sort((a, b) => a.path < b.path ? -1 : a.path > b.path ? 1 : 0);
8558
+ const items = all.slice(offset, offset + limit).map((item) => ({ ...item, revisions: item.revisions.slice(-1) }));
8559
+ return { items, total: all.length, nextOffset: offset + items.length < all.length ? offset + items.length : void 0 };
8560
+ }
8540
8561
  async scanUnlocked(project) {
8541
8562
  await this.workspace.root(project);
8542
8563
  const index = await readJson(this.index(project)) ?? {}, seen = /* @__PURE__ */ new Set();
@@ -8585,7 +8606,7 @@ var Artifacts = class {
8585
8606
  }
8586
8607
  for (const item of Object.values(index)) if (!seen.has(item.path) && !item.revisions.at(-1)?.deleted) {
8587
8608
  try {
8588
- await stat5(await this.workspace.path(project, item.path));
8609
+ await stat6(await this.workspace.path(project, item.path));
8589
8610
  } catch (e) {
8590
8611
  if (e.code === "ENOENT") {
8591
8612
  item.revisions.push({ hash: "", timestamp: (/* @__PURE__ */ new Date()).toISOString(), deleted: true });
@@ -8596,7 +8617,7 @@ var Artifacts = class {
8596
8617
  }
8597
8618
  if (changed) {
8598
8619
  await atomicJson(this.index(project), index);
8599
- this.emit(project, "artifacts.updated", Object.values(index));
8620
+ this.emit(project, "artifacts.updated", { total: Object.keys(index).length });
8600
8621
  }
8601
8622
  return Object.values(index);
8602
8623
  }
@@ -8607,6 +8628,14 @@ var Artifacts = class {
8607
8628
  if (!/^[a-f0-9]{64}$/.test(hash) || !(await this.history(project, path)).some((r) => r.hash === hash)) throw new Error("Unknown revision");
8608
8629
  return readFile4(join6(this.directory, project, "blobs", hash), "utf8");
8609
8630
  }
8631
+ async preview(project, path, hash) {
8632
+ if (!/^[a-f0-9]{64}$/.test(hash) || !(await this.history(project, path)).some((r) => r.hash === hash)) throw new Error("Unknown revision");
8633
+ const blob = join6(this.directory, project, "blobs", hash);
8634
+ if ((await stat6(blob)).size > 256 * 1024) return { content: "", notice: "\u6587\u4EF6\u8D85\u8FC7 256 KiB\uFF0C\u5DF2\u8DF3\u8FC7\u9884\u89C8\uFF0C\u8BF7\u5728\u6587\u4EF6\u9762\u677F\u6216\u672C\u5730\u67E5\u770B\u3002" };
8635
+ const content = await readFile4(blob, "utf8");
8636
+ if (content.split("\n").length > 2e3) return { content: "", notice: "\u6587\u4EF6\u8D85\u8FC7 2000 \u884C\uFF0C\u5DF2\u8DF3\u8FC7\u9884\u89C8\uFF0C\u8BF7\u5728\u6587\u4EF6\u9762\u677F\u6216\u672C\u5730\u67E5\u770B\u3002" };
8637
+ return { content };
8638
+ }
8610
8639
  associate(project, path, sessionId) {
8611
8640
  return this.serial(project, () => this.associateUnlocked(project, path, sessionId));
8612
8641
  }
@@ -8628,7 +8657,7 @@ import { TLSSocket, createSecureContext } from "node:tls";
8628
8657
 
8629
8658
  // src/host-auth.ts
8630
8659
  import { spawn } from "node:child_process";
8631
- import { chmod as chmod3, mkdir as mkdir5, rename as rename3, stat as stat6, unlink as unlink5 } from "node:fs/promises";
8660
+ import { chmod as chmod3, mkdir as mkdir5, rename as rename3, stat as stat7, unlink as unlink5 } from "node:fs/promises";
8632
8661
  import { userInfo, platform as platform2 } from "node:os";
8633
8662
  import { randomUUID as randomUUID6 } from "node:crypto";
8634
8663
  import { join as join7 } from "node:path";
@@ -8643,8 +8672,8 @@ async function ensureHostAuth(stateDirectory) {
8643
8672
  if (system !== "darwin" && system !== "linux") throw new Error("Host authentication is supported only on macOS and Linux");
8644
8673
  const mac = system === "darwin";
8645
8674
  const source = fileURLToPath2(new URL(mac ? "../native/auth-macos.m" : "../native/auth-linux.c", import.meta.url));
8646
- const sourceInfo = await stat6(source);
8647
- const existing = await stat6(target).catch(() => null);
8675
+ const sourceInfo = await stat7(source);
8676
+ const existing = await stat7(target).catch(() => null);
8648
8677
  if (existing?.isFile() && existing.mtimeMs >= sourceInfo.mtimeMs && (existing.mode & 63) === 0) return target;
8649
8678
  const temporary = `${target}.${randomUUID6()}`;
8650
8679
  const args = mac ? ["-O2", "-framework", "Foundation", "-framework", "OpenDirectory", source, "-o", target] : ["-O2", source, "-o", temporary, "-lpam"];
@@ -13617,7 +13646,7 @@ function createAdapter(runtime, workspace, uploads, emit) {
13617
13646
  }
13618
13647
 
13619
13648
  // src/project-registry.ts
13620
- import { realpath as realpath6, readFile as readFile9, stat as stat7 } from "node:fs/promises";
13649
+ import { realpath as realpath6, readFile as readFile9, stat as stat8 } from "node:fs/promises";
13621
13650
  import { isAbsolute as isAbsolute4 } from "node:path";
13622
13651
  import { isDeepStrictEqual } from "node:util";
13623
13652
  var ProjectMutationSchema = ProjectSchema.extend({
@@ -13705,7 +13734,7 @@ var ProjectRegistry = class {
13705
13734
  const raw = await this.readCurrent();
13706
13735
  const directory = expandHome(parsed.directory);
13707
13736
  if (!isAbsolute4(directory)) throw new Error("\u9879\u76EE\u76EE\u5F55\u5FC5\u987B\u662F Proxy \u4E3B\u673A\u4E0A\u7684\u7EDD\u5BF9\u8DEF\u5F84");
13708
- const info = await stat7(directory).catch(() => void 0);
13737
+ const info = await stat8(directory).catch(() => void 0);
13709
13738
  if (!info?.isDirectory()) throw new Error(`\u9879\u76EE\u76EE\u5F55\u4E0D\u5B58\u5728\u6216\u4E0D\u662F\u76EE\u5F55\uFF1A${directory}`);
13710
13739
  const canonical = await realpath6(directory);
13711
13740
  if (this.config.projects.some((project2) => project2.id === parsed.id)) throw new Error(`\u9879\u76EE ID \u5DF2\u5B58\u5728\uFF1A${parsed.id}`);
@@ -13724,7 +13753,7 @@ var ProjectRegistry = class {
13724
13753
  if (index < 0) throw new Error(`\u672A\u77E5\u6388\u6743\u9879\u76EE\uFF1A${parsed.id}`);
13725
13754
  const directory = expandHome(parsed.directory);
13726
13755
  if (!isAbsolute4(directory)) throw new Error("\u9879\u76EE\u76EE\u5F55\u5FC5\u987B\u662F Proxy \u4E3B\u673A\u4E0A\u7684\u7EDD\u5BF9\u8DEF\u5F84");
13727
- const info = await stat7(directory).catch(() => void 0);
13756
+ const info = await stat8(directory).catch(() => void 0);
13728
13757
  if (!info?.isDirectory()) throw new Error(`\u9879\u76EE\u76EE\u5F55\u4E0D\u5B58\u5728\u6216\u4E0D\u662F\u76EE\u5F55\uFF1A${directory}`);
13729
13758
  const canonical = await realpath6(directory);
13730
13759
  for (const project of this.config.projects) {
@@ -13755,7 +13784,7 @@ var ProjectRegistry = class {
13755
13784
 
13756
13785
  // src/terminal.ts
13757
13786
  import { randomUUID as randomUUID17 } from "node:crypto";
13758
- import { access as access2, stat as stat9 } from "node:fs/promises";
13787
+ import { access as access2, stat as stat10 } from "node:fs/promises";
13759
13788
  import { constants as constants5 } from "node:fs";
13760
13789
  import { platform as platform3, userInfo as userInfo3 } from "node:os";
13761
13790
  import { delimiter } from "node:path";
@@ -13763,7 +13792,7 @@ import * as pty from "node-pty";
13763
13792
 
13764
13793
  // src/terminal-support.ts
13765
13794
  import { createRequire } from "node:module";
13766
- import { access, chmod as chmod4, stat as stat8 } from "node:fs/promises";
13795
+ import { access, chmod as chmod4, stat as stat9 } from "node:fs/promises";
13767
13796
  import { constants as constants4 } from "node:fs";
13768
13797
  import { dirname as dirname6, resolve as resolve4 } from "node:path";
13769
13798
  var require2 = createRequire(import.meta.url);
@@ -13773,7 +13802,7 @@ function terminalHelperPath() {
13773
13802
  return resolve4(dirname6(utilsPath), native.dir, "spawn-helper");
13774
13803
  }
13775
13804
  async function prepareTerminalHelper(path, fix) {
13776
- const info = await stat8(path);
13805
+ const info = await stat9(path);
13777
13806
  if (!info.isFile()) throw new Error(`\u7EC8\u7AEF\u8F85\u52A9\u7A0B\u5E8F\u4E0D\u662F\u666E\u901A\u6587\u4EF6\uFF1A${path}`);
13778
13807
  try {
13779
13808
  await access(path, constants4.X_OK);
@@ -13832,7 +13861,7 @@ var TerminalManager = class {
13832
13861
  const count = [...this.terminals.values()].filter((t) => t.owner === owner).length;
13833
13862
  if (count >= 8) throw new Error("Terminal limit reached");
13834
13863
  const cwd = await this.workspace.path(project, typeof p.cwd === "string" ? p.cwd : ".");
13835
- if (!(await stat9(cwd)).isDirectory()) throw new Error(`\u7EC8\u7AEF\u5DE5\u4F5C\u76EE\u5F55\u4E0D\u662F\u76EE\u5F55\uFF1A${cwd}`);
13864
+ if (!(await stat10(cwd)).isDirectory()) throw new Error(`\u7EC8\u7AEF\u5DE5\u4F5C\u76EE\u5F55\u4E0D\u662F\u76EE\u5F55\uFF1A${cwd}`);
13836
13865
  const shell = process.env.SHELL || (platform3() === "darwin" ? "/bin/zsh" : "/bin/sh");
13837
13866
  try {
13838
13867
  await access2(shell, constants5.X_OK);
@@ -13959,8 +13988,49 @@ var PlatformDesktopSession = class {
13959
13988
 
13960
13989
  // src/remote-manager.ts
13961
13990
  import { fork } from "node:child_process";
13962
- import { randomBytes as randomBytes2, randomUUID as randomUUID18 } from "node:crypto";
13991
+ import { randomBytes as randomBytes2, randomUUID as randomUUID19 } from "node:crypto";
13992
+ import { fileURLToPath as fileURLToPath5 } from "node:url";
13993
+
13994
+ // src/remote-native.ts
13995
+ import { chmod as chmod5, mkdir as mkdir9, readFile as readFile10, rename as rename5, rm as rm3, stat as stat11, writeFile as writeFile7 } from "node:fs/promises";
13996
+ import { join as join14 } from "node:path";
13963
13997
  import { fileURLToPath as fileURLToPath4 } from "node:url";
13998
+ import { createHash as createHash10, randomUUID as randomUUID18 } from "node:crypto";
13999
+ async function ensureRemoteHelper(stateDirectory, adapter = RemotePlatformRegistry.standard.resolve()) {
14000
+ await mkdir9(stateDirectory, { recursive: true, mode: 448 });
14001
+ const build = adapter.helperBuild();
14002
+ const source = fileURLToPath4(new URL(`../native/${build.source}`, import.meta.url));
14003
+ const target = join14(stateDirectory, "octopus-remote-desktop");
14004
+ const stamp = `${target}.sha256`;
14005
+ const fingerprint = createHash10("sha256").update(await readFile10(source)).update(JSON.stringify([adapter.context.platform, adapter.context.arch, build.flags])).digest("hex");
14006
+ const existing = await stat11(target).catch(() => void 0);
14007
+ if (existing?.isFile() && (existing.mode & 63) === 0 && await readFile10(stamp, "utf8").catch(() => "") === fingerprint) return target;
14008
+ const temporary = `${target}.${randomUUID18()}`, temporaryStamp = `${temporary}.sha256`;
14009
+ const args = build.args(source, temporary);
14010
+ try {
14011
+ await nativeDependencies.execute(build.compiler, args, { timeout: 6e4, maxBuffer: 16384 });
14012
+ await chmod5(temporary, 448);
14013
+ await writeFile7(temporaryStamp, fingerprint, { mode: 384 });
14014
+ await rename5(temporary, target);
14015
+ await rename5(temporaryStamp, stamp);
14016
+ return target;
14017
+ } catch {
14018
+ throw new Error(build.error);
14019
+ } finally {
14020
+ await Promise.all([rm3(temporary, { force: true }), rm3(temporaryStamp, { force: true })]);
14021
+ }
14022
+ }
14023
+ async function probeRemote(stateDirectory, config, adapter = RemotePlatformRegistry.standard.resolve()) {
14024
+ const platformError = adapter.platformError();
14025
+ if (platformError) throw new Error(platformError);
14026
+ await import("@roamhq/wrtc").catch(() => {
14027
+ throw new Error("WebRTC \u539F\u751F\u4F9D\u8D56\u4E0D\u53EF\u7528\uFF0C\u8BF7\u91CD\u65B0\u5B89\u88C5\u65B0\u7248 Proxy");
14028
+ });
14029
+ const helper = await ensureRemoteHelper(stateDirectory, adapter);
14030
+ const result = JSON.parse((await nativeDependencies.execute(helper, ["check"], { timeout: 5e3, maxBuffer: 4096 })).stdout);
14031
+ await adapter.checkDependencies(config);
14032
+ return { helper, ...result };
14033
+ }
13964
14034
 
13965
14035
  // src/remote-stun.ts
13966
14036
  import { Resolver } from "node:dns/promises";
@@ -14027,13 +14097,13 @@ async function discoverRemoteStunUrls(hubUrl, configured, resolve5 = resolvePubl
14027
14097
 
14028
14098
  // src/remote-manager.ts
14029
14099
  function spawnWorker(message) {
14030
- const worker = fileURLToPath4(new URL(import.meta.url.endsWith(".ts") ? "./remote-worker.ts" : "./remote-worker.js", import.meta.url));
14100
+ const worker = fileURLToPath5(new URL(import.meta.url.endsWith(".ts") ? "./remote-worker.ts" : "./remote-worker.js", import.meta.url));
14031
14101
  const child = fork(worker, [], { stdio: ["ignore", "ignore", "ignore", "ipc"], execArgv: import.meta.url.endsWith(".ts") ? ["--import", "tsx"] : [] });
14032
14102
  child.send({ kind: "init", ...message });
14033
14103
  return child;
14034
14104
  }
14035
14105
  var RemoteManager = class {
14036
- constructor(host, desktop, desktopSession, send, touch, workerFactory = spawnWorker, probe = probeRemote, discoverStun = discoverRemoteStunUrls) {
14106
+ constructor(host, desktop, desktopSession, send, touch, workerFactory = spawnWorker, probe = probeRemote, discoverStun = discoverRemoteStunUrls, platformRegistry = RemotePlatformRegistry.standard) {
14037
14107
  this.host = host;
14038
14108
  this.desktop = desktop;
14039
14109
  this.desktopSession = desktopSession;
@@ -14042,23 +14112,25 @@ var RemoteManager = class {
14042
14112
  this.workerFactory = workerFactory;
14043
14113
  this.probe = probe;
14044
14114
  this.discoverStun = discoverStun;
14115
+ this.platform = platformRegistry.resolve();
14045
14116
  this.config = RemoteConfigSchema.parse(host.desktopRemote ?? {});
14046
14117
  }
14047
14118
  current;
14048
14119
  stopping = false;
14049
14120
  config;
14121
+ platform;
14050
14122
  stunUrls() {
14051
14123
  return this.config.stunUrls ?? defaultRemoteStunUrls(this.host.hubUrl);
14052
14124
  }
14053
14125
  async status() {
14054
- const base = { qualityModes: RemoteQualitySchema.options, platform: process.platform, passwordInput: process.platform === "darwin", enabled: this.config.enabled, busy: !!this.current, stunUrls: this.stunUrls(), budget: { ...REMOTE_DEFAULTS, totalBitrate: this.config.totalBitrate, videoMaxBitrate: this.config.videoMaxBitrate } };
14126
+ const base = { qualityModes: RemoteQualitySchema.options, platform: this.platform.context.platform, passwordInput: this.platform.passwordInput, enabled: this.config.enabled, busy: !!this.current, stunUrls: this.stunUrls(), budget: { ...REMOTE_DEFAULTS, totalBitrate: this.config.totalBitrate, videoMaxBitrate: this.config.videoMaxBitrate } };
14055
14127
  if (!this.config.enabled) return { ...base, available: false, message: "\u4E3B\u673A\u5DF2\u5173\u95ED\u8FDC\u7A0B\u63A7\u5236" };
14056
14128
  try {
14057
- const permissions = await this.probe(this.host.stateDirectory, this.config);
14058
- const lockUnsupported = permissions.locked === true && permissions.lockCapture === false;
14059
- const available = permissions.session && permissions.capture && permissions.input && !lockUnsupported;
14129
+ const permissions = await this.probe(this.host.stateDirectory, this.config, this.platform);
14130
+ const message = this.platform.unavailableReason(permissions);
14131
+ const available = message === void 0;
14060
14132
  if (available) await this.desktopSession.sessionIdentity();
14061
- return { ...base, available, permissions, ...!available ? { message: lockUnsupported ? "\u9501\u5C4F\u8FDC\u63A7\u9700\u8981 macOS 14 \u53CA\u4EE5\u4E0A\u53CA\u65B0\u7248 Xcode Command Line Tools\uFF1B\u5F53\u524D\u4E3B\u673A\u8BF7\u5148\u672C\u5730\u89E3\u9501" : "\u8BF7\u68C0\u67E5\u4E3B\u673A\u56FE\u5F62\u4F1A\u8BDD\uFF0C\u5E76\u4E3A\u5B9E\u9645\u8FD0\u884C Proxy \u7684\u7A0B\u5E8F\u6388\u6743\u5C4F\u5E55\u5F55\u5236\u548C\u8F85\u52A9\u529F\u80FD" } : {} };
14133
+ return { ...base, available, permissions, ...!available ? { message } : {} };
14062
14134
  } catch (error) {
14063
14135
  return { ...base, available: false, message: error instanceof Error ? error.message : "\u8FDC\u63A7\u4F9D\u8D56\u4E0D\u53EF\u7528" };
14064
14136
  }
@@ -14094,15 +14166,16 @@ var RemoteManager = class {
14094
14166
  if (this.current) throw new Error("\u4E3B\u673A\u6B63\u5728\u88AB\u63A7\u5236");
14095
14167
  const quality = RemoteQualitySchema.parse(payload.quality ?? "hd");
14096
14168
  const budget = remoteSessionBudget(this.config, quality, payload.quality === void 0);
14097
- const session = { remoteId: randomUUID18(), owner, closed: false, signaling: Promise.resolve(), signals: 0, signalWindow: Date.now(), candidates: 0 };
14169
+ const session = { remoteId: randomUUID19(), owner, closed: false, signaling: Promise.resolve(), signals: 0, signalWindow: Date.now(), candidates: 0 };
14098
14170
  this.current = session;
14099
14171
  try {
14100
14172
  const [permissions, stunUrls] = await Promise.all([
14101
- this.probe(this.host.stateDirectory, this.config),
14173
+ this.probe(this.host.stateDirectory, this.config, this.platform),
14102
14174
  this.discoverStun(this.host.hubUrl, this.config.stunUrls)
14103
14175
  ]);
14104
14176
  if (!permissions.session || !permissions.capture || !permissions.input) throw new Error("\u8FDC\u63A7\u9700\u8981\u4E3B\u673A\u56FE\u5F62\u4F1A\u8BDD\u3001\u5C4F\u5E55\u5F55\u5236\u53CA\u8F85\u52A9\u529F\u80FD\u6743\u9650");
14105
- if (permissions.locked === true && permissions.lockCapture === false) throw new Error("\u9501\u5C4F\u8FDC\u63A7\u9700\u8981 macOS 14 \u53CA\u4EE5\u4E0A\u53CA\u65B0\u7248 Xcode Command Line Tools\uFF1B\u5F53\u524D\u4E3B\u673A\u8BF7\u5148\u672C\u5730\u89E3\u9501");
14177
+ const unavailable = this.platform.unavailableReason(permissions);
14178
+ if (unavailable) throw new Error(unavailable);
14106
14179
  const identity = await this.desktopSession.sessionIdentity();
14107
14180
  const display = (await this.desktop.list()).find((d) => d.id === payload.displayId);
14108
14181
  if (!display) throw new Error("\u663E\u793A\u5668\u5DF2\u65AD\u5F00\uFF0C\u8BF7\u5237\u65B0\u663E\u793A\u5668\u5217\u8868");
@@ -14173,16 +14246,16 @@ var RemoteManager = class {
14173
14246
  };
14174
14247
 
14175
14248
  // src/tool-selection.ts
14176
- import { randomBytes as randomBytes3, randomUUID as randomUUID19 } from "node:crypto";
14177
- import { access as access3, constants as constants6, readFile as readFile10 } from "node:fs/promises";
14249
+ import { randomBytes as randomBytes3, randomUUID as randomUUID20 } from "node:crypto";
14250
+ import { access as access3, constants as constants6, readFile as readFile11 } from "node:fs/promises";
14178
14251
  import { platform as platform5 } from "node:os";
14179
14252
  import { isDeepStrictEqual as isDeepStrictEqual2 } from "node:util";
14180
14253
 
14181
14254
  // src/node-resolution.ts
14182
- import { realpath as realpath7, stat as stat10 } from "node:fs/promises";
14255
+ import { realpath as realpath7, stat as stat12 } from "node:fs/promises";
14183
14256
  import { execFile as execFile7 } from "node:child_process";
14184
14257
  import { promisify as promisify7 } from "node:util";
14185
- import { join as join14 } from "node:path";
14258
+ import { join as join15 } from "node:path";
14186
14259
  import { homedir as homedir2, platform as platform4 } from "node:os";
14187
14260
  var run4 = promisify7(execFile7);
14188
14261
  var VERSION_MANAGED_SEGMENTS = /* @__PURE__ */ new Set([".nvm", "nvm", ".volta", "volta", ".fnm", "fnm", ".asdf", "asdf", ".mise", "mise"]);
@@ -14226,15 +14299,15 @@ async function resolveNodePath(options = {}) {
14226
14299
  return { nodePath: null, source: "exec" };
14227
14300
  }
14228
14301
  function toolSearchDirs(kind) {
14229
- const dirs = [...describeTool(kind).searchDirs, join14(homedir2(), ".local", "bin"), join14(homedir2(), ".opencode", "bin"), join14(homedir2(), ".codex", "bin")];
14302
+ const dirs = [...describeTool(kind).searchDirs, join15(homedir2(), ".local", "bin"), join15(homedir2(), ".opencode", "bin"), join15(homedir2(), ".codex", "bin")];
14230
14303
  return [...new Set(dirs)];
14231
14304
  }
14232
14305
  async function resolveCommandOnPath(name, extraDirs = []) {
14233
14306
  const dirs = [...(process.env.PATH ?? "").split(":"), ...extraDirs].filter(Boolean);
14234
14307
  for (const dir of new Set(dirs)) {
14235
- const full = join14(dir, name);
14308
+ const full = join15(dir, name);
14236
14309
  try {
14237
- const info = await stat10(full);
14310
+ const info = await stat12(full);
14238
14311
  if (info.isFile() && info.mode & 73) return full;
14239
14312
  } catch {
14240
14313
  }
@@ -14317,7 +14390,7 @@ var ToolSelection = class {
14317
14390
  });
14318
14391
  }
14319
14392
  modelCommand(projectId) {
14320
- return { type: "command", id: randomUUID19(), botId: this.config.botId, operation: "configuration.models", projectId, payload: {} };
14393
+ return { type: "command", id: randomUUID20(), botId: this.config.botId, operation: "configuration.models", projectId, payload: {} };
14321
14394
  }
14322
14395
  profiles(raw) {
14323
14396
  return raw.toolProfiles && typeof raw.toolProfiles === "object" && !Array.isArray(raw.toolProfiles) ? raw.toolProfiles : {};
@@ -14359,7 +14432,7 @@ var ToolSelection = class {
14359
14432
  if (!isDeepStrictEqual2(JSON.parse(JSON.stringify(disk.tool)), JSON.parse(JSON.stringify(this.config.tool))) || !isDeepStrictEqual2(disk.projects, this.config.projects) || disk.botId !== this.config.botId || disk.secretKey !== this.config.secretKey) {
14360
14433
  throw new Error("Proxy \u914D\u7F6E\u5DF2\u5728\u4E3B\u673A\u4E0A\u88AB\u4FEE\u6539\uFF1B\u8BF7\u91CD\u542F Proxy \u540E\u518D\u5207\u6362\u667A\u80FD\u4F53");
14361
14434
  }
14362
- return JSON.parse(await readFile10(this.configPath, "utf8"));
14435
+ return JSON.parse(await readFile11(this.configPath, "utf8"));
14363
14436
  }
14364
14437
  async release(instance) {
14365
14438
  instance.adapter.close();
@@ -14376,8 +14449,8 @@ var ToolSelection = class {
14376
14449
  // src/daemon.ts
14377
14450
  async function runDaemon(configPath, authenticate = verifyHostPassword) {
14378
14451
  const config = await loadConfig(configPath);
14379
- await mkdir9(config.stateDirectory, { recursive: true, mode: 448 });
14380
- const workspace = new Workspace(config.projects), uploads = new Uploads(workspace, join15(config.stateDirectory, "uploads")), journal = new Journal(join15(config.stateDirectory, "journal"));
14452
+ await mkdir10(config.stateDirectory, { recursive: true, mode: 448 });
14453
+ const workspace = new Workspace(config.projects), uploads = new Uploads(workspace, join16(config.stateDirectory, "uploads")), journal = new Journal(join16(config.stateDirectory, "journal"));
14381
14454
  let secureChat;
14382
14455
  function emit(projectId, event, data) {
14383
14456
  secureChat?.publish(projectId, event, data);
@@ -14399,7 +14472,7 @@ async function runDaemon(configPath, authenticate = verifyHostPassword) {
14399
14472
  });
14400
14473
  return { runtime, adapter };
14401
14474
  });
14402
- const artifacts = new Artifacts(workspace, join15(config.stateDirectory, "artifacts"), emit);
14475
+ const artifacts = new Artifacts(workspace, join16(config.stateDirectory, "artifacts"), emit);
14403
14476
  const projectRegistry = () => new ProjectRegistry(configPath, config, tools.current.runtime, tools.current.adapter, (resetProjects) => {
14404
14477
  for (const id of resetProjects) terminals.closeProject(id);
14405
14478
  });
@@ -14444,7 +14517,7 @@ async function runDaemon(configPath, authenticate = verifyHostPassword) {
14444
14517
  workspace.project(project);
14445
14518
  if (c.operation === "workspace.list") return p.paged === true ? workspace.listPage(project, String(p.path ?? "."), p.cursor === void 0 ? "" : p.cursor, p.limit === void 0 ? 200 : Number(p.limit)) : workspace.list(project, String(p.path ?? "."));
14446
14519
  if (c.operation === "workspace.read") return workspace.read(project, String(p.path), Number(p.offset ?? 0), Number(p.length ?? 512 * 1024));
14447
- if (c.operation === "workspace.diff") return workspaceDiff(workspace, project);
14520
+ if (c.operation === "workspace.diff") return workspaceDiff(workspace, project, { paged: p.paged === true, offset: Number(p.offset ?? 0), limit: Number(p.limit ?? 20) });
14448
14521
  if (c.operation === "workspace.search") return workspace.search(project, String(p.query));
14449
14522
  if (c.operation === "workspace.write") return workspace.write(project, String(p.path), String(p.content), String(p.expectedHash));
14450
14523
  if (c.operation === "uploads.init") {
@@ -14458,9 +14531,9 @@ async function runDaemon(configPath, authenticate = verifyHostPassword) {
14458
14531
  if (c.operation === "uploads.chunk") return uploads.chunk(project, String(p.id), Number(p.offset), String(p.data));
14459
14532
  if (c.operation === "uploads.complete") return uploads.complete(project, String(p.id));
14460
14533
  if (c.operation === "uploads.cancel") return uploads.cancel(project, String(p.id));
14461
- if (c.operation === "artifacts.list") return artifacts.scan(project);
14534
+ if (c.operation === "artifacts.list") return p.paged === true ? artifacts.scanPage(project, Number(p.offset ?? 0), Number(p.limit ?? 20)) : artifacts.scan(project);
14462
14535
  if (c.operation === "artifacts.history") return artifacts.history(project, String(p.path));
14463
- if (c.operation === "artifacts.read") return artifacts.read(project, String(p.path), String(p.hash));
14536
+ if (c.operation === "artifacts.read") return p.preview === true ? artifacts.preview(project, String(p.path), String(p.hash)) : artifacts.read(project, String(p.path), String(p.hash));
14464
14537
  if (c.operation === "artifacts.associate") {
14465
14538
  if (!c.sessionId) throw new Error("Session required");
14466
14539
  await adapter.session(project, c.sessionId);
@@ -4449,66 +4449,16 @@ var DesktopManager = class {
4449
4449
  }
4450
4450
  };
4451
4451
 
4452
- // src/remote-native.ts
4452
+ // src/remote/native-process.ts
4453
4453
  import { execFile as execFile2, spawn } from "node:child_process";
4454
4454
  import { promisify as promisify2 } from "node:util";
4455
- import { chmod as chmod2, mkdir as mkdir2, readFile as readFile2, rename as rename2, rm as rm2, stat as stat2, writeFile as writeFile2 } from "node:fs/promises";
4456
- import { join as join2 } from "node:path";
4457
- import { fileURLToPath as fileURLToPath2 } from "node:url";
4458
- import { createHash as createHash2, randomUUID as randomUUID2 } from "node:crypto";
4459
4455
  var exec = promisify2(execFile2);
4460
- function remotePlatformError(system = process.platform, arch = process.arch, env = process.env) {
4461
- if (!["darwin", "linux"].includes(system) || !["arm64", "x64"].includes(arch)) return `\u8FDC\u63A7\u4E0D\u652F\u6301\u5F53\u524D\u5E73\u53F0 ${system}/${arch}\uFF1B\u9700\u8981 macOS 12.3+ \u6216 Linux X11\uFF08x64/ARM64\uFF09`;
4462
- if (system === "linux") {
4463
- if (env.WAYLAND_DISPLAY || env.XDG_SESSION_TYPE === "wayland") return "\u8FDC\u63A7\u6682\u4E0D\u652F\u6301 Wayland\uFF0C\u8BF7\u4F7F\u7528 X11 \u684C\u9762\u4F1A\u8BDD";
4464
- if (!env.DISPLAY) return "Proxy \u672A\u8FDE\u63A5 X11 \u56FE\u5F62\u4F1A\u8BDD\uFF1B\u8BF7\u4E3A\u5B9E\u9645\u8FD0\u884C Proxy \u7684\u670D\u52A1\u8BBE\u7F6E\u684C\u9762\u7528\u6237\u7684 DISPLAY \u548C XAUTHORITY\uFF0C\u5BB9\u5668\u4E2D\u8BF7\u5148\u542F\u52A8 Xvfb/\u684C\u9762\u670D\u52A1";
4465
- }
4466
- }
4467
- async function ensureRemoteHelper(stateDirectory) {
4468
- await mkdir2(stateDirectory, { recursive: true, mode: 448 });
4469
- const mac = process.platform === "darwin";
4470
- const source = fileURLToPath2(new URL(mac ? "../native/remote-macos.m" : "../native/remote-x11.c", import.meta.url));
4471
- const target = join2(stateDirectory, "octopus-remote-desktop");
4472
- const stamp = `${target}.sha256`;
4473
- const flags = mac ? ["-O2", "-fobjc-arc", "-mmacosx-version-min=12.3", "-framework", "Foundation", "-framework", "AppKit", "-framework", "ScreenCaptureKit", "-framework", "CoreMedia", "-framework", "CoreVideo", "-framework", "ApplicationServices", "-framework", "Carbon", "-framework", "IOKit", "-framework", "VideoToolbox"] : ["-O2", "-lX11", "-lXtst"];
4474
- const fingerprint = createHash2("sha256").update(await readFile2(source)).update(JSON.stringify([process.platform, process.arch, flags])).digest("hex");
4475
- const existing = await stat2(target).catch(() => void 0);
4476
- if (existing?.isFile() && (existing.mode & 63) === 0 && await readFile2(stamp, "utf8").catch(() => "") === fingerprint) return target;
4477
- const temporary = `${target}.${randomUUID2()}`, temporaryStamp = `${temporary}.sha256`;
4478
- const args = mac ? [...flags, source, "-o", temporary] : ["-O2", source, "-o", temporary, "-lX11", "-lXtst"];
4479
- try {
4480
- await exec(mac ? "/usr/bin/clang" : "cc", args, { timeout: 6e4, maxBuffer: 16384 });
4481
- await chmod2(temporary, 448);
4482
- await writeFile2(temporaryStamp, fingerprint, { mode: 384 });
4483
- await rename2(temporary, target);
4484
- await rename2(temporaryStamp, stamp);
4485
- return target;
4486
- } catch {
4487
- throw new Error(mac ? "\u8FDC\u63A7\u8F85\u52A9\u7A0B\u5E8F\u7F16\u8BD1\u5931\u8D25\uFF0C\u8BF7\u5B89\u88C5 Xcode Command Line Tools" : "\u8FDC\u63A7\u8F85\u52A9\u7A0B\u5E8F\u7F16\u8BD1\u5931\u8D25\uFF0C\u8BF7\u5B89\u88C5 cc\u3001libx11-dev\u3001libxtst-dev");
4488
- } finally {
4489
- await Promise.all([rm2(temporary, { force: true }), rm2(temporaryStamp, { force: true })]);
4490
- }
4491
- }
4492
- async function probeRemote(stateDirectory, config) {
4493
- const platformError = remotePlatformError();
4494
- if (platformError) throw new Error(platformError);
4495
- await import("@roamhq/wrtc").catch(() => {
4496
- throw new Error("WebRTC \u539F\u751F\u4F9D\u8D56\u4E0D\u53EF\u7528\uFF0C\u8BF7\u91CD\u65B0\u5B89\u88C5\u65B0\u7248 Proxy");
4497
- });
4498
- const helper = await ensureRemoteHelper(stateDirectory);
4499
- const result = JSON.parse((await exec(helper, ["check"], { timeout: 5e3, maxBuffer: 4096 })).stdout);
4500
- if (process.platform === "linux") {
4501
- const version = await exec(config.ffmpeg, ["-version"], { timeout: 5e3, maxBuffer: 32768 }).catch(() => {
4502
- throw new Error("\u8BF7\u5B89\u88C5\u652F\u6301 x11grab \u7684 LGPL FFmpeg");
4503
- });
4504
- assertLgplFFmpeg(version.stdout);
4505
- const devices = await exec(config.ffmpeg, ["-hide_banner", "-devices"], { timeout: 5e3, maxBuffer: 32768 });
4506
- if (!devices.stdout.includes("x11grab")) throw new Error("FFmpeg \u7F3A\u5C11 x11grab \u91C7\u96C6\u652F\u6301");
4507
- }
4508
- return { helper, ...result };
4509
- }
4510
- function nativeProcess(helper, displayId, video) {
4511
- return spawn(helper, ["serve", displayId, String(video.width), String(video.height), String(video.fps)], { stdio: ["pipe", "pipe", "pipe", "pipe"] });
4456
+ var nativeDependencies = {
4457
+ execute: async (file, args, options) => ({ stdout: String((await exec(file, args, options)).stdout) }),
4458
+ spawn
4459
+ };
4460
+ function nativeProcess(helper, displayId, video, dependencies = nativeDependencies) {
4461
+ return dependencies.spawn(helper, ["serve", displayId, String(video.width), String(video.height), String(video.fps)], { stdio: ["pipe", "pipe", "pipe", "pipe"] });
4512
4462
  }
4513
4463
  function terminateNative(child) {
4514
4464
  if (!child) return;
@@ -4519,6 +4469,307 @@ function terminateNative(child) {
4519
4469
  child.once("exit", () => clearTimeout(timer));
4520
4470
  }
4521
4471
 
4472
+ // src/remote/native-session.ts
4473
+ var NativeSession = class {
4474
+ constructor(options, dependencies = nativeDependencies) {
4475
+ this.options = options;
4476
+ this.dependencies = dependencies;
4477
+ this.video = options.video;
4478
+ }
4479
+ helper;
4480
+ stopped = false;
4481
+ video;
4482
+ starting;
4483
+ start() {
4484
+ return this.starting ??= this.startOnce().catch((error) => {
4485
+ this.stop();
4486
+ throw error;
4487
+ });
4488
+ }
4489
+ async startOnce() {
4490
+ if (this.stopped) return;
4491
+ await this.prepare();
4492
+ if (this.stopped) return;
4493
+ const child = nativeProcess(this.options.helper, this.options.display.id, this.video, this.dependencies);
4494
+ this.helper = child;
4495
+ child.on("error", () => this.fail("\u4E3B\u673A\u8F93\u5165\u8F85\u52A9\u7A0B\u5E8F\u4E0D\u53EF\u7528"));
4496
+ child.on("exit", () => this.fail("\u4E3B\u673A\u6743\u9650\u3001\u663E\u793A\u5668\u6216\u56FE\u5F62\u4F1A\u8BDD\u5DF2\u53D8\u5316"));
4497
+ child.stdin?.on("error", () => this.fail("\u4E3B\u673A\u8F93\u5165\u8FDE\u63A5\u5DF2\u5173\u95ED"));
4498
+ let lines = "";
4499
+ child.stdout?.on("data", (chunk) => {
4500
+ if (this.stopped) return;
4501
+ lines += String(chunk);
4502
+ if (lines.length > 15e5) {
4503
+ this.fail("\u4E3B\u673A\u526A\u8D34\u677F\u8D85\u8FC7\u4E0A\u9650");
4504
+ return;
4505
+ }
4506
+ let end;
4507
+ while (!this.stopped && (end = lines.indexOf("\n")) >= 0) {
4508
+ const line = lines.slice(0, end);
4509
+ lines = lines.slice(end + 1);
4510
+ try {
4511
+ const message = JSON.parse(line);
4512
+ if (message.kind === "error") {
4513
+ this.fail("\u4E3B\u673A\u6743\u9650\u3001\u663E\u793A\u5668\u6216\u56FE\u5F62\u4F1A\u8BDD\u5DF2\u53D8\u5316");
4514
+ return;
4515
+ }
4516
+ this.options.onMessage(message);
4517
+ } catch {
4518
+ this.fail("\u4E3B\u673A\u8F85\u52A9\u7A0B\u5E8F\u8FD4\u56DE\u65E0\u6548\u6570\u636E");
4519
+ }
4520
+ }
4521
+ });
4522
+ this.startCapture(child);
4523
+ }
4524
+ async prepare() {
4525
+ }
4526
+ stopCapture() {
4527
+ }
4528
+ fail(message) {
4529
+ if (this.stopped) return;
4530
+ this.stop();
4531
+ this.options.onError(message);
4532
+ }
4533
+ writeCommand(line) {
4534
+ if (this.stopped || !this.helper?.stdin || this.helper.stdin.destroyed) return;
4535
+ if (this.helper.stdin.writableLength > 2 * 1024 * 1024) {
4536
+ this.fail("\u4E3B\u673A\u8F93\u5165\u5904\u7406\u8D85\u65F6");
4537
+ return;
4538
+ }
4539
+ this.helper.stdin.write(`${line}
4540
+ `);
4541
+ }
4542
+ input(input) {
4543
+ switch (input.kind) {
4544
+ case "pointer":
4545
+ this.writeCommand(`pointer ${input.x} ${input.y}`);
4546
+ break;
4547
+ case "button":
4548
+ this.writeCommand(`button ${input.x} ${input.y} ${input.button} ${input.down ? 1 : 0}`);
4549
+ break;
4550
+ case "wheel":
4551
+ this.writeCommand(`wheel ${input.x} ${input.y} ${Math.round(input.dx)} ${Math.round(input.dy)}`);
4552
+ break;
4553
+ case "key":
4554
+ this.writeCommand(`key ${input.code} ${input.down ? 1 : 0}`);
4555
+ break;
4556
+ case "text":
4557
+ this.writeCommand(`text ${input.terminal ? 1 : 0} ${Buffer.from(input.text).toString("base64")}`);
4558
+ break;
4559
+ case "release":
4560
+ this.writeCommand("release");
4561
+ break;
4562
+ }
4563
+ }
4564
+ stop() {
4565
+ if (this.stopped) return;
4566
+ this.stopped = true;
4567
+ this.stopCapture();
4568
+ const helper = this.helper;
4569
+ this.helper = void 0;
4570
+ terminateNative(helper);
4571
+ }
4572
+ };
4573
+
4574
+ // src/remote/platform-adapter.ts
4575
+ function unsupportedPlatform(context) {
4576
+ return `\u8FDC\u63A7\u4E0D\u652F\u6301\u5F53\u524D\u5E73\u53F0 ${context.platform}/${context.arch}\uFF1B\u9700\u8981 macOS 12.3+ \u6216 Linux X11\uFF08x64/ARM64\uFF09`;
4577
+ }
4578
+ function missingPermissions(permissions) {
4579
+ if (!permissions.session || !permissions.capture || !permissions.input) return "\u8BF7\u68C0\u67E5\u4E3B\u673A\u56FE\u5F62\u4F1A\u8BDD\uFF0C\u5E76\u4E3A\u5B9E\u9645\u8FD0\u884C Proxy \u7684\u7A0B\u5E8F\u6388\u6743\u5C4F\u5E55\u5F55\u5236\u548C\u8F85\u52A9\u529F\u80FD";
4580
+ }
4581
+
4582
+ // src/remote/linux-x11-adapter.ts
4583
+ var LinuxX11RemoteAdapter = class {
4584
+ constructor(context, dependencies = nativeDependencies) {
4585
+ this.context = context;
4586
+ this.dependencies = dependencies;
4587
+ }
4588
+ passwordInput = false;
4589
+ platformError() {
4590
+ if (!["arm64", "x64"].includes(this.context.arch)) return unsupportedPlatform(this.context);
4591
+ const env = this.context.env;
4592
+ if (env.WAYLAND_DISPLAY || env.XDG_SESSION_TYPE === "wayland") return "\u8FDC\u63A7\u6682\u4E0D\u652F\u6301 Wayland\uFF0C\u8BF7\u4F7F\u7528 X11 \u684C\u9762\u4F1A\u8BDD";
4593
+ if (!env.DISPLAY) return "Proxy \u672A\u8FDE\u63A5 X11 \u56FE\u5F62\u4F1A\u8BDD\uFF1B\u8BF7\u4E3A\u5B9E\u9645\u8FD0\u884C Proxy \u7684\u670D\u52A1\u8BBE\u7F6E\u684C\u9762\u7528\u6237\u7684 DISPLAY \u548C XAUTHORITY\uFF0C\u5BB9\u5668\u4E2D\u8BF7\u5148\u542F\u52A8 Xvfb/\u684C\u9762\u670D\u52A1";
4594
+ }
4595
+ helperBuild() {
4596
+ return { source: "remote-x11.c", compiler: "cc", flags: ["-O2", "-lX11", "-lXtst"], args: (source, target) => ["-O2", source, "-o", target, "-lX11", "-lXtst"], error: "\u8FDC\u63A7\u8F85\u52A9\u7A0B\u5E8F\u7F16\u8BD1\u5931\u8D25\uFF0C\u8BF7\u5B89\u88C5 cc\u3001libx11-dev\u3001libxtst-dev" };
4597
+ }
4598
+ async checkDependencies(config) {
4599
+ const execute = this.dependencies.execute;
4600
+ const version = await execute(config.ffmpeg, ["-version"], { timeout: 5e3, maxBuffer: 32768 }).catch(() => {
4601
+ throw new Error("\u8BF7\u5B89\u88C5\u652F\u6301 x11grab \u7684 LGPL FFmpeg");
4602
+ });
4603
+ assertLgplFFmpeg(version.stdout);
4604
+ const devices = await execute(config.ffmpeg, ["-hide_banner", "-devices"], { timeout: 5e3, maxBuffer: 32768 });
4605
+ if (!devices.stdout.includes("x11grab")) throw new Error("FFmpeg \u7F3A\u5C11 x11grab \u91C7\u96C6\u652F\u6301");
4606
+ }
4607
+ unavailableReason = missingPermissions;
4608
+ createSession(options) {
4609
+ return new LinuxX11RemoteSession(options, this.context, this.dependencies);
4610
+ }
4611
+ };
4612
+ var LinuxX11RemoteSession = class extends NativeSession {
4613
+ constructor(options, context, dependencies) {
4614
+ super(options, dependencies);
4615
+ this.context = context;
4616
+ }
4617
+ capture;
4618
+ geometry;
4619
+ async prepare() {
4620
+ const displays = parseXrandr((await this.dependencies.execute("xrandr", ["--listactivemonitors"], { timeout: 5e3 })).stdout);
4621
+ if (this.stopped) return;
4622
+ const display = displays.find((d) => d.id === this.options.display.id);
4623
+ if (!display || display.width !== this.options.display.width || display.height !== this.options.display.height) throw new Error("display");
4624
+ this.geometry = { x: display.x, y: display.y, width: display.width, height: display.height };
4625
+ }
4626
+ startCapture(_child) {
4627
+ const g = this.geometry;
4628
+ this.writeCommand(`geometry ${g.x} ${g.y} ${g.width} ${g.height}`);
4629
+ this.captureFrames();
4630
+ }
4631
+ captureFrames() {
4632
+ this.stopCapture();
4633
+ if (this.stopped) return;
4634
+ const g = this.geometry, { width, height, fps } = this.video;
4635
+ const child = this.dependencies.spawn(this.options.config.ffmpeg, ["-nostdin", "-hide_banner", "-loglevel", "error", "-f", "x11grab", "-framerate", String(fps), "-video_size", `${g.width}x${g.height}`, "-grab_x", String(g.x), "-grab_y", String(g.y), "-draw_mouse", "1", "-i", this.context.env.DISPLAY, "-vf", `scale=${width}:${height}`, "-pix_fmt", "yuv420p", "-f", "rawvideo", "pipe:1"], { stdio: ["ignore", "pipe", "ignore"], env: this.context.env });
4636
+ this.capture = child;
4637
+ const current = () => !this.stopped && this.capture === child;
4638
+ let buffer = Buffer.alloc(0);
4639
+ const length = width * height * 1.5;
4640
+ child.stdout.on("data", (chunk) => {
4641
+ if (!current()) return;
4642
+ buffer = Buffer.concat([buffer, chunk]);
4643
+ if (buffer.length > length * 4) {
4644
+ this.fail("\u4E3B\u673A\u91C7\u96C6\u7F13\u51B2\u533A\u8D85\u9650");
4645
+ return;
4646
+ }
4647
+ if (buffer.length >= length) {
4648
+ const count = Math.floor(buffer.length / length), frame = buffer.subarray((count - 1) * length, count * length);
4649
+ buffer = buffer.subarray(count * length);
4650
+ this.options.onFrame({ width, height, data: new Uint8Array(frame) });
4651
+ }
4652
+ });
4653
+ child.on("error", () => {
4654
+ if (current()) this.fail("FFmpeg \u91C7\u96C6\u5931\u8D25");
4655
+ });
4656
+ child.on("exit", () => {
4657
+ if (current()) this.fail("\u663E\u793A\u5668\u5DF2\u53D8\u5316\u6216 FFmpeg \u91C7\u96C6\u5931\u8D25");
4658
+ });
4659
+ }
4660
+ setVideo(video) {
4661
+ this.video = video;
4662
+ if (this.helper && !this.stopped) this.captureFrames();
4663
+ }
4664
+ stopCapture() {
4665
+ const child = this.capture;
4666
+ this.capture = void 0;
4667
+ terminateNative(child);
4668
+ }
4669
+ };
4670
+
4671
+ // src/remote/macos-adapter.ts
4672
+ var MAC_LOCK_CAPTURE_ERROR = "\u9501\u5C4F\u8FDC\u63A7\u9700\u8981 macOS 14 \u53CA\u4EE5\u4E0A\u53CA\u65B0\u7248 Xcode Command Line Tools\uFF1B\u5F53\u524D\u4E3B\u673A\u8BF7\u5148\u672C\u5730\u89E3\u9501";
4673
+ var MacOSRemoteAdapter = class {
4674
+ constructor(context, dependencies = nativeDependencies) {
4675
+ this.context = context;
4676
+ this.dependencies = dependencies;
4677
+ }
4678
+ passwordInput = true;
4679
+ platformError() {
4680
+ if (!["arm64", "x64"].includes(this.context.arch)) return unsupportedPlatform(this.context);
4681
+ }
4682
+ helperBuild() {
4683
+ const flags = ["-O2", "-fobjc-arc", "-mmacosx-version-min=12.3", "-framework", "Foundation", "-framework", "AppKit", "-framework", "ScreenCaptureKit", "-framework", "CoreMedia", "-framework", "CoreVideo", "-framework", "ApplicationServices", "-framework", "Carbon", "-framework", "IOKit", "-framework", "VideoToolbox"];
4684
+ return { source: "remote-macos.m", compiler: "/usr/bin/clang", flags, args: (source, target) => [...flags, source, "-o", target], error: "\u8FDC\u63A7\u8F85\u52A9\u7A0B\u5E8F\u7F16\u8BD1\u5931\u8D25\uFF0C\u8BF7\u5B89\u88C5 Xcode Command Line Tools" };
4685
+ }
4686
+ async checkDependencies(_config) {
4687
+ }
4688
+ unavailableReason(permissions) {
4689
+ if (permissions.locked === true && permissions.lockCapture === false) return MAC_LOCK_CAPTURE_ERROR;
4690
+ return missingPermissions(permissions);
4691
+ }
4692
+ createSession(options) {
4693
+ return new MacOSRemoteSession(options, this.dependencies);
4694
+ }
4695
+ };
4696
+ var MacOSRemoteSession = class extends NativeSession {
4697
+ startCapture(child) {
4698
+ const stream = child.stdio[3];
4699
+ let buffer = Buffer.alloc(0);
4700
+ stream.on("data", (chunk) => {
4701
+ if (this.stopped) return;
4702
+ buffer = Buffer.concat([buffer, chunk]);
4703
+ if (buffer.length > REMOTE_DEFAULTS.maxWidth * REMOTE_DEFAULTS.maxHeight * 3 + 24) {
4704
+ this.fail("\u8FDC\u63A7\u753B\u9762\u7F13\u51B2\u533A\u8D85\u9650");
4705
+ return;
4706
+ }
4707
+ let latest;
4708
+ while (buffer.length >= 12) {
4709
+ const width = buffer.readUInt32BE(0), height = buffer.readUInt32BE(4), length = buffer.readUInt32BE(8);
4710
+ if (!width || !height || width > REMOTE_DEFAULTS.maxWidth || height > REMOTE_DEFAULTS.maxHeight || width % 2 || height % 2 || length !== width * height * 1.5) {
4711
+ this.fail("\u4E3B\u673A\u753B\u9762\u683C\u5F0F\u65E0\u6548");
4712
+ return;
4713
+ }
4714
+ if (buffer.length < length + 12) break;
4715
+ latest = { width, height, data: new Uint8Array(buffer.subarray(12, 12 + length)) };
4716
+ buffer = buffer.subarray(12 + length);
4717
+ }
4718
+ if (latest) this.options.onFrame(latest);
4719
+ });
4720
+ }
4721
+ setVideo(video) {
4722
+ this.video = video;
4723
+ this.writeCommand(`video ${video.width} ${video.height} ${video.fps}`);
4724
+ }
4725
+ input(input) {
4726
+ if (input.kind === "password") this.writeCommand(`password ${Buffer.from(input.text).toString("base64")}`);
4727
+ else super.input(input);
4728
+ }
4729
+ };
4730
+
4731
+ // src/remote/unsupported-adapter.ts
4732
+ var UnsupportedRemoteAdapter = class {
4733
+ constructor(context) {
4734
+ this.context = context;
4735
+ }
4736
+ passwordInput = false;
4737
+ platformError() {
4738
+ return unsupportedPlatform(this.context);
4739
+ }
4740
+ helperBuild() {
4741
+ throw new Error(this.platformError());
4742
+ }
4743
+ async checkDependencies(_config) {
4744
+ throw new Error(this.platformError());
4745
+ }
4746
+ unavailableReason(_permissions) {
4747
+ return this.platformError();
4748
+ }
4749
+ createSession(_options) {
4750
+ throw new Error(this.platformError());
4751
+ }
4752
+ };
4753
+
4754
+ // src/remote/platform-registry.ts
4755
+ var RemotePlatformRegistry = class _RemotePlatformRegistry {
4756
+ static standard = new _RemotePlatformRegistry({
4757
+ darwin: (context) => new MacOSRemoteAdapter(context),
4758
+ linux: (context) => new LinuxX11RemoteAdapter(context)
4759
+ });
4760
+ factories;
4761
+ constructor(factories) {
4762
+ this.factories = Object.freeze({ ...factories });
4763
+ }
4764
+ withAdapter(platform, factory) {
4765
+ return new _RemotePlatformRegistry({ ...this.factories, [platform]: factory });
4766
+ }
4767
+ resolve(context = { platform: process.platform, arch: process.arch, env: process.env }) {
4768
+ const factory = Object.hasOwn(this.factories, context.platform) ? this.factories[context.platform] : void 0;
4769
+ return factory?.(context) ?? new UnsupportedRemoteAdapter(context);
4770
+ }
4771
+ };
4772
+
4522
4773
  export {
4523
4774
  __commonJS,
4524
4775
  __toESM,
@@ -4539,10 +4790,8 @@ export {
4539
4790
  DESKTOP_PERMISSION_GUIDANCE,
4540
4791
  requestDesktopPermission,
4541
4792
  checkDesktop,
4542
- parseXrandr,
4543
4793
  PlatformDesktop,
4544
4794
  DesktopManager,
4545
- probeRemote,
4546
- nativeProcess,
4547
- terminateNative
4795
+ nativeDependencies,
4796
+ RemotePlatformRegistry
4548
4797
  };
package/dist/cli.js CHANGED
@@ -35,13 +35,13 @@ import {
35
35
  supportedPlatform,
36
36
  systemdUnitPath,
37
37
  toolSearchDirs
38
- } from "./chunk-IV5ZGRAM.js";
38
+ } from "./chunk-6G2W4KWC.js";
39
39
  import {
40
40
  DESKTOP_PERMISSION_GUIDANCE,
41
41
  PlatformDesktop,
42
42
  checkDesktop,
43
43
  requestDesktopPermission
44
- } from "./chunk-UFLXQ4HQ.js";
44
+ } from "./chunk-OTKNRBZ7.js";
45
45
 
46
46
  // src/cli.ts
47
47
  import { spawn as spawn2 } from "node:child_process";
package/dist/main.js CHANGED
@@ -2,8 +2,8 @@
2
2
  import {
3
3
  ConfigError,
4
4
  runDaemon
5
- } from "./chunk-IV5ZGRAM.js";
6
- import "./chunk-UFLXQ4HQ.js";
5
+ } from "./chunk-6G2W4KWC.js";
6
+ import "./chunk-OTKNRBZ7.js";
7
7
 
8
8
  // src/main.ts
9
9
  runDaemon(process.env.OCTOPUS_PROXY_CONFIG ?? "proxy.json").catch((error) => {
@@ -1,25 +1,20 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  InputSchema,
4
- REMOTE_DEFAULTS,
5
4
  REMOTE_LEGACY_PROFILE,
6
5
  REMOTE_QUALITY_PROFILES,
7
6
  RemoteInputOrder,
7
+ RemotePlatformRegistry,
8
8
  SignalSchema,
9
9
  assertDirectSignal,
10
10
  capVideoSdp,
11
11
  fitVideo,
12
- nativeProcess,
13
- parseXrandr,
14
12
  remoteResolutionTiers,
15
- remoteSessionBudget,
16
- terminateNative
17
- } from "./chunk-UFLXQ4HQ.js";
13
+ remoteSessionBudget
14
+ } from "./chunk-OTKNRBZ7.js";
18
15
 
19
16
  // src/remote-worker.ts
20
17
  import rtc from "@roamhq/wrtc";
21
- import { spawn, execFile } from "node:child_process";
22
- import { promisify } from "node:util";
23
18
  import { timingSafeEqual } from "node:crypto";
24
19
 
25
20
  // src/remote-budget.ts
@@ -158,7 +153,6 @@ var RemoteLiveness = class {
158
153
  };
159
154
 
160
155
  // src/remote-worker.ts
161
- var exec = promisify(execFile);
162
156
  var post = (kind, data = {}) => {
163
157
  if (process.connected) process.send?.({ kind, ...data });
164
158
  };
@@ -207,8 +201,7 @@ var RemotePeer = class {
207
201
  source = new rtc.nonstandard.RTCVideoSource({ isScreencast: true });
208
202
  track = this.source.createTrack();
209
203
  sender;
210
- helper;
211
- capture;
204
+ native;
212
205
  channels = /* @__PURE__ */ new Map();
213
206
  ready = false;
214
207
  ended = false;
@@ -239,7 +232,6 @@ var RemotePeer = class {
239
232
  legacy;
240
233
  quality;
241
234
  budget;
242
- geometry;
243
235
  video;
244
236
  signal(raw) {
245
237
  let stage = "validation";
@@ -379,155 +371,54 @@ var RemotePeer = class {
379
371
  if (channel?.readyState === "open") channel.send(JSON.stringify({ ...value, remoteId: this.init.remoteId, revision: 1 }));
380
372
  }
381
373
  async startNative() {
382
- if (process.platform === "linux") {
383
- const displays = parseXrandr((await exec("xrandr", ["--listactivemonitors"], { timeout: 5e3 })).stdout);
384
- if (this.ended) return;
385
- const display = displays.find((d) => d.id === this.init.display.id);
386
- if (!display || display.width !== this.init.display.width || display.height !== this.init.display.height) throw new Error("display");
387
- this.geometry = { x: display.x, y: display.y, width: display.width, height: display.height };
388
- }
389
374
  if (this.ended) return;
390
- const child = nativeProcess(this.init.helper, this.init.display.id, this.video);
391
- this.helper = child;
392
- child.on("error", () => this.stop("\u4E3B\u673A\u8F93\u5165\u8F85\u52A9\u7A0B\u5E8F\u4E0D\u53EF\u7528"));
393
- child.on("exit", () => {
394
- if (this.helper === child) this.stop("\u4E3B\u673A\u6743\u9650\u3001\u663E\u793A\u5668\u6216\u56FE\u5F62\u4F1A\u8BDD\u5DF2\u53D8\u5316");
395
- });
396
- child.stdin?.on("error", () => this.stop("\u4E3B\u673A\u8F93\u5165\u8FDE\u63A5\u5DF2\u5173\u95ED"));
397
- let lines = "";
398
- child.stdout?.on("data", (chunk) => {
399
- lines += String(chunk);
400
- if (lines.length > 15e5) {
401
- this.stop("\u4E3B\u673A\u526A\u8D34\u677F\u8D85\u8FC7\u4E0A\u9650");
402
- return;
403
- }
404
- let end;
405
- while ((end = lines.indexOf("\n")) >= 0) {
406
- const line = lines.slice(0, end);
407
- lines = lines.slice(end + 1);
408
- try {
409
- const message = JSON.parse(line);
410
- if (message.kind === "clipboard" && typeof message.id === "string" && typeof message.data === "string") {
411
- const callback = this.nativePending.get(message.id);
412
- this.nativePending.delete(message.id);
413
- callback?.(Buffer.from(message.data, "base64").toString("utf8"));
414
- }
415
- if (message.kind === "clipboard-written" && this.pendingClipboard?.id === message.id) {
416
- this.pendingClipboard = void 0;
417
- this.send("clipboard", { kind: "clipboard.done", id: message.id });
418
- }
419
- if (message.kind === "clipboard-error") {
420
- if (typeof message.id === "string") this.nativePending.delete(message.id);
421
- this.pendingClipboard = void 0;
422
- this.send("clipboard", { kind: "clipboard.error", id: message.id, message: "\u4E3B\u673A\u526A\u8D34\u677F\u8BFB\u53D6\u5931\u8D25\u6216\u8D85\u8FC7 1 MiB" });
423
- }
424
- if (message.kind === "input-error") this.send("control", { kind: "input.error", message: "\u4E3B\u673A\u9501\u5C4F\u72B6\u6001\u5DF2\u53D8\u5316\uFF0C\u5DF2\u505C\u6B62\u8F93\u5165\u5BC6\u7801\uFF1B\u8BF7\u68C0\u67E5\u8FDC\u7AEF\u754C\u9762" });
425
- if (message.kind === "error") this.stop("\u4E3B\u673A\u6743\u9650\u3001\u663E\u793A\u5668\u6216\u56FE\u5F62\u4F1A\u8BDD\u5DF2\u53D8\u5316");
426
- } catch {
427
- this.stop("\u4E3B\u673A\u8F85\u52A9\u7A0B\u5E8F\u8FD4\u56DE\u65E0\u6548\u6570\u636E");
428
- }
429
- }
430
- });
431
- if (process.platform === "darwin") this.macFrames(child.stdio[3]);
432
- else {
433
- const g = this.geometry;
434
- this.writeNative(`geometry ${g.x} ${g.y} ${g.width} ${g.height}`);
435
- this.linuxFrames();
436
- }
437
- }
438
- macFrames(stream) {
439
- let buffer = Buffer.alloc(0);
440
- stream.on("data", (chunk) => {
441
- if (this.ended) return;
442
- buffer = Buffer.concat([buffer, chunk]);
443
- if (buffer.length > REMOTE_DEFAULTS.maxWidth * REMOTE_DEFAULTS.maxHeight * 3 + 24) {
444
- this.stop("\u8FDC\u63A7\u753B\u9762\u7F13\u51B2\u533A\u8D85\u9650");
445
- return;
446
- }
447
- let latest;
448
- while (buffer.length >= 12) {
449
- const width = buffer.readUInt32BE(0), height = buffer.readUInt32BE(4), length = buffer.readUInt32BE(8);
450
- if (!width || !height || width > REMOTE_DEFAULTS.maxWidth || height > REMOTE_DEFAULTS.maxHeight || width % 2 || height % 2 || length !== width * height * 1.5) {
451
- this.stop("\u4E3B\u673A\u753B\u9762\u683C\u5F0F\u65E0\u6548");
452
- return;
453
- }
454
- if (buffer.length < length + 12) break;
455
- latest = { width, height, data: new Uint8Array(buffer.subarray(12, 12 + length)) };
456
- buffer = buffer.subarray(12 + length);
457
- }
458
- if (latest && this.direct) {
459
- this.firstFrameAt ||= Date.now();
460
- this.frame(latest);
461
- }
462
- });
463
- }
464
- linuxFrames() {
465
- const old = this.capture;
466
- this.capture = void 0;
467
- old?.kill("SIGTERM");
468
- const g = this.geometry, { width, height, fps } = this.video;
469
- const child = spawn(this.init.config.ffmpeg, ["-nostdin", "-hide_banner", "-loglevel", "error", "-f", "x11grab", "-framerate", String(fps), "-video_size", `${g.width}x${g.height}`, "-grab_x", String(g.x), "-grab_y", String(g.y), "-draw_mouse", "1", "-i", process.env.DISPLAY, "-vf", `scale=${width}:${height}`, "-pix_fmt", "yuv420p", "-f", "rawvideo", "pipe:1"], { stdio: ["ignore", "pipe", "ignore"] });
470
- this.capture = child;
471
- let buffer = Buffer.alloc(0);
472
- const length = width * height * 1.5;
473
- child.stdout.on("data", (chunk) => {
474
- if (this.capture !== child || this.ended) return;
475
- buffer = Buffer.concat([buffer, chunk]);
476
- if (buffer.length > length * 4) {
477
- this.stop("\u4E3B\u673A\u91C7\u96C6\u7F13\u51B2\u533A\u8D85\u9650");
478
- return;
479
- }
480
- if (buffer.length >= length) {
481
- const count = Math.floor(buffer.length / length), frame = buffer.subarray((count - 1) * length, count * length);
482
- if (this.direct) {
375
+ const adapter = RemotePlatformRegistry.standard.resolve();
376
+ const error = adapter.platformError();
377
+ if (error) throw new Error(error);
378
+ const session = adapter.createSession({
379
+ helper: this.init.helper,
380
+ display: this.init.display,
381
+ video: this.video,
382
+ config: this.init.config,
383
+ onFrame: (frame) => {
384
+ if (!this.ended && this.direct) {
483
385
  this.firstFrameAt ||= Date.now();
484
- this.frame({ width, height, data: new Uint8Array(frame) });
386
+ this.frame(frame);
485
387
  }
486
- buffer = buffer.subarray(count * length);
487
- }
488
- });
489
- child.on("error", () => this.stop("FFmpeg \u91C7\u96C6\u5931\u8D25"));
490
- child.on("exit", () => {
491
- if (this.capture === child) this.stop("\u663E\u793A\u5668\u5DF2\u53D8\u5316\u6216 FFmpeg \u91C7\u96C6\u5931\u8D25");
388
+ },
389
+ onMessage: (message) => {
390
+ if (!this.ended) this.nativeMessage(message);
391
+ },
392
+ onError: (message) => this.stop(message)
492
393
  });
394
+ this.native = session;
395
+ await session.start();
396
+ }
397
+ nativeMessage(message) {
398
+ if (message.kind === "clipboard" && typeof message.id === "string" && typeof message.data === "string") {
399
+ const callback = this.nativePending.get(message.id);
400
+ this.nativePending.delete(message.id);
401
+ callback?.(Buffer.from(message.data, "base64").toString("utf8"));
402
+ }
403
+ if (message.kind === "clipboard-written" && this.pendingClipboard?.id === message.id) {
404
+ this.pendingClipboard = void 0;
405
+ this.send("clipboard", { kind: "clipboard.done", id: message.id });
406
+ }
407
+ if (message.kind === "clipboard-error") {
408
+ if (typeof message.id === "string") this.nativePending.delete(message.id);
409
+ this.pendingClipboard = void 0;
410
+ this.send("clipboard", { kind: "clipboard.error", id: message.id, message: "\u4E3B\u673A\u526A\u8D34\u677F\u8BFB\u53D6\u5931\u8D25\u6216\u8D85\u8FC7 1 MiB" });
411
+ }
412
+ if (message.kind === "input-error") this.send("control", { kind: "input.error", message: "\u4E3B\u673A\u9501\u5C4F\u72B6\u6001\u5DF2\u53D8\u5316\uFF0C\u5DF2\u505C\u6B62\u8F93\u5165\u5BC6\u7801\uFF1B\u8BF7\u68C0\u67E5\u8FDC\u7AEF\u754C\u9762" });
493
413
  }
494
414
  frame(frame) {
495
415
  if (this.legacy || this.frameGate.accept(frame, Date.now())) this.source.onFrame(frame);
496
416
  }
497
417
  writeNative(line) {
498
- if (!this.helper?.stdin || this.helper.stdin.destroyed) return;
499
- if (this.helper.stdin.writableLength > 2 * 1024 * 1024) {
500
- this.stop("\u4E3B\u673A\u8F93\u5165\u5904\u7406\u8D85\u65F6");
501
- return;
502
- }
503
- this.helper.stdin.write(`${line}
504
- `);
418
+ this.native?.writeCommand(line);
505
419
  }
506
420
  input(input) {
507
- switch (input.kind) {
508
- case "pointer":
509
- this.writeNative(`pointer ${input.x} ${input.y}`);
510
- break;
511
- case "button":
512
- this.writeNative(`button ${input.x} ${input.y} ${input.button} ${input.down ? 1 : 0}`);
513
- break;
514
- case "wheel":
515
- this.writeNative(`wheel ${input.x} ${input.y} ${Math.round(input.dx)} ${Math.round(input.dy)}`);
516
- break;
517
- case "key":
518
- this.writeNative(`key ${input.code} ${input.down ? 1 : 0}`);
519
- break;
520
- case "password":
521
- if (process.platform !== "darwin") return;
522
- this.writeNative(`password ${Buffer.from(input.text).toString("base64")}`);
523
- break;
524
- case "text":
525
- this.writeNative(`text ${input.terminal ? 1 : 0} ${Buffer.from(input.text).toString("base64")}`);
526
- break;
527
- case "release":
528
- this.writeNative("release");
529
- break;
530
- }
421
+ this.native?.input(input);
531
422
  }
532
423
  clipboard(value) {
533
424
  if (typeof value.id !== "string" || !/^[a-zA-Z0-9_-]{1,64}$/.test(value.id)) throw new Error("clipboard id");
@@ -609,10 +500,7 @@ var RemotePeer = class {
609
500
  const video = this.budget.video(this.init.display.width, this.init.display.height);
610
501
  const changed = video.width !== this.video.width || video.height !== this.video.height || video.fps !== this.video.fps;
611
502
  this.video = video;
612
- if (changed && this.ready) {
613
- if (process.platform === "darwin") this.writeNative(`video ${video.width} ${video.height} ${video.fps}`);
614
- else this.linuxFrames();
615
- }
503
+ if (changed && this.ready) this.native?.setVideo(video);
616
504
  }
617
505
  async encoding() {
618
506
  if (this.parameterMode === "sdp") return;
@@ -677,9 +565,8 @@ var RemotePeer = class {
677
565
  clearInterval(this.timer);
678
566
  clearInterval(this.clipboardTimer);
679
567
  clearTimeout(this.handshake);
680
- this.writeNative("release");
681
- terminateNative(this.helper);
682
- this.capture?.kill("SIGTERM");
568
+ this.native?.stop();
569
+ this.native = void 0;
683
570
  this.track.stop();
684
571
  for (const c of this.channels.values()) c.close();
685
572
  this.pc.close();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@skylandnpm/octopus-proxy",
3
- "version": "0.1.66",
3
+ "version": "0.1.68",
4
4
  "type": "module",
5
5
  "description": "Octopus host proxy: OpenCode, Codex, ZCode and Pi adapters with service installer CLI",
6
6
  "bin": {