@pab-iprg/huohuo 0.0.0-stage → 1.0.0-preview.2
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/README.md
CHANGED
|
@@ -1,3 +1,13 @@
|
|
|
1
|
-
#
|
|
1
|
+
# 火火安装入口
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
入口在设备预检后安装 DSH 和火火运行环境。npm 预览包内置火火运行文件,Python 运行时和依赖从固定上游地址获取并校验摘要。
|
|
4
|
+
|
|
5
|
+
构建时需要已完成平台测量与摘要绑定的 `release.json`。没有发行清单时入口明确停止,不从不可信页面选择资源,也不使用虚构的最低配置。
|
|
6
|
+
|
|
7
|
+
发布后的预览入口为 `npx --yes @pab-iprg/huohuo@next`。首版验收基线为 macOS Apple Silicon、Node 24.8.0 和 npm 11.6.0。默认持久目录为 `~/.huohuo`,普通重开保留已安装版本,安装中断后继续原目标。
|
|
8
|
+
|
|
9
|
+
命令支持 `huohuo [start|doctor|stop|upgrade|uninstall] [--home 目录] [--no-open]`。
|
|
10
|
+
|
|
11
|
+
显式升级检查旧迁移不可变、追加迁移顺序、执行库格式与 DSH 会话兼容范围,先一致性备份再迁移和验证,成功后切换;不自动恢复旧备份覆盖新数据。默认卸载保留实例、凭据、会话和备份,不提供直接彻底删除业务数据的快捷入口。
|
|
12
|
+
|
|
13
|
+
首版终端前台管理生命周期,Ctrl+C 停止本次实例。退出终端、休眠或关机后不承诺持续运行。模型凭据在现有设置页面填写,安装检查不调用真实模型或通知。
|
|
Binary file
|
|
Binary file
|
package/cli.js
ADDED
|
@@ -0,0 +1,1169 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// distribution/bootstrap/entry.ts
|
|
4
|
+
import { readFile as readFile7, realpath as realpath5 } from "node:fs/promises";
|
|
5
|
+
import { createReadStream as createReadStream2 } from "node:fs";
|
|
6
|
+
import { Readable } from "node:stream";
|
|
7
|
+
import { homedir } from "node:os";
|
|
8
|
+
import { dirname as dirname5, join as join6, resolve as resolve5 } from "node:path";
|
|
9
|
+
import { fileURLToPath } from "node:url";
|
|
10
|
+
|
|
11
|
+
// distribution/bootstrap/install.ts
|
|
12
|
+
import { readFile as readFile4, mkdir as mkdir3, access, rename as rename3, rm as rm2, realpath as realpath3, readdir } from "node:fs/promises";
|
|
13
|
+
import { createHash as createHash2, randomUUID as randomUUID3 } from "node:crypto";
|
|
14
|
+
import { join as join3, resolve as resolve3 } from "node:path";
|
|
15
|
+
import { release as osRelease } from "node:os";
|
|
16
|
+
import { spawn, execFile as execFile3 } from "node:child_process";
|
|
17
|
+
import { promisify as promisify3 } from "node:util";
|
|
18
|
+
|
|
19
|
+
// distribution/bootstrap/state.ts
|
|
20
|
+
import { open, rename, unlink, readFile, mkdir } from "node:fs/promises";
|
|
21
|
+
import { dirname } from "node:path";
|
|
22
|
+
import { randomUUID } from "node:crypto";
|
|
23
|
+
import { execFile } from "node:child_process";
|
|
24
|
+
import { promisify } from "node:util";
|
|
25
|
+
var execute = promisify(execFile);
|
|
26
|
+
async function processIdentity(pid) {
|
|
27
|
+
if (!Number.isSafeInteger(pid) || pid <= 0 || !["darwin", "linux"].includes(process.platform)) throw new Error("\u8FDB\u7A0B\u542F\u52A8\u8EAB\u4EFD\u63A2\u9488\u4E0D\u652F\u6301\u6216\u65E0\u6548");
|
|
28
|
+
try {
|
|
29
|
+
const { stdout } = await execute("ps", ["-p", String(pid), "-o", "lstart=,uid="], { timeout: 2e3 });
|
|
30
|
+
if (!stdout.trim()) throw new Error("\u8FDB\u7A0B\u542F\u52A8\u8EAB\u4EFD\u63A2\u9488\u8FD4\u56DE\u7A7A\u503C");
|
|
31
|
+
return stdout.trim().replace(/\s+/g, " ");
|
|
32
|
+
} catch (e) {
|
|
33
|
+
if (e.code === 1 && !String(e.stdout ?? "").trim()) {
|
|
34
|
+
try {
|
|
35
|
+
process.kill(pid, 0);
|
|
36
|
+
} catch (signalError) {
|
|
37
|
+
if (signalError.code === "ESRCH") return null;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
throw new Error("\u8FDB\u7A0B\u542F\u52A8\u8EAB\u4EFD\u65E0\u6CD5\u6838\u9A8C\uFF1B\u672A\u56DE\u6536\u9501");
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
async function isLockOwnerDead(path, token) {
|
|
44
|
+
try {
|
|
45
|
+
const previous = JSON.parse(await readFile(path, "utf8"));
|
|
46
|
+
return previous.format === 1 && previous.path === path && previous.uid === process.getuid?.() && typeof previous.processStart === "string" && !!previous.processStart && typeof previous.token === "string" && /^[a-f0-9-]{36}$/.test(previous.token) && (token === void 0 || previous.token === token) && Number.isSafeInteger(previous.pid) && await processIdentity(previous.pid) === null;
|
|
47
|
+
} catch {
|
|
48
|
+
return false;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
async function atomicJSON(path, value) {
|
|
52
|
+
const pending = `${path}.${randomUUID()}.tmp`;
|
|
53
|
+
const file = await open(pending, "wx", 384);
|
|
54
|
+
try {
|
|
55
|
+
await file.writeFile(JSON.stringify(value, null, 2) + "\n");
|
|
56
|
+
await file.sync();
|
|
57
|
+
} catch (error) {
|
|
58
|
+
await unlink(pending).catch(() => {
|
|
59
|
+
});
|
|
60
|
+
throw error;
|
|
61
|
+
} finally {
|
|
62
|
+
await file.close();
|
|
63
|
+
}
|
|
64
|
+
try {
|
|
65
|
+
await rename(pending, path);
|
|
66
|
+
const directory = await open(dirname(path), "r");
|
|
67
|
+
try {
|
|
68
|
+
await directory.sync();
|
|
69
|
+
} finally {
|
|
70
|
+
await directory.close();
|
|
71
|
+
}
|
|
72
|
+
} finally {
|
|
73
|
+
await unlink(pending).catch(() => {
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
async function acquireLock(path) {
|
|
78
|
+
await mkdir(dirname(path), { recursive: true, mode: 448 });
|
|
79
|
+
let lock;
|
|
80
|
+
try {
|
|
81
|
+
lock = await open(path, "wx", 384);
|
|
82
|
+
} catch (cause) {
|
|
83
|
+
if (cause.code !== "EEXIST") throw cause;
|
|
84
|
+
let recovery;
|
|
85
|
+
try {
|
|
86
|
+
recovery = await open(path + ".reconcile", "wx", 384);
|
|
87
|
+
const previous = JSON.parse(await readFile(path, "utf8"));
|
|
88
|
+
if (!await isLockOwnerDead(path, previous.token)) throw new Error("\u9501\u5F52\u5C5E\u672A\u786E\u8BA4\u6216\u8FDB\u7A0B\u4ECD\u5B58\u5728");
|
|
89
|
+
await rename(path, `${path}.stale-${randomUUID()}`);
|
|
90
|
+
lock = await open(path, "wx", 384);
|
|
91
|
+
} catch {
|
|
92
|
+
throw new Error("\u5B89\u88C5\u6216\u5B9E\u4F8B\u9501\u5DF2\u5B58\u5728\uFF1B\u4E0D\u80FD\u4EC5\u51ED PID \u6216\u8D85\u65F6\u63A5\u7BA1\u3002\u8BF7\u7528 doctor \u6838\u5BF9\u672C\u673A\u8FDB\u7A0B\u4E0E\u9501\u5F52\u5C5E\u3002", { cause });
|
|
93
|
+
} finally {
|
|
94
|
+
if (recovery) {
|
|
95
|
+
await recovery.close();
|
|
96
|
+
await unlink(path + ".reconcile");
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
const token = randomUUID();
|
|
101
|
+
try {
|
|
102
|
+
const processStart = await processIdentity(process.pid);
|
|
103
|
+
if (!processStart) throw new Error("\u5F53\u524D\u8FDB\u7A0B\u542F\u52A8\u8EAB\u4EFD\u672A\u77E5");
|
|
104
|
+
await lock.writeFile(JSON.stringify({ format: 1, path, uid: process.getuid?.(), pid: process.pid, processStart, startedAt: (/* @__PURE__ */ new Date()).toISOString(), token }));
|
|
105
|
+
await lock.sync();
|
|
106
|
+
} catch (error) {
|
|
107
|
+
await lock.close();
|
|
108
|
+
await unlink(path);
|
|
109
|
+
throw error;
|
|
110
|
+
}
|
|
111
|
+
return async () => {
|
|
112
|
+
await lock.close();
|
|
113
|
+
try {
|
|
114
|
+
const current = JSON.parse(await readFile(path, "utf8"));
|
|
115
|
+
if (current.token === token) await unlink(path);
|
|
116
|
+
} catch (e) {
|
|
117
|
+
if (e.code !== "ENOENT") throw e;
|
|
118
|
+
}
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
async function withLock(path, action) {
|
|
122
|
+
const release = await acquireLock(path);
|
|
123
|
+
try {
|
|
124
|
+
return await action();
|
|
125
|
+
} finally {
|
|
126
|
+
await release();
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// distribution/bootstrap/release.ts
|
|
131
|
+
var version = /^\d+\.\d+\.\d+(?:-[a-zA-Z0-9.-]+)?$/;
|
|
132
|
+
var hash = /^[a-f0-9]{64}$/;
|
|
133
|
+
function integer(n) {
|
|
134
|
+
return Number.isSafeInteger(n) && Number(n) >= 0;
|
|
135
|
+
}
|
|
136
|
+
function artifact(a) {
|
|
137
|
+
if (!a || !hash.test(a.sha256) || !integer(a.downloadBytes) || !integer(a.expandedBytes) || !a.downloadBytes || !a.expandedBytes) return false;
|
|
138
|
+
if (a.redirectHosts !== void 0 && (!Array.isArray(a.redirectHosts) || a.redirectHosts.length > 4 || !a.redirectHosts.every((s) => typeof s === "string" && /^[a-z0-9.-]+$/.test(s)))) return false;
|
|
139
|
+
if (a.bundled !== void 0) return a.bundled === true && a.url === `https://bundled.invalid/${a.sha256}.tgz` && a.redirectHosts === void 0;
|
|
140
|
+
try {
|
|
141
|
+
const u = new URL(a.url);
|
|
142
|
+
return u.protocol === "https:" && !u.username && !u.password && !u.hash;
|
|
143
|
+
} catch {
|
|
144
|
+
return false;
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
function validateRelease(input) {
|
|
148
|
+
const r = input;
|
|
149
|
+
if (!r || r.format !== 1 || r.bootstrapFormat !== 1 || !version.test(r.version) || !integer(r.nodeMajor) || r.nodeMajor < 24 || !integer(r.npmMajor) || r.npmMajor < 11 || !r.platform?.minimumOsRelease || !["darwin", "linux", "win32"].includes(r.platform.os) || !["arm64", "x64"].includes(r.platform.arch) || !artifact(r.runtime) || r.runtime.name !== "@huohuo/research-agent" || r.runtime.version !== r.version || !/^sha512-[A-Za-z0-9+/]+={0,2}$/.test(r.runtime.integrity) || !artifact(r.python) || !artifact(r.pythonDependencies) || !r.python.build || !r.python.license || !r.python.executable || r.python.executable.startsWith("/") || r.python.executable.split(/[\\/]/).some((p) => p === ".." || !p) || !version.test(r.dsh?.version) || !hash.test(r.dsh?.assemblySha256) || !hash.test(r.requirementsSha256) || !hash.test(r.profilesSha256) || !r.budget || ![
|
|
150
|
+
"minimumEffectiveMemoryBytes",
|
|
151
|
+
"startupHeadroomBytes",
|
|
152
|
+
"heavyJobHeadroomBytes",
|
|
153
|
+
"diskReserveBytes",
|
|
154
|
+
"recoverySamples",
|
|
155
|
+
"installTemporaryBytes",
|
|
156
|
+
"dependenciesBytes",
|
|
157
|
+
"backupBytes",
|
|
158
|
+
"processTreeBudgetBytes",
|
|
159
|
+
"sampleIntervalMs",
|
|
160
|
+
"sampleMaxAgeMs"
|
|
161
|
+
].every((k) => integer(r.budget[k])) || r.budget.startupHeadroomBytes <= 0 || r.budget.minimumEffectiveMemoryBytes <= 0 || r.budget.diskReserveBytes <= 0 || r.budget.recoverySamples < 2 || r.budget.heavyJobHeadroomBytes < r.budget.startupHeadroomBytes || r.budget.processTreeBudgetBytes <= 0 || r.budget.sampleIntervalMs < 1e3 || r.budget.sampleMaxAgeMs < r.budget.sampleIntervalMs * 2 || r.budget.sampleMaxAgeMs > 3e4 || !r.schema || !integer(r.schema.minimum) || !integer(r.schema.maximum) || r.schema.minimum > r.schema.maximum) {
|
|
162
|
+
throw new Error("\u53D1\u884C\u6E05\u5355\u65E0\u6548\u6216\u5C1A\u672A\u5B8C\u6210\u5E73\u53F0\u3001\u6458\u8981\u4E0E\u8D44\u6E90\u6D4B\u91CF\uFF1B\u4E0D\u80FD\u5B89\u88C5");
|
|
163
|
+
}
|
|
164
|
+
if (!hash.test(r.schema.migrationsSha256)) throw new Error("\u7F3A\u5C11\u6570\u636E\u5E93\u8FC1\u79FB\u88C5\u914D\u6458\u8981");
|
|
165
|
+
if (!r.schema.files || !Object.keys(r.schema.files).length || !Object.entries(r.schema.files).every(([name, sha]) => /^\d{3}_[a-z0-9_]+\.sql$/.test(name) && hash.test(sha))) throw new Error("\u6570\u636E\u5E93\u8FC1\u79FB\u6587\u4EF6\u6E05\u5355\u65E0\u6548");
|
|
166
|
+
return r;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// distribution/bootstrap/artifact.ts
|
|
170
|
+
import { createHash, randomUUID as randomUUID2 } from "node:crypto";
|
|
171
|
+
import { createReadStream } from "node:fs";
|
|
172
|
+
import { mkdir as mkdir2, open as open2, rename as rename2, rm, realpath, symlink, link, lstat } from "node:fs/promises";
|
|
173
|
+
import { dirname as dirname2, resolve, sep, posix } from "node:path";
|
|
174
|
+
import { createGunzip } from "node:zlib";
|
|
175
|
+
async function verifyArtifact(path, artifact2) {
|
|
176
|
+
const digest2 = createHash("sha256"), integrity = createHash("sha512");
|
|
177
|
+
let bytes = 0;
|
|
178
|
+
for await (const chunk of createReadStream(path)) {
|
|
179
|
+
bytes += chunk.length;
|
|
180
|
+
digest2.update(chunk);
|
|
181
|
+
integrity.update(chunk);
|
|
182
|
+
}
|
|
183
|
+
if (bytes !== artifact2.downloadBytes || digest2.digest("hex") !== artifact2.sha256 || "integrity" in artifact2 && artifact2.integrity !== `sha512-${integrity.digest("base64")}`) throw new Error("\u4E0B\u8F7D\u4EA7\u7269\u5927\u5C0F\u6216\u6458\u8981\u4E0D\u7B26");
|
|
184
|
+
}
|
|
185
|
+
async function downloadArtifact(artifact2, path, fetcher = fetch) {
|
|
186
|
+
try {
|
|
187
|
+
await verifyArtifact(path, artifact2);
|
|
188
|
+
return;
|
|
189
|
+
} catch (e) {
|
|
190
|
+
if (e.code !== "ENOENT") throw e;
|
|
191
|
+
}
|
|
192
|
+
await mkdir2(dirname2(path), { recursive: true, mode: 448 });
|
|
193
|
+
const pending = `${path}.${randomUUID2()}.partial`;
|
|
194
|
+
const handle = await open2(pending, "wx", 384);
|
|
195
|
+
try {
|
|
196
|
+
const allowed = /* @__PURE__ */ new Set([new URL(artifact2.url).hostname, ...artifact2.redirectHosts ?? []]);
|
|
197
|
+
let url = artifact2.url;
|
|
198
|
+
let response;
|
|
199
|
+
const transferMs = Math.ceil(artifact2.downloadBytes / (64 * 1024)) * 1e3;
|
|
200
|
+
const signal = AbortSignal.timeout(Math.min(9e5, Math.max(12e4, 3e4 + transferMs)));
|
|
201
|
+
for (let redirects = 0; redirects <= 4; redirects++) {
|
|
202
|
+
const next = new URL(url);
|
|
203
|
+
if (next.protocol !== "https:" || next.username || next.password || !allowed.has(next.hostname)) throw new Error("\u4E0B\u8F7D\u91CD\u5B9A\u5411\u4E0D\u5728\u56FA\u5B9A\u53D1\u884C\u5141\u8BB8\u6765\u6E90\u5185");
|
|
204
|
+
response = await fetcher(url, { signal, redirect: "manual" });
|
|
205
|
+
if (![301, 302, 303, 307, 308].includes(response.status)) break;
|
|
206
|
+
const location = response.headers.get("location");
|
|
207
|
+
await response.body?.cancel();
|
|
208
|
+
if (!location || redirects === 4) throw new Error("\u4E0B\u8F7D\u91CD\u5B9A\u5411\u65E0\u6548\u6216\u8FC7\u591A");
|
|
209
|
+
url = new URL(location, url).href;
|
|
210
|
+
}
|
|
211
|
+
if (!response) throw new Error("\u4E0B\u8F7D\u672A\u8FD4\u56DE\u54CD\u5E94");
|
|
212
|
+
if (!response.ok || !response.body) throw new Error(`\u4E0B\u8F7D\u5931\u8D25\uFF1AHTTP ${response.status}`);
|
|
213
|
+
let received = 0;
|
|
214
|
+
for await (const chunk of response.body) {
|
|
215
|
+
received += chunk.byteLength;
|
|
216
|
+
if (received > artifact2.downloadBytes) throw new Error("\u4E0B\u8F7D\u8D85\u8FC7\u53D1\u884C\u6E05\u5355\u5927\u5C0F");
|
|
217
|
+
await handle.writeFile(chunk);
|
|
218
|
+
}
|
|
219
|
+
await handle.sync();
|
|
220
|
+
await handle.close();
|
|
221
|
+
await verifyArtifact(pending, artifact2);
|
|
222
|
+
await rename2(pending, path);
|
|
223
|
+
} finally {
|
|
224
|
+
await handle.close().catch(() => {
|
|
225
|
+
});
|
|
226
|
+
await rm(pending, { force: true });
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
function safePath(root, path) {
|
|
230
|
+
if (!path || path.includes("\\") || path.includes("\0") || path.startsWith("/") || /^[A-Za-z]:/.test(path) || path.split("/").includes("..")) throw new Error("\u538B\u7F29\u5305\u8DEF\u5F84\u8D8A\u754C");
|
|
231
|
+
const target = resolve(root, path);
|
|
232
|
+
if (target !== root && !target.startsWith(root + sep)) throw new Error("\u538B\u7F29\u5305\u8DEF\u5F84\u8D8A\u754C");
|
|
233
|
+
return target;
|
|
234
|
+
}
|
|
235
|
+
async function extractArchive(archive, root, expandedLimit) {
|
|
236
|
+
root = resolve(root);
|
|
237
|
+
await mkdir2(root, { mode: 448 });
|
|
238
|
+
root = await realpath(root);
|
|
239
|
+
const stream = createReadStream(archive).pipe(createGunzip());
|
|
240
|
+
const iterator = stream[Symbol.asyncIterator]();
|
|
241
|
+
let buffer = Buffer.alloc(0), expanded = 0, count = 0;
|
|
242
|
+
const take = async (size) => {
|
|
243
|
+
while (buffer.length < size) {
|
|
244
|
+
const next = await iterator.next();
|
|
245
|
+
if (next.done) throw new Error("\u538B\u7F29\u5305\u63D0\u524D\u7ED3\u675F");
|
|
246
|
+
buffer = Buffer.concat([buffer, Buffer.from(next.value)]);
|
|
247
|
+
}
|
|
248
|
+
const value = buffer.subarray(0, size);
|
|
249
|
+
buffer = buffer.subarray(size);
|
|
250
|
+
return value;
|
|
251
|
+
};
|
|
252
|
+
const string = (b) => b.toString("utf8").replace(/\0.*$/s, "");
|
|
253
|
+
const links = [];
|
|
254
|
+
let pax = {};
|
|
255
|
+
try {
|
|
256
|
+
while (true) {
|
|
257
|
+
const header = await take(512);
|
|
258
|
+
if (header.every((byte) => byte === 0)) break;
|
|
259
|
+
if (++count > 2e5) throw new Error("\u538B\u7F29\u5305\u6587\u4EF6\u6570\u8D85\u9650");
|
|
260
|
+
const checksum = parseInt(string(header.subarray(148, 156)).trim(), 8);
|
|
261
|
+
const actual = header.reduce((sum, byte, i) => sum + (i >= 148 && i < 156 ? 32 : byte), 0);
|
|
262
|
+
if (checksum !== actual) throw new Error("\u538B\u7F29\u5305\u5934\u6821\u9A8C\u5931\u8D25");
|
|
263
|
+
const sizeText = string(header.subarray(124, 136)).trim();
|
|
264
|
+
if (!/^[0-7]+$/.test(sizeText)) throw new Error("\u538B\u7F29\u5305\u6587\u4EF6\u5927\u5C0F\u65E0\u6548");
|
|
265
|
+
const size = parseInt(sizeText, 8), kind = String.fromCharCode(header[156] || 48);
|
|
266
|
+
expanded += size;
|
|
267
|
+
if (!Number.isSafeInteger(size) || expanded > expandedLimit) throw new Error("\u89E3\u538B\u8D85\u8FC7\u53D1\u884C\u9884\u7B97");
|
|
268
|
+
if (kind === "x") {
|
|
269
|
+
if (size > 64 * 1024) throw new Error("\u538B\u7F29\u5305\u6269\u5C55\u5934\u8D85\u9650");
|
|
270
|
+
const data = await take(size);
|
|
271
|
+
let offset = 0;
|
|
272
|
+
pax = {};
|
|
273
|
+
while (offset < data.length) {
|
|
274
|
+
const space = data.indexOf(32, offset), length = Number(data.subarray(offset, space).toString());
|
|
275
|
+
if (space < 0 || !Number.isSafeInteger(length) || length <= space - offset + 1 || offset + length > data.length) throw new Error("\u538B\u7F29\u5305\u6269\u5C55\u5934\u65E0\u6548");
|
|
276
|
+
const record = data.subarray(space + 1, offset + length - 1).toString("utf8"), equal = record.indexOf("=");
|
|
277
|
+
if (equal < 1) throw new Error("\u538B\u7F29\u5305\u6269\u5C55\u5934\u65E0\u6548");
|
|
278
|
+
pax[record.slice(0, equal)] = record.slice(equal + 1);
|
|
279
|
+
offset += length;
|
|
280
|
+
}
|
|
281
|
+
} else {
|
|
282
|
+
const prefix = string(header.subarray(345, 500));
|
|
283
|
+
const name = pax.path ?? (prefix ? prefix + "/" : "") + string(header.subarray(0, 100));
|
|
284
|
+
const target = safePath(root, name);
|
|
285
|
+
const linkText = pax.linkpath ?? string(header.subarray(157, 257));
|
|
286
|
+
if (Object.keys(pax).some((key) => /sparse|size/i.test(key))) throw new Error("\u4E0D\u652F\u6301\u7A00\u758F\u6216\u6539\u5199\u5927\u5C0F\u7684\u538B\u7F29\u5305");
|
|
287
|
+
pax = {};
|
|
288
|
+
await mkdir2(dirname2(target), { recursive: true, mode: 448 });
|
|
289
|
+
if (kind === "5") {
|
|
290
|
+
if (size) throw new Error("\u76EE\u5F55\u643A\u5E26\u6570\u636E");
|
|
291
|
+
await mkdir2(target, { recursive: true, mode: 448 });
|
|
292
|
+
} else if (kind === "0") {
|
|
293
|
+
const mode = parseInt(string(header.subarray(100, 108)).trim(), 8);
|
|
294
|
+
const file = await open2(target, "wx", mode & 73 ? 448 : 384);
|
|
295
|
+
try {
|
|
296
|
+
let remaining = size;
|
|
297
|
+
while (remaining) {
|
|
298
|
+
const chunk = await take(Math.min(remaining, 64 * 1024));
|
|
299
|
+
await file.writeFile(chunk);
|
|
300
|
+
remaining -= chunk.length;
|
|
301
|
+
}
|
|
302
|
+
} finally {
|
|
303
|
+
await file.close();
|
|
304
|
+
}
|
|
305
|
+
} else if (kind === "1" || kind === "2") {
|
|
306
|
+
if (size || !linkText || linkText.startsWith("/") || linkText.includes("\\")) throw new Error("\u538B\u7F29\u5305\u94FE\u63A5\u65E0\u6548");
|
|
307
|
+
const relative = kind === "2" ? posix.normalize(posix.join(posix.dirname(name), linkText)) : linkText;
|
|
308
|
+
links.push({ target, source: safePath(root, relative), symbolic: kind === "2", text: linkText });
|
|
309
|
+
} else throw new Error(`\u538B\u7F29\u5305\u6761\u76EE\u7C7B\u578B\u4E0D\u53D7\u652F\u6301\uFF1A${kind}`);
|
|
310
|
+
}
|
|
311
|
+
if (size % 512) await take(512 - size % 512);
|
|
312
|
+
}
|
|
313
|
+
let pending = links;
|
|
314
|
+
while (pending.length) {
|
|
315
|
+
const later = [];
|
|
316
|
+
for (const item of pending) {
|
|
317
|
+
let source;
|
|
318
|
+
try {
|
|
319
|
+
source = await realpath(item.source);
|
|
320
|
+
} catch (e) {
|
|
321
|
+
if (e.code === "ENOENT") {
|
|
322
|
+
later.push(item);
|
|
323
|
+
continue;
|
|
324
|
+
}
|
|
325
|
+
throw e;
|
|
326
|
+
}
|
|
327
|
+
if (!source.startsWith(root + sep) || !(await lstat(source)).isFile()) throw new Error("\u94FE\u63A5\u5FC5\u987B\u6307\u5411\u5305\u5185\u666E\u901A\u6587\u4EF6");
|
|
328
|
+
if (item.symbolic) await symlink(item.text, item.target);
|
|
329
|
+
else await link(source, item.target);
|
|
330
|
+
}
|
|
331
|
+
if (later.length === pending.length) throw new Error("\u538B\u7F29\u5305\u94FE\u63A5\u7F3A\u5931\u6216\u5FAA\u73AF");
|
|
332
|
+
pending = later;
|
|
333
|
+
}
|
|
334
|
+
} finally {
|
|
335
|
+
stream.destroy();
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
// distribution/bootstrap/resources.ts
|
|
340
|
+
import { availableParallelism, totalmem } from "node:os";
|
|
341
|
+
import { availableMemory, constrainedMemory } from "node:process";
|
|
342
|
+
import { readFile as readFile3, realpath as realpath2, stat, statfs } from "node:fs/promises";
|
|
343
|
+
import { dirname as dirname3, resolve as resolve2, sep as sep2 } from "node:path";
|
|
344
|
+
import { execFile as execFile2 } from "node:child_process";
|
|
345
|
+
import { promisify as promisify2 } from "node:util";
|
|
346
|
+
var execute2 = promisify2(execFile2);
|
|
347
|
+
async function existingParent(path) {
|
|
348
|
+
let current = resolve2(path);
|
|
349
|
+
while (true) {
|
|
350
|
+
try {
|
|
351
|
+
await stat(current);
|
|
352
|
+
return current;
|
|
353
|
+
} catch (e) {
|
|
354
|
+
if (e.code !== "ENOENT") throw e;
|
|
355
|
+
}
|
|
356
|
+
const parent = dirname3(current);
|
|
357
|
+
if (parent === current) throw new Error("\u6CA1\u6709\u53EF\u8BFB\u53D6\u7684\u6587\u4EF6\u7CFB\u7EDF\u7236\u76EE\u5F55");
|
|
358
|
+
current = parent;
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
async function sampleResources(paths) {
|
|
362
|
+
const limit = constrainedMemory();
|
|
363
|
+
let effectiveMemoryBytes = Math.min(totalmem(), limit || Infinity);
|
|
364
|
+
let availableMemoryBytes = null;
|
|
365
|
+
let pressure = "unknown";
|
|
366
|
+
try {
|
|
367
|
+
availableMemoryBytes = availableMemory();
|
|
368
|
+
if (!(availableMemoryBytes > 0)) availableMemoryBytes = null;
|
|
369
|
+
} catch {
|
|
370
|
+
}
|
|
371
|
+
if (!(effectiveMemoryBytes > 0)) effectiveMemoryBytes = null;
|
|
372
|
+
if (process.platform === "darwin") {
|
|
373
|
+
try {
|
|
374
|
+
const { stdout } = await execute2("/usr/sbin/sysctl", ["-n", "kern.memorystatus_vm_pressure_level"], { timeout: 2e3 });
|
|
375
|
+
pressure = { 1: "normal", 2: "high", 4: "critical" }[Number(stdout.trim())] ?? "unknown";
|
|
376
|
+
} catch {
|
|
377
|
+
}
|
|
378
|
+
if (pressure === "normal" && availableMemoryBytes !== null && effectiveMemoryBytes !== null) {
|
|
379
|
+
try {
|
|
380
|
+
const { stdout } = await execute2("/usr/bin/vm_stat", [], { timeout: 2e3 });
|
|
381
|
+
const pageBytes = Number(/page size of (\d+) bytes/.exec(stdout)?.[1]);
|
|
382
|
+
const pageCounts = ["free", "purgeable"].map((kind) => Number(new RegExp(`^Pages ${kind}:\\s+(\\d+)\\.`, "m").exec(stdout)?.[1]));
|
|
383
|
+
const reclaimable = pageBytes * pageCounts.reduce((sum, pages) => sum + pages, 0);
|
|
384
|
+
if (Number.isSafeInteger(reclaimable) && reclaimable > 0) {
|
|
385
|
+
availableMemoryBytes = Math.min(effectiveMemoryBytes, Math.max(availableMemoryBytes, reclaimable));
|
|
386
|
+
}
|
|
387
|
+
} catch {
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
} else if (process.platform === "linux") {
|
|
391
|
+
try {
|
|
392
|
+
const info = await readFile3("/proc/meminfo", "utf8");
|
|
393
|
+
const available = /^MemAvailable:\s+(\d+) kB$/m.exec(info);
|
|
394
|
+
if (available) availableMemoryBytes = Math.min(availableMemoryBytes ?? Infinity, Number(available[1]) * 1024);
|
|
395
|
+
if (limit) {
|
|
396
|
+
try {
|
|
397
|
+
const current = Number((await readFile3("/sys/fs/cgroup/memory.current", "utf8")).trim());
|
|
398
|
+
if (Number.isFinite(current)) availableMemoryBytes = Math.min(availableMemoryBytes ?? Infinity, Math.max(0, limit - current));
|
|
399
|
+
} catch {
|
|
400
|
+
availableMemoryBytes = null;
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
const psi = await readFile3("/proc/pressure/memory", "utf8");
|
|
404
|
+
const full = /^full avg10=(\d+(?:\.\d+)?)/m.exec(psi);
|
|
405
|
+
if (full) pressure = Number(full[1]) > 0 ? "high" : "normal";
|
|
406
|
+
} catch {
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
const disks = [];
|
|
410
|
+
for (const path of [...new Set(paths.map((p) => resolve2(p)))]) {
|
|
411
|
+
try {
|
|
412
|
+
const parent = await realpath2(await existingParent(path));
|
|
413
|
+
const [meta, space] = await Promise.all([stat(parent), statfs(parent, { bigint: true })]);
|
|
414
|
+
const device = String(meta.dev);
|
|
415
|
+
const bytes = space.bavail * space.bsize;
|
|
416
|
+
const availableBytes = bytes >= 0n && bytes <= BigInt(Number.MAX_SAFE_INTEGER) ? Number(bytes) : null;
|
|
417
|
+
const disk = disks.find((d) => d.device === device);
|
|
418
|
+
if (disk) {
|
|
419
|
+
disk.paths.push(path);
|
|
420
|
+
disk.availableBytes = disk.availableBytes === null || availableBytes === null ? null : Math.min(disk.availableBytes, availableBytes);
|
|
421
|
+
} else disks.push({ device, availableBytes, paths: [path] });
|
|
422
|
+
} catch {
|
|
423
|
+
disks.push({ device: `unknown:${path}`, availableBytes: null, paths: [path] });
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
return { effectiveMemoryBytes, availableMemoryBytes, pressure, disks, sampledAt: (/* @__PURE__ */ new Date()).toISOString(), adapter: `${process.platform}/${process.arch}/node-${process.versions.node}` };
|
|
427
|
+
}
|
|
428
|
+
function diskBudgets(sample, costs, reserve) {
|
|
429
|
+
const budgets = /* @__PURE__ */ new Map();
|
|
430
|
+
for (const cost of costs) {
|
|
431
|
+
const candidates = sample.disks.flatMap((d) => d.paths.map((path) => ({ disk: d, path }))).filter(({ path }) => cost.path === path || cost.path.startsWith(path.endsWith(sep2) ? path : path + sep2)).sort((a, b) => b.path.length - a.path.length);
|
|
432
|
+
const disk = candidates[0]?.disk;
|
|
433
|
+
const device = disk?.device ?? `unknown:${cost.path}`;
|
|
434
|
+
const budget = budgets.get(device) ?? { device, availableBytes: disk?.availableBytes ?? null, requiredBytes: reserve };
|
|
435
|
+
budget.requiredBytes += cost.bytes;
|
|
436
|
+
budgets.set(device, budget);
|
|
437
|
+
}
|
|
438
|
+
return [...budgets.values()];
|
|
439
|
+
}
|
|
440
|
+
function evaluateResources(sample, policy, costs, phase) {
|
|
441
|
+
const reasons = [];
|
|
442
|
+
let blocked = false, unknown = false, limited = false;
|
|
443
|
+
if (sample.effectiveMemoryBytes === null || sample.availableMemoryBytes === null) {
|
|
444
|
+
unknown = true;
|
|
445
|
+
reasons.push("\u5173\u952E\u5185\u5B58\u63A2\u9488\u672A\u77E5");
|
|
446
|
+
}
|
|
447
|
+
if (sample.effectiveMemoryBytes !== null && sample.effectiveMemoryBytes < policy.minimumEffectiveMemoryBytes) {
|
|
448
|
+
blocked = true;
|
|
449
|
+
reasons.push("\u6709\u6548\u5185\u5B58\u5BB9\u91CF\u4E0D\u8DB3");
|
|
450
|
+
}
|
|
451
|
+
const needed = phase === "heavy" ? policy.heavyJobHeadroomBytes : policy.startupHeadroomBytes;
|
|
452
|
+
if (sample.availableMemoryBytes !== null && sample.availableMemoryBytes < needed) {
|
|
453
|
+
blocked = true;
|
|
454
|
+
reasons.push("\u5F53\u524D\u5185\u5B58\u4F59\u91CF\u4E0D\u8DB3");
|
|
455
|
+
}
|
|
456
|
+
if (sample.pressure === "critical") {
|
|
457
|
+
blocked = true;
|
|
458
|
+
reasons.push("\u7CFB\u7EDF\u5185\u5B58\u538B\u529B\u4E25\u91CD");
|
|
459
|
+
}
|
|
460
|
+
const disks = diskBudgets(sample, costs, policy.diskReserveBytes);
|
|
461
|
+
for (const disk of disks) {
|
|
462
|
+
if (disk.availableBytes === null) {
|
|
463
|
+
unknown = true;
|
|
464
|
+
reasons.push(`\u6587\u4EF6\u7CFB\u7EDF\u53EF\u7528\u7A7A\u95F4\u672A\u77E5\uFF1A${disk.device}`);
|
|
465
|
+
} else if (disk.availableBytes < disk.requiredBytes) {
|
|
466
|
+
blocked = true;
|
|
467
|
+
const available = (disk.availableBytes / 1e9).toFixed(2);
|
|
468
|
+
const required = (disk.requiredBytes / 1e9).toFixed(2);
|
|
469
|
+
const missing = (Math.ceil((disk.requiredBytes - disk.availableBytes) / 1e7) / 100).toFixed(2);
|
|
470
|
+
reasons.push(`\u78C1\u76D8\u7A7A\u95F4\u4E0D\u8DB3\uFF1A\u5269\u4F59 ${available} GB\uFF0C\u9700\u8981 ${required} GB\u3002\u8BF7\u81F3\u5C11\u518D\u817E\u51FA ${missing} GB\u3002`);
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
if (sample.pressure === "high" || sample.pressure === "unknown") {
|
|
474
|
+
limited = true;
|
|
475
|
+
reasons.push(sample.pressure === "high" ? "\u5F53\u524D\u5185\u5B58\u6BD4\u8F83\u7D27\u5F20\uFF0C\u53EF\u4EE5\u5148\u5173\u95ED\u6682\u65F6\u4E0D\u7528\u7684\u5E94\u7528\u3002" : "\u6682\u65F6\u65E0\u6CD5\u786E\u8BA4\u5185\u5B58\u538B\u529B\u3002");
|
|
476
|
+
}
|
|
477
|
+
const status = blocked ? "blocked" : unknown ? "unknown" : limited ? "limited" : "pass";
|
|
478
|
+
return { status, reasons, disks, sample, policy, concurrency: status === "pass" ? Math.min(1, availableParallelism()) : 0 };
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
// runtime/npm.mjs
|
|
482
|
+
import { existsSync } from "node:fs";
|
|
483
|
+
import { dirname as dirname4, join as join2 } from "node:path";
|
|
484
|
+
function npmCommand() {
|
|
485
|
+
if (process.platform !== "win32") return { command: "npm", prefix: [] };
|
|
486
|
+
const cli = join2(dirname4(process.execPath), "node_modules", "npm", "bin", "npm-cli.js");
|
|
487
|
+
if (existsSync(cli)) return { command: process.execPath, prefix: [cli] };
|
|
488
|
+
return { command: process.env.ComSpec ?? "cmd.exe", prefix: ["/d", "/s", "/c", "npm"] };
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
// distribution/bootstrap/install.ts
|
|
492
|
+
var digest = (value) => createHash2("sha256").update(JSON.stringify(value)).digest("hex");
|
|
493
|
+
async function ownedDirectory(path, owner) {
|
|
494
|
+
await mkdir3(resolve3(path, ".."), { recursive: true, mode: 448 });
|
|
495
|
+
try {
|
|
496
|
+
await mkdir3(path, { mode: 448 });
|
|
497
|
+
await atomicJSON(join3(path, ".huohuo-owner.json"), { format: 1, owner });
|
|
498
|
+
} catch (e) {
|
|
499
|
+
if (e.code !== "EEXIST") throw e;
|
|
500
|
+
const marker = JSON.parse(await readFile4(join3(path, ".huohuo-owner.json"), "utf8"));
|
|
501
|
+
if (marker.format !== 1 || marker.owner !== owner) throw new Error("\u76EE\u5F55\u5F52\u5C5E\u65E0\u6CD5\u6838\u9A8C\uFF1B\u672A\u63A5\u7BA1\u6216\u8986\u76D6");
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
async function run(command, args, cwd, env = process.env) {
|
|
505
|
+
await new Promise((done, fail) => {
|
|
506
|
+
const child = spawn(command, args, { cwd, env, stdio: "inherit", detached: process.platform !== "win32" });
|
|
507
|
+
let interrupted = false;
|
|
508
|
+
let timer;
|
|
509
|
+
const kill = (signal) => {
|
|
510
|
+
if (!child.pid) return;
|
|
511
|
+
try {
|
|
512
|
+
process.kill(process.platform === "win32" ? child.pid : -child.pid, signal);
|
|
513
|
+
} catch {
|
|
514
|
+
}
|
|
515
|
+
};
|
|
516
|
+
const stop = () => {
|
|
517
|
+
interrupted = true;
|
|
518
|
+
kill("SIGTERM");
|
|
519
|
+
timer ??= setTimeout(() => kill("SIGKILL"), 3e3);
|
|
520
|
+
};
|
|
521
|
+
process.on("SIGINT", stop);
|
|
522
|
+
process.on("SIGTERM", stop);
|
|
523
|
+
const cleanup = () => {
|
|
524
|
+
clearTimeout(timer);
|
|
525
|
+
process.off("SIGINT", stop);
|
|
526
|
+
process.off("SIGTERM", stop);
|
|
527
|
+
};
|
|
528
|
+
child.once("error", (error) => {
|
|
529
|
+
cleanup();
|
|
530
|
+
fail(error);
|
|
531
|
+
});
|
|
532
|
+
child.once("close", (code) => {
|
|
533
|
+
cleanup();
|
|
534
|
+
code === 0 && !interrupted ? done() : fail(Object.assign(new Error("\u672C\u673A\u5B89\u88C5\u5B50\u8FDB\u7A0B\u5931\u8D25\u6216\u4E2D\u6B62\uFF0C\u4FDD\u7559\u539F\u9636\u6BB5\u4E0E\u5B9E\u4F8B"), { code: interrupted ? "INTERRUPTED" : `CHILD_${code}` }));
|
|
535
|
+
});
|
|
536
|
+
});
|
|
537
|
+
}
|
|
538
|
+
async function readInstallation(home, recordFile = "installation.json") {
|
|
539
|
+
let record;
|
|
540
|
+
try {
|
|
541
|
+
record = JSON.parse(await readFile4(join3(home, recordFile), "utf8"));
|
|
542
|
+
} catch (e) {
|
|
543
|
+
if (e.code === "ENOENT") return;
|
|
544
|
+
throw new Error("\u5B89\u88C5\u8BB0\u5F55\u65E0\u6CD5\u8BFB\u53D6\uFF0C\u672A\u91CD\u5EFA\u5B9E\u4F8B", { cause: e });
|
|
545
|
+
}
|
|
546
|
+
if (record.format !== 1 || record.home !== home || !record.stages || !["installing", "blocked", "failed", "prepared", "uninstalled", "ready"].includes(record.status)) throw new Error("\u5B89\u88C5\u683C\u5F0F\u4E0D\u517C\u5BB9\u6216\u76EE\u5F55\u7ED1\u5B9A\u53D8\u5316");
|
|
547
|
+
validateRelease(record.release);
|
|
548
|
+
if (record.releaseDigest !== digest(record.release)) throw new Error("\u5B89\u88C5\u53D1\u884C\u6458\u8981\u53D8\u5316\uFF0C\u505C\u6B62\u6062\u590D");
|
|
549
|
+
return record;
|
|
550
|
+
}
|
|
551
|
+
function installationPaths(home, release) {
|
|
552
|
+
const id = `${release.version}-${digest(release).slice(0, 16)}`;
|
|
553
|
+
const directory = join3(home, "releases", id);
|
|
554
|
+
const dependencyBundle = release.pythonDependencies.bundled ? `-${release.pythonDependencies.sha256.slice(0, 16)}` : "";
|
|
555
|
+
return {
|
|
556
|
+
directory,
|
|
557
|
+
runtime: join3(directory, "node_modules/@huohuo/research-agent"),
|
|
558
|
+
python: join3(home, "runtimes/python", release.python.sha256),
|
|
559
|
+
environment: join3(home, "environments", `${release.python.sha256.slice(0, 16)}-${release.requirementsSha256.slice(0, 16)}${dependencyBundle}`),
|
|
560
|
+
archive: join3(home, "cache", release.runtime.sha256 + ".tgz"),
|
|
561
|
+
pythonArchive: join3(home, "cache", release.python.sha256 + ".tgz")
|
|
562
|
+
};
|
|
563
|
+
}
|
|
564
|
+
function versionAtLeast(actual, minimum) {
|
|
565
|
+
const a = actual.split(".").map(Number), b = minimum.split(".").map(Number);
|
|
566
|
+
if ([...a, ...b].some((n) => !Number.isInteger(n))) return false;
|
|
567
|
+
for (let i = 0; i < Math.max(a.length, b.length); i++) {
|
|
568
|
+
if ((a[i] ?? 0) !== (b[i] ?? 0)) return (a[i] ?? 0) > (b[i] ?? 0);
|
|
569
|
+
}
|
|
570
|
+
return true;
|
|
571
|
+
}
|
|
572
|
+
async function install(home, candidate, hooks = {}, programsOnly = false) {
|
|
573
|
+
home = resolve3(home);
|
|
574
|
+
await mkdir3(home, { recursive: true, mode: 448 });
|
|
575
|
+
home = await realpath3(home);
|
|
576
|
+
return withLock(join3(home, "state/install.lock"), async () => {
|
|
577
|
+
if (!programsOnly) {
|
|
578
|
+
try {
|
|
579
|
+
await access(join3(home, "state/default-run.lock"));
|
|
580
|
+
throw new Error("\u5B9E\u4F8B\u4ECD\u6709\u8FD0\u884C\u9501\uFF1B\u5148\u6B63\u5E38\u505C\u6B62\u81EA\u5DF1\u7684\u5B9E\u4F8B\u518D\u4FEE\u590D\u7A0B\u5E8F");
|
|
581
|
+
} catch (e) {
|
|
582
|
+
if (e.code !== "ENOENT") throw e;
|
|
583
|
+
}
|
|
584
|
+
}
|
|
585
|
+
const recordFile = programsOnly ? "state/upgrade-target.json" : "installation.json";
|
|
586
|
+
const recordPath = join3(home, recordFile);
|
|
587
|
+
let record = await readInstallation(home, recordFile);
|
|
588
|
+
if (!programsOnly && !record && (await readdir(home)).some((name) => name !== "state")) throw new Error("\u76EE\u5F55\u542B\u65E0\u5B89\u88C5\u8BB0\u5F55\u7684\u65E2\u6709\u5185\u5BB9\uFF1B\u8BF7\u6838\u67E5\u5F52\u5C5E\u6216\u9009\u62E9\u7A7A\u76EE\u5F55");
|
|
589
|
+
if (!programsOnly && !record && (await readdir(join3(home, "state"))).some((name) => name !== "install.lock")) throw new Error("\u76EE\u5F55\u542B\u65E0\u6CD5\u786E\u8BA4\u5F52\u5C5E\u7684\u72B6\u6001\uFF1B\u672A\u91CD\u5EFA\u5B89\u88C5");
|
|
590
|
+
if (programsOnly && (await readInstallation(home))?.status !== "ready") throw new Error("\u5F53\u524D\u5B89\u88C5\u4E0D\u5B8C\u6574\uFF0C\u5148\u4FEE\u590D\u539F\u7248\u672C");
|
|
591
|
+
const release = record?.release ?? validateRelease(candidate);
|
|
592
|
+
if (release.platform.os !== process.platform || release.platform.arch !== process.arch || !versionAtLeast(osRelease(), release.platform.minimumOsRelease) || !versionAtLeast(process.versions.node, "24.8.0") || Number(process.versions.node.split(".")[0]) !== release.nodeMajor) {
|
|
593
|
+
throw new Error("\u6B64\u53D1\u884C\u5C1A\u672A\u9A8C\u8BC1\u5F53\u524D OS/CPU/Node \u7EC4\u5408\uFF1B\u672A\u5F00\u59CB\u91CD\u4E0B\u8F7D");
|
|
594
|
+
}
|
|
595
|
+
record ??= { format: 1, release, releaseDigest: digest(release), home, status: "installing", stage: "preflight", stages: {} };
|
|
596
|
+
await atomicJSON(recordPath, record);
|
|
597
|
+
const paths = installationPaths(home, release);
|
|
598
|
+
const execute4 = hooks.run ?? run;
|
|
599
|
+
const sample = hooks.sample ?? sampleResources;
|
|
600
|
+
const cached = async (path, artifact2) => {
|
|
601
|
+
try {
|
|
602
|
+
await verifyArtifact(path, artifact2);
|
|
603
|
+
return true;
|
|
604
|
+
} catch (error) {
|
|
605
|
+
if (error.code === "ENOENT") return false;
|
|
606
|
+
throw error;
|
|
607
|
+
}
|
|
608
|
+
};
|
|
609
|
+
const installed = async () => {
|
|
610
|
+
try {
|
|
611
|
+
const owner = JSON.parse(await readFile4(join3(paths.python, ".huohuo-owner.json"), "utf8"));
|
|
612
|
+
await access(join3(paths.python, release.python.executable));
|
|
613
|
+
const python = owner.owner === release.python.sha256;
|
|
614
|
+
const root = join3(home, "instances/default");
|
|
615
|
+
const initialization = JSON.parse(await readFile4(join3(root, "initialization.json"), "utf8"));
|
|
616
|
+
const dependencies = JSON.parse(await readFile4(join3(paths.environment, ".dependencies-ready.json"), "utf8"));
|
|
617
|
+
const worker = JSON.parse(await readFile4(join3(root, "worker-install.json"), "utf8"));
|
|
618
|
+
const chat = JSON.parse(await readFile4(join3(root, "dsh/profiles/huohuo-chat/installed.json"), "utf8"));
|
|
619
|
+
for (const profile of ["huohuo-chat", "huohuo-worker", "huohuo-strategy-worker"]) await access(join3(root, "dsh/profiles", profile, "node_modules/@huohuo/research-agent/package.json"));
|
|
620
|
+
await access(join3(paths.runtime, "lib/distribution/managed.js"));
|
|
621
|
+
return { python, dependencies: initialization.package_sha256 === release.runtime.sha256 && initialization.status === "complete" && dependencies.sha256 === release.pythonDependencies.sha256 && worker.complete === true && chat.complete === true };
|
|
622
|
+
} catch (error) {
|
|
623
|
+
if (error.code !== "ENOENT") throw error;
|
|
624
|
+
try {
|
|
625
|
+
const owner = JSON.parse(await readFile4(join3(paths.python, ".huohuo-owner.json"), "utf8"));
|
|
626
|
+
await access(join3(paths.python, release.python.executable));
|
|
627
|
+
return { python: owner.owner === release.python.sha256, dependencies: false };
|
|
628
|
+
} catch {
|
|
629
|
+
return { python: false, dependencies: false };
|
|
630
|
+
}
|
|
631
|
+
}
|
|
632
|
+
};
|
|
633
|
+
const check = async () => {
|
|
634
|
+
const [runtimeCached, pythonCached, wheelsCached, inventory] = await Promise.all([
|
|
635
|
+
cached(paths.archive, release.runtime),
|
|
636
|
+
cached(paths.pythonArchive, release.python),
|
|
637
|
+
cached(join3(home, "cache", release.pythonDependencies.sha256 + ".tgz"), release.pythonDependencies),
|
|
638
|
+
installed()
|
|
639
|
+
]);
|
|
640
|
+
const remainingDependencies = inventory.dependencies ? 0 : release.budget.dependenciesBytes;
|
|
641
|
+
const costs = [
|
|
642
|
+
{ path: join3(home, "cache"), bytes: (runtimeCached ? 0 : release.runtime.downloadBytes) + (pythonCached ? 0 : release.python.downloadBytes) + (wheelsCached ? 0 : release.pythonDependencies.downloadBytes) },
|
|
643
|
+
{ path: paths.directory, bytes: inventory.dependencies ? 0 : release.runtime.expandedBytes + release.budget.installTemporaryBytes },
|
|
644
|
+
{ path: paths.python, bytes: inventory.python ? 0 : release.python.expandedBytes },
|
|
645
|
+
{ path: paths.environment, bytes: 0 },
|
|
646
|
+
{ path: join3(home, "instances/default"), bytes: 0 },
|
|
647
|
+
{ path: paths.directory, bytes: inventory.dependencies ? 0 : release.pythonDependencies.expandedBytes },
|
|
648
|
+
{ path: join3(home, "backups"), bytes: release.budget.backupBytes }
|
|
649
|
+
];
|
|
650
|
+
const observation = await sample(costs.map((c) => c.path));
|
|
651
|
+
const dependencyPaths = [paths.directory, paths.environment, join3(home, "instances/default")];
|
|
652
|
+
const volumes = /* @__PURE__ */ new Set();
|
|
653
|
+
for (const path of dependencyPaths) {
|
|
654
|
+
const device = diskBudgets(observation, [{ path, bytes: 0 }], 0)[0].device;
|
|
655
|
+
if (!volumes.has(device)) {
|
|
656
|
+
volumes.add(device);
|
|
657
|
+
costs.push({ path, bytes: remainingDependencies });
|
|
658
|
+
}
|
|
659
|
+
}
|
|
660
|
+
const result = evaluateResources(observation, release.budget, costs, "install");
|
|
661
|
+
await atomicJSON(join3(home, "state/resources.json"), result);
|
|
662
|
+
if (result.status === "unknown" || result.status === "blocked") {
|
|
663
|
+
const details = result.reasons.map((reason) => `- ${reason}`).join("\n");
|
|
664
|
+
throw Object.assign(new Error(`\u706B\u706B\u6682\u65F6\u65E0\u6CD5\u7EE7\u7EED\u5B89\u88C5\u3002
|
|
665
|
+
${details}
|
|
666
|
+
\u5904\u7406\u540E\uFF0C\u91CD\u65B0\u8FD0\u884C\u521A\u624D\u7684\u5B89\u88C5\u547D\u4EE4\u3002`), { code: "RESOURCE_BLOCKED" });
|
|
667
|
+
}
|
|
668
|
+
};
|
|
669
|
+
const stage = async (name, action) => {
|
|
670
|
+
hooks.progress?.(name);
|
|
671
|
+
record.stage = name;
|
|
672
|
+
record.status = "installing";
|
|
673
|
+
const inputDigest = digest({ release: record.releaseDigest, stage: name });
|
|
674
|
+
const entry = { inputDigest, startedAt: (/* @__PURE__ */ new Date()).toISOString(), status: "running" };
|
|
675
|
+
record.stages[name] = entry;
|
|
676
|
+
await atomicJSON(recordPath, record);
|
|
677
|
+
try {
|
|
678
|
+
await check();
|
|
679
|
+
const output = await action();
|
|
680
|
+
entry.status = "verified";
|
|
681
|
+
entry.outputDigest = digest(output ?? null);
|
|
682
|
+
entry.endedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
683
|
+
} catch (error) {
|
|
684
|
+
entry.status = "failed";
|
|
685
|
+
entry.endedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
686
|
+
entry.errorCode = typeof error.code === "string" && /^[A-Z0-9_]+$/.test(error.code) ? error.code : "STAGE_FAILED";
|
|
687
|
+
record.status = entry.errorCode === "RESOURCE_BLOCKED" ? "blocked" : "failed";
|
|
688
|
+
await atomicJSON(recordPath, record);
|
|
689
|
+
throw error;
|
|
690
|
+
}
|
|
691
|
+
await atomicJSON(recordPath, record);
|
|
692
|
+
};
|
|
693
|
+
await stage("preflight", async () => {
|
|
694
|
+
const npm = npmCommand();
|
|
695
|
+
const { stdout } = await promisify3(execFile3)(npm.command, [...npm.prefix, "--version"], { timeout: 5e3 });
|
|
696
|
+
if (!/^\d+\.\d+\.\d+\s*$/.test(stdout) || Number(stdout.split(".")[0]) !== release.npmMajor) throw new Error("npm \u7248\u672C\u672A\u77E5\u6216\u4E0D\u5728\u6B64\u53D1\u884C\u5DF2\u9A8C\u8BC1\u8303\u56F4\u5185");
|
|
697
|
+
const probe = join3(home, `.write-probe-${randomUUID3()}`);
|
|
698
|
+
try {
|
|
699
|
+
await atomicJSON(probe, { probe: true });
|
|
700
|
+
} finally {
|
|
701
|
+
await rm2(probe, { force: true });
|
|
702
|
+
}
|
|
703
|
+
});
|
|
704
|
+
await stage("download", async () => {
|
|
705
|
+
await downloadArtifact(release.runtime, paths.archive, hooks.fetcher);
|
|
706
|
+
await downloadArtifact(release.python, paths.pythonArchive, hooks.fetcher);
|
|
707
|
+
await downloadArtifact(release.pythonDependencies, join3(home, "cache", release.pythonDependencies.sha256 + ".tgz"), hooks.fetcher);
|
|
708
|
+
return [release.runtime.sha256, release.python.sha256, release.pythonDependencies.sha256];
|
|
709
|
+
});
|
|
710
|
+
await stage("install-runtime", async () => {
|
|
711
|
+
try {
|
|
712
|
+
await access(paths.python);
|
|
713
|
+
const marker = JSON.parse(await readFile4(join3(paths.python, ".huohuo-owner.json"), "utf8"));
|
|
714
|
+
if (marker.owner !== release.python.sha256) throw new Error("\u53D7\u7BA1 Python \u5F52\u5C5E\u4E0D\u7B26");
|
|
715
|
+
await access(join3(paths.python, release.python.executable));
|
|
716
|
+
return release.python.sha256;
|
|
717
|
+
} catch (e) {
|
|
718
|
+
if (e.code !== "ENOENT") throw e;
|
|
719
|
+
}
|
|
720
|
+
await mkdir3(join3(home, "runtimes/python"), { recursive: true, mode: 448 });
|
|
721
|
+
const staging = `${paths.python}.${randomUUID3()}.staging`;
|
|
722
|
+
try {
|
|
723
|
+
await extractArchive(paths.pythonArchive, staging, release.python.expandedBytes);
|
|
724
|
+
await access(join3(staging, release.python.executable));
|
|
725
|
+
await atomicJSON(join3(staging, ".huohuo-owner.json"), { format: 1, owner: release.python.sha256 });
|
|
726
|
+
await rename3(staging, paths.python);
|
|
727
|
+
} finally {
|
|
728
|
+
await rm2(staging, { recursive: true, force: true });
|
|
729
|
+
}
|
|
730
|
+
return release.python.sha256;
|
|
731
|
+
});
|
|
732
|
+
await stage("install-dependencies", async () => {
|
|
733
|
+
await ownedDirectory(paths.directory, record.releaseDigest);
|
|
734
|
+
await atomicJSON(join3(paths.directory, "package.json"), {
|
|
735
|
+
name: "huohuo-managed-release",
|
|
736
|
+
private: true,
|
|
737
|
+
dependencies: { "@huohuo/research-agent": `file:${paths.archive}` }
|
|
738
|
+
});
|
|
739
|
+
const npm = npmCommand();
|
|
740
|
+
await execute4(npm.command, [...npm.prefix, "install", "--omit=dev", "--ignore-scripts", "--no-audit", "--no-fund", "--prefer-offline", "--cache", join3(home, "cache/npm")], paths.directory);
|
|
741
|
+
const runtime = JSON.parse(await readFile4(join3(paths.runtime, "package.json"), "utf8"));
|
|
742
|
+
if (runtime.name !== release.runtime.name || runtime.version !== release.version) throw new Error("\u8FD0\u884C\u5305\u8EAB\u4EFD\u4E0D\u7B26");
|
|
743
|
+
const requirements = await readFile4(join3(paths.runtime, "sidecar/requirements.lock"));
|
|
744
|
+
if (createHash2("sha256").update(requirements).digest("hex") !== release.requirementsSha256) throw new Error("Python \u4F9D\u8D56\u9501\u6458\u8981\u4E0D\u7B26");
|
|
745
|
+
const assembly = await readFile4(join3(paths.runtime, "npm-shrinkwrap.json"));
|
|
746
|
+
if (createHash2("sha256").update(assembly).digest("hex") !== release.dsh.assemblySha256) throw new Error("DSH \u88C5\u914D\u9501\u6458\u8981\u4E0D\u7B26");
|
|
747
|
+
await ownedDirectory(paths.environment, `${release.python.sha256}:${release.requirementsSha256}`);
|
|
748
|
+
const python = join3(paths.environment, process.platform === "win32" ? "Scripts/python.exe" : "bin/python");
|
|
749
|
+
const wheels = join3(paths.directory, `wheels-${release.pythonDependencies.sha256}`);
|
|
750
|
+
try {
|
|
751
|
+
await access(join3(wheels, "requirements.txt"));
|
|
752
|
+
} catch (e) {
|
|
753
|
+
if (e.code !== "ENOENT") throw e;
|
|
754
|
+
const staging = `${wheels}.${randomUUID3()}.staging`;
|
|
755
|
+
try {
|
|
756
|
+
await extractArchive(join3(home, "cache", release.pythonDependencies.sha256 + ".tgz"), staging, release.pythonDependencies.expandedBytes);
|
|
757
|
+
await access(join3(staging, "requirements.txt"));
|
|
758
|
+
await rename3(staging, wheels);
|
|
759
|
+
} finally {
|
|
760
|
+
await rm2(staging, { recursive: true, force: true });
|
|
761
|
+
}
|
|
762
|
+
}
|
|
763
|
+
let environmentReady = false;
|
|
764
|
+
try {
|
|
765
|
+
const ready = JSON.parse(await readFile4(join3(paths.environment, ".dependencies-ready.json"), "utf8"));
|
|
766
|
+
if (ready.sha256 !== release.pythonDependencies.sha256) throw new Error("\u5DF2\u6709\u73AF\u5883\u7ED1\u5B9A\u4E0D\u540C wheel \u4EA7\u7269\uFF1B\u4E0D\u80FD\u539F\u4F4D\u6539\u5199\u5171\u4EAB\u73AF\u5883");
|
|
767
|
+
await access(python);
|
|
768
|
+
environmentReady = true;
|
|
769
|
+
} catch (e) {
|
|
770
|
+
if (e.code !== "ENOENT") throw e;
|
|
771
|
+
}
|
|
772
|
+
if (!environmentReady) {
|
|
773
|
+
await execute4(join3(paths.python, release.python.executable), ["-m", "venv", paths.environment], home);
|
|
774
|
+
await execute4(python, [
|
|
775
|
+
"-m",
|
|
776
|
+
"pip",
|
|
777
|
+
"--isolated",
|
|
778
|
+
"install",
|
|
779
|
+
"--no-index",
|
|
780
|
+
"--find-links",
|
|
781
|
+
wheels,
|
|
782
|
+
"--require-hashes",
|
|
783
|
+
"--only-binary=:all:",
|
|
784
|
+
...release.pythonDependencies.bundled ? ["--no-cache-dir"] : [],
|
|
785
|
+
"-r",
|
|
786
|
+
join3(wheels, "requirements.txt")
|
|
787
|
+
], home);
|
|
788
|
+
}
|
|
789
|
+
await execute4(python, ["-m", "pip", "--isolated", "check"], home);
|
|
790
|
+
await atomicJSON(join3(paths.environment, ".dependencies-ready.json"), { sha256: release.pythonDependencies.sha256 });
|
|
791
|
+
return { python, runtime: release.runtime.sha256 };
|
|
792
|
+
});
|
|
793
|
+
if (programsOnly) {
|
|
794
|
+
record.status = "prepared";
|
|
795
|
+
record.stage = "prepared";
|
|
796
|
+
await atomicJSON(recordPath, record);
|
|
797
|
+
return record;
|
|
798
|
+
}
|
|
799
|
+
await stage("assemble-profiles", async () => {
|
|
800
|
+
await execute4(process.execPath, [join3(paths.runtime, "lib/distribution/managed.js"), "prepare", home], home);
|
|
801
|
+
return { release: record.releaseDigest };
|
|
802
|
+
});
|
|
803
|
+
await stage("verify", async () => {
|
|
804
|
+
await execute4(process.execPath, [join3(paths.runtime, "lib/distribution/managed.js"), "verify-live", home], home);
|
|
805
|
+
return { release: record.releaseDigest };
|
|
806
|
+
});
|
|
807
|
+
record.status = "ready";
|
|
808
|
+
record.stage = "ready";
|
|
809
|
+
await atomicJSON(recordPath, record);
|
|
810
|
+
return record;
|
|
811
|
+
});
|
|
812
|
+
}
|
|
813
|
+
|
|
814
|
+
// distribution/bootstrap/control.ts
|
|
815
|
+
import { createServer, request as httpRequest } from "node:http";
|
|
816
|
+
import { readFile as readFile5, rm as rm3 } from "node:fs/promises";
|
|
817
|
+
import { join as join4 } from "node:path";
|
|
818
|
+
import { execFile as execFile4 } from "node:child_process";
|
|
819
|
+
import { promisify as promisify4 } from "node:util";
|
|
820
|
+
var execute3 = promisify4(execFile4);
|
|
821
|
+
async function control(home, releaseDigest, command) {
|
|
822
|
+
let binding;
|
|
823
|
+
try {
|
|
824
|
+
binding = JSON.parse(await readFile5(join4(home, "state/default-control.json"), "utf8"));
|
|
825
|
+
} catch (e) {
|
|
826
|
+
if (e.code === "ENOENT") return false;
|
|
827
|
+
throw new Error("\u672C\u673A\u8FD0\u884C\u8BB0\u5F55\u65E0\u6CD5\u6838\u9A8C");
|
|
828
|
+
}
|
|
829
|
+
if (binding.format !== 1 || binding.home !== home || binding.releaseDigest !== releaseDigest || !/^[A-Za-z0-9_-]{43}$/.test(binding.token) || typeof binding.runOwner !== "string" || !/^[a-f0-9-]{36}$/.test(binding.runOwner) || !Number.isInteger(binding.port) || binding.port < 1024 || binding.port > 65535) throw new Error("\u672C\u673A\u8FD0\u884C\u7ED1\u5B9A\u65E0\u6548\uFF1B\u672A\u64CD\u4F5C\u4EFB\u4F55\u8FDB\u7A0B");
|
|
830
|
+
try {
|
|
831
|
+
await new Promise((done, fail) => {
|
|
832
|
+
const req = httpRequest({
|
|
833
|
+
hostname: "127.0.0.1",
|
|
834
|
+
port: binding.port,
|
|
835
|
+
path: `/${command}`,
|
|
836
|
+
method: "POST",
|
|
837
|
+
headers: { Authorization: `Bearer ${binding.token}`, Connection: "close" },
|
|
838
|
+
timeout: 3e3
|
|
839
|
+
}, (res) => {
|
|
840
|
+
let body = "";
|
|
841
|
+
res.on("data", (chunk) => {
|
|
842
|
+
body += chunk;
|
|
843
|
+
if (body.length > 8192) req.destroy(new Error("\u672C\u673A\u63A7\u5236\u54CD\u5E94\u8D85\u9650"));
|
|
844
|
+
});
|
|
845
|
+
res.once("end", () => {
|
|
846
|
+
try {
|
|
847
|
+
const result = JSON.parse(body);
|
|
848
|
+
if (res.statusCode !== 200 || result.home !== home || result.releaseDigest !== releaseDigest) throw new Error();
|
|
849
|
+
done();
|
|
850
|
+
} catch {
|
|
851
|
+
fail(new Error("\u672C\u673A\u63A7\u5236\u8EAB\u4EFD\u6216\u54CD\u5E94\u672A\u901A\u8FC7\uFF1B\u672A\u63A5\u7BA1\u8FDB\u7A0B"));
|
|
852
|
+
}
|
|
853
|
+
});
|
|
854
|
+
});
|
|
855
|
+
req.once("timeout", () => req.destroy(new Error("\u672C\u673A\u63A7\u5236\u5165\u53E3\u65E0\u54CD\u5E94\uFF1B\u8BF7\u8FD0\u884C doctor \u6838\u5BF9\u539F\u7EC8\u7AEF")));
|
|
856
|
+
req.once("error", fail);
|
|
857
|
+
req.end();
|
|
858
|
+
});
|
|
859
|
+
} catch (error) {
|
|
860
|
+
if (error.code === "ECONNREFUSED" && await isLockOwnerDead(join4(home, "state/default-run.lock"), binding.runOwner)) return false;
|
|
861
|
+
throw error;
|
|
862
|
+
}
|
|
863
|
+
return true;
|
|
864
|
+
}
|
|
865
|
+
|
|
866
|
+
// distribution/bootstrap/maintenance.ts
|
|
867
|
+
import { access as access2, readFile as readFile6, readdir as readdir2, lstat as lstat2, rename as rename4, rm as rm4, realpath as realpath4 } from "node:fs/promises";
|
|
868
|
+
import { join as join5, resolve as resolve4 } from "node:path";
|
|
869
|
+
import { randomUUID as randomUUID4 } from "node:crypto";
|
|
870
|
+
import { setTimeout as delay } from "node:timers/promises";
|
|
871
|
+
async function upgradeJournal(home) {
|
|
872
|
+
try {
|
|
873
|
+
const value = JSON.parse(await readFile6(join5(home, "state/upgrade.json"), "utf8"));
|
|
874
|
+
if (value.format !== 1 || !/^[a-f0-9]{64}$/.test(value.from) || !/^[a-f0-9]{64}$/.test(value.to) || !["stopping", "backed-up", "schema-unchanged", "profiles", "migrated", "verified", "complete"].includes(value.stage) || !value.backup.startsWith(join5(home, "backups") + "/")) throw new Error("\u5347\u7EA7\u8BB0\u5F55\u65E0\u6548");
|
|
875
|
+
return value;
|
|
876
|
+
} catch (error) {
|
|
877
|
+
if (error.code === "ENOENT") return;
|
|
878
|
+
throw error;
|
|
879
|
+
}
|
|
880
|
+
}
|
|
881
|
+
function assertCompatibleUpgrade(previous, next) {
|
|
882
|
+
if (previous.schema.minimum !== next.schema.minimum || previous.schema.maximum !== next.schema.maximum || previous.dsh.version !== next.dsh.version) {
|
|
883
|
+
throw new Error("\u6570\u636E\u6216 DSH \u4F1A\u8BDD\u8FC1\u79FB\u8D85\u51FA\u5DF2\u9A8C\u8BC1\u8303\u56F4\uFF1B\u672A\u505C\u6B62\u539F\u5B9E\u4F8B\u6216\u8986\u76D6\u65B0\u6570\u636E");
|
|
884
|
+
}
|
|
885
|
+
const oldFiles = Object.keys(previous.schema.files).sort(), added = Object.keys(next.schema.files).filter((name) => !(name in previous.schema.files)).sort();
|
|
886
|
+
if (oldFiles.some((name) => next.schema.files[name] !== previous.schema.files[name]) || added.some((name) => name <= oldFiles.at(-1)) || previous.schema.migrationsSha256 !== next.schema.migrationsSha256 && !added.length) throw new Error("\u8FC1\u79FB\u4E0D\u662F\u5DF2\u5B9A\u4E49\u7684\u4E0D\u53EF\u53D8\u524D\u7F00\u8FFD\u52A0\uFF1B\u672A\u5207\u6362\u6570\u636E\u6216\u7A0B\u5E8F");
|
|
887
|
+
const before = previous.version.split("-")[0].split(".").map(Number), after = next.version.split("-")[0].split(".").map(Number);
|
|
888
|
+
for (let i = 0; i < 3; i++) {
|
|
889
|
+
if (after[i] > before[i]) return;
|
|
890
|
+
if (after[i] < before[i]) throw new Error("upgrade \u4E0D\u6267\u884C\u7248\u672C\u964D\u7EA7\uFF1B\u9700\u8981\u660E\u786E\u7684\u517C\u5BB9\u56DE\u9000\u65B9\u6848");
|
|
891
|
+
}
|
|
892
|
+
if (!previous.version.includes("-") && next.version.includes("-")) throw new Error("\u4E0D\u81EA\u52A8\u4ECE\u7A33\u5B9A\u7248\u672C\u5207\u6362\u5230\u9884\u53D1\u5E03");
|
|
893
|
+
}
|
|
894
|
+
async function stopOwned(home, releaseDigest) {
|
|
895
|
+
if (!await control(home, releaseDigest, "stop")) return;
|
|
896
|
+
for (let i = 0; i < 100; i++) {
|
|
897
|
+
try {
|
|
898
|
+
await access2(join5(home, "state/default-run.lock"));
|
|
899
|
+
} catch (error) {
|
|
900
|
+
if (error.code === "ENOENT") return;
|
|
901
|
+
throw error;
|
|
902
|
+
}
|
|
903
|
+
await delay(100);
|
|
904
|
+
}
|
|
905
|
+
throw new Error("\u539F\u5B9E\u4F8B\u5C1A\u672A\u786E\u8BA4\u9000\u51FA\uFF1B\u672A\u5E76\u884C\u542F\u52A8\u3001\u672A\u5F3A\u5236\u6740\u8FDB\u7A0B");
|
|
906
|
+
}
|
|
907
|
+
async function backupBytes(directory) {
|
|
908
|
+
let bytes = 1024 * 1024;
|
|
909
|
+
async function visit(path) {
|
|
910
|
+
for (const item of await readdir2(path, { withFileTypes: true })) {
|
|
911
|
+
if (["node_modules", "cache", "python", "__pycache__"].includes(item.name)) continue;
|
|
912
|
+
if (item.isSymbolicLink()) throw new Error("\u5907\u4EFD\u4E0D\u63A5\u7BA1\u672A\u77E5\u7684\u5B9E\u4F8B\u94FE\u63A5");
|
|
913
|
+
if (item.isDirectory()) await visit(join5(path, item.name));
|
|
914
|
+
else {
|
|
915
|
+
const info = await lstat2(join5(path, item.name));
|
|
916
|
+
bytes += info.size + 512;
|
|
917
|
+
}
|
|
918
|
+
}
|
|
919
|
+
}
|
|
920
|
+
await visit(directory);
|
|
921
|
+
if (!Number.isSafeInteger(bytes)) throw new Error("\u5907\u4EFD\u9884\u7B97\u8D85\u51FA\u5DF2\u652F\u6301\u8303\u56F4");
|
|
922
|
+
return bytes;
|
|
923
|
+
}
|
|
924
|
+
async function upgrade(home, candidate, hooks = {}) {
|
|
925
|
+
home = await realpath4(resolve4(home));
|
|
926
|
+
const next = validateRelease(candidate);
|
|
927
|
+
let current = await readInstallation(home);
|
|
928
|
+
if (!current || current.status !== "ready") throw new Error("\u5148\u4FEE\u590D\u5F53\u524D\u5B89\u88C5\uFF0C\u4E0D\u80FD\u5728\u4E0D\u5B8C\u6574\u5B9E\u4F8B\u4E0A\u5347\u7EA7");
|
|
929
|
+
let journal = await upgradeJournal(home);
|
|
930
|
+
if (journal && journal.stage !== "complete" && journal.to !== digest(next)) throw new Error("\u5B58\u5728\u672A\u5B8C\u6210\u5347\u7EA7\uFF1B\u5FC5\u987B\u5148\u7EE7\u7EED\u539F\u56FA\u5B9A\u76EE\u6807");
|
|
931
|
+
if (current.releaseDigest === digest(next)) {
|
|
932
|
+
if (journal && journal.to === current.releaseDigest && journal.stage !== "complete") {
|
|
933
|
+
await (hooks.run ?? run)(process.execPath, [join5(installationPaths(home, next).runtime, "lib/distribution/managed.js"), "verify-live", home], home);
|
|
934
|
+
journal.stage = "complete";
|
|
935
|
+
await atomicJSON(join5(home, "state/upgrade.json"), journal);
|
|
936
|
+
}
|
|
937
|
+
hooks.progress?.("\u5DF2\u7ECF\u662F\u540C\u4E00\u53D1\u884C\uFF0C\u65E0\u9700\u8FC1\u79FB");
|
|
938
|
+
return;
|
|
939
|
+
}
|
|
940
|
+
assertCompatibleUpgrade(current.release, next);
|
|
941
|
+
if (journal?.stage === "complete") {
|
|
942
|
+
await withLock(join5(home, "state/install.lock"), async () => {
|
|
943
|
+
await rename4(join5(home, "state/upgrade.json"), join5(home, `state/upgrade-${randomUUID4()}.json`));
|
|
944
|
+
try {
|
|
945
|
+
await rename4(join5(home, "state/upgrade-target.json"), join5(home, `state/upgrade-target-${randomUUID4()}.json`));
|
|
946
|
+
} catch (error) {
|
|
947
|
+
if (error.code !== "ENOENT") throw error;
|
|
948
|
+
}
|
|
949
|
+
});
|
|
950
|
+
journal = void 0;
|
|
951
|
+
}
|
|
952
|
+
const target = await install(home, next, hooks, true);
|
|
953
|
+
if (target.releaseDigest !== digest(next)) throw new Error("\u5347\u7EA7\u6062\u590D\u76EE\u6807\u4E0E\u8BF7\u6C42\u4E0D\u540C\uFF1B\u4FDD\u7559\u539F\u56FA\u5B9A\u76EE\u6807");
|
|
954
|
+
const execute4 = hooks.run ?? run;
|
|
955
|
+
await withLock(join5(home, "state/install.lock"), async () => {
|
|
956
|
+
current = await readInstallation(home);
|
|
957
|
+
if (journal && journal.from !== current.releaseDigest) throw new Error("\u5347\u7EA7\u671F\u95F4\u539F\u53D1\u884C\u7ED1\u5B9A\u53D8\u5316\uFF1B\u8BF7\u5148\u5BF9\u8D26");
|
|
958
|
+
journal ??= {
|
|
959
|
+
format: 1,
|
|
960
|
+
from: current.releaseDigest,
|
|
961
|
+
to: target.releaseDigest,
|
|
962
|
+
backup: join5(home, "backups", `upgrade-${target.releaseDigest.slice(0, 16)}-${randomUUID4()}`),
|
|
963
|
+
stage: "stopping"
|
|
964
|
+
};
|
|
965
|
+
await atomicJSON(join5(home, "state/upgrade.json"), journal);
|
|
966
|
+
await stopOwned(home, current.releaseDigest);
|
|
967
|
+
const releaseRun = await acquireLock(join5(home, "state/default-run.lock"));
|
|
968
|
+
try {
|
|
969
|
+
const bytes = await backupBytes(join5(home, "instances/default"));
|
|
970
|
+
const result = evaluateResources(
|
|
971
|
+
await (hooks.sample ?? sampleResources)([join5(home, "backups")]),
|
|
972
|
+
target.release.budget,
|
|
973
|
+
[{ path: join5(home, "backups"), bytes }],
|
|
974
|
+
"install"
|
|
975
|
+
);
|
|
976
|
+
if (result.status === "unknown" || result.status === "blocked") throw new Error("\u4E00\u81F4\u6027\u5907\u4EFD\u8D44\u6E90\u4E0D\u8DB3\uFF1B\u539F\u6570\u636E\u4FDD\u7559");
|
|
977
|
+
if (journal.stage === "stopping") {
|
|
978
|
+
await execute4(process.execPath, [join5(installationPaths(home, current.release).runtime, "lib/distribution/managed.js"), "backup", home], home);
|
|
979
|
+
journal.stage = "backed-up";
|
|
980
|
+
await atomicJSON(join5(home, "state/upgrade.json"), journal);
|
|
981
|
+
}
|
|
982
|
+
const runtime = installationPaths(home, target.release).runtime;
|
|
983
|
+
await execute4(process.execPath, [join5(runtime, "lib/distribution/managed.js"), "prepare-upgrade", home], home);
|
|
984
|
+
journal.stage = "profiles";
|
|
985
|
+
await atomicJSON(join5(home, "state/upgrade.json"), journal);
|
|
986
|
+
if (current.release.schema.migrationsSha256 !== target.release.schema.migrationsSha256) {
|
|
987
|
+
const space = evaluateResources(
|
|
988
|
+
await (hooks.sample ?? sampleResources)([join5(home, "instances/default")]),
|
|
989
|
+
target.release.budget,
|
|
990
|
+
[{ path: join5(home, "instances/default"), bytes: bytes + target.release.budget.installTemporaryBytes }],
|
|
991
|
+
"install"
|
|
992
|
+
);
|
|
993
|
+
if (space.status === "blocked" || space.status === "unknown") throw new Error("\u8FC1\u79FB\u524D\u8D44\u6E90\u4E0D\u8DB3\uFF1B\u4FDD\u7559\u5347\u7EA7\u65E5\u5FD7\u548C\u5F53\u524D\u6570\u636E");
|
|
994
|
+
await execute4(process.execPath, [join5(runtime, "lib/distribution/managed.js"), "migrate-upgrade", home], home);
|
|
995
|
+
}
|
|
996
|
+
journal.stage = "migrated";
|
|
997
|
+
await atomicJSON(join5(home, "state/upgrade.json"), journal);
|
|
998
|
+
await execute4(process.execPath, [join5(runtime, "lib/distribution/managed.js"), "verify-upgrade", home], home);
|
|
999
|
+
journal.stage = "verified";
|
|
1000
|
+
await atomicJSON(join5(home, "state/upgrade.json"), journal);
|
|
1001
|
+
target.status = "ready";
|
|
1002
|
+
target.stage = "ready";
|
|
1003
|
+
await atomicJSON(join5(home, "installation.json"), target);
|
|
1004
|
+
journal.stage = "complete";
|
|
1005
|
+
await atomicJSON(join5(home, "state/upgrade.json"), journal);
|
|
1006
|
+
hooks.progress?.("\u5347\u7EA7\u9A8C\u8BC1\u901A\u8FC7\uFF0C\u5DF2\u5207\u6362\u53D1\u884C\u7ED1\u5B9A\uFF1B\u5907\u4EFD\u4FDD\u7559");
|
|
1007
|
+
} finally {
|
|
1008
|
+
await releaseRun();
|
|
1009
|
+
}
|
|
1010
|
+
});
|
|
1011
|
+
}
|
|
1012
|
+
async function removeOwned(path, expected) {
|
|
1013
|
+
try {
|
|
1014
|
+
const marker = JSON.parse(await readFile6(join5(path, ".huohuo-owner.json"), "utf8"));
|
|
1015
|
+
if (marker.format !== 1 || marker.owner !== expected) throw new Error("\u53D7\u7BA1\u76EE\u5F55\u5F52\u5C5E\u4E0D\u7B26\uFF0C\u672A\u5220\u9664");
|
|
1016
|
+
await rm4(path, { recursive: true });
|
|
1017
|
+
} catch (error) {
|
|
1018
|
+
if (error.code !== "ENOENT") throw error;
|
|
1019
|
+
}
|
|
1020
|
+
}
|
|
1021
|
+
async function uninstall(home, hooks = {}) {
|
|
1022
|
+
home = await realpath4(resolve4(home));
|
|
1023
|
+
await withLock(join5(home, "state/install.lock"), async () => {
|
|
1024
|
+
const current = await readInstallation(home);
|
|
1025
|
+
if (!current) throw new Error("\u6CA1\u6709\u53EF\u6838\u9A8C\u7684\u6301\u4E45\u5B89\u88C5");
|
|
1026
|
+
const journal = await upgradeJournal(home);
|
|
1027
|
+
if (journal && journal.stage !== "complete") throw new Error("\u5347\u7EA7\u5C1A\u672A\u5BF9\u8D26\uFF0C\u4E0D\u80FD\u5378\u8F7D\u7A0B\u5E8F");
|
|
1028
|
+
await stopOwned(home, current.releaseDigest);
|
|
1029
|
+
const releaseRun = await acquireLock(join5(home, "state/default-run.lock"));
|
|
1030
|
+
try {
|
|
1031
|
+
const paths = installationPaths(home, current.release);
|
|
1032
|
+
if ((await readdir2(join5(home, "instances"))).some((name) => name !== "default")) throw new Error("\u68C0\u6D4B\u5230\u5176\u4ED6\u5B9E\u4F8B\uFF1B\u5171\u4EAB\u8FD0\u884C\u73AF\u5883\u5F52\u5C5E\u9700\u5148\u6838\u67E5\uFF0C\u672A\u5220\u9664");
|
|
1033
|
+
if (current.status !== "uninstalled") {
|
|
1034
|
+
await (hooks.run ?? run)(process.execPath, [join5(paths.runtime, "lib/distribution/managed.js"), "uninstall-programs", home], home);
|
|
1035
|
+
current.status = "uninstalled";
|
|
1036
|
+
current.stage = "uninstalled";
|
|
1037
|
+
await atomicJSON(join5(home, "installation.json"), current);
|
|
1038
|
+
}
|
|
1039
|
+
await removeOwned(paths.directory, current.releaseDigest);
|
|
1040
|
+
await removeOwned(paths.environment, `${current.release.python.sha256}:${current.release.requirementsSha256}`);
|
|
1041
|
+
if (!(await readdir2(join5(home, "releases"))).length) await removeOwned(paths.python, current.release.python.sha256);
|
|
1042
|
+
hooks.progress?.(`\u5DF2\u5378\u8F7D\u53EF\u518D\u751F\u6210\u7A0B\u5E8F\uFF1B\u5B9E\u4F8B\u3001\u51ED\u636E\u3001\u4F1A\u8BDD\u548C\u5907\u4EFD\u4FDD\u7559\u5728 ${home}`);
|
|
1043
|
+
} finally {
|
|
1044
|
+
await releaseRun();
|
|
1045
|
+
}
|
|
1046
|
+
});
|
|
1047
|
+
}
|
|
1048
|
+
|
|
1049
|
+
// distribution/bootstrap/entry.ts
|
|
1050
|
+
function artifactSource(source, directory) {
|
|
1051
|
+
return async (url, options) => {
|
|
1052
|
+
const artifact2 = source && [source.runtime, source.python, source.pythonDependencies].find((item) => item.url === String(url));
|
|
1053
|
+
if (!directory && !artifact2?.bundled) return fetch(url, options);
|
|
1054
|
+
if (!artifact2) throw new Error("\u672C\u673A\u4EA7\u7269\u76EE\u5F55\u7F3A\u5C11\u8BE5\u56FA\u5B9A\u53D1\u884C\u8D44\u6E90");
|
|
1055
|
+
const root = directory ?? join6(dirname5(fileURLToPath(import.meta.url)), "assets");
|
|
1056
|
+
return new Response(Readable.toWeb(createReadStream2(join6(root, artifact2.sha256 + ".tgz"), { signal: options?.signal ?? void 0 })));
|
|
1057
|
+
};
|
|
1058
|
+
}
|
|
1059
|
+
async function main(args = process.argv.slice(2)) {
|
|
1060
|
+
let home = join6(homedir(), ".huohuo"), noOpen = false, command = "start";
|
|
1061
|
+
let releaseFile, artifactDirectory;
|
|
1062
|
+
for (let i = 0; i < args.length; i++) {
|
|
1063
|
+
if (args[i] === "--home") {
|
|
1064
|
+
if (!args[i + 1]) throw new Error("--home \u9700\u8981\u76EE\u5F55");
|
|
1065
|
+
home = resolve5(args[++i]);
|
|
1066
|
+
} else if (args[i] === "--release" || args[i] === "--artifacts") {
|
|
1067
|
+
const option = args[i];
|
|
1068
|
+
if (!args[i + 1]) throw new Error(`${option} \u9700\u8981\u672C\u673A\u8DEF\u5F84`);
|
|
1069
|
+
if (option === "--release") releaseFile = resolve5(args[++i]);
|
|
1070
|
+
else artifactDirectory = resolve5(args[++i]);
|
|
1071
|
+
} else if (args[i] === "--no-open") noOpen = true;
|
|
1072
|
+
else if (args[i] === "--version") {
|
|
1073
|
+
const packageFile = join6(dirname5(fileURLToPath(import.meta.url)), "package.json");
|
|
1074
|
+
const manifest = JSON.parse(await readFile7(packageFile, "utf8"));
|
|
1075
|
+
console.log(`${manifest.name} ${manifest.version}${manifest.private ? "\uFF08\u672C\u5730\u5019\u9009\uFF09" : ""}`);
|
|
1076
|
+
return;
|
|
1077
|
+
} else if (["start", "doctor", "stop", "upgrade", "uninstall"].includes(args[i])) command = args[i];
|
|
1078
|
+
else throw new Error("\u7528\u6CD5\uFF1Ahuohuo [start|doctor|stop|upgrade|uninstall] [--home \u76EE\u5F55] [--no-open]");
|
|
1079
|
+
}
|
|
1080
|
+
try {
|
|
1081
|
+
home = await realpath5(home);
|
|
1082
|
+
} catch (e) {
|
|
1083
|
+
if (e.code !== "ENOENT") throw e;
|
|
1084
|
+
}
|
|
1085
|
+
let record = await readInstallation(home);
|
|
1086
|
+
const labels = {
|
|
1087
|
+
preflight: "\u68C0\u67E5\u672C\u673A\u73AF\u5883",
|
|
1088
|
+
download: "\u4E0B\u8F7D\u56FA\u5B9A\u53D1\u884C\u8D44\u6E90",
|
|
1089
|
+
"install-runtime": "\u51C6\u5907 Python \u8FD0\u884C\u73AF\u5883",
|
|
1090
|
+
"install-dependencies": "\u5B89\u88C5\u706B\u706B\u4E0E\u56FA\u5B9A\u4F9D\u8D56",
|
|
1091
|
+
"assemble-profiles": "\u914D\u7F6E\u5BF9\u8BDD\u4E0E\u540E\u53F0\u7814\u7A76",
|
|
1092
|
+
verify: "\u68C0\u67E5\u5B89\u88C5\u7ED3\u679C"
|
|
1093
|
+
};
|
|
1094
|
+
const progress = (stage) => console.log(`\u706B\u706B\uFF1A${labels[stage] ?? stage}`);
|
|
1095
|
+
if (command === "upgrade") {
|
|
1096
|
+
const candidate = JSON.parse(await readFile7(releaseFile ?? join6(dirname5(fileURLToPath(import.meta.url)), "release.json"), "utf8"));
|
|
1097
|
+
await upgrade(home, candidate, { fetcher: artifactSource(candidate, artifactDirectory), progress });
|
|
1098
|
+
const current = await readInstallation(home);
|
|
1099
|
+
if (await control(home, current.releaseDigest, noOpen ? "status" : "open")) return;
|
|
1100
|
+
await run(process.execPath, [join6(installationPaths(home, current.release).runtime, "lib/distribution/managed.js"), "start", home, ...noOpen ? ["--no-open"] : []], home);
|
|
1101
|
+
return;
|
|
1102
|
+
}
|
|
1103
|
+
if (command === "uninstall") {
|
|
1104
|
+
await uninstall(home, { progress });
|
|
1105
|
+
return;
|
|
1106
|
+
}
|
|
1107
|
+
if (command === "stop") {
|
|
1108
|
+
if (!record || !await control(home, record.releaseDigest, "stop")) throw new Error("\u6CA1\u6709\u53EF\u6838\u9A8C\u7684\u672C\u673A\u8FD0\u884C\u63A7\u5236\u5165\u53E3\uFF1B\u672A\u5411\u4EFB\u4F55 PID \u53D1\u9001\u4FE1\u53F7");
|
|
1109
|
+
console.log("\u5DF2\u8BF7\u6C42\u539F\u7EC8\u7AEF\u6B63\u5E38\u505C\u6B62\u672C\u5B9E\u4F8B");
|
|
1110
|
+
return;
|
|
1111
|
+
}
|
|
1112
|
+
if (command === "doctor") {
|
|
1113
|
+
if (!record) {
|
|
1114
|
+
console.log(JSON.stringify({ status: "unknown", reason: "\u5C1A\u65E0\u6301\u4E45\u5B89\u88C5", home }));
|
|
1115
|
+
return;
|
|
1116
|
+
}
|
|
1117
|
+
const paths = installationPaths(home, record.release);
|
|
1118
|
+
const targets = [home, paths.directory, paths.environment, join6(home, "instances/default")];
|
|
1119
|
+
const resources = evaluateResources(await sampleResources(targets), record.release.budget, targets.map((path) => ({ path, bytes: 0 })), "start");
|
|
1120
|
+
console.log(JSON.stringify({
|
|
1121
|
+
home,
|
|
1122
|
+
version: record.release.version,
|
|
1123
|
+
installation: record.status,
|
|
1124
|
+
stage: record.stage,
|
|
1125
|
+
upgrade: await upgradeJournal(home),
|
|
1126
|
+
resources
|
|
1127
|
+
}, null, 2));
|
|
1128
|
+
return;
|
|
1129
|
+
}
|
|
1130
|
+
const pending = await upgradeJournal(home);
|
|
1131
|
+
if (pending && pending.stage !== "complete") throw new Error("\u5347\u7EA7\u5C1A\u672A\u5BF9\u8D26\uFF1B\u8BF7\u7EE7\u7EED\u540C\u4E00\u56FA\u5B9A\u53D1\u884C\u7684 upgrade\uFF0C\u4E0D\u4F1A\u81EA\u52A8\u5207\u56DE\u65E7\u7A0B\u5E8F\u6216\u8986\u76D6\u65B0\u6570\u636E");
|
|
1132
|
+
if (record?.status === "ready") {
|
|
1133
|
+
if (await control(home, record.releaseDigest, noOpen ? "status" : "open")) {
|
|
1134
|
+
console.log(noOpen ? "\u5F53\u524D\u5B9E\u4F8B\u5DF2\u5728\u8FD0\u884C" : "\u5DF2\u8BF7\u6C42\u5F53\u524D\u5B9E\u4F8B\u6253\u5F00\u8BA4\u8BC1\u9875\u9762");
|
|
1135
|
+
return;
|
|
1136
|
+
}
|
|
1137
|
+
const runtime = installationPaths(home, record.release).runtime;
|
|
1138
|
+
try {
|
|
1139
|
+
await readFile7(join6(runtime, "lib/distribution/managed.js"));
|
|
1140
|
+
} catch (e) {
|
|
1141
|
+
if (e.code !== "ENOENT") throw e;
|
|
1142
|
+
record = void 0;
|
|
1143
|
+
}
|
|
1144
|
+
}
|
|
1145
|
+
if (!record || record.status !== "ready") {
|
|
1146
|
+
let catalog = null;
|
|
1147
|
+
if (!record) {
|
|
1148
|
+
try {
|
|
1149
|
+
catalog = JSON.parse(await readFile7(releaseFile ?? join6(dirname5(fileURLToPath(import.meta.url)), "release.json"), "utf8"));
|
|
1150
|
+
} catch (e) {
|
|
1151
|
+
if (e.code !== "ENOENT") throw e;
|
|
1152
|
+
}
|
|
1153
|
+
}
|
|
1154
|
+
const source = (await readInstallation(home))?.release ?? catalog;
|
|
1155
|
+
const fetcher = artifactSource(source, artifactDirectory);
|
|
1156
|
+
record = await install(home, catalog, { fetcher, progress });
|
|
1157
|
+
}
|
|
1158
|
+
const installedHome = record.home;
|
|
1159
|
+
await run(process.execPath, [join6(installationPaths(installedHome, record.release).runtime, "lib/distribution/managed.js"), "start", installedHome, ...noOpen ? ["--no-open"] : []], installedHome);
|
|
1160
|
+
}
|
|
1161
|
+
if (process.argv[1] && await realpath5(resolve5(process.argv[1])).catch(() => "") === await realpath5(fileURLToPath(import.meta.url))) {
|
|
1162
|
+
main().catch((error) => {
|
|
1163
|
+
console.error(error instanceof Error ? error.message : "\u5B89\u88C5\u6216\u542F\u52A8\u5931\u8D25");
|
|
1164
|
+
process.exitCode = 1;
|
|
1165
|
+
});
|
|
1166
|
+
}
|
|
1167
|
+
export {
|
|
1168
|
+
main
|
|
1169
|
+
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,22 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pab-iprg/huohuo",
|
|
3
|
-
"version": "
|
|
4
|
-
"
|
|
5
|
-
"description": "
|
|
6
|
-
|
|
3
|
+
"version": "1.0.0-preview.2",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"description": "火火投研伙伴安装入口",
|
|
6
|
+
"bin": {
|
|
7
|
+
"huohuo": "./cli.js"
|
|
8
|
+
},
|
|
9
|
+
"engines": {
|
|
10
|
+
"node": ">=24.8.0"
|
|
11
|
+
},
|
|
12
|
+
"files": [
|
|
13
|
+
"cli.js",
|
|
14
|
+
"release.json",
|
|
15
|
+
"README.md",
|
|
16
|
+
"assets"
|
|
17
|
+
],
|
|
18
|
+
"publishConfig": {
|
|
19
|
+
"access": "public",
|
|
20
|
+
"tag": "next"
|
|
21
|
+
}
|
|
22
|
+
}
|
package/release.json
ADDED
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
{
|
|
2
|
+
"format": 1,
|
|
3
|
+
"bootstrapFormat": 1,
|
|
4
|
+
"version": "1.0.0",
|
|
5
|
+
"nodeMajor": 24,
|
|
6
|
+
"npmMajor": 11,
|
|
7
|
+
"qualification": "qualified",
|
|
8
|
+
"platform": {
|
|
9
|
+
"os": "darwin",
|
|
10
|
+
"arch": "arm64",
|
|
11
|
+
"minimumOsRelease": "25.6.0"
|
|
12
|
+
},
|
|
13
|
+
"runtime": {
|
|
14
|
+
"name": "@huohuo/research-agent",
|
|
15
|
+
"version": "1.0.0",
|
|
16
|
+
"url": "https://bundled.invalid/3126256d1e3ad445b74f67e0a97a339774bc23cdcc5249578f1c9e1b7f3d8f6d.tgz",
|
|
17
|
+
"sha256": "3126256d1e3ad445b74f67e0a97a339774bc23cdcc5249578f1c9e1b7f3d8f6d",
|
|
18
|
+
"integrity": "sha512-KmCItvQmE0LZbNiKdrWeYsUCD4p4YaBfiS+OOwsepOix03nON46tgpFgOeT/effrEWzPROmuwI64IU8g4loTYw==",
|
|
19
|
+
"downloadBytes": 2542537,
|
|
20
|
+
"expandedBytes": 8237483,
|
|
21
|
+
"bundled": true
|
|
22
|
+
},
|
|
23
|
+
"python": {
|
|
24
|
+
"url": "https://github.com/astral-sh/python-build-standalone/releases/download/20261026/cpython-3.13.18%2B20261026-aarch64-apple-darwin-install_only_stripped.tar.gz",
|
|
25
|
+
"sha256": "8392b6ea51d94391befb2c31ebe75463f045d0b69dfcf5e1bb3234e1f5fa3405",
|
|
26
|
+
"downloadBytes": 25265774,
|
|
27
|
+
"expandedBytes": 66020864,
|
|
28
|
+
"redirectHosts": [
|
|
29
|
+
"release-assets.githubusercontent.com"
|
|
30
|
+
],
|
|
31
|
+
"executable": "python/bin/python3",
|
|
32
|
+
"build": "cpython-3.13.18+20261026-aarch64-apple-darwin-install_only_stripped.tar.gz",
|
|
33
|
+
"license": "python/lib/python3.13/LICENSE.txt"
|
|
34
|
+
},
|
|
35
|
+
"pythonDependencies": {
|
|
36
|
+
"url": "https://bundled.invalid/4f008fe4491617729554e056ad59eee686ccc5d95c9f1d5259da705bf054ce21.tgz",
|
|
37
|
+
"sha256": "4f008fe4491617729554e056ad59eee686ccc5d95c9f1d5259da705bf054ce21",
|
|
38
|
+
"downloadBytes": 10235,
|
|
39
|
+
"expandedBytes": 15007,
|
|
40
|
+
"bundled": true
|
|
41
|
+
},
|
|
42
|
+
"dsh": {
|
|
43
|
+
"version": "0.2.1-rc.2",
|
|
44
|
+
"assemblySha256": "08cf11de70e49981b4891c0aaa2a5e7b872a95b6aa07557bb629713fa8f9e249"
|
|
45
|
+
},
|
|
46
|
+
"requirementsSha256": "57042fff45cee66fe33712472e2e5b88f03a084068ca20242588b2e18b3e167b",
|
|
47
|
+
"profilesSha256": "bdd628f3a599a69a9835db9904fb55d36c57e0b3486b4f3f7dbec8d8a47f36f0",
|
|
48
|
+
"budget": {
|
|
49
|
+
"minimumEffectiveMemoryBytes": 34359738368,
|
|
50
|
+
"startupHeadroomBytes": 134217728,
|
|
51
|
+
"heavyJobHeadroomBytes": 1073741824,
|
|
52
|
+
"diskReserveBytes": 1073741824,
|
|
53
|
+
"recoverySamples": 3,
|
|
54
|
+
"processTreeBudgetBytes": 17179869184,
|
|
55
|
+
"sampleIntervalMs": 2000,
|
|
56
|
+
"sampleMaxAgeMs": 10000,
|
|
57
|
+
"installTemporaryBytes": 1073741824,
|
|
58
|
+
"dependenciesBytes": 4294967296,
|
|
59
|
+
"backupBytes": 0
|
|
60
|
+
},
|
|
61
|
+
"schema": {
|
|
62
|
+
"minimum": 1,
|
|
63
|
+
"maximum": 1,
|
|
64
|
+
"migrationsSha256": "ba029fdbffa5320e61393b484d08accafce60e9aa8dacde5b6212fb79dd9146b",
|
|
65
|
+
"files": {
|
|
66
|
+
"001_init.sql": "b42b6192d872f0f65cfbbf9d0c7fb442d3aa49584a6cf321d3bcc641b31b70da",
|
|
67
|
+
"002_contact_decision.sql": "af82cb9975a09fa09c0f970b70d04a7b55a960d72e0853f4573d571efaa46f0a",
|
|
68
|
+
"003_schedules.sql": "2937f7be2693ce9b410ee9a73bd10c5e819c5081a10a6b3993c33f37ad42295f",
|
|
69
|
+
"004_condition_state.sql": "f081d250dd525ad57a450bd6bca59a75f8fb1428177d670ae9fa142ed79721d6",
|
|
70
|
+
"005_execution_handoff.sql": "ebf2e5c4890b7a73ae1ffb57a70bb15183f48a6c1fa2acf3a48b95ccb3d992b3",
|
|
71
|
+
"006_entity_confirmation.sql": "d25ee376e7f6dd55df2edeb68d7591ef5fbaa34eeb3eef662cd76e183526d3c0",
|
|
72
|
+
"007_research_tool_calls.sql": "df9c7d8d25b44b151f743e2d5167b34ae89c64e67423d8af554364fd792da942",
|
|
73
|
+
"008_followup_notifications.sql": "98865d811879afd679846087a5f01ee2db371dcb90381a9c400c940a5c1c4b37",
|
|
74
|
+
"008_investment.sql": "71cecf6215552c725797059efdcb501247e1225c7d9f662de606266d3a947e61",
|
|
75
|
+
"009_notification_channels.sql": "572424d739203a8514ae59920a0ad69cf83f278479b48d48becb88bddda25d9a",
|
|
76
|
+
"010_strategies.sql": "af023fab201db6bd2e15e2348125d112bc21cf3a59d8afd0a2d616fc05cee209",
|
|
77
|
+
"011_strategy_run_subjects.sql": "53a1c216ed775a06549dcc201f9494ceabe308bee6dc52a214b685cca2f2fe8d",
|
|
78
|
+
"012_strategy_schedule.sql": "ccddd81bb70d2c86356e6f1ad09132e7397837d17e926b515c8e281a362bdd58",
|
|
79
|
+
"014_industry_chain.sql": "b656efffe746d14ca350ea1c9ecfcce2d98b5753e4b2f862c75d0456a062fd4a",
|
|
80
|
+
"015_source_heads.sql": "652bf637892edbbe969ce5ed4ce45a4a0c43d03de395fce1b828fc4c0169f885",
|
|
81
|
+
"016_legacy_transfer.sql": "b6e3c214b103487d741226b0f83525e0cacbb0c440bd1935d0e963dc9b9bfd07",
|
|
82
|
+
"017_unified_strategy_work.sql": "8d055ca58f3e73482fb16c630c733ae93197244b37e4509cc90c7dec3bf88d60",
|
|
83
|
+
"018_personalization.sql": "7f62a4eb74707fb071f56d6057b6d6517ed7648af23ebe290f3be15d260a103c",
|
|
84
|
+
"019_position_risk.sql": "3c461fa435b352127fd4ee4e7ed3de2c924561a3727dce1535d7d3a47c436ca9",
|
|
85
|
+
"020_notification_tests.sql": "3eb0d72eadf04b074208dbe0ef6a72ee614ec11a15143108a2471568b7b4fb22",
|
|
86
|
+
"021_notification_priority.sql": "f742b4c4b796e918bfc6a08bac8c5ae206382075925acf7b1c430b033f5177e6",
|
|
87
|
+
"022_research_job_priority.sql": "fcb0c0af025806faab7cd403097a9d2c55e07331ab0a78b2926c1927a87f4ed4",
|
|
88
|
+
"023_adaptive_notifications.sql": "80630e2dad73844d48b265fbc33648b01d8321a6fd9a0587ef4dc01f362aadec",
|
|
89
|
+
"023_notification_attention.sql": "5dd85fb001192558211c7b69e3452984c52119f215750ed6283b296d3dccc842",
|
|
90
|
+
"024_t_trade_watch.sql": "aa38470ddab00e0cbca07ce6d5cc31399ba2b6e40c8d8d888e88ec093a9f89ef"
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
}
|