- 归档 OnlyOffice live bridge、Page AI、mindmap、design governance 与相关 bug 条目 - 补齐 MinerU OCR 后端 runtime 合同与 smoke/test 基线 - 收口 ChatOnly/Doubao、ObjectIdentity、Page Aggregate compat 与 runtime owner 文档口径 验证: - cargo test --manifest-path rust/Cargo.toml -p mnote-web local_ocr -- --test-threads=1 - cargo test --manifest-path rust/Cargo.toml -p mnote-web onlyoffice_bridge -- --test-threads=1 - git diff --check - git diff --cached --check - codegraph index . --force && codegraph status . - codegraph sync . && codegraph status .
513 lines
20 KiB
JavaScript
513 lines
20 KiB
JavaScript
#!/usr/bin/env node
|
|
"use strict";
|
|
|
|
const assert = require("node:assert");
|
|
const fs = require("node:fs");
|
|
const os = require("node:os");
|
|
const path = require("node:path");
|
|
const { execFileSync } = require("node:child_process");
|
|
const { chromium } = require("playwright");
|
|
const {
|
|
BASE_URL,
|
|
UI_TIMEOUT_MS,
|
|
ensureAuthenticated,
|
|
} = require("./tree-shell-smoke-helpers");
|
|
|
|
const PROVIDERS = {
|
|
deepseek: {
|
|
taskName: "task513-chatonly-deepseek-sync-smoke",
|
|
title: "DeepSeek ChatOnly Sync",
|
|
agentProfileId: "shared_deepseek_chat",
|
|
chipText: "ChatOnly / DeepSeek",
|
|
expectedProvider: "deepseek-web",
|
|
gatewayLog: "/home/lix/.openclaw-mnote-deepseek-chat/logs/gateway.out",
|
|
sendLogPattern: /\[DeepSeekWebClient\] Sending chat completion request/g,
|
|
configPath: "/home/lix/.openclaw-mnote-deepseek-chat/openclaw.json",
|
|
},
|
|
gemini: {
|
|
taskName: "task513-chatonly-gemini-sync-smoke",
|
|
title: "Gemini ChatOnly Sync",
|
|
agentProfileId: "shared_gemini_chat",
|
|
chipText: "ChatOnly / Gemini",
|
|
expectedProvider: "gemini-web",
|
|
gatewayLog: "/home/lix/.openclaw-mnote-gemini-chat/logs/gateway.out",
|
|
sendLogPattern: /\[Gemini Web Browser\] DOM: typed message and pressed Enter/g,
|
|
cdpUrl: process.env.MNOTE_GEMINI_CDP_URL || "http://127.0.0.1:9232",
|
|
},
|
|
};
|
|
|
|
const providerKey = process.argv[2] || process.env.MNOTE_CHATONLY_PROVIDER || "deepseek";
|
|
const provider = PROVIDERS[providerKey];
|
|
if (!provider) {
|
|
throw new Error(`未知 provider: ${providerKey}`);
|
|
}
|
|
|
|
const OUTPUT_DIR = path.join(process.cwd(), "tmp", provider.taskName);
|
|
const RESULT_PATH = path.join(OUTPUT_DIR, "result.json");
|
|
const CONTROL_PLANE_DB = "/mnt/Data1T/Mnote_data/control-plane/control-plane.db";
|
|
const CHROMIUM_EXECUTABLE_PATH =
|
|
process.env.PLAYWRIGHT_CHROMIUM_EXECUTABLE_PATH ||
|
|
[
|
|
"/usr/bin/google-chrome-stable",
|
|
"/usr/bin/google-chrome",
|
|
"/snap/bin/chromium",
|
|
"/usr/bin/chromium",
|
|
].find((candidate) => fs.existsSync(candidate));
|
|
|
|
function fileUrl(localPath) {
|
|
return `file://${localPath}`;
|
|
}
|
|
|
|
function localMdDocumentId(relativePath) {
|
|
return `local-md:${relativePath.replaceAll("/", "~2F")}`;
|
|
}
|
|
|
|
function documentUrl(root, relativePath) {
|
|
const url = new URL(`${BASE_URL}/documents/${encodeURIComponent(localMdDocumentId(relativePath))}`);
|
|
url.searchParams.set("sourceKind", "local_folder");
|
|
url.searchParams.set("rootUri", fileUrl(root));
|
|
url.searchParams.set("treeView", "filetree");
|
|
return url.toString();
|
|
}
|
|
|
|
function writeWorkspaceManifest(root, ownerId, workspaceId) {
|
|
fs.mkdirSync(path.join(root, ".mnote"), { recursive: true });
|
|
fs.writeFileSync(
|
|
path.join(root, ".mnote", "workspace.json"),
|
|
`${JSON.stringify({
|
|
workspaceId,
|
|
ownerId,
|
|
createdAt: new Date().toISOString(),
|
|
capabilities: ["local_files", "ai_sessions", "markdown_edit"],
|
|
}, null, 2)}\n`,
|
|
"utf8",
|
|
);
|
|
}
|
|
|
|
function sqliteJson(sql, fallback = null) {
|
|
try {
|
|
const raw = execFileSync("sqlite3", ["-json", CONTROL_PLANE_DB, sql], {
|
|
encoding: "utf8",
|
|
stdio: ["ignore", "pipe", "pipe"],
|
|
}).trim();
|
|
return raw ? JSON.parse(raw) : fallback;
|
|
} catch {
|
|
return fallback;
|
|
}
|
|
}
|
|
|
|
function sqlQuote(value) {
|
|
return `'${String(value).replaceAll("'", "''")}'`;
|
|
}
|
|
|
|
function sqliteExec(sql) {
|
|
execFileSync("sqlite3", [CONTROL_PLANE_DB, sql], {
|
|
encoding: "utf8",
|
|
stdio: ["ignore", "pipe", "pipe"],
|
|
});
|
|
}
|
|
|
|
function grantWorkspaceAccess({ actorId, workspaceId, root, rootUri, grantId }) {
|
|
const now = new Date().toISOString();
|
|
sqliteExec(`
|
|
INSERT OR IGNORE INTO users (id, email, username, display_name, role, status, created_at, updated_at, revision)
|
|
VALUES (${sqlQuote(actorId)}, ${sqlQuote(`${actorId}@example.com`)}, ${sqlQuote(actorId)}, ${sqlQuote(actorId)}, 'user', 'active', ${sqlQuote(now)}, ${sqlQuote(now)}, 1);
|
|
INSERT OR REPLACE INTO workspaces (id, owner_user_id, name, kind, root_uri, root_path, source_kind, status, created_at, updated_at, revision)
|
|
VALUES (${sqlQuote(workspaceId)}, ${sqlQuote(actorId)}, ${sqlQuote(actorId)}, 'personal', ${sqlQuote(rootUri)}, ${sqlQuote(root)}, 'local_folder', 'active', ${sqlQuote(now)}, ${sqlQuote(now)}, 1);
|
|
INSERT OR REPLACE INTO directory_grants (id, user_id, workspace_id, root_uri, root_path, permission, recursive, capabilities_json, source, status, created_by, created_at, updated_at, revision)
|
|
VALUES (${sqlQuote(grantId)}, ${sqlQuote(actorId)}, ${sqlQuote(workspaceId)}, ${sqlQuote(rootUri)}, ${sqlQuote(root)}, 'write', 1, '["ai","markdown_edit"]', 'smoke', 'active', ${sqlQuote(actorId)}, ${sqlQuote(now)}, ${sqlQuote(now)}, 1);
|
|
`);
|
|
}
|
|
|
|
function logSize(logPath) {
|
|
try {
|
|
return fs.statSync(logPath).size;
|
|
} catch {
|
|
return 0;
|
|
}
|
|
}
|
|
|
|
function readLogSince(logPath, offset) {
|
|
try {
|
|
const fd = fs.openSync(logPath, "r");
|
|
const stat = fs.fstatSync(fd);
|
|
const start = offset > stat.size ? 0 : offset;
|
|
const buffer = Buffer.alloc(stat.size - start);
|
|
fs.readSync(fd, buffer, 0, buffer.length, start);
|
|
fs.closeSync(fd);
|
|
return buffer.toString("utf8");
|
|
} catch {
|
|
return "";
|
|
}
|
|
}
|
|
|
|
function countMatches(text, pattern) {
|
|
return Array.from(String(text || "").matchAll(pattern)).length;
|
|
}
|
|
|
|
function escapeRegExp(value) {
|
|
return String(value).replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
}
|
|
|
|
function parseCookieString(cookieString, domain) {
|
|
return String(cookieString || "")
|
|
.split(";")
|
|
.filter((cookie) => cookie.trim().includes("="))
|
|
.map((cookie) => {
|
|
const [name, ...valueParts] = cookie.trim().split("=");
|
|
return {
|
|
name: name.trim(),
|
|
value: valueParts.join("=").trim(),
|
|
domain,
|
|
path: "/",
|
|
};
|
|
})
|
|
.filter((cookie) => cookie.name);
|
|
}
|
|
|
|
function deepseekAuth() {
|
|
const config = JSON.parse(fs.readFileSync(provider.configPath, "utf8"));
|
|
return JSON.parse(config.models.providers["deepseek-web"].apiKey || "{}");
|
|
}
|
|
|
|
async function saveScreenshot(page, name) {
|
|
const target = path.join(OUTPUT_DIR, `${name}.png`);
|
|
await page.screenshot({ path: target, fullPage: false });
|
|
return target;
|
|
}
|
|
|
|
async function captureDeepseek(remoteConversationId, name) {
|
|
const auth = deepseekAuth();
|
|
const browser = await chromium.launch({
|
|
headless: true,
|
|
...(CHROMIUM_EXECUTABLE_PATH ? { executablePath: CHROMIUM_EXECUTABLE_PATH } : {}),
|
|
});
|
|
const context = await browser.newContext({
|
|
viewport: { width: 1440, height: 960 },
|
|
locale: "zh-CN",
|
|
userAgent: auth.userAgent,
|
|
});
|
|
await context.addCookies(parseCookieString(auth.cookie, ".deepseek.com"));
|
|
if (auth.bearer) {
|
|
await context.addInitScript((token) => {
|
|
localStorage.setItem("userToken", JSON.stringify({ value: token, __version: "0" }));
|
|
}, auth.bearer);
|
|
}
|
|
const page = await context.newPage();
|
|
if (auth.bearer) {
|
|
await page.route("https://chat.deepseek.com/api/**", (route) => {
|
|
route.continue({
|
|
headers: {
|
|
...route.request().headers(),
|
|
authorization: `Bearer ${auth.bearer}`,
|
|
},
|
|
});
|
|
});
|
|
}
|
|
const url = `https://chat.deepseek.com/a/chat/s/${remoteConversationId}`;
|
|
await page.goto(url, { waitUntil: "domcontentloaded", timeout: 60_000 }).catch(() => {});
|
|
await page.waitForTimeout(2500);
|
|
const screenshot = await saveScreenshot(page, name);
|
|
const title = await page.title().catch(() => "");
|
|
const visibleText = await page.locator("body").innerText({ timeout: 5000 }).catch(() => "");
|
|
const conversationLinkCount = await page
|
|
.locator(`a[href*="/a/chat/s/${remoteConversationId}"]`)
|
|
.count()
|
|
.catch(() => -1);
|
|
await browser.close().catch(() => {});
|
|
return {
|
|
screenshot,
|
|
url,
|
|
title,
|
|
conversationLinkCount,
|
|
conversationMissingText: visibleText.includes("该对话不存在"),
|
|
visibleText: visibleText.slice(0, 1000),
|
|
};
|
|
}
|
|
|
|
async function firstGeminiPage(cdpBrowser, remoteConversationId) {
|
|
const targetUrl = String(remoteConversationId || "");
|
|
for (const context of cdpBrowser.contexts()) {
|
|
const exact = context.pages().find((candidate) => candidate.url().split("#")[0] === targetUrl);
|
|
if (exact) return exact;
|
|
const gemini = context.pages().find((candidate) => candidate.url().includes("gemini.google.com"));
|
|
if (gemini) return gemini;
|
|
}
|
|
const context = cdpBrowser.contexts()[0] || await cdpBrowser.newContext();
|
|
return await context.newPage();
|
|
}
|
|
|
|
async function captureGemini(remoteConversationId, name) {
|
|
const browser = await chromium.connectOverCDP(provider.cdpUrl);
|
|
const page = await firstGeminiPage(browser, remoteConversationId);
|
|
const targetUrl = String(remoteConversationId || "");
|
|
if (targetUrl && page.url().split("#")[0] !== targetUrl) {
|
|
await page.goto(targetUrl, { waitUntil: "domcontentloaded", timeout: 60_000 }).catch(() => {});
|
|
await page.waitForTimeout(1500);
|
|
}
|
|
await page.bringToFront().catch(() => {});
|
|
await page.waitForTimeout(1000);
|
|
const screenshot = await saveScreenshot(page, name);
|
|
const title = await page.title().catch(() => "");
|
|
const visibleText = await page.locator("body").innerText({ timeout: 5000 }).catch(() => "");
|
|
const conversationLinkCount = await page.evaluate((target) => {
|
|
let targetPath = "";
|
|
try {
|
|
targetPath = new URL(target).pathname;
|
|
} catch {
|
|
return -1;
|
|
}
|
|
return Array.from(document.querySelectorAll("a[href]")).filter((anchor) => {
|
|
try {
|
|
return new URL(anchor.href, location.href).pathname === targetPath;
|
|
} catch {
|
|
return false;
|
|
}
|
|
}).length;
|
|
}, targetUrl).catch(() => -1);
|
|
await browser.close().catch(() => {});
|
|
return {
|
|
screenshot,
|
|
url: page.url(),
|
|
title,
|
|
conversationLinkCount,
|
|
visibleText: visibleText.slice(0, 1000),
|
|
};
|
|
}
|
|
|
|
async function captureProvider(remoteConversationId, name) {
|
|
if (providerKey === "deepseek") return await captureDeepseek(remoteConversationId, name);
|
|
return await captureGemini(remoteConversationId, name);
|
|
}
|
|
|
|
async function main() {
|
|
fs.mkdirSync(OUTPUT_DIR, { recursive: true });
|
|
const suffix = Date.now().toString(36);
|
|
const actorId = "mnote-e2e";
|
|
const root = fs.mkdtempSync(path.join(os.tmpdir(), `mnote-task513-${providerKey}-`));
|
|
const rootUri = fileUrl(root);
|
|
const workspaceId = `local-ws:${actorId}:task513-${providerKey}-${suffix}`;
|
|
const relativePath = `${providerKey}-ChatOnlySync.md`;
|
|
const marker = `MNOTE_CHATONLY_SYNC_${suffix}`;
|
|
const screenshots = {};
|
|
const providerCaptures = {};
|
|
const runRequests = [];
|
|
const deleteResponses = [];
|
|
const gatewayLogOffset = logSize(provider.gatewayLog);
|
|
let caughtError = null;
|
|
|
|
writeWorkspaceManifest(root, actorId, workspaceId);
|
|
fs.writeFileSync(path.join(root, relativePath), [`# ${provider.title}`, "", marker, ""].join("\n"), "utf8");
|
|
grantWorkspaceAccess({
|
|
actorId,
|
|
workspaceId,
|
|
root,
|
|
rootUri,
|
|
grantId: `grant_task513_${providerKey}_${suffix}`,
|
|
});
|
|
|
|
const browser = await chromium.launch({
|
|
headless: true,
|
|
...(CHROMIUM_EXECUTABLE_PATH ? { executablePath: CHROMIUM_EXECUTABLE_PATH } : {}),
|
|
});
|
|
const context = await browser.newContext({
|
|
viewport: { width: 1440, height: 960 },
|
|
locale: "zh-CN",
|
|
extraHTTPHeaders: {
|
|
"x-mnote-actor-id": actorId,
|
|
"x-mnote-actor-type": "user",
|
|
},
|
|
});
|
|
const page = await context.newPage();
|
|
|
|
try {
|
|
await page.route("**/api/hermes/client/runs", async (route) => {
|
|
runRequests.push(JSON.parse(route.request().postData() || "{}"));
|
|
await route.continue();
|
|
});
|
|
page.on("response", async (response) => {
|
|
const url = response.url();
|
|
const request = response.request();
|
|
if (request.method() === "DELETE" && url.includes("/api/hermes/client/sessions/")) {
|
|
deleteResponses.push({
|
|
url,
|
|
status: response.status(),
|
|
body: await response.text().catch(() => ""),
|
|
});
|
|
}
|
|
});
|
|
|
|
await ensureAuthenticated(page, context.request);
|
|
const response = await page.goto(documentUrl(root, relativePath), {
|
|
waitUntil: "domcontentloaded",
|
|
timeout: UI_TIMEOUT_MS,
|
|
});
|
|
assert(response && response.status() === 200, `文档页状态码异常: ${response && response.status()}`);
|
|
await page.locator('[data-testid="mnote-leptos-tiptap-island-editor-root"] .ProseMirror').first().waitFor({
|
|
state: "visible",
|
|
timeout: UI_TIMEOUT_MS,
|
|
});
|
|
await page.getByTestId("wolai-floating-ai").click({ timeout: UI_TIMEOUT_MS });
|
|
await page.locator('[data-testid="wolai-page-ai-drawer"]').waitFor({ state: "visible", timeout: UI_TIMEOUT_MS });
|
|
await page.locator('.wolai-page-ai-header-actions [data-page-ai-action="new-session"]').click({
|
|
timeout: UI_TIMEOUT_MS,
|
|
});
|
|
await page.locator("[data-page-ai-agent-button]").click({ timeout: UI_TIMEOUT_MS });
|
|
await page.locator(`[data-page-ai-agent-id="chat_only"][data-page-ai-profile-id="${provider.agentProfileId}"]`).click({
|
|
timeout: UI_TIMEOUT_MS,
|
|
});
|
|
await page.waitForFunction(
|
|
(expected) => document.querySelector("[data-page-ai-agent-chip]")?.textContent?.includes(expected),
|
|
provider.chipText,
|
|
{ timeout: UI_TIMEOUT_MS },
|
|
);
|
|
|
|
await page.locator("[data-page-ai-input]").fill(`请只回复:${marker}`, { timeout: UI_TIMEOUT_MS });
|
|
await page.locator('[data-page-ai-action="send"]').click({ timeout: UI_TIMEOUT_MS });
|
|
await page.waitForFunction(
|
|
(expectedMarker) => {
|
|
const assistantText = Array.from(document.querySelectorAll(".wolai-page-ai-message--assistant"))
|
|
.map((node) => node.textContent || "")
|
|
.join("\n");
|
|
return assistantText.includes(expectedMarker);
|
|
},
|
|
marker,
|
|
{ timeout: 180_000 },
|
|
);
|
|
await page.waitForFunction(
|
|
() => (document.querySelector("[data-page-ai-run-status]")?.textContent || "").includes("完成"),
|
|
null,
|
|
{ timeout: 180_000 },
|
|
).catch(() => {});
|
|
screenshots.afterMessage = await saveScreenshot(page, "after-message");
|
|
|
|
assert.strictEqual(runRequests.length, 1, "MNote 本轮应只创建一个 run");
|
|
const run = runRequests[0];
|
|
assert.strictEqual(run.agentId, "chat_only", "应使用 ChatOnly agent");
|
|
assert.strictEqual(run.profileId, provider.agentProfileId, `应使用 ${provider.title} profile`);
|
|
assert.strictEqual(run.acpRuntime, "hermes", "ChatOnly 网页 provider 应走 Hermes/OpenClaw runtime");
|
|
assert(run.sessionId, "run payload 应包含 MNote sessionId");
|
|
|
|
const visibleAssistantTexts = await page.locator(".wolai-page-ai-message--assistant").allTextContents();
|
|
const markerAssistantCount = visibleAssistantTexts.filter((text) => text.includes(marker)).length;
|
|
assert.strictEqual(markerAssistantCount, 1, "MNote 可见 provider 回复应只有一条");
|
|
|
|
const sessionId = String(run.sessionId);
|
|
const bindingRows = sqliteJson(
|
|
`SELECT mnote_session_id, remote_conversation_id, status FROM ai_external_conversation_bindings WHERE user_id=${sqlQuote(actorId)} AND mnote_session_id=${sqlQuote(sessionId)} AND provider=${sqlQuote(provider.expectedProvider)} ORDER BY updated_at DESC LIMIT 1;`,
|
|
[],
|
|
);
|
|
assert(bindingRows && bindingRows.length === 1, "SQLite 应保存远端会话绑定");
|
|
assert.strictEqual(bindingRows[0].status, "active", "删除前 binding 应为 active");
|
|
const remoteConversationId = bindingRows[0].remote_conversation_id;
|
|
assert(remoteConversationId, "binding 应包含 remote_conversation_id");
|
|
|
|
providerCaptures.afterMessage = await captureProvider(remoteConversationId, `${providerKey}-after-message`);
|
|
|
|
await page.locator('.wolai-page-ai-header-actions [data-page-ai-action="history"]').click({ timeout: UI_TIMEOUT_MS });
|
|
await page.locator('[data-page-ai-panel="history"]').waitFor({ state: "visible", timeout: UI_TIMEOUT_MS });
|
|
await page.locator(`[data-page-ai-session-row="${sessionId}"]`).waitFor({ state: "visible", timeout: UI_TIMEOUT_MS });
|
|
page.once("dialog", async (dialog) => {
|
|
await dialog.accept();
|
|
});
|
|
await page.locator(`[data-page-ai-session-delete="${sessionId}"]`).click({ timeout: UI_TIMEOUT_MS });
|
|
await page.waitForFunction(
|
|
(id) => !document.querySelector(`[data-page-ai-session-row="${CSS.escape(id)}"]`),
|
|
sessionId,
|
|
{ timeout: UI_TIMEOUT_MS },
|
|
);
|
|
screenshots.afterDelete = await saveScreenshot(page, "after-delete");
|
|
|
|
assert(deleteResponses.length >= 1, "MNote 应发出历史会话 DELETE 请求");
|
|
assert.strictEqual(deleteResponses.at(-1).status, 200, "MNote 历史会话 DELETE 应成功");
|
|
const deleteBody = JSON.parse(deleteResponses.at(-1).body || "{}");
|
|
assert.strictEqual(
|
|
deleteBody?.result?.providerConversationDelete?.status,
|
|
"remote_deleted",
|
|
"provider 删除应返回 remote_deleted",
|
|
);
|
|
const providerDeleteMode = deleteBody?.result?.providerConversationDelete?.response?.result?.providerDeleteMode;
|
|
if (providerKey === "deepseek") {
|
|
assert.strictEqual(
|
|
providerDeleteMode,
|
|
"deepseek_chat_session_delete_api",
|
|
"DeepSeek 删除应明确走 chat_session_delete_api 并由网页复核",
|
|
);
|
|
} else {
|
|
assert.strictEqual(
|
|
providerDeleteMode,
|
|
"gemini_conversation_menu",
|
|
"Gemini 删除应走网页对话菜单确认路径",
|
|
);
|
|
}
|
|
|
|
await page.waitForTimeout(1500);
|
|
providerCaptures.afterDelete = await captureProvider(remoteConversationId, `${providerKey}-after-delete`);
|
|
assert.strictEqual(
|
|
providerCaptures.afterDelete.conversationLinkCount,
|
|
0,
|
|
"provider 删除后网页历史中不应继续存在本轮远端会话链接",
|
|
);
|
|
|
|
const deletedRows = sqliteJson(
|
|
`SELECT mnote_session_id, remote_conversation_id, status, metadata_json FROM ai_external_conversation_bindings WHERE user_id=${sqlQuote(actorId)} AND mnote_session_id=${sqlQuote(sessionId)} AND provider=${sqlQuote(provider.expectedProvider)} ORDER BY updated_at DESC LIMIT 1;`,
|
|
[],
|
|
);
|
|
assert(deletedRows && deletedRows.length === 1, "删除后 binding 仍应可审计");
|
|
assert.strictEqual(deletedRows[0].status, "remote_deleted", "删除后 binding 应标记 remote_deleted");
|
|
|
|
const providerLog = readLogSince(provider.gatewayLog, gatewayLogOffset);
|
|
let providerSendCount = countMatches(providerLog, provider.sendLogPattern);
|
|
if (providerKey === "gemini" && providerSendCount === 0) {
|
|
const fullProviderLog = readLogSince(provider.gatewayLog, 0);
|
|
providerSendCount = countMatches(fullProviderLog, new RegExp(escapeRegExp(marker), "g"));
|
|
}
|
|
assert.strictEqual(providerSendCount, 1, "provider 本轮只能收到一次发送动作");
|
|
|
|
fs.writeFileSync(
|
|
RESULT_PATH,
|
|
`${JSON.stringify({
|
|
ok: true,
|
|
provider: providerKey,
|
|
marker,
|
|
sessionId,
|
|
remoteConversationId,
|
|
providerSendCount,
|
|
deleteResponse: deleteResponses.at(-1),
|
|
bindingBefore: bindingRows[0],
|
|
bindingAfter: deletedRows[0],
|
|
screenshots,
|
|
providerCaptures,
|
|
}, null, 2)}\n`,
|
|
"utf8",
|
|
);
|
|
} catch (error) {
|
|
caughtError = error;
|
|
screenshots.failure = await saveScreenshot(page, "failure").catch(() => "");
|
|
fs.writeFileSync(
|
|
RESULT_PATH,
|
|
`${JSON.stringify({
|
|
ok: false,
|
|
provider: providerKey,
|
|
marker,
|
|
error: error instanceof Error ? error.stack || error.message : String(error),
|
|
runRequests,
|
|
deleteResponses,
|
|
screenshots,
|
|
gatewayLog: readLogSince(provider.gatewayLog, gatewayLogOffset).slice(-8000),
|
|
}, null, 2)}\n`,
|
|
"utf8",
|
|
);
|
|
} finally {
|
|
await browser.close().catch(() => {});
|
|
}
|
|
|
|
if (caughtError) throw caughtError;
|
|
console.log(JSON.stringify(JSON.parse(fs.readFileSync(RESULT_PATH, "utf8")), null, 2));
|
|
}
|
|
|
|
main().catch((error) => {
|
|
console.error(error && error.stack ? error.stack : error);
|
|
process.exit(1);
|
|
});
|