deskbuddy 0.2.0-beta.33 → 0.2.0-beta.34

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.
@@ -162,6 +162,7 @@ function loadConfig() {
162
162
  dumateCdpPort: portOrUndefined(parsed.dumateCdpPort),
163
163
  dumateShortcuts: typeof parsed.dumateShortcuts === "boolean" ? parsed.dumateShortcuts : void 0,
164
164
  qoderShortcuts: typeof parsed.qoderShortcuts === "boolean" ? parsed.qoderShortcuts : void 0,
165
+ qoderLaunchPort: typeof parsed.qoderLaunchPort === "boolean" ? parsed.qoderLaunchPort : void 0,
165
166
  cozeApp: typeof parsed.cozeApp === "boolean" ? parsed.cozeApp : void 0,
166
167
  cozeCdpPort: portOrUndefined(parsed.cozeCdpPort)
167
168
  };
@@ -2570,8 +2571,11 @@ function renderAmendmentQuoted(v) {
2570
2571
  if (!Array.isArray(v) || v.length === 0 || v.some((x) => typeof x !== "string" || x === "")) return null;
2571
2572
  const out = [];
2572
2573
  for (const x of v) {
2573
- if (/["\u0000-\u001f\u007f-\u009f​-‏‪-‮⁦-⁩]/.test(x)) return null;
2574
- out.push(/^[A-Za-z0-9_@%+=,./-]+$/.test(x) ? x : `"${x}"`);
2574
+ if (/[\u0000-\u001f\u007f-\u009f​-‏‪-‮⁦-⁩]/.test(x)) return null;
2575
+ if (/^[A-Za-z0-9_@%+=,./-]+$/.test(x)) out.push(x);
2576
+ else if (!x.includes('"')) out.push(`"${x}"`);
2577
+ else if (!x.includes("'")) out.push(`'${x}'`);
2578
+ else out.push(`"${x.replace(/"/g, '\\"')}"`);
2575
2579
  }
2576
2580
  return out.join(" ");
2577
2581
  }
@@ -6001,6 +6005,10 @@ var CdpKeeper = class {
6001
6005
  #hung = new HungTracker();
6002
6006
  /** 这一段卡死里「暂不重启」的原因记过哪条(同一条只记一次) */
6003
6007
  #hungNote = "";
6008
+ /** 这一段端口不通里「不重启」的原因记过哪条(同一条只记一次;端口通了 / 转成卡死时清掉。258:原先这些分支一行不记) */
6009
+ #downNote = "";
6010
+ /** 最近一轮端口不通的细分(日志 / 设备任务的等待原因用):fail = 没在监听或回包不是 WorkBuddy;timeout = 超时且监听者不是本机回环的 WorkBuddy */
6011
+ #downWhy = "";
6004
6012
  constructor(deps) {
6005
6013
  this.#d = deps;
6006
6014
  }
@@ -6141,6 +6149,16 @@ var CdpKeeper = class {
6141
6149
  this.#hung.clear();
6142
6150
  this.#hungNote = "";
6143
6151
  }
6152
+ /** 最近一轮端口不通的细分(没不通过返回 '') */
6153
+ get downWhy() {
6154
+ return this.#downWhy;
6155
+ }
6156
+ /** 端口不通期间不重启的原因:同一段里同一条只记一次 */
6157
+ #noteDown(why) {
6158
+ if (this.#downNote === why) return;
6159
+ this.#downNote = why;
6160
+ this.#d.log(`WorkBuddy \u8C03\u8BD5\u7AEF\u53E3 ${this.#d.port()} \u4E0D\u901A\uFF0C${why}`);
6161
+ }
6144
6162
  /** 卡死期间暂不重启的原因:同一段卡死里同一条只记一次 */
6145
6163
  #noteHung(why) {
6146
6164
  if (this.#hungNote === why) return;
@@ -6186,6 +6204,8 @@ var CdpKeeper = class {
6186
6204
  if (this.#hung.since) this.#d.log(`WorkBuddy \u8C03\u8BD5\u7AEF\u53E3 ${this.#d.port()} \u6062\u590D\u54CD\u5E94`);
6187
6205
  this.#markHung(false, 0);
6188
6206
  this.#downRounds = 0;
6207
+ this.#downNote = "";
6208
+ this.#downWhy = "";
6189
6209
  const ok = await this.ownerOk().catch(() => false);
6190
6210
  if (ok) await this.#rememberExe().catch(() => {
6191
6211
  });
@@ -6195,6 +6215,8 @@ var CdpKeeper = class {
6195
6215
  const now = this.#now();
6196
6216
  this.#markHung(true, now);
6197
6217
  this.#downRounds = 0;
6218
+ this.#downNote = "";
6219
+ this.#downWhy = "";
6198
6220
  if (this.#hung.confirmed(now)) {
6199
6221
  await this.#maybeRestart(true).catch((e) => this.#d.log(`WorkBuddy \u8C03\u8BD5\u7AEF\u53E3\uFF1A\u91CD\u542F\u51FA\u9519 ${e?.message ?? e}`));
6200
6222
  }
@@ -6202,6 +6224,7 @@ var CdpKeeper = class {
6202
6224
  }
6203
6225
  this.#markHung(false, 0);
6204
6226
  this.#downRounds++;
6227
+ this.#downWhy = pr === "timeout" ? "timeout" : "fail";
6205
6228
  await this.#maybeRestart().catch((e) => this.#d.log(`WorkBuddy \u8C03\u8BD5\u7AEF\u53E3\uFF1A\u91CD\u542F\u51FA\u9519 ${e?.message ?? e}`));
6206
6229
  return "down";
6207
6230
  }
@@ -6222,18 +6245,27 @@ var CdpKeeper = class {
6222
6245
  }
6223
6246
  if (this.#restarts >= RESTART_DAILY_MAX) {
6224
6247
  if (hung) this.#noteHung(`\u4ECA\u5929\u5DF2\u91CD\u542F ${RESTART_DAILY_MAX} \u6B21\uFF0C\u4E0D\u518D\u91CD\u542F\uFF1B\u8BF7\u5728\u7535\u8111\u4E0A\u91CD\u5F00 WorkBuddy`);
6248
+ else this.#noteDown(`\u4ECA\u5929\u5DF2\u91CD\u542F ${RESTART_DAILY_MAX} \u6B21\uFF0C\u4E0D\u518D\u5E26\u7AEF\u53E3\u91CD\u542F`);
6249
+ return;
6250
+ }
6251
+ if (this.#lastRestart && now - this.#lastRestart < RESTART_GAP_MS) {
6252
+ if (!hung) this.#noteDown("30 \u5206\u949F\u5185\u521A\u91CD\u542F\u8FC7\uFF0C\u5148\u4E0D\u518D\u91CD\u542F");
6225
6253
  return;
6226
6254
  }
6227
- if (this.#lastRestart && now - this.#lastRestart < RESTART_GAP_MS) return;
6228
6255
  if (this.#lastWakeLaunch && now - this.#lastWakeLaunch < WAKE_LAUNCH_GAP_MS) return;
6229
6256
  const running = await (this.#d.appRunning ?? defaultAppRunning)();
6230
- if (!running) return;
6257
+ if (!running) {
6258
+ if (!hung) this.#noteDown("WorkBuddy App \u6CA1\u5728\u8FD0\u884C");
6259
+ return;
6260
+ }
6231
6261
  if (this.#isBusy(now)) {
6232
6262
  if (hung) this.#noteHung("\u4E14\u6709\u4EFB\u52A1\u5728\u8DD1 / \u6709\u7B49\u8BBE\u5907\u7684\u5361\uFF0C\u4E0D\u91CD\u542F\uFF1B\u8BF7\u5728\u7535\u8111\u4E0A\u91CD\u5F00 WorkBuddy");
6263
+ else this.#noteDown("App \u5728\u8FD0\u884C\u4F46\u6CA1\u5E26\u8C03\u8BD5\u7AEF\u53E3\uFF08\u53EF\u80FD\u662F\u8BBE\u7F6E\u73AF\u5883\u53D8\u91CF\u4E4B\u524D\u6253\u5F00\u7684\uFF09\uFF0C\u4E14\u6709\u4EFB\u52A1\u5728\u8DD1 / \u6709\u7B49\u8BBE\u5907\u7684\u5361\uFF0C\u4E0D\u91CD\u542F");
6233
6264
  return;
6234
6265
  }
6235
6266
  if (!await this.#userAway()) {
6236
6267
  if (hung) this.#noteHung("\u7528\u6237\u5728\u7535\u8111\u524D\uFF0C\u4E0D\u6253\u65AD\uFF1B\u8BF7\u5728\u7535\u8111\u4E0A\u91CD\u5F00 WorkBuddy");
6268
+ else this.#noteDown("App \u5728\u8FD0\u884C\u4F46\u6CA1\u5E26\u8C03\u8BD5\u7AEF\u53E3\uFF08\u53EF\u80FD\u662F\u8BBE\u7F6E\u73AF\u5883\u53D8\u91CF\u4E4B\u524D\u6253\u5F00\u7684\uFF09\uFF0C\u7528\u6237\u5728\u7535\u8111\u524D\uFF0C\u4E0D\u91CD\u542F");
6237
6269
  return;
6238
6270
  }
6239
6271
  await this.#doRestart(true, hung);
@@ -8277,6 +8309,10 @@ var RESTART_DAILY_MAX2 = 3;
8277
8309
  var BUSY_WINDOW_MS2 = 30 * 60 * 1e3;
8278
8310
  var QUIT_WAIT_MS2 = 20 * 1e3;
8279
8311
  var USER_IDLE_MS = 5 * 60 * 1e3;
8312
+ var LAUNCH_FRESH_MS = 10 * 1e3;
8313
+ var LAUNCH_PORT_WINDOW_MS = 10 * 60 * 1e3;
8314
+ var LAUNCH_PORT_MAX = 2;
8315
+ var LAUNCH_SEEN_MAX = 32;
8280
8316
  function winBrandMainProc(procs, exe) {
8281
8317
  const want = exe.toLowerCase();
8282
8318
  for (const p of procs) {
@@ -8328,6 +8364,24 @@ function parseLsappBundle(out) {
8328
8364
  const m = /"CFBundleIdentifier"\s*=\s*"([^"]+)"/.exec(out);
8329
8365
  return m ? m[1] : null;
8330
8366
  }
8367
+ function parseEtime(s) {
8368
+ const m = /^(?:(?:(\d+)-)?(\d+):)?(\d+):(\d+)$/.exec(s.trim());
8369
+ if (!m) return null;
8370
+ const [d, h, mi, se] = [m[1], m[2], m[3], m[4]].map((x) => Number(x ?? 0));
8371
+ return (((d * 24 + h) * 60 + mi) * 60 + se) * 1e3;
8372
+ }
8373
+ function hasPortArg(args) {
8374
+ return /(^|\s|")--remote-debugging-port(=|\s|"|$)/.test(args);
8375
+ }
8376
+ function macBrandMains(psOut, macExe) {
8377
+ const out = [];
8378
+ for (const line of psOut.split(/\r?\n/)) {
8379
+ const m = /^\s*(\d+)\s+(\S+)\s+(.*)$/.exec(line);
8380
+ if (!m || !isMainArgs(m[3], macExe)) continue;
8381
+ out.push({ pid: Number(m[1]), ageMs: parseEtime(m[2]), hasPort: hasPortArg(m[3].slice(macExe.length)) });
8382
+ }
8383
+ return out;
8384
+ }
8331
8385
  function toolBusy(tool, now = Date.now(), dir2 = stateDir(), alive = isAlive) {
8332
8386
  if (!existsSync20(dir2)) return false;
8333
8387
  for (const f of readdirSync12(dir2)) {
@@ -8358,6 +8412,8 @@ var QoderCdpKeeper = class {
8358
8412
  #hung = new HungTracker();
8359
8413
  /** 这一段卡死里「暂不重启」的原因记过哪条(同一条只记一次) */
8360
8414
  #hungNote = "";
8415
+ /** 启动瞬间补端口处理过的主进程号(每个启动只做一次) */
8416
+ #launchSeen = [];
8361
8417
  constructor(deps) {
8362
8418
  this.#d = deps;
8363
8419
  }
@@ -8440,6 +8496,7 @@ var QoderCdpKeeper = class {
8440
8496
  const t = all?.[this.#d.brand.tool] ?? {};
8441
8497
  const out = { lastLaunch: Number(t.lastLaunch) || 0, lastRestart: Number(t.lastRestart) || 0, day: typeof t.day === "string" ? t.day : "", restarts: Number(t.restarts) || 0 };
8442
8498
  if (this.#d.brand.win && isWinAppExe(t.appExe, this.#d.brand.win.exe)) out.appExe = t.appExe;
8499
+ if (Array.isArray(t.launchPorts)) out.launchPorts = t.launchPorts.map(Number).filter((x) => Number.isFinite(x) && x > 0);
8443
8500
  return out;
8444
8501
  } catch {
8445
8502
  return { lastLaunch: 0, lastRestart: 0, day: "", restarts: 0 };
@@ -8609,6 +8666,87 @@ var QoderCdpKeeper = class {
8609
8666
  const r = await this.#exec()("ioreg", ["-c", "IOHIDSystem", "-d", "4"]);
8610
8667
  return r.code === 0 ? parseHidIdleMs2(r.stdout) : null;
8611
8668
  }
8669
+ /** 主进程(macOS;只认唯一一个;没有 / 认不出 / 多个 → null) */
8670
+ async #brandMain() {
8671
+ const r = await this.#exec()("ps", ["-axww", "-o", "pid=,etime=,args="]);
8672
+ if (r.code !== 0) return null;
8673
+ const mains = macBrandMains(r.stdout, this.#d.brand.macExe);
8674
+ return mains.length === 1 ? mains[0] : null;
8675
+ }
8676
+ /**
8677
+ * 启动瞬间补端口(spec 2026-10-12 qoder-launch-port):主进程刚起(不到 LAUNCH_FRESH_MS)、命令行没带调试端口、没有会话在跑 / 待答题 →
8678
+ * 不管用户在不在,正常退出并带端口拉起。每个启动(主进程号)只看一次:带了端口 / 已超过 10 秒 / 处理过(成败都算)都不再动;
8679
+ * 单独限流 LAUNCH_PORT_MAX 次 / LAUNCH_PORT_WINDOW_MS(落盘),不占每天 3 次的重启额度。
8680
+ * 🔴 macOS 只正常退出(osascript quit),20 秒没退就放弃、绝不强杀;Windows 关闭按钮只会缩到托盘,按原有规则结束这一个主进程树(刚起、没会话)
8681
+ */
8682
+ async fixFreshLaunch() {
8683
+ if (this.#platform() !== "darwin" || this.#skip() || this.#d.noRestart) return "skipped";
8684
+ if (this.#restarting || this.#waking || this.#launchChecking) return "skipped";
8685
+ this.#launchChecking = true;
8686
+ try {
8687
+ return await this.#fixFresh();
8688
+ } finally {
8689
+ this.#launchChecking = false;
8690
+ if (this.#launchRestarting) {
8691
+ this.#launchRestarting = false;
8692
+ this.#restarting = false;
8693
+ }
8694
+ }
8695
+ }
8696
+ /** 补端口检测进行中(只看进程表,不挡唤起) */
8697
+ #launchChecking = false;
8698
+ /** 这次 #restarting 是补端口占的(finally 里释放) */
8699
+ #launchRestarting = false;
8700
+ /** 正在启动补端口 / 重启(常开据此跳过) */
8701
+ get restarting() {
8702
+ return this.#restarting;
8703
+ }
8704
+ async #fixFresh() {
8705
+ const main = await this.#brandMain();
8706
+ if (!main) return "none";
8707
+ if (this.#launchSeen.includes(main.pid)) return "seen";
8708
+ this.#launchSeen.push(main.pid);
8709
+ if (this.#launchSeen.length > LAUNCH_SEEN_MAX) this.#launchSeen.shift();
8710
+ if (main.hasPort) return "has_port";
8711
+ if (main.ageMs === null || main.ageMs < 0 || main.ageMs >= LAUNCH_FRESH_MS) return "old";
8712
+ const b = this.#d.brand;
8713
+ const now = this.#now();
8714
+ if ((this.#d.busy ?? (() => toolBusy(b.tool, now)))()) {
8715
+ this.#d.log(`${b.label} \u521A\u542F\u52A8\u6CA1\u5E26\u8C03\u8BD5\u7AEF\u53E3\uFF0C\u4F46\u6709\u4F1A\u8BDD\u5728\u8DD1\u6216\u6709\u5F85\u7B54\u9898\uFF0C\u4E0D\u91CD\u5F00`);
8716
+ return "busy";
8717
+ }
8718
+ const t = this.#loadThrottle();
8719
+ const recent = (t.launchPorts ?? []).filter((x) => now - x < LAUNCH_PORT_WINDOW_MS && x <= now);
8720
+ if (recent.length >= LAUNCH_PORT_MAX) {
8721
+ this.#d.log(`${b.label} \u521A\u542F\u52A8\u6CA1\u5E26\u8C03\u8BD5\u7AEF\u53E3\uFF0C\u4F46 ${LAUNCH_PORT_WINDOW_MS / 6e4} \u5206\u949F\u5185\u5DF2\u91CD\u5F00 ${recent.length} \u6B21\uFF0C\u8FD9\u6B21\u4E0D\u91CD\u5F00`);
8722
+ return "limited";
8723
+ }
8724
+ const app = this.appPath();
8725
+ if (!app) return "failed";
8726
+ if (this.#restarting || this.#waking) return "skipped";
8727
+ this.#restarting = true;
8728
+ this.#launchRestarting = true;
8729
+ this.#saveThrottle({ ...t, launchPorts: [...recent, now], lastLaunch: now });
8730
+ await this.#exec()("osascript", ["-e", `quit app ${JSON.stringify(b.appName)}`], 15e3);
8731
+ let gone = false;
8732
+ for (let waited = 0; waited < QUIT_WAIT_MS2; waited += 1e3) {
8733
+ if (!await this.appRunning()) {
8734
+ gone = true;
8735
+ break;
8736
+ }
8737
+ await this.#sleep(1e3);
8738
+ }
8739
+ if (!gone) {
8740
+ this.#d.log(`${b.label} \u521A\u542F\u52A8\u6CA1\u5E26\u8C03\u8BD5\u7AEF\u53E3\uFF0C${QUIT_WAIT_MS2 / 1e3} \u79D2\u5185\u6CA1\u6709\u9000\u51FA\uFF0C\u653E\u5F03\u91CD\u5F00\uFF08\u4E0D\u5F3A\u5236\u7ED3\u675F\uFF09`);
8741
+ return "failed";
8742
+ }
8743
+ const port = this.#d.port();
8744
+ const launch = this.#d.launch ?? defaultLaunch;
8745
+ const env = (this.#d.launchEnv ?? cleanLaunchEnv)();
8746
+ const ok = await launch("open", ["-a", app, "--args", `--remote-debugging-port=${port}`], env);
8747
+ this.#d.log(ok ? `${b.label} \u521A\u542F\u52A8\u6CA1\u5E26\u8C03\u8BD5\u7AEF\u53E3\uFF0C\u5DF2\u5E26\u7AEF\u53E3\u91CD\u5F00\uFF08\u7AEF\u53E3 ${port}\uFF09` : `${b.label} \u521A\u542F\u52A8\u6CA1\u5E26\u8C03\u8BD5\u7AEF\u53E3\uFF0C\u5DF2\u9000\u51FA\u4F46\u5E26\u7AEF\u53E3\u62C9\u8D77\u5931\u8D25\uFF08\u4E0D\u518D\u91CD\u8BD5\uFF09`);
8748
+ return ok ? "reopened" : "failed";
8749
+ }
8612
8750
  /**
8613
8751
  * 设备任务到达、App 不可用时唤起(spec §3.1)。不等端口起来(调用方等连上):
8614
8752
  * - 端口已活 → running;
@@ -8616,9 +8754,9 @@ var QoderCdpKeeper = class {
8616
8754
  * - App 开着没带端口:用户在场 → present、正在跑任务 → busy(都不动 App);用户不在场且空闲 → 退出重启 → restarted;
8617
8755
  * - 没装 → not_installed;当天重启次数用完 / 拉不起来 → failed;非 macOS / 测试兜底 → skipped
8618
8756
  */
8619
- async wakeForTask() {
8757
+ async wakeForTask(source = "task") {
8620
8758
  if (this.#waking) return this.#waking;
8621
- const p = this.#wake();
8759
+ const p = this.#wake(source);
8622
8760
  this.#waking = p;
8623
8761
  try {
8624
8762
  return await p;
@@ -8626,7 +8764,7 @@ var QoderCdpKeeper = class {
8626
8764
  if (this.#waking === p) this.#waking = null;
8627
8765
  }
8628
8766
  }
8629
- async #wake() {
8767
+ async #wake(source) {
8630
8768
  if (this.#skip() || !this.#canManage() || this.#d.noRestart) return "skipped";
8631
8769
  if (this.#restarting) return "restarted";
8632
8770
  const pr = await this.#probe();
@@ -8657,7 +8795,7 @@ var QoderCdpKeeper = class {
8657
8795
  if (restarts >= RESTART_DAILY_MAX2) return "failed";
8658
8796
  const app = this.appPath();
8659
8797
  if (!app) return "not_installed";
8660
- return await this.#restart(app, t, now, restarts, false) ? "restarted" : "failed";
8798
+ return await this.#restart(app, t, now, restarts, false, source) ? "restarted" : "failed";
8661
8799
  }
8662
8800
  /**
8663
8801
  * 设备任务到达、端口在监听但无响应:空闲、用户不在场、已连续无响应满 HUNG_RESTART_MS、没超节流 → 结束并带端口重启(restarted);
@@ -8693,7 +8831,7 @@ var QoderCdpKeeper = class {
8693
8831
  * hung=false(没带端口):正常退出 20 秒没反应就放弃——🔴 可能有确认弹窗,不强杀;
8694
8832
  * hung=true(端口无响应、App 卡死):正常退出没反应时结束端口监听者(确认是这个 App 的主进程才动)
8695
8833
  */
8696
- async #restart(app, t, now, restarts, hung) {
8834
+ async #restart(app, t, now, restarts, hung, source = "task") {
8697
8835
  if (this.#restarting) return false;
8698
8836
  this.#restarting = true;
8699
8837
  const b = this.#d.brand;
@@ -8701,7 +8839,7 @@ var QoderCdpKeeper = class {
8701
8839
  try {
8702
8840
  this.#saveThrottle({ ...this.#loadThrottle(), lastRestart: now, day: localDay2(now), restarts: restarts + 1 });
8703
8841
  this.#d.log(
8704
- hung ? `${b.label} \u8C03\u8BD5\u7AEF\u53E3 ${port} \u65E0\u54CD\u5E94 ${this.#hung.minutes(now)} \u5206\u949F\u3001App \u7A7A\u95F2\u3001\u7528\u6237\u4E0D\u5728\u573A\uFF1A\u9000\u51FA\u5E76\u5E26\u7AEF\u53E3\u91CD\u542F\uFF08\u4ECA\u5929\u7B2C ${restarts + 1}/${RESTART_DAILY_MAX2} \u6B21\uFF09` : `\u8BBE\u5907\u4EFB\u52A1\u5230\u8FBE\uFF1A${b.label} \u5F00\u7740\u4F46\u6CA1\u5E26\u8C03\u8BD5\u7AEF\u53E3\u3001\u7528\u6237\u4E0D\u5728\u573A\uFF1A\u9000\u51FA\u5E76\u5E26\u7AEF\u53E3\u91CD\u542F\uFF08\u4ECA\u5929\u7B2C ${restarts + 1}/${RESTART_DAILY_MAX2} \u6B21\uFF09`
8842
+ hung ? `${b.label} \u8C03\u8BD5\u7AEF\u53E3 ${port} \u65E0\u54CD\u5E94 ${this.#hung.minutes(now)} \u5206\u949F\u3001App \u7A7A\u95F2\u3001\u7528\u6237\u4E0D\u5728\u573A\uFF1A\u9000\u51FA\u5E76\u5E26\u7AEF\u53E3\u91CD\u542F\uFF08\u4ECA\u5929\u7B2C ${restarts + 1}/${RESTART_DAILY_MAX2} \u6B21\uFF09` : source === "keep_open" ? `${b.label} \u7AEF\u53E3\u4E0D\u5728\u3001\u7528\u6237\u4E0D\u5728\u4E14\u7A7A\u95F2\uFF0C\u5E26\u7AEF\u53E3\u91CD\u5F00\uFF08\u5E38\u5F00\uFF0C\u4ECA\u5929\u7B2C ${restarts + 1}/${RESTART_DAILY_MAX2} \u6B21\uFF09` : `\u8BBE\u5907\u4EFB\u52A1\u5230\u8FBE\uFF1A${b.label} \u5F00\u7740\u4F46\u6CA1\u5E26\u8C03\u8BD5\u7AEF\u53E3\u3001\u7528\u6237\u4E0D\u5728\u573A\uFF1A\u9000\u51FA\u5E76\u5E26\u7AEF\u53E3\u91CD\u542F\uFF08\u4ECA\u5929\u7B2C ${restarts + 1}/${RESTART_DAILY_MAX2} \u6B21\uFF09`
8705
8843
  );
8706
8844
  const exec = this.#exec();
8707
8845
  if (this.#isWin()) return await this.#restartWin(app, hung);
@@ -10510,7 +10648,7 @@ var defaultLoginOpenDeps = () => ({
10510
10648
  });
10511
10649
  async function installCodebuddyHooks(log) {
10512
10650
  try {
10513
- const { installTools: installTools2 } = await import("./enable-NGQ3J3NW.js");
10651
+ const { installTools: installTools2 } = await import("./enable-OC46BDQD.js");
10514
10652
  installTools2(["workbuddy"], () => {
10515
10653
  }, null, { auto: true });
10516
10654
  } catch (e) {
@@ -11024,7 +11162,7 @@ function defaultOfferDeps(log) {
11024
11162
  }),
11025
11163
  openLogin: () => defaultOpenLogin(platform),
11026
11164
  installHooks: async () => {
11027
- const { installTools: installTools2 } = await import("./enable-NGQ3J3NW.js");
11165
+ const { installTools: installTools2 } = await import("./enable-OC46BDQD.js");
11028
11166
  installTools2(["workbuddy"], (l) => log(`[WorkBuddy \u72EC\u7ACB\u7248] ${l}`), null, { auto: true });
11029
11167
  },
11030
11168
  log: (l) => log(`[WorkBuddy \u72EC\u7ACB\u7248] ${l}`),
package/dist/cli.js CHANGED
@@ -262,7 +262,7 @@ import {
262
262
  writeState,
263
263
  writeWakeMark,
264
264
  writeWakeSecret
265
- } from "./chunk-RHIQGM66.js";
265
+ } from "./chunk-VSS27ECH.js";
266
266
 
267
267
  // src/commands/config.ts
268
268
  function runConfig(args, out = (s2) => process.stdout.write(s2 + "\n")) {
@@ -3008,23 +3008,63 @@ function qoderCnRule(lines3) {
3008
3008
  if (!v.dialog && lines3.slice(rules[rules.length - 1] + 1).some((l) => l.trim() !== "" && !Q_FOOTER_RE.test(l))) v.dialog = "popup";
3009
3009
  return v;
3010
3010
  }
3011
+ var W_RULE_RE = /^(?:─{20,}|-{20,})$/;
3012
+ var W_TRUST_RE = /Do you trust the files in this folder\?|Trust folder only/;
3013
+ var W_PERMISSION_RE = /Do you want to (?:proceed|make|create|run|allow|edit|delete|write|overwrite)|^\s*[❯>]\s*1\.\s*Yes\b|^\s*\d\.\s*Yes, and don't ask again/m;
3014
+ var W_BUSY_RE = /…\s*\(\d+[smh]\b|^\S\s+Thinking\.\.\.|\besc to interrupt\b/;
3015
+ var W_SUGGEST_RE = /↵\s*send\s*$/;
3016
+ var W_FOOTER_RE = /for shortcuts|for agents|AltT to toggle|agent-mode|accept edits|bypass permissions|plan mode|shift\s*\+\s*tab|Context left until auto-compact|auto-compact|update available|new version|upgrade|auto-update|restart to (?:apply|update)/i;
3017
+ function workbuddyRule(lines3, undimmed) {
3018
+ const rules = [];
3019
+ lines3.forEach((l, i) => {
3020
+ if (W_RULE_RE.test(l.trim())) rules.push(i);
3021
+ });
3022
+ if (rules.length === 0) return W_TRUST_RE.test(lines3.join("\n")) ? { permission: false, input: null, trust: true } : { permission: false, input: null };
3023
+ const a = rules.length >= 2 ? rules[rules.length - 2] : rules[rules.length - 1];
3024
+ const bottom = lines3.slice(a).join("\n");
3025
+ if (W_TRUST_RE.test(bottom)) return { permission: false, input: null, trust: true };
3026
+ const permission = W_PERMISSION_RE.test(bottom);
3027
+ let input = null;
3028
+ let suggest = false;
3029
+ if (rules.length >= 2) {
3030
+ const b = rules[rules.length - 1];
3031
+ const raw = lines3.slice(a + 1, b).map((l) => l.trimEnd());
3032
+ if (raw.length > 0 && /^\s*>/.test(raw[0])) {
3033
+ suggest = raw.length === 1 && W_SUGGEST_RE.test(raw[0]);
3034
+ const src = undimmed && undimmed.length === lines3.length ? undimmed.slice(a + 1, b).map((l) => l.trimEnd()) : raw;
3035
+ input = src.join("\n").replace(/^\s*>\s?/, "").trim();
3036
+ }
3037
+ }
3038
+ const v = { permission, input };
3039
+ if (lines3.slice(Math.max(0, a - 6), a).some((l) => W_BUSY_RE.test(l.trim()))) v.busy = true;
3040
+ if (input !== null && suggest) v.placeholder = true;
3041
+ else if (input !== null && input.startsWith("/")) v.dialog = "slash-menu";
3042
+ if (!v.dialog && input !== null && lines3.slice(rules[rules.length - 1] + 1).some((l) => l.trim() !== "" && !W_FOOTER_RE.test(l))) v.dialog = "popup";
3043
+ return v;
3044
+ }
3011
3045
  function textRejectFor(tool) {
3046
+ if (tool === "workbuddy") return (text) => /^\//.test(oneLine(text)) ? "slash_command" : null;
3012
3047
  if (tool !== "qoder_cn") return void 0;
3013
3048
  return (text) => /^[/!]/.test(oneLine(text)) ? "slash_command" : null;
3014
3049
  }
3015
3050
  var RULES2 = {
3016
3051
  claude_code: inspectLines,
3017
- qoder_cn: qoderCnRule
3052
+ qoder_cn: qoderCnRule,
3053
+ workbuddy: workbuddyRule
3018
3054
  };
3019
3055
  function screenRuleFor(tool) {
3020
3056
  return RULES2[tool] ?? (() => null);
3021
3057
  }
3022
3058
  var INJECT_READY = {
3023
3059
  claude_code: true,
3024
- qoder_cn: true
3060
+ qoder_cn: true,
3061
+ // workbuddy:daemon 开窗的 WorkBuddy 窗口(answerWindow)接管设备回复(问题表 258),规则 workbuddyRule。
3062
+ // 只在 Windows 测试机采样 / 端到端验证过 → 先只开 win32;macOS 维持原来的后台续跑
3063
+ workbuddy: ["win32"]
3025
3064
  };
3026
- function injectReady(tool) {
3027
- return INJECT_READY[tool] === true;
3065
+ function injectReady(tool, platform = process.platform) {
3066
+ const v = INJECT_READY[tool];
3067
+ return v === true || Array.isArray(v) && v.includes(platform);
3028
3068
  }
3029
3069
 
3030
3070
  // src/wrap/qoderPermBox.ts
@@ -7703,18 +7743,23 @@ const localeOf = () => { try { return new URL(location.href).searchParams.get('l
7703
7743
  `;
7704
7744
  var PROBE4 = String.raw`(async () => {
7705
7745
  ${PRELUDE3}
7706
- if (C && typeof C.list === 'function' && typeof C.get === 'function') {
7707
- try {
7708
- const arr = arrOf(await C.list());
7746
+ const em = (e) => String((e && (e.message || e.name)) || e || '').slice(0, 80);
7747
+ let why = !globalThis.wb ? 'no_wb' : !C ? 'no_conversations' : (typeof C.list !== 'function' || typeof C.get !== 'function') ? 'no_list_get' : '';
7748
+ if (!why) {
7749
+ let arr = null;
7750
+ try { arr = arrOf(await C.list()); } catch (e) { why = 'list_failed:' + em(e); }
7751
+ if (arr) {
7709
7752
  const any = arr.find((s) => s && s.id);
7710
7753
  if (!any) return { mode: 'sdk', locale: localeOf(), verified: false };
7711
- const conv = await C.get(any.id);
7754
+ let conv = null;
7755
+ try { conv = await C.get(any.id); } catch (e) { why = 'get_failed:' + em(e); }
7712
7756
  if (conv && typeof conv.getPendingInfo === 'function' && typeof conv.resolvePending === 'function' && typeof conv.rejectPending === 'function')
7713
7757
  return { mode: 'sdk', locale: localeOf(), verified: true };
7714
- } catch (e) {}
7758
+ if (!why) why = 'no_pending_api';
7759
+ }
7715
7760
  }
7716
- if (C && typeof C.currentId === 'string') return { mode: 'dom', locale: localeOf() };
7717
- return { mode: 'none', locale: localeOf() };
7761
+ if (C && typeof C.currentId === 'string') return { mode: 'dom', locale: localeOf(), why };
7762
+ return { mode: 'none', locale: localeOf(), why };
7718
7763
  })()`;
7719
7764
  var IDLE_STATES = "['idle','completed','cancelled','canceled','done','failed','error','terminated','archived','deleted','']";
7720
7765
  var SNAPSHOT2 = String.raw`(async () => {
@@ -7841,12 +7886,14 @@ ${PRELUDE3}
7841
7886
  }
7842
7887
  var APP_READY = String.raw`(async () => {
7843
7888
  ${PRELUDE3}
7844
- if (!C || typeof C.create !== 'function' || typeof C.list !== 'function' || typeof C.get !== 'function') return { ok: false, why: 'no_sdk' };
7889
+ if (!globalThis.wb) return { ok: false, why: 'no_wb' };
7890
+ if (!C) return { ok: false, why: 'no_conversations' };
7891
+ if (typeof C.create !== 'function' || typeof C.list !== 'function' || typeof C.get !== 'function') return { ok: false, why: 'no_sdk' };
7845
7892
  try {
7846
7893
  const r = await C.list();
7847
7894
  const ok = Array.isArray(r) || (r && Array.isArray(r.items || r.conversations || r.sessions || r.records));
7848
7895
  return ok ? { ok: true } : { ok: false, why: 'list_shape' };
7849
- } catch (e) { return { ok: false, why: 'list_failed' }; }
7896
+ } catch (e) { return { ok: false, why: 'list_failed:' + String((e && (e.message || e.name)) || e || '').slice(0, 80) }; }
7850
7897
  })()`;
7851
7898
  function appDeleteIfEmptyExpr(sessionId) {
7852
7899
  return String.raw`(async () => {
@@ -7903,6 +7950,15 @@ var WorkbuddyAppWatch = class {
7903
7950
  #keeper;
7904
7951
  #client = null;
7905
7952
  #mode = "none";
7953
+ /** 模式不是 sdk 的原因(PROBE 的 why) */
7954
+ #modeWhy = "";
7955
+ /** 这次连上后有没有探出过模式(第一次探出来不论什么模式都记一行,258:初值就是 none,原先一直 none 时一行不记) */
7956
+ #modeProbed = false;
7957
+ #connectedAt = 0;
7958
+ #nonSdkSince = 0;
7959
+ #connectError = "";
7960
+ /** 端口检查过没有(第一轮不通也记一行) */
7961
+ #portChecked = false;
7906
7962
  #probedAt = 0;
7907
7963
  #keeperAt = 0;
7908
7964
  #portStatus = "down";
@@ -7947,6 +8003,27 @@ var WorkbuddyAppWatch = class {
7947
8003
  get mode() {
7948
8004
  return this.#mode;
7949
8005
  }
8006
+ /** 设备任务等 App 就绪时的现状(258) */
8007
+ readiness() {
8008
+ let downWhy = "";
8009
+ try {
8010
+ downWhy = this.#keeper.downWhy ?? "";
8011
+ } catch {
8012
+ downWhy = "";
8013
+ }
8014
+ const connected = !!this.#client && !this.#client.closed;
8015
+ return {
8016
+ port: this.#portChecked ? this.#portStatus : null,
8017
+ ...downWhy ? { downWhy } : {},
8018
+ connected,
8019
+ ...this.#connectError ? { connectError: this.#connectError } : {},
8020
+ mode: this.#mode,
8021
+ probed: connected && this.#modeProbed,
8022
+ modeWhy: this.#modeWhy,
8023
+ connectedAt: connected ? this.#connectedAt : 0,
8024
+ nonSdkSince: connected ? this.#nonSdkSince : 0
8025
+ };
8026
+ }
7950
8027
  /** 设备任务能不能走 App(spec 2026-10-09 workbuddy-app-task §3.1):开关开着、连上且模式 sdk */
7951
8028
  available() {
7952
8029
  return !this.#stopped && this.#d.enabled() && this.#portStatus === "up" && !!this.#client && !this.#client.closed && this.#mode === "sdk";
@@ -8049,15 +8126,24 @@ var WorkbuddyAppWatch = class {
8049
8126
  const now = this.#now();
8050
8127
  if (now - this.#keeperAt >= KEEPER_EVERY_MS3 || this.#keeperAt === 0) {
8051
8128
  this.#keeperAt = now;
8052
- const prev = this.#portStatus;
8129
+ const prev = this.#portChecked ? this.#portStatus : null;
8053
8130
  this.#portStatus = await this.#keeper.tick();
8131
+ this.#portChecked = true;
8054
8132
  if (this.#stopped) return this.#disconnect(REASON_GONE3);
8055
8133
  if (this.#portStatus === "untrusted" && prev !== "untrusted") this.#d.log(`WorkBuddy \u8C03\u8BD5\u7AEF\u53E3 ${this.#d.port()} \u7684\u76D1\u542C\u8005\u4E0D\u662F\u672C\u673A\u56DE\u73AF\u4E0A\u7684 WorkBuddy\uFF0C\u4E0D\u8FDE`);
8056
- if (this.#portStatus !== "up" && this.#client) this.#disconnect(this.#portStatus === "hung" ? REASON_APP_HUNG : REASON_APP_EXITED);
8134
+ if (this.#portStatus === "down" && prev !== "down")
8135
+ this.#d.log(`WorkBuddy \u8C03\u8BD5\u7AEF\u53E3 ${this.#d.port()} \u4E0D\u901A\uFF08${this.#keeper.downWhy === "timeout" ? "\u8D85\u65F6\uFF0C\u4E14\u76D1\u542C\u8005\u4E0D\u662F\u672C\u673A\u56DE\u73AF\u4E0A\u7684 WorkBuddy" : "\u6CA1\u5728\u76D1\u542C\uFF0C\u6216\u56DE\u5305\u4E0D\u662F WorkBuddy"}\uFF09`);
8136
+ if (this.#portStatus !== "up" && this.#client) {
8137
+ this.#d.log(`WorkBuddy App \u6388\u6743\u6865\u65AD\u5F00\uFF08\u8C03\u8BD5\u7AEF\u53E3${this.#portStatus === "hung" ? "\u65E0\u54CD\u5E94" : this.#portStatus === "untrusted" ? "\u76D1\u542C\u8005\u4E0D\u5BF9" : "\u4E0D\u901A"}\uFF09`);
8138
+ this.#disconnect(this.#portStatus === "hung" ? REASON_APP_HUNG : REASON_APP_EXITED);
8139
+ }
8057
8140
  }
8058
8141
  if (this.#portStatus !== "up") return;
8059
8142
  if (!this.#client || this.#client.closed) {
8060
- if (this.#client) this.#disconnect(REASON_APP_EXITED);
8143
+ if (this.#client) {
8144
+ this.#d.log("WorkBuddy App \u6388\u6743\u6865\u8FDE\u63A5\u65AD\u4E86\uFF0C\u91CD\u8FDE");
8145
+ this.#disconnect(REASON_APP_EXITED);
8146
+ }
8061
8147
  if (now < this.#connectRetryAt) return;
8062
8148
  try {
8063
8149
  const client = await (this.#d.connect ?? ((p) => connectCdp(p)))(this.#d.port());
@@ -8070,12 +8156,17 @@ var WorkbuddyAppWatch = class {
8070
8156
  }
8071
8157
  this.#client = client;
8072
8158
  this.#probedAt = 0;
8159
+ this.#modeProbed = false;
8160
+ this.#modeWhy = "";
8161
+ this.#connectedAt = this.#now();
8162
+ this.#connectError = "";
8073
8163
  this.#connectBackoffMs = CONNECT_BACKOFF_MIN_MS3;
8074
8164
  this.#connectRetryAt = 0;
8075
8165
  this.#connectFailLogged = false;
8076
8166
  this.#d.log("WorkBuddy App \u6388\u6743\u6865\u5DF2\u8FDE\u4E0A");
8077
8167
  } catch (e) {
8078
8168
  this.#client = null;
8169
+ this.#connectError = String(e?.message ?? e).slice(0, 120);
8079
8170
  this.#connectRetryAt = this.#now() + this.#connectBackoffMs;
8080
8171
  this.#connectBackoffMs = Math.min(this.#connectBackoffMs * 2, CONNECT_BACKOFF_MAX_MS3);
8081
8172
  if (!this.#connectFailLogged) {
@@ -8088,10 +8179,17 @@ var WorkbuddyAppWatch = class {
8088
8179
  if (now - this.#probedAt >= PROBE_EVERY_MS3 || this.#probedAt === 0) {
8089
8180
  this.#probedAt = now;
8090
8181
  const prev = this.#mode;
8182
+ const prevWhy = this.#modeWhy;
8183
+ const first = !this.#modeProbed;
8091
8184
  const r = await this.#eval(PROBE4);
8092
8185
  if (this.#stopped) return this.#disconnect(REASON_GONE3);
8186
+ if (!this.#client) return;
8093
8187
  this.#mode = r?.mode === "sdk" ? "sdk" : r?.mode === "dom" ? "dom" : "none";
8094
- if (prev !== this.#mode) this.#d.log(`WorkBuddy App \u6388\u6743\u6865\u6A21\u5F0F\uFF1A${this.#mode}`);
8188
+ this.#modeWhy = this.#mode === "sdk" ? "" : !r ? "no_result" : typeof r.why === "string" ? r.why.slice(0, 120) : "";
8189
+ this.#modeProbed = true;
8190
+ if (this.#mode === "sdk") this.#nonSdkSince = 0;
8191
+ else if (first || prev === "sdk" || !this.#nonSdkSince) this.#nonSdkSince = now;
8192
+ if (first || prev !== this.#mode || prevWhy !== this.#modeWhy) this.#d.log(`WorkBuddy App \u6388\u6743\u6865\u6A21\u5F0F\uFF1A${this.#mode}${this.#modeWhy ? `\uFF08${modeWhyText(this.#modeWhy)}\uFF09` : ""}`);
8095
8193
  }
8096
8194
  if (!this.#client) return;
8097
8195
  if (this.#mode === "sdk") await this.#sdkRound();
@@ -8111,6 +8209,10 @@ var WorkbuddyAppWatch = class {
8111
8209
  }
8112
8210
  #disconnect(reason) {
8113
8211
  this.#mode = "none";
8212
+ this.#modeWhy = "";
8213
+ this.#modeProbed = false;
8214
+ this.#connectedAt = 0;
8215
+ this.#nonSdkSince = 0;
8114
8216
  for (const c of this.#cards.values()) {
8115
8217
  this.#seen.delete(c.key);
8116
8218
  this.#noneRetry.delete(c.key);
@@ -8341,9 +8443,37 @@ function questionNative(q, reply) {
8341
8443
  }
8342
8444
  return { optionId: "allow", answers };
8343
8445
  }
8446
+ function modeWhyText(why) {
8447
+ if (!why) return "";
8448
+ const [k, ...rest] = why.split(":");
8449
+ const msg = rest.join(":");
8450
+ switch (k) {
8451
+ case "no_wb":
8452
+ return "\u9875\u9762\u91CC\u6CA1\u6709 WorkBuddy SDK\uFF08globalThis.wb\uFF09";
8453
+ case "no_conversations":
8454
+ return "\u9875\u9762\u91CC\u6CA1\u6709\u4F1A\u8BDD\u63A5\u53E3\uFF08wb.conversations\uFF09";
8455
+ case "no_list_get":
8456
+ case "no_sdk":
8457
+ return "\u4F1A\u8BDD\u63A5\u53E3\u7F3A\u65B9\u6CD5\uFF08list / get / create\uFF09";
8458
+ case "list_failed":
8459
+ return `\u4F1A\u8BDD\u5217\u8868\u8BFB\u53D6\u5931\u8D25${msg ? "\uFF1A" + msg : ""}\uFF08\u53EF\u80FD\u8FD8\u6CA1\u767B\u5F55\u6216\u8FD8\u5728\u52A0\u8F7D\uFF09`;
8460
+ case "get_failed":
8461
+ return `\u8BFB\u53D6\u4F1A\u8BDD\u5931\u8D25${msg ? "\uFF1A" + msg : ""}`;
8462
+ case "list_shape":
8463
+ return "\u4F1A\u8BDD\u5217\u8868\u683C\u5F0F\u4E0D\u8BA4\u8BC6";
8464
+ case "no_pending_api":
8465
+ return "\u4F1A\u8BDD\u5BF9\u8C61\u6CA1\u6709\u5F85\u786E\u8BA4\u63A5\u53E3";
8466
+ case "no_result":
8467
+ return "\u9875\u9762\u811A\u672C\u6CA1\u7ED3\u679C\uFF08\u8D85\u65F6 / \u51FA\u9519\uFF09";
8468
+ default:
8469
+ return why;
8470
+ }
8471
+ }
8344
8472
 
8345
8473
  // src/daemon/qoderApp/conn.ts
8346
8474
  var KEEPER_EVERY_MS4 = 15 * 1e3;
8475
+ var KEEP_OPEN_DOWN_ROUNDS = 2;
8476
+ var LAUNCH_CHECK_EVERY_MS = 2 * 1e3;
8347
8477
  var PROBE_EVERY_MS4 = 60 * 1e3;
8348
8478
  var CONNECT_BACKOFF_MIN_MS4 = 15 * 1e3;
8349
8479
  var CONNECT_BACKOFF_MAX_MS4 = 5 * 60 * 1e3;
@@ -8364,6 +8494,11 @@ var QoderAppConn = class {
8364
8494
  #failLogged = false;
8365
8495
  /** 第几条连接(每次连上 +1):选择题卡据此分辨「题没了」是电脑上答掉还是 App 退出重开过 */
8366
8496
  #gen = 0;
8497
+ /** 端口连续不在的轮数(常开用) */
8498
+ #down = 0;
8499
+ #keepOpenResult = "";
8500
+ #launchAt = 0;
8501
+ #launchRunning = false;
8367
8502
  constructor(deps2) {
8368
8503
  this.#d = deps2;
8369
8504
  this.#keeper = deps2.keeper ?? new QoderCdpKeeper({ brand: deps2.brand, port: deps2.port, log: deps2.log });
@@ -8464,11 +8599,19 @@ var QoderAppConn = class {
8464
8599
  return;
8465
8600
  }
8466
8601
  const now = this.#now();
8602
+ this.#checkLaunch(now);
8467
8603
  if (now - this.#keeperAt >= KEEPER_EVERY_MS4 || this.#keeperAt === 0) {
8468
8604
  this.#keeperAt = now;
8469
8605
  const prev = this.#portStatus;
8470
8606
  this.#portStatus = await this.#keeper.tick();
8471
8607
  if (this.#stopped) return this.#disconnect();
8608
+ if (this.#portStatus === "down") {
8609
+ this.#down++;
8610
+ if (this.#down >= KEEP_OPEN_DOWN_ROUNDS) await this.#keepOpen().catch((e) => this.#d.log(`${this.#d.brand.label} \u8C03\u8BD5\u7AEF\u53E3\u5E38\u5F00\u51FA\u9519\uFF1A${e?.message ?? e}`));
8611
+ if (this.#stopped) return this.#disconnect();
8612
+ } else {
8613
+ this.#down = 0;
8614
+ }
8472
8615
  if (this.#portStatus === "untrusted" && prev !== "untrusted") this.#d.log(`${this.#d.brand.label} \u8C03\u8BD5\u7AEF\u53E3 ${this.#d.port()} \u7684\u76D1\u542C\u8005\u4E0D\u662F\u672C\u673A\u56DE\u73AF\u4E0A\u7684 ${this.#d.brand.label}\uFF0C\u4E0D\u8FDE`);
8473
8616
  if (this.#portStatus !== "up" && this.#client) this.#disconnect();
8474
8617
  }
@@ -8526,6 +8669,50 @@ var QoderAppConn = class {
8526
8669
  }
8527
8670
  }
8528
8671
  }
8672
+ /** 启动瞬间补端口:不等它(退出 + 拉起要十几秒),keeper 自己防并发;做完催一下健康检查 */
8673
+ #checkLaunch(now) {
8674
+ if ((this.#d.platform ?? process.platform) !== "darwin") return;
8675
+ if (this.#launchRunning || !this.#keeper.fixFreshLaunch || now - this.#launchAt < LAUNCH_CHECK_EVERY_MS) return;
8676
+ try {
8677
+ if (!this.#d.launchPort?.()) return;
8678
+ } catch {
8679
+ return;
8680
+ }
8681
+ this.#launchAt = now;
8682
+ this.#launchRunning = true;
8683
+ void this.#keeper.fixFreshLaunch().then((r) => {
8684
+ if (r === "reopened") this.kick();
8685
+ }).catch((e) => this.#d.log(`${this.#d.brand.label} \u542F\u52A8\u8865\u7AEF\u53E3\u51FA\u9519\uFF1A${e?.message ?? e}`)).finally(() => {
8686
+ this.#launchRunning = false;
8687
+ });
8688
+ }
8689
+ /** 常开:App 开着、没有待答卡才交给 wakeForTask(用户在场 / 忙 / 节流由 keeper 判);结论变了记一行 */
8690
+ async #keepOpen() {
8691
+ if (!this.#d.keepOpen?.() || !this.#keeper.wakeForTask || !this.#keeper.appRunning) return;
8692
+ if (this.#launchRunning || this.#keeper.restarting === true) return;
8693
+ if (this.#d.hasCards?.() === true) return;
8694
+ if (!await this.#keeper.appRunning()) {
8695
+ this.#keepOpenResult = "";
8696
+ return;
8697
+ }
8698
+ const r = await this.#keeper.wakeForTask("keep_open");
8699
+ if (r === "restarted") {
8700
+ this.#down = 0;
8701
+ this.kick();
8702
+ }
8703
+ if (r === this.#keepOpenResult) return;
8704
+ this.#keepOpenResult = r;
8705
+ const why = {
8706
+ hung: "\u7AEF\u53E3\u5728\u76D1\u542C\u4F46\u65E0\u54CD\u5E94\uFF08App \u53EF\u80FD\u5361\u4F4F\u4E86\uFF09\uFF0C\u6309\u5361\u6B7B\u7B56\u7565\u5904\u7406",
8707
+ restarted: "\u5DF2\u9000\u51FA\u5E76\u5E26\u7AEF\u53E3\u91CD\u5F00",
8708
+ busy: "\u6709\u4F1A\u8BDD\u5728\u8DD1\uFF0C\u7B49\u7A7A\u95F2\u518D\u91CD\u5F00",
8709
+ present: "\u7528\u6237\u5728\u7535\u8111\u524D\uFF0C\u4E0D\u6253\u65AD",
8710
+ failed: "\u91CD\u5F00\u5931\u8D25\u6216\u4ECA\u5929\u7684\u91CD\u542F\u6B21\u6570\u5DF2\u7528\u5B8C",
8711
+ launched: "\u521A\u62C9\u8D77\u8FC7\uFF0C\u7B49\u5B83\u8D77\u6765",
8712
+ not_installed: "\u6CA1\u627E\u5230 App \u672C\u4F53"
8713
+ };
8714
+ if (why[r]) this.#d.log(`${this.#d.brand.label} \u8C03\u8BD5\u7AEF\u53E3 ${this.#d.port()} \u4E0D\u5728\uFF1A${why[r]}`);
8715
+ }
8529
8716
  };
8530
8717
 
8531
8718
  // src/daemon/qoderApp/hookQuestions.ts
@@ -8744,6 +8931,10 @@ var QoderAppQuestions = class {
8744
8931
  #noPortLogged = /* @__PURE__ */ new Set();
8745
8932
  #busy = false;
8746
8933
  #downSince = 0;
8934
+ /** 设备上挂着几张这一路的选择题卡(有就不重启 App) */
8935
+ get pending() {
8936
+ return this.#cards.size;
8937
+ }
8747
8938
  constructor(deps2) {
8748
8939
  this.#d = deps2;
8749
8940
  }
@@ -9077,6 +9268,8 @@ var QoderHookQuestionGate = class {
9077
9268
  #d;
9078
9269
  /** 正在挂起的(sessionId:toolUseId):同一道题只推一张 */
9079
9270
  #pending = /* @__PURE__ */ new Set();
9271
+ /** 挂起中的题属于哪个品牌(同 #pending 的键):keeper 判「有没有待答题」用 */
9272
+ #pendingTool = /* @__PURE__ */ new Map();
9080
9273
  /** 推过真卡后交回电脑的题(sessionId:toolUseId → 时刻):电脑上出题后不再推兜底提示卡 */
9081
9274
  #released = /* @__PURE__ */ new Map();
9082
9275
  constructor(deps2) {
@@ -9093,6 +9286,12 @@ var QoderHookQuestionGate = class {
9093
9286
  for (const k of this.#pending) if (k.startsWith(`${sessionId}:`)) return true;
9094
9287
  return false;
9095
9288
  }
9289
+ /** 这个品牌有几道挂起中的题(有就不重启 App) */
9290
+ pendingFor(tool) {
9291
+ let n = 0;
9292
+ for (const t of this.#pendingTool.values()) if (t === tool) n++;
9293
+ return n;
9294
+ }
9096
9295
  async handle(req, signal) {
9097
9296
  const none = { decision: "none" };
9098
9297
  const started = this.#d.now();
@@ -9115,6 +9314,7 @@ var QoderHookQuestionGate = class {
9115
9314
  const key = `${sid}:${toolUseId || this.#d.now()}`;
9116
9315
  if (this.#pending.has(key)) return none;
9117
9316
  this.#pending.add(key);
9317
+ this.#pendingTool.set(key, tool);
9118
9318
  const ac = new AbortController();
9119
9319
  const onAbort = () => ac.abort(typeof signal.reason === "string" && signal.reason ? signal.reason : REASON_PC_CANCELLED);
9120
9320
  if (signal.aborted) onAbort();
@@ -9171,6 +9371,7 @@ var QoderHookQuestionGate = class {
9171
9371
  } finally {
9172
9372
  signal.removeEventListener("abort", onAbort);
9173
9373
  this.#pending.delete(key);
9374
+ this.#pendingTool.delete(key);
9174
9375
  if (toolUseId) this.#released.set(key, this.#d.now());
9175
9376
  }
9176
9377
  }
@@ -9399,6 +9600,7 @@ import { realpathSync as realpathSync2 } from "fs";
9399
9600
  import { resolve as resolve4 } from "path";
9400
9601
  var WB_APP_WAKE_WAIT_MS = 60 * 1e3;
9401
9602
  var WB_APP_WAKE_WAIT_WIN_MS = 120 * 1e3;
9603
+ var WB_APP_STUCK_MS = 120 * 1e3;
9402
9604
  var WB_APP_CONFIRM_MS = 60 * 1e3;
9403
9605
  var WB_APP_CONFIRM_POLL_MS = 5 * 1e3;
9404
9606
  var WB_APP_LOCATE_WAIT_MS = 20 * 1e3;
@@ -9473,14 +9675,60 @@ var WbAppTasks = class {
9473
9675
  * 授权桥不可用时唤起 App 并等它连上(spec §3.9):先回报一次 queued,最多等 60 秒。
9474
9676
  * 返回 false = 没装 / 正忙不重启 / 拉不起来 / 超时,由调用方退回命令行(日志写明原因)
9475
9677
  */
9476
- /** App 真正就绪:授权桥 sdk 且 wb.conversations 能 list(渲染层 / 登录都好了) */
9477
- async #appReady() {
9478
- if (!this.available()) return false;
9678
+ #readiness() {
9679
+ try {
9680
+ return this.#d.readiness?.() ?? null;
9681
+ } catch {
9682
+ return null;
9683
+ }
9684
+ }
9685
+ /**
9686
+ * App 真正就绪:授权桥 sdk 且 wb.conversations 能 list(渲染层 / 登录都好了)。
9687
+ * 返回 '' = 就绪;否则是没就绪的原因(日志用,258:原先只回 bool,等 100 秒回落后分不出卡在哪一项)
9688
+ */
9689
+ async #notReady() {
9690
+ if (!this.available()) return this.#bridgeWhy();
9479
9691
  const r = await this.#d.eval(APP_READY);
9480
- return r?.ok === true;
9692
+ if (r?.ok === true) return "";
9693
+ if (!r) return "\u4F1A\u8BDD\u63A5\u53E3\u4E0D\u53EF\u7528\uFF08\u9875\u9762\u811A\u672C\u6CA1\u7ED3\u679C\uFF1A\u8D85\u65F6 / \u51FA\u9519\uFF09";
9694
+ return `\u4F1A\u8BDD\u63A5\u53E3\u4E0D\u53EF\u7528\uFF08${modeWhyText(typeof r.why === "string" ? r.why : "") || "\u539F\u56E0\u672A\u77E5"}\uFF09`;
9695
+ }
9696
+ async #appReady() {
9697
+ return await this.#notReady() === "";
9698
+ }
9699
+ /** 授权桥不可用的具体原因 */
9700
+ #bridgeWhy() {
9701
+ const r = this.#readiness();
9702
+ if (!r) return "\u6388\u6743\u6865\u4E0D\u53EF\u7528";
9703
+ const port = this.#d.port?.() ?? 39217;
9704
+ if (r.port === null) return `\u8C03\u8BD5\u7AEF\u53E3 ${port} \u8FD8\u6CA1\u68C0\u67E5\u8FC7`;
9705
+ if (r.port === "down") return `\u8C03\u8BD5\u7AEF\u53E3 ${port} \u6CA1\u76D1\u542C\uFF08${r.downWhy === "timeout" ? "\u8D85\u65F6\uFF0C\u4E14\u76D1\u542C\u8005\u4E0D\u662F\u672C\u673A\u56DE\u73AF\u4E0A\u7684 WorkBuddy" : "App \u6CA1\u5F00\uFF0C\u6216\u5F00\u7740\u4F46\u6CA1\u5E26\u8C03\u8BD5\u7AEF\u53E3"}\uFF09`;
9706
+ if (r.port === "hung") return `\u8C03\u8BD5\u7AEF\u53E3 ${port} \u5728\u76D1\u542C\u4F46\u65E0\u54CD\u5E94\uFF08App \u53EF\u80FD\u5361\u4F4F\u4E86\uFF09`;
9707
+ if (r.port === "untrusted") return `\u8C03\u8BD5\u7AEF\u53E3 ${port} \u7684\u76D1\u542C\u8005\u4E0D\u662F\u672C\u673A\u56DE\u73AF\u4E0A\u7684 WorkBuddy`;
9708
+ if (!r.connected) return `\u8C03\u8BD5\u7AEF\u53E3 ${port} \u5728\u542C\u4F46 CDP \u8FDE\u4E0D\u4E0A${r.connectError ? `\uFF08${r.connectError}\uFF09` : ""}`;
9709
+ if (!r.probed) return "\u6388\u6743\u6865\u521A\u8FDE\u4E0A\uFF0C\u6A21\u5F0F\u8FD8\u6CA1\u63A2\u51FA\u6765";
9710
+ if (r.mode !== "sdk") return `\u6388\u6743\u6865\u6A21\u5F0F\u662F ${r.mode}\uFF0C\u4E0D\u662F sdk${r.modeWhy ? `\uFF08${modeWhyText(r.modeWhy)}\uFF09` : ""}`;
9711
+ return "\u6388\u6743\u6865\u4E0D\u53EF\u7528";
9712
+ }
9713
+ /**
9714
+ * 模式变成非 sdk 已超过冷启动上限:再等也不会好(App 早就开着,不是刚启动),新建任务不白等(258)。
9715
+ * 计时起点是「模式最近一次变成非 sdk 的时刻」(长期 sdk 时偶尔掉一下不算);页面脚本没结果(no_result,多半是一次超时)
9716
+ * 不算:催一次重探,照常等。返回原因;不是这种情况返回 ''
9717
+ */
9718
+ #stuckWhy(max) {
9719
+ const r = this.#readiness();
9720
+ if (!r || !r.connected || !r.probed || r.mode === "sdk" || !r.nonSdkSince) return "";
9721
+ if (r.modeWhy === "no_result") {
9722
+ this.#d.kick?.();
9723
+ return "";
9724
+ }
9725
+ const secs = Math.round((this.#d.now() - r.nonSdkSince) / 1e3);
9726
+ if (this.#d.now() - r.nonSdkSince < max) return "";
9727
+ return `\u6388\u6743\u6865\u6A21\u5F0F\u662F ${r.mode} \u5DF2 ${secs} \u79D2${r.modeWhy ? `\uFF08${modeWhyText(r.modeWhy)}\uFF09` : ""}\uFF0CApp \u4E0D\u662F\u521A\u6253\u5F00\uFF0C\u518D\u7B49\u4E5F\u4E0D\u4F1A\u597D`;
9481
9728
  }
9482
9729
  async #ensureReady(cmd, sid, q, report, mayWake) {
9483
- if (await this.#appReady()) return true;
9730
+ let why = await this.#notReady();
9731
+ if (!why) return true;
9484
9732
  const max = (this.#d.platform ?? process.platform) === "win32" ? WB_APP_WAKE_WAIT_WIN_MS : WB_APP_WAKE_WAIT_MS;
9485
9733
  const budget = appWaitBudget(cmd.expireAt, this.#d.now(), max);
9486
9734
  if (budget <= 0 && !sid) {
@@ -9488,6 +9736,11 @@ var WbAppTasks = class {
9488
9736
  return false;
9489
9737
  }
9490
9738
  const deadline = this.#d.now() + Math.max(0, budget);
9739
+ const stuck = sid ? "" : this.#stuckWhy(WB_APP_STUCK_MS);
9740
+ if (stuck) {
9741
+ this.#d.log(`\u6307\u4EE4 ${cmd.instructionId} WorkBuddy App \u7528\u4E0D\u4E86\uFF1A${stuck}\uFF0C\u8D70\u547D\u4EE4\u884C`);
9742
+ return false;
9743
+ }
9491
9744
  const connected = this.available();
9492
9745
  if (!connected) {
9493
9746
  if (!mayWake) {
@@ -9519,15 +9772,18 @@ var WbAppTasks = class {
9519
9772
  }
9520
9773
  if (!q.queued) {
9521
9774
  q.queued = true;
9522
- this.#d.log(`\u6307\u4EE4 ${cmd.instructionId} \u7B49 WorkBuddy App ${w === "launched" ? "\u6253\u5F00" : w === "restarted" ? "\u5E26\u7AEF\u53E3\u91CD\u542F" : "\u5C31\u7EEA"}`);
9775
+ this.#d.log(`\u6307\u4EE4 ${cmd.instructionId} \u7B49 WorkBuddy App ${w === "launched" ? "\u6253\u5F00" : w === "restarted" ? "\u5E26\u7AEF\u53E3\u91CD\u542F" : "\u5C31\u7EEA"}\uFF08\u73B0\u5728\uFF1A${why}\uFF09`);
9523
9776
  report("queued", sid);
9524
9777
  }
9525
9778
  while (this.#d.now() < deadline) {
9526
9779
  this.#d.kick?.();
9527
9780
  await this.#sleep(Math.max(1, Math.min(this.#d.pollMs ?? WB_APP_POLL_MS, deadline - this.#d.now())));
9528
- if (await this.#appReady()) return true;
9781
+ const now = await this.#notReady();
9782
+ if (!now) return true;
9783
+ if (now !== why) this.#d.log(`\u6307\u4EE4 ${cmd.instructionId} \u7B49 WorkBuddy App\uFF1A${now}`);
9784
+ why = now;
9529
9785
  }
9530
- this.#d.log(`\u6307\u4EE4 ${cmd.instructionId} WorkBuddy App ${Math.round(Math.max(0, budget) / 1e3)} \u79D2\u5185\u6CA1\u5C31\u7EEA\uFF08\u8C03\u8BD5\u7AEF\u53E3\u6CA1\u8D77\u6765\u3001\u6388\u6743\u6865\u6A21\u5F0F\u4E0D\u662F sdk\uFF0C\u6216\u4F1A\u8BDD\u63A5\u53E3\u8FD8\u4E0D\u80FD\u7528${budget < max ? "\uFF1B\u518D\u7B49\u6307\u4EE4\u5C31\u8FC7\u671F\u4E86" : ""}\uFF09\uFF0C\u8D70\u547D\u4EE4\u884C`);
9786
+ this.#d.log(`\u6307\u4EE4 ${cmd.instructionId} WorkBuddy App ${Math.round(Math.max(0, budget) / 1e3)} \u79D2\u5185\u6CA1\u5C31\u7EEA\uFF08\u6700\u540E\uFF1A${why}${budget < max ? "\uFF1B\u518D\u7B49\u6307\u4EE4\u5C31\u8FC7\u671F\u4E86" : ""}\uFF09\uFF0C\u8D70\u547D\u4EE4\u884C`);
9531
9787
  return false;
9532
9788
  }
9533
9789
  /** 只给测试和诊断用 */
@@ -14964,7 +15220,9 @@ var Daemon = class {
14964
15220
  canWakeNow: () => this.#wbApp.canWakeNow(),
14965
15221
  wakeReady: (ms) => this.#wbApp.wakeReady(ms),
14966
15222
  wake: () => this.#wbApp.wakeApp(),
14967
- kick: () => this.#wbApp.kick()
15223
+ kick: () => this.#wbApp.kick(),
15224
+ readiness: () => this.#wbApp.readiness(),
15225
+ port: () => loadConfig().workbuddyCdpPort ?? DEFAULT_CDP_PORT
14968
15226
  });
14969
15227
  for (const tool of ["qoder_cn", "qoder"]) {
14970
15228
  const injected = deps2.qoderAppTasks?.[tool];
@@ -14973,6 +15231,9 @@ var Daemon = class {
14973
15231
  continue;
14974
15232
  }
14975
15233
  const brand = QODER_BRANDS[tool];
15234
+ const port = () => (tool === "qoder_cn" ? loadConfig().qoderCnCdpPort : loadConfig().qoderCdpPort) ?? brand.defaultPort;
15235
+ const hasCards = () => (this.#qoderQuestions.get(tool)?.pending ?? 0) > 0 || this.#qoderHookQ.pendingFor(tool) > 0;
15236
+ const launchPortOn = () => loadConfig().qoderLaunchPort !== false;
14976
15237
  const conn = new QoderAppConn({
14977
15238
  brand,
14978
15239
  log,
@@ -14980,7 +15241,16 @@ var Daemon = class {
14980
15241
  const c = loadConfig();
14981
15242
  return c.enabledTools.includes(tool) && c.qoderAppTask !== false;
14982
15243
  },
14983
- port: () => (tool === "qoder_cn" ? loadConfig().qoderCnCdpPort : loadConfig().qoderCdpPort) ?? brand.defaultPort
15244
+ port,
15245
+ keeper: new QoderCdpKeeper({
15246
+ brand,
15247
+ port,
15248
+ log,
15249
+ busy: () => toolBusy(tool) || hasCards()
15250
+ }),
15251
+ keepOpen: launchPortOn,
15252
+ launchPort: launchPortOn,
15253
+ hasCards
14984
15254
  });
14985
15255
  this.#qoderConns.push(conn);
14986
15256
  this.#qoderQuestions.set(
@@ -16722,7 +16992,7 @@ var Daemon = class {
16722
16992
  try {
16723
16993
  r = await this.#deps.sendToWindow(windowId, req, (reply) => {
16724
16994
  if (reply.status === "not_here") {
16725
- log(`\u6307\u4EE4 ${cmd.instructionId} \u7A97\u53E3\u91CC\u5DF2\u4E0D\u662F\u8FD9\u4E2A\u4F1A\u8BDD\uFF0C\u6539\u8D70\u540E\u53F0`);
16995
+ log(`\u6307\u4EE4 ${cmd.instructionId} ${reply.reason === "window_unrecognized" ? "\u7A97\u53E3\u754C\u9762\u8BA4\u4E0D\u51FA\uFF0C\u6CA1\u80FD\u5199\u5165" : "\u7A97\u53E3\u91CC\u5DF2\u4E0D\u662F\u8FD9\u4E2A\u4F1A\u8BDD"}\uFF0C\u6539\u8D70\u540E\u53F0`);
16726
16996
  fallback();
16727
16997
  return;
16728
16998
  }
@@ -16748,7 +17018,7 @@ var Daemon = class {
16748
17018
  if (cmd.sessionId) {
16749
17019
  const sid = resolveAlias(cmd.sessionId);
16750
17020
  const st = readState(sid);
16751
- if (st && st.cwd && (st.interactive === true || st.embedded === true) && st.remoteWindow !== true && !!st.pid && isAlive(st.pid, st.pidName)) {
17021
+ if (st && st.cwd && (st.interactive === true || st.embedded === true) && (st.remoteWindow !== true || st.tool === "workbuddy") && !!st.pid && isAlive(st.pid, st.pidName)) {
16752
17022
  void this.#track(this.#idleReply(cmd, sid, st.cwd));
16753
17023
  return;
16754
17024
  }
@@ -18074,6 +18344,7 @@ import { StringDecoder as StringDecoder2 } from "string_decoder";
18074
18344
 
18075
18345
  // src/wrap/injector.ts
18076
18346
  var SEEN_MAX = 200;
18347
+ var WINDOW_UNRECOGNIZED = "window_unrecognized";
18077
18348
  var QUIET_MS = 1500;
18078
18349
  var STARTUP_QUIET_MS = 3e3;
18079
18350
  var KEY_QUIET_MS = 3e3;
@@ -18094,6 +18365,8 @@ var Injector = class {
18094
18365
  #injectedOnce = false;
18095
18366
  /** 出现过信任框:之后窗口关闭多半是用户选了 No / 不想在这个目录里跑,不能交回后台 */
18096
18367
  #sawTrust = false;
18368
+ /** hook 空闲但屏幕认不出,从什么时候开始(0 = 没有) */
18369
+ #unknownSince = 0;
18097
18370
  /** instructionId → 那一条(去重:daemon 等 ack 超时重送时,不能写进窗口两遍) */
18098
18371
  #seen = /* @__PURE__ */ new Map();
18099
18372
  constructor(deps2) {
@@ -18251,6 +18524,7 @@ var Injector = class {
18251
18524
  this.#d.log?.(`\u7A7A\u95F2\u5224\u65AD\u51FA\u9519\uFF1A${e.message}`);
18252
18525
  }
18253
18526
  if (why.length > 0) {
18527
+ if (this.#giveUpUnrecognized(job)) return;
18254
18528
  const key = why.join(",");
18255
18529
  if (key !== this.#lastWhy) {
18256
18530
  this.#lastWhy = key;
@@ -18261,6 +18535,7 @@ var Injector = class {
18261
18535
  return;
18262
18536
  }
18263
18537
  this.#lastWhy = "";
18538
+ this.#unknownSince = 0;
18264
18539
  this.#busy = true;
18265
18540
  try {
18266
18541
  await this.#inject(job);
@@ -18271,6 +18546,25 @@ var Injector = class {
18271
18546
  this.#busy = false;
18272
18547
  }
18273
18548
  }
18549
+ /** 读屏兜底:hook 空闲而屏幕连续认不出(无输入框 / 弹层)够久 → 回 not_here 交回 daemon(还没碰窗口,交回不会重复执行) */
18550
+ #giveUpUnrecognized(job) {
18551
+ const limit = this.#d.unrecognizedGiveUpMs;
18552
+ if (!limit) return false;
18553
+ const v = this.#d.view();
18554
+ const last = this.#d.lastEvent();
18555
+ const unknown = !!v && !v.permission && !v.trust && (v.input === null || v.dialog === "popup") && !!last && IDLE_EVENTS.has(last.ev);
18556
+ const now = this.#d.now();
18557
+ if (!unknown || now - this.#lastKey < KEY_QUIET_MS) {
18558
+ this.#unknownSince = 0;
18559
+ return false;
18560
+ }
18561
+ if (!this.#unknownSince) this.#unknownSince = now;
18562
+ if (now - this.#unknownSince < limit) return false;
18563
+ this.#unknownSince = 0;
18564
+ this.#d.log?.(`\u7A97\u53E3\u754C\u9762\u8BA4\u4E0D\u51FA\uFF08${v.input === null ? "\u627E\u4E0D\u5230\u8F93\u5165\u6846" : "\u6709\u5F39\u5C42"}\uFF09${Math.round(limit / 1e3)} \u79D2\uFF0C\u6CA1\u80FD\u5199\u5165 ${job.instructionId}\uFF0C\u4EA4\u56DE daemon`);
18565
+ this.#final(job, { status: "not_here", reason: WINDOW_UNRECOGNIZED });
18566
+ return true;
18567
+ }
18274
18568
  async #inject(job) {
18275
18569
  const body = prepare(job.text);
18276
18570
  this.#injectedOnce = true;
@@ -19144,6 +19438,8 @@ var LoginWatcher = class {
19144
19438
  };
19145
19439
 
19146
19440
  // src/wrap/answerWindow.ts
19441
+ var DEFAULT_TTL_MS = 10 * 60 * 1e3;
19442
+ var UNRECOGNIZED_GIVE_UP_MS = 20 * 1e3;
19147
19443
  var TRUST_TICK_MS = 300;
19148
19444
  function trustContext(tool, forms, cwd) {
19149
19445
  const repo = findRepoScope(tool, cwd);
@@ -19193,9 +19489,13 @@ async function runAnswerWindow(tool, real2, args, env, windowId, d) {
19193
19489
  const cols = stdout6.columns || 80;
19194
19490
  const rows = stdout6.rows || 24;
19195
19491
  let term = new Headless({ cols, rows, allowProposedApi: true });
19492
+ const takeover = d.takeover ?? (tool === "workbuddy" && injectReady(tool, d.platform));
19493
+ const childEnv2 = { ...env };
19494
+ for (const k of Object.keys(childEnv2)) if (k.toUpperCase() === "DESKBUDDY_WINDOW") delete childEnv2[k];
19495
+ if (takeover) childEnv2.DESKBUDDY_WINDOW = windowId;
19196
19496
  let p;
19197
19497
  try {
19198
- p = pty.spawn(real2.file, [...real2.prefixArgs, ...args], { name: env.TERM || "xterm-256color", cols, rows, cwd: d.cwd, env });
19498
+ p = pty.spawn(real2.file, [...real2.prefixArgs, ...args], { name: env.TERM || "xterm-256color", cols, rows, cwd: d.cwd, env: childEnv2 });
19199
19499
  } catch (e) {
19200
19500
  d.log(`pty \u8D77\u4E0D\u6765\uFF0C\u76F4\u63A5\u8FD0\u884C\uFF1A${e.message}`);
19201
19501
  return null;
@@ -19207,6 +19507,32 @@ async function runAnswerWindow(tool, real2, args, env, windowId, d) {
19207
19507
  } catch {
19208
19508
  }
19209
19509
  };
19510
+ let held = null;
19511
+ let holdDepth = 0;
19512
+ const holdKeys = (on) => {
19513
+ if (on) {
19514
+ holdDepth++;
19515
+ held = held ?? [];
19516
+ return;
19517
+ }
19518
+ holdDepth = Math.max(0, holdDepth - 1);
19519
+ if (holdDepth > 0) return;
19520
+ const h = held ?? [];
19521
+ held = null;
19522
+ for (const x of h) toPty(x);
19523
+ };
19524
+ const injector = takeover ? new Injector({
19525
+ now: () => Date.now(),
19526
+ view: () => term && !finished ? screenRuleFor(tool)(screenLines(term), screenLinesUndimmed(term)) : null,
19527
+ lastEvent: () => (d.lastEvent ?? readLastEvent)(windowId),
19528
+ write: toPty,
19529
+ sleep: (ms) => new Promise((r) => setTimeout(r, ms)),
19530
+ holdKeys,
19531
+ log: (m) => d.log(`[${windowId.slice(0, 8)}] ${m}`),
19532
+ rejectText: textRejectFor(tool),
19533
+ // 读屏规则只按 2.164.0 采样:hook 说空闲、屏幕却一直认不出输入框 / 有弹层 → 不等到过期,交回 daemon(后台续跑)
19534
+ unrecognizedGiveUpMs: UNRECOGNIZED_GIVE_UP_MS
19535
+ }) : null;
19210
19536
  p.onData((data) => {
19211
19537
  try {
19212
19538
  stdout6.write(data);
@@ -19219,10 +19545,14 @@ async function runAnswerWindow(tool, real2, args, env, windowId, d) {
19219
19545
  term = null;
19220
19546
  }
19221
19547
  }
19548
+ injector?.noteOutput();
19222
19549
  });
19223
19550
  const onStdin = (b) => {
19224
19551
  const s2 = typeof b === "string" ? b : decoder.write(b);
19225
- if (s2) toPty(s2);
19552
+ injector?.noteKey(s2);
19553
+ if (!s2) return;
19554
+ if (held) held.push(s2);
19555
+ else toPty(s2);
19226
19556
  };
19227
19557
  const onResize = () => {
19228
19558
  const c = stdout6.columns;
@@ -19272,6 +19602,8 @@ async function runAnswerWindow(tool, real2, args, env, windowId, d) {
19272
19602
  login?.tick(lines3);
19273
19603
  } catch {
19274
19604
  }
19605
+ injector?.tick().catch(() => {
19606
+ });
19275
19607
  }, TRUST_TICK_MS);
19276
19608
  trustTimer.unref?.();
19277
19609
  try {
@@ -19295,14 +19627,21 @@ async function runAnswerWindow(tool, real2, args, env, windowId, d) {
19295
19627
  void startChannelServer(
19296
19628
  sock,
19297
19629
  secret,
19298
- // 设备指令(文本)不在这里写:让 daemon 走原来的路。
19630
+ // 设备指令(文本):WorkBuddy 窗口接管时交给 Injector 空闲时写进输入框(问题表 258);不接管的(Codex)回 not_here,让 daemon 走原来的路。
19299
19631
  // 🔴 登录页此刻还在屏上(命令行没登录):回 rejected/agent_not_logged_in,不回 not_here —— not_here 会让 daemon 改走后台真跑一遍。
19300
- // 登录完离开这一页就恢复原样(not_here,交给 daemon 走正常路径)
19301
19632
  // 先回 ack 再回终态:sendToWindow 只把先回 ack 的当成「收下了」,直接回 rejected 会被 daemon 当成没收下(同样降级)
19302
- (req, reply) => {
19303
- if (!login?.onScreen) return reply({ instructionId: req.instructionId, status: "not_here" });
19304
- reply({ instructionId: req.instructionId, status: "ack" });
19305
- reply({ instructionId: req.instructionId, status: "rejected", mode: "window", reason: "agent_not_logged_in" });
19633
+ (req, reply, alive) => {
19634
+ if (login?.onScreen) {
19635
+ reply({ instructionId: req.instructionId, status: "ack" });
19636
+ reply({ instructionId: req.instructionId, status: "rejected", mode: "window", reason: "agent_not_logged_in" });
19637
+ return;
19638
+ }
19639
+ if (!injector) return reply({ instructionId: req.instructionId, status: "not_here" });
19640
+ try {
19641
+ injector.submit({ ...req, expireAt: req.expireAt || Date.now() + DEFAULT_TTL_MS, reply, alive });
19642
+ } catch (e) {
19643
+ d.log(`[${windowId.slice(0, 8)}] \u6536\u6307\u4EE4\u51FA\u9519\uFF1A${e.message}`);
19644
+ }
19306
19645
  },
19307
19646
  d.platform,
19308
19647
  (req, reply) => {
@@ -19355,6 +19694,11 @@ async function runAnswerWindow(tool, real2, args, env, windowId, d) {
19355
19694
  p.onExit(({ exitCode, signal }) => {
19356
19695
  finished = true;
19357
19696
  clearInterval(trustTimer);
19697
+ const pending = injector?.pending ?? 0;
19698
+ try {
19699
+ injector?.drain();
19700
+ } catch {
19701
+ }
19358
19702
  for (const [s2, h] of sigs) process.removeListener(s2, h);
19359
19703
  process.removeListener("SIGINT", ignoreInt);
19360
19704
  closeChannelServer(server, sock, d.platform);
@@ -19369,7 +19713,9 @@ async function runAnswerWindow(tool, real2, args, env, windowId, d) {
19369
19713
  stdout6.off("resize", onResize);
19370
19714
  } catch {
19371
19715
  }
19372
- resolve8(signal ? 128 + signal : exitCode);
19716
+ const code = signal ? 128 + signal : exitCode;
19717
+ if (pending > 0) setTimeout(() => resolve8(code), 150);
19718
+ else resolve8(code);
19373
19719
  });
19374
19720
  });
19375
19721
  }
@@ -19402,7 +19748,7 @@ async function launchRegister(tool, sessionId, cwd, firstLine, final, d = defaul
19402
19748
  }
19403
19749
 
19404
19750
  // src/wrap/run.ts
19405
- var DEFAULT_TTL_MS = 10 * 60 * 1e3;
19751
+ var DEFAULT_TTL_MS2 = 10 * 60 * 1e3;
19406
19752
  var TICK_MS = 300;
19407
19753
  var MAX_LOG_BYTES3 = 512 * 1024;
19408
19754
  var WRAP_HINT = "DeskBuddy \u5305\u88C5\u51FA\u9519\uFF0C\u8BF7\u8BBE DESKBUDDY_WRAP_OFF=1 \u4E34\u65F6\u7ED5\u5F00\uFF08\u4F8B\u5982 DESKBUDDY_WRAP_OFF=1 claude\uFF09";
@@ -19856,7 +20202,7 @@ function runAttached(tool, p, term0, windowId, deps2, launched = false, takeover
19856
20202
  secret,
19857
20203
  (req, reply, alive) => {
19858
20204
  try {
19859
- injector.submit({ ...req, expireAt: req.expireAt || Date.now() + DEFAULT_TTL_MS, reply, alive });
20205
+ injector.submit({ ...req, expireAt: req.expireAt || Date.now() + DEFAULT_TTL_MS2, reply, alive });
19860
20206
  } catch (e) {
19861
20207
  deps2.log(`\u6536\u6307\u4EE4\u51FA\u9519\uFF1A${e.message}`);
19862
20208
  }
@@ -5,7 +5,7 @@ import {
5
5
  installTools,
6
6
  parseTools,
7
7
  runEnable
8
- } from "./chunk-RHIQGM66.js";
8
+ } from "./chunk-VSS27ECH.js";
9
9
  export {
10
10
  defaultShimEnvForInstall,
11
11
  installShims,
@@ -290,3 +290,21 @@
290
290
  2026-10-12T02:42:26.632Z [opencode] opencode 版本 读不出:带说明的拒绝不走新路由
291
291
  2026-10-12T02:42:26.632Z [opencode] opencode 版本不确定支持带说明的拒绝,设备上的反馈没有带给模型
292
292
  2026-10-12T02:42:29.332Z [opencode] 后台任务里的提问 que_r2 没法在设备上回答(后台任务不能提问,请在电脑上继续),已拒绝
293
+ 2026-10-12T02:54:55.922Z [opencode] 回填选择题失败:/question/{requestID}/reply status=404
294
+ 2026-10-12T02:55:00.859Z [opencode] opencode 版本 1.4.3:带说明的拒绝不走新路由
295
+ 2026-10-12T02:55:00.859Z [opencode] opencode 版本不确定支持带说明的拒绝,设备上的反馈没有带给模型
296
+ 2026-10-12T02:55:00.962Z [opencode] opencode 版本 读不出:带说明的拒绝不走新路由
297
+ 2026-10-12T02:55:00.962Z [opencode] opencode 版本不确定支持带说明的拒绝,设备上的反馈没有带给模型
298
+ 2026-10-12T02:55:03.611Z [opencode] 后台任务里的提问 que_r2 没法在设备上回答(后台任务不能提问,请在电脑上继续),已拒绝
299
+ 2026-10-12T04:16:45.204Z [opencode] 回填选择题失败:/question/{requestID}/reply status=404
300
+ 2026-10-12T04:16:50.040Z [opencode] opencode 版本 1.4.3:带说明的拒绝不走新路由
301
+ 2026-10-12T04:16:50.040Z [opencode] opencode 版本不确定支持带说明的拒绝,设备上的反馈没有带给模型
302
+ 2026-10-12T04:16:50.148Z [opencode] opencode 版本 读不出:带说明的拒绝不走新路由
303
+ 2026-10-12T04:16:50.148Z [opencode] opencode 版本不确定支持带说明的拒绝,设备上的反馈没有带给模型
304
+ 2026-10-12T04:16:52.980Z [opencode] 后台任务里的提问 que_r2 没法在设备上回答(后台任务不能提问,请在电脑上继续),已拒绝
305
+ 2026-10-12T06:14:11.425Z [opencode] 回填选择题失败:/question/{requestID}/reply status=404
306
+ 2026-10-12T06:14:16.418Z [opencode] opencode 版本 1.4.3:带说明的拒绝不走新路由
307
+ 2026-10-12T06:14:16.419Z [opencode] opencode 版本不确定支持带说明的拒绝,设备上的反馈没有带给模型
308
+ 2026-10-12T06:14:16.558Z [opencode] opencode 版本 读不出:带说明的拒绝不走新路由
309
+ 2026-10-12T06:14:16.558Z [opencode] opencode 版本不确定支持带说明的拒绝,设备上的反馈没有带给模型
310
+ 2026-10-12T06:14:19.410Z [opencode] 后台任务里的提问 que_r2 没法在设备上回答(后台任务不能提问,请在电脑上继续),已拒绝
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "deskbuddy",
3
- "version": "0.2.0-beta.33",
3
+ "version": "0.2.0-beta.34",
4
4
  "description": "把 Claude Code、Codex 等 AI 编码智能体每个会话窗口的状态,实时同步到桌面硬件屏幕",
5
5
  "keywords": [
6
6
  "ai",