@skylandnpm/octopus-proxy 0.1.21 → 0.1.26
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 +3 -1
- package/dist/{chunk-TXFPGQ4Z.js → chunk-3IEPWZVZ.js} +188 -30
- package/dist/cli.js +1 -1
- package/dist/main.js +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -15,6 +15,8 @@ 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 或反向代理把 `/ws/proxy-files` 错误路由到控制通道,Proxy 会在连续握手冲突后自动切换为单控制通道兼容模式,避免两个连接互相挤掉而导致 Claw 永久显示离线;日志会明确记录该降级。同一套 bot ID 和 Secret 被另一台 Proxy 主机连接后,Hub 会让新实例接管,旧实例会同时关闭两个通道并停止自动重连;若要让旧实例重新接管,必须显式重启其服务。
|
|
19
|
+
|
|
18
20
|
添加授权项目时,填写项目 ID 后会询问是否继续添加;直接回车即可结束添加并继续保存配置。
|
|
19
21
|
|
|
20
22
|
如果旧配置中的项目 ID 重复或有错误项,先列出项目,再按序号删除指定项。此命令直接读取原始配置,因此配置校验失败时也能使用;删除前会生成仅当前用户可读的备份。清理完成后检查配置并重启服务:
|
|
@@ -72,6 +74,6 @@ npm run publish
|
|
|
72
74
|
|
|
73
75
|
## 编码工具账号与 OpenCode 服务
|
|
74
76
|
|
|
75
|
-
OpenCode 2.0.19 使用 external HTTP 模式。`tool.command` 应为 `/Users/luhuili/.opencode/bin/opencode`,`tool.baseUrl` 明确指向回环服务,`tool.password` 必填并由 onboard 自动生成。Proxy
|
|
77
|
+
OpenCode 2.0.19 使用 external HTTP 模式。`tool.command` 应为 `/Users/luhuili/.opencode/bin/opencode`,`tool.baseUrl` 明确指向回环服务,`tool.password` 必填并由 onboard 自动生成。Proxy 可拉起未监听的服务,也可连接以同一密码预先启动的服务;若配置端口已被密码不同的服务占用,Proxy 会在随机回环端口启动自己的实例并在后续重启时复用,避免接管或停止未知进程。启动日志保存在 `stateDirectory/opencode-serve.log`。如需只连接自行管理的服务,可设置 `autoStart: false`。旧顶层配置、managed 模式和 v1 API 不再兼容;Proxy 不停止或重启已有 OpenCode 服务。
|
|
76
78
|
|
|
77
79
|
Claw 的“编码工具账号”窗口支持 Codex API Key/设备码、ZCode 原生 Z.AI OAuth、OpenCode 原生 API Key/OAuth。凭据由编码工具保存,模型列表从原生接口读取。详细行为与验证边界见 [编码工具账号](../../docs/tool-authentication.md)。
|
|
@@ -12459,6 +12459,9 @@ var EventStream = class {
|
|
|
12459
12459
|
|
|
12460
12460
|
// src/proxy-snapshot.ts
|
|
12461
12461
|
import { randomUUID as randomUUID5 } from "node:crypto";
|
|
12462
|
+
import { execFile as execFile2 } from "node:child_process";
|
|
12463
|
+
import { promisify as promisify2 } from "node:util";
|
|
12464
|
+
var execute2 = promisify2(execFile2);
|
|
12462
12465
|
function record(value) {
|
|
12463
12466
|
return value !== null && typeof value === "object" && !Array.isArray(value) ? value : {};
|
|
12464
12467
|
}
|
|
@@ -12478,10 +12481,32 @@ function publicModels(value) {
|
|
|
12478
12481
|
}
|
|
12479
12482
|
return result;
|
|
12480
12483
|
}
|
|
12484
|
+
async function gitBranch(directory) {
|
|
12485
|
+
const git = async (...args) => (await execute2("git", ["--no-optional-locks", ...args], {
|
|
12486
|
+
cwd: directory,
|
|
12487
|
+
encoding: "utf8",
|
|
12488
|
+
timeout: 3e3,
|
|
12489
|
+
maxBuffer: 64 * 1024
|
|
12490
|
+
})).stdout.trim();
|
|
12491
|
+
try {
|
|
12492
|
+
const branch = await git("branch", "--show-current");
|
|
12493
|
+
if (branch) return branch;
|
|
12494
|
+
const commit = await git("rev-parse", "--short", "HEAD");
|
|
12495
|
+
return commit ? `HEAD@${commit}` : void 0;
|
|
12496
|
+
} catch {
|
|
12497
|
+
return void 0;
|
|
12498
|
+
}
|
|
12499
|
+
}
|
|
12500
|
+
async function publicProjects(config) {
|
|
12501
|
+
return Promise.all(config.projects.map(async ({ id, name, directory, specDirectories }) => {
|
|
12502
|
+
const branch = await gitBranch(directory);
|
|
12503
|
+
return { id, name, directory, specDirectories, ...branch ? { gitBranch: branch } : {} };
|
|
12504
|
+
}));
|
|
12505
|
+
}
|
|
12481
12506
|
async function buildProxySnapshot(config, runtime, adapter, requestedProjectId) {
|
|
12482
12507
|
const capabilities = adapter.supportedOperations();
|
|
12483
12508
|
const status = record(await runtime.status());
|
|
12484
|
-
const projects =
|
|
12509
|
+
const projects = await publicProjects(config);
|
|
12485
12510
|
const projectId = projects.find((project) => project.id === requestedProjectId)?.id ?? projects[0]?.id;
|
|
12486
12511
|
const models = [];
|
|
12487
12512
|
let modelCatalogStatus = capabilities.includes("configuration.models") ? "unavailable" : "unsupported";
|
|
@@ -12505,7 +12530,7 @@ async function buildProxySnapshot(config, runtime, adapter, requestedProjectId)
|
|
|
12505
12530
|
command: config.tool.command,
|
|
12506
12531
|
mode: config.tool.mode,
|
|
12507
12532
|
autoStart: config.tool.autoStart,
|
|
12508
|
-
...config.tool.kind === "opencode" ? { baseUrl: config.tool.baseUrl } : {}
|
|
12533
|
+
...config.tool.kind === "opencode" ? { baseUrl: typeof status.baseUrl === "string" ? status.baseUrl : config.tool.baseUrl } : {}
|
|
12509
12534
|
}
|
|
12510
12535
|
},
|
|
12511
12536
|
projects,
|
|
@@ -14244,7 +14269,7 @@ var OpenCodeAdapter = class {
|
|
|
14244
14269
|
media;
|
|
14245
14270
|
async request(project, method, path, body, signal) {
|
|
14246
14271
|
const root = await this.workspace.root(project);
|
|
14247
|
-
const url = new URL(path, this.runtime.
|
|
14272
|
+
const url = new URL(path, this.runtime.baseUrl);
|
|
14248
14273
|
if (path === "/api/session") url.searchParams.set("directory", root);
|
|
14249
14274
|
else if (/^\/api\/(?:agent|model|provider|mcp|command|config|pty|permission\/request|form|integration|location|experimental\/mcp)(?:\/|$)/.test(path)) url.searchParams.set("location[directory]", root);
|
|
14250
14275
|
const response = await fetch(url, {
|
|
@@ -14490,7 +14515,7 @@ var OpenCodeAdapter = class {
|
|
|
14490
14515
|
const root = await this.workspace.root(project);
|
|
14491
14516
|
while (!controller.signal.aborted) {
|
|
14492
14517
|
try {
|
|
14493
|
-
const response = await fetch(new URL("/api/event", this.runtime.
|
|
14518
|
+
const response = await fetch(new URL("/api/event", this.runtime.baseUrl), { headers: this.runtime.headers(), signal: controller.signal });
|
|
14494
14519
|
if (!response.ok || !response.body) throw new Error("Event connection unavailable");
|
|
14495
14520
|
this.emit(project, "octopus.resync", {});
|
|
14496
14521
|
let buffer = "";
|
|
@@ -14536,7 +14561,7 @@ var OpenCodeAdapter = class {
|
|
|
14536
14561
|
const previous = this.terminals.get(key);
|
|
14537
14562
|
this.terminals.delete(key);
|
|
14538
14563
|
previous?.close();
|
|
14539
|
-
const url = new URL(`/api/pty/${id}/connect`, this.runtime.
|
|
14564
|
+
const url = new URL(`/api/pty/${id}/connect`, this.runtime.baseUrl);
|
|
14540
14565
|
url.protocol = url.protocol === "https:" ? "wss:" : "ws:";
|
|
14541
14566
|
url.searchParams.set("location[directory]", await this.workspace.root(project));
|
|
14542
14567
|
const socket2 = new WebSocket(url, { headers: this.runtime.headers() });
|
|
@@ -14646,17 +14671,32 @@ function redact(value) {
|
|
|
14646
14671
|
|
|
14647
14672
|
// src/runtime.ts
|
|
14648
14673
|
import { spawn } from "node:child_process";
|
|
14649
|
-
import {
|
|
14674
|
+
import { createHash as createHash7 } from "node:crypto";
|
|
14675
|
+
import { mkdir as mkdir4, open as open4, readFile as readFile5, rm, writeFile as writeFile4 } from "node:fs/promises";
|
|
14676
|
+
import { createServer } from "node:net";
|
|
14650
14677
|
import { join as join5 } from "node:path";
|
|
14651
14678
|
var Runtime = class {
|
|
14652
14679
|
constructor(config) {
|
|
14653
14680
|
this.config = config;
|
|
14681
|
+
this.endpoint = config.tool.baseUrl;
|
|
14654
14682
|
}
|
|
14655
14683
|
kind = "opencode";
|
|
14656
14684
|
version = "";
|
|
14657
14685
|
state = "external";
|
|
14658
14686
|
starting;
|
|
14687
|
+
endpoint;
|
|
14688
|
+
endpointRestored = false;
|
|
14689
|
+
get baseUrl() {
|
|
14690
|
+
return this.endpoint;
|
|
14691
|
+
}
|
|
14692
|
+
get endpointStatePath() {
|
|
14693
|
+
return join5(this.config.stateDirectory, "opencode-endpoint.json");
|
|
14694
|
+
}
|
|
14695
|
+
get passwordHash() {
|
|
14696
|
+
return createHash7("sha256").update(this.config.tool.password).digest("hex");
|
|
14697
|
+
}
|
|
14659
14698
|
async init() {
|
|
14699
|
+
await this.restoreEndpoint();
|
|
14660
14700
|
if (this.config.tool.autoStart) await this.start();
|
|
14661
14701
|
else await this.health();
|
|
14662
14702
|
}
|
|
@@ -14666,7 +14706,7 @@ var Runtime = class {
|
|
|
14666
14706
|
}
|
|
14667
14707
|
async health() {
|
|
14668
14708
|
try {
|
|
14669
|
-
const response = await fetch(new URL("/api/info", this.
|
|
14709
|
+
const response = await fetch(new URL("/api/info", this.baseUrl), { headers: this.headers(), signal: AbortSignal.timeout(2e3) });
|
|
14670
14710
|
if (!response.ok) return false;
|
|
14671
14711
|
const data = await response.json();
|
|
14672
14712
|
this.version = data.version ?? this.version;
|
|
@@ -14683,7 +14723,9 @@ var Runtime = class {
|
|
|
14683
14723
|
state: this.state,
|
|
14684
14724
|
healthy,
|
|
14685
14725
|
version: this.version,
|
|
14686
|
-
|
|
14726
|
+
baseUrl: this.baseUrl,
|
|
14727
|
+
...this.baseUrl === this.config.tool.baseUrl ? {} : { configuredBaseUrl: this.config.tool.baseUrl },
|
|
14728
|
+
...healthy ? {} : { error: `\u65E0\u6CD5\u8FDE\u63A5 OpenCode\uFF1A${this.baseUrl}\u3002\u53EF\u5728 Claw \u4E2D\u70B9\u51FB\u201C\u542F\u52A8\u670D\u52A1\u201D\uFF0C\u6216\u5728\u8BE5\u4E3B\u673A\u542F\u52A8 opencode serve\uFF1B\u8BF7\u68C0\u67E5\u670D\u52A1\u5730\u5740\u548C\u5BC6\u7801\u3002` }
|
|
14687
14729
|
};
|
|
14688
14730
|
}
|
|
14689
14731
|
async start() {
|
|
@@ -14692,7 +14734,8 @@ var Runtime = class {
|
|
|
14692
14734
|
});
|
|
14693
14735
|
}
|
|
14694
14736
|
async launch() {
|
|
14695
|
-
|
|
14737
|
+
await this.restoreEndpoint();
|
|
14738
|
+
let url = new URL(this.baseUrl);
|
|
14696
14739
|
const endpoint = new URL("/api/info", url);
|
|
14697
14740
|
let response;
|
|
14698
14741
|
try {
|
|
@@ -14702,7 +14745,12 @@ var Runtime = class {
|
|
|
14702
14745
|
}
|
|
14703
14746
|
if (response) {
|
|
14704
14747
|
if (response.ok) return this.status();
|
|
14705
|
-
|
|
14748
|
+
if ((response.status === 401 || response.status === 403) && url.origin === new URL(this.config.tool.baseUrl).origin) {
|
|
14749
|
+
url = await this.reserveFallbackUrl(url);
|
|
14750
|
+
this.endpoint = url.origin;
|
|
14751
|
+
} else {
|
|
14752
|
+
throw new Error(`OpenCode \u5730\u5740 ${url.origin} \u5DF2\u6709 HTTP \u670D\u52A1\uFF0C\u4F46\u5065\u5EB7\u68C0\u67E5\u5931\u8D25\uFF08HTTP ${response.status}\uFF09\uFF1B\u8BF7\u68C0\u67E5\u670D\u52A1\u5BC6\u7801\u6216\u7AEF\u53E3`);
|
|
14753
|
+
}
|
|
14706
14754
|
}
|
|
14707
14755
|
await mkdir4(this.config.stateDirectory, { recursive: true, mode: 448 });
|
|
14708
14756
|
const logPath = join5(this.config.stateDirectory, "opencode-serve.log");
|
|
@@ -14724,7 +14772,20 @@ var Runtime = class {
|
|
|
14724
14772
|
});
|
|
14725
14773
|
child.unref();
|
|
14726
14774
|
} catch (error) {
|
|
14775
|
+
this.endpoint = this.config.tool.baseUrl;
|
|
14727
14776
|
throw new Error(`\u65E0\u6CD5\u6267\u884C OpenCode \u547D\u4EE4 ${this.config.tool.command}\uFF1A${error instanceof Error ? error.message : String(error)}`);
|
|
14777
|
+
}
|
|
14778
|
+
try {
|
|
14779
|
+
await writeFile4(this.endpointStatePath, JSON.stringify({
|
|
14780
|
+
configuredBaseUrl: this.config.tool.baseUrl,
|
|
14781
|
+
baseUrl: url.origin,
|
|
14782
|
+
passwordHash: this.passwordHash,
|
|
14783
|
+
pid: child.pid
|
|
14784
|
+
}, null, 2), { mode: 384 });
|
|
14785
|
+
} catch (error) {
|
|
14786
|
+
if (child.exitCode === null && child.signalCode === null) child.kill("SIGTERM");
|
|
14787
|
+
this.endpoint = this.config.tool.baseUrl;
|
|
14788
|
+
throw new Error(`\u65E0\u6CD5\u4FDD\u5B58 OpenCode \u8FD0\u884C\u7AEF\u70B9\uFF1A${error instanceof Error ? error.message : String(error)}`);
|
|
14728
14789
|
} finally {
|
|
14729
14790
|
await log.close();
|
|
14730
14791
|
}
|
|
@@ -14735,8 +14796,40 @@ var Runtime = class {
|
|
|
14735
14796
|
await new Promise((resolve4) => setTimeout(resolve4, 250));
|
|
14736
14797
|
}
|
|
14737
14798
|
if (child.exitCode === null && child.signalCode === null) child.kill("SIGTERM");
|
|
14799
|
+
await rm(this.endpointStatePath, { force: true }).catch(() => {
|
|
14800
|
+
});
|
|
14801
|
+
this.endpoint = this.config.tool.baseUrl;
|
|
14738
14802
|
throw new Error(`OpenCode \u542F\u52A8\u5931\u8D25\u6216\u5065\u5EB7\u68C0\u67E5\u8D85\u65F6\uFF1B\u8BF7\u68C0\u67E5 ${logPath}\u3001\u670D\u52A1\u5730\u5740\u548C\u5BC6\u7801`);
|
|
14739
14803
|
}
|
|
14804
|
+
async restoreEndpoint() {
|
|
14805
|
+
if (this.endpointRestored) return;
|
|
14806
|
+
this.endpointRestored = true;
|
|
14807
|
+
this.endpoint = this.config.tool.baseUrl;
|
|
14808
|
+
try {
|
|
14809
|
+
const saved = JSON.parse(await readFile5(this.endpointStatePath, "utf8"));
|
|
14810
|
+
if (saved.configuredBaseUrl !== this.config.tool.baseUrl || saved.passwordHash !== this.passwordHash || typeof saved.baseUrl !== "string") return;
|
|
14811
|
+
const url = new URL(saved.baseUrl);
|
|
14812
|
+
if (url.protocol !== "http:" || !["127.0.0.1", "localhost", "[::1]"].includes(url.hostname)) return;
|
|
14813
|
+
this.endpoint = url.origin;
|
|
14814
|
+
if (!await this.health()) this.endpoint = this.config.tool.baseUrl;
|
|
14815
|
+
} catch {
|
|
14816
|
+
}
|
|
14817
|
+
}
|
|
14818
|
+
async reserveFallbackUrl(configured) {
|
|
14819
|
+
const host = configured.hostname === "[::1]" ? "::1" : configured.hostname;
|
|
14820
|
+
const server = createServer();
|
|
14821
|
+
await new Promise((resolve4, reject) => {
|
|
14822
|
+
server.once("error", reject);
|
|
14823
|
+
server.listen(0, host, resolve4);
|
|
14824
|
+
});
|
|
14825
|
+
const address = server.address();
|
|
14826
|
+
const port = typeof address === "object" && address ? address.port : 0;
|
|
14827
|
+
await new Promise((resolve4, reject) => server.close((error) => error ? reject(error) : resolve4()));
|
|
14828
|
+
if (!port) throw new Error("\u65E0\u6CD5\u4E3A OpenCode \u5206\u914D\u5907\u7528\u56DE\u73AF\u7AEF\u53E3");
|
|
14829
|
+
const fallback = new URL(configured.origin);
|
|
14830
|
+
fallback.port = String(port);
|
|
14831
|
+
return fallback;
|
|
14832
|
+
}
|
|
14740
14833
|
async stop() {
|
|
14741
14834
|
throw new Error("OpenCode \u670D\u52A1\u7531\u5916\u90E8\u7BA1\u7406\uFF0CProxy \u4E0D\u652F\u6301\u505C\u6B62");
|
|
14742
14835
|
}
|
|
@@ -14763,8 +14856,8 @@ function zcodeEnvironment(command) {
|
|
|
14763
14856
|
}
|
|
14764
14857
|
|
|
14765
14858
|
// src/tools/stdio-runtime.ts
|
|
14766
|
-
import { spawn as spawn2, execFile as
|
|
14767
|
-
import { promisify as
|
|
14859
|
+
import { spawn as spawn2, execFile as execFile3 } from "node:child_process";
|
|
14860
|
+
import { promisify as promisify3 } from "node:util";
|
|
14768
14861
|
import { mkdir as mkdir5, open as open5 } from "node:fs/promises";
|
|
14769
14862
|
import { join as join7 } from "node:path";
|
|
14770
14863
|
|
|
@@ -14958,7 +15051,7 @@ function wrapEnvelope(frame) {
|
|
|
14958
15051
|
}
|
|
14959
15052
|
|
|
14960
15053
|
// src/tools/stdio-runtime.ts
|
|
14961
|
-
var run =
|
|
15054
|
+
var run = promisify3(execFile3);
|
|
14962
15055
|
var StdioRuntime = class {
|
|
14963
15056
|
constructor(config) {
|
|
14964
15057
|
this.config = config;
|
|
@@ -16019,7 +16112,7 @@ var ZcodeAdapter = class {
|
|
|
16019
16112
|
|
|
16020
16113
|
// src/tools/pi.ts
|
|
16021
16114
|
import { randomUUID as randomUUID11 } from "node:crypto";
|
|
16022
|
-
import { readFile as
|
|
16115
|
+
import { readFile as readFile6, readdir as readdir3, rm as rm2 } from "node:fs/promises";
|
|
16023
16116
|
import { dirname as dirname5 } from "node:path";
|
|
16024
16117
|
var OPERATIONS3 = [
|
|
16025
16118
|
...DAEMON_OPERATIONS,
|
|
@@ -16265,8 +16358,8 @@ var PiAdapter = class {
|
|
|
16265
16358
|
this.deleting.add(this.key(project, id));
|
|
16266
16359
|
try {
|
|
16267
16360
|
await this.runtime.closeSession(project, id);
|
|
16268
|
-
await
|
|
16269
|
-
await
|
|
16361
|
+
await rm2(this.runtime.sessionFile(project, id), { force: true });
|
|
16362
|
+
await rm2(this.metadataPath(project, id));
|
|
16270
16363
|
this.clearQuestions(project, id);
|
|
16271
16364
|
} finally {
|
|
16272
16365
|
this.deleting.delete(this.key(project, id));
|
|
@@ -16299,7 +16392,7 @@ var PiAdapter = class {
|
|
|
16299
16392
|
for (const uploadId of Array.isArray(p.attachments) ? p.attachments : []) {
|
|
16300
16393
|
const upload = await this.uploads.attachment(project, String(uploadId), id);
|
|
16301
16394
|
if (["image/png", "image/jpeg", "image/webp", "image/gif"].includes(upload.mime)) {
|
|
16302
|
-
images.push({ type: "image", data: (await
|
|
16395
|
+
images.push({ type: "image", data: (await readFile6(await this.workspace.path(project, upload.destination))).toString("base64"), mimeType: upload.mime });
|
|
16303
16396
|
} else lines.push(`Attached file: ${upload.destination} (${upload.name}, ${upload.mime}). Read this file if relevant to the task.`);
|
|
16304
16397
|
}
|
|
16305
16398
|
if (!lines.length && !images.length) throw new Error("Message or attachments required");
|
|
@@ -16316,11 +16409,11 @@ var PiAdapter = class {
|
|
|
16316
16409
|
};
|
|
16317
16410
|
|
|
16318
16411
|
// src/tools/pi-runtime.ts
|
|
16319
|
-
import { execFile as
|
|
16412
|
+
import { execFile as execFile4, spawn as spawn4 } from "node:child_process";
|
|
16320
16413
|
import { mkdir as mkdir6, open as open6 } from "node:fs/promises";
|
|
16321
16414
|
import { join as join9 } from "node:path";
|
|
16322
|
-
import { promisify as
|
|
16323
|
-
var run2 =
|
|
16415
|
+
import { promisify as promisify4 } from "node:util";
|
|
16416
|
+
var run2 = promisify4(execFile4);
|
|
16324
16417
|
var PiRuntime = class {
|
|
16325
16418
|
constructor(config) {
|
|
16326
16419
|
this.config = config;
|
|
@@ -16489,14 +16582,55 @@ function createAdapter(runtime, workspace, uploads, emit) {
|
|
|
16489
16582
|
return new OpenCodeAdapter(runtime, workspace, uploads, forward);
|
|
16490
16583
|
}
|
|
16491
16584
|
|
|
16585
|
+
// src/hub-channel-compatibility.ts
|
|
16586
|
+
var HubChannelCompatibility = class {
|
|
16587
|
+
enabled = false;
|
|
16588
|
+
filesAttemptedAt = 0;
|
|
16589
|
+
filesFailures = 0;
|
|
16590
|
+
filesAttempt(now = Date.now()) {
|
|
16591
|
+
this.filesAttemptedAt = now;
|
|
16592
|
+
}
|
|
16593
|
+
filesWelcome() {
|
|
16594
|
+
this.filesFailures = 0;
|
|
16595
|
+
}
|
|
16596
|
+
controlClosed(filesPending, now = Date.now()) {
|
|
16597
|
+
if (this.enabled || !filesPending || now - this.filesAttemptedAt >= 5e3) return false;
|
|
16598
|
+
this.enabled = true;
|
|
16599
|
+
return true;
|
|
16600
|
+
}
|
|
16601
|
+
filesClosedBeforeWelcome(stopping) {
|
|
16602
|
+
if (this.enabled || stopping || ++this.filesFailures < 3) return false;
|
|
16603
|
+
this.enabled = true;
|
|
16604
|
+
return true;
|
|
16605
|
+
}
|
|
16606
|
+
};
|
|
16607
|
+
|
|
16608
|
+
// src/proxy-binding.ts
|
|
16609
|
+
var PROXY_REPLACED_CLOSE_CODE = 4009;
|
|
16610
|
+
var ProxyBinding = class {
|
|
16611
|
+
replaced = false;
|
|
16612
|
+
get active() {
|
|
16613
|
+
return !this.replaced;
|
|
16614
|
+
}
|
|
16615
|
+
observeClose(code) {
|
|
16616
|
+
if (code !== PROXY_REPLACED_CLOSE_CODE) return false;
|
|
16617
|
+
const first = !this.replaced;
|
|
16618
|
+
this.replaced = true;
|
|
16619
|
+
return first;
|
|
16620
|
+
}
|
|
16621
|
+
};
|
|
16622
|
+
|
|
16492
16623
|
// src/daemon.ts
|
|
16493
16624
|
async function runDaemon(configPath) {
|
|
16494
16625
|
const config = await loadConfig(configPath);
|
|
16495
16626
|
await mkdir7(config.stateDirectory, { recursive: true, mode: 448 });
|
|
16496
16627
|
const workspace = new Workspace(config.projects), runtime = createRuntime(config), uploads = new Uploads(workspace, join10(config.stateDirectory, "uploads")), journal = new Journal(join10(config.stateDirectory, "journal"));
|
|
16497
16628
|
const epoch = randomUUID12();
|
|
16498
|
-
let stopping = false, botId = "";
|
|
16629
|
+
let stopping = false, botId = "", filesStarted = false;
|
|
16630
|
+
const hubChannels = new HubChannelCompatibility();
|
|
16631
|
+
const binding = new ProxyBinding();
|
|
16499
16632
|
const sockets = /* @__PURE__ */ new Set();
|
|
16633
|
+
let filesSocket;
|
|
16500
16634
|
const events = new EventStream(epoch, () => {
|
|
16501
16635
|
for (const p of config.projects) emit(p.id, "octopus.resync", {});
|
|
16502
16636
|
});
|
|
@@ -16513,7 +16647,7 @@ async function runDaemon(configPath) {
|
|
|
16513
16647
|
if (c.operation === "runtime.start") return runtime.start();
|
|
16514
16648
|
if (c.operation === "runtime.stop") return runtime.stop();
|
|
16515
16649
|
if (c.operation === "runtime.restart") return runtime.restart();
|
|
16516
|
-
if (c.operation === "workspace.projects") return config
|
|
16650
|
+
if (c.operation === "workspace.projects") return publicProjects(config);
|
|
16517
16651
|
const project = c.projectId;
|
|
16518
16652
|
if (!project) throw new Error("Project required");
|
|
16519
16653
|
workspace.project(project);
|
|
@@ -16544,13 +16678,17 @@ async function runDaemon(configPath) {
|
|
|
16544
16678
|
return adapter.dispatch(c);
|
|
16545
16679
|
}
|
|
16546
16680
|
function connect(files, attempt = 0) {
|
|
16547
|
-
if (stopping) return;
|
|
16681
|
+
if (stopping || !binding.active || files && hubChannels.enabled) return;
|
|
16548
16682
|
const url = new URL(files ? "/ws/proxy-files" : "/ws/proxy", config.hubUrl);
|
|
16549
16683
|
url.protocol = url.protocol === "https:" ? "wss:" : "ws:";
|
|
16550
|
-
const headers = { Authorization: `SecretKey ${config.secretKey}`, "X-Octopus-Bot-Id": config.botId };
|
|
16684
|
+
const headers = { Authorization: `SecretKey ${config.secretKey}`, "X-Octopus-Bot-Id": config.botId, "X-Octopus-Proxy-Instance-Id": epoch };
|
|
16551
16685
|
const socket = new WebSocket2(url, { headers, maxPayload: 3 * 1024 * 1024 });
|
|
16552
16686
|
sockets.add(socket);
|
|
16553
|
-
|
|
16687
|
+
if (files) {
|
|
16688
|
+
filesSocket = socket;
|
|
16689
|
+
hubChannels.filesAttempt();
|
|
16690
|
+
}
|
|
16691
|
+
let lastPong = Date.now(), welcomed = false;
|
|
16554
16692
|
const heartbeat = setInterval(() => {
|
|
16555
16693
|
if (Date.now() - lastPong > 45e3) socket.terminate();
|
|
16556
16694
|
else if (socket.readyState === WebSocket2.OPEN) socket.send(JSON.stringify({ type: "ping", proxyVersion }));
|
|
@@ -16571,11 +16709,17 @@ async function runDaemon(configPath) {
|
|
|
16571
16709
|
return;
|
|
16572
16710
|
}
|
|
16573
16711
|
if (value.type === "welcome") {
|
|
16712
|
+
welcomed = true;
|
|
16713
|
+
if (files) hubChannels.filesWelcome();
|
|
16574
16714
|
botId = value.botId;
|
|
16575
16715
|
uploads.limits = value.limits;
|
|
16576
16716
|
if (!files) {
|
|
16577
16717
|
events.bind(socket, value.resume);
|
|
16578
16718
|
for (const p of config.projects) emit(p.id, "octopus.resync", {});
|
|
16719
|
+
if (!filesStarted && !hubChannels.enabled) {
|
|
16720
|
+
filesStarted = true;
|
|
16721
|
+
connect(true);
|
|
16722
|
+
}
|
|
16579
16723
|
}
|
|
16580
16724
|
return;
|
|
16581
16725
|
}
|
|
@@ -16588,18 +16732,32 @@ async function runDaemon(configPath) {
|
|
|
16588
16732
|
console.error("Rejected message:", error instanceof Error ? error.message : "invalid message");
|
|
16589
16733
|
}
|
|
16590
16734
|
});
|
|
16591
|
-
socket.on("
|
|
16592
|
-
});
|
|
16593
|
-
socket.on("close", () => {
|
|
16735
|
+
socket.on("unexpected-response", (_request, response) => console.error(`Hub ${files ? "files" : "control"} handshake failed: HTTP ${response.statusCode}`));
|
|
16736
|
+
socket.on("error", (error) => console.error(`Hub ${files ? "files" : "control"} connection: ${error.message}`));
|
|
16737
|
+
socket.on("close", (code, reason) => {
|
|
16594
16738
|
sockets.delete(socket);
|
|
16595
16739
|
clearInterval(heartbeat);
|
|
16596
16740
|
events.unbind(socket);
|
|
16597
|
-
if (
|
|
16741
|
+
if (files && filesSocket === socket) filesSocket = void 0;
|
|
16742
|
+
const replaced = binding.observeClose(code);
|
|
16743
|
+
if (replaced) {
|
|
16744
|
+
console.error("This bot was claimed by another Proxy instance; automatic reconnection is disabled until this service is restarted");
|
|
16745
|
+
for (const peer of sockets) peer.close();
|
|
16746
|
+
}
|
|
16747
|
+
const collision = binding.active && !files && hubChannels.controlClosed(filesSocket !== void 0 && filesSocket.readyState !== WebSocket2.CLOSED);
|
|
16748
|
+
const rejectedFilesChannel = binding.active && files && !welcomed && hubChannels.filesClosedBeforeWelcome(stopping);
|
|
16749
|
+
if (collision || rejectedFilesChannel) {
|
|
16750
|
+
console.warn("Hub routes proxy-files as the control channel; using compatibility mode with one control connection");
|
|
16751
|
+
filesSocket?.close();
|
|
16752
|
+
filesSocket = void 0;
|
|
16753
|
+
} else if (!stopping && binding.active && code !== 1e3) {
|
|
16754
|
+
console.warn(`Hub ${files ? "files" : "control"} connection closed: ${code}${reason.length ? ` ${reason.toString()}` : ""}`);
|
|
16755
|
+
}
|
|
16756
|
+
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);
|
|
16598
16757
|
});
|
|
16599
16758
|
}
|
|
16600
16759
|
await runtime.init().catch((error) => console.error(`${runtime.kind} startup:`, error.message));
|
|
16601
16760
|
connect(false);
|
|
16602
|
-
connect(true);
|
|
16603
16761
|
const scan = setInterval(async () => {
|
|
16604
16762
|
for (const p of config.projects) await artifacts.scan(p.id).catch(() => {
|
|
16605
16763
|
});
|
package/dist/cli.js
CHANGED
package/dist/main.js
CHANGED