dsh-deeppilot 0.6.2 → 0.7.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/COMPATIBILITY.md +21 -5
- package/README.md +30 -18
- package/README.zh-CN.md +22 -13
- package/SECURITY.md +1 -1
- package/lib/client.js +42 -27
- package/lib/client.js.map +1 -1
- package/lib/index.d.ts +34 -4
- package/lib/index.js +399 -68
- package/lib/index.js.map +1 -1
- package/package.json +19 -16
package/lib/index.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { createRequire } from "node:module";
|
|
2
|
-
import { createHash, createPrivateKey, createPublicKey, randomBytes, randomUUID, sign, timingSafeEqual, verify } from "node:crypto";
|
|
2
|
+
import { X509Certificate, createHash, createPrivateKey, createPublicKey, randomBytes, randomUUID, sign, timingSafeEqual, verify } from "node:crypto";
|
|
3
3
|
import { access, chmod, mkdir, readFile, readdir, rename, rm, unlink, writeFile } from "node:fs/promises";
|
|
4
4
|
import { dirname, join, resolve } from "node:path";
|
|
5
5
|
import { WebSocketServer } from "ws";
|
|
@@ -11,10 +11,11 @@ import { TypertRemoteService } from "@deepseek-ai/dsh-typert-protocol";
|
|
|
11
11
|
import { connect } from "node:http2";
|
|
12
12
|
import { spawn } from "node:child_process";
|
|
13
13
|
import { fileURLToPath } from "node:url";
|
|
14
|
-
import { request } from "node:https";
|
|
14
|
+
import { createServer, request } from "node:https";
|
|
15
15
|
import z from "@deepseek-ai/schemastery";
|
|
16
16
|
import { isIP } from "node:net";
|
|
17
|
-
import { createServer } from "node:http";
|
|
17
|
+
import { createServer as createServer$1 } from "node:http";
|
|
18
|
+
import { generate } from "selfsigned";
|
|
18
19
|
function validSendId(id) {
|
|
19
20
|
return typeof id === "string" && /^\d{13}-[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(id);
|
|
20
21
|
}
|
|
@@ -644,7 +645,7 @@ function requiredScope(type) {
|
|
|
644
645
|
if (type === "c2s.approval.respond" || type === "c2s.question.respond") return "interactions.respond";
|
|
645
646
|
if (type === "c2s.liveActivity.register" || type === "c2s.liveActivity.unregister") return "notifications.register";
|
|
646
647
|
if (type === "c2s.push.register" || type === "c2s.widget.push.register") return "notifications.register";
|
|
647
|
-
if (type === "c2s.workspace.create" || type === "c2s.session.create" || type === "c2s.session.rename" || type === "c2s.session.archive" || type === "c2s.session.cancel" || type === "c2s.session.selectModel") return "sessions.manage";
|
|
648
|
+
if (type === "c2s.workspace.create" || type === "c2s.session.create" || type === "c2s.session.rename" || type === "c2s.session.archive" || type === "c2s.session.unarchive" || type === "c2s.session.cancel" || type === "c2s.session.selectModel") return "sessions.manage";
|
|
648
649
|
if (type.startsWith("c2s.")) return "sessions.read";
|
|
649
650
|
}
|
|
650
651
|
/** Broadcast and replay authorization. Unknown frame types fail closed. */
|
|
@@ -1024,6 +1025,18 @@ var BridgeConnection = class {
|
|
|
1024
1025
|
this.send("s2c.session.archived", { sessionId: p.sessionId }, env.id);
|
|
1025
1026
|
return;
|
|
1026
1027
|
}
|
|
1028
|
+
case "c2s.sessions.archived":
|
|
1029
|
+
this.send("s2c.sessions.archived.snapshot", { sessions: this.deps.bridge.listArchivedSessions() }, env.id);
|
|
1030
|
+
return;
|
|
1031
|
+
case "c2s.session.unarchive": {
|
|
1032
|
+
const p = env.payload;
|
|
1033
|
+
if (!p?.sessionId) return this.fail(env.id, "E_PROTOCOL", "sessionId required");
|
|
1034
|
+
if (!this.deps.bridge.capabilities.sessionRestore) return this.fail(env.id, "E_UNSUPPORTED", "session restore unavailable on this host version");
|
|
1035
|
+
const result = await this.deps.bridge.unarchiveSession(p.sessionId);
|
|
1036
|
+
if (!result.ok) return this.fail(env.id, managementErrorCode(result.kind), result.message);
|
|
1037
|
+
this.send("s2c.session.unarchived", { sessionId: p.sessionId }, env.id);
|
|
1038
|
+
return;
|
|
1039
|
+
}
|
|
1027
1040
|
case "c2s.session.cancel": {
|
|
1028
1041
|
const p = env.payload;
|
|
1029
1042
|
if (!p?.sessionId) return this.fail(env.id, "E_PROTOCOL", "sessionId required");
|
|
@@ -1434,14 +1447,28 @@ function projectEvent(sessionId, event) {
|
|
|
1434
1447
|
}
|
|
1435
1448
|
case "tool/result": {
|
|
1436
1449
|
const data = event.data;
|
|
1450
|
+
const ok = data?.error === void 0;
|
|
1451
|
+
const result = limitMessageProjection({
|
|
1452
|
+
seq: event.seq,
|
|
1453
|
+
role: "tool",
|
|
1454
|
+
ts: tsOf(event),
|
|
1455
|
+
tool: {
|
|
1456
|
+
name: "result",
|
|
1457
|
+
state: ok ? "ok" : "error",
|
|
1458
|
+
summary: ok ? summarizeResult(data?.message?.content) : "失败"
|
|
1459
|
+
},
|
|
1460
|
+
...ok ? attachmentProjection(data?.message) : {}
|
|
1461
|
+
});
|
|
1437
1462
|
return {
|
|
1438
1463
|
kind: "tool.end",
|
|
1439
1464
|
data: {
|
|
1440
|
-
seq:
|
|
1441
|
-
role:
|
|
1442
|
-
|
|
1443
|
-
|
|
1444
|
-
|
|
1465
|
+
seq: result.seq,
|
|
1466
|
+
role: result.role,
|
|
1467
|
+
ts: result.ts,
|
|
1468
|
+
ok,
|
|
1469
|
+
summary: result.tool?.summary,
|
|
1470
|
+
...result.attachments ? { attachments: result.attachments } : {},
|
|
1471
|
+
...data?.callId ? { callId: String(data.callId) } : {}
|
|
1445
1472
|
}
|
|
1446
1473
|
};
|
|
1447
1474
|
}
|
|
@@ -1679,19 +1706,23 @@ function projectHistory(events) {
|
|
|
1679
1706
|
const target = callId ? toolByCall.get(callId) : void 0;
|
|
1680
1707
|
const failed = data?.error !== void 0;
|
|
1681
1708
|
const summary = failed ? "失败" : summarizeResult(data?.message?.content);
|
|
1682
|
-
|
|
1683
|
-
|
|
1684
|
-
|
|
1685
|
-
|
|
1686
|
-
|
|
1687
|
-
|
|
1709
|
+
const attachments = failed ? {} : attachmentProjection(data?.message);
|
|
1710
|
+
if (target?.tool) {
|
|
1711
|
+
target.tool = {
|
|
1712
|
+
...target.tool,
|
|
1713
|
+
state: failed ? "error" : "ok",
|
|
1714
|
+
summary
|
|
1715
|
+
};
|
|
1716
|
+
if (attachments.attachments) Object.assign(target, attachments);
|
|
1717
|
+
} else messages.push({
|
|
1688
1718
|
...base,
|
|
1689
1719
|
role: "tool",
|
|
1690
1720
|
tool: {
|
|
1691
1721
|
name: "result",
|
|
1692
1722
|
state: failed ? "error" : "ok",
|
|
1693
1723
|
summary
|
|
1694
|
-
}
|
|
1724
|
+
},
|
|
1725
|
+
...attachments
|
|
1695
1726
|
});
|
|
1696
1727
|
break;
|
|
1697
1728
|
}
|
|
@@ -1815,9 +1846,13 @@ var HostBridge = class {
|
|
|
1815
1846
|
historyBufferMax;
|
|
1816
1847
|
id = ++BRIDGE_SEQ;
|
|
1817
1848
|
summaries = /* @__PURE__ */ new Map();
|
|
1849
|
+
activeTools = /* @__PURE__ */ new Map();
|
|
1818
1850
|
approvals = /* @__PURE__ */ new Map();
|
|
1819
1851
|
questions = /* @__PURE__ */ new Map();
|
|
1820
1852
|
archivedSessionIds = /* @__PURE__ */ new Set();
|
|
1853
|
+
/** Mirrors archived rows from the last sessions.list so the phone can browse
|
|
1854
|
+
* and restore them. Excluded from `summaries` and from the live broadcast. */
|
|
1855
|
+
archivedSummaries = /* @__PURE__ */ new Map();
|
|
1821
1856
|
subagentSessionIds = /* @__PURE__ */ new Set();
|
|
1822
1857
|
sinks = /* @__PURE__ */ new Set();
|
|
1823
1858
|
ring = [];
|
|
@@ -1852,6 +1887,7 @@ var HostBridge = class {
|
|
|
1852
1887
|
notifyAllCategories: true,
|
|
1853
1888
|
models: typeof this.apiProxy.sessions.models === "function" && typeof this.apiProxy.sessions.selectModel === "function",
|
|
1854
1889
|
sessionManagement: typeof this.apiProxy.sessions.rename === "function" && typeof this.apiProxy.workspace?.archiveSession === "function",
|
|
1890
|
+
sessionRestore: typeof this.apiProxy.workspace?.unarchiveSession === "function",
|
|
1855
1891
|
projectSelection: typeof this.apiProxy.workspace?.list === "function" && typeof this.apiProxy.workspace?.create === "function",
|
|
1856
1892
|
push: this.pushOutlet?.isAvailable() === true,
|
|
1857
1893
|
widgetPush: true,
|
|
@@ -1901,7 +1937,7 @@ var HostBridge = class {
|
|
|
1901
1937
|
* PROTOCOL §6 + §7), then fan the same payload out to offline devices
|
|
1902
1938
|
* holding an APNs token. */
|
|
1903
1939
|
emitNotify(args) {
|
|
1904
|
-
if (this.subagentSessionIds.has(args.sessionId)) return;
|
|
1940
|
+
if ((args.category === "turn.completed" || args.category === "session.error") && this.subagentSessionIds.has(args.sessionId)) return;
|
|
1905
1941
|
const body = args.body.length > 120 ? args.body.slice(0, 119) + "…" : args.body;
|
|
1906
1942
|
this.record("s2c.notify", {
|
|
1907
1943
|
notificationId: args.notificationId,
|
|
@@ -2077,9 +2113,12 @@ var HostBridge = class {
|
|
|
2077
2113
|
const event = frame.event;
|
|
2078
2114
|
const sessionId = String(frame.sessionId ?? "");
|
|
2079
2115
|
if (!event || !sessionId) break;
|
|
2116
|
+
if (frame.isSubagent === true) this.subagentSessionIds.add(sessionId);
|
|
2117
|
+
if (this.subagentSessionIds.has(sessionId)) break;
|
|
2080
2118
|
this.noteActivity(sessionId, event);
|
|
2081
2119
|
this.captureAssistantText(sessionId, event);
|
|
2082
2120
|
const projection = projectEvent(sessionId, event);
|
|
2121
|
+
this.captureActivity(sessionId, event, projection?.data.tool);
|
|
2083
2122
|
if (projection) this.record("s2c.session.event", {
|
|
2084
2123
|
sessionId,
|
|
2085
2124
|
kind: projection.kind,
|
|
@@ -2114,6 +2153,7 @@ var HostBridge = class {
|
|
|
2114
2153
|
summary,
|
|
2115
2154
|
riskLevel: riskOf(toolName)
|
|
2116
2155
|
});
|
|
2156
|
+
if (p.callId) this.loadApprovalArguments(p.approvalId, p.callId);
|
|
2117
2157
|
this.emitNotify({
|
|
2118
2158
|
sessionId,
|
|
2119
2159
|
category: "approval.required",
|
|
@@ -2196,17 +2236,27 @@ var HostBridge = class {
|
|
|
2196
2236
|
const workspaceBySession = /* @__PURE__ */ new Map();
|
|
2197
2237
|
for (const workspace of workspaces) for (const sessionId of workspace.sessionIds ?? []) workspaceBySession.set(String(sessionId), workspace);
|
|
2198
2238
|
const next = /* @__PURE__ */ new Map();
|
|
2199
|
-
const
|
|
2239
|
+
const nextArchived = /* @__PURE__ */ new Map();
|
|
2200
2240
|
for (const row of response.result.value.items ?? []) {
|
|
2201
|
-
if (this.
|
|
2202
|
-
if (
|
|
2203
|
-
|
|
2241
|
+
if (isSubagentRow(row)) this.subagentSessionIds.add(row.sessionId);
|
|
2242
|
+
if (this.subagentSessionIds.has(row.sessionId)) continue;
|
|
2243
|
+
const summary = toSummary(row, this.approvals, this.questions, workspaceBySession.get(row.sessionId));
|
|
2244
|
+
if (this.archivedSessionIds.has(row.sessionId)) {
|
|
2245
|
+
nextArchived.set(row.sessionId, {
|
|
2246
|
+
...summary,
|
|
2247
|
+
archived: true
|
|
2248
|
+
});
|
|
2204
2249
|
continue;
|
|
2205
2250
|
}
|
|
2206
|
-
next.set(row.sessionId,
|
|
2251
|
+
next.set(row.sessionId, summary);
|
|
2252
|
+
}
|
|
2253
|
+
for (const [id, calls] of this.activeTools) {
|
|
2254
|
+
const row = next.get(id);
|
|
2255
|
+
if (!row || row.status !== "running" && !row.pendingApproval && !row.pendingQuestion) this.activeTools.delete(id);
|
|
2256
|
+
else row.activity = [...calls.values()].at(-1) ?? null;
|
|
2207
2257
|
}
|
|
2208
|
-
this.subagentSessionIds = subagentIds;
|
|
2209
2258
|
this.summaries = next;
|
|
2259
|
+
this.archivedSummaries = nextArchived;
|
|
2210
2260
|
const removedIds = [...previousIds].filter((id) => !next.has(id));
|
|
2211
2261
|
for (const id of this.lastAssistantText.keys()) if (!next.has(id)) this.lastAssistantText.delete(id);
|
|
2212
2262
|
this.record("s2c.sessions.delta", {
|
|
@@ -2224,6 +2274,24 @@ var HostBridge = class {
|
|
|
2224
2274
|
row.title = firstUser.text.replace(/\s+/g, " ").trim().slice(0, 60);
|
|
2225
2275
|
this.pushSummary(row);
|
|
2226
2276
|
}
|
|
2277
|
+
captureActivity(sessionId, event, tool) {
|
|
2278
|
+
const row = this.summaries.get(sessionId);
|
|
2279
|
+
if (!row) return;
|
|
2280
|
+
const data = event.data;
|
|
2281
|
+
if (event.type === "tool/call" && data?.callId) {
|
|
2282
|
+
const calls = this.activeTools.get(sessionId) ?? /* @__PURE__ */ new Map();
|
|
2283
|
+
const detail = [tool?.name, tool?.summary].filter(Boolean).join(": ");
|
|
2284
|
+
calls.set(data.callId, Array.from(detail).slice(0, 160).join(""));
|
|
2285
|
+
if (calls.size > 64) calls.delete(calls.keys().next().value);
|
|
2286
|
+
this.activeTools.set(sessionId, calls);
|
|
2287
|
+
} else if (event.type === "tool/result" && data?.callId) this.activeTools.get(sessionId)?.delete(data.callId);
|
|
2288
|
+
else if (event.type === "turn/start" || event.type === "turn/end") this.activeTools.delete(sessionId);
|
|
2289
|
+
else return;
|
|
2290
|
+
const activity = [...this.activeTools.get(sessionId)?.values() ?? []].at(-1) ?? null;
|
|
2291
|
+
if (row.activity === activity) return;
|
|
2292
|
+
row.activity = activity;
|
|
2293
|
+
this.pushSummary(row);
|
|
2294
|
+
}
|
|
2227
2295
|
noteActivity(sessionId, event) {
|
|
2228
2296
|
const row = this.summaries.get(sessionId);
|
|
2229
2297
|
if (!row) return;
|
|
@@ -2282,6 +2350,14 @@ var HostBridge = class {
|
|
|
2282
2350
|
return [...this.summaries.values()].sort((a, b) => b.lastActivityTs - a.lastActivityTs);
|
|
2283
2351
|
}
|
|
2284
2352
|
/**
|
|
2353
|
+
* Archived rows from the last sessions.list, newest first. Served only on
|
|
2354
|
+
* demand so the live session list and its broadcast keep their shape for
|
|
2355
|
+
* clients that predate `c2s.sessions.archived`.
|
|
2356
|
+
*/
|
|
2357
|
+
listArchivedSessions() {
|
|
2358
|
+
return [...this.archivedSummaries.values()].sort((a, b) => b.lastActivityTs - a.lastActivityTs);
|
|
2359
|
+
}
|
|
2360
|
+
/**
|
|
2285
2361
|
* Complete transient interaction state. Unlike the replay ring, this remains
|
|
2286
2362
|
* authoritative after a long disconnect and is rehydrated by apiProxy's mux
|
|
2287
2363
|
* stream when the bridge itself restarts.
|
|
@@ -2293,6 +2369,7 @@ var HostBridge = class {
|
|
|
2293
2369
|
sessionId: pending.sessionId,
|
|
2294
2370
|
toolName: pending.toolName,
|
|
2295
2371
|
summary: pending.reason,
|
|
2372
|
+
...pending.toolArguments !== void 0 ? { toolArguments: pending.toolArguments } : {},
|
|
2296
2373
|
riskLevel: riskOf(pending.toolName)
|
|
2297
2374
|
})),
|
|
2298
2375
|
questions: [...this.questions.entries()].map(([requestId, pending]) => ({
|
|
@@ -2302,6 +2379,35 @@ var HostBridge = class {
|
|
|
2302
2379
|
}))
|
|
2303
2380
|
};
|
|
2304
2381
|
}
|
|
2382
|
+
/** Resolve only the exact invocation in this session; never guess from the latest tool. */
|
|
2383
|
+
async loadApprovalArguments(requestId, callId) {
|
|
2384
|
+
const pending = this.approvals.get(requestId);
|
|
2385
|
+
if (!pending) return;
|
|
2386
|
+
try {
|
|
2387
|
+
const response = await this.apiProxy.sessions.history({
|
|
2388
|
+
rpcId: randomUUID(),
|
|
2389
|
+
payload: {
|
|
2390
|
+
sessionId: pending.sessionId,
|
|
2391
|
+
maxMessages: 200
|
|
2392
|
+
}
|
|
2393
|
+
});
|
|
2394
|
+
if (this.disposed || this.approvals.get(requestId) !== pending || !response.result?.ok) return;
|
|
2395
|
+
const args = ((response.result.value.events ?? []).map((row) => row.event).find((event) => {
|
|
2396
|
+
const data = event.data;
|
|
2397
|
+
return event.type === "tool/call" && data?.callId === callId && data?.name === pending.toolName;
|
|
2398
|
+
})?.data)?.arguments;
|
|
2399
|
+
if (typeof args !== "string" || !args.trim()) return;
|
|
2400
|
+
pending.toolArguments = args;
|
|
2401
|
+
this.record("s2c.pending.approval", {
|
|
2402
|
+
requestId,
|
|
2403
|
+
sessionId: pending.sessionId,
|
|
2404
|
+
toolName: pending.toolName,
|
|
2405
|
+
summary: pending.reason,
|
|
2406
|
+
riskLevel: riskOf(pending.toolName),
|
|
2407
|
+
toolArguments: args
|
|
2408
|
+
});
|
|
2409
|
+
} catch {}
|
|
2410
|
+
}
|
|
2305
2411
|
/** Tail history for an opened session; pushes s2c.session.tail to the sink. */
|
|
2306
2412
|
async openSession(sink, sessionId, tailCount) {
|
|
2307
2413
|
try {
|
|
@@ -2515,6 +2621,38 @@ var HostBridge = class {
|
|
|
2515
2621
|
};
|
|
2516
2622
|
}
|
|
2517
2623
|
}
|
|
2624
|
+
async unarchiveSession(sessionId) {
|
|
2625
|
+
const unarchive = this.apiProxy.workspace?.unarchiveSession;
|
|
2626
|
+
if (typeof unarchive !== "function") return {
|
|
2627
|
+
ok: false,
|
|
2628
|
+
kind: "unsupported",
|
|
2629
|
+
message: "session restore unavailable on this host version"
|
|
2630
|
+
};
|
|
2631
|
+
try {
|
|
2632
|
+
const response = await unarchive.call(this.apiProxy.workspace, {
|
|
2633
|
+
rpcId: randomUUID(),
|
|
2634
|
+
payload: { sessionId }
|
|
2635
|
+
});
|
|
2636
|
+
if (!response.result) return {
|
|
2637
|
+
ok: false,
|
|
2638
|
+
kind: "internal",
|
|
2639
|
+
message: "session restore returned no result"
|
|
2640
|
+
};
|
|
2641
|
+
if (!response.result.ok) return hostSessionManagementError(response.result.error);
|
|
2642
|
+
this.archivedSessionIds = new Set((response.result.value.archivedSessionIds ?? []).map(String));
|
|
2643
|
+
await this.refreshSummaries();
|
|
2644
|
+
return {
|
|
2645
|
+
ok: true,
|
|
2646
|
+
value: true
|
|
2647
|
+
};
|
|
2648
|
+
} catch (error) {
|
|
2649
|
+
return {
|
|
2650
|
+
ok: false,
|
|
2651
|
+
kind: "internal",
|
|
2652
|
+
message: String(error)
|
|
2653
|
+
};
|
|
2654
|
+
}
|
|
2655
|
+
}
|
|
2518
2656
|
async cancelSession(sessionId) {
|
|
2519
2657
|
const cancel = this.apiProxy.sessions.cancel;
|
|
2520
2658
|
if (typeof cancel !== "function") return {
|
|
@@ -3248,6 +3386,7 @@ var Dsh012ApiProxy = class {
|
|
|
3248
3386
|
if (session === void 0) throw new Error("dsh 0.1.2 sessionController is unavailable");
|
|
3249
3387
|
this.session = session;
|
|
3250
3388
|
this.workspaceController = ctx.get("workspaceController");
|
|
3389
|
+
if (typeof this.workspaceController?.unarchiveSession !== "function") delete this.workspace?.unarchiveSession;
|
|
3251
3390
|
this.directoryPicker = ctx.get("directoryPickerController");
|
|
3252
3391
|
this.shouldSurfaceInteraction = options.shouldSurfaceInteraction ?? (() => true);
|
|
3253
3392
|
}
|
|
@@ -3265,7 +3404,7 @@ var Dsh012ApiProxy = class {
|
|
|
3265
3404
|
}
|
|
3266
3405
|
sessions = {
|
|
3267
3406
|
list: async () => this.call(async () => {
|
|
3268
|
-
return { items: (await this.session.list({}, new AbortController().signal)).items.map(toPhoneSessionRow)
|
|
3407
|
+
return { items: (await this.session.list({}, new AbortController().signal)).items.map(toPhoneSessionRow) };
|
|
3269
3408
|
}),
|
|
3270
3409
|
history: async (request) => this.call(async () => {
|
|
3271
3410
|
const sessionId = request.payload.sessionId;
|
|
@@ -3327,6 +3466,12 @@ var Dsh012ApiProxy = class {
|
|
|
3327
3466
|
archiveSession: async (request) => this.call(async () => {
|
|
3328
3467
|
if (this.workspaceController === void 0) throw unavailable("workspace controller unavailable");
|
|
3329
3468
|
return { archivedSessionIds: [...(await this.workspaceController.archiveSession(request.payload)).archivedSessionIds] };
|
|
3469
|
+
}),
|
|
3470
|
+
unarchiveSession: async (request) => this.call(async () => {
|
|
3471
|
+
if (this.workspaceController === void 0) throw unavailable("workspace controller unavailable");
|
|
3472
|
+
const unarchive = this.workspaceController.unarchiveSession;
|
|
3473
|
+
if (typeof unarchive !== "function") throw unavailable("session restore unavailable on this host version");
|
|
3474
|
+
return { archivedSessionIds: [...(await unarchive.call(this.workspaceController, request.payload)).archivedSessionIds] };
|
|
3330
3475
|
})
|
|
3331
3476
|
};
|
|
3332
3477
|
host = {
|
|
@@ -3363,6 +3508,7 @@ var Dsh012ApiProxy = class {
|
|
|
3363
3508
|
queue.push({
|
|
3364
3509
|
type: "session/event",
|
|
3365
3510
|
sessionId: String(session.id),
|
|
3511
|
+
isSubagent: session.header?.origin === "subagent",
|
|
3366
3512
|
event
|
|
3367
3513
|
});
|
|
3368
3514
|
}), { global: true });
|
|
@@ -3411,7 +3557,8 @@ var Dsh012ApiProxy = class {
|
|
|
3411
3557
|
sessionId,
|
|
3412
3558
|
approvalId: rpcId,
|
|
3413
3559
|
toolName: String(request.toolName ?? "tool"),
|
|
3414
|
-
reason: String(request.reason ?? "")
|
|
3560
|
+
reason: String(request.reason ?? ""),
|
|
3561
|
+
...request.callId ? { callId: request.callId } : {}
|
|
3415
3562
|
});
|
|
3416
3563
|
return response.promise.finally(() => {
|
|
3417
3564
|
request.signal?.removeEventListener("abort", abort);
|
|
@@ -3750,10 +3897,16 @@ function parseLocal(value) {
|
|
|
3750
3897
|
if (!Array.isArray(endpoints) || endpoints.some((entry) => typeof entry !== "string")) reject("local.endpoints");
|
|
3751
3898
|
const message = s.message;
|
|
3752
3899
|
if (message !== void 0 && typeof message !== "string") reject("local.message");
|
|
3900
|
+
const tlsFingerprint = s.tlsFingerprint;
|
|
3901
|
+
if (tlsFingerprint !== void 0 && typeof tlsFingerprint !== "string") reject("local.tlsFingerprint");
|
|
3902
|
+
const tlsIdentityRegenerated = s.tlsIdentityRegenerated;
|
|
3903
|
+
if (tlsIdentityRegenerated !== void 0 && typeof tlsIdentityRegenerated !== "boolean") reject("local.tlsIdentityRegenerated");
|
|
3753
3904
|
return {
|
|
3754
3905
|
phase,
|
|
3755
3906
|
port: int(s, "port", "local.port"),
|
|
3756
3907
|
endpoints,
|
|
3908
|
+
...typeof tlsFingerprint === "string" ? { tlsFingerprint } : {},
|
|
3909
|
+
...tlsIdentityRegenerated === true ? { tlsIdentityRegenerated: true } : {},
|
|
3757
3910
|
...typeof message === "string" ? { message } : {},
|
|
3758
3911
|
updatedAt: int(s, "updatedAt", "local.updatedAt")
|
|
3759
3912
|
};
|
|
@@ -5120,9 +5273,10 @@ const MAX_LOCAL_PORT = 65535;
|
|
|
5120
5273
|
function normalizeLocalPort(value) {
|
|
5121
5274
|
return typeof value === "number" && Number.isInteger(value) && value >= 1024 && value <= 65535 ? value : DEFAULT_LOCAL_PORT;
|
|
5122
5275
|
}
|
|
5276
|
+
/** The LAN listener is TLS-only, so every advertised endpoint is `https://`. */
|
|
5123
5277
|
function localEndpointURLs(addresses, port) {
|
|
5124
5278
|
const normalizedPort = normalizeLocalPort(port);
|
|
5125
|
-
return [...new Set(addresses)].map((address) => `
|
|
5279
|
+
return [...new Set(addresses)].map((address) => `https://${address}:${normalizedPort}`);
|
|
5126
5280
|
}
|
|
5127
5281
|
function localListenError(error, port) {
|
|
5128
5282
|
const code = error?.code;
|
|
@@ -5141,7 +5295,7 @@ const Config = z.object({
|
|
|
5141
5295
|
debug: z.boolean().default(false),
|
|
5142
5296
|
local: z.object({
|
|
5143
5297
|
enabled: z.boolean().default(true),
|
|
5144
|
-
port: z.natural().min(MIN_LOCAL_PORT).max(MAX_LOCAL_PORT).default(DEFAULT_LOCAL_PORT).description("DeepPilot
|
|
5298
|
+
port: z.natural().min(MIN_LOCAL_PORT).max(MAX_LOCAL_PORT).default(DEFAULT_LOCAL_PORT).description("DeepPilot 局域网独立端口(仅 TLS;默认 3098,修改后本地连接会短暂重连)")
|
|
5145
5299
|
}).default({
|
|
5146
5300
|
enabled: true,
|
|
5147
5301
|
port: DEFAULT_LOCAL_PORT
|
|
@@ -5430,15 +5584,15 @@ var WidgetPushScheduler = class {
|
|
|
5430
5584
|
//#endregion
|
|
5431
5585
|
//#region src/live-activity.ts
|
|
5432
5586
|
function liveActivityState(session) {
|
|
5433
|
-
const total = Math.max(0, session?.todos?.total ?? 0);
|
|
5587
|
+
const total = Math.max(0, session?.todos?.total ?? session?.todoItems?.length ?? 0);
|
|
5434
5588
|
const done = Math.min(total, Math.max(0, session?.todos?.done ?? 0));
|
|
5435
5589
|
const task = session?.todoItems?.find((item) => item.status === "in_progress") ?? session?.todoItems?.find((item) => item.status === "pending");
|
|
5436
5590
|
return {
|
|
5437
5591
|
title: Array.from(session?.title ?? "").slice(0, 100).join(""),
|
|
5438
|
-
task: Array.from(task?.content
|
|
5592
|
+
task: Array.from(session?.activity?.trim() || task?.content || "").slice(0, 160).join(""),
|
|
5439
5593
|
done,
|
|
5440
5594
|
total,
|
|
5441
|
-
phase: !session ? "unavailable" : session.pendingApproval ? "approval" : session.pendingQuestion ? "question" : session.status === "running" ? "running" : session.status === "idle" ? "ended" : "unavailable"
|
|
5595
|
+
phase: !session || total === 0 ? "unavailable" : session.pendingApproval ? "approval" : session.pendingQuestion ? "question" : session.status === "running" ? "running" : session.status === "idle" ? "ended" : "unavailable"
|
|
5442
5596
|
};
|
|
5443
5597
|
}
|
|
5444
5598
|
/** Persist terminal state before coalescing so a rapid next round cannot revive an activity. */
|
|
@@ -5506,9 +5660,13 @@ var LiveActivityPushManager = class {
|
|
|
5506
5660
|
* A deliberately narrow transport listener. Both the LAN endpoint and the
|
|
5507
5661
|
* loopback-only Funnel origin use this factory, so neither can accidentally
|
|
5508
5662
|
* inherit DSH's wider web/API route surface.
|
|
5663
|
+
*
|
|
5664
|
+
* With `tls` the listener speaks HTTPS/WSS only (the LAN posture); without
|
|
5665
|
+
* it the listener is plain HTTP, which is reserved for the loopback Funnel
|
|
5666
|
+
* origin where tailscaled terminates TLS.
|
|
5509
5667
|
*/
|
|
5510
|
-
function createPhoneServer(handlers) {
|
|
5511
|
-
const
|
|
5668
|
+
function createPhoneServer(handlers, tls) {
|
|
5669
|
+
const onRequest = (req, res) => {
|
|
5512
5670
|
const path = requestPath(req);
|
|
5513
5671
|
if (path === "/phone/health") handlers.health(req, res);
|
|
5514
5672
|
else if (path === "/phone/pair") handlers.pair(req, res);
|
|
@@ -5516,7 +5674,12 @@ function createPhoneServer(handlers) {
|
|
|
5516
5674
|
res.statusCode = 404;
|
|
5517
5675
|
res.end("not found");
|
|
5518
5676
|
}
|
|
5519
|
-
}
|
|
5677
|
+
};
|
|
5678
|
+
const server = tls === void 0 ? createServer$1(onRequest) : createServer({
|
|
5679
|
+
key: tls.key,
|
|
5680
|
+
cert: tls.cert,
|
|
5681
|
+
minVersion: "TLSv1.2"
|
|
5682
|
+
}, onRequest);
|
|
5520
5683
|
server.on("upgrade", (req, socket, head) => {
|
|
5521
5684
|
if (requestPath(req) !== "/phone") {
|
|
5522
5685
|
socket.end("HTTP/1.1 404 Not Found\r\nConnection: close\r\n\r\n");
|
|
@@ -5553,12 +5716,175 @@ function closeServer(server) {
|
|
|
5553
5716
|
return new Promise((resolve) => server.close(() => resolve()));
|
|
5554
5717
|
}
|
|
5555
5718
|
//#endregion
|
|
5719
|
+
//#region src/lan-tls.ts
|
|
5720
|
+
const LAN_TLS_COMMON_NAME = "dsh-deeppilot";
|
|
5721
|
+
const LAN_TLS_KEY_FILE = "key.pem";
|
|
5722
|
+
const LAN_TLS_CERT_FILE = "cert.pem";
|
|
5723
|
+
const CERT_VALIDITY_MS = 31536e7;
|
|
5724
|
+
/** Re-sign this far ahead of expiry so a long-running host never serves a stale leaf. */
|
|
5725
|
+
const CERT_RENEW_LEAD_MS = 2592e6;
|
|
5726
|
+
/** Compute the pinned public-key fingerprint of a PEM certificate. */
|
|
5727
|
+
function spkiFingerprint(certPem) {
|
|
5728
|
+
const der = createPublicKey(certPem).export({
|
|
5729
|
+
type: "spki",
|
|
5730
|
+
format: "der"
|
|
5731
|
+
});
|
|
5732
|
+
return "sha256:" + createHash("sha256").update(der).digest("base64url");
|
|
5733
|
+
}
|
|
5734
|
+
/**
|
|
5735
|
+
* Load the LAN listener's TLS identity from `dir`, creating or repairing it
|
|
5736
|
+
* as needed. The private key is the durable identity: it is created once and
|
|
5737
|
+
* reused for every subsequent certificate, so pinned devices survive
|
|
5738
|
+
* certificate renewals. Only a missing or unreadable key forces a new one.
|
|
5739
|
+
*/
|
|
5740
|
+
async function loadOrCreateLanTlsIdentity(dir, now = Date.now()) {
|
|
5741
|
+
await mkdir(dir, {
|
|
5742
|
+
recursive: true,
|
|
5743
|
+
mode: 448
|
|
5744
|
+
});
|
|
5745
|
+
const keyPath = join(dir, LAN_TLS_KEY_FILE);
|
|
5746
|
+
const certPath = join(dir, LAN_TLS_CERT_FILE);
|
|
5747
|
+
let key = await readPem(keyPath);
|
|
5748
|
+
let regenerated = false;
|
|
5749
|
+
if (key === void 0 || !validPrivateKey(key)) {
|
|
5750
|
+
regenerated = true;
|
|
5751
|
+
key = void 0;
|
|
5752
|
+
}
|
|
5753
|
+
let cert = regenerated ? void 0 : await readPem(certPath);
|
|
5754
|
+
let resigned = false;
|
|
5755
|
+
const existing = key !== void 0 && cert !== void 0 ? inspectCertificate(cert, key, now) : void 0;
|
|
5756
|
+
if (existing === void 0) {
|
|
5757
|
+
const issued = await issueCertificate(key, now);
|
|
5758
|
+
if (key === void 0) {
|
|
5759
|
+
key = pemFile(issued.key);
|
|
5760
|
+
await writePrivate(keyPath, key);
|
|
5761
|
+
} else resigned = true;
|
|
5762
|
+
cert = pemFile(issued.cert);
|
|
5763
|
+
await writePrivate(certPath, cert);
|
|
5764
|
+
}
|
|
5765
|
+
if (key === void 0 || cert === void 0) throw new Error("LAN TLS identity unavailable");
|
|
5766
|
+
return {
|
|
5767
|
+
key,
|
|
5768
|
+
cert,
|
|
5769
|
+
fingerprint: spkiFingerprint(cert),
|
|
5770
|
+
notAfter: existing ?? new X509Certificate(cert).validToDate.getTime(),
|
|
5771
|
+
regenerated,
|
|
5772
|
+
resigned
|
|
5773
|
+
};
|
|
5774
|
+
}
|
|
5775
|
+
async function readPem(path) {
|
|
5776
|
+
try {
|
|
5777
|
+
const text = await readFile(path, "utf8");
|
|
5778
|
+
return text.trim().length > 0 ? text : void 0;
|
|
5779
|
+
} catch (error) {
|
|
5780
|
+
if (error.code === "ENOENT") return void 0;
|
|
5781
|
+
return;
|
|
5782
|
+
}
|
|
5783
|
+
}
|
|
5784
|
+
function validPrivateKey(pem) {
|
|
5785
|
+
try {
|
|
5786
|
+
return createPrivateKey(pem).asymmetricKeyType === "ec";
|
|
5787
|
+
} catch {
|
|
5788
|
+
return false;
|
|
5789
|
+
}
|
|
5790
|
+
}
|
|
5791
|
+
/**
|
|
5792
|
+
* Return the certificate's expiry when it is usable with `keyPem`, or
|
|
5793
|
+
* `undefined` when it must be re-issued (malformed, wrong key, or expiring).
|
|
5794
|
+
*/
|
|
5795
|
+
function inspectCertificate(certPem, keyPem, now) {
|
|
5796
|
+
try {
|
|
5797
|
+
const cert = new X509Certificate(certPem);
|
|
5798
|
+
const certSpki = cert.publicKey.export({
|
|
5799
|
+
type: "spki",
|
|
5800
|
+
format: "der"
|
|
5801
|
+
});
|
|
5802
|
+
const keySpki = createPublicKey(createPrivateKey(keyPem)).export({
|
|
5803
|
+
type: "spki",
|
|
5804
|
+
format: "der"
|
|
5805
|
+
});
|
|
5806
|
+
if (!certSpki.equals(keySpki)) return void 0;
|
|
5807
|
+
const notAfter = cert.validToDate.getTime();
|
|
5808
|
+
if (!(cert.validFromDate.getTime() <= now && notAfter - CERT_RENEW_LEAD_MS > now)) return void 0;
|
|
5809
|
+
return notAfter;
|
|
5810
|
+
} catch {
|
|
5811
|
+
return;
|
|
5812
|
+
}
|
|
5813
|
+
}
|
|
5814
|
+
async function issueCertificate(existingKey, now) {
|
|
5815
|
+
const notBeforeDate = /* @__PURE__ */ new Date(now - 3e5);
|
|
5816
|
+
const notAfterDate = new Date(now + CERT_VALIDITY_MS);
|
|
5817
|
+
const keyPair = existingKey === void 0 ? void 0 : {
|
|
5818
|
+
privateKey: createPrivateKey(existingKey).export({
|
|
5819
|
+
type: "pkcs8",
|
|
5820
|
+
format: "pem"
|
|
5821
|
+
}),
|
|
5822
|
+
publicKey: createPublicKey(existingKey).export({
|
|
5823
|
+
type: "spki",
|
|
5824
|
+
format: "pem"
|
|
5825
|
+
})
|
|
5826
|
+
};
|
|
5827
|
+
const result = await generate([{
|
|
5828
|
+
name: "commonName",
|
|
5829
|
+
value: LAN_TLS_COMMON_NAME
|
|
5830
|
+
}], {
|
|
5831
|
+
keyType: "ec",
|
|
5832
|
+
curve: "P-256",
|
|
5833
|
+
algorithm: "sha256",
|
|
5834
|
+
notBeforeDate,
|
|
5835
|
+
notAfterDate,
|
|
5836
|
+
...keyPair ? { keyPair } : {},
|
|
5837
|
+
extensions: [
|
|
5838
|
+
{
|
|
5839
|
+
name: "basicConstraints",
|
|
5840
|
+
cA: false,
|
|
5841
|
+
critical: true
|
|
5842
|
+
},
|
|
5843
|
+
{
|
|
5844
|
+
name: "keyUsage",
|
|
5845
|
+
digitalSignature: true,
|
|
5846
|
+
keyAgreement: true,
|
|
5847
|
+
critical: true
|
|
5848
|
+
},
|
|
5849
|
+
{
|
|
5850
|
+
name: "extKeyUsage",
|
|
5851
|
+
serverAuth: true
|
|
5852
|
+
},
|
|
5853
|
+
{
|
|
5854
|
+
name: "subjectAltName",
|
|
5855
|
+
altNames: [{
|
|
5856
|
+
type: 2,
|
|
5857
|
+
value: LAN_TLS_COMMON_NAME
|
|
5858
|
+
}]
|
|
5859
|
+
}
|
|
5860
|
+
]
|
|
5861
|
+
});
|
|
5862
|
+
return {
|
|
5863
|
+
key: existingKey ?? result.private,
|
|
5864
|
+
cert: result.cert
|
|
5865
|
+
};
|
|
5866
|
+
}
|
|
5867
|
+
/** Exact on-disk form, so a freshly issued value equals its later reload. */
|
|
5868
|
+
function pemFile(pem) {
|
|
5869
|
+
return pem.replace(/\r\n/g, "\n").trimEnd() + "\n";
|
|
5870
|
+
}
|
|
5871
|
+
async function writePrivate(path, contents) {
|
|
5872
|
+
const tmp = `${path}.${process.pid}.tmp`;
|
|
5873
|
+
try {
|
|
5874
|
+
await writeFile(tmp, contents, { mode: 384 });
|
|
5875
|
+
await rename(tmp, path);
|
|
5876
|
+
} catch (error) {
|
|
5877
|
+
await unlink(tmp).catch(() => {});
|
|
5878
|
+
throw error;
|
|
5879
|
+
}
|
|
5880
|
+
}
|
|
5881
|
+
//#endregion
|
|
5556
5882
|
//#region src/index.ts
|
|
5557
5883
|
/**
|
|
5558
5884
|
* dsh-deeppilot — data bridge between the DSH host and DeepPilot
|
|
5559
|
-
* clients. Owns independent, narrowly routed LAN
|
|
5560
|
-
*
|
|
5561
|
-
*
|
|
5885
|
+
* clients. Owns independent, narrowly routed LAN (TLS-only, pinned by paired
|
|
5886
|
+
* devices) and loopback Funnel-origin listeners. The web UI and the rest of
|
|
5887
|
+
* DSH's API are never exposed by these listeners.
|
|
5562
5888
|
*
|
|
5563
5889
|
* Data plane: an in-process HostBridge consumes a local compatibility façade
|
|
5564
5890
|
* over DSH 0.1.2 Session/Workspace controllers, mirrors session summaries,
|
|
@@ -5569,7 +5895,7 @@ function closeServer(server) {
|
|
|
5569
5895
|
* Swift models mirror that v2 contract.
|
|
5570
5896
|
*/
|
|
5571
5897
|
const name = "deeppilot";
|
|
5572
|
-
/** No
|
|
5898
|
+
/** No web-service requirement: the plugin owns its own transport listeners. */
|
|
5573
5899
|
const inject = [];
|
|
5574
5900
|
const SERVER_VERSION = readOwnPackageVersion();
|
|
5575
5901
|
const MAX_CLIENT_CONNECTIONS = 16;
|
|
@@ -6100,9 +6426,27 @@ function apply(ctx, options) {
|
|
|
6100
6426
|
endpoints: [],
|
|
6101
6427
|
updatedAt: Date.now()
|
|
6102
6428
|
};
|
|
6429
|
+
let lanTlsIdentity;
|
|
6430
|
+
let tlsIdentityRegenerated = false;
|
|
6431
|
+
const loadLanTls = () => {
|
|
6432
|
+
lanTlsIdentity ??= (async () => {
|
|
6433
|
+
await ready;
|
|
6434
|
+
const identity = await loadOrCreateLanTlsIdentity(join(dataDir, "lan-tls"));
|
|
6435
|
+
if (identity.regenerated) {
|
|
6436
|
+
tlsIdentityRegenerated = true;
|
|
6437
|
+
log(`LAN TLS identity created: fingerprint=${identity.fingerprint}; previously paired LAN devices must pair again`);
|
|
6438
|
+
} else if (identity.resigned) log(`LAN TLS certificate renewed (fingerprint unchanged: ${identity.fingerprint})`);
|
|
6439
|
+
return identity;
|
|
6440
|
+
})();
|
|
6441
|
+
lanTlsIdentity.catch(() => {
|
|
6442
|
+
lanTlsIdentity = void 0;
|
|
6443
|
+
});
|
|
6444
|
+
return lanTlsIdentity;
|
|
6445
|
+
};
|
|
6103
6446
|
const localStatus = (addresses) => ({
|
|
6104
6447
|
...localState,
|
|
6105
|
-
endpoints: localState.phase === "online" ? localEndpointURLs(addresses, localState.port) : []
|
|
6448
|
+
endpoints: localState.phase === "online" ? localEndpointURLs(addresses, localState.port) : [],
|
|
6449
|
+
...tlsIdentityRegenerated ? { tlsIdentityRegenerated: true } : {}
|
|
6106
6450
|
});
|
|
6107
6451
|
const remoteStatus = () => remoteSupervisor?.status() ?? {
|
|
6108
6452
|
provider: "tailscale-funnel",
|
|
@@ -6302,13 +6646,15 @@ function apply(ctx, options) {
|
|
|
6302
6646
|
}, Date.now());
|
|
6303
6647
|
authRateLimiter.recordSuccess(source);
|
|
6304
6648
|
log(`device paired id=${auditLabel(record.deviceId)} source=${auditLabel(source)}`);
|
|
6649
|
+
const tlsFingerprint = req.socket.encrypted === true ? (await lanTlsIdentity)?.fingerprint : void 0;
|
|
6305
6650
|
res.statusCode = 201;
|
|
6306
6651
|
res.end(JSON.stringify({
|
|
6307
6652
|
ok: true,
|
|
6308
6653
|
v: 2,
|
|
6309
6654
|
deviceId: record.deviceId,
|
|
6310
6655
|
audience,
|
|
6311
|
-
scopes: record.scopes ?? []
|
|
6656
|
+
scopes: record.scopes ?? [],
|
|
6657
|
+
...tlsFingerprint ? { tlsFingerprint } : {}
|
|
6312
6658
|
}));
|
|
6313
6659
|
} catch (error) {
|
|
6314
6660
|
res.statusCode = error instanceof SyntaxError || error instanceof TypeError ? 400 : 503;
|
|
@@ -6461,9 +6807,15 @@ function apply(ctx, options) {
|
|
|
6461
6807
|
endpoints: [],
|
|
6462
6808
|
updatedAt: Date.now()
|
|
6463
6809
|
};
|
|
6464
|
-
|
|
6465
|
-
localServer = server;
|
|
6810
|
+
let server;
|
|
6466
6811
|
try {
|
|
6812
|
+
const tls = await loadLanTls();
|
|
6813
|
+
if (localDisposed || appliedLocalKey !== nextKey) return;
|
|
6814
|
+
server = createPhoneServer(phoneHandlers, {
|
|
6815
|
+
key: tls.key,
|
|
6816
|
+
cert: tls.cert
|
|
6817
|
+
});
|
|
6818
|
+
localServer = server;
|
|
6467
6819
|
await listen(server, next.port, "0.0.0.0");
|
|
6468
6820
|
if (localDisposed || localServer !== server) {
|
|
6469
6821
|
await closeServer(server);
|
|
@@ -6473,12 +6825,15 @@ function apply(ctx, options) {
|
|
|
6473
6825
|
phase: "online",
|
|
6474
6826
|
port: next.port,
|
|
6475
6827
|
endpoints: [],
|
|
6828
|
+
tlsFingerprint: tls.fingerprint,
|
|
6476
6829
|
updatedAt: Date.now()
|
|
6477
6830
|
};
|
|
6478
|
-
log(`local transport listening on 0.0.0.0:${next.port}`);
|
|
6831
|
+
log(`local transport listening on https://0.0.0.0:${next.port} (tls fingerprint ${tls.fingerprint})`);
|
|
6479
6832
|
} catch (error) {
|
|
6480
|
-
if (
|
|
6481
|
-
|
|
6833
|
+
if (server !== void 0) {
|
|
6834
|
+
if (localServer === server) localServer = void 0;
|
|
6835
|
+
await closeServer(server);
|
|
6836
|
+
}
|
|
6482
6837
|
const message = localListenError(error, next.port);
|
|
6483
6838
|
localState = {
|
|
6484
6839
|
phase: "error",
|
|
@@ -6611,30 +6966,6 @@ function apply(ctx, options) {
|
|
|
6611
6966
|
bridge.dispose();
|
|
6612
6967
|
}, "deeppilot: host streams");
|
|
6613
6968
|
});
|
|
6614
|
-
ctx.inject(["webServer"], (sub) => {
|
|
6615
|
-
const webCtx = sub;
|
|
6616
|
-
const web = webCtx.webServer;
|
|
6617
|
-
if (!web) {
|
|
6618
|
-
log("webServer service absent; 3080 compatibility routes unavailable (independent transports remain active)");
|
|
6619
|
-
return;
|
|
6620
|
-
}
|
|
6621
|
-
webCtx.effect(() => web.registerUpgrade({
|
|
6622
|
-
path: "/phone",
|
|
6623
|
-
handler: handleUpgrade
|
|
6624
|
-
}), "deeppilot: /phone WebSocket");
|
|
6625
|
-
webCtx.effect(() => web.register({
|
|
6626
|
-
kind: "exact",
|
|
6627
|
-
path: "/phone/health",
|
|
6628
|
-
handler: handleHealth
|
|
6629
|
-
}), "deeppilot: /phone/health");
|
|
6630
|
-
webCtx.effect(() => web.register({
|
|
6631
|
-
kind: "exact",
|
|
6632
|
-
path: "/phone/pair",
|
|
6633
|
-
handler: handlePair
|
|
6634
|
-
}), "deeppilot: /phone/pair");
|
|
6635
|
-
if (enabledNow()) log("/phone compatibility routes registered on DSH web server");
|
|
6636
|
-
else log("bridge disabled; /phone refuses connections until re-enabled");
|
|
6637
|
-
});
|
|
6638
6969
|
ctx.effect(() => async () => {
|
|
6639
6970
|
closeAllConnections();
|
|
6640
6971
|
const bridge = state.bridge;
|