dsh-deeppilot 0.2.1 → 0.3.0
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 +5 -0
- package/README.zh-CN.md +5 -1
- package/bin/SHA256SUMS +6 -1
- package/bin/darwin-amd64/dsh-deeppilot-tunnel +0 -0
- package/bin/darwin-arm64/dsh-deeppilot-tunnel +0 -0
- package/bin/linux-amd64/dsh-deeppilot-tunnel +0 -0
- package/bin/linux-arm64/dsh-deeppilot-tunnel +0 -0
- package/bin/windows-amd64/dsh-deeppilot-tunnel.exe +0 -0
- package/bin/windows-arm64/dsh-deeppilot-tunnel.exe +0 -0
- package/lib/client.js +14 -3
- package/lib/client.js.map +1 -1
- package/lib/index.js +280 -6
- package/lib/index.js.map +1 -1
- package/package.json +3 -1
package/lib/index.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { createRequire } from "node:module";
|
|
1
2
|
import { createServer } from "node:http";
|
|
2
3
|
import { createPrivateKey, randomBytes, randomUUID, sign, timingSafeEqual } from "node:crypto";
|
|
3
4
|
import { access, mkdir, readFile, rename, writeFile } from "node:fs/promises";
|
|
@@ -11,6 +12,7 @@ import { connect } from "node:http2";
|
|
|
11
12
|
import { spawn } from "node:child_process";
|
|
12
13
|
import { constants } from "node:fs";
|
|
13
14
|
import { fileURLToPath } from "node:url";
|
|
15
|
+
import { request } from "node:https";
|
|
14
16
|
//#region src/token.ts
|
|
15
17
|
/** Expand a leading ~ using the process home directory. */
|
|
16
18
|
function expandHome(p) {
|
|
@@ -2322,9 +2324,13 @@ function parseReport(value) {
|
|
|
2322
2324
|
const lanAddresses = s.lanAddresses;
|
|
2323
2325
|
if (!Array.isArray(devices)) reject("devices");
|
|
2324
2326
|
if (!Array.isArray(lanAddresses) || lanAddresses.some((value) => typeof value !== "string")) reject("lanAddresses");
|
|
2327
|
+
const releaseUrl = s.releaseUrl;
|
|
2325
2328
|
return {
|
|
2326
2329
|
protocolVersion: num(s, "protocolVersion", "protocolVersion"),
|
|
2327
2330
|
serverVersion: str(s, "serverVersion", "serverVersion"),
|
|
2331
|
+
pluginVersion: str(s, "pluginVersion", "pluginVersion"),
|
|
2332
|
+
...s.updateAvailable === true ? { updateAvailable: true } : {},
|
|
2333
|
+
...typeof releaseUrl === "string" && releaseUrl.length > 0 ? { releaseUrl } : {},
|
|
2328
2334
|
enabled: bool(s, "enabled", "enabled"),
|
|
2329
2335
|
tokenPath: str(s, "tokenPath", "tokenPath"),
|
|
2330
2336
|
tokenReady: bool(s, "tokenReady", "tokenReady"),
|
|
@@ -2876,9 +2882,35 @@ function parseHelperEvent(line) {
|
|
|
2876
2882
|
return null;
|
|
2877
2883
|
}
|
|
2878
2884
|
}
|
|
2879
|
-
|
|
2885
|
+
/** Translate Node's platform/architecture names to the GOOS/GOARCH directory
|
|
2886
|
+
* names used by the committed helper matrix. */
|
|
2887
|
+
function bundledHelperPlatformDir(platform = process.platform, arch = process.arch) {
|
|
2888
|
+
return `${platform === "win32" ? "windows" : platform}-${arch === "x64" ? "amd64" : arch}`;
|
|
2889
|
+
}
|
|
2890
|
+
/** Build the list of candidate locations for the embedded tunnel helper, in
|
|
2891
|
+
* priority order. The first existing executable wins at start() time. The
|
|
2892
|
+
* order matters: explicit config (handled by the caller) > npm install
|
|
2893
|
+
* layout > DSH-bundled layout > user data dir. */
|
|
2894
|
+
function bundledHelperCandidates(platform = process.platform, arch = process.arch) {
|
|
2880
2895
|
const here = dirname(fileURLToPath(import.meta.url));
|
|
2881
|
-
|
|
2896
|
+
const pkgRoot = resolve(here, "..");
|
|
2897
|
+
const fileName = platform === "win32" ? "dsh-deeppilot-tunnel.exe" : "dsh-deeppilot-tunnel";
|
|
2898
|
+
const platformDir = bundledHelperPlatformDir(platform, arch);
|
|
2899
|
+
const candidates = [];
|
|
2900
|
+
candidates.push(resolve(pkgRoot, "bin", platformDir, fileName));
|
|
2901
|
+
candidates.push(resolve(pkgRoot, "..", "..", "..", "node_modules", "dsh-deeppilot", "bin", platformDir, fileName));
|
|
2902
|
+
candidates.push(resolve(pkgRoot, "..", "..", "dsh-deeppilot", "bin", platformDir, fileName));
|
|
2903
|
+
candidates.push(resolve(pkgRoot, "..", "..", "..", "..", "node_modules", "dsh-deeppilot", "bin", platformDir, fileName));
|
|
2904
|
+
try {
|
|
2905
|
+
const resolved = createRequire(import.meta.url).resolve(`dsh-deeppilot/bin/${platformDir}/${fileName}`);
|
|
2906
|
+
if (!candidates.includes(resolved)) candidates.push(resolved);
|
|
2907
|
+
} catch {}
|
|
2908
|
+
const home = process.env.DSH_HOME?.trim() || process.env.HOME || process.env.USERPROFILE;
|
|
2909
|
+
if (home && home.length > 0) {
|
|
2910
|
+
const dataDir = resolve(home, ".dsh");
|
|
2911
|
+
candidates.push(join(dataDir, "deeppilot", "bin", platformDir, fileName));
|
|
2912
|
+
}
|
|
2913
|
+
return candidates;
|
|
2882
2914
|
}
|
|
2883
2915
|
/** Owns exactly one embedded tunnel helper and restarts it after failures. */
|
|
2884
2916
|
var RemoteSupervisor = class {
|
|
@@ -2901,10 +2933,29 @@ var RemoteSupervisor = class {
|
|
|
2901
2933
|
}
|
|
2902
2934
|
async start(originURL) {
|
|
2903
2935
|
if (!this.options.enabled || this.child !== void 0 || this.stopping) return;
|
|
2904
|
-
const helper = expandHome(this.options.helperPath ?? bundledHelperPath());
|
|
2905
2936
|
const statePath = expandHome(this.options.statePath);
|
|
2937
|
+
const configured = this.options.helperPath?.trim() ?? "";
|
|
2938
|
+
const candidates = configured ? [expandHome(configured)] : bundledHelperCandidates();
|
|
2939
|
+
let helper;
|
|
2940
|
+
let lastError;
|
|
2941
|
+
for (const candidate of candidates) try {
|
|
2942
|
+
await access(candidate, constants.X_OK);
|
|
2943
|
+
helper = candidate;
|
|
2944
|
+
break;
|
|
2945
|
+
} catch (error) {
|
|
2946
|
+
lastError = error;
|
|
2947
|
+
}
|
|
2948
|
+
if (helper === void 0) {
|
|
2949
|
+
if (this.stopping) return;
|
|
2950
|
+
const platform = `${process.platform}-${process.arch}`;
|
|
2951
|
+
const message = configured ? `embedded tunnel helper unavailable: ${configured}: ${String(lastError ?? "not found")}` : `embedded tunnel helper not found for ${platform} (tried: ${candidates.join(", ")}); set remote.helperPath to override`;
|
|
2952
|
+
this.setStatus({
|
|
2953
|
+
phase: "unavailable",
|
|
2954
|
+
message
|
|
2955
|
+
});
|
|
2956
|
+
return;
|
|
2957
|
+
}
|
|
2906
2958
|
try {
|
|
2907
|
-
await access(helper, constants.X_OK);
|
|
2908
2959
|
await mkdir(statePath, {
|
|
2909
2960
|
recursive: true,
|
|
2910
2961
|
mode: 448
|
|
@@ -2913,7 +2964,7 @@ var RemoteSupervisor = class {
|
|
|
2913
2964
|
if (this.stopping) return;
|
|
2914
2965
|
this.setStatus({
|
|
2915
2966
|
phase: "unavailable",
|
|
2916
|
-
message: `
|
|
2967
|
+
message: `cannot create remote state dir: ${String(error)}`
|
|
2917
2968
|
});
|
|
2918
2969
|
return;
|
|
2919
2970
|
}
|
|
@@ -3076,6 +3127,203 @@ function localLANIPv4Addresses() {
|
|
|
3076
3127
|
return [...new Set(candidates.map(({ address }) => address))];
|
|
3077
3128
|
}
|
|
3078
3129
|
//#endregion
|
|
3130
|
+
//#region src/update-check.ts
|
|
3131
|
+
/**
|
|
3132
|
+
* Lightweight self-update check for dsh-deeppilot.
|
|
3133
|
+
*
|
|
3134
|
+
* On Host boot we ask the GitHub Releases API (REST) which is the latest
|
|
3135
|
+
* stable tag, compare it to the installed plugin version, and surface a
|
|
3136
|
+
* "newer release exists" flag plus the GitHub release URL through the
|
|
3137
|
+
* report Remote. The settings page renders one small line at the bottom;
|
|
3138
|
+
* a successful check is enough — no manual button, no persistent cache
|
|
3139
|
+
* (the host process is the lifetime of the answer).
|
|
3140
|
+
*
|
|
3141
|
+
* Deliberately no third-party dependency: we use {@link https.request}
|
|
3142
|
+
* directly to keep parity with the rest of the project (remote-supervisor
|
|
3143
|
+
* uses node:http, host-bridge uses ws, etc).
|
|
3144
|
+
*
|
|
3145
|
+
* Failure policy: every network/parse error collapses to a single log
|
|
3146
|
+
* line and the in-memory snapshot stays "unknown". The bridge must never
|
|
3147
|
+
* crash because GitHub rate-limited us, returned a 5xx, or the user is
|
|
3148
|
+
* offline.
|
|
3149
|
+
*/
|
|
3150
|
+
/** GitHub repo (no .git suffix). Public, unauthenticated, low rate limit. */
|
|
3151
|
+
const RELEASES_PATH = "/repos/Mars-Sea/dsh-deeppilot/releases";
|
|
3152
|
+
/** Hard ceiling on the network round-trip. The host must never hang. */
|
|
3153
|
+
const FETCH_TIMEOUT_MS = 8e3;
|
|
3154
|
+
/** Per-page limit. We only need the first stable release, but pre-releases
|
|
3155
|
+
* tend to be listed first; fetching 20 gives the comparator enough room. */
|
|
3156
|
+
const PER_PAGE = 20;
|
|
3157
|
+
function isPlainObject(value) {
|
|
3158
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
3159
|
+
}
|
|
3160
|
+
function parseStableEntry(value) {
|
|
3161
|
+
if (!isPlainObject(value)) return null;
|
|
3162
|
+
const tag = value.tag_name;
|
|
3163
|
+
if (typeof tag !== "string") return null;
|
|
3164
|
+
if (value.prerelease === true || value.draft === true) return null;
|
|
3165
|
+
if (parseStableTag(tag) === null) return null;
|
|
3166
|
+
const url = value.html_url;
|
|
3167
|
+
return {
|
|
3168
|
+
tag,
|
|
3169
|
+
url: typeof url === "string" ? url : null
|
|
3170
|
+
};
|
|
3171
|
+
}
|
|
3172
|
+
/** Parse one stable release from the `tag_name` shape `vX.Y.Z` (the v is
|
|
3173
|
+
* optional; `1.2.3` is also accepted). Pre-release tags like `0.3.0-rc.1`
|
|
3174
|
+
* return null — the policy is "stable channel only". */
|
|
3175
|
+
function parseStableTag(tag) {
|
|
3176
|
+
const match = /^v?(\d+)\.(\d+)\.(\d+)$/.exec(tag.trim());
|
|
3177
|
+
if (match === null) return null;
|
|
3178
|
+
return {
|
|
3179
|
+
major: Number(match[1]),
|
|
3180
|
+
minor: Number(match[2]),
|
|
3181
|
+
patch: Number(match[3])
|
|
3182
|
+
};
|
|
3183
|
+
}
|
|
3184
|
+
/** Semver compare for X.Y.Z. Returns -1 / 0 / 1. */
|
|
3185
|
+
function compareSemver(a, b) {
|
|
3186
|
+
const pa = parseStableTag(a);
|
|
3187
|
+
const pb = parseStableTag(b);
|
|
3188
|
+
if (pa === null && pb === null) return 0;
|
|
3189
|
+
if (pa === null) return -1;
|
|
3190
|
+
if (pb === null) return 1;
|
|
3191
|
+
if (pa.major !== pb.major) return pa.major < pb.major ? -1 : 1;
|
|
3192
|
+
if (pa.minor !== pb.minor) return pa.minor < pb.minor ? -1 : 1;
|
|
3193
|
+
if (pa.patch !== pb.patch) return pa.patch < pb.patch ? -1 : 1;
|
|
3194
|
+
return 0;
|
|
3195
|
+
}
|
|
3196
|
+
/** Hit the GitHub Releases API. Resolves with the first stable release
|
|
3197
|
+
* GitHub returned, or null if the list contains no stable entries.
|
|
3198
|
+
* Network / parse errors reject — the caller is responsible for
|
|
3199
|
+
* collapsing them to a log line. */
|
|
3200
|
+
function fetchLatestStableRelease() {
|
|
3201
|
+
return new Promise((resolve, reject) => {
|
|
3202
|
+
const req = request({
|
|
3203
|
+
method: "GET",
|
|
3204
|
+
host: "api.github.com",
|
|
3205
|
+
path: `${RELEASES_PATH}?per_page=${PER_PAGE}`,
|
|
3206
|
+
headers: {
|
|
3207
|
+
"user-agent": "dsh-deeppilot-update-check",
|
|
3208
|
+
"accept": "application/vnd.github+json"
|
|
3209
|
+
}
|
|
3210
|
+
}, (res) => {
|
|
3211
|
+
const status = res.statusCode ?? 0;
|
|
3212
|
+
if (status < 200 || status >= 300) {
|
|
3213
|
+
res.resume();
|
|
3214
|
+
reject(/* @__PURE__ */ new Error(`github releases http ${status}`));
|
|
3215
|
+
return;
|
|
3216
|
+
}
|
|
3217
|
+
const chunks = [];
|
|
3218
|
+
res.on("data", (chunk) => chunks.push(chunk));
|
|
3219
|
+
res.on("end", () => {
|
|
3220
|
+
try {
|
|
3221
|
+
const body = Buffer.concat(chunks).toString("utf8");
|
|
3222
|
+
const parsed = JSON.parse(body);
|
|
3223
|
+
if (!Array.isArray(parsed)) {
|
|
3224
|
+
reject(/* @__PURE__ */ new Error("github releases: response is not an array"));
|
|
3225
|
+
return;
|
|
3226
|
+
}
|
|
3227
|
+
for (const entry of parsed) {
|
|
3228
|
+
const stable = parseStableEntry(entry);
|
|
3229
|
+
if (stable !== null) {
|
|
3230
|
+
resolve(stable);
|
|
3231
|
+
return;
|
|
3232
|
+
}
|
|
3233
|
+
}
|
|
3234
|
+
resolve(null);
|
|
3235
|
+
} catch (error) {
|
|
3236
|
+
reject(error instanceof Error ? error : new Error(String(error)));
|
|
3237
|
+
}
|
|
3238
|
+
});
|
|
3239
|
+
res.on("error", (error) => reject(error));
|
|
3240
|
+
});
|
|
3241
|
+
req.setTimeout(FETCH_TIMEOUT_MS, () => {
|
|
3242
|
+
req.destroy(/* @__PURE__ */ new Error("github releases: timeout after 8000ms"));
|
|
3243
|
+
});
|
|
3244
|
+
req.on("error", (error) => reject(error));
|
|
3245
|
+
req.end();
|
|
3246
|
+
});
|
|
3247
|
+
}
|
|
3248
|
+
/**
|
|
3249
|
+
* Process-wide check state. Constructed once in `apply()`, queried
|
|
3250
|
+
* synchronously by the report snapshot. The check itself fires once in
|
|
3251
|
+
* the background shortly after boot; the answer lives for the lifetime
|
|
3252
|
+
* of the host process — re-running the page in the Web UI does not
|
|
3253
|
+
* trigger another network call.
|
|
3254
|
+
*/
|
|
3255
|
+
var UpdateChecker = class {
|
|
3256
|
+
log;
|
|
3257
|
+
currentVersion;
|
|
3258
|
+
fetchImpl;
|
|
3259
|
+
initialDelayMs;
|
|
3260
|
+
snapshot;
|
|
3261
|
+
inflight = null;
|
|
3262
|
+
constructor(options) {
|
|
3263
|
+
this.log = options.log;
|
|
3264
|
+
this.currentVersion = options.currentVersion;
|
|
3265
|
+
this.fetchImpl = options.fetchImpl ?? fetchLatestStableRelease;
|
|
3266
|
+
this.initialDelayMs = options.initialDelayMs ?? 2e3;
|
|
3267
|
+
this.snapshot = {
|
|
3268
|
+
currentVersion: this.currentVersion,
|
|
3269
|
+
available: false,
|
|
3270
|
+
releaseUrl: null,
|
|
3271
|
+
latestVersion: null
|
|
3272
|
+
};
|
|
3273
|
+
}
|
|
3274
|
+
/** Return the current in-memory snapshot — safe to call from any host
|
|
3275
|
+
* thread. Never throws, never awaits. */
|
|
3276
|
+
get() {
|
|
3277
|
+
return this.snapshot;
|
|
3278
|
+
}
|
|
3279
|
+
/**
|
|
3280
|
+
* Schedule one background refresh after the configured initial delay.
|
|
3281
|
+
* Used by the plugin entry to do the first check without blocking boot.
|
|
3282
|
+
*/
|
|
3283
|
+
scheduleInitial() {
|
|
3284
|
+
if (this.initialDelayMs <= 0) {
|
|
3285
|
+
this.runOnce();
|
|
3286
|
+
return;
|
|
3287
|
+
}
|
|
3288
|
+
const timer = setTimeout(() => {
|
|
3289
|
+
this.runOnce();
|
|
3290
|
+
}, this.initialDelayMs);
|
|
3291
|
+
if (typeof timer.unref === "function") timer.unref();
|
|
3292
|
+
}
|
|
3293
|
+
async runOnce() {
|
|
3294
|
+
if (this.inflight !== null) {
|
|
3295
|
+
await this.inflight;
|
|
3296
|
+
return;
|
|
3297
|
+
}
|
|
3298
|
+
const task = (async () => {
|
|
3299
|
+
try {
|
|
3300
|
+
const stable = await this.fetchImpl();
|
|
3301
|
+
if (stable === null) return;
|
|
3302
|
+
if (compareSemver(stable.tag, this.currentVersion) > 0) this.snapshot = {
|
|
3303
|
+
currentVersion: this.currentVersion,
|
|
3304
|
+
available: true,
|
|
3305
|
+
releaseUrl: stable.url,
|
|
3306
|
+
latestVersion: stable.tag
|
|
3307
|
+
};
|
|
3308
|
+
else this.snapshot = {
|
|
3309
|
+
currentVersion: this.currentVersion,
|
|
3310
|
+
available: false,
|
|
3311
|
+
releaseUrl: null,
|
|
3312
|
+
latestVersion: null
|
|
3313
|
+
};
|
|
3314
|
+
} catch (error) {
|
|
3315
|
+
this.log("update check failed: " + (error instanceof Error ? error.message : String(error)));
|
|
3316
|
+
}
|
|
3317
|
+
})();
|
|
3318
|
+
this.inflight = task.finally(() => {
|
|
3319
|
+
this.inflight = null;
|
|
3320
|
+
});
|
|
3321
|
+
return this.inflight;
|
|
3322
|
+
}
|
|
3323
|
+
/** No-op kept for API symmetry with the host lifecycle wiring. */
|
|
3324
|
+
dispose() {}
|
|
3325
|
+
};
|
|
3326
|
+
//#endregion
|
|
3079
3327
|
//#region src/index.ts
|
|
3080
3328
|
/**
|
|
3081
3329
|
* dsh-deeppilot — data bridge between the DSH host and DeepPilot
|
|
@@ -3134,7 +3382,7 @@ const Config = z.object({
|
|
|
3134
3382
|
relayToken: ""
|
|
3135
3383
|
})
|
|
3136
3384
|
});
|
|
3137
|
-
const SERVER_VERSION =
|
|
3385
|
+
const SERVER_VERSION = readOwnPackageVersion();
|
|
3138
3386
|
const MAX_CLIENT_CONNECTIONS = 16;
|
|
3139
3387
|
/**
|
|
3140
3388
|
* Single-frame bound. Covers the protocol maximum (4 × 8 MB base64 images
|
|
@@ -3142,6 +3390,22 @@ const MAX_CLIENT_CONNECTIONS = 16;
|
|
|
3142
3390
|
* pre-hello buffering far below ws's 100 MiB default.
|
|
3143
3391
|
*/
|
|
3144
3392
|
const MAX_FRAME_BYTES = 67108864;
|
|
3393
|
+
/**
|
|
3394
|
+
* Resolve the host plugin's own version from the installed package.json.
|
|
3395
|
+
* Sourced at boot so the wire / UI always agrees with what npm published.
|
|
3396
|
+
* `createRequire(import.meta.url)` is the tsdown-bundled ESM equivalent of
|
|
3397
|
+
* CommonJS's `require`; the package.json sits next to lib/index.js after
|
|
3398
|
+
* the build, so `../package.json` resolves to the published manifest.
|
|
3399
|
+
*/
|
|
3400
|
+
function readOwnPackageVersion() {
|
|
3401
|
+
try {
|
|
3402
|
+
const pkg = createRequire(import.meta.url)("../package.json");
|
|
3403
|
+
if (typeof pkg.version === "string" && pkg.version.length > 0) return pkg.version;
|
|
3404
|
+
} catch {}
|
|
3405
|
+
const envVersion = process.env.npm_package_version;
|
|
3406
|
+
if (typeof envVersion === "string" && envVersion.length > 0) return envVersion;
|
|
3407
|
+
return "0.0.0+unknown";
|
|
3408
|
+
}
|
|
3145
3409
|
function rejectUpgrade(socket, status, reason) {
|
|
3146
3410
|
const body = JSON.stringify({ error: reason });
|
|
3147
3411
|
socket.end("HTTP/1.1 " + status + " Forbidden\r\nContent-Type: application/json\r\nContent-Length: " + Buffer.byteLength(body) + "\r\nConnection: close\r\n\r\n" + body);
|
|
@@ -3588,6 +3852,12 @@ function apply(ctx, options) {
|
|
|
3588
3852
|
phase: currentConfig().remote?.enabled === true ? "stopped" : "disabled",
|
|
3589
3853
|
updatedAt: Date.now()
|
|
3590
3854
|
};
|
|
3855
|
+
const updateChecker = new UpdateChecker({
|
|
3856
|
+
log,
|
|
3857
|
+
currentVersion: SERVER_VERSION
|
|
3858
|
+
});
|
|
3859
|
+
updateChecker.scheduleInitial();
|
|
3860
|
+
const updateInfo = () => updateChecker.get();
|
|
3591
3861
|
applyReportRemote(ctx, async () => {
|
|
3592
3862
|
let tokenReady = false;
|
|
3593
3863
|
let devices = [];
|
|
@@ -3606,9 +3876,13 @@ function apply(ctx, options) {
|
|
|
3606
3876
|
} } : {}
|
|
3607
3877
|
}));
|
|
3608
3878
|
} catch {}
|
|
3879
|
+
const update = updateInfo();
|
|
3609
3880
|
return {
|
|
3610
3881
|
protocolVersion: 1,
|
|
3611
3882
|
serverVersion: SERVER_VERSION,
|
|
3883
|
+
pluginVersion: update.currentVersion,
|
|
3884
|
+
...update.available ? { updateAvailable: true } : {},
|
|
3885
|
+
...update.releaseUrl !== null ? { releaseUrl: update.releaseUrl } : {},
|
|
3612
3886
|
enabled: currentConfig().enabled === true,
|
|
3613
3887
|
tokenPath: expandHome(currentConfig().authTokenPath ?? join(bridgeDataDir(), "auth-token")),
|
|
3614
3888
|
tokenReady,
|