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 (/[
|
|
2574
|
-
|
|
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)
|
|
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-
|
|
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-
|
|
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-
|
|
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
|
-
|
|
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
|
-
|
|
7707
|
-
|
|
7708
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 (!
|
|
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
|
|
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)
|
|
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
|
-
|
|
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
|
-
|
|
9477
|
-
|
|
9478
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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\
|
|
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
|
|
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
|
-
|
|
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
|
-
//
|
|
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 (
|
|
19304
|
-
|
|
19305
|
-
|
|
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
|
-
|
|
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
|
|
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() +
|
|
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
|
}
|
package/hook/hook-debug.log
CHANGED
|
@@ -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 没法在设备上回答(后台任务不能提问,请在电脑上继续),已拒绝
|