Files
mnote/scripts/lib/pi-lab-warmup.js
T

242 lines
8.5 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"use strict";
const fs = require("node:fs");
const path = require("node:path");
function isEnabledEnv(value) {
const normalized = String(value || "").trim().toLowerCase();
return normalized === "1" || normalized === "true" || normalized === "yes" || normalized === "on";
}
function actorSegment(actorId) {
const value = String(actorId || "").trim().replace(/[/:\\]/g, "_");
return value || "mnote-e2e";
}
function rootUriFromPath(rootPath) {
return `file://${path.resolve(rootPath)}`;
}
function resolvePiLabWarmupPlan(env = process.env, rootDir = path.resolve(__dirname, "../..")) {
const actorId = String(env.MNOTE_PAGE_AI_PI_WARMUP_ACTOR_ID || env.MNOTE_E2E_ACTOR_ID || "mnote-e2e").trim();
const defaultRootPath = String(
env.MNOTE_PAGE_AI_PI_WARMUP_ROOT_PATH ||
env.MNOTE_E2E_ROOT_PATH ||
"/mnt/Data1T/Mnote_data/users/mnote-e2e/workspaces/my-space",
).trim();
const rootUri = String(
env.MNOTE_PAGE_AI_PI_WARMUP_ROOT_URI ||
env.MNOTE_E2E_ROOT_URI ||
rootUriFromPath(fs.existsSync(defaultRootPath) ? defaultRootPath : rootDir),
).trim();
const baseUrl = String(
env.MNOTE_PAGE_AI_PI_WARMUP_BASE_URL ||
env.MNOTE_UI_BASE_URL ||
env.MNOTE_WEB_PUBLIC_URL ||
"http://127.0.0.1:3000",
).replace(/\/+$/, "");
return {
enabled: isEnabledEnv(env.MNOTE_PAGE_AI_PI_WARMUP),
sendEnabled: isEnabledEnv(env.MNOTE_PAGE_AI_PI_WARMUP_SEND),
baseUrl,
actorId,
auth: String(env.MNOTE_PAGE_AI_PI_WARMUP_AUTH || "Bearer pi-lab-smoke").trim(),
sessionId: String(
env.MNOTE_PAGE_AI_PI_WARMUP_SESSION_ID || `pi_lab_dev_warm_${actorSegment(actorId)}`,
).trim(),
rootUri,
workspaceId: String(
env.MNOTE_PAGE_AI_PI_WARMUP_WORKSPACE_ID ||
env.MNOTE_E2E_WORKSPACE_ID ||
`local-ws:${actorId}:my-space`,
).trim(),
pagePath: String(env.MNOTE_PAGE_AI_PI_WARMUP_PAGE_PATH || "pi-lab-warmup.md").trim(),
pageTitle: String(env.MNOTE_PAGE_AI_PI_WARMUP_PAGE_TITLE || "Pi Lab dev warmup").trim(),
modelProvider: String(
env.MNOTE_PAGE_AI_PI_WARMUP_MODEL_PROVIDER || env.MNOTE_PAGE_AI_PI_DEFAULT_MODEL_PROVIDER || "",
).trim(),
modelId: String(
env.MNOTE_PAGE_AI_PI_WARMUP_MODEL_ID ||
env.MNOTE_PAGE_AI_PI_DEFAULT_MODEL ||
env.MNOTE_PAGE_AI_PI_DEFAULT_MODEL_ID ||
"",
).trim(),
thinkingLevel: String(
env.MNOTE_PAGE_AI_PI_WARMUP_THINKING ||
env.MNOTE_PAGE_AI_PI_THINKING ||
"medium",
).trim(),
permissionMode: String(env.MNOTE_PAGE_AI_PI_WARMUP_PERMISSION_MODE || "plan").trim(),
prompt: String(env.MNOTE_PAGE_AI_PI_WARMUP_PROMPT || "只回复 OK,不要解释。").trim(),
readyTimeoutMs: Number.parseInt(env.MNOTE_PAGE_AI_PI_WARMUP_READY_TIMEOUT_MS || "180000", 10),
requestTimeoutMs: Number.parseInt(env.MNOTE_PAGE_AI_PI_WARMUP_REQUEST_TIMEOUT_MS || "30000", 10),
sendTimeoutMs: Number.parseInt(env.MNOTE_PAGE_AI_PI_WARMUP_SEND_TIMEOUT_MS || "120000", 10),
};
}
function warmupHeaders(plan) {
return {
"Content-Type": "application/json",
Accept: "application/json",
Authorization: plan.auth,
"x-mnote-actor-id": plan.actorId,
};
}
async function sleep(ms) {
await new Promise((resolve) => setTimeout(resolve, ms));
}
async function fetchJson(url, options, timeoutMs) {
const controller = new AbortController();
const timer = setTimeout(() => controller.abort(), timeoutMs);
try {
const response = await fetch(url, { ...options, signal: controller.signal });
const text = await response.text();
let body = {};
try {
body = text ? JSON.parse(text) : {};
} catch {
body = { raw: text };
}
return { response, body };
} finally {
clearTimeout(timer);
}
}
async function waitForStatus(plan, log) {
const startedAt = Date.now();
const statusUrl = `${plan.baseUrl}/api/page-ai/pi/status`;
let lastError = "";
while (Date.now() - startedAt <= plan.readyTimeoutMs) {
try {
const result = await fetchJson(statusUrl, {
method: "GET",
headers: warmupHeaders(plan),
}, Math.min(plan.requestTimeoutMs, 5000));
if (result.response.ok) {
return result.body;
}
lastError = `HTTP ${result.response.status}`;
} catch (error) {
lastError = error && error.message ? error.message : String(error);
}
await sleep(1000);
}
throw new Error(`等待 Pi Lab status 超时:${lastError || statusUrl}`);
}
async function waitForWarmupReply(plan, log) {
const startedAt = Date.now();
const eventsUrl = `${plan.baseUrl}/api/page-ai/pi/sessions/${encodeURIComponent(plan.sessionId)}/events?limit=200`;
while (Date.now() - startedAt <= plan.sendTimeoutMs) {
const result = await fetchJson(eventsUrl, {
method: "GET",
headers: warmupHeaders(plan),
}, Math.min(plan.requestTimeoutMs, 5000));
if (result.response.ok) {
const events = Array.isArray(result.body.events) ? result.body.events : [];
const terminal = events.some((event) => {
if (!event || event.eventType !== "pi_rpc_event") return false;
const payload = event.payload || {};
const type = payload.type || (payload.assistantMessageEvent && payload.assistantMessageEvent.type);
return type === "agent_end" || type === "message_end" || type === "done" || type === "error";
});
const firstText = events.some((event) => {
const payload = event && event.payload ? event.payload : {};
const msg = payload.assistantMessageEvent || payload;
return msg.type === "text_delta" || msg.type === "text_start" || payload.type === "message";
});
if (terminal || firstText) return { terminal, firstText };
}
await sleep(1000);
}
log("warmup prompt 已发送,但等待首个模型事件超时;运行时仍保持可复用。");
return { terminal: false, firstText: false, timeout: true };
}
async function runPiLabWarmup(env = process.env, options = {}) {
const rootDir = options.rootDir || path.resolve(__dirname, "../..");
const log = options.log || (() => undefined);
const plan = resolvePiLabWarmupPlan(env, rootDir);
if (!plan.enabled) return { ok: true, skipped: true, reason: "disabled" };
const startedAt = Date.now();
log(`等待 Pi Lab status${plan.baseUrl}`);
const status = await waitForStatus(plan, log);
if (status.enabled === false) {
log("Pi Lab 未启用,跳过 warmup。");
return { ok: true, skipped: true, reason: "disabled_by_backend" };
}
const startBody = {
sessionId: plan.sessionId,
rootUri: plan.rootUri,
workspaceId: plan.workspaceId,
pagePath: plan.pagePath,
pageTitle: plan.pageTitle,
thinkingLevel: plan.thinkingLevel,
permissionMode: plan.permissionMode,
};
if (plan.modelProvider) startBody.modelProvider = plan.modelProvider;
if (plan.modelId) startBody.modelId = plan.modelId;
log(`启动 Pi Lab warm session${plan.sessionId}`);
const start = await fetchJson(`${plan.baseUrl}/api/page-ai/pi/start`, {
method: "POST",
headers: warmupHeaders(plan),
body: JSON.stringify(startBody),
}, plan.requestTimeoutMs);
if (!start.response.ok || start.body.ok !== true) {
throw new Error(`Pi Lab warmup start 失败:HTTP ${start.response.status} ${JSON.stringify(start.body).slice(0, 500)}`);
}
if (plan.sendEnabled) {
log("发送 Pi Lab warmup prompt;这会产生一次真实模型请求。");
const send = await fetchJson(`${plan.baseUrl}/api/page-ai/pi/send`, {
method: "POST",
headers: warmupHeaders(plan),
body: JSON.stringify({
sessionId: plan.sessionId,
message: plan.prompt,
}),
}, plan.requestTimeoutMs);
if (!send.response.ok || send.body.accepted !== true) {
throw new Error(`Pi Lab warmup send 失败:HTTP ${send.response.status} ${JSON.stringify(send.body).slice(0, 500)}`);
}
await waitForWarmupReply(plan, log);
} else {
log("已预启动 Pi runtime / MCP cache;未发送模型请求。");
}
return {
ok: true,
skipped: false,
sessionId: plan.sessionId,
sendEnabled: plan.sendEnabled,
elapsedMs: Date.now() - startedAt,
};
}
if (require.main === module) {
runPiLabWarmup(process.env, {
rootDir: path.resolve(__dirname, "../.."),
log: (message) => console.log(`[pi-warmup] ${message}`),
}).then((result) => {
if (!result.skipped) {
console.log(`[pi-warmup] 完成:session=${result.sessionId} elapsedMs=${result.elapsedMs}`);
}
}).catch((error) => {
console.error(`[pi-warmup] 失败:${error.message}`);
process.exit(1);
});
}
module.exports = {
isEnabledEnv,
resolvePiLabWarmupPlan,
runPiLabWarmup,
};