deskbuddy 0.2.0-beta.4 → 0.2.0-beta.6
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 +84 -12
- package/dist/hook.cjs +29 -3
- package/hook/hook.cjs +29 -3
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -3873,9 +3873,17 @@ function tooLongToInject(text) {
|
|
|
3873
3873
|
function prepare(text) {
|
|
3874
3874
|
return " " + oneLine(text);
|
|
3875
3875
|
}
|
|
3876
|
+
var TRUST_RE = /trust (the files in )?this folder|Yes, I trust this folder/i;
|
|
3877
|
+
var PLACEHOLDER_RE = /^Try ".*"$/;
|
|
3878
|
+
function isTerminalReport(s) {
|
|
3879
|
+
if (!s) return true;
|
|
3880
|
+
const rest = s.replace(/\x1b\[[IO]/g, "").replace(/\x1b\[\d+;\d+R/g, "").replace(/\x1b\[[?>][\d;]*\$?[a-zA-Z]/g, "").replace(/\x1b\][^\x07\x1b]*(?:\x07|\x1b\\)/g, "").replace(/\x1bP[^\x1b]*\x1b\\/g, "");
|
|
3881
|
+
return rest === "";
|
|
3882
|
+
}
|
|
3876
3883
|
var PERMISSION_RE = /Do you want to (proceed|make|create|run|allow)|[❯>]\s*1\. Yes/;
|
|
3877
|
-
function inspectLines(lines2) {
|
|
3884
|
+
function inspectLines(lines2, undimmed) {
|
|
3878
3885
|
const text = lines2.join("\n");
|
|
3886
|
+
const trust = TRUST_RE.test(text);
|
|
3879
3887
|
const permission = PERMISSION_RE.test(text);
|
|
3880
3888
|
const rules = [];
|
|
3881
3889
|
lines2.forEach((l, i) => {
|
|
@@ -3885,9 +3893,13 @@ function inspectLines(lines2) {
|
|
|
3885
3893
|
if (rules.length >= 2) {
|
|
3886
3894
|
const a = rules[rules.length - 2];
|
|
3887
3895
|
const b = rules[rules.length - 1];
|
|
3888
|
-
|
|
3896
|
+
const src = undimmed && undimmed.length === lines2.length ? undimmed : lines2;
|
|
3897
|
+
input = src.slice(a + 1, b).map((l) => l.trimEnd()).join("\n").replace(/^\s*[❯>]\s?/, "").trimEnd();
|
|
3889
3898
|
}
|
|
3890
|
-
|
|
3899
|
+
const v = { permission, input };
|
|
3900
|
+
if (trust) v.trust = true;
|
|
3901
|
+
if (input !== null && PLACEHOLDER_RE.test(input.trim())) v.placeholder = true;
|
|
3902
|
+
return v;
|
|
3891
3903
|
}
|
|
3892
3904
|
function boxMatches(view, body) {
|
|
3893
3905
|
if (view.input === null || view.permission) return false;
|
|
@@ -4590,6 +4602,8 @@ var PING_FRAME_SERVICE = "ping";
|
|
|
4590
4602
|
var PONG_FRAME = JSON.stringify({ service: "pong", version: "1.0" });
|
|
4591
4603
|
var CODEX_REMOTE_REFRESH_MS = 30 * 1e3;
|
|
4592
4604
|
var CLAIM_WAIT_MS = 15 * 1e3;
|
|
4605
|
+
var FIRST_LINE_WAIT_MS = 10 * 60 * 1e3;
|
|
4606
|
+
var FIRST_LINE_CAP_MS = 18 * 60 * 1e3;
|
|
4593
4607
|
var WINDOW_READY_MS = 30 * 1e3;
|
|
4594
4608
|
var CODEX_WATCH_MS = 5 * 1e3;
|
|
4595
4609
|
function defaultCodexAppServers() {
|
|
@@ -4973,6 +4987,7 @@ var Daemon = class {
|
|
|
4973
4987
|
return;
|
|
4974
4988
|
}
|
|
4975
4989
|
this.#seen.save();
|
|
4990
|
+
cmd.receivedAt = this.#deps.now();
|
|
4976
4991
|
this.#route(cmd);
|
|
4977
4992
|
}
|
|
4978
4993
|
/**
|
|
@@ -5113,9 +5128,9 @@ var Daemon = class {
|
|
|
5113
5128
|
* 返回时:窗口已收下首句 / 已按失败回报 / 降级的后台那次已跑完
|
|
5114
5129
|
*/
|
|
5115
5130
|
async #openWindowFor(cmd, sid, cwd) {
|
|
5116
|
-
const headless = (why) => {
|
|
5131
|
+
const headless = (why, c = cmd) => {
|
|
5117
5132
|
log(`\u6307\u4EE4 ${cmd.instructionId} ${why}\uFF0C\u6539\u540E\u53F0\u6267\u884C`);
|
|
5118
|
-
return this.#headless(
|
|
5133
|
+
return this.#headless(c, sid, cwd);
|
|
5119
5134
|
};
|
|
5120
5135
|
let gui = false;
|
|
5121
5136
|
try {
|
|
@@ -5130,6 +5145,7 @@ var Daemon = class {
|
|
|
5130
5145
|
} catch (e) {
|
|
5131
5146
|
return headless(`\u5199\u4EA4\u63A5\u5355\u5931\u8D25\uFF1A${e.message}`);
|
|
5132
5147
|
}
|
|
5148
|
+
const openedAt = this.#deps.now();
|
|
5133
5149
|
try {
|
|
5134
5150
|
await this.#deps.openWindow(runId);
|
|
5135
5151
|
} catch (e) {
|
|
@@ -5145,7 +5161,9 @@ var Daemon = class {
|
|
|
5145
5161
|
removeCommandFile(deskbuddyHome(), runId);
|
|
5146
5162
|
if (launchPending(runId) && claimLaunch(runId)) return headless(`${Math.round(this.#deps.claimWaitMs / 1e3)} \u79D2\u5185\u7A97\u53E3\u6CA1\u8D77\u6765`);
|
|
5147
5163
|
log(`\u6307\u4EE4 ${cmd.instructionId} \u7A97\u53E3\u5DF2\u8D77\u6765\uFF08${runId.slice(0, 8)}\uFF09\uFF0C\u7B49\u5B83\u5C31\u7EEA\u540E\u5199\u5165\u9996\u53E5`);
|
|
5148
|
-
const
|
|
5164
|
+
const firstLineExpire = Math.min(Math.max(cmd.expireAt, openedAt + FIRST_LINE_WAIT_MS), (cmd.receivedAt ?? openedAt) + FIRST_LINE_CAP_MS);
|
|
5165
|
+
const wcmd = cmd.expireAt > 0 ? { ...cmd, expireAt: Math.max(cmd.expireAt, firstLineExpire) } : cmd;
|
|
5166
|
+
const req2 = { instructionId: cmd.instructionId, sessionId: "", text: cmd.text, expireAt: wcmd.expireAt };
|
|
5149
5167
|
const fb = { p: null, final: false };
|
|
5150
5168
|
const readyDeadline = this.#deps.now() + this.#deps.windowReadyMs;
|
|
5151
5169
|
while (this.#deps.now() < readyDeadline) {
|
|
@@ -5167,7 +5185,7 @@ var Daemon = class {
|
|
|
5167
5185
|
if (r === "handled") {
|
|
5168
5186
|
log(`\u6307\u4EE4 ${cmd.instructionId} \u9996\u53E5\u5DF2\u4EA4\u7ED9\u65B0\u7A97\u53E3 len=${cmd.text.length}`);
|
|
5169
5187
|
if (sid) {
|
|
5170
|
-
const left =
|
|
5188
|
+
const left = wcmd.expireAt > 0 ? Math.max(wcmd.expireAt - this.#deps.now(), 0) : FIRST_LINE_WAIT_MS;
|
|
5171
5189
|
const cap = this.#deps.now() + left + this.#deps.windowReadyMs;
|
|
5172
5190
|
while (!fb.final && this.#deps.now() < cap) {
|
|
5173
5191
|
let found = null;
|
|
@@ -6300,6 +6318,31 @@ function screenLines(term) {
|
|
|
6300
6318
|
}
|
|
6301
6319
|
return out;
|
|
6302
6320
|
}
|
|
6321
|
+
function screenLinesUndimmed(term) {
|
|
6322
|
+
const b = term.buffer.active;
|
|
6323
|
+
const out = [];
|
|
6324
|
+
for (let i = b.viewportY; i < b.viewportY + term.rows; i++) {
|
|
6325
|
+
const l = b.getLine(i);
|
|
6326
|
+
if (!l) {
|
|
6327
|
+
out.push("");
|
|
6328
|
+
continue;
|
|
6329
|
+
}
|
|
6330
|
+
if (typeof l.getCell !== "function") {
|
|
6331
|
+
out.push(l.translateToString(true));
|
|
6332
|
+
continue;
|
|
6333
|
+
}
|
|
6334
|
+
let s = "";
|
|
6335
|
+
for (let x = 0; x < term.cols; x++) {
|
|
6336
|
+
const c = l.getCell(x);
|
|
6337
|
+
if (!c) break;
|
|
6338
|
+
const w = c.getWidth();
|
|
6339
|
+
if (w === 0) continue;
|
|
6340
|
+
s += c.isDim() ? " ".repeat(w) : c.getChars() || " ";
|
|
6341
|
+
}
|
|
6342
|
+
out.push(s.trimEnd());
|
|
6343
|
+
}
|
|
6344
|
+
return out;
|
|
6345
|
+
}
|
|
6303
6346
|
|
|
6304
6347
|
// src/commands/enable.ts
|
|
6305
6348
|
function stdout(line) {
|
|
@@ -6676,6 +6719,8 @@ async function runSetup(opts = {}) {
|
|
|
6676
6719
|
bound.remoteCwd = remoteCwd;
|
|
6677
6720
|
saveConfig(bound);
|
|
6678
6721
|
out(` \u5DF2\u8BB0\u4E0B\uFF1A${remoteCwd}`);
|
|
6722
|
+
out(" \u63D0\u793A\uFF1AClaude Code \u7B2C\u4E00\u6B21\u5728\u65B0\u76EE\u5F55\u91CC\u542F\u52A8\u4F1A\u95EE\u300C\u662F\u5426\u4FE1\u4EFB\u6B64\u6587\u4EF6\u5939\u300D\uFF0CDeskBuddy \u4E0D\u4F1A\u66FF\u4F60\u56DE\u7B54\u3002");
|
|
6723
|
+
out(" \u5EFA\u8BAE\u5148\u5728\u8FD9\u4E2A\u76EE\u5F55\u91CC\u624B\u52A8\u8FD0\u884C\u4E00\u6B21 claude \u5E76\u9009\u62E9\u4FE1\u4EFB\uFF0C\u5426\u5219\u65B0\u5EFA\u4EFB\u52A1\u8981\u7B49\u4F60\u5728\u7535\u8111\u4E0A\u70B9\u4E86\u624D\u4F1A\u5F00\u59CB\u3002");
|
|
6679
6724
|
out("");
|
|
6680
6725
|
out("\u2705 \u6388\u6743\u6210\u529F\u3002\u63A5\u4E0B\u6765\u8FD0\u884C\uFF1Adeskbuddy enable claude_code,codex");
|
|
6681
6726
|
return 0;
|
|
@@ -7079,6 +7124,13 @@ var Injector = class {
|
|
|
7079
7124
|
#lastKey = 0;
|
|
7080
7125
|
#busy = false;
|
|
7081
7126
|
#closed = false;
|
|
7127
|
+
#lastWhy = "";
|
|
7128
|
+
/** 用户在这个窗口里按过键(终端报告不算) */
|
|
7129
|
+
#userKeyed = false;
|
|
7130
|
+
/** 本窗口已经写进过一条 */
|
|
7131
|
+
#injectedOnce = false;
|
|
7132
|
+
/** 出现过信任框:之后窗口关闭多半是用户选了 No / 不想在这个目录里跑,不能交回后台 */
|
|
7133
|
+
#sawTrust = false;
|
|
7082
7134
|
/** instructionId → 那一条(去重:daemon 等 ack 超时重送时,不能写进窗口两遍) */
|
|
7083
7135
|
#seen = /* @__PURE__ */ new Map();
|
|
7084
7136
|
constructor(deps) {
|
|
@@ -7088,8 +7140,11 @@ var Injector = class {
|
|
|
7088
7140
|
noteOutput() {
|
|
7089
7141
|
this.#lastOut = this.#d.now();
|
|
7090
7142
|
}
|
|
7091
|
-
|
|
7143
|
+
/** 终端输入(不传 = 按一次真按键算)。只有终端自己回的报告(焦点进出、光标位置等)不算按键 */
|
|
7144
|
+
noteKey(input) {
|
|
7145
|
+
if (input !== void 0 && isTerminalReport(input)) return;
|
|
7092
7146
|
this.#lastKey = this.#d.now();
|
|
7147
|
+
this.#userKeyed = true;
|
|
7093
7148
|
}
|
|
7094
7149
|
get pending() {
|
|
7095
7150
|
return this.#queue.length;
|
|
@@ -7100,6 +7155,7 @@ var Injector = class {
|
|
|
7100
7155
|
if (i >= 0) this.#queue.splice(i, 1);
|
|
7101
7156
|
if (job.done) return;
|
|
7102
7157
|
job.done = true;
|
|
7158
|
+
this.#d.log?.(`\u7ED3\u5C40 ${job.instructionId} ${msg.status}${msg.reason ? "/" + msg.reason : ""}`);
|
|
7103
7159
|
job.finalMsg = { instructionId: job.instructionId, ...msg };
|
|
7104
7160
|
job.reply(job.finalMsg);
|
|
7105
7161
|
}
|
|
@@ -7160,6 +7216,7 @@ var Injector = class {
|
|
|
7160
7216
|
return;
|
|
7161
7217
|
}
|
|
7162
7218
|
this.#remember(job);
|
|
7219
|
+
this.#d.log?.(`\u6536\u5230 ${job.instructionId} len=${job.text.length} head=${JSON.stringify(job.text.slice(0, 12))}`);
|
|
7163
7220
|
job.phase = "queued";
|
|
7164
7221
|
job.reply({ instructionId: job.instructionId, status: "ack" });
|
|
7165
7222
|
this.#queue.push(job);
|
|
@@ -7176,10 +7233,15 @@ var Injector = class {
|
|
|
7176
7233
|
const why = [];
|
|
7177
7234
|
const v = this.#d.view();
|
|
7178
7235
|
if (!v) return ["no-screen"];
|
|
7236
|
+
const last = this.#d.lastEvent();
|
|
7237
|
+
if (v.trust) {
|
|
7238
|
+
why.push("trust-dialog");
|
|
7239
|
+
this.#sawTrust = true;
|
|
7240
|
+
}
|
|
7179
7241
|
if (v.permission) why.push("permission");
|
|
7242
|
+
const pristine = this.#d.startupIdle === true && !this.#userKeyed && !this.#injectedOnce && !last;
|
|
7180
7243
|
if (v.input === null) why.push("no-input-box");
|
|
7181
|
-
else if (v.input !== "") why.push("user-typing");
|
|
7182
|
-
const last = this.#d.lastEvent();
|
|
7244
|
+
else if (v.input !== "" && !v.placeholder && !pristine) why.push("user-typing");
|
|
7183
7245
|
if (!last) {
|
|
7184
7246
|
if (!this.#d.startupIdle) why.push("hook:none");
|
|
7185
7247
|
} else if (!IDLE_EVENTS.has(last.ev)) why.push("hook:" + last.ev);
|
|
@@ -7215,9 +7277,16 @@ var Injector = class {
|
|
|
7215
7277
|
this.#d.log?.(`\u7A7A\u95F2\u5224\u65AD\u51FA\u9519\uFF1A${e.message}`);
|
|
7216
7278
|
}
|
|
7217
7279
|
if (why.length > 0) {
|
|
7280
|
+
const key = why.join(",");
|
|
7281
|
+
if (key !== this.#lastWhy) {
|
|
7282
|
+
this.#lastWhy = key;
|
|
7283
|
+
const v = this.#d.view();
|
|
7284
|
+
this.#d.log?.(`\u6392\u961F\u4E2D\uFF1A${key} input=${v?.input === null ? "null" : JSON.stringify((v?.input ?? "").slice(0, 12)) + "/" + (v?.input ?? "").length} last=${this.#d.lastEvent()?.ev ?? "none"}`);
|
|
7285
|
+
}
|
|
7218
7286
|
this.#markQueued(job);
|
|
7219
7287
|
return;
|
|
7220
7288
|
}
|
|
7289
|
+
this.#lastWhy = "";
|
|
7221
7290
|
this.#busy = true;
|
|
7222
7291
|
try {
|
|
7223
7292
|
await this.#inject(job);
|
|
@@ -7230,6 +7299,8 @@ var Injector = class {
|
|
|
7230
7299
|
}
|
|
7231
7300
|
async #inject(job) {
|
|
7232
7301
|
const body = prepare(job.text);
|
|
7302
|
+
this.#injectedOnce = true;
|
|
7303
|
+
this.#d.log?.(`\u5199\u5165 ${job.instructionId} len=${body.length - 1}`);
|
|
7233
7304
|
let enterAt = 0;
|
|
7234
7305
|
this.#d.holdKeys(true);
|
|
7235
7306
|
try {
|
|
@@ -7276,6 +7347,7 @@ var Injector = class {
|
|
|
7276
7347
|
for (const job of [...this.#queue]) {
|
|
7277
7348
|
if (job.phase === "entered") this.#final(job, { status: "started", mode: "window", reason: "" });
|
|
7278
7349
|
else if (job.phase === "pasted") this.#final(job, { status: "failed", mode: "window", reason: "window_failed" });
|
|
7350
|
+
else if (this.#sawTrust) this.#final(job, { status: "rejected", mode: "window", reason: "window_failed" });
|
|
7279
7351
|
else this.#final(job, { status: "not_here" });
|
|
7280
7352
|
}
|
|
7281
7353
|
}
|
|
@@ -7951,7 +8023,7 @@ function runAttached(tool, p, term0, windowId, deps, launched = false) {
|
|
|
7951
8023
|
};
|
|
7952
8024
|
const injector = new Injector({
|
|
7953
8025
|
now: () => Date.now(),
|
|
7954
|
-
view: () => term ? inspectLines(screenLines(term)) : null,
|
|
8026
|
+
view: () => term ? inspectLines(screenLines(term), screenLinesUndimmed(term)) : null,
|
|
7955
8027
|
lastEvent: () => readLastEvent(windowId),
|
|
7956
8028
|
write: toPty,
|
|
7957
8029
|
sleep: (ms) => new Promise((r) => setTimeout(r, ms)),
|
|
@@ -7982,8 +8054,8 @@ function runAttached(tool, p, term0, windowId, deps, launched = false) {
|
|
|
7982
8054
|
injector.noteOutput();
|
|
7983
8055
|
});
|
|
7984
8056
|
const onStdin = (b) => {
|
|
7985
|
-
injector.noteKey();
|
|
7986
8057
|
const s = typeof b === "string" ? b : decoder.write(b);
|
|
8058
|
+
injector.noteKey(s);
|
|
7987
8059
|
if (!s) return;
|
|
7988
8060
|
if (held) held.push(s);
|
|
7989
8061
|
else toPty(s);
|
package/dist/hook.cjs
CHANGED
|
@@ -1077,6 +1077,19 @@ function codexRemoteHas(sessionId) {
|
|
|
1077
1077
|
return !!r && r.has(sessionId)
|
|
1078
1078
|
}
|
|
1079
1079
|
|
|
1080
|
+
/** 并发 hook 的先后差不会超过一次 hook 的执行时长;超出这个差值说明是时钟回拨,不能当作「更晚的事件」 */
|
|
1081
|
+
const SUPERSEDE_MAX_MS = 2 * 60 * 1000
|
|
1082
|
+
|
|
1083
|
+
/**
|
|
1084
|
+
* 状态里已是比这次事件更晚发生的事件写的(hook 并发、先发生的后写完),这次不能再覆盖。
|
|
1085
|
+
* 🔴 差值带上限(评审):开机后校时往回拨时,状态里的 updatedAt 落在「未来」,不设上限的话在墙钟追上之前该会话的事件全被丢掉
|
|
1086
|
+
*/
|
|
1087
|
+
function superseded(cur, eventAt) {
|
|
1088
|
+
if (!cur || typeof cur.updatedAt !== 'number') return false
|
|
1089
|
+
const ahead = cur.updatedAt - eventAt
|
|
1090
|
+
return ahead > 0 && ahead < SUPERSEDE_MAX_MS
|
|
1091
|
+
}
|
|
1092
|
+
|
|
1080
1093
|
async function main(argv) {
|
|
1081
1094
|
// 同一份 hook 被 Claude Code 与 Codex 复用,靠命令行参数区分是哪个工具
|
|
1082
1095
|
const tool = (argv && argv[2]) || 'claude_code'
|
|
@@ -1084,6 +1097,10 @@ async function main(argv) {
|
|
|
1084
1097
|
const event = payload.hook_event_name || ''
|
|
1085
1098
|
const sessionId = payload.session_id || ''
|
|
1086
1099
|
if (!sessionId) return
|
|
1100
|
+
// 🔴 事件时间取在任何慢操作之前:Codex 的 hook 是 async(不等跑完),一轮很短时 UserPromptSubmit 与 Stop
|
|
1101
|
+
// 的 hook 并发跑;Windows 上新会话首个 hook 要起 PowerShell 查进程,常比 Stop 晚写完,
|
|
1102
|
+
// 按写完先后落盘就会把 done 覆盖回 watch,屏上一直「进行中」直到窗口关掉(10-01 WIN-LVT5M8H5670 实测)
|
|
1103
|
+
const eventAt = Date.now()
|
|
1087
1104
|
|
|
1088
1105
|
if (NOTICE_EVENTS[event]) {
|
|
1089
1106
|
const notice = takeNotice(sessionId)
|
|
@@ -1140,6 +1157,7 @@ async function main(argv) {
|
|
|
1140
1157
|
const prev = readState(sessionId)
|
|
1141
1158
|
const now = Date.now()
|
|
1142
1159
|
if (event === 'UserPromptSubmit') recordDir(payload.cwd, now)
|
|
1160
|
+
if (superseded(prev, eventAt)) return
|
|
1143
1161
|
// 状态里记的进程还活着才沿用(评审):先远程 -p 跑过、后又在窗口 resume 同一会话时,
|
|
1144
1162
|
// 不能拿死掉的 -p 进程的 interactive=false 挡不住窗口
|
|
1145
1163
|
const prevAlive = !!(prev && prev.pid && pidExists(prev.pid))
|
|
@@ -1158,8 +1176,9 @@ async function main(argv) {
|
|
|
1158
1176
|
// ⚠️ 只落本机,绝不上报:快照里发出去的仍然只有 basename。
|
|
1159
1177
|
cwd: payload.cwd || (prev && prev.cwd) || '',
|
|
1160
1178
|
state: action,
|
|
1161
|
-
startedAt: (prev && prev.startedAt) ||
|
|
1162
|
-
|
|
1179
|
+
startedAt: (prev && prev.startedAt) || eventAt,
|
|
1180
|
+
// 记事件发生时间而不是写完时间,后来的事件才比得过先发生、却后写完的那个
|
|
1181
|
+
updatedAt: eventAt,
|
|
1163
1182
|
pid: pid,
|
|
1164
1183
|
pidName: pid ? (prevAlive && prev.pidName) || processName(pid) || undefined : undefined,
|
|
1165
1184
|
// 二期 P1:窗口开着的会话,设备不能再往里下指令(两个进程会互相吞掉对话)。每个会话只判一次
|
|
@@ -1181,7 +1200,11 @@ async function main(argv) {
|
|
|
1181
1200
|
|
|
1182
1201
|
// 进 / 出 DeskBuddy 窗口不一定伴随状态跃迁(窗口里 --resume 一个已完成的会话),要立刻报,否则设备最长 2 分钟仍被 8348 挡
|
|
1183
1202
|
const windowChanged = !!(prev && prev.remoteWindow) !== !!next.remoteWindow
|
|
1184
|
-
|
|
1203
|
+
// 跃迁要跟磁盘上的最新状态比,不能只跟开头读到的 prev 比(评审):上一轮停在 done 时 Stop 读到的 prev 是旧 done,
|
|
1204
|
+
// 并发的 UserPromptSubmit 已把 watch 写盘并上报了,Stop 若按 done→done 静默落盘,设备收到的最后一份就是 watch
|
|
1205
|
+
const latest = readState(sessionId)
|
|
1206
|
+
if (superseded(latest, eventAt)) return
|
|
1207
|
+
if (!windowChanged && !shouldReport(latest || prev, next, now)) {
|
|
1185
1208
|
writeState(next)
|
|
1186
1209
|
return
|
|
1187
1210
|
}
|
|
@@ -1197,6 +1220,8 @@ async function main(argv) {
|
|
|
1197
1220
|
next.title ||
|
|
1198
1221
|
''
|
|
1199
1222
|
next.lastReportAt = now
|
|
1223
|
+
// 取标题 / 查进程期间可能已有更晚发生的事件写完了:那份更新,这次既不落盘也不上报
|
|
1224
|
+
if (superseded(readState(sessionId), eventAt)) return
|
|
1200
1225
|
writeState(next)
|
|
1201
1226
|
|
|
1202
1227
|
await reportIfAuthorized(tool, now)
|
|
@@ -1248,6 +1273,7 @@ module.exports = {
|
|
|
1248
1273
|
markClear,
|
|
1249
1274
|
pairClear,
|
|
1250
1275
|
shouldReport,
|
|
1276
|
+
superseded,
|
|
1251
1277
|
sign,
|
|
1252
1278
|
titleSources,
|
|
1253
1279
|
writeState,
|
package/hook/hook.cjs
CHANGED
|
@@ -1077,6 +1077,19 @@ function codexRemoteHas(sessionId) {
|
|
|
1077
1077
|
return !!r && r.has(sessionId)
|
|
1078
1078
|
}
|
|
1079
1079
|
|
|
1080
|
+
/** 并发 hook 的先后差不会超过一次 hook 的执行时长;超出这个差值说明是时钟回拨,不能当作「更晚的事件」 */
|
|
1081
|
+
const SUPERSEDE_MAX_MS = 2 * 60 * 1000
|
|
1082
|
+
|
|
1083
|
+
/**
|
|
1084
|
+
* 状态里已是比这次事件更晚发生的事件写的(hook 并发、先发生的后写完),这次不能再覆盖。
|
|
1085
|
+
* 🔴 差值带上限(评审):开机后校时往回拨时,状态里的 updatedAt 落在「未来」,不设上限的话在墙钟追上之前该会话的事件全被丢掉
|
|
1086
|
+
*/
|
|
1087
|
+
function superseded(cur, eventAt) {
|
|
1088
|
+
if (!cur || typeof cur.updatedAt !== 'number') return false
|
|
1089
|
+
const ahead = cur.updatedAt - eventAt
|
|
1090
|
+
return ahead > 0 && ahead < SUPERSEDE_MAX_MS
|
|
1091
|
+
}
|
|
1092
|
+
|
|
1080
1093
|
async function main(argv) {
|
|
1081
1094
|
// 同一份 hook 被 Claude Code 与 Codex 复用,靠命令行参数区分是哪个工具
|
|
1082
1095
|
const tool = (argv && argv[2]) || 'claude_code'
|
|
@@ -1084,6 +1097,10 @@ async function main(argv) {
|
|
|
1084
1097
|
const event = payload.hook_event_name || ''
|
|
1085
1098
|
const sessionId = payload.session_id || ''
|
|
1086
1099
|
if (!sessionId) return
|
|
1100
|
+
// 🔴 事件时间取在任何慢操作之前:Codex 的 hook 是 async(不等跑完),一轮很短时 UserPromptSubmit 与 Stop
|
|
1101
|
+
// 的 hook 并发跑;Windows 上新会话首个 hook 要起 PowerShell 查进程,常比 Stop 晚写完,
|
|
1102
|
+
// 按写完先后落盘就会把 done 覆盖回 watch,屏上一直「进行中」直到窗口关掉(10-01 WIN-LVT5M8H5670 实测)
|
|
1103
|
+
const eventAt = Date.now()
|
|
1087
1104
|
|
|
1088
1105
|
if (NOTICE_EVENTS[event]) {
|
|
1089
1106
|
const notice = takeNotice(sessionId)
|
|
@@ -1140,6 +1157,7 @@ async function main(argv) {
|
|
|
1140
1157
|
const prev = readState(sessionId)
|
|
1141
1158
|
const now = Date.now()
|
|
1142
1159
|
if (event === 'UserPromptSubmit') recordDir(payload.cwd, now)
|
|
1160
|
+
if (superseded(prev, eventAt)) return
|
|
1143
1161
|
// 状态里记的进程还活着才沿用(评审):先远程 -p 跑过、后又在窗口 resume 同一会话时,
|
|
1144
1162
|
// 不能拿死掉的 -p 进程的 interactive=false 挡不住窗口
|
|
1145
1163
|
const prevAlive = !!(prev && prev.pid && pidExists(prev.pid))
|
|
@@ -1158,8 +1176,9 @@ async function main(argv) {
|
|
|
1158
1176
|
// ⚠️ 只落本机,绝不上报:快照里发出去的仍然只有 basename。
|
|
1159
1177
|
cwd: payload.cwd || (prev && prev.cwd) || '',
|
|
1160
1178
|
state: action,
|
|
1161
|
-
startedAt: (prev && prev.startedAt) ||
|
|
1162
|
-
|
|
1179
|
+
startedAt: (prev && prev.startedAt) || eventAt,
|
|
1180
|
+
// 记事件发生时间而不是写完时间,后来的事件才比得过先发生、却后写完的那个
|
|
1181
|
+
updatedAt: eventAt,
|
|
1163
1182
|
pid: pid,
|
|
1164
1183
|
pidName: pid ? (prevAlive && prev.pidName) || processName(pid) || undefined : undefined,
|
|
1165
1184
|
// 二期 P1:窗口开着的会话,设备不能再往里下指令(两个进程会互相吞掉对话)。每个会话只判一次
|
|
@@ -1181,7 +1200,11 @@ async function main(argv) {
|
|
|
1181
1200
|
|
|
1182
1201
|
// 进 / 出 DeskBuddy 窗口不一定伴随状态跃迁(窗口里 --resume 一个已完成的会话),要立刻报,否则设备最长 2 分钟仍被 8348 挡
|
|
1183
1202
|
const windowChanged = !!(prev && prev.remoteWindow) !== !!next.remoteWindow
|
|
1184
|
-
|
|
1203
|
+
// 跃迁要跟磁盘上的最新状态比,不能只跟开头读到的 prev 比(评审):上一轮停在 done 时 Stop 读到的 prev 是旧 done,
|
|
1204
|
+
// 并发的 UserPromptSubmit 已把 watch 写盘并上报了,Stop 若按 done→done 静默落盘,设备收到的最后一份就是 watch
|
|
1205
|
+
const latest = readState(sessionId)
|
|
1206
|
+
if (superseded(latest, eventAt)) return
|
|
1207
|
+
if (!windowChanged && !shouldReport(latest || prev, next, now)) {
|
|
1185
1208
|
writeState(next)
|
|
1186
1209
|
return
|
|
1187
1210
|
}
|
|
@@ -1197,6 +1220,8 @@ async function main(argv) {
|
|
|
1197
1220
|
next.title ||
|
|
1198
1221
|
''
|
|
1199
1222
|
next.lastReportAt = now
|
|
1223
|
+
// 取标题 / 查进程期间可能已有更晚发生的事件写完了:那份更新,这次既不落盘也不上报
|
|
1224
|
+
if (superseded(readState(sessionId), eventAt)) return
|
|
1200
1225
|
writeState(next)
|
|
1201
1226
|
|
|
1202
1227
|
await reportIfAuthorized(tool, now)
|
|
@@ -1248,6 +1273,7 @@ module.exports = {
|
|
|
1248
1273
|
markClear,
|
|
1249
1274
|
pairClear,
|
|
1250
1275
|
shouldReport,
|
|
1276
|
+
superseded,
|
|
1251
1277
|
sign,
|
|
1252
1278
|
titleSources,
|
|
1253
1279
|
writeState,
|