dsh-deeppilot 0.9.13 → 0.9.15
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 +113 -19
- package/README.md +34 -23
- package/README.zh-CN.md +26 -19
- 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 +38 -3
- package/docs/DSH_RELEASE_MEMORY.md +191 -2
- package/docs/NPM_PUBLISHING.md +48 -1
- package/lib/client.js +582 -468
- package/lib/client.js.map +1 -1
- package/lib/index.d.ts +350 -33
- package/lib/index.js +1858 -128
- package/lib/index.js.map +1 -1
- package/locale/en.json +2 -1
- package/locale/zh.json +2 -1
- package/package.json +31 -23
package/lib/index.js
CHANGED
|
@@ -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
|
*
|
|
@@ -42,7 +42,8 @@ const ERROR_CODES = {
|
|
|
42
42
|
E_BUSY: "session is busy",
|
|
43
43
|
E_UNSUPPORTED: "protocol version or capability unsupported",
|
|
44
44
|
E_INTERNAL: "internal error",
|
|
45
|
-
E_STEER_UNAVAILABLE: "queued item can no longer steer the current turn"
|
|
45
|
+
E_STEER_UNAVAILABLE: "queued item can no longer steer the current turn",
|
|
46
|
+
E_TOO_LARGE: "response exceeds the client frame limit"
|
|
46
47
|
};
|
|
47
48
|
/** Bridge 未携带 code 的结果(内部守卫失败等)落到这一档。 */
|
|
48
49
|
const WIRE_ERROR_FALLBACK = "E_INTERNAL";
|
|
@@ -90,6 +91,9 @@ const DOMAIN_TABLES = {
|
|
|
90
91
|
"schedule_not_found": "E_NOT_FOUND",
|
|
91
92
|
"delivery_cursor_not_found": "E_NOT_FOUND",
|
|
92
93
|
"schedule_conflict": "E_BUSY",
|
|
94
|
+
"schedule_execution_pending": "E_BUSY",
|
|
95
|
+
"schedule/target-archived": "E_NOT_FOUND",
|
|
96
|
+
"schedule/input-invalid": "E_PROTOCOL",
|
|
93
97
|
"invalid_prompt": "E_PROTOCOL",
|
|
94
98
|
"invalid_selector": "E_PROTOCOL",
|
|
95
99
|
"invalid_rule": "E_PROTOCOL",
|
|
@@ -209,9 +213,10 @@ function pendingResponseMessage(kind, reason) {
|
|
|
209
213
|
case "transport": return "host connection failed while answering " + kind;
|
|
210
214
|
}
|
|
211
215
|
}
|
|
212
|
-
|
|
213
|
-
//#region src/connection-policy.ts
|
|
216
|
+
// src/connection-policy.ts
|
|
214
217
|
const AUTH_TIMEOUT_MS = 35e3;
|
|
218
|
+
/** 附件 ack 在 `{mediaType,data}` 之外的信封字段(v/type/id/ts)余量。 */
|
|
219
|
+
const ATTACHMENT_ENVELOPE_HEADROOM_BYTES = 1024;
|
|
215
220
|
const IMAGE_MEDIA_TYPES = /* @__PURE__ */ new Set([
|
|
216
221
|
"image/png",
|
|
217
222
|
"image/jpeg",
|
|
@@ -287,8 +292,20 @@ function sanitizeImageName(value) {
|
|
|
287
292
|
function sanitizeDocumentField(value, maxChars) {
|
|
288
293
|
return value.replace(/[\u0000-\u001F\u007F]/g, " ").trim().slice(0, maxChars);
|
|
289
294
|
}
|
|
290
|
-
|
|
291
|
-
|
|
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
|
|
292
309
|
const DEFAULT_AUTH_RATE_POLICY = {
|
|
293
310
|
windowMs: 6e4,
|
|
294
311
|
attemptsPerSource: 12,
|
|
@@ -405,8 +422,7 @@ var AuthRateLimiter = class {
|
|
|
405
422
|
}
|
|
406
423
|
}
|
|
407
424
|
};
|
|
408
|
-
|
|
409
|
-
//#region src/diagnostics.ts
|
|
425
|
+
// src/diagnostics.ts
|
|
410
426
|
/**
|
|
411
427
|
* 连接诊断事件模型(Diagnostics)——插件唯一的「失败可观测」面。
|
|
412
428
|
*
|
|
@@ -469,7 +485,7 @@ function defaultLevel(kind) {
|
|
|
469
485
|
function sanitizeText(value, maxChars) {
|
|
470
486
|
return value.replace(/[\u0000-\u001f\u007f-\u009f]+/gu, " ").replace(/\s+/g, " ").trim().slice(0, maxChars);
|
|
471
487
|
}
|
|
472
|
-
function normalizeReason(reason) {
|
|
488
|
+
function normalizeReason$1(reason) {
|
|
473
489
|
return sanitizeText(reason, MAX_REASON_CHARS).replace(/\s+/g, "-");
|
|
474
490
|
}
|
|
475
491
|
/**
|
|
@@ -550,7 +566,7 @@ var DiagnosticsStore = class {
|
|
|
550
566
|
const event = {
|
|
551
567
|
ts,
|
|
552
568
|
kind,
|
|
553
|
-
reason: normalizeReason(reason),
|
|
569
|
+
reason: normalizeReason$1(reason),
|
|
554
570
|
level: extra.level ?? defaultLevel(kind),
|
|
555
571
|
...attemptId !== void 0 ? { attemptId } : {},
|
|
556
572
|
...source !== void 0 ? { source } : {},
|
|
@@ -732,8 +748,7 @@ function buildHealthPayload(input) {
|
|
|
732
748
|
}
|
|
733
749
|
};
|
|
734
750
|
}
|
|
735
|
-
|
|
736
|
-
//#region src/device-auth.ts
|
|
751
|
+
// src/device-auth.ts
|
|
737
752
|
function getPairingCodeTtlMs() {
|
|
738
753
|
return Number(process.env.DEEPPILOT_PAIRING_TTL_MS) || 3e5;
|
|
739
754
|
}
|
|
@@ -822,6 +837,30 @@ function normalizeDeviceScopes(value) {
|
|
|
822
837
|
const allowed = new Set(DEVICE_SCOPES);
|
|
823
838
|
return [...new Set(value.filter((scope) => typeof scope === "string" && allowed.has(scope)))];
|
|
824
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
|
+
}
|
|
825
864
|
/** One active, single-use pairing grant per plugin runtime. */
|
|
826
865
|
var PairingCodeManager = class {
|
|
827
866
|
active = null;
|
|
@@ -857,8 +896,7 @@ function createAuthChallenge(audience, now = Date.now()) {
|
|
|
857
896
|
expiresAt: now + AUTH_CHALLENGE_TTL_MS
|
|
858
897
|
};
|
|
859
898
|
}
|
|
860
|
-
|
|
861
|
-
//#region src/wire-interaction.ts
|
|
899
|
+
// src/wire-interaction.ts
|
|
862
900
|
/**
|
|
863
901
|
* 交互特性的 wire 行:回答待决的 approval 与 question。
|
|
864
902
|
*
|
|
@@ -918,8 +956,7 @@ const interactionRows = [{
|
|
|
918
956
|
ctx.send("s2c.ack", {});
|
|
919
957
|
}
|
|
920
958
|
}];
|
|
921
|
-
|
|
922
|
-
//#region src/wire-device.ts
|
|
959
|
+
// src/wire-device.ts
|
|
923
960
|
/**
|
|
924
961
|
* 设备生命周期特性的 wire 行:自撤销(解绑)。
|
|
925
962
|
*
|
|
@@ -957,8 +994,7 @@ const deviceRows = [{
|
|
|
957
994
|
ctx.close(4401, "device revoked");
|
|
958
995
|
}
|
|
959
996
|
}];
|
|
960
|
-
|
|
961
|
-
//#region src/token.ts
|
|
997
|
+
// src/token.ts
|
|
962
998
|
/** Expand a leading ~ using the process home directory. */
|
|
963
999
|
function expandHome(p) {
|
|
964
1000
|
if (p === "~") return homedir();
|
|
@@ -1035,7 +1071,20 @@ var DeviceStore = class DeviceStore {
|
|
|
1035
1071
|
const store = new DeviceStore(filePath);
|
|
1036
1072
|
try {
|
|
1037
1073
|
const raw = JSON.parse(await readFile(expandHome(filePath), "utf8"));
|
|
1038
|
-
|
|
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
|
+
}
|
|
1039
1088
|
} catch (error) {
|
|
1040
1089
|
if (error?.code === "ENOENT") {} else console.log(formatLogLine("warn", "device registry unreadable, starting empty: " + String(error)));
|
|
1041
1090
|
}
|
|
@@ -1260,8 +1309,7 @@ var DeviceStore = class DeviceStore {
|
|
|
1260
1309
|
console.log(formatLogLine("warn", "device registry write failed" + (typeof code === "string" ? " code=" + code : "") + ": " + message.slice(0, 160)));
|
|
1261
1310
|
}
|
|
1262
1311
|
};
|
|
1263
|
-
|
|
1264
|
-
//#region src/wire-push.ts
|
|
1312
|
+
// src/wire-push.ts
|
|
1265
1313
|
/** Live Activity 需要的三个 scope:通知注册 + 会话读取 + 交互应答。 */
|
|
1266
1314
|
const LIVE_ACTIVITY_SCOPES = [
|
|
1267
1315
|
"notifications.register",
|
|
@@ -1408,21 +1456,83 @@ const pushRows = [
|
|
|
1408
1456
|
}
|
|
1409
1457
|
}
|
|
1410
1458
|
];
|
|
1411
|
-
|
|
1412
|
-
//#region src/wire-schedule.ts
|
|
1459
|
+
// src/wire-schedule.ts
|
|
1413
1460
|
/**
|
|
1414
|
-
* 定时任务特性的 wire
|
|
1461
|
+
* 定时任务特性的 wire 行:会话级(列表、历史、创建、修改、删除)与宿主级
|
|
1462
|
+
* (list/view/create/edit/run/limits)两套。
|
|
1415
1463
|
*
|
|
1416
1464
|
* scope 取 `schedule.manage` + `sessions.read`:PROTOCOL.md scope 映射表写明
|
|
1417
1465
|
* 「`schedule.manage` 定时任务/提醒的列表、历史、创建、修改和删除;同时需要
|
|
1418
1466
|
* `sessions.read` 才能读取任务内容」。迁移前这条组合一半住在 requiredScope、
|
|
1419
1467
|
* 一半住在 connection.ts 调用点的一行补充规则里(334-336),现在是一行声明。
|
|
1420
1468
|
*
|
|
1421
|
-
*
|
|
1422
|
-
* (见 host-capabilities.ts 的 G4
|
|
1469
|
+
* 五个会话级帧共享能力位 `schedules`;该位现在只要求 Schedule 服务存在
|
|
1470
|
+
* (见 host-capabilities.ts 的 G4 修正)。六个宿主级帧共享 `schedulesHost`:
|
|
1471
|
+
* 该位由 adapter 判别位推导,false/缺失时新帧一律 E_UNSUPPORTED,客户端只能
|
|
1472
|
+
* 走会话级帧(PROTOCOL.md「Schedule 宿主级任务扩展」)。
|
|
1423
1473
|
*/
|
|
1424
1474
|
/** PROTOCOL.md scope 映射表:schedule.manage 需搭配 sessions.read。 */
|
|
1425
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
|
+
}
|
|
1426
1536
|
const scheduleRows = [
|
|
1427
1537
|
{
|
|
1428
1538
|
type: "c2s.schedule.list",
|
|
@@ -1454,14 +1564,16 @@ const scheduleRows = [
|
|
|
1454
1564
|
validate: (payload) => {
|
|
1455
1565
|
const checked = payloadObject(payload);
|
|
1456
1566
|
if (!checked.ok) return checked;
|
|
1457
|
-
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");
|
|
1458
1568
|
return checked;
|
|
1459
1569
|
},
|
|
1460
1570
|
handle: async (ctx, payload) => {
|
|
1461
|
-
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);
|
|
1462
1574
|
if (!result.ok) return ctx.fail(result.code, result.message);
|
|
1463
1575
|
ctx.send("s2c.schedule.history", {
|
|
1464
|
-
sessionId:
|
|
1576
|
+
...sessionId !== void 0 ? { sessionId } : {},
|
|
1465
1577
|
history: result.value
|
|
1466
1578
|
});
|
|
1467
1579
|
}
|
|
@@ -1553,10 +1665,159 @@ const scheduleRows = [
|
|
|
1553
1665
|
...result.replayed ? { replayed: true } : {}
|
|
1554
1666
|
});
|
|
1555
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
|
+
}
|
|
1556
1818
|
}
|
|
1557
1819
|
];
|
|
1558
|
-
|
|
1559
|
-
//#region src/document-payload.ts
|
|
1820
|
+
// src/document-payload.ts
|
|
1560
1821
|
const PREFIX = "[DeepPilot document:";
|
|
1561
1822
|
const SUFFIX = "]";
|
|
1562
1823
|
/**
|
|
@@ -1593,8 +1854,7 @@ function projectedDocument(text) {
|
|
|
1593
1854
|
return;
|
|
1594
1855
|
}
|
|
1595
1856
|
}
|
|
1596
|
-
|
|
1597
|
-
//#region src/host-event-projection.ts
|
|
1857
|
+
// src/host-event-projection.ts
|
|
1598
1858
|
const MAX_MESSAGE_PROJECTION_BYTES = 262144;
|
|
1599
1859
|
/** One durable host event sequence becomes exactly one phone message row.
|
|
1600
1860
|
* Keep the last projection when a host history response repeats an event. */
|
|
@@ -1682,7 +1942,8 @@ function projectEvent(sessionId, event) {
|
|
|
1682
1942
|
name: String(data?.name ?? "tool"),
|
|
1683
1943
|
state: "running",
|
|
1684
1944
|
summary: summarizeArgs(data?.arguments),
|
|
1685
|
-
...data?.callId ? { callId: String(data.callId) } : {}
|
|
1945
|
+
...data?.callId ? { callId: String(data.callId) } : {},
|
|
1946
|
+
...editField(data?.name, data?.arguments)
|
|
1686
1947
|
},
|
|
1687
1948
|
ts: tsOf(event)
|
|
1688
1949
|
}
|
|
@@ -1730,6 +1991,12 @@ function userMessageSource(data) {
|
|
|
1730
1991
|
if (obj.source && typeof obj.source === "object") return obj.source;
|
|
1731
1992
|
if (obj.message && typeof obj.message === "object" && obj.message.source && typeof obj.message.source === "object") return obj.message.source;
|
|
1732
1993
|
}
|
|
1994
|
+
/** Source kind of one user/message payload (`user`, `schedule`, injected
|
|
1995
|
+
* context kinds, …); undefined on legacy hosts that record no source. */
|
|
1996
|
+
function userMessageSourceKind(data) {
|
|
1997
|
+
const kind = userMessageSource(data)?.kind;
|
|
1998
|
+
return typeof kind === "string" ? kind : void 0;
|
|
1999
|
+
}
|
|
1733
2000
|
/** Wire role for one user/message payload. A payload without any readable
|
|
1734
2001
|
* source degrades to 'user' so history written by older hosts stays visible;
|
|
1735
2002
|
* a present source follows the host's own trajectory rule — anything whose
|
|
@@ -1882,6 +2149,10 @@ function chunkText(data) {
|
|
|
1882
2149
|
const direct = data;
|
|
1883
2150
|
return typeof direct.text === "string" ? direct.text : "";
|
|
1884
2151
|
}
|
|
2152
|
+
function editField(name, raw) {
|
|
2153
|
+
const edit = toolEditOf(name, raw);
|
|
2154
|
+
return edit ? { edit } : {};
|
|
2155
|
+
}
|
|
1885
2156
|
function summarizeArgs(raw) {
|
|
1886
2157
|
if (typeof raw !== "string" || raw.length === 0) return "";
|
|
1887
2158
|
try {
|
|
@@ -1893,6 +2164,68 @@ function summarizeArgs(raw) {
|
|
|
1893
2164
|
return truncate(raw, 90);
|
|
1894
2165
|
}
|
|
1895
2166
|
}
|
|
2167
|
+
/** 宿主的文件编辑工具名(DSH `dsh-tool-str-replace-editor`)。 */
|
|
2168
|
+
const EDITOR_TOOL_NAME = "str_replace_editor";
|
|
2169
|
+
/**
|
|
2170
|
+
* 把编辑类工具调用的原始参数归一化成 `ToolEditProjection`。
|
|
2171
|
+
*
|
|
2172
|
+
* 只认 `str_replace_editor` 的 `str_replace` / `create` / `insert`(`view` 不改文件),
|
|
2173
|
+
* 其他工具或无法解析的参数返回 undefined,工具行保持原样。与 iOS `ToolEditDiff.parse`
|
|
2174
|
+
* 的解析口径一致:`new_str` 缺失视为删除(空串)。
|
|
2175
|
+
*/
|
|
2176
|
+
function toolEditOf(name, raw) {
|
|
2177
|
+
if (name !== EDITOR_TOOL_NAME || typeof raw !== "string" || raw.length === 0) return void 0;
|
|
2178
|
+
let args;
|
|
2179
|
+
try {
|
|
2180
|
+
const parsed = JSON.parse(raw);
|
|
2181
|
+
if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) return void 0;
|
|
2182
|
+
args = parsed;
|
|
2183
|
+
} catch {
|
|
2184
|
+
return;
|
|
2185
|
+
}
|
|
2186
|
+
const path = args.path;
|
|
2187
|
+
if (typeof path !== "string" || path.length === 0) return void 0;
|
|
2188
|
+
let edit;
|
|
2189
|
+
switch (args.command) {
|
|
2190
|
+
case "str_replace":
|
|
2191
|
+
if (typeof args.old_str !== "string") return void 0;
|
|
2192
|
+
edit = {
|
|
2193
|
+
path,
|
|
2194
|
+
operation: "replace",
|
|
2195
|
+
oldText: args.old_str,
|
|
2196
|
+
newText: typeof args.new_str === "string" ? args.new_str : ""
|
|
2197
|
+
};
|
|
2198
|
+
break;
|
|
2199
|
+
case "create":
|
|
2200
|
+
if (typeof args.file_text !== "string") return void 0;
|
|
2201
|
+
edit = {
|
|
2202
|
+
path,
|
|
2203
|
+
operation: "create",
|
|
2204
|
+
newText: args.file_text
|
|
2205
|
+
};
|
|
2206
|
+
break;
|
|
2207
|
+
case "insert":
|
|
2208
|
+
if (typeof args.new_str !== "string") return void 0;
|
|
2209
|
+
edit = {
|
|
2210
|
+
path,
|
|
2211
|
+
operation: "insert",
|
|
2212
|
+
newText: args.new_str,
|
|
2213
|
+
...Number.isSafeInteger(args.insert_line) ? { insertLine: args.insert_line } : {}
|
|
2214
|
+
};
|
|
2215
|
+
break;
|
|
2216
|
+
default: return;
|
|
2217
|
+
}
|
|
2218
|
+
return [edit.oldText, edit.newText].some((text) => text !== void 0 && Buffer.byteLength(text, "utf8") > 16384) ? withoutEditBodies(edit) : edit;
|
|
2219
|
+
}
|
|
2220
|
+
/** 去掉改动正文,只留路径、操作与 truncated 标记(超限或整条消息超限时用)。 */
|
|
2221
|
+
function withoutEditBodies(edit) {
|
|
2222
|
+
return {
|
|
2223
|
+
path: edit.path,
|
|
2224
|
+
operation: edit.operation,
|
|
2225
|
+
...edit.insertLine !== void 0 ? { insertLine: edit.insertLine } : {},
|
|
2226
|
+
truncated: true
|
|
2227
|
+
};
|
|
2228
|
+
}
|
|
1896
2229
|
function truncate(text, max) {
|
|
1897
2230
|
return text.length <= max ? text : text.slice(0, max - 1) + "…";
|
|
1898
2231
|
}
|
|
@@ -1936,7 +2269,8 @@ function projectHistory(events) {
|
|
|
1936
2269
|
tool: {
|
|
1937
2270
|
name: String(data?.name ?? "tool"),
|
|
1938
2271
|
state: "running",
|
|
1939
|
-
summary: summarizeArgs(data?.arguments)
|
|
2272
|
+
summary: summarizeArgs(data?.arguments),
|
|
2273
|
+
...editField(data?.name, data?.arguments)
|
|
1940
2274
|
}
|
|
1941
2275
|
};
|
|
1942
2276
|
messages.push(row);
|
|
@@ -1985,7 +2319,8 @@ function limitMessageProjection(message) {
|
|
|
1985
2319
|
...message.tool ? { tool: {
|
|
1986
2320
|
...message.tool,
|
|
1987
2321
|
name: truncateUtf8(message.tool.name, 4096),
|
|
1988
|
-
summary: truncateUtf8(message.tool.summary, 65536)
|
|
2322
|
+
summary: truncateUtf8(message.tool.summary, 65536),
|
|
2323
|
+
...message.tool.edit ? { edit: withoutEditBodies(message.tool.edit) } : {}
|
|
1989
2324
|
} } : {},
|
|
1990
2325
|
...message.attachments ? { attachments: message.attachments.slice(0, 16).map((attachment) => ({
|
|
1991
2326
|
...attachment,
|
|
@@ -2129,8 +2464,7 @@ function historyWindow(source, options) {
|
|
|
2129
2464
|
hasMore: older.length > 0
|
|
2130
2465
|
};
|
|
2131
2466
|
}
|
|
2132
|
-
|
|
2133
|
-
//#region src/wire-session.ts
|
|
2467
|
+
// src/wire-session.ts
|
|
2134
2468
|
/**
|
|
2135
2469
|
* 会话特性的 wire 行:列表/归档、打开与关闭、创建/分支/重命名/取消、历史与
|
|
2136
2470
|
* 附件、模型目录与切换、prompt 投递。
|
|
@@ -2464,6 +2798,9 @@ const sessionRows = [
|
|
|
2464
2798
|
const invalid = requireSessionId(checked.value);
|
|
2465
2799
|
if (invalid) return invalid;
|
|
2466
2800
|
if (!isText(checked.value.attachmentId)) return reject$1("E_PROTOCOL", "invalid attachmentId");
|
|
2801
|
+
const maxFrameBytes = checked.value.maxFrameBytes;
|
|
2802
|
+
if (maxFrameBytes !== void 0 && (!Number.isSafeInteger(maxFrameBytes) || maxFrameBytes <= 0)) return reject$1("E_PROTOCOL", "invalid maxFrameBytes");
|
|
2803
|
+
if (checked.value.binary !== void 0 && typeof checked.value.binary !== "boolean") return reject$1("E_PROTOCOL", "invalid binary");
|
|
2467
2804
|
return checked;
|
|
2468
2805
|
},
|
|
2469
2806
|
handle: async (ctx, payload) => {
|
|
@@ -2471,6 +2808,34 @@ const sessionRows = [
|
|
|
2471
2808
|
if (attachmentId.length === 0) return ctx.fail("E_PROTOCOL", "sessionId and attachmentId required");
|
|
2472
2809
|
const image = await ctx.bridge.attachmentData(payload.sessionId, attachmentId);
|
|
2473
2810
|
if (!image) return ctx.fail("E_NOT_FOUND", "attachment unavailable");
|
|
2811
|
+
if (payload.binary === true && ctx.sendBinary) {
|
|
2812
|
+
const bytes = Buffer.from(image.data, "base64");
|
|
2813
|
+
if (bytes.length === 0) return ctx.fail("E_NOT_FOUND", "attachment unavailable");
|
|
2814
|
+
if (bytes.length > 12582912) return ctx.fail("E_TOO_LARGE", `attachment ${bytes.length} bytes exceeds limit`);
|
|
2815
|
+
let index = 0;
|
|
2816
|
+
try {
|
|
2817
|
+
for (let offset = 0; offset < bytes.length; offset += BINARY_CHUNK_BYTES) {
|
|
2818
|
+
await ctx.waitForWritable?.();
|
|
2819
|
+
ctx.sendBinary("s2c.session.attachment.chunk", { index: index++ }, bytes.subarray(offset, offset + BINARY_CHUNK_BYTES));
|
|
2820
|
+
}
|
|
2821
|
+
} catch {
|
|
2822
|
+
return;
|
|
2823
|
+
}
|
|
2824
|
+
const ack = {
|
|
2825
|
+
...image.mediaType !== void 0 ? { mediaType: image.mediaType } : {},
|
|
2826
|
+
chunks: index,
|
|
2827
|
+
bytes: bytes.length
|
|
2828
|
+
};
|
|
2829
|
+
return ctx.send("s2c.ack", ack);
|
|
2830
|
+
}
|
|
2831
|
+
const clientLimit = payload.maxFrameBytes ?? 1048576;
|
|
2832
|
+
const frameBytes = image.data.length + (image.mediaType?.length ?? 0) + ATTACHMENT_ENVELOPE_HEADROOM_BYTES;
|
|
2833
|
+
if (frameBytes > Math.min(clientLimit, 2097152)) return ctx.fail("E_TOO_LARGE", `attachment frame ${frameBytes} bytes exceeds limit`);
|
|
2834
|
+
try {
|
|
2835
|
+
await ctx.waitForWritable?.();
|
|
2836
|
+
} catch {
|
|
2837
|
+
return;
|
|
2838
|
+
}
|
|
2474
2839
|
ctx.send("s2c.ack", image);
|
|
2475
2840
|
}
|
|
2476
2841
|
},
|
|
@@ -2682,8 +3047,7 @@ const sessionRows = [
|
|
|
2682
3047
|
}
|
|
2683
3048
|
}
|
|
2684
3049
|
];
|
|
2685
|
-
|
|
2686
|
-
//#region src/wire-command.ts
|
|
3050
|
+
// src/wire-command.ts
|
|
2687
3051
|
function sessionPayload(value) {
|
|
2688
3052
|
const result = payloadObject(value);
|
|
2689
3053
|
return result.ok && !isText(result.value.sessionId) ? reject$1("E_PROTOCOL", "会话标识无效") : result;
|
|
@@ -2755,6 +3119,7 @@ const commandRows = [...[
|
|
|
2755
3119
|
const result = sessionPayload(value);
|
|
2756
3120
|
if (!result.ok) return result;
|
|
2757
3121
|
if (result.value.format !== void 0 && result.value.format !== "zip" && result.value.format !== "markdown") return reject$1("E_PROTOCOL", "导出格式无效");
|
|
3122
|
+
if (result.value.binary !== void 0 && typeof result.value.binary !== "boolean") return reject$1("E_PROTOCOL", "传输方式无效");
|
|
2758
3123
|
return result;
|
|
2759
3124
|
},
|
|
2760
3125
|
handle: async (ctx, payload) => {
|
|
@@ -2768,22 +3133,34 @@ const commandRows = [...[
|
|
|
2768
3133
|
try {
|
|
2769
3134
|
reader = (format === void 0 ? await ctx.bridge.exportSession(payload.sessionId, signal) : await ctx.bridge.exportSession(payload.sessionId, signal, format)).body.getReader();
|
|
2770
3135
|
signal.addEventListener("abort", cancel, { once: true });
|
|
3136
|
+
const binary = payload.binary === true && ctx.sendBinary !== void 0;
|
|
2771
3137
|
let index = 0, bytes = 0;
|
|
3138
|
+
const sendChunk = async (chunk) => {
|
|
3139
|
+
bytes += chunk.length;
|
|
3140
|
+
await ctx.waitForWritable?.();
|
|
3141
|
+
if (binary) ctx.sendBinary("s2c.session.export.chunk", { index: index++ }, chunk);
|
|
3142
|
+
else ctx.send("s2c.session.export.chunk", {
|
|
3143
|
+
index: index++,
|
|
3144
|
+
data: Buffer.from(chunk).toString("base64")
|
|
3145
|
+
});
|
|
3146
|
+
};
|
|
3147
|
+
let carry;
|
|
2772
3148
|
while (true) {
|
|
2773
3149
|
signal.throwIfAborted();
|
|
2774
3150
|
const next = await reader.read();
|
|
2775
3151
|
if (next.done) break;
|
|
2776
|
-
|
|
2777
|
-
|
|
2778
|
-
|
|
2779
|
-
|
|
2780
|
-
|
|
2781
|
-
|
|
2782
|
-
|
|
2783
|
-
|
|
3152
|
+
if (!binary) {
|
|
3153
|
+
for (let offset = 0; offset < next.value.length; offset += 49152) await sendChunk(next.value.subarray(offset, offset + 49152));
|
|
3154
|
+
continue;
|
|
3155
|
+
}
|
|
3156
|
+
carry = carry ? Buffer.concat([carry, next.value]) : Buffer.from(next.value);
|
|
3157
|
+
while (carry.length >= BINARY_CHUNK_BYTES) {
|
|
3158
|
+
await sendChunk(carry.subarray(0, BINARY_CHUNK_BYTES));
|
|
3159
|
+
carry = carry.subarray(BINARY_CHUNK_BYTES);
|
|
2784
3160
|
}
|
|
2785
3161
|
}
|
|
2786
3162
|
signal.throwIfAborted();
|
|
3163
|
+
if (carry !== void 0 && carry.length > 0) await sendChunk(carry);
|
|
2787
3164
|
ctx.send("s2c.ack", {
|
|
2788
3165
|
sessionId: payload.sessionId,
|
|
2789
3166
|
fileName: format === "markdown" ? "dsh-session.md" : "dsh-session-log.zip",
|
|
@@ -2806,8 +3183,7 @@ const commandRows = [...[
|
|
|
2806
3183
|
}
|
|
2807
3184
|
}
|
|
2808
3185
|
}];
|
|
2809
|
-
|
|
2810
|
-
//#region src/plugin-update.ts
|
|
3186
|
+
// src/plugin-update.ts
|
|
2811
3187
|
const PACKAGE = "dsh-deeppilot";
|
|
2812
3188
|
const PHASES = [
|
|
2813
3189
|
"preparing",
|
|
@@ -3254,8 +3630,7 @@ function supportsPluginUpdate(value) {
|
|
|
3254
3630
|
"registries"
|
|
3255
3631
|
].every((key) => typeof value[key] === "function");
|
|
3256
3632
|
}
|
|
3257
|
-
|
|
3258
|
-
//#region src/wire-plugin-update.ts
|
|
3633
|
+
// src/wire-plugin-update.ts
|
|
3259
3634
|
function payloadObject$1(value) {
|
|
3260
3635
|
if (value === void 0 || value === null) return {
|
|
3261
3636
|
ok: true,
|
|
@@ -3459,8 +3834,7 @@ const ROWS_BY_TYPE = new Map(WIRE_FRAME_ROWS.map((row) => [row.type, row]));
|
|
|
3459
3834
|
function registryRowFor(type) {
|
|
3460
3835
|
return ROWS_BY_TYPE.get(type);
|
|
3461
3836
|
}
|
|
3462
|
-
|
|
3463
|
-
//#region src/connection-gate.ts
|
|
3837
|
+
// src/connection-gate.ts
|
|
3464
3838
|
/**
|
|
3465
3839
|
* 连接门(ConnectionGate)——「一个匿名 socket 被允许做什么」的唯一属主。
|
|
3466
3840
|
*
|
|
@@ -3752,6 +4126,7 @@ var ConnectionGate = class {
|
|
|
3752
4126
|
debug: host.debug,
|
|
3753
4127
|
log: (message) => host.log(message),
|
|
3754
4128
|
send: (type, payload) => host.send(type, payload, env.id),
|
|
4129
|
+
...host.sendBinary ? { sendBinary: (type, payload, body) => host.sendBinary(type, payload, body, env.id) } : {},
|
|
3755
4130
|
fail: (code, message) => host.fail(env.id, code, message),
|
|
3756
4131
|
close: (code, reason) => host.close(code, reason),
|
|
3757
4132
|
canReceive: (scope) => host.canReceive(scope),
|
|
@@ -3779,8 +4154,7 @@ var ConnectionGate = class {
|
|
|
3779
4154
|
};
|
|
3780
4155
|
}
|
|
3781
4156
|
};
|
|
3782
|
-
|
|
3783
|
-
//#region src/connection.ts
|
|
4157
|
+
// src/connection.ts
|
|
3784
4158
|
/**
|
|
3785
4159
|
* One connected phone. Implements BridgeSink so the HostBridge can push
|
|
3786
4160
|
* projected frames and replays.
|
|
@@ -3852,6 +4226,7 @@ var BridgeConnection = class {
|
|
|
3852
4226
|
},
|
|
3853
4227
|
log: (message) => deps.log(message),
|
|
3854
4228
|
send: (type, payload, id) => connection.send(type, payload, id),
|
|
4229
|
+
sendBinary: (type, payload, body, id) => connection.send(type, payload, id, void 0, body),
|
|
3855
4230
|
fail: (id, code, message) => connection.fail(id, code, message),
|
|
3856
4231
|
close: (code, reason) => connection.close(code, reason),
|
|
3857
4232
|
canReceive: (scope) => connection.canReceive(scope),
|
|
@@ -4021,7 +4396,8 @@ var BridgeConnection = class {
|
|
|
4021
4396
|
this.ws.terminate();
|
|
4022
4397
|
}
|
|
4023
4398
|
}
|
|
4024
|
-
|
|
4399
|
+
/** `body` 存在时按二进制帧发送(见 binary-frame.ts),头部仍是同一个 envelope。 */
|
|
4400
|
+
send(type, payload, id, seq, body) {
|
|
4025
4401
|
const envelope = {
|
|
4026
4402
|
v: 2,
|
|
4027
4403
|
type,
|
|
@@ -4035,7 +4411,7 @@ var BridgeConnection = class {
|
|
|
4035
4411
|
this.close(1013, "client too slow");
|
|
4036
4412
|
return;
|
|
4037
4413
|
}
|
|
4038
|
-
this.ws.send(JSON.stringify(envelope));
|
|
4414
|
+
this.ws.send(body === void 0 ? JSON.stringify(envelope) : encodeBinaryFrame(envelope, body));
|
|
4039
4415
|
}
|
|
4040
4416
|
fail(id, code, message) {
|
|
4041
4417
|
this.send("s2c.error", {
|
|
@@ -4069,8 +4445,7 @@ var BridgeConnection = class {
|
|
|
4069
4445
|
function normalizeFunnelConnectionLimit(value) {
|
|
4070
4446
|
return typeof value === "number" && Number.isInteger(value) && value >= 1 && value <= 16 ? value : 8;
|
|
4071
4447
|
}
|
|
4072
|
-
|
|
4073
|
-
//#region src/tailscale-version.ts
|
|
4448
|
+
// src/tailscale-version.ts
|
|
4074
4449
|
/**
|
|
4075
4450
|
* 内嵌 helper 上报的 tailscale.com 模块版本。
|
|
4076
4451
|
*
|
|
@@ -4093,8 +4468,7 @@ const MAX_TAILSCALE_VERSION_LENGTH = 80;
|
|
|
4093
4468
|
function isTailscaleVersion(value) {
|
|
4094
4469
|
return typeof value === "string" && value.length <= MAX_TAILSCALE_VERSION_LENGTH && TAILSCALE_VERSION_PATTERN.test(value);
|
|
4095
4470
|
}
|
|
4096
|
-
|
|
4097
|
-
//#region src/remote-supervisor.ts
|
|
4471
|
+
// src/remote-supervisor.ts
|
|
4098
4472
|
const RESTART_DELAYS_MS = [
|
|
4099
4473
|
1e3,
|
|
4100
4474
|
2e3,
|
|
@@ -4523,8 +4897,7 @@ var RemoteSupervisor = class {
|
|
|
4523
4897
|
else if (next.phase === "error" || next.phase === "unavailable") this.options.log(`remote Funnel ${next.phase}: ${next.message ?? "unknown error"}`);
|
|
4524
4898
|
}
|
|
4525
4899
|
};
|
|
4526
|
-
|
|
4527
|
-
//#region src/local-policy.ts
|
|
4900
|
+
// src/local-policy.ts
|
|
4528
4901
|
const DEFAULT_LOCAL_PORT = 3098;
|
|
4529
4902
|
const MIN_LOCAL_PORT = 1024;
|
|
4530
4903
|
const MAX_LOCAL_PORT = 65535;
|
|
@@ -4542,8 +4915,7 @@ function localListenError(error, port) {
|
|
|
4542
4915
|
if (code === "EACCES") return `permission denied while opening local port ${port}`;
|
|
4543
4916
|
return error instanceof Error ? error.message : String(error);
|
|
4544
4917
|
}
|
|
4545
|
-
|
|
4546
|
-
//#region src/config.ts
|
|
4918
|
+
// src/config.ts
|
|
4547
4919
|
/** Operator-run relay used by distributed builds; overridable via config. */
|
|
4548
4920
|
const DEFAULT_RELAY_URL = "https://pilot.hailab.dev";
|
|
4549
4921
|
const Config = z.object({
|
|
@@ -4632,8 +5004,7 @@ function normalizeOptions(options) {
|
|
|
4632
5004
|
if (options && typeof options === "object") return plainConfig(options);
|
|
4633
5005
|
return plainConfig(Config(void 0) ?? {});
|
|
4634
5006
|
}
|
|
4635
|
-
|
|
4636
|
-
//#region src/apns.ts
|
|
5007
|
+
// src/apns.ts
|
|
4637
5008
|
/**
|
|
4638
5009
|
* Minimal APNs provider client (HTTP/2) with zero npm dependencies.
|
|
4639
5010
|
*
|
|
@@ -4870,8 +5241,7 @@ var ApnsClient = class {
|
|
|
4870
5241
|
return token.length <= 10 ? "…" : token.slice(0, 6) + "…" + token.slice(-4);
|
|
4871
5242
|
}
|
|
4872
5243
|
};
|
|
4873
|
-
|
|
4874
|
-
//#region src/relay-url.ts
|
|
5244
|
+
// src/relay-url.ts
|
|
4875
5245
|
/**
|
|
4876
5246
|
* Normalize an operator-configured relay URL without a backtracking regular
|
|
4877
5247
|
* expression. Scanning from the end keeps even pathological inputs linear.
|
|
@@ -4882,8 +5252,7 @@ function normalizeRelayBaseUrl(value) {
|
|
|
4882
5252
|
while (end > 0 && trimmed.charCodeAt(end - 1) === 47) end -= 1;
|
|
4883
5253
|
return end === trimmed.length ? trimmed : trimmed.slice(0, end);
|
|
4884
5254
|
}
|
|
4885
|
-
|
|
4886
|
-
//#region src/relay-client.ts
|
|
5255
|
+
// src/relay-client.ts
|
|
4887
5256
|
var RelayClient = class {
|
|
4888
5257
|
base;
|
|
4889
5258
|
token;
|
|
@@ -4976,8 +5345,7 @@ var RelayClient = class {
|
|
|
4976
5345
|
}
|
|
4977
5346
|
}
|
|
4978
5347
|
};
|
|
4979
|
-
|
|
4980
|
-
//#region src/widget-push.ts
|
|
5348
|
+
// src/widget-push.ts
|
|
4981
5349
|
/** Coalesce bursts and bound continuous updates, without starving the trailing
|
|
4982
5350
|
* state. One scheduler per host, not per token or per streaming text frame. */
|
|
4983
5351
|
var WidgetPushScheduler = class {
|
|
@@ -5014,8 +5382,7 @@ var WidgetPushScheduler = class {
|
|
|
5014
5382
|
this.timer = void 0;
|
|
5015
5383
|
}
|
|
5016
5384
|
};
|
|
5017
|
-
|
|
5018
|
-
//#region src/live-activity.ts
|
|
5385
|
+
// src/live-activity.ts
|
|
5019
5386
|
function liveActivityState(session) {
|
|
5020
5387
|
const total = Math.max(0, session?.todos?.total ?? session?.todoItems?.length ?? 0);
|
|
5021
5388
|
const done = Math.min(total, Math.max(0, session?.todos?.done ?? 0));
|
|
@@ -5087,8 +5454,7 @@ var LiveActivityPushManager = class {
|
|
|
5087
5454
|
this.scheduler.dispose();
|
|
5088
5455
|
}
|
|
5089
5456
|
};
|
|
5090
|
-
|
|
5091
|
-
//#region src/push-policy.ts
|
|
5457
|
+
// src/push-policy.ts
|
|
5092
5458
|
/** Prune only when the provider supplies an authoritative token-lifecycle verdict. */
|
|
5093
5459
|
function shouldPrunePushToken(outcome, reason) {
|
|
5094
5460
|
return outcome === "invalid-token" && (reason === "Unregistered" || reason === "ExpiredToken");
|
|
@@ -5124,8 +5490,7 @@ function pushContent(notification, mode) {
|
|
|
5124
5490
|
body: bodies[notification.category] ?? "Open DeepPilot for an update."
|
|
5125
5491
|
};
|
|
5126
5492
|
}
|
|
5127
|
-
|
|
5128
|
-
//#region src/relay-test.ts
|
|
5493
|
+
// src/relay-test.ts
|
|
5129
5494
|
/**
|
|
5130
5495
|
* Relay connectivity self-test used by the settings page "测试中继" button.
|
|
5131
5496
|
*
|
|
@@ -5251,8 +5616,7 @@ async function runRelayProbe(options) {
|
|
|
5251
5616
|
steps
|
|
5252
5617
|
};
|
|
5253
5618
|
}
|
|
5254
|
-
|
|
5255
|
-
//#region src/push-gateway.ts
|
|
5619
|
+
// src/push-gateway.ts
|
|
5256
5620
|
/**
|
|
5257
5621
|
* 推送网关(PushGateway)——离线推送的单一属主。
|
|
5258
5622
|
*
|
|
@@ -5742,8 +6106,7 @@ var PushGateway = class {
|
|
|
5742
6106
|
await this.ensureRelayEnrolled(url);
|
|
5743
6107
|
}
|
|
5744
6108
|
};
|
|
5745
|
-
|
|
5746
|
-
//#region src/phone-server.ts
|
|
6109
|
+
// src/phone-server.ts
|
|
5747
6110
|
/**
|
|
5748
6111
|
* A deliberately narrow transport listener. Both the LAN endpoint and the
|
|
5749
6112
|
* loopback-only Funnel origin use this factory, so neither can accidentally
|
|
@@ -5822,8 +6185,7 @@ function closeServer(server) {
|
|
|
5822
6185
|
if (server === void 0 || !server.listening) return Promise.resolve();
|
|
5823
6186
|
return new Promise((resolve) => server.close(() => resolve()));
|
|
5824
6187
|
}
|
|
5825
|
-
|
|
5826
|
-
//#region src/transport-reconciler.ts
|
|
6188
|
+
// src/transport-reconciler.ts
|
|
5827
6189
|
var TransportReconciler = class {
|
|
5828
6190
|
spec;
|
|
5829
6191
|
config;
|
|
@@ -6051,8 +6413,7 @@ function createRemoteTransport(deps) {
|
|
|
6051
6413
|
statusOf: (supervisor) => supervisor?.status() ?? fallbackStatus()
|
|
6052
6414
|
}, deps.config, deps.log, fallbackStatus());
|
|
6053
6415
|
}
|
|
6054
|
-
|
|
6055
|
-
//#region src/dispatch-journal.ts
|
|
6416
|
+
// src/dispatch-journal.ts
|
|
6056
6417
|
/**
|
|
6057
6418
|
* at-most-once dispatch journal:prompt 投递与非 prompt 变更的**同一个** module。
|
|
6058
6419
|
*
|
|
@@ -6484,8 +6845,7 @@ function scheduleMutationCodec(persistValues) {
|
|
|
6484
6845
|
}))
|
|
6485
6846
|
};
|
|
6486
6847
|
}
|
|
6487
|
-
|
|
6488
|
-
//#region src/notification-text.ts
|
|
6848
|
+
// src/notification-text.ts
|
|
6489
6849
|
/**
|
|
6490
6850
|
* 通知正文的纯文本投影。
|
|
6491
6851
|
*
|
|
@@ -6528,8 +6888,7 @@ function decodePercentRuns(text) {
|
|
|
6528
6888
|
}
|
|
6529
6889
|
});
|
|
6530
6890
|
}
|
|
6531
|
-
|
|
6532
|
-
//#region src/host-api.ts
|
|
6891
|
+
// src/host-api.ts
|
|
6533
6892
|
/**
|
|
6534
6893
|
* Subagent sessions are host-internal workers of a parent conversation.
|
|
6535
6894
|
* They must never surface on the phone: not in the project/session list,
|
|
@@ -6552,8 +6911,231 @@ function unwrapStreamItem(item) {
|
|
|
6552
6911
|
} : nested;
|
|
6553
6912
|
return item;
|
|
6554
6913
|
}
|
|
6555
|
-
|
|
6556
|
-
|
|
6914
|
+
// src/quiet-completion.ts
|
|
6915
|
+
/**
|
|
6916
|
+
* 计划任务按需完成通知(#35)。
|
|
6917
|
+
*
|
|
6918
|
+
* heartbeat 式计划任务大多数轮次"一切正常",每轮都推「任务完成」是噪音。插件
|
|
6919
|
+
* 给计划任务会话提供一个 Agent 工具:Agent 按任务提示词里的规则判断本轮不需要
|
|
6920
|
+
* 提醒时调用它,本轮成功结束就不发完成通知(不写 s2c.notify、不推 APNs)。
|
|
6921
|
+
* 失败、审批、提问照常通知;模型忘了调用时照常提醒,宁可多一次打扰也不漏报。
|
|
6922
|
+
*
|
|
6923
|
+
* 职责划分:
|
|
6924
|
+
* - scheduledTurnOf:同步读宿主会话日志,判定当前这一轮是否由计划任务投递驱动。
|
|
6925
|
+
* 工具的 `when` 与 `execute` 都在宿主进程内同步调用它,不依赖桥接层异步收到的
|
|
6926
|
+
* 事件(实测 `when` 求值时本轮 user/message 尚未落盘,桥接层也还没收到)。
|
|
6927
|
+
* - QuietCompletionTracker:计划任务会话的粘性集合,以及每个会话"本轮已请求静默"
|
|
6928
|
+
* 的记录;HostBridge 在 user/message 与 turn.end 处调用它。
|
|
6929
|
+
* - quietCompletionTool:面向模型的工具定义,由 index.ts 注册到宿主 `ctx.tools`。
|
|
6930
|
+
*
|
|
6931
|
+
* 只在宿主级计划任务(`schedulesHost`,DSH 0.2.2 起)上注册:0.2.1 的
|
|
6932
|
+
* `tools.register` 不支持 `when`,多余参数会被静默忽略,工具会变成全局可见。
|
|
6933
|
+
*/
|
|
6934
|
+
const QUIET_COMPLETION_TOOL_NAME = "deeppilot_skip_completion_notice";
|
|
6935
|
+
/** 往回扫描会话日志的上限:一轮内的事件远少于此,防止异常长轮次拖慢步骤准备。 */
|
|
6936
|
+
const MAX_TURN_SCAN_EVENTS = 2e3;
|
|
6937
|
+
function sessionLogOf(agent) {
|
|
6938
|
+
const session = agent?.session;
|
|
6939
|
+
if (!session || typeof session.id !== "string" || typeof session.seq !== "number" || typeof session.eventAt !== "function") return;
|
|
6940
|
+
return session;
|
|
6941
|
+
}
|
|
6942
|
+
function sourceKindOf(value) {
|
|
6943
|
+
const kind = value?.source?.kind;
|
|
6944
|
+
return typeof kind === "string" ? kind : void 0;
|
|
6945
|
+
}
|
|
6946
|
+
/** 一次 inbox 拼接里插入的条目(按插入顺序)及其目标队列。 */
|
|
6947
|
+
function insertedOf(event) {
|
|
6948
|
+
if (event.type !== "agent/inbox/spliced") return void 0;
|
|
6949
|
+
const data = event.data;
|
|
6950
|
+
const items = Array.isArray(data?.inserted) ? data.inserted : [];
|
|
6951
|
+
return items.length > 0 ? {
|
|
6952
|
+
target: data?.target,
|
|
6953
|
+
items
|
|
6954
|
+
} : void 0;
|
|
6955
|
+
}
|
|
6956
|
+
/**
|
|
6957
|
+
* 判定当前这一轮是否由计划任务投递驱动(同步读宿主会话日志,DSH 0.2.2-rc.1 实测形状)。
|
|
6958
|
+
*
|
|
6959
|
+
* 宿主把待处理消息先拼进 inbox(`agent/inbox/spliced`,带条目本体与目标队列),
|
|
6960
|
+
* `turn/start` 之后的拼接只是取走(`removedCount`,不带内容),条目随后才落成
|
|
6961
|
+
* user/message。因此按 inbox 插入判定:
|
|
6962
|
+
* - 本轮开始之后插入 `next-step` 的用户条目 = 用户中途插话,不算;
|
|
6963
|
+
* - 本轮开始时取走的是上一轮开始之后插入 `next-turn` 的条目;其中最后插入的一条
|
|
6964
|
+
* 来源为 `schedule` 才算(计划任务投递);来源为 `user` 不算;
|
|
6965
|
+
* - 同为 `schedule` 来源的任务变更提示插入的是 `next-step`(宿主注明"不是投递"),不参与判定;
|
|
6966
|
+
* - 扫不到本轮 `turn/start` 时按普通轮次处理,宁可照常提醒。
|
|
6967
|
+
*/
|
|
6968
|
+
function scheduledTurnOf(session) {
|
|
6969
|
+
const floor = Math.max(0, session.seq - MAX_TURN_SCAN_EVENTS);
|
|
6970
|
+
let turn;
|
|
6971
|
+
let seenTurnStart = false;
|
|
6972
|
+
for (let seq = session.seq - 1; seq >= floor; seq--) {
|
|
6973
|
+
const event = session.eventAt(seq);
|
|
6974
|
+
if (!event) continue;
|
|
6975
|
+
if (event.type === "turn/start") {
|
|
6976
|
+
if (seenTurnStart) break;
|
|
6977
|
+
seenTurnStart = true;
|
|
6978
|
+
const value = event.data?.turn;
|
|
6979
|
+
if (typeof value === "number") turn = value;
|
|
6980
|
+
continue;
|
|
6981
|
+
}
|
|
6982
|
+
const splice = insertedOf(event);
|
|
6983
|
+
if (splice === void 0) continue;
|
|
6984
|
+
if (!seenTurnStart) {
|
|
6985
|
+
if (splice.target === "next-step" && splice.items.some((item) => sourceKindOf(item) === "user")) return {
|
|
6986
|
+
scheduled: false,
|
|
6987
|
+
...turn !== void 0 ? { turn } : {}
|
|
6988
|
+
};
|
|
6989
|
+
continue;
|
|
6990
|
+
}
|
|
6991
|
+
if (splice.target !== "next-turn") continue;
|
|
6992
|
+
for (let index = splice.items.length - 1; index >= 0; index--) {
|
|
6993
|
+
const kind = sourceKindOf(splice.items[index]);
|
|
6994
|
+
if (kind === "user") return {
|
|
6995
|
+
scheduled: false,
|
|
6996
|
+
...turn !== void 0 ? { turn } : {}
|
|
6997
|
+
};
|
|
6998
|
+
if (kind === "schedule") return {
|
|
6999
|
+
scheduled: true,
|
|
7000
|
+
...turn !== void 0 ? { turn } : {}
|
|
7001
|
+
};
|
|
7002
|
+
}
|
|
7003
|
+
}
|
|
7004
|
+
return {
|
|
7005
|
+
scheduled: false,
|
|
7006
|
+
...turn !== void 0 ? { turn } : {}
|
|
7007
|
+
};
|
|
7008
|
+
}
|
|
7009
|
+
var QuietCompletionTracker = class {
|
|
7010
|
+
/**
|
|
7011
|
+
* 判定过计划任务轮次的会话。粘性集合:加入后不再移除,工具不按轮出现或消失。
|
|
7012
|
+
* 宿主每次工具列表变化都会往会话追加一条工具增减消息并让提示词缓存失效,
|
|
7013
|
+
* 按轮开关会让同一会话反复产生这类消息。
|
|
7014
|
+
*/
|
|
7015
|
+
scheduleSessions = /* @__PURE__ */ new Set();
|
|
7016
|
+
quiet = /* @__PURE__ */ new Map();
|
|
7017
|
+
/** 启动时用宿主任务列表里的投递目标会话预热,避免重启后工具先消失再出现。 */
|
|
7018
|
+
seed(sessionIds) {
|
|
7019
|
+
for (const sessionId of sessionIds) if (sessionId) this.scheduleSessions.add(sessionId);
|
|
7020
|
+
}
|
|
7021
|
+
isScheduleSession(sessionId) {
|
|
7022
|
+
return this.scheduleSessions.has(sessionId);
|
|
7023
|
+
}
|
|
7024
|
+
/** 加入粘性集合;返回是否为新加入(调用方据此记一次日志)。 */
|
|
7025
|
+
markScheduleSession(sessionId) {
|
|
7026
|
+
if (this.scheduleSessions.has(sessionId)) return false;
|
|
7027
|
+
this.scheduleSessions.add(sessionId);
|
|
7028
|
+
return true;
|
|
7029
|
+
}
|
|
7030
|
+
/** 工具执行:只在计划任务轮次内生效。 */
|
|
7031
|
+
requestQuiet(sessionId, verdict, seq) {
|
|
7032
|
+
if (sessionId === void 0 || !verdict.scheduled) return {
|
|
7033
|
+
suppressed: false,
|
|
7034
|
+
note: "This run was not started by a scheduled task; the completion notification will be sent as usual."
|
|
7035
|
+
};
|
|
7036
|
+
this.quiet.set(sessionId, {
|
|
7037
|
+
...verdict.turn !== void 0 ? { turn: verdict.turn } : {},
|
|
7038
|
+
seq
|
|
7039
|
+
});
|
|
7040
|
+
return { suppressed: true };
|
|
7041
|
+
}
|
|
7042
|
+
/**
|
|
7043
|
+
* 桥接层观察到 user/message:用户在请求静默之后插话(日志位置更晚)就撤销静默,
|
|
7044
|
+
* 本轮照常提醒。计划任务与注入上下文不影响。
|
|
7045
|
+
*/
|
|
7046
|
+
noteUserMessage(sessionId, sourceKind, seq) {
|
|
7047
|
+
const request = this.quiet.get(sessionId);
|
|
7048
|
+
if (request === void 0 || sourceKind !== "user") return;
|
|
7049
|
+
if (seq === void 0 || seq > request.seq) this.quiet.delete(sessionId);
|
|
7050
|
+
}
|
|
7051
|
+
/**
|
|
7052
|
+
* turn.end 时调用。返回 true 表示本轮完成通知应被抑制:成功结束、且本轮请求过静默。
|
|
7053
|
+
* 轮次号能对上时按轮次号判定;迟到的上一轮 turn.end 保留本轮的请求。
|
|
7054
|
+
*/
|
|
7055
|
+
consumeTurnEnd(sessionId, ok, turn, seq) {
|
|
7056
|
+
const request = this.quiet.get(sessionId);
|
|
7057
|
+
if (request === void 0) return false;
|
|
7058
|
+
if (turn !== void 0 && request.turn !== void 0) {
|
|
7059
|
+
if (turn < request.turn) return false;
|
|
7060
|
+
this.quiet.delete(sessionId);
|
|
7061
|
+
return ok && turn === request.turn;
|
|
7062
|
+
}
|
|
7063
|
+
if (seq !== void 0 && seq < request.seq) return false;
|
|
7064
|
+
this.quiet.delete(sessionId);
|
|
7065
|
+
return ok;
|
|
7066
|
+
}
|
|
7067
|
+
};
|
|
7068
|
+
const DESCRIPTION = [
|
|
7069
|
+
"Call this during a scheduled run only when the task prompt says the user does not need to be notified",
|
|
7070
|
+
"for results like this one. It suppresses only this run's \"task completed\" notification; approvals,",
|
|
7071
|
+
"questions and errors still notify. Do not call it if anything needs the user's attention, or if the",
|
|
7072
|
+
"prompt gives no such rule."
|
|
7073
|
+
].join(" ");
|
|
7074
|
+
/** reason 归一化:非字符串按空串处理,去首尾空白并截断。 */
|
|
7075
|
+
function normalizeReason(raw) {
|
|
7076
|
+
const text = typeof raw === "string" ? raw.trim() : "";
|
|
7077
|
+
return text.length > 200 ? text.slice(0, 200) : text;
|
|
7078
|
+
}
|
|
7079
|
+
/**
|
|
7080
|
+
* 面向模型的工具定义(宿主 ToolDefinition 原始形状:JSON Schema 参数、输出 schema
|
|
7081
|
+
* 与 render、execute)。`log` 只记会话 ID、结果与 reason 长度:reason 由模型撰写,
|
|
7082
|
+
* 可能引用结果内容,按插件日志约定不打正文。
|
|
7083
|
+
*/
|
|
7084
|
+
function quietCompletionTool(tracker, log = () => {}) {
|
|
7085
|
+
return {
|
|
7086
|
+
name: QUIET_COMPLETION_TOOL_NAME,
|
|
7087
|
+
description: DESCRIPTION,
|
|
7088
|
+
parameters: {
|
|
7089
|
+
type: "object",
|
|
7090
|
+
additionalProperties: false,
|
|
7091
|
+
required: ["reason"],
|
|
7092
|
+
properties: { reason: {
|
|
7093
|
+
type: "string",
|
|
7094
|
+
description: `Why this run does not need the user's attention (at most 200 characters).`
|
|
7095
|
+
} }
|
|
7096
|
+
},
|
|
7097
|
+
output: {
|
|
7098
|
+
schema: {
|
|
7099
|
+
type: "object",
|
|
7100
|
+
additionalProperties: false,
|
|
7101
|
+
required: ["suppressed"],
|
|
7102
|
+
properties: {
|
|
7103
|
+
suppressed: { type: "boolean" },
|
|
7104
|
+
note: { type: "string" }
|
|
7105
|
+
}
|
|
7106
|
+
},
|
|
7107
|
+
render(_args, value) {
|
|
7108
|
+
return [{
|
|
7109
|
+
type: "text",
|
|
7110
|
+
text: value.suppressed ? "The completion notification for this run will not be sent." : value.note ?? "The completion notification will be sent as usual."
|
|
7111
|
+
}];
|
|
7112
|
+
}
|
|
7113
|
+
},
|
|
7114
|
+
async execute(args, exec) {
|
|
7115
|
+
const session = sessionLogOf(exec.agent);
|
|
7116
|
+
const reason = normalizeReason(args?.reason);
|
|
7117
|
+
const result = session === void 0 ? tracker.requestQuiet(void 0, { scheduled: false }, 0) : tracker.requestQuiet(session.id, scheduledTurnOf(session), session.seq);
|
|
7118
|
+
log(`quiet completion requested session=${session?.id ?? "?"} suppressed=${result.suppressed} reasonChars=${reason.length}`);
|
|
7119
|
+
return result;
|
|
7120
|
+
}
|
|
7121
|
+
};
|
|
7122
|
+
}
|
|
7123
|
+
/**
|
|
7124
|
+
* 注册工具。`when`:粘性集合内的会话直接放行;否则同步判定本轮是否计划任务轮次,
|
|
7125
|
+
* 是则加入集合(首轮即可见,`new` 目标每次新建的会话同样可见)。普通会话看不到。
|
|
7126
|
+
* 返回宿主给的注销函数。
|
|
7127
|
+
*/
|
|
7128
|
+
function registerQuietCompletionTool(tools, tracker, log = () => {}) {
|
|
7129
|
+
return tools.register(quietCompletionTool(tracker, log), { when: (context) => {
|
|
7130
|
+
const session = sessionLogOf(context.agent);
|
|
7131
|
+
if (session === void 0) return false;
|
|
7132
|
+
if (tracker.isScheduleSession(session.id)) return true;
|
|
7133
|
+
if (!scheduledTurnOf(session).scheduled) return false;
|
|
7134
|
+
if (tracker.markScheduleSession(session.id)) log(`quiet completion: session joined scheduled set session=${session.id}`);
|
|
7135
|
+
return true;
|
|
7136
|
+
} });
|
|
7137
|
+
}
|
|
7138
|
+
// src/host-capabilities.ts
|
|
6557
7139
|
const pluginUpdateProbes = /* @__PURE__ */ new WeakMap();
|
|
6558
7140
|
function setPluginUpdateProbe(proxy, probe) {
|
|
6559
7141
|
if (probe) pluginUpdateProbes.set(proxy, probe);
|
|
@@ -6569,6 +7151,7 @@ const HOST_CAPABILITY_PROBES = {
|
|
|
6569
7151
|
sessionRestore: (proxy) => typeof proxy.workspace?.unarchiveSession === "function",
|
|
6570
7152
|
projectSelection: (proxy) => typeof proxy.workspace?.list === "function" && typeof proxy.workspace?.create === "function",
|
|
6571
7153
|
schedules: (proxy) => proxy.schedule !== void 0,
|
|
7154
|
+
schedulesHost: (proxy) => proxy.schedule?.mode === "host",
|
|
6572
7155
|
promptQueue: (proxy) => typeof proxy.sessions.updateQueue === "function",
|
|
6573
7156
|
sessionMarkdown: (proxy) => proxy.commands?.markdownAvailable === true && typeof proxy.commands?.export === "function",
|
|
6574
7157
|
assistantStream: (proxy) => proxy.assistantStreamAvailable === true,
|
|
@@ -6582,6 +7165,7 @@ function capabilityBits(proxy, pushOutlet) {
|
|
|
6582
7165
|
return {
|
|
6583
7166
|
historyPaging: true,
|
|
6584
7167
|
replay: true,
|
|
7168
|
+
binaryFrames: true,
|
|
6585
7169
|
approvals: true,
|
|
6586
7170
|
questions: true,
|
|
6587
7171
|
pendingSnapshot: true,
|
|
@@ -6595,6 +7179,7 @@ function capabilityBits(proxy, pushOutlet) {
|
|
|
6595
7179
|
sessionRestore: HOST_CAPABILITY_PROBES.sessionRestore(proxy),
|
|
6596
7180
|
projectSelection: HOST_CAPABILITY_PROBES.projectSelection(proxy),
|
|
6597
7181
|
schedules: HOST_CAPABILITY_PROBES.schedules(proxy),
|
|
7182
|
+
schedulesHost: HOST_CAPABILITY_PROBES.schedulesHost(proxy),
|
|
6598
7183
|
promptQueue: HOST_CAPABILITY_PROBES.promptQueue(proxy),
|
|
6599
7184
|
sessionMarkdown: HOST_CAPABILITY_PROBES.sessionMarkdown(proxy),
|
|
6600
7185
|
assistantStream: HOST_CAPABILITY_PROBES.assistantStream(proxy),
|
|
@@ -6605,8 +7190,7 @@ function capabilityBits(proxy, pushOutlet) {
|
|
|
6605
7190
|
deviceRevoke: true
|
|
6606
7191
|
};
|
|
6607
7192
|
}
|
|
6608
|
-
|
|
6609
|
-
//#region src/prompt-queue.ts
|
|
7193
|
+
// src/prompt-queue.ts
|
|
6610
7194
|
/**
|
|
6611
7195
|
* 运行中排队与转向(PROTOCOL.md「运行中排队与转向扩展」)的纯函数部分。
|
|
6612
7196
|
*
|
|
@@ -6857,8 +7441,7 @@ var AssistantStream = class {
|
|
|
6857
7441
|
this.attempts.clear();
|
|
6858
7442
|
}
|
|
6859
7443
|
};
|
|
6860
|
-
|
|
6861
|
-
//#region src/host-bridge.ts
|
|
7444
|
+
// src/host-bridge.ts
|
|
6862
7445
|
const MAX_RING_DEFAULT = 2e3;
|
|
6863
7446
|
/**
|
|
6864
7447
|
* Process-wide bridge state: session mirror, pending approvals/questions,
|
|
@@ -6880,6 +7463,8 @@ var HostBridge = class {
|
|
|
6880
7463
|
* and restore them. Excluded from `summaries` and from the live broadcast. */
|
|
6881
7464
|
archivedSummaries = /* @__PURE__ */ new Map();
|
|
6882
7465
|
subagentSessionIds = /* @__PURE__ */ new Set();
|
|
7466
|
+
/** 计划任务按需完成通知(quiet-completion.ts):index.ts 用它注册 Agent 工具。 */
|
|
7467
|
+
quietCompletion = new QuietCompletionTracker();
|
|
6883
7468
|
/**
|
|
6884
7469
|
* 每个会话最近一次已知的队列(inbox 投影的折叠结果)及其指纹。来源是实时
|
|
6885
7470
|
* 投影通知或打开会话时读到的基线;指纹用来丢掉内容没变的重复通知。
|
|
@@ -7127,6 +7712,35 @@ var HostBridge = class {
|
|
|
7127
7712
|
this.runMuxStream();
|
|
7128
7713
|
this.refreshSummaries();
|
|
7129
7714
|
}
|
|
7715
|
+
/**
|
|
7716
|
+
* 用宿主任务列表里的投递目标会话预热计划任务会话集合,避免插件重启后工具
|
|
7717
|
+
* 先消失、等下一次投递再出现(每次变化都会给会话追加工具增减消息)。
|
|
7718
|
+
* `new` 目标没有固定会话,只能从最近一次投递回执里取。失败只记日志:集合仍会
|
|
7719
|
+
* 在下一次投递时补上。由 index.ts 在 schedule 服务就绪、注册工具时调用。
|
|
7720
|
+
*/
|
|
7721
|
+
async seedQuietCompletionSessions() {
|
|
7722
|
+
if (this.hostSchedule === void 0) return;
|
|
7723
|
+
const sessionIds = [];
|
|
7724
|
+
let offset = 0;
|
|
7725
|
+
for (;;) {
|
|
7726
|
+
const result = await this.listHostSchedules({
|
|
7727
|
+
limit: HOST_SCHEDULE_PAGE_SIZE,
|
|
7728
|
+
offset
|
|
7729
|
+
});
|
|
7730
|
+
if (!result.ok) {
|
|
7731
|
+
this.log?.(`quiet completion seed skipped: ${result.code}`);
|
|
7732
|
+
return;
|
|
7733
|
+
}
|
|
7734
|
+
for (const task of result.value.tasks) {
|
|
7735
|
+
if (task.target.kind !== "new") sessionIds.push(task.target.sessionId);
|
|
7736
|
+
if (task.lastDelivery) sessionIds.push(task.lastDelivery.targetSessionId);
|
|
7737
|
+
}
|
|
7738
|
+
offset += result.value.tasks.length;
|
|
7739
|
+
if (result.value.tasks.length === 0 || offset >= result.value.total) break;
|
|
7740
|
+
}
|
|
7741
|
+
if (this.disposed) return;
|
|
7742
|
+
this.quietCompletion.seed(sessionIds);
|
|
7743
|
+
}
|
|
7130
7744
|
dispose() {
|
|
7131
7745
|
if (this.disposed) return;
|
|
7132
7746
|
this.disposed = true;
|
|
@@ -7217,6 +7831,7 @@ var HostBridge = class {
|
|
|
7217
7831
|
if (event.type === "turn/end") this.assistantStream.abandon(sessionId);
|
|
7218
7832
|
if (event.type === "agent-preset/selected") this.record("s2c.skills.changed", { sessionId });
|
|
7219
7833
|
this.noteActivity(sessionId, event);
|
|
7834
|
+
if (event.type === "user/message") this.quietCompletion.noteUserMessage(sessionId, userMessageSourceKind(event.data), event.seq);
|
|
7220
7835
|
this.captureAssistantText(sessionId, event);
|
|
7221
7836
|
const projection = projectEvent(sessionId, event);
|
|
7222
7837
|
this.captureActivity(sessionId, event, projection?.data.tool);
|
|
@@ -7229,7 +7844,12 @@ var HostBridge = class {
|
|
|
7229
7844
|
...streamId && projection.kind === "message.final" ? { streamId } : {}
|
|
7230
7845
|
}
|
|
7231
7846
|
});
|
|
7232
|
-
if (projection?.kind === "turn.end")
|
|
7847
|
+
if (projection?.kind === "turn.end") {
|
|
7848
|
+
const ok = projection.data.ok === true;
|
|
7849
|
+
const turn = event.data?.turn;
|
|
7850
|
+
if (this.quietCompletion.consumeTurnEnd(sessionId, ok, typeof turn === "number" ? turn : void 0, event.seq)) this.log?.(`completion notice skipped by scheduled run session=${sessionId}`);
|
|
7851
|
+
else this.emitTurnCompletedNotify(sessionId, ok);
|
|
7852
|
+
}
|
|
7233
7853
|
break;
|
|
7234
7854
|
}
|
|
7235
7855
|
case "session/projection": {
|
|
@@ -8182,6 +8802,17 @@ var HostBridge = class {
|
|
|
8182
8802
|
replayed: result.replayed
|
|
8183
8803
|
};
|
|
8184
8804
|
}
|
|
8805
|
+
/**
|
|
8806
|
+
* 当前 adapter 是否宿主级计划任务模型。
|
|
8807
|
+
*
|
|
8808
|
+
* 能力位 `schedulesHost`(host-capabilities.ts)与新帧、旧帧映射全部读这一个
|
|
8809
|
+
* 判别位:探测只发生一次(DshApiProxy.resolveSchedule),这里不再看控制器
|
|
8810
|
+
* 方法,也不看版本号字符串。
|
|
8811
|
+
*/
|
|
8812
|
+
get hostSchedule() {
|
|
8813
|
+
const schedule = this.apiProxy.schedule;
|
|
8814
|
+
return schedule?.mode === "host" ? schedule : void 0;
|
|
8815
|
+
}
|
|
8185
8816
|
async listSchedules(sessionId) {
|
|
8186
8817
|
const schedule = this.apiProxy.schedule;
|
|
8187
8818
|
if (schedule === void 0) return {
|
|
@@ -8189,6 +8820,7 @@ var HostBridge = class {
|
|
|
8189
8820
|
code: "E_UNSUPPORTED",
|
|
8190
8821
|
message: "schedules unavailable on this host version"
|
|
8191
8822
|
};
|
|
8823
|
+
if (schedule.mode === "host") return this.listHostSchedulesAsLegacy(schedule, sessionId);
|
|
8192
8824
|
try {
|
|
8193
8825
|
const response = await schedule.list({
|
|
8194
8826
|
rpcId: randomUUID(),
|
|
@@ -8212,15 +8844,376 @@ var HostBridge = class {
|
|
|
8212
8844
|
};
|
|
8213
8845
|
}
|
|
8214
8846
|
}
|
|
8215
|
-
|
|
8216
|
-
|
|
8847
|
+
/** 宿主级列表 -> 旧会话级视图:过滤 session 目标、丢弃 ended、按 scheduledAt 升序。 */
|
|
8848
|
+
async listHostSchedulesAsLegacy(schedule, sessionId) {
|
|
8849
|
+
try {
|
|
8850
|
+
const tasks = [];
|
|
8851
|
+
let offset = 0;
|
|
8852
|
+
for (;;) {
|
|
8853
|
+
const response = await schedule.list({
|
|
8854
|
+
rpcId: randomUUID(),
|
|
8855
|
+
payload: {
|
|
8856
|
+
limit: HOST_SCHEDULE_PAGE_SIZE,
|
|
8857
|
+
offset
|
|
8858
|
+
}
|
|
8859
|
+
});
|
|
8860
|
+
if (!response.result) return {
|
|
8861
|
+
ok: false,
|
|
8862
|
+
code: "E_INTERNAL",
|
|
8863
|
+
message: "schedule list returned no result"
|
|
8864
|
+
};
|
|
8865
|
+
if (!response.result.ok) return wireErrorOf("schedule", response.result.error);
|
|
8866
|
+
const page = response.result.value;
|
|
8867
|
+
tasks.push(...page.tasks);
|
|
8868
|
+
offset += page.tasks.length;
|
|
8869
|
+
if (page.tasks.length === 0 || offset >= page.total) break;
|
|
8870
|
+
}
|
|
8871
|
+
const views = [];
|
|
8872
|
+
for (const task of tasks) {
|
|
8873
|
+
if (task.target.kind !== "session" || task.target.sessionId !== sessionId) continue;
|
|
8874
|
+
if (task.status === "ended") continue;
|
|
8875
|
+
const view = toLegacyScheduleView(task);
|
|
8876
|
+
if (view !== void 0) views.push(view);
|
|
8877
|
+
}
|
|
8878
|
+
views.sort((left, right) => left.scheduledAt < right.scheduledAt ? -1 : left.scheduledAt > right.scheduledAt ? 1 : 0);
|
|
8879
|
+
return {
|
|
8880
|
+
ok: true,
|
|
8881
|
+
value: views
|
|
8882
|
+
};
|
|
8883
|
+
} catch (error) {
|
|
8884
|
+
return wireErrorOf("schedule", hostFailureOf(error, "internal"));
|
|
8885
|
+
}
|
|
8886
|
+
}
|
|
8887
|
+
async listHostSchedules(request = {}) {
|
|
8888
|
+
const schedule = this.hostSchedule;
|
|
8217
8889
|
if (schedule === void 0) return {
|
|
8218
8890
|
ok: false,
|
|
8219
8891
|
code: "E_UNSUPPORTED",
|
|
8220
|
-
message: "schedules unavailable on this host version"
|
|
8892
|
+
message: "host-level schedules unavailable on this host version"
|
|
8221
8893
|
};
|
|
8222
8894
|
try {
|
|
8223
|
-
const response = await schedule.
|
|
8895
|
+
const response = await schedule.list({
|
|
8896
|
+
rpcId: randomUUID(),
|
|
8897
|
+
payload: request
|
|
8898
|
+
});
|
|
8899
|
+
if (!response.result) return {
|
|
8900
|
+
ok: false,
|
|
8901
|
+
code: "E_INTERNAL",
|
|
8902
|
+
message: "schedule host list returned no result"
|
|
8903
|
+
};
|
|
8904
|
+
if (!response.result.ok) return wireErrorOf("schedule", response.result.error);
|
|
8905
|
+
return {
|
|
8906
|
+
ok: true,
|
|
8907
|
+
value: response.result.value
|
|
8908
|
+
};
|
|
8909
|
+
} catch (error) {
|
|
8910
|
+
return wireErrorOf("schedule", hostFailureOf(error, "internal"));
|
|
8911
|
+
}
|
|
8912
|
+
}
|
|
8913
|
+
async viewHostSchedule(id) {
|
|
8914
|
+
const schedule = this.hostSchedule;
|
|
8915
|
+
if (schedule === void 0) return {
|
|
8916
|
+
ok: false,
|
|
8917
|
+
code: "E_UNSUPPORTED",
|
|
8918
|
+
message: "host-level schedules unavailable on this host version"
|
|
8919
|
+
};
|
|
8920
|
+
try {
|
|
8921
|
+
const response = await schedule.view({
|
|
8922
|
+
rpcId: randomUUID(),
|
|
8923
|
+
payload: { id }
|
|
8924
|
+
});
|
|
8925
|
+
if (!response.result) return {
|
|
8926
|
+
ok: false,
|
|
8927
|
+
code: "E_INTERNAL",
|
|
8928
|
+
message: "schedule host view returned no result"
|
|
8929
|
+
};
|
|
8930
|
+
if (!response.result.ok) return wireErrorOf("schedule", response.result.error);
|
|
8931
|
+
const value = response.result.value;
|
|
8932
|
+
if (value.kind === "not-found") return {
|
|
8933
|
+
ok: false,
|
|
8934
|
+
code: "E_NOT_FOUND",
|
|
8935
|
+
message: "schedule not found"
|
|
8936
|
+
};
|
|
8937
|
+
return {
|
|
8938
|
+
ok: true,
|
|
8939
|
+
value: value.task
|
|
8940
|
+
};
|
|
8941
|
+
} catch (error) {
|
|
8942
|
+
return wireErrorOf("schedule", hostFailureOf(error, "internal"));
|
|
8943
|
+
}
|
|
8944
|
+
}
|
|
8945
|
+
async createHostSchedule(deviceId, payload) {
|
|
8946
|
+
const schedule = this.hostSchedule;
|
|
8947
|
+
if (schedule === void 0) return {
|
|
8948
|
+
ok: false,
|
|
8949
|
+
code: "E_UNSUPPORTED",
|
|
8950
|
+
message: "host-level schedules unavailable on this host version"
|
|
8951
|
+
};
|
|
8952
|
+
const { clientRequestId, ...request } = payload;
|
|
8953
|
+
const result = await this.scheduleMutations.dispatch(deviceId, clientRequestId, request, async () => {
|
|
8954
|
+
const response = await schedule.create({
|
|
8955
|
+
rpcId: randomUUID(),
|
|
8956
|
+
payload: {
|
|
8957
|
+
title: request.title,
|
|
8958
|
+
prompt: request.prompt,
|
|
8959
|
+
timing: request.timing,
|
|
8960
|
+
...request.target !== void 0 ? { target: request.target } : {}
|
|
8961
|
+
}
|
|
8962
|
+
});
|
|
8963
|
+
if (!response.result) return {
|
|
8964
|
+
ok: false,
|
|
8965
|
+
code: "E_INTERNAL",
|
|
8966
|
+
message: "schedule host create returned no result"
|
|
8967
|
+
};
|
|
8968
|
+
if (!response.result.ok) return wireErrorOf("schedule", response.result.error);
|
|
8969
|
+
return {
|
|
8970
|
+
ok: true,
|
|
8971
|
+
value: response.result.value
|
|
8972
|
+
};
|
|
8973
|
+
});
|
|
8974
|
+
if (result.ok) {
|
|
8975
|
+
this.record("s2c.schedule.changed", {});
|
|
8976
|
+
return {
|
|
8977
|
+
ok: true,
|
|
8978
|
+
value: result.value,
|
|
8979
|
+
replayed: result.replayed
|
|
8980
|
+
};
|
|
8981
|
+
}
|
|
8982
|
+
return {
|
|
8983
|
+
ok: false,
|
|
8984
|
+
code: result.code,
|
|
8985
|
+
message: result.message ?? result.code,
|
|
8986
|
+
replayed: result.replayed
|
|
8987
|
+
};
|
|
8988
|
+
}
|
|
8989
|
+
async editHostSchedule(deviceId, payload) {
|
|
8990
|
+
const schedule = this.hostSchedule;
|
|
8991
|
+
if (schedule === void 0) return {
|
|
8992
|
+
ok: false,
|
|
8993
|
+
code: "E_UNSUPPORTED",
|
|
8994
|
+
message: "host-level schedules unavailable on this host version"
|
|
8995
|
+
};
|
|
8996
|
+
const { clientRequestId, ...request } = payload;
|
|
8997
|
+
const result = await this.scheduleMutations.dispatch(deviceId, clientRequestId, request, async () => {
|
|
8998
|
+
const expected = request.expectedUpdatedAt !== void 0 ? { expectedUpdatedAt: request.expectedUpdatedAt } : {};
|
|
8999
|
+
const response = await schedule.edit({
|
|
9000
|
+
rpcId: randomUUID(),
|
|
9001
|
+
payload: request.action === "update" ? {
|
|
9002
|
+
id: request.id,
|
|
9003
|
+
action: "update",
|
|
9004
|
+
patch: request.patch,
|
|
9005
|
+
...expected
|
|
9006
|
+
} : {
|
|
9007
|
+
id: request.id,
|
|
9008
|
+
action: "delete",
|
|
9009
|
+
...expected
|
|
9010
|
+
}
|
|
9011
|
+
});
|
|
9012
|
+
if (!response.result) return {
|
|
9013
|
+
ok: false,
|
|
9014
|
+
code: "E_INTERNAL",
|
|
9015
|
+
message: "schedule host edit returned no result"
|
|
9016
|
+
};
|
|
9017
|
+
if (!response.result.ok) return wireErrorOf("schedule", response.result.error);
|
|
9018
|
+
const value = response.result.value;
|
|
9019
|
+
if (value.kind === "conflict") return {
|
|
9020
|
+
ok: false,
|
|
9021
|
+
code: "E_BUSY",
|
|
9022
|
+
message: "schedule changed since it was read"
|
|
9023
|
+
};
|
|
9024
|
+
if (value.kind === "not-found") return {
|
|
9025
|
+
ok: false,
|
|
9026
|
+
code: "E_NOT_FOUND",
|
|
9027
|
+
message: "schedule not found"
|
|
9028
|
+
};
|
|
9029
|
+
if (request.action === "delete") {
|
|
9030
|
+
if (value.kind !== "deleted") return {
|
|
9031
|
+
ok: false,
|
|
9032
|
+
code: "E_INTERNAL",
|
|
9033
|
+
message: "schedule delete returned an updated task"
|
|
9034
|
+
};
|
|
9035
|
+
return {
|
|
9036
|
+
ok: true,
|
|
9037
|
+
value: {
|
|
9038
|
+
deleted: true,
|
|
9039
|
+
id: value.id
|
|
9040
|
+
}
|
|
9041
|
+
};
|
|
9042
|
+
}
|
|
9043
|
+
if (value.kind !== "task") return {
|
|
9044
|
+
ok: false,
|
|
9045
|
+
code: "E_INTERNAL",
|
|
9046
|
+
message: "schedule update returned a deletion"
|
|
9047
|
+
};
|
|
9048
|
+
return {
|
|
9049
|
+
ok: true,
|
|
9050
|
+
value: { task: value.task }
|
|
9051
|
+
};
|
|
9052
|
+
});
|
|
9053
|
+
if (result.ok) {
|
|
9054
|
+
this.record("s2c.schedule.changed", {});
|
|
9055
|
+
return {
|
|
9056
|
+
ok: true,
|
|
9057
|
+
value: result.value,
|
|
9058
|
+
replayed: result.replayed
|
|
9059
|
+
};
|
|
9060
|
+
}
|
|
9061
|
+
return {
|
|
9062
|
+
ok: false,
|
|
9063
|
+
code: result.code,
|
|
9064
|
+
message: result.message ?? result.code,
|
|
9065
|
+
replayed: result.replayed
|
|
9066
|
+
};
|
|
9067
|
+
}
|
|
9068
|
+
async runHostSchedule(deviceId, payload) {
|
|
9069
|
+
const schedule = this.hostSchedule;
|
|
9070
|
+
if (schedule === void 0) return {
|
|
9071
|
+
ok: false,
|
|
9072
|
+
code: "E_UNSUPPORTED",
|
|
9073
|
+
message: "host-level schedules unavailable on this host version"
|
|
9074
|
+
};
|
|
9075
|
+
const result = await this.scheduleMutations.dispatch(deviceId, payload.clientRequestId, { id: payload.id }, async () => {
|
|
9076
|
+
const response = await schedule.run({
|
|
9077
|
+
rpcId: randomUUID(),
|
|
9078
|
+
payload: { id: payload.id }
|
|
9079
|
+
});
|
|
9080
|
+
if (!response.result) return {
|
|
9081
|
+
ok: false,
|
|
9082
|
+
code: "E_INTERNAL",
|
|
9083
|
+
message: "schedule host run returned no result"
|
|
9084
|
+
};
|
|
9085
|
+
if (!response.result.ok) return wireErrorOf("schedule", response.result.error);
|
|
9086
|
+
const value = response.result.value;
|
|
9087
|
+
if (value.kind === "pending") return {
|
|
9088
|
+
ok: false,
|
|
9089
|
+
code: "E_BUSY",
|
|
9090
|
+
message: "schedule execution already pending"
|
|
9091
|
+
};
|
|
9092
|
+
if (value.kind === "not-found") return {
|
|
9093
|
+
ok: false,
|
|
9094
|
+
code: "E_NOT_FOUND",
|
|
9095
|
+
message: "schedule not found"
|
|
9096
|
+
};
|
|
9097
|
+
return {
|
|
9098
|
+
ok: true,
|
|
9099
|
+
value: value.task
|
|
9100
|
+
};
|
|
9101
|
+
});
|
|
9102
|
+
if (result.ok) {
|
|
9103
|
+
this.record("s2c.schedule.changed", {});
|
|
9104
|
+
return {
|
|
9105
|
+
ok: true,
|
|
9106
|
+
value: result.value,
|
|
9107
|
+
replayed: result.replayed
|
|
9108
|
+
};
|
|
9109
|
+
}
|
|
9110
|
+
return {
|
|
9111
|
+
ok: false,
|
|
9112
|
+
code: result.code,
|
|
9113
|
+
message: result.message ?? result.code,
|
|
9114
|
+
replayed: result.replayed
|
|
9115
|
+
};
|
|
9116
|
+
}
|
|
9117
|
+
/**
|
|
9118
|
+
* 删除宿主上全部已结束(`status="ended"`)的任务,返回删除条数。
|
|
9119
|
+
*
|
|
9120
|
+
* 与其他变更帧共用 scheduleMutations journal:同一 clientRequestId 重放时直接回放
|
|
9121
|
+
* 首次结果,不会再删一次。成功后广播 `s2c.schedule.changed`;失败时同样广播——上游
|
|
9122
|
+
* 文档说明失败的批次无法确定哪些行已删除,客户端应重新读取列表。
|
|
9123
|
+
*/
|
|
9124
|
+
async deleteEndedHostSchedules(deviceId, payload) {
|
|
9125
|
+
const schedule = this.hostSchedule;
|
|
9126
|
+
if (schedule === void 0) return {
|
|
9127
|
+
ok: false,
|
|
9128
|
+
code: "E_UNSUPPORTED",
|
|
9129
|
+
message: "host-level schedules unavailable on this host version"
|
|
9130
|
+
};
|
|
9131
|
+
const result = await this.scheduleMutations.dispatch(deviceId, payload.clientRequestId, { operation: "deleteEnded" }, async () => {
|
|
9132
|
+
const response = await schedule.deleteEnded({
|
|
9133
|
+
rpcId: randomUUID(),
|
|
9134
|
+
payload: {}
|
|
9135
|
+
});
|
|
9136
|
+
if (!response.result) return {
|
|
9137
|
+
ok: false,
|
|
9138
|
+
code: "E_INTERNAL",
|
|
9139
|
+
message: "schedule delete ended returned no result"
|
|
9140
|
+
};
|
|
9141
|
+
if (!response.result.ok) return wireErrorOf("schedule", response.result.error);
|
|
9142
|
+
return {
|
|
9143
|
+
ok: true,
|
|
9144
|
+
value: response.result.value
|
|
9145
|
+
};
|
|
9146
|
+
});
|
|
9147
|
+
this.record("s2c.schedule.changed", {});
|
|
9148
|
+
if (result.ok) return {
|
|
9149
|
+
ok: true,
|
|
9150
|
+
value: result.value ?? {},
|
|
9151
|
+
replayed: result.replayed
|
|
9152
|
+
};
|
|
9153
|
+
return {
|
|
9154
|
+
ok: false,
|
|
9155
|
+
code: result.code,
|
|
9156
|
+
message: result.message ?? result.code,
|
|
9157
|
+
replayed: result.replayed
|
|
9158
|
+
};
|
|
9159
|
+
}
|
|
9160
|
+
async scheduleLimits() {
|
|
9161
|
+
const schedule = this.hostSchedule;
|
|
9162
|
+
if (schedule === void 0) return {
|
|
9163
|
+
ok: false,
|
|
9164
|
+
code: "E_UNSUPPORTED",
|
|
9165
|
+
message: "host-level schedules unavailable on this host version"
|
|
9166
|
+
};
|
|
9167
|
+
try {
|
|
9168
|
+
const response = await schedule.limits({
|
|
9169
|
+
rpcId: randomUUID(),
|
|
9170
|
+
payload: {}
|
|
9171
|
+
});
|
|
9172
|
+
if (!response.result) return {
|
|
9173
|
+
ok: false,
|
|
9174
|
+
code: "E_INTERNAL",
|
|
9175
|
+
message: "schedule limits returned no result"
|
|
9176
|
+
};
|
|
9177
|
+
if (!response.result.ok) return wireErrorOf("schedule", response.result.error);
|
|
9178
|
+
return {
|
|
9179
|
+
ok: true,
|
|
9180
|
+
value: response.result.value
|
|
9181
|
+
};
|
|
9182
|
+
} catch (error) {
|
|
9183
|
+
return wireErrorOf("schedule", hostFailureOf(error, "internal"));
|
|
9184
|
+
}
|
|
9185
|
+
}
|
|
9186
|
+
async scheduleHistory(sessionId, id, limit, before) {
|
|
9187
|
+
const schedule = this.apiProxy.schedule;
|
|
9188
|
+
if (schedule === void 0) return {
|
|
9189
|
+
ok: false,
|
|
9190
|
+
code: "E_UNSUPPORTED",
|
|
9191
|
+
message: "schedules unavailable on this host version"
|
|
9192
|
+
};
|
|
9193
|
+
if (schedule.mode === "host") try {
|
|
9194
|
+
const response = await schedule.history({
|
|
9195
|
+
rpcId: randomUUID(),
|
|
9196
|
+
payload: {
|
|
9197
|
+
id,
|
|
9198
|
+
limit,
|
|
9199
|
+
...before !== void 0 ? { before } : {}
|
|
9200
|
+
}
|
|
9201
|
+
});
|
|
9202
|
+
if (!response.result) return {
|
|
9203
|
+
ok: false,
|
|
9204
|
+
code: "E_INTERNAL",
|
|
9205
|
+
message: "schedule history returned no result"
|
|
9206
|
+
};
|
|
9207
|
+
if (!response.result.ok) return wireErrorOf("schedule", response.result.error);
|
|
9208
|
+
return {
|
|
9209
|
+
ok: true,
|
|
9210
|
+
value: response.result.value
|
|
9211
|
+
};
|
|
9212
|
+
} catch (error) {
|
|
9213
|
+
return wireErrorOf("schedule", hostFailureOf(error, "internal"));
|
|
9214
|
+
}
|
|
9215
|
+
try {
|
|
9216
|
+
const response = await schedule.history({
|
|
8224
9217
|
rpcId: randomUUID(),
|
|
8225
9218
|
payload: {
|
|
8226
9219
|
sessionId,
|
|
@@ -8254,6 +9247,68 @@ var HostBridge = class {
|
|
|
8254
9247
|
code: "E_UNSUPPORTED",
|
|
8255
9248
|
message: "schedules unavailable on this host version"
|
|
8256
9249
|
};
|
|
9250
|
+
if (schedule.mode === "host") {
|
|
9251
|
+
const { clientRequestId, sessionId, title, prompt, ...selectors } = payload;
|
|
9252
|
+
const timing = hostTimingFromLegacySelector(selectors);
|
|
9253
|
+
if (timing === void 0) return {
|
|
9254
|
+
ok: false,
|
|
9255
|
+
code: "E_PROTOCOL",
|
|
9256
|
+
message: "unsupported schedule selector"
|
|
9257
|
+
};
|
|
9258
|
+
const request = {
|
|
9259
|
+
title,
|
|
9260
|
+
prompt,
|
|
9261
|
+
timing,
|
|
9262
|
+
target: {
|
|
9263
|
+
kind: "session",
|
|
9264
|
+
sessionId
|
|
9265
|
+
}
|
|
9266
|
+
};
|
|
9267
|
+
const result = await this.scheduleMutations.dispatch(deviceId, clientRequestId, request, async () => {
|
|
9268
|
+
const response = await schedule.create({
|
|
9269
|
+
rpcId: randomUUID(),
|
|
9270
|
+
payload: {
|
|
9271
|
+
title,
|
|
9272
|
+
prompt,
|
|
9273
|
+
timing,
|
|
9274
|
+
target: {
|
|
9275
|
+
kind: "session",
|
|
9276
|
+
sessionId
|
|
9277
|
+
}
|
|
9278
|
+
}
|
|
9279
|
+
});
|
|
9280
|
+
if (!response.result) return {
|
|
9281
|
+
ok: false,
|
|
9282
|
+
code: "E_INTERNAL",
|
|
9283
|
+
message: "schedule create returned no result"
|
|
9284
|
+
};
|
|
9285
|
+
if (!response.result.ok) return wireErrorOf("schedule", response.result.error);
|
|
9286
|
+
const legacy = toLegacyScheduleView(response.result.value);
|
|
9287
|
+
if (legacy === void 0) return {
|
|
9288
|
+
ok: false,
|
|
9289
|
+
code: "E_PROTOCOL",
|
|
9290
|
+
message: "schedule cannot be represented in the session-scoped view"
|
|
9291
|
+
};
|
|
9292
|
+
return {
|
|
9293
|
+
ok: true,
|
|
9294
|
+
value: legacy
|
|
9295
|
+
};
|
|
9296
|
+
});
|
|
9297
|
+
if (result.ok) {
|
|
9298
|
+
this.record("s2c.schedule.changed", {});
|
|
9299
|
+
return {
|
|
9300
|
+
ok: true,
|
|
9301
|
+
value: result.value,
|
|
9302
|
+
replayed: result.replayed
|
|
9303
|
+
};
|
|
9304
|
+
}
|
|
9305
|
+
return {
|
|
9306
|
+
ok: false,
|
|
9307
|
+
code: result.code,
|
|
9308
|
+
message: result.message ?? result.code,
|
|
9309
|
+
replayed: result.replayed
|
|
9310
|
+
};
|
|
9311
|
+
}
|
|
8257
9312
|
const { clientRequestId, sessionId, ...request } = payload;
|
|
8258
9313
|
const result = await this.scheduleMutations.dispatch(deviceId, clientRequestId, {
|
|
8259
9314
|
sessionId,
|
|
@@ -8299,6 +9354,106 @@ var HostBridge = class {
|
|
|
8299
9354
|
code: "E_UNSUPPORTED",
|
|
8300
9355
|
message: "schedules unavailable on this host version"
|
|
8301
9356
|
};
|
|
9357
|
+
if (schedule.mode === "host") {
|
|
9358
|
+
const request = {
|
|
9359
|
+
sessionId: payload.sessionId,
|
|
9360
|
+
id: payload.id,
|
|
9361
|
+
expected: payload.expected,
|
|
9362
|
+
title: payload.title,
|
|
9363
|
+
prompt: payload.prompt,
|
|
9364
|
+
change: payload.change
|
|
9365
|
+
};
|
|
9366
|
+
const result = await this.scheduleMutations.dispatch(deviceId, payload.clientRequestId, request, async () => {
|
|
9367
|
+
const current = await schedule.view({
|
|
9368
|
+
rpcId: randomUUID(),
|
|
9369
|
+
payload: { id: payload.id }
|
|
9370
|
+
});
|
|
9371
|
+
if (!current.result) return {
|
|
9372
|
+
ok: false,
|
|
9373
|
+
code: "E_INTERNAL",
|
|
9374
|
+
message: "schedule view returned no result"
|
|
9375
|
+
};
|
|
9376
|
+
if (!current.result.ok) return wireErrorOf("schedule", current.result.error);
|
|
9377
|
+
if (current.result.value.kind === "not-found") return {
|
|
9378
|
+
ok: false,
|
|
9379
|
+
code: "E_NOT_FOUND",
|
|
9380
|
+
message: "schedule not found"
|
|
9381
|
+
};
|
|
9382
|
+
const currentTask = current.result.value.task;
|
|
9383
|
+
if (!legacyScheduleMatchesExpected(currentTask, payload.expected)) return {
|
|
9384
|
+
ok: false,
|
|
9385
|
+
code: "E_BUSY",
|
|
9386
|
+
message: "schedule changed since it was read"
|
|
9387
|
+
};
|
|
9388
|
+
const patch = hostPatchFromLegacyUpdate(payload);
|
|
9389
|
+
if (Object.keys(patch).length === 0) {
|
|
9390
|
+
const legacy = toLegacyScheduleView(currentTask);
|
|
9391
|
+
if (legacy === void 0) return {
|
|
9392
|
+
ok: false,
|
|
9393
|
+
code: "E_PROTOCOL",
|
|
9394
|
+
message: "schedule cannot be represented in the session-scoped view"
|
|
9395
|
+
};
|
|
9396
|
+
return {
|
|
9397
|
+
ok: true,
|
|
9398
|
+
value: legacy
|
|
9399
|
+
};
|
|
9400
|
+
}
|
|
9401
|
+
const response = await schedule.edit({
|
|
9402
|
+
rpcId: randomUUID(),
|
|
9403
|
+
payload: {
|
|
9404
|
+
id: payload.id,
|
|
9405
|
+
action: "update",
|
|
9406
|
+
patch
|
|
9407
|
+
}
|
|
9408
|
+
});
|
|
9409
|
+
if (!response.result) return {
|
|
9410
|
+
ok: false,
|
|
9411
|
+
code: "E_INTERNAL",
|
|
9412
|
+
message: "schedule edit returned no result"
|
|
9413
|
+
};
|
|
9414
|
+
if (!response.result.ok) return wireErrorOf("schedule", response.result.error);
|
|
9415
|
+
const value = response.result.value;
|
|
9416
|
+
if (value.kind === "conflict") return {
|
|
9417
|
+
ok: false,
|
|
9418
|
+
code: "E_BUSY",
|
|
9419
|
+
message: "schedule changed since it was read"
|
|
9420
|
+
};
|
|
9421
|
+
if (value.kind === "not-found") return {
|
|
9422
|
+
ok: false,
|
|
9423
|
+
code: "E_NOT_FOUND",
|
|
9424
|
+
message: "schedule not found"
|
|
9425
|
+
};
|
|
9426
|
+
if (value.kind !== "task") return {
|
|
9427
|
+
ok: false,
|
|
9428
|
+
code: "E_INTERNAL",
|
|
9429
|
+
message: "schedule edit returned an unexpected deletion"
|
|
9430
|
+
};
|
|
9431
|
+
const legacy = toLegacyScheduleView(value.task);
|
|
9432
|
+
if (legacy === void 0) return {
|
|
9433
|
+
ok: false,
|
|
9434
|
+
code: "E_PROTOCOL",
|
|
9435
|
+
message: "schedule cannot be represented in the session-scoped view"
|
|
9436
|
+
};
|
|
9437
|
+
return {
|
|
9438
|
+
ok: true,
|
|
9439
|
+
value: legacy
|
|
9440
|
+
};
|
|
9441
|
+
});
|
|
9442
|
+
if (result.ok) {
|
|
9443
|
+
this.record("s2c.schedule.changed", {});
|
|
9444
|
+
return {
|
|
9445
|
+
ok: true,
|
|
9446
|
+
value: result.value,
|
|
9447
|
+
replayed: result.replayed
|
|
9448
|
+
};
|
|
9449
|
+
}
|
|
9450
|
+
return {
|
|
9451
|
+
ok: false,
|
|
9452
|
+
code: result.code,
|
|
9453
|
+
message: result.message ?? result.code,
|
|
9454
|
+
replayed: result.replayed
|
|
9455
|
+
};
|
|
9456
|
+
}
|
|
8302
9457
|
const { clientRequestId, ...request } = payload;
|
|
8303
9458
|
const result = await this.scheduleMutations.dispatch(deviceId, clientRequestId, request, async () => {
|
|
8304
9459
|
const response = await schedule.update({
|
|
@@ -8344,6 +9499,63 @@ var HostBridge = class {
|
|
|
8344
9499
|
code: "E_UNSUPPORTED",
|
|
8345
9500
|
message: "schedules unavailable on this host version"
|
|
8346
9501
|
};
|
|
9502
|
+
if (schedule.mode === "host") {
|
|
9503
|
+
const result = await this.scheduleMutations.dispatch(deviceId, payload.clientRequestId, {
|
|
9504
|
+
sessionId: payload.sessionId,
|
|
9505
|
+
id: payload.id
|
|
9506
|
+
}, async () => {
|
|
9507
|
+
const response = await schedule.edit({
|
|
9508
|
+
rpcId: randomUUID(),
|
|
9509
|
+
payload: {
|
|
9510
|
+
id: payload.id,
|
|
9511
|
+
action: "delete"
|
|
9512
|
+
}
|
|
9513
|
+
});
|
|
9514
|
+
if (!response.result) return {
|
|
9515
|
+
ok: false,
|
|
9516
|
+
code: "E_INTERNAL",
|
|
9517
|
+
message: "schedule delete returned no result"
|
|
9518
|
+
};
|
|
9519
|
+
if (!response.result.ok) return wireErrorOf("schedule", response.result.error);
|
|
9520
|
+
const value = response.result.value;
|
|
9521
|
+
if (value.kind === "deleted") return {
|
|
9522
|
+
ok: true,
|
|
9523
|
+
value: {
|
|
9524
|
+
id: value.id,
|
|
9525
|
+
deleted: true
|
|
9526
|
+
}
|
|
9527
|
+
};
|
|
9528
|
+
if (value.kind === "not-found") return {
|
|
9529
|
+
ok: false,
|
|
9530
|
+
code: "E_NOT_FOUND",
|
|
9531
|
+
message: "schedule not found"
|
|
9532
|
+
};
|
|
9533
|
+
if (value.kind === "conflict") return {
|
|
9534
|
+
ok: false,
|
|
9535
|
+
code: "E_BUSY",
|
|
9536
|
+
message: "schedule changed since it was read"
|
|
9537
|
+
};
|
|
9538
|
+
return {
|
|
9539
|
+
ok: false,
|
|
9540
|
+
code: "E_INTERNAL",
|
|
9541
|
+
message: "schedule delete returned an updated task"
|
|
9542
|
+
};
|
|
9543
|
+
});
|
|
9544
|
+
if (result.ok) {
|
|
9545
|
+
this.record("s2c.schedule.changed", {});
|
|
9546
|
+
return {
|
|
9547
|
+
ok: true,
|
|
9548
|
+
value: result.value,
|
|
9549
|
+
replayed: result.replayed
|
|
9550
|
+
};
|
|
9551
|
+
}
|
|
9552
|
+
return {
|
|
9553
|
+
ok: false,
|
|
9554
|
+
code: result.code,
|
|
9555
|
+
message: result.message ?? result.code,
|
|
9556
|
+
replayed: result.replayed
|
|
9557
|
+
};
|
|
9558
|
+
}
|
|
8347
9559
|
const result = await this.scheduleMutations.dispatch(deviceId, payload.clientRequestId, {
|
|
8348
9560
|
sessionId: payload.sessionId,
|
|
8349
9561
|
id: payload.id
|
|
@@ -8728,8 +9940,236 @@ function projectSessionModels(value) {
|
|
|
8728
9940
|
};
|
|
8729
9941
|
}
|
|
8730
9942
|
/** Project a history page (raw events) into MessageProjection rows. */
|
|
8731
|
-
|
|
8732
|
-
|
|
9943
|
+
/** 宿主级列表全量拉取时的页大小(Host 允许的最大值)。 */
|
|
9944
|
+
const HOST_SCHEDULE_PAGE_SIZE = 500;
|
|
9945
|
+
/** 参与旧帧冲突比较的字段:`state`/`deliveryMode` 是派生字段,不参与。 */
|
|
9946
|
+
const LEGACY_PROJECTION_FIELDS = [
|
|
9947
|
+
"id",
|
|
9948
|
+
"kind",
|
|
9949
|
+
"title",
|
|
9950
|
+
"prompt",
|
|
9951
|
+
"scheduledAt",
|
|
9952
|
+
"afterSeconds",
|
|
9953
|
+
"everySeconds",
|
|
9954
|
+
"time",
|
|
9955
|
+
"timeZone",
|
|
9956
|
+
"weekdays",
|
|
9957
|
+
"expression"
|
|
9958
|
+
];
|
|
9959
|
+
function isPlainObject$1(value) {
|
|
9960
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
9961
|
+
}
|
|
9962
|
+
function legacyNumber(...values) {
|
|
9963
|
+
for (const value of values) if (typeof value === "number" && Number.isFinite(value)) return value;
|
|
9964
|
+
}
|
|
9965
|
+
function legacyString(...values) {
|
|
9966
|
+
for (const value of values) if (typeof value === "string" && value.length > 0) return value;
|
|
9967
|
+
}
|
|
9968
|
+
function normalizeAtInput(value) {
|
|
9969
|
+
if (typeof value === "string" && value.length > 0) return value;
|
|
9970
|
+
if (!isPlainObject$1(value)) return void 0;
|
|
9971
|
+
const date = legacyString(value.date);
|
|
9972
|
+
const time = legacyString(value.time);
|
|
9973
|
+
const zone = legacyString(value.time_zone, value.timeZone);
|
|
9974
|
+
return date !== void 0 && time !== void 0 && zone !== void 0 ? {
|
|
9975
|
+
date,
|
|
9976
|
+
time,
|
|
9977
|
+
time_zone: zone
|
|
9978
|
+
} : void 0;
|
|
9979
|
+
}
|
|
9980
|
+
function normalizeLocalTime(value) {
|
|
9981
|
+
if (!isPlainObject$1(value)) return void 0;
|
|
9982
|
+
const time = legacyString(value.time);
|
|
9983
|
+
const zone = legacyString(value.time_zone, value.timeZone);
|
|
9984
|
+
return time !== void 0 && zone !== void 0 ? {
|
|
9985
|
+
time,
|
|
9986
|
+
time_zone: zone
|
|
9987
|
+
} : void 0;
|
|
9988
|
+
}
|
|
9989
|
+
function normalizeWeekdays(value) {
|
|
9990
|
+
if (!Array.isArray(value) || value.length === 0) return void 0;
|
|
9991
|
+
const days = value.filter((day) => typeof day === "number" && Number.isSafeInteger(day) && day >= 1 && day <= 7);
|
|
9992
|
+
return days.length === value.length ? days : void 0;
|
|
9993
|
+
}
|
|
9994
|
+
/** 旧创建帧的选择器 -> 宿主级 timing。旧 wire 校验保证恰好一个选择器。 */
|
|
9995
|
+
function hostTimingFromLegacySelector(selectors) {
|
|
9996
|
+
const after = legacyNumber(selectors.after_seconds);
|
|
9997
|
+
if (after !== void 0) return {
|
|
9998
|
+
kind: "after",
|
|
9999
|
+
after_seconds: after
|
|
10000
|
+
};
|
|
10001
|
+
if (selectors.at !== void 0) {
|
|
10002
|
+
const at = normalizeAtInput(selectors.at);
|
|
10003
|
+
return at === void 0 ? void 0 : {
|
|
10004
|
+
kind: "at",
|
|
10005
|
+
at
|
|
10006
|
+
};
|
|
10007
|
+
}
|
|
10008
|
+
const every = legacyNumber(selectors.every_seconds);
|
|
10009
|
+
if (every !== void 0) return {
|
|
10010
|
+
kind: "every",
|
|
10011
|
+
every_seconds: every
|
|
10012
|
+
};
|
|
10013
|
+
if (selectors.daily !== void 0) {
|
|
10014
|
+
const daily = normalizeLocalTime(selectors.daily);
|
|
10015
|
+
return daily === void 0 ? void 0 : {
|
|
10016
|
+
kind: "daily",
|
|
10017
|
+
daily
|
|
10018
|
+
};
|
|
10019
|
+
}
|
|
10020
|
+
if (selectors.weekly !== void 0) {
|
|
10021
|
+
if (!isPlainObject$1(selectors.weekly)) return void 0;
|
|
10022
|
+
const time = normalizeLocalTime(selectors.weekly);
|
|
10023
|
+
const weekdays = normalizeWeekdays(selectors.weekly.weekdays);
|
|
10024
|
+
return time === void 0 || weekdays === void 0 ? void 0 : {
|
|
10025
|
+
kind: "weekly",
|
|
10026
|
+
weekly: {
|
|
10027
|
+
...time,
|
|
10028
|
+
weekdays
|
|
10029
|
+
}
|
|
10030
|
+
};
|
|
10031
|
+
}
|
|
10032
|
+
if (selectors.cron !== void 0) {
|
|
10033
|
+
if (!isPlainObject$1(selectors.cron)) return void 0;
|
|
10034
|
+
const expression = legacyString(selectors.cron.expression);
|
|
10035
|
+
const zone = legacyString(selectors.cron.time_zone, selectors.cron.timeZone);
|
|
10036
|
+
return expression === void 0 || zone === void 0 ? void 0 : {
|
|
10037
|
+
kind: "cron",
|
|
10038
|
+
cron: {
|
|
10039
|
+
expression,
|
|
10040
|
+
time_zone: zone
|
|
10041
|
+
}
|
|
10042
|
+
};
|
|
10043
|
+
}
|
|
10044
|
+
}
|
|
10045
|
+
/**
|
|
10046
|
+
* 旧更新帧的 `change` -> 宿主级 timing。
|
|
10047
|
+
*
|
|
10048
|
+
* 两种历史写法都认:选择器式(`{kind:'daily', daily:{...}}`,与创建帧同形)与
|
|
10049
|
+
* 扁平式(`{kind:'daily', time, timeZone}` / `{kind:'cron', expression}`)。
|
|
10050
|
+
*/
|
|
10051
|
+
function hostTimingFromLegacyChange(change) {
|
|
10052
|
+
if (!isPlainObject$1(change)) return void 0;
|
|
10053
|
+
const selector = hostTimingFromLegacySelector(change);
|
|
10054
|
+
if (selector !== void 0) return selector;
|
|
10055
|
+
switch (change.kind) {
|
|
10056
|
+
case "daily": {
|
|
10057
|
+
const time = legacyString(change.time);
|
|
10058
|
+
const zone = legacyString(change.time_zone, change.timeZone);
|
|
10059
|
+
return time === void 0 || zone === void 0 ? void 0 : {
|
|
10060
|
+
kind: "daily",
|
|
10061
|
+
daily: {
|
|
10062
|
+
time,
|
|
10063
|
+
time_zone: zone
|
|
10064
|
+
}
|
|
10065
|
+
};
|
|
10066
|
+
}
|
|
10067
|
+
case "weekly": {
|
|
10068
|
+
const time = legacyString(change.time);
|
|
10069
|
+
const zone = legacyString(change.time_zone, change.timeZone);
|
|
10070
|
+
const weekdays = normalizeWeekdays(change.weekdays);
|
|
10071
|
+
return time === void 0 || zone === void 0 || weekdays === void 0 ? void 0 : {
|
|
10072
|
+
kind: "weekly",
|
|
10073
|
+
weekly: {
|
|
10074
|
+
time,
|
|
10075
|
+
time_zone: zone,
|
|
10076
|
+
weekdays
|
|
10077
|
+
}
|
|
10078
|
+
};
|
|
10079
|
+
}
|
|
10080
|
+
case "cron": {
|
|
10081
|
+
const expression = legacyString(change.expression);
|
|
10082
|
+
const zone = legacyString(change.time_zone, change.timeZone);
|
|
10083
|
+
return expression === void 0 || zone === void 0 ? void 0 : {
|
|
10084
|
+
kind: "cron",
|
|
10085
|
+
cron: {
|
|
10086
|
+
expression,
|
|
10087
|
+
time_zone: zone
|
|
10088
|
+
}
|
|
10089
|
+
};
|
|
10090
|
+
}
|
|
10091
|
+
case "rrule": {
|
|
10092
|
+
const rrule = legacyString(change.rrule);
|
|
10093
|
+
return rrule === void 0 ? void 0 : {
|
|
10094
|
+
kind: "rrule",
|
|
10095
|
+
rrule
|
|
10096
|
+
};
|
|
10097
|
+
}
|
|
10098
|
+
default: return;
|
|
10099
|
+
}
|
|
10100
|
+
}
|
|
10101
|
+
/** 旧更新帧的 title/prompt/change -> 宿主级 patch。 */
|
|
10102
|
+
function hostPatchFromLegacyUpdate(payload) {
|
|
10103
|
+
const patch = {};
|
|
10104
|
+
if (typeof payload.title === "string") patch.title = payload.title;
|
|
10105
|
+
if (typeof payload.prompt === "string") patch.prompt = payload.prompt;
|
|
10106
|
+
const timing = hostTimingFromLegacyChange(payload.change);
|
|
10107
|
+
if (timing !== void 0) patch.timing = timing;
|
|
10108
|
+
return patch;
|
|
10109
|
+
}
|
|
10110
|
+
/**
|
|
10111
|
+
* 宿主级任务视图 -> 旧会话级视图。
|
|
10112
|
+
*
|
|
10113
|
+
* `state` 只由 `scheduledAt` 是否已过推导(PROTOCOL.md 映射表);`rrule` 在旧枚举
|
|
10114
|
+
* 里没有对应 kind,返回 undefined 让调用方跳过——旧客户端按枚举解码列表,塞进
|
|
10115
|
+
* 未知 kind 会让整页解码失败,静默隐藏比让旧 App 打不开列表更安全。
|
|
10116
|
+
*/
|
|
10117
|
+
function toLegacyScheduleView(task) {
|
|
10118
|
+
const instant = Date.parse(task.scheduledAt);
|
|
10119
|
+
const base = {
|
|
10120
|
+
id: task.id,
|
|
10121
|
+
title: task.title,
|
|
10122
|
+
prompt: task.prompt,
|
|
10123
|
+
scheduledAt: task.scheduledAt,
|
|
10124
|
+
state: Number.isFinite(instant) && instant < Date.now() ? "overdue" : "scheduled",
|
|
10125
|
+
deliveryMode: "host"
|
|
10126
|
+
};
|
|
10127
|
+
switch (task.timing.kind) {
|
|
10128
|
+
case "after": return {
|
|
10129
|
+
...base,
|
|
10130
|
+
kind: "after",
|
|
10131
|
+
afterSeconds: task.timing.after_seconds
|
|
10132
|
+
};
|
|
10133
|
+
case "at": return {
|
|
10134
|
+
...base,
|
|
10135
|
+
kind: "at"
|
|
10136
|
+
};
|
|
10137
|
+
case "every": return {
|
|
10138
|
+
...base,
|
|
10139
|
+
kind: "every",
|
|
10140
|
+
everySeconds: task.timing.every_seconds
|
|
10141
|
+
};
|
|
10142
|
+
case "daily": return {
|
|
10143
|
+
...base,
|
|
10144
|
+
kind: "daily",
|
|
10145
|
+
time: task.timing.daily.time,
|
|
10146
|
+
timeZone: task.timing.daily.time_zone
|
|
10147
|
+
};
|
|
10148
|
+
case "weekly": return {
|
|
10149
|
+
...base,
|
|
10150
|
+
kind: "weekly",
|
|
10151
|
+
time: task.timing.weekly.time,
|
|
10152
|
+
timeZone: task.timing.weekly.time_zone,
|
|
10153
|
+
weekdays: [...task.timing.weekly.weekdays]
|
|
10154
|
+
};
|
|
10155
|
+
case "cron": return {
|
|
10156
|
+
...base,
|
|
10157
|
+
kind: "cron",
|
|
10158
|
+
expression: task.timing.cron.expression,
|
|
10159
|
+
timeZone: task.timing.cron.time_zone
|
|
10160
|
+
};
|
|
10161
|
+
default: return;
|
|
10162
|
+
}
|
|
10163
|
+
}
|
|
10164
|
+
/** 旧帧的冲突保护:当前任务的旧投影与客户端回传的 `expected` 逐字段相等。 */
|
|
10165
|
+
function legacyScheduleMatchesExpected(task, expected) {
|
|
10166
|
+
const view = toLegacyScheduleView(task);
|
|
10167
|
+
if (view === void 0) return false;
|
|
10168
|
+
if (!isPlainObject$1(expected)) return false;
|
|
10169
|
+
const actual = view;
|
|
10170
|
+
return LEGACY_PROJECTION_FIELDS.every((key) => JSON.stringify(actual[key]) === JSON.stringify(expected[key]));
|
|
10171
|
+
}
|
|
10172
|
+
// src/dsh-remote-interactions.ts
|
|
8733
10173
|
/**
|
|
8734
10174
|
* Resident DSH Remote Events client for phone interactions.
|
|
8735
10175
|
*
|
|
@@ -8917,8 +10357,7 @@ async function startDshRemoteInteractions(hostCtx, handlers) {
|
|
|
8917
10357
|
throw error;
|
|
8918
10358
|
}
|
|
8919
10359
|
}
|
|
8920
|
-
|
|
8921
|
-
//#region src/command-api.ts
|
|
10360
|
+
// src/command-api.ts
|
|
8922
10361
|
/**
|
|
8923
10362
|
* 给本域自造的失败带上 Host 风格的码。
|
|
8924
10363
|
*
|
|
@@ -9065,8 +10504,7 @@ var DshCommandApi = class {
|
|
|
9065
10504
|
return response;
|
|
9066
10505
|
}
|
|
9067
10506
|
};
|
|
9068
|
-
|
|
9069
|
-
//#region src/dsh-api-proxy.ts
|
|
10507
|
+
// src/dsh-api-proxy.ts
|
|
9070
10508
|
/**
|
|
9071
10509
|
* Adapter for the DSH rc.2 controller API.
|
|
9072
10510
|
*
|
|
@@ -9075,6 +10513,14 @@ var DshCommandApi = class {
|
|
|
9075
10513
|
* Session/Workspace controllers and Gateway-backed Remote Events, keeping
|
|
9076
10514
|
* the phone protocol isolated from the Host API.
|
|
9077
10515
|
*/
|
|
10516
|
+
/**
|
|
10517
|
+
* 形状探测:宿主级模型只有 `edit`(没有 `update`)。不按版本号字符串判断——
|
|
10518
|
+
* 版本号无法说明 Schedule bundle 是否挂载、也无法覆盖回退组合。
|
|
10519
|
+
*/
|
|
10520
|
+
function isHostScheduleController(controller) {
|
|
10521
|
+
const value = controller;
|
|
10522
|
+
return typeof value.edit === "function" && typeof value.update !== "function";
|
|
10523
|
+
}
|
|
9078
10524
|
/** Direct-controller adapter with the shape HostBridge consumes. */
|
|
9079
10525
|
var DshApiProxy = class {
|
|
9080
10526
|
ctx;
|
|
@@ -9145,14 +10591,16 @@ var DshApiProxy = class {
|
|
|
9145
10591
|
const controller = this.ctx.get("schedule");
|
|
9146
10592
|
if (controller === void 0) return;
|
|
9147
10593
|
this.scheduleController = controller;
|
|
9148
|
-
this.scheduleApi = this.
|
|
10594
|
+
this.scheduleApi = isHostScheduleController(controller) ? this.createHostScheduleApi(controller) : this.createLegacyScheduleApi(controller);
|
|
9149
10595
|
}
|
|
9150
10596
|
get schedule() {
|
|
9151
10597
|
this.resolveSchedule();
|
|
9152
10598
|
return this.scheduleApi;
|
|
9153
10599
|
}
|
|
9154
|
-
|
|
10600
|
+
/** 会话级适配器:行为与宿主级扩展加入前逐字节一致,只多一个判别位。 */
|
|
10601
|
+
createLegacyScheduleApi(controller) {
|
|
9155
10602
|
return {
|
|
10603
|
+
mode: "legacy",
|
|
9156
10604
|
list: async (request) => this.call(async () => ({
|
|
9157
10605
|
sessionId: request.payload.sessionId,
|
|
9158
10606
|
tasks: (await controller.list({ sessionId: request.payload.sessionId })).map(toScheduleTask)
|
|
@@ -9201,6 +10649,83 @@ var DshApiProxy = class {
|
|
|
9201
10649
|
};
|
|
9202
10650
|
}
|
|
9203
10651
|
/**
|
|
10652
|
+
* 宿主级适配器:把 DSH `ScheduleService` 的分页列表/结果对象归一化成 Bridge
|
|
10653
|
+
* 消费的稳定视图。
|
|
10654
|
+
*
|
|
10655
|
+
* 失败分两类:`view/edit/run/history` 的「找不到」是**结果对象**里的 code
|
|
10656
|
+
* (非变更 miss,不抛错),这里翻译成 outcome 或带 code 的 Error;
|
|
10657
|
+
* `create/edit/run` 的输入非法与目标已归档是 RemoteError 抛出,由
|
|
10658
|
+
* `this.call` 收成 `{code}`,再由 Bridge 按错误词表映射。
|
|
10659
|
+
*/
|
|
10660
|
+
createHostScheduleApi(controller) {
|
|
10661
|
+
return {
|
|
10662
|
+
mode: "host",
|
|
10663
|
+
list: async (request) => this.call(async () => {
|
|
10664
|
+
const payload = request.payload ?? {};
|
|
10665
|
+
const value = await controller.list({
|
|
10666
|
+
...payload.active !== void 0 ? { active: payload.active } : {},
|
|
10667
|
+
...payload.limit !== void 0 ? { limit: payload.limit } : {},
|
|
10668
|
+
...payload.offset !== void 0 ? { offset: payload.offset } : {}
|
|
10669
|
+
});
|
|
10670
|
+
const items = Array.isArray(value.items) ? value.items : [];
|
|
10671
|
+
return {
|
|
10672
|
+
tasks: items.map(toHostScheduleTask),
|
|
10673
|
+
shown: finiteCount(value.shown, items.length),
|
|
10674
|
+
total: finiteCount(value.total, items.length),
|
|
10675
|
+
limit: finiteCount(value.limit, payload.limit ?? 100),
|
|
10676
|
+
offset: finiteCount(value.offset, payload.offset ?? 0)
|
|
10677
|
+
};
|
|
10678
|
+
}),
|
|
10679
|
+
view: async (request) => this.call(async () => {
|
|
10680
|
+
return toHostScheduleViewOutcome(await controller.view({ id: request.payload.id }), request.payload.id);
|
|
10681
|
+
}),
|
|
10682
|
+
create: async (request) => this.call(async () => {
|
|
10683
|
+
const payload = request.payload;
|
|
10684
|
+
return toHostScheduleTask(await controller.create({
|
|
10685
|
+
title: payload.title,
|
|
10686
|
+
prompt: payload.prompt,
|
|
10687
|
+
timing: payload.timing,
|
|
10688
|
+
...payload.target !== void 0 ? { target: normalizeTargetRequest(payload.target) } : {}
|
|
10689
|
+
}));
|
|
10690
|
+
}),
|
|
10691
|
+
edit: async (request) => this.call(async () => {
|
|
10692
|
+
const payload = request.payload;
|
|
10693
|
+
const expected = payload.expectedUpdatedAt !== void 0 ? { expectedUpdatedAt: payload.expectedUpdatedAt } : {};
|
|
10694
|
+
const edit = payload.action === "update" ? {
|
|
10695
|
+
id: payload.id,
|
|
10696
|
+
action: "update",
|
|
10697
|
+
patch: normalizeHostPatch(payload.patch),
|
|
10698
|
+
...expected
|
|
10699
|
+
} : {
|
|
10700
|
+
id: payload.id,
|
|
10701
|
+
action: "delete",
|
|
10702
|
+
...expected
|
|
10703
|
+
};
|
|
10704
|
+
return toHostScheduleEditOutcome(await controller.edit(edit), payload.id);
|
|
10705
|
+
}),
|
|
10706
|
+
run: async (request) => this.call(async () => {
|
|
10707
|
+
return toHostScheduleRunOutcome(await controller.run({ id: request.payload.id }), request.payload.id);
|
|
10708
|
+
}),
|
|
10709
|
+
history: async (request) => this.call(async () => {
|
|
10710
|
+
const value = await controller.history({
|
|
10711
|
+
id: request.payload.id,
|
|
10712
|
+
limit: request.payload.limit,
|
|
10713
|
+
...request.payload.before !== void 0 ? { before: request.payload.before } : {}
|
|
10714
|
+
});
|
|
10715
|
+
if (typeof value.code === "string") throw Object.assign(new Error(String(value.code)), { code: value.code });
|
|
10716
|
+
return toScheduleHistory(value);
|
|
10717
|
+
}),
|
|
10718
|
+
limits: async () => this.call(async () => {
|
|
10719
|
+
const value = controller.deliveryLimits();
|
|
10720
|
+
return { newSessionIntervalSeconds: Math.max(0, Math.trunc(Number(value?.newSessionIntervalSeconds) || 0)) };
|
|
10721
|
+
}),
|
|
10722
|
+
deleteEnded: async () => this.call(async () => {
|
|
10723
|
+
const value = await controller.deleteInactive();
|
|
10724
|
+
return { deleted: Math.max(0, Math.trunc(Number(value?.deleted) || 0)) };
|
|
10725
|
+
})
|
|
10726
|
+
};
|
|
10727
|
+
}
|
|
10728
|
+
/**
|
|
9204
10729
|
* Decide whether this Gateway Client can surface a phone card. The decision
|
|
9205
10730
|
* fails open to false so a broken device registry delegates this delivery
|
|
9206
10731
|
* while the official Web Client remains able to answer.
|
|
@@ -9613,6 +11138,206 @@ function toScheduleHistory(value) {
|
|
|
9613
11138
|
...typeof row.nextBefore === "string" ? { nextBefore: row.nextBefore } : {}
|
|
9614
11139
|
};
|
|
9615
11140
|
}
|
|
11141
|
+
/**
|
|
11142
|
+
* 请求侧 target:`new_once`/`new` 的会话由 Host 预留或新建,客户端若把查看结果
|
|
11143
|
+
* 里的 `sessionId` 原样回传,不向上游透传(Host 的创建/编辑请求没有这个字段)。
|
|
11144
|
+
*/
|
|
11145
|
+
function normalizeTargetRequest(value) {
|
|
11146
|
+
if (value.kind === "session") return {
|
|
11147
|
+
kind: "session",
|
|
11148
|
+
sessionId: String(value.sessionId ?? "")
|
|
11149
|
+
};
|
|
11150
|
+
const create = toHostScheduleCreateSpec(value.create);
|
|
11151
|
+
return {
|
|
11152
|
+
kind: value.kind,
|
|
11153
|
+
...create !== void 0 ? { create } : {}
|
|
11154
|
+
};
|
|
11155
|
+
}
|
|
11156
|
+
/** 编辑补丁归一化:字段按契约逐个透传,target 走请求侧投影。 */
|
|
11157
|
+
function normalizeHostPatch(value) {
|
|
11158
|
+
return {
|
|
11159
|
+
...value.title !== void 0 ? { title: value.title } : {},
|
|
11160
|
+
...value.prompt !== void 0 ? { prompt: value.prompt } : {},
|
|
11161
|
+
...value.timing !== void 0 ? { timing: value.timing } : {},
|
|
11162
|
+
...value.target !== void 0 ? { target: normalizeTargetRequest(value.target) } : {},
|
|
11163
|
+
...value.active !== void 0 ? { active: value.active } : {}
|
|
11164
|
+
};
|
|
11165
|
+
}
|
|
11166
|
+
function isRecord(value) {
|
|
11167
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
11168
|
+
}
|
|
11169
|
+
function toRecord(value) {
|
|
11170
|
+
return isRecord(value) ? value : {};
|
|
11171
|
+
}
|
|
11172
|
+
/** 非负安全整数;形状不符时退回 fallback(分页字段只用于展示)。 */
|
|
11173
|
+
function finiteCount(value, fallback) {
|
|
11174
|
+
return typeof value === "number" && Number.isSafeInteger(value) && value >= 0 ? value : fallback;
|
|
11175
|
+
}
|
|
11176
|
+
function positiveSeconds(value) {
|
|
11177
|
+
return typeof value === "number" && Number.isFinite(value) && value > 0 ? value : 0;
|
|
11178
|
+
}
|
|
11179
|
+
function hostResultCode(value) {
|
|
11180
|
+
return typeof value.code === "string" ? value.code : void 0;
|
|
11181
|
+
}
|
|
11182
|
+
/** 结果对象里的「非变更 miss」不是抛错,这里翻译成 outcome 或带 code 的 Error。 */
|
|
11183
|
+
function throwHostResultCode(code) {
|
|
11184
|
+
throw Object.assign(new Error(code), { code });
|
|
11185
|
+
}
|
|
11186
|
+
function toHostScheduleCreateSpec(value) {
|
|
11187
|
+
const row = toRecord(value);
|
|
11188
|
+
const spec = {
|
|
11189
|
+
...typeof row.cwd === "string" ? { cwd: row.cwd } : {},
|
|
11190
|
+
...typeof row.agentPreset === "string" ? { agentPreset: row.agentPreset } : {}
|
|
11191
|
+
};
|
|
11192
|
+
return Object.keys(spec).length > 0 ? spec : void 0;
|
|
11193
|
+
}
|
|
11194
|
+
/**
|
|
11195
|
+
* 交付目标投影。`session` 与 `new_once` 带 sessionId(后者是创建时 Host 预留
|
|
11196
|
+
* 的会话);`new` 每次投递新建会话。`create` 省略时按 Host 默认。
|
|
11197
|
+
*/
|
|
11198
|
+
function toHostScheduleTarget(value) {
|
|
11199
|
+
const target = toRecord(value);
|
|
11200
|
+
const create = toHostScheduleCreateSpec(target.create);
|
|
11201
|
+
if (target.kind === "session") return {
|
|
11202
|
+
kind: "session",
|
|
11203
|
+
sessionId: String(target.sessionId ?? "")
|
|
11204
|
+
};
|
|
11205
|
+
if (target.kind === "new_once") return {
|
|
11206
|
+
kind: "new_once",
|
|
11207
|
+
sessionId: String(target.sessionId ?? ""),
|
|
11208
|
+
...create !== void 0 ? { create } : {}
|
|
11209
|
+
};
|
|
11210
|
+
return {
|
|
11211
|
+
kind: "new",
|
|
11212
|
+
...create !== void 0 ? { create } : {}
|
|
11213
|
+
};
|
|
11214
|
+
}
|
|
11215
|
+
function toHostScheduleReceipt(value) {
|
|
11216
|
+
const row = toRecord(value);
|
|
11217
|
+
return {
|
|
11218
|
+
scheduledAt: String(row.scheduledAt ?? ""),
|
|
11219
|
+
deliveredAt: String(row.deliveredAt ?? ""),
|
|
11220
|
+
messageId: String(row.messageId ?? ""),
|
|
11221
|
+
targetSessionId: String(row.targetSessionId ?? "")
|
|
11222
|
+
};
|
|
11223
|
+
}
|
|
11224
|
+
/**
|
|
11225
|
+
* timing 反向投影:DSH 存储记录用 camelCase(`afterSeconds`/`timeZone`),wire
|
|
11226
|
+
* 用 PROTOCOL.md 的 snake_case 嵌套结构。
|
|
11227
|
+
*
|
|
11228
|
+
* `at` 存储后只剩 `scheduledAt`(原始绝对时刻不是独立字段),因此用排定时刻
|
|
11229
|
+
* 回填——对 active 的一次性任务它就是目标时刻。
|
|
11230
|
+
*/
|
|
11231
|
+
function toHostScheduleTiming(row) {
|
|
11232
|
+
switch (row.kind) {
|
|
11233
|
+
case "after": return {
|
|
11234
|
+
kind: "after",
|
|
11235
|
+
after_seconds: positiveSeconds(row.afterSeconds)
|
|
11236
|
+
};
|
|
11237
|
+
case "every": return {
|
|
11238
|
+
kind: "every",
|
|
11239
|
+
every_seconds: positiveSeconds(row.everySeconds)
|
|
11240
|
+
};
|
|
11241
|
+
case "daily": return {
|
|
11242
|
+
kind: "daily",
|
|
11243
|
+
daily: localTimeInput(row)
|
|
11244
|
+
};
|
|
11245
|
+
case "weekly": return {
|
|
11246
|
+
kind: "weekly",
|
|
11247
|
+
weekly: {
|
|
11248
|
+
...localTimeInput(row),
|
|
11249
|
+
weekdays: Array.isArray(row.weekdays) ? row.weekdays.filter((day) => typeof day === "number") : []
|
|
11250
|
+
}
|
|
11251
|
+
};
|
|
11252
|
+
case "cron": return {
|
|
11253
|
+
kind: "cron",
|
|
11254
|
+
cron: {
|
|
11255
|
+
expression: String(row.expression ?? ""),
|
|
11256
|
+
time_zone: String(row.timeZone ?? "")
|
|
11257
|
+
}
|
|
11258
|
+
};
|
|
11259
|
+
case "rrule": return {
|
|
11260
|
+
kind: "rrule",
|
|
11261
|
+
rrule: String(row.rrule ?? "")
|
|
11262
|
+
};
|
|
11263
|
+
default: return {
|
|
11264
|
+
kind: "at",
|
|
11265
|
+
at: String(row.scheduledAt ?? "")
|
|
11266
|
+
};
|
|
11267
|
+
}
|
|
11268
|
+
}
|
|
11269
|
+
function localTimeInput(row) {
|
|
11270
|
+
return {
|
|
11271
|
+
time: String(row.time ?? ""),
|
|
11272
|
+
time_zone: String(row.timeZone ?? "")
|
|
11273
|
+
};
|
|
11274
|
+
}
|
|
11275
|
+
/** Host `ScheduleCatalogEntry` -> wire `schedule.host.task`(丢弃 involvedSessionIds)。 */
|
|
11276
|
+
function toHostScheduleTask(value) {
|
|
11277
|
+
const row = toRecord(value);
|
|
11278
|
+
return {
|
|
11279
|
+
id: String(row.id ?? ""),
|
|
11280
|
+
title: String(row.title ?? ""),
|
|
11281
|
+
prompt: String(row.prompt ?? ""),
|
|
11282
|
+
timing: toHostScheduleTiming(row),
|
|
11283
|
+
target: toHostScheduleTarget(row.target),
|
|
11284
|
+
status: row.status === "paused" || row.status === "ended" ? row.status : "active",
|
|
11285
|
+
scheduledAt: String(row.scheduledAt ?? ""),
|
|
11286
|
+
...typeof row.createdAt === "string" ? { createdAt: row.createdAt } : {},
|
|
11287
|
+
...typeof row.updatedAt === "string" ? { updatedAt: row.updatedAt } : {},
|
|
11288
|
+
...typeof row.executionActive === "boolean" ? { executionActive: row.executionActive } : {},
|
|
11289
|
+
...typeof row.deliveryFloorEndsAt === "string" ? { deliveryFloorEndsAt: row.deliveryFloorEndsAt } : {},
|
|
11290
|
+
...isRecord(row.lastDelivery) ? { lastDelivery: toHostScheduleReceipt(row.lastDelivery) } : {}
|
|
11291
|
+
};
|
|
11292
|
+
}
|
|
11293
|
+
function toHostScheduleViewOutcome(value, id) {
|
|
11294
|
+
const code = hostResultCode(value);
|
|
11295
|
+
if (code === "schedule_not_found") return {
|
|
11296
|
+
kind: "not-found",
|
|
11297
|
+
id
|
|
11298
|
+
};
|
|
11299
|
+
if (code !== void 0) throwHostResultCode(code);
|
|
11300
|
+
return {
|
|
11301
|
+
kind: "task",
|
|
11302
|
+
task: toHostScheduleTask(value)
|
|
11303
|
+
};
|
|
11304
|
+
}
|
|
11305
|
+
function toHostScheduleEditOutcome(value, id) {
|
|
11306
|
+
const code = hostResultCode(value);
|
|
11307
|
+
if (code === "schedule_conflict") return {
|
|
11308
|
+
kind: "conflict",
|
|
11309
|
+
id
|
|
11310
|
+
};
|
|
11311
|
+
if (code === "schedule_not_found") return {
|
|
11312
|
+
kind: "not-found",
|
|
11313
|
+
id
|
|
11314
|
+
};
|
|
11315
|
+
if (code !== void 0) throwHostResultCode(code);
|
|
11316
|
+
if (value.deleted === true) return {
|
|
11317
|
+
kind: "deleted",
|
|
11318
|
+
id: String(value.id ?? id)
|
|
11319
|
+
};
|
|
11320
|
+
return {
|
|
11321
|
+
kind: "task",
|
|
11322
|
+
task: toHostScheduleTask(value)
|
|
11323
|
+
};
|
|
11324
|
+
}
|
|
11325
|
+
function toHostScheduleRunOutcome(value, id) {
|
|
11326
|
+
const code = hostResultCode(value);
|
|
11327
|
+
if (code === "schedule_execution_pending") return {
|
|
11328
|
+
kind: "pending",
|
|
11329
|
+
id
|
|
11330
|
+
};
|
|
11331
|
+
if (code === "schedule_not_found") return {
|
|
11332
|
+
kind: "not-found",
|
|
11333
|
+
id
|
|
11334
|
+
};
|
|
11335
|
+
if (code !== void 0) throwHostResultCode(code);
|
|
11336
|
+
return {
|
|
11337
|
+
kind: "task",
|
|
11338
|
+
task: toHostScheduleTask(value)
|
|
11339
|
+
};
|
|
11340
|
+
}
|
|
9616
11341
|
async function readWorkspaceBaseline(controller) {
|
|
9617
11342
|
const abort = new AbortController();
|
|
9618
11343
|
const iterator = controller.follow(abort.signal)[Symbol.asyncIterator]();
|
|
@@ -9642,8 +11367,7 @@ async function projectModels(catalog, sessionId, list) {
|
|
|
9642
11367
|
failures: Array.isArray(value.failures) ? value.failures : []
|
|
9643
11368
|
};
|
|
9644
11369
|
}
|
|
9645
|
-
|
|
9646
|
-
//#region src/report-service.ts
|
|
11370
|
+
// src/report-service.ts
|
|
9647
11371
|
/**
|
|
9648
11372
|
* The Typert receiver the Gateway resolves for the DeepPilot Bridge report.
|
|
9649
11373
|
* Snapshot data stays non-secret; the token crosses the boundary only through
|
|
@@ -9689,8 +11413,7 @@ var DeepPilotReportService = class extends TypertRemoteService {
|
|
|
9689
11413
|
return this.pushTester();
|
|
9690
11414
|
}
|
|
9691
11415
|
};
|
|
9692
|
-
|
|
9693
|
-
//#region src/report-wire.ts
|
|
11416
|
+
// src/report-wire.ts
|
|
9694
11417
|
/** The npm package identity both contribution registrations claim. */
|
|
9695
11418
|
const REPORT_REMOTE_PACKAGE = "dsh-deeppilot";
|
|
9696
11419
|
/** Canonical `<namespace>/<method>` endpoint of the report Remote. */
|
|
@@ -10075,8 +11798,7 @@ const REPORT_HOST_CONTRIBUTION = {
|
|
|
10075
11798
|
}
|
|
10076
11799
|
]
|
|
10077
11800
|
};
|
|
10078
|
-
|
|
10079
|
-
//#region src/report-remote.ts
|
|
11801
|
+
// src/report-remote.ts
|
|
10080
11802
|
/**
|
|
10081
11803
|
* Provide the report service and register its Remote descriptor. Rides an
|
|
10082
11804
|
* optional `typert` inject: profiles without the web stack never activate it.
|
|
@@ -10088,8 +11810,7 @@ function applyReportRemote(ctx, snapshot, pairingStarter, deviceRevoker, deviceR
|
|
|
10088
11810
|
remoteCtx.effect(() => () => void unregister(), "dsh-deeppilot: report remote");
|
|
10089
11811
|
});
|
|
10090
11812
|
}
|
|
10091
|
-
|
|
10092
|
-
//#region src/local-address.ts
|
|
11813
|
+
// src/local-address.ts
|
|
10093
11814
|
function octetsOf(address) {
|
|
10094
11815
|
const octets = address.split(".").map(Number);
|
|
10095
11816
|
if (octets.length !== 4 || octets.some((value) => !Number.isInteger(value) || value < 0 || value > 255)) return null;
|
|
@@ -10140,8 +11861,7 @@ function localLANIPv4Addresses() {
|
|
|
10140
11861
|
candidates.sort((left, right) => priority(left.name) - priority(right.name) || left.name.localeCompare(right.name));
|
|
10141
11862
|
return [...new Set(candidates.map(({ address }) => address))];
|
|
10142
11863
|
}
|
|
10143
|
-
|
|
10144
|
-
//#region src/update-check.ts
|
|
11864
|
+
// src/update-check.ts
|
|
10145
11865
|
/**
|
|
10146
11866
|
* Lightweight self-update check for dsh-deeppilot.
|
|
10147
11867
|
*
|
|
@@ -10337,8 +12057,7 @@ var UpdateChecker = class {
|
|
|
10337
12057
|
/** No-op kept for API symmetry with the host lifecycle wiring. */
|
|
10338
12058
|
dispose() {}
|
|
10339
12059
|
};
|
|
10340
|
-
|
|
10341
|
-
//#region src/phone-http.ts
|
|
12060
|
+
// src/phone-http.ts
|
|
10342
12061
|
const CLIENT_IP_HEADER = "x-deeppilot-client-ip";
|
|
10343
12062
|
function rejectUpgrade(socket, status, reason, retryAfterSeconds) {
|
|
10344
12063
|
const body = JSON.stringify({ error: reason });
|
|
@@ -10372,8 +12091,7 @@ function requestClientIdentity(req) {
|
|
|
10372
12091
|
}
|
|
10373
12092
|
return normalizedAddress(req.socket.remoteAddress) ?? "unknown";
|
|
10374
12093
|
}
|
|
10375
|
-
|
|
10376
|
-
//#region src/lan-tls.ts
|
|
12094
|
+
// src/lan-tls.ts
|
|
10377
12095
|
const LAN_TLS_COMMON_NAME = "dsh-deeppilot";
|
|
10378
12096
|
const LAN_TLS_KEY_FILE = "key.pem";
|
|
10379
12097
|
const LAN_TLS_CERT_FILE = "cert.pem";
|
|
@@ -10546,8 +12264,7 @@ async function writePrivate(path, contents) {
|
|
|
10546
12264
|
throw error;
|
|
10547
12265
|
}
|
|
10548
12266
|
}
|
|
10549
|
-
|
|
10550
|
-
//#region src/index.ts
|
|
12267
|
+
// src/index.ts
|
|
10551
12268
|
/**
|
|
10552
12269
|
* dsh-deeppilot — data bridge between the DSH host and DeepPilot
|
|
10553
12270
|
* clients. Owns independent, narrowly routed LAN (TLS-only, pinned by paired
|
|
@@ -11184,6 +12901,20 @@ function apply(ctx, options) {
|
|
|
11184
12901
|
if (state.bridge === bridge) state.bridge = void 0;
|
|
11185
12902
|
bridge.dispose();
|
|
11186
12903
|
}, "deeppilot: host streams");
|
|
12904
|
+
apiCtx.inject(["tools", "schedule"], (sub) => {
|
|
12905
|
+
if (bridge.capabilities.schedulesHost !== true) {
|
|
12906
|
+
log("quiet completion tool not registered: host-level schedules unavailable");
|
|
12907
|
+
return;
|
|
12908
|
+
}
|
|
12909
|
+
const tools = sub.tools;
|
|
12910
|
+
if (typeof tools?.register !== "function") {
|
|
12911
|
+
log("quiet completion tool not registered: tools service unavailable", "warn");
|
|
12912
|
+
return;
|
|
12913
|
+
}
|
|
12914
|
+
sub.effect(() => registerQuietCompletionTool(tools, bridge.quietCompletion, (message) => log(message)), "deeppilot: quiet completion tool");
|
|
12915
|
+
log("quiet completion tool registered");
|
|
12916
|
+
bridge.seedQuietCompletionSessions();
|
|
12917
|
+
});
|
|
11187
12918
|
});
|
|
11188
12919
|
ctx.effect(() => async () => {
|
|
11189
12920
|
closeAllConnections();
|
|
@@ -11197,7 +12928,6 @@ function apply(ctx, options) {
|
|
|
11197
12928
|
await diagnostics.dispose();
|
|
11198
12929
|
}, "deeppilot: process resources");
|
|
11199
12930
|
}
|
|
11200
|
-
//#endregion
|
|
11201
12931
|
export { Config, HostBridge, apply, inject, mayReceivePush, name, shouldPrunePushToken, shouldReEnrollRelayToken };
|
|
11202
12932
|
|
|
11203
12933
|
//# sourceMappingURL=index.js.map
|