dsh-remote-control 1.2.0 → 2.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/bundle/client.cjs +155 -72
- package/dist/bundle/index.js +1164 -243
- package/package.json +2 -2
package/dist/bundle/index.js
CHANGED
|
@@ -4501,7 +4501,7 @@ var require_websocket = __commonJS({
|
|
|
4501
4501
|
var http = __require("http");
|
|
4502
4502
|
var net = __require("net");
|
|
4503
4503
|
var tls = __require("tls");
|
|
4504
|
-
var { randomBytes: randomBytes5, createHash:
|
|
4504
|
+
var { randomBytes: randomBytes5, createHash: createHash4 } = __require("crypto");
|
|
4505
4505
|
var { Duplex, Readable } = __require("stream");
|
|
4506
4506
|
var { URL: URL2 } = __require("url");
|
|
4507
4507
|
var PerMessageDeflate2 = require_permessage_deflate();
|
|
@@ -5182,7 +5182,7 @@ var require_websocket = __commonJS({
|
|
|
5182
5182
|
abortHandshake(websocket, socket, "Invalid Upgrade header");
|
|
5183
5183
|
return;
|
|
5184
5184
|
}
|
|
5185
|
-
const digest =
|
|
5185
|
+
const digest = createHash4("sha1").update(key + GUID).digest("base64");
|
|
5186
5186
|
if (res.headers["sec-websocket-accept"] !== digest) {
|
|
5187
5187
|
abortHandshake(websocket, socket, "Invalid Sec-WebSocket-Accept header");
|
|
5188
5188
|
return;
|
|
@@ -5551,7 +5551,7 @@ var require_websocket_server = __commonJS({
|
|
|
5551
5551
|
var EventEmitter = __require("events");
|
|
5552
5552
|
var http = __require("http");
|
|
5553
5553
|
var { Duplex } = __require("stream");
|
|
5554
|
-
var { createHash:
|
|
5554
|
+
var { createHash: createHash4 } = __require("crypto");
|
|
5555
5555
|
var extension2 = require_extension();
|
|
5556
5556
|
var PerMessageDeflate2 = require_permessage_deflate();
|
|
5557
5557
|
var subprotocol2 = require_subprotocol();
|
|
@@ -5858,7 +5858,7 @@ var require_websocket_server = __commonJS({
|
|
|
5858
5858
|
);
|
|
5859
5859
|
}
|
|
5860
5860
|
if (this._state > RUNNING) return abortHandshake(socket, 503);
|
|
5861
|
-
const digest =
|
|
5861
|
+
const digest = createHash4("sha1").update(key + GUID).digest("base64");
|
|
5862
5862
|
const headers = [
|
|
5863
5863
|
"HTTP/1.1 101 Switching Protocols",
|
|
5864
5864
|
"Upgrade: websocket",
|
|
@@ -7486,10 +7486,10 @@ var require_segments = __commonJS({
|
|
|
7486
7486
|
const segs = getSegmentsFromString(data, Utils.isKanjiModeEnabled());
|
|
7487
7487
|
const nodes = buildNodes(segs);
|
|
7488
7488
|
const graph = buildGraph(nodes, version2);
|
|
7489
|
-
const
|
|
7489
|
+
const path7 = dijkstra.find_path(graph.map, "start", "end");
|
|
7490
7490
|
const optimizedSegs = [];
|
|
7491
|
-
for (let i = 1; i <
|
|
7492
|
-
optimizedSegs.push(graph.table[
|
|
7491
|
+
for (let i = 1; i < path7.length - 1; i++) {
|
|
7492
|
+
optimizedSegs.push(graph.table[path7[i]].node);
|
|
7493
7493
|
}
|
|
7494
7494
|
return exports.fromArray(mergeSegments(optimizedSegs));
|
|
7495
7495
|
};
|
|
@@ -9925,7 +9925,7 @@ var require_utils2 = __commonJS({
|
|
|
9925
9925
|
// ../../node_modules/.pnpm/qrcode@1.5.4/node_modules/qrcode/lib/renderer/png.js
|
|
9926
9926
|
var require_png2 = __commonJS({
|
|
9927
9927
|
"../../node_modules/.pnpm/qrcode@1.5.4/node_modules/qrcode/lib/renderer/png.js"(exports) {
|
|
9928
|
-
var
|
|
9928
|
+
var fs2 = __require("fs");
|
|
9929
9929
|
var PNG = require_png().PNG;
|
|
9930
9930
|
var Utils = require_utils2();
|
|
9931
9931
|
exports.render = function render(qrData, options) {
|
|
@@ -9966,7 +9966,7 @@ var require_png2 = __commonJS({
|
|
|
9966
9966
|
});
|
|
9967
9967
|
png.pack();
|
|
9968
9968
|
};
|
|
9969
|
-
exports.renderToFile = function renderToFile(
|
|
9969
|
+
exports.renderToFile = function renderToFile(path7, qrData, options, cb) {
|
|
9970
9970
|
if (typeof cb === "undefined") {
|
|
9971
9971
|
cb = options;
|
|
9972
9972
|
options = void 0;
|
|
@@ -9977,7 +9977,7 @@ var require_png2 = __commonJS({
|
|
|
9977
9977
|
called = true;
|
|
9978
9978
|
cb.apply(null, args);
|
|
9979
9979
|
};
|
|
9980
|
-
const stream =
|
|
9980
|
+
const stream = fs2.createWriteStream(path7);
|
|
9981
9981
|
stream.on("error", done);
|
|
9982
9982
|
stream.on("close", done);
|
|
9983
9983
|
exports.renderToFileStream(stream, qrData, options);
|
|
@@ -10039,14 +10039,14 @@ var require_utf8 = __commonJS({
|
|
|
10039
10039
|
}
|
|
10040
10040
|
return output2;
|
|
10041
10041
|
};
|
|
10042
|
-
exports.renderToFile = function renderToFile(
|
|
10042
|
+
exports.renderToFile = function renderToFile(path7, qrData, options, cb) {
|
|
10043
10043
|
if (typeof cb === "undefined") {
|
|
10044
10044
|
cb = options;
|
|
10045
10045
|
options = void 0;
|
|
10046
10046
|
}
|
|
10047
|
-
const
|
|
10047
|
+
const fs2 = __require("fs");
|
|
10048
10048
|
const utf82 = exports.render(qrData, options);
|
|
10049
|
-
|
|
10049
|
+
fs2.writeFile(path7, utf82, cb);
|
|
10050
10050
|
};
|
|
10051
10051
|
}
|
|
10052
10052
|
});
|
|
@@ -10167,7 +10167,7 @@ var require_svg_tag = __commonJS({
|
|
|
10167
10167
|
return str;
|
|
10168
10168
|
}
|
|
10169
10169
|
function qrToPath(data, size, margin) {
|
|
10170
|
-
let
|
|
10170
|
+
let path7 = "";
|
|
10171
10171
|
let moveBy = 0;
|
|
10172
10172
|
let newRow = false;
|
|
10173
10173
|
let lineLength = 0;
|
|
@@ -10178,19 +10178,19 @@ var require_svg_tag = __commonJS({
|
|
|
10178
10178
|
if (data[i]) {
|
|
10179
10179
|
lineLength++;
|
|
10180
10180
|
if (!(i > 0 && col > 0 && data[i - 1])) {
|
|
10181
|
-
|
|
10181
|
+
path7 += newRow ? svgCmd("M", col + margin, 0.5 + row + margin) : svgCmd("m", moveBy, 0);
|
|
10182
10182
|
moveBy = 0;
|
|
10183
10183
|
newRow = false;
|
|
10184
10184
|
}
|
|
10185
10185
|
if (!(col + 1 < size && data[i + 1])) {
|
|
10186
|
-
|
|
10186
|
+
path7 += svgCmd("h", lineLength);
|
|
10187
10187
|
lineLength = 0;
|
|
10188
10188
|
}
|
|
10189
10189
|
} else {
|
|
10190
10190
|
moveBy++;
|
|
10191
10191
|
}
|
|
10192
10192
|
}
|
|
10193
|
-
return
|
|
10193
|
+
return path7;
|
|
10194
10194
|
}
|
|
10195
10195
|
exports.render = function render(qrData, options, cb) {
|
|
10196
10196
|
const opts = Utils.getOptions(options);
|
|
@@ -10198,10 +10198,10 @@ var require_svg_tag = __commonJS({
|
|
|
10198
10198
|
const data = qrData.modules.data;
|
|
10199
10199
|
const qrcodesize = size + opts.margin * 2;
|
|
10200
10200
|
const bg = !opts.color.light.a ? "" : "<path " + getColorAttrib(opts.color.light, "fill") + ' d="M0 0h' + qrcodesize + "v" + qrcodesize + 'H0z"/>';
|
|
10201
|
-
const
|
|
10201
|
+
const path7 = "<path " + getColorAttrib(opts.color.dark, "stroke") + ' d="' + qrToPath(data, size, opts.margin) + '"/>';
|
|
10202
10202
|
const viewBox = 'viewBox="0 0 ' + qrcodesize + " " + qrcodesize + '"';
|
|
10203
10203
|
const width = !opts.width ? "" : 'width="' + opts.width + '" height="' + opts.width + '" ';
|
|
10204
|
-
const svgTag = '<svg xmlns="http://www.w3.org/2000/svg" ' + width + viewBox + ' shape-rendering="crispEdges">' + bg +
|
|
10204
|
+
const svgTag = '<svg xmlns="http://www.w3.org/2000/svg" ' + width + viewBox + ' shape-rendering="crispEdges">' + bg + path7 + "</svg>\n";
|
|
10205
10205
|
if (typeof cb === "function") {
|
|
10206
10206
|
cb(null, svgTag);
|
|
10207
10207
|
}
|
|
@@ -10215,15 +10215,15 @@ var require_svg = __commonJS({
|
|
|
10215
10215
|
"../../node_modules/.pnpm/qrcode@1.5.4/node_modules/qrcode/lib/renderer/svg.js"(exports) {
|
|
10216
10216
|
var svgTagRenderer = require_svg_tag();
|
|
10217
10217
|
exports.render = svgTagRenderer.render;
|
|
10218
|
-
exports.renderToFile = function renderToFile(
|
|
10218
|
+
exports.renderToFile = function renderToFile(path7, qrData, options, cb) {
|
|
10219
10219
|
if (typeof cb === "undefined") {
|
|
10220
10220
|
cb = options;
|
|
10221
10221
|
options = void 0;
|
|
10222
10222
|
}
|
|
10223
|
-
const
|
|
10223
|
+
const fs2 = __require("fs");
|
|
10224
10224
|
const svgTag = exports.render(qrData, options);
|
|
10225
10225
|
const xmlStr = '<?xml version="1.0" encoding="utf-8"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">' + svgTag;
|
|
10226
|
-
|
|
10226
|
+
fs2.writeFile(path7, xmlStr, cb);
|
|
10227
10227
|
};
|
|
10228
10228
|
}
|
|
10229
10229
|
});
|
|
@@ -10381,8 +10381,8 @@ var require_server = __commonJS({
|
|
|
10381
10381
|
cb
|
|
10382
10382
|
};
|
|
10383
10383
|
}
|
|
10384
|
-
function getTypeFromFilename(
|
|
10385
|
-
return
|
|
10384
|
+
function getTypeFromFilename(path7) {
|
|
10385
|
+
return path7.slice((path7.lastIndexOf(".") - 1 >>> 0) + 2).toLowerCase();
|
|
10386
10386
|
}
|
|
10387
10387
|
function getRendererFromType(type) {
|
|
10388
10388
|
switch (type) {
|
|
@@ -10446,17 +10446,17 @@ var require_server = __commonJS({
|
|
|
10446
10446
|
const renderer = getRendererFromType(params.opts.type);
|
|
10447
10447
|
return render(renderer.renderToBuffer, text, params);
|
|
10448
10448
|
};
|
|
10449
|
-
exports.toFile = function toFile(
|
|
10450
|
-
if (typeof
|
|
10449
|
+
exports.toFile = function toFile(path7, text, opts, cb) {
|
|
10450
|
+
if (typeof path7 !== "string" || !(typeof text === "string" || typeof text === "object")) {
|
|
10451
10451
|
throw new Error("Invalid argument");
|
|
10452
10452
|
}
|
|
10453
10453
|
if (arguments.length < 3 && !canPromise()) {
|
|
10454
10454
|
throw new Error("Too few arguments provided");
|
|
10455
10455
|
}
|
|
10456
10456
|
const params = checkParams(text, opts, cb);
|
|
10457
|
-
const type = params.opts.type || getTypeFromFilename(
|
|
10457
|
+
const type = params.opts.type || getTypeFromFilename(path7);
|
|
10458
10458
|
const renderer = getRendererFromType(type);
|
|
10459
|
-
const renderToFile = renderer.renderToFile.bind(null,
|
|
10459
|
+
const renderToFile = renderer.renderToFile.bind(null, path7);
|
|
10460
10460
|
return render(renderToFile, text, params);
|
|
10461
10461
|
};
|
|
10462
10462
|
exports.toFileStream = function toFileStream(stream, text, opts) {
|
|
@@ -10655,9 +10655,9 @@ var init_mock_kernel = __esm({
|
|
|
10655
10655
|
});
|
|
10656
10656
|
|
|
10657
10657
|
// src/index.ts
|
|
10658
|
-
import
|
|
10658
|
+
import path6 from "node:path";
|
|
10659
10659
|
|
|
10660
|
-
// ../../node_modules/.pnpm/dsh-remote-wire@1.
|
|
10660
|
+
// ../../node_modules/.pnpm/dsh-remote-wire@1.3.0/node_modules/dsh-remote-wire/dist/src/crypto/bytes.js
|
|
10661
10661
|
import { Buffer as Buffer2 } from "node:buffer";
|
|
10662
10662
|
var encoder = new TextEncoder();
|
|
10663
10663
|
var decoder = new TextDecoder("utf-8", { fatal: false });
|
|
@@ -10684,7 +10684,7 @@ function concat(parts) {
|
|
|
10684
10684
|
return out;
|
|
10685
10685
|
}
|
|
10686
10686
|
|
|
10687
|
-
// ../../node_modules/.pnpm/dsh-remote-wire@1.
|
|
10687
|
+
// ../../node_modules/.pnpm/dsh-remote-wire@1.3.0/node_modules/dsh-remote-wire/dist/src/crypto/keys.js
|
|
10688
10688
|
import { createHash, randomBytes } from "node:crypto";
|
|
10689
10689
|
var KDF_NAMESPACE = "dsh-rc/v1";
|
|
10690
10690
|
var UNIT_SEPARATOR = 31;
|
|
@@ -10714,7 +10714,7 @@ function randomNonce() {
|
|
|
10714
10714
|
return new Uint8Array(randomBytes(NONCE_BYTES));
|
|
10715
10715
|
}
|
|
10716
10716
|
|
|
10717
|
-
// ../../node_modules/.pnpm/dsh-remote-wire@1.
|
|
10717
|
+
// ../../node_modules/.pnpm/dsh-remote-wire@1.3.0/node_modules/dsh-remote-wire/dist/src/crypto/record.js
|
|
10718
10718
|
var import_tweetnacl = __toESM(require_nacl_fast(), 1);
|
|
10719
10719
|
var MAC_BYTES = 16;
|
|
10720
10720
|
var MIN_RECORD_BYTES = NONCE_BYTES + MAC_BYTES;
|
|
@@ -10747,7 +10747,7 @@ function open(key, record2) {
|
|
|
10747
10747
|
}
|
|
10748
10748
|
}
|
|
10749
10749
|
|
|
10750
|
-
// ../../node_modules/.pnpm/dsh-remote-wire@1.
|
|
10750
|
+
// ../../node_modules/.pnpm/dsh-remote-wire@1.3.0/node_modules/dsh-remote-wire/dist/src/identity/pairing.js
|
|
10751
10751
|
import { randomBytes as randomBytes2 } from "node:crypto";
|
|
10752
10752
|
var URI_PREFIX = "dshr:";
|
|
10753
10753
|
var TOKEN_RE = /^\d{6}$/;
|
|
@@ -11593,10 +11593,10 @@ function mergeDefs(...defs) {
|
|
|
11593
11593
|
function cloneDef(schema) {
|
|
11594
11594
|
return mergeDefs(schema._zod.def);
|
|
11595
11595
|
}
|
|
11596
|
-
function getElementAtPath(obj,
|
|
11597
|
-
if (!
|
|
11596
|
+
function getElementAtPath(obj, path7) {
|
|
11597
|
+
if (!path7)
|
|
11598
11598
|
return obj;
|
|
11599
|
-
return
|
|
11599
|
+
return path7.reduce((acc, key) => acc?.[key], obj);
|
|
11600
11600
|
}
|
|
11601
11601
|
function promiseAllObject(promisesObj) {
|
|
11602
11602
|
const keys = Object.keys(promisesObj);
|
|
@@ -11936,11 +11936,11 @@ function explicitlyAborted(x, startIndex = 0) {
|
|
|
11936
11936
|
}
|
|
11937
11937
|
return false;
|
|
11938
11938
|
}
|
|
11939
|
-
function prefixIssues(
|
|
11939
|
+
function prefixIssues(path7, issues) {
|
|
11940
11940
|
return issues.map((iss) => {
|
|
11941
11941
|
var _a3;
|
|
11942
11942
|
(_a3 = iss).path ?? (_a3.path = []);
|
|
11943
|
-
iss.path.unshift(
|
|
11943
|
+
iss.path.unshift(path7);
|
|
11944
11944
|
return iss;
|
|
11945
11945
|
});
|
|
11946
11946
|
}
|
|
@@ -12390,16 +12390,16 @@ function flattenError(error62, mapper = (issue2) => issue2.message) {
|
|
|
12390
12390
|
}
|
|
12391
12391
|
function formatError(error62, mapper = (issue2) => issue2.message) {
|
|
12392
12392
|
const fieldErrors = { _errors: [] };
|
|
12393
|
-
const processError = (error63,
|
|
12393
|
+
const processError = (error63, path7 = []) => {
|
|
12394
12394
|
for (const issue2 of error63.issues) {
|
|
12395
12395
|
if (issue2.code === "invalid_union" && issue2.errors.length) {
|
|
12396
|
-
issue2.errors.map((issues) => processError({ issues }, [...
|
|
12396
|
+
issue2.errors.map((issues) => processError({ issues }, [...path7, ...issue2.path]));
|
|
12397
12397
|
} else if (issue2.code === "invalid_key") {
|
|
12398
|
-
processError({ issues: issue2.issues }, [...
|
|
12398
|
+
processError({ issues: issue2.issues }, [...path7, ...issue2.path]);
|
|
12399
12399
|
} else if (issue2.code === "invalid_element") {
|
|
12400
|
-
processError({ issues: issue2.issues }, [...
|
|
12400
|
+
processError({ issues: issue2.issues }, [...path7, ...issue2.path]);
|
|
12401
12401
|
} else {
|
|
12402
|
-
const fullpath = [...
|
|
12402
|
+
const fullpath = [...path7, ...issue2.path];
|
|
12403
12403
|
if (fullpath.length === 0) {
|
|
12404
12404
|
fieldErrors._errors.push(mapper(issue2));
|
|
12405
12405
|
} else {
|
|
@@ -12438,17 +12438,17 @@ function formatError(error62, mapper = (issue2) => issue2.message) {
|
|
|
12438
12438
|
}
|
|
12439
12439
|
function treeifyError(error62, mapper = (issue2) => issue2.message) {
|
|
12440
12440
|
const result2 = { errors: [] };
|
|
12441
|
-
const processError = (error63,
|
|
12441
|
+
const processError = (error63, path7 = []) => {
|
|
12442
12442
|
var _a3;
|
|
12443
12443
|
for (const issue2 of error63.issues) {
|
|
12444
12444
|
if (issue2.code === "invalid_union" && issue2.errors.length) {
|
|
12445
|
-
issue2.errors.map((issues) => processError({ issues }, [...
|
|
12445
|
+
issue2.errors.map((issues) => processError({ issues }, [...path7, ...issue2.path]));
|
|
12446
12446
|
} else if (issue2.code === "invalid_key") {
|
|
12447
|
-
processError({ issues: issue2.issues }, [...
|
|
12447
|
+
processError({ issues: issue2.issues }, [...path7, ...issue2.path]);
|
|
12448
12448
|
} else if (issue2.code === "invalid_element") {
|
|
12449
|
-
processError({ issues: issue2.issues }, [...
|
|
12449
|
+
processError({ issues: issue2.issues }, [...path7, ...issue2.path]);
|
|
12450
12450
|
} else {
|
|
12451
|
-
const fullpath = [...
|
|
12451
|
+
const fullpath = [...path7, ...issue2.path];
|
|
12452
12452
|
if (fullpath.length === 0) {
|
|
12453
12453
|
result2.errors.push(mapper(issue2));
|
|
12454
12454
|
continue;
|
|
@@ -12487,8 +12487,8 @@ function treeifyError(error62, mapper = (issue2) => issue2.message) {
|
|
|
12487
12487
|
}
|
|
12488
12488
|
function toDotPath(_path) {
|
|
12489
12489
|
const segs = [];
|
|
12490
|
-
const
|
|
12491
|
-
for (const seg of
|
|
12490
|
+
const path7 = _path.map((seg) => typeof seg === "object" ? seg.key : seg);
|
|
12491
|
+
for (const seg of path7) {
|
|
12492
12492
|
if (typeof seg === "number")
|
|
12493
12493
|
segs.push(`[${seg}]`);
|
|
12494
12494
|
else if (typeof seg === "symbol")
|
|
@@ -29590,13 +29590,13 @@ function resolveRef(ref, ctx) {
|
|
|
29590
29590
|
if (!ref.startsWith("#")) {
|
|
29591
29591
|
throw new Error("External $ref is not supported, only local refs (#/...) are allowed");
|
|
29592
29592
|
}
|
|
29593
|
-
const
|
|
29594
|
-
if (
|
|
29593
|
+
const path7 = ref.slice(1).split("/").filter(Boolean);
|
|
29594
|
+
if (path7.length === 0) {
|
|
29595
29595
|
return ctx.rootSchema;
|
|
29596
29596
|
}
|
|
29597
29597
|
const defsKey = ctx.version === "draft-2020-12" ? "$defs" : "definitions";
|
|
29598
|
-
if (
|
|
29599
|
-
const key =
|
|
29598
|
+
if (path7[0] === defsKey) {
|
|
29599
|
+
const key = path7[1] === void 0 ? void 0 : decodeJSONPointerSegment(path7[1]);
|
|
29600
29600
|
if (!key || !ctx.defs[key]) {
|
|
29601
29601
|
throw new Error(`Reference not found: ${ref}`);
|
|
29602
29602
|
}
|
|
@@ -30445,7 +30445,7 @@ function date4(params) {
|
|
|
30445
30445
|
return _coercedDate(ZodDate, params);
|
|
30446
30446
|
}
|
|
30447
30447
|
|
|
30448
|
-
// ../../node_modules/.pnpm/dsh-remote-wire@1.
|
|
30448
|
+
// ../../node_modules/.pnpm/dsh-remote-wire@1.3.0/node_modules/dsh-remote-wire/dist/src/wire/frames.js
|
|
30449
30449
|
var PAIRING_TOKEN_RE = /^\d{6}$/;
|
|
30450
30450
|
var nonEmpty = external_exports.string().min(1);
|
|
30451
30451
|
var pairingToken = external_exports.string().regex(PAIRING_TOKEN_RE);
|
|
@@ -30468,8 +30468,7 @@ var helloFrame = external_exports.object({
|
|
|
30468
30468
|
});
|
|
30469
30469
|
var pairBeginFrame = external_exports.object({
|
|
30470
30470
|
t: external_exports.literal("pair-begin"),
|
|
30471
|
-
pairingToken
|
|
30472
|
-
hostLabel: external_exports.string().max(128).optional()
|
|
30471
|
+
pairingToken
|
|
30473
30472
|
});
|
|
30474
30473
|
var pairClaimFrame = external_exports.object({
|
|
30475
30474
|
t: external_exports.literal("pair-begin-client"),
|
|
@@ -30605,7 +30604,7 @@ function parseRelayFrameText(raw) {
|
|
|
30605
30604
|
}
|
|
30606
30605
|
}
|
|
30607
30606
|
|
|
30608
|
-
// ../../node_modules/.pnpm/dsh-remote-wire@1.
|
|
30607
|
+
// ../../node_modules/.pnpm/dsh-remote-wire@1.3.0/node_modules/dsh-remote-wire/dist/src/wire/payloads.js
|
|
30609
30608
|
var SESSION_STATES = [
|
|
30610
30609
|
"idle",
|
|
30611
30610
|
"running",
|
|
@@ -30670,6 +30669,13 @@ var evPermissionRequest = external_exports.object({
|
|
|
30670
30669
|
options: external_exports.array(choiceOption).optional(),
|
|
30671
30670
|
expiresAt: external_exports.string().optional()
|
|
30672
30671
|
});
|
|
30672
|
+
var evPermissionResolved = external_exports.object({
|
|
30673
|
+
t: external_exports.literal("ev.permission_resolved"),
|
|
30674
|
+
requestId: nonEmpty2,
|
|
30675
|
+
sessionId: nonEmpty2.optional(),
|
|
30676
|
+
/** 谁收的场。手机只按"要不要收掉这张卡"读它,所以不做必填。 */
|
|
30677
|
+
by: external_exports.enum(["desktop", "cancelled"]).optional()
|
|
30678
|
+
});
|
|
30673
30679
|
var questionItem = external_exports.object({
|
|
30674
30680
|
id: nonEmpty2,
|
|
30675
30681
|
question: nonEmpty2,
|
|
@@ -30680,7 +30686,23 @@ var evQuestionRequest = external_exports.object({
|
|
|
30680
30686
|
t: external_exports.literal("ev.question_request"),
|
|
30681
30687
|
sessionId: nonEmpty2.optional(),
|
|
30682
30688
|
requestId: nonEmpty2,
|
|
30683
|
-
questions: external_exports.array(questionItem)
|
|
30689
|
+
questions: external_exports.array(questionItem),
|
|
30690
|
+
/**
|
|
30691
|
+
* 这张提问卡什么时候作废(ISO 字符串)。
|
|
30692
|
+
*
|
|
30693
|
+
* 为什么审批有而提问没有过:主机侧的提问等待本来就带超时(`questionTimeoutMs`,默认 300s),
|
|
30694
|
+
* 但手机上那张卡**没有任何倒计时**——用户看不见自己按的按钮什么时候失效,
|
|
30695
|
+
* 而这违反伞仓 `docs/PRODUCT.md` §3 第 3 条「超时必须可见」。
|
|
30696
|
+
* 与审批那条同一个形状:可选、ISO 字符串、由主机每次发问时现算。
|
|
30697
|
+
*/
|
|
30698
|
+
expiresAt: external_exports.string().optional()
|
|
30699
|
+
});
|
|
30700
|
+
var evQuestionResolved = external_exports.object({
|
|
30701
|
+
t: external_exports.literal("ev.question_resolved"),
|
|
30702
|
+
requestId: nonEmpty2,
|
|
30703
|
+
sessionId: nonEmpty2.optional(),
|
|
30704
|
+
/** 谁收的场。手机只按"要不要收掉这张卡"读它,所以不做必填。 */
|
|
30705
|
+
by: external_exports.enum(["desktop", "cancelled"]).optional()
|
|
30684
30706
|
});
|
|
30685
30707
|
var evRunState = external_exports.object({
|
|
30686
30708
|
t: external_exports.literal("ev.run_state"),
|
|
@@ -30697,6 +30719,24 @@ var evKeepAwakeState = external_exports.object({
|
|
|
30697
30719
|
backend: nonEmpty2,
|
|
30698
30720
|
reason: external_exports.string().optional()
|
|
30699
30721
|
});
|
|
30722
|
+
var modelOption = external_exports.object({
|
|
30723
|
+
/** 机器名(回传用),例如 `deepseek-chat`。 */
|
|
30724
|
+
value: nonEmpty2,
|
|
30725
|
+
/** 给人看的名字;缺省时小程序自己截断 `value`。 */
|
|
30726
|
+
label: external_exports.string().optional(),
|
|
30727
|
+
provider: external_exports.string().optional()
|
|
30728
|
+
});
|
|
30729
|
+
var evModel = external_exports.object({
|
|
30730
|
+
t: external_exports.literal("ev.model"),
|
|
30731
|
+
provider: nonEmpty2.optional(),
|
|
30732
|
+
model: nonEmpty2,
|
|
30733
|
+
/** 主机能不能换。false 时小程序**不许**渲染成可点的下拉。 */
|
|
30734
|
+
canSwitch: external_exports.boolean(),
|
|
30735
|
+
/** 候选清单;`canSwitch` 为 true 时至少一项。 */
|
|
30736
|
+
options: external_exports.array(modelOption).optional(),
|
|
30737
|
+
/** 不能切时给手机一句人话,直接显示,不要让小程序自己编措辞。 */
|
|
30738
|
+
reason: external_exports.string().optional()
|
|
30739
|
+
});
|
|
30700
30740
|
var evResult = external_exports.object({
|
|
30701
30741
|
t: external_exports.literal("ev.result"),
|
|
30702
30742
|
cmdId: external_exports.string(),
|
|
@@ -30718,18 +30758,37 @@ var evPayload = external_exports.discriminatedUnion("t", [
|
|
|
30718
30758
|
evMessageDelta,
|
|
30719
30759
|
evToolEvent,
|
|
30720
30760
|
evPermissionRequest,
|
|
30761
|
+
evPermissionResolved,
|
|
30721
30762
|
evQuestionRequest,
|
|
30763
|
+
evQuestionResolved,
|
|
30722
30764
|
evRunState,
|
|
30723
30765
|
evKeepAwakeState,
|
|
30766
|
+
evModel,
|
|
30724
30767
|
evResult,
|
|
30725
30768
|
evSessionHistory
|
|
30726
30769
|
]);
|
|
30770
|
+
var imageAttachment = external_exports.object({
|
|
30771
|
+
/** 落盘用的文件名(mp 侧生成,形如 `img-<ms>-<n>.jpg`;主机侧还会再收敛一次字符集)。 */
|
|
30772
|
+
name: nonEmpty2,
|
|
30773
|
+
mediaType: external_exports.literal("image/jpeg"),
|
|
30774
|
+
/** base64 字节,**不带** `data:` 前缀。 */
|
|
30775
|
+
data: nonEmpty2,
|
|
30776
|
+
/** 压缩后的像素尺寸,主机写旁车元数据用(排错时对得上)。 */
|
|
30777
|
+
width: external_exports.number().int().positive().optional(),
|
|
30778
|
+
height: external_exports.number().int().positive().optional()
|
|
30779
|
+
});
|
|
30727
30780
|
var cmdSendPrompt = external_exports.object({
|
|
30728
30781
|
t: external_exports.literal("cmd.send_prompt"),
|
|
30729
30782
|
cmdId: nonEmpty2,
|
|
30730
30783
|
/** DSH 会话 id,不是配对通道 id。 */
|
|
30731
30784
|
sessionId: nonEmpty2,
|
|
30732
|
-
text: external_exports.string()
|
|
30785
|
+
text: external_exports.string(),
|
|
30786
|
+
/**
|
|
30787
|
+
* 随消息的图片附件。**可选**:老版本手机不带这个字段,老主机看到也不认
|
|
30788
|
+
* (zod 默认 strip 未知键,而 text 照旧)——所以这是向后兼容的增补,不升协议版本。
|
|
30789
|
+
* 上限 4 张:再多对"看清楚这张图"没有帮助,只是把帧撑爆。
|
|
30790
|
+
*/
|
|
30791
|
+
images: external_exports.array(imageAttachment).max(4).optional()
|
|
30733
30792
|
});
|
|
30734
30793
|
var answerItem = external_exports.object({
|
|
30735
30794
|
questionId: nonEmpty2,
|
|
@@ -30808,14 +30867,17 @@ var PAYLOAD_TYPES = {
|
|
|
30808
30867
|
evMessageDelta: "ev.message_delta",
|
|
30809
30868
|
evToolEvent: "ev.tool_event",
|
|
30810
30869
|
evPermissionRequest: "ev.permission_request",
|
|
30870
|
+
evPermissionResolved: "ev.permission_resolved",
|
|
30811
30871
|
evQuestionRequest: "ev.question_request",
|
|
30872
|
+
evQuestionResolved: "ev.question_resolved",
|
|
30812
30873
|
evRunState: "ev.run_state",
|
|
30813
30874
|
evKeepAwakeState: "ev.keep_awake_state",
|
|
30875
|
+
evModel: "ev.model",
|
|
30814
30876
|
evResult: "ev.result",
|
|
30815
30877
|
evSessionHistory: "ev.session_history"
|
|
30816
30878
|
};
|
|
30817
30879
|
|
|
30818
|
-
// ../../node_modules/.pnpm/dsh-remote-wire@1.
|
|
30880
|
+
// ../../node_modules/.pnpm/dsh-remote-wire@1.3.0/node_modules/dsh-remote-wire/dist/src/wire/outbound.js
|
|
30819
30881
|
function sessionChanged(sessions, reason) {
|
|
30820
30882
|
return { t: PAYLOAD_TYPES.evSessionChanged, sessions, ...reason === void 0 ? {} : { reason } };
|
|
30821
30883
|
}
|
|
@@ -30828,15 +30890,31 @@ function toolEvent(args) {
|
|
|
30828
30890
|
function permissionRequest(args) {
|
|
30829
30891
|
return { t: PAYLOAD_TYPES.evPermissionRequest, ...args };
|
|
30830
30892
|
}
|
|
30893
|
+
function permissionResolved(args) {
|
|
30894
|
+
return { t: PAYLOAD_TYPES.evPermissionResolved, ...args };
|
|
30895
|
+
}
|
|
30831
30896
|
function questionRequest(args) {
|
|
30832
30897
|
return { t: PAYLOAD_TYPES.evQuestionRequest, ...args };
|
|
30833
30898
|
}
|
|
30899
|
+
function questionResolved(args) {
|
|
30900
|
+
return { t: PAYLOAD_TYPES.evQuestionResolved, ...args };
|
|
30901
|
+
}
|
|
30834
30902
|
function runState(args) {
|
|
30835
30903
|
return { t: PAYLOAD_TYPES.evRunState, ...args };
|
|
30836
30904
|
}
|
|
30837
30905
|
function keepAwakeState(args) {
|
|
30838
30906
|
return { t: PAYLOAD_TYPES.evKeepAwakeState, ...args };
|
|
30839
30907
|
}
|
|
30908
|
+
function model(args) {
|
|
30909
|
+
const payload = { t: PAYLOAD_TYPES.evModel, model: args.model, canSwitch: args.canSwitch };
|
|
30910
|
+
if (args.provider !== void 0)
|
|
30911
|
+
payload.provider = args.provider;
|
|
30912
|
+
if (args.canSwitch && args.options?.length)
|
|
30913
|
+
payload.options = args.options;
|
|
30914
|
+
if (args.reason !== void 0)
|
|
30915
|
+
payload.reason = args.reason;
|
|
30916
|
+
return payload;
|
|
30917
|
+
}
|
|
30840
30918
|
function result(cmdId, ok, extras = {}) {
|
|
30841
30919
|
const { message, data } = extras;
|
|
30842
30920
|
return {
|
|
@@ -30858,7 +30936,7 @@ function sessionHistory(args) {
|
|
|
30858
30936
|
};
|
|
30859
30937
|
}
|
|
30860
30938
|
|
|
30861
|
-
// ../../node_modules/.pnpm/dsh-remote-wire@1.
|
|
30939
|
+
// ../../node_modules/.pnpm/dsh-remote-wire@1.3.0/node_modules/dsh-remote-wire/dist/src/platform/sleep.js
|
|
30862
30940
|
function detectBackend(platform) {
|
|
30863
30941
|
if (platform === "darwin")
|
|
30864
30942
|
return "caffeinate";
|
|
@@ -30899,6 +30977,81 @@ function buildSleepCommand(platform, ownerPid, keepDisplay = false) {
|
|
|
30899
30977
|
// src/core/runtime.ts
|
|
30900
30978
|
import { randomUUID } from "node:crypto";
|
|
30901
30979
|
|
|
30980
|
+
// src/shell/uploads.ts
|
|
30981
|
+
import fs from "node:fs";
|
|
30982
|
+
import path from "node:path";
|
|
30983
|
+
function safeSegment(raw, fallback, maxLen = 96) {
|
|
30984
|
+
const cleaned = String(raw || "").replace(/[^A-Za-z0-9._-]+/g, "_").replace(/^[._-]+/, "").slice(0, maxLen).replace(/[._-]+$/, "");
|
|
30985
|
+
return cleaned === "" ? fallback : cleaned;
|
|
30986
|
+
}
|
|
30987
|
+
function decodeBase64(data) {
|
|
30988
|
+
if (!/^[A-Za-z0-9+/=\s]+$/.test(data)) return null;
|
|
30989
|
+
const buf = Buffer.from(data, "base64");
|
|
30990
|
+
return buf.length > 0 ? buf : null;
|
|
30991
|
+
}
|
|
30992
|
+
function looksLikeJpeg(buf) {
|
|
30993
|
+
return buf.length > 3 && buf[0] === 255 && buf[1] === 216 && buf[2] === 255;
|
|
30994
|
+
}
|
|
30995
|
+
function saveImageAttachments(options) {
|
|
30996
|
+
const maxCount = options.maxCount ?? 4;
|
|
30997
|
+
const maxBytes = options.maxBytesPerImage ?? 4 * 1024 * 1024;
|
|
30998
|
+
const images = options.images || [];
|
|
30999
|
+
if (images.length === 0) return { ok: true, saved: [], dir: options.dir };
|
|
31000
|
+
if (images.length > maxCount) {
|
|
31001
|
+
return { ok: false, message: `\u4E00\u6B21\u6700\u591A\u5E26 ${maxCount} \u5F20\u56FE\u7247\uFF0C\u6536\u5230 ${images.length} \u5F20` };
|
|
31002
|
+
}
|
|
31003
|
+
const decoded = [];
|
|
31004
|
+
for (const [i, img] of images.entries()) {
|
|
31005
|
+
if (img.mediaType !== "image/jpeg") {
|
|
31006
|
+
return { ok: false, message: `\u7B2C ${i + 1} \u5F20\u4E0D\u662F jpeg\uFF08${img.mediaType || "\u7A7A"}\uFF09\uFF0C\u4E3B\u673A\u53EA\u6536 jpeg` };
|
|
31007
|
+
}
|
|
31008
|
+
const buf = decodeBase64(img.data);
|
|
31009
|
+
if (!buf) return { ok: false, message: `\u7B2C ${i + 1} \u5F20\u7684\u89E3\u7801\u5931\u8D25\uFF08\u4E0D\u662F\u5408\u6CD5 base64\uFF09` };
|
|
31010
|
+
if (buf.length > maxBytes) {
|
|
31011
|
+
return {
|
|
31012
|
+
ok: false,
|
|
31013
|
+
message: `\u7B2C ${i + 1} \u5F20\u6709 ${Math.round(buf.length / 1024)}KB\uFF0C\u8D85\u8FC7\u5355\u5F20\u4E0A\u9650 ${Math.round(maxBytes / 1024)}KB`
|
|
31014
|
+
};
|
|
31015
|
+
}
|
|
31016
|
+
if (!looksLikeJpeg(buf)) {
|
|
31017
|
+
return { ok: false, message: `\u7B2C ${i + 1} \u5F20\u7684\u5185\u5BB9\u4E0D\u662F jpeg\uFF08\u6587\u4EF6\u5934\u5BF9\u4E0D\u4E0A\uFF09` };
|
|
31018
|
+
}
|
|
31019
|
+
decoded.push({ img, buf });
|
|
31020
|
+
}
|
|
31021
|
+
const sessionDir = path.join(options.dir, safeSegment(options.sessionId, "session"));
|
|
31022
|
+
const written = [];
|
|
31023
|
+
try {
|
|
31024
|
+
fs.mkdirSync(sessionDir, { recursive: true });
|
|
31025
|
+
for (const [i, one] of decoded.entries()) {
|
|
31026
|
+
const base = safeSegment(one.img.name, `img-${i + 1}.jpg`).replace(/\.jpg$/i, "") + ".jpg";
|
|
31027
|
+
let name = base;
|
|
31028
|
+
let n = 1;
|
|
31029
|
+
while (fs.existsSync(path.join(sessionDir, name))) {
|
|
31030
|
+
name = base.replace(/\.jpg$/, `-${++n}.jpg`);
|
|
31031
|
+
}
|
|
31032
|
+
const file2 = path.join(sessionDir, name);
|
|
31033
|
+
fs.writeFileSync(file2, one.buf, { mode: 384 });
|
|
31034
|
+
written.push({ name, path: file2, bytes: one.buf.length });
|
|
31035
|
+
}
|
|
31036
|
+
} catch (error62) {
|
|
31037
|
+
return {
|
|
31038
|
+
ok: false,
|
|
31039
|
+
message: `\u56FE\u7247\u843D\u76D8\u5931\u8D25\uFF1A${String(error62?.message ?? error62).slice(0, 160)}`
|
|
31040
|
+
};
|
|
31041
|
+
}
|
|
31042
|
+
return { ok: true, saved: written, dir: sessionDir };
|
|
31043
|
+
}
|
|
31044
|
+
function appendImageNote(text, saved) {
|
|
31045
|
+
if (saved.length === 0) return text;
|
|
31046
|
+
const lines = saved.map((img, i) => `${i + 1}. ${img.path}`);
|
|
31047
|
+
const note = `
|
|
31048
|
+
|
|
31049
|
+
[\u56FE\u7247\u9644\u4EF6 ${saved.length} \u5F20\uFF0C\u5DF2\u5B58\u5230\u672C\u673A]
|
|
31050
|
+
${lines.join("\n")}`;
|
|
31051
|
+
const head = String(text || "");
|
|
31052
|
+
return head === "" ? note.replace(/^\n\n/, "") : head + note;
|
|
31053
|
+
}
|
|
31054
|
+
|
|
30902
31055
|
// src/core/window.ts
|
|
30903
31056
|
var DEFAULT_WINDOW_OPTIONS = {
|
|
30904
31057
|
windowMs: 120,
|
|
@@ -31069,7 +31222,11 @@ var DEFAULTS = {
|
|
|
31069
31222
|
listingRefreshMs: 15e3,
|
|
31070
31223
|
approvalTimeoutMs: 18e4,
|
|
31071
31224
|
questionTimeoutMs: 3e5,
|
|
31072
|
-
unarchiveOnPrompt: true
|
|
31225
|
+
unarchiveOnPrompt: true,
|
|
31226
|
+
// 图片附件:默认**不收**(uploadDir 空 = 拒收)。要放开必须在插件配置里显式
|
|
31227
|
+
// 给一个目录——落盘是往用户磁盘写文件,不该由库默认值悄悄代劳。
|
|
31228
|
+
uploadDir: "",
|
|
31229
|
+
maxImageBytes: 4 * 1024 * 1024
|
|
31073
31230
|
};
|
|
31074
31231
|
var LISTING_LIMIT = 100;
|
|
31075
31232
|
var HISTORY_LIMIT = 40;
|
|
@@ -31156,7 +31313,32 @@ var HostRuntime = class {
|
|
|
31156
31313
|
return;
|
|
31157
31314
|
}
|
|
31158
31315
|
}
|
|
31159
|
-
|
|
31316
|
+
let text = cmd.text;
|
|
31317
|
+
const images = Array.isArray(cmd.images) ? cmd.images : [];
|
|
31318
|
+
if (images.length > 0) {
|
|
31319
|
+
if (!this.options.uploadDir) {
|
|
31320
|
+
reply(false, { message: "\u8FD9\u53F0\u4E3B\u673A\u6CA1\u914D\u56FE\u7247\u843D\u76D8\u76EE\u5F55\uFF08uploadDir\uFF09\uFF0C\u6536\u4E0D\u4E86\u56FE\u7247\u9644\u4EF6" });
|
|
31321
|
+
return;
|
|
31322
|
+
}
|
|
31323
|
+
const saved = saveImageAttachments({
|
|
31324
|
+
images,
|
|
31325
|
+
dir: this.options.uploadDir,
|
|
31326
|
+
sessionId: cmd.sessionId,
|
|
31327
|
+
maxBytesPerImage: this.options.maxImageBytes
|
|
31328
|
+
});
|
|
31329
|
+
if (!saved.ok) {
|
|
31330
|
+
reply(false, { message: saved.message });
|
|
31331
|
+
return;
|
|
31332
|
+
}
|
|
31333
|
+
this.options.log?.("\u56FE\u7247\u9644\u4EF6\u843D\u76D8", {
|
|
31334
|
+
sessionId: cmd.sessionId,
|
|
31335
|
+
images: saved.saved.length,
|
|
31336
|
+
bytes: saved.saved.reduce((sum, img) => sum + img.bytes, 0),
|
|
31337
|
+
dir: saved.dir
|
|
31338
|
+
});
|
|
31339
|
+
text = appendImageNote(text, saved.saved);
|
|
31340
|
+
}
|
|
31341
|
+
const sent = await this.kernel.sendPrompt(cmd.sessionId, text);
|
|
31160
31342
|
reply(sent.ok, sent.message ? { message: sent.message } : {});
|
|
31161
31343
|
if (sent.ok) {
|
|
31162
31344
|
this.sleep.markActive();
|
|
@@ -31244,6 +31426,25 @@ var HostRuntime = class {
|
|
|
31244
31426
|
get stats() {
|
|
31245
31427
|
return { ...this.outboundCount };
|
|
31246
31428
|
}
|
|
31429
|
+
/**
|
|
31430
|
+
* 有几件事正挂在手机上等回答,以及**最久的那件已经等了多久**(秒)。
|
|
31431
|
+
*
|
|
31432
|
+
* 为什么这条要出到界面上(而不只是内部状态):一张挂起的审批阻塞着远端一条正在跑的回合,
|
|
31433
|
+
* 而回合阻塞着用户的下班时间。桌面那颗 pill 原来在"已配对 + 有东西在等"时仍然只说
|
|
31434
|
+
* `已配对`——那是这整条链上唯一看得见它的地方,等于没有。判据见伞仓 docs/PRODUCT.md §3。
|
|
31435
|
+
*
|
|
31436
|
+
* `oldestSec` 用 `clock.now()` 而不是 `Date.now()`: FakeClock 要能演"等了 4 分钟",
|
|
31437
|
+
* 否则这条时长在测试里永远是 0,也就永远不会红。
|
|
31438
|
+
*/
|
|
31439
|
+
get waiting() {
|
|
31440
|
+
const now = this.clock.now();
|
|
31441
|
+
let oldest = 0;
|
|
31442
|
+
for (const item of this.pending.values()) {
|
|
31443
|
+
const age = Math.max(0, Math.floor((now - item.askedAt) / 1e3));
|
|
31444
|
+
if (age > oldest) oldest = age;
|
|
31445
|
+
}
|
|
31446
|
+
return { count: this.pending.size, oldestSec: oldest };
|
|
31447
|
+
}
|
|
31247
31448
|
countOutbound(payload, sent) {
|
|
31248
31449
|
const key = payload.t.replace(/^ev\./, "");
|
|
31249
31450
|
this.outboundCount[key] = (this.outboundCount[key] ?? 0) + 1;
|
|
@@ -31321,8 +31522,40 @@ var HostRuntime = class {
|
|
|
31321
31522
|
}
|
|
31322
31523
|
this.broadcast(sessionChanged(this.sessions, reason));
|
|
31323
31524
|
this.broadcast(keepAwakeState(this.sleep.snapshot()));
|
|
31525
|
+
this.broadcastModel();
|
|
31324
31526
|
return this.sessions;
|
|
31325
31527
|
}
|
|
31528
|
+
/**
|
|
31529
|
+
* 广播当前模型。
|
|
31530
|
+
*
|
|
31531
|
+
* 跟着 `pushSessions` 一起发而不是单独起一条:模型与防休眠都是**全局状态**,
|
|
31532
|
+
* 而 `pushSessions` 已经是"状态变了就推一次"的唯一入口,另开一条推送路径
|
|
31533
|
+
* 必然出现「会话更新了但模型没更新」这种半同步状态。
|
|
31534
|
+
*
|
|
31535
|
+
* **读不到就不发**(而不是发一个 `model: ''`):`ev.model` 的 `model` 是必填的
|
|
31536
|
+
* 非空串,硬塞空串会让手机把「不知道用什么模型」显示成「模型名为空」——
|
|
31537
|
+
* 后者看起来像 bug,前者只是没显示。内核缺 `currentSelection` 时真机上是常态
|
|
31538
|
+
* (见 carrier 的 modelFace 探测)。
|
|
31539
|
+
*/
|
|
31540
|
+
broadcastModel() {
|
|
31541
|
+
let selection;
|
|
31542
|
+
try {
|
|
31543
|
+
selection = this.kernel.modelSelection?.();
|
|
31544
|
+
} catch {
|
|
31545
|
+
return;
|
|
31546
|
+
}
|
|
31547
|
+
if (!selection?.model) return;
|
|
31548
|
+
const face = this.kernel.modelOptions?.();
|
|
31549
|
+
this.broadcast(
|
|
31550
|
+
model({
|
|
31551
|
+
model: selection.model,
|
|
31552
|
+
provider: selection.provider,
|
|
31553
|
+
canSwitch: Boolean(face?.canSwitch),
|
|
31554
|
+
options: face?.options,
|
|
31555
|
+
reason: face?.canSwitch ? void 0 : face?.reason ?? "\u4E3B\u673A\u5185\u6838\u672A\u63D0\u4F9B\u5207\u6362\u6A21\u578B\u7684\u80FD\u529B"
|
|
31556
|
+
})
|
|
31557
|
+
);
|
|
31558
|
+
}
|
|
31326
31559
|
refreshLoop() {
|
|
31327
31560
|
if (this.stopped) return;
|
|
31328
31561
|
void this.pushSessions("refresh");
|
|
@@ -31336,24 +31569,33 @@ var HostRuntime = class {
|
|
|
31336
31569
|
* **没有在线对端就返回 `'decline'`**,让适配器去调平台的 `next()` 把决定权交还桌面 UI;
|
|
31337
31570
|
* 超时同样返回 `'decline'`(我们已经把卡片发出去了,但手机迟迟没点,
|
|
31338
31571
|
* 与其替用户决定,不如让桌面去决定)。
|
|
31572
|
+
*
|
|
31573
|
+
* 返回值只说明"手机这一侧算出了什么",**不代表这张卡是手机答掉的**:
|
|
31574
|
+
* 桌面与手机是同时被问的(见 `carrier-services.ts` 的 `participate`),
|
|
31575
|
+
* 所以收尾时要按"有没有真的有人在手机上点过"决定要不要把卡收回去。
|
|
31339
31576
|
*/
|
|
31340
31577
|
async onApprovalRequest(info) {
|
|
31341
31578
|
const conversationId = this.pickConversation(info.sessionId);
|
|
31342
|
-
if (!conversationId || !this.transport.
|
|
31579
|
+
if (!conversationId || !this.transport.hasClient(conversationId)) return "decline";
|
|
31343
31580
|
const id = `ap_${randomUUID().slice(0, 8)}`;
|
|
31344
31581
|
this.sleep.hold(id);
|
|
31345
31582
|
const answered = await new Promise((resolve) => {
|
|
31346
31583
|
const timer = this.clock.setTimeout(() => {
|
|
31347
|
-
this.
|
|
31348
|
-
resolve("decline");
|
|
31584
|
+
this.settle(id, "decline", "timeout");
|
|
31349
31585
|
}, this.options.approvalTimeoutMs);
|
|
31350
31586
|
this.pending.set(id, {
|
|
31351
31587
|
conversationId,
|
|
31352
31588
|
sessionId: info.sessionId,
|
|
31353
31589
|
resolve: (value) => resolve(value === void 0 ? "decline" : value),
|
|
31354
|
-
timer
|
|
31590
|
+
timer,
|
|
31591
|
+
kind: "approval",
|
|
31592
|
+
askedAt: this.clock.now()
|
|
31355
31593
|
});
|
|
31356
|
-
info.signal?.addEventListener(
|
|
31594
|
+
info.signal?.addEventListener(
|
|
31595
|
+
"abort",
|
|
31596
|
+
() => this.settle(id, "cancelled", info.signal?.reason === "desktop" ? "desktop" : "withdrawn"),
|
|
31597
|
+
{ once: true }
|
|
31598
|
+
);
|
|
31357
31599
|
this.replyTo(
|
|
31358
31600
|
conversationId,
|
|
31359
31601
|
permissionRequest({
|
|
@@ -31361,7 +31603,8 @@ var HostRuntime = class {
|
|
|
31361
31603
|
sessionId: info.sessionId,
|
|
31362
31604
|
action: info.action,
|
|
31363
31605
|
...info.reason === void 0 ? {} : { reason: info.reason },
|
|
31364
|
-
options: APPROVAL_OPTIONS
|
|
31606
|
+
options: APPROVAL_OPTIONS,
|
|
31607
|
+
expiresAt: new Date(Date.now() + this.options.approvalTimeoutMs).toISOString()
|
|
31365
31608
|
})
|
|
31366
31609
|
);
|
|
31367
31610
|
});
|
|
@@ -31370,28 +31613,36 @@ var HostRuntime = class {
|
|
|
31370
31613
|
}
|
|
31371
31614
|
async onQuestionRequest(info) {
|
|
31372
31615
|
const conversationId = this.pickConversation(info.sessionId);
|
|
31373
|
-
if (!conversationId || !this.transport.
|
|
31616
|
+
if (!conversationId || !this.transport.hasClient(conversationId)) return null;
|
|
31374
31617
|
const id = `q_${randomUUID().slice(0, 8)}`;
|
|
31375
31618
|
this.sleep.hold(id);
|
|
31376
31619
|
const answer = await new Promise((resolve) => {
|
|
31377
31620
|
const timer = this.clock.setTimeout(() => {
|
|
31378
|
-
this.
|
|
31379
|
-
resolve(null);
|
|
31621
|
+
this.settle(id, null, "timeout");
|
|
31380
31622
|
}, this.options.questionTimeoutMs);
|
|
31381
31623
|
this.pending.set(id, {
|
|
31382
31624
|
conversationId,
|
|
31383
31625
|
sessionId: info.sessionId,
|
|
31384
31626
|
resolve: (value) => resolve(value ?? null),
|
|
31385
31627
|
timer,
|
|
31386
|
-
options: info.questions
|
|
31628
|
+
options: info.questions,
|
|
31629
|
+
kind: "question",
|
|
31630
|
+
askedAt: this.clock.now()
|
|
31387
31631
|
});
|
|
31388
|
-
info.signal?.addEventListener(
|
|
31632
|
+
info.signal?.addEventListener(
|
|
31633
|
+
"abort",
|
|
31634
|
+
() => this.settle(id, null, info.signal?.reason === "desktop" ? "desktop" : "withdrawn"),
|
|
31635
|
+
{ once: true }
|
|
31636
|
+
);
|
|
31389
31637
|
this.replyTo(
|
|
31390
31638
|
conversationId,
|
|
31391
31639
|
questionRequest({
|
|
31392
31640
|
requestId: id,
|
|
31393
31641
|
sessionId: info.sessionId,
|
|
31394
|
-
questions: info.questions
|
|
31642
|
+
questions: info.questions,
|
|
31643
|
+
// 提问这张卡以前**没有**到期时刻:主机 300 秒就判"没答上",而手机上看不见任何倒计时,
|
|
31644
|
+
// 用户不知道自己按的按钮什么时候作废(伞仓 docs/PRODUCT.md §3 第 3 条)。
|
|
31645
|
+
expiresAt: new Date(Date.now() + this.options.questionTimeoutMs).toISOString()
|
|
31395
31646
|
})
|
|
31396
31647
|
);
|
|
31397
31648
|
});
|
|
@@ -31423,13 +31674,47 @@ var HostRuntime = class {
|
|
|
31423
31674
|
this.settle(requestId, value);
|
|
31424
31675
|
return true;
|
|
31425
31676
|
}
|
|
31426
|
-
settle(id, value) {
|
|
31677
|
+
settle(id, value, voidAs) {
|
|
31427
31678
|
const item = this.pending.get(id);
|
|
31428
31679
|
if (!item) return;
|
|
31429
31680
|
this.pending.delete(id);
|
|
31430
31681
|
this.clock.clearTimeout(item.timer);
|
|
31682
|
+
if (voidAs) this.voidStaleCard(id, item, voidAs);
|
|
31431
31683
|
item.resolve(value);
|
|
31432
31684
|
}
|
|
31685
|
+
/**
|
|
31686
|
+
* 让手机上那张已经作废的卡片**当场**收掉:先按 `requestId` 发一条精确帧,
|
|
31687
|
+
* 再补一帧 `ev.run_state` 兜住"手机上装的是老版小程序"这一档。
|
|
31688
|
+
*
|
|
31689
|
+
* 为什么要两条而不是一条:`ev.permission_resolved` / `ev.question_resolved` 是按
|
|
31690
|
+
* `requestId` 收单的(一次答完不会误收别的会话、别的请求那张卡),这是**正路**——
|
|
31691
|
+
* mp 1.0.1 起就认这两帧(`pages/chat/chat.js` 的 `_onPermissionResolved` /
|
|
31692
|
+
* `_onQuestionResolved`)。而 `ev.run_state` 是它从第一版就认的粗粒度信号
|
|
31693
|
+
* (整会话两张卡一起收),发它只为兜住 **1.0.1 以前**的安装存量——那一档小程序
|
|
31694
|
+
* 的分发是一串 `if (p.t === …)`,认不出来的 `t` 静默忽略。
|
|
31695
|
+
* (`pages/chat/chat.js` 的 `_onRunState` 同时清 `pendingPermission` 与 `pendingQuestion`,
|
|
31696
|
+
* 并停掉那条还在走的本地倒数)。所以两条都发:新版手机收到精确帧就精收一张,
|
|
31697
|
+
* 老版手机靠 `run_state` 也能当场收卡,不用等用户重新上传小程序。
|
|
31698
|
+
*
|
|
31699
|
+
* 内核**不会**为"审批/提问被别人答掉了"发状态跳变(这一回合自始至终是 running),
|
|
31700
|
+
* 所以这一帧只能由我们补发;补的是**当前真相**而不是硬编码 `running`——
|
|
31701
|
+
* 手机上那个"思考中"跟着这一帧走,发错方向会一直错到下一次真实跳变。
|
|
31702
|
+
* 读不到真相时退回缓存快照,再退 `idle`:宁可少一个转圈,不能凭空多一个转圈。
|
|
31703
|
+
*/
|
|
31704
|
+
voidStaleCard(requestId, item, reason) {
|
|
31705
|
+
const by = reason === "desktop" ? "desktop" : "cancelled";
|
|
31706
|
+
this.replyTo(
|
|
31707
|
+
item.conversationId,
|
|
31708
|
+
item.kind === "approval" ? permissionResolved({ requestId, sessionId: item.sessionId, by }) : questionResolved({ requestId, sessionId: item.sessionId, by })
|
|
31709
|
+
);
|
|
31710
|
+
const emit = (running) => {
|
|
31711
|
+
this.broadcast(runState({ sessionId: item.sessionId, state: running ? "running" : "idle" }));
|
|
31712
|
+
this.log("pending card voided", { requestId, reason, kind: item.kind, state: running ? "running" : "idle" });
|
|
31713
|
+
};
|
|
31714
|
+
void this.kernel.runState(item.sessionId).then((truth) => emit(truth.running)).catch(() => {
|
|
31715
|
+
emit(this.sessions.some((session) => session.id === item.sessionId && session.running === true));
|
|
31716
|
+
});
|
|
31717
|
+
}
|
|
31433
31718
|
/**
|
|
31434
31719
|
* 挑一条配对通道把这轮交互发出去。
|
|
31435
31720
|
*
|
|
@@ -31441,7 +31726,7 @@ var HostRuntime = class {
|
|
|
31441
31726
|
pickConversation(_sessionId) {
|
|
31442
31727
|
const ids = this.transport.conversationIds();
|
|
31443
31728
|
if (ids.length === 0) return void 0;
|
|
31444
|
-
return ids.find((id) => this.transport.
|
|
31729
|
+
return ids.find((id) => this.transport.hasClient(id)) ?? ids[0];
|
|
31445
31730
|
}
|
|
31446
31731
|
log(message, fields) {
|
|
31447
31732
|
this.options.log?.(message, fields);
|
|
@@ -31603,6 +31888,10 @@ var DEFAULT_PRUNE_POLICY = {
|
|
|
31603
31888
|
// 一天没有任何收发就认为这条通道不会再被用(手机真回来时会重新扫码,
|
|
31604
31889
|
// 而且它会先撞上 `unknown_session` → 中文提示,而不是"永远转圈")。
|
|
31605
31890
|
idleTtlMs: 24 * 3600 * 1e3,
|
|
31891
|
+
// 恢复出来的会话不按上面那条剪:默认给 7 天,与中继的 `DRC_CONV_IDLE_TTL_MS`
|
|
31892
|
+
// (`config.ts` 的 7×24h)逐字对齐 —— 中继忘掉它的那天本来就得重扫,
|
|
31893
|
+
// 主机提前剪只是把同一次重扫提前到用户还没察觉的时间点,反而更难解释。
|
|
31894
|
+
restoredIdleTtlMs: 7 * 24 * 3600 * 1e3,
|
|
31606
31895
|
maxConversations: 64
|
|
31607
31896
|
};
|
|
31608
31897
|
var ConversationBook = class {
|
|
@@ -31620,13 +31909,48 @@ var ConversationBook = class {
|
|
|
31620
31909
|
kH2C: derivePskKey(args.psk, H2C, args.id),
|
|
31621
31910
|
psk: args.psk,
|
|
31622
31911
|
seqHost: 0,
|
|
31623
|
-
generation: args.generation,
|
|
31624
31912
|
createdAt: args.now,
|
|
31625
31913
|
lastActivityAt: args.now
|
|
31626
31914
|
};
|
|
31627
31915
|
this.byId.set(args.id, conversation);
|
|
31628
31916
|
return conversation;
|
|
31629
31917
|
}
|
|
31918
|
+
/**
|
|
31919
|
+
* 从落盘记录恢复会话(跨进程续用,见 `shell/pair-store.ts`)。
|
|
31920
|
+
*
|
|
31921
|
+
* 两条刻意的选择:
|
|
31922
|
+
*
|
|
31923
|
+
* - **`clientIds` 一律置空**:那是"此刻谁连着"的瞬时态,进程刚起来时没有人连着主机。
|
|
31924
|
+
* 填了旧值会让主机一启动就往每条会话广播,而手机端还没重连 ——
|
|
31925
|
+
* 那些帧全被中继计成丢帧(`relay.ts` 的 `broadcast` 闸门当初就是为这个加的)。
|
|
31926
|
+
* 手机回来发的第一帧会经 `RelayClient.onEncrypted` 把它重新登记进来。
|
|
31927
|
+
* - **`restored: true`**:让剪枝对这条会话用另一条 TTL,理由见该字段的注释。
|
|
31928
|
+
*
|
|
31929
|
+
* 返回真正恢复出来的会话 id:**同一条 id 重复出现时后者覆盖前者**,
|
|
31930
|
+
* 而一条形状不合的记录整条丢弃(宁可少一条通道,也不要一条解不开的)。
|
|
31931
|
+
*/
|
|
31932
|
+
restore(records, now) {
|
|
31933
|
+
const restored = [];
|
|
31934
|
+
for (const record2 of records) {
|
|
31935
|
+
const conversation = this.open({ id: record2.id, psk: record2.psk, now });
|
|
31936
|
+
conversation.seqHost = record2.seqHost;
|
|
31937
|
+
conversation.createdAt = record2.createdAt;
|
|
31938
|
+
conversation.lastActivityAt = record2.lastActivityAt;
|
|
31939
|
+
conversation.restored = true;
|
|
31940
|
+
restored.push(record2.id);
|
|
31941
|
+
}
|
|
31942
|
+
return restored;
|
|
31943
|
+
}
|
|
31944
|
+
/** 导出可落盘的记录(**不含 clientIds**,理由见 `restore`)。 */
|
|
31945
|
+
snapshot() {
|
|
31946
|
+
return [...this.byId.values()].map((conversation) => ({
|
|
31947
|
+
id: conversation.id,
|
|
31948
|
+
psk: conversation.psk,
|
|
31949
|
+
seqHost: conversation.seqHost,
|
|
31950
|
+
createdAt: conversation.createdAt,
|
|
31951
|
+
lastActivityAt: conversation.lastActivityAt
|
|
31952
|
+
}));
|
|
31953
|
+
}
|
|
31630
31954
|
/** 有实际收发就记一笔活跃(剪枝只看这个时刻)。未知通道返回 false,不顺手建条目。 */
|
|
31631
31955
|
touch(id, now) {
|
|
31632
31956
|
const conversation = this.byId.get(id);
|
|
@@ -31640,33 +31964,43 @@ var ConversationBook = class {
|
|
|
31640
31964
|
has(id) {
|
|
31641
31965
|
return this.byId.has(id);
|
|
31642
31966
|
}
|
|
31967
|
+
/**
|
|
31968
|
+
* 这条会话**现在有没有活着的客户端**。
|
|
31969
|
+
*
|
|
31970
|
+
* 与 `has()` 是两件事,而且必须分开:`has()` 是"我手里有这条通道的密钥",
|
|
31971
|
+
* 手机回前台时要用同一个 `convId` 回来,所以密钥**必须留着**(D3);
|
|
31972
|
+
* 但"有密钥"不等于"现在有人能收到"。
|
|
31973
|
+
*
|
|
31974
|
+
* 原来只有 `has()` 一个判据,于是 `hasPeer()` 答的是"有密钥",广播就照着它发——
|
|
31975
|
+
* 真机后果:手机断开之后(会话默认 7 天 TTL)主机每 15 秒仍往这条空会话发
|
|
31976
|
+
* `session_changed` / `model` / `keep_awake_state` 三帧,中继每一帧计一次丢帧,
|
|
31977
|
+
* 实测 45 秒涨 9、`/healthz` 的 `droppedFrames` 因此被这一路噪声主导(5737)。
|
|
31978
|
+
*/
|
|
31979
|
+
hasClient(id) {
|
|
31980
|
+
const conversation = this.byId.get(id);
|
|
31981
|
+
return conversation !== void 0 && conversation.clientIds.size > 0;
|
|
31982
|
+
}
|
|
31643
31983
|
close(id) {
|
|
31644
31984
|
return this.byId.delete(id);
|
|
31645
31985
|
}
|
|
31646
|
-
/** 中继重启(generation 变化)时把旧会话全部丢掉:它们再也解不开了。 */
|
|
31647
|
-
closeBefore(generation) {
|
|
31648
|
-
const dropped = [];
|
|
31649
|
-
for (const [id, conversation] of [...this.byId]) {
|
|
31650
|
-
if (conversation.generation < generation) {
|
|
31651
|
-
this.byId.delete(id);
|
|
31652
|
-
dropped.push(id);
|
|
31653
|
-
}
|
|
31654
|
-
}
|
|
31655
|
-
return dropped;
|
|
31656
|
-
}
|
|
31657
31986
|
/**
|
|
31658
31987
|
* 按策略剪枝,返回被剪掉的 id(调用方负责通知中继与上层,见
|
|
31659
31988
|
* `RelayClient.pruneConversations`)。
|
|
31660
31989
|
*
|
|
31661
|
-
*
|
|
31990
|
+
* 三条规则,因为它们是三种不同的失控:
|
|
31662
31991
|
* - 空闲超 TTL:一条通道一天没有任何收发,就不会再有人用它了;
|
|
31992
|
+
* - **恢复出来的会话走另一条 TTL**(`restoredIdleTtlMs`):见 `Conversation.restored`
|
|
31993
|
+
* 与 `DEFAULT_PRUNE_POLICY.restoredIdleTtlMs` 的理由 —— 它不该按"新配对却没人用"的
|
|
31994
|
+
* 标准剪掉,否则长假之后主机一启动就把唯一那条通道清了,免扫码重连白做;
|
|
31663
31995
|
* - 超出硬上界:从**最不活跃**的开始剪。没有这条,一个每天配一次对的用户
|
|
31664
31996
|
* 会在两周内攒下 260 条密钥,并且每次广播都对它们逐个密封一遍。
|
|
31665
31997
|
*/
|
|
31666
31998
|
pruneStale(now, policy = DEFAULT_PRUNE_POLICY) {
|
|
31667
31999
|
const dropped = [];
|
|
32000
|
+
const restoredTtl = policy.restoredIdleTtlMs ?? policy.idleTtlMs;
|
|
31668
32001
|
for (const [id, conversation] of [...this.byId]) {
|
|
31669
|
-
|
|
32002
|
+
const ttl = conversation.restored ? restoredTtl : policy.idleTtlMs;
|
|
32003
|
+
if (now - conversation.lastActivityAt > ttl) {
|
|
31670
32004
|
this.byId.delete(id);
|
|
31671
32005
|
dropped.push(id);
|
|
31672
32006
|
}
|
|
@@ -31683,6 +32017,25 @@ var ConversationBook = class {
|
|
|
31683
32017
|
ids() {
|
|
31684
32018
|
return [...this.byId.keys()];
|
|
31685
32019
|
}
|
|
32020
|
+
/**
|
|
32021
|
+
* 当前**真正连着**的客户端数(去重后的 clientId,不是会话数)。
|
|
32022
|
+
*
|
|
32023
|
+
* 为什么必须单独算:一条会话(convId)对应一次配对,而**同一部手机**在
|
|
32024
|
+
* 「解除配对再重新配对」之后会开一条新会话 —— 于是 `size`(会话数)会增长,
|
|
32025
|
+
* 手机却还是那一台。把 `size` 当成"有几部手机"报出去,用户解除配对后看到
|
|
32026
|
+
* 数字反而变大,会以为解配没生效。
|
|
32027
|
+
*
|
|
32028
|
+
* 反过来也算不准:会话被剪掉了但手机还在(`pruneStale`),
|
|
32029
|
+
* 所以这个数只保证"不比会话数更离谱",不保证等于真实在线数 ——
|
|
32030
|
+
* 界面上它只该当"有手机连着没有"的粗判据,不该当精确计数。
|
|
32031
|
+
*/
|
|
32032
|
+
clientCount() {
|
|
32033
|
+
const ids = /* @__PURE__ */ new Set();
|
|
32034
|
+
for (const conversation of this.byId.values()) {
|
|
32035
|
+
for (const clientId of conversation.clientIds) ids.add(clientId);
|
|
32036
|
+
}
|
|
32037
|
+
return ids.size;
|
|
32038
|
+
}
|
|
31686
32039
|
get size() {
|
|
31687
32040
|
return this.byId.size;
|
|
31688
32041
|
}
|
|
@@ -31849,6 +32202,14 @@ var RelayClient = class {
|
|
|
31849
32202
|
options;
|
|
31850
32203
|
conversations = new ConversationBook();
|
|
31851
32204
|
socket;
|
|
32205
|
+
/**
|
|
32206
|
+
* **主机侧连接代号**:每收到一次 `hello-ok` 就 +1。
|
|
32207
|
+
*
|
|
32208
|
+
* 它现在**只**喂给 `PairingWindow`("中继的 pending-pair 表是不是清空了"),
|
|
32209
|
+
* 绝不用来清会话 —— 理由见文件头。它递增本身没有权威性(中继不回代次),
|
|
32210
|
+
* 所以配对窗口用它判"换码"是保守正确的:中继真重启时代次一定变了,
|
|
32211
|
+
* 中继没重启时多换一张码的代价只是浪费一个 6 位码。
|
|
32212
|
+
*/
|
|
31852
32213
|
generation = 0;
|
|
31853
32214
|
attempts = 0;
|
|
31854
32215
|
stopped = false;
|
|
@@ -31862,6 +32223,39 @@ var RelayClient = class {
|
|
|
31862
32223
|
get conversationCount() {
|
|
31863
32224
|
return this.conversations.size;
|
|
31864
32225
|
}
|
|
32226
|
+
/**
|
|
32227
|
+
* 当前连着的**手机台数**(去重的 clientId)。
|
|
32228
|
+
*
|
|
32229
|
+
* 与 `conversationCount` 分开是因为「解除配对 → 重新配对」会开一条新会话,
|
|
32230
|
+
* 手机却还是那一台:拿会话数当手机数报,用户解完配对看到数字变大,
|
|
32231
|
+
* 会以为解配没生效。
|
|
32232
|
+
*/
|
|
32233
|
+
get clientCount() {
|
|
32234
|
+
return this.conversations.clientCount();
|
|
32235
|
+
}
|
|
32236
|
+
/** 本轮已连上的中继代号(`status.json` 与配对窗口都读它)。 */
|
|
32237
|
+
get relayGeneration() {
|
|
32238
|
+
return this.generation;
|
|
32239
|
+
}
|
|
32240
|
+
/**
|
|
32241
|
+
* 恢复上次进程留下的会话(密钥来自 `pair-store`)。
|
|
32242
|
+
*
|
|
32243
|
+
* **恢复出来的会话里 `clientIds` 是空的**(见 `ConversationBook.restore`),所以
|
|
32244
|
+
* `conversationCount > 0` 而 `clientCount === 0` 是这一阶段的正常形态:
|
|
32245
|
+
* 密钥还在、手机还没回来。调用方据此知道**不能**等 `onPeerJoined` 才启动内核订阅
|
|
32246
|
+
* —— 否则主机重启后手机能连上、主机却不订阅任何事件,表现与配对丢失一模一样。
|
|
32247
|
+
*/
|
|
32248
|
+
restoreConversations(records) {
|
|
32249
|
+
const restored = this.conversations.restore(records, this.options.clock.now());
|
|
32250
|
+
if (restored.length > 0) {
|
|
32251
|
+
this.options.log("conversations restored from disk", {
|
|
32252
|
+
count: restored.length,
|
|
32253
|
+
// 记一下有没有手机已经登记进来了:恢复阶段应当恒为 0。
|
|
32254
|
+
clients: this.conversations.clientCount()
|
|
32255
|
+
});
|
|
32256
|
+
}
|
|
32257
|
+
return restored;
|
|
32258
|
+
}
|
|
31865
32259
|
connect() {
|
|
31866
32260
|
if (this.stopped) return;
|
|
31867
32261
|
this.closeSocket();
|
|
@@ -31936,23 +32330,34 @@ var RelayClient = class {
|
|
|
31936
32330
|
this.options.log("pruned stale conversations", { count: dropped.length, remaining: this.conversations.size });
|
|
31937
32331
|
return dropped;
|
|
31938
32332
|
}
|
|
31939
|
-
/** 向一条配对会话发一条载荷;返回 false =
|
|
32333
|
+
/** 向一条配对会话发一条载荷;返回 false = 这条会话已经没了,或**现在没有活着的客户端**。 */
|
|
31940
32334
|
send(conversationId, payload) {
|
|
31941
32335
|
const conversation = this.conversations.get(conversationId);
|
|
31942
32336
|
if (!conversation) return false;
|
|
32337
|
+
if (conversation.clientIds.size === 0) return false;
|
|
31943
32338
|
const record2 = seal(conversation.kH2C, payload);
|
|
31944
32339
|
conversation.lastActivityAt = this.options.clock.now();
|
|
32340
|
+
this.options.onBookActivity?.();
|
|
31945
32341
|
return this.raw({ t: "enc", sessionId: conversationId, seq: ++conversation.seqHost, ciphertext: record2.ciphertext });
|
|
31946
32342
|
}
|
|
31947
32343
|
broadcast(payload) {
|
|
31948
32344
|
let sent = 0;
|
|
31949
32345
|
for (const id of this.conversations.ids()) {
|
|
32346
|
+
if (!this.conversations.hasClient(id)) continue;
|
|
31950
32347
|
if (this.send(id, payload)) sent += 1;
|
|
31951
32348
|
}
|
|
31952
32349
|
return sent;
|
|
31953
32350
|
}
|
|
31954
|
-
|
|
31955
|
-
|
|
32351
|
+
/**
|
|
32352
|
+
* 这条会话**现在**有没有能收到东西的手机。
|
|
32353
|
+
*
|
|
32354
|
+
* 原来这个方法叫 `hasPeer`、答的是"我手里有没有这条通道的密钥"——两者在手机上线时恰好
|
|
32355
|
+
* 同真,所以错误一直藏着:手机断开后审批仍然"发得出去"(发进一条空会话),
|
|
32356
|
+
* 桌面那一半要等满 180 秒才拿到决定权。名字换成 `hasClient` 是为了让下一次
|
|
32357
|
+
* 想写 `conversations.has()` 的人当场看见这两个概念不是一回事。
|
|
32358
|
+
*/
|
|
32359
|
+
hasClient(conversationId) {
|
|
32360
|
+
return this.conversations.hasClient(conversationId);
|
|
31956
32361
|
}
|
|
31957
32362
|
conversationIds() {
|
|
31958
32363
|
return this.conversations.ids();
|
|
@@ -31978,6 +32383,7 @@ var RelayClient = class {
|
|
|
31978
32383
|
this.voided.add(conversationId);
|
|
31979
32384
|
this.undecryptable.delete(conversationId);
|
|
31980
32385
|
this.conversations.close(conversationId);
|
|
32386
|
+
this.options.onStructuralChange?.();
|
|
31981
32387
|
this.raw({ t: "session-leave", sessionId: conversationId });
|
|
31982
32388
|
this.options.onConversationGone(conversationId);
|
|
31983
32389
|
}
|
|
@@ -32000,18 +32406,24 @@ var RelayClient = class {
|
|
|
32000
32406
|
switch (frame.t) {
|
|
32001
32407
|
case "hello-ok": {
|
|
32002
32408
|
this.generation += 1;
|
|
32003
|
-
const dropped = this.conversations.closeBefore(this.generation);
|
|
32004
|
-
for (const id of dropped) this.options.onConversationGone(id);
|
|
32005
32409
|
this.emitState("online");
|
|
32006
32410
|
this.raw({ t: "resync", sessionIds: this.conversations.ids() });
|
|
32007
|
-
this.options.log("relay online", {
|
|
32411
|
+
this.options.log("relay online", {
|
|
32412
|
+
generation: this.generation,
|
|
32413
|
+
conversations: this.conversations.size,
|
|
32414
|
+
clients: this.conversations.clientCount()
|
|
32415
|
+
});
|
|
32008
32416
|
return;
|
|
32009
32417
|
}
|
|
32010
32418
|
case "pair-ready":
|
|
32011
32419
|
this.options.onPairReady(frame.pairingToken, frame.ttlMs);
|
|
32012
32420
|
return;
|
|
32013
32421
|
case "peer-joined": {
|
|
32014
|
-
if (!frame.pairingToken)
|
|
32422
|
+
if (!frame.pairingToken) {
|
|
32423
|
+
const conversation = this.conversations.get(frame.sessionId);
|
|
32424
|
+
if (conversation && frame.clientId) conversation.clientIds.add(frame.clientId);
|
|
32425
|
+
return;
|
|
32426
|
+
}
|
|
32015
32427
|
const slot = this.options.lookupPairingSlot(frame.pairingToken);
|
|
32016
32428
|
if (!slot) {
|
|
32017
32429
|
this.options.log("peer-joined without a known pairing token", { sessionId: frame.sessionId });
|
|
@@ -32020,9 +32432,10 @@ var RelayClient = class {
|
|
|
32020
32432
|
this.conversations.open({
|
|
32021
32433
|
id: frame.sessionId,
|
|
32022
32434
|
psk: slot.psk,
|
|
32023
|
-
generation: this.generation,
|
|
32024
32435
|
now: this.options.clock.now()
|
|
32025
32436
|
});
|
|
32437
|
+
this.options.onStructuralChange?.();
|
|
32438
|
+
if (frame.clientId) this.conversations.get(frame.sessionId)?.clientIds.add(frame.clientId);
|
|
32026
32439
|
this.voided.delete(frame.sessionId);
|
|
32027
32440
|
this.raw({ t: "resync", sessionIds: this.conversations.ids() });
|
|
32028
32441
|
this.emitState("online");
|
|
@@ -32030,6 +32443,7 @@ var RelayClient = class {
|
|
|
32030
32443
|
return;
|
|
32031
32444
|
}
|
|
32032
32445
|
case "peer-left":
|
|
32446
|
+
if (frame.clientId) this.conversations.get(frame.sessionId)?.clientIds.delete(frame.clientId);
|
|
32033
32447
|
this.options.onClientLeft(frame.sessionId, frame.clientId);
|
|
32034
32448
|
this.emitState("online");
|
|
32035
32449
|
return;
|
|
@@ -32062,6 +32476,7 @@ var RelayClient = class {
|
|
|
32062
32476
|
this.voidConversation(frame.sessionId);
|
|
32063
32477
|
return;
|
|
32064
32478
|
}
|
|
32479
|
+
if (frame.clientId) conversation.clientIds.add(frame.clientId);
|
|
32065
32480
|
const records = frame.items ?? (frame.ciphertext === void 0 ? [] : [{ ciphertext: frame.ciphertext }]);
|
|
32066
32481
|
let decryptedAny = false;
|
|
32067
32482
|
for (const record2 of records) {
|
|
@@ -32080,8 +32495,11 @@ var RelayClient = class {
|
|
|
32080
32495
|
decryptedAny = true;
|
|
32081
32496
|
this.options.onCommand(frame.sessionId, cmd, frame.clientId);
|
|
32082
32497
|
}
|
|
32083
|
-
if (decryptedAny)
|
|
32084
|
-
|
|
32498
|
+
if (decryptedAny) {
|
|
32499
|
+
conversation.lastActivityAt = this.options.clock.now();
|
|
32500
|
+
this.options.onBookActivity?.();
|
|
32501
|
+
this.undecryptable.delete(frame.sessionId);
|
|
32502
|
+
}
|
|
32085
32503
|
}
|
|
32086
32504
|
/** 两次解不开就作废(与手机端"两帧解不开就丢配对"对称,避免一端永久静默)。 */
|
|
32087
32505
|
noteUndecryptable(conversationId) {
|
|
@@ -32118,7 +32536,10 @@ var RelayClient = class {
|
|
|
32118
32536
|
this.options.onState({
|
|
32119
32537
|
relay,
|
|
32120
32538
|
...problem === void 0 ? {} : { problem },
|
|
32121
|
-
|
|
32539
|
+
// 之前这里写的是 `conversations.size`,把"会话数"顶替成了"客户端数":
|
|
32540
|
+
// 一部手机解配再重配就多一条会话,于是这个字段会**变大** ——
|
|
32541
|
+
// 拿它回答"现在有几台手机连着"会得出相反的结论。
|
|
32542
|
+
clients: this.conversations.clientCount(),
|
|
32122
32543
|
conversations: this.conversations.size,
|
|
32123
32544
|
generation: this.generation
|
|
32124
32545
|
});
|
|
@@ -32230,7 +32651,7 @@ var WATERFALL_EVENTS = [
|
|
|
32230
32651
|
"tools/post-execute",
|
|
32231
32652
|
"tools/code-dispatch-log"
|
|
32232
32653
|
];
|
|
32233
|
-
var WATERFALL_PARTICIPANTS = ["approval/request"];
|
|
32654
|
+
var WATERFALL_PARTICIPANTS = ["approval/request", "user-questions/request"];
|
|
32234
32655
|
var allowed = new Set(SUBSCRIBABLE_EVENTS);
|
|
32235
32656
|
var participants = new Set(WATERFALL_PARTICIPANTS);
|
|
32236
32657
|
function canParticipate(name) {
|
|
@@ -32265,11 +32686,45 @@ function guardedSubscribe(on, name, listener, warn) {
|
|
|
32265
32686
|
}
|
|
32266
32687
|
|
|
32267
32688
|
// src/platform/carrier-services.ts
|
|
32689
|
+
var PARTICIPATE_OPTIONS = { global: true, prepend: true };
|
|
32690
|
+
var PARTICIPATED_EVENTS = ["approval/request", "user-questions/request"];
|
|
32691
|
+
function fuseSignals(platform, ours) {
|
|
32692
|
+
if (!platform) return ours;
|
|
32693
|
+
const any2 = AbortSignal.any;
|
|
32694
|
+
if (typeof any2 === "function") return any2.call(AbortSignal, [platform, ours]);
|
|
32695
|
+
const fused = new AbortController();
|
|
32696
|
+
for (const source of [platform, ours]) {
|
|
32697
|
+
if (fused.signal.aborted) break;
|
|
32698
|
+
if (source.aborted) {
|
|
32699
|
+
fused.abort(source.reason);
|
|
32700
|
+
break;
|
|
32701
|
+
}
|
|
32702
|
+
source.addEventListener("abort", () => fused.abort(source.reason), { once: true });
|
|
32703
|
+
}
|
|
32704
|
+
return fused.signal;
|
|
32705
|
+
}
|
|
32268
32706
|
var MAX_TITLE_BATCH_DEFAULT = 25;
|
|
32269
32707
|
var TITLE_TIMEOUT_DEFAULT = 4e3;
|
|
32270
32708
|
var HISTORY_TIMEOUT_DEFAULT = 15e3;
|
|
32271
32709
|
var CREATE_TIMEOUT_DEFAULT = 8e3;
|
|
32272
32710
|
var LIST_LIMIT_DEFAULT = 100;
|
|
32711
|
+
var MODEL_FACE_METHODS = [
|
|
32712
|
+
"currentSelection",
|
|
32713
|
+
"list",
|
|
32714
|
+
"listModels",
|
|
32715
|
+
"available",
|
|
32716
|
+
"availableModels",
|
|
32717
|
+
"selections",
|
|
32718
|
+
"options",
|
|
32719
|
+
"get",
|
|
32720
|
+
"set",
|
|
32721
|
+
"select",
|
|
32722
|
+
"setSelection",
|
|
32723
|
+
"setDefault",
|
|
32724
|
+
"update"
|
|
32725
|
+
];
|
|
32726
|
+
var MODEL_LIST_METHODS = /* @__PURE__ */ new Set(["list", "listModels", "available", "availableModels", "selections", "options"]);
|
|
32727
|
+
var MODEL_SET_METHODS = /* @__PURE__ */ new Set(["set", "select", "setSelection", "setDefault", "update"]);
|
|
32273
32728
|
function createServicesKernel(services, options) {
|
|
32274
32729
|
const log = (message, fields = {}) => options.log?.(message, fields);
|
|
32275
32730
|
const titleCache = /* @__PURE__ */ new Map();
|
|
@@ -32278,8 +32733,25 @@ function createServicesKernel(services, options) {
|
|
|
32278
32733
|
const unmappedEventTypes = /* @__PURE__ */ new Set();
|
|
32279
32734
|
const injectedUserMessages = /* @__PURE__ */ new Map();
|
|
32280
32735
|
const listenerErrors = /* @__PURE__ */ new Map();
|
|
32281
|
-
|
|
32282
|
-
|
|
32736
|
+
const faces = {
|
|
32737
|
+
"approval/request": {
|
|
32738
|
+
registered: "not-attached",
|
|
32739
|
+
calls: 0,
|
|
32740
|
+
last: "not-called",
|
|
32741
|
+
desktopVoided: 0,
|
|
32742
|
+
handoff: "unknown"
|
|
32743
|
+
},
|
|
32744
|
+
"user-questions/request": {
|
|
32745
|
+
registered: "not-attached",
|
|
32746
|
+
calls: 0,
|
|
32747
|
+
last: "not-called",
|
|
32748
|
+
desktopVoided: 0,
|
|
32749
|
+
handoff: "unknown"
|
|
32750
|
+
}
|
|
32751
|
+
};
|
|
32752
|
+
let approvalAsked = 0;
|
|
32753
|
+
let approvalDecided = "not-seen";
|
|
32754
|
+
const modelMethods = [];
|
|
32283
32755
|
const fn = (owner, name) => {
|
|
32284
32756
|
if (!owner) return void 0;
|
|
32285
32757
|
const value = owner[name];
|
|
@@ -32303,6 +32775,17 @@ function createServicesKernel(services, options) {
|
|
|
32303
32775
|
return void 0;
|
|
32304
32776
|
}
|
|
32305
32777
|
}
|
|
32778
|
+
function firstLiveAgent() {
|
|
32779
|
+
const list = fn(services.agents, "list");
|
|
32780
|
+
if (list) {
|
|
32781
|
+
try {
|
|
32782
|
+
const all = list.call(services.agents);
|
|
32783
|
+
if (Array.isArray(all) && all.length) return all[0];
|
|
32784
|
+
} catch {
|
|
32785
|
+
}
|
|
32786
|
+
}
|
|
32787
|
+
return void 0;
|
|
32788
|
+
}
|
|
32306
32789
|
function agentStatus(sessionId) {
|
|
32307
32790
|
const agent = liveAgent(sessionId);
|
|
32308
32791
|
const status = agent?.status;
|
|
@@ -32467,6 +32950,27 @@ function createServicesKernel(services, options) {
|
|
|
32467
32950
|
return void 0;
|
|
32468
32951
|
}
|
|
32469
32952
|
}
|
|
32953
|
+
function probeModelFace() {
|
|
32954
|
+
const owner = services.agentDefaultModel;
|
|
32955
|
+
if (!owner) return { hits: [], canList: false, canSet: false };
|
|
32956
|
+
const hits = [];
|
|
32957
|
+
for (const name of MODEL_FACE_METHODS) {
|
|
32958
|
+
if (fn(owner, name)) hits.push(name);
|
|
32959
|
+
}
|
|
32960
|
+
modelMethods.length = 0;
|
|
32961
|
+
modelMethods.push(...hits);
|
|
32962
|
+
return {
|
|
32963
|
+
hits,
|
|
32964
|
+
canList: hits.some((n) => MODEL_LIST_METHODS.has(n)),
|
|
32965
|
+
canSet: hits.some((n) => MODEL_SET_METHODS.has(n))
|
|
32966
|
+
};
|
|
32967
|
+
}
|
|
32968
|
+
function modelFaceSummary() {
|
|
32969
|
+
if (!services.agentDefaultModel) return "absent";
|
|
32970
|
+
const { hits, canList, canSet } = probeModelFace();
|
|
32971
|
+
if (!hits.length) return `none (tried=${MODEL_FACE_METHODS.length})`;
|
|
32972
|
+
return `${canList ? "list" : "no-list"}+${canSet ? "set" : "no-set"} via=${hits.join(",")}`.slice(0, 180);
|
|
32973
|
+
}
|
|
32470
32974
|
async function sendPrompt(sessionId, text) {
|
|
32471
32975
|
const agent = liveAgent(sessionId);
|
|
32472
32976
|
if (!agent) return { ok: false, message: `\u4F1A\u8BDD\u6CA1\u6709\u6D3B\u7684 agent\uFF08agents.get(${sessionId}) \u4E3A\u7A7A\uFF0C\u8BF7\u5148\u6062\u590D\u4F1A\u8BDD\uFF09` };
|
|
@@ -32591,6 +33095,8 @@ function createServicesKernel(services, options) {
|
|
|
32591
33095
|
if (!sessionId || !type) return;
|
|
32592
33096
|
seenEventTypes.add(type);
|
|
32593
33097
|
const data = event?.data ?? {};
|
|
33098
|
+
if (type === "approval/asked") approvalAsked += 1;
|
|
33099
|
+
if (type === "approval/decided") approvalDecided = String(data.outcome ?? "?");
|
|
32594
33100
|
for (const mapped of sessionEventKernelEvents({ sessionId, type, seq: event?.seq, data })) {
|
|
32595
33101
|
if (mapped.kind === "title") titleCache.set(sessionId, { title: mapped.title, at: options.clock.now() });
|
|
32596
33102
|
onEvent(mapped);
|
|
@@ -32617,59 +33123,34 @@ function createServicesKernel(services, options) {
|
|
|
32617
33123
|
sink = next;
|
|
32618
33124
|
const disposers = [];
|
|
32619
33125
|
const on = services.on;
|
|
32620
|
-
if (typeof on
|
|
32621
|
-
const name = "
|
|
32622
|
-
|
|
32623
|
-
|
|
32624
|
-
try {
|
|
32625
|
-
const disposer = on.call(services, name, listener);
|
|
32626
|
-
approvalFace = "registered";
|
|
32627
|
-
disposers.push(() => {
|
|
32628
|
-
if (typeof disposer === "function") disposer();
|
|
32629
|
-
else if (typeof services.off === "function") services.off(name, listener);
|
|
32630
|
-
});
|
|
32631
|
-
} catch (error62) {
|
|
32632
|
-
approvalFace = `failed: ${messageOf3(error62)}`.slice(0, 80);
|
|
32633
|
-
log("approval/request participation failed", { message: messageOf3(error62) });
|
|
32634
|
-
}
|
|
32635
|
-
} else {
|
|
32636
|
-
approvalFace = "refused by guard";
|
|
32637
|
-
}
|
|
32638
|
-
} else {
|
|
32639
|
-
approvalFace = "no on()";
|
|
32640
|
-
}
|
|
32641
|
-
const questions = services.userQuestions;
|
|
32642
|
-
if (!options.takeOverQuestions) {
|
|
32643
|
-
questionsFace = "not-taken-over";
|
|
32644
|
-
} else if (!questions || typeof questions.registerProvider !== "function") {
|
|
32645
|
-
questionsFace = `no registerProvider (keys=${questions ? shapeOf(questions) : "service absent"})`.slice(0, 120);
|
|
32646
|
-
}
|
|
32647
|
-
if (options.takeOverQuestions && questions && typeof questions.registerProvider === "function") {
|
|
32648
|
-
const provider = {
|
|
32649
|
-
ask: async (request) => {
|
|
32650
|
-
const agent = request.agent;
|
|
32651
|
-
const sessionId = String(agent?.session?.id ?? request.sessionId ?? "");
|
|
32652
|
-
const items = Array.isArray(request.questions) ? request.questions : [];
|
|
32653
|
-
const mapped = items.map((item, index) => mapQuestion(item, index));
|
|
32654
|
-
const answer = await sink?.question({
|
|
32655
|
-
sessionId,
|
|
32656
|
-
questions: mapped,
|
|
32657
|
-
...request.signal ? { signal: request.signal } : {}
|
|
32658
|
-
});
|
|
32659
|
-
if (!answer) {
|
|
32660
|
-
throw new Error("\u624B\u673A\u7AEF\u672A\u5E94\u7B54\u8FD9\u6B21\u63D0\u95EE\uFF08\u672A\u914D\u5BF9\u3001\u4E0D\u5728\u7EBF\u6216\u5DF2\u8D85\u65F6\uFF09");
|
|
32661
|
-
}
|
|
32662
|
-
return { answers: answer.answers };
|
|
32663
|
-
}
|
|
33126
|
+
if (typeof on !== "function") {
|
|
33127
|
+
for (const name of PARTICIPATED_EVENTS) faces[name].registered = "no on()";
|
|
33128
|
+
return () => {
|
|
33129
|
+
sink = void 0;
|
|
32664
33130
|
};
|
|
33131
|
+
}
|
|
33132
|
+
for (const name of PARTICIPATED_EVENTS) {
|
|
33133
|
+
const face = faces[name];
|
|
33134
|
+
if (!canParticipate(name)) {
|
|
33135
|
+
face.registered = "refused by guard";
|
|
33136
|
+
continue;
|
|
33137
|
+
}
|
|
33138
|
+
const listener = (...args) => participate(name, args);
|
|
32665
33139
|
try {
|
|
32666
|
-
const
|
|
32667
|
-
|
|
32668
|
-
|
|
32669
|
-
|
|
33140
|
+
const disposer = on.call(
|
|
33141
|
+
services,
|
|
33142
|
+
name,
|
|
33143
|
+
listener,
|
|
33144
|
+
PARTICIPATE_OPTIONS
|
|
33145
|
+
);
|
|
33146
|
+
face.registered = "registered";
|
|
33147
|
+
disposers.push(() => {
|
|
33148
|
+
if (typeof disposer === "function") disposer();
|
|
33149
|
+
else if (typeof services.off === "function") services.off(name, listener);
|
|
33150
|
+
});
|
|
32670
33151
|
} catch (error62) {
|
|
32671
|
-
|
|
32672
|
-
log(
|
|
33152
|
+
face.registered = `failed: ${messageOf3(error62)}`.slice(0, 80);
|
|
33153
|
+
log(`${name} participation failed`, { message: messageOf3(error62) });
|
|
32673
33154
|
}
|
|
32674
33155
|
}
|
|
32675
33156
|
return () => {
|
|
@@ -32682,23 +33163,103 @@ function createServicesKernel(services, options) {
|
|
|
32682
33163
|
}
|
|
32683
33164
|
};
|
|
32684
33165
|
}
|
|
32685
|
-
async function participate(args) {
|
|
33166
|
+
async function participate(event, args) {
|
|
33167
|
+
const isApproval = event === "approval/request";
|
|
33168
|
+
const face = faces[event];
|
|
33169
|
+
face.calls += 1;
|
|
32686
33170
|
const request = args.find(
|
|
32687
|
-
(arg) => arg && typeof arg === "object" && typeof arg.toolName === "string"
|
|
33171
|
+
(arg) => Boolean(arg) && typeof arg === "object" && (isApproval ? typeof arg.toolName === "string" : Array.isArray(arg.questions))
|
|
32688
33172
|
);
|
|
32689
33173
|
const next = args.find((arg) => typeof arg === "function");
|
|
32690
33174
|
const sessionId = String(request?.agent?.session?.id ?? "");
|
|
32691
33175
|
if (!sink || !sessionId || !servicesPeers()) {
|
|
32692
|
-
|
|
33176
|
+
face.last = "handed-back(no phone target)";
|
|
33177
|
+
const value = await next?.();
|
|
33178
|
+
return isApproval ? String(value ?? "unavailable") : value;
|
|
33179
|
+
}
|
|
33180
|
+
const controller = new AbortController();
|
|
33181
|
+
const platform = request?.signal;
|
|
33182
|
+
if (platform) {
|
|
33183
|
+
if (platform.aborted) controller.abort("platform");
|
|
33184
|
+
else platform.addEventListener("abort", () => controller.abort("platform"), { once: true });
|
|
33185
|
+
}
|
|
33186
|
+
const canceller = new AbortController();
|
|
33187
|
+
let handedOff = false;
|
|
33188
|
+
if (request) {
|
|
33189
|
+
try {
|
|
33190
|
+
const fused = fuseSignals(platform, canceller.signal);
|
|
33191
|
+
request.signal = fused;
|
|
33192
|
+
handedOff = request.signal === fused;
|
|
33193
|
+
face.handoff = handedOff ? "fused" : "ignored";
|
|
33194
|
+
} catch (error62) {
|
|
33195
|
+
face.handoff = `failed: ${messageOf3(error62)}`.slice(0, 80);
|
|
33196
|
+
}
|
|
32693
33197
|
}
|
|
32694
|
-
const
|
|
33198
|
+
const finish = (reason) => {
|
|
33199
|
+
if (!canceller.signal.aborted) canceller.abort(reason);
|
|
33200
|
+
};
|
|
33201
|
+
const desktop = Promise.resolve().then(() => next?.()).then(
|
|
33202
|
+
(value) => (
|
|
33203
|
+
// `undefined` 是"这个参与者没认领"(会把整条链冲掉),不是答案;
|
|
33204
|
+
// `'unavailable'` 是审批那条链末端的"没人可问",也不是答案。
|
|
33205
|
+
value === void 0 || isApproval && value === "unavailable" ? { answered: false, value } : { answered: true, value }
|
|
33206
|
+
),
|
|
33207
|
+
(error62) => ({ answered: false, error: error62 })
|
|
33208
|
+
);
|
|
33209
|
+
const asked = isApproval ? sink.approval({
|
|
32695
33210
|
sessionId,
|
|
32696
33211
|
action: String(request?.toolName ?? "\u5DE5\u5177\u8C03\u7528"),
|
|
32697
33212
|
...request?.reason ? { reason: String(request.reason) } : {},
|
|
32698
|
-
|
|
33213
|
+
signal: controller.signal
|
|
33214
|
+
}) : sink.question({
|
|
33215
|
+
sessionId,
|
|
33216
|
+
questions: (request?.questions ?? []).map((item, index) => mapQuestion(item, index)),
|
|
33217
|
+
signal: controller.signal
|
|
32699
33218
|
});
|
|
32700
|
-
|
|
32701
|
-
|
|
33219
|
+
const phone = asked.then(
|
|
33220
|
+
(value) => (
|
|
33221
|
+
// 每一支都**带着自己是谁**回来:`'rejected'` 这个词手机和桌面都可能给出,
|
|
33222
|
+
// 只比数值就没法知道该收哪一张卡(第一版就是这么错的)。
|
|
33223
|
+
isApproval ? value === "allowed-once" || value === "rejected" ? { answered: true, value } : { answered: false, value } : value === null || value === void 0 ? { answered: false, value } : { answered: true, value }
|
|
33224
|
+
),
|
|
33225
|
+
// runtime 抛了(未配对、不在线、被撤)同样不算答案。
|
|
33226
|
+
() => ({ answered: false })
|
|
33227
|
+
);
|
|
33228
|
+
const pending = new Promise(() => {
|
|
33229
|
+
});
|
|
33230
|
+
const winner = await Promise.race([
|
|
33231
|
+
phone.then((chain) => chain.answered ? { src: "phone", chain } : pending),
|
|
33232
|
+
desktop.then((chain) => chain.answered ? { src: "desktop", chain } : pending),
|
|
33233
|
+
Promise.all([phone, desktop]).then(([, chain]) => ({ src: "desktop", chain }))
|
|
33234
|
+
]);
|
|
33235
|
+
if (!winner.chain.answered) {
|
|
33236
|
+
controller.abort("desktop");
|
|
33237
|
+
finish("settled-without-answer");
|
|
33238
|
+
if (isApproval) {
|
|
33239
|
+
face.last = "handed-back(neither answered)";
|
|
33240
|
+
return "unavailable";
|
|
33241
|
+
}
|
|
33242
|
+
face.last = "no-answer";
|
|
33243
|
+
if (winner.chain.error !== void 0) throw winner.chain.error;
|
|
33244
|
+
throw new Error("\u624B\u673A\u7AEF\u672A\u5E94\u7B54\u8FD9\u6B21\u63D0\u95EE\uFF08\u672A\u914D\u5BF9\u3001\u4E0D\u5728\u7EBF\u6216\u5DF2\u8D85\u65F6\uFF09");
|
|
33245
|
+
}
|
|
33246
|
+
if (winner.src === "desktop") controller.abort("desktop");
|
|
33247
|
+
if (winner.src === "phone") {
|
|
33248
|
+
finish("answered-by-phone");
|
|
33249
|
+
if (handedOff) face.desktopVoided += 1;
|
|
33250
|
+
} else {
|
|
33251
|
+
finish("answered-by-desktop");
|
|
33252
|
+
}
|
|
33253
|
+
face.last = `${winner.src === "phone" ? "answered-by-phone" : "answered-by-desktop"}(${describeOutcome(
|
|
33254
|
+
isApproval,
|
|
33255
|
+
winner.chain.value
|
|
33256
|
+
)})`;
|
|
33257
|
+
return winner.chain.value;
|
|
33258
|
+
}
|
|
33259
|
+
function describeOutcome(isApproval, value) {
|
|
33260
|
+
if (isApproval) return String(value);
|
|
33261
|
+
const answers = value?.answers;
|
|
33262
|
+
return Array.isArray(answers) ? `${answers.length} \u9879` : typeof value;
|
|
32702
33263
|
}
|
|
32703
33264
|
function servicesPeers() {
|
|
32704
33265
|
return sink !== void 0;
|
|
@@ -32737,6 +33298,22 @@ function createServicesKernel(services, options) {
|
|
|
32737
33298
|
// 与发指令/续跑走同一条读取路径:这里原来还有一份实现,而且是 `current()` 裸调用
|
|
32738
33299
|
// (不带 receiver;真要用 this 的服务方法会当场抛),两份迟早分叉。
|
|
32739
33300
|
modelSelection: currentSelection,
|
|
33301
|
+
/**
|
|
33302
|
+
* 可切换模型清单。**这一代内核给不出清单也换不了**(`agentDefaultModel` 上只有
|
|
33303
|
+
* `currentSelection`),所以这里报 `canSwitch: false` 并附一句人话。
|
|
33304
|
+
*
|
|
33305
|
+
* 报false 而不是省略这个方法:省略会被 core 当成"只读"但**说不出原因**,
|
|
33306
|
+
* 手机上就只剩一个置灰的模型名,用户不知道是主机不支持还是自己哪里做错了。
|
|
33307
|
+
*/
|
|
33308
|
+
modelOptions() {
|
|
33309
|
+
const { hits, canList, canSet } = probeModelFace();
|
|
33310
|
+
return {
|
|
33311
|
+
// 两个都要有才算能切:只有 list 没有 set 时手机上会列出候选却点了没反应,
|
|
33312
|
+
// 那比"换不了"更糟。
|
|
33313
|
+
canSwitch: canList && canSet,
|
|
33314
|
+
reason: canList && canSet ? void 0 : `\u4E3B\u673A\u5185\u6838\u53EA\u63D0\u4F9B\u8BFB\u53D6\uFF08\u547D\u4E2D\uFF1A${hits.join(",") || "\u65E0"}\uFF09`
|
|
33315
|
+
};
|
|
33316
|
+
},
|
|
32740
33317
|
describe() {
|
|
32741
33318
|
return {
|
|
32742
33319
|
carrier: "services",
|
|
@@ -32748,19 +33325,41 @@ function createServicesKernel(services, options) {
|
|
|
32748
33325
|
// 新建会话只依赖这一条。和 historyFace 同理:真机上"新建没反应"第一件要分清的事
|
|
32749
33326
|
// 是宿主没有这个能力,还是我们没调对。
|
|
32750
33327
|
createFace: typeof fn(services.sessionController?.commands, "create") === "function" ? "commands.create" : `absent (keys=${shapeOf(services.sessionController)})`.slice(0, 120),
|
|
33328
|
+
// 第二条换模型的可能路径:命令控制器上自带的方法(`setModel` / `switchModel`…),
|
|
33329
|
+
// 以及活agent 自己暴露的模型字段。前者能列出来就说明这条路通不通。
|
|
33330
|
+
commandsFace: shapeOf(services.sessionController?.commands),
|
|
33331
|
+
agentModelFields: (() => {
|
|
33332
|
+
const first = firstLiveAgent();
|
|
33333
|
+
if (!first) return "no-live-agent";
|
|
33334
|
+
return Object.keys(first).filter((k) => /model|provider|preset/i.test(k)).slice(0, 8).join(",") || "none";
|
|
33335
|
+
})(),
|
|
32751
33336
|
agents: typeof services.agents === "object",
|
|
32752
33337
|
agentDefaultModel: typeof services.agentDefaultModel === "object",
|
|
33338
|
+
// 模型面到底有没有"写"的能力(能列可选、能切换)。手机上模型下拉该不该置灰,
|
|
33339
|
+
// 取决于这个而不是取决于"读得到当前值" —— 读得到只够显示一行文字。
|
|
33340
|
+
modelFace: modelFaceSummary(),
|
|
32753
33341
|
workspaceRegistry: typeof services.workspaceRegistry === "object",
|
|
32754
33342
|
userQuestions: typeof services.userQuestions === "object",
|
|
32755
|
-
takeOverQuestions: options.takeOverQuestions === true,
|
|
32756
33343
|
archivedSessions: archivedIds().size,
|
|
32757
33344
|
hasOn: typeof services.on === "function",
|
|
32758
33345
|
// 真机排错的第一现场:内核到底发了哪些事件类型、其中哪些我们没认。
|
|
32759
33346
|
eventTypes: [...seenEventTypes].slice(-24).join("|"),
|
|
32760
33347
|
unmappedEventTypes: [...unmappedEventTypes].join("|"),
|
|
32761
33348
|
injectedUserMessages: [...injectedUserMessages].map(([kind, count]) => `${count}\xD7${kind}`).join("|") || "none",
|
|
32762
|
-
approvalFace,
|
|
32763
|
-
|
|
33349
|
+
approvalFace: faces["approval/request"].registered,
|
|
33350
|
+
approvalCalls: faces["approval/request"].calls,
|
|
33351
|
+
approvalLast: faces["approval/request"].last,
|
|
33352
|
+
approvalAsked,
|
|
33353
|
+
approvalDecided,
|
|
33354
|
+
approvalDesktopVoided: faces["approval/request"].desktopVoided,
|
|
33355
|
+
approvalSignalHandoff: faces["approval/request"].handoff,
|
|
33356
|
+
// 提问那一条与审批同构,所以读数也同一套:登记结果 / 被派发几次 / 最后走到哪一步 /
|
|
33357
|
+
// 手机先答时撤销了几次下游 signal / 那份 signal 换没换成功。
|
|
33358
|
+
questionsFace: faces["user-questions/request"].registered,
|
|
33359
|
+
questionsCalls: faces["user-questions/request"].calls,
|
|
33360
|
+
questionsLast: faces["user-questions/request"].last,
|
|
33361
|
+
questionsDesktopVoided: faces["user-questions/request"].desktopVoided,
|
|
33362
|
+
questionsSignalHandoff: faces["user-questions/request"].handoff,
|
|
32764
33363
|
listenerErrors: [...listenerErrors].slice(0, 4).map(([key, count]) => `${count}\xD7${key}`).join(" | ") || "none"
|
|
32765
33364
|
};
|
|
32766
33365
|
}
|
|
@@ -32782,17 +33381,19 @@ function sessionEventKernelEvents(input2) {
|
|
|
32782
33381
|
switch (type) {
|
|
32783
33382
|
case "turn/start":
|
|
32784
33383
|
return [{ kind: "run-state", sessionId, state: "running" }];
|
|
32785
|
-
case "assistant/message":
|
|
33384
|
+
case "assistant/message": {
|
|
33385
|
+
const text = stripToolCallXml(textOf(data));
|
|
32786
33386
|
return [
|
|
32787
33387
|
{
|
|
32788
33388
|
kind: "delta",
|
|
32789
33389
|
sessionId,
|
|
32790
33390
|
messageId: messageIdOf(data, input2.seq),
|
|
32791
|
-
text
|
|
33391
|
+
text,
|
|
32792
33392
|
role: "assistant",
|
|
32793
33393
|
done: true
|
|
32794
33394
|
}
|
|
32795
33395
|
];
|
|
33396
|
+
}
|
|
32796
33397
|
case "user/message": {
|
|
32797
33398
|
if (!isHumanUserMessage(data)) return [];
|
|
32798
33399
|
return [
|
|
@@ -32815,8 +33416,8 @@ function sessionEventKernelEvents(input2) {
|
|
|
32815
33416
|
callId: String(data.callId ?? data.id ?? `tool_${input2.seq ?? 0}`),
|
|
32816
33417
|
phase: argsRaw === void 0 || argsRaw === null ? "started" : "args",
|
|
32817
33418
|
tool: typeof data.name === "string" ? data.name : void 0,
|
|
32818
|
-
title: typeof data.title === "string" ? data.title :
|
|
32819
|
-
...argsRaw === void 0 ? {} : { argsPreview:
|
|
33419
|
+
title: typeof data.title === "string" ? data.title : argsTitle(argsRaw),
|
|
33420
|
+
...argsRaw === void 0 ? {} : { argsPreview: argsPreviewOf(argsRaw) }
|
|
32820
33421
|
}
|
|
32821
33422
|
];
|
|
32822
33423
|
}
|
|
@@ -32967,9 +33568,67 @@ function messageIdOf(data, seq) {
|
|
|
32967
33568
|
if (typeof id === "string" && id) return id;
|
|
32968
33569
|
return `msg_${String(seq ?? Date.now())}`;
|
|
32969
33570
|
}
|
|
32970
|
-
|
|
33571
|
+
var TOOL_CALL_BLOCK = /<tool_call>\s*<function=[\s\S]*?<\/tool_call>/g;
|
|
33572
|
+
var TOOL_CALL_UNCLOSED = /<tool_call>\s*<function=[\s\S]*$/;
|
|
33573
|
+
function stripToolCallXml(text) {
|
|
33574
|
+
const stripped = text.replace(TOOL_CALL_BLOCK, "").replace(TOOL_CALL_UNCLOSED, "");
|
|
33575
|
+
return stripped.trim() === "" ? "" : stripped;
|
|
33576
|
+
}
|
|
33577
|
+
function argsTitle(argsRaw) {
|
|
33578
|
+
const parsed = tryParseJson(argsRaw);
|
|
33579
|
+
if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
|
|
33580
|
+
const record2 = parsed;
|
|
33581
|
+
for (const key of ["description", "prompt", "command", "path", "file_path", "pattern", "query", "url"]) {
|
|
33582
|
+
const value = record2[key];
|
|
33583
|
+
if (typeof value === "string" && value.trim()) return oneLine(value);
|
|
33584
|
+
}
|
|
33585
|
+
}
|
|
33586
|
+
return previewOf(argsRaw);
|
|
33587
|
+
}
|
|
33588
|
+
function argsPreviewOf(argsRaw) {
|
|
33589
|
+
const parsed = tryParseJson(argsRaw);
|
|
33590
|
+
if (parsed !== void 0) {
|
|
33591
|
+
try {
|
|
33592
|
+
return previewOf(JSON.stringify(parsed, null, 2));
|
|
33593
|
+
} catch {
|
|
33594
|
+
}
|
|
33595
|
+
}
|
|
33596
|
+
return previewOf(argsRaw);
|
|
33597
|
+
}
|
|
33598
|
+
function tryParseJson(value) {
|
|
33599
|
+
if (typeof value !== "string" || !value.trim()) return void 0;
|
|
33600
|
+
try {
|
|
33601
|
+
return JSON.parse(value);
|
|
33602
|
+
} catch {
|
|
33603
|
+
return void 0;
|
|
33604
|
+
}
|
|
33605
|
+
}
|
|
33606
|
+
function oneLine(text) {
|
|
33607
|
+
const flat = text.replace(/\s+/g, " ").trim();
|
|
33608
|
+
return flat.length > 120 ? `${flat.slice(0, 120)}\u2026` : flat;
|
|
33609
|
+
}
|
|
33610
|
+
function readableText(value) {
|
|
32971
33611
|
if (value === void 0 || value === null) return void 0;
|
|
32972
|
-
|
|
33612
|
+
if (typeof value === "string") return value;
|
|
33613
|
+
if (typeof value === "number" || typeof value === "boolean") return String(value);
|
|
33614
|
+
if (Array.isArray(value)) {
|
|
33615
|
+
const parts = value.map((part) => readableText(part)).filter((part) => typeof part === "string" && part.length > 0);
|
|
33616
|
+
return parts.length > 0 ? parts.join("\n") : void 0;
|
|
33617
|
+
}
|
|
33618
|
+
if (typeof value === "object") {
|
|
33619
|
+
const record2 = value;
|
|
33620
|
+
for (const key of ["content", "text", "output", "stdout", "stderr", "message", "result", "error"]) {
|
|
33621
|
+
if (record2[key] === void 0 || record2[key] === null) continue;
|
|
33622
|
+
const nested = readableText(record2[key]);
|
|
33623
|
+
if (nested !== void 0 && nested !== "") return nested;
|
|
33624
|
+
}
|
|
33625
|
+
if (typeof record2.type === "string" && record2.type !== "text") return void 0;
|
|
33626
|
+
return safeJson(record2);
|
|
33627
|
+
}
|
|
33628
|
+
return safeJson(value);
|
|
33629
|
+
}
|
|
33630
|
+
function previewOf(value) {
|
|
33631
|
+
const text = readableText(value);
|
|
32973
33632
|
if (!text) return void 0;
|
|
32974
33633
|
return text.length > 200 ? `${text.slice(0, 200)}\u2026` : text;
|
|
32975
33634
|
}
|
|
@@ -33033,7 +33692,7 @@ function createOneShotTimers(clock) {
|
|
|
33033
33692
|
|
|
33034
33693
|
// src/shell/status.ts
|
|
33035
33694
|
import { mkdirSync, renameSync, writeFileSync } from "node:fs";
|
|
33036
|
-
import
|
|
33695
|
+
import path2 from "node:path";
|
|
33037
33696
|
var StatusFile = class {
|
|
33038
33697
|
constructor(file2, clock, refreshMs = 3e3) {
|
|
33039
33698
|
this.file = file2;
|
|
@@ -33047,7 +33706,7 @@ var StatusFile = class {
|
|
|
33047
33706
|
write(snapshot) {
|
|
33048
33707
|
if (!this.file) return;
|
|
33049
33708
|
try {
|
|
33050
|
-
mkdirSync(
|
|
33709
|
+
mkdirSync(path2.dirname(this.file), { recursive: true });
|
|
33051
33710
|
const tmp = `${this.file}.tmp-${process.pid}`;
|
|
33052
33711
|
writeFileSync(
|
|
33053
33712
|
tmp,
|
|
@@ -33087,9 +33746,174 @@ var StatusFile = class {
|
|
|
33087
33746
|
}
|
|
33088
33747
|
};
|
|
33089
33748
|
|
|
33749
|
+
// src/shell/host-id.ts
|
|
33750
|
+
import { randomBytes as randomBytes4 } from "node:crypto";
|
|
33751
|
+
import { chmodSync, mkdirSync as mkdirSync2, readFileSync, writeFileSync as writeFileSync2 } from "node:fs";
|
|
33752
|
+
import path3 from "node:path";
|
|
33753
|
+
var HOST_ID = /^[A-Za-z0-9][A-Za-z0-9_.-]{0,63}$/;
|
|
33754
|
+
function newHostId() {
|
|
33755
|
+
return `drc-${randomBytes4(6).toString("hex")}`;
|
|
33756
|
+
}
|
|
33757
|
+
function resolveHostId(dir, configured) {
|
|
33758
|
+
if (configured) return configured;
|
|
33759
|
+
if (!dir) return newHostId();
|
|
33760
|
+
const file2 = path3.join(dir, "host-id");
|
|
33761
|
+
let existing;
|
|
33762
|
+
try {
|
|
33763
|
+
existing = readFileSync(file2, "utf8").trim();
|
|
33764
|
+
} catch {
|
|
33765
|
+
existing = void 0;
|
|
33766
|
+
}
|
|
33767
|
+
if (existing !== void 0 && HOST_ID.test(existing)) return existing;
|
|
33768
|
+
const fresh = newHostId();
|
|
33769
|
+
try {
|
|
33770
|
+
mkdirSync2(dir, { recursive: true });
|
|
33771
|
+
writeFileSync2(file2, `${fresh}
|
|
33772
|
+
`, {
|
|
33773
|
+
encoding: "utf8",
|
|
33774
|
+
mode: 384,
|
|
33775
|
+
flag: existing === void 0 ? "wx" : "w"
|
|
33776
|
+
});
|
|
33777
|
+
chmodSync(file2, 384);
|
|
33778
|
+
return fresh;
|
|
33779
|
+
} catch {
|
|
33780
|
+
try {
|
|
33781
|
+
const raced = readFileSync(file2, "utf8").trim();
|
|
33782
|
+
if (HOST_ID.test(raced)) return raced;
|
|
33783
|
+
} catch {
|
|
33784
|
+
}
|
|
33785
|
+
return fresh;
|
|
33786
|
+
}
|
|
33787
|
+
}
|
|
33788
|
+
|
|
33789
|
+
// src/shell/pair-store.ts
|
|
33790
|
+
import { chmodSync as chmodSync2, mkdirSync as mkdirSync3, readFileSync as readFileSync2, renameSync as renameSync2, unlinkSync, writeFileSync as writeFileSync3 } from "node:fs";
|
|
33791
|
+
import { createHash as createHash2 } from "node:crypto";
|
|
33792
|
+
import path4 from "node:path";
|
|
33793
|
+
var storedConversationSchema = external_exports.object({
|
|
33794
|
+
id: external_exports.string().min(1),
|
|
33795
|
+
/** 配对 PSK(base64)。两把方向密钥在恢复时由它现派生,不落盘。 */
|
|
33796
|
+
psk: external_exports.string().min(1),
|
|
33797
|
+
/** host→client 方向的本地序号。中继会重新编号,客户端从不读它(F13),
|
|
33798
|
+
* 但保留它能让恢复后的编号不跳号,便于对拍排查。 */
|
|
33799
|
+
seqHost: external_exports.number().int().nonnegative(),
|
|
33800
|
+
createdAt: external_exports.number().finite().nonnegative(),
|
|
33801
|
+
lastActivityAt: external_exports.number().finite().nonnegative()
|
|
33802
|
+
});
|
|
33803
|
+
var storeSchema = external_exports.object({
|
|
33804
|
+
version: external_exports.literal(1),
|
|
33805
|
+
/** 这个文件属于哪台主机:换了主机身份就不该认(凭据不该跨身份复用)。 */
|
|
33806
|
+
hostId: external_exports.string().min(1),
|
|
33807
|
+
savedAt: external_exports.number().finite().nonnegative(),
|
|
33808
|
+
conversations: external_exports.array(storedConversationSchema)
|
|
33809
|
+
});
|
|
33810
|
+
function safeSegment2(hostId) {
|
|
33811
|
+
const cleaned = hostId.replace(/[^A-Za-z0-9._-]/g, "_").slice(0, 48);
|
|
33812
|
+
const tag = createHash2("sha256").update(hostId).digest("hex").slice(0, 8);
|
|
33813
|
+
return `${cleaned || "host"}-${tag}`;
|
|
33814
|
+
}
|
|
33815
|
+
function defaultPairStoreFile(statusFile, hostId) {
|
|
33816
|
+
if (!statusFile) return "";
|
|
33817
|
+
return path4.join(path4.dirname(statusFile), `conversations-${safeSegment2(hostId)}.json`);
|
|
33818
|
+
}
|
|
33819
|
+
var PairStore = class {
|
|
33820
|
+
constructor(options) {
|
|
33821
|
+
this.options = options;
|
|
33822
|
+
}
|
|
33823
|
+
options;
|
|
33824
|
+
/** 上一次成功写盘的时刻,供 status.json 回答"这份落盘是什么时候的"。 */
|
|
33825
|
+
savedAt = 0;
|
|
33826
|
+
/** 上次 save 之后发生过任何变化(含只改了 lastActivityAt)。 */
|
|
33827
|
+
dirty = false;
|
|
33828
|
+
get file() {
|
|
33829
|
+
return this.options.file;
|
|
33830
|
+
}
|
|
33831
|
+
get lastSavedAt() {
|
|
33832
|
+
return this.savedAt;
|
|
33833
|
+
}
|
|
33834
|
+
/** 有没有待落盘的变更。status 的 3 秒 tick 用它决定是否真写盘。 */
|
|
33835
|
+
get hasPendingChanges() {
|
|
33836
|
+
return this.dirty;
|
|
33837
|
+
}
|
|
33838
|
+
/** 只标脏,不写盘。高频变动(每次广播都动 `seqHost`)走这条。 */
|
|
33839
|
+
markDirty() {
|
|
33840
|
+
this.dirty = true;
|
|
33841
|
+
}
|
|
33842
|
+
/**
|
|
33843
|
+
* 读回本机的会话记录。
|
|
33844
|
+
*
|
|
33845
|
+
* 三种"读不出来"都归到**空簿 + 一条日志**,绝不抛(红线:插件起不来比丢配对更糟):
|
|
33846
|
+
* 文件不存在(第一次跑)、JSON 坏了(半写/被手改)、形状不认识(版本升级)。
|
|
33847
|
+
* `hostId` 不匹配也算读不出来:那份文件属于另一台主机。
|
|
33848
|
+
*/
|
|
33849
|
+
load() {
|
|
33850
|
+
const { file: file2, hostId, log } = this.options;
|
|
33851
|
+
if (!file2) return [];
|
|
33852
|
+
let raw;
|
|
33853
|
+
try {
|
|
33854
|
+
raw = readFileSync2(file2, "utf8");
|
|
33855
|
+
} catch {
|
|
33856
|
+
return [];
|
|
33857
|
+
}
|
|
33858
|
+
const parsed = storeSchema.safeParse(safeJsonParse(raw));
|
|
33859
|
+
if (!parsed.success) {
|
|
33860
|
+
log?.("pair-store unreadable, starting with an empty book", {
|
|
33861
|
+
reason: parsed.error.issues[0]?.message?.slice(0, 80) ?? "shape mismatch"
|
|
33862
|
+
});
|
|
33863
|
+
return [];
|
|
33864
|
+
}
|
|
33865
|
+
if (parsed.data.hostId !== hostId) {
|
|
33866
|
+
log?.("pair-store belongs to another host, starting with an empty book", {
|
|
33867
|
+
fileHostId: parsed.data.hostId.slice(0, 24)
|
|
33868
|
+
});
|
|
33869
|
+
return [];
|
|
33870
|
+
}
|
|
33871
|
+
this.savedAt = parsed.data.savedAt;
|
|
33872
|
+
return parsed.data.conversations;
|
|
33873
|
+
}
|
|
33874
|
+
/**
|
|
33875
|
+
* 写盘。**结构性的变化(新建/作废会话)必须立刻调这个**;
|
|
33876
|
+
* 只改了 `lastActivityAt`/`seqHost` 的场合走 {@link markDirty},由 3 秒 tick 批量落。
|
|
33877
|
+
*
|
|
33878
|
+
* 失败只记日志:写不进盘(目录只读、磁盘满)时,本次运行仍能正常收发,
|
|
33879
|
+
* 只是下次重启要重新扫码 —— 那是可接受的退化,让插件崩掉不是。
|
|
33880
|
+
*/
|
|
33881
|
+
save(conversations, now) {
|
|
33882
|
+
const { file: file2, hostId, log } = this.options;
|
|
33883
|
+
if (!file2) return false;
|
|
33884
|
+
const body = JSON.stringify({ version: 1, hostId, savedAt: now, conversations }) + "\n";
|
|
33885
|
+
const tmp = `${file2}.tmp-${process.pid}`;
|
|
33886
|
+
try {
|
|
33887
|
+
mkdirSync3(path4.dirname(file2), { recursive: true });
|
|
33888
|
+
writeFileSync3(tmp, body, { encoding: "utf8", mode: 384 });
|
|
33889
|
+
chmodSync2(tmp, 384);
|
|
33890
|
+
renameSync2(tmp, file2);
|
|
33891
|
+
this.savedAt = now;
|
|
33892
|
+
this.dirty = false;
|
|
33893
|
+
return true;
|
|
33894
|
+
} catch (error62) {
|
|
33895
|
+
log?.("pair-store save failed (pairing will need a rescan after the next restart)", {
|
|
33896
|
+
message: String(error62?.message ?? error62).slice(0, 120)
|
|
33897
|
+
});
|
|
33898
|
+
try {
|
|
33899
|
+
unlinkSync(tmp);
|
|
33900
|
+
} catch {
|
|
33901
|
+
}
|
|
33902
|
+
return false;
|
|
33903
|
+
}
|
|
33904
|
+
}
|
|
33905
|
+
};
|
|
33906
|
+
function safeJsonParse(raw) {
|
|
33907
|
+
try {
|
|
33908
|
+
return JSON.parse(raw);
|
|
33909
|
+
} catch {
|
|
33910
|
+
return void 0;
|
|
33911
|
+
}
|
|
33912
|
+
}
|
|
33913
|
+
|
|
33090
33914
|
// src/shell/config.ts
|
|
33091
33915
|
import { homedir } from "node:os";
|
|
33092
|
-
import
|
|
33916
|
+
import path5 from "node:path";
|
|
33093
33917
|
var DEFAULT_CONFIG = {
|
|
33094
33918
|
enabled: true,
|
|
33095
33919
|
serverUrl: "ws://127.0.0.1:8787",
|
|
@@ -33099,19 +33923,44 @@ var DEFAULT_CONFIG = {
|
|
|
33099
33923
|
hostLabel: "dsh-host",
|
|
33100
33924
|
keepAwake: { enabled: true, idleReleaseSec: 300, keepDisplay: false },
|
|
33101
33925
|
mockBridge: false,
|
|
33102
|
-
statusFile:
|
|
33926
|
+
statusFile: path5.join(homedir(), ".dsh", "dsh-remote-control", "status.json"),
|
|
33103
33927
|
unarchiveOnPrompt: true,
|
|
33104
33928
|
pairOnStartSec: 0,
|
|
33105
33929
|
pairTtlMs: 12e4,
|
|
33106
33930
|
carrierGraceMs: 5e3,
|
|
33107
|
-
takeOverQuestions: false,
|
|
33108
33931
|
approvalTimeoutSec: 180,
|
|
33109
33932
|
listingRefreshSec: 15,
|
|
33110
33933
|
conversationIdleTtlSec: 86400,
|
|
33934
|
+
// 恢复出来的会话按 7 天剪,与中继的 DRC_CONV_IDLE_TTL_MS 对齐(理由见该字段注释)。
|
|
33935
|
+
restoredIdleTtlSec: 604800,
|
|
33936
|
+
// 密钥簿落盘:默认跟着 statusFile 走(见 `resolvePairStoreFile`)。
|
|
33937
|
+
pairStoreFile: "",
|
|
33938
|
+
// 图片附件落盘:默认 status.json 同目录的 uploads/(见 `resolveUploadDir`)。
|
|
33939
|
+
uploadDir: "",
|
|
33940
|
+
maxImageBytes: 4 * 1024 * 1024,
|
|
33111
33941
|
// 配对的唯一入口。关掉它 = 这台主机**没有**配对入口(`/drc pair` 与文本二维码都在
|
|
33112
33942
|
// 2026-10-03 删掉了),所以 index.ts 会把它记成一条 warn 而不是安静地什么都不做。
|
|
33113
33943
|
pill: { enabled: true }
|
|
33114
33944
|
};
|
|
33945
|
+
var PAIR_STORE_OFF = "off";
|
|
33946
|
+
function resolvePairStoreFile(config2, hostId) {
|
|
33947
|
+
if (config2.pairStoreFile === PAIR_STORE_OFF) return "";
|
|
33948
|
+
const configured = config2.pairStoreFile.trim();
|
|
33949
|
+
if (!configured) return defaultPairStoreFile(config2.statusFile, hostId);
|
|
33950
|
+
if (configured.startsWith("/")) return configured;
|
|
33951
|
+
if (!config2.statusFile) return configured;
|
|
33952
|
+
return path5.join(path5.dirname(config2.statusFile), configured);
|
|
33953
|
+
}
|
|
33954
|
+
function resolveUploadDir(config2) {
|
|
33955
|
+
const configured = config2.uploadDir.trim();
|
|
33956
|
+
if (!configured) {
|
|
33957
|
+
const base = config2.statusFile ? path5.dirname(config2.statusFile) : path5.join(homedir(), ".dsh", "dsh-remote-control");
|
|
33958
|
+
return path5.join(base, "uploads");
|
|
33959
|
+
}
|
|
33960
|
+
if (configured.startsWith("/")) return configured;
|
|
33961
|
+
if (!config2.statusFile) return configured;
|
|
33962
|
+
return path5.join(path5.dirname(config2.statusFile), configured);
|
|
33963
|
+
}
|
|
33115
33964
|
var loopback = /^wss?:\/\/(localhost|127\.0\.0\.1|\[::1\])(:|\/|$)/;
|
|
33116
33965
|
var wsUrl = /^wss?:\/\/.+/;
|
|
33117
33966
|
function envSeconds(value) {
|
|
@@ -33139,7 +33988,6 @@ function readConfig(injected, env = process.env) {
|
|
|
33139
33988
|
if (fromEnv("DRC_PAIR_ON_START_SEC")) merged.pairOnStartSec = envSeconds(env.DRC_PAIR_ON_START_SEC);
|
|
33140
33989
|
if (fromEnv("DRC_PILL")) merged.pill.enabled = envFlag(env.DRC_PILL, merged.pill.enabled);
|
|
33141
33990
|
if (fromEnv("DRC_MOCK_BRIDGE") === "1") merged.mockBridge = true;
|
|
33142
|
-
if (fromEnv("DRC_TAKE_OVER_QUESTIONS") === "1") merged.takeOverQuestions = true;
|
|
33143
33991
|
if (merged.hostTokenEnv) merged.hostToken = env[merged.hostTokenEnv] ?? merged.hostToken;
|
|
33144
33992
|
return merged;
|
|
33145
33993
|
}
|
|
@@ -33196,6 +34044,14 @@ function validateConfig(config2) {
|
|
|
33196
34044
|
});
|
|
33197
34045
|
config2.conversationIdleTtlSec = 86400;
|
|
33198
34046
|
}
|
|
34047
|
+
if (!secondsSchema.safeParse(config2.restoredIdleTtlSec).success || config2.restoredIdleTtlSec <= 0) {
|
|
34048
|
+
problems.push({
|
|
34049
|
+
level: "warn",
|
|
34050
|
+
field: "restoredIdleTtlSec",
|
|
34051
|
+
message: '\u5FC5\u987B\u662F\u6B63\u6570\u79D2\uFF1B\u8FD9\u91CC\u4E0D\u63A5\u53D7"\u6C38\u4E0D\u526A\u679D"\uFF08\u65E0\u754C\u7684 PSK \u7C3F\u662F\u590D\u6838 \u{1F7E1}8 \u7684\u539F\u59CB\u7F3A\u9677\uFF09\uFF0C\u5DF2\u5939\u56DE 604800'
|
|
34052
|
+
});
|
|
34053
|
+
config2.restoredIdleTtlSec = 604800;
|
|
34054
|
+
}
|
|
33199
34055
|
if (config2.pairOnStartSec > 0) {
|
|
33200
34056
|
problems.push({
|
|
33201
34057
|
level: "warn",
|
|
@@ -33206,13 +34062,6 @@ function validateConfig(config2) {
|
|
|
33206
34062
|
if (!config2.statusFile) {
|
|
33207
34063
|
problems.push({ level: "warn", field: "statusFile", message: "\u4E3A\u7A7A\u5C06\u5173\u95ED\u72B6\u6001\u5FEB\u7167\uFF1BGUI \u5BBF\u4E3B\u91CC\u8FD9\u662F\u552F\u4E00\u7684\u6392\u9519\u5165\u53E3" });
|
|
33208
34064
|
}
|
|
33209
|
-
if (config2.takeOverQuestions) {
|
|
33210
|
-
problems.push({
|
|
33211
|
-
level: "warn",
|
|
33212
|
-
field: "takeOverQuestions",
|
|
33213
|
-
message: "\u63D0\u95EE\u63A5\u7BA1\u4F1A**\u53D6\u4EE3\u684C\u9762 UI \u7684\u63D0\u95EE\u80FD\u529B**\uFF08ctx.userQuestions \u53EA\u5141\u8BB8\u4E00\u4E2A\u6D3B\u8DC3 provider\uFF09\uFF1B\u624B\u673A\u7AEF\u4E0D\u5728\u7EBF\u65F6\u63D0\u95EE\u4F1A\u76F4\u63A5\u5931\u8D25"
|
|
33214
|
-
});
|
|
33215
|
-
}
|
|
33216
34065
|
return problems;
|
|
33217
34066
|
}
|
|
33218
34067
|
function redact(value) {
|
|
@@ -33221,7 +34070,7 @@ function redact(value) {
|
|
|
33221
34070
|
}
|
|
33222
34071
|
|
|
33223
34072
|
// src/pill/routes.ts
|
|
33224
|
-
import { createHash as
|
|
34073
|
+
import { createHash as createHash3 } from "node:crypto";
|
|
33225
34074
|
|
|
33226
34075
|
// src/platform/qr.ts
|
|
33227
34076
|
var import_qrcode = __toESM(require_lib(), 1);
|
|
@@ -33238,10 +34087,11 @@ async function qrPng(text, options = {}) {
|
|
|
33238
34087
|
var PAIR_NEW_ROUTE = "/plugins/dsh-remote-control/pairing/new";
|
|
33239
34088
|
var PAIR_IMAGE_ROUTE = "/plugins/dsh-remote-control/pairing.png";
|
|
33240
34089
|
var PAIR_STATUS_ROUTE = "/plugins/dsh-remote-control/status";
|
|
34090
|
+
var PAIR_UNPAIR_ROUTE = "/plugins/dsh-remote-control/unpair";
|
|
33241
34091
|
var PAIR_MARKER_HEADER = "x-drc-pair";
|
|
33242
34092
|
var PAIR_MARKER_VALUE = "1";
|
|
33243
34093
|
function pairingEpoch(qr) {
|
|
33244
|
-
return
|
|
34094
|
+
return createHash3("sha256").update(qr).digest("hex").slice(0, 16);
|
|
33245
34095
|
}
|
|
33246
34096
|
async function renderPairingPng(qr) {
|
|
33247
34097
|
return qrPng(qr);
|
|
@@ -33368,20 +34218,45 @@ function pillStatusHandler(deps) {
|
|
|
33368
34218
|
json2(response, 200, {
|
|
33369
34219
|
relay: status.relay,
|
|
33370
34220
|
paired: status.paired,
|
|
33371
|
-
|
|
33372
|
-
serverUrl: status.serverUrl
|
|
34221
|
+
pairings: status.pairings,
|
|
34222
|
+
serverUrl: status.serverUrl,
|
|
34223
|
+
version: status.version,
|
|
34224
|
+
waiting: status.waiting,
|
|
34225
|
+
waitingOldestSec: status.waitingOldestSec
|
|
33373
34226
|
});
|
|
33374
34227
|
} catch (error62) {
|
|
33375
34228
|
json2(response, 500, { error: String(error62?.message ?? error62).slice(0, 200) });
|
|
33376
34229
|
}
|
|
33377
34230
|
};
|
|
33378
34231
|
}
|
|
34232
|
+
function unpairHandler(deps) {
|
|
34233
|
+
return (request, response) => {
|
|
34234
|
+
if (request.method !== "POST") {
|
|
34235
|
+
json2(response, 405, { error: "method not allowed" });
|
|
34236
|
+
return;
|
|
34237
|
+
}
|
|
34238
|
+
const guard = rejectedBy(request);
|
|
34239
|
+
if (guard) {
|
|
34240
|
+
deps.log("\u89E3\u914D\u8BF7\u6C42\u88AB\u5B88\u536B\u62D2", { guard });
|
|
34241
|
+
json2(response, 403, { error: "request-not-trusted", guard });
|
|
34242
|
+
return;
|
|
34243
|
+
}
|
|
34244
|
+
try {
|
|
34245
|
+
const unpaired = deps.unpair();
|
|
34246
|
+
deps.log("\u7ECF\u72B6\u6001\u680F\u9000\u51FA\u914D\u5BF9", { unpaired });
|
|
34247
|
+
json2(response, 200, { state: "ok", unpaired });
|
|
34248
|
+
} catch (error62) {
|
|
34249
|
+
json2(response, 500, { error: String(error62?.message ?? error62).slice(0, 200) });
|
|
34250
|
+
}
|
|
34251
|
+
};
|
|
34252
|
+
}
|
|
33379
34253
|
function registerPillRoutes(webServer, deps) {
|
|
33380
34254
|
const off = [];
|
|
33381
34255
|
const definitions = [
|
|
33382
34256
|
{ kind: "exact", path: PAIR_NEW_ROUTE, handler: pairNewHandler(deps) },
|
|
33383
34257
|
{ kind: "exact", path: PAIR_IMAGE_ROUTE, handler: pairImageHandler(deps) },
|
|
33384
|
-
{ kind: "exact", path: PAIR_STATUS_ROUTE, handler: pillStatusHandler(deps) }
|
|
34258
|
+
{ kind: "exact", path: PAIR_STATUS_ROUTE, handler: pillStatusHandler(deps) },
|
|
34259
|
+
{ kind: "exact", path: PAIR_UNPAIR_ROUTE, handler: unpairHandler(deps) }
|
|
33385
34260
|
];
|
|
33386
34261
|
try {
|
|
33387
34262
|
for (const definition of definitions) off.push(webServer.register(definition));
|
|
@@ -33420,6 +34295,7 @@ function startPill(ctx, deps) {
|
|
|
33420
34295
|
ensureFresh: () => deps.ensureFresh(),
|
|
33421
34296
|
current: () => deps.current(),
|
|
33422
34297
|
status: () => deps.status(),
|
|
34298
|
+
unpair: () => deps.unpair(),
|
|
33423
34299
|
log
|
|
33424
34300
|
});
|
|
33425
34301
|
probe.routes = "registered";
|
|
@@ -33505,6 +34381,9 @@ function startPill(ctx, deps) {
|
|
|
33505
34381
|
};
|
|
33506
34382
|
}
|
|
33507
34383
|
|
|
34384
|
+
// src/version.ts
|
|
34385
|
+
var PLUGIN_VERSION = true ? "2.0.1" : "dev";
|
|
34386
|
+
|
|
33508
34387
|
// src/index.ts
|
|
33509
34388
|
var SERVICE_NAMES = [
|
|
33510
34389
|
"sessions",
|
|
@@ -33559,7 +34438,7 @@ function applyInner(ctx, injected) {
|
|
|
33559
34438
|
const clock = systemClock;
|
|
33560
34439
|
const oneShot = createOneShotTimers(clock);
|
|
33561
34440
|
const status = new StatusFile(config2.statusFile, clock);
|
|
33562
|
-
const
|
|
34441
|
+
const hostId = resolveHostId(config2.statusFile ? path6.dirname(config2.statusFile) : "", config2.hostId);
|
|
33563
34442
|
const log = (message, fields = {}) => {
|
|
33564
34443
|
try {
|
|
33565
34444
|
process.stdout.write(
|
|
@@ -33569,6 +34448,10 @@ function applyInner(ctx, injected) {
|
|
|
33569
34448
|
} catch {
|
|
33570
34449
|
}
|
|
33571
34450
|
};
|
|
34451
|
+
const pairStoreFile = resolvePairStoreFile(config2, hostId);
|
|
34452
|
+
const pairStore = new PairStore({ file: pairStoreFile, hostId, log });
|
|
34453
|
+
const restoredAtBoot = pairStoreFile ? pairStore.load() : [];
|
|
34454
|
+
const problems = validateConfig(config2);
|
|
33572
34455
|
for (const problem of problems) {
|
|
33573
34456
|
log(`config ${problem.level}`, { field: problem.field, message: problem.message });
|
|
33574
34457
|
}
|
|
@@ -33608,8 +34491,8 @@ function applyInner(ctx, injected) {
|
|
|
33608
34491
|
broadcast(payload) {
|
|
33609
34492
|
return relay ? relay.broadcast(payload) : 0;
|
|
33610
34493
|
},
|
|
33611
|
-
|
|
33612
|
-
return relay?.
|
|
34494
|
+
hasClient(conversationId) {
|
|
34495
|
+
return relay?.hasClient(conversationId) ?? false;
|
|
33613
34496
|
},
|
|
33614
34497
|
conversationIds() {
|
|
33615
34498
|
return relay?.conversationIds() ?? [];
|
|
@@ -33624,7 +34507,7 @@ function applyInner(ctx, injected) {
|
|
|
33624
34507
|
startedCarrier = port.carrier;
|
|
33625
34508
|
relay = new RelayClient({
|
|
33626
34509
|
url: config2.serverUrl,
|
|
33627
|
-
hostId
|
|
34510
|
+
hostId,
|
|
33628
34511
|
label: config2.hostLabel,
|
|
33629
34512
|
token: config2.hostToken,
|
|
33630
34513
|
clock,
|
|
@@ -33635,6 +34518,9 @@ function applyInner(ctx, injected) {
|
|
|
33635
34518
|
pairingWindow.applyServerTtl(token, ttlMs);
|
|
33636
34519
|
},
|
|
33637
34520
|
onPeerJoined: (conversationId, pairingToken2) => {
|
|
34521
|
+
for (const id of relay?.conversationIds() ?? []) {
|
|
34522
|
+
if (id !== conversationId) relay?.voidConversation(id);
|
|
34523
|
+
}
|
|
33638
34524
|
runtime?.start();
|
|
33639
34525
|
void runtime?.pushSessions("peer-joined");
|
|
33640
34526
|
if (pairingToken2) {
|
|
@@ -33655,19 +34541,46 @@ function applyInner(ctx, injected) {
|
|
|
33655
34541
|
},
|
|
33656
34542
|
// 配对通道长存(D3)之后必须自己剪枝:中继那边的空闲 TTL 是 7 天,
|
|
33657
34543
|
// 不剪就是无界的 PSK 簿 + 每次广播对废弃通道逐个密封。
|
|
33658
|
-
|
|
34544
|
+
// 两条 TTL 分开:新配对的按 24 小时剪,从盘上恢复的按 `restoredIdleTtlSec`
|
|
34545
|
+
// (默认 7 天,与中继对齐)—— 理由见 config.ts 与 keys.ts 的 `Conversation.restored`。
|
|
34546
|
+
prunePolicy: {
|
|
34547
|
+
idleTtlMs: config2.conversationIdleTtlSec * 1e3,
|
|
34548
|
+
restoredIdleTtlMs: config2.restoredIdleTtlSec * 1e3,
|
|
34549
|
+
maxConversations: MAX_CONVERSATIONS
|
|
34550
|
+
},
|
|
34551
|
+
// 结构性变化(新配对 / 作废)立刻落盘。只改了 seqHost 与 lastActivityAt 的场合
|
|
34552
|
+
// 由 status 的 3 秒 tick 批量落(见下面 `pairStore.markDirty()` 那一段)——
|
|
34553
|
+
// 挂在这里等于每广播一次写一次盘。
|
|
34554
|
+
onStructuralChange: () => {
|
|
34555
|
+
if (!pairStoreFile || !relay) return;
|
|
34556
|
+
pairStore.save(relay.conversations.snapshot(), clock.now());
|
|
34557
|
+
},
|
|
34558
|
+
onBookActivity: () => {
|
|
34559
|
+
pairStore.markDirty();
|
|
34560
|
+
}
|
|
33659
34561
|
});
|
|
33660
34562
|
runtime = new HostRuntime(port, transport, sleep, clock, {
|
|
33661
34563
|
listingRefreshMs: config2.listingRefreshSec * 1e3,
|
|
33662
34564
|
unarchiveOnPrompt: config2.unarchiveOnPrompt,
|
|
34565
|
+
uploadDir: resolveUploadDir(config2),
|
|
34566
|
+
maxImageBytes: config2.maxImageBytes,
|
|
33663
34567
|
approvalTimeoutMs: config2.approvalTimeoutSec * 1e3,
|
|
33664
34568
|
log: (message, fields) => log(message, fields)
|
|
33665
34569
|
});
|
|
34570
|
+
const restoredIds = relay.restoreConversations(restoredAtBoot);
|
|
34571
|
+
if (restoredIds.length > 0) {
|
|
34572
|
+
runtime.start();
|
|
34573
|
+
log("runtime started from restored conversations", { conversations: restoredIds.length });
|
|
34574
|
+
}
|
|
33666
34575
|
relay.connect();
|
|
33667
34576
|
pairingWindow.tick();
|
|
33668
34577
|
status.start(() => {
|
|
33669
34578
|
pairingWindow.tick();
|
|
33670
34579
|
relay?.pruneConversations();
|
|
34580
|
+
if (pairStoreFile && pairStore.hasPendingChanges && relay) {
|
|
34581
|
+
pairStore.save(relay.conversations.snapshot(), clock.now());
|
|
34582
|
+
}
|
|
34583
|
+
slots.prune();
|
|
33671
34584
|
return state();
|
|
33672
34585
|
});
|
|
33673
34586
|
};
|
|
@@ -33692,17 +34605,46 @@ function applyInner(ctx, injected) {
|
|
|
33692
34605
|
carrier: startedCarrier || (config2.mockBridge ? "mock" : "probing"),
|
|
33693
34606
|
serverUrl: config2.serverUrl,
|
|
33694
34607
|
hostLabel: config2.hostLabel,
|
|
34608
|
+
// 主机身份。中继日志里认的就是它,所以"手机连不上/丢帧涨"第一件事是看这个值
|
|
34609
|
+
// 在两次重启之间有没有变(变了就是身份没稳住,见 shell/host-id.ts)。
|
|
34610
|
+
hostId,
|
|
33695
34611
|
relay: relay ? relayState() : "idle",
|
|
33696
34612
|
relayProblem: relayProblem(),
|
|
33697
34613
|
conversations: relay?.conversationCount ?? 0,
|
|
34614
|
+
/**
|
|
34615
|
+
* 当前连着的手机台数(去重的 clientId)。
|
|
34616
|
+
*
|
|
34617
|
+
* 为什么必须与 `conversations` 分开报:「解除配对 → 重新配对」会开一条**新会话**,
|
|
34618
|
+
* 而手机还是那一台。所以拿会话数回答"几台手机连着",用户解完配对看到数字反而变大 ——
|
|
34619
|
+
* 那个现象与"我的解配没生效"完全一致,最容易让人把解配按钮当坏的。
|
|
34620
|
+
* `pendingPairs` 是另一个东西:那是**还挂着、还没被用掉的配对码**。
|
|
34621
|
+
*/
|
|
34622
|
+
pairedClients: relay?.clientCount ?? 0,
|
|
33698
34623
|
pendingPairs: slots.size,
|
|
34624
|
+
/**
|
|
34625
|
+
* 密钥簿落盘的排障面。
|
|
34626
|
+
*
|
|
34627
|
+
* `restored` 是"本次启动从盘上恢复了几条会话"——它与 `conversations` 的差值
|
|
34628
|
+
* 就是"落盘没生效"的判据:`conversations > 0 && restored === 0 && pairStore.enabled`
|
|
34629
|
+
* 意味着这些会话是本次运行新配出来的(正常),反过来
|
|
34630
|
+
* `restored > 0` 而 `conversations === 0` 才是异常。
|
|
34631
|
+
*
|
|
34632
|
+
* **`lastSavedAt` 与路径都不含 PSK**:这份快照会被外部脚本读,
|
|
34633
|
+
* 文件路径本身也不是凭据(里面的内容才是)。
|
|
34634
|
+
*/
|
|
34635
|
+
pairStore: {
|
|
34636
|
+
enabled: pairStoreFile !== "",
|
|
34637
|
+
file: pairStoreFile || void 0,
|
|
34638
|
+
restored: restoredAtBoot.length,
|
|
34639
|
+
lastSavedAt: pairStoreFile && pairStore.lastSavedAt > 0 ? new Date(pairStore.lastSavedAt).toISOString() : void 0
|
|
34640
|
+
},
|
|
33699
34641
|
pairing: describeActivePairing(),
|
|
33700
34642
|
keepAwake: snapshot,
|
|
33701
34643
|
kernel: kernel?.describe() ?? null,
|
|
33702
34644
|
// 出站计数:手机上"没收到 X"的第一现场对比点(插件没发 vs 发了但路上丢了)。
|
|
33703
34645
|
outbound: runtime?.stats ?? null,
|
|
33704
34646
|
probe,
|
|
33705
|
-
// 配对入口(那颗 pill
|
|
34647
|
+
// 配对入口(那颗 pill 的四条路由)为什么没起,是"配不了对"唯一的排查入口:
|
|
33706
34648
|
// 这里直接把软探测的结果带出来。
|
|
33707
34649
|
pill: pillRoutes?.probe ?? { webServer: "not-started" },
|
|
33708
34650
|
// token 永远不出现真值;这条有测试锁住。
|
|
@@ -33752,22 +34694,37 @@ function applyInner(ctx, injected) {
|
|
|
33752
34694
|
function currentPairing() {
|
|
33753
34695
|
const shown = active.pairing;
|
|
33754
34696
|
if (!shown) return null;
|
|
33755
|
-
|
|
33756
|
-
|
|
34697
|
+
const slot = slots.resolveFor(shown.token);
|
|
34698
|
+
if (!slot) return null;
|
|
34699
|
+
return { qr: shown.qr, token: shown.token, expiresAt: slot.expiresAt };
|
|
33757
34700
|
}
|
|
33758
34701
|
function ensureFreshPairing() {
|
|
33759
34702
|
const fresh = currentPairing() ?? createPairing();
|
|
33760
34703
|
if (!fresh) return null;
|
|
34704
|
+
if (fresh.expiresAt <= Date.now()) return createPairing();
|
|
33761
34705
|
return { qr: fresh.qr, token: fresh.token, expiresAt: fresh.expiresAt };
|
|
33762
34706
|
}
|
|
33763
34707
|
function pillStatus() {
|
|
34708
|
+
const waiting = runtime?.waiting ?? { count: 0, oldestSec: 0 };
|
|
33764
34709
|
return {
|
|
33765
34710
|
relay: relay ? relayState() : "idle",
|
|
33766
|
-
paired: relay?.
|
|
33767
|
-
|
|
33768
|
-
|
|
34711
|
+
paired: relay?.clientCount ?? 0,
|
|
34712
|
+
// 配对簿里几条会话 = 配对还在不在。为什么必须与 paired 分开:pair-store 之后
|
|
34713
|
+
// 配对按 D3 长存、落盘,手机退后台只是 socket 断——只用 clientCount 回答 配没配上,
|
|
34714
|
+
// 用户把小程序放进后台的每一分钟,桌面那颗 pill 都会说 未配对,点开还会烧一张
|
|
34715
|
+
// 不需要的新码(2026-10-04 用户报的误解)。
|
|
34716
|
+
pairings: relay?.conversationCount ?? 0,
|
|
34717
|
+
serverUrl: config2.serverUrl,
|
|
34718
|
+
version: PLUGIN_VERSION,
|
|
34719
|
+
waiting: waiting.count,
|
|
34720
|
+
waitingOldestSec: waiting.oldestSec
|
|
33769
34721
|
};
|
|
33770
34722
|
}
|
|
34723
|
+
function unpairAll() {
|
|
34724
|
+
const ids = relay?.conversationIds() ?? [];
|
|
34725
|
+
for (const id of ids) relay?.voidConversation(id);
|
|
34726
|
+
return ids.length;
|
|
34727
|
+
}
|
|
33771
34728
|
const pairingWindow = new PairingWindow(
|
|
33772
34729
|
{
|
|
33773
34730
|
clock,
|
|
@@ -33798,7 +34755,7 @@ function applyInner(ctx, injected) {
|
|
|
33798
34755
|
return;
|
|
33799
34756
|
}
|
|
33800
34757
|
decided = true;
|
|
33801
|
-
start(createServicesKernel(collected, { log,
|
|
34758
|
+
start(createServicesKernel(collected, { log, clock }));
|
|
33802
34759
|
return;
|
|
33803
34760
|
}
|
|
33804
34761
|
};
|
|
@@ -34025,10 +34982,11 @@ function applyInner(ctx, injected) {
|
|
|
34025
34982
|
try {
|
|
34026
34983
|
pillRoutes = startPill(ctx, {
|
|
34027
34984
|
enabled: config2.pill.enabled,
|
|
34028
|
-
//
|
|
34985
|
+
// 四条路由要的四件事:幂等发码、只读看当前码、读连接状态、退出配对。
|
|
34029
34986
|
ensureFresh: () => ensureFreshPairing(),
|
|
34030
34987
|
current: () => currentPairing(),
|
|
34031
34988
|
status: () => pillStatus(),
|
|
34989
|
+
unpair: () => unpairAll(),
|
|
34032
34990
|
log
|
|
34033
34991
|
});
|
|
34034
34992
|
} catch (error62) {
|
|
@@ -34037,46 +34995,6 @@ function applyInner(ctx, injected) {
|
|
|
34037
34995
|
message: String(error62?.message ?? error62).slice(0, 200)
|
|
34038
34996
|
});
|
|
34039
34997
|
}
|
|
34040
|
-
const commandDefinition = {
|
|
34041
|
-
name: "drc",
|
|
34042
|
-
description: "DSH \u8FDC\u7A0B\u63A7\u5236\uFF1A\u914D\u5BF9\u8BF7\u70B9\u72B6\u6001\u680F\u7684 dsh-remote-control\uFF1B/drc status \u770B\u8FDE\u63A5\u4E0E\u95EE\u9898\uFF0C/drc unpair \u89E3\u914D",
|
|
34043
|
-
input: { hint: "status | unpair" },
|
|
34044
|
-
handler: async (invocation) => {
|
|
34045
|
-
const argument = String(invocation.rawInput ?? "").trim().toLowerCase();
|
|
34046
|
-
if (argument.startsWith("unpair")) {
|
|
34047
|
-
for (const id of relay?.conversationIds() ?? []) relay?.voidConversation(id);
|
|
34048
|
-
return { kind: "success", text: '\u5DF2\u89E3\u9664\u6240\u6709\u914D\u5BF9\uFF1B\u624B\u673A\u7AEF\u4F1A\u663E\u793A"\u4E3B\u673A\u5DF2\u65AD\u5F00\uFF0C\u8BF7\u91CD\u65B0\u914D\u5BF9"\u3002' };
|
|
34049
|
-
}
|
|
34050
|
-
return { kind: "success", text: JSON.stringify(state(), null, 2) };
|
|
34051
|
-
}
|
|
34052
|
-
};
|
|
34053
|
-
let commandsTaken = false;
|
|
34054
|
-
const takeCommands = (source) => {
|
|
34055
|
-
if (commandsTaken || !source || typeof source !== "object") return;
|
|
34056
|
-
const holder = source;
|
|
34057
|
-
if (typeof holder.register !== "function") return;
|
|
34058
|
-
holder.register(commandDefinition);
|
|
34059
|
-
commandsTaken = true;
|
|
34060
|
-
probe.commands = "registered";
|
|
34061
|
-
};
|
|
34062
|
-
try {
|
|
34063
|
-
takeCommands(ctx.commands);
|
|
34064
|
-
} catch {
|
|
34065
|
-
}
|
|
34066
|
-
try {
|
|
34067
|
-
takeCommands(ctx.get?.("commands", true));
|
|
34068
|
-
} catch {
|
|
34069
|
-
}
|
|
34070
|
-
try {
|
|
34071
|
-
const child = ctx.inject?.(["commands"], ((...cbArgs) => {
|
|
34072
|
-
for (const candidate of [cbArgs[0], ...cbArgs, ctx]) {
|
|
34073
|
-
takeCommands(pickFrom(candidate, "commands"));
|
|
34074
|
-
if (commandsTaken) break;
|
|
34075
|
-
}
|
|
34076
|
-
}));
|
|
34077
|
-
if (child !== void 0) injectChildren.push(["commands", child]);
|
|
34078
|
-
} catch {
|
|
34079
|
-
}
|
|
34080
34998
|
return {
|
|
34081
34999
|
config: config2,
|
|
34082
35000
|
state,
|
|
@@ -34085,6 +35003,9 @@ function applyInner(ctx, injected) {
|
|
|
34085
35003
|
oneShot.cancelAll();
|
|
34086
35004
|
pillRoutes?.stop();
|
|
34087
35005
|
status.stop();
|
|
35006
|
+
if (pairStoreFile && relay && pairStore.hasPendingChanges) {
|
|
35007
|
+
pairStore.save(relay.conversations.snapshot(), clock.now());
|
|
35008
|
+
}
|
|
34088
35009
|
runtime?.stop();
|
|
34089
35010
|
relay?.stop();
|
|
34090
35011
|
sleep.stop();
|