@nextclaw/x-harness 0.0.0-stage → 0.1.1
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/LICENSE +10 -0
- package/README.md +41 -2
- package/cli/main.js +219 -0
- package/dist/api.d.ts +10 -0
- package/dist/api.js +308 -0
- package/dist/boundary.d.ts +2 -0
- package/dist/browser-common.d.ts +4 -0
- package/dist/browser-common.js +29 -0
- package/dist/browser-dom-capabilities.d.ts +4 -0
- package/dist/browser-dom-capabilities.js +110 -0
- package/dist/browser-dom-dependencies.d.ts +16 -0
- package/dist/browser-dom-editing.d.ts +148 -0
- package/dist/browser-dom-editing.js +386 -0
- package/dist/browser-dom-navigation.d.ts +9 -0
- package/dist/browser-dom-navigation.js +165 -0
- package/dist/browser-dom-network.d.ts +1 -0
- package/dist/browser-dom-network.js +128 -0
- package/dist/browser-dom-submission.d.ts +2 -0
- package/dist/browser-dom-submission.js +82 -0
- package/dist/browser-dom.d.ts +2 -0
- package/dist/browser-dom.js +103 -0
- package/dist/browser-native.d.ts +1 -0
- package/dist/browser-native.js +215 -0
- package/dist/browser.d.ts +4 -0
- package/dist/browser.js +147 -0
- package/dist/creator.d.ts +29 -0
- package/dist/creator.js +164 -0
- package/dist/growth-batch.d.ts +5 -0
- package/dist/growth-batch.js +168 -0
- package/dist/growth-constants.d.ts +5 -0
- package/dist/growth-constants.js +13 -0
- package/dist/growth-dependencies.d.ts +33 -0
- package/dist/growth-execution.d.ts +11 -0
- package/dist/growth-execution.js +315 -0
- package/dist/growth-parallel.d.ts +4 -0
- package/dist/growth-parallel.js +301 -0
- package/dist/growth-plan.d.ts +4 -0
- package/dist/growth-plan.js +207 -0
- package/dist/growth-policy.d.ts +9 -0
- package/dist/growth-policy.js +181 -0
- package/dist/growth-publishing.d.ts +5 -0
- package/dist/growth-publishing.js +240 -0
- package/dist/growth-reading.d.ts +4 -0
- package/dist/growth-reading.js +135 -0
- package/dist/growth-recovery.d.ts +6 -0
- package/dist/growth-recovery.js +158 -0
- package/dist/growth-state.d.ts +8 -0
- package/dist/growth-state.js +183 -0
- package/dist/growth-tasks.d.ts +17 -0
- package/dist/growth-tasks.js +101 -0
- package/dist/growth.d.ts +2 -0
- package/dist/growth.js +138 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.js +22 -0
- package/dist/observation.d.ts +1 -0
- package/dist/observation.js +2 -0
- package/dist/platform-capabilities.d.ts +4 -0
- package/dist/platform-capabilities.js +19 -0
- package/dist/platform-collections.d.ts +7 -0
- package/dist/platform-collections.js +223 -0
- package/dist/platform-dependencies.d.ts +43 -0
- package/dist/platform-diagnostics.d.ts +9 -0
- package/dist/platform-diagnostics.js +51 -0
- package/dist/platform-observation.d.ts +8 -0
- package/dist/platform-observation.js +75 -0
- package/dist/platform-rules.d.ts +17 -0
- package/dist/platform-rules.js +41 -0
- package/dist/platform-targets.d.ts +7 -0
- package/dist/platform-targets.js +129 -0
- package/dist/platform-writing.d.ts +8 -0
- package/dist/platform-writing.js +171 -0
- package/dist/platform.d.ts +2 -0
- package/dist/platform.js +60 -0
- package/dist/readers-actions.d.ts +2 -0
- package/dist/readers-actions.js +66 -0
- package/dist/readers-ax-host.d.ts +2 -0
- package/dist/readers-ax-host.js +125 -0
- package/dist/readers-ax-parser.d.ts +1 -0
- package/dist/readers-ax-parser.js +69 -0
- package/dist/readers-ax-program.d.ts +1 -0
- package/dist/readers-ax-tree.d.ts +19 -0
- package/dist/readers-ax-tree.js +110 -0
- package/dist/readers-ax-views.d.ts +87 -0
- package/dist/readers-ax-views.js +247 -0
- package/dist/readers-cli.d.ts +6 -0
- package/dist/readers-composer.d.ts +2 -0
- package/dist/readers-composer.js +131 -0
- package/dist/readers-navigation.d.ts +2 -0
- package/dist/readers-navigation.js +66 -0
- package/dist/readers-posts.d.ts +2 -0
- package/dist/readers-posts.js +141 -0
- package/dist/readers-profile.d.ts +3 -0
- package/dist/readers-profile.js +157 -0
- package/dist/readers.d.ts +9 -0
- package/dist/results.d.ts +6 -0
- package/dist/results.js +233 -0
- package/dist/round-state.d.ts +3 -0
- package/dist/round-state.js +155 -0
- package/dist/store.d.ts +30 -0
- package/dist/store.js +175 -0
- package/dist/types.d.ts +407 -0
- package/generated/observation.d.mts +1 -0
- package/generated/observation.mjs +515 -0
- package/package.json +29 -3
- package/skill/SKILL.md +33 -0
- package/skill/references/api.md +10 -0
- package/skill/references/reads.md +23 -0
- package/skill/references/recovery.md +25 -0
- package/skill/references/setup.md +23 -0
- package/skill/references/writes.md +51 -0
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
import { readFileSync, existsSync } from "node:fs";
|
|
2
|
+
const phases = [
|
|
3
|
+
"RUNNING",
|
|
4
|
+
"VERIFYING",
|
|
5
|
+
"SETTLING",
|
|
6
|
+
"WAITING",
|
|
7
|
+
"BLOCKED",
|
|
8
|
+
"COMPLETE",
|
|
9
|
+
];
|
|
10
|
+
export function checkRound(state, { now = Date.now(), evidenceExists = existsSync } = {}) {
|
|
11
|
+
const errors = [];
|
|
12
|
+
const require = (ok, message) => {
|
|
13
|
+
if (!ok)
|
|
14
|
+
errors.push(message);
|
|
15
|
+
};
|
|
16
|
+
require(state && phases.includes(state.status), "运行标记缺失或无效");
|
|
17
|
+
require(typeof state?.batchId === "string" &&
|
|
18
|
+
state.batchId.length > 0, "原批次ID缺失");
|
|
19
|
+
require(Number.isFinite(Date.parse(state?.updatedAt)), "实际更新时间缺失");
|
|
20
|
+
const items = state?.items ?? [];
|
|
21
|
+
const scope = state?.scopeKinds ?? ["follow", "original", "reply"];
|
|
22
|
+
require(Array.isArray(scope) &&
|
|
23
|
+
scope.length > 0 &&
|
|
24
|
+
new Set(scope).size === scope.length &&
|
|
25
|
+
scope.every((k) => ["follow", "original", "reply"].includes(k)), "轮次职责范围无效");
|
|
26
|
+
if (state?.scopeKinds)
|
|
27
|
+
require(typeof state.scopeEvidenceRef === "string" &&
|
|
28
|
+
evidenceExists(state.scopeEvidenceRef), "职责分工缺原合同证据");
|
|
29
|
+
require(Array.isArray(items) &&
|
|
30
|
+
items.length === 3, "必须逐项核关注、原创、回复");
|
|
31
|
+
for (const kind of ["follow", "original", "reply"]) {
|
|
32
|
+
const matches = items.filter((i) => i.kind === kind);
|
|
33
|
+
require(matches.length === 1, `${kind}缺失或重复`);
|
|
34
|
+
}
|
|
35
|
+
const pending = [];
|
|
36
|
+
for (const item of items) {
|
|
37
|
+
require(["planned", "verified", "unknown"].every((k) => Number.isInteger(item[k]) && item[k] >= 0), `${item.kind}数量必须为非负整数`);
|
|
38
|
+
require(item.verified <= item.planned &&
|
|
39
|
+
item.unknown <=
|
|
40
|
+
item.planned - item.verified, `${item.kind}未知不得算成功`);
|
|
41
|
+
require(typeof item.actionable ===
|
|
42
|
+
"boolean", `${item.kind}当前可执行性缺失`);
|
|
43
|
+
require(item.verified === 0 ||
|
|
44
|
+
(Array.isArray(item.resultEvidence) &&
|
|
45
|
+
item.resultEvidence.length > 0 &&
|
|
46
|
+
item.resultEvidence.every(evidenceExists)), `${item.kind}完成量缺真实结果证据`);
|
|
47
|
+
if (Array.isArray(scope) &&
|
|
48
|
+
scope.includes(item.kind) &&
|
|
49
|
+
(item.verified < item.planned || item.unknown > 0))
|
|
50
|
+
pending.push(item);
|
|
51
|
+
}
|
|
52
|
+
const closing = ["WAITING", "BLOCKED", "COMPLETE"].includes(state?.status);
|
|
53
|
+
if (state?.status === "COMPLETE")
|
|
54
|
+
require(pending.length === 0, "目标有欠量或未知,禁止标完成");
|
|
55
|
+
if (state?.status === "WAITING" || state?.status === "BLOCKED") {
|
|
56
|
+
require(pending.length > 0, "无剩余项不得伪造等待或阻塞");
|
|
57
|
+
require(pending.every((i) => i.actionable === false), "仍有当前可执行项,禁止结束");
|
|
58
|
+
for (const item of pending) {
|
|
59
|
+
require(typeof item.reason === "string" &&
|
|
60
|
+
item.reason.trim().length > 0, `${item.kind}缺具体等待/阻塞原因`);
|
|
61
|
+
require(typeof item.resumeWhen === "string" &&
|
|
62
|
+
item.resumeWhen.trim().length > 0, `${item.kind}缺恢复条件`);
|
|
63
|
+
require(Array.isArray(item.blockEvidence) &&
|
|
64
|
+
item.blockEvidence.length > 0 &&
|
|
65
|
+
item.blockEvidence.every(evidenceExists), `${item.kind}缺存在的阻塞证据`);
|
|
66
|
+
}
|
|
67
|
+
require(state.stop?.independentActionsHandled ===
|
|
68
|
+
true, "未核独立可执行事项,禁止结束");
|
|
69
|
+
require(Number.isFinite(Date.parse(state.stop?.nextScheduledCheckAt)) &&
|
|
70
|
+
Date.parse(state.stop.nextScheduledCheckAt) >
|
|
71
|
+
now, "下一真实调度检查机会缺失或已到,必须接续");
|
|
72
|
+
if (state.status === "WAITING")
|
|
73
|
+
require(Number.isFinite(Date.parse(state.stop?.nextEligibleCheckAt)) &&
|
|
74
|
+
Date.parse(state.stop.nextEligibleCheckAt) >
|
|
75
|
+
now, "等待条件已到或缺失,必须接续");
|
|
76
|
+
if (state.status === "BLOCKED")
|
|
77
|
+
require(new Set(state.stop?.attemptedPaths ?? []).size >=
|
|
78
|
+
2, "单一路径失败不足以结束:须核独立替代路径");
|
|
79
|
+
}
|
|
80
|
+
// Reply sourcing is work: an empty queue is never an exit permission.
|
|
81
|
+
if (state?.scopeKinds?.length === 1 &&
|
|
82
|
+
state.scopeKinds[0] === "reply" &&
|
|
83
|
+
["WAITING", "BLOCKED"].includes(state.status)) {
|
|
84
|
+
const exit = state.stop?.replyExit;
|
|
85
|
+
const allowed = [
|
|
86
|
+
"platform-restriction",
|
|
87
|
+
"authentication-required",
|
|
88
|
+
"tool-unavailable",
|
|
89
|
+
"user-stop",
|
|
90
|
+
"outside-window",
|
|
91
|
+
"pending-results",
|
|
92
|
+
];
|
|
93
|
+
require(exit &&
|
|
94
|
+
allowed.includes(exit.reason), "评论专责缺许可停止原因;候选不足、空队列或等待下次唤醒必须继续");
|
|
95
|
+
if (exit && allowed.includes(exit.reason)) {
|
|
96
|
+
require(typeof exit.evidenceRef === "string" &&
|
|
97
|
+
evidenceExists(exit.evidenceRef), "评论停止缺真实限制证据");
|
|
98
|
+
require(typeof exit.independentEvidenceRef === "string" &&
|
|
99
|
+
evidenceExists(exit.independentEvidenceRef), "评论停止缺独立可做项审计证据");
|
|
100
|
+
const age = now - Date.parse(exit.observedAt);
|
|
101
|
+
require(Number.isFinite(age) &&
|
|
102
|
+
age >= 0 &&
|
|
103
|
+
age <= 300000, "评论停止证据必须为最近5分钟实际观察");
|
|
104
|
+
const reply = items.find((item) => item.kind === "reply");
|
|
105
|
+
if (exit.reason === "pending-results")
|
|
106
|
+
require(reply &&
|
|
107
|
+
reply.unknown > 0 &&
|
|
108
|
+
reply.planned - reply.verified ===
|
|
109
|
+
reply.unknown, "未提交欠量仍在;未知提交不能冻结新对象");
|
|
110
|
+
if (exit.reason === "outside-window") {
|
|
111
|
+
const shanghaiHour = (new Date(now).getUTCHours() + 8) % 24;
|
|
112
|
+
require(shanghaiHour <
|
|
113
|
+
7, "仍在上海07至24点评论执行窗口,不能以时段截止结束");
|
|
114
|
+
}
|
|
115
|
+
if (exit.reason === "user-stop")
|
|
116
|
+
require(typeof exit.instructionEvidenceRef === "string" &&
|
|
117
|
+
evidenceExists(exit.instructionEvidenceRef), "用户停止须有明确原指令证据");
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
return {
|
|
121
|
+
valid: errors.length === 0,
|
|
122
|
+
mayEnd: closing && errors.length === 0,
|
|
123
|
+
status: state?.status,
|
|
124
|
+
scopeKinds: scope,
|
|
125
|
+
pending: pending.map((i) => ({
|
|
126
|
+
kind: i.kind,
|
|
127
|
+
remaining: i.planned - i.verified,
|
|
128
|
+
unknown: i.unknown,
|
|
129
|
+
})),
|
|
130
|
+
errors,
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
export function readRound(markdown) {
|
|
134
|
+
const match = markdown.match(/```x-round-state\s*\n([\s\S]*?)\n```/);
|
|
135
|
+
if (!match)
|
|
136
|
+
throw new Error("当前台账缺运行标记:默认视为未结束,须接续");
|
|
137
|
+
return JSON.parse(match[1]);
|
|
138
|
+
}
|
|
139
|
+
export function runRoundCLI() {
|
|
140
|
+
try {
|
|
141
|
+
const file = process.argv.slice(2).find((a) => !a.startsWith("--"));
|
|
142
|
+
if (!file)
|
|
143
|
+
throw new Error("用法:node scripts/x-round-state.mjs <原台账> [--check-end]");
|
|
144
|
+
const result = checkRound(readRound(readFileSync(file, "utf8")));
|
|
145
|
+
console.log(JSON.stringify(result));
|
|
146
|
+
process.exitCode =
|
|
147
|
+
result.valid && (!process.argv.includes("--check-end") || result.mayEnd)
|
|
148
|
+
? 0
|
|
149
|
+
: 1;
|
|
150
|
+
}
|
|
151
|
+
catch (error) {
|
|
152
|
+
console.log(JSON.stringify({ valid: false, mayEnd: false, errors: [error.message] }));
|
|
153
|
+
process.exitCode = 1;
|
|
154
|
+
}
|
|
155
|
+
}
|
package/dist/store.d.ts
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import type { BoundaryRecord } from "./boundary.js";
|
|
2
|
+
export declare class HarnessConflict extends Error {
|
|
3
|
+
code: string;
|
|
4
|
+
constructor(message: string);
|
|
5
|
+
}
|
|
6
|
+
export interface RoundSnapshot {
|
|
7
|
+
state: BoundaryRecord;
|
|
8
|
+
revision: string;
|
|
9
|
+
path: string;
|
|
10
|
+
}
|
|
11
|
+
export interface RoundStore {
|
|
12
|
+
path: string;
|
|
13
|
+
evidenceExists(path: string): boolean;
|
|
14
|
+
fingerprint(path: string): string;
|
|
15
|
+
processId: number | null;
|
|
16
|
+
ownerAlive(pid: unknown): boolean;
|
|
17
|
+
transaction(fn: (snapshot: RoundSnapshot) => BoundaryRecord | undefined | Promise<BoundaryRecord | undefined>): Promise<RoundSnapshot>;
|
|
18
|
+
load(): Promise<RoundSnapshot>;
|
|
19
|
+
save(snapshot: RoundSnapshot, state: BoundaryRecord): Promise<RoundSnapshot>;
|
|
20
|
+
withLock<T>(fn: () => T | Promise<T>): Promise<T>;
|
|
21
|
+
recoverAbandonedLock(): Promise<{
|
|
22
|
+
recovered: boolean;
|
|
23
|
+
evidenceRef: string;
|
|
24
|
+
}>;
|
|
25
|
+
recordProof(proof: unknown): Promise<string>;
|
|
26
|
+
}
|
|
27
|
+
export declare function createFileRoundStore({ ledgerPath, proofDirectory, }: {
|
|
28
|
+
ledgerPath: string;
|
|
29
|
+
proofDirectory: string;
|
|
30
|
+
}): RoundStore;
|
package/dist/store.js
ADDED
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
import { readFileSync, writeFileSync, openSync, closeSync, unlinkSync, renameSync, mkdirSync, statSync, existsSync, } from "node:fs";
|
|
2
|
+
import { dirname, resolve, join } from "node:path";
|
|
3
|
+
import { createHash, randomUUID } from "node:crypto";
|
|
4
|
+
import { readRound } from "./round-state.js";
|
|
5
|
+
const hash = (text) => createHash("sha256").update(text).digest("hex");
|
|
6
|
+
const block = /```x-round-state\s*\n([\s\S]*?)\n```/;
|
|
7
|
+
export class HarnessConflict extends Error {
|
|
8
|
+
code;
|
|
9
|
+
constructor(message) {
|
|
10
|
+
super(message);
|
|
11
|
+
this.code = "CONFLICT";
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
export function createFileRoundStore({ ledgerPath, proofDirectory, }) {
|
|
15
|
+
const path = resolve(ledgerPath);
|
|
16
|
+
const lockPath = join(dirname(path), ".x-harness.lock");
|
|
17
|
+
const proofDir = resolve(proofDirectory);
|
|
18
|
+
let held = false;
|
|
19
|
+
return {
|
|
20
|
+
path,
|
|
21
|
+
evidenceExists: existsSync,
|
|
22
|
+
fingerprint: (file) => hash(readFileSync(resolve(file))),
|
|
23
|
+
processId: typeof process !== "undefined" ? process.pid : null,
|
|
24
|
+
ownerAlive(ownerPid) {
|
|
25
|
+
if (typeof ownerPid !== "number" ||
|
|
26
|
+
!Number.isInteger(ownerPid) ||
|
|
27
|
+
ownerPid < 1)
|
|
28
|
+
return true;
|
|
29
|
+
if (typeof process === "undefined" || typeof process.kill !== "function")
|
|
30
|
+
return true;
|
|
31
|
+
try {
|
|
32
|
+
process.kill(ownerPid, 0);
|
|
33
|
+
return true;
|
|
34
|
+
}
|
|
35
|
+
catch (error) {
|
|
36
|
+
if (error.code === "ESRCH")
|
|
37
|
+
return false;
|
|
38
|
+
throw error;
|
|
39
|
+
}
|
|
40
|
+
},
|
|
41
|
+
// Only bounded local state work belongs here. Browser I/O never holds this lock.
|
|
42
|
+
async transaction(fn) {
|
|
43
|
+
const start = Date.now();
|
|
44
|
+
while (true) {
|
|
45
|
+
let entered = false;
|
|
46
|
+
try {
|
|
47
|
+
return await this.withLock(async () => {
|
|
48
|
+
entered = true;
|
|
49
|
+
const snapshot = await this.load();
|
|
50
|
+
const state = await fn(snapshot);
|
|
51
|
+
return state ? this.save(snapshot, state) : snapshot;
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
catch (error) {
|
|
55
|
+
if (entered ||
|
|
56
|
+
error.code !== "CONFLICT" ||
|
|
57
|
+
Date.now() - start >= 2000)
|
|
58
|
+
throw error;
|
|
59
|
+
await new Promise((resolve) => setTimeout(resolve, 10));
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
},
|
|
63
|
+
async load() {
|
|
64
|
+
const text = readFileSync(path, "utf8");
|
|
65
|
+
return { state: readRound(text), revision: hash(text), path };
|
|
66
|
+
},
|
|
67
|
+
async save(snapshot, state) {
|
|
68
|
+
if (!held)
|
|
69
|
+
throw new HarnessConflict("保存原台账必须持有账号锁");
|
|
70
|
+
const text = readFileSync(path, "utf8");
|
|
71
|
+
if (snapshot.revision !== hash(text))
|
|
72
|
+
throw new HarnessConflict("原台账已变化:重新定向读取,禁止覆盖");
|
|
73
|
+
if (readRound(text).batchId !== state.batchId)
|
|
74
|
+
throw new HarnessConflict("禁止替换原批次ID");
|
|
75
|
+
const next = text.replace(block, () => "```x-round-state\n" + JSON.stringify(state, null, 2) + "\n```");
|
|
76
|
+
const temp = `${path}.harness-${randomUUID()}.tmp`;
|
|
77
|
+
try {
|
|
78
|
+
writeFileSync(temp, next, {
|
|
79
|
+
flag: "wx",
|
|
80
|
+
mode: statSync(path).mode & 0o777,
|
|
81
|
+
});
|
|
82
|
+
// Other writers must honor the same lock; CAS additionally detects observed external edits.
|
|
83
|
+
if (hash(readFileSync(path, "utf8")) !== snapshot.revision)
|
|
84
|
+
throw new HarnessConflict("原台账在结算中变化");
|
|
85
|
+
renameSync(temp, path);
|
|
86
|
+
}
|
|
87
|
+
finally {
|
|
88
|
+
if (existsSync(temp))
|
|
89
|
+
unlinkSync(temp);
|
|
90
|
+
}
|
|
91
|
+
return { state, revision: hash(next), path };
|
|
92
|
+
},
|
|
93
|
+
async withLock(fn) {
|
|
94
|
+
if (held)
|
|
95
|
+
throw new HarnessConflict("账号操作禁止重入");
|
|
96
|
+
if (existsSync(`${lockPath}.recovery`))
|
|
97
|
+
throw new HarnessConflict("原锁恢复核验进行中");
|
|
98
|
+
let fd;
|
|
99
|
+
try {
|
|
100
|
+
fd = openSync(lockPath, "wx", 0o600);
|
|
101
|
+
}
|
|
102
|
+
catch (error) {
|
|
103
|
+
if (error.code === "EEXIST")
|
|
104
|
+
throw new HarnessConflict("账号锁占用:不并发写、不自动偷锁");
|
|
105
|
+
throw error;
|
|
106
|
+
}
|
|
107
|
+
try {
|
|
108
|
+
writeFileSync(fd, JSON.stringify({
|
|
109
|
+
pid: typeof process !== "undefined" ? process.pid : null,
|
|
110
|
+
acquiredAt: new Date().toISOString(),
|
|
111
|
+
ledgerPath: path,
|
|
112
|
+
}));
|
|
113
|
+
held = true;
|
|
114
|
+
return await fn();
|
|
115
|
+
}
|
|
116
|
+
finally {
|
|
117
|
+
held = false;
|
|
118
|
+
closeSync(fd);
|
|
119
|
+
unlinkSync(lockPath);
|
|
120
|
+
}
|
|
121
|
+
},
|
|
122
|
+
async recoverAbandonedLock() {
|
|
123
|
+
if (held)
|
|
124
|
+
throw new HarnessConflict("不能恢复当前正在持有的锁");
|
|
125
|
+
const recovery = `${lockPath}.recovery`;
|
|
126
|
+
let fd;
|
|
127
|
+
try {
|
|
128
|
+
fd = openSync(recovery, "wx", 0o600);
|
|
129
|
+
}
|
|
130
|
+
catch (error) {
|
|
131
|
+
if (error.code === "EEXIST")
|
|
132
|
+
throw new HarnessConflict("另一执行器正在核验原锁");
|
|
133
|
+
throw error;
|
|
134
|
+
}
|
|
135
|
+
try {
|
|
136
|
+
const original = readFileSync(lockPath, "utf8"), owner = JSON.parse(original);
|
|
137
|
+
if (!Number.isInteger(owner.pid) || owner.pid < 1)
|
|
138
|
+
throw new HarnessConflict("锁owner缺失,不能自动移除");
|
|
139
|
+
if (typeof process === "undefined" ||
|
|
140
|
+
typeof process.kill !== "function")
|
|
141
|
+
throw new HarnessConflict("当前宿主无进程核验能力,禁止恢复原锁");
|
|
142
|
+
try {
|
|
143
|
+
process.kill(owner.pid, 0);
|
|
144
|
+
throw new HarnessConflict("原进程仍在:禁止抢占账号锁");
|
|
145
|
+
}
|
|
146
|
+
catch (error) {
|
|
147
|
+
if (error.code !== "ESRCH")
|
|
148
|
+
throw error;
|
|
149
|
+
}
|
|
150
|
+
const proof = await this.recordProof({
|
|
151
|
+
observedAt: new Date().toISOString(),
|
|
152
|
+
recovery: "owner-process-confirmed-exited",
|
|
153
|
+
owner,
|
|
154
|
+
});
|
|
155
|
+
if (readFileSync(lockPath, "utf8") !== original)
|
|
156
|
+
throw new HarnessConflict("原锁已变化,禁止移除");
|
|
157
|
+
unlinkSync(lockPath);
|
|
158
|
+
return { recovered: true, evidenceRef: proof };
|
|
159
|
+
}
|
|
160
|
+
finally {
|
|
161
|
+
closeSync(fd);
|
|
162
|
+
unlinkSync(recovery);
|
|
163
|
+
}
|
|
164
|
+
},
|
|
165
|
+
async recordProof(proof) {
|
|
166
|
+
mkdirSync(proofDir, { recursive: true });
|
|
167
|
+
const file = join(proofDir, `${randomUUID()}.json`);
|
|
168
|
+
writeFileSync(file, JSON.stringify(proof, null, 2), {
|
|
169
|
+
flag: "wx",
|
|
170
|
+
mode: 0o600,
|
|
171
|
+
});
|
|
172
|
+
return file;
|
|
173
|
+
},
|
|
174
|
+
};
|
|
175
|
+
}
|