mirror of
https://github.com/modelstudioai/cli.git
synced 2026-09-14 19:49:23 +08:00
feat: sync wiki data from oss by fc
This commit is contained in:
@@ -25,7 +25,8 @@
|
||||
},
|
||||
"files": [
|
||||
"dist",
|
||||
"README.zh.md"
|
||||
"README.zh.md",
|
||||
"postinstall.js"
|
||||
],
|
||||
"type": "module",
|
||||
"exports": {
|
||||
@@ -45,12 +46,14 @@
|
||||
"build": "vp pack",
|
||||
"dev": "tsx src/main.ts",
|
||||
"test": "vp test",
|
||||
"check": "vp check"
|
||||
"check": "vp check",
|
||||
"postinstall": "node postinstall.js"
|
||||
},
|
||||
"dependencies": {
|
||||
"bailian-cli-commands": "workspace:*",
|
||||
"bailian-cli-core": "workspace:*",
|
||||
"bailian-cli-runtime": "workspace:*"
|
||||
"bailian-cli-runtime": "workspace:*",
|
||||
"tar-stream": "catalog:"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@clack/prompts": "^0.7.0",
|
||||
|
||||
@@ -0,0 +1,160 @@
|
||||
/**
|
||||
* postinstall.js —— Wiki 数据同步(第一层:npm install 触发)
|
||||
*
|
||||
* npm/pnpm 装完 bailian-cli 后自动执行:无条件下载全量 Wiki 数据包并覆盖本地目录,
|
||||
* 保证用户首次使用 `bl advisor recommend` 时数据已就位。
|
||||
*
|
||||
* 流程:
|
||||
* 1. fetch FC 签名函数 → 拿 OSS 预签名 URL
|
||||
* 2. 下载 manifest.json + wiki-doc-full.tar.br(~2.15MB)
|
||||
* 3. 校验 sha256
|
||||
* 4. Node 原生 brotli 解压 + tar-stream 解包到同盘临时目录
|
||||
* 5. renameSync 原子替换到 ~/.bailian/skills/bailian-docs-llm-wiki/
|
||||
* 6. 写 ~/.bailian/wiki-sync-state.json
|
||||
*
|
||||
* 设计约束:
|
||||
* - 无条件覆盖:每次 install 都全量替换,不比对已有版本
|
||||
* - 失败静默:任何一步失败 → console.warn → process.exit(0),绝不阻塞安装
|
||||
* - 独立实现:不 import bailian-cli-core,避免打包后 ESM 路径问题
|
||||
* - 依赖 Node 原生模块 + tar-stream(与 sync.ts / Crawler oss-upload.mjs 一致)
|
||||
*/
|
||||
import { createHash } from "node:crypto";
|
||||
import {
|
||||
createWriteStream,
|
||||
existsSync,
|
||||
mkdirSync,
|
||||
renameSync,
|
||||
rmSync,
|
||||
writeFileSync,
|
||||
} from "node:fs";
|
||||
import { homedir } from "node:os";
|
||||
import { dirname, join } from "node:path";
|
||||
import { Readable } from "node:stream";
|
||||
import { pipeline } from "node:stream/promises";
|
||||
import { createBrotliDecompress } from "node:zlib";
|
||||
import tar from "tar-stream";
|
||||
|
||||
const FC_SIGN_URL = "https://signature-gmqkxxozrl.cn-hangzhou.fcapp.run";
|
||||
const CONFIG_DIR_NAME = ".bailian";
|
||||
const SKILL_DIR_NAME = "skills/bailian-docs-llm-wiki";
|
||||
const STATE_FILE_NAME = "wiki-sync-state.json";
|
||||
const MANIFEST_KEY = "manifest.json";
|
||||
const ASSET_KEY = "wiki-doc-full.tar.br";
|
||||
|
||||
const MANIFEST_TIMEOUT_MS = 3000;
|
||||
const DOWNLOAD_TIMEOUT_MS = 30000;
|
||||
|
||||
function getConfigDir() {
|
||||
if (process.env.BAILIAN_CONFIG_DIR) return process.env.BAILIAN_CONFIG_DIR;
|
||||
return join(homedir(), CONFIG_DIR_NAME);
|
||||
}
|
||||
|
||||
function getCatalogDir() {
|
||||
return join(getConfigDir(), SKILL_DIR_NAME);
|
||||
}
|
||||
|
||||
function getStatePath() {
|
||||
return join(getConfigDir(), STATE_FILE_NAME);
|
||||
}
|
||||
|
||||
async function fetchJson(url, timeoutMs) {
|
||||
const res = await fetch(url, { signal: AbortSignal.timeout(timeoutMs) });
|
||||
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
||||
return res.json();
|
||||
}
|
||||
|
||||
async function downloadBuffer(url) {
|
||||
const res = await fetch(url, { signal: AbortSignal.timeout(DOWNLOAD_TIMEOUT_MS) });
|
||||
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
||||
return Buffer.from(await res.arrayBuffer());
|
||||
}
|
||||
|
||||
/** brotli 解压 + tar-stream 解包到 destDir(与 Crawler tar.pack() 对称)。 */
|
||||
async function extractTarBr(tarBrBuffer, destDir) {
|
||||
const extract = tar.extract();
|
||||
|
||||
extract.on("entry", (header, stream, next) => {
|
||||
const filePath = join(destDir, header.name);
|
||||
if (header.type === "directory") {
|
||||
mkdirSync(filePath, { recursive: true });
|
||||
stream.resume();
|
||||
stream.on("end", next);
|
||||
return;
|
||||
}
|
||||
mkdirSync(dirname(filePath), { recursive: true });
|
||||
const ws = createWriteStream(filePath);
|
||||
stream.pipe(ws);
|
||||
ws.on("finish", next);
|
||||
ws.on("error", next);
|
||||
});
|
||||
|
||||
await pipeline(Readable.from(tarBrBuffer), createBrotliDecompress(), extract);
|
||||
}
|
||||
|
||||
/** 原子替换:tmpDir(同盘)→ catalogDir。 */
|
||||
function atomicSwap(tmpDir, catalogDir) {
|
||||
mkdirSync(dirname(catalogDir), { recursive: true });
|
||||
const backup = `${catalogDir}.old-${Date.now()}`;
|
||||
if (existsSync(catalogDir)) renameSync(catalogDir, backup);
|
||||
try {
|
||||
renameSync(tmpDir, catalogDir);
|
||||
} catch (err) {
|
||||
if (existsSync(backup) && !existsSync(catalogDir)) renameSync(backup, catalogDir);
|
||||
throw err;
|
||||
}
|
||||
if (existsSync(backup)) rmSync(backup, { recursive: true, force: true });
|
||||
}
|
||||
|
||||
async function main() {
|
||||
// 1. 拿全部签名 URL
|
||||
const signResp = await fetchJson(FC_SIGN_URL, MANIFEST_TIMEOUT_MS);
|
||||
const urls = signResp?.urls;
|
||||
if (!urls?.[MANIFEST_KEY] || !urls?.[ASSET_KEY]) {
|
||||
throw new Error("签名函数未返回所需 URL");
|
||||
}
|
||||
|
||||
// 2. 下载 manifest
|
||||
const manifest = await fetchJson(urls[MANIFEST_KEY], MANIFEST_TIMEOUT_MS);
|
||||
if (!manifest?.version) throw new Error("manifest 无 version");
|
||||
|
||||
// 3. 下载 tar.br + 校验
|
||||
const tarBuf = await downloadBuffer(urls[ASSET_KEY]);
|
||||
const sha256 = createHash("sha256").update(tarBuf).digest("hex");
|
||||
if (manifest.asset?.sha256 && sha256 !== manifest.asset.sha256) {
|
||||
throw new Error("sha256 校验失败");
|
||||
}
|
||||
|
||||
// 4. 解包到同盘临时目录 + 原子替换
|
||||
const catalogDir = getCatalogDir();
|
||||
const tmpDir = `${catalogDir}.tmp-${process.pid}-${Date.now()}`;
|
||||
try {
|
||||
mkdirSync(tmpDir, { recursive: true });
|
||||
await extractTarBr(tarBuf, tmpDir);
|
||||
atomicSwap(tmpDir, catalogDir);
|
||||
} catch (err) {
|
||||
if (existsSync(tmpDir)) rmSync(tmpDir, { recursive: true, force: true });
|
||||
throw err;
|
||||
}
|
||||
|
||||
// 5. 写 state
|
||||
try {
|
||||
writeFileSync(
|
||||
getStatePath(),
|
||||
JSON.stringify({ lastChecked: Date.now(), version: manifest.version }),
|
||||
);
|
||||
} catch {
|
||||
/* state 写失败不影响:首次 recommend 会重新检查 */
|
||||
}
|
||||
|
||||
process.stdout.write(`bailian-cli: wiki 数据已就绪 (v${manifest.version})\n`);
|
||||
}
|
||||
|
||||
main().catch((err) => {
|
||||
// 无条件放行:安装期网络/权限问题不应阻塞 npm install,
|
||||
// 首次 `bl advisor recommend` 时 sync.ts 会兜底同步。
|
||||
const msg = err instanceof Error ? err.message : String(err);
|
||||
process.stderr.write(`bailian-cli: wiki 数据预下载跳过 (${msg}),首次使用时将自动同步。\n`);
|
||||
// Force a success exit code so a download failure never fails `npm install`.
|
||||
// eslint-disable-next-line unicorn/no-process-exit
|
||||
process.exit(0);
|
||||
});
|
||||
@@ -6,6 +6,7 @@ import {
|
||||
type GetModelsOptions,
|
||||
getModels,
|
||||
type IntentProfile,
|
||||
maybeSyncWikiData,
|
||||
type PipelineStep,
|
||||
type RecommendedModel,
|
||||
type RecommendResult,
|
||||
@@ -248,6 +249,12 @@ export default defineCommand({
|
||||
const { settings, flags } = ctx;
|
||||
const userInput = flags.message;
|
||||
const top = 3;
|
||||
|
||||
// Keep the local wiki catalog fresh: throttled (12h) version check against
|
||||
// the remote manifest, silently replaces data when a newer version exists.
|
||||
// Never throws — a sync failure must not block recommendation.
|
||||
await maybeSyncWikiData();
|
||||
|
||||
// Default to JSON for structured output; render boxen cards only when the
|
||||
// user explicitly asked for text output.
|
||||
const format = settings.outputExplicit ? detectOutputFormat(settings.output) : "json";
|
||||
|
||||
@@ -40,11 +40,13 @@
|
||||
"check": "vp check"
|
||||
},
|
||||
"dependencies": {
|
||||
"tar-stream": "catalog:",
|
||||
"yaml": "^2.8.3",
|
||||
"yauzl": "catalog:"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "catalog:",
|
||||
"@types/tar-stream": "catalog:",
|
||||
"@types/yauzl": "catalog:",
|
||||
"@typescript/native-preview": "7.0.0-dev.20260328.1",
|
||||
"typescript": "^6.0.2",
|
||||
|
||||
@@ -7,6 +7,7 @@ export { recallCandidates } from "./recall.ts";
|
||||
export { recallSemantic, isSemanticAvailable } from "./recall-semantic.ts";
|
||||
export type { RecommendOptions } from "./recommend.ts";
|
||||
export { buildDocLink, rankModels } from "./recommend.ts";
|
||||
export { maybeSyncWikiData } from "./sync.ts";
|
||||
export type { ModelSource } from "./sources/types.ts";
|
||||
export type {
|
||||
Budget,
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { cpSync, existsSync, mkdirSync, readFileSync } from "node:fs";
|
||||
import { dirname, join } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { existsSync, readFileSync } from "node:fs";
|
||||
import { join } from "node:path";
|
||||
import { getConfigDir } from "../../config/paths.ts";
|
||||
import type { ModelPrice, ModelProfile, QpmLimit } from "../types.ts";
|
||||
import type { ModelSource } from "./types.ts";
|
||||
@@ -13,12 +12,9 @@ function getCatalogDir(): string {
|
||||
}
|
||||
|
||||
function getCatalogPath(): string {
|
||||
return join(getCatalogDir(), MODELS_FILE);
|
||||
}
|
||||
|
||||
function getMonorepoModelsDir(): string {
|
||||
const coreDir = dirname(fileURLToPath(import.meta.url));
|
||||
return join(coreDir, "../../../../../skills/bailian-docs-llm-wiki/models");
|
||||
// Full-package layout keeps the `models/` subdir (raw/, wiki/, models/, …),
|
||||
// so models.jsonl lives at <skill>/models/models.jsonl — not at the skill root.
|
||||
return join(getCatalogDir(), "models", MODELS_FILE);
|
||||
}
|
||||
|
||||
function fromJsonlRecord(raw: Record<string, unknown>): ModelProfile | null {
|
||||
@@ -62,41 +58,24 @@ function readJsonlModels(filePath: string): ModelProfile[] {
|
||||
return models;
|
||||
}
|
||||
|
||||
function installFromMonorepo(): boolean {
|
||||
const src = getMonorepoModelsDir();
|
||||
if (!existsSync(join(src, MODELS_FILE))) return false;
|
||||
const dest = getCatalogDir();
|
||||
try {
|
||||
mkdirSync(dest, { recursive: true });
|
||||
cpSync(src, dest, { recursive: true });
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
export interface CatalogSourceOptions {
|
||||
onPrepareStart?: () => void;
|
||||
}
|
||||
|
||||
export class CatalogSource implements ModelSource {
|
||||
readonly name = "catalog";
|
||||
private options: CatalogSourceOptions;
|
||||
|
||||
constructor(options?: CatalogSourceOptions) {
|
||||
this.options = options ?? {};
|
||||
}
|
||||
// Options retained for API compatibility. Data is now always provisioned by
|
||||
// the CLI postinstall hook and refreshed by advisor sync, so the previous
|
||||
// `onPrepareStart` install callback is obsolete.
|
||||
constructor(_options?: CatalogSourceOptions) {}
|
||||
|
||||
available(): boolean {
|
||||
return existsSync(getCatalogPath());
|
||||
}
|
||||
|
||||
async load(): Promise<ModelProfile[]> {
|
||||
if (!this.available()) {
|
||||
this.options.onPrepareStart?.();
|
||||
const installed = installFromMonorepo();
|
||||
if (!installed) return [];
|
||||
}
|
||||
if (!this.available()) return [];
|
||||
return readJsonlModels(getCatalogPath());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,226 @@
|
||||
/**
|
||||
* sync.ts —— Wiki 数据同步(第二层:recommend 触发)
|
||||
*
|
||||
* `bl advisor recommend` 执行时调用 `maybeSyncWikiData()`:
|
||||
* 1. 12h throttle:距上次检查不足 12h 直接跳过
|
||||
* 2. 拉 FC 签名 URL → 下载 manifest.json 比对版本
|
||||
* 3. 版本相同 → 仅刷新 lastChecked
|
||||
* 4. 版本不同 → 下载 tar.br → 校验 sha256 → brotli 解压 + tar-stream 解包
|
||||
* 到同盘临时目录 → renameSync 原子替换 → 写 state
|
||||
*
|
||||
* 与 postinstall.js(第一层,npm install 无条件覆盖)互补。二者都用
|
||||
* Node 原生 brotli + tar-stream extract(),与 Crawler 端 tar.pack() 对称。
|
||||
*
|
||||
* 失败策略:任何一步失败都静默返回且不更新 lastChecked,下次 recommend 立即重试。
|
||||
*/
|
||||
import { createHash } from "node:crypto";
|
||||
import {
|
||||
createWriteStream,
|
||||
existsSync,
|
||||
mkdirSync,
|
||||
readFileSync,
|
||||
renameSync,
|
||||
rmSync,
|
||||
writeFileSync,
|
||||
} from "node:fs";
|
||||
import { dirname, join } from "node:path";
|
||||
import { Readable } from "node:stream";
|
||||
import { pipeline } from "node:stream/promises";
|
||||
import { createBrotliDecompress } from "node:zlib";
|
||||
import tar from "tar-stream";
|
||||
import { getConfigDir } from "../config/paths.ts";
|
||||
|
||||
/** FC 匿名签名函数入口,返回 OSS 预签名 URL(硬编码,不走 env)。 */
|
||||
const FC_SIGN_URL = "https://signature-gmqkxxozrl.cn-hangzhou.fcapp.run";
|
||||
const SKILL_DIR_NAME = "skills/bailian-docs-llm-wiki";
|
||||
const STATE_FILE_NAME = "wiki-sync-state.json";
|
||||
const MODELS_FILE = "models.jsonl";
|
||||
const MANIFEST_KEY = "manifest.json";
|
||||
const ASSET_KEY = "wiki-doc-full.tar.br";
|
||||
|
||||
const THROTTLE_MS = 12 * 60 * 60 * 1000; // 12h
|
||||
const MANIFEST_TIMEOUT_MS = 3000;
|
||||
const DOWNLOAD_TIMEOUT_MS = 30000;
|
||||
|
||||
interface SyncState {
|
||||
lastChecked: number;
|
||||
version: string;
|
||||
}
|
||||
|
||||
interface Manifest {
|
||||
name: string;
|
||||
version: string;
|
||||
publishedAt?: string;
|
||||
asset: {
|
||||
name: string;
|
||||
url?: string;
|
||||
sha256: string;
|
||||
size: number;
|
||||
compression: string;
|
||||
};
|
||||
}
|
||||
|
||||
function getCatalogDir(): string {
|
||||
return join(getConfigDir(), SKILL_DIR_NAME);
|
||||
}
|
||||
|
||||
/**
|
||||
* 本地 Wiki 数据是否已就绪。以 `models.jsonl` 作为存在信号,与
|
||||
* `CatalogSource.available()` 判定一致:只要 advisor 真正消费的文件在,
|
||||
* 就认为数据可用。
|
||||
*/
|
||||
function catalogDataExists(): boolean {
|
||||
return existsSync(join(getCatalogDir(), "models", MODELS_FILE));
|
||||
}
|
||||
|
||||
function getStatePath(): string {
|
||||
return join(getConfigDir(), STATE_FILE_NAME);
|
||||
}
|
||||
|
||||
function readState(): SyncState | null {
|
||||
try {
|
||||
return JSON.parse(readFileSync(getStatePath(), "utf-8")) as SyncState;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function writeState(state: SyncState): void {
|
||||
try {
|
||||
writeFileSync(getStatePath(), JSON.stringify(state));
|
||||
} catch {
|
||||
/* 非关键:state 写失败下次会重新检查 */
|
||||
}
|
||||
}
|
||||
|
||||
/** 拉全部 OSS 预签名 URL:`{ urls: { "manifest.json": "...", "wiki-doc-full.tar.br": "..." } }` */
|
||||
async function fetchSignedUrls(): Promise<Record<string, string> | null> {
|
||||
try {
|
||||
const res = await fetch(FC_SIGN_URL, {
|
||||
signal: AbortSignal.timeout(MANIFEST_TIMEOUT_MS),
|
||||
});
|
||||
if (!res.ok) return null;
|
||||
const data = (await res.json()) as { urls?: Record<string, string> };
|
||||
return data.urls ?? null;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
async function fetchManifest(url: string): Promise<Manifest | null> {
|
||||
try {
|
||||
const res = await fetch(url, { signal: AbortSignal.timeout(MANIFEST_TIMEOUT_MS) });
|
||||
if (!res.ok) return null;
|
||||
return (await res.json()) as Manifest;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
async function downloadBuffer(url: string): Promise<Buffer | null> {
|
||||
try {
|
||||
const res = await fetch(url, { signal: AbortSignal.timeout(DOWNLOAD_TIMEOUT_MS) });
|
||||
if (!res.ok) return null;
|
||||
return Buffer.from(await res.arrayBuffer());
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/** brotli 解压 + tar-stream 解包到 destDir(与 Crawler tar.pack() 对称)。 */
|
||||
async function extractTarBr(tarBrBuffer: Buffer, destDir: string): Promise<void> {
|
||||
const extract = tar.extract();
|
||||
|
||||
extract.on("entry", (header, stream, next) => {
|
||||
const filePath = join(destDir, header.name);
|
||||
if (header.type === "directory") {
|
||||
mkdirSync(filePath, { recursive: true });
|
||||
stream.resume();
|
||||
stream.on("end", next);
|
||||
return;
|
||||
}
|
||||
mkdirSync(dirname(filePath), { recursive: true });
|
||||
const ws = createWriteStream(filePath);
|
||||
stream.pipe(ws);
|
||||
ws.on("finish", next);
|
||||
ws.on("error", next);
|
||||
});
|
||||
|
||||
await pipeline(Readable.from(tarBrBuffer), createBrotliDecompress(), extract);
|
||||
}
|
||||
|
||||
/**
|
||||
* 原子替换:把 tmpDir 解包好的内容替换到 catalogDir。
|
||||
* tmpDir 必须与 catalogDir 同盘(同一 parent 下),renameSync 才是原子的。
|
||||
*/
|
||||
function atomicSwap(tmpDir: string, catalogDir: string): void {
|
||||
mkdirSync(dirname(catalogDir), { recursive: true });
|
||||
const backup = `${catalogDir}.old-${Date.now()}`;
|
||||
if (existsSync(catalogDir)) renameSync(catalogDir, backup);
|
||||
try {
|
||||
renameSync(tmpDir, catalogDir);
|
||||
} catch (err) {
|
||||
// 替换失败 → 回滚旧目录,避免留下空洞
|
||||
if (existsSync(backup) && !existsSync(catalogDir)) renameSync(backup, catalogDir);
|
||||
throw err;
|
||||
}
|
||||
if (existsSync(backup)) rmSync(backup, { recursive: true, force: true });
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查并同步 Wiki 数据。静默执行,任何异常都不抛出。
|
||||
* @returns 是否实际更新了数据(用于测试/调试)
|
||||
*/
|
||||
export async function maybeSyncWikiData(): Promise<boolean> {
|
||||
const state = readState();
|
||||
const now = Date.now();
|
||||
|
||||
// 1. throttle gate:仅当「在 12h 窗口内」且「本地数据确实存在」时才跳过。
|
||||
// 数据缺失(用户手动删除、postinstall 失败但 state 残留等)时无视 throttle,
|
||||
// 立即走同步补齐,避免 advisor 拿不到数据。
|
||||
if (state && now - state.lastChecked < THROTTLE_MS && catalogDataExists()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// 2. 拿签名 URL + manifest
|
||||
const urls = await fetchSignedUrls();
|
||||
if (!urls?.[MANIFEST_KEY]) return false; // 失败不写 lastChecked,下次重试
|
||||
|
||||
const manifest = await fetchManifest(urls[MANIFEST_KEY]);
|
||||
if (!manifest?.version) return false;
|
||||
|
||||
// 3. 版本相同且本地数据存在:仅刷新 lastChecked,无需重新下载。
|
||||
// 若数据缺失,即便版本相同也要继续走下载补齐(落到第 4 步)。
|
||||
if (state && manifest.version === state.version && catalogDataExists()) {
|
||||
writeState({ lastChecked: now, version: state.version });
|
||||
return false;
|
||||
}
|
||||
|
||||
// 4. 版本不同:下载 + 校验 + 解包 + 原子替换
|
||||
const assetUrl = urls[ASSET_KEY];
|
||||
if (!assetUrl) return false;
|
||||
|
||||
const tarBuf = await downloadBuffer(assetUrl);
|
||||
if (!tarBuf) return false;
|
||||
|
||||
// sha256 校验
|
||||
const sha256 = createHash("sha256").update(tarBuf).digest("hex");
|
||||
if (manifest.asset?.sha256 && sha256 !== manifest.asset.sha256) return false;
|
||||
|
||||
const catalogDir = getCatalogDir();
|
||||
// 同盘临时目录:extract 到这里再 rename,跨盘 rename 会 EXDEV
|
||||
const tmpDir = `${catalogDir}.tmp-${process.pid}-${Date.now()}`;
|
||||
try {
|
||||
mkdirSync(tmpDir, { recursive: true });
|
||||
await extractTarBr(tarBuf, tmpDir);
|
||||
atomicSwap(tmpDir, catalogDir);
|
||||
} catch {
|
||||
// 解包/替换失败 → 清理临时目录,不动现有数据,不写 state
|
||||
if (existsSync(tmpDir)) rmSync(tmpDir, { recursive: true, force: true });
|
||||
return false;
|
||||
}
|
||||
|
||||
// 5. 成功:写 state
|
||||
writeState({ lastChecked: now, version: manifest.version });
|
||||
return true;
|
||||
}
|
||||
Generated
+157
@@ -9,6 +9,9 @@ catalogs:
|
||||
'@types/node':
|
||||
specifier: ^24
|
||||
version: 24.12.2
|
||||
'@types/tar-stream':
|
||||
specifier: ^3.1.4
|
||||
version: 3.1.4
|
||||
'@types/yauzl':
|
||||
specifier: ^3.4.0
|
||||
version: 3.4.0
|
||||
@@ -21,6 +24,9 @@ catalogs:
|
||||
chalk:
|
||||
specifier: ^5.6.2
|
||||
version: 5.6.2
|
||||
tar-stream:
|
||||
specifier: ^3.2.0
|
||||
version: 3.2.0
|
||||
tsx:
|
||||
specifier: ^4.23.0
|
||||
version: 4.23.0
|
||||
@@ -63,6 +69,9 @@ importers:
|
||||
bailian-cli-runtime:
|
||||
specifier: workspace:*
|
||||
version: link:../runtime
|
||||
tar-stream:
|
||||
specifier: 'catalog:'
|
||||
version: 3.2.0
|
||||
devDependencies:
|
||||
'@clack/prompts':
|
||||
specifier: ^0.7.0
|
||||
@@ -134,6 +143,9 @@ importers:
|
||||
|
||||
packages/core:
|
||||
dependencies:
|
||||
tar-stream:
|
||||
specifier: 'catalog:'
|
||||
version: 3.2.0
|
||||
yaml:
|
||||
specifier: ^2.8.3
|
||||
version: 2.8.3
|
||||
@@ -144,6 +156,9 @@ importers:
|
||||
'@types/node':
|
||||
specifier: 'catalog:'
|
||||
version: 24.12.2
|
||||
'@types/tar-stream':
|
||||
specifier: 'catalog:'
|
||||
version: 3.1.4
|
||||
'@types/yauzl':
|
||||
specifier: 'catalog:'
|
||||
version: 3.4.0
|
||||
@@ -847,6 +862,9 @@ packages:
|
||||
'@types/node@25.6.0':
|
||||
resolution: {integrity: sha512-+qIYRKdNYJwY3vRCZMdJbPLJAtGjQBudzZzdzwQYkEPQd+PJGixUL5QfvCLDaULoLv+RhT3LDkwEfKaAkgSmNQ==}
|
||||
|
||||
'@types/tar-stream@3.1.4':
|
||||
resolution: {integrity: sha512-921gW0+g29mCJX0fRvqeHzBlE/XclDaAG0Ousy1LCghsOhvaKacDeRGEVzQP9IPfKn8Vysy7FEXAIxycpc/CMg==}
|
||||
|
||||
'@types/yauzl@3.4.0':
|
||||
resolution: {integrity: sha512-NRPn5w6h8dhcnmx3YIRQcqMywY/+nND/uOkJessedcrowO3C0AssHp3tMJpxKAwOhFOo0OV1y9VtsC5hbKKBAw==}
|
||||
|
||||
@@ -1057,6 +1075,51 @@ packages:
|
||||
resolution: {integrity: sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==}
|
||||
engines: {node: '>=12'}
|
||||
|
||||
b4a@1.8.1:
|
||||
resolution: {integrity: sha512-aiqre1Nr0B/6DgE2N5vwTc+2/oQZ4Wh1t4NznYY4E00y8LCt6NqdRv81so00oo27D8MVKTpUa/MwUUtBLXCoDw==}
|
||||
peerDependencies:
|
||||
react-native-b4a: '*'
|
||||
peerDependenciesMeta:
|
||||
react-native-b4a:
|
||||
optional: true
|
||||
|
||||
bare-events@2.9.1:
|
||||
resolution: {integrity: sha512-Z0oHEHAFDZkffN8Qc39zNZjQlMDkPJRyyyZieU1VH7u8c5S+qHZ2S8ixdKIAxEjfHO7FJxXmJWgteOghVanIsg==}
|
||||
peerDependencies:
|
||||
bare-abort-controller: '*'
|
||||
peerDependenciesMeta:
|
||||
bare-abort-controller:
|
||||
optional: true
|
||||
|
||||
bare-fs@4.7.4:
|
||||
resolution: {integrity: sha512-y1kC+ffIx/tPLdTE693uNjHfzTfr+ravR5tvWlMXe25nELbkqV400S71qHDwbkAQ1FVEZobB1NFRzFbCCcyBCQ==}
|
||||
engines: {bare: '>=1.16.0'}
|
||||
peerDependencies:
|
||||
bare-buffer: '*'
|
||||
peerDependenciesMeta:
|
||||
bare-buffer:
|
||||
optional: true
|
||||
|
||||
bare-path@3.1.1:
|
||||
resolution: {integrity: sha512-JprUlveX3QjApC1cTpsUOiscADftCGVWkzitbHsRqv84hzYwYHw2mbluddsq5TvI8mH/8Ov1f4BiMAdcB0oYnQ==}
|
||||
|
||||
bare-stream@2.13.3:
|
||||
resolution: {integrity: sha512-Kc+brLqvEqGkjyfiwJmImAOqLZL7OsoLKuavx+hJjgVV3nLTOjloJyPMFxjUPerGGHrNH0fLU06jjykMLWrERQ==}
|
||||
peerDependencies:
|
||||
bare-abort-controller: '*'
|
||||
bare-buffer: '*'
|
||||
bare-events: '*'
|
||||
peerDependenciesMeta:
|
||||
bare-abort-controller:
|
||||
optional: true
|
||||
bare-buffer:
|
||||
optional: true
|
||||
bare-events:
|
||||
optional: true
|
||||
|
||||
bare-url@2.4.5:
|
||||
resolution: {integrity: sha512-K+y9xF1tN+CdPu4qWwr0QiK1Al07eFPGYK5M2pDXcmHdMdgC/tT/bpmMe1hrmRHaidKLkXrC+cRNYf3XVDUhSQ==}
|
||||
|
||||
boxen@8.0.1:
|
||||
resolution: {integrity: sha512-F3PH5k5juxom4xktynS7MoFY+NUWH5LC4CnH11YB8NPew+HLpmBLCybSAEyb2F+4pRXhuhWqFesoQd6DAyc2hw==}
|
||||
engines: {node: '>=18'}
|
||||
@@ -1091,9 +1154,15 @@ packages:
|
||||
engines: {node: '>=18'}
|
||||
hasBin: true
|
||||
|
||||
events-universal@1.0.1:
|
||||
resolution: {integrity: sha512-LUd5euvbMLpwOF8m6ivPCbhQeSiYVNb8Vs0fQ8QjXo0JTkEHpz8pxdQf0gStltaPpw0Cca8b39KxvK9cfKRiAw==}
|
||||
|
||||
fast-deep-equal@3.1.3:
|
||||
resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==}
|
||||
|
||||
fast-fifo@1.3.2:
|
||||
resolution: {integrity: sha512-/d9sfos4yxzpwkDkuN7k2SqFKtYNmCTzgfEpz82x34IM9/zc8KGxQoXg1liNC/izpRM/MBdt44Nmx41ZWqk+FQ==}
|
||||
|
||||
fast-uri@3.1.2:
|
||||
resolution: {integrity: sha512-rVjf7ArG3LTk+FS6Yw81V1DLuZl1bRbNrev6Tmd/9RaroeeRRJhAt7jg/6YFxbvAQXUCavSoZhPPj6oOx+5KjQ==}
|
||||
|
||||
@@ -1276,6 +1345,9 @@ packages:
|
||||
std-env@4.1.0:
|
||||
resolution: {integrity: sha512-Rq7ybcX2RuC55r9oaPVEW7/xu3tj8u4GeBYHBWCychFtzMIr86A7e3PPEBPT37sHStKX3+TiX/Fr/ACmJLVlLQ==}
|
||||
|
||||
streamx@2.28.0:
|
||||
resolution: {integrity: sha512-1Yowhzjf0ivGMrTIkY9hav5TxobO9qIVqUE41fiCGMGgc3CLlf4MY+9AHmZqBWgDTue0fY9zWjYFVyf6Diuobw==}
|
||||
|
||||
string-width@4.2.3:
|
||||
resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==}
|
||||
engines: {node: '>=8'}
|
||||
@@ -1292,6 +1364,15 @@ packages:
|
||||
resolution: {integrity: sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==}
|
||||
engines: {node: '>=12'}
|
||||
|
||||
tar-stream@3.2.0:
|
||||
resolution: {integrity: sha512-ojzvCvVaNp6aOTFmG7jaRD0meowIAuPc3cMMhSgKiVWws1GyHbGd/xvnyuRKcKlMpt3qvxx6r0hreCNITP9hIg==}
|
||||
|
||||
teex@1.0.1:
|
||||
resolution: {integrity: sha512-eYE6iEI62Ni1H8oIa7KlDU6uQBtqr4Eajni3wX7rpfXD8ysFx8z0+dri+KWEPWpBsxXfxu58x/0jvTVT1ekOSg==}
|
||||
|
||||
text-decoder@1.2.7:
|
||||
resolution: {integrity: sha512-vlLytXkeP4xvEq2otHeJfSQIRyWxo/oZGEbXrtEEF9Hnmrdly59sUbzZ/QgyWuLYHctCHxFF4tRQZNQ9k60ExQ==}
|
||||
|
||||
tinybench@2.9.0:
|
||||
resolution: {integrity: sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==}
|
||||
|
||||
@@ -1744,6 +1825,10 @@ snapshots:
|
||||
dependencies:
|
||||
undici-types: 7.19.2
|
||||
|
||||
'@types/tar-stream@3.1.4':
|
||||
dependencies:
|
||||
'@types/node': 25.6.0
|
||||
|
||||
'@types/yauzl@3.4.0':
|
||||
dependencies:
|
||||
'@types/node': 25.6.0
|
||||
@@ -1932,6 +2017,37 @@ snapshots:
|
||||
|
||||
assertion-error@2.0.1: {}
|
||||
|
||||
b4a@1.8.1: {}
|
||||
|
||||
bare-events@2.9.1: {}
|
||||
|
||||
bare-fs@4.7.4:
|
||||
dependencies:
|
||||
bare-events: 2.9.1
|
||||
bare-path: 3.1.1
|
||||
bare-stream: 2.13.3(bare-events@2.9.1)
|
||||
bare-url: 2.4.5
|
||||
fast-fifo: 1.3.2
|
||||
transitivePeerDependencies:
|
||||
- bare-abort-controller
|
||||
- react-native-b4a
|
||||
|
||||
bare-path@3.1.1: {}
|
||||
|
||||
bare-stream@2.13.3(bare-events@2.9.1):
|
||||
dependencies:
|
||||
b4a: 1.8.1
|
||||
streamx: 2.28.0
|
||||
teex: 1.0.1
|
||||
optionalDependencies:
|
||||
bare-events: 2.9.1
|
||||
transitivePeerDependencies:
|
||||
- react-native-b4a
|
||||
|
||||
bare-url@2.4.5:
|
||||
dependencies:
|
||||
bare-path: 3.1.1
|
||||
|
||||
boxen@8.0.1:
|
||||
dependencies:
|
||||
ansi-align: 3.0.1
|
||||
@@ -1986,8 +2102,16 @@ snapshots:
|
||||
'@esbuild/win32-ia32': 0.28.1
|
||||
'@esbuild/win32-x64': 0.28.1
|
||||
|
||||
events-universal@1.0.1:
|
||||
dependencies:
|
||||
bare-events: 2.9.1
|
||||
transitivePeerDependencies:
|
||||
- bare-abort-controller
|
||||
|
||||
fast-deep-equal@3.1.3: {}
|
||||
|
||||
fast-fifo@1.3.2: {}
|
||||
|
||||
fast-uri@3.1.2: {}
|
||||
|
||||
fdir@6.5.0(picomatch@4.0.4):
|
||||
@@ -2170,6 +2294,15 @@ snapshots:
|
||||
|
||||
std-env@4.1.0: {}
|
||||
|
||||
streamx@2.28.0:
|
||||
dependencies:
|
||||
events-universal: 1.0.1
|
||||
fast-fifo: 1.3.2
|
||||
text-decoder: 1.2.7
|
||||
transitivePeerDependencies:
|
||||
- bare-abort-controller
|
||||
- react-native-b4a
|
||||
|
||||
string-width@4.2.3:
|
||||
dependencies:
|
||||
emoji-regex: 8.0.0
|
||||
@@ -2190,6 +2323,30 @@ snapshots:
|
||||
dependencies:
|
||||
ansi-regex: 6.2.2
|
||||
|
||||
tar-stream@3.2.0:
|
||||
dependencies:
|
||||
b4a: 1.8.1
|
||||
bare-fs: 4.7.4
|
||||
fast-fifo: 1.3.2
|
||||
streamx: 2.28.0
|
||||
transitivePeerDependencies:
|
||||
- bare-abort-controller
|
||||
- bare-buffer
|
||||
- react-native-b4a
|
||||
|
||||
teex@1.0.1:
|
||||
dependencies:
|
||||
streamx: 2.28.0
|
||||
transitivePeerDependencies:
|
||||
- bare-abort-controller
|
||||
- react-native-b4a
|
||||
|
||||
text-decoder@1.2.7:
|
||||
dependencies:
|
||||
b4a: 1.8.1
|
||||
transitivePeerDependencies:
|
||||
- react-native-b4a
|
||||
|
||||
tinybench@2.9.0: {}
|
||||
|
||||
tinyexec@1.1.2: {}
|
||||
|
||||
@@ -4,10 +4,12 @@ packages:
|
||||
|
||||
catalog:
|
||||
"@types/node": ^24
|
||||
"@types/tar-stream": ^3.1.4
|
||||
"@types/yauzl": ^3.4.0
|
||||
ajv: ^8.20.0
|
||||
boxen: ^8.0.1
|
||||
chalk: ^5.6.2
|
||||
tar-stream: ^3.2.0
|
||||
tsx: ^4.23.0
|
||||
typescript: ^5
|
||||
undici: ^8.4.1
|
||||
|
||||
Reference in New Issue
Block a user