dsh-deeppilot 0.2.0 → 0.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +9 -3
- package/README.zh-CN.md +9 -3
- package/lib/index.d.ts +47 -3
- package/lib/index.js +266 -41
- package/lib/index.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -63,9 +63,9 @@ do not pass through the relay. Read [PRIVACY.md](./PRIVACY.md) and
|
|
|
63
63
|
|
|
64
64
|
## Screenshots
|
|
65
65
|
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
66
|
+
| Home | Sidebar | Chat | Settings |
|
|
67
|
+
| --- | --- | --- | --- |
|
|
68
|
+
| <img src="./assets/screenshots/home.png" alt="DeepPilot home screen" width="220"> | <img src="./assets/screenshots/sidebar.png" alt="DeepPilot sidebar" width="220"> | <img src="./assets/screenshots/chat.png" alt="DeepPilot chat screen" width="220"> | <img src="./assets/screenshots/settings.png" alt="DeepPilot settings screen" width="220"> |
|
|
69
69
|
|
|
70
70
|
## Compatibility
|
|
71
71
|
|
|
@@ -73,6 +73,12 @@ See [COMPATIBILITY.md](./COMPATIBILITY.md) for the tested baseline and current
|
|
|
73
73
|
limitations. DSH is still evolving; include exact DSH and plugin versions when
|
|
74
74
|
reporting an issue.
|
|
75
75
|
|
|
76
|
+
## Protocol
|
|
77
|
+
|
|
78
|
+
[PROTOCOL.md](./PROTOCOL.md) is the normative DeepPilot bridge protocol. Any
|
|
79
|
+
wire change must update that document and `src/protocol.ts` together, preserve
|
|
80
|
+
protocol-v1 compatibility, and be coordinated with the private iOS client.
|
|
81
|
+
|
|
76
82
|
## Development
|
|
77
83
|
|
|
78
84
|
```sh
|
package/README.zh-CN.md
CHANGED
|
@@ -59,15 +59,21 @@ dsh plugin --profile web remove dsh-deeppilot
|
|
|
59
59
|
|
|
60
60
|
## App 截图
|
|
61
61
|
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
62
|
+
| 首页 | 侧边栏 | 聊天 | 设置 |
|
|
63
|
+
| --- | --- | --- | --- |
|
|
64
|
+
| <img src="./assets/screenshots/home.png" alt="DeepPilot 首页" width="220"> | <img src="./assets/screenshots/sidebar.png" alt="DeepPilot 侧边栏" width="220"> | <img src="./assets/screenshots/chat.png" alt="DeepPilot 聊天界面" width="220"> | <img src="./assets/screenshots/settings.png" alt="DeepPilot 设置界面" width="220"> |
|
|
65
65
|
|
|
66
66
|
## 兼容性
|
|
67
67
|
|
|
68
68
|
已验证的基线与当前限制见 [COMPATIBILITY.md](./COMPATIBILITY.md)。DSH 仍在快速
|
|
69
69
|
迭代;提交问题时请附上准确的 DSH 与插件版本。
|
|
70
70
|
|
|
71
|
+
## 协议
|
|
72
|
+
|
|
73
|
+
[PROTOCOL.md](./PROTOCOL.md) 是 DeepPilot 桥接协议的规范性文档。任何 wire
|
|
74
|
+
变更都必须同步更新该文档与 `src/protocol.ts`,保持协议 v1 向后兼容,并与
|
|
75
|
+
私有 iOS 客户端协调。
|
|
76
|
+
|
|
71
77
|
## 开发
|
|
72
78
|
|
|
73
79
|
```sh
|
package/lib/index.d.ts
CHANGED
|
@@ -39,6 +39,32 @@ interface PushNotification {
|
|
|
39
39
|
title: string;
|
|
40
40
|
body: string;
|
|
41
41
|
}
|
|
42
|
+
interface PendingApprovalPayload {
|
|
43
|
+
requestId: string;
|
|
44
|
+
sessionId: string;
|
|
45
|
+
toolName: string;
|
|
46
|
+
summary: string;
|
|
47
|
+
riskLevel: 'read' | 'write' | 'destructive';
|
|
48
|
+
}
|
|
49
|
+
interface PendingQuestionOption {
|
|
50
|
+
label: string;
|
|
51
|
+
description?: string;
|
|
52
|
+
}
|
|
53
|
+
interface PendingQuestionItem {
|
|
54
|
+
id: string;
|
|
55
|
+
question: string;
|
|
56
|
+
multiSelect?: boolean;
|
|
57
|
+
options?: PendingQuestionOption[];
|
|
58
|
+
}
|
|
59
|
+
interface PendingQuestionPayload {
|
|
60
|
+
requestId: string;
|
|
61
|
+
sessionId: string;
|
|
62
|
+
questions: PendingQuestionItem[];
|
|
63
|
+
}
|
|
64
|
+
interface PendingSnapshotPayload {
|
|
65
|
+
approvals: PendingApprovalPayload[];
|
|
66
|
+
questions: PendingQuestionPayload[];
|
|
67
|
+
}
|
|
42
68
|
//#endregion
|
|
43
69
|
//#region src/host-bridge.d.ts
|
|
44
70
|
interface RpcOk<T> {
|
|
@@ -291,6 +317,17 @@ interface ApiProxyLike {
|
|
|
291
317
|
host(req: RpcRequestLike<Record<string, never>>, signal: AbortSignal): AsyncIterable<MuxFrameLike | ApiStreamItemLike>;
|
|
292
318
|
};
|
|
293
319
|
}
|
|
320
|
+
/**
|
|
321
|
+
* Outcome of answering a pending approval/question. The host distinguishes
|
|
322
|
+
* "never/no longer pending" from "payload rejected", and collapsing both into
|
|
323
|
+
* a boolean made every rejection read as `question not pending` on the phone.
|
|
324
|
+
*/
|
|
325
|
+
type PendingResponseOutcome = {
|
|
326
|
+
ok: true;
|
|
327
|
+
} | {
|
|
328
|
+
ok: false;
|
|
329
|
+
reason: 'not-pending' | 'bad-response' | 'transport';
|
|
330
|
+
};
|
|
294
331
|
/** Downward sink every connected phone registers (one per WebSocket). */
|
|
295
332
|
interface BridgeSink {
|
|
296
333
|
push(type: string, payload: unknown, seq?: number): void;
|
|
@@ -343,6 +380,7 @@ declare class HostBridge {
|
|
|
343
380
|
replay: boolean;
|
|
344
381
|
approvals: boolean;
|
|
345
382
|
questions: boolean;
|
|
383
|
+
pendingSnapshot: boolean;
|
|
346
384
|
models: boolean;
|
|
347
385
|
sessionManagement: boolean;
|
|
348
386
|
projectSelection: boolean;
|
|
@@ -392,6 +430,12 @@ declare class HostBridge {
|
|
|
392
430
|
private bumpPendingFlags;
|
|
393
431
|
private pushSummary;
|
|
394
432
|
listSessions(): SessionSummary[];
|
|
433
|
+
/**
|
|
434
|
+
* Complete transient interaction state. Unlike the replay ring, this remains
|
|
435
|
+
* authoritative after a long disconnect and is rehydrated by apiProxy's mux
|
|
436
|
+
* stream when the bridge itself restarts.
|
|
437
|
+
*/
|
|
438
|
+
pendingSnapshot(): PendingSnapshotPayload;
|
|
395
439
|
/** Tail history for an opened session; pushes s2c.session.tail to the sink. */
|
|
396
440
|
openSession(sink: BridgeSink, sessionId: string, tailCount: number): Promise<boolean>;
|
|
397
441
|
historyPage(sink: BridgeSink, sessionId: string, beforeSeq: number, limit: number): Promise<boolean>;
|
|
@@ -431,9 +475,9 @@ declare class HostBridge {
|
|
|
431
475
|
mediaType: 'image/png' | 'image/jpeg' | 'image/webp' | 'image/gif';
|
|
432
476
|
data: string;
|
|
433
477
|
name?: string;
|
|
434
|
-
}>): Promise<number
|
|
435
|
-
respondApproval(requestId: string, decision: 'allow' | 'deny'): Promise<
|
|
436
|
-
respondQuestion(requestId: string, answers: unknown): Promise<
|
|
478
|
+
}>): Promise<SessionManagementResult<number>>;
|
|
479
|
+
respondApproval(requestId: string, decision: 'allow' | 'deny', reason?: string): Promise<PendingResponseOutcome>;
|
|
480
|
+
respondQuestion(requestId: string, answers: unknown): Promise<PendingResponseOutcome>;
|
|
437
481
|
}
|
|
438
482
|
//#endregion
|
|
439
483
|
//#region src/index.d.ts
|
package/lib/index.js
CHANGED
|
@@ -362,6 +362,9 @@ var BridgeConnection = class {
|
|
|
362
362
|
sessions: this.deps.bridge.listSessions()
|
|
363
363
|
}, env.id);
|
|
364
364
|
return;
|
|
365
|
+
case "c2s.pending.list":
|
|
366
|
+
this.send("s2c.pending.snapshot", this.deps.bridge.pendingSnapshot(), env.id);
|
|
367
|
+
return;
|
|
365
368
|
case "c2s.workspaces.list": {
|
|
366
369
|
if (!this.deps.bridge.capabilities.projectSelection) return this.fail(env.id, "E_UNSUPPORTED", "project selection unavailable on this host version");
|
|
367
370
|
const result = await this.deps.bridge.listWorkspaces();
|
|
@@ -395,10 +398,15 @@ var BridgeConnection = class {
|
|
|
395
398
|
}
|
|
396
399
|
case "c2s.session.open": {
|
|
397
400
|
const p = env.payload;
|
|
398
|
-
if (!p?.sessionId) return this.fail(env.id, "E_PROTOCOL", "sessionId required");
|
|
399
|
-
|
|
400
|
-
this.
|
|
401
|
-
|
|
401
|
+
if (!p?.sessionId || typeof p.sessionId !== "string") return this.fail(env.id, "E_PROTOCOL", "sessionId required");
|
|
402
|
+
const sessionId = p.sessionId;
|
|
403
|
+
this.openSessions.add(sessionId);
|
|
404
|
+
this.deps.bridge.markSinkOpen(this, sessionId);
|
|
405
|
+
if (!await this.deps.bridge.openSession(this, sessionId, p.tailCount ?? 100)) {
|
|
406
|
+
this.openSessions.delete(sessionId);
|
|
407
|
+
this.deps.bridge.markSinkClosed(this, sessionId);
|
|
408
|
+
return this.fail(env.id, "E_NOT_FOUND", "session history unavailable");
|
|
409
|
+
}
|
|
402
410
|
return;
|
|
403
411
|
}
|
|
404
412
|
case "c2s.session.close": {
|
|
@@ -526,21 +534,23 @@ var BridgeConnection = class {
|
|
|
526
534
|
});
|
|
527
535
|
}
|
|
528
536
|
const userSeq = await this.deps.bridge.sendPrompt(p.sessionId, text, images);
|
|
529
|
-
if (userSeq
|
|
530
|
-
this.send("s2c.ack", { userSeq }, env.id);
|
|
537
|
+
if (!userSeq.ok) return this.fail(env.id, managementErrorCode(userSeq.kind), userSeq.message);
|
|
538
|
+
this.send("s2c.ack", { userSeq: userSeq.value }, env.id);
|
|
531
539
|
return;
|
|
532
540
|
}
|
|
533
541
|
case "c2s.approval.respond": {
|
|
534
542
|
const p = env.payload;
|
|
535
543
|
if (!p?.requestId || p.decision !== "allow" && p.decision !== "deny") return this.fail(env.id, "E_PROTOCOL", "requestId and decision required");
|
|
536
|
-
|
|
544
|
+
const outcome = await this.deps.bridge.respondApproval(p.requestId, p.decision, typeof p.reason === "string" ? p.reason : void 0);
|
|
545
|
+
if (!outcome.ok) return this.fail(env.id, pendingResponseErrorCode(outcome.reason), pendingResponseMessage("approval", outcome.reason));
|
|
537
546
|
this.send("s2c.ack", {}, env.id);
|
|
538
547
|
return;
|
|
539
548
|
}
|
|
540
549
|
case "c2s.question.respond": {
|
|
541
550
|
const p = env.payload;
|
|
542
551
|
if (!p?.requestId || !Array.isArray(p.answers)) return this.fail(env.id, "E_PROTOCOL", "requestId and answers required");
|
|
543
|
-
|
|
552
|
+
const outcome = await this.deps.bridge.respondQuestion(p.requestId, p.answers);
|
|
553
|
+
if (!outcome.ok) return this.fail(env.id, pendingResponseErrorCode(outcome.reason), pendingResponseMessage("question", outcome.reason));
|
|
544
554
|
this.send("s2c.ack", {}, env.id);
|
|
545
555
|
return;
|
|
546
556
|
}
|
|
@@ -612,6 +622,23 @@ var BridgeConnection = class {
|
|
|
612
622
|
}
|
|
613
623
|
}
|
|
614
624
|
};
|
|
625
|
+
/** Error code for a failed approval/question response outcome. */
|
|
626
|
+
function pendingResponseErrorCode(reason) {
|
|
627
|
+
switch (reason) {
|
|
628
|
+
case "not-pending": return "E_NOT_FOUND";
|
|
629
|
+
case "bad-response": return "E_PROTOCOL";
|
|
630
|
+
case "transport": return "E_INTERNAL";
|
|
631
|
+
}
|
|
632
|
+
}
|
|
633
|
+
/** Human-readable failure detail; `question not pending` must only ever mean
|
|
634
|
+
* "nothing pending", never "the host rejected the answer". */
|
|
635
|
+
function pendingResponseMessage(kind, reason) {
|
|
636
|
+
switch (reason) {
|
|
637
|
+
case "not-pending": return kind + " not pending";
|
|
638
|
+
case "bad-response": return kind + " answer rejected by host: answer does not match the asked questions";
|
|
639
|
+
case "transport": return "host connection failed while answering " + kind;
|
|
640
|
+
}
|
|
641
|
+
}
|
|
615
642
|
function managementErrorCode(kind) {
|
|
616
643
|
switch (kind) {
|
|
617
644
|
case "unsupported": return "E_UNSUPPORTED";
|
|
@@ -677,6 +704,7 @@ var HostBridge = class {
|
|
|
677
704
|
replay: true,
|
|
678
705
|
approvals: true,
|
|
679
706
|
questions: true,
|
|
707
|
+
pendingSnapshot: true,
|
|
680
708
|
models: typeof this.apiProxy.sessions.models === "function" && typeof this.apiProxy.sessions.selectModel === "function",
|
|
681
709
|
sessionManagement: typeof this.apiProxy.sessions.rename === "function" && typeof this.apiProxy.workspace?.archiveSession === "function",
|
|
682
710
|
projectSelection: typeof this.apiProxy.workspace?.list === "function" && typeof this.apiProxy.workspace?.create === "function",
|
|
@@ -698,7 +726,7 @@ var HostBridge = class {
|
|
|
698
726
|
/** Whether the ring still holds everything after the cursor. */
|
|
699
727
|
canResumeFrom(cursor) {
|
|
700
728
|
const oldest = this.ring.length > 0 ? this.ring[0].seq : this.cursor + 1;
|
|
701
|
-
return cursor + 1 >= oldest;
|
|
729
|
+
return cursor <= this.cursor && cursor + 1 >= oldest;
|
|
702
730
|
}
|
|
703
731
|
sinkSessions = /* @__PURE__ */ new Map();
|
|
704
732
|
lastAssistantText = /* @__PURE__ */ new Map();
|
|
@@ -1040,6 +1068,27 @@ var HostBridge = class {
|
|
|
1040
1068
|
listSessions() {
|
|
1041
1069
|
return [...this.summaries.values()].sort((a, b) => b.lastActivityTs - a.lastActivityTs);
|
|
1042
1070
|
}
|
|
1071
|
+
/**
|
|
1072
|
+
* Complete transient interaction state. Unlike the replay ring, this remains
|
|
1073
|
+
* authoritative after a long disconnect and is rehydrated by apiProxy's mux
|
|
1074
|
+
* stream when the bridge itself restarts.
|
|
1075
|
+
*/
|
|
1076
|
+
pendingSnapshot() {
|
|
1077
|
+
return {
|
|
1078
|
+
approvals: [...this.approvals.entries()].map(([requestId, pending]) => ({
|
|
1079
|
+
requestId,
|
|
1080
|
+
sessionId: pending.sessionId,
|
|
1081
|
+
toolName: pending.toolName,
|
|
1082
|
+
summary: pending.reason,
|
|
1083
|
+
riskLevel: riskOf(pending.toolName)
|
|
1084
|
+
})),
|
|
1085
|
+
questions: [...this.questions.entries()].map(([requestId, pending]) => ({
|
|
1086
|
+
requestId,
|
|
1087
|
+
sessionId: pending.sessionId,
|
|
1088
|
+
questions: Array.isArray(pending.questions) ? pending.questions : []
|
|
1089
|
+
}))
|
|
1090
|
+
};
|
|
1091
|
+
}
|
|
1043
1092
|
/** Tail history for an opened session; pushes s2c.session.tail to the sink. */
|
|
1044
1093
|
async openSession(sink, sessionId, tailCount) {
|
|
1045
1094
|
try {
|
|
@@ -1445,22 +1494,38 @@ var HostBridge = class {
|
|
|
1445
1494
|
clientTimeZone: localTimeZone()
|
|
1446
1495
|
}
|
|
1447
1496
|
});
|
|
1448
|
-
if (!response.result
|
|
1497
|
+
if (!response.result) return {
|
|
1498
|
+
ok: false,
|
|
1499
|
+
kind: "internal",
|
|
1500
|
+
message: "prompt returned no result"
|
|
1501
|
+
};
|
|
1502
|
+
if (!response.result.ok) return hostSessionManagementError(response.result.error);
|
|
1449
1503
|
const row = this.summaries.get(sessionId);
|
|
1450
1504
|
if (row) {
|
|
1451
1505
|
row.lastActivityTs = Date.now();
|
|
1452
1506
|
this.pushSummary(row);
|
|
1453
1507
|
}
|
|
1454
|
-
return
|
|
1455
|
-
|
|
1456
|
-
|
|
1508
|
+
return {
|
|
1509
|
+
ok: true,
|
|
1510
|
+
value: Date.now()
|
|
1511
|
+
};
|
|
1512
|
+
} catch (error) {
|
|
1513
|
+
return {
|
|
1514
|
+
ok: false,
|
|
1515
|
+
kind: "internal",
|
|
1516
|
+
message: String(error)
|
|
1517
|
+
};
|
|
1457
1518
|
}
|
|
1458
1519
|
}
|
|
1459
|
-
async respondApproval(requestId, decision) {
|
|
1520
|
+
async respondApproval(requestId, decision, reason) {
|
|
1460
1521
|
const pending = this.approvals.get(requestId);
|
|
1461
|
-
if (!pending) return
|
|
1522
|
+
if (!pending) return {
|
|
1523
|
+
ok: false,
|
|
1524
|
+
reason: "not-pending"
|
|
1525
|
+
};
|
|
1462
1526
|
this.approvals.delete(requestId);
|
|
1463
1527
|
const outcome = decision === "allow" ? "allowed-once" : "rejected";
|
|
1528
|
+
const denialReason = typeof reason === "string" ? reason.trim().slice(0, 500) : "";
|
|
1464
1529
|
try {
|
|
1465
1530
|
const receipt = await this.apiProxy.respond({
|
|
1466
1531
|
type: "client-response",
|
|
@@ -1470,21 +1535,35 @@ var HostBridge = class {
|
|
|
1470
1535
|
value: {
|
|
1471
1536
|
sessionId: pending.sessionId,
|
|
1472
1537
|
approvalId: requestId,
|
|
1473
|
-
outcome
|
|
1538
|
+
outcome,
|
|
1539
|
+
...denialReason.length > 0 ? { reason: denialReason } : {}
|
|
1474
1540
|
}
|
|
1475
1541
|
}
|
|
1476
1542
|
});
|
|
1477
|
-
|
|
1478
|
-
|
|
1479
|
-
|
|
1543
|
+
if (!Boolean(receipt?.accepted)) {
|
|
1544
|
+
const failure = receiptFailureReason(receipt);
|
|
1545
|
+
if (failure !== "not-pending" && !this.approvals.has(requestId)) this.approvals.set(requestId, pending);
|
|
1546
|
+
return {
|
|
1547
|
+
ok: false,
|
|
1548
|
+
reason: failure
|
|
1549
|
+
};
|
|
1550
|
+
}
|
|
1551
|
+
this.bumpPendingFlags(pending.sessionId);
|
|
1552
|
+
return { ok: true };
|
|
1480
1553
|
} catch {
|
|
1481
1554
|
if (!this.approvals.has(requestId)) this.approvals.set(requestId, pending);
|
|
1482
|
-
return
|
|
1555
|
+
return {
|
|
1556
|
+
ok: false,
|
|
1557
|
+
reason: "transport"
|
|
1558
|
+
};
|
|
1483
1559
|
}
|
|
1484
1560
|
}
|
|
1485
1561
|
async respondQuestion(requestId, answers) {
|
|
1486
1562
|
const pending = this.questions.get(requestId);
|
|
1487
|
-
if (!pending) return
|
|
1563
|
+
if (!pending) return {
|
|
1564
|
+
ok: false,
|
|
1565
|
+
reason: "not-pending"
|
|
1566
|
+
};
|
|
1488
1567
|
this.questions.delete(requestId);
|
|
1489
1568
|
try {
|
|
1490
1569
|
const receipt = await this.apiProxy.respond({
|
|
@@ -1494,19 +1573,65 @@ var HostBridge = class {
|
|
|
1494
1573
|
ok: true,
|
|
1495
1574
|
value: {
|
|
1496
1575
|
sessionId: pending.sessionId,
|
|
1497
|
-
answer: { answers }
|
|
1576
|
+
answer: { answers: normalizeAnswerItems(answers, pending.questions) }
|
|
1498
1577
|
}
|
|
1499
1578
|
}
|
|
1500
1579
|
});
|
|
1501
|
-
|
|
1502
|
-
|
|
1503
|
-
|
|
1580
|
+
if (!Boolean(receipt?.accepted)) {
|
|
1581
|
+
const failure = receiptFailureReason(receipt);
|
|
1582
|
+
if (failure !== "not-pending" && !this.questions.has(requestId)) this.questions.set(requestId, pending);
|
|
1583
|
+
return {
|
|
1584
|
+
ok: false,
|
|
1585
|
+
reason: failure
|
|
1586
|
+
};
|
|
1587
|
+
}
|
|
1588
|
+
this.bumpPendingFlags(pending.sessionId);
|
|
1589
|
+
return { ok: true };
|
|
1504
1590
|
} catch {
|
|
1505
1591
|
if (!this.questions.has(requestId)) this.questions.set(requestId, pending);
|
|
1506
|
-
return
|
|
1592
|
+
return {
|
|
1593
|
+
ok: false,
|
|
1594
|
+
reason: "transport"
|
|
1595
|
+
};
|
|
1507
1596
|
}
|
|
1508
1597
|
}
|
|
1509
1598
|
};
|
|
1599
|
+
/**
|
|
1600
|
+
* The host validates question answers strictly (core dsh-user-questions via
|
|
1601
|
+
* apiProxy): a present-but-empty `custom` fails `matchesQuestions`, and a
|
|
1602
|
+
* single-select question rejects `custom` combined with a selection. Clients
|
|
1603
|
+
* may send lenient shapes (the phone historically always attached
|
|
1604
|
+
* `"custom": ""`, which made EVERY option-only answer fail), so normalize to
|
|
1605
|
+
* exactly what the host accepts before forwarding.
|
|
1606
|
+
*/
|
|
1607
|
+
function normalizeAnswerItems(raw, questions) {
|
|
1608
|
+
if (!Array.isArray(raw)) return [];
|
|
1609
|
+
const askedById = /* @__PURE__ */ new Map();
|
|
1610
|
+
if (Array.isArray(questions)) {
|
|
1611
|
+
for (const q of questions) if (typeof q === "object" && q !== null && typeof q.id === "string") askedById.set(q.id, q);
|
|
1612
|
+
}
|
|
1613
|
+
const items = [];
|
|
1614
|
+
for (const entry of raw) {
|
|
1615
|
+
if (typeof entry !== "object" || entry === null) continue;
|
|
1616
|
+
const r = entry;
|
|
1617
|
+
if (typeof r.id !== "string") continue;
|
|
1618
|
+
const selected = [...new Set(Array.isArray(r.selected) ? r.selected.filter((s) => typeof s === "string") : [])];
|
|
1619
|
+
const customText = typeof r.custom === "string" ? r.custom : "";
|
|
1620
|
+
let custom;
|
|
1621
|
+
if (customText.trim().length > 0) custom = customText;
|
|
1622
|
+
if (custom !== void 0 && selected.length > 0 && askedById.get(r.id)?.multiSelect !== true) custom = void 0;
|
|
1623
|
+
items.push({
|
|
1624
|
+
id: r.id,
|
|
1625
|
+
selected,
|
|
1626
|
+
...custom !== void 0 ? { custom } : {}
|
|
1627
|
+
});
|
|
1628
|
+
}
|
|
1629
|
+
return items;
|
|
1630
|
+
}
|
|
1631
|
+
/** Map an apiProxy respond receipt onto the failure vocabulary. */
|
|
1632
|
+
function receiptFailureReason(receipt) {
|
|
1633
|
+
return receipt?.reason === "not-pending" ? "not-pending" : "bad-response";
|
|
1634
|
+
}
|
|
1510
1635
|
function clampTail(n) {
|
|
1511
1636
|
if (!Number.isFinite(n)) return 100;
|
|
1512
1637
|
return Math.max(10, Math.min(500, Math.floor(n)));
|
|
@@ -1598,9 +1723,10 @@ function projectEvent(sessionId, event) {
|
|
|
1598
1723
|
kind: "message.final",
|
|
1599
1724
|
data: {
|
|
1600
1725
|
seq: event.seq,
|
|
1601
|
-
role:
|
|
1726
|
+
role: userRoleOf(event.data),
|
|
1602
1727
|
text: messageText(event.data),
|
|
1603
1728
|
...attachmentProjection(event.data),
|
|
1729
|
+
...contextProjectionOf(event.data),
|
|
1604
1730
|
ts: tsOf(event)
|
|
1605
1731
|
}
|
|
1606
1732
|
};
|
|
@@ -1644,27 +1770,99 @@ function projectEvent(sessionId, event) {
|
|
|
1644
1770
|
tool: {
|
|
1645
1771
|
name: String(data?.name ?? "tool"),
|
|
1646
1772
|
state: "running",
|
|
1647
|
-
summary: summarizeArgs(data?.arguments)
|
|
1773
|
+
summary: summarizeArgs(data?.arguments),
|
|
1774
|
+
...data?.callId ? { callId: String(data.callId) } : {}
|
|
1648
1775
|
},
|
|
1649
1776
|
ts: tsOf(event)
|
|
1650
1777
|
}
|
|
1651
1778
|
};
|
|
1652
1779
|
}
|
|
1653
|
-
case "tool/result":
|
|
1654
|
-
|
|
1655
|
-
|
|
1656
|
-
|
|
1657
|
-
|
|
1658
|
-
|
|
1659
|
-
|
|
1660
|
-
|
|
1661
|
-
|
|
1780
|
+
case "tool/result": {
|
|
1781
|
+
const data = event.data;
|
|
1782
|
+
return {
|
|
1783
|
+
kind: "tool.end",
|
|
1784
|
+
data: {
|
|
1785
|
+
seq: event.seq,
|
|
1786
|
+
role: "tool",
|
|
1787
|
+
ok: !event.data || data?.error === void 0,
|
|
1788
|
+
...data?.callId ? { callId: String(data.callId) } : {},
|
|
1789
|
+
ts: tsOf(event)
|
|
1790
|
+
}
|
|
1791
|
+
};
|
|
1792
|
+
}
|
|
1662
1793
|
default: return null;
|
|
1663
1794
|
}
|
|
1664
1795
|
}
|
|
1665
1796
|
function tsOf(event) {
|
|
1666
1797
|
return typeof event.time === "number" ? event.time : Date.now();
|
|
1667
1798
|
}
|
|
1799
|
+
/** Read the durable message source off one user/message payload. Handles both
|
|
1800
|
+
* bare-message payloads and older `{message: {...}}` wrappers; undefined when
|
|
1801
|
+
* the shape carries no readable source (legacy hosts). */
|
|
1802
|
+
function userMessageSource(data) {
|
|
1803
|
+
if (!data || typeof data !== "object") return void 0;
|
|
1804
|
+
const obj = data;
|
|
1805
|
+
if (obj.source && typeof obj.source === "object") return obj.source;
|
|
1806
|
+
if (obj.message && typeof obj.message === "object" && obj.message.source && typeof obj.message.source === "object") return obj.message.source;
|
|
1807
|
+
}
|
|
1808
|
+
/** Wire role for one user/message payload. A payload without any readable
|
|
1809
|
+
* source degrades to 'user' so history written by older hosts stays visible;
|
|
1810
|
+
* a present source follows the host's own trajectory rule — anything whose
|
|
1811
|
+
* `kind` is not 'user' is injected context and projects as 'system'. */
|
|
1812
|
+
function userRoleOf(data) {
|
|
1813
|
+
const source = userMessageSource(data);
|
|
1814
|
+
if (!source) return "user";
|
|
1815
|
+
return source.kind === "user" ? "user" : "system";
|
|
1816
|
+
}
|
|
1817
|
+
/** Producer name of one injected-context source, mirroring how the DSH client
|
|
1818
|
+
* runtime derives its trajectory label: plugin name, skill name, instruction
|
|
1819
|
+
* paths, session-reference labels, or the raw kind as fallback. */
|
|
1820
|
+
function contextLabelOf(source) {
|
|
1821
|
+
const kind = typeof source.kind === "string" ? source.kind : "";
|
|
1822
|
+
const joined = (member) => {
|
|
1823
|
+
const list = source[member];
|
|
1824
|
+
if (!Array.isArray(list)) return void 0;
|
|
1825
|
+
const names = list.flatMap((entry) => {
|
|
1826
|
+
if (!entry || typeof entry !== "object") return [];
|
|
1827
|
+
const record = entry;
|
|
1828
|
+
return [typeof record.label === "string" ? record.label : typeof record.path === "string" ? record.path : ""];
|
|
1829
|
+
}).filter((name) => name.length > 0);
|
|
1830
|
+
return names.length > 0 ? names.join(", ") : void 0;
|
|
1831
|
+
};
|
|
1832
|
+
switch (kind) {
|
|
1833
|
+
case "session-reference": return joined("references") ?? (kind || void 0);
|
|
1834
|
+
case "agent-instructions": return joined("changes") ?? (kind || void 0);
|
|
1835
|
+
case "plugin": return typeof source.plugin === "string" && source.plugin.length > 0 ? source.plugin : kind || void 0;
|
|
1836
|
+
case "skill-invocation": return typeof source.name === "string" && source.name.length > 0 ? source.name : kind || void 0;
|
|
1837
|
+
default: return kind || void 0;
|
|
1838
|
+
}
|
|
1839
|
+
}
|
|
1840
|
+
/** Semantic ContextForm declared by the producer ('snapshot', 'notice', …);
|
|
1841
|
+
* anything unrecognized stays undefined so clients render it opaque. */
|
|
1842
|
+
function contextFormOf(source) {
|
|
1843
|
+
if (typeof source.form !== "string" || source.form.length === 0) return void 0;
|
|
1844
|
+
return [
|
|
1845
|
+
"instructions",
|
|
1846
|
+
"catalog",
|
|
1847
|
+
"snapshot",
|
|
1848
|
+
"notice",
|
|
1849
|
+
"relay",
|
|
1850
|
+
"recall"
|
|
1851
|
+
].includes(source.form) ? source.form : void 0;
|
|
1852
|
+
}
|
|
1853
|
+
/** Optional `context` metadata for one system row; {} on user rows. */
|
|
1854
|
+
function contextProjectionOf(data) {
|
|
1855
|
+
if (userRoleOf(data) !== "system") return {};
|
|
1856
|
+
const source = userMessageSource(data);
|
|
1857
|
+
if (!source) return {};
|
|
1858
|
+
const label = contextLabelOf(source);
|
|
1859
|
+
const form = contextFormOf(source);
|
|
1860
|
+
if (!label && !form) return {};
|
|
1861
|
+
return { context: {
|
|
1862
|
+
...label ? { label } : {},
|
|
1863
|
+
...form ? { form } : {}
|
|
1864
|
+
} };
|
|
1865
|
+
}
|
|
1668
1866
|
/** Extract plain text from user/assistant message payloads across shapes. */
|
|
1669
1867
|
function messageText(data) {
|
|
1670
1868
|
if (typeof data === "string") return data;
|
|
@@ -1874,9 +2072,10 @@ function projectHistory(events) {
|
|
|
1874
2072
|
case "user/message":
|
|
1875
2073
|
messages.push({
|
|
1876
2074
|
...base,
|
|
1877
|
-
role:
|
|
2075
|
+
role: userRoleOf(event.data),
|
|
1878
2076
|
text: messageText(event.data),
|
|
1879
|
-
...attachmentProjection(event.data)
|
|
2077
|
+
...attachmentProjection(event.data),
|
|
2078
|
+
...contextProjectionOf(event.data)
|
|
1880
2079
|
});
|
|
1881
2080
|
break;
|
|
1882
2081
|
case "assistant/message": {
|
|
@@ -2711,12 +2910,14 @@ var RemoteSupervisor = class {
|
|
|
2711
2910
|
mode: 448
|
|
2712
2911
|
});
|
|
2713
2912
|
} catch (error) {
|
|
2913
|
+
if (this.stopping) return;
|
|
2714
2914
|
this.setStatus({
|
|
2715
2915
|
phase: "unavailable",
|
|
2716
2916
|
message: `embedded tunnel helper unavailable: ${String(error)}`
|
|
2717
2917
|
});
|
|
2718
2918
|
return;
|
|
2719
2919
|
}
|
|
2920
|
+
if (this.stopping) return;
|
|
2720
2921
|
this.setStatus({
|
|
2721
2922
|
phase: "starting",
|
|
2722
2923
|
message: void 0
|
|
@@ -3207,6 +3408,10 @@ function apply(ctx, options) {
|
|
|
3207
3408
|
let enrollAttemptFor;
|
|
3208
3409
|
let enrollLastAttemptAt = 0;
|
|
3209
3410
|
const ensureRelayEnrolled = async (url) => {
|
|
3411
|
+
if (!/^https:\/\//i.test(url.trim())) {
|
|
3412
|
+
log("push relay enrollment refused: relayUrl must be an https URL");
|
|
3413
|
+
return;
|
|
3414
|
+
}
|
|
3210
3415
|
if (enrollmentCell.token) return enrollmentCell.token;
|
|
3211
3416
|
const fingerprint = url + ":" + String(enrollmentCell.enrollKey ?? "");
|
|
3212
3417
|
if (fingerprint !== enrollAttemptFor) {
|
|
@@ -3239,7 +3444,14 @@ function apply(ctx, options) {
|
|
|
3239
3444
|
}
|
|
3240
3445
|
};
|
|
3241
3446
|
let cachedSender;
|
|
3447
|
+
/**
|
|
3448
|
+
* Last failed APNs-sender build. The config fingerprint cannot see the
|
|
3449
|
+
* filesystem, so remembering a failure forever meant "copy the .p8 into
|
|
3450
|
+
* place later" never recovered without an edit or restart; throttle the
|
|
3451
|
+
* retry by time instead — same pattern as relay enrollment below.
|
|
3452
|
+
*/
|
|
3242
3453
|
let senderFailedFor;
|
|
3454
|
+
const SENDER_FAILURE_RETRY_MS = 6e4;
|
|
3243
3455
|
/**
|
|
3244
3456
|
* Lazily build the push sender for the current config. A broken config
|
|
3245
3457
|
* (unreadable .p8) disables push for that fingerprint with exactly one log
|
|
@@ -3248,7 +3460,7 @@ function apply(ctx, options) {
|
|
|
3248
3460
|
const senderFor = async (resolved) => {
|
|
3249
3461
|
const fingerprint = JSON.stringify(resolved);
|
|
3250
3462
|
if (cachedSender?.fingerprint === fingerprint) return cachedSender.send;
|
|
3251
|
-
if (senderFailedFor === fingerprint)
|
|
3463
|
+
if (senderFailedFor?.fingerprint === fingerprint && Date.now() - senderFailedFor.at < SENDER_FAILURE_RETRY_MS) return;
|
|
3252
3464
|
if (cachedSender) {
|
|
3253
3465
|
await cachedSender.dispose?.().catch(() => {});
|
|
3254
3466
|
cachedSender = void 0;
|
|
@@ -3269,7 +3481,10 @@ function apply(ctx, options) {
|
|
|
3269
3481
|
try {
|
|
3270
3482
|
await readFile(expandHome(resolved.keyPath), "utf8");
|
|
3271
3483
|
} catch (error) {
|
|
3272
|
-
senderFailedFor =
|
|
3484
|
+
senderFailedFor = {
|
|
3485
|
+
fingerprint,
|
|
3486
|
+
at: Date.now()
|
|
3487
|
+
};
|
|
3273
3488
|
log("apns push unavailable (key unreadable at " + resolved.keyPath + "): " + String(error));
|
|
3274
3489
|
return;
|
|
3275
3490
|
}
|
|
@@ -3422,6 +3637,16 @@ function apply(ctx, options) {
|
|
|
3422
3637
|
}]
|
|
3423
3638
|
};
|
|
3424
3639
|
const url = (push.relayUrl ?? "").trim() || DEFAULT_RELAY_URL;
|
|
3640
|
+
if (!/^https:\/\//i.test(url)) return {
|
|
3641
|
+
url,
|
|
3642
|
+
overall: "failed",
|
|
3643
|
+
tokenIssued: false,
|
|
3644
|
+
steps: [{
|
|
3645
|
+
id: "health",
|
|
3646
|
+
ok: false,
|
|
3647
|
+
message: "relayUrl 必须是 https 地址:注册请求携带共享密钥,明文 HTTP 会把它暴露给链路上的任何节点"
|
|
3648
|
+
}]
|
|
3649
|
+
};
|
|
3425
3650
|
if (!enrollmentCell.clientId && enrollmentCell.enrollKey) {
|
|
3426
3651
|
enrollmentCell.clientId = "u_" + randomBytes(16).toString("base64url");
|
|
3427
3652
|
persistEnrollment();
|