@skylandnpm/octopus-proxy 0.1.27 → 0.1.29

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
@@ -15,7 +15,7 @@ octopus-proxy onboard
15
15
 
16
16
  向导默认使用 `https://octopus.wuyehao.com` 作为 Hub 地址;按需可输入其他地址,或用 `--hub-url` 指定。再次运行向导时会读取现有配置,直接回车即可保留各项设置;机器人 Secret 只提示已配置,不在终端显示原值。macOS 上选择编码工具时,向导优先显示已配置或探测到的命令路径,否则给出该工具的 macOS 路径示例;回车后会检查文件可执行性并运行版本命令,失败时要求重新输入。无交互环境可使用 `octopus-proxy onboard --non-interactive`,未提供 `--hub-url` 时也使用该默认地址;完整选项见 `octopus-proxy help`。安装服务后可运行 `octopus-proxy status` 和 `octopus-proxy doctor` 检查状态。
17
17
 
18
- Proxy 始终使用独立的控制与文件 WebSocket;文件读取和上传不会回退到控制通道。两个通道会同时连接并各自重连,Hub 只会把文件命令发送到同一 Proxy 实例的已就绪文件通道。若旧版 Hub 或反向代理没有正确转发 `/ws/proxy-files`,Claw 会明确显示文件通道离线,此时应修复路由配置。同一套 bot ID 和 Secret 被另一台 Proxy 主机连接后,Hub 会让新实例接管,旧实例会同时关闭两个通道并停止自动重连;若要让旧实例重新接管,必须显式重启其服务。
18
+ Proxy 始终使用独立的控制与文件 WebSocket;文件读取和上传不会回退到控制通道。两个通道会同时连接并各自重连,Hub 只会把文件命令发送到同一 Proxy 实例的已就绪文件通道。文件连接同时携带经过 Proxy 凭据认证的通道标识,因此即使反向代理将 `/ws/proxy-files` 重写为上游 `/ws/proxy`,Hub 仍能正确路由文件命令;反向代理仍需转发 WebSocket Upgrade 与 `X-Octopus-Proxy-Channel` 请求头。若旧 Hub 无法识别该标识,Proxy 会保留控制通道以免影响对话,同时在日志中提示升级 Hub 路由。同一套 bot ID 和 Secret 被另一台 Proxy 主机连接后,Hub 会让新实例接管,旧实例会同时关闭两个通道并停止自动重连;若要让旧实例重新接管,必须显式重启其服务。
19
19
 
20
20
  添加授权项目时,填写项目 ID 后会询问是否继续添加;直接回车即可结束添加并继续保存配置。
21
21
 
@@ -16630,6 +16630,29 @@ function createAdapter(runtime, workspace, uploads, emit) {
16630
16630
  return new OpenCodeAdapter(runtime, workspace, uploads, forward);
16631
16631
  }
16632
16632
 
16633
+ // src/hub-channel-compatibility.ts
16634
+ var HubChannelCompatibility = class {
16635
+ enabled = false;
16636
+ filesAttemptedAt = 0;
16637
+ filesFailures = 0;
16638
+ filesAttempt(now = Date.now()) {
16639
+ this.filesAttemptedAt = now;
16640
+ }
16641
+ filesWelcome() {
16642
+ this.filesFailures = 0;
16643
+ }
16644
+ controlClosed(filesPending, now = Date.now()) {
16645
+ if (this.enabled || !filesPending || now - this.filesAttemptedAt >= 5e3) return false;
16646
+ this.enabled = true;
16647
+ return true;
16648
+ }
16649
+ filesClosedBeforeWelcome(stopping) {
16650
+ if (this.enabled || stopping || ++this.filesFailures < 3) return false;
16651
+ this.enabled = true;
16652
+ return true;
16653
+ }
16654
+ };
16655
+
16633
16656
  // src/proxy-binding.ts
16634
16657
  var PROXY_REPLACED_CLOSE_CODE = 4009;
