feat(control-plane): add libSQL Turso backend
This commit is contained in:
@@ -5,18 +5,20 @@ 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 {
|
||||
setupWorkspaceAccess,
|
||||
findExternalConversationBinding,
|
||||
} = require("./lib/control-plane-dev-seed");
|
||||
|
||||
const TASK = "task512-chatonly-doubao-sync-smoke";
|
||||
const OUTPUT_DIR = path.join(process.cwd(), "tmp", TASK);
|
||||
const RESULT_PATH = path.join(OUTPUT_DIR, "result.json");
|
||||
const CONTROL_PLANE_DB = "/mnt/Data1T/Mnote_data/control-plane/control-plane.db";
|
||||
const DOUBAO_CDP_URL = process.env.MNOTE_DOUBAO_CDP_URL || "http://127.0.0.1:9233";
|
||||
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"]
|
||||
@@ -52,41 +54,6 @@ function writeWorkspaceManifest(root, ownerId, workspaceId) {
|
||||
);
|
||||
}
|
||||
|
||||
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)}, 'Task512 Doubao Smoke', '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);
|
||||
`);
|
||||
}
|
||||
|
||||
async function firstDoubaoPage(cdpBrowser) {
|
||||
for (const context of cdpBrowser.contexts()) {
|
||||
const page = context.pages().find((candidate) => candidate.url().includes("doubao.com"));
|
||||
@@ -226,13 +193,6 @@ async function main() {
|
||||
|
||||
writeWorkspaceManifest(root, actorId, workspaceId);
|
||||
fs.writeFileSync(path.join(root, relativePath), ["# Doubao ChatOnly Sync", "", marker, ""].join("\n"), "utf8");
|
||||
grantWorkspaceAccess({
|
||||
actorId,
|
||||
workspaceId,
|
||||
root,
|
||||
rootUri,
|
||||
grantId: `grant_task512_${suffix}`,
|
||||
});
|
||||
|
||||
const doubaoBrowser = await chromium.connectOverCDP(DOUBAO_CDP_URL);
|
||||
const doubaoPage = await firstDoubaoPage(doubaoBrowser);
|
||||
@@ -275,6 +235,15 @@ async function main() {
|
||||
});
|
||||
|
||||
await ensureAuthenticated(page, context.request);
|
||||
await setupWorkspaceAccess(context.request, BASE_URL, {
|
||||
actorId,
|
||||
workspaceId,
|
||||
workspaceName: "Task512 Doubao Smoke",
|
||||
rootPath: root,
|
||||
rootUri,
|
||||
capabilities: ["ai", "markdown_edit"],
|
||||
timeoutMs: UI_TIMEOUT_MS,
|
||||
});
|
||||
const response = await page.goto(documentUrl(root, relativePath), {
|
||||
waitUntil: "domcontentloaded",
|
||||
timeout: UI_TIMEOUT_MS,
|
||||
@@ -337,13 +306,16 @@ async function main() {
|
||||
assert.strictEqual(markerAssistantCount, 1, "MNote 可见豆包回复应只有一条");
|
||||
|
||||
const sessionId = String(run.sessionId);
|
||||
const bindingRows = sqliteJson(
|
||||
`SELECT mnote_session_id, remote_conversation_id, status FROM ai_external_conversation_bindings WHERE user_id='${actorId}' AND mnote_session_id='${sessionId}' AND provider='doubao-web' 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;
|
||||
const bindingBefore = await findExternalConversationBinding(context.request, BASE_URL, {
|
||||
userId: actorId,
|
||||
workspaceId,
|
||||
mnoteSessionId: sessionId,
|
||||
provider: "doubao-web",
|
||||
timeoutMs: UI_TIMEOUT_MS,
|
||||
});
|
||||
assert(bindingBefore, "control-plane 应保存豆包远端会话绑定");
|
||||
assert.strictEqual(bindingBefore.status, "active", "删除前 binding 应为 active");
|
||||
const remoteConversationId = bindingBefore.remoteConversationId;
|
||||
assert(remoteConversationId, "binding 应包含 remote_conversation_id");
|
||||
|
||||
providerCaptures.afterMessage = await captureDoubaoPage(doubaoPage, {
|
||||
@@ -428,12 +400,15 @@ async function main() {
|
||||
"豆包删除后左侧历史列表不应继续存在本轮 conversation 行",
|
||||
);
|
||||
|
||||
const deletedRows = sqliteJson(
|
||||
`SELECT mnote_session_id, remote_conversation_id, status, metadata_json FROM ai_external_conversation_bindings WHERE user_id='${actorId}' AND mnote_session_id='${sessionId}' AND provider='doubao-web' ORDER BY updated_at DESC LIMIT 1;`,
|
||||
[],
|
||||
);
|
||||
assert(deletedRows && deletedRows.length === 1, "删除后 binding 仍应可审计");
|
||||
assert.strictEqual(deletedRows[0].status, "remote_deleted", "删除后 binding 应标记 remote_deleted");
|
||||
const bindingAfter = await findExternalConversationBinding(context.request, BASE_URL, {
|
||||
userId: actorId,
|
||||
workspaceId,
|
||||
mnoteSessionId: sessionId,
|
||||
provider: "doubao-web",
|
||||
timeoutMs: UI_TIMEOUT_MS,
|
||||
});
|
||||
assert(bindingAfter, "删除后 binding 仍应可审计");
|
||||
assert.strictEqual(bindingAfter.status, "remote_deleted", "删除后 binding 应标记 remote_deleted");
|
||||
|
||||
fs.writeFileSync(
|
||||
RESULT_PATH,
|
||||
@@ -446,8 +421,8 @@ async function main() {
|
||||
doubaoMessageStats,
|
||||
deleteCallCount: deleteCalls.length,
|
||||
deleteResponse: deleteResponses.at(-1),
|
||||
bindingBefore: bindingRows[0],
|
||||
bindingAfter: deletedRows[0],
|
||||
bindingBefore,
|
||||
bindingAfter,
|
||||
screenshots,
|
||||
providerCaptures,
|
||||
}, null, 2)}\n`,
|
||||
|
||||
Reference in New Issue
Block a user