fix: stabilize local OCR and prod startup
This commit is contained in:
@@ -1506,6 +1506,12 @@ export const createResourceTabRuntime = (dependencies = {}) => {
|
|||||||
const statusTextForLocalOcrJob = (job) => {
|
const statusTextForLocalOcrJob = (job) => {
|
||||||
const status = String(job?.status || '').trim();
|
const status = String(job?.status || '').trim();
|
||||||
if (job?.stageLabel) return String(job.stageLabel);
|
if (job?.stageLabel) return String(job.stageLabel);
|
||||||
|
if (job?.taskKind === 'local_index') {
|
||||||
|
return status === 'done' ? '索引已完成'
|
||||||
|
: status === 'failed' ? '索引失败'
|
||||||
|
: status === 'running' ? '索引中'
|
||||||
|
: status || '未知';
|
||||||
|
}
|
||||||
return status === 'done' ? '已识别'
|
return status === 'done' ? '已识别'
|
||||||
: status === 'failed' ? '识别失败'
|
: status === 'failed' ? '识别失败'
|
||||||
: status === 'stale' ? '来源已变化'
|
: status === 'stale' ? '来源已变化'
|
||||||
@@ -1513,6 +1519,11 @@ export const createResourceTabRuntime = (dependencies = {}) => {
|
|||||||
: status || '未知';
|
: status || '未知';
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const localOcrDisplayStatus = (job, fallback = 'done') => {
|
||||||
|
const status = String(job?.status || fallback).trim() || fallback;
|
||||||
|
return job?.stale === true && status === 'done' ? 'stale' : status;
|
||||||
|
};
|
||||||
|
|
||||||
const localOcrTaskCategory = (job) => {
|
const localOcrTaskCategory = (job) => {
|
||||||
const status = String(job?.status || '').trim();
|
const status = String(job?.status || '').trim();
|
||||||
if (['failed', 'stale', 'retry_scheduled'].includes(status)) return 'attention';
|
if (['failed', 'stale', 'retry_scheduled'].includes(status)) return 'attention';
|
||||||
@@ -1755,11 +1766,12 @@ export const createResourceTabRuntime = (dependencies = {}) => {
|
|||||||
if (open instanceof HTMLButtonElement) {
|
if (open instanceof HTMLButtonElement) {
|
||||||
open.setAttribute('data-mnote-local-ocr-task-open', String(job.sourceRootRelativePath || ''));
|
open.setAttribute('data-mnote-local-ocr-task-open', String(job.sourceRootRelativePath || ''));
|
||||||
open.disabled = !job.ocrRootRelativePath;
|
open.disabled = !job.ocrRootRelativePath;
|
||||||
|
open.hidden = job.taskKind === 'local_index';
|
||||||
}
|
}
|
||||||
const retry = row.querySelector('[data-mnote-local-ocr-task-retry]');
|
const retry = row.querySelector('[data-mnote-local-ocr-task-retry]');
|
||||||
if (retry instanceof HTMLButtonElement) {
|
if (retry instanceof HTMLButtonElement) {
|
||||||
retry.setAttribute('data-mnote-local-ocr-task-retry', String(job.sourceRootRelativePath || ''));
|
retry.setAttribute('data-mnote-local-ocr-task-retry', String(job.sourceRootRelativePath || ''));
|
||||||
retry.hidden = !['failed', 'stale'].includes(String(job.status || ''));
|
retry.hidden = job.taskKind === 'local_index' || !['failed', 'stale'].includes(String(job.status || ''));
|
||||||
}
|
}
|
||||||
const clear = row.querySelector('[data-mnote-local-ocr-task-clear]');
|
const clear = row.querySelector('[data-mnote-local-ocr-task-clear]');
|
||||||
if (clear instanceof HTMLButtonElement) {
|
if (clear instanceof HTMLButtonElement) {
|
||||||
@@ -1768,7 +1780,7 @@ export const createResourceTabRuntime = (dependencies = {}) => {
|
|||||||
const deleteOcr = row.querySelector('[data-mnote-local-ocr-task-delete]');
|
const deleteOcr = row.querySelector('[data-mnote-local-ocr-task-delete]');
|
||||||
if (deleteOcr instanceof HTMLButtonElement) {
|
if (deleteOcr instanceof HTMLButtonElement) {
|
||||||
deleteOcr.setAttribute('data-mnote-local-ocr-task-delete', String(job.sourceRootRelativePath || ''));
|
deleteOcr.setAttribute('data-mnote-local-ocr-task-delete', String(job.sourceRootRelativePath || ''));
|
||||||
deleteOcr.hidden = !job.ocrRootRelativePath;
|
deleteOcr.hidden = job.taskKind === 'local_index' || !job.ocrRootRelativePath;
|
||||||
}
|
}
|
||||||
list.appendChild(row);
|
list.appendChild(row);
|
||||||
});
|
});
|
||||||
@@ -1882,7 +1894,7 @@ export const createResourceTabRuntime = (dependencies = {}) => {
|
|||||||
});
|
});
|
||||||
const payload = await response.json().catch(() => null);
|
const payload = await response.json().catch(() => null);
|
||||||
if (!response.ok || !payload || payload.ok !== true) {
|
if (!response.ok || !payload || payload.ok !== true) {
|
||||||
const message = payload?.error?.message || `local_ocr_job_failed_${response.status}`;
|
const message = payload?.error?.message || payload?.message || payload?.error?.code || `local_ocr_job_failed_${response.status}`;
|
||||||
const failedJob = {
|
const failedJob = {
|
||||||
...pendingJob,
|
...pendingJob,
|
||||||
status: 'failed',
|
status: 'failed',
|
||||||
@@ -1896,7 +1908,8 @@ export const createResourceTabRuntime = (dependencies = {}) => {
|
|||||||
throw new Error(message);
|
throw new Error(message);
|
||||||
}
|
}
|
||||||
const job = payload.job && typeof payload.job === 'object' ? { ...payload.job, rootUri: entryRootUri } : null;
|
const job = payload.job && typeof payload.job === 'object' ? { ...payload.job, rootUri: entryRootUri } : null;
|
||||||
setLocalOcrStatus(entry, String(job?.status || 'done'), job?.stale ? 'OCR 需更新' : 'OCR 已完成', job);
|
const displayStatus = localOcrDisplayStatus(job, 'done');
|
||||||
|
setLocalOcrStatus(entry, displayStatus, displayStatus === 'stale' ? 'OCR 需更新' : 'OCR 已完成', job);
|
||||||
if (job) updateLocalOcrTaskState(job);
|
if (job) updateLocalOcrTaskState(job);
|
||||||
return job;
|
return job;
|
||||||
};
|
};
|
||||||
@@ -2052,6 +2065,25 @@ export const createResourceTabRuntime = (dependencies = {}) => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
window.addEventListener('mnote:local-background-task-updated', (event) => {
|
||||||
|
const task = event?.detail?.task && typeof event.detail.task === 'object' ? event.detail.task : null;
|
||||||
|
if (!task) return;
|
||||||
|
updateLocalOcrTaskState({
|
||||||
|
taskKind: String(task.taskKind || 'local_index'),
|
||||||
|
jobId: String(task.jobId || task.taskId || `local-index-${Date.now()}`),
|
||||||
|
sourceRootRelativePath: String(task.sourceRootRelativePath || task.taskId || 'local-index'),
|
||||||
|
rootUri: String(task.rootUri || localOcrTaskState.rootUri || currentWebShellRootUri() || '').trim(),
|
||||||
|
ocrRootRelativePath: '',
|
||||||
|
provider: String(task.provider || 'mnote-web'),
|
||||||
|
status: String(task.status || 'running'),
|
||||||
|
stageLabel: String(task.stageLabel || ''),
|
||||||
|
stale: false,
|
||||||
|
updatedAtMs: Number(task.updatedAtMs || Date.now()),
|
||||||
|
finishedAtMs: task.finishedAtMs == null ? null : Number(task.finishedAtMs),
|
||||||
|
error: task.error || null,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
const renderLocalOcrToolbar = (entry) => {
|
const renderLocalOcrToolbar = (entry) => {
|
||||||
if (!isLocalOcrSourceEntry(entry) || !(entry.panel instanceof HTMLElement)) return;
|
if (!isLocalOcrSourceEntry(entry) || !(entry.panel instanceof HTMLElement)) return;
|
||||||
const toolbar = entry.panel.querySelector('[data-testid="mnote-local-ocr-toolbar"]');
|
const toolbar = entry.panel.querySelector('[data-testid="mnote-local-ocr-toolbar"]');
|
||||||
@@ -2092,7 +2124,8 @@ export const createResourceTabRuntime = (dependencies = {}) => {
|
|||||||
void loadLocalOcrJobs(entry.rootUri).catch(() => undefined);
|
void loadLocalOcrJobs(entry.rootUri).catch(() => undefined);
|
||||||
void readLocalOcrStatus(entry).then((job) => {
|
void readLocalOcrStatus(entry).then((job) => {
|
||||||
if (!job) return;
|
if (!job) return;
|
||||||
setLocalOcrStatus(entry, job.stale ? 'stale' : String(job.status || 'done'), job.stale ? 'OCR 需更新' : 'OCR 已完成', job);
|
const displayStatus = localOcrDisplayStatus(job, 'done');
|
||||||
|
setLocalOcrStatus(entry, displayStatus, displayStatus === 'stale' ? 'OCR 需更新' : 'OCR 已完成', job);
|
||||||
updateLocalOcrTaskState(job);
|
updateLocalOcrTaskState(job);
|
||||||
}).catch(() => undefined);
|
}).catch(() => undefined);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -961,8 +961,28 @@ export function createSidebarPageSettingsRuntime(context) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function emitLocalIndexBackgroundTask(status, label, error) {
|
||||||
|
var now = Date.now();
|
||||||
|
window.dispatchEvent(new CustomEvent('mnote:local-background-task-updated', {
|
||||||
|
detail: {
|
||||||
|
task: {
|
||||||
|
taskKind: 'local_index',
|
||||||
|
taskId: 'local-index',
|
||||||
|
sourceRootRelativePath: '本地索引',
|
||||||
|
rootUri: currentRootUri(),
|
||||||
|
status: status,
|
||||||
|
stageLabel: label,
|
||||||
|
updatedAtMs: now,
|
||||||
|
finishedAtMs: status === 'running' ? null : now,
|
||||||
|
error: error || null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
async function refreshLocalIndex() {
|
async function refreshLocalIndex() {
|
||||||
if (!pageSettingsLocalIndexIsAvailable()) return;
|
if (!pageSettingsLocalIndexIsAvailable()) return;
|
||||||
|
emitLocalIndexBackgroundTask('running', '本地索引重建中', null);
|
||||||
pageUiState.localIndexSummary = Object.assign({}, pageUiState.localIndexSummary || {}, {
|
pageUiState.localIndexSummary = Object.assign({}, pageUiState.localIndexSummary || {}, {
|
||||||
scopeKey: pageSettingsLocalIndexScopeKey(),
|
scopeKey: pageSettingsLocalIndexScopeKey(),
|
||||||
loading: true,
|
loading: true,
|
||||||
@@ -982,8 +1002,10 @@ export function createSidebarPageSettingsRuntime(context) {
|
|||||||
if (!response.ok || !payload || payload.ok !== true) {
|
if (!response.ok || !payload || payload.ok !== true) {
|
||||||
throw new Error(payload && payload.error && payload.error.message ? payload.error.message : 'local_index_refresh_' + response.status);
|
throw new Error(payload && payload.error && payload.error.message ? payload.error.message : 'local_index_refresh_' + response.status);
|
||||||
}
|
}
|
||||||
|
emitLocalIndexBackgroundTask('done', '本地索引已完成', null);
|
||||||
await loadPageSettingsLocalIndex(true);
|
await loadPageSettingsLocalIndex(true);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
emitLocalIndexBackgroundTask('failed', '本地索引失败', error instanceof Error ? error.message : String(error));
|
||||||
pageUiState.localIndexSummary = Object.assign({}, pageUiState.localIndexSummary || {}, {
|
pageUiState.localIndexSummary = Object.assign({}, pageUiState.localIndexSummary || {}, {
|
||||||
scopeKey: pageSettingsLocalIndexScopeKey(),
|
scopeKey: pageSettingsLocalIndexScopeKey(),
|
||||||
loading: false,
|
loading: false,
|
||||||
|
|||||||
@@ -1712,7 +1712,7 @@ fn broadcast_ocr_job_update(state: &AppState, root: &Path, root_uri: &str, entry
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn ocr_job_payload(root: &Path, entry: &OcrIndexEntry) -> Value {
|
fn ocr_job_payload(root: &Path, entry: &OcrIndexEntry) -> Value {
|
||||||
let stale = source_is_stale(root, entry);
|
let stale = entry.status == "done" && source_is_stale(root, entry);
|
||||||
json!({
|
json!({
|
||||||
"jobId": entry.job_id,
|
"jobId": entry.job_id,
|
||||||
"ownerDocumentId": entry.owner_document_id,
|
"ownerDocumentId": entry.owner_document_id,
|
||||||
|
|||||||
+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 RUN_STATE_PATH = path.join(RUN_DIR, "process.json");
|
||||||
const DEFAULT_PORT = 3003;
|
const DEFAULT_PORT = 3003;
|
||||||
const DEFAULT_CONTROL_PLANE_DB = "/mnt/Data1T/Mnote_data/control-plane/control-plane.db";
|
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 = {}) {
|
function runChecked(command, args, options = {}) {
|
||||||
const result = spawnSync(command, args, {
|
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() {
|
async function listReleases() {
|
||||||
if (!(await pathExists(RELEASE_ROOT))) return [];
|
if (!(await pathExists(RELEASE_ROOT))) return [];
|
||||||
const entries = await fsp.readdir(RELEASE_ROOT, { withFileTypes: true });
|
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 metadata = JSON.parse(await fsp.readFile(metadataPath, "utf8"));
|
||||||
const binaryPath = path.join(releaseDir, "mnote-web");
|
const binaryPath = path.join(releaseDir, "mnote-web");
|
||||||
const sourceDir = path.join(releaseDir, "source");
|
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 });
|
releases.push({ version: entry.name, releaseDir, metadata, usable });
|
||||||
} catch {
|
} catch {
|
||||||
releases.push({ version: entry.name, releaseDir, metadata: null, usable: false });
|
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) {
|
async function buildRelease(head) {
|
||||||
const version = await nextReleaseVersion();
|
const version = await nextReleaseVersion();
|
||||||
const releaseDir = path.join(RELEASE_ROOT, version);
|
const releaseDir = path.join(RELEASE_ROOT, version);
|
||||||
@@ -180,8 +283,9 @@ async function buildRelease(head) {
|
|||||||
await fsp.mkdir(sourceDir, { recursive: true });
|
await fsp.mkdir(sourceDir, { recursive: true });
|
||||||
console.log(`[prod] 创建 release ${version}: ${releaseDir}`);
|
console.log(`[prod] 创建 release ${version}: ${releaseDir}`);
|
||||||
await pipeGitArchive(head, sourceDir);
|
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 });
|
await fsp.mkdir(targetDir, { recursive: true });
|
||||||
|
|
||||||
console.log(`[prod] 构建已提交 HEAD ${head.slice(0, 12)} ...`);
|
console.log(`[prod] 构建已提交 HEAD ${head.slice(0, 12)} ...`);
|
||||||
@@ -204,14 +308,17 @@ async function buildRelease(head) {
|
|||||||
|
|
||||||
const metadata = {
|
const metadata = {
|
||||||
schema: "mnote.prod_release.v1",
|
schema: "mnote.prod_release.v1",
|
||||||
|
buildSchema: RELEASE_BUILD_SCHEMA,
|
||||||
version,
|
version,
|
||||||
gitHead: head,
|
gitHead: head,
|
||||||
builtAt: new Date().toISOString(),
|
builtAt: new Date().toISOString(),
|
||||||
binaryPath,
|
binaryPath,
|
||||||
sourceDir,
|
sourceDir,
|
||||||
cargoTargetDir: targetDir,
|
cargoTargetDir: targetDir,
|
||||||
|
runtimeAssets: SUPPLEMENTAL_RUNTIME_ASSETS.map((asset) => asset.target),
|
||||||
};
|
};
|
||||||
await fsp.writeFile(metadataPath, `${JSON.stringify(metadata, null, 2)}\n`, "utf8");
|
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 };
|
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) {
|
async function waitForReady(port, logPath, pid) {
|
||||||
for (let i = 0; i < 60; i += 1) {
|
for (let i = 0; i < 60; i += 1) {
|
||||||
if (!isProcessAlive(pid)) break;
|
if (!isProcessAlive(pid)) break;
|
||||||
@@ -374,6 +503,18 @@ async function waitForReady(port, logPath, pid) {
|
|||||||
throw new Error(`mnote-web 启动失败或未就绪,日志:\n${logTail}`);
|
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) {
|
async function startRelease(release) {
|
||||||
const port = Number(process.env.MNOTE_PROD_PORT || DEFAULT_PORT);
|
const port = Number(process.env.MNOTE_PROD_PORT || DEFAULT_PORT);
|
||||||
if (!Number.isInteger(port) || port <= 0 || port > 65535) {
|
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 fsp.writeFile(RUN_STATE_PATH, `${JSON.stringify(state, null, 2)}\n`, "utf8");
|
||||||
await waitForReady(port, logPath, child.pid);
|
await waitForReady(port, logPath, child.pid);
|
||||||
|
await verifyStartupAssets(port);
|
||||||
|
|
||||||
console.log(`[prod] 已启动 mnote-web release ${release.version}`);
|
console.log(`[prod] 已启动 mnote-web release ${release.version}`);
|
||||||
console.log(`[prod] pid=${child.pid}`);
|
console.log(`[prod] pid=${child.pid}`);
|
||||||
@@ -430,6 +572,7 @@ async function startRelease(release) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function main() {
|
async function main() {
|
||||||
|
loadProdEnv();
|
||||||
const head = currentGitHead();
|
const head = currentGitHead();
|
||||||
await fsp.mkdir(RELEASE_ROOT, { recursive: true });
|
await fsp.mkdir(RELEASE_ROOT, { recursive: true });
|
||||||
|
|
||||||
@@ -443,7 +586,9 @@ async function main() {
|
|||||||
await startRelease(release);
|
await startRelease(release);
|
||||||
}
|
}
|
||||||
|
|
||||||
main().catch((error) => {
|
if (require.main === module) {
|
||||||
console.error(`[prod] ${error.message}`);
|
main().catch((error) => {
|
||||||
process.exit(1);
|
console.error(`[prod] ${error.message}`);
|
||||||
});
|
process.exit(1);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user