dsh-remote-control 1.1.0 → 1.2.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.
@@ -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 path5 = dijkstra.find_path(graph.map, "start", "end");
7489
+ const path3 = dijkstra.find_path(graph.map, "start", "end");
7490
7490
  const optimizedSegs = [];
7491
- for (let i = 1; i < path5.length - 1; i++) {
7492
- optimizedSegs.push(graph.table[path5[i]].node);
7491
+ for (let i = 1; i < path3.length - 1; i++) {
7492
+ optimizedSegs.push(graph.table[path3[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(path5, qrData, options, cb) {
9969
+ exports.renderToFile = function renderToFile(path3, 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(path5);
9980
+ const stream = fs.createWriteStream(path3);
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(path5, qrData, options, cb) {
10042
+ exports.renderToFile = function renderToFile(path3, 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(path5, utf82, cb);
10049
+ fs.writeFile(path3, 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 path5 = "";
10170
+ let path3 = "";
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
- path5 += newRow ? svgCmd("M", col + margin, 0.5 + row + margin) : svgCmd("m", moveBy, 0);
10181
+ path3 += 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
- path5 += svgCmd("h", lineLength);
10186
+ path3 += svgCmd("h", lineLength);
10187
10187
  lineLength = 0;
10188
10188
  }
10189
10189
  } else {
10190
10190
  moveBy++;
10191
10191
  }
10192
10192
  }
10193
- return path5;
10193
+ return path3;
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 path5 = "<path " + getColorAttrib(opts.color.dark, "stroke") + ' d="' + qrToPath(data, size, opts.margin) + '"/>';
10201
+ const path3 = "<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 + path5 + "</svg>\n";
10204
+ const svgTag = '<svg xmlns="http://www.w3.org/2000/svg" ' + width + viewBox + ' shape-rendering="crispEdges">' + bg + path3 + "</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(path5, qrData, options, cb) {
10218
+ exports.renderToFile = function renderToFile(path3, 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(path5, xmlStr, cb);
10226
+ fs.writeFile(path3, 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 QRCode3 = require_qrcode();
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 = QRCode3.create(text, opts);
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 = QRCode3.create(text, opts);
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 = QRCode3.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 QRCode3 = require_qrcode();
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(path5) {
10385
- return path5.slice((path5.lastIndexOf(".") - 1 >>> 0) + 2).toLowerCase();
10384
+ function getTypeFromFilename(path3) {
10385
+ return path3.slice((path3.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 = QRCode3.create(text, params.opts);
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 = QRCode3.create(text, params.opts);
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 = QRCode3.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(path5, text, opts, cb) {
10450
- if (typeof path5 !== "string" || !(typeof text === "string" || typeof text === "object")) {
10449
+ exports.toFile = function toFile(path3, text, opts, cb) {
10450
+ if (typeof path3 !== "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(path5);
10457
+ const type = params.opts.type || getTypeFromFilename(path3);
10458
10458
  const renderer = getRendererFromType(type);
10459
- const renderToFile = renderer.renderToFile.bind(null, path5);
10459
+ const renderToFile = renderer.renderToFile.bind(null, path3);
10460
10460
  return render(renderToFile, text, params);
10461
10461
  };
10462
10462
  exports.toFileStream = function toFileStream(stream, text, opts) {
@@ -10655,7 +10655,6 @@ var init_mock_kernel = __esm({
10655
10655
  });
10656
10656
 
10657
10657
  // src/index.ts
10658
- import { execFile } from "node:child_process";
10659
10658
  import { randomBytes as randomBytes4 } from "node:crypto";
10660
10659
 
10661
10660
  // ../../node_modules/.pnpm/dsh-remote-wire@1.0.0/node_modules/dsh-remote-wire/dist/src/crypto/bytes.js
@@ -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, path5) {
11598
- if (!path5)
11596
+ function getElementAtPath(obj, path3) {
11597
+ if (!path3)
11599
11598
  return obj;
11600
- return path5.reduce((acc, key) => acc?.[key], obj);
11599
+ return path3.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(path5, issues) {
11939
+ function prefixIssues(path3, issues) {
11941
11940
  return issues.map((iss) => {
11942
11941
  var _a3;
11943
11942
  (_a3 = iss).path ?? (_a3.path = []);
11944
- iss.path.unshift(path5);
11943
+ iss.path.unshift(path3);
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, path5 = []) => {
12393
+ const processError = (error63, path3 = []) => {
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 }, [...path5, ...issue2.path]));
12396
+ issue2.errors.map((issues) => processError({ issues }, [...path3, ...issue2.path]));
12398
12397
  } else if (issue2.code === "invalid_key") {
12399
- processError({ issues: issue2.issues }, [...path5, ...issue2.path]);
12398
+ processError({ issues: issue2.issues }, [...path3, ...issue2.path]);
12400
12399
  } else if (issue2.code === "invalid_element") {
12401
- processError({ issues: issue2.issues }, [...path5, ...issue2.path]);
12400
+ processError({ issues: issue2.issues }, [...path3, ...issue2.path]);
12402
12401
  } else {
12403
- const fullpath = [...path5, ...issue2.path];
12402
+ const fullpath = [...path3, ...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, path5 = []) => {
12441
+ const processError = (error63, path3 = []) => {
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 }, [...path5, ...issue2.path]));
12445
+ issue2.errors.map((issues) => processError({ issues }, [...path3, ...issue2.path]));
12447
12446
  } else if (issue2.code === "invalid_key") {
12448
- processError({ issues: issue2.issues }, [...path5, ...issue2.path]);
12447
+ processError({ issues: issue2.issues }, [...path3, ...issue2.path]);
12449
12448
  } else if (issue2.code === "invalid_element") {
12450
- processError({ issues: issue2.issues }, [...path5, ...issue2.path]);
12449
+ processError({ issues: issue2.issues }, [...path3, ...issue2.path]);
12451
12450
  } else {
12452
- const fullpath = [...path5, ...issue2.path];
12451
+ const fullpath = [...path3, ...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 path5 = _path.map((seg) => typeof seg === "object" ? seg.key : seg);
12492
- for (const seg of path5) {
12490
+ const path3 = _path.map((seg) => typeof seg === "object" ? seg.key : seg);
12491
+ for (const seg of path3) {
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 path5 = ref.slice(1).split("/").filter(Boolean);
29595
- if (path5.length === 0) {
29593
+ const path3 = ref.slice(1).split("/").filter(Boolean);
29594
+ if (path3.length === 0) {
29596
29595
  return ctx.rootSchema;
29597
29596
  }
29598
29597
  const defsKey = ctx.version === "draft-2020-12" ? "$defs" : "definitions";
29599
- if (path5[0] === defsKey) {
29600
- const key = path5[1] === void 0 ? void 0 : decodeJSONPointerSegment(path5[1]);
29598
+ if (path3[0] === defsKey) {
29599
+ const key = path3[1] === void 0 ? void 0 : decodeJSONPointerSegment(path3[1]);
29601
29600
  if (!key || !ctx.defs[key]) {
29602
29601
  throw new Error(`Reference not found: ${ref}`);
29603
29602
  }
@@ -31135,7 +31134,7 @@ var HostRuntime = class {
31135
31134
  this.clock.clearTimeout(this.mergeTimer);
31136
31135
  this.mergeTimer = void 0;
31137
31136
  }
31138
- for (const [id, item] of [...this.pending]) this.settle(id, void 0);
31137
+ for (const [id] of [...this.pending]) this.settle(id, void 0);
31139
31138
  }
31140
31139
  /** 手机发来的命令。`conversationId` 是配对通道 id,与载荷里的 sessionId 不是一回事(F3)。 */
31141
31140
  async handleCommand(cmd, conversationId) {
@@ -32309,17 +32308,6 @@ function createServicesKernel(services, options) {
32309
32308
  const status = agent?.status;
32310
32309
  return typeof status === "string" ? status : "idle";
32311
32310
  }
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
32311
  function toSummary(record2, archived) {
32324
32312
  const cached2 = titleCache.get(record2.id);
32325
32313
  const running = agentStatus(record2.id) === "running";
@@ -32746,7 +32734,6 @@ function createServicesKernel(services, options) {
32746
32734
  subscribe,
32747
32735
  attachInteractionSink,
32748
32736
  ensureRunnable,
32749
- sessionWorkspace,
32750
32737
  // 与发指令/续跑走同一条读取路径:这里原来还有一份实现,而且是 `current()` 裸调用
32751
32738
  // (不带 receiver;真要用 this 的服务方法会当场抛),两份迟早分叉。
32752
32739
  modelSelection: currentSelection,
@@ -32761,9 +32748,6 @@ function createServicesKernel(services, options) {
32761
32748
  // 新建会话只依赖这一条。和 historyFace 同理:真机上"新建没反应"第一件要分清的事
32762
32749
  // 是宿主没有这个能力,还是我们没调对。
32763
32750
  createFace: typeof fn(services.sessionController?.commands, "create") === "function" ? "commands.create" : `absent (keys=${shapeOf(services.sessionController)})`.slice(0, 120),
32764
- // 二维码落点依赖这一条。它缺席时 PNG 会退回 ~/.dsh/,卡片上的路径会跟着变——
32765
- // 用户看到"图不在工作区"时,第一件事就是看这个字段是不是 absent。
32766
- workspaceFace: typeof fn(services.sessions, "get") === "function" ? "sessions.get" : `absent (keys=${shapeOf(services.sessions)})`.slice(0, 120),
32767
32751
  agents: typeof services.agents === "object",
32768
32752
  agentDefaultModel: typeof services.agentDefaultModel === "object",
32769
32753
  workspaceRegistry: typeof services.workspaceRegistry === "object",
@@ -33049,7 +33033,6 @@ function createOneShotTimers(clock) {
33049
33033
 
33050
33034
  // src/shell/status.ts
33051
33035
  import { mkdirSync, renameSync, writeFileSync } from "node:fs";
33052
- import { homedir } from "node:os";
33053
33036
  import path from "node:path";
33054
33037
  var StatusFile = class {
33055
33038
  constructor(file2, clock, refreshMs = 3e3) {
@@ -33061,7 +33044,6 @@ var StatusFile = class {
33061
33044
  clock;
33062
33045
  refreshMs;
33063
33046
  timer;
33064
- lastWritten = 0;
33065
33047
  write(snapshot) {
33066
33048
  if (!this.file) return;
33067
33049
  try {
@@ -33075,7 +33057,6 @@ var StatusFile = class {
33075
33057
  }
33076
33058
  );
33077
33059
  renameSync(tmp, this.file);
33078
- this.lastWritten = this.clock.now();
33079
33060
  } catch {
33080
33061
  }
33081
33062
  }
@@ -33104,47 +33085,10 @@ var StatusFile = class {
33104
33085
  this.timer = void 0;
33105
33086
  }
33106
33087
  }
33107
- /** 此刻是否在按节拍刷新(测试与 status 自身的 `refreshing` 字段读它)。 */
33108
- get refreshing() {
33109
- return this.timer !== void 0;
33110
- }
33111
- get path() {
33112
- return this.file;
33113
- }
33114
33088
  };
33115
- function pairingImagePath(statusFile, workspaceDir) {
33116
- if (!statusFile) return "";
33117
- if (workspaceDir) return path.join(workspaceDir, ".dsh", "pairing-qr.png");
33118
- return path.join(homedir(), ".dsh", "pairing-qr.png");
33119
- }
33120
- function writePrivateFile(file2, data) {
33121
- if (!file2) return false;
33122
- try {
33123
- mkdirSync(path.dirname(file2), { recursive: true });
33124
- writeFileSync(file2, data, { mode: 384 });
33125
- return true;
33126
- } catch {
33127
- return false;
33128
- }
33129
- }
33130
-
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
33089
 
33146
33090
  // src/shell/config.ts
33147
- import { homedir as homedir2 } from "node:os";
33091
+ import { homedir } from "node:os";
33148
33092
  import path2 from "node:path";
33149
33093
  var DEFAULT_CONFIG = {
33150
33094
  enabled: true,
@@ -33155,34 +33099,18 @@ var DEFAULT_CONFIG = {
33155
33099
  hostLabel: "dsh-host",
33156
33100
  keepAwake: { enabled: true, idleReleaseSec: 300, keepDisplay: false },
33157
33101
  mockBridge: false,
33158
- statusFile: path2.join(homedir2(), ".dsh", "dsh-remote-control", "status.json"),
33102
+ statusFile: path2.join(homedir(), ".dsh", "dsh-remote-control", "status.json"),
33159
33103
  unarchiveOnPrompt: true,
33160
33104
  pairOnStartSec: 0,
33161
33105
  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
33106
  carrierGraceMs: 5e3,
33178
33107
  takeOverQuestions: false,
33179
33108
  approvalTimeoutSec: 180,
33180
33109
  listingRefreshSec: 15,
33181
33110
  conversationIdleTtlSec: 86400,
33182
- // 图默认落在会话工作区(`<workspace>/.dsh/sidebar-qr.png`),这里只是**兜底位**:
33183
- // 会话 → 工作区解析不出来时才用它。故意不写进 patch YAML——把某个人的家目录
33184
- // 焊进分发包是错的(拆分版同一理由)。
33185
- sidebarQr: { enabled: true, imageFile: path2.join(homedir2(), ".dsh", "sidebar-qr.png"), refreshMs: 2e3 }
33111
+ // 配对的唯一入口。关掉它 = 这台主机**没有**配对入口(`/drc pair` 与文本二维码都在
33112
+ // 2026-10-03 删掉了),所以 index.ts 会把它记成一条 warn 而不是安静地什么都不做。
33113
+ pill: { enabled: true }
33186
33114
  };
33187
33115
  var loopback = /^wss?:\/\/(localhost|127\.0\.0\.1|\[::1\])(:|\/|$)/;
33188
33116
  var wsUrl = /^wss?:\/\/.+/;
@@ -33200,23 +33128,16 @@ function readConfig(injected, env = process.env) {
33200
33128
  ...DEFAULT_CONFIG,
33201
33129
  ...injected ?? {},
33202
33130
  keepAwake: { ...DEFAULT_CONFIG.keepAwake, ...injected?.keepAwake ?? {} },
33203
- // 折进来的那一半同样是**逐键合并**:patch 里只写 `sidebarQr.refreshMs` 时,
33204
- // enabled 与 imageFile 必须仍取默认值,而不是被整个对象覆盖成 undefined。
33205
- sidebarQr: { ...DEFAULT_CONFIG.sidebarQr, ...injected?.sidebarQr ?? {} }
33131
+ // 逐键合并:patch 里只写 `pill.enabled` 时不能把整个对象覆盖掉(虽然它现在只有一个键,
33132
+ // 但"注入对象整体替换默认对象"这个形状在 keepAwake 上已经坑过一次)。
33133
+ pill: { ...DEFAULT_CONFIG.pill, ...injected?.pill ?? {} }
33206
33134
  };
33207
33135
  const fromEnv = (name) => env[name];
33208
33136
  if (fromEnv("DRC_SERVER_URL")) merged.serverUrl = env.DRC_SERVER_URL;
33209
33137
  if (fromEnv("DRC_HOST_LABEL")) merged.hostLabel = env.DRC_HOST_LABEL;
33210
33138
  if (fromEnv("DRC_HOST_ID")) merged.hostId = env.DRC_HOST_ID;
33211
33139
  if (fromEnv("DRC_PAIR_ON_START_SEC")) merged.pairOnStartSec = envSeconds(env.DRC_PAIR_ON_START_SEC);
33212
- if (fromEnv("DRC_QR_IMAGE")) merged.qrImage = envFlag(env.DRC_QR_IMAGE, merged.qrImage);
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
- }
33140
+ if (fromEnv("DRC_PILL")) merged.pill.enabled = envFlag(env.DRC_PILL, merged.pill.enabled);
33220
33141
  if (fromEnv("DRC_MOCK_BRIDGE") === "1") merged.mockBridge = true;
33221
33142
  if (fromEnv("DRC_TAKE_OVER_QUESTIONS") === "1") merged.takeOverQuestions = true;
33222
33143
  if (merged.hostTokenEnv) merged.hostToken = env[merged.hostTokenEnv] ?? merged.hostToken;
@@ -33267,21 +33188,6 @@ function validateConfig(config2) {
33267
33188
  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
33189
  config2.pairOnStartSec = 0;
33269
33190
  }
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
33191
  if (!secondsSchema.safeParse(config2.conversationIdleTtlSec).success || config2.conversationIdleTtlSec <= 0) {
33286
33192
  problems.push({
33287
33193
  level: "warn",
@@ -33307,77 +33213,18 @@ function validateConfig(config2) {
33307
33213
  message: "\u63D0\u95EE\u63A5\u7BA1\u4F1A**\u53D6\u4EE3\u684C\u9762 UI \u7684\u63D0\u95EE\u80FD\u529B**\uFF08ctx.userQuestions \u53EA\u5141\u8BB8\u4E00\u4E2A\u6D3B\u8DC3 provider\uFF09\uFF1B\u624B\u673A\u7AEF\u4E0D\u5728\u7EBF\u65F6\u63D0\u95EE\u4F1A\u76F4\u63A5\u5931\u8D25"
33308
33214
  });
33309
33215
  }
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
33216
  return problems;
33333
33217
  }
33334
- function redactSecret(value) {
33218
+ function redact(value) {
33335
33219
  if (value.length <= 8) return "****";
33336
33220
  return `${value.slice(0, 4)}\u2026${value.slice(-2)}(${value.length})`;
33337
33221
  }
33338
33222
 
33223
+ // src/pill/routes.ts
33224
+ import { createHash as createHash2 } from "node:crypto";
33225
+
33339
33226
  // src/platform/qr.ts
33340
33227
  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
33228
  async function qrPng(text, options = {}) {
33382
33229
  return import_qrcode.default.toBuffer(text, {
33383
33230
  errorCorrectionLevel: options.ecc ?? "M",
@@ -33387,112 +33234,18 @@ async function qrPng(text, options = {}) {
33387
33234
  });
33388
33235
  }
33389
33236
 
33390
- // src/presentation/sidebar.ts
33391
- import path4 from "node:path";
33392
-
33393
- // src/presentation/presenter.ts
33394
- var import_qrcode2 = __toESM(require_lib(), 1);
33395
- import { createHash as createHash2 } from "node:crypto";
33396
- import { mkdirSync as mkdirSync2, renameSync as renameSync2, writeFileSync as writeFileSync2 } from "node:fs";
33397
- import path3 from "node:path";
33398
- function toEpochMs(value) {
33399
- if (typeof value === "number") return Number.isFinite(value) ? value : 0;
33400
- if (typeof value === "string") {
33401
- const parsed = Date.parse(value);
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}`;
33237
+ // src/pill/routes.ts
33238
+ var PAIR_NEW_ROUTE = "/plugins/dsh-remote-control/pairing/new";
33239
+ var PAIR_IMAGE_ROUTE = "/plugins/dsh-remote-control/pairing.png";
33240
+ var PAIR_STATUS_ROUTE = "/plugins/dsh-remote-control/status";
33241
+ var PAIR_MARKER_HEADER = "x-drc-pair";
33242
+ var PAIR_MARKER_VALUE = "1";
33243
+ function pairingEpoch(qr) {
33244
+ return createHash2("sha256").update(qr).digest("hex").slice(0, 16);
33245
+ }
33246
+ async function renderPairingPng(qr) {
33247
+ return qrPng(qr);
33489
33248
  }
33490
- function isUsableSessionId(value) {
33491
- return typeof value === "string" && /^[A-Za-z0-9._:-]{1,200}$/.test(value);
33492
- }
33493
-
33494
- // src/presentation/route.ts
33495
- var PAIRING_ROUTE = "/plugins/dsh-remote-control/pairing";
33496
33249
  var LOOPBACK_HOSTS = /* @__PURE__ */ new Set(["localhost", "127.0.0.1", "::1", "[::1]"]);
33497
33250
  function hostnameOf(host) {
33498
33251
  const trimmed = host.trim().toLowerCase();
@@ -33513,161 +33266,202 @@ function hostIsLoopback(host) {
33513
33266
  if (host === void 0 || host.trim() === "") return false;
33514
33267
  return LOOPBACK_HOSTS.has(hostnameOf(host));
33515
33268
  }
33516
- function originIsLoopback(origin) {
33517
- if (origin === void 0) return true;
33269
+ function originIsLoopbackOrAbsent(origin) {
33270
+ if (origin === void 0 || origin.trim() === "") return true;
33271
+ return originLooksLikeTheHostItself(origin);
33272
+ }
33273
+ function originLooksLikeTheHostItself(origin) {
33518
33274
  try {
33519
- const { hostname: hostname3 } = new URL(origin);
33520
- return LOOPBACK_HOSTS.has(hostname3) || hostname3 === "::1";
33275
+ const url2 = new URL(origin);
33276
+ if (url2.protocol !== "http:" && url2.protocol !== "https:") return origin.trim() !== "null";
33277
+ return LOOPBACK_HOSTS.has(url2.hostname);
33521
33278
  } catch {
33522
33279
  return false;
33523
33280
  }
33524
33281
  }
33282
+ function rejectedBy(request) {
33283
+ if (!hostIsLoopback(request.headers.host)) return "host-not-loopback";
33284
+ if (request.headers[PAIR_MARKER_HEADER] !== PAIR_MARKER_VALUE) return "pair-marker-missing";
33285
+ const origin = request.headers.origin;
33286
+ if (origin !== void 0 && origin.trim() !== "" && !originLooksLikeTheHostItself(origin)) return "origin-not-trusted";
33287
+ return void 0;
33288
+ }
33525
33289
  function json2(response, status, body) {
33526
33290
  const payload = JSON.stringify(body);
33527
33291
  response.writeHead(status, { "Content-Type": "application/json", "Content-Length": Buffer.byteLength(payload) });
33528
33292
  response.end(payload);
33529
33293
  }
33530
- function pairingHandler(options) {
33531
- return async (request, response) => {
33532
- if (request.method !== "GET") {
33294
+ function pairNewHandler(deps) {
33295
+ return (request, response) => {
33296
+ if (request.method !== "POST") {
33533
33297
  json2(response, 405, { error: "method not allowed" });
33534
33298
  return;
33535
33299
  }
33536
- if (!hostIsLoopback(request.headers.host) || !originIsLoopback(request.headers.origin)) {
33537
- json2(response, 403, { error: "request-not-trusted" });
33300
+ const guard = rejectedBy(request);
33301
+ if (guard) {
33302
+ deps.log("\u53D1\u7801\u8BF7\u6C42\u88AB\u5B88\u536B\u62D2", { guard });
33303
+ json2(response, 403, { error: "request-not-trusted", guard });
33538
33304
  return;
33539
33305
  }
33540
33306
  try {
33541
- const url2 = new URL(request.url ?? "/", "http://dsh.invalid");
33542
- const sessionId = url2.searchParams.get("session");
33543
- const snapshot = options.snapshot();
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" });
33307
+ const pairing = deps.ensureFresh();
33308
+ if (!pairing) {
33309
+ json2(response, 200, { state: "unavailable", reason: "relay-offline" });
33555
33310
  return;
33556
33311
  }
33312
+ deps.log("\u914D\u5BF9\u7801\u7ECF\u72B6\u6001\u680F\u53D1\u51FA", { token: pairing.token });
33557
33313
  json2(response, 200, {
33558
33314
  state: "ready",
33559
- epoch: snapshot.epoch,
33560
- address: sessionFileAddress(sessionId, target.file),
33561
- expiresAt: snapshot.expiresAt
33315
+ epoch: pairingEpoch(pairing.qr),
33316
+ token: pairing.token,
33317
+ expiresInMs: Math.max(0, pairing.expiresAt - Date.now())
33562
33318
  });
33563
33319
  } catch (error62) {
33564
33320
  json2(response, 500, { error: String(error62?.message ?? error62).slice(0, 200) });
33565
33321
  }
33566
33322
  };
33567
33323
  }
33568
- function registerPairingRoute(webServer, options) {
33569
- return webServer.register({ kind: "exact", path: PAIRING_ROUTE, handler: pairingHandler(options) });
33570
- }
33571
-
33572
- // src/presentation/sidebar.ts
33573
- function startSidebarQr(ctx, deps) {
33574
- const { config: config2, log } = deps;
33575
- const probe = {};
33576
- const presenter = new PairingPresenter(liveDeps(log));
33577
- let web;
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;
33324
+ function pairImageHandler(deps) {
33325
+ return async (request, response) => {
33326
+ if (request.method !== "GET" && request.method !== "HEAD") {
33327
+ json2(response, 405, { error: "method not allowed" });
33328
+ return;
33329
+ }
33330
+ if (!hostIsLoopback(request.headers.host) || !originIsLoopbackOrAbsent(request.headers.origin)) {
33331
+ json2(response, 403, { error: "request-not-trusted" });
33332
+ return;
33333
+ }
33585
33334
  try {
33586
- workspace = deps.workspaceOf(sessionId);
33335
+ const pairing = deps.current();
33336
+ if (!pairing) {
33337
+ response.writeHead(204);
33338
+ response.end();
33339
+ return;
33340
+ }
33341
+ const png = await renderPairingPng(pairing.qr);
33342
+ response.writeHead(200, {
33343
+ "Content-Type": "image/png",
33344
+ "Content-Length": png.length,
33345
+ // 一张码会被就地换掉(半程刷新、被手机用掉),所以绝不许缓存。
33346
+ // ETag 给的是这一版的哈希,pill 拿它判断要不要重画,省掉撕票式的刷新。
33347
+ "Cache-Control": "no-store",
33348
+ ETag: `"${pairingEpoch(pairing.qr)}"`
33349
+ });
33350
+ response.end(request.method === "HEAD" ? void 0 : png);
33587
33351
  } catch (error62) {
33588
- log("\u4F1A\u8BDD\u5DE5\u4F5C\u533A\u89E3\u6790\u5931\u8D25", { message: String(error62?.message ?? error62).slice(0, 160) });
33589
- workspace = void 0;
33352
+ json2(response, 500, { error: String(error62?.message ?? error62).slice(0, 200) });
33590
33353
  }
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
33354
  };
33607
- const tick = async () => {
33608
- let pairing = null;
33609
- try {
33610
- const raw = deps.pairing();
33611
- pairing = raw && typeof raw === "object" ? raw : null;
33612
- } catch (error62) {
33613
- log("read pairing state failed", { message: String(error62?.message ?? error62).slice(0, 160) });
33355
+ }
33356
+ function pillStatusHandler(deps) {
33357
+ return (request, response) => {
33358
+ if (request.method !== "GET" && request.method !== "HEAD") {
33359
+ json2(response, 405, { error: "method not allowed" });
33360
+ return;
33361
+ }
33362
+ if (!hostIsLoopback(request.headers.host) || !originIsLoopbackOrAbsent(request.headers.origin)) {
33363
+ json2(response, 403, { error: "request-not-trusted" });
33614
33364
  return;
33615
33365
  }
33616
33366
  try {
33617
- await presenter.observe(pairing);
33367
+ const status = deps.status();
33368
+ json2(response, 200, {
33369
+ relay: status.relay,
33370
+ paired: status.paired,
33371
+ hostLabel: status.hostLabel,
33372
+ serverUrl: status.serverUrl
33373
+ });
33618
33374
  } catch (error62) {
33619
- log("observe failed", { message: String(error62?.message ?? error62).slice(0, 160) });
33375
+ json2(response, 500, { error: String(error62?.message ?? error62).slice(0, 200) });
33376
+ }
33377
+ };
33378
+ }
33379
+ function registerPillRoutes(webServer, deps) {
33380
+ const off = [];
33381
+ const definitions = [
33382
+ { kind: "exact", path: PAIR_NEW_ROUTE, handler: pairNewHandler(deps) },
33383
+ { kind: "exact", path: PAIR_IMAGE_ROUTE, handler: pairImageHandler(deps) },
33384
+ { kind: "exact", path: PAIR_STATUS_ROUTE, handler: pillStatusHandler(deps) }
33385
+ ];
33386
+ try {
33387
+ for (const definition of definitions) off.push(webServer.register(definition));
33388
+ } catch (error62) {
33389
+ for (const unregister of off.splice(0)) {
33390
+ try {
33391
+ unregister();
33392
+ } catch {
33393
+ }
33394
+ }
33395
+ throw error62;
33396
+ }
33397
+ return () => {
33398
+ for (const unregister of off) {
33399
+ try {
33400
+ unregister();
33401
+ } catch {
33402
+ }
33620
33403
  }
33621
33404
  };
33405
+ }
33406
+
33407
+ // src/pill/start.ts
33408
+ function startPill(ctx, deps) {
33409
+ const { log } = deps;
33410
+ const probe = {};
33411
+ let web;
33412
+ let unregister;
33413
+ let started = false;
33414
+ let stopped = false;
33622
33415
  const tryStart = () => {
33623
33416
  if (started || stopped || !web) return;
33624
33417
  started = true;
33625
33418
  try {
33626
- unregister = registerPairingRoute(web, {
33627
- snapshot: () => presenter.snapshot(),
33628
- target: targetFor
33419
+ unregister = registerPillRoutes(web, {
33420
+ ensureFresh: () => deps.ensureFresh(),
33421
+ current: () => deps.current(),
33422
+ status: () => deps.status(),
33423
+ log
33629
33424
  });
33630
- probe.route = "registered";
33425
+ probe.routes = "registered";
33631
33426
  } catch (error62) {
33632
- probe.route = `register threw: ${String(error62?.message ?? error62).slice(0, 80)}`;
33633
- log("route registration failed", { message: String(error62?.message ?? error62).slice(0, 200) });
33427
+ probe.routes = `register threw: ${String(error62?.message ?? error62).slice(0, 80)}`;
33634
33428
  started = false;
33429
+ log("pill \u8DEF\u7531\u6CE8\u518C\u5931\u8D25\uFF08\u914D\u5BF9\u94FE\u8DEF\u4E0D\u53D7\u5F71\u54CD\uFF09", {
33430
+ message: String(error62?.message ?? error62).slice(0, 200)
33431
+ });
33635
33432
  return;
33636
33433
  }
33637
- timer = setInterval(() => void tick(), config2.refreshMs);
33638
- timer.unref?.();
33639
- void tick();
33640
- log("sidebar started", { fallbackImageFile: config2.imageFile, refreshMs: config2.refreshMs });
33434
+ log("pill routes started");
33641
33435
  };
33642
- const pickFrom = (source, name) => {
33436
+ const acceptWebServer = (value, via) => {
33437
+ if (!value || typeof value !== "object") return false;
33438
+ if (typeof value.register !== "function") {
33439
+ probe.webServer = "object without register()";
33440
+ return false;
33441
+ }
33442
+ web = value;
33443
+ probe.webServer = via;
33444
+ tryStart();
33445
+ return true;
33446
+ };
33447
+ const pickFrom = (source) => {
33643
33448
  if (!source || typeof source !== "object") return void 0;
33644
33449
  const asContext = source;
33645
33450
  if (typeof asContext.get === "function") {
33646
33451
  try {
33647
- const got = asContext.get(name, true);
33452
+ const got = asContext.get("webServer", true);
33648
33453
  if (got) return got;
33649
33454
  } catch {
33650
33455
  }
33651
33456
  }
33652
33457
  try {
33653
- return source[name];
33458
+ return source["webServer"];
33654
33459
  } catch {
33655
33460
  return void 0;
33656
33461
  }
33657
33462
  };
33658
- const acceptWebServer = (value, via) => {
33659
- if (!value || typeof value !== "object") return false;
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";
33463
+ if (!deps.enabled) {
33464
+ probe.pill = "disabled";
33671
33465
  } else {
33672
33466
  try {
33673
33467
  const found = ctx.get?.("webServer", true);
@@ -33679,12 +33473,9 @@ function startSidebarQr(ctx, deps) {
33679
33473
  try {
33680
33474
  ctx.inject?.(["webServer"], ((...args) => {
33681
33475
  try {
33682
- let value;
33683
33476
  for (const candidate of [args[0], ...args, ctx]) {
33684
- value = pickFrom(candidate, "webServer");
33685
- if (value) break;
33477
+ if (acceptWebServer(pickFrom(candidate), "via inject")) return;
33686
33478
  }
33687
- if (acceptWebServer(value, "via inject")) return;
33688
33479
  probe.webServer = `${probe.webServer ?? "none"} (inject fired, empty)`;
33689
33480
  } catch (error62) {
33690
33481
  probe["injectThrew:webServer"] = String(error62?.message ?? error62).slice(0, 160);
@@ -33697,18 +33488,21 @@ function startSidebarQr(ctx, deps) {
33697
33488
  const stop = () => {
33698
33489
  if (stopped) return;
33699
33490
  stopped = true;
33700
- if (timer !== void 0) {
33701
- clearInterval(timer);
33702
- timer = void 0;
33703
- }
33491
+ const off = unregister;
33492
+ unregister = void 0;
33704
33493
  try {
33705
- unregister?.();
33494
+ off?.();
33706
33495
  } catch {
33707
33496
  }
33708
- unregister = void 0;
33709
- if (started) log("sidebar stopped");
33497
+ if (started) log("pill routes stopped");
33498
+ };
33499
+ return {
33500
+ stop,
33501
+ probe,
33502
+ get available() {
33503
+ return started && probe.routes === "registered";
33504
+ }
33710
33505
  };
33711
- return { stop, probe };
33712
33506
  }
33713
33507
 
33714
33508
  // src/index.ts
@@ -33740,11 +33534,7 @@ function apply(ctx, injected = {}) {
33740
33534
  get state() {
33741
33535
  return handle?.state() ?? { relay: "offline", carrier: "none", reason: "not-started" };
33742
33536
  },
33743
- createPairing: () => handle?.createPairing() ?? null,
33744
- // presentation 那一半要按会话解析落点(右栏那张图放哪),而"会话 → 工作区"这点
33745
- // 平台知识只在本插件里有。**在这里开一个口,而不是让那一半自己去探内核服务**:
33746
- // 两份探测迟早分叉,而分叉的表现是"卡片说 A 地、右栏读 B 地",很难查。
33747
- sessionWorkspace: (sessionId) => handle?.sessionWorkspace(sessionId)
33537
+ createPairing: () => handle?.createPairing() ?? null
33748
33538
  });
33749
33539
  } catch {
33750
33540
  }
@@ -33807,7 +33597,7 @@ function applyInner(ctx, injected) {
33807
33597
  let runtime;
33808
33598
  let relay;
33809
33599
  let startedCarrier = "";
33810
- let sidebar;
33600
+ let pillRoutes;
33811
33601
  let lastRelayState = "connecting";
33812
33602
  let lastRelayProblem;
33813
33603
  let lastRelayGeneration = 0;
@@ -33912,13 +33702,21 @@ function applyInner(ctx, injected) {
33912
33702
  // 出站计数:手机上"没收到 X"的第一现场对比点(插件没发 vs 发了但路上丢了)。
33913
33703
  outbound: runtime?.stats ?? null,
33914
33704
  probe,
33915
- // 右栏那一半为什么没起,是"二维码不弹"唯一的排查入口:这里直接把软探测的结果带出来。
33916
- sidebar: sidebar?.probe ?? { webServer: "not-started" },
33705
+ // 配对入口(那颗 pill 的三条路由)为什么没起,是"配不了对"唯一的排查入口:
33706
+ // 这里直接把软探测的结果带出来。
33707
+ pill: pillRoutes?.probe ?? { webServer: "not-started" },
33917
33708
  // token 永远不出现真值;这条有测试锁住。
33918
33709
  hostToken: void 0,
33919
- hostTokenShape: config2.hostToken ? redactSecret(config2.hostToken) : void 0,
33710
+ hostTokenShape: config2.hostToken ? redact(config2.hostToken) : void 0,
33920
33711
  mockBridgeForced: config2.mockBridge,
33921
- problems: problems.map((problem) => `${problem.level}:${problem.field}`),
33712
+ problems: [
33713
+ ...problems.map((problem) => `${problem.level}:${problem.field}`),
33714
+ // **每次快照现判,不在 apply 里 push 一次**。`webServer` 可能是 `ctx.inject` 的回调
33715
+ // 晚到才拿到的,apply 返回那一刻 `available` 还是 false——留在 apply 里就会写出
33716
+ // `routes:"registered"` 与 `problems:["warn:pill"]` 并存的假警报
33717
+ // (2026-10-03 真机重启后实测抓到)。为什么没挂上从同一个快照的 `pill` 探针读。
33718
+ ...pillRoutes?.available ? [] : ["warn:pill"]
33719
+ ],
33922
33720
  ...config2.pairOnStartSec > 0 ? { pairOnStartWarn: "status.json \u91CC\u5E26\u7740\u4ECD\u7136\u6709\u6548\u7684\u914D\u5BF9\u7801\u4E0E PSK" } : {}
33923
33721
  };
33924
33722
  };
@@ -33951,6 +33749,25 @@ function applyInner(ctx, injected) {
33951
33749
  return created;
33952
33750
  };
33953
33751
  const createPairing = () => wrappedPublish();
33752
+ function currentPairing() {
33753
+ const shown = active.pairing;
33754
+ if (!shown) return null;
33755
+ if (!slots.resolveFor(shown.token)) return null;
33756
+ return { qr: shown.qr, token: shown.token, expiresAt: shown.expiresAt };
33757
+ }
33758
+ function ensureFreshPairing() {
33759
+ const fresh = currentPairing() ?? createPairing();
33760
+ if (!fresh) return null;
33761
+ return { qr: fresh.qr, token: fresh.token, expiresAt: fresh.expiresAt };
33762
+ }
33763
+ function pillStatus() {
33764
+ return {
33765
+ relay: relay ? relayState() : "idle",
33766
+ paired: relay?.conversationCount ?? 0,
33767
+ hostLabel: config2.hostLabel,
33768
+ serverUrl: config2.serverUrl
33769
+ };
33770
+ }
33954
33771
  const pairingWindow = new PairingWindow(
33955
33772
  {
33956
33773
  clock,
@@ -33967,7 +33784,6 @@ function applyInner(ctx, injected) {
33967
33784
  );
33968
33785
  const collected = {};
33969
33786
  const hasServices = () => typeof collected.sessions === "object" && collected.sessions !== null;
33970
- const apiProxyOf = () => collected.apiProxy;
33971
33787
  const probe = {};
33972
33788
  const injectFired = [];
33973
33789
  let hasGet = true;
@@ -33985,9 +33801,6 @@ function applyInner(ctx, injected) {
33985
33801
  start(createServicesKernel(collected, { log, takeOverQuestions: config2.takeOverQuestions, clock }));
33986
33802
  return;
33987
33803
  }
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
33804
  };
33992
33805
  const requiredMember = { sessions: "list", sessionQuery: "listSessions", agents: "get" };
33993
33806
  const keysOf = (value) => {
@@ -34199,23 +34012,6 @@ function applyInner(ctx, injected) {
34199
34012
  } catch {
34200
34013
  }
34201
34014
  }
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
34015
  tryStart();
34220
34016
  if (!runtime && config2.mockBridge) {
34221
34017
  decided = true;
@@ -34224,44 +34020,29 @@ function applyInner(ctx, injected) {
34224
34020
  }
34225
34021
  if (!runtime) {
34226
34022
  status.write({ ...state(), carrier: "none", reason: "no-carrier", probe });
34227
- log("no kernel carrier available", { hint: "\u9700\u8981 sessions/apiProxy \u670D\u52A1\uFF0C\u6216\u628A mockBridge \u6253\u5F00\u505A\u5F00\u53D1" });
34023
+ log("no kernel carrier available", { hint: "\u9700\u8981 sessions \u670D\u52A1\uFF0C\u6216\u628A mockBridge \u6253\u5F00\u505A\u5F00\u53D1" });
34024
+ }
34025
+ try {
34026
+ pillRoutes = startPill(ctx, {
34027
+ enabled: config2.pill.enabled,
34028
+ // 三条路由要的三件事:幂等发码、只读看当前码、读连接状态。
34029
+ ensureFresh: () => ensureFreshPairing(),
34030
+ current: () => currentPairing(),
34031
+ status: () => pillStatus(),
34032
+ log
34033
+ });
34034
+ } catch (error62) {
34035
+ pillRoutes = void 0;
34036
+ log("pill start threw\uFF08\u914D\u5BF9\u94FE\u8DEF\u4E0D\u53D7\u5F71\u54CD\uFF09", {
34037
+ message: String(error62?.message ?? error62).slice(0, 200)
34038
+ });
34228
34039
  }
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
34040
  const commandDefinition = {
34240
34041
  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" },
34042
+ description: "DSH \u8FDC\u7A0B\u63A7\u5236\uFF1A\u914D\u5BF9\u8BF7\u70B9\u72B6\u6001\u680F\u7684 dsh-remote-control\uFF1B/drc status \u770B\u8FDE\u63A5\u4E0E\u95EE\u9898\uFF0C/drc unpair \u89E3\u914D",
34043
+ input: { hint: "status | unpair" },
34243
34044
  handler: async (invocation) => {
34244
34045
  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
34046
  if (argument.startsWith("unpair")) {
34266
34047
  for (const id of relay?.conversationIds() ?? []) relay?.voidConversation(id);
34267
34048
  return { kind: "success", text: '\u5DF2\u89E3\u9664\u6240\u6709\u914D\u5BF9\uFF1B\u624B\u673A\u7AEF\u4F1A\u663E\u793A"\u4E3B\u673A\u5DF2\u65AD\u5F00\uFF0C\u8BF7\u91CD\u65B0\u914D\u5BF9"\u3002' };
@@ -34296,39 +34077,13 @@ function applyInner(ctx, injected) {
34296
34077
  if (child !== void 0) injectChildren.push(["commands", child]);
34297
34078
  } catch {
34298
34079
  }
34299
- try {
34300
- sidebar = startSidebarQr(ctx, {
34301
- config: config2.sidebarQr,
34302
- pairing: () => describeActivePairing(),
34303
- workspaceOf(sessionId) {
34304
- try {
34305
- return kernel?.sessionWorkspace?.(sessionId);
34306
- } catch {
34307
- return void 0;
34308
- }
34309
- },
34310
- log
34311
- });
34312
- } catch (error62) {
34313
- sidebar = void 0;
34314
- log("sidebar start threw\uFF08\u914D\u5BF9\u94FE\u8DEF\u4E0D\u53D7\u5F71\u54CD\uFF09", {
34315
- message: String(error62?.message ?? error62).slice(0, 200)
34316
- });
34317
- }
34318
34080
  return {
34319
34081
  config: config2,
34320
34082
  state,
34321
34083
  createPairing,
34322
- sessionWorkspace(sessionId) {
34323
- try {
34324
- return kernel?.sessionWorkspace?.(sessionId);
34325
- } catch {
34326
- return void 0;
34327
- }
34328
- },
34329
34084
  stop() {
34330
34085
  oneShot.cancelAll();
34331
- sidebar?.stop();
34086
+ pillRoutes?.stop();
34332
34087
  status.stop();
34333
34088
  runtime?.stop();
34334
34089
  relay?.stop();
@@ -34336,20 +34091,6 @@ function applyInner(ctx, injected) {
34336
34091
  }
34337
34092
  };
34338
34093
  }
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
34094
  export {
34354
34095
  DEFAULT_CONFIG,
34355
34096
  apply,