feat: integrate pi rust lab runtime

This commit is contained in:
Agent Board
2026-07-10 10:54:34 +08:00
parent c8472bb898
commit 896c94b696
73 changed files with 19852 additions and 1152 deletions
+56 -32
View File
@@ -38,6 +38,21 @@ function pathMatchesPage(value, expectedPagePath) {
return normalized === expectedPagePath || normalized.endsWith(`/${expectedPagePath}`);
}
function readSessionEvidence(sessionDir) {
const pending = [sessionDir];
const files = [];
while (pending.length) {
const current = pending.pop();
if (!current || !fs.existsSync(current)) continue;
for (const entry of fs.readdirSync(current, { withFileTypes: true })) {
const target = path.join(current, entry.name);
if (entry.isDirectory()) pending.push(target);
else if (entry.isFile() && entry.name.endsWith(".jsonl")) files.push(target);
}
}
return files.sort().map((file) => fs.readFileSync(file, "utf8")).join("\n");
}
async function addAuth(context) {
const headers = authHeaders();
if (headers.Authorization) await context.setExtraHTTPHeaders({ Authorization: headers.Authorization });
@@ -135,25 +150,13 @@ async function main() {
await page.locator('[data-page-ai-pi-lab="drawer"]').waitFor({ state: "visible", timeout: UI_TIMEOUT_MS });
console.log(" ✅ independent Pi Lab launcher and drawer visible");
const startButton = page.locator("[data-page-ai-pi-lab-btn-start]");
let startJson = null;
if (await startButton.isVisible({ timeout: 2000 }).catch(() => false)) {
const startRespPromise = page.waitForResponse((res) => res.url().includes("/api/page-ai/pi/start") && res.request().method() === "POST", {
timeout: UI_TIMEOUT_MS,
});
await startButton.click();
const startResp = await startRespPromise;
assert(startResp.ok(), `start response failed: ${startResp.status()}`);
startJson = await startResp.json();
assert(pathMatchesPage(startJson.session?.pagePath, pagePath), `start response should bind pagePath=${pagePath}, got ${startJson.session?.pagePath}`);
}
await page.waitForFunction(() => {
const text = document.querySelector("[data-page-ai-pi-lab-status-text]")?.textContent || "";
return text.includes("ready");
}, null, { timeout: UI_TIMEOUT_MS });
const statusAfterStart = await page.request.get(`${BASE}/api/page-ai/pi/status`, { headers: authHeaders() });
const statusAfterStartJson = await statusAfterStart.json();
const activeSession = startJson?.session || statusAfterStartJson.session || {};
const activeSession = statusAfterStartJson.session || {};
const sessionId = activeSession.sessionId || statusAfterStartJson.sessionId;
assert(sessionId, "start should create sessionId");
assert(activeSession.runtimePid || statusAfterStartJson.pid, "RPC start should expose runtime pid");
@@ -191,25 +194,34 @@ async function main() {
}
}
});
await page.waitForFunction(() => {
const selectionDetected = await page.waitForFunction(() => {
const text = document.querySelector("[data-page-ai-pi-lab-selection]")?.textContent || "";
return text.includes("已选中");
}, null, { timeout: UI_TIMEOUT_MS });
await page.locator('.wolai-page-ai-pi-lab-composer-bar [data-page-ai-pi-lab-quick="selection"]').click();
await page.waitForFunction(() => {
const input = document.querySelector("[data-page-ai-pi-lab-input]");
return (input?.value || "").includes("Browser RPC Original");
}, null, { timeout: UI_TIMEOUT_MS });
console.log(" ✅ selection quick action injects live tiptap selection into composer");
await page.locator('.wolai-page-ai-pi-lab-composer-bar [data-page-ai-pi-lab-quick="read-page"]').click();
await page.waitForFunction(() => {
const input = document.querySelector("[data-page-ai-pi-lab-input]");
return (input?.value || "").includes("Browser RPC Original");
}, null, { timeout: UI_TIMEOUT_MS });
console.log(" ✅ current page quick action calls mnote.current_page.read");
}, null, { timeout: 5000 }).then(() => true).catch(() => false);
const selectionButton = page.locator('[data-page-ai-pi-lab-quick="selection"]');
if (selectionDetected && await selectionButton.isVisible({ timeout: 2000 }).catch(() => false)) {
await selectionButton.click();
await page.waitForFunction(() => {
return document.querySelector('[data-page-ai-pi-lab-quick="selection"]')?.getAttribute("aria-pressed") === "true";
}, null, { timeout: UI_TIMEOUT_MS });
console.log(" ✅ selection quick action enables live tiptap selection context");
}
}
const currentPageButton = page.locator('[data-page-ai-pi-lab-quick="read-page"]');
await currentPageButton.waitFor({ state: "visible", timeout: UI_TIMEOUT_MS });
await currentPageButton.click();
await page.waitForFunction(() => {
return document.querySelector('[data-page-ai-pi-lab-quick="read-page"]')?.getAttribute("aria-pressed") === "true";
}, null, { timeout: UI_TIMEOUT_MS });
const currentPageMenuAction = page.locator('[data-page-ai-pi-lab-menu-action="read-page"]');
assert.equal(
await currentPageMenuAction.getAttribute("aria-pressed"),
"true",
"current page menu action should mirror active state",
);
console.log(" ✅ clicked 使用当前页 and enabled current-page context");
const denyResp = await page.request.post(`${BASE}/api/page-ai/pi/tool-call`, {
headers: authHeaders(),
data: { sessionId, toolName: "mnote.local_file.read", params: { path: "/etc/hosts" } },
@@ -248,18 +260,30 @@ async function main() {
return text.includes("denied") && text.includes("diff");
}, null, { timeout: UI_TIMEOUT_MS });
await page.locator("[data-page-ai-pi-lab-input]").fill(`请只回复 ${MARKER},不要解释。`);
await page.locator("[data-page-ai-pi-lab-input]").fill(
`必须调用 mnote_current_page_read。工具结果 content 包含 "Browser RPC Patched" 后,只回复 ${MARKER},不要解释。`,
);
await page.locator("[data-page-ai-pi-lab-btn-send]").click();
const assistantMarker = page
.locator('[data-page-ai-pi-lab-message-role="assistant"] .wolai-page-ai-pi-lab-text')
.locator(
'[data-page-ai-pi-lab-message-role="assistant"] .wolai-page-ai-pi-lab-text, '
+ '[data-page-ai-pi-lab-message-role="assistant"] .wolai-page-ai-pi-lab-markdown',
)
.filter({ hasText: MARKER })
.last();
await assistantMarker.waitFor({ state: "visible", timeout: UI_TIMEOUT_MS });
const visibleText = await assistantMarker.textContent();
assert(visibleText.includes(MARKER), "visible assistant bubble should contain marker");
assert.equal(visibleText.trim(), MARKER, "visible assistant bubble should equal marker");
assert(!visibleText.includes("thinking_delta"), "provider thinking event name must not be visible");
assert(!visibleText.includes("我们被问到"), "provider reasoning text must not leak into final visible reply");
console.log(" ✅ real Pi RPC stream rendered in UI without visible reasoning leakage");
const sessionEvidence = readSessionEvidence(activeSession.piSessionDir);
assert(sessionEvidence.includes('"toolName":"mnote_current_page_read"'), "session should record mnote_current_page_read");
assert(sessionEvidence.includes('"transport":"pi-rust-native-fs"'), "session should record Pi Rust native fs transport");
const toolTimelines = await page.locator("[data-page-ai-pi-lab-tool-timeline], [data-page-ai-pi-tool-timeline]").all();
for (const timeline of toolTimelines) {
await timeline.evaluate((node) => { node.open = true; }).catch(() => {});
}
console.log(" ✅ real browser conversation used mnote_current_page_read via pi-rust-native-fs");
fs.mkdirSync(path.dirname(SCREENSHOT), { recursive: true });
await page.screenshot({ path: SCREENSHOT, fullPage: false });