feat: integrate pi rust lab runtime
This commit is contained in:
@@ -33,20 +33,20 @@ async function setupWorkspaceAccess(requestContext, baseUrl, options) {
|
||||
[
|
||||
{
|
||||
kind: "setupWorkspace",
|
||||
userId: actorId,
|
||||
user_id: actorId,
|
||||
email: options.email || `${actorId}@example.com`,
|
||||
username: options.username || actorId,
|
||||
displayName: options.displayName || actorId,
|
||||
display_name: options.displayName || actorId,
|
||||
role: options.role || "user",
|
||||
workspaceId: options.workspaceId,
|
||||
workspaceName: options.workspaceName || "MNote Smoke Workspace",
|
||||
rootUri,
|
||||
rootPath,
|
||||
sourceKind: options.sourceKind || "local_folder",
|
||||
workspace_id: options.workspaceId,
|
||||
workspace_name: options.workspaceName || "MNote Smoke Workspace",
|
||||
root_uri: rootUri,
|
||||
root_path: rootPath,
|
||||
source_kind: options.sourceKind || "local_folder",
|
||||
permission: options.permission || "write",
|
||||
capabilities: options.capabilities || ["ai"],
|
||||
grantSource: options.grantSource || "smoke",
|
||||
grantCreatedBy: options.grantCreatedBy || actorId,
|
||||
grant_source: options.grantSource || "smoke",
|
||||
grant_created_by: options.grantCreatedBy || actorId,
|
||||
},
|
||||
],
|
||||
options.timeoutMs,
|
||||
@@ -61,19 +61,42 @@ async function seedAiRuntime(requestContext, baseUrl, options) {
|
||||
{
|
||||
kind: "seedAiRuntime",
|
||||
id: options.id,
|
||||
userId: options.userId,
|
||||
workspaceId: options.workspaceId,
|
||||
documentId: options.documentId,
|
||||
sessionId: options.sessionId,
|
||||
runId: options.runId,
|
||||
user_id: options.userId,
|
||||
workspace_id: options.workspaceId,
|
||||
document_id: options.documentId,
|
||||
session_id: options.sessionId,
|
||||
run_id: options.runId,
|
||||
title: options.title,
|
||||
profile: options.profile || "reasonix",
|
||||
acpRuntime: options.acpRuntime || options.profile || "reasonix",
|
||||
traceId: options.traceId,
|
||||
acp_runtime: options.acpRuntime || options.profile || "reasonix",
|
||||
trace_id: options.traceId,
|
||||
status: options.status || "running",
|
||||
runtimeJson: options.runtimeJson || {},
|
||||
payloadJson: options.payloadJson || {},
|
||||
events: options.events || [],
|
||||
runtime_json: options.runtimeJson || {},
|
||||
payload_json: options.payloadJson || {},
|
||||
events: (options.events || []).map((event) => ({
|
||||
id: event.id,
|
||||
event_type: event.event_type || event.eventType,
|
||||
payload_json: event.payload_json || event.payloadJson || {},
|
||||
})),
|
||||
},
|
||||
],
|
||||
options.timeoutMs,
|
||||
);
|
||||
}
|
||||
|
||||
async function seedAiPolicy(requestContext, baseUrl, options) {
|
||||
return postDevSeed(
|
||||
requestContext,
|
||||
baseUrl,
|
||||
[
|
||||
{
|
||||
kind: "seedAiPolicy",
|
||||
id: options.id,
|
||||
user_id: options.userId,
|
||||
workspace_id: options.workspaceId,
|
||||
allowed_roots_json: options.allowedRootsJson || [],
|
||||
model_policy_json: options.modelPolicyJson || {},
|
||||
quota_json: options.quotaJson || {},
|
||||
},
|
||||
],
|
||||
options.timeoutMs,
|
||||
@@ -87,8 +110,8 @@ async function clearRuntimeEvents(requestContext, baseUrl, options) {
|
||||
[
|
||||
{
|
||||
kind: "clearRuntimeEvents",
|
||||
userId: options.userId,
|
||||
runId: options.runId,
|
||||
user_id: options.userId,
|
||||
run_id: options.runId,
|
||||
},
|
||||
],
|
||||
options.timeoutMs,
|
||||
@@ -102,8 +125,8 @@ async function getAiRuntimeRun(requestContext, baseUrl, options) {
|
||||
[
|
||||
{
|
||||
kind: "getAiRuntimeRun",
|
||||
userId: options.userId,
|
||||
runId: options.runId,
|
||||
user_id: options.userId,
|
||||
run_id: options.runId,
|
||||
},
|
||||
],
|
||||
options.timeoutMs,
|
||||
@@ -118,10 +141,10 @@ async function listAiRuntimeRuns(requestContext, baseUrl, options) {
|
||||
[
|
||||
{
|
||||
kind: "listAiRuntimeRuns",
|
||||
userId: options.userId,
|
||||
workspaceId: options.workspaceId,
|
||||
documentId: options.documentId,
|
||||
sessionId: options.sessionId,
|
||||
user_id: options.userId,
|
||||
workspace_id: options.workspaceId,
|
||||
document_id: options.documentId,
|
||||
session_id: options.sessionId,
|
||||
limit: options.limit,
|
||||
},
|
||||
],
|
||||
@@ -137,10 +160,10 @@ async function listAiRuntimeEvents(requestContext, baseUrl, options) {
|
||||
[
|
||||
{
|
||||
kind: "listAiRuntimeEvents",
|
||||
userId: options.userId,
|
||||
runId: options.runId,
|
||||
user_id: options.userId,
|
||||
run_id: options.runId,
|
||||
limit: options.limit,
|
||||
eventType: options.eventType,
|
||||
event_type: options.eventType,
|
||||
},
|
||||
],
|
||||
options.timeoutMs,
|
||||
@@ -155,9 +178,9 @@ async function countAiRuntimeEvents(requestContext, baseUrl, options) {
|
||||
[
|
||||
{
|
||||
kind: "countAiRuntimeEvents",
|
||||
userId: options.userId,
|
||||
runId: options.runId,
|
||||
eventType: options.eventType,
|
||||
user_id: options.userId,
|
||||
run_id: options.runId,
|
||||
event_type: options.eventType,
|
||||
},
|
||||
],
|
||||
options.timeoutMs,
|
||||
@@ -172,9 +195,9 @@ async function findExternalConversationBinding(requestContext, baseUrl, options)
|
||||
[
|
||||
{
|
||||
kind: "findExternalConversationBinding",
|
||||
userId: options.userId,
|
||||
workspaceId: options.workspaceId,
|
||||
mnoteSessionId: options.mnoteSessionId,
|
||||
user_id: options.userId,
|
||||
workspace_id: options.workspaceId,
|
||||
mnote_session_id: options.mnoteSessionId,
|
||||
provider: options.provider,
|
||||
},
|
||||
],
|
||||
@@ -190,9 +213,9 @@ async function listExternalConversationBindings(requestContext, baseUrl, options
|
||||
[
|
||||
{
|
||||
kind: "listExternalConversationBindings",
|
||||
userId: options.userId,
|
||||
workspaceId: options.workspaceId,
|
||||
mnoteSessionId: options.mnoteSessionId,
|
||||
user_id: options.userId,
|
||||
workspace_id: options.workspaceId,
|
||||
mnote_session_id: options.mnoteSessionId,
|
||||
limit: options.limit,
|
||||
},
|
||||
],
|
||||
@@ -205,6 +228,7 @@ module.exports = {
|
||||
postDevSeed,
|
||||
setupWorkspaceAccess,
|
||||
seedAiRuntime,
|
||||
seedAiPolicy,
|
||||
clearRuntimeEvents,
|
||||
getAiRuntimeRun,
|
||||
listAiRuntimeRuns,
|
||||
|
||||
@@ -0,0 +1,241 @@
|
||||
"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,
|
||||
};
|
||||
Reference in New Issue
Block a user