推进本地优先迁移与资源闭环

- 补齐共享只读页面 AI 浏览器 smoke,并回填当前优先级 checklist 的 P4/P5/P6 证据。

- 为 OnlyOffice 资源增加 /office/{documentId}/{assetId} 对象壳,固定 resource identity 与侧边栏打开路径。

- 扩展 Convex 导出脚本,支持 dry-run、manifest、冲突报告、索引刷新与 rollback,并补充 smoke。

- 补充资源 AI 工具合同与 Convex 导出 Web 入口设计。
This commit is contained in:
lix-2026
2026-05-19 11:39:04 +08:00
parent a1b28a8e38
commit 94be1c927b
11 changed files with 1097 additions and 73 deletions
@@ -0,0 +1,108 @@
#!/usr/bin/env node
"use strict";
const assert = require("node:assert/strict");
const fs = require("node:fs");
const os = require("node:os");
const path = require("node:path");
const { spawnSync } = require("node:child_process");
const repoRoot = path.resolve(__dirname, "..");
const tmpRoot = fs.mkdtempSync(path.join(os.tmpdir(), "mnote-convex-export-plan-"));
const fixturePath = path.join(tmpRoot, "fixture.json");
const outRoot = path.join(tmpRoot, "local-workspace");
const manifestPath = path.join(tmpRoot, "migration-manifest.json");
const conflictPath = path.join(tmpRoot, "conflicts.json");
const fixture = {
workspace: {
id: "ws_legacy_plan",
name: "迁移计划空间",
ownerId: "user_plan",
},
documents: [
{
id: "doc_root",
title: "Project",
parent_id: null,
content: "# Project\n\n导出正文\n",
wide_layout: true,
},
],
mediaAssets: [
{
id: "asset_logo",
fileName: "logo.txt",
documentId: "doc_root",
content: "LOGO-NEW",
},
],
aiSessions: [],
};
function runExport(args, expectedStatus = 0) {
const result = spawnSync(
process.execPath,
[path.join(repoRoot, "scripts", "export-convex-workspace-to-local.js"), ...args],
{ cwd: repoRoot, encoding: "utf8" },
);
assert.equal(result.status, expectedStatus, result.stderr || result.stdout);
return result;
}
try {
fs.writeFileSync(fixturePath, JSON.stringify(fixture, null, 2), "utf8");
runExport([
"--fixture",
fixturePath,
"--out",
outRoot,
"--dry-run",
"--manifest",
manifestPath,
"--conflict-report",
conflictPath,
]);
assert.equal(fs.existsSync(path.join(outRoot, "pages", "Project.md")), false, "dry run 不应写入页面文件");
const dryRunManifest = JSON.parse(fs.readFileSync(manifestPath, "utf8"));
assert.equal(dryRunManifest.dryRun, true);
assert.equal(dryRunManifest.operations.some((item) => item.action === "create" && item.relativePath === "pages/Project.md"), true);
fs.mkdirSync(path.join(outRoot, "pages"), { recursive: true });
fs.writeFileSync(path.join(outRoot, "pages", "Project.md"), "原有内容\n", "utf8");
const conflict = runExport([
"--fixture",
fixturePath,
"--out",
outRoot,
"--manifest",
manifestPath,
"--conflict-report",
conflictPath,
], 2);
assert.match(conflict.stderr + conflict.stdout, /migration_conflict/);
assert.equal(fs.readFileSync(path.join(outRoot, "pages", "Project.md"), "utf8"), "原有内容\n");
const conflictReport = JSON.parse(fs.readFileSync(conflictPath, "utf8"));
assert.equal(conflictReport.conflicts.some((item) => item.relativePath === "pages/Project.md"), true);
fs.rmSync(outRoot, { recursive: true, force: true });
runExport([
"--fixture",
fixturePath,
"--out",
outRoot,
"--manifest",
manifestPath,
]);
assert.match(fs.readFileSync(path.join(outRoot, "pages", "Project.md"), "utf8"), /导出正文/);
assert.equal(fs.existsSync(path.join(outRoot, ".mnote", "migration-manifest.json")), true);
runExport(["--rollback", path.join(outRoot, ".mnote", "migration-manifest.json")]);
assert.equal(fs.existsSync(path.join(outRoot, "pages", "Project.md")), false, "rollback 应删除本次新增页面");
assert.equal(fs.existsSync(path.join(outRoot, "pages", "Project.assets", "logo.txt")), false, "rollback 应删除本次新增资源");
console.log(JSON.stringify({ ok: true, smoke: "task455-convex-export-plan-rollback" }, null, 2));
} finally {
fs.rmSync(tmpRoot, { recursive: true, force: true });
}