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