deskbuddy 0.2.0-beta.36 → 0.2.0-beta.38
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 +134 -60
- package/dist/gate.cjs +68 -3
- package/dist/launcher.cjs +106 -6
- package/hook/gate.cjs +68 -3
- package/hook/hook-debug.log +42 -0
- package/hook/launcher.cjs +106 -6
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -469,9 +469,9 @@ var defaultExec2 = (file, args, opts) => new Promise((resolve8, reject) => {
|
|
|
469
469
|
} else resolve8();
|
|
470
470
|
});
|
|
471
471
|
});
|
|
472
|
-
function ptyCandidatesIn(dir2, platform = process.platform,
|
|
472
|
+
function ptyCandidatesIn(dir2, platform = process.platform, arch2 = process.arch) {
|
|
473
473
|
const pkg = join2(dir2, "node_modules", PACKAGE_NAME);
|
|
474
|
-
return [...hasVendoredPty(platform,
|
|
474
|
+
return [...hasVendoredPty(platform, arch2) ? [join2(pkg, "vendor", "node-pty")] : [], join2(dir2, "node_modules", "node-pty")];
|
|
475
475
|
}
|
|
476
476
|
function ptyLoadsIn(dir2, execPath = process.execPath) {
|
|
477
477
|
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)";
|
|
@@ -797,7 +797,7 @@ var AutoEnabler = class {
|
|
|
797
797
|
import { randomBytes as randomBytes7 } from "crypto";
|
|
798
798
|
import { existsSync as existsSync7, readdirSync as readdirSync11, readFileSync as readFileSync14, statSync as statSync13 } from "fs";
|
|
799
799
|
import { homedir as homedir8 } from "os";
|
|
800
|
-
import { basename as basename9, dirname as dirname9, join as
|
|
800
|
+
import { basename as basename9, dirname as dirname9, join as join28 } from "path";
|
|
801
801
|
import WebSocket2 from "ws";
|
|
802
802
|
|
|
803
803
|
// src/core/transcript/locate.ts
|
|
@@ -6924,10 +6924,10 @@ var SNAPSHOT = `(async () => {${PRELUDE2}
|
|
|
6924
6924
|
if (JSON.stringify(out).length > ${SNAPSHOT_MAX}) return { ok: false, why: 'too_big' };
|
|
6925
6925
|
return out;
|
|
6926
6926
|
})()`;
|
|
6927
|
-
function answerExpr2(sessionId, interruptId,
|
|
6928
|
-
const payload = { type, ...body.tool_use_id ? { tool_use_id: body.tool_use_id } : {}, answers: body.answers, state: body.state };
|
|
6927
|
+
function answerExpr2(sessionId, interruptId, type2, body) {
|
|
6928
|
+
const payload = { type: type2, ...body.tool_use_id ? { tool_use_id: body.tool_use_id } : {}, answers: body.answers, state: body.state };
|
|
6929
6929
|
return `(async () => {${PRELUDE2}
|
|
6930
|
-
const sid = ${JSON.stringify(sessionId)}, iid = ${JSON.stringify(interruptId)}, want = ${JSON.stringify(
|
|
6930
|
+
const sid = ${JSON.stringify(sessionId)}, iid = ${JSON.stringify(interruptId)}, want = ${JSON.stringify(type2)};
|
|
6931
6931
|
const payload = ${JSON.stringify(payload)};
|
|
6932
6932
|
const list = (S().agentInterruptCards || {})[sid] || [];
|
|
6933
6933
|
const c = list.find((x) => x && String(x.interruptId) === iid);
|
|
@@ -13108,11 +13108,11 @@ var CodexWatch = class {
|
|
|
13108
13108
|
}
|
|
13109
13109
|
/** 会话状态变了(广播 / 补查):active 订阅,其余退订 */
|
|
13110
13110
|
#applyStatus(id, status) {
|
|
13111
|
-
const
|
|
13112
|
-
if (typeof
|
|
13111
|
+
const type2 = status.type;
|
|
13112
|
+
if (typeof type2 !== "string") return;
|
|
13113
13113
|
const prev = this.#status.get(id);
|
|
13114
|
-
this.#status.set(id,
|
|
13115
|
-
if (
|
|
13114
|
+
this.#status.set(id, type2);
|
|
13115
|
+
if (type2 === "idle" && prev !== "idle") {
|
|
13116
13116
|
try {
|
|
13117
13117
|
this.#d.onIdle?.(id);
|
|
13118
13118
|
} catch {
|
|
@@ -13121,7 +13121,7 @@ var CodexWatch = class {
|
|
|
13121
13121
|
const flags = Array.isArray(status.activeFlags) ? status.activeFlags : [];
|
|
13122
13122
|
if (flags.includes("waitingOnApproval")) this.#waiting.add(id);
|
|
13123
13123
|
else this.#waiting.delete(id);
|
|
13124
|
-
if (
|
|
13124
|
+
if (type2 === "active") {
|
|
13125
13125
|
this.#idle.delete(id);
|
|
13126
13126
|
this.#subscribe(id);
|
|
13127
13127
|
} else {
|
|
@@ -13220,8 +13220,8 @@ var CodexWatch = class {
|
|
|
13220
13220
|
};
|
|
13221
13221
|
this.#own.set(tid, (this.#own.get(tid) ?? 0) + 1);
|
|
13222
13222
|
const prev = this.#chain.get(tid);
|
|
13223
|
-
let
|
|
13224
|
-
const mine = new Promise((r) =>
|
|
13223
|
+
let release2;
|
|
13224
|
+
const mine = new Promise((r) => release2 = r);
|
|
13225
13225
|
const tail4 = (prev ?? Promise.resolve()).then(() => mine);
|
|
13226
13226
|
this.#chain.set(tid, tail4);
|
|
13227
13227
|
try {
|
|
@@ -13234,7 +13234,7 @@ var CodexWatch = class {
|
|
|
13234
13234
|
this.#d.log(`Codex \u53D1\u4E00\u8F6E\u51FA\u9519\uFF1A${e.message}`);
|
|
13235
13235
|
return { sent: false, reason: "start_failed" };
|
|
13236
13236
|
} finally {
|
|
13237
|
-
|
|
13237
|
+
release2();
|
|
13238
13238
|
if (this.#chain.get(tid) === tail4) this.#chain.delete(tid);
|
|
13239
13239
|
const n = (this.#own.get(tid) ?? 1) - 1;
|
|
13240
13240
|
if (n > 0) this.#own.set(tid, n);
|
|
@@ -13425,11 +13425,11 @@ var CodexWatch = class {
|
|
|
13425
13425
|
case "thread/status/changed": {
|
|
13426
13426
|
const id = typeof p.threadId === "string" ? p.threadId : "";
|
|
13427
13427
|
const status = p.status ?? {};
|
|
13428
|
-
const
|
|
13428
|
+
const type2 = status.type;
|
|
13429
13429
|
if (!id) return;
|
|
13430
|
-
if (
|
|
13431
|
-
else if (typeof
|
|
13432
|
-
if (typeof
|
|
13430
|
+
if (type2 === "notLoaded") this.#setLoaded(id, false);
|
|
13431
|
+
else if (typeof type2 === "string") this.#setLoaded(id, true);
|
|
13432
|
+
if (typeof type2 === "string") this.#applyStatus(id, status);
|
|
13433
13433
|
else {
|
|
13434
13434
|
this.#idle.add(id);
|
|
13435
13435
|
this.#unsubscribe(id);
|
|
@@ -13852,6 +13852,7 @@ function removeCodexRemote() {
|
|
|
13852
13852
|
import { closeSync as closeSync8, openSync as openSync8, readdirSync as readdirSync9, readSync as readSync7, statSync as statSync11 } from "fs";
|
|
13853
13853
|
import { dirname as dirname6, join as join22 } from "path";
|
|
13854
13854
|
var INTERRUPT_TOOLS = ["claude_code", "qoder_cn", "qoder"];
|
|
13855
|
+
var CODEX_INTERRUPT_TOOL = "codex";
|
|
13855
13856
|
function transcriptHome(tool) {
|
|
13856
13857
|
if (tool === "claude_code") return claudeHome();
|
|
13857
13858
|
if (tool === "qoder_cn") return dirname6(qoderCnSettingsPath());
|
|
@@ -13974,14 +13975,53 @@ function claudeTranscriptFile(sessionId, cwd, home = claudeHome()) {
|
|
|
13974
13975
|
var INTERRUPT_SETTLE_MS = 2e3;
|
|
13975
13976
|
var INTERRUPT_TOLERANCE_MS = 5e3;
|
|
13976
13977
|
function shouldSettleInterrupt(st, markerAt, now) {
|
|
13977
|
-
|
|
13978
|
+
const codex = st.tool === CODEX_INTERRUPT_TOOL;
|
|
13979
|
+
if (markerAt === null || !INTERRUPT_TOOLS.includes(st.tool) && !codex) return false;
|
|
13978
13980
|
if (st.state !== "watch" && st.state !== "ask_approval") return false;
|
|
13979
|
-
if (st.interactive !== true && st.embedded !== true) return false;
|
|
13981
|
+
if (!codex && st.interactive !== true && st.embedded !== true) return false;
|
|
13980
13982
|
if (now - markerAt < INTERRUPT_SETTLE_MS) return false;
|
|
13981
13983
|
if (st.updatedAt > markerAt + INTERRUPT_TOLERANCE_MS) return false;
|
|
13982
13984
|
if ((st.recent ?? []).some((r) => r.e === "UserPromptSubmit" && r.at > markerAt)) return false;
|
|
13983
13985
|
return true;
|
|
13984
13986
|
}
|
|
13987
|
+
function codexInterruptedAt(file) {
|
|
13988
|
+
let fd;
|
|
13989
|
+
try {
|
|
13990
|
+
const st = statSync11(file);
|
|
13991
|
+
const len = Math.min(st.size, TAIL);
|
|
13992
|
+
const buf = Buffer.alloc(len);
|
|
13993
|
+
fd = openSync8(file, "r");
|
|
13994
|
+
readSync7(fd, buf, 0, len, st.size - len);
|
|
13995
|
+
const lines3 = buf.toString("utf8").split("\n");
|
|
13996
|
+
for (let i = lines3.length - 1; i >= 0; i--) {
|
|
13997
|
+
const line = lines3[i];
|
|
13998
|
+
if (line.indexOf('"event_msg"') === -1) continue;
|
|
13999
|
+
let o;
|
|
14000
|
+
try {
|
|
14001
|
+
o = JSON.parse(line);
|
|
14002
|
+
} catch {
|
|
14003
|
+
continue;
|
|
14004
|
+
}
|
|
14005
|
+
if (!o || o.type !== "event_msg" || !o.payload) continue;
|
|
14006
|
+
const t = o.payload.type;
|
|
14007
|
+
if (t === "turn_aborted") {
|
|
14008
|
+
const at = typeof o.timestamp === "string" ? Date.parse(o.timestamp) : NaN;
|
|
14009
|
+
return Number.isFinite(at) ? at : st.mtimeMs;
|
|
14010
|
+
}
|
|
14011
|
+
if (t === "task_complete" || t === "turn_complete" || t === "task_started" || t === "turn_started" || t === "user_message") return null;
|
|
14012
|
+
}
|
|
14013
|
+
return null;
|
|
14014
|
+
} catch {
|
|
14015
|
+
return null;
|
|
14016
|
+
} finally {
|
|
14017
|
+
if (fd !== void 0) {
|
|
14018
|
+
try {
|
|
14019
|
+
closeSync8(fd);
|
|
14020
|
+
} catch {
|
|
14021
|
+
}
|
|
14022
|
+
}
|
|
14023
|
+
}
|
|
14024
|
+
}
|
|
13985
14025
|
|
|
13986
14026
|
// src/daemon/qoderAppDb.ts
|
|
13987
14027
|
import { execFile as execFile4 } from "child_process";
|
|
@@ -14246,9 +14286,37 @@ function describe2(r) {
|
|
|
14246
14286
|
return [r.status, r.n !== void 0 ? `n=${r.n}` : "", r.detail ?? ""].filter(Boolean).join(" ");
|
|
14247
14287
|
}
|
|
14248
14288
|
|
|
14289
|
+
// src/daemon/health.ts
|
|
14290
|
+
import { arch, release, type } from "os";
|
|
14291
|
+
import { join as join24 } from "path";
|
|
14292
|
+
var MEM_LOG_MS = 30 * 60 * 1e3;
|
|
14293
|
+
function envLine() {
|
|
14294
|
+
return `\u8FD0\u884C\u73AF\u5883\uFF1A${type()} ${release()} ${arch()}\uFF1BNode ${process.version}\uFF1BDeskBuddy ${packageVersion()}`;
|
|
14295
|
+
}
|
|
14296
|
+
var mb = (n) => Math.round(n / 1024 / 1024);
|
|
14297
|
+
function memLine() {
|
|
14298
|
+
const m = process.memoryUsage();
|
|
14299
|
+
let handles = -1;
|
|
14300
|
+
try {
|
|
14301
|
+
handles = process.getActiveResourcesInfo().length;
|
|
14302
|
+
} catch {
|
|
14303
|
+
}
|
|
14304
|
+
return `\u5185\u5B58\uFF1Arss ${mb(m.rss)}MB\uFF0C\u5806 ${mb(m.heapUsed)}/${mb(m.heapTotal)}MB\uFF0Cexternal ${mb(m.external)}MB\uFF0CarrayBuffers ${mb(m.arrayBuffers)}MB\uFF1B\u6D3B\u52A8\u53E5\u67C4 ${handles}`;
|
|
14305
|
+
}
|
|
14306
|
+
function enableFatalReport(home) {
|
|
14307
|
+
try {
|
|
14308
|
+
const r = process.report;
|
|
14309
|
+
if (!r) return;
|
|
14310
|
+
r.directory = join24(home, "reports");
|
|
14311
|
+
r.reportOnFatalError = true;
|
|
14312
|
+
r.compact = true;
|
|
14313
|
+
} catch {
|
|
14314
|
+
}
|
|
14315
|
+
}
|
|
14316
|
+
|
|
14249
14317
|
// src/daemon/dshVersion.ts
|
|
14250
14318
|
import { readFileSync as readFileSync11, realpathSync as realpathSync5 } from "fs";
|
|
14251
|
-
import { dirname as dirname8, join as
|
|
14319
|
+
import { dirname as dirname8, join as join25 } from "path";
|
|
14252
14320
|
var DSH_HEADLESS_MIN_VERSION = "0.0.1-rc.1";
|
|
14253
14321
|
var DSH_PKG = "@deepseek-ai/dsh";
|
|
14254
14322
|
var DSH_VERSION_UNSUPPORTED = "tool_version_unsupported";
|
|
@@ -14280,7 +14348,7 @@ function dshSupport(d = defaultDeps2) {
|
|
|
14280
14348
|
}
|
|
14281
14349
|
let dir2 = dirname8(real2);
|
|
14282
14350
|
for (let i = 0; i < 4; i++) {
|
|
14283
|
-
const raw = d.readFile(
|
|
14351
|
+
const raw = d.readFile(join25(dir2, "package.json"));
|
|
14284
14352
|
if (raw !== null) {
|
|
14285
14353
|
let pkg = {};
|
|
14286
14354
|
try {
|
|
@@ -14798,17 +14866,17 @@ function startWakeServer(path, hub, secret, platform = process.platform) {
|
|
|
14798
14866
|
|
|
14799
14867
|
// src/core/appended.ts
|
|
14800
14868
|
import { chmodSync as chmodSync4, mkdirSync as mkdirSync11, readFileSync as readFileSync12, renameSync as renameSync8, rmSync as rmSync9, writeFileSync as writeFileSync10 } from "fs";
|
|
14801
|
-
import { join as
|
|
14869
|
+
import { join as join26 } from "path";
|
|
14802
14870
|
var APPENDED_MAX_ITEMS = 5;
|
|
14803
14871
|
var APPENDED_TEXT_MAX = 500;
|
|
14804
14872
|
var APPENDED_RESULT_MAX = 800;
|
|
14805
14873
|
var APPENDED_TTL_MS = 7 * 24 * 60 * 60 * 1e3;
|
|
14806
14874
|
function appendedDir() {
|
|
14807
|
-
return
|
|
14875
|
+
return join26(deskbuddyHome(), "appended");
|
|
14808
14876
|
}
|
|
14809
14877
|
function appendedPath(sessionId) {
|
|
14810
14878
|
const safe2 = sessionId.replace(/[^A-Za-z0-9_-]/g, "");
|
|
14811
|
-
return safe2 ?
|
|
14879
|
+
return safe2 ? join26(appendedDir(), `${safe2}.json`) : null;
|
|
14812
14880
|
}
|
|
14813
14881
|
function readAppended(sessionId) {
|
|
14814
14882
|
const file = appendedPath(sessionId);
|
|
@@ -14838,13 +14906,13 @@ function writeAppended(sessionId, item) {
|
|
|
14838
14906
|
var RUNNING_TTL_MS = 60 * 60 * 1e3;
|
|
14839
14907
|
function runningPath(sessionId) {
|
|
14840
14908
|
const safe2 = sessionId.replace(/[^A-Za-z0-9_-]/g, "");
|
|
14841
|
-
return safe2 ?
|
|
14909
|
+
return safe2 ? join26(deskbuddyHome(), "running", `${safe2}.json`) : null;
|
|
14842
14910
|
}
|
|
14843
14911
|
function writeRunning(sessionId, now = Date.now()) {
|
|
14844
14912
|
const file = runningPath(sessionId);
|
|
14845
14913
|
if (!file) return;
|
|
14846
14914
|
try {
|
|
14847
|
-
mkdirSync11(
|
|
14915
|
+
mkdirSync11(join26(deskbuddyHome(), "running"), { recursive: true, mode: 448 });
|
|
14848
14916
|
writeFileSync10(`${file}.tmp`, JSON.stringify({ at: now, pid: process.pid }), { encoding: "utf8", mode: 384 });
|
|
14849
14917
|
renameSync8(`${file}.tmp`, file);
|
|
14850
14918
|
} catch {
|
|
@@ -14892,7 +14960,7 @@ function resultSummary(tool, stdout6) {
|
|
|
14892
14960
|
|
|
14893
14961
|
// src/core/dshMailbox.ts
|
|
14894
14962
|
import { chmodSync as chmodSync5, existsSync as existsSync6, mkdirSync as mkdirSync12, readdirSync as readdirSync10, readFileSync as readFileSync13, renameSync as renameSync9, rmSync as rmSync10, writeFileSync as writeFileSync11 } from "fs";
|
|
14895
|
-
import { join as
|
|
14963
|
+
import { join as join27 } from "path";
|
|
14896
14964
|
var DSH_HEARTBEAT_FRESH_MS = 5 * 1e3;
|
|
14897
14965
|
var DSH_ACK_TIMEOUT_MS = 8 * 1e3;
|
|
14898
14966
|
var ACK_POLL_MS = 200;
|
|
@@ -14903,10 +14971,10 @@ var REVOKE_RETRY_MS = 50;
|
|
|
14903
14971
|
var SID_RE2 = /^[A-Za-z0-9_-]{1,128}$/;
|
|
14904
14972
|
var IID_RE = /^[A-Za-z0-9_-]{1,128}$/;
|
|
14905
14973
|
function dshMailboxDir(home = deskbuddyHome(), channel = "dsh") {
|
|
14906
|
-
return
|
|
14974
|
+
return join27(home, channel);
|
|
14907
14975
|
}
|
|
14908
14976
|
function sub(home, name, channel = "dsh") {
|
|
14909
|
-
return
|
|
14977
|
+
return join27(dshMailboxDir(home, channel), name);
|
|
14910
14978
|
}
|
|
14911
14979
|
function ensureDir2(p) {
|
|
14912
14980
|
mkdirSync12(p, { recursive: true, mode: 448 });
|
|
@@ -14946,7 +15014,7 @@ function liveHeartbeats(sessionId, d0 = {}) {
|
|
|
14946
15014
|
if (!/^\d+\.json$/.test(n)) continue;
|
|
14947
15015
|
let hb;
|
|
14948
15016
|
try {
|
|
14949
|
-
hb = JSON.parse(readFileSync13(
|
|
15017
|
+
hb = JSON.parse(readFileSync13(join27(dir2, n), "utf8"));
|
|
14950
15018
|
} catch {
|
|
14951
15019
|
continue;
|
|
14952
15020
|
}
|
|
@@ -14970,8 +15038,8 @@ async function dshDeliver(sessionId, msg, d0 = {}) {
|
|
|
14970
15038
|
const ackDir = sub(d.home, "ack", d.channel);
|
|
14971
15039
|
ensureDir2(inbox);
|
|
14972
15040
|
ensureDir2(ackDir);
|
|
14973
|
-
const letter =
|
|
14974
|
-
const ackPath =
|
|
15041
|
+
const letter = join27(inbox, `${sessionId}.${msg.instructionId}.json`);
|
|
15042
|
+
const ackPath = join27(ackDir, `${msg.instructionId}.json`);
|
|
14975
15043
|
if (existsSync6(letter)) return { ok: false, reason: "inbox_busy" };
|
|
14976
15044
|
const writtenAt = d.now();
|
|
14977
15045
|
writeJson(letter, {
|
|
@@ -15040,7 +15108,7 @@ function dshClearInbox(home = deskbuddyHome(), channel = "dsh") {
|
|
|
15040
15108
|
var DSH_REPLY_WAIT_MS = 4 * 60 * 1e3;
|
|
15041
15109
|
function takeMailboxReply(instructionId, home = deskbuddyHome(), channel = "dsh") {
|
|
15042
15110
|
if (!IID_RE.test(instructionId)) return null;
|
|
15043
|
-
const p =
|
|
15111
|
+
const p = join27(sub(home, "reply", channel), `${instructionId}.json`);
|
|
15044
15112
|
let v = null;
|
|
15045
15113
|
try {
|
|
15046
15114
|
v = JSON.parse(readFileSync13(p, "utf8"));
|
|
@@ -15069,7 +15137,7 @@ function clearDir(inbox) {
|
|
|
15069
15137
|
let n = 0;
|
|
15070
15138
|
for (const f of names) {
|
|
15071
15139
|
try {
|
|
15072
|
-
rmSync10(
|
|
15140
|
+
rmSync10(join27(inbox, f), { force: true });
|
|
15073
15141
|
n++;
|
|
15074
15142
|
} catch {
|
|
15075
15143
|
}
|
|
@@ -15144,7 +15212,7 @@ function defaultCodexAppServers() {
|
|
|
15144
15212
|
}
|
|
15145
15213
|
function opencodeReady() {
|
|
15146
15214
|
const mod = ocPluginModule(installedHookPath());
|
|
15147
|
-
return isOcPluginInstalled(opencodePluginPath()) && existsSync7(mod) && existsSync7(
|
|
15215
|
+
return isOcPluginInstalled(opencodePluginPath()) && existsSync7(mod) && existsSync7(join28(dirname9(mod), "ocTranslate.mjs"));
|
|
15148
15216
|
}
|
|
15149
15217
|
var INTERNAL_PROMPT_MARK = "[DeskBuddy]";
|
|
15150
15218
|
function orphanResumeText(summary2) {
|
|
@@ -15211,6 +15279,7 @@ var Daemon = class {
|
|
|
15211
15279
|
#ws = null;
|
|
15212
15280
|
#idleTimer = null;
|
|
15213
15281
|
#windowScan = null;
|
|
15282
|
+
#memLog = null;
|
|
15214
15283
|
#aliveReport = null;
|
|
15215
15284
|
#reportRetryTimer = null;
|
|
15216
15285
|
#reportRetry = new ReportRetry({
|
|
@@ -15564,6 +15633,10 @@ var Daemon = class {
|
|
|
15564
15633
|
async startGate() {
|
|
15565
15634
|
const port = await this.#gate.start();
|
|
15566
15635
|
log(`\u6388\u6743\u7AEF\u70B9\u5DF2\u5C31\u7EEA 127.0.0.1:${port}`);
|
|
15636
|
+
enableFatalReport(deskbuddyHome());
|
|
15637
|
+
log(`${envLine()}\uFF1B${memLine()}`);
|
|
15638
|
+
this.#memLog = setInterval(() => log(memLine()), MEM_LOG_MS);
|
|
15639
|
+
this.#memLog.unref();
|
|
15567
15640
|
this.#windowScan = setInterval(() => this.#coord.scanWindows(), 1e3);
|
|
15568
15641
|
this.#windowScan.unref();
|
|
15569
15642
|
this.#aliveReport = setInterval(() => void this.#reportAlive(), ALIVE_REPORT_MS);
|
|
@@ -15752,7 +15825,7 @@ var Daemon = class {
|
|
|
15752
15825
|
#settleInterrupted() {
|
|
15753
15826
|
if (this.#stopped) return;
|
|
15754
15827
|
const enabled = loadConfig().enabledTools;
|
|
15755
|
-
const tools = INTERRUPT_TOOLS.filter((t) => enabled.includes(t));
|
|
15828
|
+
const tools = [...INTERRUPT_TOOLS, CODEX_INTERRUPT_TOOL].filter((t) => enabled.includes(t));
|
|
15756
15829
|
if (tools.length === 0) return;
|
|
15757
15830
|
const now = this.#deps.now();
|
|
15758
15831
|
const alive = /* @__PURE__ */ new Set();
|
|
@@ -15760,7 +15833,7 @@ var Daemon = class {
|
|
|
15760
15833
|
let changed = false;
|
|
15761
15834
|
for (const st of this.#aliveStates(tool)) {
|
|
15762
15835
|
alive.add(st.sessionId);
|
|
15763
|
-
if (!BUSY_STATES3.has(st.state) || st.interactive !== true && st.embedded !== true) {
|
|
15836
|
+
if (!BUSY_STATES3.has(st.state) || tool !== CODEX_INTERRUPT_TOOL && st.interactive !== true && st.embedded !== true) {
|
|
15764
15837
|
this.#transcriptSeen.delete(st.sessionId);
|
|
15765
15838
|
continue;
|
|
15766
15839
|
}
|
|
@@ -15772,16 +15845,16 @@ var Daemon = class {
|
|
|
15772
15845
|
if (!file) continue;
|
|
15773
15846
|
const sig = transcriptSig(file);
|
|
15774
15847
|
if (sig && this.#transcriptSeen.get(st.sessionId) === sig) continue;
|
|
15775
|
-
const at = interruptedAt(file);
|
|
15848
|
+
const at = tool === CODEX_INTERRUPT_TOOL ? codexInterruptedAt(file) : interruptedAt(file);
|
|
15776
15849
|
if (sig && (at === null || now - at >= INTERRUPT_SETTLE_MS)) this.#transcriptSeen.set(st.sessionId, sig);
|
|
15777
15850
|
if (at === null || !shouldSettleInterrupt(st, at, now)) continue;
|
|
15778
|
-
if (this.#settleBusy(st.sessionId, tool, st.updatedAt,
|
|
15851
|
+
if (this.#settleBusy(st.sessionId, tool, st.updatedAt, `${tool === CODEX_INTERRUPT_TOOL ? "\u88AB\u7528\u6237\u505C\u6B62" : "\u5728\u7A97\u53E3\u91CC\u88AB\u6253\u65AD"}\uFF08${tool} \u4E0D\u53D1 Stop\uFF09`)) {
|
|
15779
15852
|
changed = true;
|
|
15780
15853
|
this.#idleWindowAfterInterrupt(st.sessionId, tool, at);
|
|
15781
15854
|
this.#coord.settleInterruptedWindows(st.sessionId, at);
|
|
15782
15855
|
}
|
|
15783
15856
|
}
|
|
15784
|
-
if (changed) void this.#reportTool(tool);
|
|
15857
|
+
if (changed) void (tool === CODEX_INTERRUPT_TOOL ? this.#reportCodex() : this.#reportTool(tool));
|
|
15785
15858
|
}
|
|
15786
15859
|
for (const m of [this.#transcripts, this.#transcriptMiss, this.#transcriptSeen]) for (const k of [...m.keys()]) if (!alive.has(k)) m.delete(k);
|
|
15787
15860
|
}
|
|
@@ -15802,7 +15875,7 @@ var Daemon = class {
|
|
|
15802
15875
|
const missAt = this.#transcriptMiss.get(st.sessionId);
|
|
15803
15876
|
if (missAt !== void 0 && now - missAt < TRANSCRIPT_MISS_TTL_MS) return null;
|
|
15804
15877
|
const home = transcriptHome(st.tool);
|
|
15805
|
-
const f = home ? claudeTranscriptFile(st.sessionId, st.cwd, home) : null;
|
|
15878
|
+
const f = st.tool === CODEX_INTERRUPT_TOOL ? findCodexRollout(codexHome(), st.sessionId, 7) : home ? claudeTranscriptFile(st.sessionId, st.cwd, home) : null;
|
|
15806
15879
|
if (f) {
|
|
15807
15880
|
this.#transcripts.set(st.sessionId, f);
|
|
15808
15881
|
this.#transcriptMiss.delete(st.sessionId);
|
|
@@ -15962,6 +16035,7 @@ var Daemon = class {
|
|
|
15962
16035
|
this.#clearIdle();
|
|
15963
16036
|
if (this.#windowScan) clearInterval(this.#windowScan);
|
|
15964
16037
|
if (this.#aliveReport) clearInterval(this.#aliveReport);
|
|
16038
|
+
if (this.#memLog) clearInterval(this.#memLog);
|
|
15965
16039
|
if (this.#reportRetryTimer) clearInterval(this.#reportRetryTimer);
|
|
15966
16040
|
if (this.#codexTimer) clearInterval(this.#codexTimer);
|
|
15967
16041
|
if (this.#wakeTimer) clearInterval(this.#wakeTimer);
|
|
@@ -16542,8 +16616,8 @@ var Daemon = class {
|
|
|
16542
16616
|
}
|
|
16543
16617
|
const sid = resolveAlias(cmd.sessionId);
|
|
16544
16618
|
for (let p = this.#opening.get(sid); p; p = this.#opening.get(sid)) await p;
|
|
16545
|
-
let
|
|
16546
|
-
this.#opening.set(sid, new Promise((r) =>
|
|
16619
|
+
let release2;
|
|
16620
|
+
this.#opening.set(sid, new Promise((r) => release2 = r));
|
|
16547
16621
|
try {
|
|
16548
16622
|
if (this.#queue.busy(sid)) await this.#queue.enqueue(sid, async () => {
|
|
16549
16623
|
});
|
|
@@ -16565,7 +16639,7 @@ var Daemon = class {
|
|
|
16565
16639
|
await this.#openWindowFor(cmd, sid, r.cwd);
|
|
16566
16640
|
} finally {
|
|
16567
16641
|
this.#opening.delete(sid);
|
|
16568
|
-
|
|
16642
|
+
release2();
|
|
16569
16643
|
}
|
|
16570
16644
|
}
|
|
16571
16645
|
#guiAvailable() {
|
|
@@ -17004,7 +17078,7 @@ var Daemon = class {
|
|
|
17004
17078
|
for (const n of names) {
|
|
17005
17079
|
if (!n.endsWith(".json")) continue;
|
|
17006
17080
|
try {
|
|
17007
|
-
const s2 = JSON.parse(readFileSync14(
|
|
17081
|
+
const s2 = JSON.parse(readFileSync14(join28(stateDir(), n), "utf8"));
|
|
17008
17082
|
if (typeof s2.sessionId === "string" && typeof s2.tool === "string" && enabled.has(s2.tool)) out.push({ tool: s2.tool, sessionId: s2.sessionId, state: typeof s2.state === "string" ? s2.state : "", cwd: typeof s2.cwd === "string" ? s2.cwd : "" });
|
|
17009
17083
|
} catch {
|
|
17010
17084
|
}
|
|
@@ -17038,12 +17112,12 @@ var Daemon = class {
|
|
|
17038
17112
|
}
|
|
17039
17113
|
case "deepseek_harness": {
|
|
17040
17114
|
if (!TRANSCRIPT_SID_RE.test(s2.sessionId)) return null;
|
|
17041
|
-
const p =
|
|
17115
|
+
const p = join28(deskbuddyHome(), "dsh", "transcript", `${s2.sessionId}.jsonl`);
|
|
17042
17116
|
return existsSync7(p) ? { kind: "file", path: p, format: "dsh" } : null;
|
|
17043
17117
|
}
|
|
17044
17118
|
case "opencode": {
|
|
17045
17119
|
if (!TRANSCRIPT_SID_RE.test(s2.sessionId)) return null;
|
|
17046
|
-
return snapshotSource(`opencode-snap:${s2.sessionId}`,
|
|
17120
|
+
return snapshotSource(`opencode-snap:${s2.sessionId}`, join28(deskbuddyHome(), "opencode", "transcript", `${s2.sessionId}.json`));
|
|
17047
17121
|
}
|
|
17048
17122
|
case "dumate":
|
|
17049
17123
|
return { kind: "http", key: "dumate", fetch: () => this.#dumate.transcriptMessages(s2.sessionId) };
|
|
@@ -17991,7 +18065,7 @@ async function runDisable(tools, opts = {}) {
|
|
|
17991
18065
|
|
|
17992
18066
|
// src/commands/install.ts
|
|
17993
18067
|
import { mkdirSync as mkdirSync13, rmSync as rmSync12, writeFileSync as writeFileSync13 } from "fs";
|
|
17994
|
-
import { join as
|
|
18068
|
+
import { join as join29 } from "path";
|
|
17995
18069
|
|
|
17996
18070
|
// src/commands/setup.ts
|
|
17997
18071
|
import qrcode from "qrcode-terminal";
|
|
@@ -18208,7 +18282,7 @@ function inSandbox(env) {
|
|
|
18208
18282
|
}
|
|
18209
18283
|
function precheckWritable(dirs) {
|
|
18210
18284
|
for (const dir2 of dirs) {
|
|
18211
|
-
const probe =
|
|
18285
|
+
const probe = join29(dir2, `.deskbuddy-probe-${process.pid}`);
|
|
18212
18286
|
try {
|
|
18213
18287
|
mkdirSync13(dir2, { recursive: true });
|
|
18214
18288
|
writeFileSync13(probe, "");
|
|
@@ -18618,7 +18692,7 @@ async function runAutoSwitch(opts) {
|
|
|
18618
18692
|
// src/wrap/run.ts
|
|
18619
18693
|
import { randomBytes as randomBytes10 } from "crypto";
|
|
18620
18694
|
import { appendFileSync as appendFileSync2, mkdirSync as mkdirSync15, statSync as statSync16, truncateSync as truncateSync3 } from "fs";
|
|
18621
|
-
import { join as
|
|
18695
|
+
import { join as join32 } from "path";
|
|
18622
18696
|
import { StringDecoder as StringDecoder2 } from "string_decoder";
|
|
18623
18697
|
|
|
18624
18698
|
// src/wrap/injector.ts
|
|
@@ -19118,7 +19192,7 @@ import { execFileSync as execFileSync4, spawn as spawn4 } from "child_process";
|
|
|
19118
19192
|
import { closeSync as closeSync9, mkdirSync as mkdirSync14, openSync as openSync9, readdirSync as readdirSync12, readFileSync as readFileSync16, rmSync as rmSync14, statSync as statSync15, truncateSync as truncateSync2, writeFileSync as writeFileSync14, renameSync as renameSync10 } from "fs";
|
|
19119
19193
|
import { request as request2 } from "http";
|
|
19120
19194
|
import { createServer as createServer5 } from "net";
|
|
19121
|
-
import { join as
|
|
19195
|
+
import { join as join30 } from "path";
|
|
19122
19196
|
var TAKEOVER_VALUE_OPTS = /* @__PURE__ */ new Set(["-c", "--config", "-m", "--model", "-s", "--sandbox", "-a", "--ask-for-approval", "-C", "--cd", "--enable", "--disable"]);
|
|
19123
19197
|
var SHORT_VALUE_OPTS = /* @__PURE__ */ new Set(["-c", "-m", "-s", "-a", "-C"]);
|
|
19124
19198
|
var TAKEOVER_FLAGS = /* @__PURE__ */ new Set(["--search", "--no-alt-screen", "--dangerously-bypass-approvals-and-sandbox"]);
|
|
@@ -19287,10 +19361,10 @@ async function runCodexWindow(real2, args, d) {
|
|
|
19287
19361
|
}
|
|
19288
19362
|
var MAX_LOG_BYTES2 = 1024 * 1024;
|
|
19289
19363
|
function appServerLogPath() {
|
|
19290
|
-
return
|
|
19364
|
+
return join30(deskbuddyHome(), "codex-app-server.log");
|
|
19291
19365
|
}
|
|
19292
19366
|
function windowsDir2() {
|
|
19293
|
-
return
|
|
19367
|
+
return join30(deskbuddyHome(), "codex-windows");
|
|
19294
19368
|
}
|
|
19295
19369
|
function pidAlive5(pid) {
|
|
19296
19370
|
try {
|
|
@@ -19380,7 +19454,7 @@ function killOwnServer(pid, url, args = freshArgs, kill = (p) => killServer(p))
|
|
|
19380
19454
|
if (a && isAppServerArgs(a, url)) kill(pid);
|
|
19381
19455
|
}
|
|
19382
19456
|
function recordPath(rec) {
|
|
19383
|
-
return
|
|
19457
|
+
return join30(windowsDir2(), `${rec.server}.json`);
|
|
19384
19458
|
}
|
|
19385
19459
|
function writeRecord(rec) {
|
|
19386
19460
|
try {
|
|
@@ -19405,7 +19479,7 @@ function reapWindows(dir2 = windowsDir2(), alive = pidAlive5, args = freshArgs,
|
|
|
19405
19479
|
return;
|
|
19406
19480
|
}
|
|
19407
19481
|
for (const n of names) {
|
|
19408
|
-
const p =
|
|
19482
|
+
const p = join30(dir2, n);
|
|
19409
19483
|
let rec;
|
|
19410
19484
|
try {
|
|
19411
19485
|
rec = JSON.parse(readFileSync16(p, "utf8"));
|
|
@@ -19428,7 +19502,7 @@ function reapWindows(dir2 = windowsDir2(), alive = pidAlive5, args = freshArgs,
|
|
|
19428
19502
|
function launchLog(line) {
|
|
19429
19503
|
try {
|
|
19430
19504
|
mkdirSync14(deskbuddyHome(), { recursive: true });
|
|
19431
|
-
writeFileSync14(
|
|
19505
|
+
writeFileSync14(join30(deskbuddyHome(), "wrap.log"), `${(/* @__PURE__ */ new Date()).toISOString()} [codex ${process.pid}] ${line}
|
|
19432
19506
|
`, { flag: "a" });
|
|
19433
19507
|
} catch {
|
|
19434
19508
|
}
|
|
@@ -19453,7 +19527,7 @@ function defaultCodexWindowDeps(base) {
|
|
|
19453
19527
|
// src/wrap/promptLaunch.ts
|
|
19454
19528
|
import { randomBytes as randomBytes8, randomUUID as randomUUID3 } from "crypto";
|
|
19455
19529
|
import { homedir as homedir10 } from "os";
|
|
19456
|
-
import { join as
|
|
19530
|
+
import { join as join31 } from "path";
|
|
19457
19531
|
var FIRST_LINE_WAIT_MS2 = 2 * 60 * 1e3;
|
|
19458
19532
|
function promptArgs(text, tool = "") {
|
|
19459
19533
|
if (!text) return [];
|
|
@@ -19463,7 +19537,7 @@ function promptArgs(text, tool = "") {
|
|
|
19463
19537
|
function promptEnv(tool, env, launcherPid = process.pid, answerWindowId = "") {
|
|
19464
19538
|
const out = { ...env };
|
|
19465
19539
|
if (tool === "workbuddy") {
|
|
19466
|
-
if (!out.CODEBUDDY_CONFIG_DIR) out.CODEBUDDY_CONFIG_DIR =
|
|
19540
|
+
if (!out.CODEBUDDY_CONFIG_DIR) out.CODEBUDDY_CONFIG_DIR = join31(homedir10(), ".workbuddy");
|
|
19467
19541
|
out.DESKBUDDY_LAUNCHER_PID = String(launcherPid);
|
|
19468
19542
|
out.DESKBUDDY_LAUNCHED_TOOL = tool;
|
|
19469
19543
|
if (answerWindowId) out.DESKBUDDY_LAUNCHED_WINDOW = answerWindowId;
|
|
@@ -20047,7 +20121,7 @@ function childEnv(src, windowId, takeover = true) {
|
|
|
20047
20121
|
}
|
|
20048
20122
|
function wrapLog(line) {
|
|
20049
20123
|
try {
|
|
20050
|
-
const p =
|
|
20124
|
+
const p = join32(deskbuddyHome(), "wrap.log");
|
|
20051
20125
|
mkdirSync15(deskbuddyHome(), { recursive: true });
|
|
20052
20126
|
try {
|
|
20053
20127
|
if (statSync16(p).size > MAX_LOG_BYTES3) truncateSync3(p, 0);
|
package/dist/gate.cjs
CHANGED
|
@@ -64,7 +64,7 @@ function readStdin() {
|
|
|
64
64
|
})
|
|
65
65
|
}
|
|
66
66
|
|
|
67
|
-
function post(port, token, body) {
|
|
67
|
+
function post(port, token, body, info) {
|
|
68
68
|
return new Promise((resolve, reject) => {
|
|
69
69
|
const req = http.request(
|
|
70
70
|
{
|
|
@@ -87,10 +87,75 @@ function post(port, token, body) {
|
|
|
87
87
|
}
|
|
88
88
|
)
|
|
89
89
|
req.on('error', reject)
|
|
90
|
+
// 连上过(守护进程收下了请求)还是根本没连上:窗口模式据此决定要不要等守护进程重启(问题表 268)
|
|
91
|
+
if (info) req.on('socket', (sock) => (sock.connecting ? sock.once('connect', () => (info.connected = true)) : (info.connected = true)))
|
|
90
92
|
req.end(body)
|
|
91
93
|
})
|
|
92
94
|
}
|
|
93
95
|
|
|
96
|
+
/** 守护进程等待中途没了(崩溃 / 被杀),launcher 10 秒后重拉:最多等这么久,等它换了新端口再提交一次 */
|
|
97
|
+
const RESTART_WAIT_MS = 3 * 60 * 1000
|
|
98
|
+
const RESTART_POLL_MS = 2000
|
|
99
|
+
|
|
100
|
+
/** 与 src/daemon/autostart.ts STOP_MARK_FILE / STOP_MARK_TTL_MS 一致:DeskBuddy 自己的停止流程留的标记 */
|
|
101
|
+
const STOP_MARK_FILE = 'daemon.stopping'
|
|
102
|
+
const STOP_MARK_TTL_MS = 2 * 60 * 1000
|
|
103
|
+
function stopMarkFresh(now) {
|
|
104
|
+
try {
|
|
105
|
+
const t = Number(fs.readFileSync(path.join(deskbuddyHome(), STOP_MARK_FILE), 'utf8').trim())
|
|
106
|
+
return Number.isFinite(t) && now - t >= 0 && now - t < STOP_MARK_TTL_MS
|
|
107
|
+
} catch (_) {
|
|
108
|
+
return false
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function readGateConf() {
|
|
113
|
+
try {
|
|
114
|
+
const c = JSON.parse(fs.readFileSync(path.join(deskbuddyHome(), 'gate.json'), 'utf8'))
|
|
115
|
+
return c && Number.isInteger(c.port) && typeof c.token === 'string' && c.token ? c : null
|
|
116
|
+
} catch (_) {
|
|
117
|
+
return null
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* 窗口模式的 POST:守护进程在等设备答复期间没了(连上过、之后连接断开),不放弃 ——
|
|
123
|
+
* 等它重启(gate.json 换了端口 / 令牌)后把同一个授权再提交一次,设备上重新出卡(问题表 268:
|
|
124
|
+
* 崩溃后窗口里那道授权被撤、再也推不到设备)。🔴 一开始就连不上(守护进程根本不在)照旧立刻失败,不卡窗口
|
|
125
|
+
*/
|
|
126
|
+
async function postSurviving(conf, body, deps) {
|
|
127
|
+
const d = deps || {}
|
|
128
|
+
const send = d.post || post
|
|
129
|
+
const now = d.now || Date.now
|
|
130
|
+
const sleep = d.sleep || ((ms) => new Promise((r) => setTimeout(r, ms)))
|
|
131
|
+
const read = d.readConf || readGateConf
|
|
132
|
+
const stopping = d.stopping || stopMarkFresh
|
|
133
|
+
let cur = conf
|
|
134
|
+
let deadline = 0
|
|
135
|
+
for (;;) {
|
|
136
|
+
const info = { connected: false }
|
|
137
|
+
try {
|
|
138
|
+
return await send(cur.port, cur.token, body, info)
|
|
139
|
+
} catch (e) {
|
|
140
|
+
// 连上过又断了 = 守护进程这次没了,重新计时(重提交后又崩一次也能再等);没连上且不在等重启 = 守护进程根本不在
|
|
141
|
+
if (info.connected) deadline = now() + RESTART_WAIT_MS
|
|
142
|
+
else if (!deadline) throw e
|
|
143
|
+
// 等新的守护进程起来:gate.json 换了才再提交(同一个端口还连不上就接着等)
|
|
144
|
+
for (;;) {
|
|
145
|
+
// 🔴 正常停止(disable / 升级 / 未授权退出)不等:停止流程先删 gate.json,Windows 结束进程时留停止标记(评审必修)
|
|
146
|
+
if (now() >= deadline || stopping(now())) throw e
|
|
147
|
+
await sleep(RESTART_POLL_MS)
|
|
148
|
+
const c = read()
|
|
149
|
+
if (!c || stopping(now())) throw e
|
|
150
|
+
if (c.port !== cur.port || c.token !== cur.token) {
|
|
151
|
+
cur = c
|
|
152
|
+
break
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
94
159
|
/**
|
|
95
160
|
* 这个 hook 是不是由【交互窗口】里的 Claude Code 触发的。
|
|
96
161
|
* 🔴 实测(2026-09-24,2.1.259):`claude -p` 碰到要授权的工具**也会**触发 PermissionRequest,
|
|
@@ -792,7 +857,7 @@ async function windowMain() {
|
|
|
792
857
|
...(lang ? { lang } : {}),
|
|
793
858
|
...(tool === 'claude_code' ? { ccAutoMode: ccAutoMode(hook) } : {}),
|
|
794
859
|
}
|
|
795
|
-
const res = await
|
|
860
|
+
const res = await postSurviving(conf, JSON.stringify({ mode: 'window', tool, hook, ...extra }))
|
|
796
861
|
if (res.status !== 200) return fail()
|
|
797
862
|
const out = JSON.parse(res.data)
|
|
798
863
|
if (!out || (out.decision !== 'allow' && out.decision !== 'deny')) return fail()
|
|
@@ -925,5 +990,5 @@ if (require.main === module) {
|
|
|
925
990
|
} else {
|
|
926
991
|
// 只给单测(智能体永远是 `node gate.cjs` 直接跑)。
|
|
927
992
|
// 🔴 禁止通过 require 调用本关卡:main 不跑,进程会 exit 0 = 放行
|
|
928
|
-
module.exports = { preToolOutput, PRETOOL_MAX_MS, traeNoticeHook, qoderCliMatches, qoderNodeCliMatches, isInteractiveAgentWin, claudeArgsInteractive, qoderArgsInteractive, isInteractiveQoder, qoderUi, ccAutoMode, CC_POLICY_DOMAIN, CC_REG_KEYS, codebuddyAgent, codebuddyCliArgs, codebuddyAgentWin, winScriptOf, findCodebuddy, codebuddyProduct }
|
|
993
|
+
module.exports = { postSurviving, RESTART_WAIT_MS, preToolOutput, PRETOOL_MAX_MS, traeNoticeHook, qoderCliMatches, qoderNodeCliMatches, isInteractiveAgentWin, claudeArgsInteractive, qoderArgsInteractive, isInteractiveQoder, qoderUi, ccAutoMode, CC_POLICY_DOMAIN, CC_REG_KEYS, codebuddyAgent, codebuddyCliArgs, codebuddyAgentWin, winScriptOf, findCodebuddy, codebuddyProduct }
|
|
929
994
|
}
|
package/dist/launcher.cjs
CHANGED
|
@@ -54,6 +54,77 @@ function paths(h) {
|
|
|
54
54
|
crashes: path.join(versions, 'crashes.json'),
|
|
55
55
|
log: path.join(h, 'daemon.log'),
|
|
56
56
|
pid: path.join(h, 'launcher.pid'),
|
|
57
|
+
stderr: path.join(h, 'daemon.stderr.log'),
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* 子进程 stderr 落盘(问题表 268):Windows 上 Node 的致命错误(V8 内存耗尽、libuv 断言、原生模块 abort)
|
|
63
|
+
* 退出码都是 0xC0000409,原因只打在 stderr 上;launcher 是隐藏启动的,stderr 没人接 → 日志里什么都没有。
|
|
64
|
+
* 超过 1MB 先转存成 .1(只留一份)。打不开返回 null(照旧 inherit)
|
|
65
|
+
*/
|
|
66
|
+
const STDERR_MAX_BYTES = 1024 * 1024
|
|
67
|
+
function openStderr(h) {
|
|
68
|
+
const p = paths(h).stderr
|
|
69
|
+
try {
|
|
70
|
+
fs.mkdirSync(h, { recursive: true })
|
|
71
|
+
try {
|
|
72
|
+
if (fs.statSync(p).size > STDERR_MAX_BYTES) fs.renameSync(p, p + '.1')
|
|
73
|
+
} catch {
|
|
74
|
+
/* 还没有这个文件 */
|
|
75
|
+
}
|
|
76
|
+
const fd = fs.openSync(p, 'a')
|
|
77
|
+
return { fd, start: fs.fstatSync(fd).size }
|
|
78
|
+
} catch {
|
|
79
|
+
return null
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const FATAL_LINE_RE = /FATAL ERROR|Fatal error|Assertion failed|Check failed|Uncaught|Error:|Exception|out of memory|abort/i
|
|
84
|
+
|
|
85
|
+
/** 这次运行期间写进 stderr 文件的末尾(最多 1500 字符,去掉空行);没有返回 '' */
|
|
86
|
+
function stderrTail(h, start) {
|
|
87
|
+
try {
|
|
88
|
+
const p = paths(h).stderr
|
|
89
|
+
const size = fs.statSync(p).size
|
|
90
|
+
const from = Math.max(start, size - 16384)
|
|
91
|
+
if (size <= from) return ''
|
|
92
|
+
const buf = Buffer.alloc(size - from)
|
|
93
|
+
const fd = fs.openSync(p, 'r')
|
|
94
|
+
try {
|
|
95
|
+
fs.readSync(fd, buf, 0, buf.length, from)
|
|
96
|
+
} finally {
|
|
97
|
+
fs.closeSync(fd)
|
|
98
|
+
}
|
|
99
|
+
const lines = buf.toString('utf8').split(/\r?\n/).filter((l) => l.trim())
|
|
100
|
+
// 关键行(V8 / libuv / Node 的致命错误头、未捕获异常)放前面:原生调用栈在后面,只留末尾几行
|
|
101
|
+
const key = lines.filter((l) => FATAL_LINE_RE.test(l) && !/^\s*\d+: 0x/.test(l)).slice(0, 4) // 原生调用栈帧(` 1: 0x… node::Abort()`)不算
|
|
102
|
+
const tail = lines.filter((l) => !key.includes(l)).slice(-4)
|
|
103
|
+
return [...key, ...(tail.length ? ['…', ...tail] : [])].join(' | ').slice(0, 1500)
|
|
104
|
+
} catch {
|
|
105
|
+
return ''
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/** Node 致命错误时写诊断报告(JS / 原生调用栈、堆、句柄)的目录;只留最新 5 份 */
|
|
110
|
+
const REPORT_KEEP = 5
|
|
111
|
+
function reportDir(h) {
|
|
112
|
+
const d = path.join(h, 'reports')
|
|
113
|
+
try {
|
|
114
|
+
fs.mkdirSync(d, { recursive: true })
|
|
115
|
+
const old = fs.readdirSync(d).filter((n) => n.endsWith('.json')).sort()
|
|
116
|
+
for (const n of old.slice(0, Math.max(0, old.length - REPORT_KEEP))) fs.rmSync(path.join(d, n), { force: true })
|
|
117
|
+
} catch {
|
|
118
|
+
/* 清不了不影响启动 */
|
|
119
|
+
}
|
|
120
|
+
return d
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function newReports(dir, since) {
|
|
124
|
+
try {
|
|
125
|
+
return fs.readdirSync(dir).filter((n) => n.endsWith('.json') && fs.statSync(path.join(dir, n)).mtimeMs >= since)
|
|
126
|
+
} catch {
|
|
127
|
+
return []
|
|
57
128
|
}
|
|
58
129
|
}
|
|
59
130
|
|
|
@@ -255,7 +326,25 @@ const AFTER_STOP_POLL_MS = 1000
|
|
|
255
326
|
function runOnce(h, cur, args) {
|
|
256
327
|
return new Promise((resolve) => {
|
|
257
328
|
let stopping = false
|
|
258
|
-
const
|
|
329
|
+
const err = openStderr(h)
|
|
330
|
+
const reports = reportDir(h)
|
|
331
|
+
const startedAt = Date.now()
|
|
332
|
+
let child
|
|
333
|
+
try {
|
|
334
|
+
child = spawn(process.execPath, ['--report-on-fatalerror', '--report-compact', '--report-directory=' + reports, cur.cliPath, ...args], {
|
|
335
|
+
stdio: ['inherit', 'inherit', err ? err.fd : 'inherit'],
|
|
336
|
+
windowsHide: true,
|
|
337
|
+
env: childEnv(process.env),
|
|
338
|
+
})
|
|
339
|
+
} finally {
|
|
340
|
+
if (err) {
|
|
341
|
+
try {
|
|
342
|
+
fs.closeSync(err.fd) // 子进程已经拿到一份
|
|
343
|
+
} catch {
|
|
344
|
+
/* ignore */
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
}
|
|
259
348
|
const forward = (sig) => {
|
|
260
349
|
stopping = true
|
|
261
350
|
try {
|
|
@@ -269,9 +358,9 @@ function runOnce(h, cur, args) {
|
|
|
269
358
|
handlers[sig] = () => forward(sig)
|
|
270
359
|
process.on(sig, handlers[sig])
|
|
271
360
|
}
|
|
272
|
-
const done = (code, signal,
|
|
361
|
+
const done = (code, signal, e) => {
|
|
273
362
|
for (const sig of Object.keys(handlers)) process.removeListener(sig, handlers[sig])
|
|
274
|
-
resolve({ code, signal, stopping, err })
|
|
363
|
+
resolve({ code, signal, stopping, err: e, stderr: err ? stderrTail(h, err.start) : '', reports: newReports(reports, startedAt) })
|
|
275
364
|
}
|
|
276
365
|
child.on('error', (e) => done(null, null, e))
|
|
277
366
|
child.on('exit', (code, signal) => done(code, signal, null))
|
|
@@ -323,6 +412,10 @@ async function main(argv, deps) {
|
|
|
323
412
|
}
|
|
324
413
|
const startedAt = now()
|
|
325
414
|
const r = await run(cur)
|
|
415
|
+
// 🔴 退出后重读一次:跑着的时候 daemon 可能已把 verifying 改成 good(连满 3 分钟)。
|
|
416
|
+
// 用启动时的旧副本会把验证通过之后的崩溃也算成「验证期内」、不记退出码(问题表 268 的 4 次「验证期内异常退出」)
|
|
417
|
+
const latest = readCurrent(h)
|
|
418
|
+
const ran = latest && latest.version === cur.version ? latest : cur
|
|
326
419
|
const abnormal = !r.stopping && (r.err || r.signal || r.code !== 0)
|
|
327
420
|
if (r.err) log(h, '起不来版本 ' + cur.version + ':' + r.err.message)
|
|
328
421
|
if (!abnormal) return r.code == null ? 0 : r.code
|
|
@@ -340,16 +433,23 @@ async function main(argv, deps) {
|
|
|
340
433
|
log(h, '守护进程已退出(DeskBuddy 停止流程),launcher 不再重拉')
|
|
341
434
|
return 0
|
|
342
435
|
}
|
|
436
|
+
// 崩溃现场(问题表 268):退出码、stderr 末尾、诊断报告文件名记进 daemon.log
|
|
437
|
+
log(
|
|
438
|
+
h,
|
|
439
|
+
'守护进程退出码 ' + (r.code == null ? r.signal || '?' : r.code) +
|
|
440
|
+
(r.stderr ? ';stderr 末尾:' + r.stderr : ';stderr 没有输出') +
|
|
441
|
+
(r.reports && r.reports.length ? ';诊断报告:reports/' + r.reports.join(', reports/') : '')
|
|
442
|
+
)
|
|
343
443
|
const killed = terminated(r)
|
|
344
444
|
// 被外部 SIGTERM 不是这一版的毛病:不计崩溃、不触发回滚
|
|
345
|
-
const rolledBack = killed ? false : recordCrash(h,
|
|
445
|
+
const rolledBack = killed ? false : recordCrash(h, ran, now())
|
|
346
446
|
if (killed) log(h, '守护进程被外部结束(SIGTERM,不是 DeskBuddy 的停止流程),交给' + (platform === 'win32' ? ' launcher ' : '开机自启') + '重新拉起')
|
|
347
447
|
// 非 Windows:交给 launchd / systemd 按「非正常退出」重拉(它们会重新执行本 launcher,读到回滚后的 current.json)
|
|
348
448
|
if (platform !== 'win32') return r.code == null ? 1 : r.code || 1
|
|
349
449
|
// Windows:「启动」文件夹只在登录时跑一次,退了没人拉 —— 本进程自己重拉。
|
|
350
450
|
// 🔴 DeskBuddy 自己的停止流程先结束本 launcher 再结束 daemon(autostart.ts stopPidFileDaemon),并留了标记,不会走到这里
|
|
351
451
|
let wait = WIN_RESTART_MS
|
|
352
|
-
if (
|
|
452
|
+
if (ran.state !== 'verifying' && !rolledBack) {
|
|
353
453
|
winFails = now() - startedAt >= WIN_STABLE_MS ? 1 : winFails + 1
|
|
354
454
|
wait = Math.min(WIN_RESTART_MS * Math.pow(2, winFails - 1), WIN_RESTART_MAX_MS)
|
|
355
455
|
log(h, '守护进程异常退出(退出码 ' + (r.code == null ? r.signal || '?' : r.code) + '),' + Math.round(wait / 1000) + ' 秒后重拉')
|
|
@@ -358,7 +458,7 @@ async function main(argv, deps) {
|
|
|
358
458
|
}
|
|
359
459
|
}
|
|
360
460
|
|
|
361
|
-
module.exports = { main, childEnv, daemonLockName, lockOwnerAlive, intentionalStop, AFTER_STOP_ENV, SWITCH_ENV_KEYS, pickTarget, recordCrash, rollback, readCurrent, paths, CRASH_WINDOW_MS, CRASH_LIMIT, TERM_EXIT_CODE, UNBOUND_EXIT_CODE, STOP_MARK_FILE, STOP_MARK_TTL_MS, WIN_RESTART_MS, WIN_RESTART_MAX_MS }
|
|
461
|
+
module.exports = { main, runOnce, childEnv, daemonLockName, lockOwnerAlive, intentionalStop, AFTER_STOP_ENV, SWITCH_ENV_KEYS, pickTarget, recordCrash, rollback, readCurrent, paths, CRASH_WINDOW_MS, CRASH_LIMIT, TERM_EXIT_CODE, UNBOUND_EXIT_CODE, STOP_MARK_FILE, STOP_MARK_TTL_MS, WIN_RESTART_MS, WIN_RESTART_MAX_MS }
|
|
362
462
|
|
|
363
463
|
if (require.main === module) {
|
|
364
464
|
main(process.argv.slice(2)).then(
|
package/hook/gate.cjs
CHANGED
|
@@ -64,7 +64,7 @@ function readStdin() {
|
|
|
64
64
|
})
|
|
65
65
|
}
|
|
66
66
|
|
|
67
|
-
function post(port, token, body) {
|
|
67
|
+
function post(port, token, body, info) {
|
|
68
68
|
return new Promise((resolve, reject) => {
|
|
69
69
|
const req = http.request(
|
|
70
70
|
{
|
|
@@ -87,10 +87,75 @@ function post(port, token, body) {
|
|
|
87
87
|
}
|
|
88
88
|
)
|
|
89
89
|
req.on('error', reject)
|
|
90
|
+
// 连上过(守护进程收下了请求)还是根本没连上:窗口模式据此决定要不要等守护进程重启(问题表 268)
|
|
91
|
+
if (info) req.on('socket', (sock) => (sock.connecting ? sock.once('connect', () => (info.connected = true)) : (info.connected = true)))
|
|
90
92
|
req.end(body)
|
|
91
93
|
})
|
|
92
94
|
}
|
|
93
95
|
|
|
96
|
+
/** 守护进程等待中途没了(崩溃 / 被杀),launcher 10 秒后重拉:最多等这么久,等它换了新端口再提交一次 */
|
|
97
|
+
const RESTART_WAIT_MS = 3 * 60 * 1000
|
|
98
|
+
const RESTART_POLL_MS = 2000
|
|
99
|
+
|
|
100
|
+
/** 与 src/daemon/autostart.ts STOP_MARK_FILE / STOP_MARK_TTL_MS 一致:DeskBuddy 自己的停止流程留的标记 */
|
|
101
|
+
const STOP_MARK_FILE = 'daemon.stopping'
|
|
102
|
+
const STOP_MARK_TTL_MS = 2 * 60 * 1000
|
|
103
|
+
function stopMarkFresh(now) {
|
|
104
|
+
try {
|
|
105
|
+
const t = Number(fs.readFileSync(path.join(deskbuddyHome(), STOP_MARK_FILE), 'utf8').trim())
|
|
106
|
+
return Number.isFinite(t) && now - t >= 0 && now - t < STOP_MARK_TTL_MS
|
|
107
|
+
} catch (_) {
|
|
108
|
+
return false
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function readGateConf() {
|
|
113
|
+
try {
|
|
114
|
+
const c = JSON.parse(fs.readFileSync(path.join(deskbuddyHome(), 'gate.json'), 'utf8'))
|
|
115
|
+
return c && Number.isInteger(c.port) && typeof c.token === 'string' && c.token ? c : null
|
|
116
|
+
} catch (_) {
|
|
117
|
+
return null
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* 窗口模式的 POST:守护进程在等设备答复期间没了(连上过、之后连接断开),不放弃 ——
|
|
123
|
+
* 等它重启(gate.json 换了端口 / 令牌)后把同一个授权再提交一次,设备上重新出卡(问题表 268:
|
|
124
|
+
* 崩溃后窗口里那道授权被撤、再也推不到设备)。🔴 一开始就连不上(守护进程根本不在)照旧立刻失败,不卡窗口
|
|
125
|
+
*/
|
|
126
|
+
async function postSurviving(conf, body, deps) {
|
|
127
|
+
const d = deps || {}
|
|
128
|
+
const send = d.post || post
|
|
129
|
+
const now = d.now || Date.now
|
|
130
|
+
const sleep = d.sleep || ((ms) => new Promise((r) => setTimeout(r, ms)))
|
|
131
|
+
const read = d.readConf || readGateConf
|
|
132
|
+
const stopping = d.stopping || stopMarkFresh
|
|
133
|
+
let cur = conf
|
|
134
|
+
let deadline = 0
|
|
135
|
+
for (;;) {
|
|
136
|
+
const info = { connected: false }
|
|
137
|
+
try {
|
|
138
|
+
return await send(cur.port, cur.token, body, info)
|
|
139
|
+
} catch (e) {
|
|
140
|
+
// 连上过又断了 = 守护进程这次没了,重新计时(重提交后又崩一次也能再等);没连上且不在等重启 = 守护进程根本不在
|
|
141
|
+
if (info.connected) deadline = now() + RESTART_WAIT_MS
|
|
142
|
+
else if (!deadline) throw e
|
|
143
|
+
// 等新的守护进程起来:gate.json 换了才再提交(同一个端口还连不上就接着等)
|
|
144
|
+
for (;;) {
|
|
145
|
+
// 🔴 正常停止(disable / 升级 / 未授权退出)不等:停止流程先删 gate.json,Windows 结束进程时留停止标记(评审必修)
|
|
146
|
+
if (now() >= deadline || stopping(now())) throw e
|
|
147
|
+
await sleep(RESTART_POLL_MS)
|
|
148
|
+
const c = read()
|
|
149
|
+
if (!c || stopping(now())) throw e
|
|
150
|
+
if (c.port !== cur.port || c.token !== cur.token) {
|
|
151
|
+
cur = c
|
|
152
|
+
break
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
94
159
|
/**
|
|
95
160
|
* 这个 hook 是不是由【交互窗口】里的 Claude Code 触发的。
|
|
96
161
|
* 🔴 实测(2026-09-24,2.1.259):`claude -p` 碰到要授权的工具**也会**触发 PermissionRequest,
|
|
@@ -792,7 +857,7 @@ async function windowMain() {
|
|
|
792
857
|
...(lang ? { lang } : {}),
|
|
793
858
|
...(tool === 'claude_code' ? { ccAutoMode: ccAutoMode(hook) } : {}),
|
|
794
859
|
}
|
|
795
|
-
const res = await
|
|
860
|
+
const res = await postSurviving(conf, JSON.stringify({ mode: 'window', tool, hook, ...extra }))
|
|
796
861
|
if (res.status !== 200) return fail()
|
|
797
862
|
const out = JSON.parse(res.data)
|
|
798
863
|
if (!out || (out.decision !== 'allow' && out.decision !== 'deny')) return fail()
|
|
@@ -925,5 +990,5 @@ if (require.main === module) {
|
|
|
925
990
|
} else {
|
|
926
991
|
// 只给单测(智能体永远是 `node gate.cjs` 直接跑)。
|
|
927
992
|
// 🔴 禁止通过 require 调用本关卡:main 不跑,进程会 exit 0 = 放行
|
|
928
|
-
module.exports = { preToolOutput, PRETOOL_MAX_MS, traeNoticeHook, qoderCliMatches, qoderNodeCliMatches, isInteractiveAgentWin, claudeArgsInteractive, qoderArgsInteractive, isInteractiveQoder, qoderUi, ccAutoMode, CC_POLICY_DOMAIN, CC_REG_KEYS, codebuddyAgent, codebuddyCliArgs, codebuddyAgentWin, winScriptOf, findCodebuddy, codebuddyProduct }
|
|
993
|
+
module.exports = { postSurviving, RESTART_WAIT_MS, preToolOutput, PRETOOL_MAX_MS, traeNoticeHook, qoderCliMatches, qoderNodeCliMatches, isInteractiveAgentWin, claudeArgsInteractive, qoderArgsInteractive, isInteractiveQoder, qoderUi, ccAutoMode, CC_POLICY_DOMAIN, CC_REG_KEYS, codebuddyAgent, codebuddyCliArgs, codebuddyAgentWin, winScriptOf, findCodebuddy, codebuddyProduct }
|
|
929
994
|
}
|
package/hook/hook-debug.log
CHANGED
|
@@ -362,3 +362,45 @@
|
|
|
362
362
|
2026-10-12T09:01:12.293Z [opencode] opencode 版本 读不出:带说明的拒绝不走新路由
|
|
363
363
|
2026-10-12T09:01:12.293Z [opencode] opencode 版本不确定支持带说明的拒绝,设备上的反馈没有带给模型
|
|
364
364
|
2026-10-12T09:01:14.613Z [opencode] 后台任务里的提问 que_r2 没法在设备上回答(后台任务不能提问,请在电脑上继续),已拒绝
|
|
365
|
+
2026-10-12T10:52:32.632Z [opencode] 回填选择题失败:/question/{requestID}/reply status=404
|
|
366
|
+
2026-10-12T10:52:37.625Z [opencode] opencode 版本 1.4.3:带说明的拒绝不走新路由
|
|
367
|
+
2026-10-12T10:52:37.625Z [opencode] opencode 版本不确定支持带说明的拒绝,设备上的反馈没有带给模型
|
|
368
|
+
2026-10-12T10:52:37.718Z [opencode] opencode 版本 读不出:带说明的拒绝不走新路由
|
|
369
|
+
2026-10-12T10:52:37.719Z [opencode] opencode 版本不确定支持带说明的拒绝,设备上的反馈没有带给模型
|
|
370
|
+
2026-10-12T10:52:40.546Z [opencode] 后台任务里的提问 que_r2 没法在设备上回答(后台任务不能提问,请在电脑上继续),已拒绝
|
|
371
|
+
2026-10-12T10:56:52.376Z [opencode] 回填选择题失败:/question/{requestID}/reply status=404
|
|
372
|
+
2026-10-12T10:56:57.350Z [opencode] opencode 版本 1.4.3:带说明的拒绝不走新路由
|
|
373
|
+
2026-10-12T10:56:57.350Z [opencode] opencode 版本不确定支持带说明的拒绝,设备上的反馈没有带给模型
|
|
374
|
+
2026-10-12T10:56:57.454Z [opencode] opencode 版本 读不出:带说明的拒绝不走新路由
|
|
375
|
+
2026-10-12T10:56:57.454Z [opencode] opencode 版本不确定支持带说明的拒绝,设备上的反馈没有带给模型
|
|
376
|
+
2026-10-12T10:56:59.731Z [opencode] 后台任务里的提问 que_r2 没法在设备上回答(后台任务不能提问,请在电脑上继续),已拒绝
|
|
377
|
+
2026-10-12T11:00:08.567Z [opencode] 回填选择题失败:/question/{requestID}/reply status=404
|
|
378
|
+
2026-10-12T11:00:13.510Z [opencode] opencode 版本 1.4.3:带说明的拒绝不走新路由
|
|
379
|
+
2026-10-12T11:00:13.510Z [opencode] opencode 版本不确定支持带说明的拒绝,设备上的反馈没有带给模型
|
|
380
|
+
2026-10-12T11:00:13.614Z [opencode] opencode 版本 读不出:带说明的拒绝不走新路由
|
|
381
|
+
2026-10-12T11:00:13.614Z [opencode] opencode 版本不确定支持带说明的拒绝,设备上的反馈没有带给模型
|
|
382
|
+
2026-10-12T11:00:15.993Z [opencode] 后台任务里的提问 que_r2 没法在设备上回答(后台任务不能提问,请在电脑上继续),已拒绝
|
|
383
|
+
2026-10-12T11:15:58.702Z [opencode] 回填选择题失败:/question/{requestID}/reply status=404
|
|
384
|
+
2026-10-12T11:16:03.652Z [opencode] opencode 版本 1.4.3:带说明的拒绝不走新路由
|
|
385
|
+
2026-10-12T11:16:03.652Z [opencode] opencode 版本不确定支持带说明的拒绝,设备上的反馈没有带给模型
|
|
386
|
+
2026-10-12T11:16:03.752Z [opencode] opencode 版本 读不出:带说明的拒绝不走新路由
|
|
387
|
+
2026-10-12T11:16:03.753Z [opencode] opencode 版本不确定支持带说明的拒绝,设备上的反馈没有带给模型
|
|
388
|
+
2026-10-12T11:16:06.045Z [opencode] 后台任务里的提问 que_r2 没法在设备上回答(后台任务不能提问,请在电脑上继续),已拒绝
|
|
389
|
+
2026-10-12T11:21:53.902Z [opencode] 回填选择题失败:/question/{requestID}/reply status=404
|
|
390
|
+
2026-10-12T11:21:58.896Z [opencode] opencode 版本 1.4.3:带说明的拒绝不走新路由
|
|
391
|
+
2026-10-12T11:21:58.896Z [opencode] opencode 版本不确定支持带说明的拒绝,设备上的反馈没有带给模型
|
|
392
|
+
2026-10-12T11:21:58.995Z [opencode] opencode 版本 读不出:带说明的拒绝不走新路由
|
|
393
|
+
2026-10-12T11:21:58.995Z [opencode] opencode 版本不确定支持带说明的拒绝,设备上的反馈没有带给模型
|
|
394
|
+
2026-10-12T11:22:01.397Z [opencode] 后台任务里的提问 que_r2 没法在设备上回答(后台任务不能提问,请在电脑上继续),已拒绝
|
|
395
|
+
2026-10-12T11:24:28.256Z [opencode] 回填选择题失败:/question/{requestID}/reply status=404
|
|
396
|
+
2026-10-12T11:24:33.247Z [opencode] opencode 版本 1.4.3:带说明的拒绝不走新路由
|
|
397
|
+
2026-10-12T11:24:33.247Z [opencode] opencode 版本不确定支持带说明的拒绝,设备上的反馈没有带给模型
|
|
398
|
+
2026-10-12T11:24:33.342Z [opencode] opencode 版本 读不出:带说明的拒绝不走新路由
|
|
399
|
+
2026-10-12T11:24:33.342Z [opencode] opencode 版本不确定支持带说明的拒绝,设备上的反馈没有带给模型
|
|
400
|
+
2026-10-12T11:24:35.695Z [opencode] 后台任务里的提问 que_r2 没法在设备上回答(后台任务不能提问,请在电脑上继续),已拒绝
|
|
401
|
+
2026-10-12T11:26:33.586Z [opencode] 回填选择题失败:/question/{requestID}/reply status=404
|
|
402
|
+
2026-10-12T11:26:38.587Z [opencode] opencode 版本 1.4.3:带说明的拒绝不走新路由
|
|
403
|
+
2026-10-12T11:26:38.588Z [opencode] opencode 版本不确定支持带说明的拒绝,设备上的反馈没有带给模型
|
|
404
|
+
2026-10-12T11:26:38.709Z [opencode] opencode 版本 读不出:带说明的拒绝不走新路由
|
|
405
|
+
2026-10-12T11:26:38.710Z [opencode] opencode 版本不确定支持带说明的拒绝,设备上的反馈没有带给模型
|
|
406
|
+
2026-10-12T11:26:41.188Z [opencode] 后台任务里的提问 que_r2 没法在设备上回答(后台任务不能提问,请在电脑上继续),已拒绝
|
package/hook/launcher.cjs
CHANGED
|
@@ -54,6 +54,77 @@ function paths(h) {
|
|
|
54
54
|
crashes: path.join(versions, 'crashes.json'),
|
|
55
55
|
log: path.join(h, 'daemon.log'),
|
|
56
56
|
pid: path.join(h, 'launcher.pid'),
|
|
57
|
+
stderr: path.join(h, 'daemon.stderr.log'),
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* 子进程 stderr 落盘(问题表 268):Windows 上 Node 的致命错误(V8 内存耗尽、libuv 断言、原生模块 abort)
|
|
63
|
+
* 退出码都是 0xC0000409,原因只打在 stderr 上;launcher 是隐藏启动的,stderr 没人接 → 日志里什么都没有。
|
|
64
|
+
* 超过 1MB 先转存成 .1(只留一份)。打不开返回 null(照旧 inherit)
|
|
65
|
+
*/
|
|
66
|
+
const STDERR_MAX_BYTES = 1024 * 1024
|
|
67
|
+
function openStderr(h) {
|
|
68
|
+
const p = paths(h).stderr
|
|
69
|
+
try {
|
|
70
|
+
fs.mkdirSync(h, { recursive: true })
|
|
71
|
+
try {
|
|
72
|
+
if (fs.statSync(p).size > STDERR_MAX_BYTES) fs.renameSync(p, p + '.1')
|
|
73
|
+
} catch {
|
|
74
|
+
/* 还没有这个文件 */
|
|
75
|
+
}
|
|
76
|
+
const fd = fs.openSync(p, 'a')
|
|
77
|
+
return { fd, start: fs.fstatSync(fd).size }
|
|
78
|
+
} catch {
|
|
79
|
+
return null
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const FATAL_LINE_RE = /FATAL ERROR|Fatal error|Assertion failed|Check failed|Uncaught|Error:|Exception|out of memory|abort/i
|
|
84
|
+
|
|
85
|
+
/** 这次运行期间写进 stderr 文件的末尾(最多 1500 字符,去掉空行);没有返回 '' */
|
|
86
|
+
function stderrTail(h, start) {
|
|
87
|
+
try {
|
|
88
|
+
const p = paths(h).stderr
|
|
89
|
+
const size = fs.statSync(p).size
|
|
90
|
+
const from = Math.max(start, size - 16384)
|
|
91
|
+
if (size <= from) return ''
|
|
92
|
+
const buf = Buffer.alloc(size - from)
|
|
93
|
+
const fd = fs.openSync(p, 'r')
|
|
94
|
+
try {
|
|
95
|
+
fs.readSync(fd, buf, 0, buf.length, from)
|
|
96
|
+
} finally {
|
|
97
|
+
fs.closeSync(fd)
|
|
98
|
+
}
|
|
99
|
+
const lines = buf.toString('utf8').split(/\r?\n/).filter((l) => l.trim())
|
|
100
|
+
// 关键行(V8 / libuv / Node 的致命错误头、未捕获异常)放前面:原生调用栈在后面,只留末尾几行
|
|
101
|
+
const key = lines.filter((l) => FATAL_LINE_RE.test(l) && !/^\s*\d+: 0x/.test(l)).slice(0, 4) // 原生调用栈帧(` 1: 0x… node::Abort()`)不算
|
|
102
|
+
const tail = lines.filter((l) => !key.includes(l)).slice(-4)
|
|
103
|
+
return [...key, ...(tail.length ? ['…', ...tail] : [])].join(' | ').slice(0, 1500)
|
|
104
|
+
} catch {
|
|
105
|
+
return ''
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/** Node 致命错误时写诊断报告(JS / 原生调用栈、堆、句柄)的目录;只留最新 5 份 */
|
|
110
|
+
const REPORT_KEEP = 5
|
|
111
|
+
function reportDir(h) {
|
|
112
|
+
const d = path.join(h, 'reports')
|
|
113
|
+
try {
|
|
114
|
+
fs.mkdirSync(d, { recursive: true })
|
|
115
|
+
const old = fs.readdirSync(d).filter((n) => n.endsWith('.json')).sort()
|
|
116
|
+
for (const n of old.slice(0, Math.max(0, old.length - REPORT_KEEP))) fs.rmSync(path.join(d, n), { force: true })
|
|
117
|
+
} catch {
|
|
118
|
+
/* 清不了不影响启动 */
|
|
119
|
+
}
|
|
120
|
+
return d
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function newReports(dir, since) {
|
|
124
|
+
try {
|
|
125
|
+
return fs.readdirSync(dir).filter((n) => n.endsWith('.json') && fs.statSync(path.join(dir, n)).mtimeMs >= since)
|
|
126
|
+
} catch {
|
|
127
|
+
return []
|
|
57
128
|
}
|
|
58
129
|
}
|
|
59
130
|
|
|
@@ -255,7 +326,25 @@ const AFTER_STOP_POLL_MS = 1000
|
|
|
255
326
|
function runOnce(h, cur, args) {
|
|
256
327
|
return new Promise((resolve) => {
|
|
257
328
|
let stopping = false
|
|
258
|
-
const
|
|
329
|
+
const err = openStderr(h)
|
|
330
|
+
const reports = reportDir(h)
|
|
331
|
+
const startedAt = Date.now()
|
|
332
|
+
let child
|
|
333
|
+
try {
|
|
334
|
+
child = spawn(process.execPath, ['--report-on-fatalerror', '--report-compact', '--report-directory=' + reports, cur.cliPath, ...args], {
|
|
335
|
+
stdio: ['inherit', 'inherit', err ? err.fd : 'inherit'],
|
|
336
|
+
windowsHide: true,
|
|
337
|
+
env: childEnv(process.env),
|
|
338
|
+
})
|
|
339
|
+
} finally {
|
|
340
|
+
if (err) {
|
|
341
|
+
try {
|
|
342
|
+
fs.closeSync(err.fd) // 子进程已经拿到一份
|
|
343
|
+
} catch {
|
|
344
|
+
/* ignore */
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
}
|
|
259
348
|
const forward = (sig) => {
|
|
260
349
|
stopping = true
|
|
261
350
|
try {
|
|
@@ -269,9 +358,9 @@ function runOnce(h, cur, args) {
|
|
|
269
358
|
handlers[sig] = () => forward(sig)
|
|
270
359
|
process.on(sig, handlers[sig])
|
|
271
360
|
}
|
|
272
|
-
const done = (code, signal,
|
|
361
|
+
const done = (code, signal, e) => {
|
|
273
362
|
for (const sig of Object.keys(handlers)) process.removeListener(sig, handlers[sig])
|
|
274
|
-
resolve({ code, signal, stopping, err })
|
|
363
|
+
resolve({ code, signal, stopping, err: e, stderr: err ? stderrTail(h, err.start) : '', reports: newReports(reports, startedAt) })
|
|
275
364
|
}
|
|
276
365
|
child.on('error', (e) => done(null, null, e))
|
|
277
366
|
child.on('exit', (code, signal) => done(code, signal, null))
|
|
@@ -323,6 +412,10 @@ async function main(argv, deps) {
|
|
|
323
412
|
}
|
|
324
413
|
const startedAt = now()
|
|
325
414
|
const r = await run(cur)
|
|
415
|
+
// 🔴 退出后重读一次:跑着的时候 daemon 可能已把 verifying 改成 good(连满 3 分钟)。
|
|
416
|
+
// 用启动时的旧副本会把验证通过之后的崩溃也算成「验证期内」、不记退出码(问题表 268 的 4 次「验证期内异常退出」)
|
|
417
|
+
const latest = readCurrent(h)
|
|
418
|
+
const ran = latest && latest.version === cur.version ? latest : cur
|
|
326
419
|
const abnormal = !r.stopping && (r.err || r.signal || r.code !== 0)
|
|
327
420
|
if (r.err) log(h, '起不来版本 ' + cur.version + ':' + r.err.message)
|
|
328
421
|
if (!abnormal) return r.code == null ? 0 : r.code
|
|
@@ -340,16 +433,23 @@ async function main(argv, deps) {
|
|
|
340
433
|
log(h, '守护进程已退出(DeskBuddy 停止流程),launcher 不再重拉')
|
|
341
434
|
return 0
|
|
342
435
|
}
|
|
436
|
+
// 崩溃现场(问题表 268):退出码、stderr 末尾、诊断报告文件名记进 daemon.log
|
|
437
|
+
log(
|
|
438
|
+
h,
|
|
439
|
+
'守护进程退出码 ' + (r.code == null ? r.signal || '?' : r.code) +
|
|
440
|
+
(r.stderr ? ';stderr 末尾:' + r.stderr : ';stderr 没有输出') +
|
|
441
|
+
(r.reports && r.reports.length ? ';诊断报告:reports/' + r.reports.join(', reports/') : '')
|
|
442
|
+
)
|
|
343
443
|
const killed = terminated(r)
|
|
344
444
|
// 被外部 SIGTERM 不是这一版的毛病:不计崩溃、不触发回滚
|
|
345
|
-
const rolledBack = killed ? false : recordCrash(h,
|
|
445
|
+
const rolledBack = killed ? false : recordCrash(h, ran, now())
|
|
346
446
|
if (killed) log(h, '守护进程被外部结束(SIGTERM,不是 DeskBuddy 的停止流程),交给' + (platform === 'win32' ? ' launcher ' : '开机自启') + '重新拉起')
|
|
347
447
|
// 非 Windows:交给 launchd / systemd 按「非正常退出」重拉(它们会重新执行本 launcher,读到回滚后的 current.json)
|
|
348
448
|
if (platform !== 'win32') return r.code == null ? 1 : r.code || 1
|
|
349
449
|
// Windows:「启动」文件夹只在登录时跑一次,退了没人拉 —— 本进程自己重拉。
|
|
350
450
|
// 🔴 DeskBuddy 自己的停止流程先结束本 launcher 再结束 daemon(autostart.ts stopPidFileDaemon),并留了标记,不会走到这里
|
|
351
451
|
let wait = WIN_RESTART_MS
|
|
352
|
-
if (
|
|
452
|
+
if (ran.state !== 'verifying' && !rolledBack) {
|
|
353
453
|
winFails = now() - startedAt >= WIN_STABLE_MS ? 1 : winFails + 1
|
|
354
454
|
wait = Math.min(WIN_RESTART_MS * Math.pow(2, winFails - 1), WIN_RESTART_MAX_MS)
|
|
355
455
|
log(h, '守护进程异常退出(退出码 ' + (r.code == null ? r.signal || '?' : r.code) + '),' + Math.round(wait / 1000) + ' 秒后重拉')
|
|
@@ -358,7 +458,7 @@ async function main(argv, deps) {
|
|
|
358
458
|
}
|
|
359
459
|
}
|
|
360
460
|
|
|
361
|
-
module.exports = { main, childEnv, daemonLockName, lockOwnerAlive, intentionalStop, AFTER_STOP_ENV, SWITCH_ENV_KEYS, pickTarget, recordCrash, rollback, readCurrent, paths, CRASH_WINDOW_MS, CRASH_LIMIT, TERM_EXIT_CODE, UNBOUND_EXIT_CODE, STOP_MARK_FILE, STOP_MARK_TTL_MS, WIN_RESTART_MS, WIN_RESTART_MAX_MS }
|
|
461
|
+
module.exports = { main, runOnce, childEnv, daemonLockName, lockOwnerAlive, intentionalStop, AFTER_STOP_ENV, SWITCH_ENV_KEYS, pickTarget, recordCrash, rollback, readCurrent, paths, CRASH_WINDOW_MS, CRASH_LIMIT, TERM_EXIT_CODE, UNBOUND_EXIT_CODE, STOP_MARK_FILE, STOP_MARK_TTL_MS, WIN_RESTART_MS, WIN_RESTART_MAX_MS }
|
|
362
462
|
|
|
363
463
|
if (require.main === module) {
|
|
364
464
|
main(process.argv.slice(2)).then(
|