dsh-deeppilot 0.6.0-alpha.2 → 0.6.0-alpha.4
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/COMPATIBILITY.md +7 -8
- package/README.md +23 -17
- package/README.zh-CN.md +20 -15
- package/SECURITY.md +2 -2
- package/bin/SHA256SUMS +6 -6
- 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 +317 -38
- package/lib/client.js.map +1 -1
- package/lib/index.d.ts +35 -7
- package/lib/index.js +771 -185
- package/lib/index.js.map +1 -1
- package/package.json +23 -12
package/lib/index.js
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import { createRequire } from "node:module";
|
|
2
|
-
import { createServer } from "node:http";
|
|
3
2
|
import { createHash, createPrivateKey, createPublicKey, randomBytes, randomUUID, sign, timingSafeEqual, verify } from "node:crypto";
|
|
4
|
-
import { access, chmod, mkdir, readFile, rename, unlink, writeFile } from "node:fs/promises";
|
|
3
|
+
import { access, chmod, mkdir, readFile, readdir, rename, rm, unlink, writeFile } from "node:fs/promises";
|
|
5
4
|
import { dirname, join, resolve } from "node:path";
|
|
6
5
|
import { WebSocketServer } from "ws";
|
|
7
6
|
import { homedir, networkInterfaces } from "node:os";
|
|
7
|
+
import * as Cordis from "@deepseek-ai/cordis";
|
|
8
|
+
import { Context } from "@deepseek-ai/cordis";
|
|
8
9
|
import { TypertRemoteService } from "@deepseek-ai/dsh-typert-protocol";
|
|
9
10
|
import { connect } from "node:http2";
|
|
10
11
|
import { spawn } from "node:child_process";
|
|
@@ -13,8 +14,11 @@ import { fileURLToPath } from "node:url";
|
|
|
13
14
|
import { request } from "node:https";
|
|
14
15
|
import z from "@deepseek-ai/schemastery";
|
|
15
16
|
import { isIP } from "node:net";
|
|
17
|
+
import { createServer } from "node:http";
|
|
16
18
|
//#region src/device-auth.ts
|
|
17
|
-
|
|
19
|
+
function getPairingCodeTtlMs() {
|
|
20
|
+
return Number(process.env.DEEPPILOT_PAIRING_TTL_MS) || 3e5;
|
|
21
|
+
}
|
|
18
22
|
const AUTH_CHALLENGE_TTL_MS = 3e4;
|
|
19
23
|
const DEVICE_SCOPES = [
|
|
20
24
|
"sessions.read",
|
|
@@ -104,7 +108,7 @@ var PairingCodeManager = class {
|
|
|
104
108
|
issue(now = Date.now()) {
|
|
105
109
|
const grant = {
|
|
106
110
|
code: randomBytes(24).toString("base64url"),
|
|
107
|
-
expiresAt: now +
|
|
111
|
+
expiresAt: now + getPairingCodeTtlMs()
|
|
108
112
|
};
|
|
109
113
|
this.active = grant;
|
|
110
114
|
return { ...grant };
|
|
@@ -339,6 +343,9 @@ function requiredScope(type) {
|
|
|
339
343
|
function sanitizeImageName(value) {
|
|
340
344
|
return value.replace(/[\u0000-\u001F\u007F]/g, "").trim().slice(0, 120);
|
|
341
345
|
}
|
|
346
|
+
function sanitizeDocumentField(value, maxChars) {
|
|
347
|
+
return value.replace(/[\u0000-\u001F\u007F]/g, " ").trim().slice(0, maxChars);
|
|
348
|
+
}
|
|
342
349
|
/** Error code for a failed approval/question response outcome. */
|
|
343
350
|
function pendingResponseErrorCode(reason) {
|
|
344
351
|
switch (reason) {
|
|
@@ -724,9 +731,11 @@ var BridgeConnection = class {
|
|
|
724
731
|
const p = env.payload;
|
|
725
732
|
const text = typeof p?.text === "string" ? p.text : "";
|
|
726
733
|
const rawImages = Array.isArray(p?.images) ? p.images : [];
|
|
727
|
-
|
|
734
|
+
const rawDocuments = Array.isArray(p?.documents) ? p.documents : [];
|
|
735
|
+
if (!p?.sessionId || text.trim().length === 0 && rawImages.length === 0 && rawDocuments.length === 0) return this.fail(env.id, "E_PROTOCOL", "sessionId and prompt content required");
|
|
728
736
|
if (text.length > 262144) return this.fail(env.id, "E_PROTOCOL", "prompt text too long");
|
|
729
737
|
if (rawImages.length > 4) return this.fail(env.id, "E_PROTOCOL", "too many images");
|
|
738
|
+
if (rawDocuments.length > 4 || rawImages.length + rawDocuments.length > 4) return this.fail(env.id, "E_PROTOCOL", "too many prompt attachments");
|
|
730
739
|
const images = [];
|
|
731
740
|
for (const image of rawImages) {
|
|
732
741
|
if (!IMAGE_MEDIA_TYPES.has(String(image?.mediaType)) || typeof image?.data !== "string" || image.data.length === 0 || image.data.length > 8388608) return this.fail(env.id, "E_PROTOCOL", "invalid image attachment");
|
|
@@ -736,7 +745,20 @@ var BridgeConnection = class {
|
|
|
736
745
|
...typeof image.name === "string" && sanitizeImageName(image.name).length > 0 ? { name: sanitizeImageName(image.name) } : {}
|
|
737
746
|
});
|
|
738
747
|
}
|
|
739
|
-
const
|
|
748
|
+
const documents = [];
|
|
749
|
+
for (const document of rawDocuments) {
|
|
750
|
+
if (typeof document?.name !== "string" || typeof document?.mediaType !== "string" || typeof document?.text !== "string" || document.text.length === 0 || document.text.length > 262144) return this.fail(env.id, "E_PROTOCOL", "invalid document attachment");
|
|
751
|
+
const name = sanitizeDocumentField(document.name, 180);
|
|
752
|
+
const mediaType = sanitizeDocumentField(document.mediaType, 120).toLowerCase();
|
|
753
|
+
if (!name || !mediaType || mediaType.startsWith("image/")) return this.fail(env.id, "E_PROTOCOL", "invalid document attachment");
|
|
754
|
+
documents.push({
|
|
755
|
+
name,
|
|
756
|
+
mediaType,
|
|
757
|
+
text: document.text,
|
|
758
|
+
...document.truncated === true ? { truncated: true } : {}
|
|
759
|
+
});
|
|
760
|
+
}
|
|
761
|
+
const userSeq = await this.deps.bridge.sendPrompt(p.sessionId, text, images, documents);
|
|
740
762
|
if (!userSeq.ok) return this.fail(env.id, managementErrorCode(userSeq.kind), userSeq.message);
|
|
741
763
|
this.send("s2c.ack", { userSeq: userSeq.value }, env.id);
|
|
742
764
|
return;
|
|
@@ -855,6 +877,44 @@ function unwrapStreamItem(item) {
|
|
|
855
877
|
return item;
|
|
856
878
|
}
|
|
857
879
|
//#endregion
|
|
880
|
+
//#region src/document-payload.ts
|
|
881
|
+
const PREFIX = "[DeepPilot document:";
|
|
882
|
+
const SUFFIX = "]";
|
|
883
|
+
/**
|
|
884
|
+
* DSH 0.1.2 has durable image attachments but no generic file content block.
|
|
885
|
+
* Documents therefore travel as a bounded, explicitly-labelled text block.
|
|
886
|
+
* The marker lets the canonical phone row recover an attachment chip without
|
|
887
|
+
* exposing the complete document body as ordinary user-authored bubble text.
|
|
888
|
+
*/
|
|
889
|
+
function documentPromptBlock(document) {
|
|
890
|
+
const marker = {
|
|
891
|
+
v: 1,
|
|
892
|
+
name: document.name,
|
|
893
|
+
mediaType: document.mediaType,
|
|
894
|
+
...document.truncated ? { truncated: true } : {}
|
|
895
|
+
};
|
|
896
|
+
const encoded = Buffer.from(JSON.stringify(marker), "utf8").toString("base64url");
|
|
897
|
+
return `${PREFIX}${encoded}${SUFFIX}\nAttached document: ${document.name}\n\n${document.text}`;
|
|
898
|
+
}
|
|
899
|
+
function projectedDocument(text) {
|
|
900
|
+
if (!text.startsWith(PREFIX)) return void 0;
|
|
901
|
+
const end = text.indexOf(SUFFIX, 20);
|
|
902
|
+
if (end < 0) return void 0;
|
|
903
|
+
try {
|
|
904
|
+
const decoded = JSON.parse(Buffer.from(text.slice(20, end), "base64url").toString("utf8"));
|
|
905
|
+
if (decoded.v !== 1 || typeof decoded.name !== "string" || typeof decoded.mediaType !== "string") return void 0;
|
|
906
|
+
if (!decoded.name.trim() || !decoded.mediaType.trim()) return void 0;
|
|
907
|
+
return {
|
|
908
|
+
name: decoded.name,
|
|
909
|
+
mediaType: decoded.mediaType,
|
|
910
|
+
text: text.slice(end + 1).replace(/^\nAttached document:[^\n]*\n\n/, ""),
|
|
911
|
+
...decoded.truncated === true ? { truncated: true } : {}
|
|
912
|
+
};
|
|
913
|
+
} catch {
|
|
914
|
+
return;
|
|
915
|
+
}
|
|
916
|
+
}
|
|
917
|
+
//#endregion
|
|
858
918
|
//#region src/host-event-projection.ts
|
|
859
919
|
const MAX_MESSAGE_PROJECTION_BYTES = 262144;
|
|
860
920
|
/** One durable host event sequence becomes exactly one phone message row.
|
|
@@ -1049,7 +1109,7 @@ function contentText(content) {
|
|
|
1049
1109
|
if (typeof part === "string") return part;
|
|
1050
1110
|
if (part && typeof part === "object") {
|
|
1051
1111
|
const piece = part;
|
|
1052
|
-
if (piece.type === "text" && typeof piece.text === "string") return piece.text;
|
|
1112
|
+
if (piece.type === "text" && typeof piece.text === "string") return projectedDocument(piece.text) ? "" : piece.text;
|
|
1053
1113
|
}
|
|
1054
1114
|
return "";
|
|
1055
1115
|
}).join("");
|
|
@@ -1060,22 +1120,34 @@ function messageAttachments(data) {
|
|
|
1060
1120
|
const obj = data;
|
|
1061
1121
|
if (obj.message && typeof obj.message === "object") return messageAttachments(obj.message);
|
|
1062
1122
|
if (!Array.isArray(obj.content)) return [];
|
|
1063
|
-
|
|
1064
|
-
|
|
1123
|
+
const attachments = [];
|
|
1124
|
+
for (const part of obj.content) {
|
|
1125
|
+
if (!part || typeof part !== "object") continue;
|
|
1065
1126
|
const block = part;
|
|
1066
|
-
if (block.type
|
|
1127
|
+
if (block.type === "text" && typeof block.text === "string") {
|
|
1128
|
+
const document = projectedDocument(block.text);
|
|
1129
|
+
if (document) attachments.push({
|
|
1130
|
+
kind: "document",
|
|
1131
|
+
name: document.name,
|
|
1132
|
+
mediaType: document.mediaType,
|
|
1133
|
+
...document.truncated ? { truncated: true } : {}
|
|
1134
|
+
});
|
|
1135
|
+
continue;
|
|
1136
|
+
}
|
|
1137
|
+
if (block.type !== "image" || !block.attachment) continue;
|
|
1067
1138
|
const attachmentId = typeof block.attachment.attachmentId === "string" && block.attachment.attachmentId.length > 0 ? block.attachment.attachmentId : void 0;
|
|
1068
1139
|
const width = typeof block.attachment.width === "number" && Number.isFinite(block.attachment.width) ? block.attachment.width : void 0;
|
|
1069
1140
|
const height = typeof block.attachment.height === "number" && Number.isFinite(block.attachment.height) ? block.attachment.height : void 0;
|
|
1070
|
-
|
|
1141
|
+
attachments.push({
|
|
1071
1142
|
kind: "image",
|
|
1072
1143
|
...typeof block.attachment.name === "string" ? { name: block.attachment.name } : {},
|
|
1073
1144
|
...typeof block.attachment.mediaType === "string" ? { mediaType: block.attachment.mediaType } : {},
|
|
1074
1145
|
...attachmentId ? { attachmentId } : {},
|
|
1075
1146
|
...width !== void 0 ? { width } : {},
|
|
1076
1147
|
...height !== void 0 ? { height } : {}
|
|
1077
|
-
}
|
|
1078
|
-
}
|
|
1148
|
+
});
|
|
1149
|
+
}
|
|
1150
|
+
return attachments;
|
|
1079
1151
|
}
|
|
1080
1152
|
function attachmentProjection(data) {
|
|
1081
1153
|
const attachments = messageAttachments(data);
|
|
@@ -2139,7 +2211,7 @@ var HostBridge = class {
|
|
|
2139
2211
|
return null;
|
|
2140
2212
|
}
|
|
2141
2213
|
}
|
|
2142
|
-
async sendPrompt(sessionId, text, images = []) {
|
|
2214
|
+
async sendPrompt(sessionId, text, images = [], documents = []) {
|
|
2143
2215
|
try {
|
|
2144
2216
|
const content = [];
|
|
2145
2217
|
if (text.trim().length > 0) content.push({
|
|
@@ -2150,6 +2222,10 @@ var HostBridge = class {
|
|
|
2150
2222
|
type: "image",
|
|
2151
2223
|
...image
|
|
2152
2224
|
});
|
|
2225
|
+
for (const document of documents) content.push({
|
|
2226
|
+
type: "text",
|
|
2227
|
+
text: documentPromptBlock(document)
|
|
2228
|
+
});
|
|
2153
2229
|
const response = await this.apiProxy.sessions.prompt({
|
|
2154
2230
|
rpcId: randomUUID(),
|
|
2155
2231
|
payload: {
|
|
@@ -2476,16 +2552,150 @@ function projectSessionModels(value) {
|
|
|
2476
2552
|
}
|
|
2477
2553
|
/** Project a history page (raw events) into MessageProjection rows. */
|
|
2478
2554
|
//#endregion
|
|
2555
|
+
//#region src/dsh012-remote-interactions.ts
|
|
2556
|
+
/**
|
|
2557
|
+
* Resident DSH 0.1.2-alpha.3 Remote Events client for phone interactions.
|
|
2558
|
+
*
|
|
2559
|
+
* The Host-side `approval/request` and `user-questions/request` events are
|
|
2560
|
+
* owned once by DSH API Remotes. API Gateway then fans each pending waterfall
|
|
2561
|
+
* out to every connected Client and settles the first result. DeepPilot joins
|
|
2562
|
+
* that official Client plane in-process instead of registering a competing
|
|
2563
|
+
* Host waterfall listener or replacing the official Web composer.
|
|
2564
|
+
*/
|
|
2565
|
+
let clientFaces;
|
|
2566
|
+
/**
|
|
2567
|
+
* Load DSH's browser-distributed Client faces into a tiny Host-side module
|
|
2568
|
+
* table. Published `lib/client.js` files register with `window.__ModuleLoader__`
|
|
2569
|
+
* rather than exporting ESM values, so a normal Node import cannot consume
|
|
2570
|
+
* them. The shim runs only while the three official bundles register; their
|
|
2571
|
+
* apply functions then operate against a non-browser Cordis Context.
|
|
2572
|
+
*/
|
|
2573
|
+
function pluginModule(value, specifier) {
|
|
2574
|
+
if (typeof value !== "object" || value === null) throw new Error(`${specifier} Client bundle returned no exports`);
|
|
2575
|
+
if (typeof value.apply !== "function") throw new Error(`${specifier} has no Client apply() export`);
|
|
2576
|
+
return value;
|
|
2577
|
+
}
|
|
2578
|
+
async function loadClientFaces() {
|
|
2579
|
+
clientFaces ??= (async () => {
|
|
2580
|
+
const modules = /* @__PURE__ */ new Map([["@deepseek-ai/cordis", Cordis]]);
|
|
2581
|
+
const loaderGlobal = globalThis;
|
|
2582
|
+
const previousWindow = loaderGlobal.window;
|
|
2583
|
+
loaderGlobal.window = { __ModuleLoader__: { load: (definition) => {
|
|
2584
|
+
const exports = definition.factory((id) => {
|
|
2585
|
+
if (!modules.has(id)) throw new Error(`resident DSH Client cannot resolve ${JSON.stringify(id)}`);
|
|
2586
|
+
return modules.get(id);
|
|
2587
|
+
});
|
|
2588
|
+
modules.set(definition.id, exports);
|
|
2589
|
+
} } };
|
|
2590
|
+
try {
|
|
2591
|
+
for (const specifier of [
|
|
2592
|
+
"@deepseek-ai/dsh-typert-registry/client",
|
|
2593
|
+
"@deepseek-ai/dsh-client-connection/client",
|
|
2594
|
+
"@deepseek-ai/dsh-api-gateway/client"
|
|
2595
|
+
]) await import(specifier);
|
|
2596
|
+
} finally {
|
|
2597
|
+
if (previousWindow === void 0) delete loaderGlobal.window;
|
|
2598
|
+
else loaderGlobal.window = previousWindow;
|
|
2599
|
+
}
|
|
2600
|
+
return {
|
|
2601
|
+
typert: pluginModule(modules.get("@deepseek-ai/dsh-typert-registry"), "@deepseek-ai/dsh-typert-registry/client"),
|
|
2602
|
+
connection: pluginModule(modules.get("@deepseek-ai/dsh-client-connection"), "@deepseek-ai/dsh-client-connection/client"),
|
|
2603
|
+
gateway: pluginModule(modules.get("@deepseek-ai/dsh-api-gateway"), "@deepseek-ai/dsh-api-gateway/client")
|
|
2604
|
+
};
|
|
2605
|
+
})();
|
|
2606
|
+
return clientFaces;
|
|
2607
|
+
}
|
|
2608
|
+
const agentScopeKey = Symbol("deeppilot.dsh-client.agent-scope");
|
|
2609
|
+
function scopeOf(ctx) {
|
|
2610
|
+
return ctx[agentScopeKey];
|
|
2611
|
+
}
|
|
2612
|
+
function createAgentScope(ctx, identity) {
|
|
2613
|
+
return ctx.plugin(function deeppilotAgentScope() {}).ctx.extend({
|
|
2614
|
+
[agentScopeKey]: identity,
|
|
2615
|
+
[Context.filter](listenerCtx) {
|
|
2616
|
+
const listenerIdentity = scopeOf(listenerCtx);
|
|
2617
|
+
return listenerIdentity === void 0 || listenerIdentity === identity;
|
|
2618
|
+
}
|
|
2619
|
+
});
|
|
2620
|
+
}
|
|
2621
|
+
function inProcessStream(gateway, endpoint, payload, signal) {
|
|
2622
|
+
return (async function* () {
|
|
2623
|
+
yield* await gateway.wireStream.open(endpoint, payload, signal);
|
|
2624
|
+
})();
|
|
2625
|
+
}
|
|
2626
|
+
/**
|
|
2627
|
+
* Start one headless official DSH Client backed by the Host's in-process
|
|
2628
|
+
* Connection and Gateway carriers.
|
|
2629
|
+
*
|
|
2630
|
+
* The returned disposer tears down the Remote Events generation, all pending
|
|
2631
|
+
* listeners, and every lazily minted Agent scope.
|
|
2632
|
+
*/
|
|
2633
|
+
async function startDsh012RemoteInteractions(hostCtx, handlers) {
|
|
2634
|
+
const connection = hostCtx.get("connection");
|
|
2635
|
+
const gateway = hostCtx.get("typertGateway");
|
|
2636
|
+
if (connection === void 0) throw new Error("dsh 0.1.2-alpha.3 Host connection is unavailable");
|
|
2637
|
+
if (gateway === void 0) throw new Error("dsh 0.1.2-alpha.3 typertGateway is unavailable");
|
|
2638
|
+
const faces = await loadClientFaces();
|
|
2639
|
+
const client = new Context();
|
|
2640
|
+
const fetchHandler = connection.createSharedFetchHandler("/api");
|
|
2641
|
+
const transport = {
|
|
2642
|
+
fetch: (input, init) => fetchHandler.fetch(new Request(input, init)),
|
|
2643
|
+
openStream: (endpoint, payload, signal) => inProcessStream(gateway, endpoint, payload, signal),
|
|
2644
|
+
ownsHost: true
|
|
2645
|
+
};
|
|
2646
|
+
try {
|
|
2647
|
+
faces.typert.apply(client);
|
|
2648
|
+
const transportGlobal = globalThis;
|
|
2649
|
+
const previousTransport = transportGlobal.__DSH_TRANSPORT__;
|
|
2650
|
+
transportGlobal.__DSH_TRANSPORT__ = transport;
|
|
2651
|
+
try {
|
|
2652
|
+
faces.connection.apply(client);
|
|
2653
|
+
} finally {
|
|
2654
|
+
if (previousTransport === void 0) delete transportGlobal.__DSH_TRANSPORT__;
|
|
2655
|
+
else transportGlobal.__DSH_TRANSPORT__ = previousTransport;
|
|
2656
|
+
}
|
|
2657
|
+
const typert = client.get("typert");
|
|
2658
|
+
if (typert === void 0) throw new Error("resident DSH Client typert service was not installed");
|
|
2659
|
+
const scopes = /* @__PURE__ */ new Map();
|
|
2660
|
+
typert.contexts.registerClient("agent", {
|
|
2661
|
+
identity: (candidate) => scopeOf(candidate),
|
|
2662
|
+
resolve: (identity) => {
|
|
2663
|
+
let scope = scopes.get(identity);
|
|
2664
|
+
if (scope === void 0) {
|
|
2665
|
+
scope = createAgentScope(client, identity);
|
|
2666
|
+
scopes.set(identity, scope);
|
|
2667
|
+
}
|
|
2668
|
+
return scope;
|
|
2669
|
+
}
|
|
2670
|
+
});
|
|
2671
|
+
faces.gateway.apply(client);
|
|
2672
|
+
const remote = client.get("remote");
|
|
2673
|
+
if (remote === void 0) throw new Error("resident DSH Client remote service was not installed");
|
|
2674
|
+
remote.$on("approval/request", function(request, next) {
|
|
2675
|
+
return handlers.approval(scopeOf(this) ?? "", request, next);
|
|
2676
|
+
});
|
|
2677
|
+
remote.$on("user-questions/request", function(request, next) {
|
|
2678
|
+
return handlers.question(scopeOf(this) ?? "", request, next);
|
|
2679
|
+
});
|
|
2680
|
+
return async () => {
|
|
2681
|
+
await client.fiber.dispose();
|
|
2682
|
+
};
|
|
2683
|
+
} catch (error) {
|
|
2684
|
+
await client.fiber.dispose();
|
|
2685
|
+
throw error;
|
|
2686
|
+
}
|
|
2687
|
+
}
|
|
2688
|
+
//#endregion
|
|
2479
2689
|
//#region src/dsh012-api-proxy.ts
|
|
2480
2690
|
/**
|
|
2481
2691
|
* Compatibility façade for the DSH 0.1.2 controller API.
|
|
2482
2692
|
*
|
|
2483
2693
|
* DeepPilot's phone protocol deliberately speaks one stable in-process
|
|
2484
2694
|
* `apiProxy` vocabulary. Harness 0.1.2 removed that service in favor of
|
|
2485
|
-
* direct Session/Workspace controllers plus
|
|
2695
|
+
* direct Session/Workspace controllers plus Gateway-backed Remote Events.
|
|
2486
2696
|
* This adapter rebuilds the small subset the bridge needs from those public
|
|
2487
|
-
* controllers
|
|
2488
|
-
*
|
|
2697
|
+
* controllers and joins the official Client interaction plane in-process,
|
|
2698
|
+
* keeping the phone protocol isolated from the Host API migration.
|
|
2489
2699
|
*/
|
|
2490
2700
|
/** Direct-controller facade with the exact legacy shape HostBridge consumes. */
|
|
2491
2701
|
var Dsh012ApiProxy = class {
|
|
@@ -2494,13 +2704,27 @@ var Dsh012ApiProxy = class {
|
|
|
2494
2704
|
workspaceController;
|
|
2495
2705
|
directoryPicker;
|
|
2496
2706
|
interactions = /* @__PURE__ */ new Map();
|
|
2497
|
-
|
|
2707
|
+
shouldSurfaceInteraction;
|
|
2708
|
+
constructor(ctx, options = {}) {
|
|
2498
2709
|
this.ctx = ctx;
|
|
2499
2710
|
const session = ctx.get("sessionController");
|
|
2500
2711
|
if (session === void 0) throw new Error("dsh 0.1.2 sessionController is unavailable");
|
|
2501
2712
|
this.session = session;
|
|
2502
2713
|
this.workspaceController = ctx.get("workspaceController");
|
|
2503
2714
|
this.directoryPicker = ctx.get("directoryPickerController");
|
|
2715
|
+
this.shouldSurfaceInteraction = options.shouldSurfaceInteraction ?? (() => true);
|
|
2716
|
+
}
|
|
2717
|
+
/**
|
|
2718
|
+
* Decide whether this Gateway Client can surface a phone card. The decision
|
|
2719
|
+
* fails open to false so a broken device registry delegates this delivery
|
|
2720
|
+
* while the official Web Client remains able to answer.
|
|
2721
|
+
*/
|
|
2722
|
+
canSurfaceInteraction(kind) {
|
|
2723
|
+
try {
|
|
2724
|
+
return this.shouldSurfaceInteraction(kind);
|
|
2725
|
+
} catch {
|
|
2726
|
+
return false;
|
|
2727
|
+
}
|
|
2504
2728
|
}
|
|
2505
2729
|
sessions = {
|
|
2506
2730
|
list: async () => this.call(async () => {
|
|
@@ -2613,71 +2837,79 @@ var Dsh012ApiProxy = class {
|
|
|
2613
2837
|
value
|
|
2614
2838
|
});
|
|
2615
2839
|
});
|
|
2616
|
-
|
|
2617
|
-
|
|
2618
|
-
|
|
2619
|
-
|
|
2620
|
-
|
|
2621
|
-
request.signal?.addEventListener("abort", abort, { once: true });
|
|
2622
|
-
this.interactions.set(rpcId, {
|
|
2623
|
-
resolve: response.resolve,
|
|
2624
|
-
map: (value) => {
|
|
2625
|
-
const outcome = value?.outcome;
|
|
2626
|
-
return outcome === "allowed-once" || outcome === "rejected" ? outcome : "unavailable";
|
|
2627
|
-
}
|
|
2628
|
-
});
|
|
2629
|
-
queue.push({
|
|
2630
|
-
type: "approval/requested",
|
|
2631
|
-
rpcId,
|
|
2632
|
-
sessionId,
|
|
2633
|
-
approvalId: rpcId,
|
|
2634
|
-
toolName: String(request.toolName ?? "tool"),
|
|
2635
|
-
reason: String(request.reason ?? "")
|
|
2636
|
-
});
|
|
2637
|
-
return response.promise.finally(() => {
|
|
2638
|
-
request.signal?.removeEventListener("abort", abort);
|
|
2639
|
-
this.interactions.delete(rpcId);
|
|
2640
|
-
queue.push({
|
|
2641
|
-
type: "approval/resolved",
|
|
2642
|
-
approvalId: rpcId
|
|
2643
|
-
});
|
|
2644
|
-
});
|
|
2645
|
-
}), { global: true });
|
|
2646
|
-
const offQuestion = this.ctx.on("user-questions/request", ((request) => {
|
|
2647
|
-
const rpcId = randomUUID();
|
|
2648
|
-
const sessionId = String(request.agent?.session?.id ?? request.agent?.id ?? "");
|
|
2649
|
-
const response = deferred();
|
|
2650
|
-
const abort = () => response.reject(/* @__PURE__ */ new Error("question cancelled"));
|
|
2651
|
-
request.signal?.addEventListener("abort", abort, { once: true });
|
|
2652
|
-
this.interactions.set(rpcId, {
|
|
2653
|
-
resolve: response.resolve,
|
|
2654
|
-
map: (value) => value?.answer ?? value
|
|
2655
|
-
});
|
|
2656
|
-
queue.push({
|
|
2657
|
-
type: "question/requested",
|
|
2658
|
-
rpcId,
|
|
2659
|
-
sessionId,
|
|
2660
|
-
questions: request.questions ?? []
|
|
2661
|
-
});
|
|
2662
|
-
return response.promise.finally(() => {
|
|
2663
|
-
request.signal?.removeEventListener("abort", abort);
|
|
2664
|
-
this.interactions.delete(rpcId);
|
|
2665
|
-
queue.push({
|
|
2666
|
-
type: "question/resolved",
|
|
2667
|
-
questionRpcId: rpcId
|
|
2668
|
-
});
|
|
2840
|
+
let disposeInteractions;
|
|
2841
|
+
try {
|
|
2842
|
+
disposeInteractions = await startDsh012RemoteInteractions(this.ctx, {
|
|
2843
|
+
approval: (sessionId, request, next) => this.answerApproval(queue, sessionId, request, next),
|
|
2844
|
+
question: (sessionId, request, next) => this.answerQuestion(queue, sessionId, request, next)
|
|
2669
2845
|
});
|
|
2670
|
-
})
|
|
2846
|
+
} catch (error) {
|
|
2847
|
+
console.warn(`[deeppilot] DSH Remote interaction client unavailable: ${toError(error).message}`);
|
|
2848
|
+
}
|
|
2671
2849
|
try {
|
|
2672
2850
|
yield* queue.iterate();
|
|
2673
2851
|
} finally {
|
|
2674
2852
|
offEvent();
|
|
2675
2853
|
offProjection?.();
|
|
2676
|
-
|
|
2677
|
-
offQuestion();
|
|
2854
|
+
await disposeInteractions?.();
|
|
2678
2855
|
queue.close();
|
|
2679
2856
|
}
|
|
2680
2857
|
}
|
|
2858
|
+
answerApproval(queue, sessionId, request, next) {
|
|
2859
|
+
if (!this.canSurfaceInteraction("approval")) return next();
|
|
2860
|
+
const rpcId = randomUUID();
|
|
2861
|
+
const response = deferred();
|
|
2862
|
+
const abort = () => response.resolve("cancelled");
|
|
2863
|
+
request.signal?.addEventListener("abort", abort, { once: true });
|
|
2864
|
+
this.interactions.set(rpcId, {
|
|
2865
|
+
resolve: response.resolve,
|
|
2866
|
+
map: (value) => {
|
|
2867
|
+
const outcome = value?.outcome;
|
|
2868
|
+
return outcome === "allowed-once" || outcome === "rejected" ? outcome : "unavailable";
|
|
2869
|
+
}
|
|
2870
|
+
});
|
|
2871
|
+
queue.push({
|
|
2872
|
+
type: "approval/requested",
|
|
2873
|
+
rpcId,
|
|
2874
|
+
sessionId,
|
|
2875
|
+
approvalId: rpcId,
|
|
2876
|
+
toolName: String(request.toolName ?? "tool"),
|
|
2877
|
+
reason: String(request.reason ?? "")
|
|
2878
|
+
});
|
|
2879
|
+
return response.promise.finally(() => {
|
|
2880
|
+
request.signal?.removeEventListener("abort", abort);
|
|
2881
|
+
this.interactions.delete(rpcId);
|
|
2882
|
+
queue.push({
|
|
2883
|
+
type: "approval/resolved",
|
|
2884
|
+
approvalId: rpcId
|
|
2885
|
+
});
|
|
2886
|
+
});
|
|
2887
|
+
}
|
|
2888
|
+
answerQuestion(queue, sessionId, request, next) {
|
|
2889
|
+
if (!this.canSurfaceInteraction("question")) return next();
|
|
2890
|
+
const rpcId = randomUUID();
|
|
2891
|
+
const response = deferred();
|
|
2892
|
+
const abort = () => response.reject(/* @__PURE__ */ new Error("question cancelled"));
|
|
2893
|
+
request.signal?.addEventListener("abort", abort, { once: true });
|
|
2894
|
+
this.interactions.set(rpcId, {
|
|
2895
|
+
resolve: response.resolve,
|
|
2896
|
+
map: (value) => value?.answer ?? value
|
|
2897
|
+
});
|
|
2898
|
+
queue.push({
|
|
2899
|
+
type: "question/requested",
|
|
2900
|
+
rpcId,
|
|
2901
|
+
sessionId,
|
|
2902
|
+
questions: request.questions ?? []
|
|
2903
|
+
});
|
|
2904
|
+
return response.promise.finally(() => {
|
|
2905
|
+
request.signal?.removeEventListener("abort", abort);
|
|
2906
|
+
this.interactions.delete(rpcId);
|
|
2907
|
+
queue.push({
|
|
2908
|
+
type: "question/resolved",
|
|
2909
|
+
questionRpcId: rpcId
|
|
2910
|
+
});
|
|
2911
|
+
});
|
|
2912
|
+
}
|
|
2681
2913
|
async *hostEvents(signal) {
|
|
2682
2914
|
const queue = new AsyncFrameQueue(signal);
|
|
2683
2915
|
const listen = (event, type, project) => this.ctx.on(event, ((...args) => queue.push({
|
|
@@ -2967,6 +3199,28 @@ function parseRemote(value) {
|
|
|
2967
3199
|
updatedAt: int(s, "updatedAt", "remote.updatedAt")
|
|
2968
3200
|
};
|
|
2969
3201
|
}
|
|
3202
|
+
function parseLocal(value) {
|
|
3203
|
+
const s = rec(value, "local");
|
|
3204
|
+
const phase = str(s, "phase", "local.phase");
|
|
3205
|
+
if (![
|
|
3206
|
+
"disabled",
|
|
3207
|
+
"starting",
|
|
3208
|
+
"online",
|
|
3209
|
+
"error",
|
|
3210
|
+
"stopped"
|
|
3211
|
+
].includes(phase)) reject("local.phase");
|
|
3212
|
+
const endpoints = s.endpoints;
|
|
3213
|
+
if (!Array.isArray(endpoints) || endpoints.some((entry) => typeof entry !== "string")) reject("local.endpoints");
|
|
3214
|
+
const message = s.message;
|
|
3215
|
+
if (message !== void 0 && typeof message !== "string") reject("local.message");
|
|
3216
|
+
return {
|
|
3217
|
+
phase,
|
|
3218
|
+
port: int(s, "port", "local.port"),
|
|
3219
|
+
endpoints,
|
|
3220
|
+
...typeof message === "string" ? { message } : {},
|
|
3221
|
+
updatedAt: int(s, "updatedAt", "local.updatedAt")
|
|
3222
|
+
};
|
|
3223
|
+
}
|
|
2970
3224
|
function parseRelayTestStep(value) {
|
|
2971
3225
|
const st = rec(value, "step");
|
|
2972
3226
|
const id = str(st, "id", "step.id");
|
|
@@ -3048,6 +3302,7 @@ function parseReport(value) {
|
|
|
3048
3302
|
historyBufferMax: int(s, "historyBufferMax", "historyBufferMax"),
|
|
3049
3303
|
debug: bool(s, "debug", "debug"),
|
|
3050
3304
|
lanAddresses,
|
|
3305
|
+
local: parseLocal(s.local),
|
|
3051
3306
|
remote: parseRemote(s.remote),
|
|
3052
3307
|
devices: devices.map(parseDevice)
|
|
3053
3308
|
};
|
|
@@ -3207,7 +3462,32 @@ function applyReportRemote(ctx, snapshot, pairingStarter, deviceRevoker, deviceS
|
|
|
3207
3462
|
});
|
|
3208
3463
|
}
|
|
3209
3464
|
//#endregion
|
|
3465
|
+
//#region src/relay-url.ts
|
|
3466
|
+
/**
|
|
3467
|
+
* Normalize an operator-configured relay URL without a backtracking regular
|
|
3468
|
+
* expression. Scanning from the end keeps even pathological inputs linear.
|
|
3469
|
+
*/
|
|
3470
|
+
function normalizeRelayBaseUrl(value) {
|
|
3471
|
+
const trimmed = value.trim();
|
|
3472
|
+
let end = trimmed.length;
|
|
3473
|
+
while (end > 0 && trimmed.charCodeAt(end - 1) === 47) end -= 1;
|
|
3474
|
+
return end === trimmed.length ? trimmed : trimmed.slice(0, end);
|
|
3475
|
+
}
|
|
3476
|
+
//#endregion
|
|
3210
3477
|
//#region src/relay-test.ts
|
|
3478
|
+
/**
|
|
3479
|
+
* Relay connectivity self-test used by the settings page "测试中继" button.
|
|
3480
|
+
*
|
|
3481
|
+
* Two steps mirror what zero-touch enrollment actually does:
|
|
3482
|
+
* 1. health — GET {url}/healthz → is the relay reachable?
|
|
3483
|
+
* 2. enroll — POST {url}/v1/enroll → does the shared key grant a token?
|
|
3484
|
+
*
|
|
3485
|
+
* The enroll step doubles as a repair path: a token it issues is handed back
|
|
3486
|
+
* via `onEnrolled` so the bridge caches it and flips push readiness without
|
|
3487
|
+
* waiting for the next app registration.
|
|
3488
|
+
*
|
|
3489
|
+
* Transport is injectable (`fetchImpl`) so tests never touch the network.
|
|
3490
|
+
*/
|
|
3211
3491
|
const DEFAULT_TIMEOUT_MS = 6e3;
|
|
3212
3492
|
async function requestJson(fetchImpl, url, init, timeoutMs) {
|
|
3213
3493
|
const response = await fetchImpl(url, {
|
|
@@ -3224,7 +3504,7 @@ async function requestJson(fetchImpl, url, init, timeoutMs) {
|
|
|
3224
3504
|
};
|
|
3225
3505
|
}
|
|
3226
3506
|
async function runRelayProbe(options) {
|
|
3227
|
-
const base = options.url
|
|
3507
|
+
const base = normalizeRelayBaseUrl(options.url);
|
|
3228
3508
|
const timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;
|
|
3229
3509
|
const fetchImpl = options.fetchImpl ?? fetch;
|
|
3230
3510
|
const steps = [];
|
|
@@ -3534,7 +3814,7 @@ var RelayClient = class {
|
|
|
3534
3814
|
debug;
|
|
3535
3815
|
log;
|
|
3536
3816
|
constructor(opts) {
|
|
3537
|
-
this.base = opts.url
|
|
3817
|
+
this.base = normalizeRelayBaseUrl(opts.url);
|
|
3538
3818
|
this.token = (opts.token ?? "").trim();
|
|
3539
3819
|
this.timeoutMs = opts.timeoutMs ?? 1e4;
|
|
3540
3820
|
this.debug = opts.debug === true;
|
|
@@ -3665,6 +3945,137 @@ function tunnelHelperArguments(originURL, statePath, options) {
|
|
|
3665
3945
|
String(normalizeFunnelConnectionLimit(options.maxConnectionsPerSource))
|
|
3666
3946
|
];
|
|
3667
3947
|
}
|
|
3948
|
+
/**
|
|
3949
|
+
* Summarize a fatal helper crash from its stderr tail. Go panics dump the
|
|
3950
|
+
* actual reason on the `panic:` line while the trailing stack frames are
|
|
3951
|
+
* `file.go:NN +0xOFF` noise — the old behavior of reporting only the final
|
|
3952
|
+
* line surfaced exactly that noise (e.g. `.../singleflight.go:194 +0x45c`)
|
|
3953
|
+
* and hid the real panic message. Keep the crash header line (a raw
|
|
3954
|
+
* `panic:`, a runtime `fatal error:`, or net/http's `http: panic serving`),
|
|
3955
|
+
* plus an optional `[signal ...]` continuation and the first non-runtime
|
|
3956
|
+
* stack frame's symbol + `file.go:NN` as a location hint. Pointer offsets
|
|
3957
|
+
* are not architecture stable, so they are deliberately dropped.
|
|
3958
|
+
*
|
|
3959
|
+
* The frame symbol sits on the line just above the indented source line in a
|
|
3960
|
+
* Go dump, e.g.:
|
|
3961
|
+
* tailscale.com/net/dnscache.(*Resolver).lookupIP(0x1400012c000, {...})
|
|
3962
|
+
* tailscale.com@v1.102.3/net/dnscache/dnscache.go:604 +0x25c
|
|
3963
|
+
* The first symbol (shortened) is used because a bare `file.go:NN` is not
|
|
3964
|
+
* unique across tailscale (singleflight.go is one example).
|
|
3965
|
+
*/
|
|
3966
|
+
function helperCrashSummary(stderr) {
|
|
3967
|
+
const lines = stderr.split(/\r?\n/);
|
|
3968
|
+
for (let i = 0; i < lines.length; i++) {
|
|
3969
|
+
const line = lines[i].trim();
|
|
3970
|
+
if (line === "") continue;
|
|
3971
|
+
const logTimestamp = /^(\d{4}\/\d{2}\/\d{2} \d{2}:\d{2}:\d{2} )/.exec(line)?.[1] ?? "";
|
|
3972
|
+
if (!/^(panic:|fatal error:|http: panic serving)/.test(logTimestamp.length > 0 ? line.slice(logTimestamp.length) : line)) continue;
|
|
3973
|
+
let summary = logTimestamp.length > 0 ? line.slice(logTimestamp.length) : line;
|
|
3974
|
+
const next = lines[i + 1]?.trim();
|
|
3975
|
+
if (next?.startsWith("[signal")) summary += " " + next;
|
|
3976
|
+
let lastSymbol;
|
|
3977
|
+
for (let j = i + 1; j < lines.length; j++) {
|
|
3978
|
+
const frame = lines[j];
|
|
3979
|
+
const trimmed = frame.trim();
|
|
3980
|
+
if (trimmed === "") continue;
|
|
3981
|
+
const sourceMatch = /^\s+(\S+\.go:\d+)/.exec(frame);
|
|
3982
|
+
if (sourceMatch !== null) {
|
|
3983
|
+
const source = sourceMatch[1];
|
|
3984
|
+
if (source.startsWith("runtime/") || source.startsWith("runtime\\")) {
|
|
3985
|
+
lastSymbol = void 0;
|
|
3986
|
+
continue;
|
|
3987
|
+
}
|
|
3988
|
+
const slash = Math.max(source.lastIndexOf("/"), source.lastIndexOf("\\"));
|
|
3989
|
+
const file = slash >= 0 ? source.slice(slash + 1) : source;
|
|
3990
|
+
const symbol = lastSymbol !== void 0 ? shortenSymbol(lastSymbol) : void 0;
|
|
3991
|
+
summary += " @ " + (symbol !== void 0 ? `${symbol} (${file})` : file);
|
|
3992
|
+
break;
|
|
3993
|
+
}
|
|
3994
|
+
if (trimmed.includes("(") || trimmed.endsWith(")")) lastSymbol = trimmed;
|
|
3995
|
+
}
|
|
3996
|
+
return summary.replace(/\s+/g, " ").trim();
|
|
3997
|
+
}
|
|
3998
|
+
for (let i = lines.length - 1; i >= 0; i--) {
|
|
3999
|
+
const line = lines[i].trim();
|
|
4000
|
+
if (line !== "") return line;
|
|
4001
|
+
}
|
|
4002
|
+
return "";
|
|
4003
|
+
}
|
|
4004
|
+
/** Shrink a Go frame symbol to `pkg.Receiver.method` form (drop module-root
|
|
4005
|
+
* path, generic type arguments and the trailing call arguments), so the
|
|
4006
|
+
* summary hint stays readable. Keeps a `.funcN` closure suffix when present
|
|
4007
|
+
* (e.g. `doCall.func2`), since it identifies the exact closure.
|
|
4008
|
+
*
|
|
4009
|
+
* A symbol looks like:
|
|
4010
|
+
* tailscale.com/util/singleflight.(*Group[...]).doCall.func2(0x…, …)
|
|
4011
|
+
* The trailing call arguments form a balanced paren group that starts at
|
|
4012
|
+
* the function name — that group is stripped first; generics appear only
|
|
4013
|
+
* inside `(*Group[...])` receiver brackets before the final method dot, so
|
|
4014
|
+
* they are removed by trimming from the first `[` to the matching `]`.
|
|
4015
|
+
*/
|
|
4016
|
+
function shortenSymbol(symbol) {
|
|
4017
|
+
const trimmed = symbol.trim();
|
|
4018
|
+
let end = trimmed.length;
|
|
4019
|
+
if (trimmed.endsWith(")")) {
|
|
4020
|
+
let depth = 0;
|
|
4021
|
+
for (let i = trimmed.length - 1; i >= 0; i--) {
|
|
4022
|
+
const ch = trimmed[i];
|
|
4023
|
+
if (ch === ")") depth++;
|
|
4024
|
+
else if (ch === "(") {
|
|
4025
|
+
depth--;
|
|
4026
|
+
if (depth === 0) {
|
|
4027
|
+
end = i;
|
|
4028
|
+
break;
|
|
4029
|
+
}
|
|
4030
|
+
}
|
|
4031
|
+
}
|
|
4032
|
+
}
|
|
4033
|
+
let name = trimmed.slice(0, end).trim();
|
|
4034
|
+
const bracketOpen = name.indexOf("[");
|
|
4035
|
+
if (bracketOpen >= 0) {
|
|
4036
|
+
const bracketClose = name.indexOf("]", bracketOpen);
|
|
4037
|
+
if (bracketClose > bracketOpen) name = name.slice(0, bracketOpen) + name.slice(bracketClose + 1);
|
|
4038
|
+
}
|
|
4039
|
+
const slash = Math.max(name.lastIndexOf("/"), name.lastIndexOf("\\"));
|
|
4040
|
+
return slash >= 0 ? name.slice(slash + 1) : name;
|
|
4041
|
+
}
|
|
4042
|
+
/** Shared by crash summary and dump: whether the stderr looks like a Go
|
|
4043
|
+
* panic/fatal-error crash (vs. an ordinary exit). */
|
|
4044
|
+
function looksLikeCrash(stderr) {
|
|
4045
|
+
return /(?:^|\n)\s*(?:\d{4}\/\d{2}\/\d{2} \d{2}:\d{2}:\d{2} )?(?:panic:|fatal error:|http: panic serving)/.test(stderr);
|
|
4046
|
+
}
|
|
4047
|
+
/** Turn a crash stderr tail into a status message and optionally persist the
|
|
4048
|
+
* full dump under the state dir. Keeps at most MAX_CRASH_DUMPS dump files. */
|
|
4049
|
+
async function analyzeCrashExit(stderr, statePath) {
|
|
4050
|
+
const summary = helperCrashSummary(stderr) || "helper exited";
|
|
4051
|
+
if (!looksLikeCrash(stderr) || stderr.trim() === "") return { summary };
|
|
4052
|
+
const target = statePath.trim();
|
|
4053
|
+
if (target === "") return { summary };
|
|
4054
|
+
try {
|
|
4055
|
+
const file = join(target, `crash-${Date.now()}.log`);
|
|
4056
|
+
await writeFile(file, stderr, { mode: 384 });
|
|
4057
|
+
await trimCrashDumps(target);
|
|
4058
|
+
return {
|
|
4059
|
+
summary,
|
|
4060
|
+
dumpPath: file
|
|
4061
|
+
};
|
|
4062
|
+
} catch {
|
|
4063
|
+
return { summary };
|
|
4064
|
+
}
|
|
4065
|
+
}
|
|
4066
|
+
/** Remove oldest crash-*.log files beyond MAX_CRASH_DUMPS. */
|
|
4067
|
+
async function trimCrashDumps(dir) {
|
|
4068
|
+
let names;
|
|
4069
|
+
try {
|
|
4070
|
+
names = (await readdir(dir)).filter((name) => name.startsWith("crash-") && name.endsWith(".log"));
|
|
4071
|
+
} catch {
|
|
4072
|
+
return;
|
|
4073
|
+
}
|
|
4074
|
+
if (names.length <= 6) return;
|
|
4075
|
+
names.sort();
|
|
4076
|
+
const excess = names.length - 6;
|
|
4077
|
+
await Promise.all(names.slice(0, excess).map((name) => rm(join(dir, name), { force: true }).catch(() => {})));
|
|
4078
|
+
}
|
|
3668
4079
|
/** Parse one helper IPC line without ever evaluating or interpolating it. */
|
|
3669
4080
|
function parseHelperEvent(line) {
|
|
3670
4081
|
try {
|
|
@@ -3819,7 +4230,7 @@ var RemoteSupervisor = class {
|
|
|
3819
4230
|
let stderrBuffer = "";
|
|
3820
4231
|
child.stderr.setEncoding("utf8");
|
|
3821
4232
|
child.stderr.on("data", (chunk) => {
|
|
3822
|
-
stderrBuffer = (stderrBuffer + chunk).slice(-
|
|
4233
|
+
stderrBuffer = (stderrBuffer + chunk).slice(-16e3);
|
|
3823
4234
|
});
|
|
3824
4235
|
child.once("error", (error) => {
|
|
3825
4236
|
this.setStatus({
|
|
@@ -3827,7 +4238,7 @@ var RemoteSupervisor = class {
|
|
|
3827
4238
|
message: `helper launch failed: ${String(error)}`
|
|
3828
4239
|
});
|
|
3829
4240
|
});
|
|
3830
|
-
child.once("exit", (
|
|
4241
|
+
child.once("exit", () => {
|
|
3831
4242
|
if (this.child === child) this.child = void 0;
|
|
3832
4243
|
if (this.stopping) {
|
|
3833
4244
|
this.setStatus({
|
|
@@ -3836,12 +4247,15 @@ var RemoteSupervisor = class {
|
|
|
3836
4247
|
});
|
|
3837
4248
|
return;
|
|
3838
4249
|
}
|
|
3839
|
-
|
|
3840
|
-
|
|
3841
|
-
|
|
3842
|
-
|
|
4250
|
+
analyzeCrashExit(stderrBuffer, this.options.statePath).then((analysis) => {
|
|
4251
|
+
if (this.stopping) return;
|
|
4252
|
+
const message = analysis.summary + (analysis.dumpPath !== void 0 ? ` (full dump: ${analysis.dumpPath})` : "");
|
|
4253
|
+
this.setStatus({
|
|
4254
|
+
phase: "error",
|
|
4255
|
+
message
|
|
4256
|
+
});
|
|
4257
|
+
this.scheduleRestart(originURL, "crash");
|
|
3843
4258
|
});
|
|
3844
|
-
this.scheduleRestart(originURL, "crash");
|
|
3845
4259
|
});
|
|
3846
4260
|
}
|
|
3847
4261
|
async dispose() {
|
|
@@ -4134,6 +4548,24 @@ var UpdateChecker = class {
|
|
|
4134
4548
|
dispose() {}
|
|
4135
4549
|
};
|
|
4136
4550
|
//#endregion
|
|
4551
|
+
//#region src/local-policy.ts
|
|
4552
|
+
const DEFAULT_LOCAL_PORT = 3098;
|
|
4553
|
+
const MIN_LOCAL_PORT = 1024;
|
|
4554
|
+
const MAX_LOCAL_PORT = 65535;
|
|
4555
|
+
function normalizeLocalPort(value) {
|
|
4556
|
+
return typeof value === "number" && Number.isInteger(value) && value >= 1024 && value <= 65535 ? value : DEFAULT_LOCAL_PORT;
|
|
4557
|
+
}
|
|
4558
|
+
function localEndpointURLs(addresses, port) {
|
|
4559
|
+
const normalizedPort = normalizeLocalPort(port);
|
|
4560
|
+
return [...new Set(addresses)].map((address) => `http://${address}:${normalizedPort}`);
|
|
4561
|
+
}
|
|
4562
|
+
function localListenError(error, port) {
|
|
4563
|
+
const code = error?.code;
|
|
4564
|
+
if (code === "EADDRINUSE") return `local port ${port} is already in use`;
|
|
4565
|
+
if (code === "EACCES") return `permission denied while opening local port ${port}`;
|
|
4566
|
+
return error instanceof Error ? error.message : String(error);
|
|
4567
|
+
}
|
|
4568
|
+
//#endregion
|
|
4137
4569
|
//#region src/config.ts
|
|
4138
4570
|
/** Operator-run relay used by distributed builds; overridable via config. */
|
|
4139
4571
|
const DEFAULT_RELAY_URL = "https://pilot.hailab.dev";
|
|
@@ -4142,6 +4574,13 @@ const Config = z.object({
|
|
|
4142
4574
|
devicesPath: z.string().default(join(bridgeDataDir(), "devices-v2.json")),
|
|
4143
4575
|
historyBufferMax: z.natural().min(100).default(2e3),
|
|
4144
4576
|
debug: z.boolean().default(false),
|
|
4577
|
+
local: z.object({
|
|
4578
|
+
enabled: z.boolean().default(true),
|
|
4579
|
+
port: z.natural().min(MIN_LOCAL_PORT).max(MAX_LOCAL_PORT).default(DEFAULT_LOCAL_PORT).description("DeepPilot 局域网独立端口(默认 3098,修改后本地连接会短暂重连)")
|
|
4580
|
+
}).default({
|
|
4581
|
+
enabled: true,
|
|
4582
|
+
port: DEFAULT_LOCAL_PORT
|
|
4583
|
+
}),
|
|
4145
4584
|
remote: z.object({
|
|
4146
4585
|
enabled: z.boolean().default(false),
|
|
4147
4586
|
provider: z.union(["tailscale-funnel"]).default("tailscale-funnel"),
|
|
@@ -4364,12 +4803,64 @@ function shouldReEnrollRelayToken(transport, outcome, reason, opts) {
|
|
|
4364
4803
|
return transport === "relay" && outcome === "failed" && reason === "HTTP 401" && opts.hasEnrollKey && opts.usedCellToken && opts.tokenStillCurrent;
|
|
4365
4804
|
}
|
|
4366
4805
|
//#endregion
|
|
4806
|
+
//#region src/phone-server.ts
|
|
4807
|
+
/**
|
|
4808
|
+
* A deliberately narrow transport listener. Both the LAN endpoint and the
|
|
4809
|
+
* loopback-only Funnel origin use this factory, so neither can accidentally
|
|
4810
|
+
* inherit DSH's wider web/API route surface.
|
|
4811
|
+
*/
|
|
4812
|
+
function createPhoneServer(handlers) {
|
|
4813
|
+
const server = createServer((req, res) => {
|
|
4814
|
+
const path = requestPath(req);
|
|
4815
|
+
if (path === "/phone/health") handlers.health(req, res);
|
|
4816
|
+
else if (path === "/phone/pair") handlers.pair(req, res);
|
|
4817
|
+
else {
|
|
4818
|
+
res.statusCode = 404;
|
|
4819
|
+
res.end("not found");
|
|
4820
|
+
}
|
|
4821
|
+
});
|
|
4822
|
+
server.on("upgrade", (req, socket, head) => {
|
|
4823
|
+
if (requestPath(req) !== "/phone") {
|
|
4824
|
+
socket.end("HTTP/1.1 404 Not Found\r\nConnection: close\r\n\r\n");
|
|
4825
|
+
return;
|
|
4826
|
+
}
|
|
4827
|
+
handlers.upgrade(req, socket, head);
|
|
4828
|
+
});
|
|
4829
|
+
return server;
|
|
4830
|
+
}
|
|
4831
|
+
function requestPath(req) {
|
|
4832
|
+
try {
|
|
4833
|
+
return new URL(req.url ?? "/", "http://phone.local").pathname;
|
|
4834
|
+
} catch {
|
|
4835
|
+
return "/";
|
|
4836
|
+
}
|
|
4837
|
+
}
|
|
4838
|
+
function listen(server, port, host) {
|
|
4839
|
+
return new Promise((resolve, reject) => {
|
|
4840
|
+
const onError = (error) => {
|
|
4841
|
+
server.off("listening", onListening);
|
|
4842
|
+
reject(error);
|
|
4843
|
+
};
|
|
4844
|
+
const onListening = () => {
|
|
4845
|
+
server.off("error", onError);
|
|
4846
|
+
resolve();
|
|
4847
|
+
};
|
|
4848
|
+
server.once("error", onError);
|
|
4849
|
+
server.once("listening", onListening);
|
|
4850
|
+
server.listen(port, host);
|
|
4851
|
+
});
|
|
4852
|
+
}
|
|
4853
|
+
function closeServer(server) {
|
|
4854
|
+
if (server === void 0 || !server.listening) return Promise.resolve();
|
|
4855
|
+
return new Promise((resolve) => server.close(() => resolve()));
|
|
4856
|
+
}
|
|
4857
|
+
//#endregion
|
|
4367
4858
|
//#region src/index.ts
|
|
4368
4859
|
/**
|
|
4369
4860
|
* dsh-deeppilot — data bridge between the DSH host and DeepPilot
|
|
4370
|
-
* clients.
|
|
4371
|
-
*
|
|
4372
|
-
* UI
|
|
4861
|
+
* clients. Owns independent, narrowly routed LAN and Funnel-origin listeners;
|
|
4862
|
+
* temporary compatibility routes remain on the existing DSH web server. The
|
|
4863
|
+
* web UI and the rest of DSH's API are never exposed by these listeners.
|
|
4373
4864
|
*
|
|
4374
4865
|
* Data plane: an in-process HostBridge consumes a local compatibility façade
|
|
4375
4866
|
* over DSH 0.1.2 Session/Workspace controllers, mirrors session summaries,
|
|
@@ -4380,7 +4871,7 @@ function shouldReEnrollRelayToken(transport, outcome, reason, opts) {
|
|
|
4380
4871
|
* Swift models mirror that v2 contract.
|
|
4381
4872
|
*/
|
|
4382
4873
|
const name = "deeppilot";
|
|
4383
|
-
/** No eager service requirement:
|
|
4874
|
+
/** No eager web-service requirement: independent transports start on their own. */
|
|
4384
4875
|
const inject = [];
|
|
4385
4876
|
const SERVER_VERSION = readOwnPackageVersion();
|
|
4386
4877
|
const MAX_CLIENT_CONNECTIONS = 16;
|
|
@@ -4420,6 +4911,7 @@ function apply(ctx, options) {
|
|
|
4420
4911
|
*/
|
|
4421
4912
|
let liveSource;
|
|
4422
4913
|
let scheduleRemoteReconcile;
|
|
4914
|
+
let scheduleLocalReconcile;
|
|
4423
4915
|
const currentConfig = () => {
|
|
4424
4916
|
if (liveSource !== void 0) return normalizeOptions(liveSource());
|
|
4425
4917
|
return normalizeOptions(options);
|
|
@@ -4428,9 +4920,15 @@ function apply(ctx, options) {
|
|
|
4428
4920
|
const settingsHooks = {
|
|
4429
4921
|
setSource: (source) => {
|
|
4430
4922
|
liveSource = source;
|
|
4431
|
-
queueMicrotask(() =>
|
|
4923
|
+
queueMicrotask(() => {
|
|
4924
|
+
scheduleLocalReconcile?.();
|
|
4925
|
+
scheduleRemoteReconcile?.();
|
|
4926
|
+
});
|
|
4432
4927
|
},
|
|
4433
|
-
onChange: () => queueMicrotask(() =>
|
|
4928
|
+
onChange: () => queueMicrotask(() => {
|
|
4929
|
+
scheduleLocalReconcile?.();
|
|
4930
|
+
scheduleRemoteReconcile?.();
|
|
4931
|
+
})
|
|
4434
4932
|
};
|
|
4435
4933
|
ctx.inject(["settings"], (settingsCtx) => {
|
|
4436
4934
|
settingsCtx.settings.installSection(ctx, "deeppilot", Config, normalizeOptions(void 0), settingsHooks);
|
|
@@ -4836,6 +5334,16 @@ function apply(ctx, options) {
|
|
|
4836
5334
|
maxPayload: MAX_FRAME_BYTES
|
|
4837
5335
|
});
|
|
4838
5336
|
let remoteSupervisor;
|
|
5337
|
+
let localState = {
|
|
5338
|
+
phase: currentConfig().enabled === true && currentConfig().local?.enabled !== false ? "starting" : "disabled",
|
|
5339
|
+
port: normalizeLocalPort(currentConfig().local?.port),
|
|
5340
|
+
endpoints: [],
|
|
5341
|
+
updatedAt: Date.now()
|
|
5342
|
+
};
|
|
5343
|
+
const localStatus = (addresses) => ({
|
|
5344
|
+
...localState,
|
|
5345
|
+
endpoints: localState.phase === "online" ? localEndpointURLs(addresses, localState.port) : []
|
|
5346
|
+
});
|
|
4839
5347
|
const remoteStatus = () => remoteSupervisor?.status() ?? {
|
|
4840
5348
|
provider: "tailscale-funnel",
|
|
4841
5349
|
phase: currentConfig().remote?.enabled === true ? "stopped" : "disabled",
|
|
@@ -4869,6 +5377,7 @@ function apply(ctx, options) {
|
|
|
4869
5377
|
}));
|
|
4870
5378
|
} catch {}
|
|
4871
5379
|
const update = updateInfo();
|
|
5380
|
+
const lanAddresses = localLANIPv4Addresses();
|
|
4872
5381
|
return {
|
|
4873
5382
|
protocolVersion: 2,
|
|
4874
5383
|
serverVersion: SERVER_VERSION,
|
|
@@ -4881,7 +5390,8 @@ function apply(ctx, options) {
|
|
|
4881
5390
|
activeConnections: connections.size,
|
|
4882
5391
|
historyBufferMax: currentConfig().historyBufferMax ?? 2e3,
|
|
4883
5392
|
debug: currentConfig().debug === true,
|
|
4884
|
-
lanAddresses
|
|
5393
|
+
lanAddresses,
|
|
5394
|
+
local: localStatus(lanAddresses),
|
|
4885
5395
|
remote: remoteStatus(),
|
|
4886
5396
|
devices
|
|
4887
5397
|
};
|
|
@@ -5151,7 +5661,173 @@ function apply(ctx, options) {
|
|
|
5151
5661
|
res.end(JSON.stringify({ ok: false }));
|
|
5152
5662
|
}
|
|
5153
5663
|
};
|
|
5154
|
-
|
|
5664
|
+
const phoneHandlers = {
|
|
5665
|
+
health: handleHealth,
|
|
5666
|
+
pair: handlePair,
|
|
5667
|
+
upgrade: handleUpgrade
|
|
5668
|
+
};
|
|
5669
|
+
let localServer;
|
|
5670
|
+
let appliedLocalKey;
|
|
5671
|
+
let localDisposed = false;
|
|
5672
|
+
let localTail = Promise.resolve();
|
|
5673
|
+
const reconcileLocal = async () => {
|
|
5674
|
+
if (localDisposed) return;
|
|
5675
|
+
const config = currentConfig();
|
|
5676
|
+
const localConfig = config.local ?? {};
|
|
5677
|
+
const next = {
|
|
5678
|
+
enabled: config.enabled === true && localConfig.enabled !== false,
|
|
5679
|
+
port: normalizeLocalPort(localConfig.port)
|
|
5680
|
+
};
|
|
5681
|
+
const nextKey = JSON.stringify(next);
|
|
5682
|
+
if (nextKey === appliedLocalKey) return;
|
|
5683
|
+
appliedLocalKey = nextKey;
|
|
5684
|
+
const previous = localServer;
|
|
5685
|
+
localServer = void 0;
|
|
5686
|
+
await closeServer(previous);
|
|
5687
|
+
if (localDisposed) return;
|
|
5688
|
+
if (!next.enabled) {
|
|
5689
|
+
localState = {
|
|
5690
|
+
phase: "disabled",
|
|
5691
|
+
port: next.port,
|
|
5692
|
+
endpoints: [],
|
|
5693
|
+
updatedAt: Date.now()
|
|
5694
|
+
};
|
|
5695
|
+
log("local transport disabled");
|
|
5696
|
+
return;
|
|
5697
|
+
}
|
|
5698
|
+
localState = {
|
|
5699
|
+
phase: "starting",
|
|
5700
|
+
port: next.port,
|
|
5701
|
+
endpoints: [],
|
|
5702
|
+
updatedAt: Date.now()
|
|
5703
|
+
};
|
|
5704
|
+
const server = createPhoneServer(phoneHandlers);
|
|
5705
|
+
localServer = server;
|
|
5706
|
+
try {
|
|
5707
|
+
await listen(server, next.port, "0.0.0.0");
|
|
5708
|
+
if (localDisposed || localServer !== server) {
|
|
5709
|
+
await closeServer(server);
|
|
5710
|
+
return;
|
|
5711
|
+
}
|
|
5712
|
+
localState = {
|
|
5713
|
+
phase: "online",
|
|
5714
|
+
port: next.port,
|
|
5715
|
+
endpoints: [],
|
|
5716
|
+
updatedAt: Date.now()
|
|
5717
|
+
};
|
|
5718
|
+
log(`local transport listening on 0.0.0.0:${next.port}`);
|
|
5719
|
+
} catch (error) {
|
|
5720
|
+
if (localServer === server) localServer = void 0;
|
|
5721
|
+
await closeServer(server);
|
|
5722
|
+
const message = localListenError(error, next.port);
|
|
5723
|
+
localState = {
|
|
5724
|
+
phase: "error",
|
|
5725
|
+
port: next.port,
|
|
5726
|
+
endpoints: [],
|
|
5727
|
+
message,
|
|
5728
|
+
updatedAt: Date.now()
|
|
5729
|
+
};
|
|
5730
|
+
log("local transport failed: " + message);
|
|
5731
|
+
}
|
|
5732
|
+
};
|
|
5733
|
+
scheduleLocalReconcile = () => {
|
|
5734
|
+
localTail = localTail.then(reconcileLocal).catch((error) => log("local reconcile failed: " + String(error)));
|
|
5735
|
+
};
|
|
5736
|
+
const originServer = createPhoneServer(phoneHandlers);
|
|
5737
|
+
let originURL;
|
|
5738
|
+
let appliedRemoteKey;
|
|
5739
|
+
let remoteDisposed = false;
|
|
5740
|
+
let remoteTail = Promise.resolve();
|
|
5741
|
+
const reconcileRemote = async () => {
|
|
5742
|
+
if (remoteDisposed || originURL === void 0) return;
|
|
5743
|
+
const config = currentConfig();
|
|
5744
|
+
const remoteConfig = config.remote ?? {};
|
|
5745
|
+
const remotePort = remoteConfig.funnelPort === 8443 || remoteConfig.funnelPort === 1e4 ? remoteConfig.funnelPort : 443;
|
|
5746
|
+
const helperPath = remoteConfig.helperPath?.trim() || void 0;
|
|
5747
|
+
const next = {
|
|
5748
|
+
enabled: config.enabled === true && remoteConfig.enabled === true && remoteConfig.provider === "tailscale-funnel",
|
|
5749
|
+
hostname: normalizeRemoteHostname(remoteConfig.hostname),
|
|
5750
|
+
statePath: remoteConfig.statePath?.trim() || join(dataDir, "tailscale"),
|
|
5751
|
+
helperPath,
|
|
5752
|
+
funnelPort: remotePort,
|
|
5753
|
+
maxConnectionsPerSource: normalizeFunnelConnectionLimit(remoteConfig.maxConnectionsPerSource)
|
|
5754
|
+
};
|
|
5755
|
+
const nextKey = JSON.stringify(next);
|
|
5756
|
+
if (nextKey === appliedRemoteKey) return;
|
|
5757
|
+
const previous = remoteSupervisor;
|
|
5758
|
+
remoteSupervisor = void 0;
|
|
5759
|
+
if (previous !== void 0) await previous.dispose();
|
|
5760
|
+
if (remoteDisposed) return;
|
|
5761
|
+
const supervisor = new RemoteSupervisor({
|
|
5762
|
+
enabled: next.enabled,
|
|
5763
|
+
hostname: next.hostname,
|
|
5764
|
+
statePath: next.statePath,
|
|
5765
|
+
...next.helperPath ? { helperPath: next.helperPath } : {},
|
|
5766
|
+
funnelPort: next.funnelPort,
|
|
5767
|
+
maxConnectionsPerSource: next.maxConnectionsPerSource,
|
|
5768
|
+
log
|
|
5769
|
+
});
|
|
5770
|
+
remoteSupervisor = supervisor;
|
|
5771
|
+
appliedRemoteKey = nextKey;
|
|
5772
|
+
await supervisor.start(originURL);
|
|
5773
|
+
};
|
|
5774
|
+
scheduleRemoteReconcile = () => {
|
|
5775
|
+
remoteTail = remoteTail.then(reconcileRemote).catch((error) => log("remote reconcile failed: " + String(error)));
|
|
5776
|
+
};
|
|
5777
|
+
ctx.effect(() => {
|
|
5778
|
+
scheduleLocalReconcile?.();
|
|
5779
|
+
listen(originServer, 0, "127.0.0.1").then(() => {
|
|
5780
|
+
const address = originServer.address();
|
|
5781
|
+
if (address && typeof address === "object") {
|
|
5782
|
+
originURL = `http://127.0.0.1:${address.port}`;
|
|
5783
|
+
scheduleRemoteReconcile?.();
|
|
5784
|
+
}
|
|
5785
|
+
}, (error) => log("remote origin failed: " + String(error)));
|
|
5786
|
+
return async () => {
|
|
5787
|
+
localDisposed = true;
|
|
5788
|
+
remoteDisposed = true;
|
|
5789
|
+
scheduleLocalReconcile = void 0;
|
|
5790
|
+
scheduleRemoteReconcile = void 0;
|
|
5791
|
+
const activeLocal = localServer;
|
|
5792
|
+
localServer = void 0;
|
|
5793
|
+
await Promise.allSettled([closeServer(activeLocal), closeServer(originServer)]);
|
|
5794
|
+
localState = {
|
|
5795
|
+
phase: "stopped",
|
|
5796
|
+
port: localState.port,
|
|
5797
|
+
endpoints: [],
|
|
5798
|
+
updatedAt: Date.now()
|
|
5799
|
+
};
|
|
5800
|
+
await Promise.allSettled([localTail, remoteTail.then(async () => {
|
|
5801
|
+
const supervisor = remoteSupervisor;
|
|
5802
|
+
remoteSupervisor = void 0;
|
|
5803
|
+
if (supervisor !== void 0) await supervisor.dispose();
|
|
5804
|
+
})]);
|
|
5805
|
+
};
|
|
5806
|
+
}, "deeppilot: independent transports");
|
|
5807
|
+
const sweep = setInterval(() => {
|
|
5808
|
+
const now = Date.now();
|
|
5809
|
+
for (const connection of connections) if (connection.isStale(now, 6e4)) {
|
|
5810
|
+
log("dropping stale connection");
|
|
5811
|
+
connection.closeIdle();
|
|
5812
|
+
connections.delete(connection);
|
|
5813
|
+
}
|
|
5814
|
+
}, 3e4);
|
|
5815
|
+
ctx.effect(() => () => clearInterval(sweep), "deeppilot: stale sweep");
|
|
5816
|
+
/**
|
|
5817
|
+
* Whether the resident Gateway Client represents a real phone surface. A
|
|
5818
|
+
* paired device remains answerable even while offline: HostBridge retains an
|
|
5819
|
+
* authoritative pending snapshot that the app pulls on reconnect, while
|
|
5820
|
+
* APNs is only a best-effort wakeup. With no paired device this Client calls
|
|
5821
|
+
* `next()`; Gateway's independent official Web delivery is unaffected.
|
|
5822
|
+
*/
|
|
5823
|
+
const hasPairedPhoneSurface = (_kind) => {
|
|
5824
|
+
return (auth.devices?.list() ?? []).some((device) => device.revokedAt === void 0);
|
|
5825
|
+
};
|
|
5826
|
+
ctx.inject([
|
|
5827
|
+
"sessionController",
|
|
5828
|
+
"connection",
|
|
5829
|
+
"typertGateway"
|
|
5830
|
+
], (sub) => {
|
|
5155
5831
|
if (currentConfig().enabled !== true) {
|
|
5156
5832
|
log("bridge disabled; data plane stays inactive");
|
|
5157
5833
|
return;
|
|
@@ -5159,7 +5835,7 @@ function apply(ctx, options) {
|
|
|
5159
5835
|
const apiCtx = sub;
|
|
5160
5836
|
let proxy;
|
|
5161
5837
|
try {
|
|
5162
|
-
proxy = new Dsh012ApiProxy(apiCtx);
|
|
5838
|
+
proxy = new Dsh012ApiProxy(apiCtx, { shouldSurfaceInteraction: hasPairedPhoneSurface });
|
|
5163
5839
|
} catch (error) {
|
|
5164
5840
|
log("dsh 0.1.2 session bridge unavailable: " + String(error));
|
|
5165
5841
|
return;
|
|
@@ -5179,7 +5855,7 @@ function apply(ctx, options) {
|
|
|
5179
5855
|
const webCtx = sub;
|
|
5180
5856
|
const web = webCtx.webServer;
|
|
5181
5857
|
if (!web) {
|
|
5182
|
-
log("webServer service absent
|
|
5858
|
+
log("webServer service absent; 3080 compatibility routes unavailable (independent transports remain active)");
|
|
5183
5859
|
return;
|
|
5184
5860
|
}
|
|
5185
5861
|
webCtx.effect(() => web.registerUpgrade({
|
|
@@ -5196,98 +5872,8 @@ function apply(ctx, options) {
|
|
|
5196
5872
|
path: "/phone/pair",
|
|
5197
5873
|
handler: handlePair
|
|
5198
5874
|
}), "deeppilot: /phone/pair");
|
|
5199
|
-
|
|
5200
|
-
|
|
5201
|
-
for (const connection of connections) if (connection.isStale(now, 6e4)) {
|
|
5202
|
-
log("dropping stale connection");
|
|
5203
|
-
connection.closeIdle();
|
|
5204
|
-
connections.delete(connection);
|
|
5205
|
-
}
|
|
5206
|
-
}, 3e4);
|
|
5207
|
-
webCtx.effect(() => () => clearInterval(sweep), "deeppilot: stale sweep");
|
|
5208
|
-
const originServer = createServer((req, res) => {
|
|
5209
|
-
let path = "/";
|
|
5210
|
-
try {
|
|
5211
|
-
path = new URL(req.url ?? "/", "http://phone.local").pathname;
|
|
5212
|
-
} catch {}
|
|
5213
|
-
if (path === "/phone/health") handleHealth(req, res);
|
|
5214
|
-
else if (path === "/phone/pair") handlePair(req, res);
|
|
5215
|
-
else {
|
|
5216
|
-
res.statusCode = 404;
|
|
5217
|
-
res.end("not found");
|
|
5218
|
-
}
|
|
5219
|
-
});
|
|
5220
|
-
originServer.on("upgrade", (req, socket, head) => {
|
|
5221
|
-
let path = "/";
|
|
5222
|
-
try {
|
|
5223
|
-
path = new URL(req.url ?? "/", "http://phone.local").pathname;
|
|
5224
|
-
} catch {}
|
|
5225
|
-
if (path !== "/phone") {
|
|
5226
|
-
socket.end("HTTP/1.1 404 Not Found\r\nConnection: close\r\n\r\n");
|
|
5227
|
-
return;
|
|
5228
|
-
}
|
|
5229
|
-
handleUpgrade(req, socket, head);
|
|
5230
|
-
});
|
|
5231
|
-
let originURL;
|
|
5232
|
-
let appliedRemoteKey;
|
|
5233
|
-
let remoteDisposed = false;
|
|
5234
|
-
let reconcileTail = Promise.resolve();
|
|
5235
|
-
const reconcileRemote = async () => {
|
|
5236
|
-
if (remoteDisposed || originURL === void 0) return;
|
|
5237
|
-
const config = currentConfig();
|
|
5238
|
-
const remoteConfig = config.remote ?? {};
|
|
5239
|
-
const remotePort = remoteConfig.funnelPort === 8443 || remoteConfig.funnelPort === 1e4 ? remoteConfig.funnelPort : 443;
|
|
5240
|
-
const helperPath = remoteConfig.helperPath?.trim() || void 0;
|
|
5241
|
-
const next = {
|
|
5242
|
-
enabled: config.enabled === true && remoteConfig.enabled === true && remoteConfig.provider === "tailscale-funnel",
|
|
5243
|
-
hostname: normalizeRemoteHostname(remoteConfig.hostname),
|
|
5244
|
-
statePath: remoteConfig.statePath?.trim() || join(dataDir, "tailscale"),
|
|
5245
|
-
helperPath,
|
|
5246
|
-
funnelPort: remotePort,
|
|
5247
|
-
maxConnectionsPerSource: normalizeFunnelConnectionLimit(remoteConfig.maxConnectionsPerSource)
|
|
5248
|
-
};
|
|
5249
|
-
const nextKey = JSON.stringify(next);
|
|
5250
|
-
if (nextKey === appliedRemoteKey) return;
|
|
5251
|
-
const previous = remoteSupervisor;
|
|
5252
|
-
remoteSupervisor = void 0;
|
|
5253
|
-
if (previous !== void 0) await previous.dispose();
|
|
5254
|
-
if (remoteDisposed) return;
|
|
5255
|
-
const supervisor = new RemoteSupervisor({
|
|
5256
|
-
enabled: next.enabled,
|
|
5257
|
-
hostname: next.hostname,
|
|
5258
|
-
statePath: next.statePath,
|
|
5259
|
-
...next.helperPath ? { helperPath: next.helperPath } : {},
|
|
5260
|
-
funnelPort: next.funnelPort,
|
|
5261
|
-
maxConnectionsPerSource: next.maxConnectionsPerSource,
|
|
5262
|
-
log
|
|
5263
|
-
});
|
|
5264
|
-
remoteSupervisor = supervisor;
|
|
5265
|
-
appliedRemoteKey = nextKey;
|
|
5266
|
-
await supervisor.start(originURL);
|
|
5267
|
-
};
|
|
5268
|
-
scheduleRemoteReconcile = () => {
|
|
5269
|
-
reconcileTail = reconcileTail.then(reconcileRemote).catch((error) => log("remote reconcile failed: " + String(error)));
|
|
5270
|
-
};
|
|
5271
|
-
originServer.listen(0, "127.0.0.1", () => {
|
|
5272
|
-
const address = originServer.address();
|
|
5273
|
-
if (address && typeof address === "object") {
|
|
5274
|
-
originURL = `http://127.0.0.1:${address.port}`;
|
|
5275
|
-
scheduleRemoteReconcile?.();
|
|
5276
|
-
}
|
|
5277
|
-
});
|
|
5278
|
-
originServer.on("error", (error) => log("remote origin failed: " + String(error)));
|
|
5279
|
-
webCtx.effect(() => () => {
|
|
5280
|
-
remoteDisposed = true;
|
|
5281
|
-
scheduleRemoteReconcile = void 0;
|
|
5282
|
-
originServer.close();
|
|
5283
|
-
reconcileTail = reconcileTail.then(async () => {
|
|
5284
|
-
const supervisor = remoteSupervisor;
|
|
5285
|
-
remoteSupervisor = void 0;
|
|
5286
|
-
if (supervisor !== void 0) await supervisor.dispose();
|
|
5287
|
-
});
|
|
5288
|
-
}, "deeppilot: embedded Funnel");
|
|
5289
|
-
if (enabledNow()) log("/phone WebSocket registered");
|
|
5290
|
-
else log("bridge disabled; /phone refuses connections until re-enabled and restarted");
|
|
5875
|
+
if (enabledNow()) log("/phone compatibility routes registered on DSH web server");
|
|
5876
|
+
else log("bridge disabled; /phone refuses connections until re-enabled");
|
|
5291
5877
|
});
|
|
5292
5878
|
ctx.effect(() => async () => {
|
|
5293
5879
|
closeAllConnections();
|