feat(control-plane): add libSQL Turso backend

This commit is contained in:
Agent Board
2026-07-03 13:24:20 +08:00
parent ee6612028d
commit a75b3d11f9
44 changed files with 11070 additions and 666 deletions
+34 -59
View File
@@ -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`,