fix: stabilize local OCR and prod startup
This commit is contained in:
+151
-6
@@ -25,6 +25,80 @@ const RUN_DIR = path.join(ROOT, "dist", "run", "mnote-web-prod");
|
||||
const RUN_STATE_PATH = path.join(RUN_DIR, "process.json");
|
||||
const DEFAULT_PORT = 3003;
|
||||
const DEFAULT_CONTROL_PLANE_DB = "/mnt/Data1T/Mnote_data/control-plane/control-plane.db";
|
||||
const ENV_ALL_PATH = path.join(ROOT, ".env.all");
|
||||
const RELEASE_BUILD_SCHEMA = "mnote-web-prod-runtime-assets-v2";
|
||||
const SUPPLEMENTAL_RUNTIME_ASSETS = [
|
||||
{
|
||||
source: "node_modules/jszip/dist/jszip.min.js",
|
||||
target: "node_modules/jszip/dist/jszip.min.js",
|
||||
},
|
||||
{
|
||||
source: "node_modules/docx-preview/dist/docx-preview.min.js",
|
||||
target: "node_modules/docx-preview/dist/docx-preview.min.js",
|
||||
},
|
||||
{
|
||||
source: "node_modules/@e965/xlsx/dist/xlsx.full.min.js",
|
||||
target: "node_modules/@e965/xlsx/dist/xlsx.full.min.js",
|
||||
},
|
||||
{
|
||||
source: "node_modules/pptx-preview/dist/pptx-preview.umd.js",
|
||||
target: "node_modules/pptx-preview/dist/pptx-preview.umd.js",
|
||||
},
|
||||
{
|
||||
source: "node_modules/pdfjs-dist/build/pdf.mjs",
|
||||
target: "node_modules/pdfjs-dist/build/pdf.mjs",
|
||||
},
|
||||
{
|
||||
source: "node_modules/pdfjs-dist/build/pdf.worker.mjs",
|
||||
target: "node_modules/pdfjs-dist/build/pdf.worker.mjs",
|
||||
},
|
||||
{
|
||||
source: "reference-code/leptos-tiptap/src/js/generated/tiptap_mindmap_paragraph_runtime.js",
|
||||
target: "reference-code/leptos-tiptap/src/js/generated/tiptap_mindmap_paragraph_runtime.js",
|
||||
},
|
||||
];
|
||||
|
||||
const STARTUP_ASSET_PROBES = [
|
||||
"/api/office-preview/vendor/docx-preview.min.js",
|
||||
"/api/office-preview/vendor/xlsx.full.min.js",
|
||||
"/api/office-preview/vendor/pptx-preview.umd.js",
|
||||
"/api/pdfjs/pdf.mjs",
|
||||
"/api/pdfjs/pdf.worker.mjs",
|
||||
"/api/leptos-tiptap-runtime/tiptap_mindmap_paragraph_runtime.js",
|
||||
];
|
||||
|
||||
function loadEnvFile(filePath) {
|
||||
if (!fs.existsSync(filePath)) return {};
|
||||
const content = fs.readFileSync(filePath, "utf8");
|
||||
return content
|
||||
.split(/\r?\n/)
|
||||
.map((line) => line.trim())
|
||||
.filter((line) => line && !line.startsWith("#"))
|
||||
.reduce((acc, line) => {
|
||||
const normalized = line.startsWith("export ") ? line.slice("export ".length).trim() : line;
|
||||
const idx = normalized.indexOf("=");
|
||||
if (idx === -1) return acc;
|
||||
const key = normalized.slice(0, idx).trim();
|
||||
let value = normalized.slice(idx + 1).trim();
|
||||
if (!key) return acc;
|
||||
if (
|
||||
(value.startsWith('"') && value.endsWith('"'))
|
||||
|| (value.startsWith("'") && value.endsWith("'"))
|
||||
) {
|
||||
value = value.slice(1, -1);
|
||||
}
|
||||
acc[key] = value;
|
||||
return acc;
|
||||
}, {});
|
||||
}
|
||||
|
||||
function loadProdEnv() {
|
||||
const envFromAll = loadEnvFile(ENV_ALL_PATH);
|
||||
if (Object.keys(envFromAll).length > 0) {
|
||||
Object.assign(process.env, envFromAll);
|
||||
console.log(`[prod] 已加载 .env.all (${Object.keys(envFromAll).length} 项)`);
|
||||
}
|
||||
}
|
||||
|
||||
function runChecked(command, args, options = {}) {
|
||||
const result = spawnSync(command, args, {
|
||||
@@ -88,6 +162,15 @@ async function pathExists(filePath) {
|
||||
}
|
||||
}
|
||||
|
||||
async function releaseRuntimeAssetsReady(sourceDir) {
|
||||
for (const asset of SUPPLEMENTAL_RUNTIME_ASSETS) {
|
||||
if (!(await pathExists(path.join(sourceDir, asset.target)))) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
async function listReleases() {
|
||||
if (!(await pathExists(RELEASE_ROOT))) return [];
|
||||
const entries = await fsp.readdir(RELEASE_ROOT, { withFileTypes: true });
|
||||
@@ -104,7 +187,10 @@ async function listReleases() {
|
||||
const metadata = JSON.parse(await fsp.readFile(metadataPath, "utf8"));
|
||||
const binaryPath = path.join(releaseDir, "mnote-web");
|
||||
const sourceDir = path.join(releaseDir, "source");
|
||||
const usable = (await pathExists(binaryPath)) && (await pathExists(sourceDir));
|
||||
const usable = (await pathExists(binaryPath))
|
||||
&& (await pathExists(sourceDir))
|
||||
&& metadata.buildSchema === RELEASE_BUILD_SCHEMA
|
||||
&& (await releaseRuntimeAssetsReady(sourceDir));
|
||||
releases.push({ version: entry.name, releaseDir, metadata, usable });
|
||||
} catch {
|
||||
releases.push({ version: entry.name, releaseDir, metadata: null, usable: false });
|
||||
@@ -170,6 +256,23 @@ function pipeGitArchive(head, destination) {
|
||||
});
|
||||
}
|
||||
|
||||
async function copyRuntimeAsset(sourceDir, asset) {
|
||||
const from = path.join(ROOT, asset.source);
|
||||
const to = path.join(sourceDir, asset.target);
|
||||
if (!(await pathExists(from))) {
|
||||
throw new Error(`缺少生产运行时资产:${asset.source},请先在仓库根目录运行 npm install`);
|
||||
}
|
||||
await fsp.mkdir(path.dirname(to), { recursive: true });
|
||||
await fsp.copyFile(from, to);
|
||||
}
|
||||
|
||||
async function copySupplementalRuntimeAssets(sourceDir) {
|
||||
console.log("[prod] 打包 Office/PDF/思维导图运行时资产 ...");
|
||||
for (const asset of SUPPLEMENTAL_RUNTIME_ASSETS) {
|
||||
await copyRuntimeAsset(sourceDir, asset);
|
||||
}
|
||||
}
|
||||
|
||||
async function buildRelease(head) {
|
||||
const version = await nextReleaseVersion();
|
||||
const releaseDir = path.join(RELEASE_ROOT, version);
|
||||
@@ -180,8 +283,9 @@ async function buildRelease(head) {
|
||||
await fsp.mkdir(sourceDir, { recursive: true });
|
||||
console.log(`[prod] 创建 release ${version}: ${releaseDir}`);
|
||||
await pipeGitArchive(head, sourceDir);
|
||||
await copySupplementalRuntimeAssets(sourceDir);
|
||||
|
||||
const targetDir = path.join(BUILD_CACHE_DIR, "target");
|
||||
const targetDir = path.join(BUILD_CACHE_DIR, version, "target");
|
||||
await fsp.mkdir(targetDir, { recursive: true });
|
||||
|
||||
console.log(`[prod] 构建已提交 HEAD ${head.slice(0, 12)} ...`);
|
||||
@@ -204,14 +308,17 @@ async function buildRelease(head) {
|
||||
|
||||
const metadata = {
|
||||
schema: "mnote.prod_release.v1",
|
||||
buildSchema: RELEASE_BUILD_SCHEMA,
|
||||
version,
|
||||
gitHead: head,
|
||||
builtAt: new Date().toISOString(),
|
||||
binaryPath,
|
||||
sourceDir,
|
||||
cargoTargetDir: targetDir,
|
||||
runtimeAssets: SUPPLEMENTAL_RUNTIME_ASSETS.map((asset) => asset.target),
|
||||
};
|
||||
await fsp.writeFile(metadataPath, `${JSON.stringify(metadata, null, 2)}\n`, "utf8");
|
||||
await fsp.rm(targetDir, { recursive: true, force: true });
|
||||
return { version, releaseDir, metadata, usable: true };
|
||||
}
|
||||
|
||||
@@ -358,6 +465,28 @@ function requestLocalAuth(port) {
|
||||
});
|
||||
}
|
||||
|
||||
function requestLocalPath(port, requestPath) {
|
||||
return new Promise((resolve) => {
|
||||
const req = http.get(
|
||||
{
|
||||
host: "127.0.0.1",
|
||||
port,
|
||||
path: requestPath,
|
||||
timeout: 1000,
|
||||
},
|
||||
(res) => {
|
||||
res.resume();
|
||||
resolve(res.statusCode && res.statusCode >= 200 && res.statusCode < 300);
|
||||
},
|
||||
);
|
||||
req.on("timeout", () => {
|
||||
req.destroy();
|
||||
resolve(false);
|
||||
});
|
||||
req.on("error", () => resolve(false));
|
||||
});
|
||||
}
|
||||
|
||||
async function waitForReady(port, logPath, pid) {
|
||||
for (let i = 0; i < 60; i += 1) {
|
||||
if (!isProcessAlive(pid)) break;
|
||||
@@ -374,6 +503,18 @@ async function waitForReady(port, logPath, pid) {
|
||||
throw new Error(`mnote-web 启动失败或未就绪,日志:\n${logTail}`);
|
||||
}
|
||||
|
||||
async function verifyStartupAssets(port) {
|
||||
const failed = [];
|
||||
for (const requestPath of STARTUP_ASSET_PROBES) {
|
||||
// eslint-disable-next-line no-await-in-loop
|
||||
const ok = await requestLocalPath(port, requestPath);
|
||||
if (!ok) failed.push(requestPath);
|
||||
}
|
||||
if (failed.length > 0) {
|
||||
throw new Error(`生产运行时资产探测失败:${failed.join(", ")}`);
|
||||
}
|
||||
}
|
||||
|
||||
async function startRelease(release) {
|
||||
const port = Number(process.env.MNOTE_PROD_PORT || DEFAULT_PORT);
|
||||
if (!Number.isInteger(port) || port <= 0 || port > 65535) {
|
||||
@@ -419,6 +560,7 @@ async function startRelease(release) {
|
||||
};
|
||||
await fsp.writeFile(RUN_STATE_PATH, `${JSON.stringify(state, null, 2)}\n`, "utf8");
|
||||
await waitForReady(port, logPath, child.pid);
|
||||
await verifyStartupAssets(port);
|
||||
|
||||
console.log(`[prod] 已启动 mnote-web release ${release.version}`);
|
||||
console.log(`[prod] pid=${child.pid}`);
|
||||
@@ -430,6 +572,7 @@ async function startRelease(release) {
|
||||
}
|
||||
|
||||
async function main() {
|
||||
loadProdEnv();
|
||||
const head = currentGitHead();
|
||||
await fsp.mkdir(RELEASE_ROOT, { recursive: true });
|
||||
|
||||
@@ -443,7 +586,9 @@ async function main() {
|
||||
await startRelease(release);
|
||||
}
|
||||
|
||||
main().catch((error) => {
|
||||
console.error(`[prod] ${error.message}`);
|
||||
process.exit(1);
|
||||
});
|
||||
if (require.main === module) {
|
||||
main().catch((error) => {
|
||||
console.error(`[prod] ${error.message}`);
|
||||
process.exit(1);
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user