16635
16658
  var ProxyBinding = class {
@@ -16779,6 +16802,7 @@ async function runDaemon(configPath) {
16779
16802
  const workspace = new Workspace(config.projects), runtime = createRuntime(config), uploads = new Uploads(workspace, join10(config.stateDirectory, "uploads")), journal = new Journal(join10(config.stateDirectory, "journal"));
16780
16803
  const epoch = randomUUID12();
16781
16804
  let stopping = false, botId = "";
16805
+ const hubChannels = new HubChannelCompatibility();
16782
16806
  const binding = new ProxyBinding();
16783
16807
  const sockets = /* @__PURE__ */ new Set();
16784
16808
  let controlSocket, filesSocket;
@@ -16847,21 +16871,29 @@ async function runDaemon(configPath) {
16847
16871
  return adapter.dispatch(c);
16848
16872
  }
16849
16873
  function activateChannels() {
16850
- if (channelsActivated || !controlWelcome || !filesWelcomed || controlWelcome.socket.readyState !== WebSocket2.OPEN) return;
16874
+ if (channelsActivated || !controlWelcome || !filesWelcomed && !hubChannels.enabled || controlWelcome.socket.readyState !== WebSocket2.OPEN) return;
16851
16875
  channelsActivated = true;
16852
16876
  events.bind(controlWelcome.socket, controlWelcome.resume);
16853
16877
  for (const p of config.projects) emit(p.id, "octopus.resync", {});
16854
16878
  }
16855
16879
  function connect(files, attempt = 0) {
16856
- if (stopping || !binding.active) return;
16880
+ if (stopping || !binding.active || files && hubChannels.enabled) return;
16857
16881
  const url = new URL(files ? "/ws/proxy-files" : "/ws/proxy", config.hubUrl);
16858
16882
  url.protocol = url.protocol === "https:" ? "wss:" : "ws:";
16859
- const headers = { Authorization: `SecretKey ${config.secretKey}`, "X-Octopus-Bot-Id": config.botId, "X-Octopus-Proxy-Instance-Id": epoch };
16883
+ const headers = {
16884
+ Authorization: `SecretKey ${config.secretKey}`,
16885
+ "X-Octopus-Bot-Id": config.botId,
16886
+ "X-Octopus-Proxy-Instance-Id": epoch,
16887
+ // Preserve the logical channel when a reverse proxy rewrites
16888
+ // /ws/proxy-files to the generic /ws/proxy upstream endpoint.
16889
+ "X-Octopus-Proxy-Channel": files ? "files" : "control"
16890
+ };
16860
16891
  const socket = new WebSocket2(url, { headers, maxPayload: 3 * 1024 * 1024 });
16861
16892
  sockets.add(socket);
16862
16893
  if (files) {
16863
16894
  filesSocket = socket;
16864
16895
  filesWelcomed = false;
16896
+ hubChannels.filesAttempt();
16865
16897
  } else controlSocket = socket;
16866
16898
  let lastPong = Date.now();
16867
16899
  const heartbeat = setInterval(() => {
@@ -16886,8 +16918,10 @@ async function runDaemon(configPath) {
16886
16918
  if (value.type === "welcome") {
16887
16919
  botId = value.botId;
16888
16920
  uploads.limits = value.limits;
16889
- if (files) filesWelcomed = true;
16890
- else controlWelcome = { socket, resume: value.resume };
16921
+ if (files) {
16922
+ filesWelcomed = true;
16923
+ hubChannels.filesWelcome();
16924
+ } else controlWelcome = { socket, resume: value.resume };
16891
16925
  activateChannels();
16892
16926
  return;
16893
16927
  }
@@ -16922,10 +16956,17 @@ async function runDaemon(configPath) {
16922
16956
  console.error("This bot was claimed by another Proxy instance; automatic reconnection is disabled until this service is restarted");
16923
16957
  for (const peer of sockets) peer.close();
16924
16958
  }
16925
- if (!stopping && binding.active && code !== 1e3) {
16959
+ const collision = binding.active && !files && hubChannels.controlClosed(filesSocket !== void 0 && filesSocket.readyState !== WebSocket2.CLOSED);
16960
+ const rejectedFilesChannel = binding.active && files && !filesWelcomed && hubChannels.filesClosedBeforeWelcome(stopping);
16961
+ if (collision || rejectedFilesChannel) {
16962
+ console.warn("Hub does not preserve the Proxy files channel; keeping the control channel online until Hub routing is upgraded");
16963
+ filesSocket?.close();
16964
+ filesSocket = void 0;
16965
+ activateChannels();
16966
+ } else if (!stopping && binding.active && code !== 1e3) {
16926
16967
  console.warn(`Hub ${files ? "files" : "control"} connection closed: ${code}${reason.length ? ` ${reason.toString()}` : ""}`);
16927
16968
  }
16928
- if (!stopping && binding.active) setTimeout(() => connect(files, attempt + 1), Math.min(3e4, 1e3 * 2 ** Math.min(attempt, 5)) + Math.random() * 500);
16969
+ if (!stopping && binding.active && (!files || !hubChannels.enabled)) setTimeout(() => connect(files, attempt + 1), collision ? 100 : Math.min(3e4, 1e3 * 2 ** Math.min(attempt, 5)) + Math.random() * 500);
16929
16970
  });
16930
16971
  }
16931
16972
  await runtime.init().catch((error) => console.error(`${runtime.kind} startup:`, error.message));
package/dist/cli.js CHANGED
@@ -22,7 +22,7 @@ import {
22
22
  runDaemon,
23
23
  supportedPlatform,
24
24
  systemdUnitPath
25
- } from "./chunk-X2VENWTX.js";
25
+ } from "./chunk-XHNRMXZT.js";
26
26
 
27
27
  // src/cli.ts
28
28
  import { spawn } from "node:child_process";
package/dist/main.js CHANGED
@@ -2,7 +2,7 @@
2
2
  import {
3
3
  ConfigError,
4
4
  runDaemon
5
- } from "./chunk-X2VENWTX.js";
5
+ } from "./chunk-XHNRMXZT.js";
6
6
 
7
7
  // src/main.ts
8
8
  runDaemon(process.env.OCTOPUS_PROXY_CONFIG ?? "proxy.json").catch((error) => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@skylandnpm/octopus-proxy",
3
- "version": "0.1.27",
3
+ "version": "0.1.29",
4
4
  "type": "module",
5
5
  "description": "Octopus host proxy: OpenCode, Codex, ZCode and Pi adapters with service installer CLI",
6
6
  "bin": {