deskbuddy 0.2.0-beta.11 → 0.2.0-beta.14
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.js +2511 -430
- package/dist/gate.cjs +21 -13
- package/dist/hook.cjs +376 -18
- package/dist/ocTranslate.mjs +106 -0
- package/dist/opencode.mjs +104 -59
- package/hook/gate.cjs +21 -13
- package/hook/hook-debug.log +5 -0
- package/hook/hook.cjs +376 -18
- package/hook/ocTranslate.d.mts +7 -0
- package/hook/ocTranslate.mjs +106 -0
- package/hook/opencode.d.mts +5 -1
- package/hook/opencode.mjs +104 -59
- package/package.json +3 -2
- package/vendor/node-pty/LICENSE +69 -0
- package/vendor/node-pty/lib/conpty_console_list_agent.js +16 -0
- package/vendor/node-pty/lib/eventEmitter2.js +47 -0
- package/vendor/node-pty/lib/index.js +52 -0
- package/vendor/node-pty/lib/interfaces.js +7 -0
- package/vendor/node-pty/lib/shared/conout.js +11 -0
- package/vendor/node-pty/lib/terminal.js +190 -0
- package/vendor/node-pty/lib/types.js +7 -0
- package/vendor/node-pty/lib/unixTerminal.js +346 -0
- package/vendor/node-pty/lib/utils.js +39 -0
- package/vendor/node-pty/lib/windowsConoutConnection.js +125 -0
- package/vendor/node-pty/lib/windowsPtyAgent.js +320 -0
- package/vendor/node-pty/lib/windowsTerminal.js +199 -0
- package/vendor/node-pty/lib/worker/conoutSocketWorker.js +22 -0
- package/vendor/node-pty/package.json +7 -0
- package/vendor/node-pty/prebuilds/darwin-arm64/pty.node +0 -0
- package/vendor/node-pty/prebuilds/darwin-arm64/spawn-helper +0 -0
- package/vendor/node-pty/prebuilds/darwin-x64/pty.node +0 -0
- package/vendor/node-pty/prebuilds/darwin-x64/spawn-helper +0 -0
- package/vendor/node-pty/prebuilds/win32-arm64/conpty/OpenConsole.exe +0 -0
- package/vendor/node-pty/prebuilds/win32-arm64/conpty/conpty.dll +0 -0
- package/vendor/node-pty/prebuilds/win32-arm64/conpty.node +0 -0
- package/vendor/node-pty/prebuilds/win32-arm64/conpty_console_list.node +0 -0
- package/vendor/node-pty/prebuilds/win32-arm64/pty.node +0 -0
- package/vendor/node-pty/prebuilds/win32-arm64/winpty-agent.exe +0 -0
- package/vendor/node-pty/prebuilds/win32-arm64/winpty.dll +0 -0
- package/vendor/node-pty/prebuilds/win32-x64/conpty/OpenConsole.exe +0 -0
- package/vendor/node-pty/prebuilds/win32-x64/conpty/conpty.dll +0 -0
- package/vendor/node-pty/prebuilds/win32-x64/conpty.node +0 -0
- package/vendor/node-pty/prebuilds/win32-x64/conpty_console_list.node +0 -0
- package/vendor/node-pty/prebuilds/win32-x64/pty.node +0 -0
- package/vendor/node-pty/prebuilds/win32-x64/winpty-agent.exe +0 -0
- package/vendor/node-pty/prebuilds/win32-x64/winpty.dll +0 -0
package/dist/cli.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
// src/commands/daemon.ts
|
|
4
|
-
import { writeFileSync as
|
|
4
|
+
import { writeFileSync as writeFileSync18, rmSync as rmSync15 } from "fs";
|
|
5
5
|
|
|
6
6
|
// src/core/api.ts
|
|
7
7
|
import { execFileSync } from "child_process";
|
|
@@ -61,6 +61,15 @@ function workbuddySettingsPath() {
|
|
|
61
61
|
function qoderCnSettingsPath() {
|
|
62
62
|
return join(process.env.DESKBUDDY_QODER_CN_DIR || join(homedir(), ".qoder-cn"), "settings.json");
|
|
63
63
|
}
|
|
64
|
+
function qwenworkHome() {
|
|
65
|
+
return process.env.DESKBUDDY_QWENWORK_DIR || join(homedir(), ".QwenWorkCN");
|
|
66
|
+
}
|
|
67
|
+
function qwenworkSettingsPath() {
|
|
68
|
+
return join(qwenworkHome(), "settings.json");
|
|
69
|
+
}
|
|
70
|
+
function qianfanHome() {
|
|
71
|
+
return process.env.DESKBUDDY_QIANFAN_DIR || join(homedir(), ".qianfan");
|
|
72
|
+
}
|
|
64
73
|
function qoderSettingsPath() {
|
|
65
74
|
return join(process.env.DESKBUDDY_QODER_DIR || join(homedir(), ".qoder"), "settings.json");
|
|
66
75
|
}
|
|
@@ -169,7 +178,7 @@ function atomicCopy(from, to) {
|
|
|
169
178
|
function syncHookFile(opts = {}) {
|
|
170
179
|
const target = opts.target ?? installedHookPath();
|
|
171
180
|
const version = opts.version ?? packageVersion();
|
|
172
|
-
const SIBLINGS = ["gate.cjs", "opencode.mjs", "wake.cjs", "launcher.cjs"];
|
|
181
|
+
const SIBLINGS = ["gate.cjs", "ocTranslate.mjs", "opencode.mjs", "wake.cjs", "launcher.cjs"];
|
|
173
182
|
const siblingMissing = () => SIBLINGS.some((name) => {
|
|
174
183
|
if (existsSync2(join2(dirname(target), name))) return false;
|
|
175
184
|
try {
|
|
@@ -1007,25 +1016,57 @@ import { dirname as dirname5, join as join6, posix, win32 } from "path";
|
|
|
1007
1016
|
import { chmodSync, existsSync as existsSync5, statSync } from "fs";
|
|
1008
1017
|
import { createRequire as createRequire2 } from "module";
|
|
1009
1018
|
import { dirname as dirname4, join as join5 } from "path";
|
|
1019
|
+
import { fileURLToPath as fileURLToPath3 } from "url";
|
|
1010
1020
|
var req = createRequire2(import.meta.url);
|
|
1011
|
-
|
|
1021
|
+
var VENDORED_PTY_PLATFORMS = ["darwin-arm64", "darwin-x64", "win32-arm64", "win32-x64"];
|
|
1022
|
+
function hasVendoredPty(platform = process.platform, arch = process.arch) {
|
|
1023
|
+
return VENDORED_PTY_PLATFORMS.includes(`${platform}-${arch}`);
|
|
1024
|
+
}
|
|
1025
|
+
function vendoredPtyDir(from = dirname4(fileURLToPath3(import.meta.url))) {
|
|
1026
|
+
let dir = from;
|
|
1027
|
+
for (let i = 0; i < 4; i++) {
|
|
1028
|
+
const cand = join5(dir, "vendor", "node-pty");
|
|
1029
|
+
if (existsSync5(join5(cand, "package.json"))) return cand;
|
|
1030
|
+
const up = dirname4(dir);
|
|
1031
|
+
if (up === dir) break;
|
|
1032
|
+
dir = up;
|
|
1033
|
+
}
|
|
1034
|
+
return null;
|
|
1035
|
+
}
|
|
1036
|
+
function fixSpawnHelper(ptyDir) {
|
|
1012
1037
|
if (process.platform !== "darwin") return;
|
|
1013
1038
|
try {
|
|
1014
|
-
const
|
|
1015
|
-
const helper = join5(dir, "spawn-helper");
|
|
1039
|
+
const helper = join5(ptyDir, "prebuilds", `darwin-${process.arch}`, "spawn-helper");
|
|
1016
1040
|
if (existsSync5(helper) && (statSync(helper).mode & 73) === 0) chmodSync(helper, 493);
|
|
1017
1041
|
} catch {
|
|
1018
1042
|
}
|
|
1019
1043
|
}
|
|
1044
|
+
function asPty(m) {
|
|
1045
|
+
return typeof m?.spawn === "function" ? m : null;
|
|
1046
|
+
}
|
|
1020
1047
|
function loadPty() {
|
|
1048
|
+
if (hasVendoredPty()) {
|
|
1049
|
+
const dir = vendoredPtyDir();
|
|
1050
|
+
if (dir) {
|
|
1051
|
+
try {
|
|
1052
|
+
fixSpawnHelper(dir);
|
|
1053
|
+
const m = asPty(req(dir));
|
|
1054
|
+
if (m) return m;
|
|
1055
|
+
} catch {
|
|
1056
|
+
}
|
|
1057
|
+
}
|
|
1058
|
+
}
|
|
1021
1059
|
try {
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
return
|
|
1060
|
+
const dir = dirname4(req.resolve("node-pty/package.json"));
|
|
1061
|
+
fixSpawnHelper(dir);
|
|
1062
|
+
return asPty(req(dir));
|
|
1025
1063
|
} catch {
|
|
1026
1064
|
return null;
|
|
1027
1065
|
}
|
|
1028
1066
|
}
|
|
1067
|
+
function ptyMissingReason(platform = process.platform, arch = process.arch) {
|
|
1068
|
+
return hasVendoredPty(platform, arch) ? "\u5185\u7F6E\u7684 node-pty \u52A0\u8F7D\u5931\u8D25" : `${platform === "linux" ? "Linux" : `${platform}-${arch}`} \u4E0B\u6CA1\u6709\u5185\u7F6E node-pty \u9884\u7F16\u8BD1\uFF0C\u7A97\u53E3\u63A5\u7BA1\u4E0D\u53EF\u7528`;
|
|
1069
|
+
}
|
|
1029
1070
|
function loadHeadless() {
|
|
1030
1071
|
try {
|
|
1031
1072
|
const m = req("@xterm/headless");
|
|
@@ -1123,17 +1164,32 @@ function npmInstallArgs(npmCli, dir, target, registry) {
|
|
|
1123
1164
|
if (!REGISTRIES.includes(registry)) throw new Error(`\u4E0D\u5141\u8BB8\u7684\u6E90\uFF1A${registry}`);
|
|
1124
1165
|
return [npmCli, "install", "--prefix", dir, "--no-audit", "--no-fund", "--omit=dev", `${PACKAGE_NAME}@${target}`, "--registry", registry];
|
|
1125
1166
|
}
|
|
1126
|
-
|
|
1167
|
+
function installEnv(execPath, base = process.env, platform = process.platform) {
|
|
1168
|
+
const env = { ...base };
|
|
1169
|
+
const win = platform === "win32";
|
|
1170
|
+
const key = (win ? Object.keys(env).find((k) => k.toUpperCase() === "PATH") : void 0) ?? "PATH";
|
|
1171
|
+
const nodeDir = (win ? win32 : posix).dirname(execPath);
|
|
1172
|
+
const sep3 = win ? ";" : ":";
|
|
1173
|
+
const rest = (env[key] ?? "").split(sep3).filter((p) => p && p !== nodeDir);
|
|
1174
|
+
env[key] = [nodeDir, ...rest].join(sep3);
|
|
1175
|
+
return { ...env, npm_config_update_notifier: "false", npm_config_fund: "false" };
|
|
1176
|
+
}
|
|
1177
|
+
var defaultExec = (file, args, opts) => new Promise((resolve7, reject) => {
|
|
1127
1178
|
execFile2(file, args, { ...opts, maxBuffer: 16 * 1024 * 1024 }, (err, _stdout, stderr) => {
|
|
1128
1179
|
if (err) {
|
|
1129
1180
|
const tail = String(stderr || "").trim().split("\n").slice(-3).join(" | ");
|
|
1130
1181
|
reject(new Error(`${err.message.split("\n")[0]}${tail ? ` \u2014 ${tail.slice(0, 300)}` : ""}`));
|
|
1131
|
-
} else
|
|
1182
|
+
} else resolve7();
|
|
1132
1183
|
});
|
|
1133
1184
|
});
|
|
1185
|
+
function ptyCandidatesIn(dir, platform = process.platform, arch = process.arch) {
|
|
1186
|
+
const pkg = join6(dir, "node_modules", PACKAGE_NAME);
|
|
1187
|
+
return [...hasVendoredPty(platform, arch) ? [join6(pkg, "vendor", "node-pty")] : [], join6(dir, "node_modules", "node-pty")];
|
|
1188
|
+
}
|
|
1134
1189
|
function ptyLoadsIn(dir, execPath = process.execPath) {
|
|
1190
|
+
const script = "for(const p of process.argv.slice(1)){try{const m=require(p);if(typeof m.spawn==='function')process.exit(0)}catch{}}process.exit(3)";
|
|
1135
1191
|
try {
|
|
1136
|
-
execFileSync4(execPath, ["-e",
|
|
1192
|
+
execFileSync4(execPath, ["-e", script, ...ptyCandidatesIn(dir)], {
|
|
1137
1193
|
timeout: 3e4,
|
|
1138
1194
|
windowsHide: true,
|
|
1139
1195
|
shell: false,
|
|
@@ -1170,7 +1226,7 @@ async function installVersion(target, deps) {
|
|
|
1170
1226
|
timeout: INSTALL_TIMEOUT_MS,
|
|
1171
1227
|
shell: false,
|
|
1172
1228
|
windowsHide: true,
|
|
1173
|
-
env:
|
|
1229
|
+
env: installEnv(execPath, process.env, platform)
|
|
1174
1230
|
});
|
|
1175
1231
|
} catch (e) {
|
|
1176
1232
|
errors.push(`${registry} ${e.message}`);
|
|
@@ -1187,7 +1243,8 @@ async function installVersion(target, deps) {
|
|
|
1187
1243
|
continue;
|
|
1188
1244
|
}
|
|
1189
1245
|
if (oldPtyOk() && !newPtyOk(dir)) {
|
|
1190
|
-
|
|
1246
|
+
const installed = ptyCandidatesIn(dir).some((p) => existsSync6(join6(p, "package.json")));
|
|
1247
|
+
errors.push(`${registry} \u88C5\u597D\u7684\u7248\u672C\u52A0\u8F7D\u4E0D\u4E86 node-pty\uFF08${installed ? "\u5305\u91CC\u6709\u4F46\u52A0\u8F7D\u5931\u8D25" : "\u5305\u91CC\u6CA1\u6709\uFF0Cnpm \u4E5F\u8DF3\u8FC7\u4E86\u53EF\u9009\u4F9D\u8D56"}\uFF09`);
|
|
1191
1248
|
continue;
|
|
1192
1249
|
}
|
|
1193
1250
|
return cliPath;
|
|
@@ -1294,7 +1351,7 @@ var AutoUpdater = class {
|
|
|
1294
1351
|
return "failed";
|
|
1295
1352
|
}
|
|
1296
1353
|
log2(`${target} \u5DF2\u88C5\u597D\u5E76\u6821\u9A8C\u901A\u8FC7\uFF0C\u7B49\u5B88\u62A4\u8FDB\u7A0B\u7A7A\u95F2\u540E\u5207\u6362`);
|
|
1297
|
-
const
|
|
1354
|
+
const sleep3 = this.#d.sleep ?? ((ms) => new Promise((r) => setTimeout(r, ms).unref?.()));
|
|
1298
1355
|
const deadline = this.#now() + (this.#d.idleMaxWaitMs ?? IDLE_MAX_WAIT_MS);
|
|
1299
1356
|
let lastWhy = "";
|
|
1300
1357
|
for (; ; ) {
|
|
@@ -1309,7 +1366,7 @@ var AutoUpdater = class {
|
|
|
1309
1366
|
log2(`\u7B49\u4E86 24 \u5C0F\u65F6\u4ECD\u4E0D\u7A7A\u95F2\uFF0C\u653E\u5F03\u8FD9\u4E00\u8F6E\uFF0C\u4E0B\u6B21\u68C0\u67E5\u518D\u5224\u5B9A`);
|
|
1310
1367
|
return "timeout";
|
|
1311
1368
|
}
|
|
1312
|
-
await
|
|
1369
|
+
await sleep3(this.#d.idlePollMs ?? IDLE_POLL_MS);
|
|
1313
1370
|
}
|
|
1314
1371
|
if (this.#stopped) return "stopped";
|
|
1315
1372
|
this.#d.beginDrain?.();
|
|
@@ -1342,7 +1399,7 @@ var AutoUpdater = class {
|
|
|
1342
1399
|
// src/daemon/index.ts
|
|
1343
1400
|
import { randomBytes as randomBytes7 } from "crypto";
|
|
1344
1401
|
import { existsSync as existsSync13 } from "fs";
|
|
1345
|
-
import { basename as
|
|
1402
|
+
import { basename as basename8, dirname as dirname10, join as join26 } from "path";
|
|
1346
1403
|
import WebSocket2 from "ws";
|
|
1347
1404
|
|
|
1348
1405
|
// src/adapters/ocPlugin.ts
|
|
@@ -1407,6 +1464,31 @@ function processName(pid) {
|
|
|
1407
1464
|
return null;
|
|
1408
1465
|
}
|
|
1409
1466
|
}
|
|
1467
|
+
function parentPid(pid) {
|
|
1468
|
+
if (WIN) {
|
|
1469
|
+
const pp = winProcs()?.find((p) => p.pid === pid)?.ppid ?? 0;
|
|
1470
|
+
return pp > 4 && pp !== pid ? pp : null;
|
|
1471
|
+
}
|
|
1472
|
+
try {
|
|
1473
|
+
const out = execFileSync5("ps", ["-p", String(pid), "-o", "ppid="], {
|
|
1474
|
+
encoding: "utf8",
|
|
1475
|
+
stdio: ["ignore", "pipe", "ignore"]
|
|
1476
|
+
});
|
|
1477
|
+
const ppid = Number(out.trim());
|
|
1478
|
+
return Number.isFinite(ppid) && ppid > 1 ? ppid : null;
|
|
1479
|
+
} catch {
|
|
1480
|
+
return null;
|
|
1481
|
+
}
|
|
1482
|
+
}
|
|
1483
|
+
function selfAndAncestors(pid, maxDepth = 8) {
|
|
1484
|
+
const out = [];
|
|
1485
|
+
let cur = pid;
|
|
1486
|
+
for (let i = 0; i <= maxDepth && cur != null && !out.includes(cur); i++) {
|
|
1487
|
+
out.push(cur);
|
|
1488
|
+
cur = parentPid(cur);
|
|
1489
|
+
}
|
|
1490
|
+
return out;
|
|
1491
|
+
}
|
|
1410
1492
|
function isAlive(pid, expectName) {
|
|
1411
1493
|
if (!Number.isFinite(pid) || pid <= 0) return false;
|
|
1412
1494
|
try {
|
|
@@ -1439,6 +1521,8 @@ function writeEnded(s, now = Date.now()) {
|
|
|
1439
1521
|
const dir = endedDir();
|
|
1440
1522
|
mkdirSync7(dir, { recursive: true, mode: 448 });
|
|
1441
1523
|
const rec = { sessionId: s.sessionId, tool: s.tool, cwd: s.cwd ?? "", interactive: s.interactive === true, endedAt: now };
|
|
1524
|
+
if (Number.isInteger(s.turns) && s.turns >= 0) rec.turns = s.turns;
|
|
1525
|
+
if (typeof s.firstPrompt === "string" && s.firstPrompt) rec.firstPrompt = s.firstPrompt;
|
|
1442
1526
|
const file = join8(dir, `${s.sessionId}.json`);
|
|
1443
1527
|
const tmp = `${file}.${process.pid}.tmp`;
|
|
1444
1528
|
writeFileSync6(tmp, JSON.stringify(rec), { encoding: "utf8", mode: 384 });
|
|
@@ -1561,6 +1645,12 @@ function readState(sessionId) {
|
|
|
1561
1645
|
return null;
|
|
1562
1646
|
}
|
|
1563
1647
|
}
|
|
1648
|
+
function writeState(s) {
|
|
1649
|
+
mkdirSync8(stateDir(), { recursive: true });
|
|
1650
|
+
const tmp = statePath(s.sessionId) + ".tmp";
|
|
1651
|
+
writeFileSync7(tmp, JSON.stringify(s), "utf8");
|
|
1652
|
+
renameSync6(tmp, statePath(s.sessionId));
|
|
1653
|
+
}
|
|
1564
1654
|
function retireState(s, now = Date.now()) {
|
|
1565
1655
|
writeEnded(s, now);
|
|
1566
1656
|
removeState(s.sessionId);
|
|
@@ -1571,8 +1661,26 @@ function removeState(sessionId) {
|
|
|
1571
1661
|
} catch {
|
|
1572
1662
|
}
|
|
1573
1663
|
}
|
|
1574
|
-
|
|
1664
|
+
var STALE_NO_PID_MS = 2 * 60 * 60 * 1e3;
|
|
1665
|
+
var STALE_STATES = /* @__PURE__ */ new Set(["watch", "ask_approval"]);
|
|
1666
|
+
var STALE_NO_PID_LOG = "\u65E0\u8FDB\u7A0B\u53F7\u4E14 2 \u5C0F\u65F6\u65E0\u4E8B\u4EF6\uFF0C\u6309\u5DF2\u7ED3\u675F\u5904\u7406";
|
|
1667
|
+
function pendingApprovalSessions() {
|
|
1668
|
+
try {
|
|
1669
|
+
const recs = JSON.parse(readFileSync8(join9(deskbuddyHome(), "approvals.json"), "utf8"));
|
|
1670
|
+
return new Set(Array.isArray(recs) ? recs.filter((r) => r && r.status === "pending" && typeof r.sessionId === "string").map((r) => r.sessionId) : []);
|
|
1671
|
+
} catch {
|
|
1672
|
+
return /* @__PURE__ */ new Set();
|
|
1673
|
+
}
|
|
1674
|
+
}
|
|
1675
|
+
function isStaleNoPid(s, now, pending) {
|
|
1676
|
+
if (s.pid || !STALE_STATES.has(s.state) || !(now - s.updatedAt > STALE_NO_PID_MS)) return false;
|
|
1677
|
+
return !pending().has(s.sessionId);
|
|
1678
|
+
}
|
|
1679
|
+
function readAliveStates(tool, opts = {}) {
|
|
1575
1680
|
if (!existsSync8(stateDir())) return [];
|
|
1681
|
+
const now = opts.now ?? Date.now();
|
|
1682
|
+
let pend = null;
|
|
1683
|
+
const pending = () => pend ??= pendingApprovalSessions();
|
|
1576
1684
|
const alive = [];
|
|
1577
1685
|
for (const file of readdirSync3(stateDir())) {
|
|
1578
1686
|
if (!file.endsWith(".json")) continue;
|
|
@@ -1588,6 +1696,14 @@ function readAliveStates(tool) {
|
|
|
1588
1696
|
retireState(s);
|
|
1589
1697
|
continue;
|
|
1590
1698
|
}
|
|
1699
|
+
if (isStaleNoPid(s, now, pending)) {
|
|
1700
|
+
try {
|
|
1701
|
+
opts.onStale?.(s);
|
|
1702
|
+
} catch {
|
|
1703
|
+
}
|
|
1704
|
+
retireState(s);
|
|
1705
|
+
continue;
|
|
1706
|
+
}
|
|
1591
1707
|
alive.push(s);
|
|
1592
1708
|
}
|
|
1593
1709
|
return alive;
|
|
@@ -1636,19 +1752,27 @@ async function postReport(payload) {
|
|
|
1636
1752
|
import { basename as basename3 } from "path";
|
|
1637
1753
|
var KEEPALIVE_MS = 2 * 60 * 1e3;
|
|
1638
1754
|
var MAX_SESSIONS = 20;
|
|
1639
|
-
var CAPS = ["cmd_result", "window_inject", "resume_ended", "reply_idle_window"];
|
|
1755
|
+
var CAPS = ["cmd_result", "window_inject", "resume_ended", "reply_idle_window", "turns"];
|
|
1756
|
+
function turnsOf(s) {
|
|
1757
|
+
return Number.isInteger(s.turns) && s.turns >= 0 ? s.turns : void 0;
|
|
1758
|
+
}
|
|
1640
1759
|
function buildSnapshot(states, tool, cfg, nowMs, remote) {
|
|
1641
|
-
const sessions = states.filter((s) => s.tool === tool).sort((a, b) => b.updatedAt - a.updatedAt).slice(0, MAX_SESSIONS).map((s) =>
|
|
1642
|
-
|
|
1643
|
-
|
|
1644
|
-
|
|
1645
|
-
|
|
1646
|
-
|
|
1647
|
-
|
|
1648
|
-
|
|
1649
|
-
|
|
1650
|
-
|
|
1651
|
-
|
|
1760
|
+
const sessions = states.filter((s) => s.tool === tool).sort((a, b) => b.updatedAt - a.updatedAt).slice(0, MAX_SESSIONS).map((s) => {
|
|
1761
|
+
const o = {
|
|
1762
|
+
sessionId: s.sessionId,
|
|
1763
|
+
title: s.title,
|
|
1764
|
+
// 隐私:只带目录名,不带全路径——全路径会暴露公司目录结构与用户名
|
|
1765
|
+
project: basename3(s.project || ""),
|
|
1766
|
+
state: s.state,
|
|
1767
|
+
startedAt: s.startedAt,
|
|
1768
|
+
updatedAt: s.updatedAt,
|
|
1769
|
+
interactive: s.interactive === true,
|
|
1770
|
+
remoteWindow: remote ? remote.has(s.sessionId) : s.remoteWindow === true
|
|
1771
|
+
};
|
|
1772
|
+
const turns = turnsOf(s);
|
|
1773
|
+
if (turns !== void 0) o.turns = turns;
|
|
1774
|
+
return o;
|
|
1775
|
+
});
|
|
1652
1776
|
return { clientId: cfg.clientId, hostName: cfg.hostName, tool, snapshotAt: nowMs, sessions, caps: [...CAPS] };
|
|
1653
1777
|
}
|
|
1654
1778
|
|
|
@@ -1803,7 +1927,13 @@ function parseApprovalFrame(frame) {
|
|
|
1803
1927
|
if (!isValidId(approvalId)) return null;
|
|
1804
1928
|
const decision = str(d.decision);
|
|
1805
1929
|
if (decision !== "allow" && decision !== "deny" && decision !== "allow_always") return null;
|
|
1806
|
-
return {
|
|
1930
|
+
return {
|
|
1931
|
+
instructionId,
|
|
1932
|
+
approvalId,
|
|
1933
|
+
decision,
|
|
1934
|
+
deviceName: cleanDeviceName(d.deviceName),
|
|
1935
|
+
...Array.isArray(d.answers) && d.answers.length <= 8 ? { answers: d.answers } : {}
|
|
1936
|
+
};
|
|
1807
1937
|
}
|
|
1808
1938
|
function cleanDeviceName(v) {
|
|
1809
1939
|
return typeof v === "string" ? v.replace(/[\u0000-\u001f\u007f]/g, "").trim().slice(0, 32) : "";
|
|
@@ -1819,7 +1949,7 @@ import { join as join12 } from "path";
|
|
|
1819
1949
|
|
|
1820
1950
|
// src/daemon/gate/settings.ts
|
|
1821
1951
|
import { existsSync as existsSync10 } from "fs";
|
|
1822
|
-
import { fileURLToPath as
|
|
1952
|
+
import { fileURLToPath as fileURLToPath4 } from "url";
|
|
1823
1953
|
var GATE_HOOK_TIMEOUT_S = 7 * 24 * 60 * 60;
|
|
1824
1954
|
function waitModeFor(tool) {
|
|
1825
1955
|
return tool === "claude_code" ? "defer" : "hold";
|
|
@@ -1842,8 +1972,8 @@ function winPsPath(p) {
|
|
|
1842
1972
|
}
|
|
1843
1973
|
function defaultGateScript() {
|
|
1844
1974
|
const candidates = [
|
|
1845
|
-
|
|
1846
|
-
|
|
1975
|
+
fileURLToPath4(new URL("./gate.cjs", import.meta.url)),
|
|
1976
|
+
fileURLToPath4(new URL("../../../hook/gate.cjs", import.meta.url))
|
|
1847
1977
|
];
|
|
1848
1978
|
return candidates.find((p) => existsSync10(p)) ?? candidates[0];
|
|
1849
1979
|
}
|
|
@@ -1877,6 +2007,10 @@ import { execFileSync as execFileSync6 } from "child_process";
|
|
|
1877
2007
|
import { accessSync, constants, existsSync as existsSync11, lstatSync, readFileSync as readFileSync10, statSync as statSync3 } from "fs";
|
|
1878
2008
|
import { homedir as homedir4 } from "os";
|
|
1879
2009
|
import { delimiter, dirname as dirname7, join as join11, win32 as win323 } from "path";
|
|
2010
|
+
function qoderWorker(d, worker, appBin) {
|
|
2011
|
+
if (d.isExecutable(appBin)) return { file: appBin, prefixArgs: [worker], env: { ELECTRON_RUN_AS_NODE: "1" } };
|
|
2012
|
+
return { file: d.nodePath, prefixArgs: [worker] };
|
|
2013
|
+
}
|
|
1880
2014
|
var defaultLocateDeps = () => ({
|
|
1881
2015
|
home: homedir4(),
|
|
1882
2016
|
path: process.env.PATH ?? "",
|
|
@@ -2075,14 +2209,14 @@ function locateWin(tool, d) {
|
|
|
2075
2209
|
case "qoder_cn": {
|
|
2076
2210
|
for (const dir of winAppDirs(d, "Qoder CN")) {
|
|
2077
2211
|
const p = join11(dir, QODER_CN_WORKER_IN_WIN_APP);
|
|
2078
|
-
if (d.exists(p)) return
|
|
2212
|
+
if (d.exists(p)) return qoderWorker(d, p, join11(dir, "Qoder CN.exe"));
|
|
2079
2213
|
}
|
|
2080
2214
|
return null;
|
|
2081
2215
|
}
|
|
2082
2216
|
case "qoder": {
|
|
2083
2217
|
for (const dir of winAppDirs(d, "Qoder")) {
|
|
2084
2218
|
const p = join11(dir, QODER_WORKER_IN_WIN_APP);
|
|
2085
|
-
if (d.exists(p)) return
|
|
2219
|
+
if (d.exists(p)) return qoderWorker(d, p, join11(dir, "Qoder.exe"));
|
|
2086
2220
|
}
|
|
2087
2221
|
return plain("qodercli");
|
|
2088
2222
|
}
|
|
@@ -2119,14 +2253,14 @@ function locate(tool, d = defaultLocateDeps()) {
|
|
|
2119
2253
|
case "qoder_cn": {
|
|
2120
2254
|
for (const dir of appDirs(d.home)) {
|
|
2121
2255
|
const p = join11(dir, QODER_CN_WORKER_IN_APP);
|
|
2122
|
-
if (d.exists(p)) return
|
|
2256
|
+
if (d.exists(p)) return qoderWorker(d, p, join11(dir, "Qoder CN.app", "Contents", "MacOS", "Qoder CN"));
|
|
2123
2257
|
}
|
|
2124
2258
|
return null;
|
|
2125
2259
|
}
|
|
2126
2260
|
case "qoder": {
|
|
2127
2261
|
for (const dir of appDirs(d.home)) {
|
|
2128
2262
|
const p = join11(dir, QODER_WORKER_IN_APP);
|
|
2129
|
-
if (d.exists(p)) return
|
|
2263
|
+
if (d.exists(p)) return qoderWorker(d, p, join11(dir, "Qoder.app", "Contents", "MacOS", "Qoder"));
|
|
2130
2264
|
}
|
|
2131
2265
|
return plain("qodercli");
|
|
2132
2266
|
}
|
|
@@ -2253,7 +2387,7 @@ function materialize(plan, tool, d = defaultLocateDeps()) {
|
|
|
2253
2387
|
file: loc.file,
|
|
2254
2388
|
args: [...loc.prefixArgs, ...plan.args],
|
|
2255
2389
|
cwd: plan.cwd,
|
|
2256
|
-
env: { ...plan.env, PATH: childPath(d) }
|
|
2390
|
+
env: { ...plan.env, ...loc.env, PATH: childPath(d) }
|
|
2257
2391
|
};
|
|
2258
2392
|
}
|
|
2259
2393
|
function isDeferred(stdout7) {
|
|
@@ -2317,6 +2451,17 @@ function parseSessionId(tool, stdout7) {
|
|
|
2317
2451
|
if (tool === "opencode") return parseOpencodeSessionId(stdout7);
|
|
2318
2452
|
return parseClaudeSessionId(stdout7);
|
|
2319
2453
|
}
|
|
2454
|
+
var FAILURE_TAIL_CHARS = 500;
|
|
2455
|
+
function failureTail(r, text) {
|
|
2456
|
+
let s = (r.stderr ?? "").trim() || (r.stdout ?? "").trim();
|
|
2457
|
+
if (!s) return "";
|
|
2458
|
+
const plain = [text, text.replace(/\s+/g, " ").trim()];
|
|
2459
|
+
const forms = [...plain, ...plain.map((t) => JSON.stringify(t).slice(1, -1))].filter((t, i, a) => t.length >= 2 && a.indexOf(t) === i);
|
|
2460
|
+
forms.sort((a, b) => b.length - a.length);
|
|
2461
|
+
for (const f of forms) s = s.split(f).join("<\u6307\u4EE4>");
|
|
2462
|
+
s = s.replace(/\s+/g, " ").trim();
|
|
2463
|
+
return s.length > FAILURE_TAIL_CHARS ? "\u2026" + s.slice(-FAILURE_TAIL_CHARS) : s;
|
|
2464
|
+
}
|
|
2320
2465
|
function startCommand(plan, tool, limitMs = EXEC_TIMEOUT_MS) {
|
|
2321
2466
|
let remaining = limitMs;
|
|
2322
2467
|
let startedAt = Date.now();
|
|
@@ -2332,7 +2477,7 @@ function startCommand(plan, tool, limitMs = EXEC_TIMEOUT_MS) {
|
|
|
2332
2477
|
}, Math.max(0, remaining));
|
|
2333
2478
|
};
|
|
2334
2479
|
let timedOut = false;
|
|
2335
|
-
const done = new Promise((
|
|
2480
|
+
const done = new Promise((resolve7) => {
|
|
2336
2481
|
child = execFile3(
|
|
2337
2482
|
plan.file,
|
|
2338
2483
|
plan.args,
|
|
@@ -2344,16 +2489,18 @@ function startCommand(plan, tool, limitMs = EXEC_TIMEOUT_MS) {
|
|
|
2344
2489
|
// Windows:别给子进程弹黑框
|
|
2345
2490
|
windowsHide: true
|
|
2346
2491
|
},
|
|
2347
|
-
(err, stdout7) => {
|
|
2492
|
+
(err, stdout7, stderr) => {
|
|
2348
2493
|
finished = true;
|
|
2349
2494
|
if (timer) clearTimeout(timer);
|
|
2350
2495
|
const out = String(stdout7 ?? "");
|
|
2496
|
+
const errOut = String(stderr ?? "");
|
|
2351
2497
|
if (err || timedOut) {
|
|
2352
|
-
const
|
|
2353
|
-
|
|
2498
|
+
const e = err;
|
|
2499
|
+
const msg = timedOut ? `\u6267\u884C\u8D85\u8FC7 ${Math.round(limitMs / 1e3)} \u79D2\u6D3B\u52A8\u65F6\u95F4\uFF0C\u5DF2\u7EC8\u6B62` : e && typeof e.code === "number" ? `\u9000\u51FA\u7801 ${e.code}` : e && e.signal ? `\u88AB\u4FE1\u53F7 ${String(e.signal)} \u7ED3\u675F` : `\u8D77\u4E0D\u6765\uFF08${String(e?.code ?? "unknown")}\uFF09`;
|
|
2500
|
+
resolve7({ ok: false, sessionId: parseSessionId(tool, out), stdout: out, stderr: errOut, error: msg });
|
|
2354
2501
|
return;
|
|
2355
2502
|
}
|
|
2356
|
-
|
|
2503
|
+
resolve7({ ok: true, sessionId: parseSessionId(tool, out), stdout: out, stderr: errOut });
|
|
2357
2504
|
}
|
|
2358
2505
|
);
|
|
2359
2506
|
child?.stdin?.end();
|
|
@@ -2361,6 +2508,9 @@ function startCommand(plan, tool, limitMs = EXEC_TIMEOUT_MS) {
|
|
|
2361
2508
|
});
|
|
2362
2509
|
return {
|
|
2363
2510
|
done,
|
|
2511
|
+
get pid() {
|
|
2512
|
+
return child?.pid;
|
|
2513
|
+
},
|
|
2364
2514
|
pauseTimeout() {
|
|
2365
2515
|
if (finished) return;
|
|
2366
2516
|
if (paused++ === 0 && timer) {
|
|
@@ -2428,6 +2578,7 @@ var ApprovalStore = class _ApprovalStore {
|
|
|
2428
2578
|
}
|
|
2429
2579
|
}
|
|
2430
2580
|
save() {
|
|
2581
|
+
for (const r of this.#records) if (r.status !== "pending" && r.toolInput !== void 0) delete r.toolInput;
|
|
2431
2582
|
try {
|
|
2432
2583
|
mkdirSync10(deskbuddyHome(), { recursive: true });
|
|
2433
2584
|
const tmp = storePath() + ".tmp";
|
|
@@ -2483,7 +2634,9 @@ function serverBackend(client = api) {
|
|
|
2483
2634
|
summary: r.summary,
|
|
2484
2635
|
cwdName: basename5(r.cwd),
|
|
2485
2636
|
source: r.mode === "window" ? "window" : "remote",
|
|
2486
|
-
...r.remember ? { remember: r.remember } : {}
|
|
2637
|
+
...r.remember ? { remember: r.remember } : {},
|
|
2638
|
+
// 🔴 选择题只发展示用的题目和背景;原始 tool_input(r.toolInput)只留本机
|
|
2639
|
+
...r.kind === "question" && r.questions ? { questions: r.questions, ...r.context ? { context: r.context } : {} } : {}
|
|
2487
2640
|
}),
|
|
2488
2641
|
cancel: (approvalId, reason) => client.approvalCancel(approvalId, reason),
|
|
2489
2642
|
sync: async (ids) => {
|
|
@@ -2491,17 +2644,17 @@ function serverBackend(client = api) {
|
|
|
2491
2644
|
const out = [];
|
|
2492
2645
|
for (const a of res?.approvals ?? []) {
|
|
2493
2646
|
if (typeof a?.approvalId !== "string") continue;
|
|
2494
|
-
const d = mapStatus(a.status);
|
|
2495
|
-
if (d) out.push({ approvalId: a.approvalId, decision: d, deviceName: cleanDeviceName(a.deviceName) });
|
|
2647
|
+
const d = mapStatus(a.status, a.decision);
|
|
2648
|
+
if (d) out.push({ approvalId: a.approvalId, decision: d, deviceName: cleanDeviceName(a.deviceName), ...Array.isArray(a.answers) ? { answers: a.answers } : {} });
|
|
2496
2649
|
}
|
|
2497
2650
|
return out;
|
|
2498
2651
|
}
|
|
2499
2652
|
};
|
|
2500
2653
|
}
|
|
2501
|
-
function mapStatus(s) {
|
|
2654
|
+
function mapStatus(s, decision) {
|
|
2502
2655
|
switch (s) {
|
|
2503
2656
|
case "APPROVED":
|
|
2504
|
-
return "allow";
|
|
2657
|
+
return decision === "allow_always" ? "allow_always" : "allow";
|
|
2505
2658
|
case "DENIED":
|
|
2506
2659
|
return "deny";
|
|
2507
2660
|
case "CANCELLED":
|
|
@@ -2514,7 +2667,8 @@ function mapStatus(s) {
|
|
|
2514
2667
|
// src/daemon/gate/coordinator.ts
|
|
2515
2668
|
import { randomUUID as randomUUID3 } from "crypto";
|
|
2516
2669
|
import { existsSync as existsSync12, realpathSync as realpathSync2 } from "fs";
|
|
2517
|
-
import {
|
|
2670
|
+
import { homedir as homedir6 } from "os";
|
|
2671
|
+
import { basename as basename6, dirname as dirname8, join as join14, posix as posix3, win32 as win324 } from "path";
|
|
2518
2672
|
|
|
2519
2673
|
// src/daemon/gate/classify.ts
|
|
2520
2674
|
import { isAbsolute, relative as relative2, resolve as resolve2, sep as sep2 } from "path";
|
|
@@ -2661,6 +2815,152 @@ function describe(toolName, toolInput, cwd) {
|
|
|
2661
2815
|
return { kind: SHELL_TOOLS.has(toolName) ? "command" : "other", summary: truncate(detail ? `${toolName} ${detail}` : toolName) };
|
|
2662
2816
|
}
|
|
2663
2817
|
|
|
2818
|
+
// src/daemon/gate/question.ts
|
|
2819
|
+
import { closeSync as closeSync3, openSync as openSync3, readSync as readSync2, statSync as statSync4 } from "fs";
|
|
2820
|
+
var MAX_QUESTIONS = 4;
|
|
2821
|
+
var MAX_OPTIONS = 4;
|
|
2822
|
+
var HEADER_MAX = 12;
|
|
2823
|
+
var QUESTION_MAX = 150;
|
|
2824
|
+
var LABEL_MAX = 40;
|
|
2825
|
+
var DESC_MAX = 80;
|
|
2826
|
+
var SUMMARY_MAX2 = 200;
|
|
2827
|
+
var CONTEXT_MAX = 300;
|
|
2828
|
+
var OTHER_MAX = 200;
|
|
2829
|
+
function clean(v) {
|
|
2830
|
+
return typeof v === "string" ? v.replace(/[\u0000-\u001f\u007f-\u009f\u2028\u2029]+/g, " ").replace(/\s+/g, " ").trim() : "";
|
|
2831
|
+
}
|
|
2832
|
+
function cut(s, max) {
|
|
2833
|
+
const a = Array.from(s);
|
|
2834
|
+
return a.length > max ? a.slice(0, max - 1).join("") + "\u2026" : s;
|
|
2835
|
+
}
|
|
2836
|
+
var RECOMMENDED_RE = /\s*\(Recommended\)\s*$/i;
|
|
2837
|
+
function rawQuestions(toolInput) {
|
|
2838
|
+
const ans = toolInput?.answers;
|
|
2839
|
+
if (ans !== void 0 && ans !== null) return null;
|
|
2840
|
+
const qs = toolInput?.questions;
|
|
2841
|
+
if (!Array.isArray(qs) || qs.length === 0 || qs.length > MAX_QUESTIONS) return null;
|
|
2842
|
+
const seen = /* @__PURE__ */ new Set();
|
|
2843
|
+
for (const q of qs) {
|
|
2844
|
+
if (!q || typeof q !== "object" || typeof q.question !== "string" || !q.question.trim()) return null;
|
|
2845
|
+
const kind = q.kind;
|
|
2846
|
+
if (kind !== void 0 && kind !== "choice") return null;
|
|
2847
|
+
if (seen.has(q.question)) return null;
|
|
2848
|
+
seen.add(q.question);
|
|
2849
|
+
if (!Array.isArray(q.options) || q.options.length < 2) return null;
|
|
2850
|
+
for (const o of q.options) if (!o || typeof o !== "object" || typeof o.label !== "string" || !o.label.trim()) return null;
|
|
2851
|
+
}
|
|
2852
|
+
return qs;
|
|
2853
|
+
}
|
|
2854
|
+
function questionView(toolInput) {
|
|
2855
|
+
const raw = rawQuestions(toolInput);
|
|
2856
|
+
if (!raw) return null;
|
|
2857
|
+
const questions = raw.map((q) => {
|
|
2858
|
+
const opts = q.options.slice(0, MAX_OPTIONS).map((o) => {
|
|
2859
|
+
const recommended = RECOMMENDED_RE.test(o.label);
|
|
2860
|
+
return {
|
|
2861
|
+
label: cut(clean(o.label.replace(RECOMMENDED_RE, "")), LABEL_MAX),
|
|
2862
|
+
description: cut(clean(o.description), DESC_MAX),
|
|
2863
|
+
recommended
|
|
2864
|
+
};
|
|
2865
|
+
});
|
|
2866
|
+
return {
|
|
2867
|
+
header: cut(clean(q.header), HEADER_MAX),
|
|
2868
|
+
question: cut(clean(q.question), QUESTION_MAX),
|
|
2869
|
+
multiSelect: q.multiSelect === true,
|
|
2870
|
+
options: opts,
|
|
2871
|
+
...q.options.length > MAX_OPTIONS ? { truncatedOptions: true } : {}
|
|
2872
|
+
};
|
|
2873
|
+
});
|
|
2874
|
+
const first = questions[0];
|
|
2875
|
+
return { questions, summary: cut(first.question || first.header, SUMMARY_MAX2) };
|
|
2876
|
+
}
|
|
2877
|
+
function joinAnswerItems(items) {
|
|
2878
|
+
return items.map((s) => s.includes(", ") || s.includes('"') ? JSON.stringify(s) : s).join(", ");
|
|
2879
|
+
}
|
|
2880
|
+
function resolveAnswers(toolInput, answers) {
|
|
2881
|
+
const raw = rawQuestions(toolInput);
|
|
2882
|
+
if (!raw || !Array.isArray(answers) || answers.length !== raw.length) return null;
|
|
2883
|
+
const res = [];
|
|
2884
|
+
const done = /* @__PURE__ */ new Set();
|
|
2885
|
+
const ordered = [...answers].sort((x, y) => (x?.index ?? 0) - (y?.index ?? 0));
|
|
2886
|
+
for (const a of ordered) {
|
|
2887
|
+
if (!a || typeof a !== "object" || !Number.isInteger(a.index) || a.index < 0 || a.index >= raw.length || done.has(a.index)) return null;
|
|
2888
|
+
done.add(a.index);
|
|
2889
|
+
const q = raw[a.index];
|
|
2890
|
+
const shown = Math.min(q.options.length, MAX_OPTIONS);
|
|
2891
|
+
const sel = Array.isArray(a.selected) ? a.selected : [];
|
|
2892
|
+
if (!sel.every((i) => Number.isInteger(i) && i >= 0 && i < shown) || new Set(sel).size !== sel.length) return null;
|
|
2893
|
+
const other = clean(a.other);
|
|
2894
|
+
if (Array.from(other).length > OTHER_MAX) return null;
|
|
2895
|
+
if (other && q.custom === false) return null;
|
|
2896
|
+
const labels = [...sel].sort((x, y) => x - y).map((i) => q.options[i].label);
|
|
2897
|
+
if (q.multiSelect === true) {
|
|
2898
|
+
if (labels.length === 0 && !other) return null;
|
|
2899
|
+
const items = other ? [...labels, other] : labels;
|
|
2900
|
+
res.push({ question: q.question, items, joined: joinAnswerItems(items) });
|
|
2901
|
+
} else {
|
|
2902
|
+
if (labels.length === 1 && !other) res.push({ question: q.question, items: labels, joined: labels[0] });
|
|
2903
|
+
else if (labels.length === 0 && other) res.push({ question: q.question, items: [other], joined: other });
|
|
2904
|
+
else return null;
|
|
2905
|
+
}
|
|
2906
|
+
}
|
|
2907
|
+
return res;
|
|
2908
|
+
}
|
|
2909
|
+
function answersToUpdatedInput(toolInput, answers) {
|
|
2910
|
+
const r = resolveAnswers(toolInput, answers);
|
|
2911
|
+
if (!r) return null;
|
|
2912
|
+
const out = {};
|
|
2913
|
+
for (const x of r) out[x.question] = x.joined;
|
|
2914
|
+
return { ...toolInput, answers: out };
|
|
2915
|
+
}
|
|
2916
|
+
function answersSummary(updatedInput) {
|
|
2917
|
+
const a = updatedInput.answers;
|
|
2918
|
+
const s = Object.values(a ?? {}).join("\uFF1B");
|
|
2919
|
+
return cut(clean(s), 60);
|
|
2920
|
+
}
|
|
2921
|
+
var TAIL_BYTES = 256 * 1024;
|
|
2922
|
+
function lastAssistantText(transcriptPath) {
|
|
2923
|
+
if (typeof transcriptPath !== "string" || !transcriptPath) return "";
|
|
2924
|
+
let text = "";
|
|
2925
|
+
try {
|
|
2926
|
+
const size = statSync4(transcriptPath).size;
|
|
2927
|
+
const len = Math.min(size, TAIL_BYTES);
|
|
2928
|
+
const buf = Buffer.alloc(len);
|
|
2929
|
+
const fd = openSync3(transcriptPath, "r");
|
|
2930
|
+
try {
|
|
2931
|
+
readSync2(fd, buf, 0, len, size - len);
|
|
2932
|
+
} finally {
|
|
2933
|
+
closeSync3(fd);
|
|
2934
|
+
}
|
|
2935
|
+
text = buf.toString("utf8");
|
|
2936
|
+
} catch {
|
|
2937
|
+
return "";
|
|
2938
|
+
}
|
|
2939
|
+
const lines2 = text.split("\n");
|
|
2940
|
+
for (let i = lines2.length - 1; i >= 0; i--) {
|
|
2941
|
+
const line = lines2[i].trim();
|
|
2942
|
+
if (!line.startsWith("{")) continue;
|
|
2943
|
+
try {
|
|
2944
|
+
const m = JSON.parse(line);
|
|
2945
|
+
if (m.type === "user") {
|
|
2946
|
+
const c = m.message?.content;
|
|
2947
|
+
const toolResultOnly = Array.isArray(c) && c.length > 0 && c.every((x) => x?.type === "tool_result");
|
|
2948
|
+
if (!toolResultOnly) return "";
|
|
2949
|
+
continue;
|
|
2950
|
+
}
|
|
2951
|
+
if (m.type !== "assistant" || !Array.isArray(m.message?.content)) continue;
|
|
2952
|
+
const parts = m.message.content.filter((c) => c && c.type === "text" && typeof c.text === "string").map((c) => c.text);
|
|
2953
|
+
const body = parts.join("\n").trim();
|
|
2954
|
+
if (!body) continue;
|
|
2955
|
+
const paras = body.split(/\n\s*\n/).map((p) => p.trim()).filter(Boolean);
|
|
2956
|
+
return cut(clean(paras[paras.length - 1] ?? body), CONTEXT_MAX);
|
|
2957
|
+
} catch {
|
|
2958
|
+
continue;
|
|
2959
|
+
}
|
|
2960
|
+
}
|
|
2961
|
+
return "";
|
|
2962
|
+
}
|
|
2963
|
+
|
|
2664
2964
|
// src/daemon/gate/procs.ts
|
|
2665
2965
|
import { execFileSync as execFileSync8 } from "child_process";
|
|
2666
2966
|
function listDescendants(rootPid) {
|
|
@@ -2712,7 +3012,19 @@ var CMD_MATCH_MIN = 6;
|
|
|
2712
3012
|
var CMD_MATCH_LEN = 80;
|
|
2713
3013
|
|
|
2714
3014
|
// src/daemon/gate/coordinator.ts
|
|
2715
|
-
|
|
3015
|
+
var REMEMBER_TOOLS = /* @__PURE__ */ new Set([
|
|
3016
|
+
"claude_code",
|
|
3017
|
+
// Qoder CN / Qoder 的 worker 与 Claude Code 同一套 PermissionUpdate(addRules / setMode / addDirectories),
|
|
3018
|
+
// 输入原样带建议、输出的 updatedPermissions 会被应用(2026-10-02 读 Qoder CN 0.4.1 worker 源码核对)⚠️ 未实机点过
|
|
3019
|
+
"qoder_cn",
|
|
3020
|
+
"qoder",
|
|
3021
|
+
// opencode:插件把授权请求的 always 模式翻成一条 session 级 addRules 建议,设备选「记住」时插件回 always(hook/opencode.mjs)
|
|
3022
|
+
"opencode"
|
|
3023
|
+
]);
|
|
3024
|
+
function cut100(text) {
|
|
3025
|
+
return text.length > 100 ? text.slice(0, 99) + "\u2026" : text;
|
|
3026
|
+
}
|
|
3027
|
+
function pickRememberSuggestion(suggestions, opts = {}) {
|
|
2716
3028
|
if (!Array.isArray(suggestions)) return null;
|
|
2717
3029
|
for (const s of suggestions) {
|
|
2718
3030
|
if (!s || s.type !== "addRules" || s.behavior !== "allow" || !Array.isArray(s.rules) || s.rules.length === 0) continue;
|
|
@@ -2721,10 +3033,41 @@ function pickRememberSuggestion(suggestions) {
|
|
|
2721
3033
|
if (raw.some((r) => typeof r?.ruleContent !== "string" || !r.ruleContent)) continue;
|
|
2722
3034
|
const rules = raw.map((r) => r.ruleContent);
|
|
2723
3035
|
const scope = s.destination === "session" ? "\u8FD9\u6B21\u4F1A\u8BDD\u91CC" : "\u8FD9\u4E2A\u9879\u76EE\u91CC";
|
|
2724
|
-
|
|
2725
|
-
|
|
2726
|
-
|
|
2727
|
-
|
|
3036
|
+
return { updates: [s], text: cut100(`\u4EE5\u540E\u5728${scope}\u90FD\u5141\u8BB8\uFF1A${rules.join("\u3001")}`) };
|
|
3037
|
+
}
|
|
3038
|
+
if (!opts.edit) return null;
|
|
3039
|
+
const all = suggestions;
|
|
3040
|
+
const mode = all.find((s) => s && s.type === "setMode" && s.mode === "acceptEdits" && s.destination === "session");
|
|
3041
|
+
if (!mode) return null;
|
|
3042
|
+
const dirs = all.filter((s) => s && s.type === "addDirectories");
|
|
3043
|
+
if (dirs.length > 1) return null;
|
|
3044
|
+
let text = "\u672C\u4F1A\u8BDD\u81EA\u52A8\u63A5\u53D7\u6587\u4EF6\u7F16\u8F91\uFF08\u542B\u65B0\u5EFA/\u79FB\u52A8\u6587\u4EF6\u7B49\uFF09";
|
|
3045
|
+
if (dirs.length === 1) {
|
|
3046
|
+
const d = dirs[0];
|
|
3047
|
+
const list = Array.isArray(d.directories) ? d.directories : [];
|
|
3048
|
+
if (d.destination !== "session" || list.length !== 1 || !grantableDir(list[0], opts.home ?? homedir6())) return null;
|
|
3049
|
+
text += `\uFF0C\u5E76\u5141\u8BB8\u8BBF\u95EE\u76EE\u5F55 ${list[0]}`;
|
|
3050
|
+
}
|
|
3051
|
+
if (text.length > 100) return null;
|
|
3052
|
+
return { updates: dirs.length === 1 ? [mode, dirs[0]] : [mode], text };
|
|
3053
|
+
}
|
|
3054
|
+
var EDIT_TOOLS2 = /* @__PURE__ */ new Set(["Edit", "Write", "MultiEdit", "NotebookEdit"]);
|
|
3055
|
+
function normDir(p, win) {
|
|
3056
|
+
if (win) return win324.normalize(p).replace(/[\\/]+$/, "").toLowerCase();
|
|
3057
|
+
return posix3.normalize(p).replace(/\/+$/, "");
|
|
3058
|
+
}
|
|
3059
|
+
function grantableDir(dir, home) {
|
|
3060
|
+
if (typeof dir !== "string" || dir === "") return false;
|
|
3061
|
+
const win = /^[A-Za-z]:[\\/]/.test(dir) || dir.startsWith("\\\\");
|
|
3062
|
+
if (!win && !dir.startsWith("/")) return false;
|
|
3063
|
+
const n = normDir(dir, win);
|
|
3064
|
+
if (n === "" || /^[a-z]:$/.test(n) || win && /^\\\\[^\\]+\\[^\\]+$/.test(n)) return false;
|
|
3065
|
+
if (home && n === normDir(home, win)) return false;
|
|
3066
|
+
return true;
|
|
3067
|
+
}
|
|
3068
|
+
function rememberUpdatesOf(rec) {
|
|
3069
|
+
if (Array.isArray(rec.rememberUpdates) && rec.rememberUpdates.length > 0) return rec.rememberUpdates;
|
|
3070
|
+
return rec.rememberSuggestion !== void 0 ? [rec.rememberSuggestion] : null;
|
|
2728
3071
|
}
|
|
2729
3072
|
var DEVICE_FALLBACK = "\u684C\u9762\u642D\u5B50";
|
|
2730
3073
|
function windowNotice(decision, summary2, deviceName = "") {
|
|
@@ -2732,7 +3075,17 @@ function windowNotice(decision, summary2, deviceName = "") {
|
|
|
2732
3075
|
const what = summary2.length > 60 ? summary2.slice(0, 59) + "\u2026" : summary2;
|
|
2733
3076
|
return decision === "allow" ? `\u2705 \u5DF2\u5728${who}\u4E0A\u5141\u8BB8\uFF1A${what}` : `\u274C \u5DF2\u5728${who}\u4E0A\u62D2\u7EDD\uFF1A${what}`;
|
|
2734
3077
|
}
|
|
2735
|
-
var WINDOW_SKIP_TOOLS = /* @__PURE__ */ new Set(["
|
|
3078
|
+
var WINDOW_SKIP_TOOLS = /* @__PURE__ */ new Set(["ExitPlanMode"]);
|
|
3079
|
+
var QUESTION_TOOL = "AskUserQuestion";
|
|
3080
|
+
var QUESTION_TOOLS_HOSTS = /* @__PURE__ */ new Set(["claude_code", "qoder_cn", "qoder", "codex", "opencode", "qwenwork", "dumate"]);
|
|
3081
|
+
var QUESTION_SKIPPED = "\u7528\u6237\u5728\u8BBE\u5907\u4E0A\u9009\u62E9\u4E0D\u56DE\u7B54\u8FD9\u4E2A\u95EE\u9898\uFF0C\u8BF7\u5728\u7535\u8111\u4E0A\u8BE2\u95EE\u6216\u6309\u4F60\u7684\u5224\u65AD\u7EE7\u7EED";
|
|
3082
|
+
var QUESTION_UNANSWERABLE = "\u8BBE\u5907\u672A\u7ED9\u51FA\u6709\u6548\u7B54\u6848\uFF08\u8FD9\u4E2A\u95EE\u9898\u6CA1\u6CD5\u5728\u8BBE\u5907\u4E0A\u56DE\u7B54\uFF09\uFF0C\u8BF7\u6309\u4F60\u7684\u5224\u65AD\u7EE7\u7EED\uFF0C\u6216\u5728\u7ED3\u679C\u91CC\u8BF4\u660E\u9700\u8981\u7528\u6237\u51B3\u5B9A\u7684\u4E8B";
|
|
3083
|
+
var REMOTE_QUESTION_TOOLS = /* @__PURE__ */ new Set(["workbuddy"]);
|
|
3084
|
+
var NO_NOTICE_TOOLS = /* @__PURE__ */ new Set(["qwenwork", "dumate"]);
|
|
3085
|
+
function questionNotice(summary2, deviceName = "") {
|
|
3086
|
+
const who = deviceName ? `\u300C${deviceName}\u300D` : DEVICE_FALLBACK;
|
|
3087
|
+
return `\u2705 \u5DF2\u5728${who}\u4E0A\u56DE\u7B54\uFF1A${summary2}`;
|
|
3088
|
+
}
|
|
2736
3089
|
var WINDOW_SETTLED_EVENTS = /* @__PURE__ */ new Set(["UserPromptSubmit", "Stop"]);
|
|
2737
3090
|
var WINDOW_TOOL_DONE_EVENTS = /* @__PURE__ */ new Set(["PostToolUse", "PostToolUseFailure"]);
|
|
2738
3091
|
var WINDOW_PROC_WATCH_MS = 10 * 60 * 1e3;
|
|
@@ -2770,7 +3123,10 @@ var ApprovalCoordinator = class {
|
|
|
2770
3123
|
const toolName = typeof req2.hook.tool_name === "string" ? req2.hook.tool_name : "";
|
|
2771
3124
|
const sessionId = typeof req2.hook.session_id === "string" && req2.hook.session_id || run2.sessionId || "";
|
|
2772
3125
|
if (sessionId && !run2.sessionId) run2.sessionId = sessionId;
|
|
2773
|
-
const
|
|
3126
|
+
const remoteQuestion = toolName === QUESTION_TOOL && REMOTE_QUESTION_TOOLS.has(run2.tool) && run2.mode === "hold";
|
|
3127
|
+
const qv = remoteQuestion ? questionView(req2.hook.tool_input) : null;
|
|
3128
|
+
if (remoteQuestion && !qv) return { decision: "deny", reason: QUESTION_UNANSWERABLE };
|
|
3129
|
+
const v = qv ? { action: "ask", kind: "question", summary: qv.summary } : classify(toolName, req2.hook.tool_input, cwdForms(run2.cwd), this.#d.policy(), { real: realpathNearest });
|
|
2774
3130
|
if (v.action === "auto") return { decision: "allow" };
|
|
2775
3131
|
const fp = fingerprint(toolName, req2.hook.tool_input);
|
|
2776
3132
|
const store = this.#d.store;
|
|
@@ -2795,13 +3151,17 @@ var ApprovalCoordinator = class {
|
|
|
2795
3151
|
summary: v.summary,
|
|
2796
3152
|
mode: run2.mode,
|
|
2797
3153
|
status: "pending",
|
|
2798
|
-
createdAt: this.#now()
|
|
3154
|
+
createdAt: this.#now(),
|
|
3155
|
+
...qv ? (() => {
|
|
3156
|
+
const context = lastAssistantText(req2.hook.transcript_path);
|
|
3157
|
+
return { questions: qv.questions, toolInput: req2.hook.tool_input, ...context ? { context } : {} };
|
|
3158
|
+
})() : {}
|
|
2799
3159
|
};
|
|
2800
3160
|
store.add(rec);
|
|
2801
3161
|
store.save();
|
|
2802
3162
|
let early = null;
|
|
2803
3163
|
let settle;
|
|
2804
|
-
const decided = new Promise((
|
|
3164
|
+
const decided = new Promise((resolve7) => settle = resolve7);
|
|
2805
3165
|
this.#waiters.set(rec.approvalId, (r) => {
|
|
2806
3166
|
early = r;
|
|
2807
3167
|
settle(r);
|
|
@@ -2825,8 +3185,8 @@ var ApprovalCoordinator = class {
|
|
|
2825
3185
|
}
|
|
2826
3186
|
run2.onWaitStart?.();
|
|
2827
3187
|
try {
|
|
2828
|
-
return await new Promise((
|
|
2829
|
-
void decided.then(
|
|
3188
|
+
return await new Promise((resolve7) => {
|
|
3189
|
+
void decided.then(resolve7);
|
|
2830
3190
|
const onAbort = () => {
|
|
2831
3191
|
if (!this.#waiters.delete(rec.approvalId)) return;
|
|
2832
3192
|
if (rec.status === "pending") {
|
|
@@ -2835,7 +3195,7 @@ var ApprovalCoordinator = class {
|
|
|
2835
3195
|
store.save();
|
|
2836
3196
|
void this.#cancelRemote(rec.approvalId, "\u667A\u80FD\u4F53\u8FDB\u7A0B\u5DF2\u7ED3\u675F");
|
|
2837
3197
|
}
|
|
2838
|
-
|
|
3198
|
+
resolve7({ decision: "deny", reason: "\u5DF2\u53D6\u6D88" });
|
|
2839
3199
|
};
|
|
2840
3200
|
if (signal.aborted) onAbort();
|
|
2841
3201
|
else signal.addEventListener("abort", onAbort, { once: true });
|
|
@@ -2856,10 +3216,14 @@ var ApprovalCoordinator = class {
|
|
|
2856
3216
|
const sessionId = typeof hook.session_id === "string" ? hook.session_id : "";
|
|
2857
3217
|
const cwd = typeof hook.cwd === "string" ? hook.cwd : "";
|
|
2858
3218
|
if (!toolName || !sessionId || WINDOW_SKIP_TOOLS.has(toolName)) return none;
|
|
3219
|
+
const isQuestion = toolName === QUESTION_TOOL;
|
|
3220
|
+
if (isQuestion && !QUESTION_TOOLS_HOSTS.has(tool)) return none;
|
|
3221
|
+
const qv = isQuestion ? questionView(hook.tool_input) : null;
|
|
3222
|
+
if (isQuestion && !qv) return none;
|
|
2859
3223
|
for (const run2 of this.#runs.values()) if (run2.sessionId === sessionId) return none;
|
|
2860
3224
|
const store = this.#d.store;
|
|
2861
|
-
const { kind, summary: summary2 } = describe(toolName, hook.tool_input, cwd ? cwdForms(cwd) : []);
|
|
2862
|
-
const remember =
|
|
3225
|
+
const { kind, summary: summary2 } = qv ? { kind: "question", summary: qv.summary } : describe(toolName, hook.tool_input, cwd ? cwdForms(cwd) : []);
|
|
3226
|
+
const remember = !qv && REMEMBER_TOOLS.has(tool) ? pickRememberSuggestion(hook.permission_suggestions, { edit: EDIT_TOOLS2.has(toolName) || kind === "edit" }) : null;
|
|
2863
3227
|
const rec = {
|
|
2864
3228
|
approvalId: randomUUID3(),
|
|
2865
3229
|
runId: "",
|
|
@@ -2872,7 +3236,11 @@ var ApprovalCoordinator = class {
|
|
|
2872
3236
|
mode: "window",
|
|
2873
3237
|
status: "pending",
|
|
2874
3238
|
createdAt: this.#now(),
|
|
2875
|
-
...remember ? { remember: remember.text,
|
|
3239
|
+
...remember ? { remember: remember.text, rememberUpdates: remember.updates } : {},
|
|
3240
|
+
...qv ? (() => {
|
|
3241
|
+
const context = lastAssistantText(hook.transcript_path);
|
|
3242
|
+
return { questions: qv.questions, toolInput: hook.tool_input, ...context ? { context } : {} };
|
|
3243
|
+
})() : {}
|
|
2876
3244
|
};
|
|
2877
3245
|
store.add(rec);
|
|
2878
3246
|
store.save();
|
|
@@ -2887,7 +3255,7 @@ var ApprovalCoordinator = class {
|
|
|
2887
3255
|
...watchProcs ? { pid, cmd, baseline: new Set(this.#descendants(pid).map((p) => p.pid)) } : {}
|
|
2888
3256
|
});
|
|
2889
3257
|
let settle;
|
|
2890
|
-
const decided = new Promise((
|
|
3258
|
+
const decided = new Promise((resolve7) => settle = resolve7);
|
|
2891
3259
|
this.#waiters.set(rec.approvalId, settle);
|
|
2892
3260
|
try {
|
|
2893
3261
|
await this.#d.backend.create(rec);
|
|
@@ -2903,11 +3271,11 @@ var ApprovalCoordinator = class {
|
|
|
2903
3271
|
}
|
|
2904
3272
|
this.#d.log(`\u7A97\u53E3\u6388\u6743 ${rec.approvalId} \u5DF2\u63A8\u5230\u8BBE\u5907 kind=${rec.kind}`);
|
|
2905
3273
|
if (rec.status !== "pending") void this.#cancelRemote(rec.approvalId, "\u7535\u8111\u4E0A\u5DF2\u5904\u7406");
|
|
2906
|
-
return new Promise((
|
|
2907
|
-
void decided.then(
|
|
3274
|
+
return new Promise((resolve7) => {
|
|
3275
|
+
void decided.then(resolve7);
|
|
2908
3276
|
const onAbort = () => {
|
|
2909
3277
|
if (this.#waiters.has(rec.approvalId)) this.#settleWindow(rec, "hook \u5DF2\u7ED3\u675F");
|
|
2910
|
-
|
|
3278
|
+
resolve7(none);
|
|
2911
3279
|
};
|
|
2912
3280
|
if (signal.aborted) onAbort();
|
|
2913
3281
|
else signal.addEventListener("abort", onAbort, { once: true });
|
|
@@ -2936,8 +3304,10 @@ var ApprovalCoordinator = class {
|
|
|
2936
3304
|
}
|
|
2937
3305
|
const fp = rec.fingerprint.slice(0, 16);
|
|
2938
3306
|
const settled = (st.recent ?? []).some(
|
|
2939
|
-
(e) => e.at > rec.createdAt && (WINDOW_SETTLED_EVENTS.has(e.e) || // 跑完的必须正是这一步:并行的同名工具不算(评审 #2)。老版 hook 没有 fp
|
|
2940
|
-
|
|
3307
|
+
(e) => e.at > rec.createdAt && (WINDOW_SETTLED_EVENTS.has(e.e) || // 跑完的必须正是这一步:并行的同名工具不算(评审 #2)。老版 hook 没有 fp 时退回只比工具名。
|
|
3308
|
+
// 选择题只比工具名:答完后 PostToolUse 的 tool_input 带上了 answers,指纹变了(spec §3.5 / R2);
|
|
3309
|
+
// 同一会话同时只会有一道选择题(子代理用不了 AskUserQuestion,窗口里一次也只弹一个选择框)
|
|
3310
|
+
WINDOW_TOOL_DONE_EVENTS.has(e.e) && e.tool === w.toolName && (e.fp === void 0 || e.fp === fp || rec.kind === "question"))
|
|
2941
3311
|
);
|
|
2942
3312
|
if (settled) {
|
|
2943
3313
|
this.#settleWindow(rec, "\u7535\u8111\u4E0A\u5DF2\u5904\u7406");
|
|
@@ -2973,6 +3343,56 @@ var ApprovalCoordinator = class {
|
|
|
2973
3343
|
}
|
|
2974
3344
|
void this.#cancelRemote(rec.approvalId, reason);
|
|
2975
3345
|
}
|
|
3346
|
+
/**
|
|
3347
|
+
* 选择题的答复:作答 → 用本地原始 tool_input 按下标拼 updatedInput 交回 hook;跳过 → deny(不带设备名)。
|
|
3348
|
+
* 🔴 答案拼不出来(下标越界、单选给了多个……)一律不表态,窗口里的选择框照常由人答(红线 R7)
|
|
3349
|
+
*/
|
|
3350
|
+
#onQuestionDecision(rec, decision, deviceName, answers) {
|
|
3351
|
+
rec.decidedAt = this.#now();
|
|
3352
|
+
rec.consumed = true;
|
|
3353
|
+
this.#windows.delete(rec.approvalId);
|
|
3354
|
+
const waiter = this.#waiters.get(rec.approvalId);
|
|
3355
|
+
this.#waiters.delete(rec.approvalId);
|
|
3356
|
+
const remote = rec.mode !== "window";
|
|
3357
|
+
if (decision === "deny") {
|
|
3358
|
+
rec.status = "denied";
|
|
3359
|
+
this.#d.store.save();
|
|
3360
|
+
if (!waiter) return;
|
|
3361
|
+
if (remote) return waiter({ decision: "deny", reason: QUESTION_SKIPPED });
|
|
3362
|
+
const notice2 = `\u23ED \u5DF2\u5728${deviceName ? `\u300C${deviceName}\u300D` : DEVICE_FALLBACK}\u4E0A\u8DF3\u8FC7\uFF1A${rec.summary.slice(0, 60)}`;
|
|
3363
|
+
if (!NO_NOTICE_TOOLS.has(rec.tool)) {
|
|
3364
|
+
try {
|
|
3365
|
+
;
|
|
3366
|
+
(this.#d.writeNotice ?? writeNotice)(rec.sessionId, notice2);
|
|
3367
|
+
} catch {
|
|
3368
|
+
}
|
|
3369
|
+
}
|
|
3370
|
+
waiter({ decision: "deny", reason: QUESTION_SKIPPED, notice: notice2 });
|
|
3371
|
+
return;
|
|
3372
|
+
}
|
|
3373
|
+
const resolved = resolveAnswers(rec.toolInput, answers);
|
|
3374
|
+
const updatedInput = resolved ? answersToUpdatedInput(rec.toolInput, answers) : null;
|
|
3375
|
+
if (!resolved || !updatedInput) {
|
|
3376
|
+
rec.status = "cancelled";
|
|
3377
|
+
this.#d.store.save();
|
|
3378
|
+
this.#d.log(`\u9009\u62E9\u9898 ${rec.approvalId} \u7684\u7B54\u6848\u5BF9\u4E0D\u4E0A\u9898\u76EE\uFF0C${remote ? "\u62D2\u7EDD" : "\u4E0D\u8868\u6001"}`);
|
|
3379
|
+
waiter?.(remote ? { decision: "deny", reason: QUESTION_UNANSWERABLE } : { decision: "none" });
|
|
3380
|
+
return;
|
|
3381
|
+
}
|
|
3382
|
+
rec.status = "approved";
|
|
3383
|
+
this.#d.store.save();
|
|
3384
|
+
if (!waiter) return;
|
|
3385
|
+
if (remote) return waiter({ decision: "allow", updatedInput });
|
|
3386
|
+
const notice = questionNotice(answersSummary(updatedInput), deviceName);
|
|
3387
|
+
if (!NO_NOTICE_TOOLS.has(rec.tool)) {
|
|
3388
|
+
try {
|
|
3389
|
+
;
|
|
3390
|
+
(this.#d.writeNotice ?? writeNotice)(rec.sessionId, notice);
|
|
3391
|
+
} catch {
|
|
3392
|
+
}
|
|
3393
|
+
}
|
|
3394
|
+
waiter({ decision: "allow", notice, updatedInput, answerLists: resolved.map((r) => r.items) });
|
|
3395
|
+
}
|
|
2976
3396
|
#readState(sessionId) {
|
|
2977
3397
|
try {
|
|
2978
3398
|
return (this.#d.readState ?? readState)(sessionId);
|
|
@@ -2980,15 +3400,20 @@ var ApprovalCoordinator = class {
|
|
|
2980
3400
|
return null;
|
|
2981
3401
|
}
|
|
2982
3402
|
}
|
|
2983
|
-
/** 设备上按了允许/拒绝(经服务端下发,或重连后 sync
|
|
2984
|
-
onDecision(approvalId, decision, deviceName = "") {
|
|
3403
|
+
/** 设备上按了允许/拒绝(经服务端下发,或重连后 sync 补到)。选择题的答案在 answers 里(下标) */
|
|
3404
|
+
onDecision(approvalId, decision, deviceName = "", answers) {
|
|
2985
3405
|
const rec = this.#d.store.get(approvalId);
|
|
2986
3406
|
if (!rec) {
|
|
2987
3407
|
this.#d.log(`\u6536\u5230\u672A\u77E5\u6388\u6743 ${approvalId} \u7684\u51B3\u5B9A\uFF0C\u5FFD\u7565`);
|
|
2988
3408
|
return;
|
|
2989
3409
|
}
|
|
2990
3410
|
if (rec.status !== "pending") return;
|
|
2991
|
-
|
|
3411
|
+
if (rec.kind === "question") {
|
|
3412
|
+
this.#onQuestionDecision(rec, decision, deviceName, answers);
|
|
3413
|
+
return;
|
|
3414
|
+
}
|
|
3415
|
+
const updates = rememberUpdatesOf(rec);
|
|
3416
|
+
const always = decision === "allow_always" && rec.mode === "window" && updates !== null;
|
|
2992
3417
|
if (decision === "allow_always") decision = "allow";
|
|
2993
3418
|
rec.status = decision === "allow" ? "approved" : "denied";
|
|
2994
3419
|
rec.decidedAt = this.#now();
|
|
@@ -3000,13 +3425,15 @@ var ApprovalCoordinator = class {
|
|
|
3000
3425
|
this.#d.store.save();
|
|
3001
3426
|
if (rec.mode === "window") {
|
|
3002
3427
|
const notice = windowNotice(decision, rec.summary, deviceName) + (always ? "\uFF08\u5DF2\u8BB0\u4F4F\uFF09" : "");
|
|
3003
|
-
|
|
3004
|
-
|
|
3005
|
-
|
|
3006
|
-
|
|
3428
|
+
if (!NO_NOTICE_TOOLS.has(rec.tool)) {
|
|
3429
|
+
try {
|
|
3430
|
+
;
|
|
3431
|
+
(this.#d.writeNotice ?? writeNotice)(rec.sessionId, notice);
|
|
3432
|
+
} catch {
|
|
3433
|
+
}
|
|
3007
3434
|
}
|
|
3008
3435
|
waiter(
|
|
3009
|
-
decision === "allow" ? { decision: "allow", notice, ...always ? { updatedPermissions:
|
|
3436
|
+
decision === "allow" ? { decision: "allow", notice, ...always && updates ? { updatedPermissions: updates } : {} } : (
|
|
3010
3437
|
// 🔴 交给模型的原因里不带设备名:名字是家庭成员在 App 里起的,别让它进提示词。设备名只进给人看的回显
|
|
3011
3438
|
{ decision: "deny", reason: "\u7528\u6237\u5728 DeskBuddy \u8BBE\u5907\u4E0A\u62D2\u7EDD\u4E86\u8FD9\u4E00\u6B65\uFF0C\u4E0D\u8981\u91CD\u8BD5\u8FD9\u4E2A\u64CD\u4F5C", notice }
|
|
3012
3439
|
)
|
|
@@ -3031,6 +3458,16 @@ var ApprovalCoordinator = class {
|
|
|
3031
3458
|
this.#d.store.save();
|
|
3032
3459
|
if (decision === "allow") this.#d.resume(rec, "orphan");
|
|
3033
3460
|
}
|
|
3461
|
+
/**
|
|
3462
|
+
* 认出某个后台进程其实在跑 sessionId 这个会话(它还没进过关卡、run 上没记会话号,例如按进程号认出来的新建任务):
|
|
3463
|
+
* 记上会话号并标成已被取代 —— 之后它再进关卡立即被拒,不会在 hold 上挂住、让排在它后面的回复等 7 天
|
|
3464
|
+
*/
|
|
3465
|
+
supersedeRun(runId, sessionId) {
|
|
3466
|
+
const run2 = this.#runs.get(runId);
|
|
3467
|
+
if (!run2) return;
|
|
3468
|
+
if (!run2.sessionId) run2.sessionId = sessionId;
|
|
3469
|
+
run2.superseded = true;
|
|
3470
|
+
}
|
|
3034
3471
|
/**
|
|
3035
3472
|
* 同一会话来了新指令:等待中的那一步按"没批准"处理(spec E9,和窗口里弹框时直接打字一样)。
|
|
3036
3473
|
* 🔴 必须在新指令【入队之前】调:hold 模式下旧进程还占着这个会话的串行队列,
|
|
@@ -3113,7 +3550,7 @@ var ApprovalCoordinator = class {
|
|
|
3113
3550
|
const got = await this.#d.backend.sync(ids.slice(i, i + 50));
|
|
3114
3551
|
for (const g of got) {
|
|
3115
3552
|
if (g.decision === "cancelled") this.#markCancelled(g.approvalId);
|
|
3116
|
-
else this.onDecision(g.approvalId, g.decision, g.deviceName);
|
|
3553
|
+
else this.onDecision(g.approvalId, g.decision, g.deviceName, g.answers);
|
|
3117
3554
|
}
|
|
3118
3555
|
} catch (e) {
|
|
3119
3556
|
this.#d.log(`\u540C\u6B65\u6388\u6743\u72B6\u6001\u5931\u8D25\uFF1A${e.message}`);
|
|
@@ -3204,13 +3641,15 @@ function tokenEquals(a, b) {
|
|
|
3204
3641
|
}
|
|
3205
3642
|
var GateServer = class {
|
|
3206
3643
|
#handler;
|
|
3644
|
+
#notify;
|
|
3207
3645
|
#token = randomBytes3(32).toString("hex");
|
|
3208
3646
|
#server = null;
|
|
3209
3647
|
#inflight = 0;
|
|
3210
3648
|
/** 自动升级排空中:所有请求直接 503(远程 gate → exit 2 拦下;窗口 gate → 退回窗口里本地确认) */
|
|
3211
3649
|
refusing = false;
|
|
3212
|
-
constructor(handler) {
|
|
3650
|
+
constructor(handler, notify = null) {
|
|
3213
3651
|
this.#handler = handler;
|
|
3652
|
+
this.#notify = notify;
|
|
3214
3653
|
}
|
|
3215
3654
|
get token() {
|
|
3216
3655
|
return this.#token;
|
|
@@ -3222,9 +3661,9 @@ var GateServer = class {
|
|
|
3222
3661
|
/** 起监听并写 gate.json。返回实际端口 */
|
|
3223
3662
|
async start() {
|
|
3224
3663
|
const server = createServer((req2, res) => void this.#serve(req2, res));
|
|
3225
|
-
await new Promise((
|
|
3664
|
+
await new Promise((resolve7, reject) => {
|
|
3226
3665
|
server.once("error", reject);
|
|
3227
|
-
server.listen(0, "127.0.0.1", () =>
|
|
3666
|
+
server.listen(0, "127.0.0.1", () => resolve7());
|
|
3228
3667
|
});
|
|
3229
3668
|
this.#server = server;
|
|
3230
3669
|
const addr = server.address();
|
|
@@ -3245,7 +3684,7 @@ var GateServer = class {
|
|
|
3245
3684
|
}
|
|
3246
3685
|
const s = this.#server;
|
|
3247
3686
|
this.#server = null;
|
|
3248
|
-
if (s) await new Promise((
|
|
3687
|
+
if (s) await new Promise((resolve7) => s.close(() => resolve7()));
|
|
3249
3688
|
}
|
|
3250
3689
|
async #serve(req2, res) {
|
|
3251
3690
|
const reply = (status, body2) => {
|
|
@@ -3275,6 +3714,14 @@ var GateServer = class {
|
|
|
3275
3714
|
return reply(400);
|
|
3276
3715
|
}
|
|
3277
3716
|
if (!parsed.hook || typeof parsed.hook !== "object") return reply(400);
|
|
3717
|
+
if (parsed.mode === "notify") {
|
|
3718
|
+
if (typeof parsed.tool !== "string" || !/^[a-z_]{1,32}$/.test(parsed.tool)) return reply(400);
|
|
3719
|
+
try {
|
|
3720
|
+
this.#notify?.({ tool: parsed.tool, hook: parsed.hook });
|
|
3721
|
+
} catch {
|
|
3722
|
+
}
|
|
3723
|
+
return reply(200, { decision: "none" });
|
|
3724
|
+
}
|
|
3278
3725
|
const window = parsed.mode === "window";
|
|
3279
3726
|
if (!window && typeof parsed.runId !== "string") return reply(400);
|
|
3280
3727
|
const ac = new AbortController();
|
|
@@ -3357,7 +3804,7 @@ function buildHandshakeUrl(cfg, opts = {}) {
|
|
|
3357
3804
|
}
|
|
3358
3805
|
|
|
3359
3806
|
// src/daemon/log.ts
|
|
3360
|
-
import { appendFileSync, mkdirSync as mkdirSync12, statSync as
|
|
3807
|
+
import { appendFileSync, mkdirSync as mkdirSync12, statSync as statSync5, truncateSync } from "fs";
|
|
3361
3808
|
import { join as join16 } from "path";
|
|
3362
3809
|
var MAX_LOG_BYTES = 2 * 1024 * 1024;
|
|
3363
3810
|
function logPath() {
|
|
@@ -3365,7 +3812,7 @@ function logPath() {
|
|
|
3365
3812
|
}
|
|
3366
3813
|
function rotateIfNeeded(path) {
|
|
3367
3814
|
try {
|
|
3368
|
-
if (
|
|
3815
|
+
if (statSync5(path).size > MAX_LOG_BYTES) truncateSync(path, 0);
|
|
3369
3816
|
} catch {
|
|
3370
3817
|
}
|
|
3371
3818
|
}
|
|
@@ -3430,42 +3877,6 @@ var SessionQueue = class {
|
|
|
3430
3877
|
}
|
|
3431
3878
|
};
|
|
3432
3879
|
|
|
3433
|
-
// src/daemon/remote.ts
|
|
3434
|
-
import { mkdirSync as mkdirSync13, readFileSync as readFileSync12, renameSync as renameSync9, writeFileSync as writeFileSync11 } from "fs";
|
|
3435
|
-
import { join as join17 } from "path";
|
|
3436
|
-
function handleDir() {
|
|
3437
|
-
return join17(deskbuddyHome(), "remote");
|
|
3438
|
-
}
|
|
3439
|
-
function handlePath(tool) {
|
|
3440
|
-
const safe = (tool || "").replace(/[^0-9a-zA-Z_-]/g, "");
|
|
3441
|
-
return join17(handleDir(), `${safe || "unknown"}.json`);
|
|
3442
|
-
}
|
|
3443
|
-
function readHandle(tool) {
|
|
3444
|
-
try {
|
|
3445
|
-
const h = JSON.parse(readFileSync12(handlePath(tool), "utf8"));
|
|
3446
|
-
if (typeof h?.sessionId !== "string" || !h.sessionId) return null;
|
|
3447
|
-
if (typeof h?.cwd !== "string" || !h.cwd) return null;
|
|
3448
|
-
return h;
|
|
3449
|
-
} catch {
|
|
3450
|
-
return null;
|
|
3451
|
-
}
|
|
3452
|
-
}
|
|
3453
|
-
function writeHandle(tool, handle) {
|
|
3454
|
-
try {
|
|
3455
|
-
mkdirSync13(handleDir(), { recursive: true });
|
|
3456
|
-
const tmp = handlePath(tool) + ".tmp";
|
|
3457
|
-
writeFileSync11(tmp, JSON.stringify(handle), "utf8");
|
|
3458
|
-
renameSync9(tmp, handlePath(tool));
|
|
3459
|
-
} catch {
|
|
3460
|
-
}
|
|
3461
|
-
}
|
|
3462
|
-
function handleIsUsable(handle) {
|
|
3463
|
-
const s = readState(handle.sessionId);
|
|
3464
|
-
if (!s) return false;
|
|
3465
|
-
if (s.pid && !isAlive(s.pid, s.pidName)) return false;
|
|
3466
|
-
return true;
|
|
3467
|
-
}
|
|
3468
|
-
|
|
3469
3880
|
// src/daemon/resolve.ts
|
|
3470
3881
|
function resolveTarget(cmd, deps) {
|
|
3471
3882
|
if (cmd.sessionId) {
|
|
@@ -3474,23 +3885,16 @@ function resolveTarget(cmd, deps) {
|
|
|
3474
3885
|
if (!state) return { ok: false, reason: "session_gone" };
|
|
3475
3886
|
if (!state.cwd) return { ok: false, reason: "no_cwd" };
|
|
3476
3887
|
if (deps.windowOpen?.(state)) return { ok: false, reason: "window_open" };
|
|
3477
|
-
return { ok: true, sessionId, cwd: state.cwd
|
|
3888
|
+
return { ok: true, sessionId, cwd: state.cwd };
|
|
3478
3889
|
}
|
|
3479
3890
|
let dirCwd = "";
|
|
3480
3891
|
if (cmd.dirRef) {
|
|
3481
3892
|
dirCwd = deps.dirOf ? deps.dirOf(cmd.dirRef) : "";
|
|
3482
3893
|
if (!dirCwd) return { ok: false, reason: "dir_gone" };
|
|
3483
3894
|
}
|
|
3484
|
-
const handleKey = cmd.dirRef ? `${cmd.tool}_${cmd.dirRef}` : cmd.tool;
|
|
3485
|
-
const handle = deps.readHandle(handleKey);
|
|
3486
|
-
const handleState = handle ? deps.readState(handle.sessionId) : null;
|
|
3487
|
-
const handleInWindow = !!(handleState && deps.windowOpen?.(handleState));
|
|
3488
|
-
if (handle && !handleInWindow && deps.handleIsUsable(handle)) {
|
|
3489
|
-
return { ok: true, sessionId: handle.sessionId, cwd: handle.cwd, handleKey };
|
|
3490
|
-
}
|
|
3491
3895
|
const cwd = dirCwd || deps.remoteCwd();
|
|
3492
3896
|
if (!cwd) return { ok: false, reason: "no_remote_cwd" };
|
|
3493
|
-
return { ok: true, sessionId: "", cwd
|
|
3897
|
+
return { ok: true, sessionId: "", cwd };
|
|
3494
3898
|
}
|
|
3495
3899
|
function failureMessage(reason) {
|
|
3496
3900
|
switch (reason) {
|
|
@@ -3508,11 +3912,11 @@ function failureMessage(reason) {
|
|
|
3508
3912
|
}
|
|
3509
3913
|
|
|
3510
3914
|
// src/daemon/seen.ts
|
|
3511
|
-
import { mkdirSync as
|
|
3512
|
-
import { join as
|
|
3915
|
+
import { mkdirSync as mkdirSync13, readFileSync as readFileSync12, renameSync as renameSync9, writeFileSync as writeFileSync11 } from "fs";
|
|
3916
|
+
import { join as join17 } from "path";
|
|
3513
3917
|
var SEEN_CAPACITY = 200;
|
|
3514
3918
|
function seenPath() {
|
|
3515
|
-
return
|
|
3919
|
+
return join17(deskbuddyHome(), "seen.json");
|
|
3516
3920
|
}
|
|
3517
3921
|
var SeenIds = class _SeenIds {
|
|
3518
3922
|
#ids = [];
|
|
@@ -3520,55 +3924,1312 @@ var SeenIds = class _SeenIds {
|
|
|
3520
3924
|
constructor(initial = []) {
|
|
3521
3925
|
for (const id of initial) this.#push(id);
|
|
3522
3926
|
}
|
|
3523
|
-
#push(id) {
|
|
3524
|
-
if (this.#set.has(id)) return;
|
|
3525
|
-
this.#ids.push(id);
|
|
3526
|
-
this.#set.add(id);
|
|
3527
|
-
while (this.#ids.length > SEEN_CAPACITY) {
|
|
3528
|
-
const dropped = this.#ids.shift();
|
|
3529
|
-
if (dropped !== void 0) this.#set.delete(dropped);
|
|
3927
|
+
#push(id) {
|
|
3928
|
+
if (this.#set.has(id)) return;
|
|
3929
|
+
this.#ids.push(id);
|
|
3930
|
+
this.#set.add(id);
|
|
3931
|
+
while (this.#ids.length > SEEN_CAPACITY) {
|
|
3932
|
+
const dropped = this.#ids.shift();
|
|
3933
|
+
if (dropped !== void 0) this.#set.delete(dropped);
|
|
3934
|
+
}
|
|
3935
|
+
}
|
|
3936
|
+
has(id) {
|
|
3937
|
+
return this.#set.has(id);
|
|
3938
|
+
}
|
|
3939
|
+
/** 记下来。返回 true 表示这是**新的**(之前没见过) */
|
|
3940
|
+
remember(id) {
|
|
3941
|
+
if (this.#set.has(id)) return false;
|
|
3942
|
+
this.#push(id);
|
|
3943
|
+
return true;
|
|
3944
|
+
}
|
|
3945
|
+
snapshot() {
|
|
3946
|
+
return [...this.#ids];
|
|
3947
|
+
}
|
|
3948
|
+
static load() {
|
|
3949
|
+
try {
|
|
3950
|
+
const raw = JSON.parse(readFileSync12(seenPath(), "utf8"));
|
|
3951
|
+
return new _SeenIds(Array.isArray(raw?.ids) ? raw.ids.filter((x) => typeof x === "string") : []);
|
|
3952
|
+
} catch {
|
|
3953
|
+
return new _SeenIds();
|
|
3954
|
+
}
|
|
3955
|
+
}
|
|
3956
|
+
save() {
|
|
3957
|
+
try {
|
|
3958
|
+
mkdirSync13(deskbuddyHome(), { recursive: true });
|
|
3959
|
+
const tmp = seenPath() + ".tmp";
|
|
3960
|
+
writeFileSync11(tmp, JSON.stringify({ ids: this.#ids }), "utf8");
|
|
3961
|
+
renameSync9(tmp, seenPath());
|
|
3962
|
+
} catch {
|
|
3963
|
+
}
|
|
3964
|
+
}
|
|
3965
|
+
};
|
|
3966
|
+
|
|
3967
|
+
// src/daemon/qwenworkBridge.ts
|
|
3968
|
+
import { readFileSync as readFileSync13 } from "fs";
|
|
3969
|
+
import { homedir as homedir7 } from "os";
|
|
3970
|
+
import { basename as basename7, dirname as dirname9, join as join18, resolve as resolve3 } from "path";
|
|
3971
|
+
var SUBMIT_TOOL = "qwenwork_task_submit_response";
|
|
3972
|
+
var DETAIL_TOOL = "qwenwork_task_get_detail";
|
|
3973
|
+
var POLL_MS = 2e3;
|
|
3974
|
+
var REQUEST_TIMEOUT_MS = 5e3;
|
|
3975
|
+
var EARLY_WINDOW_MS = 10 * 1e3;
|
|
3976
|
+
var EARLY_RETRY_MS = 1e3;
|
|
3977
|
+
var EARLY_RETRIES = 3;
|
|
3978
|
+
var SEEN_TTL_MS = 60 * 60 * 1e3;
|
|
3979
|
+
var DENY_MESSAGE = "\u7528\u6237\u5728 DeskBuddy \u8BBE\u5907\u4E0A\u62D2\u7EDD\u4E86\u8FD9\u4E00\u6B65\uFF0C\u4E0D\u8981\u91CD\u8BD5\u8FD9\u4E2A\u64CD\u4F5C";
|
|
3980
|
+
var TERMINAL_STATUS = /* @__PURE__ */ new Set(["completed", "failed", "cancelled", "canceled", "error", "stopped", "aborted"]);
|
|
3981
|
+
var PENDING_STATES = /* @__PURE__ */ new Set(["call", "approval-requested", "awaiting-approval", "waiting-for-confirmation", "pending-confirmation"]);
|
|
3982
|
+
var DETAIL_LIMIT = 5;
|
|
3983
|
+
var NO_ID_DEDUPE_MS = 5 * 1e3;
|
|
3984
|
+
var TOOL_DONE_STATES = /* @__PURE__ */ new Set(["output-available", "output-error", "output-denied", "result", "error", "denied", "rejected"]);
|
|
3985
|
+
function isResolvedElsewhere(text) {
|
|
3986
|
+
return /already been resolved|no pending/i.test(text);
|
|
3987
|
+
}
|
|
3988
|
+
function isLoopbackHttp(url) {
|
|
3989
|
+
try {
|
|
3990
|
+
const u = new URL(url);
|
|
3991
|
+
return (u.protocol === "http:" || u.protocol === "https:") && ["127.0.0.1", "localhost", "[::1]"].includes(u.hostname);
|
|
3992
|
+
} catch {
|
|
3993
|
+
return false;
|
|
3994
|
+
}
|
|
3995
|
+
}
|
|
3996
|
+
function parseMcpConf(raw) {
|
|
3997
|
+
if (!raw) return null;
|
|
3998
|
+
let c;
|
|
3999
|
+
try {
|
|
4000
|
+
c = JSON.parse(raw);
|
|
4001
|
+
} catch {
|
|
4002
|
+
return null;
|
|
4003
|
+
}
|
|
4004
|
+
const url = typeof c.url === "string" ? c.url : "";
|
|
4005
|
+
if (!url || !isLoopbackHttp(url)) return null;
|
|
4006
|
+
const h = c.headers && typeof c.headers === "object" ? c.headers : {};
|
|
4007
|
+
const key = typeof h["x-api-key"] === "string" && h["x-api-key"] ? h["x-api-key"] : typeof c.token === "string" ? c.token : "";
|
|
4008
|
+
if (!key) return null;
|
|
4009
|
+
return { url, key };
|
|
4010
|
+
}
|
|
4011
|
+
function sameDir(a, b) {
|
|
4012
|
+
return process.platform === "linux" ? a === b : a.toLowerCase() === b.toLowerCase();
|
|
4013
|
+
}
|
|
4014
|
+
function chatIdOf(cwd, root) {
|
|
4015
|
+
if (typeof cwd !== "string" || !cwd) return null;
|
|
4016
|
+
const dir = resolve3(cwd);
|
|
4017
|
+
if (!sameDir(dirname9(dir), resolve3(root))) return null;
|
|
4018
|
+
const id = basename7(dir);
|
|
4019
|
+
return /^[A-Za-z0-9_-]{1,64}$/.test(id) ? id : null;
|
|
4020
|
+
}
|
|
4021
|
+
function submitAnswers(toolInput, lists) {
|
|
4022
|
+
const qs = toolInput?.questions;
|
|
4023
|
+
if (!Array.isArray(qs) || !Array.isArray(lists) || lists.length !== qs.length) return null;
|
|
4024
|
+
const out = {};
|
|
4025
|
+
for (let i = 0; i < qs.length; i++) {
|
|
4026
|
+
const q = qs[i] ?? {};
|
|
4027
|
+
const items = lists[i];
|
|
4028
|
+
if (typeof q.question !== "string" || !q.question || !Array.isArray(items) || items.length === 0) return null;
|
|
4029
|
+
if (!items.every((x) => typeof x === "string")) return null;
|
|
4030
|
+
out[q.question] = items.join(", ");
|
|
4031
|
+
}
|
|
4032
|
+
return out;
|
|
4033
|
+
}
|
|
4034
|
+
function parseRpcBody(text, contentType) {
|
|
4035
|
+
const tryJson = (s) => {
|
|
4036
|
+
try {
|
|
4037
|
+
const v = JSON.parse(s);
|
|
4038
|
+
return v && typeof v === "object" ? v : null;
|
|
4039
|
+
} catch {
|
|
4040
|
+
return null;
|
|
4041
|
+
}
|
|
4042
|
+
};
|
|
4043
|
+
if (!/event-stream/i.test(contentType)) return tryJson(text);
|
|
4044
|
+
let last = null;
|
|
4045
|
+
for (const line of text.split("\n")) {
|
|
4046
|
+
if (!line.startsWith("data:")) continue;
|
|
4047
|
+
const v = tryJson(line.slice(5).trim());
|
|
4048
|
+
if (v && (v.result !== void 0 || v.error !== void 0)) last = v;
|
|
4049
|
+
}
|
|
4050
|
+
return last;
|
|
4051
|
+
}
|
|
4052
|
+
var QwenWorkMcp = class {
|
|
4053
|
+
#read;
|
|
4054
|
+
#fetch;
|
|
4055
|
+
#log;
|
|
4056
|
+
/** 已核对过工具清单的那个 url + key */
|
|
4057
|
+
#checked = null;
|
|
4058
|
+
#session = "";
|
|
4059
|
+
#nextId = 1;
|
|
4060
|
+
constructor(read, fetchFn, log2) {
|
|
4061
|
+
this.#read = read;
|
|
4062
|
+
this.#fetch = fetchFn;
|
|
4063
|
+
this.#log = log2;
|
|
4064
|
+
}
|
|
4065
|
+
async #rpc(conf, method, params, notification = false) {
|
|
4066
|
+
const headers = {
|
|
4067
|
+
"content-type": "application/json",
|
|
4068
|
+
accept: "application/json, text/event-stream",
|
|
4069
|
+
"x-api-key": conf.key
|
|
4070
|
+
};
|
|
4071
|
+
if (this.#session) headers["mcp-session-id"] = this.#session;
|
|
4072
|
+
const msg = { jsonrpc: "2.0", method, ...params ? { params } : {} };
|
|
4073
|
+
if (!notification) msg.id = this.#nextId++;
|
|
4074
|
+
const ac = new AbortController();
|
|
4075
|
+
const timer = setTimeout(() => ac.abort(), REQUEST_TIMEOUT_MS);
|
|
4076
|
+
timer.unref?.();
|
|
4077
|
+
try {
|
|
4078
|
+
const r = await this.#fetch(conf.url, { method: "POST", headers, body: JSON.stringify(msg), signal: ac.signal });
|
|
4079
|
+
const sid = r.headers.get("mcp-session-id");
|
|
4080
|
+
if (sid) this.#session = sid;
|
|
4081
|
+
const text = await r.text();
|
|
4082
|
+
if (r.status === 202 && notification) return { ok: true, body: null };
|
|
4083
|
+
if (r.status < 200 || r.status >= 300) return { ok: false, message: `HTTP ${r.status}` };
|
|
4084
|
+
if (notification) return { ok: true, body: null };
|
|
4085
|
+
return { ok: true, body: parseRpcBody(text, r.headers.get("content-type") ?? "") };
|
|
4086
|
+
} catch (e) {
|
|
4087
|
+
return { ok: false, message: e?.name === "AbortError" ? "timeout" : String(e?.message ?? e) };
|
|
4088
|
+
} finally {
|
|
4089
|
+
clearTimeout(timer);
|
|
4090
|
+
}
|
|
4091
|
+
}
|
|
4092
|
+
/** 在途的握手(同一 url + key 只握一次) */
|
|
4093
|
+
#handshake = null;
|
|
4094
|
+
/** 连得上且有 submit_response。返回 config(给后续调用用),不行返回失败原因 */
|
|
4095
|
+
async ensure() {
|
|
4096
|
+
const conf = parseMcpConf(this.#read());
|
|
4097
|
+
if (!conf) return { ok: false, why: "no_config", message: "\u8BFB\u4E0D\u5230\u5343\u95EE\u529E\u516C\u7684 MCP \u914D\u7F6E\uFF08\u6216\u5730\u5740\u4E0D\u662F\u672C\u673A\u56DE\u73AF\uFF09" };
|
|
4098
|
+
const c = this.#checked;
|
|
4099
|
+
if (c && c.url === conf.url && c.key === conf.key) {
|
|
4100
|
+
return c.ok ? { ok: true, conf } : { ok: false, why: "unsupported", message: `\u5343\u95EE\u529E\u516C MCP \u6CA1\u6709 ${SUBMIT_TOOL}` };
|
|
4101
|
+
}
|
|
4102
|
+
const h = this.#handshake;
|
|
4103
|
+
if (h && h.url === conf.url && h.key === conf.key) return h.p;
|
|
4104
|
+
const p = this.#doHandshake(conf);
|
|
4105
|
+
this.#handshake = { url: conf.url, key: conf.key, p };
|
|
4106
|
+
try {
|
|
4107
|
+
return await p;
|
|
4108
|
+
} finally {
|
|
4109
|
+
if (this.#handshake?.p === p) this.#handshake = null;
|
|
4110
|
+
}
|
|
4111
|
+
}
|
|
4112
|
+
async #doHandshake(conf) {
|
|
4113
|
+
this.#session = "";
|
|
4114
|
+
const init = await this.#rpc(conf, "initialize", {
|
|
4115
|
+
protocolVersion: "2025-03-26",
|
|
4116
|
+
capabilities: {},
|
|
4117
|
+
clientInfo: { name: "deskbuddy", version: "1" }
|
|
4118
|
+
});
|
|
4119
|
+
if (!init.ok) return { ok: false, why: "network", message: `\u8FDE\u4E0D\u4E0A\u5343\u95EE\u529E\u516C MCP\uFF1A${init.message}` };
|
|
4120
|
+
if (!init.body || init.body.error) return { ok: false, why: "network", message: "\u5343\u95EE\u529E\u516C MCP \u521D\u59CB\u5316\u5931\u8D25" };
|
|
4121
|
+
await this.#rpc(conf, "notifications/initialized", void 0, true);
|
|
4122
|
+
const list = await this.#rpc(conf, "tools/list", {});
|
|
4123
|
+
if (!list.ok) return { ok: false, why: "network", message: `\u5343\u95EE\u529E\u516C MCP \u5DE5\u5177\u6E05\u5355\u53D6\u4E0D\u5230\uFF1A${list.message}` };
|
|
4124
|
+
const tools = list.body?.result?.tools ?? [];
|
|
4125
|
+
const ok = Array.isArray(tools) && tools.some((t) => t?.name === SUBMIT_TOOL);
|
|
4126
|
+
this.#checked = { url: conf.url, key: conf.key, ok };
|
|
4127
|
+
if (!ok) {
|
|
4128
|
+
this.#log(`\u5343\u95EE\u529E\u516C MCP \u6CA1\u6709 ${SUBMIT_TOOL}\uFF0C\u9009\u62E9\u9898 / \u6388\u6743\u4E0D\u63A8\u8BBE\u5907\uFF0C\u53EA\u4E0A\u62A5\u72B6\u6001`);
|
|
4129
|
+
return { ok: false, why: "unsupported", message: `\u5343\u95EE\u529E\u516C MCP \u6CA1\u6709 ${SUBMIT_TOOL}` };
|
|
4130
|
+
}
|
|
4131
|
+
return { ok: true, conf };
|
|
4132
|
+
}
|
|
4133
|
+
async call(name, args) {
|
|
4134
|
+
const e = await this.ensure();
|
|
4135
|
+
if (!e.ok) return e;
|
|
4136
|
+
const r = await this.#rpc(e.conf, "tools/call", { name, arguments: args });
|
|
4137
|
+
if (!r.ok) {
|
|
4138
|
+
this.#checked = null;
|
|
4139
|
+
return { ok: false, why: "network", message: r.message };
|
|
4140
|
+
}
|
|
4141
|
+
const body = r.body;
|
|
4142
|
+
if (!body) return { ok: false, why: "rpc", message: "\u56DE\u8BDD\u770B\u4E0D\u61C2" };
|
|
4143
|
+
if (body.error) {
|
|
4144
|
+
const m = body.error?.message;
|
|
4145
|
+
this.#checked = null;
|
|
4146
|
+
return { ok: false, why: "rpc", message: typeof m === "string" ? m : "error" };
|
|
4147
|
+
}
|
|
4148
|
+
const res = body.result ?? {};
|
|
4149
|
+
const content = Array.isArray(res.content) ? res.content : [];
|
|
4150
|
+
const text = content.map((c) => typeof c?.text === "string" ? c.text : "").join("\n");
|
|
4151
|
+
return { ok: true, text, isError: res.isError === true };
|
|
4152
|
+
}
|
|
4153
|
+
};
|
|
4154
|
+
var QwenWorkBridge = class {
|
|
4155
|
+
#d;
|
|
4156
|
+
#mcp;
|
|
4157
|
+
/** chatId → 正在等设备的那张卡(同一 chatId 同时只推一张) */
|
|
4158
|
+
#cards = /* @__PURE__ */ new Map();
|
|
4159
|
+
/** 推过的 (chatId, toolUseId) → 时间:同一道题 / 同一个授权框只推一次 */
|
|
4160
|
+
#seen = /* @__PURE__ */ new Map();
|
|
4161
|
+
/** 没带 toolUseId 的推送(只为日志限频) */
|
|
4162
|
+
#noIdLogged = /* @__PURE__ */ new Map();
|
|
4163
|
+
#timer = null;
|
|
4164
|
+
#polling = false;
|
|
4165
|
+
#stopped = false;
|
|
4166
|
+
constructor(deps) {
|
|
4167
|
+
this.#d = deps;
|
|
4168
|
+
const read = deps.readConfig ?? (() => readConfigFile(join18(qwenworkHome(), "mcp-adaptor.config")));
|
|
4169
|
+
const f = deps.fetch ?? globalThis.fetch;
|
|
4170
|
+
this.#mcp = new QwenWorkMcp(read, f, deps.log);
|
|
4171
|
+
}
|
|
4172
|
+
#now() {
|
|
4173
|
+
return this.#d.now ? this.#d.now() : Date.now();
|
|
4174
|
+
}
|
|
4175
|
+
/** 只给测试和诊断用 */
|
|
4176
|
+
get pending() {
|
|
4177
|
+
return this.#cards.size;
|
|
4178
|
+
}
|
|
4179
|
+
stop() {
|
|
4180
|
+
this.#stopped = true;
|
|
4181
|
+
if (this.#timer) clearInterval(this.#timer);
|
|
4182
|
+
this.#timer = null;
|
|
4183
|
+
this.#abortAll();
|
|
4184
|
+
}
|
|
4185
|
+
#abortAll() {
|
|
4186
|
+
for (const c of this.#cards.values()) c.ac.abort();
|
|
4187
|
+
this.#cards.clear();
|
|
4188
|
+
}
|
|
4189
|
+
/** hook 发来的 notify(GateServer mode=notify)。不抛错 */
|
|
4190
|
+
onNotify(req2) {
|
|
4191
|
+
void this.#onNotify(req2).catch((e) => this.#d.log(`\u5343\u95EE\u529E\u516C\u63A8\u5361\u51FA\u9519\uFF1A${e?.message ?? e}`));
|
|
4192
|
+
}
|
|
4193
|
+
async #onNotify(req2) {
|
|
4194
|
+
if (this.#stopped || req2.tool !== "qwenwork") return;
|
|
4195
|
+
if (this.#d.enabled && !this.#d.enabled()) return;
|
|
4196
|
+
const h = req2.hook;
|
|
4197
|
+
const event = h.hook_event_name;
|
|
4198
|
+
const sessionId = typeof h.session_id === "string" ? h.session_id : "";
|
|
4199
|
+
if (!sessionId) return;
|
|
4200
|
+
let kind;
|
|
4201
|
+
let toolName;
|
|
4202
|
+
let toolInput;
|
|
4203
|
+
let toolUseId;
|
|
4204
|
+
if (event === "PreToolUse" && h.tool_name === "AskUserQuestion") {
|
|
4205
|
+
kind = "question";
|
|
4206
|
+
toolName = "AskUserQuestion";
|
|
4207
|
+
toolInput = h.tool_input;
|
|
4208
|
+
toolUseId = typeof h.tool_use_id === "string" ? h.tool_use_id : "";
|
|
4209
|
+
} else if (event === "Notification" && h.notification_type === "permission_prompt") {
|
|
4210
|
+
const det = h.details && typeof h.details === "object" ? h.details : {};
|
|
4211
|
+
kind = "permission";
|
|
4212
|
+
toolName = typeof det.toolName === "string" ? det.toolName : "";
|
|
4213
|
+
toolInput = det.input && typeof det.input === "object" ? det.input : {};
|
|
4214
|
+
toolUseId = typeof det.toolUseId === "string" ? det.toolUseId : "";
|
|
4215
|
+
if (!toolName) return;
|
|
4216
|
+
} else return;
|
|
4217
|
+
const root = this.#d.workspaceRoot ?? join18(homedir7(), ".qwenworkcn", "workspace");
|
|
4218
|
+
const chatId = chatIdOf(h.cwd, root);
|
|
4219
|
+
if (!chatId) {
|
|
4220
|
+
this.#d.log("\u5343\u95EE\u529E\u516C\uFF1A\u8FD9\u9053\u9898 / \u6388\u6743\u4E0D\u5728\u4EFB\u52A1\u5DE5\u4F5C\u76EE\u5F55\u4E0B\uFF08\u53EF\u80FD\u662F\u5B50\u4EE3\u7406\uFF09\uFF0C\u4E0D\u63A8\u8BBE\u5907");
|
|
4221
|
+
return;
|
|
4222
|
+
}
|
|
4223
|
+
if (!toolUseId) {
|
|
4224
|
+
const fp = `${chatId}:${kind}`;
|
|
4225
|
+
const last = this.#noIdLogged.get(fp) ?? 0;
|
|
4226
|
+
if (this.#now() - last > NO_ID_DEDUPE_MS) this.#d.log(`\u5343\u95EE\u529E\u516C\uFF1A\u4EFB\u52A1 ${chatId.slice(0, 8)} \u7684${kind === "question" ? "\u9009\u62E9\u9898" : "\u6388\u6743"}\u6CA1\u5E26 toolUseId\uFF0C\u4E0D\u63A8\u8BBE\u5907`);
|
|
4227
|
+
this.#noIdLogged.set(fp, this.#now());
|
|
4228
|
+
return;
|
|
4229
|
+
}
|
|
4230
|
+
const key = `${chatId}:${toolUseId}`;
|
|
4231
|
+
this.#prune();
|
|
4232
|
+
if (this.#seen.has(key)) return;
|
|
4233
|
+
this.#seen.set(key, this.#now());
|
|
4234
|
+
this.#replaceOld(chatId, toolUseId);
|
|
4235
|
+
const e = await this.#mcp.ensure();
|
|
4236
|
+
if (!e.ok) {
|
|
4237
|
+
if (e.why !== "unsupported") this.#d.log(`\u5343\u95EE\u529E\u516C\uFF1A${e.message}\uFF0C\u8FD9\u4E00\u6761\u4E0D\u63A8\u8BBE\u5907`);
|
|
4238
|
+
return;
|
|
4239
|
+
}
|
|
4240
|
+
if (this.#stopped) return;
|
|
4241
|
+
this.#replaceOld(chatId, toolUseId);
|
|
4242
|
+
const card = { key, chatId, toolUseId, kind, toolInput, ac: new AbortController(), createdAt: this.#now() };
|
|
4243
|
+
this.#cards.set(chatId, card);
|
|
4244
|
+
this.#ensurePoll();
|
|
4245
|
+
const gateReq = {
|
|
4246
|
+
window: true,
|
|
4247
|
+
tool: "qwenwork",
|
|
4248
|
+
hook: {
|
|
4249
|
+
hook_event_name: "PermissionRequest",
|
|
4250
|
+
session_id: sessionId,
|
|
4251
|
+
cwd: h.cwd,
|
|
4252
|
+
tool_name: toolName,
|
|
4253
|
+
tool_input: toolInput,
|
|
4254
|
+
...kind === "question" && typeof h.transcript_path === "string" ? { transcript_path: h.transcript_path } : {}
|
|
4255
|
+
}
|
|
4256
|
+
};
|
|
4257
|
+
let reply;
|
|
4258
|
+
try {
|
|
4259
|
+
reply = await this.#d.gate(gateReq, card.ac.signal);
|
|
4260
|
+
} catch {
|
|
4261
|
+
if (this.#cards.get(chatId) === card) this.#cards.delete(chatId);
|
|
4262
|
+
return;
|
|
4263
|
+
}
|
|
4264
|
+
if (this.#cards.get(chatId) !== card || card.ac.signal.aborted) return;
|
|
4265
|
+
this.#cards.delete(chatId);
|
|
4266
|
+
await this.#submit(card, reply);
|
|
4267
|
+
}
|
|
4268
|
+
#replaceOld(chatId, toolUseId) {
|
|
4269
|
+
const old = this.#cards.get(chatId);
|
|
4270
|
+
if (!old || old.toolUseId === toolUseId) return;
|
|
4271
|
+
this.#cards.delete(chatId);
|
|
4272
|
+
this.#d.log(`\u5343\u95EE\u529E\u516C\uFF1A\u4EFB\u52A1 ${chatId.slice(0, 8)} \u51FA\u73B0\u65B0\u7684\u4E00\u6B65\uFF0C\u64A4\u6389\u65E7\u5361`);
|
|
4273
|
+
old.ac.abort();
|
|
4274
|
+
}
|
|
4275
|
+
/**
|
|
4276
|
+
* 提交前核对:界面上此刻在等的正是这张卡那一步(等待类状态的工具调用恰好一条、且 toolCallId 对得上)。
|
|
4277
|
+
* 对不上 / 判断不了一律返回原因(不提交)
|
|
4278
|
+
*/
|
|
4279
|
+
async #verifyPending(card) {
|
|
4280
|
+
const r = await this.#mcp.call(DETAIL_TOOL, { chatId: card.chatId, limit: DETAIL_LIMIT });
|
|
4281
|
+
if (!r.ok) return `\u67E5\u4E0D\u5230\u4EFB\u52A1\u8BE6\u60C5\uFF08${r.why}\uFF09`;
|
|
4282
|
+
return pendingMismatch(r.text, card.toolUseId);
|
|
4283
|
+
}
|
|
4284
|
+
async #submit(card, reply) {
|
|
4285
|
+
let args = null;
|
|
4286
|
+
if (card.kind === "question") {
|
|
4287
|
+
if (reply.decision !== "allow") return;
|
|
4288
|
+
const answers = submitAnswers(card.toolInput, reply.answerLists);
|
|
4289
|
+
if (!answers) {
|
|
4290
|
+
this.#d.log(`\u5343\u95EE\u529E\u516C\uFF1A\u4EFB\u52A1 ${card.chatId.slice(0, 8)} \u7684\u8BBE\u5907\u7B54\u6848\u62FC\u4E0D\u51FA\u6765\uFF0C\u754C\u9762\u7559\u7ED9\u4EBA\u7B54`);
|
|
4291
|
+
return;
|
|
4292
|
+
}
|
|
4293
|
+
args = { chatId: card.chatId, action: "answer", answers };
|
|
4294
|
+
} else if (reply.decision === "allow") {
|
|
4295
|
+
args = { chatId: card.chatId, action: "approve" };
|
|
4296
|
+
} else if (reply.decision === "deny") {
|
|
4297
|
+
args = { chatId: card.chatId, action: "deny", message: DENY_MESSAGE };
|
|
4298
|
+
} else return;
|
|
4299
|
+
if (!card.toolUseId) return;
|
|
4300
|
+
for (let attempt = 0; ; attempt++) {
|
|
4301
|
+
const why = await this.#verifyPending(card);
|
|
4302
|
+
if (why) {
|
|
4303
|
+
const early = this.#now() - card.createdAt < EARLY_WINDOW_MS && attempt < EARLY_RETRIES && /没有在等/.test(why);
|
|
4304
|
+
if (early) {
|
|
4305
|
+
await sleep(this.#d.retryMs ?? EARLY_RETRY_MS);
|
|
4306
|
+
continue;
|
|
4307
|
+
}
|
|
4308
|
+
this.#d.log(`\u5343\u95EE\u529E\u516C\uFF1A\u4EFB\u52A1 ${card.chatId.slice(0, 8)} ${why}\uFF0C\u8BBE\u5907\u7B54\u590D\u4E0D\u56DE\u586B\uFF08\u6309\u7535\u8111\u5DF2\u5904\u7406\uFF09`);
|
|
4309
|
+
return;
|
|
4310
|
+
}
|
|
4311
|
+
const r = await this.#mcp.call(SUBMIT_TOOL, args);
|
|
4312
|
+
if (!r.ok) {
|
|
4313
|
+
this.#d.log(`\u5343\u95EE\u529E\u516C\uFF1A\u56DE\u586B\u4EFB\u52A1 ${card.chatId.slice(0, 8)} \u5931\u8D25\uFF08${r.why}${r.message ? " " + r.message.slice(0, 80) : ""}\uFF09\uFF0C\u754C\u9762\u7559\u7ED9\u4EBA\u7B54`);
|
|
4314
|
+
return;
|
|
4315
|
+
}
|
|
4316
|
+
if (isResolvedElsewhere(r.text)) {
|
|
4317
|
+
const early = /no pending/i.test(r.text) && this.#now() - card.createdAt < EARLY_WINDOW_MS && attempt < EARLY_RETRIES;
|
|
4318
|
+
if (early) {
|
|
4319
|
+
await sleep(this.#d.retryMs ?? EARLY_RETRY_MS);
|
|
4320
|
+
continue;
|
|
4321
|
+
}
|
|
4322
|
+
this.#d.log(`\u5343\u95EE\u529E\u516C\uFF1A\u4EFB\u52A1 ${card.chatId.slice(0, 8)} \u7535\u8111\u4E0A\u5DF2\u5904\u7406\uFF0C\u8BBE\u5907\u7B54\u590D\u4E0D\u518D\u56DE\u586B`);
|
|
4323
|
+
return;
|
|
4324
|
+
}
|
|
4325
|
+
if (r.isError) {
|
|
4326
|
+
this.#d.log(`\u5343\u95EE\u529E\u516C\uFF1A\u56DE\u586B\u4EFB\u52A1 ${card.chatId.slice(0, 8)} \u88AB\u62D2\uFF1A${r.text.slice(0, 80)}`);
|
|
4327
|
+
return;
|
|
4328
|
+
}
|
|
4329
|
+
this.#d.log(`\u5343\u95EE\u529E\u516C\uFF1A\u4EFB\u52A1 ${card.chatId.slice(0, 8)} \u5DF2\u6309\u8BBE\u5907\u7B54\u590D\u56DE\u586B\uFF08${card.kind}\uFF09`);
|
|
4330
|
+
return;
|
|
4331
|
+
}
|
|
4332
|
+
}
|
|
4333
|
+
#prune() {
|
|
4334
|
+
const now = this.#now();
|
|
4335
|
+
for (const [k, at] of this.#seen) if (now - at > SEEN_TTL_MS) this.#seen.delete(k);
|
|
4336
|
+
for (const [k, at] of this.#noIdLogged) if (now - at > NO_ID_DEDUPE_MS) this.#noIdLogged.delete(k);
|
|
4337
|
+
}
|
|
4338
|
+
#ensurePoll() {
|
|
4339
|
+
if (this.#timer || this.#stopped) return;
|
|
4340
|
+
this.#timer = setInterval(() => void this.poll(), this.#d.pollMs ?? POLL_MS);
|
|
4341
|
+
this.#timer.unref?.();
|
|
4342
|
+
}
|
|
4343
|
+
/**
|
|
4344
|
+
* 待答期间查 get_detail:任务已不在跑、或这一步已经有结果(电脑上答过 / 批过 / 拒过)就撤卡。
|
|
4345
|
+
* 看不出来就不动(最迟靠 coordinator.scanWindows 在 PostToolUse / Stop 时撤)。MCP 连不上 → 全部撤卡
|
|
4346
|
+
*/
|
|
4347
|
+
async poll() {
|
|
4348
|
+
if (this.#polling) return;
|
|
4349
|
+
if (this.#cards.size === 0) {
|
|
4350
|
+
if (this.#timer) clearInterval(this.#timer);
|
|
4351
|
+
this.#timer = null;
|
|
4352
|
+
return;
|
|
4353
|
+
}
|
|
4354
|
+
this.#polling = true;
|
|
4355
|
+
try {
|
|
4356
|
+
for (const card of [...this.#cards.values()]) {
|
|
4357
|
+
if (this.#cards.get(card.chatId) !== card) continue;
|
|
4358
|
+
const r = await this.#mcp.call(DETAIL_TOOL, { chatId: card.chatId, limit: DETAIL_LIMIT });
|
|
4359
|
+
if (!r.ok) {
|
|
4360
|
+
if (r.why === "network" || r.why === "no_config") {
|
|
4361
|
+
this.#d.log(`\u5343\u95EE\u529E\u516C MCP \u8FDE\u4E0D\u4E0A\uFF08${r.message.slice(0, 80)}\uFF09\uFF0C\u64A4\u6389\u8BBE\u5907\u4E0A\u7B49\u7740\u7684\u5361`);
|
|
4362
|
+
this.#abortAll();
|
|
4363
|
+
return;
|
|
4364
|
+
}
|
|
4365
|
+
continue;
|
|
4366
|
+
}
|
|
4367
|
+
if (this.#cards.get(card.chatId) !== card) continue;
|
|
4368
|
+
const settled = detailSettled(r.text, card.toolUseId);
|
|
4369
|
+
if (settled) {
|
|
4370
|
+
this.#d.log(`\u5343\u95EE\u529E\u516C\uFF1A\u4EFB\u52A1 ${card.chatId.slice(0, 8)} ${settled}\uFF0C\u64A4\u5361`);
|
|
4371
|
+
this.#cards.delete(card.chatId);
|
|
4372
|
+
card.ac.abort();
|
|
4373
|
+
}
|
|
4374
|
+
}
|
|
4375
|
+
} finally {
|
|
4376
|
+
this.#polling = false;
|
|
4377
|
+
}
|
|
4378
|
+
}
|
|
4379
|
+
};
|
|
4380
|
+
function toolCallsOf(d) {
|
|
4381
|
+
const msgs = Array.isArray(d.messages) ? d.messages : [];
|
|
4382
|
+
return msgs.flatMap((m) => Array.isArray(m?.toolCalls) ? m.toolCalls : []);
|
|
4383
|
+
}
|
|
4384
|
+
function pendingIds(d) {
|
|
4385
|
+
return toolCallsOf(d).filter((c) => typeof c?.state === "string" && PENDING_STATES.has(c.state)).map((c) => typeof c.toolCallId === "string" ? c.toolCallId : "");
|
|
4386
|
+
}
|
|
4387
|
+
function pendingMismatch(text, toolUseId) {
|
|
4388
|
+
if (!toolUseId) return "\u6CA1\u6709 toolUseId\uFF0C\u6838\u5BF9\u4E0D\u4E86";
|
|
4389
|
+
let d;
|
|
4390
|
+
try {
|
|
4391
|
+
d = JSON.parse(text);
|
|
4392
|
+
} catch {
|
|
4393
|
+
return "\u4EFB\u52A1\u8BE6\u60C5\u770B\u4E0D\u61C2";
|
|
4394
|
+
}
|
|
4395
|
+
if (!d || typeof d !== "object" || d.success === false) return "\u4EFB\u52A1\u8BE6\u60C5\u770B\u4E0D\u61C2";
|
|
4396
|
+
const status = typeof d.status === "string" ? d.status.toLowerCase() : "";
|
|
4397
|
+
if (status && TERMINAL_STATUS.has(status)) return `\u4EFB\u52A1\u5DF2${status}`;
|
|
4398
|
+
const ids = pendingIds(d);
|
|
4399
|
+
if (ids.length === 0) {
|
|
4400
|
+
const mine = toolCallsOf(d).find((c) => c?.toolCallId === toolUseId);
|
|
4401
|
+
const st = mine && typeof mine.state === "string" ? mine.state : "\u672A\u627E\u5230";
|
|
4402
|
+
return `\u754C\u9762\u4E0A\u6CA1\u6709\u5728\u7B49\u7684\u4E00\u6B65\uFF08\u8FD9\u4E00\u6B65\u72B6\u6001\uFF1A${st.slice(0, 32)}\uFF09`;
|
|
4403
|
+
}
|
|
4404
|
+
if (ids.length > 1) return "\u754C\u9762\u4E0A\u540C\u65F6\u6709\u591A\u6B65\u5728\u7B49\uFF0C\u5206\u4E0D\u6E05";
|
|
4405
|
+
if (ids[0] !== toolUseId) return "\u754C\u9762\u4E0A\u5728\u7B49\u7684\u5DF2\u662F\u522B\u7684\u4E00\u6B65";
|
|
4406
|
+
return "";
|
|
4407
|
+
}
|
|
4408
|
+
function detailSettled(text, toolUseId) {
|
|
4409
|
+
let d;
|
|
4410
|
+
try {
|
|
4411
|
+
d = JSON.parse(text);
|
|
4412
|
+
} catch {
|
|
4413
|
+
return "";
|
|
4414
|
+
}
|
|
4415
|
+
if (!d || typeof d !== "object" || d.success === false) return "";
|
|
4416
|
+
const status = typeof d.status === "string" ? d.status.toLowerCase() : "";
|
|
4417
|
+
if (status && TERMINAL_STATUS.has(status)) return `\u4EFB\u52A1\u5DF2${status}`;
|
|
4418
|
+
if (!toolUseId) return "";
|
|
4419
|
+
for (const c of toolCallsOf(d)) {
|
|
4420
|
+
if (c?.toolCallId !== toolUseId) continue;
|
|
4421
|
+
const st = typeof c.state === "string" ? c.state : "";
|
|
4422
|
+
if (TOOL_DONE_STATES.has(st)) return `\u8FD9\u4E00\u6B65\u5DF2\u662F ${st}`;
|
|
4423
|
+
break;
|
|
4424
|
+
}
|
|
4425
|
+
const ids = pendingIds(d);
|
|
4426
|
+
if (ids.length > 0 && !ids.includes(toolUseId)) return "\u754C\u9762\u4E0A\u5728\u7B49\u7684\u5DF2\u662F\u522B\u7684\u4E00\u6B65";
|
|
4427
|
+
return "";
|
|
4428
|
+
}
|
|
4429
|
+
function readConfigFile(p) {
|
|
4430
|
+
try {
|
|
4431
|
+
return readFileSync13(p, "utf8");
|
|
4432
|
+
} catch {
|
|
4433
|
+
return null;
|
|
4434
|
+
}
|
|
4435
|
+
}
|
|
4436
|
+
function sleep(ms) {
|
|
4437
|
+
return new Promise((r) => setTimeout(r, ms).unref?.());
|
|
4438
|
+
}
|
|
4439
|
+
|
|
4440
|
+
// src/daemon/dumateWatch.ts
|
|
4441
|
+
import { execFileSync as execFileSync9, spawn as spawn3 } from "child_process";
|
|
4442
|
+
import { readdirSync as readdirSync4, realpathSync as realpathSync3, statSync as statSync6 } from "fs";
|
|
4443
|
+
import { request as httpRequest } from "http";
|
|
4444
|
+
import { join as join19 } from "path";
|
|
4445
|
+
|
|
4446
|
+
// hook/ocTranslate.mjs
|
|
4447
|
+
function toCcTool(tool, args) {
|
|
4448
|
+
const a = args && typeof args === "object" ? args : {};
|
|
4449
|
+
const s = (v) => typeof v === "string" ? v : "";
|
|
4450
|
+
switch (tool) {
|
|
4451
|
+
case "bash":
|
|
4452
|
+
return { tool_name: "Bash", tool_input: { command: s(a.command) } };
|
|
4453
|
+
case "read":
|
|
4454
|
+
return { tool_name: "Read", tool_input: { file_path: s(a.filePath) } };
|
|
4455
|
+
case "edit":
|
|
4456
|
+
return { tool_name: "Edit", tool_input: { file_path: s(a.filePath) } };
|
|
4457
|
+
case "write":
|
|
4458
|
+
return { tool_name: "Write", tool_input: { file_path: s(a.filePath) } };
|
|
4459
|
+
case "glob":
|
|
4460
|
+
return { tool_name: "Glob", tool_input: { pattern: s(a.pattern), path: s(a.path) } };
|
|
4461
|
+
case "grep":
|
|
4462
|
+
return { tool_name: "Grep", tool_input: { pattern: s(a.pattern), path: s(a.path) } };
|
|
4463
|
+
case "list":
|
|
4464
|
+
return { tool_name: "LS", tool_input: { path: s(a.path) } };
|
|
4465
|
+
case "webfetch":
|
|
4466
|
+
return { tool_name: "WebFetch", tool_input: { url: s(a.url) } };
|
|
4467
|
+
case "task":
|
|
4468
|
+
return { tool_name: "Task", tool_input: {} };
|
|
4469
|
+
case "todowrite":
|
|
4470
|
+
case "todoread":
|
|
4471
|
+
return { tool_name: "TodoWrite", tool_input: {} };
|
|
4472
|
+
case "patch":
|
|
4473
|
+
case "apply_patch":
|
|
4474
|
+
return { tool_name: "apply_patch", tool_input: { patch: s(a.patchText) || s(a.patch) || s(a.input) } };
|
|
4475
|
+
default:
|
|
4476
|
+
return { tool_name: String(tool || ""), tool_input: a };
|
|
4477
|
+
}
|
|
4478
|
+
}
|
|
4479
|
+
function askToCc(props) {
|
|
4480
|
+
const md = props && props.metadata || {};
|
|
4481
|
+
if (typeof md.command === "string" && md.command) return { tool_name: "Bash", tool_input: { command: md.command } };
|
|
4482
|
+
const file = md.filepath || md.filePath;
|
|
4483
|
+
if (typeof file === "string" && file) return { tool_name: "Edit", tool_input: { file_path: file } };
|
|
4484
|
+
const patterns = Array.isArray(props && props.patterns) ? props.patterns.join(" ") : "";
|
|
4485
|
+
return { tool_name: String(props && props.permission || "permission"), tool_input: { path: patterns } };
|
|
4486
|
+
}
|
|
4487
|
+
function isPlainQuestion(props, callTool) {
|
|
4488
|
+
if (callTool !== "question") return false;
|
|
4489
|
+
const qs = props && Array.isArray(props.questions) ? props.questions : [];
|
|
4490
|
+
return !qs.some((q) => q && (q.header === "Build Agent" || typeof q.question === "string" && /^Plan at .* is complete\./.test(q.question)));
|
|
4491
|
+
}
|
|
4492
|
+
function questionToCc(props) {
|
|
4493
|
+
const qs = props && Array.isArray(props.questions) ? props.questions : null;
|
|
4494
|
+
if (!qs || qs.length === 0) return null;
|
|
4495
|
+
return {
|
|
4496
|
+
tool_name: "AskUserQuestion",
|
|
4497
|
+
tool_input: {
|
|
4498
|
+
questions: qs.map((q) => ({
|
|
4499
|
+
question: q && q.question,
|
|
4500
|
+
header: q && q.header || "",
|
|
4501
|
+
multiSelect: !!(q && q.multiple),
|
|
4502
|
+
options: q && Array.isArray(q.options) ? q.options.map((o) => ({ label: o && o.label, description: o && o.description || "" })) : [],
|
|
4503
|
+
...q && q.custom === false ? { custom: false } : {}
|
|
4504
|
+
}))
|
|
4505
|
+
}
|
|
4506
|
+
};
|
|
4507
|
+
}
|
|
4508
|
+
var OC_DEFAULT_TITLE_RE = /^(New session - |Child session - )\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/;
|
|
4509
|
+
function rememberTitle(titles, info) {
|
|
4510
|
+
if (!info || typeof info.id !== "string" || !info.id || info.parentID) return;
|
|
4511
|
+
const t = typeof info.title === "string" ? info.title.trim() : "";
|
|
4512
|
+
if (!t || OC_DEFAULT_TITLE_RE.test(t)) return;
|
|
4513
|
+
titles.set(info.id, t.slice(0, 100));
|
|
4514
|
+
}
|
|
4515
|
+
|
|
4516
|
+
// src/daemon/dumateWatch.ts
|
|
4517
|
+
var ACTIVE_DIR_MS = 6 * 60 * 60 * 1e3;
|
|
4518
|
+
var MAX_POLL_DIRS = 12;
|
|
4519
|
+
var SESSION_LIST_MS = 10 * 1e3;
|
|
4520
|
+
var RESUBSCRIBE_MS = 5 * 1e3;
|
|
4521
|
+
var BUSY_WAIT_MS = 400;
|
|
4522
|
+
var QUESTION_TIMEOUT_MS = 120 * 1e3;
|
|
4523
|
+
var MISSING_GRACE_MS = 2 * 1e3;
|
|
4524
|
+
var REQUEST_TIMEOUT_MS2 = 5 * 1e3;
|
|
4525
|
+
var HOOK_KILL_MS = 10 * 1e3;
|
|
4526
|
+
var HANDLED_TTL_MS = 60 * 60 * 1e3;
|
|
4527
|
+
var SCAN_DIRS_MS = SESSION_LIST_MS;
|
|
4528
|
+
var DISCOVER_BACKOFF_MS = 10 * 1e3;
|
|
4529
|
+
var BUSY_DIR_MS = 10 * 60 * 1e3;
|
|
4530
|
+
var DAY_MS = 24 * 60 * 60 * 1e3;
|
|
4531
|
+
function questionDeadline(deadlineAt, timeoutMs, now) {
|
|
4532
|
+
if (typeof deadlineAt === "number" && Number.isFinite(deadlineAt) && deadlineAt > 0) {
|
|
4533
|
+
const ms = deadlineAt < 1e12 ? deadlineAt * 1e3 : deadlineAt;
|
|
4534
|
+
if (Math.abs(ms - now) <= DAY_MS) return ms;
|
|
4535
|
+
}
|
|
4536
|
+
if (typeof timeoutMs === "number" && Number.isFinite(timeoutMs) && timeoutMs >= 1e3 && timeoutMs <= DAY_MS) return now + timeoutMs;
|
|
4537
|
+
return now + QUESTION_TIMEOUT_MS;
|
|
4538
|
+
}
|
|
4539
|
+
var realCache = /* @__PURE__ */ new Map();
|
|
4540
|
+
function dirKey(p) {
|
|
4541
|
+
let r = realCache.get(p);
|
|
4542
|
+
if (r === void 0) {
|
|
4543
|
+
try {
|
|
4544
|
+
r = realpathSync3(p);
|
|
4545
|
+
} catch {
|
|
4546
|
+
r = p;
|
|
4547
|
+
}
|
|
4548
|
+
if (realCache.size > 2e3) realCache.clear();
|
|
4549
|
+
realCache.set(p, r);
|
|
4550
|
+
}
|
|
4551
|
+
const t = r.length > 1 ? r.replace(/\/+$/, "") : r;
|
|
4552
|
+
return process.platform === "linux" ? t : t.toLowerCase();
|
|
4553
|
+
}
|
|
4554
|
+
function parseDumateServe(psOutput, uid) {
|
|
4555
|
+
for (const line of psOutput.split("\n")) {
|
|
4556
|
+
const m = line.trim().match(/^(\d+)\s+(\d+)\s+(.*)$/);
|
|
4557
|
+
if (!m) continue;
|
|
4558
|
+
const [, pid, u, args] = m;
|
|
4559
|
+
if (Number(u) !== uid) continue;
|
|
4560
|
+
if (!/(^|\/)dumate-opencode\s+serve(\s|$)/.test(args)) continue;
|
|
4561
|
+
const port = args.match(/--port[= ](\d{1,5})(\s|$)/);
|
|
4562
|
+
if (!port) continue;
|
|
4563
|
+
const host = args.match(/--hostname[= ](\S+)/);
|
|
4564
|
+
if (host && !["127.0.0.1", "localhost", "::1"].includes(host[1])) continue;
|
|
4565
|
+
const n = Number(port[1]);
|
|
4566
|
+
if (n <= 0 || n > 65535) continue;
|
|
4567
|
+
return { pid: Number(pid), port: n };
|
|
4568
|
+
}
|
|
4569
|
+
return null;
|
|
4570
|
+
}
|
|
4571
|
+
function parseInappKey(envOutput) {
|
|
4572
|
+
const m = envOutput.match(/(?:^|\s)DUMATE_INAPP_KEY=([^\s]+)/);
|
|
4573
|
+
return m ? m[1] : null;
|
|
4574
|
+
}
|
|
4575
|
+
var keyCache = null;
|
|
4576
|
+
function defaultDiscover() {
|
|
4577
|
+
if (process.platform === "win32" || typeof process.getuid !== "function") return null;
|
|
4578
|
+
try {
|
|
4579
|
+
const ps = execFileSync9("ps", ["-A", "-ww", "-o", "pid=,uid=,args="], { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] });
|
|
4580
|
+
const hit = parseDumateServe(ps, process.getuid());
|
|
4581
|
+
if (!hit) return null;
|
|
4582
|
+
if (keyCache && keyCache.pid === hit.pid && keyCache.port === hit.port) return keyCache;
|
|
4583
|
+
const env = execFileSync9("ps", ["eww", "-p", String(hit.pid), "-o", "command="], { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] });
|
|
4584
|
+
const key = parseInappKey(env);
|
|
4585
|
+
keyCache = key ? { ...hit, key } : null;
|
|
4586
|
+
return keyCache;
|
|
4587
|
+
} catch {
|
|
4588
|
+
return null;
|
|
4589
|
+
}
|
|
4590
|
+
}
|
|
4591
|
+
function headersFor(t, directory) {
|
|
4592
|
+
return {
|
|
4593
|
+
"X-Dumate-Inapp-Key": t.key,
|
|
4594
|
+
// 实测 DuMate 会 decodeURIComponent 这个头:编码后中文目录名也能放进 HTTP 头
|
|
4595
|
+
...directory ? { "x-opencode-directory": encodeURIComponent(directory) } : {}
|
|
4596
|
+
};
|
|
4597
|
+
}
|
|
4598
|
+
var defaultHttp = async (t, req2) => {
|
|
4599
|
+
const ac = new AbortController();
|
|
4600
|
+
const timer = setTimeout(() => ac.abort(), REQUEST_TIMEOUT_MS2);
|
|
4601
|
+
timer.unref?.();
|
|
4602
|
+
try {
|
|
4603
|
+
const r = await fetch(`http://127.0.0.1:${t.port}${req2.path}`, {
|
|
4604
|
+
method: req2.method,
|
|
4605
|
+
headers: { ...headersFor(t, req2.directory), ...req2.body !== void 0 ? { "content-type": "application/json" } : {} },
|
|
4606
|
+
...req2.body !== void 0 ? { body: JSON.stringify(req2.body) } : {},
|
|
4607
|
+
signal: ac.signal
|
|
4608
|
+
});
|
|
4609
|
+
const text = await r.text();
|
|
4610
|
+
let body = null;
|
|
4611
|
+
try {
|
|
4612
|
+
body = text ? JSON.parse(text) : null;
|
|
4613
|
+
} catch {
|
|
4614
|
+
body = text;
|
|
4615
|
+
}
|
|
4616
|
+
return { status: r.status, body };
|
|
4617
|
+
} catch {
|
|
4618
|
+
return { status: 0, body: null };
|
|
4619
|
+
} finally {
|
|
4620
|
+
clearTimeout(timer);
|
|
4621
|
+
}
|
|
4622
|
+
};
|
|
4623
|
+
var defaultSubscribe = (t, path, directory, onEvent, onEnd) => {
|
|
4624
|
+
let ended = false;
|
|
4625
|
+
const end = (status) => {
|
|
4626
|
+
if (ended) return;
|
|
4627
|
+
ended = true;
|
|
4628
|
+
onEnd(status);
|
|
4629
|
+
};
|
|
4630
|
+
const req2 = httpRequest(
|
|
4631
|
+
{ host: "127.0.0.1", port: t.port, path, method: "GET", headers: { accept: "text/event-stream", ...headersFor(t, directory) } },
|
|
4632
|
+
(res) => {
|
|
4633
|
+
if (res.statusCode !== 200) {
|
|
4634
|
+
res.resume();
|
|
4635
|
+
end(res.statusCode ?? 0);
|
|
4636
|
+
return;
|
|
4637
|
+
}
|
|
4638
|
+
res.setEncoding("utf8");
|
|
4639
|
+
let buf = "";
|
|
4640
|
+
res.on("data", (chunk) => {
|
|
4641
|
+
buf += chunk;
|
|
4642
|
+
let i;
|
|
4643
|
+
while ((i = buf.indexOf("\n")) >= 0) {
|
|
4644
|
+
const line = buf.slice(0, i).replace(/\r$/, "");
|
|
4645
|
+
buf = buf.slice(i + 1);
|
|
4646
|
+
if (!line.startsWith("data:")) continue;
|
|
4647
|
+
try {
|
|
4648
|
+
const v = JSON.parse(line.slice(5).trim());
|
|
4649
|
+
if (v && typeof v === "object") onEvent(v);
|
|
4650
|
+
} catch {
|
|
4651
|
+
}
|
|
4652
|
+
}
|
|
4653
|
+
if (buf.length > 4 * 1024 * 1024) buf = "";
|
|
4654
|
+
});
|
|
4655
|
+
res.on("end", () => end(200));
|
|
4656
|
+
res.on("error", () => end(0));
|
|
4657
|
+
}
|
|
4658
|
+
);
|
|
4659
|
+
req2.on("error", () => end(0));
|
|
4660
|
+
req2.end();
|
|
4661
|
+
return () => {
|
|
4662
|
+
ended = true;
|
|
4663
|
+
req2.destroy();
|
|
4664
|
+
};
|
|
4665
|
+
};
|
|
4666
|
+
function scanSessionDirs(root, now, maxAgeMs = ACTIVE_DIR_MS) {
|
|
4667
|
+
const out = [];
|
|
4668
|
+
const list = (p) => {
|
|
4669
|
+
try {
|
|
4670
|
+
return readdirSync4(p, { withFileTypes: true }).filter((d) => d.isDirectory()).map((d) => join19(p, d.name));
|
|
4671
|
+
} catch {
|
|
4672
|
+
return [];
|
|
4673
|
+
}
|
|
4674
|
+
};
|
|
4675
|
+
for (const u of list(root))
|
|
4676
|
+
for (const day of list(u).sort().reverse().slice(0, 2))
|
|
4677
|
+
for (const dir of list(day)) {
|
|
4678
|
+
try {
|
|
4679
|
+
const at = statSync6(dir).mtimeMs;
|
|
4680
|
+
if (now - at <= maxAgeMs) out.push({ dir, at });
|
|
4681
|
+
} catch {
|
|
4682
|
+
}
|
|
4683
|
+
}
|
|
4684
|
+
return out.sort((a, b) => b.at - a.at).map((x) => x.dir);
|
|
4685
|
+
}
|
|
4686
|
+
var DumateWatch = class {
|
|
4687
|
+
#d;
|
|
4688
|
+
#target = null;
|
|
4689
|
+
#stopped = false;
|
|
4690
|
+
/** 订阅:'' = /global/event;否则按目录 */
|
|
4691
|
+
#subs = /* @__PURE__ */ new Map();
|
|
4692
|
+
#subEndedAt = 0;
|
|
4693
|
+
/** /global/event 不可用(404),退到按目录订阅 */
|
|
4694
|
+
#globalUnsupported = false;
|
|
4695
|
+
#questionUnsupported = false;
|
|
4696
|
+
#permissionUnsupported = false;
|
|
4697
|
+
#reconciling = false;
|
|
4698
|
+
#sessionListAt = 0;
|
|
4699
|
+
/** 目录 → 最近一次看到它活动的时间 */
|
|
4700
|
+
/** dirKey → 原写法 + 最近一次看到它活动的时间 */
|
|
4701
|
+
#dirs = /* @__PURE__ */ new Map();
|
|
4702
|
+
/** dirKey → 最近一次忙(busy / 工具 / 提问)的时间 */
|
|
4703
|
+
#busyDirs = /* @__PURE__ */ new Map();
|
|
4704
|
+
/** 当前这条 /global/event 已经收到过带目录的会话事件:之后只轮询有卡 / 最近忙过的目录 */
|
|
4705
|
+
#sseVerified = false;
|
|
4706
|
+
#scanAt = 0;
|
|
4707
|
+
#discoverMissAt = 0;
|
|
4708
|
+
#sessionDir = /* @__PURE__ */ new Map();
|
|
4709
|
+
#titles = /* @__PURE__ */ new Map();
|
|
4710
|
+
#children = /* @__PURE__ */ new Set();
|
|
4711
|
+
#userMsgs = /* @__PURE__ */ new Map();
|
|
4712
|
+
#promptedMsgs = /* @__PURE__ */ new Set();
|
|
4713
|
+
/** callID → 工具名 / cc 同构参数(授权卡用真实参数,选择题判来源) */
|
|
4714
|
+
#calls = /* @__PURE__ */ new Map();
|
|
4715
|
+
#callReported = /* @__PURE__ */ new Map();
|
|
4716
|
+
/** 本轮已报过 UserPromptSubmit 的会话 */
|
|
4717
|
+
#turnOpen = /* @__PURE__ */ new Set();
|
|
4718
|
+
/** 报过「进行中」类事件、还没报 Stop 的会话 */
|
|
4719
|
+
#active = /* @__PURE__ */ new Set();
|
|
4720
|
+
#busyTimers = /* @__PURE__ */ new Map();
|
|
4721
|
+
#lastUserText = /* @__PURE__ */ new Map();
|
|
4722
|
+
#cards = /* @__PURE__ */ new Map();
|
|
4723
|
+
/** 推过 / 不推的请求 id → 时间:同一个只处理一次 */
|
|
4724
|
+
#handled = /* @__PURE__ */ new Map();
|
|
4725
|
+
#reportChain = Promise.resolve();
|
|
4726
|
+
constructor(deps) {
|
|
4727
|
+
this.#d = deps;
|
|
4728
|
+
}
|
|
4729
|
+
#now() {
|
|
4730
|
+
return this.#d.now ? this.#d.now() : Date.now();
|
|
4731
|
+
}
|
|
4732
|
+
get connected() {
|
|
4733
|
+
return this.#target !== null;
|
|
4734
|
+
}
|
|
4735
|
+
/** 只给测试和诊断用 */
|
|
4736
|
+
get pending() {
|
|
4737
|
+
return this.#cards.size;
|
|
4738
|
+
}
|
|
4739
|
+
/** 定时调(2~3 秒):找进程、换了就重连、补订阅、轮询待答 */
|
|
4740
|
+
tick() {
|
|
4741
|
+
if (this.#stopped) return;
|
|
4742
|
+
if (this.#d.enabled && !this.#d.enabled()) {
|
|
4743
|
+
if (this.#target) this.#teardown("\u767E\u5EA6\u642D\u5B50\u5DF2\u505C\u7528");
|
|
4744
|
+
return;
|
|
4745
|
+
}
|
|
4746
|
+
if (!this.#target && this.#discoverMissAt && this.#now() - this.#discoverMissAt < DISCOVER_BACKOFF_MS) return;
|
|
4747
|
+
let t = null;
|
|
4748
|
+
try {
|
|
4749
|
+
t = (this.#d.discover ?? defaultDiscover)();
|
|
4750
|
+
} catch {
|
|
4751
|
+
t = null;
|
|
4752
|
+
}
|
|
4753
|
+
if (!t) {
|
|
4754
|
+
if (this.#target) this.#teardown("\u767E\u5EA6\u642D\u5B50\u5DF2\u9000\u51FA");
|
|
4755
|
+
this.#discoverMissAt = this.#now();
|
|
4756
|
+
return;
|
|
4757
|
+
}
|
|
4758
|
+
this.#discoverMissAt = 0;
|
|
4759
|
+
const cur = this.#target;
|
|
4760
|
+
if (!cur || cur.pid !== t.pid || cur.port !== t.port || cur.key !== t.key) {
|
|
4761
|
+
if (cur) this.#teardown("\u767E\u5EA6\u642D\u5B50\u91CD\u542F\u8FC7");
|
|
4762
|
+
this.#target = t;
|
|
4763
|
+
this.#d.log(`\u5DF2\u8FDE\u4E0A\u767E\u5EA6\u642D\u5B50\uFF08\u7AEF\u53E3 ${t.port}\uFF09\uFF0C\u72B6\u6001\u3001\u9009\u62E9\u9898\u4E0E\u6388\u6743\u53EF\u63A8\u5230\u8BBE\u5907`);
|
|
4764
|
+
}
|
|
4765
|
+
this.#ensureSubscribed();
|
|
4766
|
+
void this.reconcile();
|
|
4767
|
+
}
|
|
4768
|
+
stop() {
|
|
4769
|
+
this.#stopped = true;
|
|
4770
|
+
this.#teardown("");
|
|
4771
|
+
}
|
|
4772
|
+
#teardown(why) {
|
|
4773
|
+
for (const unsub of this.#subs.values()) unsub();
|
|
4774
|
+
this.#subs.clear();
|
|
4775
|
+
for (const c of this.#cards.values()) c.ac.abort();
|
|
4776
|
+
this.#cards.clear();
|
|
4777
|
+
for (const t of this.#busyTimers.values()) clearTimeout(t);
|
|
4778
|
+
this.#busyTimers.clear();
|
|
4779
|
+
this.#target = null;
|
|
4780
|
+
this.#globalUnsupported = false;
|
|
4781
|
+
this.#questionUnsupported = false;
|
|
4782
|
+
this.#permissionUnsupported = false;
|
|
4783
|
+
this.#sessionListAt = 0;
|
|
4784
|
+
this.#subEndedAt = 0;
|
|
4785
|
+
this.#dirs.clear();
|
|
4786
|
+
this.#busyDirs.clear();
|
|
4787
|
+
this.#sseVerified = false;
|
|
4788
|
+
this.#scanAt = 0;
|
|
4789
|
+
this.#sessionDir.clear();
|
|
4790
|
+
this.#children.clear();
|
|
4791
|
+
this.#userMsgs.clear();
|
|
4792
|
+
this.#promptedMsgs.clear();
|
|
4793
|
+
this.#calls.clear();
|
|
4794
|
+
this.#callReported.clear();
|
|
4795
|
+
this.#turnOpen.clear();
|
|
4796
|
+
this.#active.clear();
|
|
4797
|
+
this.#lastUserText.clear();
|
|
4798
|
+
if (why) this.#d.log(`\u5DF2\u65AD\u5F00\u767E\u5EA6\u642D\u5B50\uFF08${why}\uFF09`);
|
|
4799
|
+
}
|
|
4800
|
+
// ---------- 订阅 ----------
|
|
4801
|
+
#ensureSubscribed() {
|
|
4802
|
+
const t = this.#target;
|
|
4803
|
+
if (!t) return;
|
|
4804
|
+
if (this.#subEndedAt && this.#now() - this.#subEndedAt < RESUBSCRIBE_MS) return;
|
|
4805
|
+
const sub = this.#d.subscribe ?? defaultSubscribe;
|
|
4806
|
+
if (!this.#globalUnsupported) {
|
|
4807
|
+
if (this.#subs.has("")) return;
|
|
4808
|
+
this.#subs.set(
|
|
4809
|
+
"",
|
|
4810
|
+
sub(t, "/global/event", void 0, (ev) => this.#onEnvelope(ev, void 0), (status) => this.#onSubEnd("", t, status))
|
|
4811
|
+
);
|
|
4812
|
+
return;
|
|
4813
|
+
}
|
|
4814
|
+
const active = this.#activeDirs();
|
|
4815
|
+
const keys = new Set(active.map(dirKey));
|
|
4816
|
+
for (const [k, unsub] of [...this.#subs]) {
|
|
4817
|
+
if (k === "" || keys.has(k)) continue;
|
|
4818
|
+
unsub();
|
|
4819
|
+
this.#subs.delete(k);
|
|
4820
|
+
}
|
|
4821
|
+
for (const dir of active) {
|
|
4822
|
+
const k = dirKey(dir);
|
|
4823
|
+
if (this.#subs.has(k)) continue;
|
|
4824
|
+
this.#subs.set(
|
|
4825
|
+
k,
|
|
4826
|
+
sub(t, "/event", dir, (ev) => this.#onEnvelope(ev, dir), (status) => this.#onSubEnd(k, t, status))
|
|
4827
|
+
);
|
|
4828
|
+
}
|
|
4829
|
+
}
|
|
4830
|
+
#onSubEnd(key, t, status) {
|
|
4831
|
+
if (this.#target !== t) return;
|
|
4832
|
+
this.#subs.delete(key);
|
|
4833
|
+
this.#subEndedAt = this.#now();
|
|
4834
|
+
if (key === "" && status === 404) {
|
|
4835
|
+
this.#globalUnsupported = true;
|
|
4836
|
+
this.#d.log("\u767E\u5EA6\u642D\u5B50\u6CA1\u6709 /global/event\uFF0C\u6539\u4E3A\u6309\u4F1A\u8BDD\u76EE\u5F55\u8BA2\u9605");
|
|
4837
|
+
}
|
|
4838
|
+
}
|
|
4839
|
+
/** /global/event 的信封 `{directory, payload}`;按目录的 /event 直接是事件 */
|
|
4840
|
+
#onEnvelope(raw, dir) {
|
|
4841
|
+
const inner = raw.payload && typeof raw.payload === "object" ? raw.payload : raw;
|
|
4842
|
+
const d = typeof raw.directory === "string" && raw.directory ? raw.directory : dir;
|
|
4843
|
+
if (dir === void 0 && d && inner.type !== "server.connected" && inner.type !== "server.heartbeat") this.#sseVerified = true;
|
|
4844
|
+
try {
|
|
4845
|
+
this.onEvent(inner, d);
|
|
4846
|
+
} catch (e) {
|
|
4847
|
+
this.#d.log(`\u5904\u7406\u767E\u5EA6\u642D\u5B50\u4E8B\u4EF6\u51FA\u9519\uFF1A${e?.message ?? e}`);
|
|
4848
|
+
}
|
|
4849
|
+
}
|
|
4850
|
+
#touchDir(dir, at = this.#now()) {
|
|
4851
|
+
if (!dir || !dir.startsWith("/")) return;
|
|
4852
|
+
const k = dirKey(dir);
|
|
4853
|
+
const cur = this.#dirs.get(k);
|
|
4854
|
+
if (!cur || cur.at < at) this.#dirs.set(k, { dir: cur?.dir ?? dir, at });
|
|
4855
|
+
}
|
|
4856
|
+
#markBusy(dir) {
|
|
4857
|
+
if (dir) this.#busyDirs.set(dirKey(dir), this.#now());
|
|
4858
|
+
}
|
|
4859
|
+
#activeDirs() {
|
|
4860
|
+
const now = this.#now();
|
|
4861
|
+
for (const [k, v] of this.#dirs) if (now - v.at > ACTIVE_DIR_MS) this.#dirs.delete(k);
|
|
4862
|
+
return [...this.#dirs.values()].sort((a, b) => b.at - a.at).map((v) => v.dir).slice(0, MAX_POLL_DIRS);
|
|
4863
|
+
}
|
|
4864
|
+
/**
|
|
4865
|
+
* 这一轮要轮询的目录(评审 d):事件流还没证实收得到会话事件时全部活跃目录;
|
|
4866
|
+
* 证实之后只轮询有卡在等的、最近忙过的(提问 / 授权只会出现在忙的会话里,事件丢了靠这个兜)
|
|
4867
|
+
*/
|
|
4868
|
+
#pollDirs() {
|
|
4869
|
+
const all = this.#activeDirs();
|
|
4870
|
+
if (!this.#sseVerified || !this.#subs.has("")) return all;
|
|
4871
|
+
const now = this.#now();
|
|
4872
|
+
const cardKeys = new Set([...this.#cards.values()].filter((c) => c.dir).map((c) => dirKey(c.dir)));
|
|
4873
|
+
return all.filter((d) => {
|
|
4874
|
+
const k = dirKey(d);
|
|
4875
|
+
return cardKeys.has(k) || now - (this.#busyDirs.get(k) ?? 0) <= BUSY_DIR_MS;
|
|
4876
|
+
});
|
|
4877
|
+
}
|
|
4878
|
+
// ---------- 事件 → 状态 ----------
|
|
4879
|
+
/** 一条 opencode 事件(`{type, properties}`)。dir = 事件所属会话目录 */
|
|
4880
|
+
onEvent(ev, dir) {
|
|
4881
|
+
const type = typeof ev.type === "string" ? ev.type : "";
|
|
4882
|
+
const p = ev.properties && typeof ev.properties === "object" ? ev.properties : {};
|
|
4883
|
+
this.#touchDir(dir);
|
|
4884
|
+
switch (type) {
|
|
4885
|
+
case "session.created":
|
|
4886
|
+
case "session.updated": {
|
|
4887
|
+
const info = p.info ?? {};
|
|
4888
|
+
this.#noteSession(info);
|
|
4889
|
+
return;
|
|
4890
|
+
}
|
|
4891
|
+
case "session.deleted": {
|
|
4892
|
+
const info = p.info ?? {};
|
|
4893
|
+
const sid = typeof info.id === "string" ? info.id : typeof p.sessionID === "string" ? p.sessionID : "";
|
|
4894
|
+
if (sid && !this.#children.has(sid)) this.#report("SessionEnd", sid, dir);
|
|
4895
|
+
return;
|
|
4896
|
+
}
|
|
4897
|
+
case "session.status": {
|
|
4898
|
+
const sid = typeof p.sessionID === "string" ? p.sessionID : "";
|
|
4899
|
+
const st = p.status?.type;
|
|
4900
|
+
if (!sid) return;
|
|
4901
|
+
if (st === "busy") {
|
|
4902
|
+
this.#markBusy(this.#dirOf(sid, dir));
|
|
4903
|
+
this.#onBusy(sid, dir);
|
|
4904
|
+
} else if (st === "idle") this.#onIdle(sid, dir);
|
|
4905
|
+
return;
|
|
4906
|
+
}
|
|
4907
|
+
case "session.idle":
|
|
4908
|
+
if (typeof p.sessionID === "string") this.#onIdle(p.sessionID, dir);
|
|
4909
|
+
return;
|
|
4910
|
+
case "message.updated": {
|
|
4911
|
+
const info = p.info ?? {};
|
|
4912
|
+
if (info.role === "user" && typeof info.id === "string" && typeof info.sessionID === "string") {
|
|
4913
|
+
this.#userMsgs.set(info.id, info.sessionID);
|
|
4914
|
+
while (this.#userMsgs.size > 500) this.#userMsgs.delete(this.#userMsgs.keys().next().value);
|
|
4915
|
+
}
|
|
4916
|
+
return;
|
|
4917
|
+
}
|
|
4918
|
+
case "message.part.updated":
|
|
4919
|
+
this.#onPart(p.part ?? {}, dir);
|
|
4920
|
+
return;
|
|
4921
|
+
case "question.asked":
|
|
4922
|
+
this.#onQuestion(p, dir);
|
|
4923
|
+
return;
|
|
4924
|
+
case "question.replied":
|
|
4925
|
+
case "question.rejected":
|
|
4926
|
+
this.#settle(typeof p.requestID === "string" ? p.requestID : "", "\u7535\u8111\u4E0A\u5DF2\u7B54");
|
|
4927
|
+
return;
|
|
4928
|
+
case "permission.asked":
|
|
4929
|
+
case "permission.updated":
|
|
4930
|
+
this.#onPermission(p, dir);
|
|
4931
|
+
return;
|
|
4932
|
+
case "permission.replied":
|
|
4933
|
+
this.#settle(typeof p.requestID === "string" ? p.requestID : typeof p.permissionID === "string" ? p.permissionID : "", "\u7535\u8111\u4E0A\u5DF2\u5904\u7406");
|
|
4934
|
+
return;
|
|
4935
|
+
default:
|
|
4936
|
+
}
|
|
4937
|
+
}
|
|
4938
|
+
#noteSession(info) {
|
|
4939
|
+
const id = typeof info.id === "string" ? info.id : "";
|
|
4940
|
+
if (!id) return;
|
|
4941
|
+
if (typeof info.directory === "string" && info.directory) {
|
|
4942
|
+
this.#sessionDir.set(id, info.directory);
|
|
3530
4943
|
}
|
|
4944
|
+
if (info.parentID || info.is_subagent === true) this.#children.add(id);
|
|
4945
|
+
rememberTitle(this.#titles, info);
|
|
3531
4946
|
}
|
|
3532
|
-
|
|
3533
|
-
return this.#
|
|
4947
|
+
#dirOf(sid, dir) {
|
|
4948
|
+
return this.#sessionDir.get(sid) ?? dir ?? "";
|
|
3534
4949
|
}
|
|
3535
|
-
|
|
3536
|
-
|
|
3537
|
-
|
|
3538
|
-
|
|
3539
|
-
|
|
4950
|
+
#onBusy(sid, dir) {
|
|
4951
|
+
if (this.#turnOpen.has(sid) || this.#busyTimers.has(sid)) return;
|
|
4952
|
+
const timer = setTimeout(() => {
|
|
4953
|
+
this.#busyTimers.delete(sid);
|
|
4954
|
+
this.#openTurn(sid, this.#lastUserText.get(sid), dir);
|
|
4955
|
+
}, BUSY_WAIT_MS);
|
|
4956
|
+
timer.unref?.();
|
|
4957
|
+
this.#busyTimers.set(sid, timer);
|
|
4958
|
+
}
|
|
4959
|
+
#openTurn(sid, prompt, dir) {
|
|
4960
|
+
if (this.#turnOpen.has(sid)) return;
|
|
4961
|
+
const t = this.#busyTimers.get(sid);
|
|
4962
|
+
if (t) {
|
|
4963
|
+
clearTimeout(t);
|
|
4964
|
+
this.#busyTimers.delete(sid);
|
|
4965
|
+
}
|
|
4966
|
+
this.#turnOpen.add(sid);
|
|
4967
|
+
this.#report("UserPromptSubmit", sid, dir, prompt ? { prompt } : {});
|
|
4968
|
+
}
|
|
4969
|
+
#onIdle(sid, dir) {
|
|
4970
|
+
const t = this.#busyTimers.get(sid);
|
|
4971
|
+
if (t) {
|
|
4972
|
+
this.#openTurn(sid, this.#lastUserText.get(sid), dir);
|
|
4973
|
+
}
|
|
4974
|
+
const was = this.#turnOpen.delete(sid) || this.#active.has(sid);
|
|
4975
|
+
this.#active.delete(sid);
|
|
4976
|
+
this.#lastUserText.delete(sid);
|
|
4977
|
+
if (was) this.#report("Stop", sid, dir);
|
|
4978
|
+
}
|
|
4979
|
+
#onPart(part, dir) {
|
|
4980
|
+
const sid = typeof part.sessionID === "string" ? part.sessionID : "";
|
|
4981
|
+
if (!sid) return;
|
|
4982
|
+
if (part.type === "text") {
|
|
4983
|
+
const mid = typeof part.messageID === "string" ? part.messageID : "";
|
|
4984
|
+
if (!mid || this.#userMsgs.get(mid) !== sid || part.synthetic === true) return;
|
|
4985
|
+
const text = typeof part.text === "string" ? part.text : "";
|
|
4986
|
+
if (!text.trim() || this.#promptedMsgs.has(mid)) return;
|
|
4987
|
+
this.#promptedMsgs.add(mid);
|
|
4988
|
+
while (this.#promptedMsgs.size > 500) this.#promptedMsgs.delete(this.#promptedMsgs.values().next().value);
|
|
4989
|
+
this.#lastUserText.set(sid, text);
|
|
4990
|
+
if (!this.#turnOpen.has(sid)) this.#openTurn(sid, text, dir);
|
|
4991
|
+
return;
|
|
4992
|
+
}
|
|
4993
|
+
if (part.type !== "tool") return;
|
|
4994
|
+
this.#markBusy(this.#dirOf(sid, dir));
|
|
4995
|
+
const callID = typeof part.callID === "string" ? part.callID : "";
|
|
4996
|
+
const tool = typeof part.tool === "string" ? part.tool : "";
|
|
4997
|
+
if (!callID || !tool) return;
|
|
4998
|
+
const state = part.state ?? {};
|
|
4999
|
+
const cc = toCcTool(tool, state.input);
|
|
5000
|
+
this.#calls.set(callID, { tool, cc });
|
|
5001
|
+
while (this.#calls.size > 500) this.#calls.delete(this.#calls.keys().next().value);
|
|
5002
|
+
const status = state.status;
|
|
5003
|
+
const done = this.#callReported.get(callID);
|
|
5004
|
+
if (status === "running" && !done) {
|
|
5005
|
+
this.#callReported.set(callID, "pre");
|
|
5006
|
+
this.#active.add(sid);
|
|
5007
|
+
this.#report("PreToolUse", sid, dir, cc);
|
|
5008
|
+
} else if ((status === "completed" || status === "error") && done !== "post") {
|
|
5009
|
+
this.#callReported.set(callID, "post");
|
|
5010
|
+
this.#active.add(sid);
|
|
5011
|
+
this.#report("PostToolUse", sid, dir, cc);
|
|
5012
|
+
}
|
|
5013
|
+
while (this.#callReported.size > 500) this.#callReported.delete(this.#callReported.keys().next().value);
|
|
5014
|
+
}
|
|
5015
|
+
#report(event, sid, dir, extra = {}) {
|
|
5016
|
+
if (this.#children.has(sid)) return;
|
|
5017
|
+
const t = this.#target;
|
|
5018
|
+
if (!t) return;
|
|
5019
|
+
const input = { hook_event_name: event, session_id: sid, cwd: this.#dirOf(sid, dir), ...extra };
|
|
5020
|
+
const title = this.#titles.get(sid);
|
|
5021
|
+
if (title) input.oc_session_title = title;
|
|
5022
|
+
const run2 = this.#d.report ?? ((i, pid) => runHook(this.#d.nodePath ?? process.execPath, i, pid));
|
|
5023
|
+
this.#reportChain = this.#reportChain.then(() => run2(input, t.pid)).catch(() => {
|
|
5024
|
+
});
|
|
3540
5025
|
}
|
|
3541
|
-
|
|
3542
|
-
|
|
5026
|
+
// ---------- 选择题 / 授权 ----------
|
|
5027
|
+
#prune() {
|
|
5028
|
+
const now = this.#now();
|
|
5029
|
+
for (const [id, at] of this.#handled) if (now - at > HANDLED_TTL_MS) this.#handled.delete(id);
|
|
5030
|
+
}
|
|
5031
|
+
#onQuestion(p, dir) {
|
|
5032
|
+
const id = typeof p.id === "string" ? p.id : "";
|
|
5033
|
+
const sid = typeof p.sessionID === "string" ? p.sessionID : "";
|
|
5034
|
+
if (!id || !sid || this.#cards.has(id) || this.#handled.has(id) || !this.#target) return;
|
|
5035
|
+
this.#prune();
|
|
5036
|
+
this.#handled.set(id, this.#now());
|
|
5037
|
+
const callID = (p.tool ?? {}).callID;
|
|
5038
|
+
const callTool = typeof callID === "string" ? this.#calls.get(callID)?.tool ?? "question" : "question";
|
|
5039
|
+
if (!isPlainQuestion(p, callTool)) return;
|
|
5040
|
+
const cc = questionToCc(p);
|
|
5041
|
+
if (!cc) return;
|
|
5042
|
+
const d = this.#dirOf(sid, dir);
|
|
5043
|
+
const deadline = questionDeadline(p.deadlineAt, p.timeoutMs, this.#now());
|
|
5044
|
+
this.#push({ id, kind: "question", sessionID: sid, dir: d, deadlineAt: deadline }, cc);
|
|
5045
|
+
}
|
|
5046
|
+
#onPermission(p, dir) {
|
|
5047
|
+
const id = typeof p.id === "string" ? p.id : "";
|
|
5048
|
+
const sid = typeof p.sessionID === "string" ? p.sessionID : "";
|
|
5049
|
+
if (!id || !sid || this.#cards.has(id) || this.#handled.has(id) || !this.#target) return;
|
|
5050
|
+
this.#prune();
|
|
5051
|
+
this.#handled.set(id, this.#now());
|
|
5052
|
+
const callID = (p.tool ?? {}).callID;
|
|
5053
|
+
const call2 = typeof callID === "string" ? this.#calls.get(callID) : void 0;
|
|
5054
|
+
const cc = call2 ? call2.cc : askToCc(p);
|
|
5055
|
+
this.#report("PermissionRequest", sid, dir, cc);
|
|
5056
|
+
this.#push({ id, kind: "permission", sessionID: sid, dir: this.#dirOf(sid, dir), deadlineAt: 0 }, cc);
|
|
5057
|
+
}
|
|
5058
|
+
#push(c, cc) {
|
|
5059
|
+
const t = this.#target;
|
|
5060
|
+
if (!t) return;
|
|
5061
|
+
const card = { ...c, ac: new AbortController(), createdAt: this.#now() };
|
|
5062
|
+
this.#cards.set(card.id, card);
|
|
5063
|
+
const req2 = {
|
|
5064
|
+
window: true,
|
|
5065
|
+
tool: "dumate",
|
|
5066
|
+
hook: { hook_event_name: "PermissionRequest", session_id: card.sessionID, cwd: card.dir, tool_name: cc.tool_name, tool_input: cc.tool_input }
|
|
5067
|
+
};
|
|
5068
|
+
void this.#d.gate(req2, card.ac.signal).then(async (reply) => {
|
|
5069
|
+
if (this.#cards.get(card.id) !== card || card.ac.signal.aborted || this.#target !== t) return;
|
|
5070
|
+
this.#cards.delete(card.id);
|
|
5071
|
+
await this.#answer(t, card, reply);
|
|
5072
|
+
}).catch(() => {
|
|
5073
|
+
if (this.#cards.get(card.id) === card) this.#cards.delete(card.id);
|
|
5074
|
+
});
|
|
3543
5075
|
}
|
|
3544
|
-
|
|
3545
|
-
|
|
3546
|
-
|
|
3547
|
-
|
|
3548
|
-
|
|
3549
|
-
|
|
5076
|
+
async #answer(t, card, reply) {
|
|
5077
|
+
const http = this.#d.http ?? defaultHttp;
|
|
5078
|
+
const short = card.id.slice(0, 12);
|
|
5079
|
+
let r;
|
|
5080
|
+
if (card.kind === "question") {
|
|
5081
|
+
if (reply.decision === "allow" && Array.isArray(reply.answerLists)) {
|
|
5082
|
+
r = await http(t, { method: "POST", path: `/question/${encodeURIComponent(card.id)}/reply`, directory: card.dir, body: { answers: reply.answerLists } });
|
|
5083
|
+
} else if (reply.decision === "deny") {
|
|
5084
|
+
r = await http(t, { method: "POST", path: `/question/${encodeURIComponent(card.id)}/reject`, directory: card.dir });
|
|
5085
|
+
} else return;
|
|
5086
|
+
} else {
|
|
5087
|
+
if (reply.decision !== "allow" && reply.decision !== "deny") return;
|
|
5088
|
+
const answer = reply.decision === "allow" ? "once" : "reject";
|
|
5089
|
+
r = await http(t, { method: "POST", path: `/permission/${encodeURIComponent(card.id)}/reply`, directory: card.dir, body: { reply: answer } });
|
|
5090
|
+
if (r.status === 404) {
|
|
5091
|
+
r = await http(t, {
|
|
5092
|
+
method: "POST",
|
|
5093
|
+
path: `/session/${encodeURIComponent(card.sessionID)}/permissions/${encodeURIComponent(card.id)}`,
|
|
5094
|
+
directory: card.dir,
|
|
5095
|
+
body: { response: answer }
|
|
5096
|
+
});
|
|
5097
|
+
}
|
|
5098
|
+
}
|
|
5099
|
+
if (r.status >= 200 && r.status < 300) {
|
|
5100
|
+
this.#d.log(`\u767E\u5EA6\u642D\u5B50\uFF1A${card.kind === "question" ? "\u9009\u62E9\u9898" : "\u6388\u6743"} ${short} \u5DF2\u6309\u8BBE\u5907\u7B54\u590D\u56DE\u586B`);
|
|
5101
|
+
} else if (r.status === 404) {
|
|
5102
|
+
this.#d.log(`\u767E\u5EA6\u642D\u5B50\uFF1A${short} \u5DF2\u4E0D\u5728\u7B49\uFF08\u7535\u8111\u4E0A\u5DF2\u5904\u7406\u6216\u5DF2\u8D85\u65F6\uFF09\uFF0C\u8BBE\u5907\u7B54\u590D\u4E0D\u518D\u56DE\u586B`);
|
|
5103
|
+
} else {
|
|
5104
|
+
this.#d.log(`\u767E\u5EA6\u642D\u5B50\uFF1A\u56DE\u586B ${short} \u5931\u8D25\uFF08HTTP ${r.status || "\u8FDE\u4E0D\u4E0A"}\uFF09\uFF0C\u754C\u9762\u7559\u7ED9\u4EBA\u7B54`);
|
|
3550
5105
|
}
|
|
3551
5106
|
}
|
|
3552
|
-
|
|
5107
|
+
#settle(id, why) {
|
|
5108
|
+
if (!id) return;
|
|
5109
|
+
this.#handled.set(id, this.#now());
|
|
5110
|
+
const c = this.#cards.get(id);
|
|
5111
|
+
if (!c) return;
|
|
5112
|
+
this.#cards.delete(id);
|
|
5113
|
+
this.#d.log(`\u767E\u5EA6\u642D\u5B50\uFF1A${c.kind === "question" ? "\u9009\u62E9\u9898" : "\u6388\u6743"} ${id.slice(0, 12)} ${why}\uFF0C\u64A4\u5361`);
|
|
5114
|
+
c.ac.abort();
|
|
5115
|
+
}
|
|
5116
|
+
/**
|
|
5117
|
+
* 每个 tick:补会话目录、按目录轮询 /question 与 /permission(事件没订上或丢了时兜底推卡),
|
|
5118
|
+
* 轮询里不见了的卡、过了截止时间的选择题撤掉
|
|
5119
|
+
*/
|
|
5120
|
+
async reconcile() {
|
|
5121
|
+
const t = this.#target;
|
|
5122
|
+
if (!t || this.#reconciling) return;
|
|
5123
|
+
this.#reconciling = true;
|
|
3553
5124
|
try {
|
|
3554
|
-
|
|
3555
|
-
const
|
|
3556
|
-
|
|
3557
|
-
|
|
3558
|
-
|
|
5125
|
+
const http = this.#d.http ?? defaultHttp;
|
|
5126
|
+
const now = this.#now();
|
|
5127
|
+
if (now - this.#scanAt >= SCAN_DIRS_MS) {
|
|
5128
|
+
this.#scanAt = now;
|
|
5129
|
+
for (const d of (this.#d.sessionDirs ?? (() => scanSessionDirs(join19(qianfanHome(), "workspace", "sessions"), now)))()) {
|
|
5130
|
+
if (!this.#dirs.has(dirKey(d))) this.#touchDir(d, now);
|
|
5131
|
+
}
|
|
5132
|
+
}
|
|
5133
|
+
if (now - this.#sessionListAt >= SESSION_LIST_MS) {
|
|
5134
|
+
this.#sessionListAt = now;
|
|
5135
|
+
const r = await http(t, { method: "GET", path: "/session" });
|
|
5136
|
+
if (this.#target !== t) return;
|
|
5137
|
+
if (r.status === 200 && Array.isArray(r.body)) {
|
|
5138
|
+
for (const s of r.body) {
|
|
5139
|
+
if (!s || typeof s !== "object") continue;
|
|
5140
|
+
this.#noteSession(s);
|
|
5141
|
+
const updated = Number((s.time ?? {}).updated) || 0;
|
|
5142
|
+
if (typeof s.directory === "string" && s.directory && now - updated <= ACTIVE_DIR_MS) this.#touchDir(s.directory, updated);
|
|
5143
|
+
}
|
|
5144
|
+
}
|
|
5145
|
+
}
|
|
5146
|
+
const seen = /* @__PURE__ */ new Set();
|
|
5147
|
+
const polled = /* @__PURE__ */ new Set();
|
|
5148
|
+
for (const dir of this.#pollDirs()) {
|
|
5149
|
+
const k = dirKey(dir);
|
|
5150
|
+
if (!this.#questionUnsupported) {
|
|
5151
|
+
const q = await http(t, { method: "GET", path: "/question", directory: dir });
|
|
5152
|
+
if (this.#target !== t) return;
|
|
5153
|
+
if (q.status === 404) {
|
|
5154
|
+
this.#questionUnsupported = true;
|
|
5155
|
+
this.#d.log("\u767E\u5EA6\u642D\u5B50\u6CA1\u6709 /question\uFF0C\u9009\u62E9\u9898\u4E0D\u63A8\u8BBE\u5907\uFF0C\u53EA\u4E0A\u62A5\u72B6\u6001");
|
|
5156
|
+
} else if (q.status === 200 && Array.isArray(q.body)) {
|
|
5157
|
+
polled.add(`question:${k}`);
|
|
5158
|
+
for (const item of q.body) {
|
|
5159
|
+
if (!item || typeof item.id !== "string") continue;
|
|
5160
|
+
seen.add(item.id);
|
|
5161
|
+
this.#onQuestion(item, dir);
|
|
5162
|
+
}
|
|
5163
|
+
}
|
|
5164
|
+
}
|
|
5165
|
+
if (!this.#permissionUnsupported) {
|
|
5166
|
+
const pr = await http(t, { method: "GET", path: "/permission", directory: dir });
|
|
5167
|
+
if (this.#target !== t) return;
|
|
5168
|
+
if (pr.status === 404) {
|
|
5169
|
+
this.#permissionUnsupported = true;
|
|
5170
|
+
this.#d.log("\u767E\u5EA6\u642D\u5B50\u6CA1\u6709 /permission\uFF0C\u6388\u6743\u4E0D\u63A8\u8BBE\u5907\uFF0C\u53EA\u4E0A\u62A5\u72B6\u6001");
|
|
5171
|
+
} else if (pr.status === 200 && Array.isArray(pr.body)) {
|
|
5172
|
+
polled.add(`permission:${k}`);
|
|
5173
|
+
for (const item of pr.body) {
|
|
5174
|
+
if (!item || typeof item.id !== "string") continue;
|
|
5175
|
+
seen.add(item.id);
|
|
5176
|
+
this.#onPermission(item, dir);
|
|
5177
|
+
}
|
|
5178
|
+
}
|
|
5179
|
+
}
|
|
5180
|
+
}
|
|
5181
|
+
const later = this.#now();
|
|
5182
|
+
for (const c of [...this.#cards.values()]) {
|
|
5183
|
+
if (c.kind === "question" && c.deadlineAt > 0 && later > c.deadlineAt) {
|
|
5184
|
+
this.#settle(c.id, "\u5DF2\u8D85\u65F6");
|
|
5185
|
+
continue;
|
|
5186
|
+
}
|
|
5187
|
+
if (c.dir && polled.has(`${c.kind}:${dirKey(c.dir)}`) && !seen.has(c.id) && later - c.createdAt >= MISSING_GRACE_MS) {
|
|
5188
|
+
this.#settle(c.id, "\u5DF2\u4E0D\u5728\u7B49\uFF08\u7535\u8111\u4E0A\u5DF2\u5904\u7406\uFF09");
|
|
5189
|
+
}
|
|
5190
|
+
}
|
|
5191
|
+
} finally {
|
|
5192
|
+
this.#reconciling = false;
|
|
3559
5193
|
}
|
|
3560
5194
|
}
|
|
3561
5195
|
};
|
|
5196
|
+
function runHook(node, input, agentPid) {
|
|
5197
|
+
return new Promise((resolve7) => {
|
|
5198
|
+
let child;
|
|
5199
|
+
try {
|
|
5200
|
+
child = spawn3(node, [installedHookPath(), "dumate"], {
|
|
5201
|
+
stdio: ["pipe", "ignore", "ignore"],
|
|
5202
|
+
env: { ...process.env, DESKBUDDY_AGENT_PID: String(agentPid) }
|
|
5203
|
+
});
|
|
5204
|
+
} catch {
|
|
5205
|
+
resolve7();
|
|
5206
|
+
return;
|
|
5207
|
+
}
|
|
5208
|
+
const timer = setTimeout(() => child.kill("SIGKILL"), HOOK_KILL_MS);
|
|
5209
|
+
timer.unref?.();
|
|
5210
|
+
child.on("error", () => {
|
|
5211
|
+
clearTimeout(timer);
|
|
5212
|
+
resolve7();
|
|
5213
|
+
});
|
|
5214
|
+
child.on("close", () => {
|
|
5215
|
+
clearTimeout(timer);
|
|
5216
|
+
resolve7();
|
|
5217
|
+
});
|
|
5218
|
+
child.stdin.on("error", () => {
|
|
5219
|
+
});
|
|
5220
|
+
child.stdin.end(JSON.stringify(input));
|
|
5221
|
+
});
|
|
5222
|
+
}
|
|
3562
5223
|
|
|
3563
5224
|
// src/daemon/codexWatch.ts
|
|
3564
|
-
import { execFileSync as
|
|
5225
|
+
import { execFileSync as execFileSync10 } from "child_process";
|
|
3565
5226
|
import WebSocket from "ws";
|
|
3566
5227
|
var TURN_LIMIT_MS = 10 * 60 * 1e3;
|
|
3567
5228
|
var QUEUE_MAX_MS = 10 * 60 * 1e3;
|
|
3568
5229
|
var IDLE_GRACE_MS = 5 * 1e3;
|
|
3569
5230
|
var RESUME_RETRY_MS = 1e3;
|
|
3570
5231
|
var RESUME_MAX_TRIES = 120;
|
|
3571
|
-
var
|
|
5232
|
+
var REQUEST_TIMEOUT_MS3 = 30 * 1e3;
|
|
3572
5233
|
function finalReply(items) {
|
|
3573
5234
|
let last = "";
|
|
3574
5235
|
let final = "";
|
|
@@ -3606,7 +5267,7 @@ function discoverAppServers(configured) {
|
|
|
3606
5267
|
return parseAppServerUrls((winProcs() ?? []).map((p) => p.cmdline).join("\n"));
|
|
3607
5268
|
}
|
|
3608
5269
|
try {
|
|
3609
|
-
const out =
|
|
5270
|
+
const out = execFileSync10("ps", ["-A", "-ww", "-o", "args="], { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] });
|
|
3610
5271
|
return parseAppServerUrls(out);
|
|
3611
5272
|
} catch {
|
|
3612
5273
|
return [];
|
|
@@ -3636,6 +5297,7 @@ var CodexWatch = class {
|
|
|
3636
5297
|
/** fileChange 条目 → 改动的文件(授权请求本身不带文件,从 item/started 记) */
|
|
3637
5298
|
#changes = /* @__PURE__ */ new Map();
|
|
3638
5299
|
/** 服务端请求 id → 放掉等待的开关、所属会话 */
|
|
5300
|
+
/** 等设备答复的服务端请求。qids:选择题各题的 Codex 题目 id(按顺序),只有选择题有 —— 跟着这条记录走,不单独按请求 id 存(重连后 id 会从头编号) */
|
|
3639
5301
|
#pending = /* @__PURE__ */ new Map();
|
|
3640
5302
|
#stopped = false;
|
|
3641
5303
|
/** 已空闲的会话(有卡片在等时暂缓退订,卡片了结后补退) */
|
|
@@ -3749,13 +5411,13 @@ var CodexWatch = class {
|
|
|
3749
5411
|
}
|
|
3750
5412
|
#request(method, params) {
|
|
3751
5413
|
const id = this.#nextId++;
|
|
3752
|
-
return new Promise((
|
|
5414
|
+
return new Promise((resolve7) => {
|
|
3753
5415
|
const timer = setTimeout(() => {
|
|
3754
5416
|
this.#calls.delete(id);
|
|
3755
|
-
|
|
3756
|
-
},
|
|
5417
|
+
resolve7({ error: { message: "timeout" }, local: true });
|
|
5418
|
+
}, REQUEST_TIMEOUT_MS3);
|
|
3757
5419
|
timer.unref?.();
|
|
3758
|
-
this.#calls.set(id, { resolve:
|
|
5420
|
+
this.#calls.set(id, { resolve: resolve7, timer });
|
|
3759
5421
|
this.#send({ id, method, params });
|
|
3760
5422
|
});
|
|
3761
5423
|
}
|
|
@@ -3978,7 +5640,7 @@ var CodexWatch = class {
|
|
|
3978
5640
|
return { ...out, ...merged ? { merged } : {} };
|
|
3979
5641
|
}
|
|
3980
5642
|
#trackTurn(threadId, turnId) {
|
|
3981
|
-
return new Promise((
|
|
5643
|
+
return new Promise((resolve7) => {
|
|
3982
5644
|
const limit = this.#d.turnLimitMs ?? TURN_LIMIT_MS;
|
|
3983
5645
|
const step = this.#d.turnTickMs ?? 1e3;
|
|
3984
5646
|
const t = {
|
|
@@ -3988,7 +5650,7 @@ var CodexWatch = class {
|
|
|
3988
5650
|
used: 0,
|
|
3989
5651
|
timer: null,
|
|
3990
5652
|
grace: null,
|
|
3991
|
-
done: (end, text, error) =>
|
|
5653
|
+
done: (end, text, error) => resolve7({ sent: true, end, turnId, text, ...error ? { error } : {} })
|
|
3992
5654
|
};
|
|
3993
5655
|
this.#turns.set(turnId, t);
|
|
3994
5656
|
const early = this.#early.get(turnId);
|
|
@@ -4035,7 +5697,7 @@ var CodexWatch = class {
|
|
|
4035
5697
|
t.done(end, text || (end === "completed" ? "" : finalReply(t.items)), error);
|
|
4036
5698
|
}
|
|
4037
5699
|
#waitThread(threadId, ms) {
|
|
4038
|
-
return new Promise((
|
|
5700
|
+
return new Promise((resolve7) => {
|
|
4039
5701
|
let set = this.#wake.get(threadId);
|
|
4040
5702
|
if (!set) this.#wake.set(threadId, set = /* @__PURE__ */ new Set());
|
|
4041
5703
|
const s = set;
|
|
@@ -4043,7 +5705,7 @@ var CodexWatch = class {
|
|
|
4043
5705
|
clearTimeout(timer);
|
|
4044
5706
|
s.delete(fire);
|
|
4045
5707
|
if (s.size === 0 && this.#wake.get(threadId) === s) this.#wake.delete(threadId);
|
|
4046
|
-
|
|
5708
|
+
resolve7();
|
|
4047
5709
|
};
|
|
4048
5710
|
const timer = setTimeout(fire, ms);
|
|
4049
5711
|
timer.unref?.();
|
|
@@ -4177,6 +5839,7 @@ var CodexWatch = class {
|
|
|
4177
5839
|
const threadId = typeof p.threadId === "string" ? p.threadId : "";
|
|
4178
5840
|
if (!threadId) return;
|
|
4179
5841
|
let hook;
|
|
5842
|
+
let qids;
|
|
4180
5843
|
if (method === "item/commandExecution/requestApproval") {
|
|
4181
5844
|
const command = typeof p.command === "string" ? unwrapShell(p.command) : "";
|
|
4182
5845
|
if (!command) return;
|
|
@@ -4187,12 +5850,17 @@ var CodexWatch = class {
|
|
|
4187
5850
|
const shown = paths.join(", ");
|
|
4188
5851
|
if (!shown) return;
|
|
4189
5852
|
hook = { tool_name: "Edit", tool_input: { file_path: shown }, cwd: this.#cwd.get(threadId) ?? "" };
|
|
5853
|
+
} else if (method === "item/tool/requestUserInput") {
|
|
5854
|
+
const q = codexQuestionInput(p);
|
|
5855
|
+
if (!q) return;
|
|
5856
|
+
hook = { tool_name: "AskUserQuestion", tool_input: q.toolInput, cwd: this.#cwd.get(threadId) ?? "" };
|
|
5857
|
+
qids = q.ids;
|
|
4190
5858
|
} else {
|
|
4191
5859
|
return;
|
|
4192
5860
|
}
|
|
4193
5861
|
const key = String(id);
|
|
4194
5862
|
const ac = new AbortController();
|
|
4195
|
-
const entry = { ac, threadId };
|
|
5863
|
+
const entry = { ac, threadId, ...qids ? { qids } : {} };
|
|
4196
5864
|
this.#pending.set(key, entry);
|
|
4197
5865
|
const req2 = {
|
|
4198
5866
|
window: true,
|
|
@@ -4205,7 +5873,14 @@ var CodexWatch = class {
|
|
|
4205
5873
|
void this.#d.gate(req2, ac.signal).then((reply) => {
|
|
4206
5874
|
if (this.#pending.get(key) !== entry) return;
|
|
4207
5875
|
this.#pending.delete(key);
|
|
4208
|
-
if (
|
|
5876
|
+
if (qids) {
|
|
5877
|
+
const lists = reply.decision === "allow" ? reply.answerLists : void 0;
|
|
5878
|
+
if (Array.isArray(lists) && lists.length === qids.length) {
|
|
5879
|
+
const answers = {};
|
|
5880
|
+
qids.forEach((qid, i) => answers[qid] = { answers: lists[i] });
|
|
5881
|
+
this.#send({ id, result: { answers } });
|
|
5882
|
+
}
|
|
5883
|
+
} else if (reply.decision === "allow") this.#send({ id, result: { decision: "accept" } });
|
|
4209
5884
|
else if (reply.decision === "deny") this.#send({ id, result: { decision: "decline" } });
|
|
4210
5885
|
settled();
|
|
4211
5886
|
}).catch(() => {
|
|
@@ -4214,19 +5889,41 @@ var CodexWatch = class {
|
|
|
4214
5889
|
});
|
|
4215
5890
|
}
|
|
4216
5891
|
};
|
|
5892
|
+
function codexQuestionInput(p) {
|
|
5893
|
+
const qs = p.questions;
|
|
5894
|
+
if (!Array.isArray(qs) || qs.length === 0) return null;
|
|
5895
|
+
const ids = [];
|
|
5896
|
+
const questions = [];
|
|
5897
|
+
for (const q of qs) {
|
|
5898
|
+
if (!q || typeof q !== "object" || typeof q.id !== "string" || typeof q.question !== "string") return null;
|
|
5899
|
+
if (ids.includes(q.id)) return null;
|
|
5900
|
+
if (q.isSecret === true) return null;
|
|
5901
|
+
if (!Array.isArray(q.options) || q.options.length < 2) return null;
|
|
5902
|
+
ids.push(q.id);
|
|
5903
|
+
questions.push({
|
|
5904
|
+
question: q.question,
|
|
5905
|
+
header: typeof q.header === "string" ? q.header : "",
|
|
5906
|
+
multiSelect: false,
|
|
5907
|
+
options: q.options.map((o) => ({ label: o?.label, description: typeof o?.description === "string" ? o.description : "" })),
|
|
5908
|
+
// isOther=true 才允许自己填写(Codex 的 TUI 这时多一个「Other」)
|
|
5909
|
+
...q.isOther === true ? {} : { custom: false }
|
|
5910
|
+
});
|
|
5911
|
+
}
|
|
5912
|
+
return { toolInput: { questions }, ids };
|
|
5913
|
+
}
|
|
4217
5914
|
|
|
4218
5915
|
// src/core/codexRemote.ts
|
|
4219
|
-
import { mkdirSync as
|
|
4220
|
-
import { join as
|
|
5916
|
+
import { mkdirSync as mkdirSync14, renameSync as renameSync10, rmSync as rmSync7, writeFileSync as writeFileSync12 } from "fs";
|
|
5917
|
+
import { join as join20 } from "path";
|
|
4221
5918
|
function codexRemotePath() {
|
|
4222
|
-
return
|
|
5919
|
+
return join20(deskbuddyHome(), "codex-remote.json");
|
|
4223
5920
|
}
|
|
4224
5921
|
function writeCodexRemote(threads, pid = process.pid, now = Date.now()) {
|
|
4225
5922
|
try {
|
|
4226
|
-
|
|
5923
|
+
mkdirSync14(deskbuddyHome(), { recursive: true });
|
|
4227
5924
|
const p = codexRemotePath();
|
|
4228
|
-
|
|
4229
|
-
|
|
5925
|
+
writeFileSync12(p + ".tmp", JSON.stringify({ pid, threads: [...threads], at: now }), "utf8");
|
|
5926
|
+
renameSync10(p + ".tmp", p);
|
|
4230
5927
|
} catch {
|
|
4231
5928
|
}
|
|
4232
5929
|
}
|
|
@@ -4243,16 +5940,16 @@ import { chmodSync as chmodSync4, rmSync as rmSync9 } from "fs";
|
|
|
4243
5940
|
import { connect, createServer as createServer2 } from "net";
|
|
4244
5941
|
|
|
4245
5942
|
// src/core/wake.ts
|
|
4246
|
-
import { execFileSync as
|
|
5943
|
+
import { execFileSync as execFileSync11 } from "child_process";
|
|
4247
5944
|
import { createHash as createHash3, createHmac as createHmac2, timingSafeEqual as timingSafeEqual2 } from "crypto";
|
|
4248
|
-
import { chmodSync as chmodSync3, closeSync as
|
|
5945
|
+
import { chmodSync as chmodSync3, closeSync as closeSync4, mkdirSync as mkdirSync15, openSync as openSync4, readdirSync as readdirSync5, readFileSync as readFileSync14, readSync as readSync3, renameSync as renameSync11, rmSync as rmSync8, statSync as statSync7, writeFileSync as writeFileSync13 } from "fs";
|
|
4249
5946
|
import { tmpdir as tmpdir2 } from "os";
|
|
4250
|
-
import { join as
|
|
5947
|
+
import { join as join21 } from "path";
|
|
4251
5948
|
function wakeDir(home = deskbuddyHome()) {
|
|
4252
|
-
return
|
|
5949
|
+
return join21(home, "wake");
|
|
4253
5950
|
}
|
|
4254
5951
|
function marksDir(home = deskbuddyHome()) {
|
|
4255
|
-
return
|
|
5952
|
+
return join21(wakeDir(home), "marks");
|
|
4256
5953
|
}
|
|
4257
5954
|
function safeSid(sessionId) {
|
|
4258
5955
|
return String(sessionId).replace(/[^A-Za-z0-9_-]/g, "");
|
|
@@ -4262,10 +5959,10 @@ function homeTag(home) {
|
|
|
4262
5959
|
}
|
|
4263
5960
|
function wakeChannelPath(platform = process.platform, home = deskbuddyHome()) {
|
|
4264
5961
|
if (platform === "win32") return `\\\\.\\pipe\\deskbuddy-wake-${homeTag(home)}`;
|
|
4265
|
-
const primary =
|
|
5962
|
+
const primary = join21(wakeDir(home), "wake.sock");
|
|
4266
5963
|
if (Buffer.byteLength(primary) <= 103) return primary;
|
|
4267
5964
|
const uid = typeof process.getuid === "function" ? process.getuid() : 0;
|
|
4268
|
-
return
|
|
5965
|
+
return join21(tmpdir2(), `deskbuddy-${uid}`, `wake-${homeTag(home)}.sock`);
|
|
4269
5966
|
}
|
|
4270
5967
|
function pidExists(pid) {
|
|
4271
5968
|
if (!Number.isInteger(pid) || pid <= 0) return false;
|
|
@@ -4280,11 +5977,11 @@ function writeWakeMark(sessionId, agentPid, now = Date.now()) {
|
|
|
4280
5977
|
const safe = safeSid(sessionId);
|
|
4281
5978
|
if (!safe) return;
|
|
4282
5979
|
try {
|
|
4283
|
-
|
|
4284
|
-
const file =
|
|
5980
|
+
mkdirSync15(marksDir(), { recursive: true, mode: 448 });
|
|
5981
|
+
const file = join21(marksDir(), `${safe}.json`);
|
|
4285
5982
|
const tmp = `${file}.${process.pid}.tmp`;
|
|
4286
|
-
|
|
4287
|
-
|
|
5983
|
+
writeFileSync13(tmp, JSON.stringify({ agentPid: agentPid ?? 0, daemonPid: process.pid, at: now }), { encoding: "utf8", mode: 384 });
|
|
5984
|
+
renameSync11(tmp, file);
|
|
4288
5985
|
} catch {
|
|
4289
5986
|
}
|
|
4290
5987
|
}
|
|
@@ -4292,20 +5989,20 @@ function removeWakeMark(sessionId) {
|
|
|
4292
5989
|
const safe = safeSid(sessionId);
|
|
4293
5990
|
if (!safe) return;
|
|
4294
5991
|
try {
|
|
4295
|
-
rmSync8(
|
|
5992
|
+
rmSync8(join21(marksDir(), `${safe}.json`), { force: true });
|
|
4296
5993
|
} catch {
|
|
4297
5994
|
}
|
|
4298
5995
|
}
|
|
4299
5996
|
function clearWakeMarks() {
|
|
4300
5997
|
let names = [];
|
|
4301
5998
|
try {
|
|
4302
|
-
names =
|
|
5999
|
+
names = readdirSync5(marksDir());
|
|
4303
6000
|
} catch {
|
|
4304
6001
|
return;
|
|
4305
6002
|
}
|
|
4306
6003
|
for (const n of names) {
|
|
4307
6004
|
try {
|
|
4308
|
-
rmSync8(
|
|
6005
|
+
rmSync8(join21(marksDir(), n), { force: true });
|
|
4309
6006
|
} catch {
|
|
4310
6007
|
}
|
|
4311
6008
|
}
|
|
@@ -4314,7 +6011,7 @@ function isWakeMarked(sessionId, alive = pidExists) {
|
|
|
4314
6011
|
const safe = safeSid(sessionId);
|
|
4315
6012
|
if (!safe) return false;
|
|
4316
6013
|
try {
|
|
4317
|
-
const m = JSON.parse(readFileSync14(
|
|
6014
|
+
const m = JSON.parse(readFileSync14(join21(marksDir(), `${safe}.json`), "utf8"));
|
|
4318
6015
|
const daemonPid = Number(m?.daemonPid) || 0;
|
|
4319
6016
|
if (!daemonPid || !alive(daemonPid)) return false;
|
|
4320
6017
|
const pid = Number(m?.agentPid) || 0;
|
|
@@ -4327,30 +6024,30 @@ function withWake(states, alive) {
|
|
|
4327
6024
|
return states.map((s) => s.remoteWindow === true || !isWakeMarked(s.sessionId, alive) ? s : { ...s, remoteWindow: true });
|
|
4328
6025
|
}
|
|
4329
6026
|
function wakeSecretPath(home = deskbuddyHome()) {
|
|
4330
|
-
return
|
|
6027
|
+
return join21(wakeDir(home), "secret");
|
|
4331
6028
|
}
|
|
4332
6029
|
function writeWakeSecret(hex, platform = process.platform) {
|
|
4333
6030
|
const file = wakeSecretPath();
|
|
4334
|
-
|
|
6031
|
+
mkdirSync15(wakeDir(), { recursive: true, mode: 448 });
|
|
4335
6032
|
const tmp = `${file}.${process.pid}.tmp`;
|
|
4336
|
-
|
|
6033
|
+
writeFileSync13(tmp, hex, { encoding: "utf8", mode: 384 });
|
|
4337
6034
|
if (platform === "win32") {
|
|
4338
6035
|
try {
|
|
4339
|
-
|
|
6036
|
+
execFileSync11("icacls", [tmp, "/inheritance:r", "/grant:r", `${currentUserSid()}:F`], { stdio: "ignore", windowsHide: true });
|
|
4340
6037
|
} catch {
|
|
4341
|
-
|
|
6038
|
+
renameSync11(tmp, file);
|
|
4342
6039
|
return "\u7A97\u53E3\u5524\u9192\u5BC6\u94A5\u6587\u4EF6\u7684\u6743\u9650\u6CA1\u80FD\u6536\u7D27\uFF08icacls \u5931\u8D25\uFF09";
|
|
4343
6040
|
}
|
|
4344
6041
|
} else {
|
|
4345
6042
|
chmodSync3(tmp, 384);
|
|
4346
6043
|
}
|
|
4347
|
-
|
|
6044
|
+
renameSync11(tmp, file);
|
|
4348
6045
|
return "";
|
|
4349
6046
|
}
|
|
4350
6047
|
function restrictToUser(file, platform = process.platform) {
|
|
4351
6048
|
if (platform !== "win32") return true;
|
|
4352
6049
|
try {
|
|
4353
|
-
|
|
6050
|
+
execFileSync11("icacls", [file, "/inheritance:r", "/grant:r", `${currentUserSid()}:F`], { stdio: "ignore", windowsHide: true });
|
|
4354
6051
|
return true;
|
|
4355
6052
|
} catch {
|
|
4356
6053
|
return false;
|
|
@@ -4358,7 +6055,7 @@ function restrictToUser(file, platform = process.platform) {
|
|
|
4358
6055
|
}
|
|
4359
6056
|
function currentUserSid() {
|
|
4360
6057
|
try {
|
|
4361
|
-
const out =
|
|
6058
|
+
const out = execFileSync11("whoami", ["/user", "/fo", "csv", "/nh"], { encoding: "utf8", windowsHide: true });
|
|
4362
6059
|
const m = /"(S-1-[0-9-]+)"/.exec(out);
|
|
4363
6060
|
if (m) return `*${m[1]}`;
|
|
4364
6061
|
} catch {
|
|
@@ -4393,11 +6090,11 @@ function analyzeTranscript(file) {
|
|
|
4393
6090
|
if (!file) return null;
|
|
4394
6091
|
let fd;
|
|
4395
6092
|
try {
|
|
4396
|
-
const size =
|
|
6093
|
+
const size = statSync7(file).size;
|
|
4397
6094
|
const len = Math.min(size, TRANSCRIPT_TAIL2);
|
|
4398
6095
|
const buf = Buffer.alloc(len);
|
|
4399
|
-
fd =
|
|
4400
|
-
|
|
6096
|
+
fd = openSync4(file, "r");
|
|
6097
|
+
readSync3(fd, buf, 0, len, size - len);
|
|
4401
6098
|
const lines2 = buf.toString("utf8").split("\n");
|
|
4402
6099
|
let seg = false;
|
|
4403
6100
|
let reply = null;
|
|
@@ -4438,7 +6135,7 @@ function analyzeTranscript(file) {
|
|
|
4438
6135
|
} finally {
|
|
4439
6136
|
if (fd !== void 0) {
|
|
4440
6137
|
try {
|
|
4441
|
-
|
|
6138
|
+
closeSync4(fd);
|
|
4442
6139
|
} catch {
|
|
4443
6140
|
}
|
|
4444
6141
|
}
|
|
@@ -4489,7 +6186,7 @@ function replyMac(secret, nonceC, r) {
|
|
|
4489
6186
|
return wakeMac(secret, "r", nonceC, r.instructionId, r.status, r.reason ?? "");
|
|
4490
6187
|
}
|
|
4491
6188
|
function startChannelServer(path, secret, onRequest, platform = process.platform) {
|
|
4492
|
-
return new Promise((
|
|
6189
|
+
return new Promise((resolve7) => {
|
|
4493
6190
|
const server = createServer2((sock) => {
|
|
4494
6191
|
let open = true;
|
|
4495
6192
|
let phase = "hello";
|
|
@@ -4550,7 +6247,7 @@ function startChannelServer(path, secret, onRequest, platform = process.platform
|
|
|
4550
6247
|
};
|
|
4551
6248
|
server.once("error", () => {
|
|
4552
6249
|
restore();
|
|
4553
|
-
|
|
6250
|
+
resolve7(null);
|
|
4554
6251
|
});
|
|
4555
6252
|
server.listen(path, () => {
|
|
4556
6253
|
restore();
|
|
@@ -4562,7 +6259,7 @@ function startChannelServer(path, secret, onRequest, platform = process.platform
|
|
|
4562
6259
|
}
|
|
4563
6260
|
server.on("error", () => {
|
|
4564
6261
|
});
|
|
4565
|
-
|
|
6262
|
+
resolve7(server);
|
|
4566
6263
|
});
|
|
4567
6264
|
});
|
|
4568
6265
|
}
|
|
@@ -4580,9 +6277,9 @@ function closeChannelServer(server, path, platform = process.platform) {
|
|
|
4580
6277
|
}
|
|
4581
6278
|
var FINAL = /* @__PURE__ */ new Set(["started", "rejected", "failed", "not_here"]);
|
|
4582
6279
|
function sendToWindow(path, secret, req2, onReply, opts = {}) {
|
|
4583
|
-
return new Promise((
|
|
6280
|
+
return new Promise((resolve7) => {
|
|
4584
6281
|
if (!secret) {
|
|
4585
|
-
|
|
6282
|
+
resolve7("fallback");
|
|
4586
6283
|
return;
|
|
4587
6284
|
}
|
|
4588
6285
|
let settled = false;
|
|
@@ -4595,13 +6292,13 @@ function sendToWindow(path, secret, req2, onReply, opts = {}) {
|
|
|
4595
6292
|
settled = true;
|
|
4596
6293
|
handled = v === "handled";
|
|
4597
6294
|
clearTimeout(timer);
|
|
4598
|
-
|
|
6295
|
+
resolve7(v);
|
|
4599
6296
|
};
|
|
4600
6297
|
let sock;
|
|
4601
6298
|
try {
|
|
4602
6299
|
sock = connect(path);
|
|
4603
6300
|
} catch {
|
|
4604
|
-
|
|
6301
|
+
resolve7("fallback");
|
|
4605
6302
|
return;
|
|
4606
6303
|
}
|
|
4607
6304
|
const timer = setTimeout(() => {
|
|
@@ -4723,29 +6420,29 @@ function boxMatches(view, body) {
|
|
|
4723
6420
|
}
|
|
4724
6421
|
|
|
4725
6422
|
// src/core/wrapWindows.ts
|
|
4726
|
-
import { chmodSync as chmodSync5, mkdirSync as
|
|
6423
|
+
import { chmodSync as chmodSync5, mkdirSync as mkdirSync16, readdirSync as readdirSync6, readFileSync as readFileSync15, renameSync as renameSync12, rmSync as rmSync10, statSync as statSync8, writeFileSync as writeFileSync14 } from "fs";
|
|
4727
6424
|
import { tmpdir as tmpdir3 } from "os";
|
|
4728
|
-
import { join as
|
|
6425
|
+
import { join as join22 } from "path";
|
|
4729
6426
|
var WINDOW_ID_RE = /^[a-f0-9]{32}$/;
|
|
4730
6427
|
function windowsDir() {
|
|
4731
|
-
return
|
|
6428
|
+
return join22(deskbuddyHome(), "windows");
|
|
4732
6429
|
}
|
|
4733
6430
|
function bySessionDir() {
|
|
4734
|
-
return
|
|
6431
|
+
return join22(windowsDir(), "by-session");
|
|
4735
6432
|
}
|
|
4736
6433
|
function safeSid2(sessionId) {
|
|
4737
6434
|
return String(sessionId).replace(/[^A-Za-z0-9_-]/g, "");
|
|
4738
6435
|
}
|
|
4739
6436
|
function ensureDir(dir) {
|
|
4740
|
-
|
|
6437
|
+
mkdirSync16(dir, { recursive: true, mode: 448 });
|
|
4741
6438
|
}
|
|
4742
6439
|
function atomicWrite(file, text) {
|
|
4743
6440
|
const tmp = `${file}.${process.pid}.tmp`;
|
|
4744
|
-
|
|
4745
|
-
|
|
6441
|
+
writeFileSync14(tmp, text, { encoding: "utf8", mode: 384 });
|
|
6442
|
+
renameSync12(tmp, file);
|
|
4746
6443
|
}
|
|
4747
6444
|
function windowFile(windowId) {
|
|
4748
|
-
return
|
|
6445
|
+
return join22(windowsDir(), `${windowId}.json`);
|
|
4749
6446
|
}
|
|
4750
6447
|
function writeWindow(windowId, rec) {
|
|
4751
6448
|
if (!WINDOW_ID_RE.test(windowId)) return;
|
|
@@ -4776,12 +6473,12 @@ function removeWindow(windowId) {
|
|
|
4776
6473
|
}
|
|
4777
6474
|
let names = [];
|
|
4778
6475
|
try {
|
|
4779
|
-
names =
|
|
6476
|
+
names = readdirSync6(bySessionDir());
|
|
4780
6477
|
} catch {
|
|
4781
6478
|
return;
|
|
4782
6479
|
}
|
|
4783
6480
|
for (const n of names) {
|
|
4784
|
-
const f =
|
|
6481
|
+
const f = join22(bySessionDir(), n);
|
|
4785
6482
|
try {
|
|
4786
6483
|
if (readFileSync15(f, "utf8").trim() === windowId) rmSync10(f, { force: true });
|
|
4787
6484
|
} catch {
|
|
@@ -4792,14 +6489,14 @@ function readBySession(sessionId) {
|
|
|
4792
6489
|
const safe = safeSid2(sessionId);
|
|
4793
6490
|
if (!safe) return null;
|
|
4794
6491
|
try {
|
|
4795
|
-
const id = readFileSync15(
|
|
6492
|
+
const id = readFileSync15(join22(bySessionDir(), safe), "utf8").trim();
|
|
4796
6493
|
return WINDOW_ID_RE.test(id) ? id : null;
|
|
4797
6494
|
} catch {
|
|
4798
6495
|
return null;
|
|
4799
6496
|
}
|
|
4800
6497
|
}
|
|
4801
6498
|
function lastEventFile(windowId) {
|
|
4802
|
-
return
|
|
6499
|
+
return join22(windowsDir(), `${windowId}.last`);
|
|
4803
6500
|
}
|
|
4804
6501
|
function readLastEvent(windowId) {
|
|
4805
6502
|
if (!WINDOW_ID_RE.test(windowId)) return null;
|
|
@@ -4813,16 +6510,16 @@ function readLastEvent(windowId) {
|
|
|
4813
6510
|
function channelPath(windowId, platform = process.platform, home = deskbuddyHome()) {
|
|
4814
6511
|
if (platform === "win32") return `\\\\.\\pipe\\deskbuddy-${windowId}`;
|
|
4815
6512
|
const name = `${windowId.slice(0, 16)}.sock`;
|
|
4816
|
-
const primary =
|
|
6513
|
+
const primary = join22(home, "win", name);
|
|
4817
6514
|
if (Buffer.byteLength(primary) <= 103) return primary;
|
|
4818
6515
|
const uid = typeof process.getuid === "function" ? process.getuid() : 0;
|
|
4819
|
-
return
|
|
6516
|
+
return join22(tmpdir3(), `deskbuddy-${uid}`, name);
|
|
4820
6517
|
}
|
|
4821
6518
|
function ensureChannelDir(sockPath) {
|
|
4822
|
-
const dir =
|
|
6519
|
+
const dir = join22(sockPath, "..");
|
|
4823
6520
|
try {
|
|
4824
|
-
|
|
4825
|
-
const st =
|
|
6521
|
+
mkdirSync16(dir, { recursive: true, mode: 448 });
|
|
6522
|
+
const st = statSync8(dir);
|
|
4826
6523
|
if (typeof process.getuid === "function" && st.uid !== process.getuid()) return false;
|
|
4827
6524
|
chmodSync5(dir, 448);
|
|
4828
6525
|
return true;
|
|
@@ -4841,38 +6538,44 @@ function windowForSession(sessionId) {
|
|
|
4841
6538
|
}
|
|
4842
6539
|
|
|
4843
6540
|
// src/core/launch.ts
|
|
4844
|
-
import { mkdirSync as
|
|
4845
|
-
import { join as
|
|
6541
|
+
import { mkdirSync as mkdirSync17, readFileSync as readFileSync16, renameSync as renameSync13, rmSync as rmSync11, writeFileSync as writeFileSync15 } from "fs";
|
|
6542
|
+
import { join as join23 } from "path";
|
|
4846
6543
|
var RUN_ID_RE = /^[a-f0-9]{32}$/;
|
|
4847
6544
|
var SESSION_UUID_RE = /^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$/;
|
|
6545
|
+
var LAUNCH_TOOLS = ["claude_code", "codex", "workbuddy"];
|
|
6546
|
+
function validPlan(p) {
|
|
6547
|
+
if (!LAUNCH_TOOLS.includes(p.tool)) return false;
|
|
6548
|
+
if (p.resume && (p.tool !== "claude_code" || !SESSION_UUID_RE.test(p.resume))) return false;
|
|
6549
|
+
return true;
|
|
6550
|
+
}
|
|
4848
6551
|
function launchDir(home = deskbuddyHome()) {
|
|
4849
|
-
return
|
|
6552
|
+
return join23(home, "launch");
|
|
4850
6553
|
}
|
|
4851
6554
|
function planFile(runId) {
|
|
4852
|
-
return
|
|
6555
|
+
return join23(launchDir(), `${runId}.json`);
|
|
4853
6556
|
}
|
|
4854
6557
|
function writeLaunch(runId, plan) {
|
|
4855
6558
|
if (!RUN_ID_RE.test(runId)) throw new Error("bad runId");
|
|
4856
|
-
if (
|
|
4857
|
-
|
|
6559
|
+
if (!validPlan(plan)) throw new Error("bad launch plan");
|
|
6560
|
+
mkdirSync17(launchDir(), { recursive: true, mode: 448 });
|
|
4858
6561
|
const tmp = `${planFile(runId)}.${process.pid}.tmp`;
|
|
4859
|
-
|
|
6562
|
+
writeFileSync15(tmp, JSON.stringify(plan), { encoding: "utf8", mode: 384 });
|
|
4860
6563
|
restrictToUser(tmp);
|
|
4861
|
-
|
|
6564
|
+
renameSync13(tmp, planFile(runId));
|
|
4862
6565
|
}
|
|
4863
6566
|
function claimLaunch(runId) {
|
|
4864
6567
|
if (!RUN_ID_RE.test(runId)) return null;
|
|
4865
|
-
const claimed =
|
|
6568
|
+
const claimed = join23(launchDir(), `${runId}.${process.pid}.claimed`);
|
|
4866
6569
|
try {
|
|
4867
|
-
|
|
6570
|
+
renameSync13(planFile(runId), claimed);
|
|
4868
6571
|
} catch {
|
|
4869
6572
|
return null;
|
|
4870
6573
|
}
|
|
4871
6574
|
try {
|
|
4872
6575
|
const p = JSON.parse(readFileSync16(claimed, "utf8"));
|
|
4873
6576
|
if (!p || typeof p.cwd !== "string" || typeof p.resume !== "string") return null;
|
|
4874
|
-
if (p.
|
|
4875
|
-
return p;
|
|
6577
|
+
if (p.tool === void 0) p.tool = "claude_code";
|
|
6578
|
+
return validPlan(p) ? p : null;
|
|
4876
6579
|
} catch {
|
|
4877
6580
|
return null;
|
|
4878
6581
|
} finally {
|
|
@@ -4892,10 +6595,10 @@ function launchPending(runId) {
|
|
|
4892
6595
|
}
|
|
4893
6596
|
|
|
4894
6597
|
// src/daemon/openWindow.ts
|
|
4895
|
-
import { execFile as execFile4, execFileSync as
|
|
4896
|
-
import { chmodSync as chmodSync6, mkdirSync as
|
|
6598
|
+
import { execFile as execFile4, execFileSync as execFileSync12, spawn as spawn4 } from "child_process";
|
|
6599
|
+
import { chmodSync as chmodSync6, mkdirSync as mkdirSync18, rmSync as rmSync12, writeFileSync as writeFileSync16 } from "fs";
|
|
4897
6600
|
import { userInfo } from "os";
|
|
4898
|
-
import { join as
|
|
6601
|
+
import { join as join24 } from "path";
|
|
4899
6602
|
function safePath(p) {
|
|
4900
6603
|
return /^(?:[A-Za-z]:[\\/]|\/)/.test(p) && !/[\r\n\0"'`$%;]/.test(p);
|
|
4901
6604
|
}
|
|
@@ -4943,14 +6646,14 @@ var defaultGuiDeps = () => ({
|
|
|
4943
6646
|
env: process.env,
|
|
4944
6647
|
macManager: () => {
|
|
4945
6648
|
try {
|
|
4946
|
-
return
|
|
6649
|
+
return execFileSync12("launchctl", ["managername"], { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"], timeout: 3e3 });
|
|
4947
6650
|
} catch {
|
|
4948
6651
|
return "";
|
|
4949
6652
|
}
|
|
4950
6653
|
},
|
|
4951
6654
|
macConsoleUser: () => {
|
|
4952
6655
|
try {
|
|
4953
|
-
return
|
|
6656
|
+
return execFileSync12("stat", ["-f", "%Su", "/dev/console"], { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"], timeout: 3e3 });
|
|
4954
6657
|
} catch {
|
|
4955
6658
|
return "";
|
|
4956
6659
|
}
|
|
@@ -4964,7 +6667,7 @@ var defaultGuiDeps = () => ({
|
|
|
4964
6667
|
},
|
|
4965
6668
|
winSessionId: () => {
|
|
4966
6669
|
try {
|
|
4967
|
-
const out =
|
|
6670
|
+
const out = execFileSync12("tasklist", ["/FI", `PID eq ${process.pid}`, "/FO", "CSV", "/NH"], {
|
|
4968
6671
|
encoding: "utf8",
|
|
4969
6672
|
stdio: ["ignore", "pipe", "ignore"],
|
|
4970
6673
|
windowsHide: true,
|
|
@@ -4981,11 +6684,11 @@ var defaultGuiDeps = () => ({
|
|
|
4981
6684
|
var defaultOpenerDeps = (findWt2) => ({
|
|
4982
6685
|
platform: process.platform,
|
|
4983
6686
|
findWt: findWt2,
|
|
4984
|
-
run: (file, args) => new Promise((
|
|
4985
|
-
execFile4(file, args, { windowsHide: true, timeout: 15e3 }, (err) => err ? reject(err) :
|
|
6687
|
+
run: (file, args) => new Promise((resolve7, reject) => {
|
|
6688
|
+
execFile4(file, args, { windowsHide: true, timeout: 15e3 }, (err) => err ? reject(err) : resolve7());
|
|
4986
6689
|
}),
|
|
4987
6690
|
spawnDetached: (file, args) => {
|
|
4988
|
-
const c =
|
|
6691
|
+
const c = spawn4(file, args, { detached: true, stdio: "ignore", shell: false, windowsHide: false });
|
|
4989
6692
|
c.on("error", () => {
|
|
4990
6693
|
});
|
|
4991
6694
|
c.unref();
|
|
@@ -4994,9 +6697,9 @@ var defaultOpenerDeps = (findWt2) => ({
|
|
|
4994
6697
|
async function openTerminal(t, d) {
|
|
4995
6698
|
checkTarget(t);
|
|
4996
6699
|
if (d.platform === "darwin") {
|
|
4997
|
-
|
|
4998
|
-
const file =
|
|
4999
|
-
|
|
6700
|
+
mkdirSync18(launchDir(t.home), { recursive: true, mode: 448 });
|
|
6701
|
+
const file = join24(launchDir(t.home), `${t.runId}.command`);
|
|
6702
|
+
writeFileSync16(file, macCommandScript(t), { encoding: "utf8", mode: 448 });
|
|
5000
6703
|
chmodSync6(file, 448);
|
|
5001
6704
|
await d.run("open", ["-a", "Terminal", file]);
|
|
5002
6705
|
return "terminal";
|
|
@@ -5015,7 +6718,7 @@ async function openTerminal(t, d) {
|
|
|
5015
6718
|
function removeCommandFile(home, runId) {
|
|
5016
6719
|
if (!RUN_ID_RE.test(runId)) return;
|
|
5017
6720
|
try {
|
|
5018
|
-
rmSync12(
|
|
6721
|
+
rmSync12(join24(launchDir(home), `${runId}.command`), { force: true });
|
|
5019
6722
|
} catch {
|
|
5020
6723
|
}
|
|
5021
6724
|
}
|
|
@@ -5321,7 +7024,7 @@ function startWakeServer(path, hub, secret, platform = process.platform) {
|
|
|
5321
7024
|
} catch {
|
|
5322
7025
|
}
|
|
5323
7026
|
}
|
|
5324
|
-
return new Promise((
|
|
7027
|
+
return new Promise((resolve7) => {
|
|
5325
7028
|
const server = createServer3((sock) => {
|
|
5326
7029
|
sock.on("error", () => {
|
|
5327
7030
|
});
|
|
@@ -5407,30 +7110,30 @@ function startWakeServer(path, hub, secret, platform = process.platform) {
|
|
|
5407
7110
|
};
|
|
5408
7111
|
server.once("error", () => {
|
|
5409
7112
|
restore();
|
|
5410
|
-
|
|
7113
|
+
resolve7(null);
|
|
5411
7114
|
});
|
|
5412
7115
|
server.listen(path, () => {
|
|
5413
7116
|
restore();
|
|
5414
7117
|
server.on("error", () => {
|
|
5415
7118
|
});
|
|
5416
|
-
|
|
7119
|
+
resolve7(server);
|
|
5417
7120
|
});
|
|
5418
7121
|
});
|
|
5419
7122
|
}
|
|
5420
7123
|
|
|
5421
7124
|
// src/core/appended.ts
|
|
5422
|
-
import { chmodSync as chmodSync7, mkdirSync as
|
|
5423
|
-
import { join as
|
|
7125
|
+
import { chmodSync as chmodSync7, mkdirSync as mkdirSync19, readFileSync as readFileSync17, renameSync as renameSync14, rmSync as rmSync14, writeFileSync as writeFileSync17 } from "fs";
|
|
7126
|
+
import { join as join25 } from "path";
|
|
5424
7127
|
var APPENDED_MAX_ITEMS = 5;
|
|
5425
7128
|
var APPENDED_TEXT_MAX = 500;
|
|
5426
7129
|
var APPENDED_RESULT_MAX = 800;
|
|
5427
7130
|
var APPENDED_TTL_MS = 7 * 24 * 60 * 60 * 1e3;
|
|
5428
7131
|
function appendedDir() {
|
|
5429
|
-
return
|
|
7132
|
+
return join25(deskbuddyHome(), "appended");
|
|
5430
7133
|
}
|
|
5431
7134
|
function appendedPath(sessionId) {
|
|
5432
7135
|
const safe = sessionId.replace(/[^A-Za-z0-9_-]/g, "");
|
|
5433
|
-
return safe ?
|
|
7136
|
+
return safe ? join25(appendedDir(), `${safe}.json`) : null;
|
|
5434
7137
|
}
|
|
5435
7138
|
function readAppended(sessionId) {
|
|
5436
7139
|
const file = appendedPath(sessionId);
|
|
@@ -5449,26 +7152,26 @@ function writeAppended(sessionId, item) {
|
|
|
5449
7152
|
const items = readAppended(sessionId).concat([
|
|
5450
7153
|
{ text: item.text.slice(0, APPENDED_TEXT_MAX), result: item.result.slice(0, APPENDED_RESULT_MAX), ok: item.ok, at: item.at }
|
|
5451
7154
|
]);
|
|
5452
|
-
|
|
7155
|
+
mkdirSync19(appendedDir(), { recursive: true, mode: 448 });
|
|
5453
7156
|
const tmp = `${file}.${process.pid}.tmp`;
|
|
5454
|
-
|
|
7157
|
+
writeFileSync17(tmp, JSON.stringify({ items: items.slice(-APPENDED_MAX_ITEMS) }), { encoding: "utf8", mode: 384 });
|
|
5455
7158
|
chmodSync7(tmp, 384);
|
|
5456
|
-
|
|
7159
|
+
renameSync14(tmp, file);
|
|
5457
7160
|
} catch {
|
|
5458
7161
|
}
|
|
5459
7162
|
}
|
|
5460
7163
|
var RUNNING_TTL_MS = 60 * 60 * 1e3;
|
|
5461
7164
|
function runningPath(sessionId) {
|
|
5462
7165
|
const safe = sessionId.replace(/[^A-Za-z0-9_-]/g, "");
|
|
5463
|
-
return safe ?
|
|
7166
|
+
return safe ? join25(deskbuddyHome(), "running", `${safe}.json`) : null;
|
|
5464
7167
|
}
|
|
5465
7168
|
function writeRunning(sessionId, now = Date.now()) {
|
|
5466
7169
|
const file = runningPath(sessionId);
|
|
5467
7170
|
if (!file) return;
|
|
5468
7171
|
try {
|
|
5469
|
-
|
|
5470
|
-
|
|
5471
|
-
|
|
7172
|
+
mkdirSync19(join25(deskbuddyHome(), "running"), { recursive: true, mode: 448 });
|
|
7173
|
+
writeFileSync17(`${file}.tmp`, JSON.stringify({ at: now, pid: process.pid }), { encoding: "utf8", mode: 384 });
|
|
7174
|
+
renameSync14(`${file}.tmp`, file);
|
|
5472
7175
|
} catch {
|
|
5473
7176
|
}
|
|
5474
7177
|
}
|
|
@@ -5527,15 +7230,27 @@ var IDLE_POLL_MS2 = 1e3;
|
|
|
5527
7230
|
var IDLE_QUEUE_MAX_MS = 30 * 60 * 1e3;
|
|
5528
7231
|
var BUSY_STATES2 = /* @__PURE__ */ new Set(["watch", "ask_approval"]);
|
|
5529
7232
|
var CODEX_WATCH_MS = 5 * 1e3;
|
|
7233
|
+
var DUMATE_WATCH_MS = 2500;
|
|
5530
7234
|
function defaultCodexAppServers() {
|
|
5531
7235
|
const cfg = loadConfig();
|
|
5532
7236
|
return cfg.enabledTools.includes("codex") ? discoverAppServers(cfg.codexAppServer) : [];
|
|
5533
7237
|
}
|
|
5534
7238
|
function opencodeReady() {
|
|
5535
|
-
|
|
7239
|
+
const mod = ocPluginModule(installedHookPath());
|
|
7240
|
+
return isOcPluginInstalled(opencodePluginPath()) && existsSync13(mod) && existsSync13(join26(dirname10(mod), "ocTranslate.mjs"));
|
|
5536
7241
|
}
|
|
7242
|
+
var INTERNAL_PROMPT_MARK = "[DeskBuddy]";
|
|
5537
7243
|
function orphanResumeText(summary2) {
|
|
5538
|
-
return
|
|
7244
|
+
return `${INTERNAL_PROMPT_MARK}\uFF08\u4F60\u5728\u8BBE\u5907\u4E0A\u5DF2\u6279\u51C6\uFF1A${summary2}\uFF09\u8BF7\u7EE7\u7EED\u521A\u624D\u88AB\u4E2D\u65AD\u7684\u64CD\u4F5C\u3002`;
|
|
7245
|
+
}
|
|
7246
|
+
function staleAfterRun(read, tool, alive) {
|
|
7247
|
+
const st = read();
|
|
7248
|
+
if (!st || st.tool !== tool || !BUSY_STATES2.has(st.state)) return null;
|
|
7249
|
+
if (st.interactive === true || st.embedded === true) return null;
|
|
7250
|
+
if (st.pid && alive(st.pid, st.pidName)) return null;
|
|
7251
|
+
const again = read();
|
|
7252
|
+
if (!again || again.state !== st.state || again.updatedAt !== st.updatedAt) return null;
|
|
7253
|
+
return st;
|
|
5539
7254
|
}
|
|
5540
7255
|
var ALIVE_REPORT_MS = 60 * 1e3;
|
|
5541
7256
|
var Daemon = class {
|
|
@@ -5547,6 +7262,8 @@ var Daemon = class {
|
|
|
5547
7262
|
#gate;
|
|
5548
7263
|
/** runId → 这次 run 结束的 promise。defer 续跑要等原进程真退出了再起(见 #scheduleResume) */
|
|
5549
7264
|
#runsDone = /* @__PURE__ */ new Map();
|
|
7265
|
+
/** runId → 子进程 pid(按 hook 状态里的进程号认出新建任务建出的会话) */
|
|
7266
|
+
#runPids = /* @__PURE__ */ new Map();
|
|
5550
7267
|
#attempt = 0;
|
|
5551
7268
|
#stopped = false;
|
|
5552
7269
|
#ws = null;
|
|
@@ -5565,6 +7282,11 @@ var Daemon = class {
|
|
|
5565
7282
|
#wakeReport = null;
|
|
5566
7283
|
/** url → 连着它的 CodexWatch。插入顺序 = 发现顺序(DeskBuddy 管的在前) */
|
|
5567
7284
|
#codex = /* @__PURE__ */ new Map();
|
|
7285
|
+
/** 千问办公的选择题 / 授权推设备(spec 2026-10-03 §3.1):hook 发 notify 进来,回填走 App 内置 MCP */
|
|
7286
|
+
#qwenwork;
|
|
7287
|
+
/** 百度搭子(spec 2026-10-03 §3.2):直连 dumate-opencode 的 HTTP,状态上报 + 选择题 / 授权推设备 */
|
|
7288
|
+
#dumate;
|
|
7289
|
+
#dumateTimer = null;
|
|
5568
7290
|
#codexStarted = false;
|
|
5569
7291
|
#codexReportTimer = null;
|
|
5570
7292
|
/** daemon 交出去、还没有结局的指令流程数(开窗 / 写进窗口 / 经 app-server 发一轮)。空闲判定用 */
|
|
@@ -5590,6 +7312,14 @@ var Daemon = class {
|
|
|
5590
7312
|
sendToWindow: deps.sendToWindow ?? ((id, req2, onReply) => sendToWindow(channelPath(id), windowSecret(id), req2, onReply)),
|
|
5591
7313
|
gui: deps.gui ?? (() => guiAvailable(defaultGuiDeps())),
|
|
5592
7314
|
openWindow: deps.openWindow ?? ((runId) => this.#openTerminal(runId)),
|
|
7315
|
+
ancestors: deps.ancestors ?? ((pid) => selfAndAncestors(pid)),
|
|
7316
|
+
launchable: deps.launchable ?? ((tool) => {
|
|
7317
|
+
try {
|
|
7318
|
+
return locate(tool) != null;
|
|
7319
|
+
} catch {
|
|
7320
|
+
return false;
|
|
7321
|
+
}
|
|
7322
|
+
}),
|
|
5593
7323
|
endedCwd: deps.endedCwd ?? ((sid, tool, stateCwd) => resolveEndedCwd(sid, tool, {
|
|
5594
7324
|
readEnded: (s) => readEnded(s) ?? (stateCwd ? { sessionId: s, tool, cwd: stateCwd, interactive: false, endedAt: 0 } : null),
|
|
5595
7325
|
transcriptCwd: (s) => claudeTranscriptCwd(s),
|
|
@@ -5628,7 +7358,21 @@ var Daemon = class {
|
|
|
5628
7358
|
log,
|
|
5629
7359
|
now: this.#deps.now
|
|
5630
7360
|
});
|
|
5631
|
-
this.#
|
|
7361
|
+
this.#qwenwork = new QwenWorkBridge({
|
|
7362
|
+
gate: (req2, signal) => this.#coord.onGate(req2, signal),
|
|
7363
|
+
log,
|
|
7364
|
+
enabled: () => loadConfig().enabledTools.includes("qwenwork")
|
|
7365
|
+
});
|
|
7366
|
+
this.#dumate = new DumateWatch({
|
|
7367
|
+
gate: (req2, signal) => this.#coord.onGate(req2, signal),
|
|
7368
|
+
log,
|
|
7369
|
+
enabled: () => loadConfig().enabledTools.includes("dumate"),
|
|
7370
|
+
nodePath: this.#deps.nodePath
|
|
7371
|
+
});
|
|
7372
|
+
this.#gate = new GateServer(
|
|
7373
|
+
(req2, signal) => this.#coord.onGate(req2, signal),
|
|
7374
|
+
(req2) => this.#qwenwork.onNotify(req2)
|
|
7375
|
+
);
|
|
5632
7376
|
}
|
|
5633
7377
|
/** 起本地授权端点。必须在执行任何指令之前起好 —— 否则 gate 连不上,全部 exit 2 */
|
|
5634
7378
|
async startGate() {
|
|
@@ -5642,6 +7386,8 @@ var Daemon = class {
|
|
|
5642
7386
|
this.#codexTick();
|
|
5643
7387
|
this.#codexTimer = setInterval(() => this.#codexTick(), CODEX_WATCH_MS);
|
|
5644
7388
|
this.#codexTimer.unref();
|
|
7389
|
+
this.#dumateTimer = setInterval(() => this.#dumate.tick(), DUMATE_WATCH_MS);
|
|
7390
|
+
this.#dumateTimer.unref();
|
|
5645
7391
|
await this.startWake();
|
|
5646
7392
|
return port;
|
|
5647
7393
|
}
|
|
@@ -5683,7 +7429,7 @@ var Daemon = class {
|
|
|
5683
7429
|
const cfg = loadConfig();
|
|
5684
7430
|
if (!cfg.clientId || !cfg.boundAt || !cfg.enabledTools.includes(tool)) return;
|
|
5685
7431
|
try {
|
|
5686
|
-
await postReport(buildSnapshot(withWake(
|
|
7432
|
+
await postReport(buildSnapshot(withWake(this.#aliveStates(tool)), tool, cfg, this.#deps.now()));
|
|
5687
7433
|
} catch (e) {
|
|
5688
7434
|
log(`\u4E0A\u62A5 ${tool} \u4F1A\u8BDD\u5931\u8D25\uFF1A${e.message}`);
|
|
5689
7435
|
}
|
|
@@ -5756,7 +7502,7 @@ var Daemon = class {
|
|
|
5756
7502
|
const cfg = loadConfig();
|
|
5757
7503
|
if (!cfg.clientId || !cfg.boundAt || !cfg.enabledTools.includes("codex")) return;
|
|
5758
7504
|
try {
|
|
5759
|
-
await postReport(buildSnapshot(
|
|
7505
|
+
await postReport(buildSnapshot(this.#aliveStates("codex"), "codex", cfg, this.#deps.now(), this.#codexLoaded()));
|
|
5760
7506
|
} catch (e) {
|
|
5761
7507
|
log(`\u4E0A\u62A5 codex \u5FEB\u7167\u5931\u8D25\uFF1A${e.message}`);
|
|
5762
7508
|
}
|
|
@@ -5780,13 +7526,23 @@ var Daemon = class {
|
|
|
5780
7526
|
log(`\u4E0A\u62A5\u6700\u8FD1\u76EE\u5F55\u5931\u8D25\uFF1A${e.message}`);
|
|
5781
7527
|
}
|
|
5782
7528
|
}
|
|
7529
|
+
/** 存活会话(顺手剔除死进程、退役没记 pid 又 2 小时无事件的) */
|
|
7530
|
+
#aliveStates(tool) {
|
|
7531
|
+
return readAliveStates(tool, { now: this.#deps.now(), onStale: (s) => log(`\u4F1A\u8BDD ${s.sessionId.slice(0, 8)} ${STALE_NO_PID_LOG}`) });
|
|
7532
|
+
}
|
|
5783
7533
|
/** 按已启用的工具各报一次存活会话快照(readAliveStates 顺手剔除死进程) */
|
|
5784
7534
|
async #reportAlive() {
|
|
5785
7535
|
const cfg = loadConfig();
|
|
5786
7536
|
if (!cfg.clientId || !cfg.boundAt) return;
|
|
5787
7537
|
for (const tool of cfg.enabledTools) {
|
|
5788
7538
|
await postReport(
|
|
5789
|
-
buildSnapshot(
|
|
7539
|
+
buildSnapshot(
|
|
7540
|
+
withWake(this.#aliveStates(tool)),
|
|
7541
|
+
tool,
|
|
7542
|
+
cfg,
|
|
7543
|
+
this.#deps.now(),
|
|
7544
|
+
tool === "codex" ? this.#codexLoaded() : null
|
|
7545
|
+
)
|
|
5790
7546
|
);
|
|
5791
7547
|
}
|
|
5792
7548
|
if (++this.#dirsTick % 10 === 0) await this.#reportDirs();
|
|
@@ -5846,6 +7602,9 @@ var Daemon = class {
|
|
|
5846
7602
|
if (this.#codexReportTimer) clearTimeout(this.#codexReportTimer);
|
|
5847
7603
|
for (const w of this.#codex.values()) w.stop();
|
|
5848
7604
|
this.#codex.clear();
|
|
7605
|
+
this.#qwenwork.stop();
|
|
7606
|
+
if (this.#dumateTimer) clearInterval(this.#dumateTimer);
|
|
7607
|
+
this.#dumate.stop();
|
|
5849
7608
|
if (this.#codexStarted) removeCodexRemote();
|
|
5850
7609
|
this.#ws?.close();
|
|
5851
7610
|
void this.#gate.stop();
|
|
@@ -5950,7 +7709,7 @@ var Daemon = class {
|
|
|
5950
7709
|
if (!this.#seen.remember(ap.instructionId)) return;
|
|
5951
7710
|
this.#seen.save();
|
|
5952
7711
|
log(`\u6536\u5230\u6388\u6743\u51B3\u5B9A ${ap.approvalId} ${ap.decision}`);
|
|
5953
|
-
this.#coord.onDecision(ap.approvalId, ap.decision, ap.deviceName);
|
|
7712
|
+
this.#coord.onDecision(ap.approvalId, ap.decision, ap.deviceName, ap.answers);
|
|
5954
7713
|
return;
|
|
5955
7714
|
}
|
|
5956
7715
|
const cmd = parseCommand(frame);
|
|
@@ -5966,6 +7725,7 @@ var Daemon = class {
|
|
|
5966
7725
|
}
|
|
5967
7726
|
this.#seen.save();
|
|
5968
7727
|
cmd.receivedAt = this.#deps.now();
|
|
7728
|
+
log(`\u6536\u5230\u6307\u4EE4 ${cmd.instructionId} tool=${cmd.tool} ${cmd.sessionId ? `\u56DE\u590D ${cmd.sessionId.slice(0, 8)}` : "\u65B0\u5EFA"}`);
|
|
5969
7729
|
this.#route(cmd);
|
|
5970
7730
|
}
|
|
5971
7731
|
/**
|
|
@@ -6023,7 +7783,8 @@ var Daemon = class {
|
|
|
6023
7783
|
const sid = resolveAlias(cmd.sessionId);
|
|
6024
7784
|
if (this.#wake.submit(cmd, sid)) return;
|
|
6025
7785
|
}
|
|
6026
|
-
|
|
7786
|
+
const opensWindow = (cmd.tool === "claude_code" || !cmd.sessionId && LAUNCH_TOOLS.includes(cmd.tool)) && this.#launchable(cmd.tool);
|
|
7787
|
+
if (opensWindow && !isExpired(cmd, this.#deps.now())) {
|
|
6027
7788
|
void this.#track(this.#openOrDispatch(cmd)).catch((e) => {
|
|
6028
7789
|
log(`\u6307\u4EE4 ${cmd.instructionId} \u5F00\u7A97\u6D41\u7A0B\u51FA\u9519\uFF1A${e?.stack ?? String(e)}`);
|
|
6029
7790
|
this.#report(cmd, cmd.sessionId, "failed", "", "window_failed");
|
|
@@ -6046,11 +7807,22 @@ var Daemon = class {
|
|
|
6046
7807
|
}
|
|
6047
7808
|
this.#dispatch(cmd);
|
|
6048
7809
|
}
|
|
7810
|
+
/** 开窗前预检:找不到可执行文件就走原后台路径(它按原方式快速回报找不到),不开一个立刻 127 的窗口 */
|
|
7811
|
+
#launchable(tool) {
|
|
7812
|
+
let ok = false;
|
|
7813
|
+
try {
|
|
7814
|
+
ok = this.#deps.launchable(tool);
|
|
7815
|
+
} catch {
|
|
7816
|
+
ok = false;
|
|
7817
|
+
}
|
|
7818
|
+
if (!ok) log(`\u672C\u673A\u627E\u4E0D\u5230 ${tool} \u7684\u53EF\u6267\u884C\u6587\u4EF6\uFF0C\u4E0D\u5F00\u7A97\uFF0C\u6309\u540E\u53F0\u8DEF\u5F84\u5904\u7406`);
|
|
7819
|
+
return ok;
|
|
7820
|
+
}
|
|
6049
7821
|
/**
|
|
6050
7822
|
* 新建任务、会话已结束 → 开窗;会话还活着(后台 -p 在跑、或在用户自己开的窗口里)→ 原来的路径(排队 / window_open)
|
|
6051
7823
|
*/
|
|
6052
7824
|
async #openOrDispatch(cmd) {
|
|
6053
|
-
if (tooLongToInject(cmd.text)) {
|
|
7825
|
+
if (cmd.tool === "claude_code" && this.#guiAvailable() && tooLongToInject(cmd.text)) {
|
|
6054
7826
|
log(`\u6307\u4EE4 ${cmd.instructionId} \u592A\u957F\uFF08${cmd.text.length} \u5B57\uFF09\uFF0C\u4E0D\u6267\u884C`);
|
|
6055
7827
|
this.#report(cmd, cmd.sessionId ? resolveAlias(cmd.sessionId) : "", "rejected", "", TEXT_TOO_LONG);
|
|
6056
7828
|
return;
|
|
@@ -6101,6 +7873,13 @@ var Daemon = class {
|
|
|
6101
7873
|
release2();
|
|
6102
7874
|
}
|
|
6103
7875
|
}
|
|
7876
|
+
#guiAvailable() {
|
|
7877
|
+
try {
|
|
7878
|
+
return this.#deps.gui();
|
|
7879
|
+
} catch {
|
|
7880
|
+
return false;
|
|
7881
|
+
}
|
|
7882
|
+
}
|
|
6104
7883
|
/**
|
|
6105
7884
|
* 开窗 → 等认领 → 首句经通道交给窗口;任何一步起不来 → 后台 -p(回报 started/headless)。
|
|
6106
7885
|
* 返回时:窗口已收下首句 / 已按失败回报 / 降级的后台那次已跑完
|
|
@@ -6110,16 +7889,10 @@ var Daemon = class {
|
|
|
6110
7889
|
log(`\u6307\u4EE4 ${cmd.instructionId} ${why}\uFF0C\u6539\u540E\u53F0\u6267\u884C`);
|
|
6111
7890
|
return this.#headless(c, sid, cwd);
|
|
6112
7891
|
};
|
|
6113
|
-
|
|
6114
|
-
try {
|
|
6115
|
-
gui = this.#deps.gui();
|
|
6116
|
-
} catch {
|
|
6117
|
-
gui = false;
|
|
6118
|
-
}
|
|
6119
|
-
if (!gui) return headless("\u6CA1\u6709\u56FE\u5F62\u4F1A\u8BDD\uFF08SSH / \u9501\u5B9A\u7684\u670D\u52A1\u4F1A\u8BDD / Linux\uFF09");
|
|
7892
|
+
if (!this.#guiAvailable()) return headless("\u6CA1\u6709\u56FE\u5F62\u4F1A\u8BDD\uFF08SSH / \u9501\u5B9A\u7684\u670D\u52A1\u4F1A\u8BDD / Linux\uFF09");
|
|
6120
7893
|
const runId = randomBytes7(16).toString("hex");
|
|
6121
7894
|
try {
|
|
6122
|
-
writeLaunch(runId, { tool:
|
|
7895
|
+
writeLaunch(runId, { tool: cmd.tool, cwd, resume: sid, at: this.#deps.now() });
|
|
6123
7896
|
} catch (e) {
|
|
6124
7897
|
return headless(`\u5199\u4EA4\u63A5\u5355\u5931\u8D25\uFF1A${e.message}`);
|
|
6125
7898
|
}
|
|
@@ -6133,9 +7906,9 @@ var Daemon = class {
|
|
|
6133
7906
|
}
|
|
6134
7907
|
log(`\u6307\u4EE4 ${cmd.instructionId} \u5F00\u7A97\u547D\u4EE4\u62A5\u9519\uFF08${e.message}\uFF09\uFF0C\u4F46\u7A97\u53E3\u5DF2\u8BA4\u9886\uFF0C\u6309\u7A97\u53E3\u7EE7\u7EED`);
|
|
6135
7908
|
}
|
|
6136
|
-
const
|
|
7909
|
+
const sleep3 = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
6137
7910
|
const claimDeadline = this.#deps.now() + this.#deps.claimWaitMs;
|
|
6138
|
-
while (launchPending(runId) && this.#deps.now() < claimDeadline) await
|
|
7911
|
+
while (launchPending(runId) && this.#deps.now() < claimDeadline) await sleep3(300);
|
|
6139
7912
|
removeCommandFile(deskbuddyHome(), runId);
|
|
6140
7913
|
if (launchPending(runId) && claimLaunch(runId)) return headless(`${Math.round(this.#deps.claimWaitMs / 1e3)} \u79D2\u5185\u7A97\u53E3\u6CA1\u8D77\u6765`);
|
|
6141
7914
|
log(`\u6307\u4EE4 ${cmd.instructionId} \u7A97\u53E3\u5DF2\u8D77\u6765\uFF08${runId.slice(0, 8)}\uFF09\uFF0C\u7B49\u5B83\u5C31\u7EEA\u540E\u5199\u5165\u9996\u53E5`);
|
|
@@ -6173,14 +7946,14 @@ var Daemon = class {
|
|
|
6173
7946
|
found = null;
|
|
6174
7947
|
}
|
|
6175
7948
|
if (found) break;
|
|
6176
|
-
await
|
|
7949
|
+
await sleep3(200);
|
|
6177
7950
|
}
|
|
6178
7951
|
}
|
|
6179
|
-
await
|
|
7952
|
+
await sleep3(50);
|
|
6180
7953
|
if (fb.p) await fb.p;
|
|
6181
7954
|
return;
|
|
6182
7955
|
}
|
|
6183
|
-
await
|
|
7956
|
+
await sleep3(1e3);
|
|
6184
7957
|
}
|
|
6185
7958
|
log(`\u6307\u4EE4 ${cmd.instructionId} \u65B0\u7A97\u53E3\u7684\u901A\u9053\u4E00\u76F4\u6CA1\u5C31\u7EEA`);
|
|
6186
7959
|
this.#report(cmd, sid, "failed", "window", "window_failed");
|
|
@@ -6201,7 +7974,7 @@ var Daemon = class {
|
|
|
6201
7974
|
}
|
|
6202
7975
|
return this.#queue.enqueue(
|
|
6203
7976
|
sid,
|
|
6204
|
-
() => this.#execute(cmd, sid, cwd,
|
|
7977
|
+
() => this.#execute(cmd, sid, cwd, true),
|
|
6205
7978
|
(e) => {
|
|
6206
7979
|
log(`\u6307\u4EE4 ${cmd.instructionId} \u6267\u884C\u65F6\u51FA\u9519\uFF1A${e?.stack ?? String(e)}`);
|
|
6207
7980
|
this.#report(cmd, sid, "failed", "", "exec_failed");
|
|
@@ -6330,8 +8103,6 @@ var Daemon = class {
|
|
|
6330
8103
|
}
|
|
6331
8104
|
const target = resolveTarget(cmd, {
|
|
6332
8105
|
readState,
|
|
6333
|
-
readHandle,
|
|
6334
|
-
handleIsUsable,
|
|
6335
8106
|
remoteCwd: () => loadConfig().remoteCwd || "",
|
|
6336
8107
|
alias: resolveAlias,
|
|
6337
8108
|
dirOf: (ref) => dirOf(ref),
|
|
@@ -6342,10 +8113,28 @@ var Daemon = class {
|
|
|
6342
8113
|
this.#report(cmd, cmd.sessionId, "rejected", "", target.reason);
|
|
6343
8114
|
return;
|
|
6344
8115
|
}
|
|
8116
|
+
const before = target.sessionId ? this.#runsOfSession(target.sessionId) : [];
|
|
8117
|
+
for (const r of before) this.#coord.supersedeRun(r.runId, target.sessionId);
|
|
6345
8118
|
if (target.sessionId) this.#coord.cancelSession(target.sessionId);
|
|
8119
|
+
const key = target.sessionId || `new:${cmd.instructionId}`;
|
|
8120
|
+
if (this.#queue.busy(key) || before.length > 0) {
|
|
8121
|
+
log(`\u6307\u4EE4 ${cmd.instructionId} \u4F1A\u8BDD ${key.slice(0, 8)} \u524D\u9762\u8FD8\u6709\u4EFB\u52A1\uFF0C\u6392\u961F`);
|
|
8122
|
+
this.#report(cmd, target.sessionId, "queued", "headless");
|
|
8123
|
+
}
|
|
6346
8124
|
this.#queue.enqueue(
|
|
6347
|
-
|
|
6348
|
-
() =>
|
|
8125
|
+
key,
|
|
8126
|
+
async () => {
|
|
8127
|
+
for (const r of before) await r.done.catch(() => {
|
|
8128
|
+
});
|
|
8129
|
+
for (let more = this.#runsOfSession(target.sessionId); target.sessionId && more.length > 0; more = this.#runsOfSession(target.sessionId)) {
|
|
8130
|
+
for (const r of more) {
|
|
8131
|
+
this.#coord.supersedeRun(r.runId, target.sessionId);
|
|
8132
|
+
await r.done.catch(() => {
|
|
8133
|
+
});
|
|
8134
|
+
}
|
|
8135
|
+
}
|
|
8136
|
+
await this.#execute(cmd, target.sessionId, target.cwd);
|
|
8137
|
+
},
|
|
6349
8138
|
(e) => {
|
|
6350
8139
|
log(`\u6307\u4EE4 ${cmd.instructionId} \u6267\u884C\u65F6\u51FA\u9519\uFF1A${e?.stack ?? String(e)}`);
|
|
6351
8140
|
this.#report(cmd, target.sessionId, "failed", "", "exec_failed");
|
|
@@ -6390,7 +8179,7 @@ var Daemon = class {
|
|
|
6390
8179
|
log(`\u6307\u4EE4 ${cmd.instructionId} \u4F1A\u8BDD\u5728\u7535\u8111\u7A97\u53E3\u91CC\u7A7A\u95F2\uFF0C\u540E\u53F0\u7EED\u8DD1`);
|
|
6391
8180
|
writeRunning(sid, this.#deps.now());
|
|
6392
8181
|
try {
|
|
6393
|
-
await this.#execute(cmd, sid, runCwd,
|
|
8182
|
+
await this.#execute(cmd, sid, runCwd, true, (r) => {
|
|
6394
8183
|
const now = readState(sid);
|
|
6395
8184
|
if (!now || now.interactive !== true && now.embedded !== true || !now.pid || !isAlive(now.pid, now.pidName)) return;
|
|
6396
8185
|
writeAppended(sid, { text: cmd.text, result: resultSummary(cmd.tool, r.stdout), ok: r.ok, at: this.#deps.now() });
|
|
@@ -6404,7 +8193,12 @@ var Daemon = class {
|
|
|
6404
8193
|
this.#report(cmd, sid, "failed", "", "exec_failed");
|
|
6405
8194
|
});
|
|
6406
8195
|
}
|
|
6407
|
-
async #execute(cmd, sessionId, cwd,
|
|
8196
|
+
async #execute(cmd, sessionId, cwd, ended = false, onResult) {
|
|
8197
|
+
if (isExpired(cmd, this.#deps.now())) {
|
|
8198
|
+
log(`\u6307\u4EE4 ${cmd.instructionId} \u6392\u961F\u5230\u6267\u884C\u65F6\u5DF2\u8FC7\u671F\uFF0C\u4E0D\u6267\u884C`);
|
|
8199
|
+
this.#report(cmd, sessionId, "rejected", "", "expired");
|
|
8200
|
+
return;
|
|
8201
|
+
}
|
|
6408
8202
|
if (sessionId && !ended) {
|
|
6409
8203
|
const fresh = readState(sessionId);
|
|
6410
8204
|
if (!fresh) {
|
|
@@ -6420,7 +8214,7 @@ var Daemon = class {
|
|
|
6420
8214
|
this.#report(cmd, sessionId, "rejected", "", "tool_unsupported");
|
|
6421
8215
|
return;
|
|
6422
8216
|
}
|
|
6423
|
-
log(`\u6267\u884C ${cmd.instructionId} tool=${cmd.tool} ${sessionId ? "resume" : "new"} cwd=\u2026/${
|
|
8217
|
+
log(`\u6267\u884C ${cmd.instructionId} tool=${cmd.tool} ${sessionId ? "resume" : "new"} cwd=\u2026/${basename8(cwd)} len=${cmd.text.length}`);
|
|
6424
8218
|
const result = await this.#run(plan, cmd.tool, runId, sessionId, cwd, () => this.#report(cmd, sessionId, "started", "headless"));
|
|
6425
8219
|
if ("fail" in result) {
|
|
6426
8220
|
log(
|
|
@@ -6432,22 +8226,65 @@ var Daemon = class {
|
|
|
6432
8226
|
if (isDeferred(result.stdout)) log(`\u6307\u4EE4 ${cmd.instructionId} \u505C\u5728\u7B49\u8BBE\u5907\u6388\u6743\u4E0A\uFF0C\u8FDB\u7A0B\u5DF2\u9000\u51FA`);
|
|
6433
8227
|
onResult?.(result);
|
|
6434
8228
|
if (!result.ok) {
|
|
6435
|
-
|
|
8229
|
+
const tail = failureTail(result, cmd.text);
|
|
8230
|
+
log(`\u6307\u4EE4 ${cmd.instructionId} \u6267\u884C\u5931\u8D25\uFF1A${result.error ?? "\u672A\u77E5"}${tail ? `\uFF1B\u8F93\u51FA\u672B\u5C3E\uFF1A${tail}` : ""}`);
|
|
6436
8231
|
this.#report(cmd, sessionId || result.sessionId || "", "failed", "headless", "exec_failed");
|
|
6437
8232
|
}
|
|
6438
8233
|
if (!sessionId) {
|
|
6439
8234
|
const newId = result.sessionId ?? parseSessionId(cmd.tool, result.stdout);
|
|
6440
|
-
|
|
6441
|
-
writeHandle(handleKey, { sessionId: newId, cwd, createdAt: this.#deps.now() });
|
|
6442
|
-
log(`\u6307\u4EE4 ${cmd.instructionId} \u5EFA\u51FA\u4F1A\u8BDD ${newId}\uFF0C\u5DF2\u8BB0\u4E3A ${cmd.tool} \u7684\u8FDC\u7A0B\u4F1A\u8BDD`);
|
|
6443
|
-
} else {
|
|
6444
|
-
log(`\u6307\u4EE4 ${cmd.instructionId} \u672A\u80FD\u89E3\u6790\u51FA\u4F1A\u8BDD id\uFF0C\u4E0B\u6B21\u4ECD\u4F1A\u65B0\u5EFA`);
|
|
6445
|
-
}
|
|
8235
|
+
log(`\u6307\u4EE4 ${cmd.instructionId} ${newId ? `\u5EFA\u51FA\u4F1A\u8BDD ${newId}` : "\u672A\u80FD\u89E3\u6790\u51FA\u4F1A\u8BDD id"}`);
|
|
6446
8236
|
}
|
|
6447
8237
|
}
|
|
6448
8238
|
#gateTarget(runId) {
|
|
6449
8239
|
return { runId, nodePath: this.#deps.nodePath, gateScript: this.#deps.gateScript };
|
|
6450
8240
|
}
|
|
8241
|
+
/**
|
|
8242
|
+
* 正在跑、且已知在跑 sessionId 这个会话的后台进程(新建任务起跑时不知道会话号:
|
|
8243
|
+
* 授权关卡请求里带上来(coord.sessionOf),或 hook 写的状态里记的进程号就是它)
|
|
8244
|
+
*/
|
|
8245
|
+
#runsOfSession(sessionId) {
|
|
8246
|
+
if (!sessionId || this.#runsDone.size === 0) return [];
|
|
8247
|
+
let chain = null;
|
|
8248
|
+
const chainOf = () => {
|
|
8249
|
+
if (chain) return chain;
|
|
8250
|
+
let pid;
|
|
8251
|
+
try {
|
|
8252
|
+
pid = readState(sessionId)?.pid;
|
|
8253
|
+
} catch {
|
|
8254
|
+
pid = void 0;
|
|
8255
|
+
}
|
|
8256
|
+
try {
|
|
8257
|
+
chain = typeof pid === "number" && pid > 0 ? this.#deps.ancestors(pid) : [];
|
|
8258
|
+
} catch {
|
|
8259
|
+
chain = typeof pid === "number" && pid > 0 ? [pid] : [];
|
|
8260
|
+
}
|
|
8261
|
+
return chain;
|
|
8262
|
+
};
|
|
8263
|
+
const out = [];
|
|
8264
|
+
for (const [runId, done] of this.#runsDone) {
|
|
8265
|
+
const rp = this.#runPids.get(runId);
|
|
8266
|
+
if (this.#coord.sessionOf(runId) === sessionId || rp !== void 0 && chainOf().includes(rp)) out.push({ runId, done });
|
|
8267
|
+
}
|
|
8268
|
+
return out;
|
|
8269
|
+
}
|
|
8270
|
+
/**
|
|
8271
|
+
* 后台进程跑完了,会话状态却还停在「进行中」(Buglist 161:Codex 续跑后一直 watch)。Codex 的 hook 是 async 的,
|
|
8272
|
+
* exec 退出时 Stop 那次可能没跑完 / 没跑;hook 也可能没认出进程号(pid 空 → 永远不会按进程退出清掉)。
|
|
8273
|
+
* 状态里的进程不在了 / 没记进程号时,按这次的结果落 done / failed。🔴 进程号是个还活着的进程(用户窗口)就不动:
|
|
8274
|
+
* 那是窗口自己的状态,由它的 hook 管;停在等设备授权上(defer)的也不动
|
|
8275
|
+
*/
|
|
8276
|
+
#settleStateAfterRun(sessionId, result, tool) {
|
|
8277
|
+
if (!sessionId || isDeferred(result.stdout)) return;
|
|
8278
|
+
try {
|
|
8279
|
+
const st = staleAfterRun(() => readState(sessionId), tool, (pid, name) => isAlive(pid, name));
|
|
8280
|
+
if (!st) return;
|
|
8281
|
+
const now = this.#deps.now();
|
|
8282
|
+
writeState({ ...st, state: result.ok ? "done" : "failed", updatedAt: Math.max(now, st.updatedAt + 1) });
|
|
8283
|
+
log(`\u4F1A\u8BDD ${sessionId.slice(0, 8)} \u540E\u53F0\u8FDB\u7A0B\u5DF2\u7ED3\u675F\u4F46\u72B6\u6001\u8FD8\u662F ${st.state}\uFF0C\u6309\u7ED3\u679C\u6539\u6210 ${result.ok ? "done" : "failed"}`);
|
|
8284
|
+
void this.#reportAlive();
|
|
8285
|
+
} catch {
|
|
8286
|
+
}
|
|
8287
|
+
}
|
|
6451
8288
|
/** 设备上有了决定、需要续会话。排进该会话的串行队列,和普通指令一样不会并发打同一个会话 */
|
|
6452
8289
|
#scheduleResume(r, kind) {
|
|
6453
8290
|
if (!r.sessionId) {
|
|
@@ -6487,8 +8324,10 @@ var Daemon = class {
|
|
|
6487
8324
|
}
|
|
6488
8325
|
if ("fail" in result) log(`\u7EED\u8DD1\u4F1A\u8BDD ${r.sessionId} \u653E\u5F03\uFF1A${result.fail === "gate_unavailable" ? "\u6388\u6743\u5173\u5361\u4E0D\u53EF\u7528" : `\u672C\u673A\u627E\u4E0D\u5230 ${r.tool} \u7684\u53EF\u6267\u884C\u6587\u4EF6`}`);
|
|
6489
8326
|
else if (isDeferred(result.stdout)) log(`\u4F1A\u8BDD ${r.sessionId} \u53C8\u505C\u5728\u4E86\u4E0B\u4E00\u4E2A\u7B49\u6388\u6743\u7684\u64CD\u4F5C\u4E0A`);
|
|
6490
|
-
else if (!result.ok)
|
|
6491
|
-
|
|
8327
|
+
else if (!result.ok) {
|
|
8328
|
+
const tail = failureTail(result, "");
|
|
8329
|
+
log(`\u7EED\u8DD1\u4F1A\u8BDD ${r.sessionId} \u5931\u8D25\uFF1A${result.error ?? "\u672A\u77E5"}${tail ? `\uFF1B\u8F93\u51FA\u672B\u5C3E\uFF1A${tail}` : ""}`);
|
|
8330
|
+
} else log(`\u7EED\u8DD1\u4F1A\u8BDD ${r.sessionId} \u5B8C\u6210`);
|
|
6492
8331
|
}
|
|
6493
8332
|
/**
|
|
6494
8333
|
* 起进程并在授权协调器里登记这次 run(gate 靠 runId 认人)。
|
|
@@ -6520,13 +8359,19 @@ var Daemon = class {
|
|
|
6520
8359
|
onWaitEnd: () => rc.resumeTimeout()
|
|
6521
8360
|
});
|
|
6522
8361
|
this.#runsDone.set(runId, rc.done);
|
|
8362
|
+
const runPid = rc.pid;
|
|
8363
|
+
if (typeof runPid === "number") this.#runPids.set(runId, runPid);
|
|
8364
|
+
let result = null;
|
|
6523
8365
|
try {
|
|
6524
|
-
|
|
8366
|
+
result = await rc.done;
|
|
8367
|
+
return result;
|
|
6525
8368
|
} finally {
|
|
6526
8369
|
this.#runsDone.delete(runId);
|
|
6527
|
-
|
|
8370
|
+
this.#runPids.delete(runId);
|
|
8371
|
+
const sid = sessionId || this.#coord.sessionOf(runId) || result?.sessionId || "";
|
|
6528
8372
|
this.#coord.endRun(runId);
|
|
6529
8373
|
this.#coord.settleSession(sid, startedAt);
|
|
8374
|
+
if (result) this.#settleStateAfterRun(sid, result, tool);
|
|
6530
8375
|
}
|
|
6531
8376
|
}
|
|
6532
8377
|
};
|
|
@@ -6565,7 +8410,7 @@ async function runDaemonCommand(opts = {}) {
|
|
|
6565
8410
|
}
|
|
6566
8411
|
if (opts.foreground) process.env.DESKBUDDY_DAEMON_FOREGROUND = "1";
|
|
6567
8412
|
try {
|
|
6568
|
-
|
|
8413
|
+
writeFileSync18(pidFilePath(), String(process.pid), "utf8");
|
|
6569
8414
|
} catch {
|
|
6570
8415
|
}
|
|
6571
8416
|
let daemon;
|
|
@@ -6612,15 +8457,16 @@ async function runDaemonCommand(opts = {}) {
|
|
|
6612
8457
|
}
|
|
6613
8458
|
|
|
6614
8459
|
// src/core/registry.ts
|
|
6615
|
-
import { mkdirSync as
|
|
6616
|
-
import { join as
|
|
8460
|
+
import { mkdirSync as mkdirSync20, readFileSync as readFileSync18, writeFileSync as writeFileSync19 } from "fs";
|
|
8461
|
+
import { join as join27 } from "path";
|
|
6617
8462
|
var FALLBACK = {
|
|
6618
8463
|
version: "builtin",
|
|
6619
8464
|
tools: {
|
|
6620
8465
|
claude_code: {
|
|
6621
8466
|
format: "cc-hook",
|
|
6622
8467
|
processNames: ["claude"],
|
|
6623
|
-
titleSources: ["cc-ai-title", "git-branch", "project"],
|
|
8468
|
+
titleSources: ["cc-ai-title", "first-prompt", "git-branch", "project"],
|
|
8469
|
+
turnEvents: ["UserPromptSubmit"],
|
|
6624
8470
|
events: {
|
|
6625
8471
|
UserPromptSubmit: "watch",
|
|
6626
8472
|
PreToolUse: "watch",
|
|
@@ -6639,6 +8485,7 @@ var FALLBACK = {
|
|
|
6639
8485
|
// codex-thread-name 是机会主义的一级:session_index.jsonl 里的 thread_name
|
|
6640
8486
|
// 只有 /rename 或 VS Code 扩展起过名的才有,多数是空的,取不到用首条指令
|
|
6641
8487
|
titleSources: ["codex-thread-name", "first-prompt", "git-branch", "project"],
|
|
8488
|
+
turnEvents: ["UserPromptSubmit"],
|
|
6642
8489
|
events: {
|
|
6643
8490
|
UserPromptSubmit: "watch",
|
|
6644
8491
|
PreToolUse: "watch",
|
|
@@ -6654,6 +8501,7 @@ var FALLBACK = {
|
|
|
6654
8501
|
// WorkBuddy 的 transcript 与 Claude Code 同格式,也写 ai-title(2026-10-01 WorkBuddy 5.6.2 实测);
|
|
6655
8502
|
// 工作目录是按时间自动建的,目录名不能当标题
|
|
6656
8503
|
titleSources: ["cc-ai-title", "first-prompt", "git-branch", "project"],
|
|
8504
|
+
turnEvents: ["UserPromptSubmit"],
|
|
6657
8505
|
events: {
|
|
6658
8506
|
UserPromptSubmit: "watch",
|
|
6659
8507
|
PreToolUse: "watch",
|
|
@@ -6670,6 +8518,7 @@ var FALLBACK = {
|
|
|
6670
8518
|
// 桌面端会话库里的标题(先是首条指令,后被换成 AI 摘要);命令行跑的会话不在库里,落到首条指令
|
|
6671
8519
|
// (CLI 1.1.61 实测 transcript 里没有 ai-title,UserPromptSubmit 带 prompt)
|
|
6672
8520
|
titleSources: ["qoder-session-title", "first-prompt", "git-branch", "project"],
|
|
8521
|
+
turnEvents: ["UserPromptSubmit"],
|
|
6673
8522
|
events: {
|
|
6674
8523
|
UserPromptSubmit: "watch",
|
|
6675
8524
|
PreToolUse: "watch",
|
|
@@ -6684,6 +8533,7 @@ var FALLBACK = {
|
|
|
6684
8533
|
format: "cc-hook",
|
|
6685
8534
|
processNames: ["Qoder"],
|
|
6686
8535
|
titleSources: ["qoder-session-title", "first-prompt", "git-branch", "project"],
|
|
8536
|
+
turnEvents: ["UserPromptSubmit"],
|
|
6687
8537
|
events: {
|
|
6688
8538
|
UserPromptSubmit: "watch",
|
|
6689
8539
|
PreToolUse: "watch",
|
|
@@ -6693,11 +8543,51 @@ var FALLBACK = {
|
|
|
6693
8543
|
},
|
|
6694
8544
|
lifecycle: { SessionStart: "ignore", SessionEnd: "remove" }
|
|
6695
8545
|
},
|
|
8546
|
+
// 千问办公(QwenWorkCN.app,内核是 App 内置的 Qoder CN CLI,hooks 读 ~/.QwenWorkCN/settings.json)。
|
|
8547
|
+
// PermissionRequest 不触发:授权框只来一条 Notification(permission_prompt)(spec 2026-10-03 §2.1)
|
|
8548
|
+
qwenwork: {
|
|
8549
|
+
format: "cc-hook",
|
|
8550
|
+
processNames: ["QwenWorkCN"],
|
|
8551
|
+
// 工作目录是 ~/.qwenworkcn/workspace/<chatId>,目录名不能当标题
|
|
8552
|
+
titleSources: ["first-prompt", "project"],
|
|
8553
|
+
turnEvents: ["UserPromptSubmit"],
|
|
8554
|
+
events: {
|
|
8555
|
+
UserPromptSubmit: "watch",
|
|
8556
|
+
PreToolUse: "watch",
|
|
8557
|
+
PostToolUse: "watch",
|
|
8558
|
+
Stop: "done"
|
|
8559
|
+
},
|
|
8560
|
+
conditionalEvents: {
|
|
8561
|
+
Notification: [
|
|
8562
|
+
// 实测 message 是 "Tool Bash requires confirmation"(选择题是 "Tool AskUserQuestion requires confirmation"),与服务端注册表一致
|
|
8563
|
+
{ match: "requires confirmation|permission|approve|\u9700\u8981\u786E\u8BA4|\u6388\u6743", state: "ask_approval" },
|
|
8564
|
+
{ match: "waiting for your input|idle|\u95F2\u7F6E", state: "done" }
|
|
8565
|
+
]
|
|
8566
|
+
},
|
|
8567
|
+
lifecycle: { SessionStart: "ignore", SessionEnd: "remove" }
|
|
8568
|
+
},
|
|
8569
|
+
// 百度搭子(DuMate.app,内核是改名的 opencode:dumate-opencode serve)。不往 DuMate 里装任何东西:
|
|
8570
|
+
// daemon 的 DumateWatch 直连它的 HTTP 接口,把事件翻成 cc 同构输入喂 hook.cjs(spec 2026-10-03 §3.2)
|
|
8571
|
+
dumate: {
|
|
8572
|
+
format: "dumate-http",
|
|
8573
|
+
processNames: ["dumate-opencode"],
|
|
8574
|
+
titleSources: ["oc-session-title", "first-prompt"],
|
|
8575
|
+
turnEvents: ["UserPromptSubmit"],
|
|
8576
|
+
events: {
|
|
8577
|
+
UserPromptSubmit: "watch",
|
|
8578
|
+
PreToolUse: "watch",
|
|
8579
|
+
PostToolUse: "watch",
|
|
8580
|
+
Stop: "done",
|
|
8581
|
+
PermissionRequest: "ask_approval"
|
|
8582
|
+
},
|
|
8583
|
+
lifecycle: { SessionEnd: "remove" }
|
|
8584
|
+
},
|
|
6696
8585
|
opencode: {
|
|
6697
8586
|
format: "oc-plugin",
|
|
6698
8587
|
processNames: ["opencode"],
|
|
6699
8588
|
// opencode 自己生成的会话标题(插件从 session.updated 取,随事件传 oc_session_title),没有就用首条指令
|
|
6700
8589
|
titleSources: ["oc-session-title", "first-prompt", "git-branch", "project"],
|
|
8590
|
+
turnEvents: ["UserPromptSubmit"],
|
|
6701
8591
|
events: {
|
|
6702
8592
|
UserPromptSubmit: "watch",
|
|
6703
8593
|
PreToolUse: "watch",
|
|
@@ -6710,7 +8600,7 @@ var FALLBACK = {
|
|
|
6710
8600
|
}
|
|
6711
8601
|
};
|
|
6712
8602
|
function registryCachePath() {
|
|
6713
|
-
return
|
|
8603
|
+
return join27(deskbuddyHome(), "tool-registry.json");
|
|
6714
8604
|
}
|
|
6715
8605
|
function withFallbackTools(registry) {
|
|
6716
8606
|
if (!registry || typeof registry !== "object" || !registry.tools || typeof registry.tools !== "object") return FALLBACK;
|
|
@@ -6726,15 +8616,15 @@ function loadRegistry() {
|
|
|
6726
8616
|
async function refreshRegistry() {
|
|
6727
8617
|
try {
|
|
6728
8618
|
const registry = await api.toolRegistry();
|
|
6729
|
-
|
|
6730
|
-
|
|
8619
|
+
mkdirSync20(deskbuddyHome(), { recursive: true });
|
|
8620
|
+
writeFileSync19(registryCachePath(), JSON.stringify(registry, null, 2), "utf8");
|
|
6731
8621
|
} catch {
|
|
6732
8622
|
}
|
|
6733
8623
|
}
|
|
6734
8624
|
|
|
6735
8625
|
// src/adapters/ccHook.ts
|
|
6736
|
-
import { chmodSync as chmodSync8, existsSync as existsSync14, mkdirSync as
|
|
6737
|
-
import { dirname as
|
|
8626
|
+
import { chmodSync as chmodSync8, existsSync as existsSync14, mkdirSync as mkdirSync21, readFileSync as readFileSync19, renameSync as renameSync15, rmSync as rmSync16, statSync as statSync9, writeFileSync as writeFileSync20 } from "fs";
|
|
8627
|
+
import { dirname as dirname11, join as join28 } from "path";
|
|
6738
8628
|
var CC_EVENTS = [
|
|
6739
8629
|
"UserPromptSubmit",
|
|
6740
8630
|
"PreToolUse",
|
|
@@ -6754,17 +8644,17 @@ function readSettings(path) {
|
|
|
6754
8644
|
}
|
|
6755
8645
|
}
|
|
6756
8646
|
function writeSettings(path, settings) {
|
|
6757
|
-
|
|
8647
|
+
mkdirSync21(dirname11(path), { recursive: true });
|
|
6758
8648
|
let mode;
|
|
6759
8649
|
try {
|
|
6760
|
-
mode =
|
|
8650
|
+
mode = statSync9(path).mode & 511;
|
|
6761
8651
|
} catch {
|
|
6762
8652
|
mode = void 0;
|
|
6763
8653
|
}
|
|
6764
8654
|
const tmp = path + ".deskbuddy.tmp";
|
|
6765
|
-
|
|
8655
|
+
writeFileSync20(tmp, JSON.stringify(settings, null, 2), "utf8");
|
|
6766
8656
|
if (mode !== void 0) chmodSync8(tmp, mode);
|
|
6767
|
-
|
|
8657
|
+
renameSync15(tmp, path);
|
|
6768
8658
|
}
|
|
6769
8659
|
function mentions(command, script) {
|
|
6770
8660
|
return !!command && (command.includes(script) || command.includes(fwdSlash(script)));
|
|
@@ -6886,8 +8776,8 @@ function retagCcHook(settingsPath, scripts, from, to) {
|
|
|
6886
8776
|
function uninstallCcHook(settingsPath, hookPath) {
|
|
6887
8777
|
const settings = readSettings(settingsPath);
|
|
6888
8778
|
if (!settings.hooks) return;
|
|
6889
|
-
const gatePath =
|
|
6890
|
-
const wakePath =
|
|
8779
|
+
const gatePath = join28(dirname11(hookPath), "gate.cjs");
|
|
8780
|
+
const wakePath = join28(dirname11(hookPath), "wake.cjs");
|
|
6891
8781
|
for (const event of Object.keys(settings.hooks)) {
|
|
6892
8782
|
const groups = settings.hooks[event] ?? [];
|
|
6893
8783
|
const kept = [];
|
|
@@ -6901,14 +8791,14 @@ function uninstallCcHook(settingsPath, hookPath) {
|
|
|
6901
8791
|
}
|
|
6902
8792
|
writeSettings(settingsPath, settings);
|
|
6903
8793
|
try {
|
|
6904
|
-
rmSync16(
|
|
8794
|
+
rmSync16(join28(dirname11(hookPath), "wake", "tokens"), { recursive: true, force: true });
|
|
6905
8795
|
} catch {
|
|
6906
8796
|
}
|
|
6907
8797
|
}
|
|
6908
8798
|
|
|
6909
8799
|
// src/adapters/codexHook.ts
|
|
6910
|
-
import { chmodSync as chmodSync9, existsSync as existsSync15, mkdirSync as
|
|
6911
|
-
import { dirname as
|
|
8800
|
+
import { chmodSync as chmodSync9, existsSync as existsSync15, mkdirSync as mkdirSync22, readFileSync as readFileSync20, renameSync as renameSync16, statSync as statSync10, writeFileSync as writeFileSync21 } from "fs";
|
|
8801
|
+
import { dirname as dirname12, join as join29 } from "path";
|
|
6912
8802
|
var CODEX_MARKER_BEGIN = "# >>> deskbuddy >>>";
|
|
6913
8803
|
var CODEX_MARKER_END = "# <<< deskbuddy <<<";
|
|
6914
8804
|
var CODEX_EVENTS = [
|
|
@@ -6921,7 +8811,7 @@ var CODEX_EVENTS = [
|
|
|
6921
8811
|
];
|
|
6922
8812
|
var CODEX_TRUST_NOTICE = "\u26A0\uFE0F \u4E0B\u6B21\u542F\u52A8 Codex \u65F6\u4F1A\u8981\u6C42\u4F60\u786E\u8BA4\u4FE1\u4EFB\u8FD9\u4E2A hook\uFF0C\u8BF7\u9009\u62E9\u540C\u610F\u2014\u2014\u4E0D\u786E\u8BA4\u7684\u8BDD Codex \u4E0D\u4F1A\u6267\u884C\u5B83\u3002\n DeskBuddy \u5347\u7EA7\uFF08\u542B\u81EA\u52A8\u5347\u7EA7\uFF09\u65B0\u589E hook \u6761\u76EE\u65F6\uFF0CCodex \u53EA\u4F1A\u5C31\u65B0\u6761\u76EE\u518D\u95EE\u4E00\u6B21\uFF0C\u540C\u6837\u9009\u540C\u610F\uFF1B\u539F\u6709\u6761\u76EE\u4E0D\u53D7\u5F71\u54CD\u3001\u72B6\u6001\u7167\u5E38\u4E0A\u62A5\u3002\n Codex \u7ECF `--remote` \u8FDE\u5E38\u9A7B app-server \u4F7F\u7528\u65F6\uFF0C\u7A97\u53E3\u91CC\u7684\u6388\u6743\u6846\u4E5F\u80FD\u5728\u8BBE\u5907\u4E0A\u786E\u8BA4\uFF08\u5B88\u62A4\u8FDB\u7A0B\u5728\u8DD1\u65F6\u751F\u6548\uFF09";
|
|
6923
8813
|
function codexConfigPath() {
|
|
6924
|
-
return
|
|
8814
|
+
return join29(codexHome(), "config.toml");
|
|
6925
8815
|
}
|
|
6926
8816
|
var CODEX_APPENDED_TIMEOUT_S = 3;
|
|
6927
8817
|
function renderBlock(hookPath, events, tool, platform) {
|
|
@@ -6940,17 +8830,17 @@ function renderBlock(hookPath, events, tool, platform) {
|
|
|
6940
8830
|
return lines2.join("\n");
|
|
6941
8831
|
}
|
|
6942
8832
|
function writeAtomic(path, content) {
|
|
6943
|
-
|
|
8833
|
+
mkdirSync22(dirname12(path), { recursive: true });
|
|
6944
8834
|
let mode;
|
|
6945
8835
|
try {
|
|
6946
|
-
mode =
|
|
8836
|
+
mode = statSync10(path).mode & 511;
|
|
6947
8837
|
} catch {
|
|
6948
8838
|
mode = void 0;
|
|
6949
8839
|
}
|
|
6950
8840
|
const tmp = path + ".deskbuddy.tmp";
|
|
6951
|
-
|
|
8841
|
+
writeFileSync21(tmp, content, "utf8");
|
|
6952
8842
|
if (mode !== void 0) chmodSync9(tmp, mode);
|
|
6953
|
-
|
|
8843
|
+
renameSync16(tmp, path);
|
|
6954
8844
|
}
|
|
6955
8845
|
function assertSafeToAppend(raw, path) {
|
|
6956
8846
|
for (const quote of ['"""', "'''"]) {
|
|
@@ -7005,7 +8895,9 @@ var CC_CONFIG_PATHS = {
|
|
|
7005
8895
|
claude_code: claudeSettingsPath,
|
|
7006
8896
|
workbuddy: workbuddySettingsPath,
|
|
7007
8897
|
qoder_cn: qoderCnSettingsPath,
|
|
7008
|
-
qoder: qoderSettingsPath
|
|
8898
|
+
qoder: qoderSettingsPath,
|
|
8899
|
+
// 千问办公只装状态上报:不装 gate.cjs、不装 rewake(选择题 / 授权由 daemon 的 qwenworkBridge 经 App 内置 MCP 回填)
|
|
8900
|
+
qwenwork: qwenworkSettingsPath
|
|
7009
8901
|
};
|
|
7010
8902
|
var WINDOW_GATE_TOOLS = /* @__PURE__ */ new Set(["claude_code", "qoder_cn", "qoder"]);
|
|
7011
8903
|
var ADAPTERS = {
|
|
@@ -7053,6 +8945,26 @@ var ADAPTERS = {
|
|
|
7053
8945
|
installWindowGate: () => true
|
|
7054
8946
|
}
|
|
7055
8947
|
};
|
|
8948
|
+
var DUMATE_NOTICE = " \u767E\u5EA6\u642D\u5B50\u4E0D\u9700\u8981\u6539\u5B83\u7684\u4EFB\u4F55\u914D\u7F6E\uFF1A\u5B88\u62A4\u8FDB\u7A0B\u5728\u8DD1\u65F6\u76F4\u8FDE\u672C\u673A\u7684\u767E\u5EA6\u642D\u5B50\uFF0C\u72B6\u6001\u3001\u9009\u62E9\u9898\u548C\u6388\u6743\u90FD\u4F1A\u63A8\u5230\u8BBE\u5907";
|
|
8949
|
+
ADAPTERS["dumate-http"] = {
|
|
8950
|
+
configPath: (tool) => tool === "dumate" ? configPath() : null,
|
|
8951
|
+
install: () => {
|
|
8952
|
+
},
|
|
8953
|
+
uninstall: () => {
|
|
8954
|
+
},
|
|
8955
|
+
isInstalled: (p) => {
|
|
8956
|
+
try {
|
|
8957
|
+
const enabled = JSON.parse(readFileSync21(p, "utf8")).enabledTools;
|
|
8958
|
+
return Array.isArray(enabled) && enabled.includes("dumate");
|
|
8959
|
+
} catch {
|
|
8960
|
+
return false;
|
|
8961
|
+
}
|
|
8962
|
+
},
|
|
8963
|
+
defaultEvents: [],
|
|
8964
|
+
notice: DUMATE_NOTICE,
|
|
8965
|
+
// 选择题 / 授权由 DumateWatch 推,不另装
|
|
8966
|
+
installWindowGate: () => true
|
|
8967
|
+
};
|
|
7056
8968
|
function adapterFor(registry, tool) {
|
|
7057
8969
|
const format = registry?.tools?.[tool]?.format;
|
|
7058
8970
|
return format && ADAPTERS[format] || null;
|
|
@@ -7073,13 +8985,13 @@ function subscribedEvents(registry, tool, fallback) {
|
|
|
7073
8985
|
}
|
|
7074
8986
|
|
|
7075
8987
|
// src/commands/enable.ts
|
|
7076
|
-
import { dirname as
|
|
8988
|
+
import { dirname as dirname15, join as join32 } from "path";
|
|
7077
8989
|
|
|
7078
8990
|
// src/wrap/shim.ts
|
|
7079
|
-
import { execFileSync as
|
|
7080
|
-
import { chmodSync as chmodSync10, existsSync as existsSync17, mkdirSync as
|
|
7081
|
-
import { homedir as
|
|
7082
|
-
import { delimiter as delimiter2, join as
|
|
8991
|
+
import { execFileSync as execFileSync13 } from "child_process";
|
|
8992
|
+
import { chmodSync as chmodSync10, existsSync as existsSync17, mkdirSync as mkdirSync23, readdirSync as readdirSync7, readFileSync as readFileSync22, rmdirSync, rmSync as rmSync17, writeFileSync as writeFileSync22 } from "fs";
|
|
8993
|
+
import { homedir as homedir8 } from "os";
|
|
8994
|
+
import { delimiter as delimiter2, join as join30, resolve as resolve4 } from "path";
|
|
7083
8995
|
var SHIM_FALLBACK_EXIT = 213;
|
|
7084
8996
|
var WIN_SHIM_ENABLED = false;
|
|
7085
8997
|
var SHIM_COMMANDS = { claude_code: "claude", codex: "codex" };
|
|
@@ -7089,10 +9001,10 @@ var RC_END = "# <<< deskbuddy <<<";
|
|
|
7089
9001
|
var RC_FILES = [".zshrc", ".bashrc"];
|
|
7090
9002
|
var WIN_PENDING = "Windows \u7A97\u53E3\u63A5\u7BA1\u5F85\u9A8C\u8BC1\uFF0C\u6682\u672A\u5F00\u542F";
|
|
7091
9003
|
function shimDir() {
|
|
7092
|
-
return
|
|
9004
|
+
return join30(deskbuddyHome(), "bin");
|
|
7093
9005
|
}
|
|
7094
9006
|
function shimFile(binDir, command, platform = process.platform) {
|
|
7095
|
-
return
|
|
9007
|
+
return join30(binDir, platform === "win32" ? `${command}.cmd` : command);
|
|
7096
9008
|
}
|
|
7097
9009
|
function shQuote3(s) {
|
|
7098
9010
|
return `'${s.replace(/'/g, `'\\''`)}'`;
|
|
@@ -7180,7 +9092,7 @@ function cmdScript(tool, command, node, cli, bin) {
|
|
|
7180
9092
|
].join("\r\n");
|
|
7181
9093
|
}
|
|
7182
9094
|
function rcBlock(binDir, rcHome) {
|
|
7183
|
-
const std =
|
|
9095
|
+
const std = resolve4(rcHome, ".deskbuddy", "bin") === resolve4(binDir);
|
|
7184
9096
|
const dir = std ? "$HOME/.deskbuddy/bin" : binDir.replace(/(["\\$`])/g, "\\$1");
|
|
7185
9097
|
return [
|
|
7186
9098
|
RC_BEGIN,
|
|
@@ -7224,7 +9136,7 @@ function readRc(p) {
|
|
|
7224
9136
|
return readFileSync22(p).toString("latin1");
|
|
7225
9137
|
}
|
|
7226
9138
|
function writeRc(p, latin1) {
|
|
7227
|
-
|
|
9139
|
+
writeFileSync22(p, Buffer.from(latin1, "latin1"));
|
|
7228
9140
|
}
|
|
7229
9141
|
function toLatin1(utf8) {
|
|
7230
9142
|
return Buffer.from(utf8, "utf8").toString("latin1");
|
|
@@ -7246,7 +9158,7 @@ function removePathEntry(value, dir) {
|
|
|
7246
9158
|
var PS_READ = "$k=[Microsoft.Win32.Registry]::CurrentUser.OpenSubKey('Environment');$v='';if($k){$v=$k.GetValue('Path','',[Microsoft.Win32.RegistryValueOptions]::DoNotExpandEnvironmentNames)};[Console]::Out.Write([Convert]::ToBase64String([Text.Encoding]::UTF8.GetBytes([string]$v)))";
|
|
7247
9159
|
var PS_WRITE = "$k=[Microsoft.Win32.Registry]::CurrentUser.CreateSubKey('Environment');$kind=[Microsoft.Win32.RegistryValueKind]::ExpandString;try{$kind=$k.GetValueKind('Path')}catch{};if([string]::IsNullOrEmpty($env:DESKBUDDY_NEW_PATH)){$k.DeleteValue('Path',$false)}else{$k.SetValue('Path',$env:DESKBUDDY_NEW_PATH,$kind)};[Environment]::SetEnvironmentVariable('DESKBUDDY_PATH_REFRESH','1','User');[Environment]::SetEnvironmentVariable('DESKBUDDY_PATH_REFRESH',$null,'User')";
|
|
7248
9160
|
function psWinPath() {
|
|
7249
|
-
const ps = (script, env) =>
|
|
9161
|
+
const ps = (script, env) => execFileSync13("powershell.exe", ["-NoProfile", "-NonInteractive", "-ExecutionPolicy", "Bypass", "-Command", script], {
|
|
7250
9162
|
encoding: "utf8",
|
|
7251
9163
|
stdio: ["ignore", "pipe", "ignore"],
|
|
7252
9164
|
windowsHide: true,
|
|
@@ -7269,7 +9181,7 @@ function psWinPath() {
|
|
|
7269
9181
|
function defaultShimEnv(nodePath, cliPath) {
|
|
7270
9182
|
const env = process.env;
|
|
7271
9183
|
const isolated = !!env.VITEST || !!env.DESKBUDDY_HOME;
|
|
7272
|
-
const rcHome = env.DESKBUDDY_RC_HOME || (isolated ? null :
|
|
9184
|
+
const rcHome = env.DESKBUDDY_RC_HOME || (isolated ? null : homedir8());
|
|
7273
9185
|
return {
|
|
7274
9186
|
platform: process.platform,
|
|
7275
9187
|
binDir: shimDir(),
|
|
@@ -7293,11 +9205,11 @@ function installShim(tool, e) {
|
|
|
7293
9205
|
if (!node || !cli || !bin) {
|
|
7294
9206
|
return { ok: false, message: "\u7A97\u53E3\u63A5\u7BA1\u6CA1\u88C5\uFF1A\u5B89\u88C5\u8DEF\u5F84\u91CC\u6709\u975E ASCII \u5B57\u7B26\uFF0CWindows \u7684 .cmd \u8BFB\u4E0D\u4E86\uFF08\u4E0D\u5F71\u54CD\u5176\u5B83\u529F\u80FD\uFF09" };
|
|
7295
9207
|
}
|
|
7296
|
-
|
|
7297
|
-
|
|
9208
|
+
mkdirSync23(e.binDir, { recursive: true });
|
|
9209
|
+
writeFileSync22(file, cmdScript(tool, command, node, cli, bin), "utf8");
|
|
7298
9210
|
} else {
|
|
7299
|
-
|
|
7300
|
-
|
|
9211
|
+
mkdirSync23(e.binDir, { recursive: true, mode: 448 });
|
|
9212
|
+
writeFileSync22(file, shScript(tool, command, e.nodePath, e.cliPath, e.binDir), { encoding: "utf8", mode: 493 });
|
|
7301
9213
|
chmodSync10(file, 493);
|
|
7302
9214
|
}
|
|
7303
9215
|
const touched = [];
|
|
@@ -7312,7 +9224,7 @@ function installShim(tool, e) {
|
|
|
7312
9224
|
} else if (e.rcHome) {
|
|
7313
9225
|
const block = toLatin1(rcBlock(e.binDir, e.rcHome));
|
|
7314
9226
|
for (const name of RC_FILES) {
|
|
7315
|
-
const rc =
|
|
9227
|
+
const rc = join30(e.rcHome, name);
|
|
7316
9228
|
if (!existsSync17(rc)) continue;
|
|
7317
9229
|
const next = addRcBlock(readRc(rc), block);
|
|
7318
9230
|
if (next !== null) writeRc(rc, next);
|
|
@@ -7333,13 +9245,13 @@ function installShim(tool, e) {
|
|
|
7333
9245
|
function ourShims(binDir) {
|
|
7334
9246
|
let names;
|
|
7335
9247
|
try {
|
|
7336
|
-
names =
|
|
9248
|
+
names = readdirSync7(binDir);
|
|
7337
9249
|
} catch {
|
|
7338
9250
|
return [];
|
|
7339
9251
|
}
|
|
7340
9252
|
return names.filter((n) => {
|
|
7341
9253
|
try {
|
|
7342
|
-
return readFileSync22(
|
|
9254
|
+
return readFileSync22(join30(binDir, n), "utf8").includes(SHIM_MARKER);
|
|
7343
9255
|
} catch {
|
|
7344
9256
|
return false;
|
|
7345
9257
|
}
|
|
@@ -7370,7 +9282,7 @@ function uninstallShim(tool, e) {
|
|
|
7370
9282
|
}
|
|
7371
9283
|
} else if (e.rcHome) {
|
|
7372
9284
|
for (const name of RC_FILES) {
|
|
7373
|
-
const rc =
|
|
9285
|
+
const rc = join30(e.rcHome, name);
|
|
7374
9286
|
if (!existsSync17(rc)) continue;
|
|
7375
9287
|
const next = removeRcBlock(readRc(rc));
|
|
7376
9288
|
if (next !== null) {
|
|
@@ -7380,7 +9292,7 @@ function uninstallShim(tool, e) {
|
|
|
7380
9292
|
}
|
|
7381
9293
|
}
|
|
7382
9294
|
try {
|
|
7383
|
-
if (existsSync17(e.binDir) &&
|
|
9295
|
+
if (existsSync17(e.binDir) && readdirSync7(e.binDir).length === 0) rmdirSync(e.binDir);
|
|
7384
9296
|
} catch {
|
|
7385
9297
|
}
|
|
7386
9298
|
if (!removed && touched.length === 0) return { ok: true, message: "" };
|
|
@@ -7403,12 +9315,12 @@ function shimState(tool, binDir, pathEnv, platform = process.platform) {
|
|
|
7403
9315
|
}
|
|
7404
9316
|
|
|
7405
9317
|
// src/commands/migrateQoder.ts
|
|
7406
|
-
import { dirname as
|
|
9318
|
+
import { dirname as dirname14, join as join31 } from "path";
|
|
7407
9319
|
|
|
7408
9320
|
// src/core/detect.ts
|
|
7409
9321
|
import { existsSync as existsSync18 } from "fs";
|
|
7410
|
-
import { dirname as
|
|
7411
|
-
var ALL_TOOLS = ["claude_code", "codex", "workbuddy", "qoder_cn", "qoder", "opencode"];
|
|
9322
|
+
import { dirname as dirname13 } from "path";
|
|
9323
|
+
var ALL_TOOLS = ["claude_code", "codex", "workbuddy", "qoder_cn", "qoder", "opencode", "qwenwork", "dumate"];
|
|
7412
9324
|
function toolConfigDir(tool) {
|
|
7413
9325
|
switch (tool) {
|
|
7414
9326
|
case "claude_code":
|
|
@@ -7416,17 +9328,23 @@ function toolConfigDir(tool) {
|
|
|
7416
9328
|
case "codex":
|
|
7417
9329
|
return codexHome();
|
|
7418
9330
|
case "workbuddy":
|
|
7419
|
-
return
|
|
9331
|
+
return dirname13(workbuddySettingsPath());
|
|
7420
9332
|
case "qoder_cn":
|
|
7421
|
-
return
|
|
9333
|
+
return dirname13(qoderCnSettingsPath());
|
|
7422
9334
|
case "qoder":
|
|
7423
|
-
return
|
|
9335
|
+
return dirname13(qoderSettingsPath());
|
|
7424
9336
|
case "opencode":
|
|
7425
|
-
return
|
|
9337
|
+
return dirname13(dirname13(opencodePluginPath()));
|
|
9338
|
+
case "qwenwork":
|
|
9339
|
+
return qwenworkHome();
|
|
7426
9340
|
default:
|
|
7427
9341
|
return null;
|
|
7428
9342
|
}
|
|
7429
9343
|
}
|
|
9344
|
+
function toolDetectDir(tool) {
|
|
9345
|
+
if (tool === "dumate") return qianfanHome();
|
|
9346
|
+
return toolConfigDir(tool);
|
|
9347
|
+
}
|
|
7430
9348
|
var defaultDetectDeps = () => ({
|
|
7431
9349
|
located: (tool) => locate(tool) != null,
|
|
7432
9350
|
exists: existsSync18,
|
|
@@ -7441,7 +9359,7 @@ function detectTools(d = defaultDetectDeps()) {
|
|
|
7441
9359
|
return ALL_TOOLS.filter((tool) => {
|
|
7442
9360
|
if (tool === caller) return true;
|
|
7443
9361
|
if (d.located(tool)) return true;
|
|
7444
|
-
const dir =
|
|
9362
|
+
const dir = toolDetectDir(tool);
|
|
7445
9363
|
return dir != null && d.exists(dir);
|
|
7446
9364
|
});
|
|
7447
9365
|
}
|
|
@@ -7451,7 +9369,7 @@ function migrateLegacyQoder(out, detect = () => detectTools()) {
|
|
|
7451
9369
|
try {
|
|
7452
9370
|
const cfg = loadConfig();
|
|
7453
9371
|
const hookPath = installedHookPath();
|
|
7454
|
-
const scripts = [hookPath,
|
|
9372
|
+
const scripts = [hookPath, join31(dirname14(hookPath), "gate.cjs")];
|
|
7455
9373
|
if (!retagCcHook(qoderCnSettingsPath(), scripts, "qoder", "qoder_cn")) return false;
|
|
7456
9374
|
const hadQoder = cfg.enabledTools.includes("qoder");
|
|
7457
9375
|
let intl = false;
|
|
@@ -7502,7 +9420,8 @@ function installShims(tools, out, shim) {
|
|
|
7502
9420
|
const r = installShim(tool, shim);
|
|
7503
9421
|
if (r.message) out(r.ok ? `\u2705 ${r.message}` : `\u26A0\uFE0F ${r.message}`);
|
|
7504
9422
|
if (r.ok && tool === "claude_code" && (!loadPty() || !loadHeadless())) {
|
|
7505
|
-
|
|
9423
|
+
const why = loadPty() ? "@xterm/headless \u6CA1\u88C5\u4E0A" : ptyMissingReason();
|
|
9424
|
+
out(`\u26A0\uFE0F ${why}\uFF1A\u7A97\u53E3\u63A5\u7BA1\u5148\u964D\u7EA7\u4E3A\u76F4\u63A5\u8FD0\u884C\uFF08claude \u7167\u5E38\u80FD\u7528\uFF0C\u53EA\u662F\u8BBE\u5907\u56DE\u590D\u5199\u4E0D\u8FDB\u7A97\u53E3\uFF09`);
|
|
7506
9425
|
}
|
|
7507
9426
|
} catch (e) {
|
|
7508
9427
|
out(`\u26A0\uFE0F \u7A97\u53E3\u63A5\u7BA1\u6CA1\u88C5\u4E0A\uFF1A${e.message}\uFF08\u4E0D\u5F71\u54CD\u5176\u5B83\u529F\u80FD\uFF09`);
|
|
@@ -7543,14 +9462,14 @@ function installTools(tools, out, shim = defaultShimEnvForInstall()) {
|
|
|
7543
9462
|
installed.push(tool);
|
|
7544
9463
|
out(`\u2705 ${tool}\uFF1Ahook \u5DF2\u88C5\u8FDB ${configPath2}\uFF08${events.length} \u4E2A\u4E8B\u4EF6\uFF09`);
|
|
7545
9464
|
try {
|
|
7546
|
-
if (adapter.installWindowGate?.(configPath2,
|
|
9465
|
+
if (adapter.installWindowGate?.(configPath2, join32(dirname15(hookPath), "gate.cjs"), tool)) {
|
|
7547
9466
|
out(` \u7A97\u53E3\u91CC\u5F39\u6388\u6743\u6846\u65F6\uFF0C\u8BBE\u5907\u4E0A\u4E5F\u80FD\u786E\u8BA4\uFF08\u5B88\u62A4\u8FDB\u7A0B\u5728\u8DD1\u65F6\u751F\u6548\uFF09`);
|
|
7548
9467
|
}
|
|
7549
9468
|
} catch (e) {
|
|
7550
9469
|
out(`\u26A0\uFE0F ${tool}\uFF1A\u8BBE\u5907\u786E\u8BA4\u6388\u6743\u6CA1\u88C5\u4E0A\uFF1A${e.message}`);
|
|
7551
9470
|
}
|
|
7552
9471
|
try {
|
|
7553
|
-
if (adapter.installRewake?.(configPath2,
|
|
9472
|
+
if (adapter.installRewake?.(configPath2, join32(dirname15(hookPath), "wake.cjs"), tool)) {
|
|
7554
9473
|
out(` \u7A97\u53E3\u5F00\u7740\u65F6\uFF0C\u8BBE\u5907\u6307\u4EE4\u76F4\u63A5\u5728\u8FD9\u4E2A\u7A97\u53E3\u91CC\u6267\u884C\uFF08\u5B88\u62A4\u8FDB\u7A0B\u5728\u8DD1\u65F6\u751F\u6548\uFF09`);
|
|
7555
9474
|
}
|
|
7556
9475
|
} catch (e) {
|
|
@@ -7651,28 +9570,28 @@ async function runDisable(tools, opts = {}) {
|
|
|
7651
9570
|
}
|
|
7652
9571
|
|
|
7653
9572
|
// src/commands/install.ts
|
|
7654
|
-
import { mkdirSync as
|
|
7655
|
-
import { join as
|
|
9573
|
+
import { mkdirSync as mkdirSync24, rmSync as rmSync18, writeFileSync as writeFileSync23 } from "fs";
|
|
9574
|
+
import { join as join33 } from "path";
|
|
7656
9575
|
|
|
7657
9576
|
// src/commands/setup.ts
|
|
7658
9577
|
import qrcode from "qrcode-terminal";
|
|
7659
9578
|
|
|
7660
9579
|
// src/commands/remoteCwd.ts
|
|
7661
|
-
import { existsSync as existsSync19, statSync as
|
|
9580
|
+
import { existsSync as existsSync19, statSync as statSync11 } from "fs";
|
|
7662
9581
|
import { createInterface } from "readline";
|
|
7663
|
-
import { homedir as
|
|
7664
|
-
import { isAbsolute as isAbsolute2, resolve as
|
|
7665
|
-
function normalizePath(raw, home =
|
|
9582
|
+
import { homedir as homedir9 } from "os";
|
|
9583
|
+
import { isAbsolute as isAbsolute2, resolve as resolve5 } from "path";
|
|
9584
|
+
function normalizePath(raw, home = homedir9()) {
|
|
7666
9585
|
const s = (raw || "").trim();
|
|
7667
9586
|
if (!s) return null;
|
|
7668
|
-
const expanded = s === "~" ? home : s.startsWith("~/") || process.platform === "win32" && s.startsWith("~\\") ?
|
|
7669
|
-
const abs = isAbsolute2(expanded) ? expanded :
|
|
9587
|
+
const expanded = s === "~" ? home : s.startsWith("~/") || process.platform === "win32" && s.startsWith("~\\") ? resolve5(home, s.slice(2)) : s;
|
|
9588
|
+
const abs = isAbsolute2(expanded) ? expanded : resolve5(process.cwd(), expanded);
|
|
7670
9589
|
return abs;
|
|
7671
9590
|
}
|
|
7672
9591
|
function checkPath(abs) {
|
|
7673
9592
|
if (!existsSync19(abs)) return "missing";
|
|
7674
9593
|
try {
|
|
7675
|
-
return
|
|
9594
|
+
return statSync11(abs).isDirectory() ? "ok" : "not_a_dir";
|
|
7676
9595
|
} catch {
|
|
7677
9596
|
return "missing";
|
|
7678
9597
|
}
|
|
@@ -7781,7 +9700,7 @@ function parseApiBase(argv) {
|
|
|
7781
9700
|
}
|
|
7782
9701
|
return value;
|
|
7783
9702
|
}
|
|
7784
|
-
function
|
|
9703
|
+
function sleep2(ms) {
|
|
7785
9704
|
return new Promise((r) => setTimeout(r, ms));
|
|
7786
9705
|
}
|
|
7787
9706
|
function stdout3(line) {
|
|
@@ -7804,7 +9723,7 @@ async function runSetup(opts = {}) {
|
|
|
7804
9723
|
await pollForBinding(
|
|
7805
9724
|
authCode,
|
|
7806
9725
|
{ intervalSec: interval },
|
|
7807
|
-
{ authToken: (code) => api.authToken(code), sleep, now: () => Date.now() }
|
|
9726
|
+
{ authToken: (code) => api.authToken(code), sleep: sleep2, now: () => Date.now() }
|
|
7808
9727
|
);
|
|
7809
9728
|
markBound();
|
|
7810
9729
|
await refreshRegistry();
|
|
@@ -7842,7 +9761,9 @@ var TOOL_LABELS = {
|
|
|
7842
9761
|
workbuddy: "WorkBuddy",
|
|
7843
9762
|
qoder_cn: "Qoder CN",
|
|
7844
9763
|
qoder: "Qoder",
|
|
7845
|
-
opencode: "opencode"
|
|
9764
|
+
opencode: "opencode",
|
|
9765
|
+
qwenwork: "\u5343\u95EE\u529E\u516C",
|
|
9766
|
+
dumate: "\u767E\u5EA6\u642D\u5B50"
|
|
7846
9767
|
};
|
|
7847
9768
|
function checkApiBase(raw, env = process.env) {
|
|
7848
9769
|
const norm3 = raw.replace(/\/+$/, "");
|
|
@@ -7877,10 +9798,10 @@ function inSandbox(env) {
|
|
|
7877
9798
|
}
|
|
7878
9799
|
function precheckWritable(dirs) {
|
|
7879
9800
|
for (const dir of dirs) {
|
|
7880
|
-
const probe =
|
|
9801
|
+
const probe = join33(dir, `.deskbuddy-probe-${process.pid}`);
|
|
7881
9802
|
try {
|
|
7882
|
-
|
|
7883
|
-
|
|
9803
|
+
mkdirSync24(dir, { recursive: true });
|
|
9804
|
+
writeFileSync23(probe, "");
|
|
7884
9805
|
rmSync18(probe, { force: true });
|
|
7885
9806
|
} catch (e) {
|
|
7886
9807
|
return { ok: false, dir, error: e.message };
|
|
@@ -8015,11 +9936,11 @@ function stdout4(line) {
|
|
|
8015
9936
|
process.stdout.write(line + "\n");
|
|
8016
9937
|
}
|
|
8017
9938
|
function askYesNo(question) {
|
|
8018
|
-
return new Promise((
|
|
9939
|
+
return new Promise((resolve7) => {
|
|
8019
9940
|
const rl = createInterface2({ input: process.stdin, output: process.stdout });
|
|
8020
9941
|
rl.question(question, (answer) => {
|
|
8021
9942
|
rl.close();
|
|
8022
|
-
|
|
9943
|
+
resolve7(/^(y|yes)$/i.test(answer.trim()));
|
|
8023
9944
|
});
|
|
8024
9945
|
});
|
|
8025
9946
|
}
|
|
@@ -8104,7 +10025,7 @@ function formatStatus(input) {
|
|
|
8104
10025
|
lines2.push(` ${pad("hook \u7248\u672C", 10)}${versionNote}`);
|
|
8105
10026
|
if (input.windowWrap) {
|
|
8106
10027
|
const w = input.windowWrap;
|
|
8107
|
-
const text = w.winPending ? WIN_PENDING : !w.installed ? "\u672A\u5B89\u88C5 \u2014\u2014 \u91CD\u8DD1 deskbuddy enable claude_code" : !w.inPath ? `\u672A\u751F\u6548\uFF08\u5F53\u524D shell \u7684 PATH \u91CC\u6CA1\u6709 ${w.binDir}\uFF0C\u65B0\u5F00\u4E00\u4E2A\u7EC8\u7AEF\u518D\u770B\uFF09` : !w.ptyOk ?
|
|
10028
|
+
const text = w.winPending ? WIN_PENDING : !w.installed ? "\u672A\u5B89\u88C5 \u2014\u2014 \u91CD\u8DD1 deskbuddy enable claude_code" : !w.inPath ? `\u672A\u751F\u6548\uFF08\u5F53\u524D shell \u7684 PATH \u91CC\u6CA1\u6709 ${w.binDir}\uFF0C\u65B0\u5F00\u4E00\u4E2A\u7EC8\u7AEF\u518D\u770B\uFF09` : !w.ptyOk ? `\u5DF2\u5F00\u542F\uFF0C\u4F46 ${w.ptyWhy ?? "node-pty \u6CA1\u88C5\u4E0A"}\uFF0C\u6682\u65F6\u964D\u7EA7\u4E3A\u76F4\u63A5\u8FD0\u884C` : "\u5DF2\u5F00\u542F\uFF08\u7EC8\u7AEF\u91CC\u6572 claude \u4F1A\u7ECF DeskBuddy \u6253\u5F00\uFF0C\u8BBE\u5907\u56DE\u590D\u80FD\u5199\u8FDB\u7A97\u53E3\uFF09";
|
|
8108
10029
|
lines2.push(` ${pad("\u7A97\u53E3\u63A5\u7BA1", 10)}${text}`);
|
|
8109
10030
|
}
|
|
8110
10031
|
if (input.codexWrap) {
|
|
@@ -8149,7 +10070,7 @@ function collectStatus(now = Date.now()) {
|
|
|
8149
10070
|
installed,
|
|
8150
10071
|
notice: installed ? adapter.notice : void 0
|
|
8151
10072
|
});
|
|
8152
|
-
sessions.push(...readAliveStates(tool));
|
|
10073
|
+
sessions.push(...readAliveStates(tool, { now }));
|
|
8153
10074
|
}
|
|
8154
10075
|
const wrapTool = config.enabledTools.find((t) => t === "claude_code" && SHIM_COMMANDS[t]);
|
|
8155
10076
|
let windowWrap;
|
|
@@ -8161,7 +10082,15 @@ function collectStatus(now = Date.now()) {
|
|
|
8161
10082
|
if (wrapTool) {
|
|
8162
10083
|
const binDir = shimDir();
|
|
8163
10084
|
const st = shimState(wrapTool, binDir, process.env.PATH ?? "");
|
|
8164
|
-
|
|
10085
|
+
const pty = !!loadPty();
|
|
10086
|
+
const headless = !!loadHeadless();
|
|
10087
|
+
windowWrap = {
|
|
10088
|
+
...st,
|
|
10089
|
+
binDir,
|
|
10090
|
+
ptyOk: pty && headless,
|
|
10091
|
+
ptyWhy: !pty ? ptyMissingReason() : !headless ? "@xterm/headless \u6CA1\u88C5\u4E0A" : void 0,
|
|
10092
|
+
winPending: process.platform === "win32" && !WIN_SHIM_ENABLED
|
|
10093
|
+
};
|
|
8165
10094
|
}
|
|
8166
10095
|
return {
|
|
8167
10096
|
windowWrap,
|
|
@@ -8229,9 +10158,9 @@ async function runAutoSwitch(opts) {
|
|
|
8229
10158
|
}
|
|
8230
10159
|
|
|
8231
10160
|
// src/wrap/run.ts
|
|
8232
|
-
import { randomBytes as
|
|
8233
|
-
import { appendFileSync as appendFileSync2, mkdirSync as
|
|
8234
|
-
import { join as
|
|
10161
|
+
import { randomBytes as randomBytes9 } from "crypto";
|
|
10162
|
+
import { appendFileSync as appendFileSync2, mkdirSync as mkdirSync26, statSync as statSync13, truncateSync as truncateSync3 } from "fs";
|
|
10163
|
+
import { join as join36 } from "path";
|
|
8235
10164
|
import { StringDecoder } from "string_decoder";
|
|
8236
10165
|
|
|
8237
10166
|
// src/wrap/injector.ts
|
|
@@ -8479,10 +10408,10 @@ var Injector = class {
|
|
|
8479
10408
|
};
|
|
8480
10409
|
|
|
8481
10410
|
// src/wrap/passthrough.ts
|
|
8482
|
-
import { spawn as
|
|
8483
|
-
import { realpathSync as
|
|
10411
|
+
import { spawn as spawn5 } from "child_process";
|
|
10412
|
+
import { realpathSync as realpathSync4 } from "fs";
|
|
8484
10413
|
import { constants as constants2 } from "os";
|
|
8485
|
-
import { dirname as
|
|
10414
|
+
import { dirname as dirname16, resolve as resolve6 } from "path";
|
|
8486
10415
|
var CLAUDE_SUBCOMMANDS = /* @__PURE__ */ new Set([
|
|
8487
10416
|
"agents",
|
|
8488
10417
|
"attach",
|
|
@@ -8533,12 +10462,12 @@ function passthroughReason(i) {
|
|
|
8533
10462
|
return null;
|
|
8534
10463
|
}
|
|
8535
10464
|
function norm2(p, win) {
|
|
8536
|
-
const r =
|
|
10465
|
+
const r = resolve6(p).replace(/[\\/]+$/, "");
|
|
8537
10466
|
return win ? r.replace(/\//g, "\\").toLowerCase() : r;
|
|
8538
10467
|
}
|
|
8539
10468
|
function real(p) {
|
|
8540
10469
|
try {
|
|
8541
|
-
return
|
|
10470
|
+
return realpathSync4(p);
|
|
8542
10471
|
} catch {
|
|
8543
10472
|
return p;
|
|
8544
10473
|
}
|
|
@@ -8546,7 +10475,7 @@ function real(p) {
|
|
|
8546
10475
|
function findReal(tool, d = defaultLocateDeps(), skip = shimDir()) {
|
|
8547
10476
|
const win = (d.platform ?? process.platform) === "win32";
|
|
8548
10477
|
const skipKeys = /* @__PURE__ */ new Set([norm2(skip, win), norm2(real(skip), win)]);
|
|
8549
|
-
const inSkip = (file) => skipKeys.has(norm2(
|
|
10478
|
+
const inSkip = (file) => skipKeys.has(norm2(dirname16(file), win)) || skipKeys.has(norm2(dirname16(real(file)), win));
|
|
8550
10479
|
const dd = { ...d, skipDirs: [...d.skipDirs ?? [], skip] };
|
|
8551
10480
|
const name = SHIM_COMMANDS[tool];
|
|
8552
10481
|
if (!name) {
|
|
@@ -8575,7 +10504,7 @@ function runDirect(real2, args, env = process.env) {
|
|
|
8575
10504
|
return new Promise((done) => {
|
|
8576
10505
|
let child;
|
|
8577
10506
|
try {
|
|
8578
|
-
child =
|
|
10507
|
+
child = spawn5(real2.file, [...real2.prefixArgs, ...args], {
|
|
8579
10508
|
stdio: "inherit",
|
|
8580
10509
|
// 🔴 再经过垫片也直接放行,不会绕回来
|
|
8581
10510
|
env: withoutShimFlag({ ...env, DESKBUDDY_WRAPPED: "1" }),
|
|
@@ -8625,21 +10554,21 @@ function runDirect(real2, args, env = process.env) {
|
|
|
8625
10554
|
}
|
|
8626
10555
|
|
|
8627
10556
|
// src/wrap/codexLaunch.ts
|
|
8628
|
-
import { spawn as
|
|
8629
|
-
import { closeSync as
|
|
10557
|
+
import { spawn as spawn6 } from "child_process";
|
|
10558
|
+
import { closeSync as closeSync5, mkdirSync as mkdirSync25, openSync as openSync5, readdirSync as readdirSync8, readFileSync as readFileSync23, rmSync as rmSync20, statSync as statSync12, truncateSync as truncateSync2, writeFileSync as writeFileSync24, renameSync as renameSync17 } from "fs";
|
|
8630
10559
|
import { request } from "http";
|
|
8631
10560
|
import { createServer as createServer4 } from "net";
|
|
8632
|
-
import { join as
|
|
10561
|
+
import { join as join34 } from "path";
|
|
8633
10562
|
|
|
8634
10563
|
// src/core/codexServer.ts
|
|
8635
|
-
import { execFileSync as
|
|
10564
|
+
import { execFileSync as execFileSync14 } from "child_process";
|
|
8636
10565
|
function isAppServerArgs(args, url) {
|
|
8637
10566
|
return /\bapp-server\b/.test(args) && new RegExp(`--listen[= ]${url.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}(?:\\s|$)`).test(args);
|
|
8638
10567
|
}
|
|
8639
10568
|
function processArgs(pid) {
|
|
8640
10569
|
if (process.platform === "win32") return null;
|
|
8641
10570
|
try {
|
|
8642
|
-
const out =
|
|
10571
|
+
const out = execFileSync14("ps", ["-ww", "-o", "args=", "-p", String(pid)], {
|
|
8643
10572
|
encoding: "utf8",
|
|
8644
10573
|
stdio: ["ignore", "pipe", "ignore"]
|
|
8645
10574
|
});
|
|
@@ -8797,10 +10726,10 @@ async function runCodexWindow(real2, args, d) {
|
|
|
8797
10726
|
}
|
|
8798
10727
|
var MAX_LOG_BYTES2 = 1024 * 1024;
|
|
8799
10728
|
function appServerLogPath() {
|
|
8800
|
-
return
|
|
10729
|
+
return join34(deskbuddyHome(), "codex-app-server.log");
|
|
8801
10730
|
}
|
|
8802
10731
|
function windowsDir2() {
|
|
8803
|
-
return
|
|
10732
|
+
return join34(deskbuddyHome(), "codex-windows");
|
|
8804
10733
|
}
|
|
8805
10734
|
function pidAlive2(pid) {
|
|
8806
10735
|
try {
|
|
@@ -8823,40 +10752,40 @@ function daemonAlive(pidFile = pidFilePath(), args = processArgs) {
|
|
|
8823
10752
|
}
|
|
8824
10753
|
function probeReady(url) {
|
|
8825
10754
|
const port = Number(url.slice(url.lastIndexOf(":") + 1));
|
|
8826
|
-
return new Promise((
|
|
10755
|
+
return new Promise((resolve7) => {
|
|
8827
10756
|
const req2 = request({ host: "127.0.0.1", port, path: "/readyz", method: "GET", timeout: 800 }, (res) => {
|
|
8828
10757
|
res.resume();
|
|
8829
|
-
|
|
10758
|
+
resolve7((res.statusCode ?? 0) >= 200 && (res.statusCode ?? 0) < 300);
|
|
8830
10759
|
});
|
|
8831
10760
|
req2.on("timeout", () => req2.destroy());
|
|
8832
|
-
req2.on("error", () =>
|
|
10761
|
+
req2.on("error", () => resolve7(false));
|
|
8833
10762
|
req2.end();
|
|
8834
10763
|
});
|
|
8835
10764
|
}
|
|
8836
10765
|
function freePort() {
|
|
8837
|
-
return new Promise((
|
|
10766
|
+
return new Promise((resolve7, reject) => {
|
|
8838
10767
|
const srv = createServer4();
|
|
8839
10768
|
srv.unref();
|
|
8840
10769
|
srv.on("error", reject);
|
|
8841
10770
|
srv.listen(0, "127.0.0.1", () => {
|
|
8842
10771
|
const addr = srv.address();
|
|
8843
10772
|
const port = typeof addr === "object" && addr ? addr.port : 0;
|
|
8844
|
-
srv.close(() => port ?
|
|
10773
|
+
srv.close(() => port ? resolve7(port) : reject(new Error("no port")));
|
|
8845
10774
|
});
|
|
8846
10775
|
});
|
|
8847
10776
|
}
|
|
8848
10777
|
function startServer(real2, url, env, cwd) {
|
|
8849
10778
|
const logPath2 = appServerLogPath();
|
|
8850
|
-
|
|
10779
|
+
mkdirSync25(deskbuddyHome(), { recursive: true });
|
|
8851
10780
|
try {
|
|
8852
|
-
if (
|
|
10781
|
+
if (statSync12(logPath2).size > MAX_LOG_BYTES2) truncateSync2(logPath2, 0);
|
|
8853
10782
|
} catch {
|
|
8854
10783
|
}
|
|
8855
|
-
const fd =
|
|
10784
|
+
const fd = openSync5(logPath2, "a");
|
|
8856
10785
|
try {
|
|
8857
10786
|
const childEnv2 = { ...env, DESKBUDDY_WRAPPED: "1" };
|
|
8858
10787
|
delete childEnv2.DESKBUDDY_SHIM;
|
|
8859
|
-
const child =
|
|
10788
|
+
const child = spawn6(real2.file, [...real2.prefixArgs, "app-server", "--listen", url], {
|
|
8860
10789
|
detached: true,
|
|
8861
10790
|
stdio: ["ignore", fd, fd],
|
|
8862
10791
|
cwd,
|
|
@@ -8867,7 +10796,7 @@ function startServer(real2, url, env, cwd) {
|
|
|
8867
10796
|
child.unref();
|
|
8868
10797
|
return child.pid ?? null;
|
|
8869
10798
|
} finally {
|
|
8870
|
-
|
|
10799
|
+
closeSync5(fd);
|
|
8871
10800
|
}
|
|
8872
10801
|
}
|
|
8873
10802
|
function killServer(pid) {
|
|
@@ -8881,14 +10810,14 @@ function killOwnServer(pid, url) {
|
|
|
8881
10810
|
if (a && isAppServerArgs(a, url)) killServer(pid);
|
|
8882
10811
|
}
|
|
8883
10812
|
function recordPath(rec) {
|
|
8884
|
-
return
|
|
10813
|
+
return join34(windowsDir2(), `${rec.server}.json`);
|
|
8885
10814
|
}
|
|
8886
10815
|
function writeRecord(rec) {
|
|
8887
10816
|
try {
|
|
8888
|
-
|
|
10817
|
+
mkdirSync25(windowsDir2(), { recursive: true, mode: 448 });
|
|
8889
10818
|
const p = recordPath(rec);
|
|
8890
|
-
|
|
8891
|
-
|
|
10819
|
+
writeFileSync24(p + ".tmp", JSON.stringify(rec), { encoding: "utf8", mode: 384 });
|
|
10820
|
+
renameSync17(p + ".tmp", p);
|
|
8892
10821
|
} catch {
|
|
8893
10822
|
}
|
|
8894
10823
|
}
|
|
@@ -8901,12 +10830,12 @@ function removeRecord(rec) {
|
|
|
8901
10830
|
function reapWindows(dir = windowsDir2(), alive = pidAlive2, args = processArgs, kill = killServer) {
|
|
8902
10831
|
let names = [];
|
|
8903
10832
|
try {
|
|
8904
|
-
names =
|
|
10833
|
+
names = readdirSync8(dir).filter((n) => n.endsWith(".json"));
|
|
8905
10834
|
} catch {
|
|
8906
10835
|
return;
|
|
8907
10836
|
}
|
|
8908
10837
|
for (const n of names) {
|
|
8909
|
-
const p =
|
|
10838
|
+
const p = join34(dir, n);
|
|
8910
10839
|
let rec;
|
|
8911
10840
|
try {
|
|
8912
10841
|
rec = JSON.parse(readFileSync23(p, "utf8"));
|
|
@@ -8928,8 +10857,8 @@ function reapWindows(dir = windowsDir2(), alive = pidAlive2, args = processArgs,
|
|
|
8928
10857
|
}
|
|
8929
10858
|
function launchLog(line) {
|
|
8930
10859
|
try {
|
|
8931
|
-
|
|
8932
|
-
|
|
10860
|
+
mkdirSync25(deskbuddyHome(), { recursive: true });
|
|
10861
|
+
writeFileSync24(join34(deskbuddyHome(), "wrap.log"), `${(/* @__PURE__ */ new Date()).toISOString()} [codex ${process.pid}] ${line}
|
|
8933
10862
|
`, { flag: "a" });
|
|
8934
10863
|
} catch {
|
|
8935
10864
|
}
|
|
@@ -8951,6 +10880,129 @@ function defaultCodexWindowDeps(base) {
|
|
|
8951
10880
|
};
|
|
8952
10881
|
}
|
|
8953
10882
|
|
|
10883
|
+
// src/wrap/promptLaunch.ts
|
|
10884
|
+
import { randomBytes as randomBytes8 } from "crypto";
|
|
10885
|
+
import { homedir as homedir10 } from "os";
|
|
10886
|
+
import { join as join35 } from "path";
|
|
10887
|
+
var FIRST_LINE_WAIT_MS2 = 2 * 60 * 1e3;
|
|
10888
|
+
function promptArgs(text) {
|
|
10889
|
+
return text ? ["--", prepare(text)] : [];
|
|
10890
|
+
}
|
|
10891
|
+
function promptEnv(tool, env) {
|
|
10892
|
+
const out = { ...env };
|
|
10893
|
+
if (tool === "workbuddy" && !out.CODEBUDDY_CONFIG_DIR) out.CODEBUDDY_CONFIG_DIR = join35(homedir10(), ".workbuddy");
|
|
10894
|
+
return out;
|
|
10895
|
+
}
|
|
10896
|
+
async function runPromptLaunch(tool, windowId, cwd, d) {
|
|
10897
|
+
let real2 = null;
|
|
10898
|
+
try {
|
|
10899
|
+
real2 = d.findReal(tool);
|
|
10900
|
+
} catch {
|
|
10901
|
+
real2 = null;
|
|
10902
|
+
}
|
|
10903
|
+
if (!real2) {
|
|
10904
|
+
d.stderr.write(`deskbuddy: \u627E\u4E0D\u5230 ${tool} \u7684\u53EF\u6267\u884C\u6587\u4EF6
|
|
10905
|
+
`);
|
|
10906
|
+
return 127;
|
|
10907
|
+
}
|
|
10908
|
+
let first = null;
|
|
10909
|
+
try {
|
|
10910
|
+
first = await d.receive(windowId, tool, cwd);
|
|
10911
|
+
} catch (e) {
|
|
10912
|
+
d.log(`\u7B49\u9996\u53E5\u51FA\u9519\uFF1A${e.message}`);
|
|
10913
|
+
first = null;
|
|
10914
|
+
}
|
|
10915
|
+
let text = null;
|
|
10916
|
+
if (first) {
|
|
10917
|
+
const { req: req2 } = first;
|
|
10918
|
+
if (req2.expireAt > 0 && d.now() > req2.expireAt) {
|
|
10919
|
+
first.reply({ instructionId: req2.instructionId, status: "rejected", reason: "expired" });
|
|
10920
|
+
} else {
|
|
10921
|
+
text = req2.text;
|
|
10922
|
+
first.reply({ instructionId: req2.instructionId, status: "started", mode: "window" });
|
|
10923
|
+
}
|
|
10924
|
+
} else {
|
|
10925
|
+
d.log(`[${windowId.slice(0, 8)}] \u6CA1\u7B49\u5230\u9996\u53E5\uFF0C\u8D77\u4E00\u4E2A\u7A7A\u7684 ${tool}`);
|
|
10926
|
+
}
|
|
10927
|
+
return d.run(tool, real2, promptArgs(text), promptEnv(tool, d.env));
|
|
10928
|
+
}
|
|
10929
|
+
function receiveFirstLine(windowId, tool, cwd, opts = {}) {
|
|
10930
|
+
const platform = opts.platform ?? process.platform;
|
|
10931
|
+
const sock = channelPath(windowId, platform);
|
|
10932
|
+
const secret = randomBytes8(32);
|
|
10933
|
+
const cleanup = (server) => {
|
|
10934
|
+
closeChannelServer(server, sock, platform);
|
|
10935
|
+
try {
|
|
10936
|
+
removeWindow(windowId);
|
|
10937
|
+
} catch {
|
|
10938
|
+
}
|
|
10939
|
+
};
|
|
10940
|
+
try {
|
|
10941
|
+
writeWindow(windowId, { pid: process.pid, sock, tool, cwd, at: Date.now(), secret: secret.toString("hex"), launched: true });
|
|
10942
|
+
} catch (e) {
|
|
10943
|
+
opts.log?.(`\u767B\u8BB0\u7A97\u53E3\u5931\u8D25\uFF1A${e.message}`);
|
|
10944
|
+
return Promise.resolve(null);
|
|
10945
|
+
}
|
|
10946
|
+
if (platform !== "win32" && !ensureChannelDir(sock)) {
|
|
10947
|
+
cleanup(null);
|
|
10948
|
+
return Promise.resolve(null);
|
|
10949
|
+
}
|
|
10950
|
+
return new Promise((resolve7) => {
|
|
10951
|
+
let server = null;
|
|
10952
|
+
let done = false;
|
|
10953
|
+
let firstId = "";
|
|
10954
|
+
let final = null;
|
|
10955
|
+
const replies = [];
|
|
10956
|
+
const finish = (v) => {
|
|
10957
|
+
if (done) return;
|
|
10958
|
+
done = true;
|
|
10959
|
+
clearTimeout(timer);
|
|
10960
|
+
if (!v) cleanup(server);
|
|
10961
|
+
resolve7(v);
|
|
10962
|
+
};
|
|
10963
|
+
const timer = setTimeout(() => finish(null), opts.waitMs ?? FIRST_LINE_WAIT_MS2);
|
|
10964
|
+
void startChannelServer(
|
|
10965
|
+
sock,
|
|
10966
|
+
secret,
|
|
10967
|
+
(req2, reply) => {
|
|
10968
|
+
if (firstId && req2.instructionId === firstId) {
|
|
10969
|
+
reply({ instructionId: req2.instructionId, status: "ack" });
|
|
10970
|
+
if (final) reply(final);
|
|
10971
|
+
else replies.push(reply);
|
|
10972
|
+
return;
|
|
10973
|
+
}
|
|
10974
|
+
if (done) {
|
|
10975
|
+
reply({ instructionId: req2.instructionId, status: "not_here" });
|
|
10976
|
+
return;
|
|
10977
|
+
}
|
|
10978
|
+
firstId = req2.instructionId;
|
|
10979
|
+
replies.push(reply);
|
|
10980
|
+
reply({ instructionId: req2.instructionId, status: "ack" });
|
|
10981
|
+
finish({
|
|
10982
|
+
req: req2,
|
|
10983
|
+
reply: (r) => {
|
|
10984
|
+
if (final) return;
|
|
10985
|
+
final = r;
|
|
10986
|
+
for (const f of replies.splice(0)) {
|
|
10987
|
+
try {
|
|
10988
|
+
f(r);
|
|
10989
|
+
} catch {
|
|
10990
|
+
}
|
|
10991
|
+
}
|
|
10992
|
+
setTimeout(() => cleanup(server), opts.lingerMs ?? FINAL_LINGER_MS);
|
|
10993
|
+
}
|
|
10994
|
+
});
|
|
10995
|
+
},
|
|
10996
|
+
platform
|
|
10997
|
+
).then((s) => {
|
|
10998
|
+
server = s;
|
|
10999
|
+
if (!s) finish(null);
|
|
11000
|
+
else if (done && !firstId) cleanup(s);
|
|
11001
|
+
});
|
|
11002
|
+
});
|
|
11003
|
+
}
|
|
11004
|
+
var FINAL_LINGER_MS = 5e3;
|
|
11005
|
+
|
|
8954
11006
|
// src/wrap/run.ts
|
|
8955
11007
|
var DEFAULT_TTL_MS = 10 * 60 * 1e3;
|
|
8956
11008
|
var TICK_MS = 300;
|
|
@@ -8969,10 +11021,10 @@ function childEnv(src, windowId) {
|
|
|
8969
11021
|
}
|
|
8970
11022
|
function wrapLog(line) {
|
|
8971
11023
|
try {
|
|
8972
|
-
const p =
|
|
8973
|
-
|
|
11024
|
+
const p = join36(deskbuddyHome(), "wrap.log");
|
|
11025
|
+
mkdirSync26(deskbuddyHome(), { recursive: true });
|
|
8974
11026
|
try {
|
|
8975
|
-
if (
|
|
11027
|
+
if (statSync13(p).size > MAX_LOG_BYTES3) truncateSync3(p, 0);
|
|
8976
11028
|
} catch {
|
|
8977
11029
|
}
|
|
8978
11030
|
appendFileSync2(p, `${(/* @__PURE__ */ new Date()).toISOString()} [${process.pid}] ${line}
|
|
@@ -9057,6 +11109,34 @@ var defaultLaunchDeps = () => ({
|
|
|
9057
11109
|
isDir: isDirSync,
|
|
9058
11110
|
chdir: (p) => process.chdir(p)
|
|
9059
11111
|
});
|
|
11112
|
+
function defaultPromptDeps(deps, cwd) {
|
|
11113
|
+
return {
|
|
11114
|
+
platform: deps.platform,
|
|
11115
|
+
now: () => Date.now(),
|
|
11116
|
+
stderr: deps.stderr,
|
|
11117
|
+
findReal: deps.findReal,
|
|
11118
|
+
receive: (id, tool, c) => receiveFirstLine(id, tool, c, { platform: deps.platform, log: deps.log }),
|
|
11119
|
+
run: (tool, real2, args, env) => {
|
|
11120
|
+
if (tool === "codex") {
|
|
11121
|
+
return runCodexWindow(
|
|
11122
|
+
real2,
|
|
11123
|
+
args,
|
|
11124
|
+
defaultCodexWindowDeps({
|
|
11125
|
+
env,
|
|
11126
|
+
platform: deps.platform,
|
|
11127
|
+
stdinTTY: !!deps.stdin.isTTY,
|
|
11128
|
+
stdoutTTY: !!deps.stdout.isTTY,
|
|
11129
|
+
cwd: () => cwd,
|
|
11130
|
+
runDirect: (r, a) => runDirect(r, a, env)
|
|
11131
|
+
})
|
|
11132
|
+
);
|
|
11133
|
+
}
|
|
11134
|
+
return runDirect(real2, args, env);
|
|
11135
|
+
},
|
|
11136
|
+
env: deps.env,
|
|
11137
|
+
log: deps.log
|
|
11138
|
+
};
|
|
11139
|
+
}
|
|
9060
11140
|
async function runLaunch(argv, deps = defaultWrapDeps(), ld = defaultLaunchDeps()) {
|
|
9061
11141
|
const runId = argv[0] ?? "";
|
|
9062
11142
|
const hi = argv.indexOf("--home");
|
|
@@ -9083,6 +11163,7 @@ async function runLaunch(argv, deps = defaultWrapDeps(), ld = defaultLaunchDeps(
|
|
|
9083
11163
|
return 1;
|
|
9084
11164
|
}
|
|
9085
11165
|
ld.chdir(plan.cwd);
|
|
11166
|
+
if (plan.tool !== "claude_code") return runPromptLaunch(plan.tool, runId, plan.cwd, ld.prompt ?? defaultPromptDeps(deps, plan.cwd));
|
|
9086
11167
|
const args = plan.resume ? ["--resume", plan.resume] : [];
|
|
9087
11168
|
let real2 = null;
|
|
9088
11169
|
try {
|
|
@@ -9112,7 +11193,7 @@ async function wrapInPty(tool, real2, args, deps, opts = {}) {
|
|
|
9112
11193
|
return null;
|
|
9113
11194
|
}
|
|
9114
11195
|
const { stdout: stdout7 } = deps;
|
|
9115
|
-
const windowId = opts.windowId ??
|
|
11196
|
+
const windowId = opts.windowId ?? randomBytes9(16).toString("hex");
|
|
9116
11197
|
const cols = stdout7.columns || 80;
|
|
9117
11198
|
const rows = stdout7.rows || 24;
|
|
9118
11199
|
const term = new Headless({ cols, rows, allowProposedApi: true });
|
|
@@ -9219,7 +11300,7 @@ function runAttached(tool, p, term0, windowId, deps, launched = false) {
|
|
|
9219
11300
|
} catch (e) {
|
|
9220
11301
|
deps.log(`\u63A5\u7BA1\u7EC8\u7AEF\u51FA\u9519\uFF1A${e.message}`);
|
|
9221
11302
|
}
|
|
9222
|
-
const secret =
|
|
11303
|
+
const secret = randomBytes9(32);
|
|
9223
11304
|
try {
|
|
9224
11305
|
writeWindow(windowId, {
|
|
9225
11306
|
pid: process.pid,
|
|
@@ -9260,7 +11341,7 @@ function runAttached(tool, p, term0, windowId, deps, launched = false) {
|
|
|
9260
11341
|
injector.tick().catch(() => {
|
|
9261
11342
|
});
|
|
9262
11343
|
}, TICK_MS);
|
|
9263
|
-
return new Promise((
|
|
11344
|
+
return new Promise((resolve7) => {
|
|
9264
11345
|
let degraded = false;
|
|
9265
11346
|
const onFatal = (e) => {
|
|
9266
11347
|
deps.log(`\u5305\u88C5\u5C42\u5F02\u5E38\uFF1A${e?.stack ?? String(e)}`);
|
|
@@ -9311,8 +11392,8 @@ ${WRAP_HINT}\uFF08\u8BBE\u5907\u56DE\u590D\u6682\u65F6\u5199\u4E0D\u8FDB\u8FD9\u
|
|
|
9311
11392
|
restoreTerminal();
|
|
9312
11393
|
process.removeListener("uncaughtException", onFatal);
|
|
9313
11394
|
for (const [s, h] of sigHandlers) process.removeListener(s, h);
|
|
9314
|
-
if (pending > 0) setTimeout(() =>
|
|
9315
|
-
else
|
|
11395
|
+
if (pending > 0) setTimeout(() => resolve7(code), 150);
|
|
11396
|
+
else resolve7(code);
|
|
9316
11397
|
}
|
|
9317
11398
|
process.on("uncaughtException", onFatal);
|
|
9318
11399
|
for (const [s, h] of sigHandlers) {
|