dsh-deeppilot 0.9.11 → 0.9.14
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 +174 -19
- package/README.md +25 -18
- package/README.zh-CN.md +20 -16
- 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/docs/DSH_022_FEATURES.md +78 -0
- package/docs/DSH_RELEASE_MEMORY.md +447 -14
- package/docs/NPM_PUBLISHING.md +53 -1
- package/lib/client.js +525 -341
- package/lib/client.js.map +1 -1
- package/lib/index.d.ts +381 -35
- package/lib/index.js +2820 -254
- package/lib/index.js.map +1 -1
- package/locale/en.json +2 -1
- package/locale/zh.json +2 -1
- package/package.json +26 -22
package/lib/index.js
CHANGED
|
@@ -4,9 +4,9 @@ import { access, appendFile, chmod, mkdir, readFile, readdir, rename, rm, stat,
|
|
|
4
4
|
import { dirname, join, resolve } from "node:path";
|
|
5
5
|
import { WebSocketServer } from "ws";
|
|
6
6
|
import { homedir, networkInterfaces } from "node:os";
|
|
7
|
+
import { closeSync, constants, existsSync, fsyncSync, mkdirSync, openSync, readFileSync, renameSync, statSync, writeFileSync } from "node:fs";
|
|
7
8
|
import z from "@deepseek-ai/schemastery";
|
|
8
9
|
import { spawn } from "node:child_process";
|
|
9
|
-
import { closeSync, constants, existsSync, fsyncSync, mkdirSync, openSync, readFileSync, renameSync, statSync, writeFileSync } from "node:fs";
|
|
10
10
|
import { fileURLToPath } from "node:url";
|
|
11
11
|
import { connect } from "node:http2";
|
|
12
12
|
import { createServer } from "node:http";
|
|
@@ -16,7 +16,7 @@ import { Context } from "@deepseek-ai/cordis";
|
|
|
16
16
|
import { TypertRemoteService } from "@deepseek-ai/dsh-typert-protocol";
|
|
17
17
|
import { isIP } from "node:net";
|
|
18
18
|
import { generate } from "selfsigned";
|
|
19
|
-
|
|
19
|
+
// src/wire-errors.ts
|
|
20
20
|
/**
|
|
21
21
|
* 错误词表(error vocabulary)的唯一属主。
|
|
22
22
|
*
|
|
@@ -41,7 +41,9 @@ const ERROR_CODES = {
|
|
|
41
41
|
E_NOT_FOUND: "session or request not found",
|
|
42
42
|
E_BUSY: "session is busy",
|
|
43
43
|
E_UNSUPPORTED: "protocol version or capability unsupported",
|
|
44
|
-
E_INTERNAL: "internal error"
|
|
44
|
+
E_INTERNAL: "internal error",
|
|
45
|
+
E_STEER_UNAVAILABLE: "queued item can no longer steer the current turn",
|
|
46
|
+
E_TOO_LARGE: "response exceeds the client frame limit"
|
|
45
47
|
};
|
|
46
48
|
/** Bridge 未携带 code 的结果(内部守卫失败等)落到这一档。 */
|
|
47
49
|
const WIRE_ERROR_FALLBACK = "E_INTERNAL";
|
|
@@ -89,6 +91,9 @@ const DOMAIN_TABLES = {
|
|
|
89
91
|
"schedule_not_found": "E_NOT_FOUND",
|
|
90
92
|
"delivery_cursor_not_found": "E_NOT_FOUND",
|
|
91
93
|
"schedule_conflict": "E_BUSY",
|
|
94
|
+
"schedule_execution_pending": "E_BUSY",
|
|
95
|
+
"schedule/target-archived": "E_NOT_FOUND",
|
|
96
|
+
"schedule/input-invalid": "E_PROTOCOL",
|
|
92
97
|
"invalid_prompt": "E_PROTOCOL",
|
|
93
98
|
"invalid_selector": "E_PROTOCOL",
|
|
94
99
|
"invalid_rule": "E_PROTOCOL",
|
|
@@ -116,6 +121,13 @@ const DOMAIN_TABLES = {
|
|
|
116
121
|
"session-not-found": "E_NOT_FOUND",
|
|
117
122
|
"agent-busy": "E_BUSY",
|
|
118
123
|
"session-conflict": "E_BUSY"
|
|
124
|
+
},
|
|
125
|
+
queue: {
|
|
126
|
+
"session/queue-item-not-found": "E_NOT_FOUND",
|
|
127
|
+
"session/steer-unavailable": "E_STEER_UNAVAILABLE",
|
|
128
|
+
"session/agent-busy": "E_BUSY",
|
|
129
|
+
"session/writer-held": "E_BUSY",
|
|
130
|
+
"gateway/bad-request": "E_PROTOCOL"
|
|
119
131
|
}
|
|
120
132
|
};
|
|
121
133
|
/**
|
|
@@ -201,9 +213,10 @@ function pendingResponseMessage(kind, reason) {
|
|
|
201
213
|
case "transport": return "host connection failed while answering " + kind;
|
|
202
214
|
}
|
|
203
215
|
}
|
|
204
|
-
|
|
205
|
-
//#region src/connection-policy.ts
|
|
216
|
+
// src/connection-policy.ts
|
|
206
217
|
const AUTH_TIMEOUT_MS = 35e3;
|
|
218
|
+
/** 附件 ack 在 `{mediaType,data}` 之外的信封字段(v/type/id/ts)余量。 */
|
|
219
|
+
const ATTACHMENT_ENVELOPE_HEADROOM_BYTES = 1024;
|
|
207
220
|
const IMAGE_MEDIA_TYPES = /* @__PURE__ */ new Set([
|
|
208
221
|
"image/png",
|
|
209
222
|
"image/jpeg",
|
|
@@ -249,6 +262,10 @@ function isEnvelope(value) {
|
|
|
249
262
|
*/
|
|
250
263
|
const PUSH_SCOPE_BY_TYPE = {
|
|
251
264
|
"s2c.session.event": "sessions.read",
|
|
265
|
+
"s2c.session.delta": "sessions.read",
|
|
266
|
+
"s2c.plugin.update.status": "sessions.read",
|
|
267
|
+
"s2c.plugin.update.progress": "sessions.manage",
|
|
268
|
+
"s2c.session.queue": "sessions.read",
|
|
252
269
|
"s2c.skills.changed": "sessions.read",
|
|
253
270
|
"s2c.commands.changed": "sessions.read",
|
|
254
271
|
"s2c.models.changed": "sessions.read",
|
|
@@ -275,8 +292,20 @@ function sanitizeImageName(value) {
|
|
|
275
292
|
function sanitizeDocumentField(value, maxChars) {
|
|
276
293
|
return value.replace(/[\u0000-\u001F\u007F]/g, " ").trim().slice(0, maxChars);
|
|
277
294
|
}
|
|
278
|
-
|
|
279
|
-
|
|
295
|
+
/** 二进制模式下导出与附件的单块字节数。连接在发送缓冲降到 256 KiB 以下才发
|
|
296
|
+
* 下一块,缓冲峰值约 512 KiB,远低于 4 MiB 的慢客户端断开线。 */
|
|
297
|
+
const BINARY_CHUNK_BYTES = 262144;
|
|
298
|
+
/** 把 envelope 头部与原始字节拼成一帧。 */
|
|
299
|
+
function encodeBinaryFrame(header, body) {
|
|
300
|
+
const headerBytes = Buffer.from(JSON.stringify(header), "utf8");
|
|
301
|
+
const frame = Buffer.allocUnsafe(5 + headerBytes.length + body.length);
|
|
302
|
+
frame.writeUInt8(1, 0);
|
|
303
|
+
frame.writeUInt32BE(headerBytes.length, 1);
|
|
304
|
+
headerBytes.copy(frame, 5);
|
|
305
|
+
frame.set(body, 5 + headerBytes.length);
|
|
306
|
+
return frame;
|
|
307
|
+
}
|
|
308
|
+
// src/auth-rate-limit.ts
|
|
280
309
|
const DEFAULT_AUTH_RATE_POLICY = {
|
|
281
310
|
windowMs: 6e4,
|
|
282
311
|
attemptsPerSource: 12,
|
|
@@ -393,8 +422,7 @@ var AuthRateLimiter = class {
|
|
|
393
422
|
}
|
|
394
423
|
}
|
|
395
424
|
};
|
|
396
|
-
|
|
397
|
-
//#region src/diagnostics.ts
|
|
425
|
+
// src/diagnostics.ts
|
|
398
426
|
/**
|
|
399
427
|
* 连接诊断事件模型(Diagnostics)——插件唯一的「失败可观测」面。
|
|
400
428
|
*
|
|
@@ -720,8 +748,7 @@ function buildHealthPayload(input) {
|
|
|
720
748
|
}
|
|
721
749
|
};
|
|
722
750
|
}
|
|
723
|
-
|
|
724
|
-
//#region src/device-auth.ts
|
|
751
|
+
// src/device-auth.ts
|
|
725
752
|
function getPairingCodeTtlMs() {
|
|
726
753
|
return Number(process.env.DEEPPILOT_PAIRING_TTL_MS) || 3e5;
|
|
727
754
|
}
|
|
@@ -810,6 +837,30 @@ function normalizeDeviceScopes(value) {
|
|
|
810
837
|
const allowed = new Set(DEVICE_SCOPES);
|
|
811
838
|
return [...new Set(value.filter((scope) => typeof scope === "string" && allowed.has(scope)))];
|
|
812
839
|
}
|
|
840
|
+
/**
|
|
841
|
+
* 0.8.3 之前的「全部权限」:当时还没有 `schedule.manage`。App 配对时一直申请
|
|
842
|
+
* DeviceScope 的全部取值,设置页也没有收窄权限的入口,所以存量记录恰好等于这五项,
|
|
843
|
+
* 只说明「配对时这项权限还不存在」,不是有意关闭。
|
|
844
|
+
*/
|
|
845
|
+
const LEGACY_FULL_SCOPES = [
|
|
846
|
+
"sessions.read",
|
|
847
|
+
"prompt.send",
|
|
848
|
+
"sessions.manage",
|
|
849
|
+
"interactions.respond",
|
|
850
|
+
"notifications.register"
|
|
851
|
+
];
|
|
852
|
+
/**
|
|
853
|
+
* 存量设备权限升级:恰好等于旧版全部权限时补上之后新增的默认权限,返回新列表;
|
|
854
|
+
* 其余情况(含任何非完整集合)返回 null,原样保留。只放宽「旧版全量」这一种形态,
|
|
855
|
+
* 避免把将来可能出现的、有意收窄过的记录一并放开。
|
|
856
|
+
*/
|
|
857
|
+
function upgradeLegacyFullScopes(value) {
|
|
858
|
+
if (!Array.isArray(value)) return null;
|
|
859
|
+
const current = new Set(value);
|
|
860
|
+
if (current.size !== LEGACY_FULL_SCOPES.length) return null;
|
|
861
|
+
if (!LEGACY_FULL_SCOPES.every((scope) => current.has(scope))) return null;
|
|
862
|
+
return [...DEFAULT_DEVICE_SCOPES];
|
|
863
|
+
}
|
|
813
864
|
/** One active, single-use pairing grant per plugin runtime. */
|
|
814
865
|
var PairingCodeManager = class {
|
|
815
866
|
active = null;
|
|
@@ -845,8 +896,7 @@ function createAuthChallenge(audience, now = Date.now()) {
|
|
|
845
896
|
expiresAt: now + AUTH_CHALLENGE_TTL_MS
|
|
846
897
|
};
|
|
847
898
|
}
|
|
848
|
-
|
|
849
|
-
//#region src/wire-interaction.ts
|
|
899
|
+
// src/wire-interaction.ts
|
|
850
900
|
/**
|
|
851
901
|
* 交互特性的 wire 行:回答待决的 approval 与 question。
|
|
852
902
|
*
|
|
@@ -906,8 +956,7 @@ const interactionRows = [{
|
|
|
906
956
|
ctx.send("s2c.ack", {});
|
|
907
957
|
}
|
|
908
958
|
}];
|
|
909
|
-
|
|
910
|
-
//#region src/wire-device.ts
|
|
959
|
+
// src/wire-device.ts
|
|
911
960
|
/**
|
|
912
961
|
* 设备生命周期特性的 wire 行:自撤销(解绑)。
|
|
913
962
|
*
|
|
@@ -945,8 +994,7 @@ const deviceRows = [{
|
|
|
945
994
|
ctx.close(4401, "device revoked");
|
|
946
995
|
}
|
|
947
996
|
}];
|
|
948
|
-
|
|
949
|
-
//#region src/token.ts
|
|
997
|
+
// src/token.ts
|
|
950
998
|
/** Expand a leading ~ using the process home directory. */
|
|
951
999
|
function expandHome(p) {
|
|
952
1000
|
if (p === "~") return homedir();
|
|
@@ -1023,7 +1071,20 @@ var DeviceStore = class DeviceStore {
|
|
|
1023
1071
|
const store = new DeviceStore(filePath);
|
|
1024
1072
|
try {
|
|
1025
1073
|
const raw = JSON.parse(await readFile(expandHome(filePath), "utf8"));
|
|
1026
|
-
|
|
1074
|
+
let upgraded = 0;
|
|
1075
|
+
for (const rec of raw.devices ?? []) {
|
|
1076
|
+
if (typeof rec.deviceId !== "string") continue;
|
|
1077
|
+
const next = rec.revokedAt === void 0 ? upgradeLegacyFullScopes(rec.scopes) : null;
|
|
1078
|
+
if (next !== null) {
|
|
1079
|
+
rec.scopes = next;
|
|
1080
|
+
upgraded += 1;
|
|
1081
|
+
}
|
|
1082
|
+
store.devices.set(rec.deviceId, rec);
|
|
1083
|
+
}
|
|
1084
|
+
if (upgraded > 0) {
|
|
1085
|
+
console.log(formatLogLine("info", `device scopes upgraded for ${upgraded} legacy device(s): schedule.manage added`));
|
|
1086
|
+
store.flush();
|
|
1087
|
+
}
|
|
1027
1088
|
} catch (error) {
|
|
1028
1089
|
if (error?.code === "ENOENT") {} else console.log(formatLogLine("warn", "device registry unreadable, starting empty: " + String(error)));
|
|
1029
1090
|
}
|
|
@@ -1248,8 +1309,7 @@ var DeviceStore = class DeviceStore {
|
|
|
1248
1309
|
console.log(formatLogLine("warn", "device registry write failed" + (typeof code === "string" ? " code=" + code : "") + ": " + message.slice(0, 160)));
|
|
1249
1310
|
}
|
|
1250
1311
|
};
|
|
1251
|
-
|
|
1252
|
-
//#region src/wire-push.ts
|
|
1312
|
+
// src/wire-push.ts
|
|
1253
1313
|
/** Live Activity 需要的三个 scope:通知注册 + 会话读取 + 交互应答。 */
|
|
1254
1314
|
const LIVE_ACTIVITY_SCOPES = [
|
|
1255
1315
|
"notifications.register",
|
|
@@ -1396,21 +1456,83 @@ const pushRows = [
|
|
|
1396
1456
|
}
|
|
1397
1457
|
}
|
|
1398
1458
|
];
|
|
1399
|
-
|
|
1400
|
-
//#region src/wire-schedule.ts
|
|
1459
|
+
// src/wire-schedule.ts
|
|
1401
1460
|
/**
|
|
1402
|
-
* 定时任务特性的 wire
|
|
1461
|
+
* 定时任务特性的 wire 行:会话级(列表、历史、创建、修改、删除)与宿主级
|
|
1462
|
+
* (list/view/create/edit/run/limits)两套。
|
|
1403
1463
|
*
|
|
1404
1464
|
* scope 取 `schedule.manage` + `sessions.read`:PROTOCOL.md scope 映射表写明
|
|
1405
1465
|
* 「`schedule.manage` 定时任务/提醒的列表、历史、创建、修改和删除;同时需要
|
|
1406
1466
|
* `sessions.read` 才能读取任务内容」。迁移前这条组合一半住在 requiredScope、
|
|
1407
1467
|
* 一半住在 connection.ts 调用点的一行补充规则里(334-336),现在是一行声明。
|
|
1408
1468
|
*
|
|
1409
|
-
*
|
|
1410
|
-
* (见 host-capabilities.ts 的 G4
|
|
1469
|
+
* 五个会话级帧共享能力位 `schedules`;该位现在只要求 Schedule 服务存在
|
|
1470
|
+
* (见 host-capabilities.ts 的 G4 修正)。六个宿主级帧共享 `schedulesHost`:
|
|
1471
|
+
* 该位由 adapter 判别位推导,false/缺失时新帧一律 E_UNSUPPORTED,客户端只能
|
|
1472
|
+
* 走会话级帧(PROTOCOL.md「Schedule 宿主级任务扩展」)。
|
|
1411
1473
|
*/
|
|
1412
1474
|
/** PROTOCOL.md scope 映射表:schedule.manage 需搭配 sessions.read。 */
|
|
1413
1475
|
const SCHEDULE_SCOPES = ["schedule.manage", "sessions.read"];
|
|
1476
|
+
/** 普通 JSON 对象(宿主级 timing/target/patch 都是嵌套对象)。 */
|
|
1477
|
+
function isObjectValue(value) {
|
|
1478
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
1479
|
+
}
|
|
1480
|
+
/** `{time, time_zone}` 本地时间输入:取值合法性由 Host 完整校验。 */
|
|
1481
|
+
function isLocalTimeInput(value) {
|
|
1482
|
+
return isObjectValue(value) && isText(value.time) && isText(value.time_zone);
|
|
1483
|
+
}
|
|
1484
|
+
function isWeekdayList(value) {
|
|
1485
|
+
return Array.isArray(value) && value.length > 0 && value.every((day) => isInteger(day, 1, 7));
|
|
1486
|
+
}
|
|
1487
|
+
/**
|
|
1488
|
+
* 宿主级 timing 判别联合的形状检查。
|
|
1489
|
+
*
|
|
1490
|
+
* 只判结构与数量级(PROTOCOL.md 章节限额);日期格式、时区与频率是否可达由
|
|
1491
|
+
* Host 的 Schedule 服务负责——重复一遍只会制造两套口径。
|
|
1492
|
+
*/
|
|
1493
|
+
function isHostTiming(value) {
|
|
1494
|
+
if (!isObjectValue(value)) return false;
|
|
1495
|
+
switch (value.kind) {
|
|
1496
|
+
case "after": return isInteger(value.after_seconds, 1, Number.MAX_SAFE_INTEGER);
|
|
1497
|
+
case "at": return isText(value.at) || isObjectValue(value.at) && isText(value.at.date) && isText(value.at.time) && isText(value.at.time_zone);
|
|
1498
|
+
case "every": return isInteger(value.every_seconds, 60, Number.MAX_SAFE_INTEGER);
|
|
1499
|
+
case "daily": return isLocalTimeInput(value.daily);
|
|
1500
|
+
case "weekly": return isObjectValue(value.weekly) && isLocalTimeInput(value.weekly) && isWeekdayList(value.weekly.weekdays);
|
|
1501
|
+
case "cron": return isObjectValue(value.cron) && isText(value.cron.expression) && isText(value.cron.time_zone);
|
|
1502
|
+
case "rrule": return isText(value.rrule);
|
|
1503
|
+
default: return false;
|
|
1504
|
+
}
|
|
1505
|
+
}
|
|
1506
|
+
/** `{cwd?, agentPreset?}`:省略即 Host 默认。 */
|
|
1507
|
+
function isHostCreateSpec(value) {
|
|
1508
|
+
return isObjectValue(value) && isOptionalField(value, "cwd", (v) => isText(v)) && isOptionalField(value, "agentPreset", (v) => isText(v));
|
|
1509
|
+
}
|
|
1510
|
+
/**
|
|
1511
|
+
* 创建/编辑请求里的 target。`session` 必须给 sessionId;`new_once`/`new` 的
|
|
1512
|
+
* 会话由 Host 预留或新建,`create` 省略即默认。客户端若把查看结果里的
|
|
1513
|
+
* `new_once.sessionId` 原样回传,也接受(未知/冗余字段不该让编辑失败)。
|
|
1514
|
+
*/
|
|
1515
|
+
function isHostTarget(value) {
|
|
1516
|
+
if (!isObjectValue(value)) return false;
|
|
1517
|
+
switch (value.kind) {
|
|
1518
|
+
case "session": return isText(value.sessionId);
|
|
1519
|
+
case "new_once": return isOptionalField(value, "sessionId", (v) => isText(v)) && isOptionalField(value, "create", isHostCreateSpec);
|
|
1520
|
+
case "new": return isOptionalField(value, "create", isHostCreateSpec);
|
|
1521
|
+
default: return false;
|
|
1522
|
+
}
|
|
1523
|
+
}
|
|
1524
|
+
/** 编辑补丁:至少一个字段,且各字段形状合法。 */
|
|
1525
|
+
function isHostPatch(value) {
|
|
1526
|
+
if (!isObjectValue(value)) return false;
|
|
1527
|
+
if ([
|
|
1528
|
+
"title",
|
|
1529
|
+
"prompt",
|
|
1530
|
+
"timing",
|
|
1531
|
+
"target",
|
|
1532
|
+
"active"
|
|
1533
|
+
].filter((key) => value[key] !== void 0).length === 0) return false;
|
|
1534
|
+
return isOptionalField(value, "title", (v) => isText(v, 120)) && isOptionalField(value, "prompt", (v) => isText(v, 262144)) && isOptionalField(value, "timing", isHostTiming) && isOptionalField(value, "target", isHostTarget) && isOptionalField(value, "active", (v) => typeof v === "boolean");
|
|
1535
|
+
}
|
|
1414
1536
|
const scheduleRows = [
|
|
1415
1537
|
{
|
|
1416
1538
|
type: "c2s.schedule.list",
|
|
@@ -1442,14 +1564,16 @@ const scheduleRows = [
|
|
|
1442
1564
|
validate: (payload) => {
|
|
1443
1565
|
const checked = payloadObject(payload);
|
|
1444
1566
|
if (!checked.ok) return checked;
|
|
1445
|
-
if (!
|
|
1567
|
+
if (!isOptionalField(checked.value, "sessionId", (v) => isText(v)) || !isText(checked.value.id) || !isInteger(checked.value.limit, 1, 100) || !isOptionalField(checked.value, "before", (v) => isText(v))) return reject$1("E_PROTOCOL", "invalid schedule history");
|
|
1446
1568
|
return checked;
|
|
1447
1569
|
},
|
|
1448
1570
|
handle: async (ctx, payload) => {
|
|
1449
|
-
const
|
|
1571
|
+
const sessionId = payload.sessionId;
|
|
1572
|
+
if (sessionId === void 0 && ctx.bridge.capabilities.schedulesHost !== true) return ctx.fail("E_PROTOCOL", "invalid schedule history");
|
|
1573
|
+
const result = await ctx.bridge.scheduleHistory(sessionId ?? "", payload.id, payload.limit, payload.before);
|
|
1450
1574
|
if (!result.ok) return ctx.fail(result.code, result.message);
|
|
1451
1575
|
ctx.send("s2c.schedule.history", {
|
|
1452
|
-
sessionId:
|
|
1576
|
+
...sessionId !== void 0 ? { sessionId } : {},
|
|
1453
1577
|
history: result.value
|
|
1454
1578
|
});
|
|
1455
1579
|
}
|
|
@@ -1541,10 +1665,159 @@ const scheduleRows = [
|
|
|
1541
1665
|
...result.replayed ? { replayed: true } : {}
|
|
1542
1666
|
});
|
|
1543
1667
|
}
|
|
1668
|
+
},
|
|
1669
|
+
{
|
|
1670
|
+
type: "c2s.schedule.host.list",
|
|
1671
|
+
stage: "authenticated",
|
|
1672
|
+
scopes: SCHEDULE_SCOPES,
|
|
1673
|
+
capability: "schedulesHost",
|
|
1674
|
+
doc: "PROTOCOL.md c2s.schedule.host.list",
|
|
1675
|
+
validate: (payload) => {
|
|
1676
|
+
const checked = payloadObject(payload);
|
|
1677
|
+
if (!checked.ok) return checked;
|
|
1678
|
+
const p = checked.value;
|
|
1679
|
+
if (!isOptionalField(p, "active", (v) => typeof v === "boolean") || !isOptionalField(p, "limit", (v) => isInteger(v, 1, 500)) || !isOptionalField(p, "offset", (v) => isInteger(v, 0, Number.MAX_SAFE_INTEGER))) return reject$1("E_PROTOCOL", "invalid schedule host list");
|
|
1680
|
+
return checked;
|
|
1681
|
+
},
|
|
1682
|
+
handle: async (ctx, payload) => {
|
|
1683
|
+
const result = await ctx.bridge.listHostSchedules({
|
|
1684
|
+
...payload.active !== void 0 ? { active: payload.active } : {},
|
|
1685
|
+
...payload.limit !== void 0 ? { limit: payload.limit } : {},
|
|
1686
|
+
...payload.offset !== void 0 ? { offset: payload.offset } : {}
|
|
1687
|
+
});
|
|
1688
|
+
if (!result.ok) return ctx.fail(result.code, result.message);
|
|
1689
|
+
ctx.send("s2c.schedule.host.snapshot", result.value);
|
|
1690
|
+
}
|
|
1691
|
+
},
|
|
1692
|
+
{
|
|
1693
|
+
type: "c2s.schedule.host.view",
|
|
1694
|
+
stage: "authenticated",
|
|
1695
|
+
scopes: SCHEDULE_SCOPES,
|
|
1696
|
+
capability: "schedulesHost",
|
|
1697
|
+
doc: "PROTOCOL.md c2s.schedule.host.view",
|
|
1698
|
+
validate: (payload) => {
|
|
1699
|
+
const checked = payloadObject(payload);
|
|
1700
|
+
if (!checked.ok) return checked;
|
|
1701
|
+
if (!isText(checked.value.id)) return reject$1("E_PROTOCOL", "invalid schedule id");
|
|
1702
|
+
return checked;
|
|
1703
|
+
},
|
|
1704
|
+
handle: async (ctx, payload) => {
|
|
1705
|
+
const result = await ctx.bridge.viewHostSchedule(payload.id);
|
|
1706
|
+
if (!result.ok) return ctx.fail(result.code, result.message);
|
|
1707
|
+
ctx.send("s2c.schedule.host.task", { task: result.value });
|
|
1708
|
+
}
|
|
1709
|
+
},
|
|
1710
|
+
{
|
|
1711
|
+
type: "c2s.schedule.host.create",
|
|
1712
|
+
stage: "authenticated",
|
|
1713
|
+
scopes: SCHEDULE_SCOPES,
|
|
1714
|
+
capability: "schedulesHost",
|
|
1715
|
+
doc: "PROTOCOL.md c2s.schedule.host.create",
|
|
1716
|
+
validate: (payload) => {
|
|
1717
|
+
const checked = payloadObject(payload);
|
|
1718
|
+
if (!checked.ok) return checked;
|
|
1719
|
+
const p = checked.value;
|
|
1720
|
+
if (!validSendId(p.clientRequestId) || !isText(p.title, 120) || !isText(p.prompt, 262144) || !isHostTiming(p.timing) || !isOptionalField(p, "target", isHostTarget)) return reject$1("E_PROTOCOL", "invalid schedule host create fields");
|
|
1721
|
+
return checked;
|
|
1722
|
+
},
|
|
1723
|
+
handle: async (ctx, payload) => {
|
|
1724
|
+
const result = await ctx.bridge.createHostSchedule(ctx.deviceId, payload);
|
|
1725
|
+
if (!result.ok) return ctx.fail(result.code, result.message);
|
|
1726
|
+
ctx.send("s2c.schedule.updated", {
|
|
1727
|
+
clientRequestId: payload.clientRequestId,
|
|
1728
|
+
task: result.value,
|
|
1729
|
+
...result.replayed ? { replayed: true } : {}
|
|
1730
|
+
});
|
|
1731
|
+
}
|
|
1732
|
+
},
|
|
1733
|
+
{
|
|
1734
|
+
type: "c2s.schedule.host.edit",
|
|
1735
|
+
stage: "authenticated",
|
|
1736
|
+
scopes: SCHEDULE_SCOPES,
|
|
1737
|
+
capability: "schedulesHost",
|
|
1738
|
+
doc: "PROTOCOL.md c2s.schedule.host.edit",
|
|
1739
|
+
validate: (payload) => {
|
|
1740
|
+
const checked = payloadObject(payload);
|
|
1741
|
+
if (!checked.ok) return checked;
|
|
1742
|
+
const p = checked.value;
|
|
1743
|
+
if (!validSendId(p.clientRequestId) || !isText(p.id) || !isOptionalField(p, "expectedUpdatedAt", (v) => v === null || isText(v))) return reject$1("E_PROTOCOL", "invalid schedule host edit fields");
|
|
1744
|
+
if (p.action !== "update" && p.action !== "delete") return reject$1("E_PROTOCOL", "invalid schedule host edit action");
|
|
1745
|
+
if (p.action === "update" && !isHostPatch(p.patch)) return reject$1("E_PROTOCOL", "invalid schedule host edit patch");
|
|
1746
|
+
return checked;
|
|
1747
|
+
},
|
|
1748
|
+
handle: async (ctx, payload) => {
|
|
1749
|
+
const result = await ctx.bridge.editHostSchedule(ctx.deviceId, payload);
|
|
1750
|
+
if (!result.ok) return ctx.fail(result.code, result.message);
|
|
1751
|
+
const value = result.value;
|
|
1752
|
+
ctx.send("s2c.schedule.updated", {
|
|
1753
|
+
clientRequestId: payload.clientRequestId,
|
|
1754
|
+
..."deleted" in value ? { deleted: true } : { task: value.task },
|
|
1755
|
+
...result.replayed ? { replayed: true } : {}
|
|
1756
|
+
});
|
|
1757
|
+
}
|
|
1758
|
+
},
|
|
1759
|
+
{
|
|
1760
|
+
type: "c2s.schedule.host.run",
|
|
1761
|
+
stage: "authenticated",
|
|
1762
|
+
scopes: SCHEDULE_SCOPES,
|
|
1763
|
+
capability: "schedulesHost",
|
|
1764
|
+
doc: "PROTOCOL.md c2s.schedule.host.run",
|
|
1765
|
+
validate: (payload) => {
|
|
1766
|
+
const checked = payloadObject(payload);
|
|
1767
|
+
if (!checked.ok) return checked;
|
|
1768
|
+
if (!validSendId(checked.value.clientRequestId) || !isText(checked.value.id)) return reject$1("E_PROTOCOL", "invalid schedule host run fields");
|
|
1769
|
+
return checked;
|
|
1770
|
+
},
|
|
1771
|
+
handle: async (ctx, payload) => {
|
|
1772
|
+
const result = await ctx.bridge.runHostSchedule(ctx.deviceId, {
|
|
1773
|
+
clientRequestId: payload.clientRequestId,
|
|
1774
|
+
id: payload.id
|
|
1775
|
+
});
|
|
1776
|
+
if (!result.ok) return ctx.fail(result.code, result.message);
|
|
1777
|
+
ctx.send("s2c.schedule.updated", {
|
|
1778
|
+
clientRequestId: payload.clientRequestId,
|
|
1779
|
+
task: result.value,
|
|
1780
|
+
...result.replayed ? { replayed: true } : {}
|
|
1781
|
+
});
|
|
1782
|
+
}
|
|
1783
|
+
},
|
|
1784
|
+
{
|
|
1785
|
+
type: "c2s.schedule.host.deleteEnded",
|
|
1786
|
+
stage: "authenticated",
|
|
1787
|
+
scopes: SCHEDULE_SCOPES,
|
|
1788
|
+
capability: "schedulesHost",
|
|
1789
|
+
doc: "PROTOCOL.md c2s.schedule.host.deleteEnded",
|
|
1790
|
+
validate: (payload) => {
|
|
1791
|
+
const checked = payloadObject(payload);
|
|
1792
|
+
if (!checked.ok) return checked;
|
|
1793
|
+
if (!validSendId(checked.value.clientRequestId)) return reject$1("E_PROTOCOL", "invalid schedule host delete ended fields");
|
|
1794
|
+
return checked;
|
|
1795
|
+
},
|
|
1796
|
+
handle: async (ctx, payload) => {
|
|
1797
|
+
const result = await ctx.bridge.deleteEndedHostSchedules(ctx.deviceId, { clientRequestId: payload.clientRequestId });
|
|
1798
|
+
if (!result.ok) return ctx.fail(result.code, result.message);
|
|
1799
|
+
ctx.send("s2c.schedule.updated", {
|
|
1800
|
+
clientRequestId: payload.clientRequestId,
|
|
1801
|
+
...result.value.deleted !== void 0 ? { deletedEnded: result.value.deleted } : {},
|
|
1802
|
+
...result.replayed ? { replayed: true } : {}
|
|
1803
|
+
});
|
|
1804
|
+
}
|
|
1805
|
+
},
|
|
1806
|
+
{
|
|
1807
|
+
type: "c2s.schedule.host.limits",
|
|
1808
|
+
stage: "authenticated",
|
|
1809
|
+
scopes: SCHEDULE_SCOPES,
|
|
1810
|
+
capability: "schedulesHost",
|
|
1811
|
+
doc: "PROTOCOL.md c2s.schedule.host.limits",
|
|
1812
|
+
validate: (payload) => payloadObject(payload),
|
|
1813
|
+
handle: async (ctx) => {
|
|
1814
|
+
const result = await ctx.bridge.scheduleLimits();
|
|
1815
|
+
if (!result.ok) return ctx.fail(result.code, result.message);
|
|
1816
|
+
ctx.send("s2c.schedule.host.limits", result.value);
|
|
1817
|
+
}
|
|
1544
1818
|
}
|
|
1545
1819
|
];
|
|
1546
|
-
|
|
1547
|
-
//#region src/document-payload.ts
|
|
1820
|
+
// src/document-payload.ts
|
|
1548
1821
|
const PREFIX = "[DeepPilot document:";
|
|
1549
1822
|
const SUFFIX = "]";
|
|
1550
1823
|
/**
|
|
@@ -1581,8 +1854,7 @@ function projectedDocument(text) {
|
|
|
1581
1854
|
return;
|
|
1582
1855
|
}
|
|
1583
1856
|
}
|
|
1584
|
-
|
|
1585
|
-
//#region src/host-event-projection.ts
|
|
1857
|
+
// src/host-event-projection.ts
|
|
1586
1858
|
const MAX_MESSAGE_PROJECTION_BYTES = 262144;
|
|
1587
1859
|
/** One durable host event sequence becomes exactly one phone message row.
|
|
1588
1860
|
* Keep the last projection when a host history response repeats an event. */
|
|
@@ -1670,7 +1942,8 @@ function projectEvent(sessionId, event) {
|
|
|
1670
1942
|
name: String(data?.name ?? "tool"),
|
|
1671
1943
|
state: "running",
|
|
1672
1944
|
summary: summarizeArgs(data?.arguments),
|
|
1673
|
-
...data?.callId ? { callId: String(data.callId) } : {}
|
|
1945
|
+
...data?.callId ? { callId: String(data.callId) } : {},
|
|
1946
|
+
...editField(data?.name, data?.arguments)
|
|
1674
1947
|
},
|
|
1675
1948
|
ts: tsOf(event)
|
|
1676
1949
|
}
|
|
@@ -1870,6 +2143,10 @@ function chunkText(data) {
|
|
|
1870
2143
|
const direct = data;
|
|
1871
2144
|
return typeof direct.text === "string" ? direct.text : "";
|
|
1872
2145
|
}
|
|
2146
|
+
function editField(name, raw) {
|
|
2147
|
+
const edit = toolEditOf(name, raw);
|
|
2148
|
+
return edit ? { edit } : {};
|
|
2149
|
+
}
|
|
1873
2150
|
function summarizeArgs(raw) {
|
|
1874
2151
|
if (typeof raw !== "string" || raw.length === 0) return "";
|
|
1875
2152
|
try {
|
|
@@ -1881,6 +2158,68 @@ function summarizeArgs(raw) {
|
|
|
1881
2158
|
return truncate(raw, 90);
|
|
1882
2159
|
}
|
|
1883
2160
|
}
|
|
2161
|
+
/** 宿主的文件编辑工具名(DSH `dsh-tool-str-replace-editor`)。 */
|
|
2162
|
+
const EDITOR_TOOL_NAME = "str_replace_editor";
|
|
2163
|
+
/**
|
|
2164
|
+
* 把编辑类工具调用的原始参数归一化成 `ToolEditProjection`。
|
|
2165
|
+
*
|
|
2166
|
+
* 只认 `str_replace_editor` 的 `str_replace` / `create` / `insert`(`view` 不改文件),
|
|
2167
|
+
* 其他工具或无法解析的参数返回 undefined,工具行保持原样。与 iOS `ToolEditDiff.parse`
|
|
2168
|
+
* 的解析口径一致:`new_str` 缺失视为删除(空串)。
|
|
2169
|
+
*/
|
|
2170
|
+
function toolEditOf(name, raw) {
|
|
2171
|
+
if (name !== EDITOR_TOOL_NAME || typeof raw !== "string" || raw.length === 0) return void 0;
|
|
2172
|
+
let args;
|
|
2173
|
+
try {
|
|
2174
|
+
const parsed = JSON.parse(raw);
|
|
2175
|
+
if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) return void 0;
|
|
2176
|
+
args = parsed;
|
|
2177
|
+
} catch {
|
|
2178
|
+
return;
|
|
2179
|
+
}
|
|
2180
|
+
const path = args.path;
|
|
2181
|
+
if (typeof path !== "string" || path.length === 0) return void 0;
|
|
2182
|
+
let edit;
|
|
2183
|
+
switch (args.command) {
|
|
2184
|
+
case "str_replace":
|
|
2185
|
+
if (typeof args.old_str !== "string") return void 0;
|
|
2186
|
+
edit = {
|
|
2187
|
+
path,
|
|
2188
|
+
operation: "replace",
|
|
2189
|
+
oldText: args.old_str,
|
|
2190
|
+
newText: typeof args.new_str === "string" ? args.new_str : ""
|
|
2191
|
+
};
|
|
2192
|
+
break;
|
|
2193
|
+
case "create":
|
|
2194
|
+
if (typeof args.file_text !== "string") return void 0;
|
|
2195
|
+
edit = {
|
|
2196
|
+
path,
|
|
2197
|
+
operation: "create",
|
|
2198
|
+
newText: args.file_text
|
|
2199
|
+
};
|
|
2200
|
+
break;
|
|
2201
|
+
case "insert":
|
|
2202
|
+
if (typeof args.new_str !== "string") return void 0;
|
|
2203
|
+
edit = {
|
|
2204
|
+
path,
|
|
2205
|
+
operation: "insert",
|
|
2206
|
+
newText: args.new_str,
|
|
2207
|
+
...Number.isSafeInteger(args.insert_line) ? { insertLine: args.insert_line } : {}
|
|
2208
|
+
};
|
|
2209
|
+
break;
|
|
2210
|
+
default: return;
|
|
2211
|
+
}
|
|
2212
|
+
return [edit.oldText, edit.newText].some((text) => text !== void 0 && Buffer.byteLength(text, "utf8") > 16384) ? withoutEditBodies(edit) : edit;
|
|
2213
|
+
}
|
|
2214
|
+
/** 去掉改动正文,只留路径、操作与 truncated 标记(超限或整条消息超限时用)。 */
|
|
2215
|
+
function withoutEditBodies(edit) {
|
|
2216
|
+
return {
|
|
2217
|
+
path: edit.path,
|
|
2218
|
+
operation: edit.operation,
|
|
2219
|
+
...edit.insertLine !== void 0 ? { insertLine: edit.insertLine } : {},
|
|
2220
|
+
truncated: true
|
|
2221
|
+
};
|
|
2222
|
+
}
|
|
1884
2223
|
function truncate(text, max) {
|
|
1885
2224
|
return text.length <= max ? text : text.slice(0, max - 1) + "…";
|
|
1886
2225
|
}
|
|
@@ -1924,7 +2263,8 @@ function projectHistory(events) {
|
|
|
1924
2263
|
tool: {
|
|
1925
2264
|
name: String(data?.name ?? "tool"),
|
|
1926
2265
|
state: "running",
|
|
1927
|
-
summary: summarizeArgs(data?.arguments)
|
|
2266
|
+
summary: summarizeArgs(data?.arguments),
|
|
2267
|
+
...editField(data?.name, data?.arguments)
|
|
1928
2268
|
}
|
|
1929
2269
|
};
|
|
1930
2270
|
messages.push(row);
|
|
@@ -1973,7 +2313,8 @@ function limitMessageProjection(message) {
|
|
|
1973
2313
|
...message.tool ? { tool: {
|
|
1974
2314
|
...message.tool,
|
|
1975
2315
|
name: truncateUtf8(message.tool.name, 4096),
|
|
1976
|
-
summary: truncateUtf8(message.tool.summary, 65536)
|
|
2316
|
+
summary: truncateUtf8(message.tool.summary, 65536),
|
|
2317
|
+
...message.tool.edit ? { edit: withoutEditBodies(message.tool.edit) } : {}
|
|
1977
2318
|
} } : {},
|
|
1978
2319
|
...message.attachments ? { attachments: message.attachments.slice(0, 16).map((attachment) => ({
|
|
1979
2320
|
...attachment,
|
|
@@ -2117,8 +2458,7 @@ function historyWindow(source, options) {
|
|
|
2117
2458
|
hasMore: older.length > 0
|
|
2118
2459
|
};
|
|
2119
2460
|
}
|
|
2120
|
-
|
|
2121
|
-
//#region src/wire-session.ts
|
|
2461
|
+
// src/wire-session.ts
|
|
2122
2462
|
/**
|
|
2123
2463
|
* 会话特性的 wire 行:列表/归档、打开与关闭、创建/分支/重命名/取消、历史与
|
|
2124
2464
|
* 附件、模型目录与切换、prompt 投递。
|
|
@@ -2452,6 +2792,9 @@ const sessionRows = [
|
|
|
2452
2792
|
const invalid = requireSessionId(checked.value);
|
|
2453
2793
|
if (invalid) return invalid;
|
|
2454
2794
|
if (!isText(checked.value.attachmentId)) return reject$1("E_PROTOCOL", "invalid attachmentId");
|
|
2795
|
+
const maxFrameBytes = checked.value.maxFrameBytes;
|
|
2796
|
+
if (maxFrameBytes !== void 0 && (!Number.isSafeInteger(maxFrameBytes) || maxFrameBytes <= 0)) return reject$1("E_PROTOCOL", "invalid maxFrameBytes");
|
|
2797
|
+
if (checked.value.binary !== void 0 && typeof checked.value.binary !== "boolean") return reject$1("E_PROTOCOL", "invalid binary");
|
|
2455
2798
|
return checked;
|
|
2456
2799
|
},
|
|
2457
2800
|
handle: async (ctx, payload) => {
|
|
@@ -2459,6 +2802,34 @@ const sessionRows = [
|
|
|
2459
2802
|
if (attachmentId.length === 0) return ctx.fail("E_PROTOCOL", "sessionId and attachmentId required");
|
|
2460
2803
|
const image = await ctx.bridge.attachmentData(payload.sessionId, attachmentId);
|
|
2461
2804
|
if (!image) return ctx.fail("E_NOT_FOUND", "attachment unavailable");
|
|
2805
|
+
if (payload.binary === true && ctx.sendBinary) {
|
|
2806
|
+
const bytes = Buffer.from(image.data, "base64");
|
|
2807
|
+
if (bytes.length === 0) return ctx.fail("E_NOT_FOUND", "attachment unavailable");
|
|
2808
|
+
if (bytes.length > 12582912) return ctx.fail("E_TOO_LARGE", `attachment ${bytes.length} bytes exceeds limit`);
|
|
2809
|
+
let index = 0;
|
|
2810
|
+
try {
|
|
2811
|
+
for (let offset = 0; offset < bytes.length; offset += BINARY_CHUNK_BYTES) {
|
|
2812
|
+
await ctx.waitForWritable?.();
|
|
2813
|
+
ctx.sendBinary("s2c.session.attachment.chunk", { index: index++ }, bytes.subarray(offset, offset + BINARY_CHUNK_BYTES));
|
|
2814
|
+
}
|
|
2815
|
+
} catch {
|
|
2816
|
+
return;
|
|
2817
|
+
}
|
|
2818
|
+
const ack = {
|
|
2819
|
+
...image.mediaType !== void 0 ? { mediaType: image.mediaType } : {},
|
|
2820
|
+
chunks: index,
|
|
2821
|
+
bytes: bytes.length
|
|
2822
|
+
};
|
|
2823
|
+
return ctx.send("s2c.ack", ack);
|
|
2824
|
+
}
|
|
2825
|
+
const clientLimit = payload.maxFrameBytes ?? 1048576;
|
|
2826
|
+
const frameBytes = image.data.length + (image.mediaType?.length ?? 0) + ATTACHMENT_ENVELOPE_HEADROOM_BYTES;
|
|
2827
|
+
if (frameBytes > Math.min(clientLimit, 2097152)) return ctx.fail("E_TOO_LARGE", `attachment frame ${frameBytes} bytes exceeds limit`);
|
|
2828
|
+
try {
|
|
2829
|
+
await ctx.waitForWritable?.();
|
|
2830
|
+
} catch {
|
|
2831
|
+
return;
|
|
2832
|
+
}
|
|
2462
2833
|
ctx.send("s2c.ack", image);
|
|
2463
2834
|
}
|
|
2464
2835
|
},
|
|
@@ -2560,6 +2931,7 @@ const sessionRows = [
|
|
|
2560
2931
|
if (invalid) return invalid;
|
|
2561
2932
|
const p = checked.value;
|
|
2562
2933
|
if (p.clientSendId !== void 0 && !validSendId(p.clientSendId)) return reject$1("E_PROTOCOL", "invalid clientSendId");
|
|
2934
|
+
if (p.mode !== void 0 && p.mode !== "queue" && p.mode !== "steer") return reject$1("E_PROTOCOL", "invalid prompt mode");
|
|
2563
2935
|
if (!isOptionalField(p, "text", (v) => isText(v, 262144, false)) || !isOptionalField(p, "images", Array.isArray) || !isOptionalField(p, "documents", Array.isArray)) return reject$1("E_PROTOCOL", "invalid prompt fields");
|
|
2564
2936
|
for (const key of ["images", "documents"]) {
|
|
2565
2937
|
const items = p[key];
|
|
@@ -2607,6 +2979,7 @@ const sessionRows = [
|
|
|
2607
2979
|
text,
|
|
2608
2980
|
images,
|
|
2609
2981
|
documents,
|
|
2982
|
+
...p.mode === "steer" ? { mode: "steer" } : {},
|
|
2610
2983
|
...p.clientSendId !== void 0 ? { clientSendId: p.clientSendId } : {}
|
|
2611
2984
|
});
|
|
2612
2985
|
},
|
|
@@ -2616,26 +2989,59 @@ const sessionRows = [
|
|
|
2616
2989
|
const images = payload.images;
|
|
2617
2990
|
const documents = payload.documents;
|
|
2618
2991
|
const clientSendId = payload.clientSendId;
|
|
2992
|
+
const steer = payload.mode === "steer";
|
|
2993
|
+
if (steer && ctx.bridge.capabilities.promptQueue !== true) return ctx.fail("E_UNSUPPORTED", "prompt queue not supported");
|
|
2994
|
+
const mode = steer ? "steer" : "queue";
|
|
2619
2995
|
if (clientSendId !== void 0) {
|
|
2620
2996
|
const receipt = await ctx.bridge.promptDeliveries.dispatch(ctx.deviceId, clientSendId, {
|
|
2621
2997
|
sessionId,
|
|
2622
2998
|
content: {
|
|
2623
2999
|
text,
|
|
2624
3000
|
images,
|
|
2625
|
-
documents
|
|
3001
|
+
documents,
|
|
3002
|
+
...steer ? { mode } : {}
|
|
2626
3003
|
}
|
|
2627
|
-
}, () => ctx.bridge.sendPrompt(sessionId, text, images, documents
|
|
3004
|
+
}, () => ctx.bridge.sendPrompt(sessionId, text, images, documents, {
|
|
3005
|
+
mode,
|
|
3006
|
+
clientSendId
|
|
3007
|
+
}));
|
|
2628
3008
|
ctx.send("s2c.ack", receipt);
|
|
2629
3009
|
return;
|
|
2630
3010
|
}
|
|
2631
|
-
const userSeq = await ctx.bridge.sendPrompt(sessionId, text, images, documents);
|
|
3011
|
+
const userSeq = await ctx.bridge.sendPrompt(sessionId, text, images, documents, { mode });
|
|
2632
3012
|
if (!userSeq.ok) return ctx.fail(userSeq.code, userSeq.message);
|
|
2633
3013
|
ctx.send("s2c.ack", { userSeq: userSeq.value });
|
|
2634
3014
|
}
|
|
3015
|
+
},
|
|
3016
|
+
{
|
|
3017
|
+
type: "c2s.session.queue.update",
|
|
3018
|
+
stage: "authenticated",
|
|
3019
|
+
scopes: ["prompt.send"],
|
|
3020
|
+
capability: "promptQueue",
|
|
3021
|
+
doc: "PROTOCOL.md c2s.session.queue.update",
|
|
3022
|
+
validate: (payload) => {
|
|
3023
|
+
const checked = payloadObject(payload);
|
|
3024
|
+
if (!checked.ok) return checked;
|
|
3025
|
+
const invalid = requireSessionId(checked.value);
|
|
3026
|
+
if (invalid) return invalid;
|
|
3027
|
+
if (!isText(checked.value.itemId, 256)) return reject$1("E_PROTOCOL", "invalid itemId");
|
|
3028
|
+
const action = checked.value.action;
|
|
3029
|
+
if (action === "edit") return reject$1("E_UNSUPPORTED", "queue edit not supported");
|
|
3030
|
+
if (action !== "remove" && action !== "steer") return reject$1("E_PROTOCOL", "invalid queue action");
|
|
3031
|
+
return accept({
|
|
3032
|
+
sessionId: checked.value.sessionId,
|
|
3033
|
+
itemId: checked.value.itemId,
|
|
3034
|
+
action
|
|
3035
|
+
});
|
|
3036
|
+
},
|
|
3037
|
+
handle: async (ctx, payload) => {
|
|
3038
|
+
const result = await ctx.bridge.updateQueue(payload.sessionId, payload.itemId, payload.action);
|
|
3039
|
+
if (!result.ok) return ctx.fail(result.code, result.message);
|
|
3040
|
+
ctx.send("s2c.ack", result.value);
|
|
3041
|
+
}
|
|
2635
3042
|
}
|
|
2636
3043
|
];
|
|
2637
|
-
|
|
2638
|
-
//#region src/wire-command.ts
|
|
3044
|
+
// src/wire-command.ts
|
|
2639
3045
|
function sessionPayload(value) {
|
|
2640
3046
|
const result = payloadObject(value);
|
|
2641
3047
|
return result.ok && !isText(result.value.sessionId) ? reject$1("E_PROTOCOL", "会话标识无效") : result;
|
|
@@ -2701,38 +3107,60 @@ const commandRows = [...[
|
|
|
2701
3107
|
stage: "authenticated",
|
|
2702
3108
|
scopes: ["sessions.read"],
|
|
2703
3109
|
capability: "commands",
|
|
3110
|
+
conditionalCapabilities: ["sessionMarkdown"],
|
|
2704
3111
|
doc: "PROTOCOL.md c2s.session.export",
|
|
2705
|
-
validate:
|
|
3112
|
+
validate: (value) => {
|
|
3113
|
+
const result = sessionPayload(value);
|
|
3114
|
+
if (!result.ok) return result;
|
|
3115
|
+
if (result.value.format !== void 0 && result.value.format !== "zip" && result.value.format !== "markdown") return reject$1("E_PROTOCOL", "导出格式无效");
|
|
3116
|
+
if (result.value.binary !== void 0 && typeof result.value.binary !== "boolean") return reject$1("E_PROTOCOL", "传输方式无效");
|
|
3117
|
+
return result;
|
|
3118
|
+
},
|
|
2706
3119
|
handle: async (ctx, payload) => {
|
|
3120
|
+
const format = payload.format;
|
|
3121
|
+
if (format === "markdown" && ctx.bridge.capabilities.sessionMarkdown !== true) return ctx.fail("E_UNSUPPORTED", "当前主机不支持导出对话正文");
|
|
2707
3122
|
const signal = AbortSignal.any([ctx.signal ?? new AbortController().signal, AbortSignal.timeout(6e5)]);
|
|
2708
3123
|
let reader;
|
|
2709
3124
|
const cancel = () => {
|
|
2710
3125
|
reader?.cancel().catch(() => {});
|
|
2711
3126
|
};
|
|
2712
3127
|
try {
|
|
2713
|
-
reader = (await ctx.bridge.exportSession(payload.sessionId, signal)).body.getReader();
|
|
3128
|
+
reader = (format === void 0 ? await ctx.bridge.exportSession(payload.sessionId, signal) : await ctx.bridge.exportSession(payload.sessionId, signal, format)).body.getReader();
|
|
2714
3129
|
signal.addEventListener("abort", cancel, { once: true });
|
|
3130
|
+
const binary = payload.binary === true && ctx.sendBinary !== void 0;
|
|
2715
3131
|
let index = 0, bytes = 0;
|
|
3132
|
+
const sendChunk = async (chunk) => {
|
|
3133
|
+
bytes += chunk.length;
|
|
3134
|
+
await ctx.waitForWritable?.();
|
|
3135
|
+
if (binary) ctx.sendBinary("s2c.session.export.chunk", { index: index++ }, chunk);
|
|
3136
|
+
else ctx.send("s2c.session.export.chunk", {
|
|
3137
|
+
index: index++,
|
|
3138
|
+
data: Buffer.from(chunk).toString("base64")
|
|
3139
|
+
});
|
|
3140
|
+
};
|
|
3141
|
+
let carry;
|
|
2716
3142
|
while (true) {
|
|
2717
3143
|
signal.throwIfAborted();
|
|
2718
3144
|
const next = await reader.read();
|
|
2719
3145
|
if (next.done) break;
|
|
2720
|
-
|
|
2721
|
-
|
|
2722
|
-
|
|
2723
|
-
|
|
2724
|
-
|
|
2725
|
-
|
|
2726
|
-
|
|
2727
|
-
|
|
3146
|
+
if (!binary) {
|
|
3147
|
+
for (let offset = 0; offset < next.value.length; offset += 49152) await sendChunk(next.value.subarray(offset, offset + 49152));
|
|
3148
|
+
continue;
|
|
3149
|
+
}
|
|
3150
|
+
carry = carry ? Buffer.concat([carry, next.value]) : Buffer.from(next.value);
|
|
3151
|
+
while (carry.length >= BINARY_CHUNK_BYTES) {
|
|
3152
|
+
await sendChunk(carry.subarray(0, BINARY_CHUNK_BYTES));
|
|
3153
|
+
carry = carry.subarray(BINARY_CHUNK_BYTES);
|
|
2728
3154
|
}
|
|
2729
3155
|
}
|
|
2730
3156
|
signal.throwIfAborted();
|
|
3157
|
+
if (carry !== void 0 && carry.length > 0) await sendChunk(carry);
|
|
2731
3158
|
ctx.send("s2c.ack", {
|
|
2732
3159
|
sessionId: payload.sessionId,
|
|
2733
|
-
fileName: "dsh-session-log.zip",
|
|
3160
|
+
fileName: format === "markdown" ? "dsh-session.md" : "dsh-session-log.zip",
|
|
2734
3161
|
chunks: index,
|
|
2735
|
-
bytes
|
|
3162
|
+
bytes,
|
|
3163
|
+
...format === void 0 ? {} : { mediaType: format === "markdown" ? "text/markdown; charset=utf-8" : "application/zip" }
|
|
2736
3164
|
});
|
|
2737
3165
|
} catch (error) {
|
|
2738
3166
|
const failure = hostFailureOf(error, "command/export-cancelled");
|
|
@@ -2749,64 +3177,626 @@ const commandRows = [...[
|
|
|
2749
3177
|
}
|
|
2750
3178
|
}
|
|
2751
3179
|
}];
|
|
2752
|
-
|
|
2753
|
-
|
|
2754
|
-
|
|
2755
|
-
|
|
2756
|
-
|
|
2757
|
-
|
|
2758
|
-
|
|
2759
|
-
|
|
2760
|
-
|
|
2761
|
-
|
|
2762
|
-
|
|
2763
|
-
|
|
2764
|
-
|
|
2765
|
-
|
|
2766
|
-
|
|
2767
|
-
|
|
2768
|
-
|
|
2769
|
-
|
|
2770
|
-
|
|
2771
|
-
|
|
2772
|
-
|
|
2773
|
-
|
|
2774
|
-
|
|
2775
|
-
|
|
2776
|
-
|
|
2777
|
-
|
|
2778
|
-
|
|
2779
|
-
|
|
2780
|
-
|
|
2781
|
-
|
|
2782
|
-
}
|
|
2783
|
-
/**
|
|
2784
|
-
|
|
2785
|
-
|
|
2786
|
-
|
|
2787
|
-
|
|
2788
|
-
|
|
2789
|
-
|
|
2790
|
-
|
|
2791
|
-
|
|
2792
|
-
|
|
2793
|
-
|
|
2794
|
-
|
|
2795
|
-
|
|
2796
|
-
|
|
2797
|
-
}
|
|
3180
|
+
// src/plugin-update.ts
|
|
3181
|
+
const PACKAGE = "dsh-deeppilot";
|
|
3182
|
+
const PHASES = [
|
|
3183
|
+
"preparing",
|
|
3184
|
+
"installing",
|
|
3185
|
+
"verifying",
|
|
3186
|
+
"restart-required",
|
|
3187
|
+
"done",
|
|
3188
|
+
"rolled-back",
|
|
3189
|
+
"failed"
|
|
3190
|
+
];
|
|
3191
|
+
const CELLS = Symbol.for("deeppilot.plugin-update.cells.v1");
|
|
3192
|
+
const globals = globalThis;
|
|
3193
|
+
const cells = globals[CELLS] ??= /* @__PURE__ */ new Map();
|
|
3194
|
+
const ownerKey = (deviceId) => createHash("sha256").update(deviceId).digest("hex");
|
|
3195
|
+
const validVersion = (value) => typeof value === "string" && /^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?(?:\+[0-9A-Za-z.-]+)?$/.test(value) && value.length <= 128;
|
|
3196
|
+
const validUpdateRequestId = (value) => typeof value === "string" && /^[A-Za-z0-9._:-]{1,128}$/.test(value);
|
|
3197
|
+
const terminal = (phase) => [
|
|
3198
|
+
"done",
|
|
3199
|
+
"failed",
|
|
3200
|
+
"rolled-back",
|
|
3201
|
+
"restart-required"
|
|
3202
|
+
].includes(phase);
|
|
3203
|
+
const publicRecord = ({ ownerKey: _owner, restartVersion: _restartVersion, restartPhase: _restartPhase, restartBoot: _restartBoot, ...record }) => ({ ...record });
|
|
3204
|
+
var PluginUpdateError = class extends Error {
|
|
3205
|
+
code;
|
|
3206
|
+
constructor(code, message) {
|
|
3207
|
+
super(message);
|
|
3208
|
+
this.code = code;
|
|
3209
|
+
}
|
|
3210
|
+
};
|
|
3211
|
+
/** 精确镜像 alpha2 公开 registryPlan,避免静态依赖让旧宿主无法加载插件;由真实函数对照测试守卫。 */
|
|
3212
|
+
function updateRegistryPlan(requested, configured) {
|
|
3213
|
+
const own = configured.resolved === null ? null : normalizeUpdateRegistry(configured.resolved);
|
|
3214
|
+
const fallbacks = configured.fallbackRegistries.map(normalizeUpdateRegistry);
|
|
3215
|
+
const ownIsPublic = own !== null && (own === "https://registry.npmjs.org/" || fallbacks.includes(own));
|
|
3216
|
+
const keyOf = (registry) => registry === null ? own : normalizeUpdateRegistry(registry);
|
|
3217
|
+
const known = [];
|
|
3218
|
+
const keys = [];
|
|
3219
|
+
for (const registry of [configured.registry, ...configured.fallbackRegistries]) {
|
|
3220
|
+
if (registry === null && !ownIsPublic) continue;
|
|
3221
|
+
const key = keyOf(registry);
|
|
3222
|
+
if (keys.includes(key)) continue;
|
|
3223
|
+
known.push(registry === null ? null : normalizeUpdateRegistry(registry));
|
|
3224
|
+
keys.push(key);
|
|
3225
|
+
}
|
|
3226
|
+
const first = requested === void 0 ? configured.registry : requested;
|
|
3227
|
+
const firstKey = keyOf(first);
|
|
3228
|
+
const normalizedFirst = first === null ? null : normalizeUpdateRegistry(first);
|
|
3229
|
+
if ((first === null || firstKey === own) && !ownIsPublic) return [normalizedFirst];
|
|
3230
|
+
if (!keys.includes(firstKey)) return [normalizedFirst];
|
|
3231
|
+
return [normalizedFirst, ...known.filter((_registry, index) => keys[index] !== firstKey)];
|
|
3232
|
+
}
|
|
3233
|
+
function normalizeUpdateRegistry(registry) {
|
|
3234
|
+
const parsed = new URL(registry);
|
|
3235
|
+
if (parsed.protocol !== "http:" && parsed.protocol !== "https:") throw new Error("registry unavailable");
|
|
3236
|
+
if (!parsed.pathname.endsWith("/")) parsed.pathname += "/";
|
|
3237
|
+
return parsed.href;
|
|
3238
|
+
}
|
|
3239
|
+
function updateRegistrySources(requested, configured) {
|
|
3240
|
+
return [...new Set(updateRegistryPlan(requested, configured).map((registry) => {
|
|
3241
|
+
const resolved = registry === null ? configured.resolved : registry;
|
|
3242
|
+
if (!resolved) throw new Error("registry unresolved");
|
|
3243
|
+
return normalizeUpdateRegistry(resolved);
|
|
3244
|
+
}))];
|
|
3245
|
+
}
|
|
3246
|
+
/** 只接受具有完整规范摘要的强 SRI;宽松 base64 解码不能把截断值或弱摘要当作安装凭据。 */
|
|
3247
|
+
function strongIntegrity(value) {
|
|
3248
|
+
if (typeof value !== "string") return void 0;
|
|
3249
|
+
const match = /^(sha256|sha384|sha512)-([A-Za-z0-9+/]+={0,2})$/.exec(value);
|
|
3250
|
+
if (!match) return void 0;
|
|
3251
|
+
const digest = Buffer.from(match[2], "base64");
|
|
3252
|
+
const bytes = {
|
|
3253
|
+
sha256: 32,
|
|
3254
|
+
sha384: 48,
|
|
3255
|
+
sha512: 64
|
|
3256
|
+
}[match[1]];
|
|
3257
|
+
return digest.length === bytes && digest.toString("base64") === match[2] ? value : void 0;
|
|
3258
|
+
}
|
|
3259
|
+
/** 精确版本事实由元数据读取;软件源地址、上游响应体及包管理器诊断不进入对外错误。 */
|
|
3260
|
+
async function readTargetManifestFacts(registry, version, fetcher) {
|
|
3261
|
+
const base = new URL(registry);
|
|
3262
|
+
if (!["http:", "https:"].includes(base.protocol) || base.username || base.password || base.search || base.hash || !validVersion(version)) throw new Error("registry unavailable");
|
|
3263
|
+
base.pathname = base.pathname.replace(/\/?$/, "/") + encodeURIComponent(PACKAGE) + "/" + encodeURIComponent(version);
|
|
3264
|
+
const response = await fetcher(base, {
|
|
3265
|
+
signal: AbortSignal.timeout(1e4),
|
|
3266
|
+
redirect: "error",
|
|
3267
|
+
headers: { accept: "application/json" }
|
|
3268
|
+
});
|
|
3269
|
+
if (!response.ok || !response.body) throw new Error("manifest unavailable");
|
|
3270
|
+
const reader = response.body.getReader();
|
|
3271
|
+
const chunks = [];
|
|
3272
|
+
let bytes = 0;
|
|
3273
|
+
try {
|
|
3274
|
+
while (true) {
|
|
3275
|
+
const next = await reader.read();
|
|
3276
|
+
if (next.done) break;
|
|
3277
|
+
bytes += next.value.length;
|
|
3278
|
+
if (bytes > 1048576) throw new Error("manifest too large");
|
|
3279
|
+
chunks.push(next.value);
|
|
3280
|
+
}
|
|
3281
|
+
} finally {
|
|
3282
|
+
await reader.cancel().catch(() => {});
|
|
3283
|
+
reader.releaseLock();
|
|
3284
|
+
}
|
|
3285
|
+
const manifest = JSON.parse(Buffer.concat(chunks).toString("utf8"));
|
|
3286
|
+
if (manifest.name !== PACKAGE || manifest.version !== version || !manifest.dsh?.bundle) throw new Error("manifest mismatch");
|
|
3287
|
+
const protocol = manifest.deepPilot?.protocolVersion;
|
|
2798
3288
|
return {
|
|
2799
|
-
|
|
2800
|
-
|
|
3289
|
+
protocolVersion: typeof protocol === "number" && Number.isSafeInteger(protocol) && protocol > 0 ? protocol : void 0,
|
|
3290
|
+
integrity: strongIntegrity(manifest.dist?.integrity)
|
|
2801
3291
|
};
|
|
2802
3292
|
}
|
|
2803
|
-
/**
|
|
2804
|
-
function
|
|
2805
|
-
return
|
|
3293
|
+
/** 检查界面只预览首个软件源的协议;实际安装必须通过全部可回退源的强校验。 */
|
|
3294
|
+
async function readTargetProtocol(registry, version, fetcher = fetch) {
|
|
3295
|
+
return (await readTargetManifestFacts(registry, version, fetcher)).protocolVersion;
|
|
2806
3296
|
}
|
|
2807
|
-
|
|
2808
|
-
|
|
2809
|
-
|
|
3297
|
+
var PluginUpdater = class {
|
|
3298
|
+
options;
|
|
3299
|
+
state;
|
|
3300
|
+
path;
|
|
3301
|
+
listVersions;
|
|
3302
|
+
updateBundle;
|
|
3303
|
+
listBundles;
|
|
3304
|
+
registries;
|
|
3305
|
+
constructor(options) {
|
|
3306
|
+
this.options = options;
|
|
3307
|
+
this.path = resolve(options.journalPath);
|
|
3308
|
+
this.listVersions = options.manager.listVersions.bind(options.manager);
|
|
3309
|
+
this.updateBundle = options.manager.updateBundle.bind(options.manager);
|
|
3310
|
+
this.listBundles = options.manager.listBundles.bind(options.manager);
|
|
3311
|
+
this.registries = options.manager.registries.bind(options.manager);
|
|
3312
|
+
let state = cells.get(this.path);
|
|
3313
|
+
if (!state) {
|
|
3314
|
+
state = {
|
|
3315
|
+
bootId: randomUUID(),
|
|
3316
|
+
records: [],
|
|
3317
|
+
listeners: /* @__PURE__ */ new Set(),
|
|
3318
|
+
runningVersion: options.currentVersion,
|
|
3319
|
+
healthy: true
|
|
3320
|
+
};
|
|
3321
|
+
if (existsSync(this.path)) try {
|
|
3322
|
+
if (statSync(this.path).size > 262144) throw new Error("oversized journal");
|
|
3323
|
+
const data = JSON.parse(readFileSync(this.path, "utf8"));
|
|
3324
|
+
if (data.version !== 1 || !Array.isArray(data.records) || data.records.length > 128 || data.records.some((record) => !record || !validUpdateRequestId(record.clientRequestId) || !validVersion(record.fromVersion) || !validVersion(record.toVersion) || !PHASES.includes(record.phase) || !Number.isSafeInteger(record.startedAt) || record.startedAt < 0 || typeof record.message !== "string" || record.message.length > 1024 || !/^[a-f0-9]{64}$/.test(record.ownerKey) || record.restartVersion !== void 0 && !validVersion(record.restartVersion) || record.restartBoot !== void 0 && (typeof record.restartBoot !== "string" || record.restartBoot.length > 128) || record.restartPhase !== void 0 && record.restartPhase !== "done" && record.restartPhase !== "rolled-back") || new Set(data.records.map((record) => record.clientRequestId)).size !== data.records.length) throw new Error("invalid journal");
|
|
3325
|
+
state.records = data.records;
|
|
3326
|
+
} catch {
|
|
3327
|
+
state.healthy = false;
|
|
3328
|
+
}
|
|
3329
|
+
cells.set(this.path, state);
|
|
3330
|
+
}
|
|
3331
|
+
this.state = state;
|
|
3332
|
+
state.runningVersion = options.currentVersion;
|
|
3333
|
+
}
|
|
3334
|
+
save() {
|
|
3335
|
+
mkdirSync(dirname(this.path), {
|
|
3336
|
+
recursive: true,
|
|
3337
|
+
mode: 448
|
|
3338
|
+
});
|
|
3339
|
+
const temp = this.path + "." + randomUUID() + ".tmp";
|
|
3340
|
+
const fd = openSync(temp, "wx", 384);
|
|
3341
|
+
try {
|
|
3342
|
+
writeFileSync(fd, JSON.stringify({
|
|
3343
|
+
version: 1,
|
|
3344
|
+
records: this.state.records
|
|
3345
|
+
}));
|
|
3346
|
+
fsyncSync(fd);
|
|
3347
|
+
} finally {
|
|
3348
|
+
closeSync(fd);
|
|
3349
|
+
}
|
|
3350
|
+
renameSync(temp, this.path);
|
|
3351
|
+
const directory = openSync(dirname(this.path), "r");
|
|
3352
|
+
try {
|
|
3353
|
+
fsyncSync(directory);
|
|
3354
|
+
} catch (error) {
|
|
3355
|
+
if (![
|
|
3356
|
+
"EPERM",
|
|
3357
|
+
"EISDIR",
|
|
3358
|
+
"EINVAL"
|
|
3359
|
+
].includes(String(error.code))) throw error;
|
|
3360
|
+
} finally {
|
|
3361
|
+
closeSync(directory);
|
|
3362
|
+
}
|
|
3363
|
+
}
|
|
3364
|
+
transition(record, phase, message) {
|
|
3365
|
+
const before = { ...record };
|
|
3366
|
+
record.phase = phase;
|
|
3367
|
+
record.message = message;
|
|
3368
|
+
if (phase === "restart-required") record.restartBoot = this.state.bootId;
|
|
3369
|
+
try {
|
|
3370
|
+
this.save();
|
|
3371
|
+
} catch (error) {
|
|
3372
|
+
Object.assign(record, before);
|
|
3373
|
+
record.restartBoot = before.restartBoot;
|
|
3374
|
+
throw error;
|
|
3375
|
+
}
|
|
3376
|
+
for (const listener of this.state.listeners) try {
|
|
3377
|
+
listener({
|
|
3378
|
+
clientRequestId: record.clientRequestId,
|
|
3379
|
+
phase,
|
|
3380
|
+
message
|
|
3381
|
+
}, record.ownerKey);
|
|
3382
|
+
} catch {}
|
|
3383
|
+
}
|
|
3384
|
+
subscribe(deviceId, listener) {
|
|
3385
|
+
const key = ownerKey(deviceId);
|
|
3386
|
+
const wrapped = (event, owner) => {
|
|
3387
|
+
if (owner === key) listener(event);
|
|
3388
|
+
};
|
|
3389
|
+
this.state.listeners.add(wrapped);
|
|
3390
|
+
return () => {
|
|
3391
|
+
this.state.listeners.delete(wrapped);
|
|
3392
|
+
};
|
|
3393
|
+
}
|
|
3394
|
+
recover() {
|
|
3395
|
+
if (this.state.active || !this.state.healthy) return;
|
|
3396
|
+
for (const record of this.state.records) if (record.phase === "restart-required" && record.restartBoot !== this.state.bootId && this.options.currentVersion === (record.restartVersion ?? record.toVersion)) this.transition(record, record.restartPhase ?? "done", record.restartPhase === "rolled-back" ? "重启后已运行旧版本,宿主回滚完成" : "重启后已运行目标版本,更新完成");
|
|
3397
|
+
else if (!terminal(record.phase)) {
|
|
3398
|
+
if (this.options.currentVersion === record.toVersion) this.transition(record, "done", "已重新连接到目标版本,更新完成");
|
|
3399
|
+
else this.transition(record, "failed", "上次更新已中断,结果无法确认;请检查当前版本,原请求不会自动重试");
|
|
3400
|
+
}
|
|
3401
|
+
}
|
|
3402
|
+
async check(deviceId) {
|
|
3403
|
+
let reason;
|
|
3404
|
+
try {
|
|
3405
|
+
this.recover();
|
|
3406
|
+
} catch {
|
|
3407
|
+
reason = "更新日志保存失败,暂时无法确认更新结果";
|
|
3408
|
+
}
|
|
3409
|
+
if (!this.state.healthy) reason = "更新日志无法读取,已停用安装以保护上次更新记录";
|
|
3410
|
+
const finish = (status) => {
|
|
3411
|
+
const journal = [...this.state.records].reverse().find((record) => record.ownerKey === ownerKey(deviceId));
|
|
3412
|
+
return {
|
|
3413
|
+
...status,
|
|
3414
|
+
...journal ? { journal: publicRecord(journal) } : {}
|
|
3415
|
+
};
|
|
3416
|
+
};
|
|
3417
|
+
const base = {
|
|
3418
|
+
currentVersion: this.options.currentVersion,
|
|
3419
|
+
protocolVersion: 2,
|
|
3420
|
+
latestVersion: null,
|
|
3421
|
+
updateAvailable: false,
|
|
3422
|
+
compatible: false,
|
|
3423
|
+
protocolCompatible: false,
|
|
3424
|
+
versions: [],
|
|
3425
|
+
checkedAt: Date.now()
|
|
3426
|
+
};
|
|
3427
|
+
try {
|
|
3428
|
+
const listed = await this.listVersions(PACKAGE);
|
|
3429
|
+
if (listed.status !== "listed") return finish({
|
|
3430
|
+
...base,
|
|
3431
|
+
reason: reason ?? "暂时无法读取可更新版本,请稍后重试"
|
|
3432
|
+
});
|
|
3433
|
+
const registry = listed.registry ?? (await this.registries()).resolved;
|
|
3434
|
+
if (!registry) return finish({
|
|
3435
|
+
...base,
|
|
3436
|
+
reason: reason ?? "无法确定软件源,暂时不能核对目标版本协议"
|
|
3437
|
+
});
|
|
3438
|
+
const versions = await Promise.all(listed.versions.slice(0, 10).map(async (entry) => {
|
|
3439
|
+
let protocolVersion;
|
|
3440
|
+
try {
|
|
3441
|
+
protocolVersion = await readTargetProtocol(registry, entry.version, this.options.fetch);
|
|
3442
|
+
} catch {}
|
|
3443
|
+
return {
|
|
3444
|
+
version: entry.version,
|
|
3445
|
+
compatible: entry.bundle && entry.incompatible === void 0,
|
|
3446
|
+
...protocolVersion === void 0 ? {} : { protocolVersion }
|
|
3447
|
+
};
|
|
3448
|
+
}));
|
|
3449
|
+
const latest = versions[0];
|
|
3450
|
+
return finish({
|
|
3451
|
+
...base,
|
|
3452
|
+
latestVersion: latest?.version ?? null,
|
|
3453
|
+
updateAvailable: versions.length > 0,
|
|
3454
|
+
compatible: latest?.compatible ?? false,
|
|
3455
|
+
protocolCompatible: latest?.protocolVersion === 2,
|
|
3456
|
+
versions,
|
|
3457
|
+
...reason ? { reason } : versions.some((entry) => entry.protocolVersion === void 0) ? { reason: "部分目标版本缺少可核验的协议声明,暂时不可安装" } : {}
|
|
3458
|
+
});
|
|
3459
|
+
} catch {
|
|
3460
|
+
return finish({
|
|
3461
|
+
...base,
|
|
3462
|
+
reason: reason ?? "暂时无法检查更新,请确认网络后重试"
|
|
3463
|
+
});
|
|
3464
|
+
}
|
|
3465
|
+
}
|
|
3466
|
+
/** The apply frame is the phone's explicit confirmation. check() never mutates packages. */
|
|
3467
|
+
async apply(deviceId, version, clientRequestId) {
|
|
3468
|
+
if (!validVersion(version) || !validUpdateRequestId(clientRequestId)) throw new PluginUpdateError("E_PROTOCOL", "更新版本或请求标识无效");
|
|
3469
|
+
if (!this.state.healthy) throw new PluginUpdateError("E_INTERNAL", "更新日志无法读取,暂时不能安装");
|
|
3470
|
+
const previous = this.state.records.find((record) => record.clientRequestId === clientRequestId);
|
|
3471
|
+
if (previous) {
|
|
3472
|
+
if (previous.ownerKey !== ownerKey(deviceId)) throw new PluginUpdateError("E_FORBIDDEN", "更新请求不属于当前设备");
|
|
3473
|
+
if (previous.toVersion !== version) throw new PluginUpdateError("E_PROTOCOL", "同一更新请求不能更换目标版本");
|
|
3474
|
+
this.recover();
|
|
3475
|
+
return publicRecord(previous);
|
|
3476
|
+
}
|
|
3477
|
+
const pending = this.state.pending;
|
|
3478
|
+
if (pending?.record.clientRequestId === clientRequestId) {
|
|
3479
|
+
if (pending.record.ownerKey !== ownerKey(deviceId)) throw new PluginUpdateError("E_FORBIDDEN", "更新请求不属于当前设备");
|
|
3480
|
+
if (pending.record.toVersion !== version) throw new PluginUpdateError("E_PROTOCOL", "同一更新请求不能更换目标版本");
|
|
3481
|
+
return pending.accepted;
|
|
3482
|
+
}
|
|
3483
|
+
if (this.state.active) throw new PluginUpdateError("E_BUSY", "已有插件更新正在进行,请稍后检查结果");
|
|
3484
|
+
if (this.state.records.length >= 128) throw new PluginUpdateError("E_INTERNAL", "更新记录已满,请在电脑端检查后再更新");
|
|
3485
|
+
const record = {
|
|
3486
|
+
clientRequestId,
|
|
3487
|
+
fromVersion: this.options.currentVersion,
|
|
3488
|
+
toVersion: version,
|
|
3489
|
+
phase: "preparing",
|
|
3490
|
+
startedAt: Date.now(),
|
|
3491
|
+
message: "目标校验已通过,准备开始安装",
|
|
3492
|
+
ownerKey: ownerKey(deviceId)
|
|
3493
|
+
};
|
|
3494
|
+
let accept;
|
|
3495
|
+
let refuse;
|
|
3496
|
+
const accepted = new Promise((resolve_, reject_) => {
|
|
3497
|
+
accept = resolve_;
|
|
3498
|
+
refuse = reject_;
|
|
3499
|
+
});
|
|
3500
|
+
this.state.pending = {
|
|
3501
|
+
record,
|
|
3502
|
+
accepted
|
|
3503
|
+
};
|
|
3504
|
+
const operation = Promise.resolve().then(async () => {
|
|
3505
|
+
let registry;
|
|
3506
|
+
try {
|
|
3507
|
+
registry = await this.prepare(record);
|
|
3508
|
+
this.state.records.push(record);
|
|
3509
|
+
try {
|
|
3510
|
+
this.save();
|
|
3511
|
+
} catch {
|
|
3512
|
+
this.state.records.splice(this.state.records.indexOf(record), 1);
|
|
3513
|
+
throw new PluginUpdateError("E_INTERNAL", "更新日志保存失败,尚未开始安装");
|
|
3514
|
+
}
|
|
3515
|
+
} catch (error) {
|
|
3516
|
+
if (this.state.active === operation) this.state.active = void 0;
|
|
3517
|
+
if (this.state.pending?.record === record) this.state.pending = void 0;
|
|
3518
|
+
refuse(error instanceof PluginUpdateError ? error : new PluginUpdateError("E_INTERNAL", "无法核对目标版本,尚未开始安装"));
|
|
3519
|
+
return;
|
|
3520
|
+
}
|
|
3521
|
+
if (this.state.pending?.record === record) this.state.pending = void 0;
|
|
3522
|
+
accept(publicRecord(record));
|
|
3523
|
+
await new Promise((resolve_) => setImmediate(resolve_));
|
|
3524
|
+
try {
|
|
3525
|
+
await this.run(record, registry);
|
|
3526
|
+
} catch {
|
|
3527
|
+
try {
|
|
3528
|
+
this.transition(record, "failed", "更新结果无法确认,请检查当前版本;原请求不会自动重试");
|
|
3529
|
+
} catch {}
|
|
3530
|
+
}
|
|
3531
|
+
}).finally(() => {
|
|
3532
|
+
if (this.state.active === operation) this.state.active = void 0;
|
|
3533
|
+
});
|
|
3534
|
+
this.state.active = operation;
|
|
3535
|
+
return accepted;
|
|
3536
|
+
}
|
|
3537
|
+
/** Useful to isolated tests; production never waits for this from disposal. */
|
|
3538
|
+
async settled() {
|
|
3539
|
+
await this.state.active;
|
|
3540
|
+
}
|
|
3541
|
+
/** 所有可执行目标校验在受理前完成;宿主安装及其回滚处理仍属于独立的后续任务。 */
|
|
3542
|
+
async prepare(record) {
|
|
3543
|
+
let listed;
|
|
3544
|
+
try {
|
|
3545
|
+
listed = await this.listVersions(PACKAGE);
|
|
3546
|
+
} catch {
|
|
3547
|
+
throw new PluginUpdateError("E_INTERNAL", "无法核对可更新版本,尚未开始安装");
|
|
3548
|
+
}
|
|
3549
|
+
if (listed.status !== "listed" || listed.installed !== record.fromVersion) throw new PluginUpdateError("E_PROTOCOL", "安装版本已变化或无法读取,请重新检查更新");
|
|
3550
|
+
const target = listed.versions.find((entry) => entry.version === record.toVersion);
|
|
3551
|
+
if (!target?.bundle || target.incompatible !== void 0) throw new PluginUpdateError("E_UNSUPPORTED", "目标版本不存在或不兼容当前宿主,尚未开始安装");
|
|
3552
|
+
try {
|
|
3553
|
+
const sources = updateRegistrySources(listed.registry, await this.registries());
|
|
3554
|
+
let integrity;
|
|
3555
|
+
for (const source of sources) {
|
|
3556
|
+
const facts = await readTargetManifestFacts(source, record.toVersion, this.options.fetch ?? fetch);
|
|
3557
|
+
if (facts.protocolVersion !== 2) throw new PluginUpdateError("E_UNSUPPORTED", "候选软件源的目标协议不兼容或缺少声明,尚未开始安装");
|
|
3558
|
+
if (!facts.integrity) throw new PluginUpdateError("E_UNSUPPORTED", "候选软件源缺少有效的强完整性摘要,尚未开始安装");
|
|
3559
|
+
if (integrity !== void 0 && facts.integrity !== integrity) throw new PluginUpdateError("E_UNSUPPORTED", "候选软件源的目标包完整性摘要不一致,尚未开始安装");
|
|
3560
|
+
integrity = facts.integrity;
|
|
3561
|
+
}
|
|
3562
|
+
const current = updateRegistrySources(listed.registry, await this.registries());
|
|
3563
|
+
if (JSON.stringify(current) !== JSON.stringify(sources)) throw new PluginUpdateError("E_BUSY", "软件源配置已变化,请重新检查更新");
|
|
3564
|
+
} catch (error) {
|
|
3565
|
+
if (error instanceof PluginUpdateError) throw error;
|
|
3566
|
+
throw new PluginUpdateError("E_INTERNAL", "无法核验全部候选软件源的目标协议与完整性,尚未开始安装");
|
|
3567
|
+
}
|
|
3568
|
+
return listed.registry;
|
|
3569
|
+
}
|
|
3570
|
+
async run(record, registry) {
|
|
3571
|
+
this.transition(record, "installing", "宿主正在安装目标版本,连接可能暂时中断");
|
|
3572
|
+
const outcome = await this.updateBundle(PACKAGE, record.toVersion, {
|
|
3573
|
+
registry,
|
|
3574
|
+
requestId: record.clientRequestId
|
|
3575
|
+
});
|
|
3576
|
+
const result = outcome.result;
|
|
3577
|
+
if (outcome.from !== record.fromVersion || outcome.to !== record.toVersion) return this.transition(record, "failed", "宿主返回的更新版本不一致,请检查当前安装");
|
|
3578
|
+
if (outcome.restore) {
|
|
3579
|
+
const restored = outcome.restore;
|
|
3580
|
+
if (restored.application === "restart-required") {
|
|
3581
|
+
record.restartVersion = record.fromVersion;
|
|
3582
|
+
record.restartPhase = "rolled-back";
|
|
3583
|
+
return this.transition(record, "restart-required", "目标安装失败,旧版已重新安装;请在电脑端重启 DSH 后确认恢复");
|
|
3584
|
+
}
|
|
3585
|
+
if (restored.application === "applied" && (restored.version === void 0 || restored.version === record.fromVersion)) {
|
|
3586
|
+
if ((await this.listBundles()).find((bundle) => bundle.name === PACKAGE)?.version === record.fromVersion) return this.transition(record, "rolled-back", "目标安装失败,宿主已自动恢复旧版本");
|
|
3587
|
+
return this.transition(record, "failed", "宿主已尝试回滚,但无法核对实际旧版本,请在电脑端检查插件");
|
|
3588
|
+
}
|
|
3589
|
+
return this.transition(record, "failed", "目标安装失败,宿主恢复旧版也未完成,请在电脑端检查插件");
|
|
3590
|
+
}
|
|
3591
|
+
if (outcome.step !== "install" || result.application === "failed" || result.application === "cancelled" || result.application === "overridden") return this.transition(record, "failed", installFailureMessage(result));
|
|
3592
|
+
if (result.version !== void 0 && result.version !== record.toVersion) return this.transition(record, "failed", "实际安装版本与目标不一致,请在电脑端检查");
|
|
3593
|
+
if (result.application === "restart-required") return this.transition(record, "restart-required", "目标版本已安装,宿主需要重启;请在电脑端重启 DSH 后重新连接");
|
|
3594
|
+
this.transition(record, "verifying", "正在核对实际安装版本");
|
|
3595
|
+
if ((await this.listBundles()).find((bundle) => bundle.name === PACKAGE)?.version !== record.toVersion) return this.transition(record, "failed", "无法确认实际安装版本,请在电脑端检查");
|
|
3596
|
+
if (this.state.runningVersion !== record.toVersion) return this.transition(record, "restart-required", "目标版本已安装,当前插件尚未重新加载;请在电脑端重启 DSH 后重新连接");
|
|
3597
|
+
this.transition(record, "done", "目标版本已加载,更新完成");
|
|
3598
|
+
}
|
|
3599
|
+
};
|
|
3600
|
+
function installFailureMessage(result) {
|
|
3601
|
+
if (result.error?.code === "incompatible-version" || result.error?.incompatible?.length || result.packageResult?.incompatible?.length) return "目标版本不兼容当前宿主,安装已拒绝(incompatible-version)";
|
|
3602
|
+
if (result.pendingBuilds?.length || result.packageResult?.kind === "build-blocked") return "安装需要在电脑端明确批准构建脚本,更新未完成(build-blocked)";
|
|
3603
|
+
const kind = result.packageResult?.kind;
|
|
3604
|
+
const reasons = {
|
|
3605
|
+
"pnpm-missing": "电脑端缺少包管理器,更新未完成",
|
|
3606
|
+
timeout: "安装等待超时,请检查电脑端后重试",
|
|
3607
|
+
"not-found": "软件源尚未提供目标包,更新未完成",
|
|
3608
|
+
"no-matching-version": "软件源尚未提供目标版本,更新未完成",
|
|
3609
|
+
network: "安装无法连接软件源,请恢复网络后重新检查",
|
|
3610
|
+
"disk-full": "电脑端磁盘空间不足,更新未完成",
|
|
3611
|
+
permission: "电脑端没有写入插件的权限,更新未完成",
|
|
3612
|
+
integrity: "目标包完整性校验失败,更新未完成"
|
|
3613
|
+
};
|
|
3614
|
+
if (kind && reasons[kind]) return `${reasons[kind]}(${kind})`;
|
|
3615
|
+
if (result.application === "cancelled") return "宿主已取消目标版本安装,请检查当前版本";
|
|
3616
|
+
return "宿主未完成目标版本安装,请检查当前版本";
|
|
3617
|
+
}
|
|
3618
|
+
function supportsPluginUpdate(value) {
|
|
3619
|
+
if (!value || typeof value !== "object") return false;
|
|
3620
|
+
return [
|
|
3621
|
+
"listVersions",
|
|
3622
|
+
"updateBundle",
|
|
3623
|
+
"listBundles",
|
|
3624
|
+
"registries"
|
|
3625
|
+
].every((key) => typeof value[key] === "function");
|
|
3626
|
+
}
|
|
3627
|
+
// src/wire-plugin-update.ts
|
|
3628
|
+
function payloadObject$1(value) {
|
|
3629
|
+
if (value === void 0 || value === null) return {
|
|
3630
|
+
ok: true,
|
|
3631
|
+
value: {}
|
|
3632
|
+
};
|
|
3633
|
+
if (typeof value !== "object" || Array.isArray(value)) return {
|
|
3634
|
+
ok: false,
|
|
3635
|
+
code: "E_PROTOCOL",
|
|
3636
|
+
message: "更新请求必须是对象"
|
|
3637
|
+
};
|
|
3638
|
+
return {
|
|
3639
|
+
ok: true,
|
|
3640
|
+
value
|
|
3641
|
+
};
|
|
3642
|
+
}
|
|
3643
|
+
const updaters = /* @__PURE__ */ new WeakMap();
|
|
3644
|
+
/** Bridge replacement and optional host service discovery can arrive in either order. */
|
|
3645
|
+
function setPluginUpdater(bridge, updater) {
|
|
3646
|
+
if (updater) updaters.set(bridge, updater);
|
|
3647
|
+
else updaters.delete(bridge);
|
|
3648
|
+
}
|
|
3649
|
+
const observers = /* @__PURE__ */ new WeakMap();
|
|
3650
|
+
function observe(ctx, updater, deviceId) {
|
|
3651
|
+
if (!ctx.canReceive("sessions.manage")) return;
|
|
3652
|
+
const signal = ctx.signal;
|
|
3653
|
+
if (signal?.aborted || signal && observers.get(signal)?.has(updater)) return;
|
|
3654
|
+
let stop = () => {};
|
|
3655
|
+
const unsubscribe = updater.subscribe(deviceId, (event) => {
|
|
3656
|
+
if (!ctx.canReceive("sessions.manage")) {
|
|
3657
|
+
stop();
|
|
3658
|
+
return;
|
|
3659
|
+
}
|
|
3660
|
+
ctx.send("s2c.plugin.update.progress", event);
|
|
3661
|
+
if (!signal && finished(event.phase)) stop();
|
|
3662
|
+
});
|
|
3663
|
+
stop = () => {
|
|
3664
|
+
unsubscribe();
|
|
3665
|
+
signal?.removeEventListener("abort", stop);
|
|
3666
|
+
if (signal) observers.get(signal)?.delete(updater);
|
|
3667
|
+
};
|
|
3668
|
+
if (signal) {
|
|
3669
|
+
let connections = observers.get(signal);
|
|
3670
|
+
if (!connections) {
|
|
3671
|
+
connections = /* @__PURE__ */ new Set();
|
|
3672
|
+
observers.set(signal, connections);
|
|
3673
|
+
}
|
|
3674
|
+
connections.add(updater);
|
|
3675
|
+
signal.addEventListener("abort", stop, { once: true });
|
|
3676
|
+
}
|
|
3677
|
+
}
|
|
3678
|
+
const finished = (phase) => phase === void 0 || [
|
|
3679
|
+
"done",
|
|
3680
|
+
"failed",
|
|
3681
|
+
"rolled-back",
|
|
3682
|
+
"restart-required"
|
|
3683
|
+
].includes(phase);
|
|
3684
|
+
const pluginUpdateRows = [{
|
|
3685
|
+
type: "c2s.plugin.update.check",
|
|
3686
|
+
stage: "authenticated",
|
|
3687
|
+
scopes: ["sessions.read"],
|
|
3688
|
+
widgetPolicy: "deny",
|
|
3689
|
+
capability: "pluginUpdate",
|
|
3690
|
+
doc: "PROTOCOL.md c2s.plugin.update.check",
|
|
3691
|
+
validate: payloadObject$1,
|
|
3692
|
+
handle: async (ctx) => {
|
|
3693
|
+
const updater = updaters.get(ctx.bridge);
|
|
3694
|
+
if (!updater) return ctx.fail("E_UNSUPPORTED", "当前宿主不支持插件自更新");
|
|
3695
|
+
if (!ctx.deviceId) return ctx.fail("E_FORBIDDEN", "请先完成设备认证");
|
|
3696
|
+
observe(ctx, updater, ctx.deviceId);
|
|
3697
|
+
const status = await updater.check(ctx.deviceId);
|
|
3698
|
+
ctx.send("s2c.plugin.update.status", status);
|
|
3699
|
+
}
|
|
3700
|
+
}, {
|
|
3701
|
+
type: "c2s.plugin.update.apply",
|
|
3702
|
+
stage: "authenticated",
|
|
3703
|
+
scopes: ["sessions.manage"],
|
|
3704
|
+
widgetPolicy: "deny",
|
|
3705
|
+
capability: "pluginUpdate",
|
|
3706
|
+
doc: "PROTOCOL.md c2s.plugin.update.apply",
|
|
3707
|
+
validate: (value) => {
|
|
3708
|
+
const result = payloadObject$1(value);
|
|
3709
|
+
if (!result.ok) return result;
|
|
3710
|
+
const version = result.value.version;
|
|
3711
|
+
if (typeof version !== "string" || version.length > 128 || version.trim().length === 0 || !validUpdateRequestId(result.value.clientRequestId)) return {
|
|
3712
|
+
ok: false,
|
|
3713
|
+
code: "E_PROTOCOL",
|
|
3714
|
+
message: "更新版本或请求标识无效"
|
|
3715
|
+
};
|
|
3716
|
+
return result;
|
|
3717
|
+
},
|
|
3718
|
+
handle: async (ctx, payload) => {
|
|
3719
|
+
const updater = updaters.get(ctx.bridge);
|
|
3720
|
+
if (!updater) return ctx.fail("E_UNSUPPORTED", "当前宿主不支持插件自更新");
|
|
3721
|
+
if (!ctx.deviceId) return ctx.fail("E_FORBIDDEN", "请先完成设备认证");
|
|
3722
|
+
try {
|
|
3723
|
+
const record = await updater.apply(ctx.deviceId, payload.version, payload.clientRequestId);
|
|
3724
|
+
ctx.send("s2c.ack", {
|
|
3725
|
+
clientRequestId: record.clientRequestId,
|
|
3726
|
+
accepted: true
|
|
3727
|
+
});
|
|
3728
|
+
if (ctx.canReceive("sessions.manage")) {
|
|
3729
|
+
ctx.send("s2c.plugin.update.progress", {
|
|
3730
|
+
clientRequestId: record.clientRequestId,
|
|
3731
|
+
phase: record.phase,
|
|
3732
|
+
message: record.message
|
|
3733
|
+
});
|
|
3734
|
+
if (!finished(record.phase)) observe(ctx, updater, ctx.deviceId);
|
|
3735
|
+
}
|
|
3736
|
+
} catch (error) {
|
|
3737
|
+
if (error instanceof PluginUpdateError) return ctx.fail(error.code, error.message);
|
|
3738
|
+
ctx.fail("E_INTERNAL", "暂时无法启动插件更新,请检查当前版本");
|
|
3739
|
+
}
|
|
3740
|
+
}
|
|
3741
|
+
}];
|
|
3742
|
+
/** 第一个缺失的 scope;消息与迁移前一致(`scope <name> required`)。 */
|
|
3743
|
+
function scopeRejection(row, scopes) {
|
|
3744
|
+
for (const scope of row.scopes) if (!scopes.has(scope)) return {
|
|
3745
|
+
code: "E_FORBIDDEN",
|
|
3746
|
+
message: `scope ${scope} required`
|
|
3747
|
+
};
|
|
3748
|
+
}
|
|
3749
|
+
/** 能力位关闭时拒绝。校验先于本检查,与迁移前的次序一致。 */
|
|
3750
|
+
function capabilityRejection(row, capabilities) {
|
|
3751
|
+
if (row.capability === void 0) return void 0;
|
|
3752
|
+
if (capabilities[row.capability] === true) return void 0;
|
|
3753
|
+
return {
|
|
3754
|
+
code: "E_UNSUPPORTED",
|
|
3755
|
+
message: "capability unavailable on this host version"
|
|
3756
|
+
};
|
|
3757
|
+
}
|
|
3758
|
+
function widgetPolicyOf(row) {
|
|
3759
|
+
return row.widgetPolicy ?? "deny";
|
|
3760
|
+
}
|
|
3761
|
+
/** 校验一帧载荷;未登记 validate 的行原样通过。 */
|
|
3762
|
+
function validatePayload(row, payload) {
|
|
3763
|
+
if (row.validate === void 0) return {
|
|
3764
|
+
ok: true,
|
|
3765
|
+
value: payload ?? {}
|
|
3766
|
+
};
|
|
3767
|
+
return row.validate(payload);
|
|
3768
|
+
}
|
|
3769
|
+
/** 执行一帧。检查(阶段/scope/能力/校验)由 connection 按序先行完成。 */
|
|
3770
|
+
async function dispatchFrame(row, ctx, payload) {
|
|
3771
|
+
await row.handle(ctx, payload);
|
|
3772
|
+
}
|
|
3773
|
+
/**
|
|
3774
|
+
* 载荷必须是普通对象。此前这条判定住在 request-validation 里,对
|
|
3775
|
+
* `c2s.sessions.list` 等无参帧豁免、对 `c2s.sessions.archived` 却不通融
|
|
3776
|
+
* (同语义两种待遇,见 G2);现在豁免由各行的 validate 自己声明。
|
|
3777
|
+
*/
|
|
3778
|
+
function payloadObject(value) {
|
|
3779
|
+
if (value === void 0 || value === null) return {
|
|
3780
|
+
ok: true,
|
|
3781
|
+
value: {}
|
|
3782
|
+
};
|
|
3783
|
+
if (typeof value !== "object" || Array.isArray(value)) return {
|
|
3784
|
+
ok: false,
|
|
3785
|
+
code: "E_PROTOCOL",
|
|
3786
|
+
message: "payload must be an object"
|
|
3787
|
+
};
|
|
3788
|
+
return {
|
|
3789
|
+
ok: true,
|
|
3790
|
+
value
|
|
3791
|
+
};
|
|
3792
|
+
}
|
|
3793
|
+
/** 字符串字段:类型正确、不超长、非空(可放宽)。 */
|
|
3794
|
+
function isText(value, max = 4096, nonempty = true) {
|
|
3795
|
+
return typeof value === "string" && value.length <= max && (!nonempty || value.trim().length > 0);
|
|
3796
|
+
}
|
|
3797
|
+
/** 可选字段:缺省合法,给出时必须满足 check。 */
|
|
3798
|
+
function isOptionalField(payload, key, check) {
|
|
3799
|
+
return payload[key] === void 0 || check(payload[key]);
|
|
2810
3800
|
}
|
|
2811
3801
|
function isInteger(value, min, max) {
|
|
2812
3802
|
return typeof value === "number" && Number.isSafeInteger(value) && value >= min && value <= max;
|
|
@@ -2828,6 +3818,7 @@ function accept(value) {
|
|
|
2828
3818
|
const WIRE_FRAME_ROWS = [
|
|
2829
3819
|
...sessionRows,
|
|
2830
3820
|
...commandRows,
|
|
3821
|
+
...pluginUpdateRows,
|
|
2831
3822
|
...scheduleRows,
|
|
2832
3823
|
...interactionRows,
|
|
2833
3824
|
...pushRows,
|
|
@@ -2837,8 +3828,7 @@ const ROWS_BY_TYPE = new Map(WIRE_FRAME_ROWS.map((row) => [row.type, row]));
|
|
|
2837
3828
|
function registryRowFor(type) {
|
|
2838
3829
|
return ROWS_BY_TYPE.get(type);
|
|
2839
3830
|
}
|
|
2840
|
-
|
|
2841
|
-
//#region src/connection-gate.ts
|
|
3831
|
+
// src/connection-gate.ts
|
|
2842
3832
|
/**
|
|
2843
3833
|
* 连接门(ConnectionGate)——「一个匿名 socket 被允许做什么」的唯一属主。
|
|
2844
3834
|
*
|
|
@@ -3130,6 +4120,7 @@ var ConnectionGate = class {
|
|
|
3130
4120
|
debug: host.debug,
|
|
3131
4121
|
log: (message) => host.log(message),
|
|
3132
4122
|
send: (type, payload) => host.send(type, payload, env.id),
|
|
4123
|
+
...host.sendBinary ? { sendBinary: (type, payload, body) => host.sendBinary(type, payload, body, env.id) } : {},
|
|
3133
4124
|
fail: (code, message) => host.fail(env.id, code, message),
|
|
3134
4125
|
close: (code, reason) => host.close(code, reason),
|
|
3135
4126
|
canReceive: (scope) => host.canReceive(scope),
|
|
@@ -3157,8 +4148,7 @@ var ConnectionGate = class {
|
|
|
3157
4148
|
};
|
|
3158
4149
|
}
|
|
3159
4150
|
};
|
|
3160
|
-
|
|
3161
|
-
//#region src/connection.ts
|
|
4151
|
+
// src/connection.ts
|
|
3162
4152
|
/**
|
|
3163
4153
|
* One connected phone. Implements BridgeSink so the HostBridge can push
|
|
3164
4154
|
* projected frames and replays.
|
|
@@ -3230,6 +4220,7 @@ var BridgeConnection = class {
|
|
|
3230
4220
|
},
|
|
3231
4221
|
log: (message) => deps.log(message),
|
|
3232
4222
|
send: (type, payload, id) => connection.send(type, payload, id),
|
|
4223
|
+
sendBinary: (type, payload, body, id) => connection.send(type, payload, id, void 0, body),
|
|
3233
4224
|
fail: (id, code, message) => connection.fail(id, code, message),
|
|
3234
4225
|
close: (code, reason) => connection.close(code, reason),
|
|
3235
4226
|
canReceive: (scope) => connection.canReceive(scope),
|
|
@@ -3323,7 +4314,7 @@ var BridgeConnection = class {
|
|
|
3323
4314
|
...payload,
|
|
3324
4315
|
hostAudience: this.deps.audience
|
|
3325
4316
|
};
|
|
3326
|
-
if (type === "s2c.session.event") {
|
|
4317
|
+
if (type === "s2c.session.event" || type === "s2c.session.queue") {
|
|
3327
4318
|
const sessionId = payload?.sessionId;
|
|
3328
4319
|
if (typeof sessionId === "string") {
|
|
3329
4320
|
const buffered = this.openingSessionEvents.get(sessionId);
|
|
@@ -3399,7 +4390,8 @@ var BridgeConnection = class {
|
|
|
3399
4390
|
this.ws.terminate();
|
|
3400
4391
|
}
|
|
3401
4392
|
}
|
|
3402
|
-
|
|
4393
|
+
/** `body` 存在时按二进制帧发送(见 binary-frame.ts),头部仍是同一个 envelope。 */
|
|
4394
|
+
send(type, payload, id, seq, body) {
|
|
3403
4395
|
const envelope = {
|
|
3404
4396
|
v: 2,
|
|
3405
4397
|
type,
|
|
@@ -3413,7 +4405,7 @@ var BridgeConnection = class {
|
|
|
3413
4405
|
this.close(1013, "client too slow");
|
|
3414
4406
|
return;
|
|
3415
4407
|
}
|
|
3416
|
-
this.ws.send(JSON.stringify(envelope));
|
|
4408
|
+
this.ws.send(body === void 0 ? JSON.stringify(envelope) : encodeBinaryFrame(envelope, body));
|
|
3417
4409
|
}
|
|
3418
4410
|
fail(id, code, message) {
|
|
3419
4411
|
this.send("s2c.error", {
|
|
@@ -3447,8 +4439,30 @@ var BridgeConnection = class {
|
|
|
3447
4439
|
function normalizeFunnelConnectionLimit(value) {
|
|
3448
4440
|
return typeof value === "number" && Number.isInteger(value) && value >= 1 && value <= 16 ? value : 8;
|
|
3449
4441
|
}
|
|
3450
|
-
|
|
3451
|
-
|
|
4442
|
+
// src/tailscale-version.ts
|
|
4443
|
+
/**
|
|
4444
|
+
* 内嵌 helper 上报的 tailscale.com 模块版本。
|
|
4445
|
+
*
|
|
4446
|
+
* 版本的唯一来源是 helper 启动事件(`helper/main.go` 读 Go 构建信息里的
|
|
4447
|
+
* `tailscale.com` 依赖版本),不是 go.mod、也不是插件常量:用户可能通过
|
|
4448
|
+
* `remote.helperPath` 换成自己编译的 helper,界面必须显示实际在跑的那一个。
|
|
4449
|
+
*
|
|
4450
|
+
* 两条边界都要守卫:
|
|
4451
|
+
* - 宿主侧 `parseHelperEvent` 把 helper 的 stdout 当成不可信输入,格式不对
|
|
4452
|
+
* 就丢弃,避免把任意文本渲染进设置页;
|
|
4453
|
+
* - 设置页侧 `reportSchema` 同样校验 Host RPC 返回值,旧 Host 不带该字段
|
|
4454
|
+
* 时保持 undefined(页面显示「未获取」),不当作错误。
|
|
4455
|
+
*
|
|
4456
|
+
* 接受 Go 模块版本形态:`v1.104.2`、`v1.104.2-pre.1`、
|
|
4457
|
+
* `v0.0.0-20260101000000-abcdef123456`。长度上限防止异常输入撑爆界面。
|
|
4458
|
+
*/
|
|
4459
|
+
const TAILSCALE_VERSION_PATTERN = /^v?\d+\.\d+\.\d+(?:[-+][0-9A-Za-z.-]{1,64})?$/;
|
|
4460
|
+
const MAX_TAILSCALE_VERSION_LENGTH = 80;
|
|
4461
|
+
/** 判断外部输入是否是合法的 Tailscale 模块版本字符串。 */
|
|
4462
|
+
function isTailscaleVersion(value) {
|
|
4463
|
+
return typeof value === "string" && value.length <= MAX_TAILSCALE_VERSION_LENGTH && TAILSCALE_VERSION_PATTERN.test(value);
|
|
4464
|
+
}
|
|
4465
|
+
// src/remote-supervisor.ts
|
|
3452
4466
|
const RESTART_DELAYS_MS = [
|
|
3453
4467
|
1e3,
|
|
3454
4468
|
2e3,
|
|
@@ -3636,7 +4650,8 @@ function parseHelperEvent(line) {
|
|
|
3636
4650
|
phase: value.phase,
|
|
3637
4651
|
...typeof value.publicURL === "string" ? { publicURL: value.publicURL } : {},
|
|
3638
4652
|
...typeof value.authURL === "string" ? { authURL: value.authURL } : {},
|
|
3639
|
-
...typeof value.message === "string" ? { message: value.message.slice(0, 500) } : {}
|
|
4653
|
+
...typeof value.message === "string" ? { message: value.message.slice(0, 500) } : {},
|
|
4654
|
+
...isTailscaleVersion(value.tailscaleVersion) ? { tailscaleVersion: value.tailscaleVersion } : {}
|
|
3640
4655
|
};
|
|
3641
4656
|
} catch {
|
|
3642
4657
|
return null;
|
|
@@ -3876,8 +4891,7 @@ var RemoteSupervisor = class {
|
|
|
3876
4891
|
else if (next.phase === "error" || next.phase === "unavailable") this.options.log(`remote Funnel ${next.phase}: ${next.message ?? "unknown error"}`);
|
|
3877
4892
|
}
|
|
3878
4893
|
};
|
|
3879
|
-
|
|
3880
|
-
//#region src/local-policy.ts
|
|
4894
|
+
// src/local-policy.ts
|
|
3881
4895
|
const DEFAULT_LOCAL_PORT = 3098;
|
|
3882
4896
|
const MIN_LOCAL_PORT = 1024;
|
|
3883
4897
|
const MAX_LOCAL_PORT = 65535;
|
|
@@ -3895,8 +4909,7 @@ function localListenError(error, port) {
|
|
|
3895
4909
|
if (code === "EACCES") return `permission denied while opening local port ${port}`;
|
|
3896
4910
|
return error instanceof Error ? error.message : String(error);
|
|
3897
4911
|
}
|
|
3898
|
-
|
|
3899
|
-
//#region src/config.ts
|
|
4912
|
+
// src/config.ts
|
|
3900
4913
|
/** Operator-run relay used by distributed builds; overridable via config. */
|
|
3901
4914
|
const DEFAULT_RELAY_URL = "https://pilot.hailab.dev";
|
|
3902
4915
|
const Config = z.object({
|
|
@@ -3985,8 +4998,7 @@ function normalizeOptions(options) {
|
|
|
3985
4998
|
if (options && typeof options === "object") return plainConfig(options);
|
|
3986
4999
|
return plainConfig(Config(void 0) ?? {});
|
|
3987
5000
|
}
|
|
3988
|
-
|
|
3989
|
-
//#region src/apns.ts
|
|
5001
|
+
// src/apns.ts
|
|
3990
5002
|
/**
|
|
3991
5003
|
* Minimal APNs provider client (HTTP/2) with zero npm dependencies.
|
|
3992
5004
|
*
|
|
@@ -4223,8 +5235,7 @@ var ApnsClient = class {
|
|
|
4223
5235
|
return token.length <= 10 ? "…" : token.slice(0, 6) + "…" + token.slice(-4);
|
|
4224
5236
|
}
|
|
4225
5237
|
};
|
|
4226
|
-
|
|
4227
|
-
//#region src/relay-url.ts
|
|
5238
|
+
// src/relay-url.ts
|
|
4228
5239
|
/**
|
|
4229
5240
|
* Normalize an operator-configured relay URL without a backtracking regular
|
|
4230
5241
|
* expression. Scanning from the end keeps even pathological inputs linear.
|
|
@@ -4235,8 +5246,7 @@ function normalizeRelayBaseUrl(value) {
|
|
|
4235
5246
|
while (end > 0 && trimmed.charCodeAt(end - 1) === 47) end -= 1;
|
|
4236
5247
|
return end === trimmed.length ? trimmed : trimmed.slice(0, end);
|
|
4237
5248
|
}
|
|
4238
|
-
|
|
4239
|
-
//#region src/relay-client.ts
|
|
5249
|
+
// src/relay-client.ts
|
|
4240
5250
|
var RelayClient = class {
|
|
4241
5251
|
base;
|
|
4242
5252
|
token;
|
|
@@ -4329,8 +5339,7 @@ var RelayClient = class {
|
|
|
4329
5339
|
}
|
|
4330
5340
|
}
|
|
4331
5341
|
};
|
|
4332
|
-
|
|
4333
|
-
//#region src/widget-push.ts
|
|
5342
|
+
// src/widget-push.ts
|
|
4334
5343
|
/** Coalesce bursts and bound continuous updates, without starving the trailing
|
|
4335
5344
|
* state. One scheduler per host, not per token or per streaming text frame. */
|
|
4336
5345
|
var WidgetPushScheduler = class {
|
|
@@ -4367,8 +5376,7 @@ var WidgetPushScheduler = class {
|
|
|
4367
5376
|
this.timer = void 0;
|
|
4368
5377
|
}
|
|
4369
5378
|
};
|
|
4370
|
-
|
|
4371
|
-
//#region src/live-activity.ts
|
|
5379
|
+
// src/live-activity.ts
|
|
4372
5380
|
function liveActivityState(session) {
|
|
4373
5381
|
const total = Math.max(0, session?.todos?.total ?? session?.todoItems?.length ?? 0);
|
|
4374
5382
|
const done = Math.min(total, Math.max(0, session?.todos?.done ?? 0));
|
|
@@ -4440,8 +5448,7 @@ var LiveActivityPushManager = class {
|
|
|
4440
5448
|
this.scheduler.dispose();
|
|
4441
5449
|
}
|
|
4442
5450
|
};
|
|
4443
|
-
|
|
4444
|
-
//#region src/push-policy.ts
|
|
5451
|
+
// src/push-policy.ts
|
|
4445
5452
|
/** Prune only when the provider supplies an authoritative token-lifecycle verdict. */
|
|
4446
5453
|
function shouldPrunePushToken(outcome, reason) {
|
|
4447
5454
|
return outcome === "invalid-token" && (reason === "Unregistered" || reason === "ExpiredToken");
|
|
@@ -4477,8 +5484,7 @@ function pushContent(notification, mode) {
|
|
|
4477
5484
|
body: bodies[notification.category] ?? "Open DeepPilot for an update."
|
|
4478
5485
|
};
|
|
4479
5486
|
}
|
|
4480
|
-
|
|
4481
|
-
//#region src/relay-test.ts
|
|
5487
|
+
// src/relay-test.ts
|
|
4482
5488
|
/**
|
|
4483
5489
|
* Relay connectivity self-test used by the settings page "测试中继" button.
|
|
4484
5490
|
*
|
|
@@ -4604,8 +5610,7 @@ async function runRelayProbe(options) {
|
|
|
4604
5610
|
steps
|
|
4605
5611
|
};
|
|
4606
5612
|
}
|
|
4607
|
-
|
|
4608
|
-
//#region src/push-gateway.ts
|
|
5613
|
+
// src/push-gateway.ts
|
|
4609
5614
|
/**
|
|
4610
5615
|
* 推送网关(PushGateway)——离线推送的单一属主。
|
|
4611
5616
|
*
|
|
@@ -5095,8 +6100,7 @@ var PushGateway = class {
|
|
|
5095
6100
|
await this.ensureRelayEnrolled(url);
|
|
5096
6101
|
}
|
|
5097
6102
|
};
|
|
5098
|
-
|
|
5099
|
-
//#region src/phone-server.ts
|
|
6103
|
+
// src/phone-server.ts
|
|
5100
6104
|
/**
|
|
5101
6105
|
* A deliberately narrow transport listener. Both the LAN endpoint and the
|
|
5102
6106
|
* loopback-only Funnel origin use this factory, so neither can accidentally
|
|
@@ -5175,8 +6179,7 @@ function closeServer(server) {
|
|
|
5175
6179
|
if (server === void 0 || !server.listening) return Promise.resolve();
|
|
5176
6180
|
return new Promise((resolve) => server.close(() => resolve()));
|
|
5177
6181
|
}
|
|
5178
|
-
|
|
5179
|
-
//#region src/transport-reconciler.ts
|
|
6182
|
+
// src/transport-reconciler.ts
|
|
5180
6183
|
var TransportReconciler = class {
|
|
5181
6184
|
spec;
|
|
5182
6185
|
config;
|
|
@@ -5404,8 +6407,7 @@ function createRemoteTransport(deps) {
|
|
|
5404
6407
|
statusOf: (supervisor) => supervisor?.status() ?? fallbackStatus()
|
|
5405
6408
|
}, deps.config, deps.log, fallbackStatus());
|
|
5406
6409
|
}
|
|
5407
|
-
|
|
5408
|
-
//#region src/dispatch-journal.ts
|
|
6410
|
+
// src/dispatch-journal.ts
|
|
5409
6411
|
/**
|
|
5410
6412
|
* at-most-once dispatch journal:prompt 投递与非 prompt 变更的**同一个** module。
|
|
5411
6413
|
*
|
|
@@ -5837,8 +6839,7 @@ function scheduleMutationCodec(persistValues) {
|
|
|
5837
6839
|
}))
|
|
5838
6840
|
};
|
|
5839
6841
|
}
|
|
5840
|
-
|
|
5841
|
-
//#region src/notification-text.ts
|
|
6842
|
+
// src/notification-text.ts
|
|
5842
6843
|
/**
|
|
5843
6844
|
* 通知正文的纯文本投影。
|
|
5844
6845
|
*
|
|
@@ -5881,8 +6882,7 @@ function decodePercentRuns(text) {
|
|
|
5881
6882
|
}
|
|
5882
6883
|
});
|
|
5883
6884
|
}
|
|
5884
|
-
|
|
5885
|
-
//#region src/host-api.ts
|
|
6885
|
+
// src/host-api.ts
|
|
5886
6886
|
/**
|
|
5887
6887
|
* Subagent sessions are host-internal workers of a parent conversation.
|
|
5888
6888
|
* They must never surface on the phone: not in the project/session list,
|
|
@@ -5905,8 +6905,12 @@ function unwrapStreamItem(item) {
|
|
|
5905
6905
|
} : nested;
|
|
5906
6906
|
return item;
|
|
5907
6907
|
}
|
|
5908
|
-
|
|
5909
|
-
|
|
6908
|
+
// src/host-capabilities.ts
|
|
6909
|
+
const pluginUpdateProbes = /* @__PURE__ */ new WeakMap();
|
|
6910
|
+
function setPluginUpdateProbe(proxy, probe) {
|
|
6911
|
+
if (probe) pluginUpdateProbes.set(proxy, probe);
|
|
6912
|
+
else pluginUpdateProbes.delete(proxy);
|
|
6913
|
+
}
|
|
5910
6914
|
/** 每位一行:Host 上必须存在的依赖。缺失即该位为 false,相关帧回 E_UNSUPPORTED。 */
|
|
5911
6915
|
const HOST_CAPABILITY_PROBES = {
|
|
5912
6916
|
models: (proxy) => typeof proxy.sessions.models === "function",
|
|
@@ -5916,7 +6920,12 @@ const HOST_CAPABILITY_PROBES = {
|
|
|
5916
6920
|
sessionFork: (proxy) => typeof proxy.sessions.fork === "function",
|
|
5917
6921
|
sessionRestore: (proxy) => typeof proxy.workspace?.unarchiveSession === "function",
|
|
5918
6922
|
projectSelection: (proxy) => typeof proxy.workspace?.list === "function" && typeof proxy.workspace?.create === "function",
|
|
5919
|
-
schedules: (proxy) => proxy.schedule !== void 0
|
|
6923
|
+
schedules: (proxy) => proxy.schedule !== void 0,
|
|
6924
|
+
schedulesHost: (proxy) => proxy.schedule?.mode === "host",
|
|
6925
|
+
promptQueue: (proxy) => typeof proxy.sessions.updateQueue === "function",
|
|
6926
|
+
sessionMarkdown: (proxy) => proxy.commands?.markdownAvailable === true && typeof proxy.commands?.export === "function",
|
|
6927
|
+
assistantStream: (proxy) => proxy.assistantStreamAvailable === true,
|
|
6928
|
+
pluginUpdate: (proxy) => pluginUpdateProbes.get(proxy)?.() === true
|
|
5920
6929
|
};
|
|
5921
6930
|
/**
|
|
5922
6931
|
* 按当前 apiProxy 与推送出口算出 welcome 能力位。`push` 位描述「当前离线推送
|
|
@@ -5926,6 +6935,7 @@ function capabilityBits(proxy, pushOutlet) {
|
|
|
5926
6935
|
return {
|
|
5927
6936
|
historyPaging: true,
|
|
5928
6937
|
replay: true,
|
|
6938
|
+
binaryFrames: true,
|
|
5929
6939
|
approvals: true,
|
|
5930
6940
|
questions: true,
|
|
5931
6941
|
pendingSnapshot: true,
|
|
@@ -5939,66 +6949,327 @@ function capabilityBits(proxy, pushOutlet) {
|
|
|
5939
6949
|
sessionRestore: HOST_CAPABILITY_PROBES.sessionRestore(proxy),
|
|
5940
6950
|
projectSelection: HOST_CAPABILITY_PROBES.projectSelection(proxy),
|
|
5941
6951
|
schedules: HOST_CAPABILITY_PROBES.schedules(proxy),
|
|
6952
|
+
schedulesHost: HOST_CAPABILITY_PROBES.schedulesHost(proxy),
|
|
6953
|
+
promptQueue: HOST_CAPABILITY_PROBES.promptQueue(proxy),
|
|
6954
|
+
sessionMarkdown: HOST_CAPABILITY_PROBES.sessionMarkdown(proxy),
|
|
6955
|
+
assistantStream: HOST_CAPABILITY_PROBES.assistantStream(proxy),
|
|
6956
|
+
pluginUpdate: HOST_CAPABILITY_PROBES.pluginUpdate(proxy),
|
|
5942
6957
|
push: pushOutlet?.isAvailable() === true,
|
|
5943
6958
|
widgetPush: true,
|
|
5944
6959
|
liveActivityPush: true,
|
|
5945
6960
|
deviceRevoke: true
|
|
5946
6961
|
};
|
|
5947
6962
|
}
|
|
5948
|
-
|
|
5949
|
-
//#region src/host-bridge.ts
|
|
5950
|
-
const MAX_RING_DEFAULT = 2e3;
|
|
6963
|
+
// src/prompt-queue.ts
|
|
5951
6964
|
/**
|
|
5952
|
-
*
|
|
5953
|
-
*
|
|
5954
|
-
*
|
|
6965
|
+
* 运行中排队与转向(PROTOCOL.md「运行中排队与转向扩展」)的纯函数部分。
|
|
6966
|
+
*
|
|
6967
|
+
* 职责:
|
|
6968
|
+
* - Host `requestId` 与 wire `clientSendId` 的互转:Bridge 发送带 clientSendId 的
|
|
6969
|
+
* prompt 时以 `dp:` 前缀拼出确定的 requestId,Host 把它记为用户来源的 `rpcId`;
|
|
6970
|
+
* 快照时反向剥掉前缀,发起设备即可把待发送项与队列项对上。
|
|
6971
|
+
* - 把 DSH `inbox` 投影(`{ "next-turn": Message[], "next-step": Message[] }`,
|
|
6972
|
+
* 来源是持久化的 `agent/inbox/spliced` 事件)折叠成 `s2c.session.queue` 的条目。
|
|
6973
|
+
*
|
|
6974
|
+
* 不持有状态:缓存、去重与推送时机在 host-bridge。
|
|
5955
6975
|
*/
|
|
5956
|
-
|
|
5957
|
-
|
|
5958
|
-
|
|
5959
|
-
|
|
5960
|
-
|
|
5961
|
-
|
|
5962
|
-
|
|
5963
|
-
|
|
5964
|
-
|
|
5965
|
-
|
|
5966
|
-
|
|
5967
|
-
|
|
5968
|
-
|
|
5969
|
-
|
|
5970
|
-
|
|
5971
|
-
|
|
5972
|
-
|
|
5973
|
-
|
|
5974
|
-
|
|
5975
|
-
|
|
5976
|
-
|
|
5977
|
-
|
|
5978
|
-
|
|
5979
|
-
|
|
5980
|
-
|
|
5981
|
-
|
|
5982
|
-
|
|
5983
|
-
|
|
5984
|
-
|
|
5985
|
-
|
|
5986
|
-
|
|
5987
|
-
|
|
5988
|
-
|
|
5989
|
-
|
|
5990
|
-
|
|
5991
|
-
|
|
5992
|
-
|
|
5993
|
-
|
|
5994
|
-
|
|
5995
|
-
|
|
5996
|
-
|
|
5997
|
-
|
|
5998
|
-
}
|
|
5999
|
-
|
|
6000
|
-
|
|
6001
|
-
|
|
6976
|
+
const REQUEST_ID_PREFIX = "dp:";
|
|
6977
|
+
/** 快照正文上限(Unicode 码点)。只用于列表展示,完整正文仍在 Host 日志里。 */
|
|
6978
|
+
const MAX_QUEUE_TEXT_CODE_POINTS = 4096;
|
|
6979
|
+
/** clientSendId -> Host requestId。 */
|
|
6980
|
+
function promptRequestId(clientSendId) {
|
|
6981
|
+
return REQUEST_ID_PREFIX + clientSendId;
|
|
6982
|
+
}
|
|
6983
|
+
/** Host 来源 rpcId -> clientSendId;不是本 Bridge 用 clientSendId 发出的返回 undefined。 */
|
|
6984
|
+
function clientSendIdOf(rpcId) {
|
|
6985
|
+
if (typeof rpcId !== "string" || !rpcId.startsWith(REQUEST_ID_PREFIX)) return void 0;
|
|
6986
|
+
const id = rpcId.slice(3);
|
|
6987
|
+
return validSendId(id) ? id : void 0;
|
|
6988
|
+
}
|
|
6989
|
+
/**
|
|
6990
|
+
* inbox 投影 -> 队列条目,保持 Host 的先进先出顺序:先 `next-turn`(排队),
|
|
6991
|
+
* 再 `next-step`(转向)。值形状不对时返回 undefined,调用方据此不推送,
|
|
6992
|
+
* 而不是把「读不懂」当成「队列为空」——客户端会把消失的项当作已发出。
|
|
6993
|
+
*/
|
|
6994
|
+
function projectQueue(value) {
|
|
6995
|
+
if (value === null || typeof value !== "object" || Array.isArray(value)) return void 0;
|
|
6996
|
+
const inbox = value;
|
|
6997
|
+
const nextTurn = inbox["next-turn"];
|
|
6998
|
+
const nextStep = inbox["next-step"];
|
|
6999
|
+
if (!Array.isArray(nextTurn) || !Array.isArray(nextStep)) return void 0;
|
|
7000
|
+
const items = [];
|
|
7001
|
+
for (const [list, placement] of [[nextTurn, "queued"], [nextStep, "steering"]]) for (const message of list) {
|
|
7002
|
+
const item = queueItemOf(message, placement);
|
|
7003
|
+
if (item) items.push(item);
|
|
7004
|
+
}
|
|
7005
|
+
return items;
|
|
7006
|
+
}
|
|
7007
|
+
function queueItemOf(message, placement) {
|
|
7008
|
+
if (message === null || typeof message !== "object" || typeof message.id !== "string" || message.id.length === 0) return;
|
|
7009
|
+
const texts = [];
|
|
7010
|
+
let imageCount = 0;
|
|
7011
|
+
let documentCount = 0;
|
|
7012
|
+
for (const block of Array.isArray(message.content) ? message.content : []) {
|
|
7013
|
+
if (block === null || typeof block !== "object") continue;
|
|
7014
|
+
if (block.type === "image") imageCount += 1;
|
|
7015
|
+
else if (block.type === "text" && typeof block.text === "string") {
|
|
7016
|
+
if (projectedDocument(block.text) !== void 0) documentCount += 1;
|
|
7017
|
+
else texts.push(block.text);
|
|
7018
|
+
}
|
|
7019
|
+
}
|
|
7020
|
+
const codePoints = Array.from(texts.join("\n"));
|
|
7021
|
+
const truncated = codePoints.length > MAX_QUEUE_TEXT_CODE_POINTS;
|
|
7022
|
+
const source = message.source;
|
|
7023
|
+
const clientSendId = source?.kind === "user" ? clientSendIdOf(source.rpcId) : void 0;
|
|
7024
|
+
return {
|
|
7025
|
+
itemId: message.id,
|
|
7026
|
+
placement,
|
|
7027
|
+
text: truncated ? codePoints.slice(0, MAX_QUEUE_TEXT_CODE_POINTS).join("") : codePoints.join(""),
|
|
7028
|
+
...truncated ? { truncated: true } : {},
|
|
7029
|
+
imageCount,
|
|
7030
|
+
documentCount,
|
|
7031
|
+
...clientSendId !== void 0 ? { clientSendId } : {}
|
|
7032
|
+
};
|
|
7033
|
+
}
|
|
7034
|
+
/** Bound by serialized UTF-8 size, including escaping, without splitting a Unicode code point. */
|
|
7035
|
+
function bounded(payload) {
|
|
7036
|
+
if (Buffer.byteLength(JSON.stringify(payload)) <= 262144) return payload;
|
|
7037
|
+
const points = Array.from(payload.text);
|
|
7038
|
+
let low = 0;
|
|
7039
|
+
let high = points.length;
|
|
7040
|
+
while (low < high) {
|
|
7041
|
+
const mid = Math.ceil((low + high) / 2);
|
|
7042
|
+
const candidate = {
|
|
7043
|
+
...payload,
|
|
7044
|
+
text: points.slice(0, mid).join(""),
|
|
7045
|
+
truncated: true
|
|
7046
|
+
};
|
|
7047
|
+
if (Buffer.byteLength(JSON.stringify(candidate)) <= 262144) low = mid;
|
|
7048
|
+
else high = mid - 1;
|
|
7049
|
+
}
|
|
7050
|
+
return {
|
|
7051
|
+
...payload,
|
|
7052
|
+
text: points.slice(0, low).join(""),
|
|
7053
|
+
truncated: true
|
|
7054
|
+
};
|
|
7055
|
+
}
|
|
7056
|
+
function isFrame(value) {
|
|
7057
|
+
if (!value || typeof value !== "object") return false;
|
|
7058
|
+
const frame = value;
|
|
7059
|
+
if (typeof frame.attemptId !== "string" || !frame.attemptId || !Number.isSafeInteger(frame.revision)) return false;
|
|
7060
|
+
if (frame.type === "start") return Number.isSafeInteger(frame.turn) && Number.isSafeInteger(frame.step);
|
|
7061
|
+
if (frame.type !== "chunk" && frame.type !== "end") return false;
|
|
7062
|
+
if (!Number.isSafeInteger(frame.index) || frame.index < 0) return false;
|
|
7063
|
+
if (frame.type === "chunk") return Number.isSafeInteger(frame.time) && !!frame.chunk && typeof frame.chunk.type === "string";
|
|
7064
|
+
return frame.outcome?.kind === "abandoned" || frame.outcome?.kind === "committed" && ["assistant/message", "assistant/attempt"].includes(frame.outcome.eventType) && Number.isSafeInteger(frame.outcome.seq);
|
|
7065
|
+
}
|
|
7066
|
+
/**
|
|
7067
|
+
* One process-local attempt per session. Only the small pending window retains text;
|
|
7068
|
+
* the durable session event remains the source of truth for the completed reply.
|
|
7069
|
+
*/
|
|
7070
|
+
var AssistantStream = class {
|
|
7071
|
+
emit;
|
|
7072
|
+
isViewed;
|
|
7073
|
+
attempts = /* @__PURE__ */ new Map();
|
|
7074
|
+
constructor(emit, isViewed) {
|
|
7075
|
+
this.emit = emit;
|
|
7076
|
+
this.isViewed = isViewed;
|
|
7077
|
+
}
|
|
7078
|
+
accept(sessionId, value) {
|
|
7079
|
+
if (!sessionId || !isFrame(value)) return;
|
|
7080
|
+
const frame = value;
|
|
7081
|
+
let state = this.attempts.get(sessionId);
|
|
7082
|
+
if (frame.type === "start") {
|
|
7083
|
+
if (state?.attemptId === frame.attemptId && frame.revision <= state.revision) return;
|
|
7084
|
+
this.abandon(sessionId);
|
|
7085
|
+
state = {
|
|
7086
|
+
attemptId: frame.attemptId,
|
|
7087
|
+
streamId: randomUUID(),
|
|
7088
|
+
revision: frame.revision,
|
|
7089
|
+
nextIndex: 0,
|
|
7090
|
+
turn: frame.turn,
|
|
7091
|
+
step: frame.step,
|
|
7092
|
+
pending: [],
|
|
7093
|
+
pendingBytes: 0
|
|
7094
|
+
};
|
|
7095
|
+
this.attempts.set(sessionId, state);
|
|
7096
|
+
this.emit({
|
|
7097
|
+
sessionId,
|
|
7098
|
+
streamId: state.streamId,
|
|
7099
|
+
kind: "stream.start",
|
|
7100
|
+
text: "",
|
|
7101
|
+
ts: Date.now()
|
|
7102
|
+
});
|
|
7103
|
+
return;
|
|
7104
|
+
}
|
|
7105
|
+
if (!state || state.attemptId !== frame.attemptId || frame.revision <= state.revision || frame.index < state.nextIndex) return;
|
|
7106
|
+
state.revision = frame.revision;
|
|
7107
|
+
if (frame.type === "end") {
|
|
7108
|
+
if (frame.outcome.kind === "committed") this.flush(sessionId);
|
|
7109
|
+
this.clearPending(sessionId);
|
|
7110
|
+
this.attempts.delete(sessionId);
|
|
7111
|
+
this.emit({
|
|
7112
|
+
sessionId,
|
|
7113
|
+
streamId: state.streamId,
|
|
7114
|
+
kind: "stream.end",
|
|
7115
|
+
text: "",
|
|
7116
|
+
ts: Date.now(),
|
|
7117
|
+
outcome: frame.outcome
|
|
7118
|
+
});
|
|
7119
|
+
return;
|
|
7120
|
+
}
|
|
7121
|
+
state.nextIndex = frame.index + 1;
|
|
7122
|
+
if (state.finalSeq !== void 0 || !this.isViewed(sessionId)) return;
|
|
7123
|
+
const kind = frame.chunk.type === "text-delta" ? "message.delta" : frame.chunk.type === "reasoning-delta" ? "thinking.delta" : void 0;
|
|
7124
|
+
if (!kind || typeof frame.chunk.text !== "string" || frame.chunk.text.length === 0) return;
|
|
7125
|
+
const previous = state.pending.at(-1)?.kind === kind ? state.pending.at(-1) : void 0;
|
|
7126
|
+
if (previous?.truncated) return;
|
|
7127
|
+
const payload = bounded({
|
|
7128
|
+
sessionId,
|
|
7129
|
+
streamId: state.streamId,
|
|
7130
|
+
kind,
|
|
7131
|
+
text: (previous?.text ?? "") + frame.chunk.text,
|
|
7132
|
+
ts: frame.time
|
|
7133
|
+
});
|
|
7134
|
+
const next = {
|
|
7135
|
+
kind,
|
|
7136
|
+
text: payload.text,
|
|
7137
|
+
ts: payload.ts,
|
|
7138
|
+
...payload.truncated ? { truncated: true } : {}
|
|
7139
|
+
};
|
|
7140
|
+
const oldBytes = previous ? Buffer.byteLength(JSON.stringify(previous)) : 0;
|
|
7141
|
+
const newBytes = Buffer.byteLength(JSON.stringify(next));
|
|
7142
|
+
if (state.pendingBytes - oldBytes + newBytes > 524288) this.flush(sessionId);
|
|
7143
|
+
if (previous && state.pending.at(-1) === previous) {
|
|
7144
|
+
state.pending[state.pending.length - 1] = next;
|
|
7145
|
+
state.pendingBytes -= oldBytes;
|
|
7146
|
+
} else if (previous) {
|
|
7147
|
+
const tail = bounded({
|
|
7148
|
+
sessionId,
|
|
7149
|
+
streamId: state.streamId,
|
|
7150
|
+
kind,
|
|
7151
|
+
text: frame.chunk.text,
|
|
7152
|
+
ts: frame.time
|
|
7153
|
+
});
|
|
7154
|
+
next.text = tail.text;
|
|
7155
|
+
next.truncated = tail.truncated ? true : void 0;
|
|
7156
|
+
state.pending.push(next);
|
|
7157
|
+
} else state.pending.push(next);
|
|
7158
|
+
state.pendingBytes += Buffer.byteLength(JSON.stringify(next));
|
|
7159
|
+
state.timer ??= setTimeout(() => this.flush(sessionId), 50);
|
|
7160
|
+
state.timer.unref?.();
|
|
7161
|
+
}
|
|
7162
|
+
/** The Host appends the durable event BEFORE publishing committed end. */
|
|
7163
|
+
beforeEvent(sessionId, event) {
|
|
7164
|
+
const state = this.attempts.get(sessionId);
|
|
7165
|
+
if (!state || event.type !== "assistant/message" && event.type !== "assistant/attempt") return void 0;
|
|
7166
|
+
const data = event.data;
|
|
7167
|
+
if (data?.turn !== state.turn || data?.step !== state.step) return void 0;
|
|
7168
|
+
this.flush(sessionId);
|
|
7169
|
+
state.finalSeq = event.seq;
|
|
7170
|
+
return state.streamId;
|
|
7171
|
+
}
|
|
7172
|
+
flush(sessionId) {
|
|
7173
|
+
const state = this.attempts.get(sessionId);
|
|
7174
|
+
if (!state) return;
|
|
7175
|
+
if (state.timer) clearTimeout(state.timer);
|
|
7176
|
+
state.timer = void 0;
|
|
7177
|
+
const pending = state.pending;
|
|
7178
|
+
state.pending = [];
|
|
7179
|
+
state.pendingBytes = 0;
|
|
7180
|
+
for (const text of pending) this.emit({
|
|
7181
|
+
sessionId,
|
|
7182
|
+
streamId: state.streamId,
|
|
7183
|
+
...text
|
|
7184
|
+
});
|
|
7185
|
+
}
|
|
7186
|
+
/** Closing the last viewer must not deliver its buffered text on a later open. */
|
|
7187
|
+
clearPending(sessionId) {
|
|
7188
|
+
const state = this.attempts.get(sessionId);
|
|
7189
|
+
if (!state) return;
|
|
7190
|
+
if (state.timer) clearTimeout(state.timer);
|
|
7191
|
+
state.timer = void 0;
|
|
7192
|
+
state.pending = [];
|
|
7193
|
+
state.pendingBytes = 0;
|
|
7194
|
+
}
|
|
7195
|
+
abandon(sessionId) {
|
|
7196
|
+
const state = this.attempts.get(sessionId);
|
|
7197
|
+
if (!state) return;
|
|
7198
|
+
this.clearPending(sessionId);
|
|
7199
|
+
this.attempts.delete(sessionId);
|
|
7200
|
+
this.emit({
|
|
7201
|
+
sessionId,
|
|
7202
|
+
streamId: state.streamId,
|
|
7203
|
+
kind: "stream.end",
|
|
7204
|
+
text: "",
|
|
7205
|
+
ts: Date.now(),
|
|
7206
|
+
outcome: { kind: "abandoned" }
|
|
7207
|
+
});
|
|
7208
|
+
}
|
|
7209
|
+
dispose() {
|
|
7210
|
+
for (const sessionId of this.attempts.keys()) this.clearPending(sessionId);
|
|
7211
|
+
this.attempts.clear();
|
|
7212
|
+
}
|
|
7213
|
+
};
|
|
7214
|
+
// src/host-bridge.ts
|
|
7215
|
+
const MAX_RING_DEFAULT = 2e3;
|
|
7216
|
+
/**
|
|
7217
|
+
* Process-wide bridge state: session mirror, pending approvals/questions,
|
|
7218
|
+
* and the per-device replay ring. Consumes the in-process mux/host streams
|
|
7219
|
+
* and fans projected pushes out to every registered sink.
|
|
7220
|
+
*/
|
|
7221
|
+
let BRIDGE_SEQ = 0;
|
|
7222
|
+
var HostBridge = class {
|
|
7223
|
+
apiProxy;
|
|
7224
|
+
historyBufferMax;
|
|
7225
|
+
log;
|
|
7226
|
+
id = ++BRIDGE_SEQ;
|
|
7227
|
+
summaries = /* @__PURE__ */ new Map();
|
|
7228
|
+
activeTools = /* @__PURE__ */ new Map();
|
|
7229
|
+
approvals = /* @__PURE__ */ new Map();
|
|
7230
|
+
questions = /* @__PURE__ */ new Map();
|
|
7231
|
+
archivedSessionIds = /* @__PURE__ */ new Set();
|
|
7232
|
+
/** Mirrors archived rows from the last sessions.list so the phone can browse
|
|
7233
|
+
* and restore them. Excluded from `summaries` and from the live broadcast. */
|
|
7234
|
+
archivedSummaries = /* @__PURE__ */ new Map();
|
|
7235
|
+
subagentSessionIds = /* @__PURE__ */ new Set();
|
|
7236
|
+
/**
|
|
7237
|
+
* 每个会话最近一次已知的队列(inbox 投影的折叠结果)及其指纹。来源是实时
|
|
7238
|
+
* 投影通知或打开会话时读到的基线;指纹用来丢掉内容没变的重复通知。
|
|
7239
|
+
*/
|
|
7240
|
+
queues = /* @__PURE__ */ new Map();
|
|
7241
|
+
sinks = /* @__PURE__ */ new Set();
|
|
7242
|
+
assistantStream = new AssistantStream((payload) => this.pushAssistantDelta(payload), (sessionId) => this.hasStreamViewer(sessionId));
|
|
7243
|
+
ring = [];
|
|
7244
|
+
cursor = 0;
|
|
7245
|
+
userReceiptSeq = 0;
|
|
7246
|
+
abort = new AbortController();
|
|
7247
|
+
started = false;
|
|
7248
|
+
disposed = false;
|
|
7249
|
+
promptDeliveries;
|
|
7250
|
+
scheduleMutations;
|
|
7251
|
+
forkMutations;
|
|
7252
|
+
constructor(apiProxy, historyBufferMax = MAX_RING_DEFAULT, deliveryJournalPath, scheduleJournalPath, forkJournalPath, log) {
|
|
7253
|
+
this.apiProxy = apiProxy;
|
|
7254
|
+
this.historyBufferMax = historyBufferMax;
|
|
7255
|
+
this.log = log;
|
|
7256
|
+
this.promptDeliveries = openDispatchJournal({
|
|
7257
|
+
path: deliveryJournalPath,
|
|
7258
|
+
codec: promptDeliveryCodec,
|
|
7259
|
+
joinInFlight: true,
|
|
7260
|
+
maxFileBytes: 8388608,
|
|
7261
|
+
log: this.log
|
|
7262
|
+
});
|
|
7263
|
+
this.scheduleMutations = openDispatchJournal({
|
|
7264
|
+
path: scheduleJournalPath,
|
|
7265
|
+
codec: scheduleMutationCodec(false),
|
|
7266
|
+
joinInFlight: false,
|
|
7267
|
+
maxFileBytes: 4194304,
|
|
7268
|
+
log: this.log
|
|
7269
|
+
});
|
|
7270
|
+
this.forkMutations = openDispatchJournal({
|
|
7271
|
+
path: forkJournalPath,
|
|
7272
|
+
codec: scheduleMutationCodec(true),
|
|
6002
7273
|
joinInFlight: false,
|
|
6003
7274
|
maxFileBytes: 4194304,
|
|
6004
7275
|
log: this.log
|
|
@@ -6032,6 +7303,7 @@ var HostBridge = class {
|
|
|
6032
7303
|
}
|
|
6033
7304
|
removeSink(sink) {
|
|
6034
7305
|
this.sinks.delete(sink);
|
|
7306
|
+
this.dropSinkSessions(sink);
|
|
6035
7307
|
}
|
|
6036
7308
|
/** Whether the ring still holds everything after the cursor. */
|
|
6037
7309
|
canResumeFrom(cursor) {
|
|
@@ -6051,9 +7323,21 @@ var HostBridge = class {
|
|
|
6051
7323
|
}
|
|
6052
7324
|
markSinkClosed(sink, sessionId) {
|
|
6053
7325
|
this.sinkSessions.get(sink)?.delete(sessionId);
|
|
7326
|
+
if (!this.hasStreamViewer(sessionId)) this.assistantStream.clearPending(sessionId);
|
|
6054
7327
|
}
|
|
6055
7328
|
dropSinkSessions(sink) {
|
|
7329
|
+
const sessions = this.sinkSessions.get(sink);
|
|
6056
7330
|
this.sinkSessions.delete(sink);
|
|
7331
|
+
for (const sessionId of sessions ?? []) if (!this.hasStreamViewer(sessionId)) this.assistantStream.clearPending(sessionId);
|
|
7332
|
+
}
|
|
7333
|
+
hasStreamViewer(sessionId) {
|
|
7334
|
+
if (this.disposed || this.subagentSessionIds.has(sessionId)) return false;
|
|
7335
|
+
return [...this.sinks].some((sink) => this.isViewedBy(sink, sessionId) && sink.canReceive("sessions.read"));
|
|
7336
|
+
}
|
|
7337
|
+
/** Instant publications bypass record(): no cursor, ring, notification or push side effect. */
|
|
7338
|
+
pushAssistantDelta(payload) {
|
|
7339
|
+
if (this.disposed || this.subagentSessionIds.has(payload.sessionId)) return;
|
|
7340
|
+
for (const sink of this.sinks) if (this.isViewedBy(sink, payload.sessionId) && sink.canReceive("sessions.read")) sink.push("s2c.session.delta", payload);
|
|
6057
7341
|
}
|
|
6058
7342
|
isViewedBy(sink, sessionId) {
|
|
6059
7343
|
return this.sinkSessions.get(sink)?.has(sessionId) ?? false;
|
|
@@ -6200,6 +7484,7 @@ var HostBridge = class {
|
|
|
6200
7484
|
if (this.disposed) return;
|
|
6201
7485
|
this.disposed = true;
|
|
6202
7486
|
this.abort.abort();
|
|
7487
|
+
this.assistantStream.dispose();
|
|
6203
7488
|
this.sinks.clear();
|
|
6204
7489
|
this.sinkSessions.clear();
|
|
6205
7490
|
this.pushOutlet = void 0;
|
|
@@ -6218,7 +7503,9 @@ var HostBridge = class {
|
|
|
6218
7503
|
const frame = unwrapStreamItem(item);
|
|
6219
7504
|
this.onMuxFrame(frame);
|
|
6220
7505
|
}
|
|
6221
|
-
} catch {}
|
|
7506
|
+
} catch {} finally {
|
|
7507
|
+
this.assistantStream.dispose();
|
|
7508
|
+
}
|
|
6222
7509
|
}
|
|
6223
7510
|
onHostFrame(frame) {
|
|
6224
7511
|
switch (frame.type) {
|
|
@@ -6257,12 +7544,30 @@ var HostBridge = class {
|
|
|
6257
7544
|
case "commands/changed":
|
|
6258
7545
|
this.record("s2c.commands.changed", {});
|
|
6259
7546
|
break;
|
|
7547
|
+
case "session/assistant-stream": {
|
|
7548
|
+
const sessionId = String(frame.sessionId ?? "");
|
|
7549
|
+
if (frame.isSubagent === true) this.subagentSessionIds.add(sessionId);
|
|
7550
|
+
if (this.subagentSessionIds.has(sessionId)) {
|
|
7551
|
+
this.assistantStream.abandon(sessionId);
|
|
7552
|
+
break;
|
|
7553
|
+
}
|
|
7554
|
+
this.assistantStream.accept(sessionId, frame.value);
|
|
7555
|
+
break;
|
|
7556
|
+
}
|
|
7557
|
+
case "session/assistant-stream-disposed":
|
|
7558
|
+
this.assistantStream.abandon(String(frame.sessionId ?? ""));
|
|
7559
|
+
break;
|
|
6260
7560
|
case "session/event": {
|
|
6261
7561
|
const event = frame.event;
|
|
6262
7562
|
const sessionId = String(frame.sessionId ?? "");
|
|
6263
7563
|
if (!event || !sessionId) break;
|
|
6264
7564
|
if (frame.isSubagent === true) this.subagentSessionIds.add(sessionId);
|
|
6265
|
-
if (this.subagentSessionIds.has(sessionId))
|
|
7565
|
+
if (this.subagentSessionIds.has(sessionId)) {
|
|
7566
|
+
this.assistantStream.abandon(sessionId);
|
|
7567
|
+
break;
|
|
7568
|
+
}
|
|
7569
|
+
const streamId = this.assistantStream.beforeEvent(sessionId, event);
|
|
7570
|
+
if (event.type === "turn/end") this.assistantStream.abandon(sessionId);
|
|
6266
7571
|
if (event.type === "agent-preset/selected") this.record("s2c.skills.changed", { sessionId });
|
|
6267
7572
|
this.noteActivity(sessionId, event);
|
|
6268
7573
|
this.captureAssistantText(sessionId, event);
|
|
@@ -6272,7 +7577,10 @@ var HostBridge = class {
|
|
|
6272
7577
|
sessionId,
|
|
6273
7578
|
kind: projection.kind,
|
|
6274
7579
|
seq: event.seq,
|
|
6275
|
-
data:
|
|
7580
|
+
data: {
|
|
7581
|
+
...projection.data,
|
|
7582
|
+
...streamId && projection.kind === "message.final" ? { streamId } : {}
|
|
7583
|
+
}
|
|
6276
7584
|
});
|
|
6277
7585
|
if (projection?.kind === "turn.end") this.emitTurnCompletedNotify(sessionId, projection.data.ok === true);
|
|
6278
7586
|
break;
|
|
@@ -6293,6 +7601,7 @@ var HostBridge = class {
|
|
|
6293
7601
|
...current ? { current } : {}
|
|
6294
7602
|
});
|
|
6295
7603
|
}
|
|
7604
|
+
if (p.key === "inbox") this.applyInbox(p.sessionId, p.value, "live");
|
|
6296
7605
|
this.applyProjection(p.sessionId, String(p.key ?? ""), p.value);
|
|
6297
7606
|
break;
|
|
6298
7607
|
}
|
|
@@ -6468,6 +7777,43 @@ var HostBridge = class {
|
|
|
6468
7777
|
row.lastActivityTs = Date.now();
|
|
6469
7778
|
this.pushSummary(row);
|
|
6470
7779
|
}
|
|
7780
|
+
/**
|
|
7781
|
+
* 折叠一次 inbox 投影。只有实时变化才广播 s2c.session.queue;基线只补缓存,
|
|
7782
|
+
* 打开会话的那台设备另由 pushQueueSnapshot 点对点拿到快照。
|
|
7783
|
+
*
|
|
7784
|
+
* 基线只补缺不覆盖:实时通知带的是完整投影值,收到过就以它为准;基线读取与
|
|
7785
|
+
* 实时变化并发时,较早算出的基线不得盖掉较新的实时值。
|
|
7786
|
+
*/
|
|
7787
|
+
applyInbox(sessionId, value, origin) {
|
|
7788
|
+
if (this.capabilities.promptQueue !== true) return;
|
|
7789
|
+
if (this.subagentSessionIds.has(sessionId)) return;
|
|
7790
|
+
if (origin === "baseline" && this.queues.has(sessionId)) return;
|
|
7791
|
+
const items = projectQueue(value);
|
|
7792
|
+
if (items === void 0) return;
|
|
7793
|
+
const fingerprint = JSON.stringify(items);
|
|
7794
|
+
const previous = this.queues.get(sessionId);
|
|
7795
|
+
this.queues.set(sessionId, {
|
|
7796
|
+
items,
|
|
7797
|
+
fingerprint
|
|
7798
|
+
});
|
|
7799
|
+
if (origin === "live" && previous?.fingerprint !== fingerprint) this.record("s2c.session.queue", {
|
|
7800
|
+
sessionId,
|
|
7801
|
+
items
|
|
7802
|
+
});
|
|
7803
|
+
}
|
|
7804
|
+
/**
|
|
7805
|
+
* 打开会话时补发的快照:不占 seq,与 tail 同属打开响应。还没读到过该会话的
|
|
7806
|
+
* 投影(基线失败、Host 不提供或子代理会话)时不发,因为未知不等于空队列。
|
|
7807
|
+
*/
|
|
7808
|
+
pushQueueSnapshot(sink, sessionId) {
|
|
7809
|
+
if (this.disposed) return;
|
|
7810
|
+
const queue = this.queues.get(sessionId);
|
|
7811
|
+
if (queue === void 0) return;
|
|
7812
|
+
sink.push("s2c.session.queue", {
|
|
7813
|
+
sessionId,
|
|
7814
|
+
items: queue.items
|
|
7815
|
+
});
|
|
7816
|
+
}
|
|
6471
7817
|
applyProjection(sessionId, key, value) {
|
|
6472
7818
|
const row = this.summaries.get(sessionId);
|
|
6473
7819
|
if (!row) return;
|
|
@@ -6594,7 +7940,7 @@ var HostBridge = class {
|
|
|
6594
7940
|
hasMore: Boolean(result.hasMore) || page.dropped > 0
|
|
6595
7941
|
});
|
|
6596
7942
|
this.deriveTitleFallback(sessionId, messages);
|
|
6597
|
-
this.refreshProjections(sessionId);
|
|
7943
|
+
this.refreshProjections(sessionId).then(() => this.pushQueueSnapshot(sink, sessionId));
|
|
6598
7944
|
return true;
|
|
6599
7945
|
} catch {
|
|
6600
7946
|
return false;
|
|
@@ -6620,8 +7966,14 @@ var HostBridge = class {
|
|
|
6620
7966
|
return;
|
|
6621
7967
|
}
|
|
6622
7968
|
const baseline = result.value;
|
|
6623
|
-
if (baseline === null || baseline === void 0)
|
|
6624
|
-
|
|
7969
|
+
if (baseline === null || baseline === void 0) {
|
|
7970
|
+
this.queues.delete(sessionId);
|
|
7971
|
+
return;
|
|
7972
|
+
}
|
|
7973
|
+
for (const [key, value] of Object.entries(baseline.values ?? {})) {
|
|
7974
|
+
if (key === "inbox") this.applyInbox(sessionId, value, "baseline");
|
|
7975
|
+
this.applyProjection(sessionId, key, value);
|
|
7976
|
+
}
|
|
6625
7977
|
} catch (error) {
|
|
6626
7978
|
this.diagnostic("sessions.projections threw: " + String(error?.message ?? error));
|
|
6627
7979
|
}
|
|
@@ -6692,12 +8044,15 @@ var HostBridge = class {
|
|
|
6692
8044
|
return wireErrorOf("command", hostFailureOf(error, "command/invalid-response"));
|
|
6693
8045
|
}
|
|
6694
8046
|
}
|
|
6695
|
-
async exportSession(sessionId, signal) {
|
|
8047
|
+
async exportSession(sessionId, signal, format) {
|
|
6696
8048
|
const api = this.apiProxy.commands;
|
|
6697
8049
|
if (!api) throw Object.assign(/* @__PURE__ */ new Error("当前主机不支持日志导出"), { code: "command/export-unsupported" });
|
|
6698
8050
|
return api.export({
|
|
6699
8051
|
rpcId: randomUUID(),
|
|
6700
|
-
payload: {
|
|
8052
|
+
payload: {
|
|
8053
|
+
sessionId,
|
|
8054
|
+
...format === void 0 ? {} : { format }
|
|
8055
|
+
}
|
|
6701
8056
|
}, signal);
|
|
6702
8057
|
}
|
|
6703
8058
|
/** 由会话目录决定技能可见性;不读取正文,也不把本机路径发给移动端。 */
|
|
@@ -6913,6 +8268,47 @@ var HostBridge = class {
|
|
|
6913
8268
|
};
|
|
6914
8269
|
}
|
|
6915
8270
|
}
|
|
8271
|
+
/**
|
|
8272
|
+
* 删除或转向一条排队项。ack 只表示 Host 已提交变更,真实状态以随后的
|
|
8273
|
+
* s2c.session.queue 为准;不做幂等,项已消失时 Host 回 queue-item-not-found。
|
|
8274
|
+
*/
|
|
8275
|
+
async updateQueue(sessionId, itemId, action) {
|
|
8276
|
+
const update = this.apiProxy.sessions.updateQueue;
|
|
8277
|
+
if (typeof update !== "function") return {
|
|
8278
|
+
ok: false,
|
|
8279
|
+
code: "E_UNSUPPORTED",
|
|
8280
|
+
message: "prompt queue unavailable on this host version"
|
|
8281
|
+
};
|
|
8282
|
+
try {
|
|
8283
|
+
const response = await update.call(this.apiProxy.sessions, {
|
|
8284
|
+
rpcId: randomUUID(),
|
|
8285
|
+
payload: {
|
|
8286
|
+
sessionId,
|
|
8287
|
+
itemId,
|
|
8288
|
+
action: { kind: action }
|
|
8289
|
+
}
|
|
8290
|
+
});
|
|
8291
|
+
if (!response.result) return {
|
|
8292
|
+
ok: false,
|
|
8293
|
+
code: "E_INTERNAL",
|
|
8294
|
+
message: "queue update returned no result"
|
|
8295
|
+
};
|
|
8296
|
+
if (!response.result.ok) return wireErrorOf("queue", response.result.error);
|
|
8297
|
+
return {
|
|
8298
|
+
ok: true,
|
|
8299
|
+
value: {
|
|
8300
|
+
sessionId,
|
|
8301
|
+
itemId
|
|
8302
|
+
}
|
|
8303
|
+
};
|
|
8304
|
+
} catch (error) {
|
|
8305
|
+
return {
|
|
8306
|
+
ok: false,
|
|
8307
|
+
code: "E_INTERNAL",
|
|
8308
|
+
message: String(error)
|
|
8309
|
+
};
|
|
8310
|
+
}
|
|
8311
|
+
}
|
|
6916
8312
|
async cancelSession(sessionId) {
|
|
6917
8313
|
const cancel = this.apiProxy.sessions.cancel;
|
|
6918
8314
|
if (typeof cancel !== "function") return {
|
|
@@ -7139,6 +8535,17 @@ var HostBridge = class {
|
|
|
7139
8535
|
replayed: result.replayed
|
|
7140
8536
|
};
|
|
7141
8537
|
}
|
|
8538
|
+
/**
|
|
8539
|
+
* 当前 adapter 是否宿主级计划任务模型。
|
|
8540
|
+
*
|
|
8541
|
+
* 能力位 `schedulesHost`(host-capabilities.ts)与新帧、旧帧映射全部读这一个
|
|
8542
|
+
* 判别位:探测只发生一次(DshApiProxy.resolveSchedule),这里不再看控制器
|
|
8543
|
+
* 方法,也不看版本号字符串。
|
|
8544
|
+
*/
|
|
8545
|
+
get hostSchedule() {
|
|
8546
|
+
const schedule = this.apiProxy.schedule;
|
|
8547
|
+
return schedule?.mode === "host" ? schedule : void 0;
|
|
8548
|
+
}
|
|
7142
8549
|
async listSchedules(sessionId) {
|
|
7143
8550
|
const schedule = this.apiProxy.schedule;
|
|
7144
8551
|
if (schedule === void 0) return {
|
|
@@ -7146,6 +8553,7 @@ var HostBridge = class {
|
|
|
7146
8553
|
code: "E_UNSUPPORTED",
|
|
7147
8554
|
message: "schedules unavailable on this host version"
|
|
7148
8555
|
};
|
|
8556
|
+
if (schedule.mode === "host") return this.listHostSchedulesAsLegacy(schedule, sessionId);
|
|
7149
8557
|
try {
|
|
7150
8558
|
const response = await schedule.list({
|
|
7151
8559
|
rpcId: randomUUID(),
|
|
@@ -7169,6 +8577,345 @@ var HostBridge = class {
|
|
|
7169
8577
|
};
|
|
7170
8578
|
}
|
|
7171
8579
|
}
|
|
8580
|
+
/** 宿主级列表 -> 旧会话级视图:过滤 session 目标、丢弃 ended、按 scheduledAt 升序。 */
|
|
8581
|
+
async listHostSchedulesAsLegacy(schedule, sessionId) {
|
|
8582
|
+
try {
|
|
8583
|
+
const tasks = [];
|
|
8584
|
+
let offset = 0;
|
|
8585
|
+
for (;;) {
|
|
8586
|
+
const response = await schedule.list({
|
|
8587
|
+
rpcId: randomUUID(),
|
|
8588
|
+
payload: {
|
|
8589
|
+
limit: HOST_SCHEDULE_PAGE_SIZE,
|
|
8590
|
+
offset
|
|
8591
|
+
}
|
|
8592
|
+
});
|
|
8593
|
+
if (!response.result) return {
|
|
8594
|
+
ok: false,
|
|
8595
|
+
code: "E_INTERNAL",
|
|
8596
|
+
message: "schedule list returned no result"
|
|
8597
|
+
};
|
|
8598
|
+
if (!response.result.ok) return wireErrorOf("schedule", response.result.error);
|
|
8599
|
+
const page = response.result.value;
|
|
8600
|
+
tasks.push(...page.tasks);
|
|
8601
|
+
offset += page.tasks.length;
|
|
8602
|
+
if (page.tasks.length === 0 || offset >= page.total) break;
|
|
8603
|
+
}
|
|
8604
|
+
const views = [];
|
|
8605
|
+
for (const task of tasks) {
|
|
8606
|
+
if (task.target.kind !== "session" || task.target.sessionId !== sessionId) continue;
|
|
8607
|
+
if (task.status === "ended") continue;
|
|
8608
|
+
const view = toLegacyScheduleView(task);
|
|
8609
|
+
if (view !== void 0) views.push(view);
|
|
8610
|
+
}
|
|
8611
|
+
views.sort((left, right) => left.scheduledAt < right.scheduledAt ? -1 : left.scheduledAt > right.scheduledAt ? 1 : 0);
|
|
8612
|
+
return {
|
|
8613
|
+
ok: true,
|
|
8614
|
+
value: views
|
|
8615
|
+
};
|
|
8616
|
+
} catch (error) {
|
|
8617
|
+
return wireErrorOf("schedule", hostFailureOf(error, "internal"));
|
|
8618
|
+
}
|
|
8619
|
+
}
|
|
8620
|
+
async listHostSchedules(request = {}) {
|
|
8621
|
+
const schedule = this.hostSchedule;
|
|
8622
|
+
if (schedule === void 0) return {
|
|
8623
|
+
ok: false,
|
|
8624
|
+
code: "E_UNSUPPORTED",
|
|
8625
|
+
message: "host-level schedules unavailable on this host version"
|
|
8626
|
+
};
|
|
8627
|
+
try {
|
|
8628
|
+
const response = await schedule.list({
|
|
8629
|
+
rpcId: randomUUID(),
|
|
8630
|
+
payload: request
|
|
8631
|
+
});
|
|
8632
|
+
if (!response.result) return {
|
|
8633
|
+
ok: false,
|
|
8634
|
+
code: "E_INTERNAL",
|
|
8635
|
+
message: "schedule host list returned no result"
|
|
8636
|
+
};
|
|
8637
|
+
if (!response.result.ok) return wireErrorOf("schedule", response.result.error);
|
|
8638
|
+
return {
|
|
8639
|
+
ok: true,
|
|
8640
|
+
value: response.result.value
|
|
8641
|
+
};
|
|
8642
|
+
} catch (error) {
|
|
8643
|
+
return wireErrorOf("schedule", hostFailureOf(error, "internal"));
|
|
8644
|
+
}
|
|
8645
|
+
}
|
|
8646
|
+
async viewHostSchedule(id) {
|
|
8647
|
+
const schedule = this.hostSchedule;
|
|
8648
|
+
if (schedule === void 0) return {
|
|
8649
|
+
ok: false,
|
|
8650
|
+
code: "E_UNSUPPORTED",
|
|
8651
|
+
message: "host-level schedules unavailable on this host version"
|
|
8652
|
+
};
|
|
8653
|
+
try {
|
|
8654
|
+
const response = await schedule.view({
|
|
8655
|
+
rpcId: randomUUID(),
|
|
8656
|
+
payload: { id }
|
|
8657
|
+
});
|
|
8658
|
+
if (!response.result) return {
|
|
8659
|
+
ok: false,
|
|
8660
|
+
code: "E_INTERNAL",
|
|
8661
|
+
message: "schedule host view returned no result"
|
|
8662
|
+
};
|
|
8663
|
+
if (!response.result.ok) return wireErrorOf("schedule", response.result.error);
|
|
8664
|
+
const value = response.result.value;
|
|
8665
|
+
if (value.kind === "not-found") return {
|
|
8666
|
+
ok: false,
|
|
8667
|
+
code: "E_NOT_FOUND",
|
|
8668
|
+
message: "schedule not found"
|
|
8669
|
+
};
|
|
8670
|
+
return {
|
|
8671
|
+
ok: true,
|
|
8672
|
+
value: value.task
|
|
8673
|
+
};
|
|
8674
|
+
} catch (error) {
|
|
8675
|
+
return wireErrorOf("schedule", hostFailureOf(error, "internal"));
|
|
8676
|
+
}
|
|
8677
|
+
}
|
|
8678
|
+
async createHostSchedule(deviceId, payload) {
|
|
8679
|
+
const schedule = this.hostSchedule;
|
|
8680
|
+
if (schedule === void 0) return {
|
|
8681
|
+
ok: false,
|
|
8682
|
+
code: "E_UNSUPPORTED",
|
|
8683
|
+
message: "host-level schedules unavailable on this host version"
|
|
8684
|
+
};
|
|
8685
|
+
const { clientRequestId, ...request } = payload;
|
|
8686
|
+
const result = await this.scheduleMutations.dispatch(deviceId, clientRequestId, request, async () => {
|
|
8687
|
+
const response = await schedule.create({
|
|
8688
|
+
rpcId: randomUUID(),
|
|
8689
|
+
payload: {
|
|
8690
|
+
title: request.title,
|
|
8691
|
+
prompt: request.prompt,
|
|
8692
|
+
timing: request.timing,
|
|
8693
|
+
...request.target !== void 0 ? { target: request.target } : {}
|
|
8694
|
+
}
|
|
8695
|
+
});
|
|
8696
|
+
if (!response.result) return {
|
|
8697
|
+
ok: false,
|
|
8698
|
+
code: "E_INTERNAL",
|
|
8699
|
+
message: "schedule host create returned no result"
|
|
8700
|
+
};
|
|
8701
|
+
if (!response.result.ok) return wireErrorOf("schedule", response.result.error);
|
|
8702
|
+
return {
|
|
8703
|
+
ok: true,
|
|
8704
|
+
value: response.result.value
|
|
8705
|
+
};
|
|
8706
|
+
});
|
|
8707
|
+
if (result.ok) {
|
|
8708
|
+
this.record("s2c.schedule.changed", {});
|
|
8709
|
+
return {
|
|
8710
|
+
ok: true,
|
|
8711
|
+
value: result.value,
|
|
8712
|
+
replayed: result.replayed
|
|
8713
|
+
};
|
|
8714
|
+
}
|
|
8715
|
+
return {
|
|
8716
|
+
ok: false,
|
|
8717
|
+
code: result.code,
|
|
8718
|
+
message: result.message ?? result.code,
|
|
8719
|
+
replayed: result.replayed
|
|
8720
|
+
};
|
|
8721
|
+
}
|
|
8722
|
+
async editHostSchedule(deviceId, payload) {
|
|
8723
|
+
const schedule = this.hostSchedule;
|
|
8724
|
+
if (schedule === void 0) return {
|
|
8725
|
+
ok: false,
|
|
8726
|
+
code: "E_UNSUPPORTED",
|
|
8727
|
+
message: "host-level schedules unavailable on this host version"
|
|
8728
|
+
};
|
|
8729
|
+
const { clientRequestId, ...request } = payload;
|
|
8730
|
+
const result = await this.scheduleMutations.dispatch(deviceId, clientRequestId, request, async () => {
|
|
8731
|
+
const expected = request.expectedUpdatedAt !== void 0 ? { expectedUpdatedAt: request.expectedUpdatedAt } : {};
|
|
8732
|
+
const response = await schedule.edit({
|
|
8733
|
+
rpcId: randomUUID(),
|
|
8734
|
+
payload: request.action === "update" ? {
|
|
8735
|
+
id: request.id,
|
|
8736
|
+
action: "update",
|
|
8737
|
+
patch: request.patch,
|
|
8738
|
+
...expected
|
|
8739
|
+
} : {
|
|
8740
|
+
id: request.id,
|
|
8741
|
+
action: "delete",
|
|
8742
|
+
...expected
|
|
8743
|
+
}
|
|
8744
|
+
});
|
|
8745
|
+
if (!response.result) return {
|
|
8746
|
+
ok: false,
|
|
8747
|
+
code: "E_INTERNAL",
|
|
8748
|
+
message: "schedule host edit returned no result"
|
|
8749
|
+
};
|
|
8750
|
+
if (!response.result.ok) return wireErrorOf("schedule", response.result.error);
|
|
8751
|
+
const value = response.result.value;
|
|
8752
|
+
if (value.kind === "conflict") return {
|
|
8753
|
+
ok: false,
|
|
8754
|
+
code: "E_BUSY",
|
|
8755
|
+
message: "schedule changed since it was read"
|
|
8756
|
+
};
|
|
8757
|
+
if (value.kind === "not-found") return {
|
|
8758
|
+
ok: false,
|
|
8759
|
+
code: "E_NOT_FOUND",
|
|
8760
|
+
message: "schedule not found"
|
|
8761
|
+
};
|
|
8762
|
+
if (request.action === "delete") {
|
|
8763
|
+
if (value.kind !== "deleted") return {
|
|
8764
|
+
ok: false,
|
|
8765
|
+
code: "E_INTERNAL",
|
|
8766
|
+
message: "schedule delete returned an updated task"
|
|
8767
|
+
};
|
|
8768
|
+
return {
|
|
8769
|
+
ok: true,
|
|
8770
|
+
value: {
|
|
8771
|
+
deleted: true,
|
|
8772
|
+
id: value.id
|
|
8773
|
+
}
|
|
8774
|
+
};
|
|
8775
|
+
}
|
|
8776
|
+
if (value.kind !== "task") return {
|
|
8777
|
+
ok: false,
|
|
8778
|
+
code: "E_INTERNAL",
|
|
8779
|
+
message: "schedule update returned a deletion"
|
|
8780
|
+
};
|
|
8781
|
+
return {
|
|
8782
|
+
ok: true,
|
|
8783
|
+
value: { task: value.task }
|
|
8784
|
+
};
|
|
8785
|
+
});
|
|
8786
|
+
if (result.ok) {
|
|
8787
|
+
this.record("s2c.schedule.changed", {});
|
|
8788
|
+
return {
|
|
8789
|
+
ok: true,
|
|
8790
|
+
value: result.value,
|
|
8791
|
+
replayed: result.replayed
|
|
8792
|
+
};
|
|
8793
|
+
}
|
|
8794
|
+
return {
|
|
8795
|
+
ok: false,
|
|
8796
|
+
code: result.code,
|
|
8797
|
+
message: result.message ?? result.code,
|
|
8798
|
+
replayed: result.replayed
|
|
8799
|
+
};
|
|
8800
|
+
}
|
|
8801
|
+
async runHostSchedule(deviceId, payload) {
|
|
8802
|
+
const schedule = this.hostSchedule;
|
|
8803
|
+
if (schedule === void 0) return {
|
|
8804
|
+
ok: false,
|
|
8805
|
+
code: "E_UNSUPPORTED",
|
|
8806
|
+
message: "host-level schedules unavailable on this host version"
|
|
8807
|
+
};
|
|
8808
|
+
const result = await this.scheduleMutations.dispatch(deviceId, payload.clientRequestId, { id: payload.id }, async () => {
|
|
8809
|
+
const response = await schedule.run({
|
|
8810
|
+
rpcId: randomUUID(),
|
|
8811
|
+
payload: { id: payload.id }
|
|
8812
|
+
});
|
|
8813
|
+
if (!response.result) return {
|
|
8814
|
+
ok: false,
|
|
8815
|
+
code: "E_INTERNAL",
|
|
8816
|
+
message: "schedule host run returned no result"
|
|
8817
|
+
};
|
|
8818
|
+
if (!response.result.ok) return wireErrorOf("schedule", response.result.error);
|
|
8819
|
+
const value = response.result.value;
|
|
8820
|
+
if (value.kind === "pending") return {
|
|
8821
|
+
ok: false,
|
|
8822
|
+
code: "E_BUSY",
|
|
8823
|
+
message: "schedule execution already pending"
|
|
8824
|
+
};
|
|
8825
|
+
if (value.kind === "not-found") return {
|
|
8826
|
+
ok: false,
|
|
8827
|
+
code: "E_NOT_FOUND",
|
|
8828
|
+
message: "schedule not found"
|
|
8829
|
+
};
|
|
8830
|
+
return {
|
|
8831
|
+
ok: true,
|
|
8832
|
+
value: value.task
|
|
8833
|
+
};
|
|
8834
|
+
});
|
|
8835
|
+
if (result.ok) {
|
|
8836
|
+
this.record("s2c.schedule.changed", {});
|
|
8837
|
+
return {
|
|
8838
|
+
ok: true,
|
|
8839
|
+
value: result.value,
|
|
8840
|
+
replayed: result.replayed
|
|
8841
|
+
};
|
|
8842
|
+
}
|
|
8843
|
+
return {
|
|
8844
|
+
ok: false,
|
|
8845
|
+
code: result.code,
|
|
8846
|
+
message: result.message ?? result.code,
|
|
8847
|
+
replayed: result.replayed
|
|
8848
|
+
};
|
|
8849
|
+
}
|
|
8850
|
+
/**
|
|
8851
|
+
* 删除宿主上全部已结束(`status="ended"`)的任务,返回删除条数。
|
|
8852
|
+
*
|
|
8853
|
+
* 与其他变更帧共用 scheduleMutations journal:同一 clientRequestId 重放时直接回放
|
|
8854
|
+
* 首次结果,不会再删一次。成功后广播 `s2c.schedule.changed`;失败时同样广播——上游
|
|
8855
|
+
* 文档说明失败的批次无法确定哪些行已删除,客户端应重新读取列表。
|
|
8856
|
+
*/
|
|
8857
|
+
async deleteEndedHostSchedules(deviceId, payload) {
|
|
8858
|
+
const schedule = this.hostSchedule;
|
|
8859
|
+
if (schedule === void 0) return {
|
|
8860
|
+
ok: false,
|
|
8861
|
+
code: "E_UNSUPPORTED",
|
|
8862
|
+
message: "host-level schedules unavailable on this host version"
|
|
8863
|
+
};
|
|
8864
|
+
const result = await this.scheduleMutations.dispatch(deviceId, payload.clientRequestId, { operation: "deleteEnded" }, async () => {
|
|
8865
|
+
const response = await schedule.deleteEnded({
|
|
8866
|
+
rpcId: randomUUID(),
|
|
8867
|
+
payload: {}
|
|
8868
|
+
});
|
|
8869
|
+
if (!response.result) return {
|
|
8870
|
+
ok: false,
|
|
8871
|
+
code: "E_INTERNAL",
|
|
8872
|
+
message: "schedule delete ended returned no result"
|
|
8873
|
+
};
|
|
8874
|
+
if (!response.result.ok) return wireErrorOf("schedule", response.result.error);
|
|
8875
|
+
return {
|
|
8876
|
+
ok: true,
|
|
8877
|
+
value: response.result.value
|
|
8878
|
+
};
|
|
8879
|
+
});
|
|
8880
|
+
this.record("s2c.schedule.changed", {});
|
|
8881
|
+
if (result.ok) return {
|
|
8882
|
+
ok: true,
|
|
8883
|
+
value: result.value ?? {},
|
|
8884
|
+
replayed: result.replayed
|
|
8885
|
+
};
|
|
8886
|
+
return {
|
|
8887
|
+
ok: false,
|
|
8888
|
+
code: result.code,
|
|
8889
|
+
message: result.message ?? result.code,
|
|
8890
|
+
replayed: result.replayed
|
|
8891
|
+
};
|
|
8892
|
+
}
|
|
8893
|
+
async scheduleLimits() {
|
|
8894
|
+
const schedule = this.hostSchedule;
|
|
8895
|
+
if (schedule === void 0) return {
|
|
8896
|
+
ok: false,
|
|
8897
|
+
code: "E_UNSUPPORTED",
|
|
8898
|
+
message: "host-level schedules unavailable on this host version"
|
|
8899
|
+
};
|
|
8900
|
+
try {
|
|
8901
|
+
const response = await schedule.limits({
|
|
8902
|
+
rpcId: randomUUID(),
|
|
8903
|
+
payload: {}
|
|
8904
|
+
});
|
|
8905
|
+
if (!response.result) return {
|
|
8906
|
+
ok: false,
|
|
8907
|
+
code: "E_INTERNAL",
|
|
8908
|
+
message: "schedule limits returned no result"
|
|
8909
|
+
};
|
|
8910
|
+
if (!response.result.ok) return wireErrorOf("schedule", response.result.error);
|
|
8911
|
+
return {
|
|
8912
|
+
ok: true,
|
|
8913
|
+
value: response.result.value
|
|
8914
|
+
};
|
|
8915
|
+
} catch (error) {
|
|
8916
|
+
return wireErrorOf("schedule", hostFailureOf(error, "internal"));
|
|
8917
|
+
}
|
|
8918
|
+
}
|
|
7172
8919
|
async scheduleHistory(sessionId, id, limit, before) {
|
|
7173
8920
|
const schedule = this.apiProxy.schedule;
|
|
7174
8921
|
if (schedule === void 0) return {
|
|
@@ -7176,6 +8923,28 @@ var HostBridge = class {
|
|
|
7176
8923
|
code: "E_UNSUPPORTED",
|
|
7177
8924
|
message: "schedules unavailable on this host version"
|
|
7178
8925
|
};
|
|
8926
|
+
if (schedule.mode === "host") try {
|
|
8927
|
+
const response = await schedule.history({
|
|
8928
|
+
rpcId: randomUUID(),
|
|
8929
|
+
payload: {
|
|
8930
|
+
id,
|
|
8931
|
+
limit,
|
|
8932
|
+
...before !== void 0 ? { before } : {}
|
|
8933
|
+
}
|
|
8934
|
+
});
|
|
8935
|
+
if (!response.result) return {
|
|
8936
|
+
ok: false,
|
|
8937
|
+
code: "E_INTERNAL",
|
|
8938
|
+
message: "schedule history returned no result"
|
|
8939
|
+
};
|
|
8940
|
+
if (!response.result.ok) return wireErrorOf("schedule", response.result.error);
|
|
8941
|
+
return {
|
|
8942
|
+
ok: true,
|
|
8943
|
+
value: response.result.value
|
|
8944
|
+
};
|
|
8945
|
+
} catch (error) {
|
|
8946
|
+
return wireErrorOf("schedule", hostFailureOf(error, "internal"));
|
|
8947
|
+
}
|
|
7179
8948
|
try {
|
|
7180
8949
|
const response = await schedule.history({
|
|
7181
8950
|
rpcId: randomUUID(),
|
|
@@ -7203,14 +8972,76 @@ var HostBridge = class {
|
|
|
7203
8972
|
message: String(error)
|
|
7204
8973
|
};
|
|
7205
8974
|
}
|
|
7206
|
-
}
|
|
7207
|
-
async createSchedule(deviceId, payload) {
|
|
7208
|
-
const schedule = this.apiProxy.schedule;
|
|
7209
|
-
if (schedule === void 0) return {
|
|
7210
|
-
ok: false,
|
|
7211
|
-
code: "E_UNSUPPORTED",
|
|
7212
|
-
message: "schedules unavailable on this host version"
|
|
7213
|
-
};
|
|
8975
|
+
}
|
|
8976
|
+
async createSchedule(deviceId, payload) {
|
|
8977
|
+
const schedule = this.apiProxy.schedule;
|
|
8978
|
+
if (schedule === void 0) return {
|
|
8979
|
+
ok: false,
|
|
8980
|
+
code: "E_UNSUPPORTED",
|
|
8981
|
+
message: "schedules unavailable on this host version"
|
|
8982
|
+
};
|
|
8983
|
+
if (schedule.mode === "host") {
|
|
8984
|
+
const { clientRequestId, sessionId, title, prompt, ...selectors } = payload;
|
|
8985
|
+
const timing = hostTimingFromLegacySelector(selectors);
|
|
8986
|
+
if (timing === void 0) return {
|
|
8987
|
+
ok: false,
|
|
8988
|
+
code: "E_PROTOCOL",
|
|
8989
|
+
message: "unsupported schedule selector"
|
|
8990
|
+
};
|
|
8991
|
+
const request = {
|
|
8992
|
+
title,
|
|
8993
|
+
prompt,
|
|
8994
|
+
timing,
|
|
8995
|
+
target: {
|
|
8996
|
+
kind: "session",
|
|
8997
|
+
sessionId
|
|
8998
|
+
}
|
|
8999
|
+
};
|
|
9000
|
+
const result = await this.scheduleMutations.dispatch(deviceId, clientRequestId, request, async () => {
|
|
9001
|
+
const response = await schedule.create({
|
|
9002
|
+
rpcId: randomUUID(),
|
|
9003
|
+
payload: {
|
|
9004
|
+
title,
|
|
9005
|
+
prompt,
|
|
9006
|
+
timing,
|
|
9007
|
+
target: {
|
|
9008
|
+
kind: "session",
|
|
9009
|
+
sessionId
|
|
9010
|
+
}
|
|
9011
|
+
}
|
|
9012
|
+
});
|
|
9013
|
+
if (!response.result) return {
|
|
9014
|
+
ok: false,
|
|
9015
|
+
code: "E_INTERNAL",
|
|
9016
|
+
message: "schedule create returned no result"
|
|
9017
|
+
};
|
|
9018
|
+
if (!response.result.ok) return wireErrorOf("schedule", response.result.error);
|
|
9019
|
+
const legacy = toLegacyScheduleView(response.result.value);
|
|
9020
|
+
if (legacy === void 0) return {
|
|
9021
|
+
ok: false,
|
|
9022
|
+
code: "E_PROTOCOL",
|
|
9023
|
+
message: "schedule cannot be represented in the session-scoped view"
|
|
9024
|
+
};
|
|
9025
|
+
return {
|
|
9026
|
+
ok: true,
|
|
9027
|
+
value: legacy
|
|
9028
|
+
};
|
|
9029
|
+
});
|
|
9030
|
+
if (result.ok) {
|
|
9031
|
+
this.record("s2c.schedule.changed", {});
|
|
9032
|
+
return {
|
|
9033
|
+
ok: true,
|
|
9034
|
+
value: result.value,
|
|
9035
|
+
replayed: result.replayed
|
|
9036
|
+
};
|
|
9037
|
+
}
|
|
9038
|
+
return {
|
|
9039
|
+
ok: false,
|
|
9040
|
+
code: result.code,
|
|
9041
|
+
message: result.message ?? result.code,
|
|
9042
|
+
replayed: result.replayed
|
|
9043
|
+
};
|
|
9044
|
+
}
|
|
7214
9045
|
const { clientRequestId, sessionId, ...request } = payload;
|
|
7215
9046
|
const result = await this.scheduleMutations.dispatch(deviceId, clientRequestId, {
|
|
7216
9047
|
sessionId,
|
|
@@ -7256,6 +9087,106 @@ var HostBridge = class {
|
|
|
7256
9087
|
code: "E_UNSUPPORTED",
|
|
7257
9088
|
message: "schedules unavailable on this host version"
|
|
7258
9089
|
};
|
|
9090
|
+
if (schedule.mode === "host") {
|
|
9091
|
+
const request = {
|
|
9092
|
+
sessionId: payload.sessionId,
|
|
9093
|
+
id: payload.id,
|
|
9094
|
+
expected: payload.expected,
|
|
9095
|
+
title: payload.title,
|
|
9096
|
+
prompt: payload.prompt,
|
|
9097
|
+
change: payload.change
|
|
9098
|
+
};
|
|
9099
|
+
const result = await this.scheduleMutations.dispatch(deviceId, payload.clientRequestId, request, async () => {
|
|
9100
|
+
const current = await schedule.view({
|
|
9101
|
+
rpcId: randomUUID(),
|
|
9102
|
+
payload: { id: payload.id }
|
|
9103
|
+
});
|
|
9104
|
+
if (!current.result) return {
|
|
9105
|
+
ok: false,
|
|
9106
|
+
code: "E_INTERNAL",
|
|
9107
|
+
message: "schedule view returned no result"
|
|
9108
|
+
};
|
|
9109
|
+
if (!current.result.ok) return wireErrorOf("schedule", current.result.error);
|
|
9110
|
+
if (current.result.value.kind === "not-found") return {
|
|
9111
|
+
ok: false,
|
|
9112
|
+
code: "E_NOT_FOUND",
|
|
9113
|
+
message: "schedule not found"
|
|
9114
|
+
};
|
|
9115
|
+
const currentTask = current.result.value.task;
|
|
9116
|
+
if (!legacyScheduleMatchesExpected(currentTask, payload.expected)) return {
|
|
9117
|
+
ok: false,
|
|
9118
|
+
code: "E_BUSY",
|
|
9119
|
+
message: "schedule changed since it was read"
|
|
9120
|
+
};
|
|
9121
|
+
const patch = hostPatchFromLegacyUpdate(payload);
|
|
9122
|
+
if (Object.keys(patch).length === 0) {
|
|
9123
|
+
const legacy = toLegacyScheduleView(currentTask);
|
|
9124
|
+
if (legacy === void 0) return {
|
|
9125
|
+
ok: false,
|
|
9126
|
+
code: "E_PROTOCOL",
|
|
9127
|
+
message: "schedule cannot be represented in the session-scoped view"
|
|
9128
|
+
};
|
|
9129
|
+
return {
|
|
9130
|
+
ok: true,
|
|
9131
|
+
value: legacy
|
|
9132
|
+
};
|
|
9133
|
+
}
|
|
9134
|
+
const response = await schedule.edit({
|
|
9135
|
+
rpcId: randomUUID(),
|
|
9136
|
+
payload: {
|
|
9137
|
+
id: payload.id,
|
|
9138
|
+
action: "update",
|
|
9139
|
+
patch
|
|
9140
|
+
}
|
|
9141
|
+
});
|
|
9142
|
+
if (!response.result) return {
|
|
9143
|
+
ok: false,
|
|
9144
|
+
code: "E_INTERNAL",
|
|
9145
|
+
message: "schedule edit returned no result"
|
|
9146
|
+
};
|
|
9147
|
+
if (!response.result.ok) return wireErrorOf("schedule", response.result.error);
|
|
9148
|
+
const value = response.result.value;
|
|
9149
|
+
if (value.kind === "conflict") return {
|
|
9150
|
+
ok: false,
|
|
9151
|
+
code: "E_BUSY",
|
|
9152
|
+
message: "schedule changed since it was read"
|
|
9153
|
+
};
|
|
9154
|
+
if (value.kind === "not-found") return {
|
|
9155
|
+
ok: false,
|
|
9156
|
+
code: "E_NOT_FOUND",
|
|
9157
|
+
message: "schedule not found"
|
|
9158
|
+
};
|
|
9159
|
+
if (value.kind !== "task") return {
|
|
9160
|
+
ok: false,
|
|
9161
|
+
code: "E_INTERNAL",
|
|
9162
|
+
message: "schedule edit returned an unexpected deletion"
|
|
9163
|
+
};
|
|
9164
|
+
const legacy = toLegacyScheduleView(value.task);
|
|
9165
|
+
if (legacy === void 0) return {
|
|
9166
|
+
ok: false,
|
|
9167
|
+
code: "E_PROTOCOL",
|
|
9168
|
+
message: "schedule cannot be represented in the session-scoped view"
|
|
9169
|
+
};
|
|
9170
|
+
return {
|
|
9171
|
+
ok: true,
|
|
9172
|
+
value: legacy
|
|
9173
|
+
};
|
|
9174
|
+
});
|
|
9175
|
+
if (result.ok) {
|
|
9176
|
+
this.record("s2c.schedule.changed", {});
|
|
9177
|
+
return {
|
|
9178
|
+
ok: true,
|
|
9179
|
+
value: result.value,
|
|
9180
|
+
replayed: result.replayed
|
|
9181
|
+
};
|
|
9182
|
+
}
|
|
9183
|
+
return {
|
|
9184
|
+
ok: false,
|
|
9185
|
+
code: result.code,
|
|
9186
|
+
message: result.message ?? result.code,
|
|
9187
|
+
replayed: result.replayed
|
|
9188
|
+
};
|
|
9189
|
+
}
|
|
7259
9190
|
const { clientRequestId, ...request } = payload;
|
|
7260
9191
|
const result = await this.scheduleMutations.dispatch(deviceId, clientRequestId, request, async () => {
|
|
7261
9192
|
const response = await schedule.update({
|
|
@@ -7301,6 +9232,63 @@ var HostBridge = class {
|
|
|
7301
9232
|
code: "E_UNSUPPORTED",
|
|
7302
9233
|
message: "schedules unavailable on this host version"
|
|
7303
9234
|
};
|
|
9235
|
+
if (schedule.mode === "host") {
|
|
9236
|
+
const result = await this.scheduleMutations.dispatch(deviceId, payload.clientRequestId, {
|
|
9237
|
+
sessionId: payload.sessionId,
|
|
9238
|
+
id: payload.id
|
|
9239
|
+
}, async () => {
|
|
9240
|
+
const response = await schedule.edit({
|
|
9241
|
+
rpcId: randomUUID(),
|
|
9242
|
+
payload: {
|
|
9243
|
+
id: payload.id,
|
|
9244
|
+
action: "delete"
|
|
9245
|
+
}
|
|
9246
|
+
});
|
|
9247
|
+
if (!response.result) return {
|
|
9248
|
+
ok: false,
|
|
9249
|
+
code: "E_INTERNAL",
|
|
9250
|
+
message: "schedule delete returned no result"
|
|
9251
|
+
};
|
|
9252
|
+
if (!response.result.ok) return wireErrorOf("schedule", response.result.error);
|
|
9253
|
+
const value = response.result.value;
|
|
9254
|
+
if (value.kind === "deleted") return {
|
|
9255
|
+
ok: true,
|
|
9256
|
+
value: {
|
|
9257
|
+
id: value.id,
|
|
9258
|
+
deleted: true
|
|
9259
|
+
}
|
|
9260
|
+
};
|
|
9261
|
+
if (value.kind === "not-found") return {
|
|
9262
|
+
ok: false,
|
|
9263
|
+
code: "E_NOT_FOUND",
|
|
9264
|
+
message: "schedule not found"
|
|
9265
|
+
};
|
|
9266
|
+
if (value.kind === "conflict") return {
|
|
9267
|
+
ok: false,
|
|
9268
|
+
code: "E_BUSY",
|
|
9269
|
+
message: "schedule changed since it was read"
|
|
9270
|
+
};
|
|
9271
|
+
return {
|
|
9272
|
+
ok: false,
|
|
9273
|
+
code: "E_INTERNAL",
|
|
9274
|
+
message: "schedule delete returned an updated task"
|
|
9275
|
+
};
|
|
9276
|
+
});
|
|
9277
|
+
if (result.ok) {
|
|
9278
|
+
this.record("s2c.schedule.changed", {});
|
|
9279
|
+
return {
|
|
9280
|
+
ok: true,
|
|
9281
|
+
value: result.value,
|
|
9282
|
+
replayed: result.replayed
|
|
9283
|
+
};
|
|
9284
|
+
}
|
|
9285
|
+
return {
|
|
9286
|
+
ok: false,
|
|
9287
|
+
code: result.code,
|
|
9288
|
+
message: result.message ?? result.code,
|
|
9289
|
+
replayed: result.replayed
|
|
9290
|
+
};
|
|
9291
|
+
}
|
|
7304
9292
|
const result = await this.scheduleMutations.dispatch(deviceId, payload.clientRequestId, {
|
|
7305
9293
|
sessionId: payload.sessionId,
|
|
7306
9294
|
id: payload.id
|
|
@@ -7346,7 +9334,7 @@ var HostBridge = class {
|
|
|
7346
9334
|
replayed: result.replayed
|
|
7347
9335
|
};
|
|
7348
9336
|
}
|
|
7349
|
-
async sendPrompt(sessionId, text, images = [], documents = []) {
|
|
9337
|
+
async sendPrompt(sessionId, text, images = [], documents = [], options = {}) {
|
|
7350
9338
|
try {
|
|
7351
9339
|
const content = [];
|
|
7352
9340
|
if (text.trim().length > 0) content.push({
|
|
@@ -7362,10 +9350,10 @@ var HostBridge = class {
|
|
|
7362
9350
|
text: documentPromptBlock(document)
|
|
7363
9351
|
});
|
|
7364
9352
|
const response = await this.apiProxy.sessions.prompt({
|
|
7365
|
-
rpcId: randomUUID(),
|
|
9353
|
+
rpcId: options.clientSendId !== void 0 ? promptRequestId(options.clientSendId) : randomUUID(),
|
|
7366
9354
|
payload: {
|
|
7367
9355
|
sessionId,
|
|
7368
|
-
mode: "queue",
|
|
9356
|
+
mode: options.mode ?? "queue",
|
|
7369
9357
|
content,
|
|
7370
9358
|
clientTimeZone: localTimeZone()
|
|
7371
9359
|
}
|
|
@@ -7685,8 +9673,236 @@ function projectSessionModels(value) {
|
|
|
7685
9673
|
};
|
|
7686
9674
|
}
|
|
7687
9675
|
/** Project a history page (raw events) into MessageProjection rows. */
|
|
7688
|
-
|
|
7689
|
-
|
|
9676
|
+
/** 宿主级列表全量拉取时的页大小(Host 允许的最大值)。 */
|
|
9677
|
+
const HOST_SCHEDULE_PAGE_SIZE = 500;
|
|
9678
|
+
/** 参与旧帧冲突比较的字段:`state`/`deliveryMode` 是派生字段,不参与。 */
|
|
9679
|
+
const LEGACY_PROJECTION_FIELDS = [
|
|
9680
|
+
"id",
|
|
9681
|
+
"kind",
|
|
9682
|
+
"title",
|
|
9683
|
+
"prompt",
|
|
9684
|
+
"scheduledAt",
|
|
9685
|
+
"afterSeconds",
|
|
9686
|
+
"everySeconds",
|
|
9687
|
+
"time",
|
|
9688
|
+
"timeZone",
|
|
9689
|
+
"weekdays",
|
|
9690
|
+
"expression"
|
|
9691
|
+
];
|
|
9692
|
+
function isPlainObject$1(value) {
|
|
9693
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
9694
|
+
}
|
|
9695
|
+
function legacyNumber(...values) {
|
|
9696
|
+
for (const value of values) if (typeof value === "number" && Number.isFinite(value)) return value;
|
|
9697
|
+
}
|
|
9698
|
+
function legacyString(...values) {
|
|
9699
|
+
for (const value of values) if (typeof value === "string" && value.length > 0) return value;
|
|
9700
|
+
}
|
|
9701
|
+
function normalizeAtInput(value) {
|
|
9702
|
+
if (typeof value === "string" && value.length > 0) return value;
|
|
9703
|
+
if (!isPlainObject$1(value)) return void 0;
|
|
9704
|
+
const date = legacyString(value.date);
|
|
9705
|
+
const time = legacyString(value.time);
|
|
9706
|
+
const zone = legacyString(value.time_zone, value.timeZone);
|
|
9707
|
+
return date !== void 0 && time !== void 0 && zone !== void 0 ? {
|
|
9708
|
+
date,
|
|
9709
|
+
time,
|
|
9710
|
+
time_zone: zone
|
|
9711
|
+
} : void 0;
|
|
9712
|
+
}
|
|
9713
|
+
function normalizeLocalTime(value) {
|
|
9714
|
+
if (!isPlainObject$1(value)) return void 0;
|
|
9715
|
+
const time = legacyString(value.time);
|
|
9716
|
+
const zone = legacyString(value.time_zone, value.timeZone);
|
|
9717
|
+
return time !== void 0 && zone !== void 0 ? {
|
|
9718
|
+
time,
|
|
9719
|
+
time_zone: zone
|
|
9720
|
+
} : void 0;
|
|
9721
|
+
}
|
|
9722
|
+
function normalizeWeekdays(value) {
|
|
9723
|
+
if (!Array.isArray(value) || value.length === 0) return void 0;
|
|
9724
|
+
const days = value.filter((day) => typeof day === "number" && Number.isSafeInteger(day) && day >= 1 && day <= 7);
|
|
9725
|
+
return days.length === value.length ? days : void 0;
|
|
9726
|
+
}
|
|
9727
|
+
/** 旧创建帧的选择器 -> 宿主级 timing。旧 wire 校验保证恰好一个选择器。 */
|
|
9728
|
+
function hostTimingFromLegacySelector(selectors) {
|
|
9729
|
+
const after = legacyNumber(selectors.after_seconds);
|
|
9730
|
+
if (after !== void 0) return {
|
|
9731
|
+
kind: "after",
|
|
9732
|
+
after_seconds: after
|
|
9733
|
+
};
|
|
9734
|
+
if (selectors.at !== void 0) {
|
|
9735
|
+
const at = normalizeAtInput(selectors.at);
|
|
9736
|
+
return at === void 0 ? void 0 : {
|
|
9737
|
+
kind: "at",
|
|
9738
|
+
at
|
|
9739
|
+
};
|
|
9740
|
+
}
|
|
9741
|
+
const every = legacyNumber(selectors.every_seconds);
|
|
9742
|
+
if (every !== void 0) return {
|
|
9743
|
+
kind: "every",
|
|
9744
|
+
every_seconds: every
|
|
9745
|
+
};
|
|
9746
|
+
if (selectors.daily !== void 0) {
|
|
9747
|
+
const daily = normalizeLocalTime(selectors.daily);
|
|
9748
|
+
return daily === void 0 ? void 0 : {
|
|
9749
|
+
kind: "daily",
|
|
9750
|
+
daily
|
|
9751
|
+
};
|
|
9752
|
+
}
|
|
9753
|
+
if (selectors.weekly !== void 0) {
|
|
9754
|
+
if (!isPlainObject$1(selectors.weekly)) return void 0;
|
|
9755
|
+
const time = normalizeLocalTime(selectors.weekly);
|
|
9756
|
+
const weekdays = normalizeWeekdays(selectors.weekly.weekdays);
|
|
9757
|
+
return time === void 0 || weekdays === void 0 ? void 0 : {
|
|
9758
|
+
kind: "weekly",
|
|
9759
|
+
weekly: {
|
|
9760
|
+
...time,
|
|
9761
|
+
weekdays
|
|
9762
|
+
}
|
|
9763
|
+
};
|
|
9764
|
+
}
|
|
9765
|
+
if (selectors.cron !== void 0) {
|
|
9766
|
+
if (!isPlainObject$1(selectors.cron)) return void 0;
|
|
9767
|
+
const expression = legacyString(selectors.cron.expression);
|
|
9768
|
+
const zone = legacyString(selectors.cron.time_zone, selectors.cron.timeZone);
|
|
9769
|
+
return expression === void 0 || zone === void 0 ? void 0 : {
|
|
9770
|
+
kind: "cron",
|
|
9771
|
+
cron: {
|
|
9772
|
+
expression,
|
|
9773
|
+
time_zone: zone
|
|
9774
|
+
}
|
|
9775
|
+
};
|
|
9776
|
+
}
|
|
9777
|
+
}
|
|
9778
|
+
/**
|
|
9779
|
+
* 旧更新帧的 `change` -> 宿主级 timing。
|
|
9780
|
+
*
|
|
9781
|
+
* 两种历史写法都认:选择器式(`{kind:'daily', daily:{...}}`,与创建帧同形)与
|
|
9782
|
+
* 扁平式(`{kind:'daily', time, timeZone}` / `{kind:'cron', expression}`)。
|
|
9783
|
+
*/
|
|
9784
|
+
function hostTimingFromLegacyChange(change) {
|
|
9785
|
+
if (!isPlainObject$1(change)) return void 0;
|
|
9786
|
+
const selector = hostTimingFromLegacySelector(change);
|
|
9787
|
+
if (selector !== void 0) return selector;
|
|
9788
|
+
switch (change.kind) {
|
|
9789
|
+
case "daily": {
|
|
9790
|
+
const time = legacyString(change.time);
|
|
9791
|
+
const zone = legacyString(change.time_zone, change.timeZone);
|
|
9792
|
+
return time === void 0 || zone === void 0 ? void 0 : {
|
|
9793
|
+
kind: "daily",
|
|
9794
|
+
daily: {
|
|
9795
|
+
time,
|
|
9796
|
+
time_zone: zone
|
|
9797
|
+
}
|
|
9798
|
+
};
|
|
9799
|
+
}
|
|
9800
|
+
case "weekly": {
|
|
9801
|
+
const time = legacyString(change.time);
|
|
9802
|
+
const zone = legacyString(change.time_zone, change.timeZone);
|
|
9803
|
+
const weekdays = normalizeWeekdays(change.weekdays);
|
|
9804
|
+
return time === void 0 || zone === void 0 || weekdays === void 0 ? void 0 : {
|
|
9805
|
+
kind: "weekly",
|
|
9806
|
+
weekly: {
|
|
9807
|
+
time,
|
|
9808
|
+
time_zone: zone,
|
|
9809
|
+
weekdays
|
|
9810
|
+
}
|
|
9811
|
+
};
|
|
9812
|
+
}
|
|
9813
|
+
case "cron": {
|
|
9814
|
+
const expression = legacyString(change.expression);
|
|
9815
|
+
const zone = legacyString(change.time_zone, change.timeZone);
|
|
9816
|
+
return expression === void 0 || zone === void 0 ? void 0 : {
|
|
9817
|
+
kind: "cron",
|
|
9818
|
+
cron: {
|
|
9819
|
+
expression,
|
|
9820
|
+
time_zone: zone
|
|
9821
|
+
}
|
|
9822
|
+
};
|
|
9823
|
+
}
|
|
9824
|
+
case "rrule": {
|
|
9825
|
+
const rrule = legacyString(change.rrule);
|
|
9826
|
+
return rrule === void 0 ? void 0 : {
|
|
9827
|
+
kind: "rrule",
|
|
9828
|
+
rrule
|
|
9829
|
+
};
|
|
9830
|
+
}
|
|
9831
|
+
default: return;
|
|
9832
|
+
}
|
|
9833
|
+
}
|
|
9834
|
+
/** 旧更新帧的 title/prompt/change -> 宿主级 patch。 */
|
|
9835
|
+
function hostPatchFromLegacyUpdate(payload) {
|
|
9836
|
+
const patch = {};
|
|
9837
|
+
if (typeof payload.title === "string") patch.title = payload.title;
|
|
9838
|
+
if (typeof payload.prompt === "string") patch.prompt = payload.prompt;
|
|
9839
|
+
const timing = hostTimingFromLegacyChange(payload.change);
|
|
9840
|
+
if (timing !== void 0) patch.timing = timing;
|
|
9841
|
+
return patch;
|
|
9842
|
+
}
|
|
9843
|
+
/**
|
|
9844
|
+
* 宿主级任务视图 -> 旧会话级视图。
|
|
9845
|
+
*
|
|
9846
|
+
* `state` 只由 `scheduledAt` 是否已过推导(PROTOCOL.md 映射表);`rrule` 在旧枚举
|
|
9847
|
+
* 里没有对应 kind,返回 undefined 让调用方跳过——旧客户端按枚举解码列表,塞进
|
|
9848
|
+
* 未知 kind 会让整页解码失败,静默隐藏比让旧 App 打不开列表更安全。
|
|
9849
|
+
*/
|
|
9850
|
+
function toLegacyScheduleView(task) {
|
|
9851
|
+
const instant = Date.parse(task.scheduledAt);
|
|
9852
|
+
const base = {
|
|
9853
|
+
id: task.id,
|
|
9854
|
+
title: task.title,
|
|
9855
|
+
prompt: task.prompt,
|
|
9856
|
+
scheduledAt: task.scheduledAt,
|
|
9857
|
+
state: Number.isFinite(instant) && instant < Date.now() ? "overdue" : "scheduled",
|
|
9858
|
+
deliveryMode: "host"
|
|
9859
|
+
};
|
|
9860
|
+
switch (task.timing.kind) {
|
|
9861
|
+
case "after": return {
|
|
9862
|
+
...base,
|
|
9863
|
+
kind: "after",
|
|
9864
|
+
afterSeconds: task.timing.after_seconds
|
|
9865
|
+
};
|
|
9866
|
+
case "at": return {
|
|
9867
|
+
...base,
|
|
9868
|
+
kind: "at"
|
|
9869
|
+
};
|
|
9870
|
+
case "every": return {
|
|
9871
|
+
...base,
|
|
9872
|
+
kind: "every",
|
|
9873
|
+
everySeconds: task.timing.every_seconds
|
|
9874
|
+
};
|
|
9875
|
+
case "daily": return {
|
|
9876
|
+
...base,
|
|
9877
|
+
kind: "daily",
|
|
9878
|
+
time: task.timing.daily.time,
|
|
9879
|
+
timeZone: task.timing.daily.time_zone
|
|
9880
|
+
};
|
|
9881
|
+
case "weekly": return {
|
|
9882
|
+
...base,
|
|
9883
|
+
kind: "weekly",
|
|
9884
|
+
time: task.timing.weekly.time,
|
|
9885
|
+
timeZone: task.timing.weekly.time_zone,
|
|
9886
|
+
weekdays: [...task.timing.weekly.weekdays]
|
|
9887
|
+
};
|
|
9888
|
+
case "cron": return {
|
|
9889
|
+
...base,
|
|
9890
|
+
kind: "cron",
|
|
9891
|
+
expression: task.timing.cron.expression,
|
|
9892
|
+
timeZone: task.timing.cron.time_zone
|
|
9893
|
+
};
|
|
9894
|
+
default: return;
|
|
9895
|
+
}
|
|
9896
|
+
}
|
|
9897
|
+
/** 旧帧的冲突保护:当前任务的旧投影与客户端回传的 `expected` 逐字段相等。 */
|
|
9898
|
+
function legacyScheduleMatchesExpected(task, expected) {
|
|
9899
|
+
const view = toLegacyScheduleView(task);
|
|
9900
|
+
if (view === void 0) return false;
|
|
9901
|
+
if (!isPlainObject$1(expected)) return false;
|
|
9902
|
+
const actual = view;
|
|
9903
|
+
return LEGACY_PROJECTION_FIELDS.every((key) => JSON.stringify(actual[key]) === JSON.stringify(expected[key]));
|
|
9904
|
+
}
|
|
9905
|
+
// src/dsh-remote-interactions.ts
|
|
7690
9906
|
/**
|
|
7691
9907
|
* Resident DSH Remote Events client for phone interactions.
|
|
7692
9908
|
*
|
|
@@ -7874,8 +10090,7 @@ async function startDshRemoteInteractions(hostCtx, handlers) {
|
|
|
7874
10090
|
throw error;
|
|
7875
10091
|
}
|
|
7876
10092
|
}
|
|
7877
|
-
|
|
7878
|
-
//#region src/command-api.ts
|
|
10093
|
+
// src/command-api.ts
|
|
7879
10094
|
/**
|
|
7880
10095
|
* 给本域自造的失败带上 Host 风格的码。
|
|
7881
10096
|
*
|
|
@@ -7897,6 +10112,11 @@ var DshCommandApi = class {
|
|
|
7897
10112
|
get available() {
|
|
7898
10113
|
return this.ctx.get("commands") !== void 0 && typeof this.ctx.get("typertGateway")?.invoke === "function";
|
|
7899
10114
|
}
|
|
10115
|
+
get markdownAvailable() {
|
|
10116
|
+
const connection = this.connection();
|
|
10117
|
+
const route = connection?.fetchRoutes instanceof Map ? connection.fetchRoutes.get("/api/session.markdown") : void 0;
|
|
10118
|
+
return typeof connection?.createSharedFetchHandler === "function" && route?.methods instanceof Set && route.methods.has("GET") && typeof route.fetch === "function" && typeof this.ctx.get("sessionQuery")?.readSession === "function";
|
|
10119
|
+
}
|
|
7900
10120
|
connection() {
|
|
7901
10121
|
return this.ctx.get("connection");
|
|
7902
10122
|
}
|
|
@@ -8007,14 +10227,17 @@ var DshCommandApi = class {
|
|
|
8007
10227
|
} });
|
|
8008
10228
|
if (result?.ok !== true) throw fail(typeof result?.error?.code === "string" ? result.error.code : "command/invalid-response", typeof result?.error?.message === "string" ? result.error.message : "反馈未能保存");
|
|
8009
10229
|
}
|
|
8010
|
-
async export(sessionId, signal) {
|
|
8011
|
-
const
|
|
8012
|
-
|
|
10230
|
+
async export(sessionId, signal, format) {
|
|
10231
|
+
const markdown = format === "markdown";
|
|
10232
|
+
const path = markdown ? "/api/session.markdown?sessionId=" : "/api/session.export?includeDescendants=true&sessionId=";
|
|
10233
|
+
const response = await this.connection().createSharedFetchHandler("/api").fetch(new Request("http://localhost" + path + encodeURIComponent(sessionId), { signal }), this.owner());
|
|
10234
|
+
const contentType = response.headers.get("content-type");
|
|
10235
|
+
const matches = markdown ? contentType?.split(";")[0]?.trim().toLowerCase() === "text/markdown" : contentType?.includes("application/zip");
|
|
10236
|
+
if (!response.ok || !response.body || !matches) throw fail("command/export-unavailable", markdown ? "主机未能生成会话对话正文" : "主机未能生成会话日志归档");
|
|
8013
10237
|
return response;
|
|
8014
10238
|
}
|
|
8015
10239
|
};
|
|
8016
|
-
|
|
8017
|
-
//#region src/dsh-api-proxy.ts
|
|
10240
|
+
// src/dsh-api-proxy.ts
|
|
8018
10241
|
/**
|
|
8019
10242
|
* Adapter for the DSH rc.2 controller API.
|
|
8020
10243
|
*
|
|
@@ -8023,9 +10246,26 @@ var DshCommandApi = class {
|
|
|
8023
10246
|
* Session/Workspace controllers and Gateway-backed Remote Events, keeping
|
|
8024
10247
|
* the phone protocol isolated from the Host API.
|
|
8025
10248
|
*/
|
|
10249
|
+
/**
|
|
10250
|
+
* 形状探测:宿主级模型只有 `edit`(没有 `update`)。不按版本号字符串判断——
|
|
10251
|
+
* 版本号无法说明 Schedule bundle 是否挂载、也无法覆盖回退组合。
|
|
10252
|
+
*/
|
|
10253
|
+
function isHostScheduleController(controller) {
|
|
10254
|
+
const value = controller;
|
|
10255
|
+
return typeof value.edit === "function" && typeof value.update !== "function";
|
|
10256
|
+
}
|
|
8026
10257
|
/** Direct-controller adapter with the shape HostBridge consumes. */
|
|
8027
10258
|
var DshApiProxy = class {
|
|
8028
10259
|
ctx;
|
|
10260
|
+
/** Probe the Host's generated reflection, not this adapter's listener registration. */
|
|
10261
|
+
get assistantStreamAvailable() {
|
|
10262
|
+
const typert = this.ctx.get("typert");
|
|
10263
|
+
try {
|
|
10264
|
+
return typert?.getPackage?.("@deepseek-ai/dsh-api-session-controller", "host")?.model?.services?.some((service) => service.key === "sessionController" && service.types?.some((type) => type.name === "SessionAssistantStreamFrame")) === true;
|
|
10265
|
+
} catch {
|
|
10266
|
+
return false;
|
|
10267
|
+
}
|
|
10268
|
+
}
|
|
8029
10269
|
commandApi;
|
|
8030
10270
|
get commands() {
|
|
8031
10271
|
if (!this.commandApi.available) return void 0;
|
|
@@ -8034,7 +10274,8 @@ var DshApiProxy = class {
|
|
|
8034
10274
|
execute: (request) => this.call(() => this.commandApi.execute(request.payload.sessionId, request.payload.line, request.payload.attachments, request.payload.expectedDefinitionId)),
|
|
8035
10275
|
permissions: (request) => this.call(() => this.commandApi.permissions(request.payload.sessionId)),
|
|
8036
10276
|
feedback: (request) => this.call(() => this.commandApi.feedback(request.payload.sessionId, request.payload.text, request.payload.category)),
|
|
8037
|
-
|
|
10277
|
+
markdownAvailable: this.commandApi.markdownAvailable,
|
|
10278
|
+
export: (request, signal) => this.commandApi.export(request.payload.sessionId, signal, request.payload.format)
|
|
8038
10279
|
};
|
|
8039
10280
|
}
|
|
8040
10281
|
session;
|
|
@@ -8052,6 +10293,7 @@ var DshApiProxy = class {
|
|
|
8052
10293
|
this.session = session;
|
|
8053
10294
|
this.workspaceController = ctx.get("workspaceController");
|
|
8054
10295
|
if (this.workspaceController === void 0) delete this.workspace?.unarchiveSession;
|
|
10296
|
+
if (typeof this.session.updateQueue !== "function") delete this.sessions.updateQueue;
|
|
8055
10297
|
this.directoryPicker = ctx.get("directoryPickerController");
|
|
8056
10298
|
this.shouldSurfaceInteraction = options.shouldSurfaceInteraction ?? (() => true);
|
|
8057
10299
|
this.resolveSchedule();
|
|
@@ -8082,14 +10324,16 @@ var DshApiProxy = class {
|
|
|
8082
10324
|
const controller = this.ctx.get("schedule");
|
|
8083
10325
|
if (controller === void 0) return;
|
|
8084
10326
|
this.scheduleController = controller;
|
|
8085
|
-
this.scheduleApi = this.
|
|
10327
|
+
this.scheduleApi = isHostScheduleController(controller) ? this.createHostScheduleApi(controller) : this.createLegacyScheduleApi(controller);
|
|
8086
10328
|
}
|
|
8087
10329
|
get schedule() {
|
|
8088
10330
|
this.resolveSchedule();
|
|
8089
10331
|
return this.scheduleApi;
|
|
8090
10332
|
}
|
|
8091
|
-
|
|
10333
|
+
/** 会话级适配器:行为与宿主级扩展加入前逐字节一致,只多一个判别位。 */
|
|
10334
|
+
createLegacyScheduleApi(controller) {
|
|
8092
10335
|
return {
|
|
10336
|
+
mode: "legacy",
|
|
8093
10337
|
list: async (request) => this.call(async () => ({
|
|
8094
10338
|
sessionId: request.payload.sessionId,
|
|
8095
10339
|
tasks: (await controller.list({ sessionId: request.payload.sessionId })).map(toScheduleTask)
|
|
@@ -8138,6 +10382,83 @@ var DshApiProxy = class {
|
|
|
8138
10382
|
};
|
|
8139
10383
|
}
|
|
8140
10384
|
/**
|
|
10385
|
+
* 宿主级适配器:把 DSH `ScheduleService` 的分页列表/结果对象归一化成 Bridge
|
|
10386
|
+
* 消费的稳定视图。
|
|
10387
|
+
*
|
|
10388
|
+
* 失败分两类:`view/edit/run/history` 的「找不到」是**结果对象**里的 code
|
|
10389
|
+
* (非变更 miss,不抛错),这里翻译成 outcome 或带 code 的 Error;
|
|
10390
|
+
* `create/edit/run` 的输入非法与目标已归档是 RemoteError 抛出,由
|
|
10391
|
+
* `this.call` 收成 `{code}`,再由 Bridge 按错误词表映射。
|
|
10392
|
+
*/
|
|
10393
|
+
createHostScheduleApi(controller) {
|
|
10394
|
+
return {
|
|
10395
|
+
mode: "host",
|
|
10396
|
+
list: async (request) => this.call(async () => {
|
|
10397
|
+
const payload = request.payload ?? {};
|
|
10398
|
+
const value = await controller.list({
|
|
10399
|
+
...payload.active !== void 0 ? { active: payload.active } : {},
|
|
10400
|
+
...payload.limit !== void 0 ? { limit: payload.limit } : {},
|
|
10401
|
+
...payload.offset !== void 0 ? { offset: payload.offset } : {}
|
|
10402
|
+
});
|
|
10403
|
+
const items = Array.isArray(value.items) ? value.items : [];
|
|
10404
|
+
return {
|
|
10405
|
+
tasks: items.map(toHostScheduleTask),
|
|
10406
|
+
shown: finiteCount(value.shown, items.length),
|
|
10407
|
+
total: finiteCount(value.total, items.length),
|
|
10408
|
+
limit: finiteCount(value.limit, payload.limit ?? 100),
|
|
10409
|
+
offset: finiteCount(value.offset, payload.offset ?? 0)
|
|
10410
|
+
};
|
|
10411
|
+
}),
|
|
10412
|
+
view: async (request) => this.call(async () => {
|
|
10413
|
+
return toHostScheduleViewOutcome(await controller.view({ id: request.payload.id }), request.payload.id);
|
|
10414
|
+
}),
|
|
10415
|
+
create: async (request) => this.call(async () => {
|
|
10416
|
+
const payload = request.payload;
|
|
10417
|
+
return toHostScheduleTask(await controller.create({
|
|
10418
|
+
title: payload.title,
|
|
10419
|
+
prompt: payload.prompt,
|
|
10420
|
+
timing: payload.timing,
|
|
10421
|
+
...payload.target !== void 0 ? { target: normalizeTargetRequest(payload.target) } : {}
|
|
10422
|
+
}));
|
|
10423
|
+
}),
|
|
10424
|
+
edit: async (request) => this.call(async () => {
|
|
10425
|
+
const payload = request.payload;
|
|
10426
|
+
const expected = payload.expectedUpdatedAt !== void 0 ? { expectedUpdatedAt: payload.expectedUpdatedAt } : {};
|
|
10427
|
+
const edit = payload.action === "update" ? {
|
|
10428
|
+
id: payload.id,
|
|
10429
|
+
action: "update",
|
|
10430
|
+
patch: normalizeHostPatch(payload.patch),
|
|
10431
|
+
...expected
|
|
10432
|
+
} : {
|
|
10433
|
+
id: payload.id,
|
|
10434
|
+
action: "delete",
|
|
10435
|
+
...expected
|
|
10436
|
+
};
|
|
10437
|
+
return toHostScheduleEditOutcome(await controller.edit(edit), payload.id);
|
|
10438
|
+
}),
|
|
10439
|
+
run: async (request) => this.call(async () => {
|
|
10440
|
+
return toHostScheduleRunOutcome(await controller.run({ id: request.payload.id }), request.payload.id);
|
|
10441
|
+
}),
|
|
10442
|
+
history: async (request) => this.call(async () => {
|
|
10443
|
+
const value = await controller.history({
|
|
10444
|
+
id: request.payload.id,
|
|
10445
|
+
limit: request.payload.limit,
|
|
10446
|
+
...request.payload.before !== void 0 ? { before: request.payload.before } : {}
|
|
10447
|
+
});
|
|
10448
|
+
if (typeof value.code === "string") throw Object.assign(new Error(String(value.code)), { code: value.code });
|
|
10449
|
+
return toScheduleHistory(value);
|
|
10450
|
+
}),
|
|
10451
|
+
limits: async () => this.call(async () => {
|
|
10452
|
+
const value = controller.deliveryLimits();
|
|
10453
|
+
return { newSessionIntervalSeconds: Math.max(0, Math.trunc(Number(value?.newSessionIntervalSeconds) || 0)) };
|
|
10454
|
+
}),
|
|
10455
|
+
deleteEnded: async () => this.call(async () => {
|
|
10456
|
+
const value = await controller.deleteInactive();
|
|
10457
|
+
return { deleted: Math.max(0, Math.trunc(Number(value?.deleted) || 0)) };
|
|
10458
|
+
})
|
|
10459
|
+
};
|
|
10460
|
+
}
|
|
10461
|
+
/**
|
|
8141
10462
|
* Decide whether this Gateway Client can surface a phone card. The decision
|
|
8142
10463
|
* fails open to false so a broken device registry delegates this delivery
|
|
8143
10464
|
* while the official Web Client remains able to answer.
|
|
@@ -8155,16 +10476,33 @@ var DshApiProxy = class {
|
|
|
8155
10476
|
}),
|
|
8156
10477
|
history: async (request) => this.call(async () => {
|
|
8157
10478
|
const sessionId = request.payload.sessionId;
|
|
10479
|
+
const beforeSeq = request.payload?.beforeSeq;
|
|
10480
|
+
const limit = normalizePageLimit(request.payload?.maxMessages);
|
|
10481
|
+
const inspect = this.session.inspect;
|
|
10482
|
+
if (inspect === void 0) {
|
|
10483
|
+
const page = await this.session.page({
|
|
10484
|
+
address: {
|
|
10485
|
+
kind: "session",
|
|
10486
|
+
sessionId
|
|
10487
|
+
},
|
|
10488
|
+
...beforeSeq === void 0 ? {} : { throughSeq: beforeSeq - 1 },
|
|
10489
|
+
maxMessages: limit
|
|
10490
|
+
}, new AbortController().signal);
|
|
10491
|
+
return {
|
|
10492
|
+
events: page.records.map((record) => ({ event: record.event })),
|
|
10493
|
+
hasMore: page.hasMore
|
|
10494
|
+
};
|
|
10495
|
+
}
|
|
8158
10496
|
let inspected;
|
|
8159
10497
|
try {
|
|
8160
|
-
inspected = await this.session
|
|
10498
|
+
inspected = await inspect.call(this.session, sessionId);
|
|
8161
10499
|
} catch (error) {
|
|
8162
10500
|
console.warn(`[deeppilot] session history unavailable for ${JSON.stringify(sessionId)}: ${toError(error).code}: ${toError(error).message}`);
|
|
8163
10501
|
throw error;
|
|
8164
10502
|
}
|
|
8165
10503
|
const window = historyWindow(inspected.events, {
|
|
8166
|
-
beforeSeq
|
|
8167
|
-
limit
|
|
10504
|
+
beforeSeq,
|
|
10505
|
+
limit
|
|
8168
10506
|
});
|
|
8169
10507
|
return {
|
|
8170
10508
|
events: window.events,
|
|
@@ -8181,6 +10519,7 @@ var DshApiProxy = class {
|
|
|
8181
10519
|
selectModel: async (request) => this.call(() => this.session.selectModel(request.payload ?? {})),
|
|
8182
10520
|
rename: async (request) => this.call(() => this.session.rename(request.payload)),
|
|
8183
10521
|
cancel: async (request) => this.call(() => this.session.cancel(request.payload)),
|
|
10522
|
+
updateQueue: async (request) => this.call(() => this.session.updateQueue(request.payload)),
|
|
8184
10523
|
attachment: async (request) => this.call(() => this.session.attachment(request.payload)),
|
|
8185
10524
|
projections: async (request) => this.call(() => this.session.projections(request.payload, new AbortController().signal))
|
|
8186
10525
|
};
|
|
@@ -8248,6 +10587,18 @@ var DshApiProxy = class {
|
|
|
8248
10587
|
event
|
|
8249
10588
|
});
|
|
8250
10589
|
}), { global: true });
|
|
10590
|
+
const offAssistantStream = this.ctx.on("agent/assistant-stream", (({ agent, frame }) => {
|
|
10591
|
+
queue.push({
|
|
10592
|
+
type: "session/assistant-stream",
|
|
10593
|
+
sessionId: String(agent.session.id),
|
|
10594
|
+
isSubagent: agent.session.header?.origin === "subagent",
|
|
10595
|
+
value: frame
|
|
10596
|
+
});
|
|
10597
|
+
}), { global: true });
|
|
10598
|
+
const offAgentDisposed = this.ctx.on("agent/disposed", (({ agent }) => queue.push({
|
|
10599
|
+
type: "session/assistant-stream-disposed",
|
|
10600
|
+
sessionId: String(agent.session.id)
|
|
10601
|
+
})), { global: true });
|
|
8251
10602
|
const offProjection = this.ctx.get("sessionProjections")?.onChanged?.((session, key, value) => {
|
|
8252
10603
|
queue.push({
|
|
8253
10604
|
type: "session/projection",
|
|
@@ -8273,6 +10624,8 @@ var DshApiProxy = class {
|
|
|
8273
10624
|
yield* queue.iterate();
|
|
8274
10625
|
} finally {
|
|
8275
10626
|
offEvent();
|
|
10627
|
+
offAssistantStream();
|
|
10628
|
+
offAgentDisposed();
|
|
8276
10629
|
offProjection?.();
|
|
8277
10630
|
offSkills();
|
|
8278
10631
|
offCommands.forEach((dispose) => dispose());
|
|
@@ -8518,6 +10871,206 @@ function toScheduleHistory(value) {
|
|
|
8518
10871
|
...typeof row.nextBefore === "string" ? { nextBefore: row.nextBefore } : {}
|
|
8519
10872
|
};
|
|
8520
10873
|
}
|
|
10874
|
+
/**
|
|
10875
|
+
* 请求侧 target:`new_once`/`new` 的会话由 Host 预留或新建,客户端若把查看结果
|
|
10876
|
+
* 里的 `sessionId` 原样回传,不向上游透传(Host 的创建/编辑请求没有这个字段)。
|
|
10877
|
+
*/
|
|
10878
|
+
function normalizeTargetRequest(value) {
|
|
10879
|
+
if (value.kind === "session") return {
|
|
10880
|
+
kind: "session",
|
|
10881
|
+
sessionId: String(value.sessionId ?? "")
|
|
10882
|
+
};
|
|
10883
|
+
const create = toHostScheduleCreateSpec(value.create);
|
|
10884
|
+
return {
|
|
10885
|
+
kind: value.kind,
|
|
10886
|
+
...create !== void 0 ? { create } : {}
|
|
10887
|
+
};
|
|
10888
|
+
}
|
|
10889
|
+
/** 编辑补丁归一化:字段按契约逐个透传,target 走请求侧投影。 */
|
|
10890
|
+
function normalizeHostPatch(value) {
|
|
10891
|
+
return {
|
|
10892
|
+
...value.title !== void 0 ? { title: value.title } : {},
|
|
10893
|
+
...value.prompt !== void 0 ? { prompt: value.prompt } : {},
|
|
10894
|
+
...value.timing !== void 0 ? { timing: value.timing } : {},
|
|
10895
|
+
...value.target !== void 0 ? { target: normalizeTargetRequest(value.target) } : {},
|
|
10896
|
+
...value.active !== void 0 ? { active: value.active } : {}
|
|
10897
|
+
};
|
|
10898
|
+
}
|
|
10899
|
+
function isRecord(value) {
|
|
10900
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
10901
|
+
}
|
|
10902
|
+
function toRecord(value) {
|
|
10903
|
+
return isRecord(value) ? value : {};
|
|
10904
|
+
}
|
|
10905
|
+
/** 非负安全整数;形状不符时退回 fallback(分页字段只用于展示)。 */
|
|
10906
|
+
function finiteCount(value, fallback) {
|
|
10907
|
+
return typeof value === "number" && Number.isSafeInteger(value) && value >= 0 ? value : fallback;
|
|
10908
|
+
}
|
|
10909
|
+
function positiveSeconds(value) {
|
|
10910
|
+
return typeof value === "number" && Number.isFinite(value) && value > 0 ? value : 0;
|
|
10911
|
+
}
|
|
10912
|
+
function hostResultCode(value) {
|
|
10913
|
+
return typeof value.code === "string" ? value.code : void 0;
|
|
10914
|
+
}
|
|
10915
|
+
/** 结果对象里的「非变更 miss」不是抛错,这里翻译成 outcome 或带 code 的 Error。 */
|
|
10916
|
+
function throwHostResultCode(code) {
|
|
10917
|
+
throw Object.assign(new Error(code), { code });
|
|
10918
|
+
}
|
|
10919
|
+
function toHostScheduleCreateSpec(value) {
|
|
10920
|
+
const row = toRecord(value);
|
|
10921
|
+
const spec = {
|
|
10922
|
+
...typeof row.cwd === "string" ? { cwd: row.cwd } : {},
|
|
10923
|
+
...typeof row.agentPreset === "string" ? { agentPreset: row.agentPreset } : {}
|
|
10924
|
+
};
|
|
10925
|
+
return Object.keys(spec).length > 0 ? spec : void 0;
|
|
10926
|
+
}
|
|
10927
|
+
/**
|
|
10928
|
+
* 交付目标投影。`session` 与 `new_once` 带 sessionId(后者是创建时 Host 预留
|
|
10929
|
+
* 的会话);`new` 每次投递新建会话。`create` 省略时按 Host 默认。
|
|
10930
|
+
*/
|
|
10931
|
+
function toHostScheduleTarget(value) {
|
|
10932
|
+
const target = toRecord(value);
|
|
10933
|
+
const create = toHostScheduleCreateSpec(target.create);
|
|
10934
|
+
if (target.kind === "session") return {
|
|
10935
|
+
kind: "session",
|
|
10936
|
+
sessionId: String(target.sessionId ?? "")
|
|
10937
|
+
};
|
|
10938
|
+
if (target.kind === "new_once") return {
|
|
10939
|
+
kind: "new_once",
|
|
10940
|
+
sessionId: String(target.sessionId ?? ""),
|
|
10941
|
+
...create !== void 0 ? { create } : {}
|
|
10942
|
+
};
|
|
10943
|
+
return {
|
|
10944
|
+
kind: "new",
|
|
10945
|
+
...create !== void 0 ? { create } : {}
|
|
10946
|
+
};
|
|
10947
|
+
}
|
|
10948
|
+
function toHostScheduleReceipt(value) {
|
|
10949
|
+
const row = toRecord(value);
|
|
10950
|
+
return {
|
|
10951
|
+
scheduledAt: String(row.scheduledAt ?? ""),
|
|
10952
|
+
deliveredAt: String(row.deliveredAt ?? ""),
|
|
10953
|
+
messageId: String(row.messageId ?? ""),
|
|
10954
|
+
targetSessionId: String(row.targetSessionId ?? "")
|
|
10955
|
+
};
|
|
10956
|
+
}
|
|
10957
|
+
/**
|
|
10958
|
+
* timing 反向投影:DSH 存储记录用 camelCase(`afterSeconds`/`timeZone`),wire
|
|
10959
|
+
* 用 PROTOCOL.md 的 snake_case 嵌套结构。
|
|
10960
|
+
*
|
|
10961
|
+
* `at` 存储后只剩 `scheduledAt`(原始绝对时刻不是独立字段),因此用排定时刻
|
|
10962
|
+
* 回填——对 active 的一次性任务它就是目标时刻。
|
|
10963
|
+
*/
|
|
10964
|
+
function toHostScheduleTiming(row) {
|
|
10965
|
+
switch (row.kind) {
|
|
10966
|
+
case "after": return {
|
|
10967
|
+
kind: "after",
|
|
10968
|
+
after_seconds: positiveSeconds(row.afterSeconds)
|
|
10969
|
+
};
|
|
10970
|
+
case "every": return {
|
|
10971
|
+
kind: "every",
|
|
10972
|
+
every_seconds: positiveSeconds(row.everySeconds)
|
|
10973
|
+
};
|
|
10974
|
+
case "daily": return {
|
|
10975
|
+
kind: "daily",
|
|
10976
|
+
daily: localTimeInput(row)
|
|
10977
|
+
};
|
|
10978
|
+
case "weekly": return {
|
|
10979
|
+
kind: "weekly",
|
|
10980
|
+
weekly: {
|
|
10981
|
+
...localTimeInput(row),
|
|
10982
|
+
weekdays: Array.isArray(row.weekdays) ? row.weekdays.filter((day) => typeof day === "number") : []
|
|
10983
|
+
}
|
|
10984
|
+
};
|
|
10985
|
+
case "cron": return {
|
|
10986
|
+
kind: "cron",
|
|
10987
|
+
cron: {
|
|
10988
|
+
expression: String(row.expression ?? ""),
|
|
10989
|
+
time_zone: String(row.timeZone ?? "")
|
|
10990
|
+
}
|
|
10991
|
+
};
|
|
10992
|
+
case "rrule": return {
|
|
10993
|
+
kind: "rrule",
|
|
10994
|
+
rrule: String(row.rrule ?? "")
|
|
10995
|
+
};
|
|
10996
|
+
default: return {
|
|
10997
|
+
kind: "at",
|
|
10998
|
+
at: String(row.scheduledAt ?? "")
|
|
10999
|
+
};
|
|
11000
|
+
}
|
|
11001
|
+
}
|
|
11002
|
+
function localTimeInput(row) {
|
|
11003
|
+
return {
|
|
11004
|
+
time: String(row.time ?? ""),
|
|
11005
|
+
time_zone: String(row.timeZone ?? "")
|
|
11006
|
+
};
|
|
11007
|
+
}
|
|
11008
|
+
/** Host `ScheduleCatalogEntry` -> wire `schedule.host.task`(丢弃 involvedSessionIds)。 */
|
|
11009
|
+
function toHostScheduleTask(value) {
|
|
11010
|
+
const row = toRecord(value);
|
|
11011
|
+
return {
|
|
11012
|
+
id: String(row.id ?? ""),
|
|
11013
|
+
title: String(row.title ?? ""),
|
|
11014
|
+
prompt: String(row.prompt ?? ""),
|
|
11015
|
+
timing: toHostScheduleTiming(row),
|
|
11016
|
+
target: toHostScheduleTarget(row.target),
|
|
11017
|
+
status: row.status === "paused" || row.status === "ended" ? row.status : "active",
|
|
11018
|
+
scheduledAt: String(row.scheduledAt ?? ""),
|
|
11019
|
+
...typeof row.createdAt === "string" ? { createdAt: row.createdAt } : {},
|
|
11020
|
+
...typeof row.updatedAt === "string" ? { updatedAt: row.updatedAt } : {},
|
|
11021
|
+
...typeof row.executionActive === "boolean" ? { executionActive: row.executionActive } : {},
|
|
11022
|
+
...typeof row.deliveryFloorEndsAt === "string" ? { deliveryFloorEndsAt: row.deliveryFloorEndsAt } : {},
|
|
11023
|
+
...isRecord(row.lastDelivery) ? { lastDelivery: toHostScheduleReceipt(row.lastDelivery) } : {}
|
|
11024
|
+
};
|
|
11025
|
+
}
|
|
11026
|
+
function toHostScheduleViewOutcome(value, id) {
|
|
11027
|
+
const code = hostResultCode(value);
|
|
11028
|
+
if (code === "schedule_not_found") return {
|
|
11029
|
+
kind: "not-found",
|
|
11030
|
+
id
|
|
11031
|
+
};
|
|
11032
|
+
if (code !== void 0) throwHostResultCode(code);
|
|
11033
|
+
return {
|
|
11034
|
+
kind: "task",
|
|
11035
|
+
task: toHostScheduleTask(value)
|
|
11036
|
+
};
|
|
11037
|
+
}
|
|
11038
|
+
function toHostScheduleEditOutcome(value, id) {
|
|
11039
|
+
const code = hostResultCode(value);
|
|
11040
|
+
if (code === "schedule_conflict") return {
|
|
11041
|
+
kind: "conflict",
|
|
11042
|
+
id
|
|
11043
|
+
};
|
|
11044
|
+
if (code === "schedule_not_found") return {
|
|
11045
|
+
kind: "not-found",
|
|
11046
|
+
id
|
|
11047
|
+
};
|
|
11048
|
+
if (code !== void 0) throwHostResultCode(code);
|
|
11049
|
+
if (value.deleted === true) return {
|
|
11050
|
+
kind: "deleted",
|
|
11051
|
+
id: String(value.id ?? id)
|
|
11052
|
+
};
|
|
11053
|
+
return {
|
|
11054
|
+
kind: "task",
|
|
11055
|
+
task: toHostScheduleTask(value)
|
|
11056
|
+
};
|
|
11057
|
+
}
|
|
11058
|
+
function toHostScheduleRunOutcome(value, id) {
|
|
11059
|
+
const code = hostResultCode(value);
|
|
11060
|
+
if (code === "schedule_execution_pending") return {
|
|
11061
|
+
kind: "pending",
|
|
11062
|
+
id
|
|
11063
|
+
};
|
|
11064
|
+
if (code === "schedule_not_found") return {
|
|
11065
|
+
kind: "not-found",
|
|
11066
|
+
id
|
|
11067
|
+
};
|
|
11068
|
+
if (code !== void 0) throwHostResultCode(code);
|
|
11069
|
+
return {
|
|
11070
|
+
kind: "task",
|
|
11071
|
+
task: toHostScheduleTask(value)
|
|
11072
|
+
};
|
|
11073
|
+
}
|
|
8521
11074
|
async function readWorkspaceBaseline(controller) {
|
|
8522
11075
|
const abort = new AbortController();
|
|
8523
11076
|
const iterator = controller.follow(abort.signal)[Symbol.asyncIterator]();
|
|
@@ -8547,8 +11100,7 @@ async function projectModels(catalog, sessionId, list) {
|
|
|
8547
11100
|
failures: Array.isArray(value.failures) ? value.failures : []
|
|
8548
11101
|
};
|
|
8549
11102
|
}
|
|
8550
|
-
|
|
8551
|
-
//#region src/report-service.ts
|
|
11103
|
+
// src/report-service.ts
|
|
8552
11104
|
/**
|
|
8553
11105
|
* The Typert receiver the Gateway resolves for the DeepPilot Bridge report.
|
|
8554
11106
|
* Snapshot data stays non-secret; the token crosses the boundary only through
|
|
@@ -8594,8 +11146,7 @@ var DeepPilotReportService = class extends TypertRemoteService {
|
|
|
8594
11146
|
return this.pushTester();
|
|
8595
11147
|
}
|
|
8596
11148
|
};
|
|
8597
|
-
|
|
8598
|
-
//#region src/report-wire.ts
|
|
11149
|
+
// src/report-wire.ts
|
|
8599
11150
|
/** The npm package identity both contribution registrations claim. */
|
|
8600
11151
|
const REPORT_REMOTE_PACKAGE = "dsh-deeppilot";
|
|
8601
11152
|
/** Canonical `<namespace>/<method>` endpoint of the report Remote. */
|
|
@@ -8677,15 +11228,18 @@ function parseRemote(value) {
|
|
|
8677
11228
|
const publicURL = s.publicURL;
|
|
8678
11229
|
const authURL = s.authURL;
|
|
8679
11230
|
const message = s.message;
|
|
11231
|
+
const tailscaleVersion = s.tailscaleVersion;
|
|
8680
11232
|
if (publicURL !== void 0 && typeof publicURL !== "string") reject("remote.publicURL");
|
|
8681
11233
|
if (authURL !== void 0 && typeof authURL !== "string") reject("remote.authURL");
|
|
8682
11234
|
if (message !== void 0 && typeof message !== "string") reject("remote.message");
|
|
11235
|
+
if (tailscaleVersion !== void 0 && !isTailscaleVersion(tailscaleVersion)) reject("remote.tailscaleVersion");
|
|
8683
11236
|
return {
|
|
8684
11237
|
provider,
|
|
8685
11238
|
phase,
|
|
8686
11239
|
...typeof publicURL === "string" ? { publicURL } : {},
|
|
8687
11240
|
...typeof authURL === "string" ? { authURL } : {},
|
|
8688
11241
|
...typeof message === "string" ? { message } : {},
|
|
11242
|
+
...typeof tailscaleVersion === "string" ? { tailscaleVersion } : {},
|
|
8689
11243
|
updatedAt: int(s, "updatedAt", "remote.updatedAt")
|
|
8690
11244
|
};
|
|
8691
11245
|
}
|
|
@@ -8977,8 +11531,7 @@ const REPORT_HOST_CONTRIBUTION = {
|
|
|
8977
11531
|
}
|
|
8978
11532
|
]
|
|
8979
11533
|
};
|
|
8980
|
-
|
|
8981
|
-
//#region src/report-remote.ts
|
|
11534
|
+
// src/report-remote.ts
|
|
8982
11535
|
/**
|
|
8983
11536
|
* Provide the report service and register its Remote descriptor. Rides an
|
|
8984
11537
|
* optional `typert` inject: profiles without the web stack never activate it.
|
|
@@ -8990,8 +11543,7 @@ function applyReportRemote(ctx, snapshot, pairingStarter, deviceRevoker, deviceR
|
|
|
8990
11543
|
remoteCtx.effect(() => () => void unregister(), "dsh-deeppilot: report remote");
|
|
8991
11544
|
});
|
|
8992
11545
|
}
|
|
8993
|
-
|
|
8994
|
-
//#region src/local-address.ts
|
|
11546
|
+
// src/local-address.ts
|
|
8995
11547
|
function octetsOf(address) {
|
|
8996
11548
|
const octets = address.split(".").map(Number);
|
|
8997
11549
|
if (octets.length !== 4 || octets.some((value) => !Number.isInteger(value) || value < 0 || value > 255)) return null;
|
|
@@ -9042,8 +11594,7 @@ function localLANIPv4Addresses() {
|
|
|
9042
11594
|
candidates.sort((left, right) => priority(left.name) - priority(right.name) || left.name.localeCompare(right.name));
|
|
9043
11595
|
return [...new Set(candidates.map(({ address }) => address))];
|
|
9044
11596
|
}
|
|
9045
|
-
|
|
9046
|
-
//#region src/update-check.ts
|
|
11597
|
+
// src/update-check.ts
|
|
9047
11598
|
/**
|
|
9048
11599
|
* Lightweight self-update check for dsh-deeppilot.
|
|
9049
11600
|
*
|
|
@@ -9239,8 +11790,7 @@ var UpdateChecker = class {
|
|
|
9239
11790
|
/** No-op kept for API symmetry with the host lifecycle wiring. */
|
|
9240
11791
|
dispose() {}
|
|
9241
11792
|
};
|
|
9242
|
-
|
|
9243
|
-
//#region src/phone-http.ts
|
|
11793
|
+
// src/phone-http.ts
|
|
9244
11794
|
const CLIENT_IP_HEADER = "x-deeppilot-client-ip";
|
|
9245
11795
|
function rejectUpgrade(socket, status, reason, retryAfterSeconds) {
|
|
9246
11796
|
const body = JSON.stringify({ error: reason });
|
|
@@ -9274,8 +11824,7 @@ function requestClientIdentity(req) {
|
|
|
9274
11824
|
}
|
|
9275
11825
|
return normalizedAddress(req.socket.remoteAddress) ?? "unknown";
|
|
9276
11826
|
}
|
|
9277
|
-
|
|
9278
|
-
//#region src/lan-tls.ts
|
|
11827
|
+
// src/lan-tls.ts
|
|
9279
11828
|
const LAN_TLS_COMMON_NAME = "dsh-deeppilot";
|
|
9280
11829
|
const LAN_TLS_KEY_FILE = "key.pem";
|
|
9281
11830
|
const LAN_TLS_CERT_FILE = "cert.pem";
|
|
@@ -9448,8 +11997,7 @@ async function writePrivate(path, contents) {
|
|
|
9448
11997
|
throw error;
|
|
9449
11998
|
}
|
|
9450
11999
|
}
|
|
9451
|
-
|
|
9452
|
-
//#region src/index.ts
|
|
12000
|
+
// src/index.ts
|
|
9453
12001
|
/**
|
|
9454
12002
|
* dsh-deeppilot — data bridge between the DSH host and DeepPilot
|
|
9455
12003
|
* clients. Owns independent, narrowly routed LAN (TLS-only, pinned by paired
|
|
@@ -9726,6 +12274,23 @@ function apply(ctx, options) {
|
|
|
9726
12274
|
return updated;
|
|
9727
12275
|
}, () => pushGateway.relayTest(), () => pushGateway.selfTest());
|
|
9728
12276
|
const state = {};
|
|
12277
|
+
let pluginUpdater;
|
|
12278
|
+
ctx.inject(["pluginManager"], (sub) => {
|
|
12279
|
+
const manager = sub.pluginManager;
|
|
12280
|
+
if (!supportsPluginUpdate(manager)) return;
|
|
12281
|
+
const updater = new PluginUpdater({
|
|
12282
|
+
manager,
|
|
12283
|
+
currentVersion: SERVER_VERSION,
|
|
12284
|
+
journalPath: join(dataDir, "update-journal.json")
|
|
12285
|
+
});
|
|
12286
|
+
pluginUpdater = updater;
|
|
12287
|
+
if (state.bridge) setPluginUpdater(state.bridge, updater);
|
|
12288
|
+
sub.effect(() => () => {
|
|
12289
|
+
if (pluginUpdater !== updater) return;
|
|
12290
|
+
pluginUpdater = void 0;
|
|
12291
|
+
if (state.bridge) setPluginUpdater(state.bridge, void 0);
|
|
12292
|
+
}, "deeppilot: optional plugin updater");
|
|
12293
|
+
});
|
|
9729
12294
|
let pendingUpgrades = 0;
|
|
9730
12295
|
const authRateLimiter = new AuthRateLimiter();
|
|
9731
12296
|
const readJSONBody = async (req, maxBytes = 16384) => {
|
|
@@ -10057,8 +12622,10 @@ function apply(ctx, options) {
|
|
|
10057
12622
|
log("DSH session bridge unavailable: " + String(error), "warn");
|
|
10058
12623
|
return;
|
|
10059
12624
|
}
|
|
12625
|
+
setPluginUpdateProbe(proxy, () => pluginUpdater !== void 0);
|
|
10060
12626
|
const bridge = new HostBridge(proxy, cfg.historyBufferMax, join(dataDir, "prompt-deliveries-v1.json"), join(dataDir, "schedule-mutations-v1.json"), join(dataDir, "fork-mutations-v1.json"), log);
|
|
10061
12627
|
bridge.setPushOutlet(pushGateway);
|
|
12628
|
+
setPluginUpdater(bridge, pluginUpdater);
|
|
10062
12629
|
state.bridge = bridge;
|
|
10063
12630
|
bridge.start();
|
|
10064
12631
|
log("data plane active (mux + host streams)");
|
|
@@ -10080,7 +12647,6 @@ function apply(ctx, options) {
|
|
|
10080
12647
|
await diagnostics.dispose();
|
|
10081
12648
|
}, "deeppilot: process resources");
|
|
10082
12649
|
}
|
|
10083
|
-
//#endregion
|
|
10084
12650
|
export { Config, HostBridge, apply, inject, mayReceivePush, name, shouldPrunePushToken, shouldReEnrollRelayToken };
|
|
10085
12651
|
|
|
10086
12652
|
//# sourceMappingURL=index.js.map
|