dsh-remote-control 1.1.0 → 2.0.0
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/cordis.patch.yml +3 -5
- package/dist/bundle/client.cjs +485 -76
- package/dist/bundle/index.js +1218 -686
- package/package.json +6 -5
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 path6 = 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 < path6.length - 1; i++) {
|
|
7492
|
+
optimizedSegs.push(graph.table[path6[i]].node);
|
|
7493
7493
|
}
|
|
7494
7494
|
return exports.fromArray(mergeSegments(optimizedSegs));
|
|
7495
7495
|
};
|
|
@@ -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(path6, 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 = fs.createWriteStream(
|
|
9980
|
+
const stream = fs.createWriteStream(path6);
|
|
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(path6, qrData, options, cb) {
|
|
10043
10043
|
if (typeof cb === "undefined") {
|
|
10044
10044
|
cb = options;
|
|
10045
10045
|
options = void 0;
|
|
10046
10046
|
}
|
|
10047
10047
|
const fs = __require("fs");
|
|
10048
10048
|
const utf82 = exports.render(qrData, options);
|
|
10049
|
-
fs.writeFile(
|
|
10049
|
+
fs.writeFile(path6, 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 path6 = "";
|
|
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
|
+
path6 += 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
|
+
path6 += svgCmd("h", lineLength);
|
|
10187
10187
|
lineLength = 0;
|
|
10188
10188
|
}
|
|
10189
10189
|
} else {
|
|
10190
10190
|
moveBy++;
|
|
10191
10191
|
}
|
|
10192
10192
|
}
|
|
10193
|
-
return
|
|
10193
|
+
return path6;
|
|
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 path6 = "<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 + path6 + "</svg>\n";
|
|
10205
10205
|
if (typeof cb === "function") {
|
|
10206
10206
|
cb(null, svgTag);
|
|
10207
10207
|
}
|
|
@@ -10215,7 +10215,7 @@ 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(path6, qrData, options, cb) {
|
|
10219
10219
|
if (typeof cb === "undefined") {
|
|
10220
10220
|
cb = options;
|
|
10221
10221
|
options = void 0;
|
|
@@ -10223,7 +10223,7 @@ var require_svg = __commonJS({
|
|
|
10223
10223
|
const fs = __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
|
-
fs.writeFile(
|
|
10226
|
+
fs.writeFile(path6, xmlStr, cb);
|
|
10227
10227
|
};
|
|
10228
10228
|
}
|
|
10229
10229
|
});
|
|
@@ -10285,7 +10285,7 @@ var require_canvas = __commonJS({
|
|
|
10285
10285
|
var require_browser = __commonJS({
|
|
10286
10286
|
"../../node_modules/.pnpm/qrcode@1.5.4/node_modules/qrcode/lib/browser.js"(exports) {
|
|
10287
10287
|
var canPromise = require_can_promise();
|
|
10288
|
-
var
|
|
10288
|
+
var QRCode2 = require_qrcode();
|
|
10289
10289
|
var CanvasRenderer = require_canvas();
|
|
10290
10290
|
var SvgRenderer = require_svg_tag();
|
|
10291
10291
|
function renderCanvas(renderFunc, canvas, text, opts, cb) {
|
|
@@ -10328,7 +10328,7 @@ var require_browser = __commonJS({
|
|
|
10328
10328
|
}
|
|
10329
10329
|
return new Promise(function(resolve, reject) {
|
|
10330
10330
|
try {
|
|
10331
|
-
const data =
|
|
10331
|
+
const data = QRCode2.create(text, opts);
|
|
10332
10332
|
resolve(renderFunc(data, canvas, opts));
|
|
10333
10333
|
} catch (e) {
|
|
10334
10334
|
reject(e);
|
|
@@ -10336,13 +10336,13 @@ var require_browser = __commonJS({
|
|
|
10336
10336
|
});
|
|
10337
10337
|
}
|
|
10338
10338
|
try {
|
|
10339
|
-
const data =
|
|
10339
|
+
const data = QRCode2.create(text, opts);
|
|
10340
10340
|
cb(null, renderFunc(data, canvas, opts));
|
|
10341
10341
|
} catch (e) {
|
|
10342
10342
|
cb(e);
|
|
10343
10343
|
}
|
|
10344
10344
|
}
|
|
10345
|
-
exports.create =
|
|
10345
|
+
exports.create = QRCode2.create;
|
|
10346
10346
|
exports.toCanvas = renderCanvas.bind(null, CanvasRenderer.render);
|
|
10347
10347
|
exports.toDataURL = renderCanvas.bind(null, CanvasRenderer.renderToDataURL);
|
|
10348
10348
|
exports.toString = renderCanvas.bind(null, function(data, _, opts) {
|
|
@@ -10355,7 +10355,7 @@ var require_browser = __commonJS({
|
|
|
10355
10355
|
var require_server = __commonJS({
|
|
10356
10356
|
"../../node_modules/.pnpm/qrcode@1.5.4/node_modules/qrcode/lib/server.js"(exports) {
|
|
10357
10357
|
var canPromise = require_can_promise();
|
|
10358
|
-
var
|
|
10358
|
+
var QRCode2 = require_qrcode();
|
|
10359
10359
|
var PngRenderer = require_png2();
|
|
10360
10360
|
var Utf8Renderer = require_utf8();
|
|
10361
10361
|
var TerminalRenderer = require_terminal2();
|
|
@@ -10381,8 +10381,8 @@ var require_server = __commonJS({
|
|
|
10381
10381
|
cb
|
|
10382
10382
|
};
|
|
10383
10383
|
}
|
|
10384
|
-
function getTypeFromFilename(
|
|
10385
|
-
return
|
|
10384
|
+
function getTypeFromFilename(path6) {
|
|
10385
|
+
return path6.slice((path6.lastIndexOf(".") - 1 >>> 0) + 2).toLowerCase();
|
|
10386
10386
|
}
|
|
10387
10387
|
function getRendererFromType(type) {
|
|
10388
10388
|
switch (type) {
|
|
@@ -10412,7 +10412,7 @@ var require_server = __commonJS({
|
|
|
10412
10412
|
if (!params.cb) {
|
|
10413
10413
|
return new Promise(function(resolve, reject) {
|
|
10414
10414
|
try {
|
|
10415
|
-
const data =
|
|
10415
|
+
const data = QRCode2.create(text, params.opts);
|
|
10416
10416
|
return renderFunc(data, params.opts, function(err, data2) {
|
|
10417
10417
|
return err ? reject(err) : resolve(data2);
|
|
10418
10418
|
});
|
|
@@ -10422,13 +10422,13 @@ var require_server = __commonJS({
|
|
|
10422
10422
|
});
|
|
10423
10423
|
}
|
|
10424
10424
|
try {
|
|
10425
|
-
const data =
|
|
10425
|
+
const data = QRCode2.create(text, params.opts);
|
|
10426
10426
|
return renderFunc(data, params.opts, params.cb);
|
|
10427
10427
|
} catch (e) {
|
|
10428
10428
|
params.cb(e);
|
|
10429
10429
|
}
|
|
10430
10430
|
}
|
|
10431
|
-
exports.create =
|
|
10431
|
+
exports.create = QRCode2.create;
|
|
10432
10432
|
exports.toCanvas = require_browser().toCanvas;
|
|
10433
10433
|
exports.toString = function toString(text, opts, cb) {
|
|
10434
10434
|
const params = checkParams(text, opts, cb);
|
|
@@ -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(path6, text, opts, cb) {
|
|
10450
|
+
if (typeof path6 !== "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(path6);
|
|
10458
10458
|
const renderer = getRendererFromType(type);
|
|
10459
|
-
const renderToFile = renderer.renderToFile.bind(null,
|
|
10459
|
+
const renderToFile = renderer.renderToFile.bind(null, path6);
|
|
10460
10460
|
return render(renderToFile, text, params);
|
|
10461
10461
|
};
|
|
10462
10462
|
exports.toFileStream = function toFileStream(stream, text, opts) {
|
|
@@ -10655,10 +10655,9 @@ var init_mock_kernel = __esm({
|
|
|
10655
10655
|
});
|
|
10656
10656
|
|
|
10657
10657
|
// src/index.ts
|
|
10658
|
-
import
|
|
10659
|
-
import { randomBytes as randomBytes4 } from "node:crypto";
|
|
10658
|
+
import path5 from "node:path";
|
|
10660
10659
|
|
|
10661
|
-
// ../../node_modules/.pnpm/dsh-remote-wire@1.
|
|
10660
|
+
// ../../node_modules/.pnpm/dsh-remote-wire@1.2.0/node_modules/dsh-remote-wire/dist/src/crypto/bytes.js
|
|
10662
10661
|
import { Buffer as Buffer2 } from "node:buffer";
|
|
10663
10662
|
var encoder = new TextEncoder();
|
|
10664
10663
|
var decoder = new TextDecoder("utf-8", { fatal: false });
|
|
@@ -10685,7 +10684,7 @@ function concat(parts) {
|
|
|
10685
10684
|
return out;
|
|
10686
10685
|
}
|
|
10687
10686
|
|
|
10688
|
-
// ../../node_modules/.pnpm/dsh-remote-wire@1.
|
|
10687
|
+
// ../../node_modules/.pnpm/dsh-remote-wire@1.2.0/node_modules/dsh-remote-wire/dist/src/crypto/keys.js
|
|
10689
10688
|
import { createHash, randomBytes } from "node:crypto";
|
|
10690
10689
|
var KDF_NAMESPACE = "dsh-rc/v1";
|
|
10691
10690
|
var UNIT_SEPARATOR = 31;
|
|
@@ -10715,7 +10714,7 @@ function randomNonce() {
|
|
|
10715
10714
|
return new Uint8Array(randomBytes(NONCE_BYTES));
|
|
10716
10715
|
}
|
|
10717
10716
|
|
|
10718
|
-
// ../../node_modules/.pnpm/dsh-remote-wire@1.
|
|
10717
|
+
// ../../node_modules/.pnpm/dsh-remote-wire@1.2.0/node_modules/dsh-remote-wire/dist/src/crypto/record.js
|
|
10719
10718
|
var import_tweetnacl = __toESM(require_nacl_fast(), 1);
|
|
10720
10719
|
var MAC_BYTES = 16;
|
|
10721
10720
|
var MIN_RECORD_BYTES = NONCE_BYTES + MAC_BYTES;
|
|
@@ -10748,7 +10747,7 @@ function open(key, record2) {
|
|
|
10748
10747
|
}
|
|
10749
10748
|
}
|
|
10750
10749
|
|
|
10751
|
-
// ../../node_modules/.pnpm/dsh-remote-wire@1.
|
|
10750
|
+
// ../../node_modules/.pnpm/dsh-remote-wire@1.2.0/node_modules/dsh-remote-wire/dist/src/identity/pairing.js
|
|
10752
10751
|
import { randomBytes as randomBytes2 } from "node:crypto";
|
|
10753
10752
|
var URI_PREFIX = "dshr:";
|
|
10754
10753
|
var TOKEN_RE = /^\d{6}$/;
|
|
@@ -11594,10 +11593,10 @@ function mergeDefs(...defs) {
|
|
|
11594
11593
|
function cloneDef(schema) {
|
|
11595
11594
|
return mergeDefs(schema._zod.def);
|
|
11596
11595
|
}
|
|
11597
|
-
function getElementAtPath(obj,
|
|
11598
|
-
if (!
|
|
11596
|
+
function getElementAtPath(obj, path6) {
|
|
11597
|
+
if (!path6)
|
|
11599
11598
|
return obj;
|
|
11600
|
-
return
|
|
11599
|
+
return path6.reduce((acc, key) => acc?.[key], obj);
|
|
11601
11600
|
}
|
|
11602
11601
|
function promiseAllObject(promisesObj) {
|
|
11603
11602
|
const keys = Object.keys(promisesObj);
|
|
@@ -11937,11 +11936,11 @@ function explicitlyAborted(x, startIndex = 0) {
|
|
|
11937
11936
|
}
|
|
11938
11937
|
return false;
|
|
11939
11938
|
}
|
|
11940
|
-
function prefixIssues(
|
|
11939
|
+
function prefixIssues(path6, issues) {
|
|
11941
11940
|
return issues.map((iss) => {
|
|
11942
11941
|
var _a3;
|
|
11943
11942
|
(_a3 = iss).path ?? (_a3.path = []);
|
|
11944
|
-
iss.path.unshift(
|
|
11943
|
+
iss.path.unshift(path6);
|
|
11945
11944
|
return iss;
|
|
11946
11945
|
});
|
|
11947
11946
|
}
|
|
@@ -12391,16 +12390,16 @@ function flattenError(error62, mapper = (issue2) => issue2.message) {
|
|
|
12391
12390
|
}
|
|
12392
12391
|
function formatError(error62, mapper = (issue2) => issue2.message) {
|
|
12393
12392
|
const fieldErrors = { _errors: [] };
|
|
12394
|
-
const processError = (error63,
|
|
12393
|
+
const processError = (error63, path6 = []) => {
|
|
12395
12394
|
for (const issue2 of error63.issues) {
|
|
12396
12395
|
if (issue2.code === "invalid_union" && issue2.errors.length) {
|
|
12397
|
-
issue2.errors.map((issues) => processError({ issues }, [...
|
|
12396
|
+
issue2.errors.map((issues) => processError({ issues }, [...path6, ...issue2.path]));
|
|
12398
12397
|
} else if (issue2.code === "invalid_key") {
|
|
12399
|
-
processError({ issues: issue2.issues }, [...
|
|
12398
|
+
processError({ issues: issue2.issues }, [...path6, ...issue2.path]);
|
|
12400
12399
|
} else if (issue2.code === "invalid_element") {
|
|
12401
|
-
processError({ issues: issue2.issues }, [...
|
|
12400
|
+
processError({ issues: issue2.issues }, [...path6, ...issue2.path]);
|
|
12402
12401
|
} else {
|
|
12403
|
-
const fullpath = [...
|
|
12402
|
+
const fullpath = [...path6, ...issue2.path];
|
|
12404
12403
|
if (fullpath.length === 0) {
|
|
12405
12404
|
fieldErrors._errors.push(mapper(issue2));
|
|
12406
12405
|
} else {
|
|
@@ -12439,17 +12438,17 @@ function formatError(error62, mapper = (issue2) => issue2.message) {
|
|
|
12439
12438
|
}
|
|
12440
12439
|
function treeifyError(error62, mapper = (issue2) => issue2.message) {
|
|
12441
12440
|
const result2 = { errors: [] };
|
|
12442
|
-
const processError = (error63,
|
|
12441
|
+
const processError = (error63, path6 = []) => {
|
|
12443
12442
|
var _a3;
|
|
12444
12443
|
for (const issue2 of error63.issues) {
|
|
12445
12444
|
if (issue2.code === "invalid_union" && issue2.errors.length) {
|
|
12446
|
-
issue2.errors.map((issues) => processError({ issues }, [...
|
|
12445
|
+
issue2.errors.map((issues) => processError({ issues }, [...path6, ...issue2.path]));
|
|
12447
12446
|
} else if (issue2.code === "invalid_key") {
|
|
12448
|
-
processError({ issues: issue2.issues }, [...
|
|
12447
|
+
processError({ issues: issue2.issues }, [...path6, ...issue2.path]);
|
|
12449
12448
|
} else if (issue2.code === "invalid_element") {
|
|
12450
|
-
processError({ issues: issue2.issues }, [...
|
|
12449
|
+
processError({ issues: issue2.issues }, [...path6, ...issue2.path]);
|
|
12451
12450
|
} else {
|
|
12452
|
-
const fullpath = [...
|
|
12451
|
+
const fullpath = [...path6, ...issue2.path];
|
|
12453
12452
|
if (fullpath.length === 0) {
|
|
12454
12453
|
result2.errors.push(mapper(issue2));
|
|
12455
12454
|
continue;
|
|
@@ -12488,8 +12487,8 @@ function treeifyError(error62, mapper = (issue2) => issue2.message) {
|
|
|
12488
12487
|
}
|
|
12489
12488
|
function toDotPath(_path) {
|
|
12490
12489
|
const segs = [];
|
|
12491
|
-
const
|
|
12492
|
-
for (const seg of
|
|
12490
|
+
const path6 = _path.map((seg) => typeof seg === "object" ? seg.key : seg);
|
|
12491
|
+
for (const seg of path6) {
|
|
12493
12492
|
if (typeof seg === "number")
|
|
12494
12493
|
segs.push(`[${seg}]`);
|
|
12495
12494
|
else if (typeof seg === "symbol")
|
|
@@ -29591,13 +29590,13 @@ function resolveRef(ref, ctx) {
|
|
|
29591
29590
|
if (!ref.startsWith("#")) {
|
|
29592
29591
|
throw new Error("External $ref is not supported, only local refs (#/...) are allowed");
|
|
29593
29592
|
}
|
|
29594
|
-
const
|
|
29595
|
-
if (
|
|
29593
|
+
const path6 = ref.slice(1).split("/").filter(Boolean);
|
|
29594
|
+
if (path6.length === 0) {
|
|
29596
29595
|
return ctx.rootSchema;
|
|
29597
29596
|
}
|
|
29598
29597
|
const defsKey = ctx.version === "draft-2020-12" ? "$defs" : "definitions";
|
|
29599
|
-
if (
|
|
29600
|
-
const key =
|
|
29598
|
+
if (path6[0] === defsKey) {
|
|
29599
|
+
const key = path6[1] === void 0 ? void 0 : decodeJSONPointerSegment(path6[1]);
|
|
29601
29600
|
if (!key || !ctx.defs[key]) {
|
|
29602
29601
|
throw new Error(`Reference not found: ${ref}`);
|
|
29603
29602
|
}
|
|
@@ -30446,7 +30445,7 @@ function date4(params) {
|
|
|
30446
30445
|
return _coercedDate(ZodDate, params);
|
|
30447
30446
|
}
|
|
30448
30447
|
|
|
30449
|
-
// ../../node_modules/.pnpm/dsh-remote-wire@1.
|
|
30448
|
+
// ../../node_modules/.pnpm/dsh-remote-wire@1.2.0/node_modules/dsh-remote-wire/dist/src/wire/frames.js
|
|
30450
30449
|
var PAIRING_TOKEN_RE = /^\d{6}$/;
|
|
30451
30450
|
var nonEmpty = external_exports.string().min(1);
|
|
30452
30451
|
var pairingToken = external_exports.string().regex(PAIRING_TOKEN_RE);
|
|
@@ -30469,8 +30468,7 @@ var helloFrame = external_exports.object({
|
|
|
30469
30468
|
});
|
|
30470
30469
|
var pairBeginFrame = external_exports.object({
|
|
30471
30470
|
t: external_exports.literal("pair-begin"),
|
|
30472
|
-
pairingToken
|
|
30473
|
-
hostLabel: external_exports.string().max(128).optional()
|
|
30471
|
+
pairingToken
|
|
30474
30472
|
});
|
|
30475
30473
|
var pairClaimFrame = external_exports.object({
|
|
30476
30474
|
t: external_exports.literal("pair-begin-client"),
|
|
@@ -30606,7 +30604,7 @@ function parseRelayFrameText(raw) {
|
|
|
30606
30604
|
}
|
|
30607
30605
|
}
|
|
30608
30606
|
|
|
30609
|
-
// ../../node_modules/.pnpm/dsh-remote-wire@1.
|
|
30607
|
+
// ../../node_modules/.pnpm/dsh-remote-wire@1.2.0/node_modules/dsh-remote-wire/dist/src/wire/payloads.js
|
|
30610
30608
|
var SESSION_STATES = [
|
|
30611
30609
|
"idle",
|
|
30612
30610
|
"running",
|
|
@@ -30671,6 +30669,13 @@ var evPermissionRequest = external_exports.object({
|
|
|
30671
30669
|
options: external_exports.array(choiceOption).optional(),
|
|
30672
30670
|
expiresAt: external_exports.string().optional()
|
|
30673
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
|
+
});
|
|
30674
30679
|
var questionItem = external_exports.object({
|
|
30675
30680
|
id: nonEmpty2,
|
|
30676
30681
|
question: nonEmpty2,
|
|
@@ -30681,7 +30686,23 @@ var evQuestionRequest = external_exports.object({
|
|
|
30681
30686
|
t: external_exports.literal("ev.question_request"),
|
|
30682
30687
|
sessionId: nonEmpty2.optional(),
|
|
30683
30688
|
requestId: nonEmpty2,
|
|
30684
|
-
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()
|
|
30685
30706
|
});
|
|
30686
30707
|
var evRunState = external_exports.object({
|
|
30687
30708
|
t: external_exports.literal("ev.run_state"),
|
|
@@ -30698,6 +30719,24 @@ var evKeepAwakeState = external_exports.object({
|
|
|
30698
30719
|
backend: nonEmpty2,
|
|
30699
30720
|
reason: external_exports.string().optional()
|
|
30700
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
|
+
});
|
|
30701
30740
|
var evResult = external_exports.object({
|
|
30702
30741
|
t: external_exports.literal("ev.result"),
|
|
30703
30742
|
cmdId: external_exports.string(),
|
|
@@ -30719,9 +30758,12 @@ var evPayload = external_exports.discriminatedUnion("t", [
|
|
|
30719
30758
|
evMessageDelta,
|
|
30720
30759
|
evToolEvent,
|
|
30721
30760
|
evPermissionRequest,
|
|
30761
|
+
evPermissionResolved,
|
|
30722
30762
|
evQuestionRequest,
|
|
30763
|
+
evQuestionResolved,
|
|
30723
30764
|
evRunState,
|
|
30724
30765
|
evKeepAwakeState,
|
|
30766
|
+
evModel,
|
|
30725
30767
|
evResult,
|
|
30726
30768
|
evSessionHistory
|
|
30727
30769
|
]);
|
|
@@ -30809,14 +30851,17 @@ var PAYLOAD_TYPES = {
|
|
|
30809
30851
|
evMessageDelta: "ev.message_delta",
|
|
30810
30852
|
evToolEvent: "ev.tool_event",
|
|
30811
30853
|
evPermissionRequest: "ev.permission_request",
|
|
30854
|
+
evPermissionResolved: "ev.permission_resolved",
|
|
30812
30855
|
evQuestionRequest: "ev.question_request",
|
|
30856
|
+
evQuestionResolved: "ev.question_resolved",
|
|
30813
30857
|
evRunState: "ev.run_state",
|
|
30814
30858
|
evKeepAwakeState: "ev.keep_awake_state",
|
|
30859
|
+
evModel: "ev.model",
|
|
30815
30860
|
evResult: "ev.result",
|
|
30816
30861
|
evSessionHistory: "ev.session_history"
|
|
30817
30862
|
};
|
|
30818
30863
|
|
|
30819
|
-
// ../../node_modules/.pnpm/dsh-remote-wire@1.
|
|
30864
|
+
// ../../node_modules/.pnpm/dsh-remote-wire@1.2.0/node_modules/dsh-remote-wire/dist/src/wire/outbound.js
|
|
30820
30865
|
function sessionChanged(sessions, reason) {
|
|
30821
30866
|
return { t: PAYLOAD_TYPES.evSessionChanged, sessions, ...reason === void 0 ? {} : { reason } };
|
|
30822
30867
|
}
|
|
@@ -30829,15 +30874,31 @@ function toolEvent(args) {
|
|
|
30829
30874
|
function permissionRequest(args) {
|
|
30830
30875
|
return { t: PAYLOAD_TYPES.evPermissionRequest, ...args };
|
|
30831
30876
|
}
|
|
30877
|
+
function permissionResolved(args) {
|
|
30878
|
+
return { t: PAYLOAD_TYPES.evPermissionResolved, ...args };
|
|
30879
|
+
}
|
|
30832
30880
|
function questionRequest(args) {
|
|
30833
30881
|
return { t: PAYLOAD_TYPES.evQuestionRequest, ...args };
|
|
30834
30882
|
}
|
|
30883
|
+
function questionResolved(args) {
|
|
30884
|
+
return { t: PAYLOAD_TYPES.evQuestionResolved, ...args };
|
|
30885
|
+
}
|
|
30835
30886
|
function runState(args) {
|
|
30836
30887
|
return { t: PAYLOAD_TYPES.evRunState, ...args };
|
|
30837
30888
|
}
|
|
30838
30889
|
function keepAwakeState(args) {
|
|
30839
30890
|
return { t: PAYLOAD_TYPES.evKeepAwakeState, ...args };
|
|
30840
30891
|
}
|
|
30892
|
+
function model(args) {
|
|
30893
|
+
const payload = { t: PAYLOAD_TYPES.evModel, model: args.model, canSwitch: args.canSwitch };
|
|
30894
|
+
if (args.provider !== void 0)
|
|
30895
|
+
payload.provider = args.provider;
|
|
30896
|
+
if (args.canSwitch && args.options?.length)
|
|
30897
|
+
payload.options = args.options;
|
|
30898
|
+
if (args.reason !== void 0)
|
|
30899
|
+
payload.reason = args.reason;
|
|
30900
|
+
return payload;
|
|
30901
|
+
}
|
|
30841
30902
|
function result(cmdId, ok, extras = {}) {
|
|
30842
30903
|
const { message, data } = extras;
|
|
30843
30904
|
return {
|
|
@@ -30859,7 +30920,7 @@ function sessionHistory(args) {
|
|
|
30859
30920
|
};
|
|
30860
30921
|
}
|
|
30861
30922
|
|
|
30862
|
-
// ../../node_modules/.pnpm/dsh-remote-wire@1.
|
|
30923
|
+
// ../../node_modules/.pnpm/dsh-remote-wire@1.2.0/node_modules/dsh-remote-wire/dist/src/platform/sleep.js
|
|
30863
30924
|
function detectBackend(platform) {
|
|
30864
30925
|
if (platform === "darwin")
|
|
30865
30926
|
return "caffeinate";
|
|
@@ -31135,7 +31196,7 @@ var HostRuntime = class {
|
|
|
31135
31196
|
this.clock.clearTimeout(this.mergeTimer);
|
|
31136
31197
|
this.mergeTimer = void 0;
|
|
31137
31198
|
}
|
|
31138
|
-
for (const [id
|
|
31199
|
+
for (const [id] of [...this.pending]) this.settle(id, void 0);
|
|
31139
31200
|
}
|
|
31140
31201
|
/** 手机发来的命令。`conversationId` 是配对通道 id,与载荷里的 sessionId 不是一回事(F3)。 */
|
|
31141
31202
|
async handleCommand(cmd, conversationId) {
|
|
@@ -31245,6 +31306,25 @@ var HostRuntime = class {
|
|
|
31245
31306
|
get stats() {
|
|
31246
31307
|
return { ...this.outboundCount };
|
|
31247
31308
|
}
|
|
31309
|
+
/**
|
|
31310
|
+
* 有几件事正挂在手机上等回答,以及**最久的那件已经等了多久**(秒)。
|
|
31311
|
+
*
|
|
31312
|
+
* 为什么这条要出到界面上(而不只是内部状态):一张挂起的审批阻塞着远端一条正在跑的回合,
|
|
31313
|
+
* 而回合阻塞着用户的下班时间。桌面那颗 pill 原来在"已配对 + 有东西在等"时仍然只说
|
|
31314
|
+
* `已配对`——那是这整条链上唯一看得见它的地方,等于没有。判据见伞仓 docs/PRODUCT.md §3。
|
|
31315
|
+
*
|
|
31316
|
+
* `oldestSec` 用 `clock.now()` 而不是 `Date.now()`: FakeClock 要能演"等了 4 分钟",
|
|
31317
|
+
* 否则这条时长在测试里永远是 0,也就永远不会红。
|
|
31318
|
+
*/
|
|
31319
|
+
get waiting() {
|
|
31320
|
+
const now = this.clock.now();
|
|
31321
|
+
let oldest = 0;
|
|
31322
|
+
for (const item of this.pending.values()) {
|
|
31323
|
+
const age = Math.max(0, Math.floor((now - item.askedAt) / 1e3));
|
|
31324
|
+
if (age > oldest) oldest = age;
|
|
31325
|
+
}
|
|
31326
|
+
return { count: this.pending.size, oldestSec: oldest };
|
|
31327
|
+
}
|
|
31248
31328
|
countOutbound(payload, sent) {
|
|
31249
31329
|
const key = payload.t.replace(/^ev\./, "");
|
|
31250
31330
|
this.outboundCount[key] = (this.outboundCount[key] ?? 0) + 1;
|
|
@@ -31322,8 +31402,40 @@ var HostRuntime = class {
|
|
|
31322
31402
|
}
|
|
31323
31403
|
this.broadcast(sessionChanged(this.sessions, reason));
|
|
31324
31404
|
this.broadcast(keepAwakeState(this.sleep.snapshot()));
|
|
31405
|
+
this.broadcastModel();
|
|
31325
31406
|
return this.sessions;
|
|
31326
31407
|
}
|
|
31408
|
+
/**
|
|
31409
|
+
* 广播当前模型。
|
|
31410
|
+
*
|
|
31411
|
+
* 跟着 `pushSessions` 一起发而不是单独起一条:模型与防休眠都是**全局状态**,
|
|
31412
|
+
* 而 `pushSessions` 已经是"状态变了就推一次"的唯一入口,另开一条推送路径
|
|
31413
|
+
* 必然出现「会话更新了但模型没更新」这种半同步状态。
|
|
31414
|
+
*
|
|
31415
|
+
* **读不到就不发**(而不是发一个 `model: ''`):`ev.model` 的 `model` 是必填的
|
|
31416
|
+
* 非空串,硬塞空串会让手机把「不知道用什么模型」显示成「模型名为空」——
|
|
31417
|
+
* 后者看起来像 bug,前者只是没显示。内核缺 `currentSelection` 时真机上是常态
|
|
31418
|
+
* (见 carrier 的 modelFace 探测)。
|
|
31419
|
+
*/
|
|
31420
|
+
broadcastModel() {
|
|
31421
|
+
let selection;
|
|
31422
|
+
try {
|
|
31423
|
+
selection = this.kernel.modelSelection?.();
|
|
31424
|
+
} catch {
|
|
31425
|
+
return;
|
|
31426
|
+
}
|
|
31427
|
+
if (!selection?.model) return;
|
|
31428
|
+
const face = this.kernel.modelOptions?.();
|
|
31429
|
+
this.broadcast(
|
|
31430
|
+
model({
|
|
31431
|
+
model: selection.model,
|
|
31432
|
+
provider: selection.provider,
|
|
31433
|
+
canSwitch: Boolean(face?.canSwitch),
|
|
31434
|
+
options: face?.options,
|
|
31435
|
+
reason: face?.canSwitch ? void 0 : face?.reason ?? "\u4E3B\u673A\u5185\u6838\u672A\u63D0\u4F9B\u5207\u6362\u6A21\u578B\u7684\u80FD\u529B"
|
|
31436
|
+
})
|
|
31437
|
+
);
|
|
31438
|
+
}
|
|
31327
31439
|
refreshLoop() {
|
|
31328
31440
|
if (this.stopped) return;
|
|
31329
31441
|
void this.pushSessions("refresh");
|
|
@@ -31337,24 +31449,33 @@ var HostRuntime = class {
|
|
|
31337
31449
|
* **没有在线对端就返回 `'decline'`**,让适配器去调平台的 `next()` 把决定权交还桌面 UI;
|
|
31338
31450
|
* 超时同样返回 `'decline'`(我们已经把卡片发出去了,但手机迟迟没点,
|
|
31339
31451
|
* 与其替用户决定,不如让桌面去决定)。
|
|
31452
|
+
*
|
|
31453
|
+
* 返回值只说明"手机这一侧算出了什么",**不代表这张卡是手机答掉的**:
|
|
31454
|
+
* 桌面与手机是同时被问的(见 `carrier-services.ts` 的 `participate`),
|
|
31455
|
+
* 所以收尾时要按"有没有真的有人在手机上点过"决定要不要把卡收回去。
|
|
31340
31456
|
*/
|
|
31341
31457
|
async onApprovalRequest(info) {
|
|
31342
31458
|
const conversationId = this.pickConversation(info.sessionId);
|
|
31343
|
-
if (!conversationId || !this.transport.
|
|
31459
|
+
if (!conversationId || !this.transport.hasClient(conversationId)) return "decline";
|
|
31344
31460
|
const id = `ap_${randomUUID().slice(0, 8)}`;
|
|
31345
31461
|
this.sleep.hold(id);
|
|
31346
31462
|
const answered = await new Promise((resolve) => {
|
|
31347
31463
|
const timer = this.clock.setTimeout(() => {
|
|
31348
|
-
this.
|
|
31349
|
-
resolve("decline");
|
|
31464
|
+
this.settle(id, "decline", "timeout");
|
|
31350
31465
|
}, this.options.approvalTimeoutMs);
|
|
31351
31466
|
this.pending.set(id, {
|
|
31352
31467
|
conversationId,
|
|
31353
31468
|
sessionId: info.sessionId,
|
|
31354
31469
|
resolve: (value) => resolve(value === void 0 ? "decline" : value),
|
|
31355
|
-
timer
|
|
31470
|
+
timer,
|
|
31471
|
+
kind: "approval",
|
|
31472
|
+
askedAt: this.clock.now()
|
|
31356
31473
|
});
|
|
31357
|
-
info.signal?.addEventListener(
|
|
31474
|
+
info.signal?.addEventListener(
|
|
31475
|
+
"abort",
|
|
31476
|
+
() => this.settle(id, "cancelled", info.signal?.reason === "desktop" ? "desktop" : "withdrawn"),
|
|
31477
|
+
{ once: true }
|
|
31478
|
+
);
|
|
31358
31479
|
this.replyTo(
|
|
31359
31480
|
conversationId,
|
|
31360
31481
|
permissionRequest({
|
|
@@ -31362,7 +31483,8 @@ var HostRuntime = class {
|
|
|
31362
31483
|
sessionId: info.sessionId,
|
|
31363
31484
|
action: info.action,
|
|
31364
31485
|
...info.reason === void 0 ? {} : { reason: info.reason },
|
|
31365
|
-
options: APPROVAL_OPTIONS
|
|
31486
|
+
options: APPROVAL_OPTIONS,
|
|
31487
|
+
expiresAt: new Date(Date.now() + this.options.approvalTimeoutMs).toISOString()
|
|
31366
31488
|
})
|
|
31367
31489
|
);
|
|
31368
31490
|
});
|
|
@@ -31371,28 +31493,36 @@ var HostRuntime = class {
|
|
|
31371
31493
|
}
|
|
31372
31494
|
async onQuestionRequest(info) {
|
|
31373
31495
|
const conversationId = this.pickConversation(info.sessionId);
|
|
31374
|
-
if (!conversationId || !this.transport.
|
|
31496
|
+
if (!conversationId || !this.transport.hasClient(conversationId)) return null;
|
|
31375
31497
|
const id = `q_${randomUUID().slice(0, 8)}`;
|
|
31376
31498
|
this.sleep.hold(id);
|
|
31377
31499
|
const answer = await new Promise((resolve) => {
|
|
31378
31500
|
const timer = this.clock.setTimeout(() => {
|
|
31379
|
-
this.
|
|
31380
|
-
resolve(null);
|
|
31501
|
+
this.settle(id, null, "timeout");
|
|
31381
31502
|
}, this.options.questionTimeoutMs);
|
|
31382
31503
|
this.pending.set(id, {
|
|
31383
31504
|
conversationId,
|
|
31384
31505
|
sessionId: info.sessionId,
|
|
31385
31506
|
resolve: (value) => resolve(value ?? null),
|
|
31386
31507
|
timer,
|
|
31387
|
-
options: info.questions
|
|
31508
|
+
options: info.questions,
|
|
31509
|
+
kind: "question",
|
|
31510
|
+
askedAt: this.clock.now()
|
|
31388
31511
|
});
|
|
31389
|
-
info.signal?.addEventListener(
|
|
31512
|
+
info.signal?.addEventListener(
|
|
31513
|
+
"abort",
|
|
31514
|
+
() => this.settle(id, null, info.signal?.reason === "desktop" ? "desktop" : "withdrawn"),
|
|
31515
|
+
{ once: true }
|
|
31516
|
+
);
|
|
31390
31517
|
this.replyTo(
|
|
31391
31518
|
conversationId,
|
|
31392
31519
|
questionRequest({
|
|
31393
31520
|
requestId: id,
|
|
31394
31521
|
sessionId: info.sessionId,
|
|
31395
|
-
questions: info.questions
|
|
31522
|
+
questions: info.questions,
|
|
31523
|
+
// 提问这张卡以前**没有**到期时刻:主机 300 秒就判"没答上",而手机上看不见任何倒计时,
|
|
31524
|
+
// 用户不知道自己按的按钮什么时候作废(伞仓 docs/PRODUCT.md §3 第 3 条)。
|
|
31525
|
+
expiresAt: new Date(Date.now() + this.options.questionTimeoutMs).toISOString()
|
|
31396
31526
|
})
|
|
31397
31527
|
);
|
|
31398
31528
|
});
|
|
@@ -31424,13 +31554,45 @@ var HostRuntime = class {
|
|
|
31424
31554
|
this.settle(requestId, value);
|
|
31425
31555
|
return true;
|
|
31426
31556
|
}
|
|
31427
|
-
settle(id, value) {
|
|
31557
|
+
settle(id, value, voidAs) {
|
|
31428
31558
|
const item = this.pending.get(id);
|
|
31429
31559
|
if (!item) return;
|
|
31430
31560
|
this.pending.delete(id);
|
|
31431
31561
|
this.clock.clearTimeout(item.timer);
|
|
31562
|
+
if (voidAs) this.voidStaleCard(id, item, voidAs);
|
|
31432
31563
|
item.resolve(value);
|
|
31433
31564
|
}
|
|
31565
|
+
/**
|
|
31566
|
+
* 让手机上那张已经作废的卡片**当场**收掉:先按 `requestId` 发一条精确帧,
|
|
31567
|
+
* 再补一帧 `ev.run_state` 兜住"手机上装的是老版小程序"这一档。
|
|
31568
|
+
*
|
|
31569
|
+
* 为什么要两条而不是一条:`ev.permission_resolved` / `ev.question_resolved` 是按
|
|
31570
|
+
* `requestId` 收单的(一次答完不会误收别的会话、别的请求那张卡),但**用户手机上现在装的
|
|
31571
|
+
* 那一版还不认这两帧**——小程序的分发是一串 `if (p.t === …)`,认不出来的 `t` 静默忽略。
|
|
31572
|
+
* 而 `ev.run_state` 正好是它早就认的那条"挂着的审批/提问作废"信号
|
|
31573
|
+
* (`pages/chat/chat.js` 的 `_onRunState` 同时清 `pendingPermission` 与 `pendingQuestion`,
|
|
31574
|
+
* 并停掉那条还在走的本地倒数)。所以两条都发:新版手机收到精确帧就精收一张,
|
|
31575
|
+
* 老版手机靠 `run_state` 也能当场收卡,不用等用户重新上传小程序。
|
|
31576
|
+
*
|
|
31577
|
+
* 内核**不会**为"审批/提问被别人答掉了"发状态跳变(这一回合自始至终是 running),
|
|
31578
|
+
* 所以这一帧只能由我们补发;补的是**当前真相**而不是硬编码 `running`——
|
|
31579
|
+
* 手机上那个"思考中"跟着这一帧走,发错方向会一直错到下一次真实跳变。
|
|
31580
|
+
* 读不到真相时退回缓存快照,再退 `idle`:宁可少一个转圈,不能凭空多一个转圈。
|
|
31581
|
+
*/
|
|
31582
|
+
voidStaleCard(requestId, item, reason) {
|
|
31583
|
+
const by = reason === "desktop" ? "desktop" : "cancelled";
|
|
31584
|
+
this.replyTo(
|
|
31585
|
+
item.conversationId,
|
|
31586
|
+
item.kind === "approval" ? permissionResolved({ requestId, sessionId: item.sessionId, by }) : questionResolved({ requestId, sessionId: item.sessionId, by })
|
|
31587
|
+
);
|
|
31588
|
+
const emit = (running) => {
|
|
31589
|
+
this.broadcast(runState({ sessionId: item.sessionId, state: running ? "running" : "idle" }));
|
|
31590
|
+
this.log("pending card voided", { requestId, reason, kind: item.kind, state: running ? "running" : "idle" });
|
|
31591
|
+
};
|
|
31592
|
+
void this.kernel.runState(item.sessionId).then((truth) => emit(truth.running)).catch(() => {
|
|
31593
|
+
emit(this.sessions.some((session) => session.id === item.sessionId && session.running === true));
|
|
31594
|
+
});
|
|
31595
|
+
}
|
|
31434
31596
|
/**
|
|
31435
31597
|
* 挑一条配对通道把这轮交互发出去。
|
|
31436
31598
|
*
|
|
@@ -31442,7 +31604,7 @@ var HostRuntime = class {
|
|
|
31442
31604
|
pickConversation(_sessionId) {
|
|
31443
31605
|
const ids = this.transport.conversationIds();
|
|
31444
31606
|
if (ids.length === 0) return void 0;
|
|
31445
|
-
return ids.find((id) => this.transport.
|
|
31607
|
+
return ids.find((id) => this.transport.hasClient(id)) ?? ids[0];
|
|
31446
31608
|
}
|
|
31447
31609
|
log(message, fields) {
|
|
31448
31610
|
this.options.log?.(message, fields);
|
|
@@ -31604,6 +31766,10 @@ var DEFAULT_PRUNE_POLICY = {
|
|
|
31604
31766
|
// 一天没有任何收发就认为这条通道不会再被用(手机真回来时会重新扫码,
|
|
31605
31767
|
// 而且它会先撞上 `unknown_session` → 中文提示,而不是"永远转圈")。
|
|
31606
31768
|
idleTtlMs: 24 * 3600 * 1e3,
|
|
31769
|
+
// 恢复出来的会话不按上面那条剪:默认给 7 天,与中继的 `DRC_CONV_IDLE_TTL_MS`
|
|
31770
|
+
// (`config.ts` 的 7×24h)逐字对齐 —— 中继忘掉它的那天本来就得重扫,
|
|
31771
|
+
// 主机提前剪只是把同一次重扫提前到用户还没察觉的时间点,反而更难解释。
|
|
31772
|
+
restoredIdleTtlMs: 7 * 24 * 3600 * 1e3,
|
|
31607
31773
|
maxConversations: 64
|
|
31608
31774
|
};
|
|
31609
31775
|
var ConversationBook = class {
|
|
@@ -31621,13 +31787,48 @@ var ConversationBook = class {
|
|
|
31621
31787
|
kH2C: derivePskKey(args.psk, H2C, args.id),
|
|
31622
31788
|
psk: args.psk,
|
|
31623
31789
|
seqHost: 0,
|
|
31624
|
-
generation: args.generation,
|
|
31625
31790
|
createdAt: args.now,
|
|
31626
31791
|
lastActivityAt: args.now
|
|
31627
31792
|
};
|
|
31628
31793
|
this.byId.set(args.id, conversation);
|
|
31629
31794
|
return conversation;
|
|
31630
31795
|
}
|
|
31796
|
+
/**
|
|
31797
|
+
* 从落盘记录恢复会话(跨进程续用,见 `shell/pair-store.ts`)。
|
|
31798
|
+
*
|
|
31799
|
+
* 两条刻意的选择:
|
|
31800
|
+
*
|
|
31801
|
+
* - **`clientIds` 一律置空**:那是"此刻谁连着"的瞬时态,进程刚起来时没有人连着主机。
|
|
31802
|
+
* 填了旧值会让主机一启动就往每条会话广播,而手机端还没重连 ——
|
|
31803
|
+
* 那些帧全被中继计成丢帧(`relay.ts` 的 `broadcast` 闸门当初就是为这个加的)。
|
|
31804
|
+
* 手机回来发的第一帧会经 `RelayClient.onEncrypted` 把它重新登记进来。
|
|
31805
|
+
* - **`restored: true`**:让剪枝对这条会话用另一条 TTL,理由见该字段的注释。
|
|
31806
|
+
*
|
|
31807
|
+
* 返回真正恢复出来的会话 id:**同一条 id 重复出现时后者覆盖前者**,
|
|
31808
|
+
* 而一条形状不合的记录整条丢弃(宁可少一条通道,也不要一条解不开的)。
|
|
31809
|
+
*/
|
|
31810
|
+
restore(records, now) {
|
|
31811
|
+
const restored = [];
|
|
31812
|
+
for (const record2 of records) {
|
|
31813
|
+
const conversation = this.open({ id: record2.id, psk: record2.psk, now });
|
|
31814
|
+
conversation.seqHost = record2.seqHost;
|
|
31815
|
+
conversation.createdAt = record2.createdAt;
|
|
31816
|
+
conversation.lastActivityAt = record2.lastActivityAt;
|
|
31817
|
+
conversation.restored = true;
|
|
31818
|
+
restored.push(record2.id);
|
|
31819
|
+
}
|
|
31820
|
+
return restored;
|
|
31821
|
+
}
|
|
31822
|
+
/** 导出可落盘的记录(**不含 clientIds**,理由见 `restore`)。 */
|
|
31823
|
+
snapshot() {
|
|
31824
|
+
return [...this.byId.values()].map((conversation) => ({
|
|
31825
|
+
id: conversation.id,
|
|
31826
|
+
psk: conversation.psk,
|
|
31827
|
+
seqHost: conversation.seqHost,
|
|
31828
|
+
createdAt: conversation.createdAt,
|
|
31829
|
+
lastActivityAt: conversation.lastActivityAt
|
|
31830
|
+
}));
|
|
31831
|
+
}
|
|
31631
31832
|
/** 有实际收发就记一笔活跃(剪枝只看这个时刻)。未知通道返回 false,不顺手建条目。 */
|
|
31632
31833
|
touch(id, now) {
|
|
31633
31834
|
const conversation = this.byId.get(id);
|
|
@@ -31641,33 +31842,43 @@ var ConversationBook = class {
|
|
|
31641
31842
|
has(id) {
|
|
31642
31843
|
return this.byId.has(id);
|
|
31643
31844
|
}
|
|
31845
|
+
/**
|
|
31846
|
+
* 这条会话**现在有没有活着的客户端**。
|
|
31847
|
+
*
|
|
31848
|
+
* 与 `has()` 是两件事,而且必须分开:`has()` 是"我手里有这条通道的密钥",
|
|
31849
|
+
* 手机回前台时要用同一个 `convId` 回来,所以密钥**必须留着**(D3);
|
|
31850
|
+
* 但"有密钥"不等于"现在有人能收到"。
|
|
31851
|
+
*
|
|
31852
|
+
* 原来只有 `has()` 一个判据,于是 `hasPeer()` 答的是"有密钥",广播就照着它发——
|
|
31853
|
+
* 真机后果:手机断开之后(会话默认 7 天 TTL)主机每 15 秒仍往这条空会话发
|
|
31854
|
+
* `session_changed` / `model` / `keep_awake_state` 三帧,中继每一帧计一次丢帧,
|
|
31855
|
+
* 实测 45 秒涨 9、`/healthz` 的 `droppedFrames` 因此被这一路噪声主导(5737)。
|
|
31856
|
+
*/
|
|
31857
|
+
hasClient(id) {
|
|
31858
|
+
const conversation = this.byId.get(id);
|
|
31859
|
+
return conversation !== void 0 && conversation.clientIds.size > 0;
|
|
31860
|
+
}
|
|
31644
31861
|
close(id) {
|
|
31645
31862
|
return this.byId.delete(id);
|
|
31646
31863
|
}
|
|
31647
|
-
/** 中继重启(generation 变化)时把旧会话全部丢掉:它们再也解不开了。 */
|
|
31648
|
-
closeBefore(generation) {
|
|
31649
|
-
const dropped = [];
|
|
31650
|
-
for (const [id, conversation] of [...this.byId]) {
|
|
31651
|
-
if (conversation.generation < generation) {
|
|
31652
|
-
this.byId.delete(id);
|
|
31653
|
-
dropped.push(id);
|
|
31654
|
-
}
|
|
31655
|
-
}
|
|
31656
|
-
return dropped;
|
|
31657
|
-
}
|
|
31658
31864
|
/**
|
|
31659
31865
|
* 按策略剪枝,返回被剪掉的 id(调用方负责通知中继与上层,见
|
|
31660
31866
|
* `RelayClient.pruneConversations`)。
|
|
31661
31867
|
*
|
|
31662
|
-
*
|
|
31868
|
+
* 三条规则,因为它们是三种不同的失控:
|
|
31663
31869
|
* - 空闲超 TTL:一条通道一天没有任何收发,就不会再有人用它了;
|
|
31870
|
+
* - **恢复出来的会话走另一条 TTL**(`restoredIdleTtlMs`):见 `Conversation.restored`
|
|
31871
|
+
* 与 `DEFAULT_PRUNE_POLICY.restoredIdleTtlMs` 的理由 —— 它不该按"新配对却没人用"的
|
|
31872
|
+
* 标准剪掉,否则长假之后主机一启动就把唯一那条通道清了,免扫码重连白做;
|
|
31664
31873
|
* - 超出硬上界:从**最不活跃**的开始剪。没有这条,一个每天配一次对的用户
|
|
31665
31874
|
* 会在两周内攒下 260 条密钥,并且每次广播都对它们逐个密封一遍。
|
|
31666
31875
|
*/
|
|
31667
31876
|
pruneStale(now, policy = DEFAULT_PRUNE_POLICY) {
|
|
31668
31877
|
const dropped = [];
|
|
31878
|
+
const restoredTtl = policy.restoredIdleTtlMs ?? policy.idleTtlMs;
|
|
31669
31879
|
for (const [id, conversation] of [...this.byId]) {
|
|
31670
|
-
|
|
31880
|
+
const ttl = conversation.restored ? restoredTtl : policy.idleTtlMs;
|
|
31881
|
+
if (now - conversation.lastActivityAt > ttl) {
|
|
31671
31882
|
this.byId.delete(id);
|
|
31672
31883
|
dropped.push(id);
|
|
31673
31884
|
}
|
|
@@ -31684,6 +31895,25 @@ var ConversationBook = class {
|
|
|
31684
31895
|
ids() {
|
|
31685
31896
|
return [...this.byId.keys()];
|
|
31686
31897
|
}
|
|
31898
|
+
/**
|
|
31899
|
+
* 当前**真正连着**的客户端数(去重后的 clientId,不是会话数)。
|
|
31900
|
+
*
|
|
31901
|
+
* 为什么必须单独算:一条会话(convId)对应一次配对,而**同一部手机**在
|
|
31902
|
+
* 「解除配对再重新配对」之后会开一条新会话 —— 于是 `size`(会话数)会增长,
|
|
31903
|
+
* 手机却还是那一台。把 `size` 当成"有几部手机"报出去,用户解除配对后看到
|
|
31904
|
+
* 数字反而变大,会以为解配没生效。
|
|
31905
|
+
*
|
|
31906
|
+
* 反过来也算不准:会话被剪掉了但手机还在(`pruneStale`),
|
|
31907
|
+
* 所以这个数只保证"不比会话数更离谱",不保证等于真实在线数 ——
|
|
31908
|
+
* 界面上它只该当"有手机连着没有"的粗判据,不该当精确计数。
|
|
31909
|
+
*/
|
|
31910
|
+
clientCount() {
|
|
31911
|
+
const ids = /* @__PURE__ */ new Set();
|
|
31912
|
+
for (const conversation of this.byId.values()) {
|
|
31913
|
+
for (const clientId of conversation.clientIds) ids.add(clientId);
|
|
31914
|
+
}
|
|
31915
|
+
return ids.size;
|
|
31916
|
+
}
|
|
31687
31917
|
get size() {
|
|
31688
31918
|
return this.byId.size;
|
|
31689
31919
|
}
|
|
@@ -31850,6 +32080,14 @@ var RelayClient = class {
|
|
|
31850
32080
|
options;
|
|
31851
32081
|
conversations = new ConversationBook();
|
|
31852
32082
|
socket;
|
|
32083
|
+
/**
|
|
32084
|
+
* **主机侧连接代号**:每收到一次 `hello-ok` 就 +1。
|
|
32085
|
+
*
|
|
32086
|
+
* 它现在**只**喂给 `PairingWindow`("中继的 pending-pair 表是不是清空了"),
|
|
32087
|
+
* 绝不用来清会话 —— 理由见文件头。它递增本身没有权威性(中继不回代次),
|
|
32088
|
+
* 所以配对窗口用它判"换码"是保守正确的:中继真重启时代次一定变了,
|
|
32089
|
+
* 中继没重启时多换一张码的代价只是浪费一个 6 位码。
|
|
32090
|
+
*/
|
|
31853
32091
|
generation = 0;
|
|
31854
32092
|
attempts = 0;
|
|
31855
32093
|
stopped = false;
|
|
@@ -31863,6 +32101,39 @@ var RelayClient = class {
|
|
|
31863
32101
|
get conversationCount() {
|
|
31864
32102
|
return this.conversations.size;
|
|
31865
32103
|
}
|
|
32104
|
+
/**
|
|
32105
|
+
* 当前连着的**手机台数**(去重的 clientId)。
|
|
32106
|
+
*
|
|
32107
|
+
* 与 `conversationCount` 分开是因为「解除配对 → 重新配对」会开一条新会话,
|
|
32108
|
+
* 手机却还是那一台:拿会话数当手机数报,用户解完配对看到数字变大,
|
|
32109
|
+
* 会以为解配没生效。
|
|
32110
|
+
*/
|
|
32111
|
+
get clientCount() {
|
|
32112
|
+
return this.conversations.clientCount();
|
|
32113
|
+
}
|
|
32114
|
+
/** 本轮已连上的中继代号(`status.json` 与配对窗口都读它)。 */
|
|
32115
|
+
get relayGeneration() {
|
|
32116
|
+
return this.generation;
|
|
32117
|
+
}
|
|
32118
|
+
/**
|
|
32119
|
+
* 恢复上次进程留下的会话(密钥来自 `pair-store`)。
|
|
32120
|
+
*
|
|
32121
|
+
* **恢复出来的会话里 `clientIds` 是空的**(见 `ConversationBook.restore`),所以
|
|
32122
|
+
* `conversationCount > 0` 而 `clientCount === 0` 是这一阶段的正常形态:
|
|
32123
|
+
* 密钥还在、手机还没回来。调用方据此知道**不能**等 `onPeerJoined` 才启动内核订阅
|
|
32124
|
+
* —— 否则主机重启后手机能连上、主机却不订阅任何事件,表现与配对丢失一模一样。
|
|
32125
|
+
*/
|
|
32126
|
+
restoreConversations(records) {
|
|
32127
|
+
const restored = this.conversations.restore(records, this.options.clock.now());
|
|
32128
|
+
if (restored.length > 0) {
|
|
32129
|
+
this.options.log("conversations restored from disk", {
|
|
32130
|
+
count: restored.length,
|
|
32131
|
+
// 记一下有没有手机已经登记进来了:恢复阶段应当恒为 0。
|
|
32132
|
+
clients: this.conversations.clientCount()
|
|
32133
|
+
});
|
|
32134
|
+
}
|
|
32135
|
+
return restored;
|
|
32136
|
+
}
|
|
31866
32137
|
connect() {
|
|
31867
32138
|
if (this.stopped) return;
|
|
31868
32139
|
this.closeSocket();
|
|
@@ -31937,23 +32208,34 @@ var RelayClient = class {
|
|
|
31937
32208
|
this.options.log("pruned stale conversations", { count: dropped.length, remaining: this.conversations.size });
|
|
31938
32209
|
return dropped;
|
|
31939
32210
|
}
|
|
31940
|
-
/** 向一条配对会话发一条载荷;返回 false =
|
|
32211
|
+
/** 向一条配对会话发一条载荷;返回 false = 这条会话已经没了,或**现在没有活着的客户端**。 */
|
|
31941
32212
|
send(conversationId, payload) {
|
|
31942
32213
|
const conversation = this.conversations.get(conversationId);
|
|
31943
32214
|
if (!conversation) return false;
|
|
32215
|
+
if (conversation.clientIds.size === 0) return false;
|
|
31944
32216
|
const record2 = seal(conversation.kH2C, payload);
|
|
31945
32217
|
conversation.lastActivityAt = this.options.clock.now();
|
|
32218
|
+
this.options.onBookActivity?.();
|
|
31946
32219
|
return this.raw({ t: "enc", sessionId: conversationId, seq: ++conversation.seqHost, ciphertext: record2.ciphertext });
|
|
31947
32220
|
}
|
|
31948
32221
|
broadcast(payload) {
|
|
31949
32222
|
let sent = 0;
|
|
31950
32223
|
for (const id of this.conversations.ids()) {
|
|
32224
|
+
if (!this.conversations.hasClient(id)) continue;
|
|
31951
32225
|
if (this.send(id, payload)) sent += 1;
|
|
31952
32226
|
}
|
|
31953
32227
|
return sent;
|
|
31954
32228
|
}
|
|
31955
|
-
|
|
31956
|
-
|
|
32229
|
+
/**
|
|
32230
|
+
* 这条会话**现在**有没有能收到东西的手机。
|
|
32231
|
+
*
|
|
32232
|
+
* 原来这个方法叫 `hasPeer`、答的是"我手里有没有这条通道的密钥"——两者在手机上线时恰好
|
|
32233
|
+
* 同真,所以错误一直藏着:手机断开后审批仍然"发得出去"(发进一条空会话),
|
|
32234
|
+
* 桌面那一半要等满 180 秒才拿到决定权。名字换成 `hasClient` 是为了让下一次
|
|
32235
|
+
* 想写 `conversations.has()` 的人当场看见这两个概念不是一回事。
|
|
32236
|
+
*/
|
|
32237
|
+
hasClient(conversationId) {
|
|
32238
|
+
return this.conversations.hasClient(conversationId);
|
|
31957
32239
|
}
|
|
31958
32240
|
conversationIds() {
|
|
31959
32241
|
return this.conversations.ids();
|
|
@@ -31979,6 +32261,7 @@ var RelayClient = class {
|
|
|
31979
32261
|
this.voided.add(conversationId);
|
|
31980
32262
|
this.undecryptable.delete(conversationId);
|
|
31981
32263
|
this.conversations.close(conversationId);
|
|
32264
|
+
this.options.onStructuralChange?.();
|
|
31982
32265
|
this.raw({ t: "session-leave", sessionId: conversationId });
|
|
31983
32266
|
this.options.onConversationGone(conversationId);
|
|
31984
32267
|
}
|
|
@@ -32001,18 +32284,24 @@ var RelayClient = class {
|
|
|
32001
32284
|
switch (frame.t) {
|
|
32002
32285
|
case "hello-ok": {
|
|
32003
32286
|
this.generation += 1;
|
|
32004
|
-
const dropped = this.conversations.closeBefore(this.generation);
|
|
32005
|
-
for (const id of dropped) this.options.onConversationGone(id);
|
|
32006
32287
|
this.emitState("online");
|
|
32007
32288
|
this.raw({ t: "resync", sessionIds: this.conversations.ids() });
|
|
32008
|
-
this.options.log("relay online", {
|
|
32289
|
+
this.options.log("relay online", {
|
|
32290
|
+
generation: this.generation,
|
|
32291
|
+
conversations: this.conversations.size,
|
|
32292
|
+
clients: this.conversations.clientCount()
|
|
32293
|
+
});
|
|
32009
32294
|
return;
|
|
32010
32295
|
}
|
|
32011
32296
|
case "pair-ready":
|
|
32012
32297
|
this.options.onPairReady(frame.pairingToken, frame.ttlMs);
|
|
32013
32298
|
return;
|
|
32014
32299
|
case "peer-joined": {
|
|
32015
|
-
if (!frame.pairingToken)
|
|
32300
|
+
if (!frame.pairingToken) {
|
|
32301
|
+
const conversation = this.conversations.get(frame.sessionId);
|
|
32302
|
+
if (conversation && frame.clientId) conversation.clientIds.add(frame.clientId);
|
|
32303
|
+
return;
|
|
32304
|
+
}
|
|
32016
32305
|
const slot = this.options.lookupPairingSlot(frame.pairingToken);
|
|
32017
32306
|
if (!slot) {
|
|
32018
32307
|
this.options.log("peer-joined without a known pairing token", { sessionId: frame.sessionId });
|
|
@@ -32021,9 +32310,10 @@ var RelayClient = class {
|
|
|
32021
32310
|
this.conversations.open({
|
|
32022
32311
|
id: frame.sessionId,
|
|
32023
32312
|
psk: slot.psk,
|
|
32024
|
-
generation: this.generation,
|
|
32025
32313
|
now: this.options.clock.now()
|
|
32026
32314
|
});
|
|
32315
|
+
this.options.onStructuralChange?.();
|
|
32316
|
+
if (frame.clientId) this.conversations.get(frame.sessionId)?.clientIds.add(frame.clientId);
|
|
32027
32317
|
this.voided.delete(frame.sessionId);
|
|
32028
32318
|
this.raw({ t: "resync", sessionIds: this.conversations.ids() });
|
|
32029
32319
|
this.emitState("online");
|
|
@@ -32031,6 +32321,7 @@ var RelayClient = class {
|
|
|
32031
32321
|
return;
|
|
32032
32322
|
}
|
|
32033
32323
|
case "peer-left":
|
|
32324
|
+
if (frame.clientId) this.conversations.get(frame.sessionId)?.clientIds.delete(frame.clientId);
|
|
32034
32325
|
this.options.onClientLeft(frame.sessionId, frame.clientId);
|
|
32035
32326
|
this.emitState("online");
|
|
32036
32327
|
return;
|
|
@@ -32063,6 +32354,7 @@ var RelayClient = class {
|
|
|
32063
32354
|
this.voidConversation(frame.sessionId);
|
|
32064
32355
|
return;
|
|
32065
32356
|
}
|
|
32357
|
+
if (frame.clientId) conversation.clientIds.add(frame.clientId);
|
|
32066
32358
|
const records = frame.items ?? (frame.ciphertext === void 0 ? [] : [{ ciphertext: frame.ciphertext }]);
|
|
32067
32359
|
let decryptedAny = false;
|
|
32068
32360
|
for (const record2 of records) {
|
|
@@ -32081,8 +32373,11 @@ var RelayClient = class {
|
|
|
32081
32373
|
decryptedAny = true;
|
|
32082
32374
|
this.options.onCommand(frame.sessionId, cmd, frame.clientId);
|
|
32083
32375
|
}
|
|
32084
|
-
if (decryptedAny)
|
|
32085
|
-
|
|
32376
|
+
if (decryptedAny) {
|
|
32377
|
+
conversation.lastActivityAt = this.options.clock.now();
|
|
32378
|
+
this.options.onBookActivity?.();
|
|
32379
|
+
this.undecryptable.delete(frame.sessionId);
|
|
32380
|
+
}
|
|
32086
32381
|
}
|
|
32087
32382
|
/** 两次解不开就作废(与手机端"两帧解不开就丢配对"对称,避免一端永久静默)。 */
|
|
32088
32383
|
noteUndecryptable(conversationId) {
|
|
@@ -32119,7 +32414,10 @@ var RelayClient = class {
|
|
|
32119
32414
|
this.options.onState({
|
|
32120
32415
|
relay,
|
|
32121
32416
|
...problem === void 0 ? {} : { problem },
|
|
32122
|
-
|
|
32417
|
+
// 之前这里写的是 `conversations.size`,把"会话数"顶替成了"客户端数":
|
|
32418
|
+
// 一部手机解配再重配就多一条会话,于是这个字段会**变大** ——
|
|
32419
|
+
// 拿它回答"现在有几台手机连着"会得出相反的结论。
|
|
32420
|
+
clients: this.conversations.clientCount(),
|
|
32123
32421
|
conversations: this.conversations.size,
|
|
32124
32422
|
generation: this.generation
|
|
32125
32423
|
});
|
|
@@ -32231,7 +32529,7 @@ var WATERFALL_EVENTS = [
|
|
|
32231
32529
|
"tools/post-execute",
|
|
32232
32530
|
"tools/code-dispatch-log"
|
|
32233
32531
|
];
|
|
32234
|
-
var WATERFALL_PARTICIPANTS = ["approval/request"];
|
|
32532
|
+
var WATERFALL_PARTICIPANTS = ["approval/request", "user-questions/request"];
|
|
32235
32533
|
var allowed = new Set(SUBSCRIBABLE_EVENTS);
|
|
32236
32534
|
var participants = new Set(WATERFALL_PARTICIPANTS);
|
|
32237
32535
|
function canParticipate(name) {
|
|
@@ -32266,11 +32564,45 @@ function guardedSubscribe(on, name, listener, warn) {
|
|
|
32266
32564
|
}
|
|
32267
32565
|
|
|
32268
32566
|
// src/platform/carrier-services.ts
|
|
32567
|
+
var PARTICIPATE_OPTIONS = { global: true, prepend: true };
|
|
32568
|
+
var PARTICIPATED_EVENTS = ["approval/request", "user-questions/request"];
|
|
32569
|
+
function fuseSignals(platform, ours) {
|
|
32570
|
+
if (!platform) return ours;
|
|
32571
|
+
const any2 = AbortSignal.any;
|
|
32572
|
+
if (typeof any2 === "function") return any2.call(AbortSignal, [platform, ours]);
|
|
32573
|
+
const fused = new AbortController();
|
|
32574
|
+
for (const source of [platform, ours]) {
|
|
32575
|
+
if (fused.signal.aborted) break;
|
|
32576
|
+
if (source.aborted) {
|
|
32577
|
+
fused.abort(source.reason);
|
|
32578
|
+
break;
|
|
32579
|
+
}
|
|
32580
|
+
source.addEventListener("abort", () => fused.abort(source.reason), { once: true });
|
|
32581
|
+
}
|
|
32582
|
+
return fused.signal;
|
|
32583
|
+
}
|
|
32269
32584
|
var MAX_TITLE_BATCH_DEFAULT = 25;
|
|
32270
32585
|
var TITLE_TIMEOUT_DEFAULT = 4e3;
|
|
32271
32586
|
var HISTORY_TIMEOUT_DEFAULT = 15e3;
|
|
32272
32587
|
var CREATE_TIMEOUT_DEFAULT = 8e3;
|
|
32273
32588
|
var LIST_LIMIT_DEFAULT = 100;
|
|
32589
|
+
var MODEL_FACE_METHODS = [
|
|
32590
|
+
"currentSelection",
|
|
32591
|
+
"list",
|
|
32592
|
+
"listModels",
|
|
32593
|
+
"available",
|
|
32594
|
+
"availableModels",
|
|
32595
|
+
"selections",
|
|
32596
|
+
"options",
|
|
32597
|
+
"get",
|
|
32598
|
+
"set",
|
|
32599
|
+
"select",
|
|
32600
|
+
"setSelection",
|
|
32601
|
+
"setDefault",
|
|
32602
|
+
"update"
|
|
32603
|
+
];
|
|
32604
|
+
var MODEL_LIST_METHODS = /* @__PURE__ */ new Set(["list", "listModels", "available", "availableModels", "selections", "options"]);
|
|
32605
|
+
var MODEL_SET_METHODS = /* @__PURE__ */ new Set(["set", "select", "setSelection", "setDefault", "update"]);
|
|
32274
32606
|
function createServicesKernel(services, options) {
|
|
32275
32607
|
const log = (message, fields = {}) => options.log?.(message, fields);
|
|
32276
32608
|
const titleCache = /* @__PURE__ */ new Map();
|
|
@@ -32279,8 +32611,25 @@ function createServicesKernel(services, options) {
|
|
|
32279
32611
|
const unmappedEventTypes = /* @__PURE__ */ new Set();
|
|
32280
32612
|
const injectedUserMessages = /* @__PURE__ */ new Map();
|
|
32281
32613
|
const listenerErrors = /* @__PURE__ */ new Map();
|
|
32282
|
-
|
|
32283
|
-
|
|
32614
|
+
const faces = {
|
|
32615
|
+
"approval/request": {
|
|
32616
|
+
registered: "not-attached",
|
|
32617
|
+
calls: 0,
|
|
32618
|
+
last: "not-called",
|
|
32619
|
+
desktopVoided: 0,
|
|
32620
|
+
handoff: "unknown"
|
|
32621
|
+
},
|
|
32622
|
+
"user-questions/request": {
|
|
32623
|
+
registered: "not-attached",
|
|
32624
|
+
calls: 0,
|
|
32625
|
+
last: "not-called",
|
|
32626
|
+
desktopVoided: 0,
|
|
32627
|
+
handoff: "unknown"
|
|
32628
|
+
}
|
|
32629
|
+
};
|
|
32630
|
+
let approvalAsked = 0;
|
|
32631
|
+
let approvalDecided = "not-seen";
|
|
32632
|
+
const modelMethods = [];
|
|
32284
32633
|
const fn = (owner, name) => {
|
|
32285
32634
|
if (!owner) return void 0;
|
|
32286
32635
|
const value = owner[name];
|
|
@@ -32304,22 +32653,22 @@ function createServicesKernel(services, options) {
|
|
|
32304
32653
|
return void 0;
|
|
32305
32654
|
}
|
|
32306
32655
|
}
|
|
32656
|
+
function firstLiveAgent() {
|
|
32657
|
+
const list = fn(services.agents, "list");
|
|
32658
|
+
if (list) {
|
|
32659
|
+
try {
|
|
32660
|
+
const all = list.call(services.agents);
|
|
32661
|
+
if (Array.isArray(all) && all.length) return all[0];
|
|
32662
|
+
} catch {
|
|
32663
|
+
}
|
|
32664
|
+
}
|
|
32665
|
+
return void 0;
|
|
32666
|
+
}
|
|
32307
32667
|
function agentStatus(sessionId) {
|
|
32308
32668
|
const agent = liveAgent(sessionId);
|
|
32309
32669
|
const status = agent?.status;
|
|
32310
32670
|
return typeof status === "string" ? status : "idle";
|
|
32311
32671
|
}
|
|
32312
|
-
function sessionWorkspace(sessionId) {
|
|
32313
|
-
const get = fn(services.sessions, "get");
|
|
32314
|
-
if (!get || sessionId === "") return void 0;
|
|
32315
|
-
try {
|
|
32316
|
-
const session = get.call(services.sessions, sessionId);
|
|
32317
|
-
const cwd = session?.header?.cwd ?? session?.cwd;
|
|
32318
|
-
return typeof cwd === "string" && cwd !== "" ? cwd : void 0;
|
|
32319
|
-
} catch {
|
|
32320
|
-
return void 0;
|
|
32321
|
-
}
|
|
32322
|
-
}
|
|
32323
32672
|
function toSummary(record2, archived) {
|
|
32324
32673
|
const cached2 = titleCache.get(record2.id);
|
|
32325
32674
|
const running = agentStatus(record2.id) === "running";
|
|
@@ -32479,6 +32828,27 @@ function createServicesKernel(services, options) {
|
|
|
32479
32828
|
return void 0;
|
|
32480
32829
|
}
|
|
32481
32830
|
}
|
|
32831
|
+
function probeModelFace() {
|
|
32832
|
+
const owner = services.agentDefaultModel;
|
|
32833
|
+
if (!owner) return { hits: [], canList: false, canSet: false };
|
|
32834
|
+
const hits = [];
|
|
32835
|
+
for (const name of MODEL_FACE_METHODS) {
|
|
32836
|
+
if (fn(owner, name)) hits.push(name);
|
|
32837
|
+
}
|
|
32838
|
+
modelMethods.length = 0;
|
|
32839
|
+
modelMethods.push(...hits);
|
|
32840
|
+
return {
|
|
32841
|
+
hits,
|
|
32842
|
+
canList: hits.some((n) => MODEL_LIST_METHODS.has(n)),
|
|
32843
|
+
canSet: hits.some((n) => MODEL_SET_METHODS.has(n))
|
|
32844
|
+
};
|
|
32845
|
+
}
|
|
32846
|
+
function modelFaceSummary() {
|
|
32847
|
+
if (!services.agentDefaultModel) return "absent";
|
|
32848
|
+
const { hits, canList, canSet } = probeModelFace();
|
|
32849
|
+
if (!hits.length) return `none (tried=${MODEL_FACE_METHODS.length})`;
|
|
32850
|
+
return `${canList ? "list" : "no-list"}+${canSet ? "set" : "no-set"} via=${hits.join(",")}`.slice(0, 180);
|
|
32851
|
+
}
|
|
32482
32852
|
async function sendPrompt(sessionId, text) {
|
|
32483
32853
|
const agent = liveAgent(sessionId);
|
|
32484
32854
|
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` };
|
|
@@ -32603,6 +32973,8 @@ function createServicesKernel(services, options) {
|
|
|
32603
32973
|
if (!sessionId || !type) return;
|
|
32604
32974
|
seenEventTypes.add(type);
|
|
32605
32975
|
const data = event?.data ?? {};
|
|
32976
|
+
if (type === "approval/asked") approvalAsked += 1;
|
|
32977
|
+
if (type === "approval/decided") approvalDecided = String(data.outcome ?? "?");
|
|
32606
32978
|
for (const mapped of sessionEventKernelEvents({ sessionId, type, seq: event?.seq, data })) {
|
|
32607
32979
|
if (mapped.kind === "title") titleCache.set(sessionId, { title: mapped.title, at: options.clock.now() });
|
|
32608
32980
|
onEvent(mapped);
|
|
@@ -32629,59 +33001,34 @@ function createServicesKernel(services, options) {
|
|
|
32629
33001
|
sink = next;
|
|
32630
33002
|
const disposers = [];
|
|
32631
33003
|
const on = services.on;
|
|
32632
|
-
if (typeof on
|
|
32633
|
-
const name = "
|
|
32634
|
-
|
|
32635
|
-
|
|
32636
|
-
try {
|
|
32637
|
-
const disposer = on.call(services, name, listener);
|
|
32638
|
-
approvalFace = "registered";
|
|
32639
|
-
disposers.push(() => {
|
|
32640
|
-
if (typeof disposer === "function") disposer();
|
|
32641
|
-
else if (typeof services.off === "function") services.off(name, listener);
|
|
32642
|
-
});
|
|
32643
|
-
} catch (error62) {
|
|
32644
|
-
approvalFace = `failed: ${messageOf3(error62)}`.slice(0, 80);
|
|
32645
|
-
log("approval/request participation failed", { message: messageOf3(error62) });
|
|
32646
|
-
}
|
|
32647
|
-
} else {
|
|
32648
|
-
approvalFace = "refused by guard";
|
|
32649
|
-
}
|
|
32650
|
-
} else {
|
|
32651
|
-
approvalFace = "no on()";
|
|
32652
|
-
}
|
|
32653
|
-
const questions = services.userQuestions;
|
|
32654
|
-
if (!options.takeOverQuestions) {
|
|
32655
|
-
questionsFace = "not-taken-over";
|
|
32656
|
-
} else if (!questions || typeof questions.registerProvider !== "function") {
|
|
32657
|
-
questionsFace = `no registerProvider (keys=${questions ? shapeOf(questions) : "service absent"})`.slice(0, 120);
|
|
32658
|
-
}
|
|
32659
|
-
if (options.takeOverQuestions && questions && typeof questions.registerProvider === "function") {
|
|
32660
|
-
const provider = {
|
|
32661
|
-
ask: async (request) => {
|
|
32662
|
-
const agent = request.agent;
|
|
32663
|
-
const sessionId = String(agent?.session?.id ?? request.sessionId ?? "");
|
|
32664
|
-
const items = Array.isArray(request.questions) ? request.questions : [];
|
|
32665
|
-
const mapped = items.map((item, index) => mapQuestion(item, index));
|
|
32666
|
-
const answer = await sink?.question({
|
|
32667
|
-
sessionId,
|
|
32668
|
-
questions: mapped,
|
|
32669
|
-
...request.signal ? { signal: request.signal } : {}
|
|
32670
|
-
});
|
|
32671
|
-
if (!answer) {
|
|
32672
|
-
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");
|
|
32673
|
-
}
|
|
32674
|
-
return { answers: answer.answers };
|
|
32675
|
-
}
|
|
33004
|
+
if (typeof on !== "function") {
|
|
33005
|
+
for (const name of PARTICIPATED_EVENTS) faces[name].registered = "no on()";
|
|
33006
|
+
return () => {
|
|
33007
|
+
sink = void 0;
|
|
32676
33008
|
};
|
|
33009
|
+
}
|
|
33010
|
+
for (const name of PARTICIPATED_EVENTS) {
|
|
33011
|
+
const face = faces[name];
|
|
33012
|
+
if (!canParticipate(name)) {
|
|
33013
|
+
face.registered = "refused by guard";
|
|
33014
|
+
continue;
|
|
33015
|
+
}
|
|
33016
|
+
const listener = (...args) => participate(name, args);
|
|
32677
33017
|
try {
|
|
32678
|
-
const
|
|
32679
|
-
|
|
32680
|
-
|
|
32681
|
-
|
|
33018
|
+
const disposer = on.call(
|
|
33019
|
+
services,
|
|
33020
|
+
name,
|
|
33021
|
+
listener,
|
|
33022
|
+
PARTICIPATE_OPTIONS
|
|
33023
|
+
);
|
|
33024
|
+
face.registered = "registered";
|
|
33025
|
+
disposers.push(() => {
|
|
33026
|
+
if (typeof disposer === "function") disposer();
|
|
33027
|
+
else if (typeof services.off === "function") services.off(name, listener);
|
|
33028
|
+
});
|
|
32682
33029
|
} catch (error62) {
|
|
32683
|
-
|
|
32684
|
-
log(
|
|
33030
|
+
face.registered = `failed: ${messageOf3(error62)}`.slice(0, 80);
|
|
33031
|
+
log(`${name} participation failed`, { message: messageOf3(error62) });
|
|
32685
33032
|
}
|
|
32686
33033
|
}
|
|
32687
33034
|
return () => {
|
|
@@ -32694,23 +33041,103 @@ function createServicesKernel(services, options) {
|
|
|
32694
33041
|
}
|
|
32695
33042
|
};
|
|
32696
33043
|
}
|
|
32697
|
-
async function participate(args) {
|
|
33044
|
+
async function participate(event, args) {
|
|
33045
|
+
const isApproval = event === "approval/request";
|
|
33046
|
+
const face = faces[event];
|
|
33047
|
+
face.calls += 1;
|
|
32698
33048
|
const request = args.find(
|
|
32699
|
-
(arg) => arg && typeof arg === "object" && typeof arg.toolName === "string"
|
|
33049
|
+
(arg) => Boolean(arg) && typeof arg === "object" && (isApproval ? typeof arg.toolName === "string" : Array.isArray(arg.questions))
|
|
32700
33050
|
);
|
|
32701
33051
|
const next = args.find((arg) => typeof arg === "function");
|
|
32702
33052
|
const sessionId = String(request?.agent?.session?.id ?? "");
|
|
32703
33053
|
if (!sink || !sessionId || !servicesPeers()) {
|
|
32704
|
-
|
|
33054
|
+
face.last = "handed-back(no phone target)";
|
|
33055
|
+
const value = await next?.();
|
|
33056
|
+
return isApproval ? String(value ?? "unavailable") : value;
|
|
33057
|
+
}
|
|
33058
|
+
const controller = new AbortController();
|
|
33059
|
+
const platform = request?.signal;
|
|
33060
|
+
if (platform) {
|
|
33061
|
+
if (platform.aborted) controller.abort("platform");
|
|
33062
|
+
else platform.addEventListener("abort", () => controller.abort("platform"), { once: true });
|
|
33063
|
+
}
|
|
33064
|
+
const canceller = new AbortController();
|
|
33065
|
+
let handedOff = false;
|
|
33066
|
+
if (request) {
|
|
33067
|
+
try {
|
|
33068
|
+
const fused = fuseSignals(platform, canceller.signal);
|
|
33069
|
+
request.signal = fused;
|
|
33070
|
+
handedOff = request.signal === fused;
|
|
33071
|
+
face.handoff = handedOff ? "fused" : "ignored";
|
|
33072
|
+
} catch (error62) {
|
|
33073
|
+
face.handoff = `failed: ${messageOf3(error62)}`.slice(0, 80);
|
|
33074
|
+
}
|
|
32705
33075
|
}
|
|
32706
|
-
const
|
|
33076
|
+
const finish = (reason) => {
|
|
33077
|
+
if (!canceller.signal.aborted) canceller.abort(reason);
|
|
33078
|
+
};
|
|
33079
|
+
const desktop = Promise.resolve().then(() => next?.()).then(
|
|
33080
|
+
(value) => (
|
|
33081
|
+
// `undefined` 是"这个参与者没认领"(会把整条链冲掉),不是答案;
|
|
33082
|
+
// `'unavailable'` 是审批那条链末端的"没人可问",也不是答案。
|
|
33083
|
+
value === void 0 || isApproval && value === "unavailable" ? { answered: false, value } : { answered: true, value }
|
|
33084
|
+
),
|
|
33085
|
+
(error62) => ({ answered: false, error: error62 })
|
|
33086
|
+
);
|
|
33087
|
+
const asked = isApproval ? sink.approval({
|
|
32707
33088
|
sessionId,
|
|
32708
33089
|
action: String(request?.toolName ?? "\u5DE5\u5177\u8C03\u7528"),
|
|
32709
33090
|
...request?.reason ? { reason: String(request.reason) } : {},
|
|
32710
|
-
|
|
33091
|
+
signal: controller.signal
|
|
33092
|
+
}) : sink.question({
|
|
33093
|
+
sessionId,
|
|
33094
|
+
questions: (request?.questions ?? []).map((item, index) => mapQuestion(item, index)),
|
|
33095
|
+
signal: controller.signal
|
|
33096
|
+
});
|
|
33097
|
+
const phone = asked.then(
|
|
33098
|
+
(value) => (
|
|
33099
|
+
// 每一支都**带着自己是谁**回来:`'rejected'` 这个词手机和桌面都可能给出,
|
|
33100
|
+
// 只比数值就没法知道该收哪一张卡(第一版就是这么错的)。
|
|
33101
|
+
isApproval ? value === "allowed-once" || value === "rejected" ? { answered: true, value } : { answered: false, value } : value === null || value === void 0 ? { answered: false, value } : { answered: true, value }
|
|
33102
|
+
),
|
|
33103
|
+
// runtime 抛了(未配对、不在线、被撤)同样不算答案。
|
|
33104
|
+
() => ({ answered: false })
|
|
33105
|
+
);
|
|
33106
|
+
const pending = new Promise(() => {
|
|
32711
33107
|
});
|
|
32712
|
-
|
|
32713
|
-
|
|
33108
|
+
const winner = await Promise.race([
|
|
33109
|
+
phone.then((chain) => chain.answered ? { src: "phone", chain } : pending),
|
|
33110
|
+
desktop.then((chain) => chain.answered ? { src: "desktop", chain } : pending),
|
|
33111
|
+
Promise.all([phone, desktop]).then(([, chain]) => ({ src: "desktop", chain }))
|
|
33112
|
+
]);
|
|
33113
|
+
if (!winner.chain.answered) {
|
|
33114
|
+
controller.abort("desktop");
|
|
33115
|
+
finish("settled-without-answer");
|
|
33116
|
+
if (isApproval) {
|
|
33117
|
+
face.last = "handed-back(neither answered)";
|
|
33118
|
+
return "unavailable";
|
|
33119
|
+
}
|
|
33120
|
+
face.last = "no-answer";
|
|
33121
|
+
if (winner.chain.error !== void 0) throw winner.chain.error;
|
|
33122
|
+
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");
|
|
33123
|
+
}
|
|
33124
|
+
if (winner.src === "desktop") controller.abort("desktop");
|
|
33125
|
+
if (winner.src === "phone") {
|
|
33126
|
+
finish("answered-by-phone");
|
|
33127
|
+
if (handedOff) face.desktopVoided += 1;
|
|
33128
|
+
} else {
|
|
33129
|
+
finish("answered-by-desktop");
|
|
33130
|
+
}
|
|
33131
|
+
face.last = `${winner.src === "phone" ? "answered-by-phone" : "answered-by-desktop"}(${describeOutcome(
|
|
33132
|
+
isApproval,
|
|
33133
|
+
winner.chain.value
|
|
33134
|
+
)})`;
|
|
33135
|
+
return winner.chain.value;
|
|
33136
|
+
}
|
|
33137
|
+
function describeOutcome(isApproval, value) {
|
|
33138
|
+
if (isApproval) return String(value);
|
|
33139
|
+
const answers = value?.answers;
|
|
33140
|
+
return Array.isArray(answers) ? `${answers.length} \u9879` : typeof value;
|
|
32714
33141
|
}
|
|
32715
33142
|
function servicesPeers() {
|
|
32716
33143
|
return sink !== void 0;
|
|
@@ -32746,10 +33173,25 @@ function createServicesKernel(services, options) {
|
|
|
32746
33173
|
subscribe,
|
|
32747
33174
|
attachInteractionSink,
|
|
32748
33175
|
ensureRunnable,
|
|
32749
|
-
sessionWorkspace,
|
|
32750
33176
|
// 与发指令/续跑走同一条读取路径:这里原来还有一份实现,而且是 `current()` 裸调用
|
|
32751
33177
|
// (不带 receiver;真要用 this 的服务方法会当场抛),两份迟早分叉。
|
|
32752
33178
|
modelSelection: currentSelection,
|
|
33179
|
+
/**
|
|
33180
|
+
* 可切换模型清单。**这一代内核给不出清单也换不了**(`agentDefaultModel` 上只有
|
|
33181
|
+
* `currentSelection`),所以这里报 `canSwitch: false` 并附一句人话。
|
|
33182
|
+
*
|
|
33183
|
+
* 报false 而不是省略这个方法:省略会被 core 当成"只读"但**说不出原因**,
|
|
33184
|
+
* 手机上就只剩一个置灰的模型名,用户不知道是主机不支持还是自己哪里做错了。
|
|
33185
|
+
*/
|
|
33186
|
+
modelOptions() {
|
|
33187
|
+
const { hits, canList, canSet } = probeModelFace();
|
|
33188
|
+
return {
|
|
33189
|
+
// 两个都要有才算能切:只有 list 没有 set 时手机上会列出候选却点了没反应,
|
|
33190
|
+
// 那比"换不了"更糟。
|
|
33191
|
+
canSwitch: canList && canSet,
|
|
33192
|
+
reason: canList && canSet ? void 0 : `\u4E3B\u673A\u5185\u6838\u53EA\u63D0\u4F9B\u8BFB\u53D6\uFF08\u547D\u4E2D\uFF1A${hits.join(",") || "\u65E0"}\uFF09`
|
|
33193
|
+
};
|
|
33194
|
+
},
|
|
32753
33195
|
describe() {
|
|
32754
33196
|
return {
|
|
32755
33197
|
carrier: "services",
|
|
@@ -32761,22 +33203,47 @@ function createServicesKernel(services, options) {
|
|
|
32761
33203
|
// 新建会话只依赖这一条。和 historyFace 同理:真机上"新建没反应"第一件要分清的事
|
|
32762
33204
|
// 是宿主没有这个能力,还是我们没调对。
|
|
32763
33205
|
createFace: typeof fn(services.sessionController?.commands, "create") === "function" ? "commands.create" : `absent (keys=${shapeOf(services.sessionController)})`.slice(0, 120),
|
|
32764
|
-
//
|
|
32765
|
-
//
|
|
32766
|
-
|
|
33206
|
+
// 第二条换模型的可能路径:命令控制器上自带的方法(`setModel` / `switchModel`…),
|
|
33207
|
+
// 以及活agent 自己暴露的模型字段。前者能列出来就说明这条路通不通。
|
|
33208
|
+
commandsFace: shapeOf(services.sessionController?.commands),
|
|
33209
|
+
agentModelFields: (() => {
|
|
33210
|
+
const first = firstLiveAgent();
|
|
33211
|
+
if (!first) return "no-live-agent";
|
|
33212
|
+
return Object.keys(first).filter((k) => /model|provider|preset/i.test(k)).slice(0, 8).join(",") || "none";
|
|
33213
|
+
})(),
|
|
32767
33214
|
agents: typeof services.agents === "object",
|
|
32768
33215
|
agentDefaultModel: typeof services.agentDefaultModel === "object",
|
|
33216
|
+
// 模型面到底有没有"写"的能力(能列可选、能切换)。手机上模型下拉该不该置灰,
|
|
33217
|
+
// 取决于这个而不是取决于"读得到当前值" —— 读得到只够显示一行文字。
|
|
33218
|
+
modelFace: modelFaceSummary(),
|
|
32769
33219
|
workspaceRegistry: typeof services.workspaceRegistry === "object",
|
|
32770
33220
|
userQuestions: typeof services.userQuestions === "object",
|
|
33221
|
+
// 这个键**已经不生效**了:提问现在跟审批一样是 waterfall 参与者,两端同弹、
|
|
33222
|
+
// 谁先答谁算,不再有"接管/取代桌面"这回事(那条 `registerProvider` 单提供者
|
|
33223
|
+
// 路线在这一代宿主上根本不存在)。这里照实把配置值报出来,真正的状态看
|
|
33224
|
+
// `questionsFace`。(删键要动 config.ts/index.ts,那两棵树此刻有并行会话在飞的改动。)
|
|
32771
33225
|
takeOverQuestions: options.takeOverQuestions === true,
|
|
33226
|
+
takeOverQuestionsInert: options.takeOverQuestions === true,
|
|
32772
33227
|
archivedSessions: archivedIds().size,
|
|
32773
33228
|
hasOn: typeof services.on === "function",
|
|
32774
33229
|
// 真机排错的第一现场:内核到底发了哪些事件类型、其中哪些我们没认。
|
|
32775
33230
|
eventTypes: [...seenEventTypes].slice(-24).join("|"),
|
|
32776
33231
|
unmappedEventTypes: [...unmappedEventTypes].join("|"),
|
|
32777
33232
|
injectedUserMessages: [...injectedUserMessages].map(([kind, count]) => `${count}\xD7${kind}`).join("|") || "none",
|
|
32778
|
-
approvalFace,
|
|
32779
|
-
|
|
33233
|
+
approvalFace: faces["approval/request"].registered,
|
|
33234
|
+
approvalCalls: faces["approval/request"].calls,
|
|
33235
|
+
approvalLast: faces["approval/request"].last,
|
|
33236
|
+
approvalAsked,
|
|
33237
|
+
approvalDecided,
|
|
33238
|
+
approvalDesktopVoided: faces["approval/request"].desktopVoided,
|
|
33239
|
+
approvalSignalHandoff: faces["approval/request"].handoff,
|
|
33240
|
+
// 提问那一条与审批同构,所以读数也同一套:登记结果 / 被派发几次 / 最后走到哪一步 /
|
|
33241
|
+
// 手机先答时撤销了几次下游 signal / 那份 signal 换没换成功。
|
|
33242
|
+
questionsFace: faces["user-questions/request"].registered,
|
|
33243
|
+
questionsCalls: faces["user-questions/request"].calls,
|
|
33244
|
+
questionsLast: faces["user-questions/request"].last,
|
|
33245
|
+
questionsDesktopVoided: faces["user-questions/request"].desktopVoided,
|
|
33246
|
+
questionsSignalHandoff: faces["user-questions/request"].handoff,
|
|
32780
33247
|
listenerErrors: [...listenerErrors].slice(0, 4).map(([key, count]) => `${count}\xD7${key}`).join(" | ") || "none"
|
|
32781
33248
|
};
|
|
32782
33249
|
}
|
|
@@ -32798,17 +33265,19 @@ function sessionEventKernelEvents(input2) {
|
|
|
32798
33265
|
switch (type) {
|
|
32799
33266
|
case "turn/start":
|
|
32800
33267
|
return [{ kind: "run-state", sessionId, state: "running" }];
|
|
32801
|
-
case "assistant/message":
|
|
33268
|
+
case "assistant/message": {
|
|
33269
|
+
const text = stripToolCallXml(textOf(data));
|
|
32802
33270
|
return [
|
|
32803
33271
|
{
|
|
32804
33272
|
kind: "delta",
|
|
32805
33273
|
sessionId,
|
|
32806
33274
|
messageId: messageIdOf(data, input2.seq),
|
|
32807
|
-
text
|
|
33275
|
+
text,
|
|
32808
33276
|
role: "assistant",
|
|
32809
33277
|
done: true
|
|
32810
33278
|
}
|
|
32811
33279
|
];
|
|
33280
|
+
}
|
|
32812
33281
|
case "user/message": {
|
|
32813
33282
|
if (!isHumanUserMessage(data)) return [];
|
|
32814
33283
|
return [
|
|
@@ -32831,8 +33300,8 @@ function sessionEventKernelEvents(input2) {
|
|
|
32831
33300
|
callId: String(data.callId ?? data.id ?? `tool_${input2.seq ?? 0}`),
|
|
32832
33301
|
phase: argsRaw === void 0 || argsRaw === null ? "started" : "args",
|
|
32833
33302
|
tool: typeof data.name === "string" ? data.name : void 0,
|
|
32834
|
-
title: typeof data.title === "string" ? data.title :
|
|
32835
|
-
...argsRaw === void 0 ? {} : { argsPreview:
|
|
33303
|
+
title: typeof data.title === "string" ? data.title : argsTitle(argsRaw),
|
|
33304
|
+
...argsRaw === void 0 ? {} : { argsPreview: argsPreviewOf(argsRaw) }
|
|
32836
33305
|
}
|
|
32837
33306
|
];
|
|
32838
33307
|
}
|
|
@@ -32983,9 +33452,67 @@ function messageIdOf(data, seq) {
|
|
|
32983
33452
|
if (typeof id === "string" && id) return id;
|
|
32984
33453
|
return `msg_${String(seq ?? Date.now())}`;
|
|
32985
33454
|
}
|
|
32986
|
-
|
|
33455
|
+
var TOOL_CALL_BLOCK = /<tool_call>\s*<function=[\s\S]*?<\/tool_call>/g;
|
|
33456
|
+
var TOOL_CALL_UNCLOSED = /<tool_call>\s*<function=[\s\S]*$/;
|
|
33457
|
+
function stripToolCallXml(text) {
|
|
33458
|
+
const stripped = text.replace(TOOL_CALL_BLOCK, "").replace(TOOL_CALL_UNCLOSED, "");
|
|
33459
|
+
return stripped.trim() === "" ? "" : stripped;
|
|
33460
|
+
}
|
|
33461
|
+
function argsTitle(argsRaw) {
|
|
33462
|
+
const parsed = tryParseJson(argsRaw);
|
|
33463
|
+
if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
|
|
33464
|
+
const record2 = parsed;
|
|
33465
|
+
for (const key of ["description", "prompt", "command", "path", "file_path", "pattern", "query", "url"]) {
|
|
33466
|
+
const value = record2[key];
|
|
33467
|
+
if (typeof value === "string" && value.trim()) return oneLine(value);
|
|
33468
|
+
}
|
|
33469
|
+
}
|
|
33470
|
+
return previewOf(argsRaw);
|
|
33471
|
+
}
|
|
33472
|
+
function argsPreviewOf(argsRaw) {
|
|
33473
|
+
const parsed = tryParseJson(argsRaw);
|
|
33474
|
+
if (parsed !== void 0) {
|
|
33475
|
+
try {
|
|
33476
|
+
return previewOf(JSON.stringify(parsed, null, 2));
|
|
33477
|
+
} catch {
|
|
33478
|
+
}
|
|
33479
|
+
}
|
|
33480
|
+
return previewOf(argsRaw);
|
|
33481
|
+
}
|
|
33482
|
+
function tryParseJson(value) {
|
|
33483
|
+
if (typeof value !== "string" || !value.trim()) return void 0;
|
|
33484
|
+
try {
|
|
33485
|
+
return JSON.parse(value);
|
|
33486
|
+
} catch {
|
|
33487
|
+
return void 0;
|
|
33488
|
+
}
|
|
33489
|
+
}
|
|
33490
|
+
function oneLine(text) {
|
|
33491
|
+
const flat = text.replace(/\s+/g, " ").trim();
|
|
33492
|
+
return flat.length > 120 ? `${flat.slice(0, 120)}\u2026` : flat;
|
|
33493
|
+
}
|
|
33494
|
+
function readableText(value) {
|
|
32987
33495
|
if (value === void 0 || value === null) return void 0;
|
|
32988
|
-
|
|
33496
|
+
if (typeof value === "string") return value;
|
|
33497
|
+
if (typeof value === "number" || typeof value === "boolean") return String(value);
|
|
33498
|
+
if (Array.isArray(value)) {
|
|
33499
|
+
const parts = value.map((part) => readableText(part)).filter((part) => typeof part === "string" && part.length > 0);
|
|
33500
|
+
return parts.length > 0 ? parts.join("\n") : void 0;
|
|
33501
|
+
}
|
|
33502
|
+
if (typeof value === "object") {
|
|
33503
|
+
const record2 = value;
|
|
33504
|
+
for (const key of ["content", "text", "output", "stdout", "stderr", "message", "result", "error"]) {
|
|
33505
|
+
if (record2[key] === void 0 || record2[key] === null) continue;
|
|
33506
|
+
const nested = readableText(record2[key]);
|
|
33507
|
+
if (nested !== void 0 && nested !== "") return nested;
|
|
33508
|
+
}
|
|
33509
|
+
if (typeof record2.type === "string" && record2.type !== "text") return void 0;
|
|
33510
|
+
return safeJson(record2);
|
|
33511
|
+
}
|
|
33512
|
+
return safeJson(value);
|
|
33513
|
+
}
|
|
33514
|
+
function previewOf(value) {
|
|
33515
|
+
const text = readableText(value);
|
|
32989
33516
|
if (!text) return void 0;
|
|
32990
33517
|
return text.length > 200 ? `${text.slice(0, 200)}\u2026` : text;
|
|
32991
33518
|
}
|
|
@@ -33049,7 +33576,6 @@ function createOneShotTimers(clock) {
|
|
|
33049
33576
|
|
|
33050
33577
|
// src/shell/status.ts
|
|
33051
33578
|
import { mkdirSync, renameSync, writeFileSync } from "node:fs";
|
|
33052
|
-
import { homedir } from "node:os";
|
|
33053
33579
|
import path from "node:path";
|
|
33054
33580
|
var StatusFile = class {
|
|
33055
33581
|
constructor(file2, clock, refreshMs = 3e3) {
|
|
@@ -33061,7 +33587,6 @@ var StatusFile = class {
|
|
|
33061
33587
|
clock;
|
|
33062
33588
|
refreshMs;
|
|
33063
33589
|
timer;
|
|
33064
|
-
lastWritten = 0;
|
|
33065
33590
|
write(snapshot) {
|
|
33066
33591
|
if (!this.file) return;
|
|
33067
33592
|
try {
|
|
@@ -33075,7 +33600,6 @@ var StatusFile = class {
|
|
|
33075
33600
|
}
|
|
33076
33601
|
);
|
|
33077
33602
|
renameSync(tmp, this.file);
|
|
33078
|
-
this.lastWritten = this.clock.now();
|
|
33079
33603
|
} catch {
|
|
33080
33604
|
}
|
|
33081
33605
|
}
|
|
@@ -33104,48 +33628,176 @@ var StatusFile = class {
|
|
|
33104
33628
|
this.timer = void 0;
|
|
33105
33629
|
}
|
|
33106
33630
|
}
|
|
33107
|
-
|
|
33108
|
-
|
|
33109
|
-
|
|
33631
|
+
};
|
|
33632
|
+
|
|
33633
|
+
// src/shell/host-id.ts
|
|
33634
|
+
import { randomBytes as randomBytes4 } from "node:crypto";
|
|
33635
|
+
import { chmodSync, mkdirSync as mkdirSync2, readFileSync, writeFileSync as writeFileSync2 } from "node:fs";
|
|
33636
|
+
import path2 from "node:path";
|
|
33637
|
+
var HOST_ID = /^[A-Za-z0-9][A-Za-z0-9_.-]{0,63}$/;
|
|
33638
|
+
function newHostId() {
|
|
33639
|
+
return `drc-${randomBytes4(6).toString("hex")}`;
|
|
33640
|
+
}
|
|
33641
|
+
function resolveHostId(dir, configured) {
|
|
33642
|
+
if (configured) return configured;
|
|
33643
|
+
if (!dir) return newHostId();
|
|
33644
|
+
const file2 = path2.join(dir, "host-id");
|
|
33645
|
+
let existing;
|
|
33646
|
+
try {
|
|
33647
|
+
existing = readFileSync(file2, "utf8").trim();
|
|
33648
|
+
} catch {
|
|
33649
|
+
existing = void 0;
|
|
33110
33650
|
}
|
|
33111
|
-
|
|
33112
|
-
|
|
33651
|
+
if (existing !== void 0 && HOST_ID.test(existing)) return existing;
|
|
33652
|
+
const fresh = newHostId();
|
|
33653
|
+
try {
|
|
33654
|
+
mkdirSync2(dir, { recursive: true });
|
|
33655
|
+
writeFileSync2(file2, `${fresh}
|
|
33656
|
+
`, {
|
|
33657
|
+
encoding: "utf8",
|
|
33658
|
+
mode: 384,
|
|
33659
|
+
flag: existing === void 0 ? "wx" : "w"
|
|
33660
|
+
});
|
|
33661
|
+
chmodSync(file2, 384);
|
|
33662
|
+
return fresh;
|
|
33663
|
+
} catch {
|
|
33664
|
+
try {
|
|
33665
|
+
const raced = readFileSync(file2, "utf8").trim();
|
|
33666
|
+
if (HOST_ID.test(raced)) return raced;
|
|
33667
|
+
} catch {
|
|
33668
|
+
}
|
|
33669
|
+
return fresh;
|
|
33113
33670
|
}
|
|
33114
|
-
}
|
|
33115
|
-
|
|
33671
|
+
}
|
|
33672
|
+
|
|
33673
|
+
// src/shell/pair-store.ts
|
|
33674
|
+
import { chmodSync as chmodSync2, mkdirSync as mkdirSync3, readFileSync as readFileSync2, renameSync as renameSync2, unlinkSync, writeFileSync as writeFileSync3 } from "node:fs";
|
|
33675
|
+
import { createHash as createHash2 } from "node:crypto";
|
|
33676
|
+
import path3 from "node:path";
|
|
33677
|
+
var storedConversationSchema = external_exports.object({
|
|
33678
|
+
id: external_exports.string().min(1),
|
|
33679
|
+
/** 配对 PSK(base64)。两把方向密钥在恢复时由它现派生,不落盘。 */
|
|
33680
|
+
psk: external_exports.string().min(1),
|
|
33681
|
+
/** host→client 方向的本地序号。中继会重新编号,客户端从不读它(F13),
|
|
33682
|
+
* 但保留它能让恢复后的编号不跳号,便于对拍排查。 */
|
|
33683
|
+
seqHost: external_exports.number().int().nonnegative(),
|
|
33684
|
+
createdAt: external_exports.number().finite().nonnegative(),
|
|
33685
|
+
lastActivityAt: external_exports.number().finite().nonnegative()
|
|
33686
|
+
});
|
|
33687
|
+
var storeSchema = external_exports.object({
|
|
33688
|
+
version: external_exports.literal(1),
|
|
33689
|
+
/** 这个文件属于哪台主机:换了主机身份就不该认(凭据不该跨身份复用)。 */
|
|
33690
|
+
hostId: external_exports.string().min(1),
|
|
33691
|
+
savedAt: external_exports.number().finite().nonnegative(),
|
|
33692
|
+
conversations: external_exports.array(storedConversationSchema)
|
|
33693
|
+
});
|
|
33694
|
+
function safeSegment(hostId) {
|
|
33695
|
+
const cleaned = hostId.replace(/[^A-Za-z0-9._-]/g, "_").slice(0, 48);
|
|
33696
|
+
const tag = createHash2("sha256").update(hostId).digest("hex").slice(0, 8);
|
|
33697
|
+
return `${cleaned || "host"}-${tag}`;
|
|
33698
|
+
}
|
|
33699
|
+
function defaultPairStoreFile(statusFile, hostId) {
|
|
33116
33700
|
if (!statusFile) return "";
|
|
33117
|
-
|
|
33118
|
-
return path.join(homedir(), ".dsh", "pairing-qr.png");
|
|
33701
|
+
return path3.join(path3.dirname(statusFile), `conversations-${safeSegment(hostId)}.json`);
|
|
33119
33702
|
}
|
|
33120
|
-
|
|
33121
|
-
|
|
33703
|
+
var PairStore = class {
|
|
33704
|
+
constructor(options) {
|
|
33705
|
+
this.options = options;
|
|
33706
|
+
}
|
|
33707
|
+
options;
|
|
33708
|
+
/** 上一次成功写盘的时刻,供 status.json 回答"这份落盘是什么时候的"。 */
|
|
33709
|
+
savedAt = 0;
|
|
33710
|
+
/** 上次 save 之后发生过任何变化(含只改了 lastActivityAt)。 */
|
|
33711
|
+
dirty = false;
|
|
33712
|
+
get file() {
|
|
33713
|
+
return this.options.file;
|
|
33714
|
+
}
|
|
33715
|
+
get lastSavedAt() {
|
|
33716
|
+
return this.savedAt;
|
|
33717
|
+
}
|
|
33718
|
+
/** 有没有待落盘的变更。status 的 3 秒 tick 用它决定是否真写盘。 */
|
|
33719
|
+
get hasPendingChanges() {
|
|
33720
|
+
return this.dirty;
|
|
33721
|
+
}
|
|
33722
|
+
/** 只标脏,不写盘。高频变动(每次广播都动 `seqHost`)走这条。 */
|
|
33723
|
+
markDirty() {
|
|
33724
|
+
this.dirty = true;
|
|
33725
|
+
}
|
|
33726
|
+
/**
|
|
33727
|
+
* 读回本机的会话记录。
|
|
33728
|
+
*
|
|
33729
|
+
* 三种"读不出来"都归到**空簿 + 一条日志**,绝不抛(红线:插件起不来比丢配对更糟):
|
|
33730
|
+
* 文件不存在(第一次跑)、JSON 坏了(半写/被手改)、形状不认识(版本升级)。
|
|
33731
|
+
* `hostId` 不匹配也算读不出来:那份文件属于另一台主机。
|
|
33732
|
+
*/
|
|
33733
|
+
load() {
|
|
33734
|
+
const { file: file2, hostId, log } = this.options;
|
|
33735
|
+
if (!file2) return [];
|
|
33736
|
+
let raw;
|
|
33737
|
+
try {
|
|
33738
|
+
raw = readFileSync2(file2, "utf8");
|
|
33739
|
+
} catch {
|
|
33740
|
+
return [];
|
|
33741
|
+
}
|
|
33742
|
+
const parsed = storeSchema.safeParse(safeJsonParse(raw));
|
|
33743
|
+
if (!parsed.success) {
|
|
33744
|
+
log?.("pair-store unreadable, starting with an empty book", {
|
|
33745
|
+
reason: parsed.error.issues[0]?.message?.slice(0, 80) ?? "shape mismatch"
|
|
33746
|
+
});
|
|
33747
|
+
return [];
|
|
33748
|
+
}
|
|
33749
|
+
if (parsed.data.hostId !== hostId) {
|
|
33750
|
+
log?.("pair-store belongs to another host, starting with an empty book", {
|
|
33751
|
+
fileHostId: parsed.data.hostId.slice(0, 24)
|
|
33752
|
+
});
|
|
33753
|
+
return [];
|
|
33754
|
+
}
|
|
33755
|
+
this.savedAt = parsed.data.savedAt;
|
|
33756
|
+
return parsed.data.conversations;
|
|
33757
|
+
}
|
|
33758
|
+
/**
|
|
33759
|
+
* 写盘。**结构性的变化(新建/作废会话)必须立刻调这个**;
|
|
33760
|
+
* 只改了 `lastActivityAt`/`seqHost` 的场合走 {@link markDirty},由 3 秒 tick 批量落。
|
|
33761
|
+
*
|
|
33762
|
+
* 失败只记日志:写不进盘(目录只读、磁盘满)时,本次运行仍能正常收发,
|
|
33763
|
+
* 只是下次重启要重新扫码 —— 那是可接受的退化,让插件崩掉不是。
|
|
33764
|
+
*/
|
|
33765
|
+
save(conversations, now) {
|
|
33766
|
+
const { file: file2, hostId, log } = this.options;
|
|
33767
|
+
if (!file2) return false;
|
|
33768
|
+
const body = JSON.stringify({ version: 1, hostId, savedAt: now, conversations }) + "\n";
|
|
33769
|
+
const tmp = `${file2}.tmp-${process.pid}`;
|
|
33770
|
+
try {
|
|
33771
|
+
mkdirSync3(path3.dirname(file2), { recursive: true });
|
|
33772
|
+
writeFileSync3(tmp, body, { encoding: "utf8", mode: 384 });
|
|
33773
|
+
chmodSync2(tmp, 384);
|
|
33774
|
+
renameSync2(tmp, file2);
|
|
33775
|
+
this.savedAt = now;
|
|
33776
|
+
this.dirty = false;
|
|
33777
|
+
return true;
|
|
33778
|
+
} catch (error62) {
|
|
33779
|
+
log?.("pair-store save failed (pairing will need a rescan after the next restart)", {
|
|
33780
|
+
message: String(error62?.message ?? error62).slice(0, 120)
|
|
33781
|
+
});
|
|
33782
|
+
try {
|
|
33783
|
+
unlinkSync(tmp);
|
|
33784
|
+
} catch {
|
|
33785
|
+
}
|
|
33786
|
+
return false;
|
|
33787
|
+
}
|
|
33788
|
+
}
|
|
33789
|
+
};
|
|
33790
|
+
function safeJsonParse(raw) {
|
|
33122
33791
|
try {
|
|
33123
|
-
|
|
33124
|
-
writeFileSync(file2, data, { mode: 384 });
|
|
33125
|
-
return true;
|
|
33792
|
+
return JSON.parse(raw);
|
|
33126
33793
|
} catch {
|
|
33127
|
-
return
|
|
33794
|
+
return void 0;
|
|
33128
33795
|
}
|
|
33129
33796
|
}
|
|
33130
33797
|
|
|
33131
|
-
// src/shell/pairing-text.ts
|
|
33132
|
-
var PAIR_UNAVAILABLE_TEXT = "\u4E2D\u7EE7\u8FD8\u6CA1\u8FDE\u4E0A\uFF0C\u6682\u65F6\u65E0\u6CD5\u914D\u5BF9\u3002\u770B status.json \u7684 relayProblem\u3002";
|
|
33133
|
-
function pairingPairText(view) {
|
|
33134
|
-
const ttlSec = Math.max(0, Math.round((view.expiresAt - view.now) / 1e3));
|
|
33135
|
-
const lines = [`\u914D\u5BF9\u7801 ${view.token} \xB7 ${ttlSec} \u79D2\u5185\u6709\u6548 \xB7 \u4E00\u6B21\u6027`, "\u5C0F\u7A0B\u5E8F\u91CC\u626B\u7801\uFF0C\u6216\u624B\u8F93\u8FD9 6 \u4F4D\u6570\u5B57\u3002"];
|
|
33136
|
-
if (view.qrImage && view.imageFile) {
|
|
33137
|
-
lines.push("", `\u4E8C\u7EF4\u7801\u4F1A\u81EA\u52A8\u5F39\u51FA\u5230\u53F3\u680F\uFF1B\u6CA1\u5F39\u51FA\u5C31\u6253\u5F00\u8FD9\u5F20 PNG \u518D\u626B\uFF1A${view.imageFile}`);
|
|
33138
|
-
} else if (view.terminalQr) {
|
|
33139
|
-
lines.push("", view.terminalQr);
|
|
33140
|
-
}
|
|
33141
|
-
lines.push("", view.qr);
|
|
33142
|
-
if (!view.qrImage) lines.push("", "\uFF08\u5982\u9700\u56FE\u7247\u7248\uFF1A\u628A patch \u91CC\u7684 qrImage \u8BBE\u4E3A true\uFF09");
|
|
33143
|
-
return lines.join("\n");
|
|
33144
|
-
}
|
|
33145
|
-
|
|
33146
33798
|
// src/shell/config.ts
|
|
33147
|
-
import { homedir
|
|
33148
|
-
import
|
|
33799
|
+
import { homedir } from "node:os";
|
|
33800
|
+
import path4 from "node:path";
|
|
33149
33801
|
var DEFAULT_CONFIG = {
|
|
33150
33802
|
enabled: true,
|
|
33151
33803
|
serverUrl: "ws://127.0.0.1:8787",
|
|
@@ -33155,35 +33807,32 @@ var DEFAULT_CONFIG = {
|
|
|
33155
33807
|
hostLabel: "dsh-host",
|
|
33156
33808
|
keepAwake: { enabled: true, idleReleaseSec: 300, keepDisplay: false },
|
|
33157
33809
|
mockBridge: false,
|
|
33158
|
-
statusFile:
|
|
33810
|
+
statusFile: path4.join(homedir(), ".dsh", "dsh-remote-control", "status.json"),
|
|
33159
33811
|
unarchiveOnPrompt: true,
|
|
33160
33812
|
pairOnStartSec: 0,
|
|
33161
33813
|
pairTtlMs: 12e4,
|
|
33162
|
-
// **默认走图片**。2026-10-02 取证推翻了原判断(原判断:文本二维码在任何等宽渲染器下
|
|
33163
|
-
// 都能扫,图片是可有可无的旁路):
|
|
33164
|
-
// · DSH 命令卡的等宽输出块是 `white-space: pre` + `line-height: 1.6`(app bundle 里
|
|
33165
|
-
// 的那条规则,实测行间留白约占行高 37%),每两行模块之间被塞进一条整行宽的缝隙,
|
|
33166
|
-
// 半块二维码被横切成条。
|
|
33167
|
-
// · 用用户真机贴出来的那段输出做受控实验:把字号行距调到 1.0 时 zxing 解得出来,
|
|
33168
|
-
// 调到 ≥1.15 就扫不出来;而 DSH 固定 1.6。也就是说"文本码能扫"在**唯一宿主**上不成立。
|
|
33169
|
-
// · 而 ANSI 反色救不了:TerminalBlock 把每行渲染成内联 span,背景只覆盖字形盒,
|
|
33170
|
-
// 不覆盖行间留白(见 dsh-client-ui-primitives 的 parseAnsiLines/renderLine)。
|
|
33171
|
-
// 图片是自包含位图矩阵,跟宿主的行高、字体、配色都无关——这才是"扫得出来"的那条路。
|
|
33172
|
-
qrImage: true,
|
|
33173
|
-
// 写图片 ≠ 替用户打开查看器:仍然默认关(原话「不要用打开一个图片的方式」)。
|
|
33174
|
-
qrOpen: false,
|
|
33175
|
-
qrAnsi: false,
|
|
33176
|
-
qrStyle: "half",
|
|
33177
33814
|
carrierGraceMs: 5e3,
|
|
33178
33815
|
takeOverQuestions: false,
|
|
33179
33816
|
approvalTimeoutSec: 180,
|
|
33180
33817
|
listingRefreshSec: 15,
|
|
33181
33818
|
conversationIdleTtlSec: 86400,
|
|
33182
|
-
//
|
|
33183
|
-
|
|
33184
|
-
//
|
|
33185
|
-
|
|
33819
|
+
// 恢复出来的会话按 7 天剪,与中继的 DRC_CONV_IDLE_TTL_MS 对齐(理由见该字段注释)。
|
|
33820
|
+
restoredIdleTtlSec: 604800,
|
|
33821
|
+
// 密钥簿落盘:默认跟着 statusFile 走(见 `resolvePairStoreFile`)。
|
|
33822
|
+
pairStoreFile: "",
|
|
33823
|
+
// 配对的唯一入口。关掉它 = 这台主机**没有**配对入口(`/drc pair` 与文本二维码都在
|
|
33824
|
+
// 2026-10-03 删掉了),所以 index.ts 会把它记成一条 warn 而不是安静地什么都不做。
|
|
33825
|
+
pill: { enabled: true }
|
|
33186
33826
|
};
|
|
33827
|
+
var PAIR_STORE_OFF = "off";
|
|
33828
|
+
function resolvePairStoreFile(config2, hostId) {
|
|
33829
|
+
if (config2.pairStoreFile === PAIR_STORE_OFF) return "";
|
|
33830
|
+
const configured = config2.pairStoreFile.trim();
|
|
33831
|
+
if (!configured) return defaultPairStoreFile(config2.statusFile, hostId);
|
|
33832
|
+
if (configured.startsWith("/")) return configured;
|
|
33833
|
+
if (!config2.statusFile) return configured;
|
|
33834
|
+
return path4.join(path4.dirname(config2.statusFile), configured);
|
|
33835
|
+
}
|
|
33187
33836
|
var loopback = /^wss?:\/\/(localhost|127\.0\.0\.1|\[::1\])(:|\/|$)/;
|
|
33188
33837
|
var wsUrl = /^wss?:\/\/.+/;
|
|
33189
33838
|
function envSeconds(value) {
|
|
@@ -33200,23 +33849,16 @@ function readConfig(injected, env = process.env) {
|
|
|
33200
33849
|
...DEFAULT_CONFIG,
|
|
33201
33850
|
...injected ?? {},
|
|
33202
33851
|
keepAwake: { ...DEFAULT_CONFIG.keepAwake, ...injected?.keepAwake ?? {} },
|
|
33203
|
-
//
|
|
33204
|
-
//
|
|
33205
|
-
|
|
33852
|
+
// 逐键合并:patch 里只写 `pill.enabled` 时不能把整个对象覆盖掉(虽然它现在只有一个键,
|
|
33853
|
+
// 但"注入对象整体替换默认对象"这个形状在 keepAwake 上已经坑过一次)。
|
|
33854
|
+
pill: { ...DEFAULT_CONFIG.pill, ...injected?.pill ?? {} }
|
|
33206
33855
|
};
|
|
33207
33856
|
const fromEnv = (name) => env[name];
|
|
33208
33857
|
if (fromEnv("DRC_SERVER_URL")) merged.serverUrl = env.DRC_SERVER_URL;
|
|
33209
33858
|
if (fromEnv("DRC_HOST_LABEL")) merged.hostLabel = env.DRC_HOST_LABEL;
|
|
33210
33859
|
if (fromEnv("DRC_HOST_ID")) merged.hostId = env.DRC_HOST_ID;
|
|
33211
33860
|
if (fromEnv("DRC_PAIR_ON_START_SEC")) merged.pairOnStartSec = envSeconds(env.DRC_PAIR_ON_START_SEC);
|
|
33212
|
-
if (fromEnv("
|
|
33213
|
-
if (fromEnv("DRC_QR_OPEN")) merged.qrOpen = envFlag(env.DRC_QR_OPEN, merged.qrOpen);
|
|
33214
|
-
if (fromEnv("DRC_QR_ANSI")) merged.qrAnsi = envFlag(env.DRC_QR_ANSI, merged.qrAnsi);
|
|
33215
|
-
if (fromEnv("DRC_SIDEBAR_QR")) merged.sidebarQr.enabled = envFlag(env.DRC_SIDEBAR_QR, merged.sidebarQr.enabled);
|
|
33216
|
-
if (fromEnv("DRC_QR_STYLE")) {
|
|
33217
|
-
const style = env.DRC_QR_STYLE;
|
|
33218
|
-
if (["ascii", "block", "half"].includes(style)) merged.qrStyle = style;
|
|
33219
|
-
}
|
|
33861
|
+
if (fromEnv("DRC_PILL")) merged.pill.enabled = envFlag(env.DRC_PILL, merged.pill.enabled);
|
|
33220
33862
|
if (fromEnv("DRC_MOCK_BRIDGE") === "1") merged.mockBridge = true;
|
|
33221
33863
|
if (fromEnv("DRC_TAKE_OVER_QUESTIONS") === "1") merged.takeOverQuestions = true;
|
|
33222
33864
|
if (merged.hostTokenEnv) merged.hostToken = env[merged.hostTokenEnv] ?? merged.hostToken;
|
|
@@ -33267,21 +33909,6 @@ function validateConfig(config2) {
|
|
|
33267
33909
|
problems.push({ level: "warn", field: "pairOnStartSec", message: "\u5FC5\u987B\u662F\u975E\u8D1F\u7684\u6709\u9650\u79D2\u6570\uFF0C\u5DF2\u6298\u56DE 0\uFF08\u5173\u95ED\u81EA\u52A8\u53D1\u7801\uFF09" });
|
|
33268
33910
|
config2.pairOnStartSec = 0;
|
|
33269
33911
|
}
|
|
33270
|
-
if (config2.qrOpen && !config2.qrImage) {
|
|
33271
|
-
problems.push({
|
|
33272
|
-
level: "warn",
|
|
33273
|
-
field: "qrOpen",
|
|
33274
|
-
message: "qrOpen \u9700\u8981 qrImage=true \u624D\u4F1A\u5199\u56FE\u7247\u6587\u4EF6\uFF1B\u73B0\u5728\u53EA\u51FA\u6587\u672C\u4E8C\u7EF4\u7801\uFF08\u6587\u672C\u7801\u5728 DSH \u547D\u4EE4\u5361\u91CC\u626B\u4E0D\u51FA\u6765\uFF0C\u89C1 config.ts \u9ED8\u8BA4\u503C\u6CE8\u91CA\uFF09"
|
|
33275
|
-
});
|
|
33276
|
-
}
|
|
33277
|
-
if (!["ascii", "block", "half"].includes(config2.qrStyle)) {
|
|
33278
|
-
problems.push({
|
|
33279
|
-
level: "warn",
|
|
33280
|
-
field: "qrStyle",
|
|
33281
|
-
message: `qrStyle \u53EA\u8BA4 ascii/block/half\uFF0C\u6536\u5230 ${JSON.stringify(config2.qrStyle)}\uFF0C\u5DF2\u9000\u56DE half`
|
|
33282
|
-
});
|
|
33283
|
-
config2.qrStyle = "half";
|
|
33284
|
-
}
|
|
33285
33912
|
if (!secondsSchema.safeParse(config2.conversationIdleTtlSec).success || config2.conversationIdleTtlSec <= 0) {
|
|
33286
33913
|
problems.push({
|
|
33287
33914
|
level: "warn",
|
|
@@ -33290,6 +33917,14 @@ function validateConfig(config2) {
|
|
|
33290
33917
|
});
|
|
33291
33918
|
config2.conversationIdleTtlSec = 86400;
|
|
33292
33919
|
}
|
|
33920
|
+
if (!secondsSchema.safeParse(config2.restoredIdleTtlSec).success || config2.restoredIdleTtlSec <= 0) {
|
|
33921
|
+
problems.push({
|
|
33922
|
+
level: "warn",
|
|
33923
|
+
field: "restoredIdleTtlSec",
|
|
33924
|
+
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'
|
|
33925
|
+
});
|
|
33926
|
+
config2.restoredIdleTtlSec = 604800;
|
|
33927
|
+
}
|
|
33293
33928
|
if (config2.pairOnStartSec > 0) {
|
|
33294
33929
|
problems.push({
|
|
33295
33930
|
level: "warn",
|
|
@@ -33307,77 +33942,18 @@ function validateConfig(config2) {
|
|
|
33307
33942
|
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"
|
|
33308
33943
|
});
|
|
33309
33944
|
}
|
|
33310
|
-
if (typeof config2.sidebarQr.imageFile !== "string" || config2.sidebarQr.imageFile === "") {
|
|
33311
|
-
problems.push({
|
|
33312
|
-
level: "warn",
|
|
33313
|
-
field: "sidebarQr.imageFile",
|
|
33314
|
-
message: "\u4E3A\u7A7A\u5C31\u65E0\u5904\u843D\u515C\u5E95\u56FE\uFF0C\u5DF2\u5939\u56DE ~/.dsh/sidebar-qr.png\uFF08\u5DE5\u4F5C\u533A\u89E3\u6790\u5F97\u51FA\u6765\u65F6\u672C\u6765\u4E5F\u7528\u4E0D\u5230\u5B83\uFF09"
|
|
33315
|
-
});
|
|
33316
|
-
config2.sidebarQr.imageFile = DEFAULT_CONFIG.sidebarQr.imageFile;
|
|
33317
|
-
} else if (!path2.isAbsolute(config2.sidebarQr.imageFile)) {
|
|
33318
|
-
problems.push({
|
|
33319
|
-
level: "warn",
|
|
33320
|
-
field: "sidebarQr.imageFile",
|
|
33321
|
-
message: `\u4E0D\u662F\u7EDD\u5BF9\u8DEF\u5F84\uFF08${config2.sidebarQr.imageFile}\uFF09\uFF0C\u5DE5\u4F5C\u533A\u89E3\u6790\u4E0D\u51FA\u6765\u65F6\u4F1A\u88AB\u89E3\u6790\u5230\u8FDB\u7A0B\u5DE5\u4F5C\u76EE\u5F55\u4E0B`
|
|
33322
|
-
});
|
|
33323
|
-
}
|
|
33324
|
-
if (!secondsSchema.safeParse(config2.sidebarQr.refreshMs).success || config2.sidebarQr.refreshMs < 200) {
|
|
33325
|
-
problems.push({
|
|
33326
|
-
level: "warn",
|
|
33327
|
-
field: "sidebarQr.refreshMs",
|
|
33328
|
-
message: "\u5FC5\u987B\u662F \u2265200 \u7684\u6709\u9650\u6BEB\u79D2\u6570\uFF0C\u5DF2\u5939\u56DE 2000\uFF08\u539F\u503C " + JSON.stringify(config2.sidebarQr.refreshMs) + "\uFF09"
|
|
33329
|
-
});
|
|
33330
|
-
config2.sidebarQr.refreshMs = DEFAULT_CONFIG.sidebarQr.refreshMs;
|
|
33331
|
-
}
|
|
33332
33945
|
return problems;
|
|
33333
33946
|
}
|
|
33334
|
-
function
|
|
33947
|
+
function redact(value) {
|
|
33335
33948
|
if (value.length <= 8) return "****";
|
|
33336
33949
|
return `${value.slice(0, 4)}\u2026${value.slice(-2)}(${value.length})`;
|
|
33337
33950
|
}
|
|
33338
33951
|
|
|
33952
|
+
// src/pill/routes.ts
|
|
33953
|
+
import { createHash as createHash3 } from "node:crypto";
|
|
33954
|
+
|
|
33339
33955
|
// src/platform/qr.ts
|
|
33340
33956
|
var import_qrcode = __toESM(require_lib(), 1);
|
|
33341
|
-
async function qrMatrix(text, ecc = "M", quietZone = 2) {
|
|
33342
|
-
const qr = import_qrcode.default.create(text, { errorCorrectionLevel: ecc });
|
|
33343
|
-
const size = qr.modules.size;
|
|
33344
|
-
const total = size + quietZone * 2;
|
|
33345
|
-
const out = [];
|
|
33346
|
-
for (let y = 0; y < total; y++) {
|
|
33347
|
-
const row = [];
|
|
33348
|
-
for (let x = 0; x < total; x++) {
|
|
33349
|
-
const mx = x - quietZone;
|
|
33350
|
-
const my = y - quietZone;
|
|
33351
|
-
const dark = mx >= 0 && mx < size && my >= 0 && my < size && Boolean(qr.modules.get(my, mx));
|
|
33352
|
-
row.push(dark);
|
|
33353
|
-
}
|
|
33354
|
-
out.push(row);
|
|
33355
|
-
}
|
|
33356
|
-
return out;
|
|
33357
|
-
}
|
|
33358
|
-
async function renderTerminalQr(text, options = {}) {
|
|
33359
|
-
const { ecc = "M", quietZone = 2, style = "ascii", ansi = false } = options;
|
|
33360
|
-
const matrix = await qrMatrix(text, ecc, quietZone);
|
|
33361
|
-
let body;
|
|
33362
|
-
if (style === "half") {
|
|
33363
|
-
const lines = [];
|
|
33364
|
-
for (let y = 0; y < matrix.length; y += 2) {
|
|
33365
|
-
const top = matrix[y];
|
|
33366
|
-
const bottom = matrix[y + 1] ?? top.map(() => false);
|
|
33367
|
-
let line = "";
|
|
33368
|
-
for (let x = 0; x < top.length; x++) {
|
|
33369
|
-
line += top[x] && bottom[x] ? "\u2588" : top[x] ? "\u2580" : bottom[x] ? "\u2584" : " ";
|
|
33370
|
-
}
|
|
33371
|
-
lines.push(line);
|
|
33372
|
-
}
|
|
33373
|
-
body = lines.join("\n");
|
|
33374
|
-
} else {
|
|
33375
|
-
const dark = style === "ascii" ? "##" : "\u2588\u2588";
|
|
33376
|
-
const light = style === "ascii" ? ".." : " ";
|
|
33377
|
-
body = matrix.map((row) => row.map((cell) => cell ? dark : light).join("")).join("\n");
|
|
33378
|
-
}
|
|
33379
|
-
return ansi ? `\x1B[30;107m${body}\x1B[0m` : body;
|
|
33380
|
-
}
|
|
33381
33957
|
async function qrPng(text, options = {}) {
|
|
33382
33958
|
return import_qrcode.default.toBuffer(text, {
|
|
33383
33959
|
errorCorrectionLevel: options.ecc ?? "M",
|
|
@@ -33387,112 +33963,19 @@ async function qrPng(text, options = {}) {
|
|
|
33387
33963
|
});
|
|
33388
33964
|
}
|
|
33389
33965
|
|
|
33390
|
-
// src/
|
|
33391
|
-
|
|
33392
|
-
|
|
33393
|
-
|
|
33394
|
-
var
|
|
33395
|
-
|
|
33396
|
-
|
|
33397
|
-
|
|
33398
|
-
|
|
33399
|
-
|
|
33400
|
-
|
|
33401
|
-
|
|
33402
|
-
return Number.isFinite(parsed) ? parsed : 0;
|
|
33403
|
-
}
|
|
33404
|
-
return 0;
|
|
33405
|
-
}
|
|
33406
|
-
var PairingPresenter = class {
|
|
33407
|
-
constructor(deps) {
|
|
33408
|
-
this.deps = deps;
|
|
33409
|
-
}
|
|
33410
|
-
deps;
|
|
33411
|
-
value = { state: "none" };
|
|
33412
|
-
artifact = null;
|
|
33413
|
-
rendering = false;
|
|
33414
|
-
snapshot() {
|
|
33415
|
-
return this.value;
|
|
33416
|
-
}
|
|
33417
|
-
/**
|
|
33418
|
-
* 当前这一版的 PNG 字节;没有可看的码时是 null。
|
|
33419
|
-
*
|
|
33420
|
-
* **路由必须先拿它落盘成功,再回答 `ready`**——反过来就等于把用户送到一个不存在的
|
|
33421
|
-
* 文件上(右栏会弹出一张破图,比不弹更糟)。
|
|
33422
|
-
*/
|
|
33423
|
-
current() {
|
|
33424
|
-
return this.value.state === "ready" ? this.artifact : null;
|
|
33425
|
-
}
|
|
33426
|
-
/**
|
|
33427
|
-
* 看一眼当前的码。
|
|
33428
|
-
*
|
|
33429
|
-
* 三种结果:
|
|
33430
|
-
* - 没有码 / 码已过期 → `none`(右栏不该被一张废码顶开);
|
|
33431
|
-
* - 码没变 → 只更新 `expiresAt`(同一版码的过期时间可能被服务端 TTL 改写);
|
|
33432
|
-
* - 码变了 → 渲染成功后认这一版;渲染抛错就是 `none`(宁可不弹,也不弹一张上一版的图)。
|
|
33433
|
-
*/
|
|
33434
|
-
async observe(pairing) {
|
|
33435
|
-
const qr = typeof pairing?.qr === "string" && pairing.qr !== "" ? pairing.qr : null;
|
|
33436
|
-
const expiresAt = toEpochMs(pairing?.expiresAt);
|
|
33437
|
-
if (qr === null || expiresAt <= this.deps.now()) {
|
|
33438
|
-
this.value = { state: "none" };
|
|
33439
|
-
this.artifact = null;
|
|
33440
|
-
return;
|
|
33441
|
-
}
|
|
33442
|
-
const epoch = this.deps.digest(qr);
|
|
33443
|
-
if (this.artifact?.epoch !== epoch) {
|
|
33444
|
-
if (this.rendering) return;
|
|
33445
|
-
this.rendering = true;
|
|
33446
|
-
try {
|
|
33447
|
-
const buffer = await this.deps.render(qr);
|
|
33448
|
-
this.artifact = { epoch, buffer };
|
|
33449
|
-
} catch (error62) {
|
|
33450
|
-
this.artifact = null;
|
|
33451
|
-
this.deps.log("\u4E8C\u7EF4\u7801\u6E32\u67D3\u5931\u8D25", { message: String(error62?.message ?? error62).slice(0, 200) });
|
|
33452
|
-
} finally {
|
|
33453
|
-
this.rendering = false;
|
|
33454
|
-
}
|
|
33455
|
-
}
|
|
33456
|
-
this.value = this.artifact?.epoch === epoch ? { state: "ready", epoch, expiresAt } : { state: "none" };
|
|
33457
|
-
}
|
|
33458
|
-
};
|
|
33459
|
-
function liveDeps(log) {
|
|
33460
|
-
return {
|
|
33461
|
-
now: () => Date.now(),
|
|
33462
|
-
render: (qr) => import_qrcode2.default.toBuffer(qr, { errorCorrectionLevel: "M", margin: 4, scale: 8, type: "png" }),
|
|
33463
|
-
digest: (qr) => createHash2("sha256").update(qr).digest("hex").slice(0, 16),
|
|
33464
|
-
log
|
|
33465
|
-
};
|
|
33466
|
-
}
|
|
33467
|
-
function writePrivatePng(file2, data) {
|
|
33468
|
-
if (!file2) return false;
|
|
33469
|
-
try {
|
|
33470
|
-
mkdirSync2(path3.dirname(file2), { recursive: true });
|
|
33471
|
-
const tmp = `${file2}.tmp-${process.pid}`;
|
|
33472
|
-
writeFileSync2(tmp, data, { mode: 384 });
|
|
33473
|
-
renameSync2(tmp, file2);
|
|
33474
|
-
return true;
|
|
33475
|
-
} catch {
|
|
33476
|
-
return false;
|
|
33477
|
-
}
|
|
33478
|
-
}
|
|
33479
|
-
|
|
33480
|
-
// src/presentation/address.ts
|
|
33481
|
-
var FILE_ADDRESS_PREFIX = "dsh-resource://file/";
|
|
33482
|
-
function encodeSegment(segment) {
|
|
33483
|
-
return encodeURIComponent(segment).replace(/%3A/gi, ":");
|
|
33484
|
-
}
|
|
33485
|
-
function sessionFileAddress(sessionId, filePath) {
|
|
33486
|
-
const normalized = filePath.replace(/\\/g, "/");
|
|
33487
|
-
const encoded = normalized.split("/").map(encodeSegment).join("/");
|
|
33488
|
-
return `${FILE_ADDRESS_PREFIX}session/${encodeSegment(sessionId)}/${encoded}`;
|
|
33489
|
-
}
|
|
33490
|
-
function isUsableSessionId(value) {
|
|
33491
|
-
return typeof value === "string" && /^[A-Za-z0-9._:-]{1,200}$/.test(value);
|
|
33966
|
+
// src/pill/routes.ts
|
|
33967
|
+
var PAIR_NEW_ROUTE = "/plugins/dsh-remote-control/pairing/new";
|
|
33968
|
+
var PAIR_IMAGE_ROUTE = "/plugins/dsh-remote-control/pairing.png";
|
|
33969
|
+
var PAIR_STATUS_ROUTE = "/plugins/dsh-remote-control/status";
|
|
33970
|
+
var PAIR_UNPAIR_ROUTE = "/plugins/dsh-remote-control/unpair";
|
|
33971
|
+
var PAIR_MARKER_HEADER = "x-drc-pair";
|
|
33972
|
+
var PAIR_MARKER_VALUE = "1";
|
|
33973
|
+
function pairingEpoch(qr) {
|
|
33974
|
+
return createHash3("sha256").update(qr).digest("hex").slice(0, 16);
|
|
33975
|
+
}
|
|
33976
|
+
async function renderPairingPng(qr) {
|
|
33977
|
+
return qrPng(qr);
|
|
33492
33978
|
}
|
|
33493
|
-
|
|
33494
|
-
// src/presentation/route.ts
|
|
33495
|
-
var PAIRING_ROUTE = "/plugins/dsh-remote-control/pairing";
|
|
33496
33979
|
var LOOPBACK_HOSTS = /* @__PURE__ */ new Set(["localhost", "127.0.0.1", "::1", "[::1]"]);
|
|
33497
33980
|
function hostnameOf(host) {
|
|
33498
33981
|
const trimmed = host.trim().toLowerCase();
|
|
@@ -33513,161 +33996,227 @@ function hostIsLoopback(host) {
|
|
|
33513
33996
|
if (host === void 0 || host.trim() === "") return false;
|
|
33514
33997
|
return LOOPBACK_HOSTS.has(hostnameOf(host));
|
|
33515
33998
|
}
|
|
33516
|
-
function
|
|
33517
|
-
if (origin === void 0) return true;
|
|
33999
|
+
function originIsLoopbackOrAbsent(origin) {
|
|
34000
|
+
if (origin === void 0 || origin.trim() === "") return true;
|
|
34001
|
+
return originLooksLikeTheHostItself(origin);
|
|
34002
|
+
}
|
|
34003
|
+
function originLooksLikeTheHostItself(origin) {
|
|
33518
34004
|
try {
|
|
33519
|
-
const
|
|
33520
|
-
return
|
|
34005
|
+
const url2 = new URL(origin);
|
|
34006
|
+
if (url2.protocol !== "http:" && url2.protocol !== "https:") return origin.trim() !== "null";
|
|
34007
|
+
return LOOPBACK_HOSTS.has(url2.hostname);
|
|
33521
34008
|
} catch {
|
|
33522
34009
|
return false;
|
|
33523
34010
|
}
|
|
33524
34011
|
}
|
|
34012
|
+
function rejectedBy(request) {
|
|
34013
|
+
if (!hostIsLoopback(request.headers.host)) return "host-not-loopback";
|
|
34014
|
+
if (request.headers[PAIR_MARKER_HEADER] !== PAIR_MARKER_VALUE) return "pair-marker-missing";
|
|
34015
|
+
const origin = request.headers.origin;
|
|
34016
|
+
if (origin !== void 0 && origin.trim() !== "" && !originLooksLikeTheHostItself(origin)) return "origin-not-trusted";
|
|
34017
|
+
return void 0;
|
|
34018
|
+
}
|
|
33525
34019
|
function json2(response, status, body) {
|
|
33526
34020
|
const payload = JSON.stringify(body);
|
|
33527
34021
|
response.writeHead(status, { "Content-Type": "application/json", "Content-Length": Buffer.byteLength(payload) });
|
|
33528
34022
|
response.end(payload);
|
|
33529
34023
|
}
|
|
33530
|
-
function
|
|
33531
|
-
return
|
|
33532
|
-
if (request.method !== "
|
|
34024
|
+
function pairNewHandler(deps) {
|
|
34025
|
+
return (request, response) => {
|
|
34026
|
+
if (request.method !== "POST") {
|
|
33533
34027
|
json2(response, 405, { error: "method not allowed" });
|
|
33534
34028
|
return;
|
|
33535
34029
|
}
|
|
33536
|
-
|
|
33537
|
-
|
|
34030
|
+
const guard = rejectedBy(request);
|
|
34031
|
+
if (guard) {
|
|
34032
|
+
deps.log("\u53D1\u7801\u8BF7\u6C42\u88AB\u5B88\u536B\u62D2", { guard });
|
|
34033
|
+
json2(response, 403, { error: "request-not-trusted", guard });
|
|
33538
34034
|
return;
|
|
33539
34035
|
}
|
|
33540
34036
|
try {
|
|
33541
|
-
const
|
|
33542
|
-
|
|
33543
|
-
|
|
33544
|
-
if (snapshot.state !== "ready") {
|
|
33545
|
-
json2(response, 200, { state: "none" });
|
|
33546
|
-
return;
|
|
33547
|
-
}
|
|
33548
|
-
if (!isUsableSessionId(sessionId)) {
|
|
33549
|
-
json2(response, 400, { state: "none", error: "session parameter required" });
|
|
33550
|
-
return;
|
|
33551
|
-
}
|
|
33552
|
-
const target = options.target(sessionId);
|
|
33553
|
-
if (!target.persist()) {
|
|
33554
|
-
json2(response, 200, { state: "none" });
|
|
34037
|
+
const pairing = deps.ensureFresh();
|
|
34038
|
+
if (!pairing) {
|
|
34039
|
+
json2(response, 200, { state: "unavailable", reason: "relay-offline" });
|
|
33555
34040
|
return;
|
|
33556
34041
|
}
|
|
34042
|
+
deps.log("\u914D\u5BF9\u7801\u7ECF\u72B6\u6001\u680F\u53D1\u51FA", { token: pairing.token });
|
|
33557
34043
|
json2(response, 200, {
|
|
33558
34044
|
state: "ready",
|
|
33559
|
-
epoch:
|
|
33560
|
-
|
|
33561
|
-
|
|
34045
|
+
epoch: pairingEpoch(pairing.qr),
|
|
34046
|
+
token: pairing.token,
|
|
34047
|
+
expiresInMs: Math.max(0, pairing.expiresAt - Date.now())
|
|
33562
34048
|
});
|
|
33563
34049
|
} catch (error62) {
|
|
33564
34050
|
json2(response, 500, { error: String(error62?.message ?? error62).slice(0, 200) });
|
|
33565
34051
|
}
|
|
33566
34052
|
};
|
|
33567
34053
|
}
|
|
33568
|
-
function
|
|
33569
|
-
return
|
|
33570
|
-
|
|
33571
|
-
|
|
33572
|
-
|
|
33573
|
-
|
|
33574
|
-
|
|
33575
|
-
|
|
33576
|
-
|
|
33577
|
-
|
|
33578
|
-
let timer;
|
|
33579
|
-
let unregister;
|
|
33580
|
-
let started = false;
|
|
33581
|
-
let stopped = false;
|
|
33582
|
-
let written = null;
|
|
33583
|
-
const targetFor = (sessionId) => {
|
|
33584
|
-
let workspace;
|
|
34054
|
+
function pairImageHandler(deps) {
|
|
34055
|
+
return async (request, response) => {
|
|
34056
|
+
if (request.method !== "GET" && request.method !== "HEAD") {
|
|
34057
|
+
json2(response, 405, { error: "method not allowed" });
|
|
34058
|
+
return;
|
|
34059
|
+
}
|
|
34060
|
+
if (!hostIsLoopback(request.headers.host) || !originIsLoopbackOrAbsent(request.headers.origin)) {
|
|
34061
|
+
json2(response, 403, { error: "request-not-trusted" });
|
|
34062
|
+
return;
|
|
34063
|
+
}
|
|
33585
34064
|
try {
|
|
33586
|
-
|
|
34065
|
+
const pairing = deps.current();
|
|
34066
|
+
if (!pairing) {
|
|
34067
|
+
response.writeHead(204);
|
|
34068
|
+
response.end();
|
|
34069
|
+
return;
|
|
34070
|
+
}
|
|
34071
|
+
const png = await renderPairingPng(pairing.qr);
|
|
34072
|
+
response.writeHead(200, {
|
|
34073
|
+
"Content-Type": "image/png",
|
|
34074
|
+
"Content-Length": png.length,
|
|
34075
|
+
// 一张码会被就地换掉(半程刷新、被手机用掉),所以绝不许缓存。
|
|
34076
|
+
// ETag 给的是这一版的哈希,pill 拿它判断要不要重画,省掉撕票式的刷新。
|
|
34077
|
+
"Cache-Control": "no-store",
|
|
34078
|
+
ETag: `"${pairingEpoch(pairing.qr)}"`
|
|
34079
|
+
});
|
|
34080
|
+
response.end(request.method === "HEAD" ? void 0 : png);
|
|
33587
34081
|
} catch (error62) {
|
|
33588
|
-
|
|
33589
|
-
workspace = void 0;
|
|
34082
|
+
json2(response, 500, { error: String(error62?.message ?? error62).slice(0, 200) });
|
|
33590
34083
|
}
|
|
33591
|
-
const file2 = workspace ? path4.join(workspace, ".dsh", "sidebar-qr.png") : config2.imageFile;
|
|
33592
|
-
return {
|
|
33593
|
-
file: file2,
|
|
33594
|
-
persist: () => {
|
|
33595
|
-
const artifact = presenter.current();
|
|
33596
|
-
if (!artifact) return false;
|
|
33597
|
-
if (written && written.epoch === artifact.epoch && written.file === file2) return true;
|
|
33598
|
-
if (!writePrivatePng(file2, artifact.buffer)) {
|
|
33599
|
-
log("\u4E8C\u7EF4\u7801\u843D\u76D8\u5931\u8D25", { file: file2 });
|
|
33600
|
-
return false;
|
|
33601
|
-
}
|
|
33602
|
-
written = { epoch: artifact.epoch, file: file2 };
|
|
33603
|
-
return true;
|
|
33604
|
-
}
|
|
33605
|
-
};
|
|
33606
34084
|
};
|
|
33607
|
-
|
|
33608
|
-
|
|
34085
|
+
}
|
|
34086
|
+
function pillStatusHandler(deps) {
|
|
34087
|
+
return (request, response) => {
|
|
34088
|
+
if (request.method !== "GET" && request.method !== "HEAD") {
|
|
34089
|
+
json2(response, 405, { error: "method not allowed" });
|
|
34090
|
+
return;
|
|
34091
|
+
}
|
|
34092
|
+
if (!hostIsLoopback(request.headers.host) || !originIsLoopbackOrAbsent(request.headers.origin)) {
|
|
34093
|
+
json2(response, 403, { error: "request-not-trusted" });
|
|
34094
|
+
return;
|
|
34095
|
+
}
|
|
33609
34096
|
try {
|
|
33610
|
-
const
|
|
33611
|
-
|
|
34097
|
+
const status = deps.status();
|
|
34098
|
+
json2(response, 200, {
|
|
34099
|
+
relay: status.relay,
|
|
34100
|
+
paired: status.paired,
|
|
34101
|
+
serverUrl: status.serverUrl,
|
|
34102
|
+
version: status.version,
|
|
34103
|
+
waiting: status.waiting,
|
|
34104
|
+
waitingOldestSec: status.waitingOldestSec
|
|
34105
|
+
});
|
|
33612
34106
|
} catch (error62) {
|
|
33613
|
-
|
|
34107
|
+
json2(response, 500, { error: String(error62?.message ?? error62).slice(0, 200) });
|
|
34108
|
+
}
|
|
34109
|
+
};
|
|
34110
|
+
}
|
|
34111
|
+
function unpairHandler(deps) {
|
|
34112
|
+
return (request, response) => {
|
|
34113
|
+
if (request.method !== "POST") {
|
|
34114
|
+
json2(response, 405, { error: "method not allowed" });
|
|
34115
|
+
return;
|
|
34116
|
+
}
|
|
34117
|
+
const guard = rejectedBy(request);
|
|
34118
|
+
if (guard) {
|
|
34119
|
+
deps.log("\u89E3\u914D\u8BF7\u6C42\u88AB\u5B88\u536B\u62D2", { guard });
|
|
34120
|
+
json2(response, 403, { error: "request-not-trusted", guard });
|
|
33614
34121
|
return;
|
|
33615
34122
|
}
|
|
33616
34123
|
try {
|
|
33617
|
-
|
|
34124
|
+
const unpaired = deps.unpair();
|
|
34125
|
+
deps.log("\u7ECF\u72B6\u6001\u680F\u9000\u51FA\u914D\u5BF9", { unpaired });
|
|
34126
|
+
json2(response, 200, { state: "ok", unpaired });
|
|
33618
34127
|
} catch (error62) {
|
|
33619
|
-
|
|
34128
|
+
json2(response, 500, { error: String(error62?.message ?? error62).slice(0, 200) });
|
|
33620
34129
|
}
|
|
33621
34130
|
};
|
|
34131
|
+
}
|
|
34132
|
+
function registerPillRoutes(webServer, deps) {
|
|
34133
|
+
const off = [];
|
|
34134
|
+
const definitions = [
|
|
34135
|
+
{ kind: "exact", path: PAIR_NEW_ROUTE, handler: pairNewHandler(deps) },
|
|
34136
|
+
{ kind: "exact", path: PAIR_IMAGE_ROUTE, handler: pairImageHandler(deps) },
|
|
34137
|
+
{ kind: "exact", path: PAIR_STATUS_ROUTE, handler: pillStatusHandler(deps) },
|
|
34138
|
+
{ kind: "exact", path: PAIR_UNPAIR_ROUTE, handler: unpairHandler(deps) }
|
|
34139
|
+
];
|
|
34140
|
+
try {
|
|
34141
|
+
for (const definition of definitions) off.push(webServer.register(definition));
|
|
34142
|
+
} catch (error62) {
|
|
34143
|
+
for (const unregister of off.splice(0)) {
|
|
34144
|
+
try {
|
|
34145
|
+
unregister();
|
|
34146
|
+
} catch {
|
|
34147
|
+
}
|
|
34148
|
+
}
|
|
34149
|
+
throw error62;
|
|
34150
|
+
}
|
|
34151
|
+
return () => {
|
|
34152
|
+
for (const unregister of off) {
|
|
34153
|
+
try {
|
|
34154
|
+
unregister();
|
|
34155
|
+
} catch {
|
|
34156
|
+
}
|
|
34157
|
+
}
|
|
34158
|
+
};
|
|
34159
|
+
}
|
|
34160
|
+
|
|
34161
|
+
// src/pill/start.ts
|
|
34162
|
+
function startPill(ctx, deps) {
|
|
34163
|
+
const { log } = deps;
|
|
34164
|
+
const probe = {};
|
|
34165
|
+
let web;
|
|
34166
|
+
let unregister;
|
|
34167
|
+
let started = false;
|
|
34168
|
+
let stopped = false;
|
|
33622
34169
|
const tryStart = () => {
|
|
33623
34170
|
if (started || stopped || !web) return;
|
|
33624
34171
|
started = true;
|
|
33625
34172
|
try {
|
|
33626
|
-
unregister =
|
|
33627
|
-
|
|
33628
|
-
|
|
34173
|
+
unregister = registerPillRoutes(web, {
|
|
34174
|
+
ensureFresh: () => deps.ensureFresh(),
|
|
34175
|
+
current: () => deps.current(),
|
|
34176
|
+
status: () => deps.status(),
|
|
34177
|
+
unpair: () => deps.unpair(),
|
|
34178
|
+
log
|
|
33629
34179
|
});
|
|
33630
|
-
probe.
|
|
34180
|
+
probe.routes = "registered";
|
|
33631
34181
|
} catch (error62) {
|
|
33632
|
-
probe.
|
|
33633
|
-
log("route registration failed", { message: String(error62?.message ?? error62).slice(0, 200) });
|
|
34182
|
+
probe.routes = `register threw: ${String(error62?.message ?? error62).slice(0, 80)}`;
|
|
33634
34183
|
started = false;
|
|
34184
|
+
log("pill \u8DEF\u7531\u6CE8\u518C\u5931\u8D25\uFF08\u914D\u5BF9\u94FE\u8DEF\u4E0D\u53D7\u5F71\u54CD\uFF09", {
|
|
34185
|
+
message: String(error62?.message ?? error62).slice(0, 200)
|
|
34186
|
+
});
|
|
33635
34187
|
return;
|
|
33636
34188
|
}
|
|
33637
|
-
|
|
33638
|
-
timer.unref?.();
|
|
33639
|
-
void tick();
|
|
33640
|
-
log("sidebar started", { fallbackImageFile: config2.imageFile, refreshMs: config2.refreshMs });
|
|
34189
|
+
log("pill routes started");
|
|
33641
34190
|
};
|
|
33642
|
-
const
|
|
34191
|
+
const acceptWebServer = (value, via) => {
|
|
34192
|
+
if (!value || typeof value !== "object") return false;
|
|
34193
|
+
if (typeof value.register !== "function") {
|
|
34194
|
+
probe.webServer = "object without register()";
|
|
34195
|
+
return false;
|
|
34196
|
+
}
|
|
34197
|
+
web = value;
|
|
34198
|
+
probe.webServer = via;
|
|
34199
|
+
tryStart();
|
|
34200
|
+
return true;
|
|
34201
|
+
};
|
|
34202
|
+
const pickFrom = (source) => {
|
|
33643
34203
|
if (!source || typeof source !== "object") return void 0;
|
|
33644
34204
|
const asContext = source;
|
|
33645
34205
|
if (typeof asContext.get === "function") {
|
|
33646
34206
|
try {
|
|
33647
|
-
const got = asContext.get(
|
|
34207
|
+
const got = asContext.get("webServer", true);
|
|
33648
34208
|
if (got) return got;
|
|
33649
34209
|
} catch {
|
|
33650
34210
|
}
|
|
33651
34211
|
}
|
|
33652
34212
|
try {
|
|
33653
|
-
return source[
|
|
34213
|
+
return source["webServer"];
|
|
33654
34214
|
} catch {
|
|
33655
34215
|
return void 0;
|
|
33656
34216
|
}
|
|
33657
34217
|
};
|
|
33658
|
-
|
|
33659
|
-
|
|
33660
|
-
if (typeof value.register !== "function") {
|
|
33661
|
-
probe.webServer = "object without register()";
|
|
33662
|
-
return false;
|
|
33663
|
-
}
|
|
33664
|
-
web = value;
|
|
33665
|
-
probe.webServer = via;
|
|
33666
|
-
tryStart();
|
|
33667
|
-
return true;
|
|
33668
|
-
};
|
|
33669
|
-
if (!config2.enabled) {
|
|
33670
|
-
probe.sidebar = "disabled";
|
|
34218
|
+
if (!deps.enabled) {
|
|
34219
|
+
probe.pill = "disabled";
|
|
33671
34220
|
} else {
|
|
33672
34221
|
try {
|
|
33673
34222
|
const found = ctx.get?.("webServer", true);
|
|
@@ -33679,12 +34228,9 @@ function startSidebarQr(ctx, deps) {
|
|
|
33679
34228
|
try {
|
|
33680
34229
|
ctx.inject?.(["webServer"], ((...args) => {
|
|
33681
34230
|
try {
|
|
33682
|
-
let value;
|
|
33683
34231
|
for (const candidate of [args[0], ...args, ctx]) {
|
|
33684
|
-
|
|
33685
|
-
if (value) break;
|
|
34232
|
+
if (acceptWebServer(pickFrom(candidate), "via inject")) return;
|
|
33686
34233
|
}
|
|
33687
|
-
if (acceptWebServer(value, "via inject")) return;
|
|
33688
34234
|
probe.webServer = `${probe.webServer ?? "none"} (inject fired, empty)`;
|
|
33689
34235
|
} catch (error62) {
|
|
33690
34236
|
probe["injectThrew:webServer"] = String(error62?.message ?? error62).slice(0, 160);
|
|
@@ -33697,20 +34243,26 @@ function startSidebarQr(ctx, deps) {
|
|
|
33697
34243
|
const stop = () => {
|
|
33698
34244
|
if (stopped) return;
|
|
33699
34245
|
stopped = true;
|
|
33700
|
-
|
|
33701
|
-
|
|
33702
|
-
timer = void 0;
|
|
33703
|
-
}
|
|
34246
|
+
const off = unregister;
|
|
34247
|
+
unregister = void 0;
|
|
33704
34248
|
try {
|
|
33705
|
-
|
|
34249
|
+
off?.();
|
|
33706
34250
|
} catch {
|
|
33707
34251
|
}
|
|
33708
|
-
|
|
33709
|
-
|
|
34252
|
+
if (started) log("pill routes stopped");
|
|
34253
|
+
};
|
|
34254
|
+
return {
|
|
34255
|
+
stop,
|
|
34256
|
+
probe,
|
|
34257
|
+
get available() {
|
|
34258
|
+
return started && probe.routes === "registered";
|
|
34259
|
+
}
|
|
33710
34260
|
};
|
|
33711
|
-
return { stop, probe };
|
|
33712
34261
|
}
|
|
33713
34262
|
|
|
34263
|
+
// src/version.ts
|
|
34264
|
+
var PLUGIN_VERSION = true ? "2.0.0" : "dev";
|
|
34265
|
+
|
|
33714
34266
|
// src/index.ts
|
|
33715
34267
|
var SERVICE_NAMES = [
|
|
33716
34268
|
"sessions",
|
|
@@ -33740,11 +34292,7 @@ function apply(ctx, injected = {}) {
|
|
|
33740
34292
|
get state() {
|
|
33741
34293
|
return handle?.state() ?? { relay: "offline", carrier: "none", reason: "not-started" };
|
|
33742
34294
|
},
|
|
33743
|
-
createPairing: () => handle?.createPairing() ?? null
|
|
33744
|
-
// presentation 那一半要按会话解析落点(右栏那张图放哪),而"会话 → 工作区"这点
|
|
33745
|
-
// 平台知识只在本插件里有。**在这里开一个口,而不是让那一半自己去探内核服务**:
|
|
33746
|
-
// 两份探测迟早分叉,而分叉的表现是"卡片说 A 地、右栏读 B 地",很难查。
|
|
33747
|
-
sessionWorkspace: (sessionId) => handle?.sessionWorkspace(sessionId)
|
|
34295
|
+
createPairing: () => handle?.createPairing() ?? null
|
|
33748
34296
|
});
|
|
33749
34297
|
} catch {
|
|
33750
34298
|
}
|
|
@@ -33769,7 +34317,7 @@ function applyInner(ctx, injected) {
|
|
|
33769
34317
|
const clock = systemClock;
|
|
33770
34318
|
const oneShot = createOneShotTimers(clock);
|
|
33771
34319
|
const status = new StatusFile(config2.statusFile, clock);
|
|
33772
|
-
const
|
|
34320
|
+
const hostId = resolveHostId(config2.statusFile ? path5.dirname(config2.statusFile) : "", config2.hostId);
|
|
33773
34321
|
const log = (message, fields = {}) => {
|
|
33774
34322
|
try {
|
|
33775
34323
|
process.stdout.write(
|
|
@@ -33779,6 +34327,10 @@ function applyInner(ctx, injected) {
|
|
|
33779
34327
|
} catch {
|
|
33780
34328
|
}
|
|
33781
34329
|
};
|
|
34330
|
+
const pairStoreFile = resolvePairStoreFile(config2, hostId);
|
|
34331
|
+
const pairStore = new PairStore({ file: pairStoreFile, hostId, log });
|
|
34332
|
+
const restoredAtBoot = pairStoreFile ? pairStore.load() : [];
|
|
34333
|
+
const problems = validateConfig(config2);
|
|
33782
34334
|
for (const problem of problems) {
|
|
33783
34335
|
log(`config ${problem.level}`, { field: problem.field, message: problem.message });
|
|
33784
34336
|
}
|
|
@@ -33807,7 +34359,7 @@ function applyInner(ctx, injected) {
|
|
|
33807
34359
|
let runtime;
|
|
33808
34360
|
let relay;
|
|
33809
34361
|
let startedCarrier = "";
|
|
33810
|
-
let
|
|
34362
|
+
let pillRoutes;
|
|
33811
34363
|
let lastRelayState = "connecting";
|
|
33812
34364
|
let lastRelayProblem;
|
|
33813
34365
|
let lastRelayGeneration = 0;
|
|
@@ -33818,8 +34370,8 @@ function applyInner(ctx, injected) {
|
|
|
33818
34370
|
broadcast(payload) {
|
|
33819
34371
|
return relay ? relay.broadcast(payload) : 0;
|
|
33820
34372
|
},
|
|
33821
|
-
|
|
33822
|
-
return relay?.
|
|
34373
|
+
hasClient(conversationId) {
|
|
34374
|
+
return relay?.hasClient(conversationId) ?? false;
|
|
33823
34375
|
},
|
|
33824
34376
|
conversationIds() {
|
|
33825
34377
|
return relay?.conversationIds() ?? [];
|
|
@@ -33834,7 +34386,7 @@ function applyInner(ctx, injected) {
|
|
|
33834
34386
|
startedCarrier = port.carrier;
|
|
33835
34387
|
relay = new RelayClient({
|
|
33836
34388
|
url: config2.serverUrl,
|
|
33837
|
-
hostId
|
|
34389
|
+
hostId,
|
|
33838
34390
|
label: config2.hostLabel,
|
|
33839
34391
|
token: config2.hostToken,
|
|
33840
34392
|
clock,
|
|
@@ -33845,6 +34397,9 @@ function applyInner(ctx, injected) {
|
|
|
33845
34397
|
pairingWindow.applyServerTtl(token, ttlMs);
|
|
33846
34398
|
},
|
|
33847
34399
|
onPeerJoined: (conversationId, pairingToken2) => {
|
|
34400
|
+
for (const id of relay?.conversationIds() ?? []) {
|
|
34401
|
+
if (id !== conversationId) relay?.voidConversation(id);
|
|
34402
|
+
}
|
|
33848
34403
|
runtime?.start();
|
|
33849
34404
|
void runtime?.pushSessions("peer-joined");
|
|
33850
34405
|
if (pairingToken2) {
|
|
@@ -33865,7 +34420,23 @@ function applyInner(ctx, injected) {
|
|
|
33865
34420
|
},
|
|
33866
34421
|
// 配对通道长存(D3)之后必须自己剪枝:中继那边的空闲 TTL 是 7 天,
|
|
33867
34422
|
// 不剪就是无界的 PSK 簿 + 每次广播对废弃通道逐个密封。
|
|
33868
|
-
|
|
34423
|
+
// 两条 TTL 分开:新配对的按 24 小时剪,从盘上恢复的按 `restoredIdleTtlSec`
|
|
34424
|
+
// (默认 7 天,与中继对齐)—— 理由见 config.ts 与 keys.ts 的 `Conversation.restored`。
|
|
34425
|
+
prunePolicy: {
|
|
34426
|
+
idleTtlMs: config2.conversationIdleTtlSec * 1e3,
|
|
34427
|
+
restoredIdleTtlMs: config2.restoredIdleTtlSec * 1e3,
|
|
34428
|
+
maxConversations: MAX_CONVERSATIONS
|
|
34429
|
+
},
|
|
34430
|
+
// 结构性变化(新配对 / 作废)立刻落盘。只改了 seqHost 与 lastActivityAt 的场合
|
|
34431
|
+
// 由 status 的 3 秒 tick 批量落(见下面 `pairStore.markDirty()` 那一段)——
|
|
34432
|
+
// 挂在这里等于每广播一次写一次盘。
|
|
34433
|
+
onStructuralChange: () => {
|
|
34434
|
+
if (!pairStoreFile || !relay) return;
|
|
34435
|
+
pairStore.save(relay.conversations.snapshot(), clock.now());
|
|
34436
|
+
},
|
|
34437
|
+
onBookActivity: () => {
|
|
34438
|
+
pairStore.markDirty();
|
|
34439
|
+
}
|
|
33869
34440
|
});
|
|
33870
34441
|
runtime = new HostRuntime(port, transport, sleep, clock, {
|
|
33871
34442
|
listingRefreshMs: config2.listingRefreshSec * 1e3,
|
|
@@ -33873,11 +34444,20 @@ function applyInner(ctx, injected) {
|
|
|
33873
34444
|
approvalTimeoutMs: config2.approvalTimeoutSec * 1e3,
|
|
33874
34445
|
log: (message, fields) => log(message, fields)
|
|
33875
34446
|
});
|
|
34447
|
+
const restoredIds = relay.restoreConversations(restoredAtBoot);
|
|
34448
|
+
if (restoredIds.length > 0) {
|
|
34449
|
+
runtime.start();
|
|
34450
|
+
log("runtime started from restored conversations", { conversations: restoredIds.length });
|
|
34451
|
+
}
|
|
33876
34452
|
relay.connect();
|
|
33877
34453
|
pairingWindow.tick();
|
|
33878
34454
|
status.start(() => {
|
|
33879
34455
|
pairingWindow.tick();
|
|
33880
34456
|
relay?.pruneConversations();
|
|
34457
|
+
if (pairStoreFile && pairStore.hasPendingChanges && relay) {
|
|
34458
|
+
pairStore.save(relay.conversations.snapshot(), clock.now());
|
|
34459
|
+
}
|
|
34460
|
+
slots.prune();
|
|
33881
34461
|
return state();
|
|
33882
34462
|
});
|
|
33883
34463
|
};
|
|
@@ -33902,23 +34482,60 @@ function applyInner(ctx, injected) {
|
|
|
33902
34482
|
carrier: startedCarrier || (config2.mockBridge ? "mock" : "probing"),
|
|
33903
34483
|
serverUrl: config2.serverUrl,
|
|
33904
34484
|
hostLabel: config2.hostLabel,
|
|
34485
|
+
// 主机身份。中继日志里认的就是它,所以"手机连不上/丢帧涨"第一件事是看这个值
|
|
34486
|
+
// 在两次重启之间有没有变(变了就是身份没稳住,见 shell/host-id.ts)。
|
|
34487
|
+
hostId,
|
|
33905
34488
|
relay: relay ? relayState() : "idle",
|
|
33906
34489
|
relayProblem: relayProblem(),
|
|
33907
34490
|
conversations: relay?.conversationCount ?? 0,
|
|
34491
|
+
/**
|
|
34492
|
+
* 当前连着的手机台数(去重的 clientId)。
|
|
34493
|
+
*
|
|
34494
|
+
* 为什么必须与 `conversations` 分开报:「解除配对 → 重新配对」会开一条**新会话**,
|
|
34495
|
+
* 而手机还是那一台。所以拿会话数回答"几台手机连着",用户解完配对看到数字反而变大 ——
|
|
34496
|
+
* 那个现象与"我的解配没生效"完全一致,最容易让人把解配按钮当坏的。
|
|
34497
|
+
* `pendingPairs` 是另一个东西:那是**还挂着、还没被用掉的配对码**。
|
|
34498
|
+
*/
|
|
34499
|
+
pairedClients: relay?.clientCount ?? 0,
|
|
33908
34500
|
pendingPairs: slots.size,
|
|
34501
|
+
/**
|
|
34502
|
+
* 密钥簿落盘的排障面。
|
|
34503
|
+
*
|
|
34504
|
+
* `restored` 是"本次启动从盘上恢复了几条会话"——它与 `conversations` 的差值
|
|
34505
|
+
* 就是"落盘没生效"的判据:`conversations > 0 && restored === 0 && pairStore.enabled`
|
|
34506
|
+
* 意味着这些会话是本次运行新配出来的(正常),反过来
|
|
34507
|
+
* `restored > 0` 而 `conversations === 0` 才是异常。
|
|
34508
|
+
*
|
|
34509
|
+
* **`lastSavedAt` 与路径都不含 PSK**:这份快照会被外部脚本读,
|
|
34510
|
+
* 文件路径本身也不是凭据(里面的内容才是)。
|
|
34511
|
+
*/
|
|
34512
|
+
pairStore: {
|
|
34513
|
+
enabled: pairStoreFile !== "",
|
|
34514
|
+
file: pairStoreFile || void 0,
|
|
34515
|
+
restored: restoredAtBoot.length,
|
|
34516
|
+
lastSavedAt: pairStoreFile && pairStore.lastSavedAt > 0 ? new Date(pairStore.lastSavedAt).toISOString() : void 0
|
|
34517
|
+
},
|
|
33909
34518
|
pairing: describeActivePairing(),
|
|
33910
34519
|
keepAwake: snapshot,
|
|
33911
34520
|
kernel: kernel?.describe() ?? null,
|
|
33912
34521
|
// 出站计数:手机上"没收到 X"的第一现场对比点(插件没发 vs 发了但路上丢了)。
|
|
33913
34522
|
outbound: runtime?.stats ?? null,
|
|
33914
34523
|
probe,
|
|
33915
|
-
//
|
|
33916
|
-
|
|
34524
|
+
// 配对入口(那颗 pill 的四条路由)为什么没起,是"配不了对"唯一的排查入口:
|
|
34525
|
+
// 这里直接把软探测的结果带出来。
|
|
34526
|
+
pill: pillRoutes?.probe ?? { webServer: "not-started" },
|
|
33917
34527
|
// token 永远不出现真值;这条有测试锁住。
|
|
33918
34528
|
hostToken: void 0,
|
|
33919
|
-
hostTokenShape: config2.hostToken ?
|
|
34529
|
+
hostTokenShape: config2.hostToken ? redact(config2.hostToken) : void 0,
|
|
33920
34530
|
mockBridgeForced: config2.mockBridge,
|
|
33921
|
-
problems:
|
|
34531
|
+
problems: [
|
|
34532
|
+
...problems.map((problem) => `${problem.level}:${problem.field}`),
|
|
34533
|
+
// **每次快照现判,不在 apply 里 push 一次**。`webServer` 可能是 `ctx.inject` 的回调
|
|
34534
|
+
// 晚到才拿到的,apply 返回那一刻 `available` 还是 false——留在 apply 里就会写出
|
|
34535
|
+
// `routes:"registered"` 与 `problems:["warn:pill"]` 并存的假警报
|
|
34536
|
+
// (2026-10-03 真机重启后实测抓到)。为什么没挂上从同一个快照的 `pill` 探针读。
|
|
34537
|
+
...pillRoutes?.available ? [] : ["warn:pill"]
|
|
34538
|
+
],
|
|
33922
34539
|
...config2.pairOnStartSec > 0 ? { pairOnStartWarn: "status.json \u91CC\u5E26\u7740\u4ECD\u7136\u6709\u6548\u7684\u914D\u5BF9\u7801\u4E0E PSK" } : {}
|
|
33923
34540
|
};
|
|
33924
34541
|
};
|
|
@@ -33951,6 +34568,33 @@ function applyInner(ctx, injected) {
|
|
|
33951
34568
|
return created;
|
|
33952
34569
|
};
|
|
33953
34570
|
const createPairing = () => wrappedPublish();
|
|
34571
|
+
function currentPairing() {
|
|
34572
|
+
const shown = active.pairing;
|
|
34573
|
+
if (!shown) return null;
|
|
34574
|
+
if (!slots.resolveFor(shown.token)) return null;
|
|
34575
|
+
return { qr: shown.qr, token: shown.token, expiresAt: shown.expiresAt };
|
|
34576
|
+
}
|
|
34577
|
+
function ensureFreshPairing() {
|
|
34578
|
+
const fresh = currentPairing() ?? createPairing();
|
|
34579
|
+
if (!fresh) return null;
|
|
34580
|
+
return { qr: fresh.qr, token: fresh.token, expiresAt: fresh.expiresAt };
|
|
34581
|
+
}
|
|
34582
|
+
function pillStatus() {
|
|
34583
|
+
const waiting = runtime?.waiting ?? { count: 0, oldestSec: 0 };
|
|
34584
|
+
return {
|
|
34585
|
+
relay: relay ? relayState() : "idle",
|
|
34586
|
+
paired: relay?.clientCount ?? 0,
|
|
34587
|
+
serverUrl: config2.serverUrl,
|
|
34588
|
+
version: PLUGIN_VERSION,
|
|
34589
|
+
waiting: waiting.count,
|
|
34590
|
+
waitingOldestSec: waiting.oldestSec
|
|
34591
|
+
};
|
|
34592
|
+
}
|
|
34593
|
+
function unpairAll() {
|
|
34594
|
+
const ids = relay?.conversationIds() ?? [];
|
|
34595
|
+
for (const id of ids) relay?.voidConversation(id);
|
|
34596
|
+
return ids.length;
|
|
34597
|
+
}
|
|
33954
34598
|
const pairingWindow = new PairingWindow(
|
|
33955
34599
|
{
|
|
33956
34600
|
clock,
|
|
@@ -33967,7 +34611,6 @@ function applyInner(ctx, injected) {
|
|
|
33967
34611
|
);
|
|
33968
34612
|
const collected = {};
|
|
33969
34613
|
const hasServices = () => typeof collected.sessions === "object" && collected.sessions !== null;
|
|
33970
|
-
const apiProxyOf = () => collected.apiProxy;
|
|
33971
34614
|
const probe = {};
|
|
33972
34615
|
const injectFired = [];
|
|
33973
34616
|
let hasGet = true;
|
|
@@ -33985,9 +34628,6 @@ function applyInner(ctx, injected) {
|
|
|
33985
34628
|
start(createServicesKernel(collected, { log, takeOverQuestions: config2.takeOverQuestions, clock }));
|
|
33986
34629
|
return;
|
|
33987
34630
|
}
|
|
33988
|
-
if (typeof ctx.get?.("apiProxy", true) === "object") {
|
|
33989
|
-
log("apiProxy present but unsupported by this build", { hint: "\u53EA\u505A\u8BCA\u65AD\u8BB0\u5F55\uFF0C\u4E0D\u63A5\u8FD9\u6761\u8F7D\u4F53" });
|
|
33990
|
-
}
|
|
33991
34631
|
};
|
|
33992
34632
|
const requiredMember = { sessions: "list", sessionQuery: "listSessions", agents: "get" };
|
|
33993
34633
|
const keysOf = (value) => {
|
|
@@ -34199,23 +34839,6 @@ function applyInner(ctx, injected) {
|
|
|
34199
34839
|
} catch {
|
|
34200
34840
|
}
|
|
34201
34841
|
}
|
|
34202
|
-
try {
|
|
34203
|
-
const proxy = ctx.get?.("apiProxy", true);
|
|
34204
|
-
if (proxy) collected.apiProxy = proxy;
|
|
34205
|
-
} catch {
|
|
34206
|
-
}
|
|
34207
|
-
try {
|
|
34208
|
-
ctx.inject?.(["apiProxy"], ((scoped) => {
|
|
34209
|
-
const value = scoped?.get?.("apiProxy", true);
|
|
34210
|
-
if (!value) return;
|
|
34211
|
-
collected.apiProxy = value;
|
|
34212
|
-
if (!decided) {
|
|
34213
|
-
oneShot.schedule(tryStart, config2.carrierGraceMs);
|
|
34214
|
-
}
|
|
34215
|
-
tryStart();
|
|
34216
|
-
}));
|
|
34217
|
-
} catch {
|
|
34218
|
-
}
|
|
34219
34842
|
tryStart();
|
|
34220
34843
|
if (!runtime && config2.mockBridge) {
|
|
34221
34844
|
decided = true;
|
|
@@ -34224,94 +34847,21 @@ function applyInner(ctx, injected) {
|
|
|
34224
34847
|
}
|
|
34225
34848
|
if (!runtime) {
|
|
34226
34849
|
status.write({ ...state(), carrier: "none", reason: "no-carrier", probe });
|
|
34227
|
-
log("no kernel carrier available", { hint: "\u9700\u8981 sessions
|
|
34228
|
-
}
|
|
34229
|
-
const workspaceOfInvocation = (invocation) => {
|
|
34230
|
-
const agent = invocation?.agent;
|
|
34231
|
-
const sessionId = typeof agent?.session?.id === "string" ? agent.session.id : "";
|
|
34232
|
-
if (sessionId === "") return void 0;
|
|
34233
|
-
try {
|
|
34234
|
-
return kernel?.sessionWorkspace?.(sessionId);
|
|
34235
|
-
} catch {
|
|
34236
|
-
return void 0;
|
|
34237
|
-
}
|
|
34238
|
-
};
|
|
34239
|
-
const commandDefinition = {
|
|
34240
|
-
name: "drc",
|
|
34241
|
-
description: "DSH \u8FDC\u7A0B\u63A7\u5236\uFF1A\u53D1\u5E03\u914D\u5BF9\u4E8C\u7EF4\u7801\u3001\u67E5\u770B\u4E2D\u7EE7\u4E0E\u5185\u6838\u63A5\u5408\u72B6\u6001",
|
|
34242
|
-
input: { hint: "pair | status | unpair" },
|
|
34243
|
-
handler: async (invocation) => {
|
|
34244
|
-
const argument = String(invocation.rawInput ?? "").trim().toLowerCase();
|
|
34245
|
-
if (argument.startsWith("pair")) {
|
|
34246
|
-
const pairing = createPairing();
|
|
34247
|
-
if (!pairing) return { kind: "success", text: PAIR_UNAVAILABLE_TEXT };
|
|
34248
|
-
const workspaceDir = workspaceOfInvocation(invocation);
|
|
34249
|
-
const imageFile = config2.qrImage ? pairingImagePath(config2.statusFile, workspaceDir) : "";
|
|
34250
|
-
if (imageFile) writePairingImage(imageFile, pairing.qr, config2, log);
|
|
34251
|
-
const terminal = config2.qrImage ? void 0 : await renderTerminalQr(pairing.qr, { style: config2.qrStyle, ansi: config2.qrAnsi });
|
|
34252
|
-
return {
|
|
34253
|
-
kind: "success",
|
|
34254
|
-
text: pairingPairText({
|
|
34255
|
-
token: pairing.token,
|
|
34256
|
-
qr: pairing.qr,
|
|
34257
|
-
expiresAt: pairing.expiresAt,
|
|
34258
|
-
now: clock.now(),
|
|
34259
|
-
qrImage: config2.qrImage,
|
|
34260
|
-
imageFile,
|
|
34261
|
-
terminalQr: terminal
|
|
34262
|
-
})
|
|
34263
|
-
};
|
|
34264
|
-
}
|
|
34265
|
-
if (argument.startsWith("unpair")) {
|
|
34266
|
-
for (const id of relay?.conversationIds() ?? []) relay?.voidConversation(id);
|
|
34267
|
-
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' };
|
|
34268
|
-
}
|
|
34269
|
-
return { kind: "success", text: JSON.stringify(state(), null, 2) };
|
|
34270
|
-
}
|
|
34271
|
-
};
|
|
34272
|
-
let commandsTaken = false;
|
|
34273
|
-
const takeCommands = (source) => {
|
|
34274
|
-
if (commandsTaken || !source || typeof source !== "object") return;
|
|
34275
|
-
const holder = source;
|
|
34276
|
-
if (typeof holder.register !== "function") return;
|
|
34277
|
-
holder.register(commandDefinition);
|
|
34278
|
-
commandsTaken = true;
|
|
34279
|
-
probe.commands = "registered";
|
|
34280
|
-
};
|
|
34281
|
-
try {
|
|
34282
|
-
takeCommands(ctx.commands);
|
|
34283
|
-
} catch {
|
|
34284
|
-
}
|
|
34285
|
-
try {
|
|
34286
|
-
takeCommands(ctx.get?.("commands", true));
|
|
34287
|
-
} catch {
|
|
34288
|
-
}
|
|
34289
|
-
try {
|
|
34290
|
-
const child = ctx.inject?.(["commands"], ((...cbArgs) => {
|
|
34291
|
-
for (const candidate of [cbArgs[0], ...cbArgs, ctx]) {
|
|
34292
|
-
takeCommands(pickFrom(candidate, "commands"));
|
|
34293
|
-
if (commandsTaken) break;
|
|
34294
|
-
}
|
|
34295
|
-
}));
|
|
34296
|
-
if (child !== void 0) injectChildren.push(["commands", child]);
|
|
34297
|
-
} catch {
|
|
34850
|
+
log("no kernel carrier available", { hint: "\u9700\u8981 sessions \u670D\u52A1\uFF0C\u6216\u628A mockBridge \u6253\u5F00\u505A\u5F00\u53D1" });
|
|
34298
34851
|
}
|
|
34299
34852
|
try {
|
|
34300
|
-
|
|
34301
|
-
|
|
34302
|
-
|
|
34303
|
-
|
|
34304
|
-
|
|
34305
|
-
|
|
34306
|
-
|
|
34307
|
-
return void 0;
|
|
34308
|
-
}
|
|
34309
|
-
},
|
|
34853
|
+
pillRoutes = startPill(ctx, {
|
|
34854
|
+
enabled: config2.pill.enabled,
|
|
34855
|
+
// 四条路由要的四件事:幂等发码、只读看当前码、读连接状态、退出配对。
|
|
34856
|
+
ensureFresh: () => ensureFreshPairing(),
|
|
34857
|
+
current: () => currentPairing(),
|
|
34858
|
+
status: () => pillStatus(),
|
|
34859
|
+
unpair: () => unpairAll(),
|
|
34310
34860
|
log
|
|
34311
34861
|
});
|
|
34312
34862
|
} catch (error62) {
|
|
34313
|
-
|
|
34314
|
-
log("
|
|
34863
|
+
pillRoutes = void 0;
|
|
34864
|
+
log("pill start threw\uFF08\u914D\u5BF9\u94FE\u8DEF\u4E0D\u53D7\u5F71\u54CD\uFF09", {
|
|
34315
34865
|
message: String(error62?.message ?? error62).slice(0, 200)
|
|
34316
34866
|
});
|
|
34317
34867
|
}
|
|
@@ -34319,37 +34869,19 @@ function applyInner(ctx, injected) {
|
|
|
34319
34869
|
config: config2,
|
|
34320
34870
|
state,
|
|
34321
34871
|
createPairing,
|
|
34322
|
-
sessionWorkspace(sessionId) {
|
|
34323
|
-
try {
|
|
34324
|
-
return kernel?.sessionWorkspace?.(sessionId);
|
|
34325
|
-
} catch {
|
|
34326
|
-
return void 0;
|
|
34327
|
-
}
|
|
34328
|
-
},
|
|
34329
34872
|
stop() {
|
|
34330
34873
|
oneShot.cancelAll();
|
|
34331
|
-
|
|
34874
|
+
pillRoutes?.stop();
|
|
34332
34875
|
status.stop();
|
|
34876
|
+
if (pairStoreFile && relay && pairStore.hasPendingChanges) {
|
|
34877
|
+
pairStore.save(relay.conversations.snapshot(), clock.now());
|
|
34878
|
+
}
|
|
34333
34879
|
runtime?.stop();
|
|
34334
34880
|
relay?.stop();
|
|
34335
34881
|
sleep.stop();
|
|
34336
34882
|
}
|
|
34337
34883
|
};
|
|
34338
34884
|
}
|
|
34339
|
-
function writePairingImage(file2, qrText, config2, log) {
|
|
34340
|
-
if (!config2.qrImage || !file2) return;
|
|
34341
|
-
qrPng(qrText).then((buffer) => {
|
|
34342
|
-
if (!writePrivateFile(file2, buffer)) {
|
|
34343
|
-
log("qr png write failed", { file: file2 });
|
|
34344
|
-
return;
|
|
34345
|
-
}
|
|
34346
|
-
if (!config2.qrOpen) return;
|
|
34347
|
-
const opener = process.platform === "darwin" ? "open" : process.platform === "win32" ? "cmd" : "xdg-open";
|
|
34348
|
-
const args = process.platform === "win32" ? ["/c", "start", "", file2] : [file2];
|
|
34349
|
-
execFile(opener, args, () => {
|
|
34350
|
-
});
|
|
34351
|
-
}).catch((error62) => log("qr png render failed", { message: String(error62?.message ?? error62) }));
|
|
34352
|
-
}
|
|
34353
34885
|
export {
|
|
34354
34886
|
DEFAULT_CONFIG,
|
|
34355
34887
|
apply,
|