@pab-iprg/huohuo 1.0.0-preview.2 → 1.0.0-preview.4
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
|
@@ -6,7 +6,11 @@
|
|
|
6
6
|
|
|
7
7
|
发布后的预览入口为 `npx --yes @pab-iprg/huohuo@next`。首版验收基线为 macOS Apple Silicon、Node 24.8.0 和 npm 11.6.0。默认持久目录为 `~/.huohuo`,普通重开保留已安装版本,安装中断后继续原目标。
|
|
8
8
|
|
|
9
|
-
|
|
9
|
+
安装前会列出磁盘预留量、内存要求、固定下载项和缓存位置。输入 `y` 继续,直接回车取消。自动化环境可用 `npx --yes @pab-iprg/huohuo@next --yes` 显式同意安装。固定资源显示实际接收量,下载失败可重跑并复用已经校验的文件。
|
|
10
|
+
|
|
11
|
+
发行清单可为同一份固定资源配置备用 HTTPS 地址。确认安装后会探测这些地址,优先从响应较快的可用地址下载,并核对同一个 SHA-256。只有一个已核验来源时,网络中断会自动重试一次。npm 和 pip 仍会按各自的依赖清单安装。
|
|
12
|
+
|
|
13
|
+
命令支持 `huohuo [start|status|doctor|stop|upgrade|uninstall] [--home 目录] [--no-open]`。在另一个终端运行 `npx --yes @pab-iprg/huohuo@next status --watch` 可打开实时状态看板,按 Ctrl+C 退出看板。
|
|
10
14
|
|
|
11
15
|
显式升级检查旧迁移不可变、追加迁移顺序、执行库格式与 DSH 会话兼容范围,先一致性备份再迁移和验证,成功后切换;不自动恢复旧备份覆盖新数据。默认卸载保留实例、凭据、会话和备份,不提供直接彻底删除业务数据的快捷入口。
|
|
12
16
|
|
|
Binary file
|
package/cli.js
CHANGED
|
@@ -1,11 +1,10 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
// distribution/bootstrap/entry.ts
|
|
4
|
-
import { readFile as
|
|
5
|
-
import { createReadStream as createReadStream2 } from "node:fs";
|
|
4
|
+
import { open as open3, readFile as readFile8, realpath as realpath5 } from "node:fs/promises";
|
|
6
5
|
import { Readable } from "node:stream";
|
|
7
6
|
import { homedir } from "node:os";
|
|
8
|
-
import { dirname as dirname5, join as
|
|
7
|
+
import { dirname as dirname5, join as join7, resolve as resolve5 } from "node:path";
|
|
9
8
|
import { fileURLToPath } from "node:url";
|
|
10
9
|
|
|
11
10
|
// distribution/bootstrap/install.ts
|
|
@@ -133,20 +132,24 @@ var hash = /^[a-f0-9]{64}$/;
|
|
|
133
132
|
function integer(n) {
|
|
134
133
|
return Number.isSafeInteger(n) && Number(n) >= 0;
|
|
135
134
|
}
|
|
136
|
-
function
|
|
137
|
-
if (
|
|
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;
|
|
135
|
+
function httpsURL(value) {
|
|
136
|
+
if (typeof value !== "string") return false;
|
|
140
137
|
try {
|
|
141
|
-
const u = new URL(
|
|
138
|
+
const u = new URL(value);
|
|
142
139
|
return u.protocol === "https:" && !u.username && !u.password && !u.hash;
|
|
143
140
|
} catch {
|
|
144
141
|
return false;
|
|
145
142
|
}
|
|
146
143
|
}
|
|
144
|
+
function artifact(a) {
|
|
145
|
+
if (!a || !hash.test(a.sha256) || !integer(a.downloadBytes) || !integer(a.expandedBytes) || !a.downloadBytes || !a.expandedBytes) return false;
|
|
146
|
+
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;
|
|
147
|
+
if (a.bundled !== void 0) return a.bundled === true && a.url === `https://bundled.invalid/${a.sha256}.tgz` && a.redirectHosts === void 0 && a.mirrors === void 0;
|
|
148
|
+
return httpsURL(a.url) && (a.mirrors === void 0 || Array.isArray(a.mirrors) && a.mirrors.length <= 3 && a.mirrors.every(httpsURL));
|
|
149
|
+
}
|
|
147
150
|
function validateRelease(input) {
|
|
148
151
|
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 || ![
|
|
152
|
+
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.pythonDependencies.upstreamWheelCount !== void 0 && !integer(r.pythonDependencies.upstreamWheelCount) || !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
153
|
"minimumEffectiveMemoryBytes",
|
|
151
154
|
"startupHeadroomBytes",
|
|
152
155
|
"heavyJobHeadroomBytes",
|
|
@@ -158,7 +161,7 @@ function validateRelease(input) {
|
|
|
158
161
|
"processTreeBudgetBytes",
|
|
159
162
|
"sampleIntervalMs",
|
|
160
163
|
"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) {
|
|
164
|
+
].every((k) => integer(r.budget[k])) || r.budget.observedInstallRssBytes !== void 0 && (!integer(r.budget.observedInstallRssBytes) || r.budget.observedInstallRssBytes <= 0) || 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
165
|
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
166
|
}
|
|
164
167
|
if (!hash.test(r.schema.migrationsSha256)) throw new Error("\u7F3A\u5C11\u6570\u636E\u5E93\u8FC1\u79FB\u88C5\u914D\u6458\u8981");
|
|
@@ -182,45 +185,102 @@ async function verifyArtifact(path, artifact2) {
|
|
|
182
185
|
}
|
|
183
186
|
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
187
|
}
|
|
185
|
-
|
|
188
|
+
var RetryableDownload = class extends Error {
|
|
189
|
+
};
|
|
190
|
+
async function downloadArtifact(artifact2, path, fetcher = fetch, progress) {
|
|
186
191
|
try {
|
|
187
192
|
await verifyArtifact(path, artifact2);
|
|
193
|
+
progress?.({ status: "cached", bytes: artifact2.downloadBytes });
|
|
188
194
|
return;
|
|
189
195
|
} catch (e) {
|
|
190
196
|
if (e.code !== "ENOENT") throw e;
|
|
191
197
|
}
|
|
192
198
|
await mkdir2(dirname2(path), { recursive: true, mode: 448 });
|
|
199
|
+
const sources = [.../* @__PURE__ */ new Set([artifact2.url, ...artifact2.mirrors ?? []])];
|
|
200
|
+
if (sources.length > 1) {
|
|
201
|
+
progress?.({ status: "probing", bytes: 0 });
|
|
202
|
+
const results = await Promise.all(sources.map(async (source) => {
|
|
203
|
+
const started = performance.now();
|
|
204
|
+
try {
|
|
205
|
+
const response = await requestSource(artifact2, source, fetcher, AbortSignal.timeout(5e3), "HEAD");
|
|
206
|
+
await response.body?.cancel();
|
|
207
|
+
return { source, latency: response.ok ? performance.now() - started : Infinity };
|
|
208
|
+
} catch {
|
|
209
|
+
return { source, latency: Infinity };
|
|
210
|
+
}
|
|
211
|
+
}));
|
|
212
|
+
results.sort((a, b) => a.latency - b.latency);
|
|
213
|
+
sources.splice(0, sources.length, ...results.map((result) => result.source));
|
|
214
|
+
} else if (!artifact2.bundled) sources.push(artifact2.url);
|
|
215
|
+
for (const [index, source] of sources.entries()) {
|
|
216
|
+
const host = artifact2.bundled ? void 0 : new URL(source).hostname;
|
|
217
|
+
progress?.({ status: "start", bytes: 0, host });
|
|
218
|
+
try {
|
|
219
|
+
await transfer(artifact2, path, source, fetcher, (bytes) => progress?.({ status: "receiving", bytes, host }));
|
|
220
|
+
progress?.({ status: "verified", bytes: artifact2.downloadBytes, host });
|
|
221
|
+
return;
|
|
222
|
+
} catch (error) {
|
|
223
|
+
if (!(error instanceof RetryableDownload)) throw error;
|
|
224
|
+
if (index === sources.length - 1) throw new Error(`${host} \u4E0B\u8F7D\u672A\u5B8C\u6210\uFF08${error.message}\uFF09\u3002\u8BF7\u68C0\u67E5\u7F51\u7EDC\u540E\u91CD\u8DD1\uFF0C\u5DF2\u6821\u9A8C\u7684\u6587\u4EF6\u4F1A\u590D\u7528\u3002`, { cause: error });
|
|
225
|
+
progress?.({ status: "retry", bytes: 0, host });
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
async function requestSource(artifact2, source, fetcher, signal, method, onResponse) {
|
|
230
|
+
const allowed = /* @__PURE__ */ new Set([new URL(source).hostname, ...artifact2.redirectHosts ?? []]);
|
|
231
|
+
let url = source;
|
|
232
|
+
for (let redirects = 0; redirects <= 4; redirects++) {
|
|
233
|
+
const next = new URL(url);
|
|
234
|
+
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");
|
|
235
|
+
const response = await fetcher(url, { signal, method, redirect: "manual" });
|
|
236
|
+
onResponse?.();
|
|
237
|
+
if (![301, 302, 303, 307, 308].includes(response.status)) return response;
|
|
238
|
+
const location = response.headers.get("location");
|
|
239
|
+
await response.body?.cancel();
|
|
240
|
+
if (!location || redirects === 4) throw new Error("\u4E0B\u8F7D\u91CD\u5B9A\u5411\u65E0\u6548\u6216\u8FC7\u591A");
|
|
241
|
+
url = new URL(location, url).href;
|
|
242
|
+
}
|
|
243
|
+
throw new Error("\u4E0B\u8F7D\u672A\u8FD4\u56DE\u54CD\u5E94");
|
|
244
|
+
}
|
|
245
|
+
async function transfer(artifact2, path, source, fetcher, onBytes) {
|
|
193
246
|
const pending = `${path}.${randomUUID2()}.partial`;
|
|
194
247
|
const handle = await open2(pending, "wx", 384);
|
|
248
|
+
const idle = new AbortController();
|
|
249
|
+
let timer;
|
|
250
|
+
const keepAlive = () => {
|
|
251
|
+
clearTimeout(timer);
|
|
252
|
+
timer = setTimeout(() => idle.abort(), 45e3);
|
|
253
|
+
timer.unref();
|
|
254
|
+
};
|
|
255
|
+
const signal = AbortSignal.any([idle.signal, AbortSignal.timeout(30 * 6e4)]);
|
|
256
|
+
keepAlive();
|
|
195
257
|
try {
|
|
196
|
-
const
|
|
197
|
-
|
|
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");
|
|
258
|
+
const response = await requestSource(artifact2, source, fetcher, signal, "GET", keepAlive);
|
|
259
|
+
if (!response.ok || !response.body) {
|
|
207
260
|
await response.body?.cancel();
|
|
208
|
-
if (!
|
|
209
|
-
|
|
261
|
+
if (!artifact2.bundled && (response.status === 408 || response.status === 429 || response.status >= 500 || artifact2.mirrors?.length && [403, 404].includes(response.status))) {
|
|
262
|
+
throw new RetryableDownload(`HTTP ${response.status}`);
|
|
263
|
+
}
|
|
264
|
+
throw new Error(`\u4E0B\u8F7D\u5931\u8D25\uFF1AHTTP ${response.status}`);
|
|
210
265
|
}
|
|
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
266
|
let received = 0;
|
|
214
267
|
for await (const chunk of response.body) {
|
|
215
268
|
received += chunk.byteLength;
|
|
269
|
+
keepAlive();
|
|
216
270
|
if (received > artifact2.downloadBytes) throw new Error("\u4E0B\u8F7D\u8D85\u8FC7\u53D1\u884C\u6E05\u5355\u5927\u5C0F");
|
|
217
271
|
await handle.writeFile(chunk);
|
|
272
|
+
onBytes(received);
|
|
218
273
|
}
|
|
219
274
|
await handle.sync();
|
|
220
275
|
await handle.close();
|
|
221
276
|
await verifyArtifact(pending, artifact2);
|
|
222
277
|
await rename2(pending, path);
|
|
278
|
+
} catch (error) {
|
|
279
|
+
if (!artifact2.bundled && signal.aborted) throw new RetryableDownload(idle.signal.aborted ? "\u8FDE\u7EED 45 \u79D2\u672A\u6536\u5230\u6570\u636E" : "\u8D85\u8FC7\u5355\u6B21\u4E0B\u8F7D\u65F6\u9650", { cause: error });
|
|
280
|
+
if (!artifact2.bundled && error instanceof TypeError) throw new RetryableDownload("\u7F51\u7EDC\u8FDE\u63A5\u4E2D\u65AD", { cause: error });
|
|
281
|
+
throw error;
|
|
223
282
|
} finally {
|
|
283
|
+
clearTimeout(timer);
|
|
224
284
|
await handle.close().catch(() => {
|
|
225
285
|
});
|
|
226
286
|
await rm(pending, { force: true });
|
|
@@ -588,6 +648,32 @@ async function install(home, candidate, hooks = {}, programsOnly = false) {
|
|
|
588
648
|
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
649
|
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
650
|
if (programsOnly && (await readInstallation(home))?.status !== "ready") throw new Error("\u5F53\u524D\u5B89\u88C5\u4E0D\u5B8C\u6574\uFF0C\u5148\u4FEE\u590D\u539F\u7248\u672C");
|
|
651
|
+
const bundleDirectory = hooks.bundleDirectory;
|
|
652
|
+
if (!programsOnly && record && bundleDirectory && candidate && ["installing", "blocked", "failed"].includes(record.status) && ["preflight", "download"].includes(record.stage) && Object.keys(record.stages).every((stage2) => stage2 === "preflight" || stage2 === "download")) {
|
|
653
|
+
const hasFile = async (path) => {
|
|
654
|
+
try {
|
|
655
|
+
await access(path);
|
|
656
|
+
return true;
|
|
657
|
+
} catch (error) {
|
|
658
|
+
if (error.code === "ENOENT") return false;
|
|
659
|
+
throw error;
|
|
660
|
+
}
|
|
661
|
+
};
|
|
662
|
+
const bundled = (release2) => [release2.runtime, release2.python, release2.pythonDependencies].filter((artifact2) => artifact2.bundled);
|
|
663
|
+
const oldAssets = bundled(record.release);
|
|
664
|
+
const available = await Promise.all(oldAssets.map(async (artifact2) => {
|
|
665
|
+
const file = artifact2.sha256 + ".tgz";
|
|
666
|
+
return await hasFile(join3(bundleDirectory, file)) || await hasFile(join3(home, "cache", file));
|
|
667
|
+
}));
|
|
668
|
+
if (available.includes(false)) {
|
|
669
|
+
const next = validateRelease(candidate);
|
|
670
|
+
const nextAssets = bundled(next);
|
|
671
|
+
if (next.runtime.bundled && next.version === record.release.version && (await Promise.all(nextAssets.map((artifact2) => hasFile(join3(bundleDirectory, artifact2.sha256 + ".tgz"))))).every(Boolean)) {
|
|
672
|
+
record = void 0;
|
|
673
|
+
hooks.progress?.("resume-current-package");
|
|
674
|
+
}
|
|
675
|
+
}
|
|
676
|
+
}
|
|
591
677
|
const release = record?.release ?? validateRelease(candidate);
|
|
592
678
|
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
679
|
throw new Error("\u6B64\u53D1\u884C\u5C1A\u672A\u9A8C\u8BC1\u5F53\u524D OS/CPU/Node \u7EC4\u5408\uFF1B\u672A\u5F00\u59CB\u91CD\u4E0B\u8F7D");
|
|
@@ -597,6 +683,20 @@ async function install(home, candidate, hooks = {}, programsOnly = false) {
|
|
|
597
683
|
const paths = installationPaths(home, release);
|
|
598
684
|
const execute4 = hooks.run ?? run;
|
|
599
685
|
const sample = hooks.sample ?? sampleResources;
|
|
686
|
+
const files = [
|
|
687
|
+
{ name: "\u706B\u706B\u8FD0\u884C\u5305", artifact: release.runtime, path: paths.archive },
|
|
688
|
+
{ name: "Python \u8FD0\u884C\u73AF\u5883", artifact: release.python, path: paths.pythonArchive },
|
|
689
|
+
{ name: "Python \u4F9D\u8D56\u6E05\u5355\u6216\u79BB\u7EBF\u5305", artifact: release.pythonDependencies, path: join3(home, "cache", release.pythonDependencies.sha256 + ".tgz") }
|
|
690
|
+
];
|
|
691
|
+
const withActivity = async (label, action) => {
|
|
692
|
+
hooks.progress?.(label);
|
|
693
|
+
const timer = hooks.progress && setInterval(() => hooks.progress?.(`${label}\u4ECD\u5728\u8FDB\u884C\uFF0C\u8BF7\u7A0D\u5019`), 2e4);
|
|
694
|
+
try {
|
|
695
|
+
await action();
|
|
696
|
+
} finally {
|
|
697
|
+
if (timer) clearInterval(timer);
|
|
698
|
+
}
|
|
699
|
+
};
|
|
600
700
|
const cached = async (path, artifact2) => {
|
|
601
701
|
try {
|
|
602
702
|
await verifyArtifact(path, artifact2);
|
|
@@ -631,15 +731,10 @@ async function install(home, candidate, hooks = {}, programsOnly = false) {
|
|
|
631
731
|
}
|
|
632
732
|
};
|
|
633
733
|
const check = async () => {
|
|
634
|
-
const [
|
|
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
|
-
]);
|
|
734
|
+
const [cacheHits, inventory] = await Promise.all([Promise.all(files.map((file) => cached(file.path, file.artifact))), installed()]);
|
|
640
735
|
const remainingDependencies = inventory.dependencies ? 0 : release.budget.dependenciesBytes;
|
|
641
736
|
const costs = [
|
|
642
|
-
{ path: join3(home, "cache"), bytes: (
|
|
737
|
+
{ path: join3(home, "cache"), bytes: files.reduce((sum, file, index) => sum + (cacheHits[index] ? 0 : file.artifact.downloadBytes), 0) },
|
|
643
738
|
{ path: paths.directory, bytes: inventory.dependencies ? 0 : release.runtime.expandedBytes + release.budget.installTemporaryBytes },
|
|
644
739
|
{ path: paths.python, bytes: inventory.python ? 0 : release.python.expandedBytes },
|
|
645
740
|
{ path: paths.environment, bytes: 0 },
|
|
@@ -665,6 +760,19 @@ async function install(home, candidate, hooks = {}, programsOnly = false) {
|
|
|
665
760
|
${details}
|
|
666
761
|
\u5904\u7406\u540E\uFF0C\u91CD\u65B0\u8FD0\u884C\u521A\u624D\u7684\u5B89\u88C5\u547D\u4EE4\u3002`), { code: "RESOURCE_BLOCKED" });
|
|
667
762
|
}
|
|
763
|
+
const pendingFiles = files.filter((_, index) => !cacheHits[index]);
|
|
764
|
+
return {
|
|
765
|
+
home,
|
|
766
|
+
resources: result,
|
|
767
|
+
cached: files.length - pendingFiles.length,
|
|
768
|
+
bundled: pendingFiles.filter((file) => file.artifact.bundled).length,
|
|
769
|
+
upstreamWheelCount: inventory.dependencies ? void 0 : release.pythonDependencies.upstreamWheelCount,
|
|
770
|
+
downloads: pendingFiles.filter((file) => !file.artifact.bundled).map((file) => ({
|
|
771
|
+
name: file.name,
|
|
772
|
+
size: file.artifact.downloadBytes,
|
|
773
|
+
host: new URL(file.artifact.url).hostname
|
|
774
|
+
}))
|
|
775
|
+
};
|
|
668
776
|
};
|
|
669
777
|
const stage = async (name, action) => {
|
|
670
778
|
hooks.progress?.(name);
|
|
@@ -675,8 +783,8 @@ ${details}
|
|
|
675
783
|
record.stages[name] = entry;
|
|
676
784
|
await atomicJSON(recordPath, record);
|
|
677
785
|
try {
|
|
678
|
-
await check();
|
|
679
|
-
const output = await action();
|
|
786
|
+
const plan = await check();
|
|
787
|
+
const output = await action(plan);
|
|
680
788
|
entry.status = "verified";
|
|
681
789
|
entry.outputDigest = digest(output ?? null);
|
|
682
790
|
entry.endedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
@@ -684,13 +792,13 @@ ${details}
|
|
|
684
792
|
entry.status = "failed";
|
|
685
793
|
entry.endedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
686
794
|
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";
|
|
795
|
+
record.status = entry.errorCode === "RESOURCE_BLOCKED" || entry.errorCode === "INSTALL_CANCELLED" ? "blocked" : "failed";
|
|
688
796
|
await atomicJSON(recordPath, record);
|
|
689
797
|
throw error;
|
|
690
798
|
}
|
|
691
799
|
await atomicJSON(recordPath, record);
|
|
692
800
|
};
|
|
693
|
-
await stage("preflight", async () => {
|
|
801
|
+
await stage("preflight", async (plan) => {
|
|
694
802
|
const npm = npmCommand();
|
|
695
803
|
const { stdout } = await promisify3(execFile3)(npm.command, [...npm.prefix, "--version"], { timeout: 5e3 });
|
|
696
804
|
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");
|
|
@@ -700,11 +808,25 @@ ${details}
|
|
|
700
808
|
} finally {
|
|
701
809
|
await rm2(probe, { force: true });
|
|
702
810
|
}
|
|
811
|
+
if (hooks.confirm && !await hooks.confirm(plan)) {
|
|
812
|
+
throw Object.assign(new Error("\u5DF2\u53D6\u6D88\u5B89\u88C5\uFF0C\u672C\u6B21\u6CA1\u6709\u8FDB\u5165\u4E0B\u8F7D\u3002"), { code: "INSTALL_CANCELLED" });
|
|
813
|
+
}
|
|
703
814
|
});
|
|
704
815
|
await stage("download", async () => {
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
816
|
+
for (const [index, file] of files.entries()) {
|
|
817
|
+
const progress = (event) => hooks.downloadProgress?.({
|
|
818
|
+
name: file.name,
|
|
819
|
+
index: index + 1,
|
|
820
|
+
total: files.length,
|
|
821
|
+
size: file.artifact.downloadBytes
|
|
822
|
+
}, event);
|
|
823
|
+
try {
|
|
824
|
+
await downloadArtifact(file.artifact, file.path, hooks.fetcher, progress);
|
|
825
|
+
} catch (error) {
|
|
826
|
+
progress({ status: "failed", bytes: 0 });
|
|
827
|
+
throw new Error(`${file.name}\u5904\u7406\u5931\u8D25\u3002 ${error instanceof Error ? error.message : "\u8BF7\u91CD\u8BD5\u5B89\u88C5\u3002"}`, { cause: error });
|
|
828
|
+
}
|
|
829
|
+
}
|
|
708
830
|
return [release.runtime.sha256, release.python.sha256, release.pythonDependencies.sha256];
|
|
709
831
|
});
|
|
710
832
|
await stage("install-runtime", async () => {
|
|
@@ -737,7 +859,7 @@ ${details}
|
|
|
737
859
|
dependencies: { "@huohuo/research-agent": `file:${paths.archive}` }
|
|
738
860
|
});
|
|
739
861
|
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);
|
|
862
|
+
await withActivity("\u5B89\u88C5 npm \u4F9D\u8D56", () => execute4(npm.command, [...npm.prefix, "install", "--omit=dev", "--ignore-scripts", "--no-audit", "--no-fund", "--prefer-offline", "--cache", join3(home, "cache/npm")], paths.directory));
|
|
741
863
|
const runtime = JSON.parse(await readFile4(join3(paths.runtime, "package.json"), "utf8"));
|
|
742
864
|
if (runtime.name !== release.runtime.name || runtime.version !== release.version) throw new Error("\u8FD0\u884C\u5305\u8EAB\u4EFD\u4E0D\u7B26");
|
|
743
865
|
const requirements = await readFile4(join3(paths.runtime, "sidecar/requirements.lock"));
|
|
@@ -771,7 +893,12 @@ ${details}
|
|
|
771
893
|
}
|
|
772
894
|
if (!environmentReady) {
|
|
773
895
|
await execute4(join3(paths.python, release.python.executable), ["-m", "venv", paths.environment], home);
|
|
774
|
-
|
|
896
|
+
if (release.pythonDependencies.bundled && hooks.progress) {
|
|
897
|
+
const lock = await readFile4(join3(wheels, "requirements.txt"), "utf8");
|
|
898
|
+
const upstream = lock.split("\n").filter((line) => /^[\w.-]+\s+@\s+https:\/\//.test(line)).length;
|
|
899
|
+
if (upstream) hooks.progress?.(`Python \u4F9D\u8D56\u6E05\u5355\u6709 ${upstream} \u4E2A\u4E0A\u6E38\u6587\u4EF6\uFF0C\u4E0B\u65B9\u663E\u793A\u6B63\u5728\u5904\u7406\u7684\u6587\u4EF6`);
|
|
900
|
+
}
|
|
901
|
+
await withActivity("\u5B89\u88C5 Python \u4F9D\u8D56", () => execute4(python, [
|
|
775
902
|
"-m",
|
|
776
903
|
"pip",
|
|
777
904
|
"--isolated",
|
|
@@ -784,7 +911,7 @@ ${details}
|
|
|
784
911
|
...release.pythonDependencies.bundled ? ["--no-cache-dir"] : [],
|
|
785
912
|
"-r",
|
|
786
913
|
join3(wheels, "requirements.txt")
|
|
787
|
-
], home);
|
|
914
|
+
], home));
|
|
788
915
|
}
|
|
789
916
|
await execute4(python, ["-m", "pip", "--isolated", "check"], home);
|
|
790
917
|
await atomicJSON(join3(paths.environment, ".dependencies-ready.json"), { sha256: release.pythonDependencies.sha256 });
|
|
@@ -797,7 +924,7 @@ ${details}
|
|
|
797
924
|
return record;
|
|
798
925
|
}
|
|
799
926
|
await stage("assemble-profiles", async () => {
|
|
800
|
-
await execute4(process.execPath, [join3(paths.runtime, "lib/distribution/managed.js"), "prepare", home], home);
|
|
927
|
+
await withActivity("\u914D\u7F6E\u5BF9\u8BDD\u4E0E\u540E\u53F0\u8FD0\u884C\u4F9D\u8D56", () => execute4(process.execPath, [join3(paths.runtime, "lib/distribution/managed.js"), "prepare", home], home));
|
|
801
928
|
return { release: record.releaseDigest };
|
|
802
929
|
});
|
|
803
930
|
await stage("verify", async () => {
|
|
@@ -1046,18 +1173,185 @@ async function uninstall(home, hooks = {}) {
|
|
|
1046
1173
|
});
|
|
1047
1174
|
}
|
|
1048
1175
|
|
|
1176
|
+
// distribution/bootstrap/install-ui.ts
|
|
1177
|
+
import { createInterface } from "node:readline";
|
|
1178
|
+
var gb = (bytes) => bytes === null ? "\u672A\u77E5" : `${(bytes / 1e9).toFixed(2)} GB`;
|
|
1179
|
+
var mb = (bytes) => bytes < 1e5 ? `${(bytes / 1e3).toFixed(1)} KB` : `${(bytes / 1e6).toFixed(1)} MB`;
|
|
1180
|
+
function installUI(assumeYes) {
|
|
1181
|
+
const interactive = process.stdin.isTTY && process.stderr.isTTY;
|
|
1182
|
+
const output = process.stderr;
|
|
1183
|
+
let lastUpdate = 0;
|
|
1184
|
+
let lineActive = false;
|
|
1185
|
+
const clear = () => {
|
|
1186
|
+
if (lineActive) {
|
|
1187
|
+
output.write("\r\x1B[2K");
|
|
1188
|
+
lineActive = false;
|
|
1189
|
+
}
|
|
1190
|
+
};
|
|
1191
|
+
const line = (text) => {
|
|
1192
|
+
clear();
|
|
1193
|
+
output.write(text + "\n");
|
|
1194
|
+
};
|
|
1195
|
+
return {
|
|
1196
|
+
async confirm(plan) {
|
|
1197
|
+
const { sample, policy, disks } = plan.resources;
|
|
1198
|
+
line(`\u5B89\u88C5\u76EE\u5F55 ${plan.home}`);
|
|
1199
|
+
for (const [index, disk] of disks.entries()) {
|
|
1200
|
+
const location = sample.disks.find((item) => item.device === disk.device)?.paths[0] ?? plan.home;
|
|
1201
|
+
line(`\u78C1\u76D8 ${index + 1}\uFF08${location}\uFF09 \u5B89\u88C5\u9884\u7559 ${gb(disk.requiredBytes)}\uFF0C\u5F53\u524D\u53EF\u7528 ${gb(disk.availableBytes)}`);
|
|
1202
|
+
}
|
|
1203
|
+
line(`\u5185\u5B58 \u672C\u673A\u5BB9\u91CF ${gb(sample.effectiveMemoryBytes)}\uFF0C\u5F53\u524D\u53EF\u7528 ${gb(sample.availableMemoryBytes)}`);
|
|
1204
|
+
line(`\u5185\u5B58\u8981\u6C42 \u6700\u4F4E\u5BB9\u91CF ${gb(policy.minimumEffectiveMemoryBytes)}\uFF0C\u542F\u52A8\u9700\u7559\u51FA ${gb(policy.startupHeadroomBytes)}`);
|
|
1205
|
+
line(policy.observedInstallRssBytes ? `\u5B89\u88C5\u5185\u5B58\u53C2\u8003 \u540C\u5E73\u53F0\u5386\u53F2\u8FDB\u7A0B\u5185\u5B58\u5CF0\u503C ${gb(policy.observedInstallRssBytes)}\uFF0C\u5B9E\u9645\u5360\u7528\u4F1A\u53D8\u5316` : "\u5B89\u88C5\u5185\u5B58\u5CF0\u503C\u968F\u4F9D\u8D56\u548C\u672C\u673A\u72B6\u6001\u53D8\u5316\uFF0C\u5F53\u524D\u6E05\u5355\u6682\u65E0\u5B9E\u6D4B\u503C");
|
|
1206
|
+
for (const reason of plan.resources.reasons) line(reason);
|
|
1207
|
+
const bytes = plan.downloads.reduce((total, file) => total + file.size, 0);
|
|
1208
|
+
line(`\u56FA\u5B9A\u8D44\u6E90 \u8054\u7F51\u4E0B\u8F7D ${plan.downloads.length} \u9879\uFF08${mb(bytes)}\uFF09\uFF0C\u968F\u5305 ${plan.bundled} \u9879\uFF0C\u7F13\u5B58 ${plan.cached} \u9879`);
|
|
1209
|
+
for (const file of plan.downloads) line(` ${file.name} ${mb(file.size)} ${file.host}`);
|
|
1210
|
+
line(`\u56FA\u5B9A\u8D44\u6E90\u4FDD\u5B58\u81F3 ${plan.home}/cache`);
|
|
1211
|
+
if (plan.upstreamWheelCount !== void 0) line(`Python \u4F9D\u8D56\u6E05\u5355 ${plan.upstreamWheelCount} \u9879\u56FA\u5B9A\u4E0A\u6E38\u6587\u4EF6\uFF0C\u5DF2\u5B89\u88C5\u9879\u53EF\u80FD\u8DF3\u8FC7\u3002`);
|
|
1212
|
+
line("\u540E\u7EED\u8FD8\u4F1A\u5B89\u88C5 npm \u548C Python \u4F9D\u8D56\uFF0C\u5B9E\u9645\u8054\u7F51\u4E0B\u8F7D\u91CF\u53D6\u51B3\u4E8E\u672C\u673A\u7F13\u5B58\u548C\u4F9D\u8D56\u89E3\u6790\u7ED3\u679C\u3002");
|
|
1213
|
+
if (assumeYes) return true;
|
|
1214
|
+
if (!interactive) throw new Error("\u9700\u8981\u786E\u8BA4\u5B89\u88C5\u3002\u8BF7\u5728\u7EC8\u7AEF\u8FD0\u884C\uFF0C\u6216\u5728\u706B\u706B\u547D\u4EE4\u672B\u5C3E\u52A0\u4E0A --yes\u3002");
|
|
1215
|
+
const terminal = createInterface({ input: process.stdin, output, terminal: true });
|
|
1216
|
+
try {
|
|
1217
|
+
const answers = terminal[Symbol.asyncIterator]();
|
|
1218
|
+
output.write("\u7EE7\u7EED\u5B89\u88C5\uFF1F[y/N] ");
|
|
1219
|
+
const answer = await answers.next();
|
|
1220
|
+
return !answer.done && /^(y|yes|是)$/i.test(answer.value.trim());
|
|
1221
|
+
} finally {
|
|
1222
|
+
terminal.close();
|
|
1223
|
+
}
|
|
1224
|
+
},
|
|
1225
|
+
downloadProgress(file, event) {
|
|
1226
|
+
const label = `[${file.index}/${file.total}] ${file.name}`;
|
|
1227
|
+
if (event.status === "probing") {
|
|
1228
|
+
line(`${label} \u6B63\u5728\u68C0\u67E5\u4E0B\u8F7D\u6E90\u662F\u5426\u53EF\u8FBE`);
|
|
1229
|
+
} else if (event.status === "start") {
|
|
1230
|
+
lastUpdate = Date.now();
|
|
1231
|
+
line(`${label} ${event.host ? `\u6B63\u5728\u8FDE\u63A5 ${event.host}` : "\u6B63\u5728\u8BFB\u53D6\u968F\u5305\u8D44\u6E90"} ${mb(file.size)}`);
|
|
1232
|
+
} else if (event.status === "cached") {
|
|
1233
|
+
line(`${label} \u5DF2\u6821\u9A8C\u5E76\u590D\u7528\u7F13\u5B58`);
|
|
1234
|
+
} else if (event.status === "verified") {
|
|
1235
|
+
line(`${label} \u5DF2\u5B8C\u6210\u5E76\u6821\u9A8C ${mb(file.size)}`);
|
|
1236
|
+
} else if (event.status === "retry") {
|
|
1237
|
+
line(`${label} ${event.host} \u4E0B\u8F7D\u672A\u5B8C\u6210\uFF0C\u51C6\u5907\u91CD\u8BD5`);
|
|
1238
|
+
} else if (event.status === "failed") {
|
|
1239
|
+
clear();
|
|
1240
|
+
} else {
|
|
1241
|
+
const now = Date.now();
|
|
1242
|
+
if (now - lastUpdate < (interactive ? 180 : 15e3)) return;
|
|
1243
|
+
lastUpdate = now;
|
|
1244
|
+
const percent = Math.min(99, Math.floor(100 * event.bytes / file.size));
|
|
1245
|
+
const filled = Math.floor(percent / 10);
|
|
1246
|
+
const value = `[${"#".repeat(filled)}${"-".repeat(10 - filled)}] ${percent}% ${mb(event.bytes)} / ${mb(file.size)}`;
|
|
1247
|
+
if (interactive && (output.columns ?? 0) >= value.length) {
|
|
1248
|
+
clear();
|
|
1249
|
+
output.write(value);
|
|
1250
|
+
lineActive = true;
|
|
1251
|
+
} else line(`${label} ${value}`);
|
|
1252
|
+
}
|
|
1253
|
+
}
|
|
1254
|
+
};
|
|
1255
|
+
}
|
|
1256
|
+
|
|
1257
|
+
// distribution/bootstrap/status-ui.ts
|
|
1258
|
+
import { readFile as readFile7 } from "node:fs/promises";
|
|
1259
|
+
import { join as join6 } from "node:path";
|
|
1260
|
+
import { setTimeout as delay2 } from "node:timers/promises";
|
|
1261
|
+
var mib = (bytes) => typeof bytes === "number" && Number.isFinite(bytes) ? `${Math.round(bytes / 2 ** 20)} MiB` : "\u672A\u77E5";
|
|
1262
|
+
var pressureLabels = { normal: "\u6B63\u5E38", high: "\u504F\u9AD8", critical: "\u4E25\u91CD", unknown: "\u672A\u77E5" };
|
|
1263
|
+
async function statusLines(home) {
|
|
1264
|
+
const title = "\u706B\u706B \xB7 \u8FD0\u884C\u72B6\u6001";
|
|
1265
|
+
const installed = await readInstallation(home);
|
|
1266
|
+
if (!installed) return [title, "\u5C1A\u672A\u5B89\u88C5\u706B\u706B\u3002"];
|
|
1267
|
+
if (installed.status !== "ready") return [title, "\u5B89\u88C5\u5C1A\u672A\u5B8C\u6210\u3002"];
|
|
1268
|
+
let admission;
|
|
1269
|
+
try {
|
|
1270
|
+
admission = JSON.parse(await readFile7(join6(home, "state/default-admission.json"), "utf8"));
|
|
1271
|
+
} catch (error) {
|
|
1272
|
+
if (error.code !== "ENOENT") return [title, "\u6682\u65F6\u65E0\u6CD5\u8BFB\u53D6\u8D44\u6E90\u72B6\u6001\u3002"];
|
|
1273
|
+
}
|
|
1274
|
+
const now = Date.now() / 1e3;
|
|
1275
|
+
if (!admission || admission.format !== 1 || admission.releaseDigest !== installed.releaseDigest || !Number.isFinite(admission.sampledAt) || !(admission.validUntil > now) || typeof admission.allowHeavy !== "boolean" || !admission.resources?.sample) {
|
|
1276
|
+
return [title, "\u6682\u65E0\u6709\u6548\u7684\u8FD0\u884C\u91C7\u6837\u3002\u8BF7\u68C0\u67E5\u706B\u706B\u670D\u52A1\u662F\u5426\u6B63\u5728\u8FD0\u884C\u3002"];
|
|
1277
|
+
}
|
|
1278
|
+
const { sample, reasons } = admission.resources;
|
|
1279
|
+
const needed = installed.release.budget.heavyJobHeadroomBytes;
|
|
1280
|
+
const gap = sample.availableMemoryBytes === null ? null : Math.max(0, needed - sample.availableMemoryBytes);
|
|
1281
|
+
const lines = [
|
|
1282
|
+
title,
|
|
1283
|
+
`\u540E\u53F0\u7814\u7A76 ${admission.allowHeavy ? "\u53EF\u63A5\u6536\u4EFB\u52A1" : "\u6682\u7F13\u65B0\u4EFB\u52A1"}`,
|
|
1284
|
+
`\u7CFB\u7EDF\u5185\u5B58 \u53EF\u7528 ${mib(sample.availableMemoryBytes)} / \u603B\u5BB9\u91CF ${mib(sample.effectiveMemoryBytes)}`,
|
|
1285
|
+
`\u4EFB\u52A1\u95E8\u69DB \u9884\u7559 ${mib(needed)}${gap ? `\uFF0C\u8FD8\u5DEE ${mib(gap)}` : ""}`,
|
|
1286
|
+
`\u5185\u5B58\u538B\u529B ${pressureLabels[sample.pressure] ?? "\u672A\u77E5"}`,
|
|
1287
|
+
`\u706B\u706B\u8FDB\u7A0B \u4F30\u7B97\u5360\u7528 ${mib(admission.rssSumEstimateBytes)}`
|
|
1288
|
+
];
|
|
1289
|
+
if (!admission.allowHeavy) lines.push(reasons?.[0] ? `\u6682\u7F13\u539F\u56E0 ${reasons[0]}` : "\u6B63\u5728\u7B49\u5F85\u8FDE\u7EED\u5065\u5EB7\u91C7\u6837\u3002");
|
|
1290
|
+
lines.push(`\u91C7\u6837\u65F6\u95F4 ${new Date(admission.sampledAt * 1e3).toLocaleTimeString()}`);
|
|
1291
|
+
return lines;
|
|
1292
|
+
}
|
|
1293
|
+
async function showStatus(home, watch) {
|
|
1294
|
+
const output = process.stdout;
|
|
1295
|
+
if (!watch || !output.isTTY || process.env.TERM === "dumb") {
|
|
1296
|
+
output.write((await statusLines(home)).join("\n") + "\n");
|
|
1297
|
+
return;
|
|
1298
|
+
}
|
|
1299
|
+
const stop = new AbortController();
|
|
1300
|
+
const close = () => stop.abort();
|
|
1301
|
+
const input = process.stdin;
|
|
1302
|
+
const raw = input.isTTY;
|
|
1303
|
+
const wasRaw = input.isRaw;
|
|
1304
|
+
const onKey = (chunk) => {
|
|
1305
|
+
if (chunk.includes(3)) close();
|
|
1306
|
+
};
|
|
1307
|
+
process.once("SIGINT", close);
|
|
1308
|
+
process.once("SIGTERM", close);
|
|
1309
|
+
try {
|
|
1310
|
+
if (raw) {
|
|
1311
|
+
input.setRawMode(true);
|
|
1312
|
+
input.on("data", onKey);
|
|
1313
|
+
input.resume();
|
|
1314
|
+
}
|
|
1315
|
+
output.write("\x1B[?1049h\x1B[?25l");
|
|
1316
|
+
while (!stop.signal.aborted) {
|
|
1317
|
+
const lines = await statusLines(home);
|
|
1318
|
+
if (stop.signal.aborted) break;
|
|
1319
|
+
output.write("\x1B[H\x1B[0J" + [...lines, "", "\u6BCF 2 \u79D2\u5237\u65B0 \xB7 Ctrl+C \u5173\u95ED\u770B\u677F"].join("\n"));
|
|
1320
|
+
await delay2(2e3, void 0, { signal: stop.signal }).catch((error) => {
|
|
1321
|
+
if (!stop.signal.aborted) throw error;
|
|
1322
|
+
});
|
|
1323
|
+
}
|
|
1324
|
+
} finally {
|
|
1325
|
+
output.write("\x1B[?25h\x1B[?1049l");
|
|
1326
|
+
if (raw) {
|
|
1327
|
+
input.off("data", onKey);
|
|
1328
|
+
input.setRawMode(Boolean(wasRaw));
|
|
1329
|
+
input.pause();
|
|
1330
|
+
}
|
|
1331
|
+
process.off("SIGINT", close);
|
|
1332
|
+
process.off("SIGTERM", close);
|
|
1333
|
+
}
|
|
1334
|
+
}
|
|
1335
|
+
|
|
1049
1336
|
// distribution/bootstrap/entry.ts
|
|
1050
1337
|
function artifactSource(source, directory) {
|
|
1051
1338
|
return async (url, options) => {
|
|
1339
|
+
const bundled = /^https:\/\/bundled\.invalid\/([a-f0-9]{64})\.tgz$/.exec(String(url));
|
|
1340
|
+
if (!directory && !bundled) return fetch(url, options);
|
|
1052
1341
|
const artifact2 = source && [source.runtime, source.python, source.pythonDependencies].find((item) => item.url === String(url));
|
|
1053
|
-
|
|
1054
|
-
if (!
|
|
1055
|
-
const root = directory ??
|
|
1056
|
-
|
|
1342
|
+
const sha256 = bundled?.[1] ?? artifact2?.sha256;
|
|
1343
|
+
if (!sha256) throw new Error("\u672C\u673A\u4EA7\u7269\u76EE\u5F55\u7F3A\u5C11\u8BE5\u56FA\u5B9A\u53D1\u884C\u8D44\u6E90");
|
|
1344
|
+
const root = directory ?? join7(dirname5(fileURLToPath(import.meta.url)), "assets");
|
|
1345
|
+
const path = join7(root, sha256 + ".tgz");
|
|
1346
|
+
const file = await open3(path).catch((error) => {
|
|
1347
|
+
if (error.code === "ENOENT" && bundled) throw new Error("\u7F3A\u5C11\u4E0A\u6B21\u5B89\u88C5\u4F7F\u7528\u7684\u6587\u4EF6\u3002\u8BF7\u7528\u4E0A\u6B21\u5B89\u88C5\u65F6\u7684\u7248\u672C\u7EE7\u7EED\u3002");
|
|
1348
|
+
throw error;
|
|
1349
|
+
});
|
|
1350
|
+
return new Response(Readable.toWeb(file.createReadStream({ signal: options?.signal ?? void 0 })));
|
|
1057
1351
|
};
|
|
1058
1352
|
}
|
|
1059
1353
|
async function main(args = process.argv.slice(2)) {
|
|
1060
|
-
let home =
|
|
1354
|
+
let home = join7(homedir(), ".huohuo"), noOpen = false, assumeYes = false, watch = false, command = "start";
|
|
1061
1355
|
let releaseFile, artifactDirectory;
|
|
1062
1356
|
for (let i = 0; i < args.length; i++) {
|
|
1063
1357
|
if (args[i] === "--home") {
|
|
@@ -1069,19 +1363,23 @@ async function main(args = process.argv.slice(2)) {
|
|
|
1069
1363
|
if (option === "--release") releaseFile = resolve5(args[++i]);
|
|
1070
1364
|
else artifactDirectory = resolve5(args[++i]);
|
|
1071
1365
|
} else if (args[i] === "--no-open") noOpen = true;
|
|
1366
|
+
else if (args[i] === "--yes") assumeYes = true;
|
|
1367
|
+
else if (args[i] === "--watch") watch = true;
|
|
1072
1368
|
else if (args[i] === "--version") {
|
|
1073
|
-
const packageFile =
|
|
1074
|
-
const manifest = JSON.parse(await
|
|
1369
|
+
const packageFile = join7(dirname5(fileURLToPath(import.meta.url)), "package.json");
|
|
1370
|
+
const manifest = JSON.parse(await readFile8(packageFile, "utf8"));
|
|
1075
1371
|
console.log(`${manifest.name} ${manifest.version}${manifest.private ? "\uFF08\u672C\u5730\u5019\u9009\uFF09" : ""}`);
|
|
1076
1372
|
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]");
|
|
1373
|
+
} else if (["start", "status", "doctor", "stop", "upgrade", "uninstall"].includes(args[i])) command = args[i];
|
|
1374
|
+
else throw new Error("\u7528\u6CD5\uFF1Ahuohuo [start|status|doctor|stop|upgrade|uninstall] [--home \u76EE\u5F55] [--no-open]");
|
|
1079
1375
|
}
|
|
1376
|
+
if (watch && command !== "status") throw new Error("--watch \u8BF7\u4E0E status \u4E00\u8D77\u4F7F\u7528");
|
|
1080
1377
|
try {
|
|
1081
1378
|
home = await realpath5(home);
|
|
1082
1379
|
} catch (e) {
|
|
1083
1380
|
if (e.code !== "ENOENT") throw e;
|
|
1084
1381
|
}
|
|
1382
|
+
if (command === "status") return showStatus(home, watch);
|
|
1085
1383
|
let record = await readInstallation(home);
|
|
1086
1384
|
const labels = {
|
|
1087
1385
|
preflight: "\u68C0\u67E5\u672C\u673A\u73AF\u5883",
|
|
@@ -1089,15 +1387,22 @@ async function main(args = process.argv.slice(2)) {
|
|
|
1089
1387
|
"install-runtime": "\u51C6\u5907 Python \u8FD0\u884C\u73AF\u5883",
|
|
1090
1388
|
"install-dependencies": "\u5B89\u88C5\u706B\u706B\u4E0E\u56FA\u5B9A\u4F9D\u8D56",
|
|
1091
1389
|
"assemble-profiles": "\u914D\u7F6E\u5BF9\u8BDD\u4E0E\u540E\u53F0\u7814\u7A76",
|
|
1092
|
-
verify: "\u68C0\u67E5\u5B89\u88C5\u7ED3\u679C"
|
|
1390
|
+
verify: "\u68C0\u67E5\u5B89\u88C5\u7ED3\u679C",
|
|
1391
|
+
"resume-current-package": "\u4E0A\u6B21\u5B89\u88C5\u5C1A\u672A\u5199\u5165\u8FD0\u884C\u73AF\u5883\uFF0C\u5C06\u4F7F\u7528\u5F53\u524D\u7248\u672C\u91CD\u65B0\u68C0\u67E5"
|
|
1093
1392
|
};
|
|
1094
|
-
const
|
|
1393
|
+
const installStages = ["preflight", "download", "install-runtime", "install-dependencies", "assemble-profiles", "verify"];
|
|
1394
|
+
const progress = (stage) => {
|
|
1395
|
+
const index = command === "start" ? installStages.indexOf(stage) : -1;
|
|
1396
|
+
const step = index < 0 ? "" : `[${index + 1}/${installStages.length}] `;
|
|
1397
|
+
console.log(`\u706B\u706B\uFF1A${step}${labels[stage] ?? stage}`);
|
|
1398
|
+
};
|
|
1399
|
+
const ui = installUI(assumeYes);
|
|
1095
1400
|
if (command === "upgrade") {
|
|
1096
|
-
const candidate = JSON.parse(await
|
|
1097
|
-
await upgrade(home, candidate, { fetcher: artifactSource(candidate, artifactDirectory), progress });
|
|
1401
|
+
const candidate = JSON.parse(await readFile8(releaseFile ?? join7(dirname5(fileURLToPath(import.meta.url)), "release.json"), "utf8"));
|
|
1402
|
+
await upgrade(home, candidate, { fetcher: artifactSource(candidate, artifactDirectory), progress, ...ui });
|
|
1098
1403
|
const current = await readInstallation(home);
|
|
1099
1404
|
if (await control(home, current.releaseDigest, noOpen ? "status" : "open")) return;
|
|
1100
|
-
await run(process.execPath, [
|
|
1405
|
+
await run(process.execPath, [join7(installationPaths(home, current.release).runtime, "lib/distribution/managed.js"), "start", home, ...noOpen ? ["--no-open"] : []], home);
|
|
1101
1406
|
return;
|
|
1102
1407
|
}
|
|
1103
1408
|
if (command === "uninstall") {
|
|
@@ -1115,7 +1420,7 @@ async function main(args = process.argv.slice(2)) {
|
|
|
1115
1420
|
return;
|
|
1116
1421
|
}
|
|
1117
1422
|
const paths = installationPaths(home, record.release);
|
|
1118
|
-
const targets = [home, paths.directory, paths.environment,
|
|
1423
|
+
const targets = [home, paths.directory, paths.environment, join7(home, "instances/default")];
|
|
1119
1424
|
const resources = evaluateResources(await sampleResources(targets), record.release.budget, targets.map((path) => ({ path, bytes: 0 })), "start");
|
|
1120
1425
|
console.log(JSON.stringify({
|
|
1121
1426
|
home,
|
|
@@ -1136,7 +1441,7 @@ async function main(args = process.argv.slice(2)) {
|
|
|
1136
1441
|
}
|
|
1137
1442
|
const runtime = installationPaths(home, record.release).runtime;
|
|
1138
1443
|
try {
|
|
1139
|
-
await
|
|
1444
|
+
await readFile8(join7(runtime, "lib/distribution/managed.js"));
|
|
1140
1445
|
} catch (e) {
|
|
1141
1446
|
if (e.code !== "ENOENT") throw e;
|
|
1142
1447
|
record = void 0;
|
|
@@ -1144,24 +1449,25 @@ async function main(args = process.argv.slice(2)) {
|
|
|
1144
1449
|
}
|
|
1145
1450
|
if (!record || record.status !== "ready") {
|
|
1146
1451
|
let catalog = null;
|
|
1147
|
-
if (!record) {
|
|
1452
|
+
if (!record || ["preflight", "download"].includes(record.stage)) {
|
|
1148
1453
|
try {
|
|
1149
|
-
catalog = JSON.parse(await
|
|
1454
|
+
catalog = JSON.parse(await readFile8(releaseFile ?? join7(dirname5(fileURLToPath(import.meta.url)), "release.json"), "utf8"));
|
|
1150
1455
|
} catch (e) {
|
|
1151
1456
|
if (e.code !== "ENOENT") throw e;
|
|
1152
1457
|
}
|
|
1153
1458
|
}
|
|
1154
1459
|
const source = (await readInstallation(home))?.release ?? catalog;
|
|
1155
1460
|
const fetcher = artifactSource(source, artifactDirectory);
|
|
1156
|
-
|
|
1461
|
+
const bundleDirectory = !releaseFile && !artifactDirectory ? join7(dirname5(fileURLToPath(import.meta.url)), "assets") : void 0;
|
|
1462
|
+
record = await install(home, catalog, { fetcher, progress, bundleDirectory, ...ui });
|
|
1157
1463
|
}
|
|
1158
1464
|
const installedHome = record.home;
|
|
1159
|
-
await run(process.execPath, [
|
|
1465
|
+
await run(process.execPath, [join7(installationPaths(installedHome, record.release).runtime, "lib/distribution/managed.js"), "start", installedHome, ...noOpen ? ["--no-open"] : []], installedHome);
|
|
1160
1466
|
}
|
|
1161
1467
|
if (process.argv[1] && await realpath5(resolve5(process.argv[1])).catch(() => "") === await realpath5(fileURLToPath(import.meta.url))) {
|
|
1162
1468
|
main().catch((error) => {
|
|
1163
1469
|
console.error(error instanceof Error ? error.message : "\u5B89\u88C5\u6216\u542F\u52A8\u5931\u8D25");
|
|
1164
|
-
process.exitCode = 1;
|
|
1470
|
+
if (error?.code !== "INSTALL_CANCELLED") process.exitCode = 1;
|
|
1165
1471
|
});
|
|
1166
1472
|
}
|
|
1167
1473
|
export {
|
package/package.json
CHANGED
package/release.json
CHANGED
|
@@ -13,11 +13,11 @@
|
|
|
13
13
|
"runtime": {
|
|
14
14
|
"name": "@huohuo/research-agent",
|
|
15
15
|
"version": "1.0.0",
|
|
16
|
-
"url": "https://bundled.invalid/
|
|
17
|
-
"sha256": "
|
|
18
|
-
"integrity": "sha512-
|
|
19
|
-
"downloadBytes":
|
|
20
|
-
"expandedBytes":
|
|
16
|
+
"url": "https://bundled.invalid/550095205f2f92e0641b286f15fbb01ece0b29339ecf7c5ee18539a2b7f5def8.tgz",
|
|
17
|
+
"sha256": "550095205f2f92e0641b286f15fbb01ece0b29339ecf7c5ee18539a2b7f5def8",
|
|
18
|
+
"integrity": "sha512-AI87lAFcPyGM5qtQ4B2Ji3CJWE10twn48t0tSnuEeIxlVAKVEJBWRe7aSCkOKwLvpeai8CRts50oEMh0juDWrA==",
|
|
19
|
+
"downloadBytes": 2548683,
|
|
20
|
+
"expandedBytes": 8255144,
|
|
21
21
|
"bundled": true
|
|
22
22
|
},
|
|
23
23
|
"python": {
|
|
@@ -37,7 +37,8 @@
|
|
|
37
37
|
"sha256": "4f008fe4491617729554e056ad59eee686ccc5d95c9f1d5259da705bf054ce21",
|
|
38
38
|
"downloadBytes": 10235,
|
|
39
39
|
"expandedBytes": 15007,
|
|
40
|
-
"bundled": true
|
|
40
|
+
"bundled": true,
|
|
41
|
+
"upstreamWheelCount": 39
|
|
41
42
|
},
|
|
42
43
|
"dsh": {
|
|
43
44
|
"version": "0.2.1-rc.2",
|
|
@@ -56,7 +57,8 @@
|
|
|
56
57
|
"sampleMaxAgeMs": 10000,
|
|
57
58
|
"installTemporaryBytes": 1073741824,
|
|
58
59
|
"dependenciesBytes": 4294967296,
|
|
59
|
-
"backupBytes": 0
|
|
60
|
+
"backupBytes": 0,
|
|
61
|
+
"observedInstallRssBytes": 1904771072
|
|
60
62
|
},
|
|
61
63
|
"schema": {
|
|
62
64
|
"minimum": 1,
|