deskbuddy 0.2.0-beta.37 → 0.2.0-beta.39
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 +88 -53
- package/dist/gate.cjs +68 -3
- package/dist/launcher.cjs +106 -6
- package/hook/gate.cjs +68 -3
- package/hook/hook-debug.log +78 -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);
|
|
@@ -13851,12 +13851,13 @@ function removeCodexRemote() {
|
|
|
13851
13851
|
// src/core/interrupt.ts
|
|
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
|
-
var INTERRUPT_TOOLS = ["claude_code", "qoder_cn", "qoder"];
|
|
13854
|
+
var INTERRUPT_TOOLS = ["claude_code", "qoder_cn", "qoder", "qwenwork"];
|
|
13855
13855
|
var CODEX_INTERRUPT_TOOL = "codex";
|
|
13856
13856
|
function transcriptHome(tool) {
|
|
13857
13857
|
if (tool === "claude_code") return claudeHome();
|
|
13858
13858
|
if (tool === "qoder_cn") return dirname6(qoderCnSettingsPath());
|
|
13859
13859
|
if (tool === "qoder") return dirname6(qoderSettingsPath());
|
|
13860
|
+
if (tool === "qwenwork") return qwenworkDataHome();
|
|
13860
13861
|
return null;
|
|
13861
13862
|
}
|
|
13862
13863
|
var TAIL = 64 * 1024;
|
|
@@ -14286,9 +14287,37 @@ function describe2(r) {
|
|
|
14286
14287
|
return [r.status, r.n !== void 0 ? `n=${r.n}` : "", r.detail ?? ""].filter(Boolean).join(" ");
|
|
14287
14288
|
}
|
|
14288
14289
|
|
|
14290
|
+
// src/daemon/health.ts
|
|
14291
|
+
import { arch, release, type } from "os";
|
|
14292
|
+
import { join as join24 } from "path";
|
|
14293
|
+
var MEM_LOG_MS = 30 * 60 * 1e3;
|
|
14294
|
+
function envLine() {
|
|
14295
|
+
return `\u8FD0\u884C\u73AF\u5883\uFF1A${type()} ${release()} ${arch()}\uFF1BNode ${process.version}\uFF1BDeskBuddy ${packageVersion()}`;
|
|
14296
|
+
}
|
|
14297
|
+
var mb = (n) => Math.round(n / 1024 / 1024);
|
|
14298
|
+
function memLine() {
|
|
14299
|
+
const m = process.memoryUsage();
|
|
14300
|
+
let handles = -1;
|
|
14301
|
+
try {
|
|
14302
|
+
handles = process.getActiveResourcesInfo().length;
|
|
14303
|
+
} catch {
|
|
14304
|
+
}
|
|
14305
|
+
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}`;
|
|
14306
|
+
}
|
|
14307
|
+
function enableFatalReport(home) {
|
|
14308
|
+
try {
|
|
14309
|
+
const r = process.report;
|
|
14310
|
+
if (!r) return;
|
|
14311
|
+
r.directory = join24(home, "reports");
|
|
14312
|
+
r.reportOnFatalError = true;
|
|
14313
|
+
r.compact = true;
|
|
14314
|
+
} catch {
|
|
14315
|
+
}
|
|
14316
|
+
}
|
|
14317
|
+
|
|
14289
14318
|
// src/daemon/dshVersion.ts
|
|
14290
14319
|
import { readFileSync as readFileSync11, realpathSync as realpathSync5 } from "fs";
|
|
14291
|
-
import { dirname as dirname8, join as
|
|
14320
|
+
import { dirname as dirname8, join as join25 } from "path";
|
|
14292
14321
|
var DSH_HEADLESS_MIN_VERSION = "0.0.1-rc.1";
|
|
14293
14322
|
var DSH_PKG = "@deepseek-ai/dsh";
|
|
14294
14323
|
var DSH_VERSION_UNSUPPORTED = "tool_version_unsupported";
|
|
@@ -14320,7 +14349,7 @@ function dshSupport(d = defaultDeps2) {
|
|
|
14320
14349
|
}
|
|
14321
14350
|
let dir2 = dirname8(real2);
|
|
14322
14351
|
for (let i = 0; i < 4; i++) {
|
|
14323
|
-
const raw = d.readFile(
|
|
14352
|
+
const raw = d.readFile(join25(dir2, "package.json"));
|
|
14324
14353
|
if (raw !== null) {
|
|
14325
14354
|
let pkg = {};
|
|
14326
14355
|
try {
|
|
@@ -14838,17 +14867,17 @@ function startWakeServer(path, hub, secret, platform = process.platform) {
|
|
|
14838
14867
|
|
|
14839
14868
|
// src/core/appended.ts
|
|
14840
14869
|
import { chmodSync as chmodSync4, mkdirSync as mkdirSync11, readFileSync as readFileSync12, renameSync as renameSync8, rmSync as rmSync9, writeFileSync as writeFileSync10 } from "fs";
|
|
14841
|
-
import { join as
|
|
14870
|
+
import { join as join26 } from "path";
|
|
14842
14871
|
var APPENDED_MAX_ITEMS = 5;
|
|
14843
14872
|
var APPENDED_TEXT_MAX = 500;
|
|
14844
14873
|
var APPENDED_RESULT_MAX = 800;
|
|
14845
14874
|
var APPENDED_TTL_MS = 7 * 24 * 60 * 60 * 1e3;
|
|
14846
14875
|
function appendedDir() {
|
|
14847
|
-
return
|
|
14876
|
+
return join26(deskbuddyHome(), "appended");
|
|
14848
14877
|
}
|
|
14849
14878
|
function appendedPath(sessionId) {
|
|
14850
14879
|
const safe2 = sessionId.replace(/[^A-Za-z0-9_-]/g, "");
|
|
14851
|
-
return safe2 ?
|
|
14880
|
+
return safe2 ? join26(appendedDir(), `${safe2}.json`) : null;
|
|
14852
14881
|
}
|
|
14853
14882
|
function readAppended(sessionId) {
|
|
14854
14883
|
const file = appendedPath(sessionId);
|
|
@@ -14878,13 +14907,13 @@ function writeAppended(sessionId, item) {
|
|
|
14878
14907
|
var RUNNING_TTL_MS = 60 * 60 * 1e3;
|
|
14879
14908
|
function runningPath(sessionId) {
|
|
14880
14909
|
const safe2 = sessionId.replace(/[^A-Za-z0-9_-]/g, "");
|
|
14881
|
-
return safe2 ?
|
|
14910
|
+
return safe2 ? join26(deskbuddyHome(), "running", `${safe2}.json`) : null;
|
|
14882
14911
|
}
|
|
14883
14912
|
function writeRunning(sessionId, now = Date.now()) {
|
|
14884
14913
|
const file = runningPath(sessionId);
|
|
14885
14914
|
if (!file) return;
|
|
14886
14915
|
try {
|
|
14887
|
-
mkdirSync11(
|
|
14916
|
+
mkdirSync11(join26(deskbuddyHome(), "running"), { recursive: true, mode: 448 });
|
|
14888
14917
|
writeFileSync10(`${file}.tmp`, JSON.stringify({ at: now, pid: process.pid }), { encoding: "utf8", mode: 384 });
|
|
14889
14918
|
renameSync8(`${file}.tmp`, file);
|
|
14890
14919
|
} catch {
|
|
@@ -14932,7 +14961,7 @@ function resultSummary(tool, stdout6) {
|
|
|
14932
14961
|
|
|
14933
14962
|
// src/core/dshMailbox.ts
|
|
14934
14963
|
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";
|
|
14935
|
-
import { join as
|
|
14964
|
+
import { join as join27 } from "path";
|
|
14936
14965
|
var DSH_HEARTBEAT_FRESH_MS = 5 * 1e3;
|
|
14937
14966
|
var DSH_ACK_TIMEOUT_MS = 8 * 1e3;
|
|
14938
14967
|
var ACK_POLL_MS = 200;
|
|
@@ -14943,10 +14972,10 @@ var REVOKE_RETRY_MS = 50;
|
|
|
14943
14972
|
var SID_RE2 = /^[A-Za-z0-9_-]{1,128}$/;
|
|
14944
14973
|
var IID_RE = /^[A-Za-z0-9_-]{1,128}$/;
|
|
14945
14974
|
function dshMailboxDir(home = deskbuddyHome(), channel = "dsh") {
|
|
14946
|
-
return
|
|
14975
|
+
return join27(home, channel);
|
|
14947
14976
|
}
|
|
14948
14977
|
function sub(home, name, channel = "dsh") {
|
|
14949
|
-
return
|
|
14978
|
+
return join27(dshMailboxDir(home, channel), name);
|
|
14950
14979
|
}
|
|
14951
14980
|
function ensureDir2(p) {
|
|
14952
14981
|
mkdirSync12(p, { recursive: true, mode: 448 });
|
|
@@ -14986,7 +15015,7 @@ function liveHeartbeats(sessionId, d0 = {}) {
|
|
|
14986
15015
|
if (!/^\d+\.json$/.test(n)) continue;
|
|
14987
15016
|
let hb;
|
|
14988
15017
|
try {
|
|
14989
|
-
hb = JSON.parse(readFileSync13(
|
|
15018
|
+
hb = JSON.parse(readFileSync13(join27(dir2, n), "utf8"));
|
|
14990
15019
|
} catch {
|
|
14991
15020
|
continue;
|
|
14992
15021
|
}
|
|
@@ -15010,8 +15039,8 @@ async function dshDeliver(sessionId, msg, d0 = {}) {
|
|
|
15010
15039
|
const ackDir = sub(d.home, "ack", d.channel);
|
|
15011
15040
|
ensureDir2(inbox);
|
|
15012
15041
|
ensureDir2(ackDir);
|
|
15013
|
-
const letter =
|
|
15014
|
-
const ackPath =
|
|
15042
|
+
const letter = join27(inbox, `${sessionId}.${msg.instructionId}.json`);
|
|
15043
|
+
const ackPath = join27(ackDir, `${msg.instructionId}.json`);
|
|
15015
15044
|
if (existsSync6(letter)) return { ok: false, reason: "inbox_busy" };
|
|
15016
15045
|
const writtenAt = d.now();
|
|
15017
15046
|
writeJson(letter, {
|
|
@@ -15080,7 +15109,7 @@ function dshClearInbox(home = deskbuddyHome(), channel = "dsh") {
|
|
|
15080
15109
|
var DSH_REPLY_WAIT_MS = 4 * 60 * 1e3;
|
|
15081
15110
|
function takeMailboxReply(instructionId, home = deskbuddyHome(), channel = "dsh") {
|
|
15082
15111
|
if (!IID_RE.test(instructionId)) return null;
|
|
15083
|
-
const p =
|
|
15112
|
+
const p = join27(sub(home, "reply", channel), `${instructionId}.json`);
|
|
15084
15113
|
let v = null;
|
|
15085
15114
|
try {
|
|
15086
15115
|
v = JSON.parse(readFileSync13(p, "utf8"));
|
|
@@ -15109,7 +15138,7 @@ function clearDir(inbox) {
|
|
|
15109
15138
|
let n = 0;
|
|
15110
15139
|
for (const f of names) {
|
|
15111
15140
|
try {
|
|
15112
|
-
rmSync10(
|
|
15141
|
+
rmSync10(join27(inbox, f), { force: true });
|
|
15113
15142
|
n++;
|
|
15114
15143
|
} catch {
|
|
15115
15144
|
}
|
|
@@ -15184,7 +15213,7 @@ function defaultCodexAppServers() {
|
|
|
15184
15213
|
}
|
|
15185
15214
|
function opencodeReady() {
|
|
15186
15215
|
const mod = ocPluginModule(installedHookPath());
|
|
15187
|
-
return isOcPluginInstalled(opencodePluginPath()) && existsSync7(mod) && existsSync7(
|
|
15216
|
+
return isOcPluginInstalled(opencodePluginPath()) && existsSync7(mod) && existsSync7(join28(dirname9(mod), "ocTranslate.mjs"));
|
|
15188
15217
|
}
|
|
15189
15218
|
var INTERNAL_PROMPT_MARK = "[DeskBuddy]";
|
|
15190
15219
|
function orphanResumeText(summary2) {
|
|
@@ -15251,6 +15280,7 @@ var Daemon = class {
|
|
|
15251
15280
|
#ws = null;
|
|
15252
15281
|
#idleTimer = null;
|
|
15253
15282
|
#windowScan = null;
|
|
15283
|
+
#memLog = null;
|
|
15254
15284
|
#aliveReport = null;
|
|
15255
15285
|
#reportRetryTimer = null;
|
|
15256
15286
|
#reportRetry = new ReportRetry({
|
|
@@ -15604,6 +15634,10 @@ var Daemon = class {
|
|
|
15604
15634
|
async startGate() {
|
|
15605
15635
|
const port = await this.#gate.start();
|
|
15606
15636
|
log(`\u6388\u6743\u7AEF\u70B9\u5DF2\u5C31\u7EEA 127.0.0.1:${port}`);
|
|
15637
|
+
enableFatalReport(deskbuddyHome());
|
|
15638
|
+
log(`${envLine()}\uFF1B${memLine()}`);
|
|
15639
|
+
this.#memLog = setInterval(() => log(memLine()), MEM_LOG_MS);
|
|
15640
|
+
this.#memLog.unref();
|
|
15607
15641
|
this.#windowScan = setInterval(() => this.#coord.scanWindows(), 1e3);
|
|
15608
15642
|
this.#windowScan.unref();
|
|
15609
15643
|
this.#aliveReport = setInterval(() => void this.#reportAlive(), ALIVE_REPORT_MS);
|
|
@@ -16002,6 +16036,7 @@ var Daemon = class {
|
|
|
16002
16036
|
this.#clearIdle();
|
|
16003
16037
|
if (this.#windowScan) clearInterval(this.#windowScan);
|
|
16004
16038
|
if (this.#aliveReport) clearInterval(this.#aliveReport);
|
|
16039
|
+
if (this.#memLog) clearInterval(this.#memLog);
|
|
16005
16040
|
if (this.#reportRetryTimer) clearInterval(this.#reportRetryTimer);
|
|
16006
16041
|
if (this.#codexTimer) clearInterval(this.#codexTimer);
|
|
16007
16042
|
if (this.#wakeTimer) clearInterval(this.#wakeTimer);
|
|
@@ -16582,8 +16617,8 @@ var Daemon = class {
|
|
|
16582
16617
|
}
|
|
16583
16618
|
const sid = resolveAlias(cmd.sessionId);
|
|
16584
16619
|
for (let p = this.#opening.get(sid); p; p = this.#opening.get(sid)) await p;
|
|
16585
|
-
let
|
|
16586
|
-
this.#opening.set(sid, new Promise((r) =>
|
|
16620
|
+
let release2;
|
|
16621
|
+
this.#opening.set(sid, new Promise((r) => release2 = r));
|
|
16587
16622
|
try {
|
|
16588
16623
|
if (this.#queue.busy(sid)) await this.#queue.enqueue(sid, async () => {
|
|
16589
16624
|
});
|
|
@@ -16605,7 +16640,7 @@ var Daemon = class {
|
|
|
16605
16640
|
await this.#openWindowFor(cmd, sid, r.cwd);
|
|
16606
16641
|
} finally {
|
|
16607
16642
|
this.#opening.delete(sid);
|
|
16608
|
-
|
|
16643
|
+
release2();
|
|
16609
16644
|
}
|
|
16610
16645
|
}
|
|
16611
16646
|
#guiAvailable() {
|
|
@@ -17044,7 +17079,7 @@ var Daemon = class {
|
|
|
17044
17079
|
for (const n of names) {
|
|
17045
17080
|
if (!n.endsWith(".json")) continue;
|
|
17046
17081
|
try {
|
|
17047
|
-
const s2 = JSON.parse(readFileSync14(
|
|
17082
|
+
const s2 = JSON.parse(readFileSync14(join28(stateDir(), n), "utf8"));
|
|
17048
17083
|
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 : "" });
|
|
17049
17084
|
} catch {
|
|
17050
17085
|
}
|
|
@@ -17078,12 +17113,12 @@ var Daemon = class {
|
|
|
17078
17113
|
}
|
|
17079
17114
|
case "deepseek_harness": {
|
|
17080
17115
|
if (!TRANSCRIPT_SID_RE.test(s2.sessionId)) return null;
|
|
17081
|
-
const p =
|
|
17116
|
+
const p = join28(deskbuddyHome(), "dsh", "transcript", `${s2.sessionId}.jsonl`);
|
|
17082
17117
|
return existsSync7(p) ? { kind: "file", path: p, format: "dsh" } : null;
|
|
17083
17118
|
}
|
|
17084
17119
|
case "opencode": {
|
|
17085
17120
|
if (!TRANSCRIPT_SID_RE.test(s2.sessionId)) return null;
|
|
17086
|
-
return snapshotSource(`opencode-snap:${s2.sessionId}`,
|
|
17121
|
+
return snapshotSource(`opencode-snap:${s2.sessionId}`, join28(deskbuddyHome(), "opencode", "transcript", `${s2.sessionId}.json`));
|
|
17087
17122
|
}
|
|
17088
17123
|
case "dumate":
|
|
17089
17124
|
return { kind: "http", key: "dumate", fetch: () => this.#dumate.transcriptMessages(s2.sessionId) };
|
|
@@ -18031,7 +18066,7 @@ async function runDisable(tools, opts = {}) {
|
|
|
18031
18066
|
|
|
18032
18067
|
// src/commands/install.ts
|
|
18033
18068
|
import { mkdirSync as mkdirSync13, rmSync as rmSync12, writeFileSync as writeFileSync13 } from "fs";
|
|
18034
|
-
import { join as
|
|
18069
|
+
import { join as join29 } from "path";
|
|
18035
18070
|
|
|
18036
18071
|
// src/commands/setup.ts
|
|
18037
18072
|
import qrcode from "qrcode-terminal";
|
|
@@ -18248,7 +18283,7 @@ function inSandbox(env) {
|
|
|
18248
18283
|
}
|
|
18249
18284
|
function precheckWritable(dirs) {
|
|
18250
18285
|
for (const dir2 of dirs) {
|
|
18251
|
-
const probe =
|
|
18286
|
+
const probe = join29(dir2, `.deskbuddy-probe-${process.pid}`);
|
|
18252
18287
|
try {
|
|
18253
18288
|
mkdirSync13(dir2, { recursive: true });
|
|
18254
18289
|
writeFileSync13(probe, "");
|
|
@@ -18658,7 +18693,7 @@ async function runAutoSwitch(opts) {
|
|
|
18658
18693
|
// src/wrap/run.ts
|
|
18659
18694
|
import { randomBytes as randomBytes10 } from "crypto";
|
|
18660
18695
|
import { appendFileSync as appendFileSync2, mkdirSync as mkdirSync15, statSync as statSync16, truncateSync as truncateSync3 } from "fs";
|
|
18661
|
-
import { join as
|
|
18696
|
+
import { join as join32 } from "path";
|
|
18662
18697
|
import { StringDecoder as StringDecoder2 } from "string_decoder";
|
|
18663
18698
|
|
|
18664
18699
|
// src/wrap/injector.ts
|
|
@@ -19158,7 +19193,7 @@ import { execFileSync as execFileSync4, spawn as spawn4 } from "child_process";
|
|
|
19158
19193
|
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";
|
|
19159
19194
|
import { request as request2 } from "http";
|
|
19160
19195
|
import { createServer as createServer5 } from "net";
|
|
19161
|
-
import { join as
|
|
19196
|
+
import { join as join30 } from "path";
|
|
19162
19197
|
var TAKEOVER_VALUE_OPTS = /* @__PURE__ */ new Set(["-c", "--config", "-m", "--model", "-s", "--sandbox", "-a", "--ask-for-approval", "-C", "--cd", "--enable", "--disable"]);
|
|
19163
19198
|
var SHORT_VALUE_OPTS = /* @__PURE__ */ new Set(["-c", "-m", "-s", "-a", "-C"]);
|
|
19164
19199
|
var TAKEOVER_FLAGS = /* @__PURE__ */ new Set(["--search", "--no-alt-screen", "--dangerously-bypass-approvals-and-sandbox"]);
|
|
@@ -19327,10 +19362,10 @@ async function runCodexWindow(real2, args, d) {
|
|
|
19327
19362
|
}
|
|
19328
19363
|
var MAX_LOG_BYTES2 = 1024 * 1024;
|
|
19329
19364
|
function appServerLogPath() {
|
|
19330
|
-
return
|
|
19365
|
+
return join30(deskbuddyHome(), "codex-app-server.log");
|
|
19331
19366
|
}
|
|
19332
19367
|
function windowsDir2() {
|
|
19333
|
-
return
|
|
19368
|
+
return join30(deskbuddyHome(), "codex-windows");
|
|
19334
19369
|
}
|
|
19335
19370
|
function pidAlive5(pid) {
|
|
19336
19371
|
try {
|
|
@@ -19420,7 +19455,7 @@ function killOwnServer(pid, url, args = freshArgs, kill = (p) => killServer(p))
|
|
|
19420
19455
|
if (a && isAppServerArgs(a, url)) kill(pid);
|
|
19421
19456
|
}
|
|
19422
19457
|
function recordPath(rec) {
|
|
19423
|
-
return
|
|
19458
|
+
return join30(windowsDir2(), `${rec.server}.json`);
|
|
19424
19459
|
}
|
|
19425
19460
|
function writeRecord(rec) {
|
|
19426
19461
|
try {
|
|
@@ -19445,7 +19480,7 @@ function reapWindows(dir2 = windowsDir2(), alive = pidAlive5, args = freshArgs,
|
|
|
19445
19480
|
return;
|
|
19446
19481
|
}
|
|
19447
19482
|
for (const n of names) {
|
|
19448
|
-
const p =
|
|
19483
|
+
const p = join30(dir2, n);
|
|
19449
19484
|
let rec;
|
|
19450
19485
|
try {
|
|
19451
19486
|
rec = JSON.parse(readFileSync16(p, "utf8"));
|
|
@@ -19468,7 +19503,7 @@ function reapWindows(dir2 = windowsDir2(), alive = pidAlive5, args = freshArgs,
|
|
|
19468
19503
|
function launchLog(line) {
|
|
19469
19504
|
try {
|
|
19470
19505
|
mkdirSync14(deskbuddyHome(), { recursive: true });
|
|
19471
|
-
writeFileSync14(
|
|
19506
|
+
writeFileSync14(join30(deskbuddyHome(), "wrap.log"), `${(/* @__PURE__ */ new Date()).toISOString()} [codex ${process.pid}] ${line}
|
|
19472
19507
|
`, { flag: "a" });
|
|
19473
19508
|
} catch {
|
|
19474
19509
|
}
|
|
@@ -19493,7 +19528,7 @@ function defaultCodexWindowDeps(base) {
|
|
|
19493
19528
|
// src/wrap/promptLaunch.ts
|
|
19494
19529
|
import { randomBytes as randomBytes8, randomUUID as randomUUID3 } from "crypto";
|
|
19495
19530
|
import { homedir as homedir10 } from "os";
|
|
19496
|
-
import { join as
|
|
19531
|
+
import { join as join31 } from "path";
|
|
19497
19532
|
var FIRST_LINE_WAIT_MS2 = 2 * 60 * 1e3;
|
|
19498
19533
|
function promptArgs(text, tool = "") {
|
|
19499
19534
|
if (!text) return [];
|
|
@@ -19503,7 +19538,7 @@ function promptArgs(text, tool = "") {
|
|
|
19503
19538
|
function promptEnv(tool, env, launcherPid = process.pid, answerWindowId = "") {
|
|
19504
19539
|
const out = { ...env };
|
|
19505
19540
|
if (tool === "workbuddy") {
|
|
19506
|
-
if (!out.CODEBUDDY_CONFIG_DIR) out.CODEBUDDY_CONFIG_DIR =
|
|
19541
|
+
if (!out.CODEBUDDY_CONFIG_DIR) out.CODEBUDDY_CONFIG_DIR = join31(homedir10(), ".workbuddy");
|
|
19507
19542
|
out.DESKBUDDY_LAUNCHER_PID = String(launcherPid);
|
|
19508
19543
|
out.DESKBUDDY_LAUNCHED_TOOL = tool;
|
|
19509
19544
|
if (answerWindowId) out.DESKBUDDY_LAUNCHED_WINDOW = answerWindowId;
|
|
@@ -20087,7 +20122,7 @@ function childEnv(src, windowId, takeover = true) {
|
|
|
20087
20122
|
}
|
|
20088
20123
|
function wrapLog(line) {
|
|
20089
20124
|
try {
|
|
20090
|
-
const p =
|
|
20125
|
+
const p = join32(deskbuddyHome(), "wrap.log");
|
|
20091
20126
|
mkdirSync15(deskbuddyHome(), { recursive: true });
|
|
20092
20127
|
try {
|
|
20093
20128
|
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
|
@@ -380,3 +380,81 @@
|
|
|
380
380
|
2026-10-12T11:00:13.614Z [opencode] opencode 版本 读不出:带说明的拒绝不走新路由
|
|
381
381
|
2026-10-12T11:00:13.614Z [opencode] opencode 版本不确定支持带说明的拒绝,设备上的反馈没有带给模型
|
|
382
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 没法在设备上回答(后台任务不能提问,请在电脑上继续),已拒绝
|
|
407
|
+
2026-10-12T11:53:44.348Z [opencode] 回填选择题失败:/question/{requestID}/reply status=404
|
|
408
|
+
2026-10-12T11:53:49.284Z [opencode] opencode 版本 1.4.3:带说明的拒绝不走新路由
|
|
409
|
+
2026-10-12T11:53:49.284Z [opencode] opencode 版本不确定支持带说明的拒绝,设备上的反馈没有带给模型
|
|
410
|
+
2026-10-12T11:53:49.446Z [opencode] opencode 版本 读不出:带说明的拒绝不走新路由
|
|
411
|
+
2026-10-12T11:53:49.446Z [opencode] opencode 版本不确定支持带说明的拒绝,设备上的反馈没有带给模型
|
|
412
|
+
2026-10-12T11:53:51.984Z [opencode] 后台任务里的提问 que_r2 没法在设备上回答(后台任务不能提问,请在电脑上继续),已拒绝
|
|
413
|
+
2026-10-12T12:01:16.233Z [opencode] 回填选择题失败:/question/{requestID}/reply status=404
|
|
414
|
+
2026-10-12T12:01:21.165Z [opencode] opencode 版本 1.4.3:带说明的拒绝不走新路由
|
|
415
|
+
2026-10-12T12:01:21.165Z [opencode] opencode 版本不确定支持带说明的拒绝,设备上的反馈没有带给模型
|
|
416
|
+
2026-10-12T12:01:21.273Z [opencode] opencode 版本 读不出:带说明的拒绝不走新路由
|
|
417
|
+
2026-10-12T12:01:21.273Z [opencode] opencode 版本不确定支持带说明的拒绝,设备上的反馈没有带给模型
|
|
418
|
+
2026-10-12T12:01:23.792Z [opencode] 后台任务里的提问 que_r2 没法在设备上回答(后台任务不能提问,请在电脑上继续),已拒绝
|
|
419
|
+
2026-10-12T13:26:01.239Z [opencode] 回填选择题失败:/question/{requestID}/reply status=404
|
|
420
|
+
2026-10-12T13:26:06.166Z [opencode] opencode 版本 1.4.3:带说明的拒绝不走新路由
|
|
421
|
+
2026-10-12T13:26:06.166Z [opencode] opencode 版本不确定支持带说明的拒绝,设备上的反馈没有带给模型
|
|
422
|
+
2026-10-12T13:26:06.274Z [opencode] opencode 版本 读不出:带说明的拒绝不走新路由
|
|
423
|
+
2026-10-12T13:26:06.274Z [opencode] opencode 版本不确定支持带说明的拒绝,设备上的反馈没有带给模型
|
|
424
|
+
2026-10-12T13:26:08.595Z [opencode] 后台任务里的提问 que_r2 没法在设备上回答(后台任务不能提问,请在电脑上继续),已拒绝
|
|
425
|
+
2026-10-12T13:31:47.167Z [opencode] 回填选择题失败:/question/{requestID}/reply status=404
|
|
426
|
+
2026-10-12T13:31:52.410Z [opencode] opencode 版本 1.4.3:带说明的拒绝不走新路由
|
|
427
|
+
2026-10-12T13:31:52.410Z [opencode] opencode 版本不确定支持带说明的拒绝,设备上的反馈没有带给模型
|
|
428
|
+
2026-10-12T13:31:52.546Z [opencode] opencode 版本 读不出:带说明的拒绝不走新路由
|
|
429
|
+
2026-10-12T13:31:52.547Z [opencode] opencode 版本不确定支持带说明的拒绝,设备上的反馈没有带给模型
|
|
430
|
+
2026-10-12T13:31:57.168Z [opencode] 后台任务里的提问 que_r2 没法在设备上回答(后台任务不能提问,请在电脑上继续),已拒绝
|
|
431
|
+
2026-10-12T13:54:05.955Z [opencode] 回填选择题失败:/question/{requestID}/reply status=404
|
|
432
|
+
2026-10-12T13:54:10.941Z [opencode] opencode 版本 1.4.3:带说明的拒绝不走新路由
|
|
433
|
+
2026-10-12T13:54:10.941Z [opencode] opencode 版本不确定支持带说明的拒绝,设备上的反馈没有带给模型
|
|
434
|
+
2026-10-12T13:54:11.055Z [opencode] opencode 版本 读不出:带说明的拒绝不走新路由
|
|
435
|
+
2026-10-12T13:54:11.055Z [opencode] opencode 版本不确定支持带说明的拒绝,设备上的反馈没有带给模型
|
|
436
|
+
2026-10-12T13:54:13.799Z [opencode] 后台任务里的提问 que_r2 没法在设备上回答(后台任务不能提问,请在电脑上继续),已拒绝
|
|
437
|
+
2026-10-12T13:55:46.760Z [opencode] 回填选择题失败:/question/{requestID}/reply status=404
|
|
438
|
+
2026-10-12T13:55:51.749Z [opencode] opencode 版本 1.4.3:带说明的拒绝不走新路由
|
|
439
|
+
2026-10-12T13:55:51.749Z [opencode] opencode 版本不确定支持带说明的拒绝,设备上的反馈没有带给模型
|
|
440
|
+
2026-10-12T13:55:51.883Z [opencode] opencode 版本 读不出:带说明的拒绝不走新路由
|
|
441
|
+
2026-10-12T13:55:51.883Z [opencode] opencode 版本不确定支持带说明的拒绝,设备上的反馈没有带给模型
|
|
442
|
+
2026-10-12T13:55:54.538Z [opencode] 后台任务里的提问 que_r2 没法在设备上回答(后台任务不能提问,请在电脑上继续),已拒绝
|
|
443
|
+
2026-10-12T13:57:26.485Z [opencode] 回填选择题失败:/question/{requestID}/reply status=404
|
|
444
|
+
2026-10-12T13:57:31.490Z [opencode] opencode 版本 1.4.3:带说明的拒绝不走新路由
|
|
445
|
+
2026-10-12T13:57:31.490Z [opencode] opencode 版本不确定支持带说明的拒绝,设备上的反馈没有带给模型
|
|
446
|
+
2026-10-12T13:57:31.589Z [opencode] opencode 版本 读不出:带说明的拒绝不走新路由
|
|
447
|
+
2026-10-12T13:57:31.589Z [opencode] opencode 版本不确定支持带说明的拒绝,设备上的反馈没有带给模型
|
|
448
|
+
2026-10-12T13:57:34.003Z [opencode] 后台任务里的提问 que_r2 没法在设备上回答(后台任务不能提问,请在电脑上继续),已拒绝
|
|
449
|
+
2026-10-12T14:45:07.654Z [opencode] 回填选择题失败:/question/{requestID}/reply status=404
|
|
450
|
+
2026-10-12T14:45:13.017Z [opencode] opencode 版本 1.4.3:带说明的拒绝不走新路由
|
|
451
|
+
2026-10-12T14:45:13.017Z [opencode] opencode 版本不确定支持带说明的拒绝,设备上的反馈没有带给模型
|
|
452
|
+
2026-10-12T14:45:13.185Z [opencode] opencode 版本 读不出:带说明的拒绝不走新路由
|
|
453
|
+
2026-10-12T14:45:13.186Z [opencode] opencode 版本不确定支持带说明的拒绝,设备上的反馈没有带给模型
|
|
454
|
+
2026-10-12T14:45:17.825Z [opencode] 后台任务里的提问 que_r2 没法在设备上回答(后台任务不能提问,请在电脑上继续),已拒绝
|
|
455
|
+
2026-10-12T14:48:34.229Z [opencode] 回填选择题失败:/question/{requestID}/reply status=404
|
|
456
|
+
2026-10-12T14:48:39.288Z [opencode] opencode 版本 1.4.3:带说明的拒绝不走新路由
|
|
457
|
+
2026-10-12T14:48:39.288Z [opencode] opencode 版本不确定支持带说明的拒绝,设备上的反馈没有带给模型
|
|
458
|
+
2026-10-12T14:48:39.435Z [opencode] opencode 版本 读不出:带说明的拒绝不走新路由
|
|
459
|
+
2026-10-12T14:48:39.435Z [opencode] opencode 版本不确定支持带说明的拒绝,设备上的反馈没有带给模型
|
|
460
|
+
2026-10-12T14:48:43.024Z [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(
|