dsh-deeppilot 0.6.0-alpha.2 → 0.6.0-alpha.3
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 +10 -6
- package/README.md +15 -10
- package/README.zh-CN.md +12 -8
- 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 +4 -1
- package/lib/client.js.map +1 -1
- package/lib/index.d.ts +12 -3
- package/lib/index.js +316 -72
- package/lib/index.js.map +1 -1
- package/package.json +22 -11
package/lib/index.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import z from "@deepseek-ai/schemastery";
|
|
2
1
|
import { Context } from "@deepseek-ai/cordis";
|
|
2
|
+
import z from "@deepseek-ai/schemastery";
|
|
3
3
|
//#region src/protocol.d.ts
|
|
4
4
|
type SessionStatus = "running" | "idle" | "error" | "unknown";
|
|
5
5
|
type SessionTodoStatus = "pending" | "in_progress" | "completed";
|
|
@@ -40,12 +40,13 @@ interface MessageContextInfo {
|
|
|
40
40
|
/** One image carried by a user message. `attachmentId` keys the read-back RPC
|
|
41
41
|
* (c2s.session.attachment); width/height let clients reserve layout space. */
|
|
42
42
|
interface MessageAttachment {
|
|
43
|
-
kind: 'image';
|
|
43
|
+
kind: 'image' | 'document';
|
|
44
44
|
name?: string;
|
|
45
45
|
mediaType?: string;
|
|
46
46
|
attachmentId?: string;
|
|
47
47
|
width?: number;
|
|
48
48
|
height?: number;
|
|
49
|
+
truncated?: boolean;
|
|
49
50
|
}
|
|
50
51
|
interface MessageProjection {
|
|
51
52
|
/** Durable row identity; unique within one session/page. */
|
|
@@ -402,6 +403,14 @@ interface PushOutlet {
|
|
|
402
403
|
isAvailable(): boolean;
|
|
403
404
|
}
|
|
404
405
|
//#endregion
|
|
406
|
+
//#region src/document-payload.d.ts
|
|
407
|
+
interface PromptDocument {
|
|
408
|
+
name: string;
|
|
409
|
+
mediaType: string;
|
|
410
|
+
text: string;
|
|
411
|
+
truncated?: boolean;
|
|
412
|
+
}
|
|
413
|
+
//#endregion
|
|
405
414
|
//#region src/host-bridge.d.ts
|
|
406
415
|
declare class HostBridge {
|
|
407
416
|
private readonly apiProxy;
|
|
@@ -537,7 +546,7 @@ declare class HostBridge {
|
|
|
537
546
|
mediaType: 'image/png' | 'image/jpeg' | 'image/webp' | 'image/gif';
|
|
538
547
|
data: string;
|
|
539
548
|
name?: string;
|
|
540
|
-
}
|
|
549
|
+
}>, documents?: PromptDocument[]): Promise<SessionManagementResult<number>>;
|
|
541
550
|
respondApproval(requestId: string, decision: 'allow' | 'deny', reason?: string): Promise<PendingResponseOutcome>;
|
|
542
551
|
respondQuestion(requestId: string, answers: unknown): Promise<PendingResponseOutcome>;
|
|
543
552
|
}
|
package/lib/index.js
CHANGED
|
@@ -5,6 +5,8 @@ import { access, chmod, mkdir, readFile, rename, unlink, writeFile } from "node:
|
|
|
5
5
|
import { dirname, join, resolve } from "node:path";
|
|
6
6
|
import { WebSocketServer } from "ws";
|
|
7
7
|
import { homedir, networkInterfaces } from "node:os";
|
|
8
|
+
import * as Cordis from "@deepseek-ai/cordis";
|
|
9
|
+
import { Context } from "@deepseek-ai/cordis";
|
|
8
10
|
import { TypertRemoteService } from "@deepseek-ai/dsh-typert-protocol";
|
|
9
11
|
import { connect } from "node:http2";
|
|
10
12
|
import { spawn } from "node:child_process";
|
|
@@ -339,6 +341,9 @@ function requiredScope(type) {
|
|
|
339
341
|
function sanitizeImageName(value) {
|
|
340
342
|
return value.replace(/[\u0000-\u001F\u007F]/g, "").trim().slice(0, 120);
|
|
341
343
|
}
|
|
344
|
+
function sanitizeDocumentField(value, maxChars) {
|
|
345
|
+
return value.replace(/[\u0000-\u001F\u007F]/g, " ").trim().slice(0, maxChars);
|
|
346
|
+
}
|
|
342
347
|
/** Error code for a failed approval/question response outcome. */
|
|
343
348
|
function pendingResponseErrorCode(reason) {
|
|
344
349
|
switch (reason) {
|
|
@@ -724,9 +729,11 @@ var BridgeConnection = class {
|
|
|
724
729
|
const p = env.payload;
|
|
725
730
|
const text = typeof p?.text === "string" ? p.text : "";
|
|
726
731
|
const rawImages = Array.isArray(p?.images) ? p.images : [];
|
|
727
|
-
|
|
732
|
+
const rawDocuments = Array.isArray(p?.documents) ? p.documents : [];
|
|
733
|
+
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
734
|
if (text.length > 262144) return this.fail(env.id, "E_PROTOCOL", "prompt text too long");
|
|
729
735
|
if (rawImages.length > 4) return this.fail(env.id, "E_PROTOCOL", "too many images");
|
|
736
|
+
if (rawDocuments.length > 4 || rawImages.length + rawDocuments.length > 4) return this.fail(env.id, "E_PROTOCOL", "too many prompt attachments");
|
|
730
737
|
const images = [];
|
|
731
738
|
for (const image of rawImages) {
|
|
732
739
|
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 +743,20 @@ var BridgeConnection = class {
|
|
|
736
743
|
...typeof image.name === "string" && sanitizeImageName(image.name).length > 0 ? { name: sanitizeImageName(image.name) } : {}
|
|
737
744
|
});
|
|
738
745
|
}
|
|
739
|
-
const
|
|
746
|
+
const documents = [];
|
|
747
|
+
for (const document of rawDocuments) {
|
|
748
|
+
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");
|
|
749
|
+
const name = sanitizeDocumentField(document.name, 180);
|
|
750
|
+
const mediaType = sanitizeDocumentField(document.mediaType, 120).toLowerCase();
|
|
751
|
+
if (!name || !mediaType || mediaType.startsWith("image/")) return this.fail(env.id, "E_PROTOCOL", "invalid document attachment");
|
|
752
|
+
documents.push({
|
|
753
|
+
name,
|
|
754
|
+
mediaType,
|
|
755
|
+
text: document.text,
|
|
756
|
+
...document.truncated === true ? { truncated: true } : {}
|
|
757
|
+
});
|
|
758
|
+
}
|
|
759
|
+
const userSeq = await this.deps.bridge.sendPrompt(p.sessionId, text, images, documents);
|
|
740
760
|
if (!userSeq.ok) return this.fail(env.id, managementErrorCode(userSeq.kind), userSeq.message);
|
|
741
761
|
this.send("s2c.ack", { userSeq: userSeq.value }, env.id);
|
|
742
762
|
return;
|
|
@@ -855,6 +875,44 @@ function unwrapStreamItem(item) {
|
|
|
855
875
|
return item;
|
|
856
876
|
}
|
|
857
877
|
//#endregion
|
|
878
|
+
//#region src/document-payload.ts
|
|
879
|
+
const PREFIX = "[DeepPilot document:";
|
|
880
|
+
const SUFFIX = "]";
|
|
881
|
+
/**
|
|
882
|
+
* DSH 0.1.2 has durable image attachments but no generic file content block.
|
|
883
|
+
* Documents therefore travel as a bounded, explicitly-labelled text block.
|
|
884
|
+
* The marker lets the canonical phone row recover an attachment chip without
|
|
885
|
+
* exposing the complete document body as ordinary user-authored bubble text.
|
|
886
|
+
*/
|
|
887
|
+
function documentPromptBlock(document) {
|
|
888
|
+
const marker = {
|
|
889
|
+
v: 1,
|
|
890
|
+
name: document.name,
|
|
891
|
+
mediaType: document.mediaType,
|
|
892
|
+
...document.truncated ? { truncated: true } : {}
|
|
893
|
+
};
|
|
894
|
+
const encoded = Buffer.from(JSON.stringify(marker), "utf8").toString("base64url");
|
|
895
|
+
return `${PREFIX}${encoded}${SUFFIX}\nAttached document: ${document.name}\n\n${document.text}`;
|
|
896
|
+
}
|
|
897
|
+
function projectedDocument(text) {
|
|
898
|
+
if (!text.startsWith(PREFIX)) return void 0;
|
|
899
|
+
const end = text.indexOf(SUFFIX, 20);
|
|
900
|
+
if (end < 0) return void 0;
|
|
901
|
+
try {
|
|
902
|
+
const decoded = JSON.parse(Buffer.from(text.slice(20, end), "base64url").toString("utf8"));
|
|
903
|
+
if (decoded.v !== 1 || typeof decoded.name !== "string" || typeof decoded.mediaType !== "string") return void 0;
|
|
904
|
+
if (!decoded.name.trim() || !decoded.mediaType.trim()) return void 0;
|
|
905
|
+
return {
|
|
906
|
+
name: decoded.name,
|
|
907
|
+
mediaType: decoded.mediaType,
|
|
908
|
+
text: text.slice(end + 1).replace(/^\nAttached document:[^\n]*\n\n/, ""),
|
|
909
|
+
...decoded.truncated === true ? { truncated: true } : {}
|
|
910
|
+
};
|
|
911
|
+
} catch {
|
|
912
|
+
return;
|
|
913
|
+
}
|
|
914
|
+
}
|
|
915
|
+
//#endregion
|
|
858
916
|
//#region src/host-event-projection.ts
|
|
859
917
|
const MAX_MESSAGE_PROJECTION_BYTES = 262144;
|
|
860
918
|
/** One durable host event sequence becomes exactly one phone message row.
|
|
@@ -1049,7 +1107,7 @@ function contentText(content) {
|
|
|
1049
1107
|
if (typeof part === "string") return part;
|
|
1050
1108
|
if (part && typeof part === "object") {
|
|
1051
1109
|
const piece = part;
|
|
1052
|
-
if (piece.type === "text" && typeof piece.text === "string") return piece.text;
|
|
1110
|
+
if (piece.type === "text" && typeof piece.text === "string") return projectedDocument(piece.text) ? "" : piece.text;
|
|
1053
1111
|
}
|
|
1054
1112
|
return "";
|
|
1055
1113
|
}).join("");
|
|
@@ -1060,22 +1118,34 @@ function messageAttachments(data) {
|
|
|
1060
1118
|
const obj = data;
|
|
1061
1119
|
if (obj.message && typeof obj.message === "object") return messageAttachments(obj.message);
|
|
1062
1120
|
if (!Array.isArray(obj.content)) return [];
|
|
1063
|
-
|
|
1064
|
-
|
|
1121
|
+
const attachments = [];
|
|
1122
|
+
for (const part of obj.content) {
|
|
1123
|
+
if (!part || typeof part !== "object") continue;
|
|
1065
1124
|
const block = part;
|
|
1066
|
-
if (block.type
|
|
1125
|
+
if (block.type === "text" && typeof block.text === "string") {
|
|
1126
|
+
const document = projectedDocument(block.text);
|
|
1127
|
+
if (document) attachments.push({
|
|
1128
|
+
kind: "document",
|
|
1129
|
+
name: document.name,
|
|
1130
|
+
mediaType: document.mediaType,
|
|
1131
|
+
...document.truncated ? { truncated: true } : {}
|
|
1132
|
+
});
|
|
1133
|
+
continue;
|
|
1134
|
+
}
|
|
1135
|
+
if (block.type !== "image" || !block.attachment) continue;
|
|
1067
1136
|
const attachmentId = typeof block.attachment.attachmentId === "string" && block.attachment.attachmentId.length > 0 ? block.attachment.attachmentId : void 0;
|
|
1068
1137
|
const width = typeof block.attachment.width === "number" && Number.isFinite(block.attachment.width) ? block.attachment.width : void 0;
|
|
1069
1138
|
const height = typeof block.attachment.height === "number" && Number.isFinite(block.attachment.height) ? block.attachment.height : void 0;
|
|
1070
|
-
|
|
1139
|
+
attachments.push({
|
|
1071
1140
|
kind: "image",
|
|
1072
1141
|
...typeof block.attachment.name === "string" ? { name: block.attachment.name } : {},
|
|
1073
1142
|
...typeof block.attachment.mediaType === "string" ? { mediaType: block.attachment.mediaType } : {},
|
|
1074
1143
|
...attachmentId ? { attachmentId } : {},
|
|
1075
1144
|
...width !== void 0 ? { width } : {},
|
|
1076
1145
|
...height !== void 0 ? { height } : {}
|
|
1077
|
-
}
|
|
1078
|
-
}
|
|
1146
|
+
});
|
|
1147
|
+
}
|
|
1148
|
+
return attachments;
|
|
1079
1149
|
}
|
|
1080
1150
|
function attachmentProjection(data) {
|
|
1081
1151
|
const attachments = messageAttachments(data);
|
|
@@ -2139,7 +2209,7 @@ var HostBridge = class {
|
|
|
2139
2209
|
return null;
|
|
2140
2210
|
}
|
|
2141
2211
|
}
|
|
2142
|
-
async sendPrompt(sessionId, text, images = []) {
|
|
2212
|
+
async sendPrompt(sessionId, text, images = [], documents = []) {
|
|
2143
2213
|
try {
|
|
2144
2214
|
const content = [];
|
|
2145
2215
|
if (text.trim().length > 0) content.push({
|
|
@@ -2150,6 +2220,10 @@ var HostBridge = class {
|
|
|
2150
2220
|
type: "image",
|
|
2151
2221
|
...image
|
|
2152
2222
|
});
|
|
2223
|
+
for (const document of documents) content.push({
|
|
2224
|
+
type: "text",
|
|
2225
|
+
text: documentPromptBlock(document)
|
|
2226
|
+
});
|
|
2153
2227
|
const response = await this.apiProxy.sessions.prompt({
|
|
2154
2228
|
rpcId: randomUUID(),
|
|
2155
2229
|
payload: {
|
|
@@ -2476,16 +2550,150 @@ function projectSessionModels(value) {
|
|
|
2476
2550
|
}
|
|
2477
2551
|
/** Project a history page (raw events) into MessageProjection rows. */
|
|
2478
2552
|
//#endregion
|
|
2553
|
+
//#region src/dsh012-remote-interactions.ts
|
|
2554
|
+
/**
|
|
2555
|
+
* Resident DSH 0.1.2-alpha.3 Remote Events client for phone interactions.
|
|
2556
|
+
*
|
|
2557
|
+
* The Host-side `approval/request` and `user-questions/request` events are
|
|
2558
|
+
* owned once by DSH API Remotes. API Gateway then fans each pending waterfall
|
|
2559
|
+
* out to every connected Client and settles the first result. DeepPilot joins
|
|
2560
|
+
* that official Client plane in-process instead of registering a competing
|
|
2561
|
+
* Host waterfall listener or replacing the official Web composer.
|
|
2562
|
+
*/
|
|
2563
|
+
let clientFaces;
|
|
2564
|
+
/**
|
|
2565
|
+
* Load DSH's browser-distributed Client faces into a tiny Host-side module
|
|
2566
|
+
* table. Published `lib/client.js` files register with `window.__ModuleLoader__`
|
|
2567
|
+
* rather than exporting ESM values, so a normal Node import cannot consume
|
|
2568
|
+
* them. The shim runs only while the three official bundles register; their
|
|
2569
|
+
* apply functions then operate against a non-browser Cordis Context.
|
|
2570
|
+
*/
|
|
2571
|
+
function pluginModule(value, specifier) {
|
|
2572
|
+
if (typeof value !== "object" || value === null) throw new Error(`${specifier} Client bundle returned no exports`);
|
|
2573
|
+
if (typeof value.apply !== "function") throw new Error(`${specifier} has no Client apply() export`);
|
|
2574
|
+
return value;
|
|
2575
|
+
}
|
|
2576
|
+
async function loadClientFaces() {
|
|
2577
|
+
clientFaces ??= (async () => {
|
|
2578
|
+
const modules = /* @__PURE__ */ new Map([["@deepseek-ai/cordis", Cordis]]);
|
|
2579
|
+
const loaderGlobal = globalThis;
|
|
2580
|
+
const previousWindow = loaderGlobal.window;
|
|
2581
|
+
loaderGlobal.window = { __ModuleLoader__: { load: (definition) => {
|
|
2582
|
+
const exports = definition.factory((id) => {
|
|
2583
|
+
if (!modules.has(id)) throw new Error(`resident DSH Client cannot resolve ${JSON.stringify(id)}`);
|
|
2584
|
+
return modules.get(id);
|
|
2585
|
+
});
|
|
2586
|
+
modules.set(definition.id, exports);
|
|
2587
|
+
} } };
|
|
2588
|
+
try {
|
|
2589
|
+
for (const specifier of [
|
|
2590
|
+
"@deepseek-ai/dsh-typert-registry/client",
|
|
2591
|
+
"@deepseek-ai/dsh-client-connection/client",
|
|
2592
|
+
"@deepseek-ai/dsh-api-gateway/client"
|
|
2593
|
+
]) await import(specifier);
|
|
2594
|
+
} finally {
|
|
2595
|
+
if (previousWindow === void 0) delete loaderGlobal.window;
|
|
2596
|
+
else loaderGlobal.window = previousWindow;
|
|
2597
|
+
}
|
|
2598
|
+
return {
|
|
2599
|
+
typert: pluginModule(modules.get("@deepseek-ai/dsh-typert-registry"), "@deepseek-ai/dsh-typert-registry/client"),
|
|
2600
|
+
connection: pluginModule(modules.get("@deepseek-ai/dsh-client-connection"), "@deepseek-ai/dsh-client-connection/client"),
|
|
2601
|
+
gateway: pluginModule(modules.get("@deepseek-ai/dsh-api-gateway"), "@deepseek-ai/dsh-api-gateway/client")
|
|
2602
|
+
};
|
|
2603
|
+
})();
|
|
2604
|
+
return clientFaces;
|
|
2605
|
+
}
|
|
2606
|
+
const agentScopeKey = Symbol("deeppilot.dsh-client.agent-scope");
|
|
2607
|
+
function scopeOf(ctx) {
|
|
2608
|
+
return ctx[agentScopeKey];
|
|
2609
|
+
}
|
|
2610
|
+
function createAgentScope(ctx, identity) {
|
|
2611
|
+
return ctx.plugin(function deeppilotAgentScope() {}).ctx.extend({
|
|
2612
|
+
[agentScopeKey]: identity,
|
|
2613
|
+
[Context.filter](listenerCtx) {
|
|
2614
|
+
const listenerIdentity = scopeOf(listenerCtx);
|
|
2615
|
+
return listenerIdentity === void 0 || listenerIdentity === identity;
|
|
2616
|
+
}
|
|
2617
|
+
});
|
|
2618
|
+
}
|
|
2619
|
+
function inProcessStream(gateway, endpoint, payload, signal) {
|
|
2620
|
+
return (async function* () {
|
|
2621
|
+
yield* await gateway.wireStream.open(endpoint, payload, signal);
|
|
2622
|
+
})();
|
|
2623
|
+
}
|
|
2624
|
+
/**
|
|
2625
|
+
* Start one headless official DSH Client backed by the Host's in-process
|
|
2626
|
+
* Connection and Gateway carriers.
|
|
2627
|
+
*
|
|
2628
|
+
* The returned disposer tears down the Remote Events generation, all pending
|
|
2629
|
+
* listeners, and every lazily minted Agent scope.
|
|
2630
|
+
*/
|
|
2631
|
+
async function startDsh012RemoteInteractions(hostCtx, handlers) {
|
|
2632
|
+
const connection = hostCtx.get("connection");
|
|
2633
|
+
const gateway = hostCtx.get("typertGateway");
|
|
2634
|
+
if (connection === void 0) throw new Error("dsh 0.1.2-alpha.3 Host connection is unavailable");
|
|
2635
|
+
if (gateway === void 0) throw new Error("dsh 0.1.2-alpha.3 typertGateway is unavailable");
|
|
2636
|
+
const faces = await loadClientFaces();
|
|
2637
|
+
const client = new Context();
|
|
2638
|
+
const fetchHandler = connection.createSharedFetchHandler("/api");
|
|
2639
|
+
const transport = {
|
|
2640
|
+
fetch: (input, init) => fetchHandler.fetch(new Request(input, init)),
|
|
2641
|
+
openStream: (endpoint, payload, signal) => inProcessStream(gateway, endpoint, payload, signal),
|
|
2642
|
+
ownsHost: true
|
|
2643
|
+
};
|
|
2644
|
+
try {
|
|
2645
|
+
faces.typert.apply(client);
|
|
2646
|
+
const transportGlobal = globalThis;
|
|
2647
|
+
const previousTransport = transportGlobal.__DSH_TRANSPORT__;
|
|
2648
|
+
transportGlobal.__DSH_TRANSPORT__ = transport;
|
|
2649
|
+
try {
|
|
2650
|
+
faces.connection.apply(client);
|
|
2651
|
+
} finally {
|
|
2652
|
+
if (previousTransport === void 0) delete transportGlobal.__DSH_TRANSPORT__;
|
|
2653
|
+
else transportGlobal.__DSH_TRANSPORT__ = previousTransport;
|
|
2654
|
+
}
|
|
2655
|
+
const typert = client.get("typert");
|
|
2656
|
+
if (typert === void 0) throw new Error("resident DSH Client typert service was not installed");
|
|
2657
|
+
const scopes = /* @__PURE__ */ new Map();
|
|
2658
|
+
typert.contexts.registerClient("agent", {
|
|
2659
|
+
identity: (candidate) => scopeOf(candidate),
|
|
2660
|
+
resolve: (identity) => {
|
|
2661
|
+
let scope = scopes.get(identity);
|
|
2662
|
+
if (scope === void 0) {
|
|
2663
|
+
scope = createAgentScope(client, identity);
|
|
2664
|
+
scopes.set(identity, scope);
|
|
2665
|
+
}
|
|
2666
|
+
return scope;
|
|
2667
|
+
}
|
|
2668
|
+
});
|
|
2669
|
+
faces.gateway.apply(client);
|
|
2670
|
+
const remote = client.get("remote");
|
|
2671
|
+
if (remote === void 0) throw new Error("resident DSH Client remote service was not installed");
|
|
2672
|
+
remote.$on("approval/request", function(request, next) {
|
|
2673
|
+
return handlers.approval(scopeOf(this) ?? "", request, next);
|
|
2674
|
+
});
|
|
2675
|
+
remote.$on("user-questions/request", function(request, next) {
|
|
2676
|
+
return handlers.question(scopeOf(this) ?? "", request, next);
|
|
2677
|
+
});
|
|
2678
|
+
return async () => {
|
|
2679
|
+
await client.fiber.dispose();
|
|
2680
|
+
};
|
|
2681
|
+
} catch (error) {
|
|
2682
|
+
await client.fiber.dispose();
|
|
2683
|
+
throw error;
|
|
2684
|
+
}
|
|
2685
|
+
}
|
|
2686
|
+
//#endregion
|
|
2479
2687
|
//#region src/dsh012-api-proxy.ts
|
|
2480
2688
|
/**
|
|
2481
2689
|
* Compatibility façade for the DSH 0.1.2 controller API.
|
|
2482
2690
|
*
|
|
2483
2691
|
* DeepPilot's phone protocol deliberately speaks one stable in-process
|
|
2484
2692
|
* `apiProxy` vocabulary. Harness 0.1.2 removed that service in favor of
|
|
2485
|
-
* direct Session/Workspace controllers plus
|
|
2693
|
+
* direct Session/Workspace controllers plus Gateway-backed Remote Events.
|
|
2486
2694
|
* This adapter rebuilds the small subset the bridge needs from those public
|
|
2487
|
-
* controllers
|
|
2488
|
-
*
|
|
2695
|
+
* controllers and joins the official Client interaction plane in-process,
|
|
2696
|
+
* keeping the phone protocol isolated from the Host API migration.
|
|
2489
2697
|
*/
|
|
2490
2698
|
/** Direct-controller facade with the exact legacy shape HostBridge consumes. */
|
|
2491
2699
|
var Dsh012ApiProxy = class {
|
|
@@ -2494,13 +2702,27 @@ var Dsh012ApiProxy = class {
|
|
|
2494
2702
|
workspaceController;
|
|
2495
2703
|
directoryPicker;
|
|
2496
2704
|
interactions = /* @__PURE__ */ new Map();
|
|
2497
|
-
|
|
2705
|
+
shouldSurfaceInteraction;
|
|
2706
|
+
constructor(ctx, options = {}) {
|
|
2498
2707
|
this.ctx = ctx;
|
|
2499
2708
|
const session = ctx.get("sessionController");
|
|
2500
2709
|
if (session === void 0) throw new Error("dsh 0.1.2 sessionController is unavailable");
|
|
2501
2710
|
this.session = session;
|
|
2502
2711
|
this.workspaceController = ctx.get("workspaceController");
|
|
2503
2712
|
this.directoryPicker = ctx.get("directoryPickerController");
|
|
2713
|
+
this.shouldSurfaceInteraction = options.shouldSurfaceInteraction ?? (() => true);
|
|
2714
|
+
}
|
|
2715
|
+
/**
|
|
2716
|
+
* Decide whether this Gateway Client can surface a phone card. The decision
|
|
2717
|
+
* fails open to false so a broken device registry delegates this delivery
|
|
2718
|
+
* while the official Web Client remains able to answer.
|
|
2719
|
+
*/
|
|
2720
|
+
canSurfaceInteraction(kind) {
|
|
2721
|
+
try {
|
|
2722
|
+
return this.shouldSurfaceInteraction(kind);
|
|
2723
|
+
} catch {
|
|
2724
|
+
return false;
|
|
2725
|
+
}
|
|
2504
2726
|
}
|
|
2505
2727
|
sessions = {
|
|
2506
2728
|
list: async () => this.call(async () => {
|
|
@@ -2613,71 +2835,79 @@ var Dsh012ApiProxy = class {
|
|
|
2613
2835
|
value
|
|
2614
2836
|
});
|
|
2615
2837
|
});
|
|
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
|
-
});
|
|
2838
|
+
let disposeInteractions;
|
|
2839
|
+
try {
|
|
2840
|
+
disposeInteractions = await startDsh012RemoteInteractions(this.ctx, {
|
|
2841
|
+
approval: (sessionId, request, next) => this.answerApproval(queue, sessionId, request, next),
|
|
2842
|
+
question: (sessionId, request, next) => this.answerQuestion(queue, sessionId, request, next)
|
|
2669
2843
|
});
|
|
2670
|
-
})
|
|
2844
|
+
} catch (error) {
|
|
2845
|
+
console.warn(`[deeppilot] DSH Remote interaction client unavailable: ${toError(error).message}`);
|
|
2846
|
+
}
|
|
2671
2847
|
try {
|
|
2672
2848
|
yield* queue.iterate();
|
|
2673
2849
|
} finally {
|
|
2674
2850
|
offEvent();
|
|
2675
2851
|
offProjection?.();
|
|
2676
|
-
|
|
2677
|
-
offQuestion();
|
|
2852
|
+
await disposeInteractions?.();
|
|
2678
2853
|
queue.close();
|
|
2679
2854
|
}
|
|
2680
2855
|
}
|
|
2856
|
+
answerApproval(queue, sessionId, request, next) {
|
|
2857
|
+
if (!this.canSurfaceInteraction("approval")) return next();
|
|
2858
|
+
const rpcId = randomUUID();
|
|
2859
|
+
const response = deferred();
|
|
2860
|
+
const abort = () => response.resolve("cancelled");
|
|
2861
|
+
request.signal?.addEventListener("abort", abort, { once: true });
|
|
2862
|
+
this.interactions.set(rpcId, {
|
|
2863
|
+
resolve: response.resolve,
|
|
2864
|
+
map: (value) => {
|
|
2865
|
+
const outcome = value?.outcome;
|
|
2866
|
+
return outcome === "allowed-once" || outcome === "rejected" ? outcome : "unavailable";
|
|
2867
|
+
}
|
|
2868
|
+
});
|
|
2869
|
+
queue.push({
|
|
2870
|
+
type: "approval/requested",
|
|
2871
|
+
rpcId,
|
|
2872
|
+
sessionId,
|
|
2873
|
+
approvalId: rpcId,
|
|
2874
|
+
toolName: String(request.toolName ?? "tool"),
|
|
2875
|
+
reason: String(request.reason ?? "")
|
|
2876
|
+
});
|
|
2877
|
+
return response.promise.finally(() => {
|
|
2878
|
+
request.signal?.removeEventListener("abort", abort);
|
|
2879
|
+
this.interactions.delete(rpcId);
|
|
2880
|
+
queue.push({
|
|
2881
|
+
type: "approval/resolved",
|
|
2882
|
+
approvalId: rpcId
|
|
2883
|
+
});
|
|
2884
|
+
});
|
|
2885
|
+
}
|
|
2886
|
+
answerQuestion(queue, sessionId, request, next) {
|
|
2887
|
+
if (!this.canSurfaceInteraction("question")) return next();
|
|
2888
|
+
const rpcId = randomUUID();
|
|
2889
|
+
const response = deferred();
|
|
2890
|
+
const abort = () => response.reject(/* @__PURE__ */ new Error("question cancelled"));
|
|
2891
|
+
request.signal?.addEventListener("abort", abort, { once: true });
|
|
2892
|
+
this.interactions.set(rpcId, {
|
|
2893
|
+
resolve: response.resolve,
|
|
2894
|
+
map: (value) => value?.answer ?? value
|
|
2895
|
+
});
|
|
2896
|
+
queue.push({
|
|
2897
|
+
type: "question/requested",
|
|
2898
|
+
rpcId,
|
|
2899
|
+
sessionId,
|
|
2900
|
+
questions: request.questions ?? []
|
|
2901
|
+
});
|
|
2902
|
+
return response.promise.finally(() => {
|
|
2903
|
+
request.signal?.removeEventListener("abort", abort);
|
|
2904
|
+
this.interactions.delete(rpcId);
|
|
2905
|
+
queue.push({
|
|
2906
|
+
type: "question/resolved",
|
|
2907
|
+
questionRpcId: rpcId
|
|
2908
|
+
});
|
|
2909
|
+
});
|
|
2910
|
+
}
|
|
2681
2911
|
async *hostEvents(signal) {
|
|
2682
2912
|
const queue = new AsyncFrameQueue(signal);
|
|
2683
2913
|
const listen = (event, type, project) => this.ctx.on(event, ((...args) => queue.push({
|
|
@@ -5151,7 +5381,21 @@ function apply(ctx, options) {
|
|
|
5151
5381
|
res.end(JSON.stringify({ ok: false }));
|
|
5152
5382
|
}
|
|
5153
5383
|
};
|
|
5154
|
-
|
|
5384
|
+
/**
|
|
5385
|
+
* Whether the resident Gateway Client represents a real phone surface. A
|
|
5386
|
+
* paired device remains answerable even while offline: HostBridge retains an
|
|
5387
|
+
* authoritative pending snapshot that the app pulls on reconnect, while
|
|
5388
|
+
* APNs is only a best-effort wakeup. With no paired device this Client calls
|
|
5389
|
+
* `next()`; Gateway's independent official Web delivery is unaffected.
|
|
5390
|
+
*/
|
|
5391
|
+
const hasPairedPhoneSurface = (_kind) => {
|
|
5392
|
+
return (auth.devices?.list() ?? []).some((device) => device.revokedAt === void 0);
|
|
5393
|
+
};
|
|
5394
|
+
ctx.inject([
|
|
5395
|
+
"sessionController",
|
|
5396
|
+
"connection",
|
|
5397
|
+
"typertGateway"
|
|
5398
|
+
], (sub) => {
|
|
5155
5399
|
if (currentConfig().enabled !== true) {
|
|
5156
5400
|
log("bridge disabled; data plane stays inactive");
|
|
5157
5401
|
return;
|
|
@@ -5159,7 +5403,7 @@ function apply(ctx, options) {
|
|
|
5159
5403
|
const apiCtx = sub;
|
|
5160
5404
|
let proxy;
|
|
5161
5405
|
try {
|
|
5162
|
-
proxy = new Dsh012ApiProxy(apiCtx);
|
|
5406
|
+
proxy = new Dsh012ApiProxy(apiCtx, { shouldSurfaceInteraction: hasPairedPhoneSurface });
|
|
5163
5407
|
} catch (error) {
|
|
5164
5408
|
log("dsh 0.1.2 session bridge unavailable: " + String(error));
|
|
5165
5409
|
return;
|