feat: land page aggregate and phase7 document ai mainline

- 收口 page aggregate 读取、本地状态与命令客户端\n- 接入 phase7 document ai sidecar 与前端编排入口\n- 更新 architecture 与 design 状态迁移
This commit is contained in:
lix-2026
2026-04-23 07:38:34 +08:00
parent 8353aea2f9
commit 41e958769e
93 changed files with 8778 additions and 2222 deletions
@@ -1,7 +1,10 @@
import { beforeEach, describe, expect, it, vi } from "vitest";
const mockBuildMnoteWebForwardHeaders = vi.fn();
const mockBuildMnoteWebStreamUrl = vi.fn();
const mockGetAuthedConvexClient = vi.fn();
const mockBuildDocumentBridgeContext = vi.fn();
const mockBuildDocumentQueryEnvelope = vi.fn();
const mockExecuteRustBridgeQueryTransport = vi.fn();
const mockResolveRustBridgeQueryPlan = vi.fn();
const mockDocumentBridgeErrorResponse = vi.fn((error: unknown) =>
Response.json(
{
@@ -11,41 +14,99 @@ const mockDocumentBridgeErrorResponse = vi.fn((error: unknown) =>
),
);
vi.mock("@/lib/server/mnote-web", () => ({
buildMnoteWebForwardHeaders: mockBuildMnoteWebForwardHeaders,
buildMnoteWebStreamUrl: mockBuildMnoteWebStreamUrl,
vi.mock("@/lib/convex/route", () => ({
getAuthedConvexClient: mockGetAuthedConvexClient,
}));
vi.mock("@/lib/documents/bridge", () => ({
buildDocumentBridgeContext: mockBuildDocumentBridgeContext,
buildDocumentQueryEnvelope: mockBuildDocumentQueryEnvelope,
documentBridgeErrorResponse: mockDocumentBridgeErrorResponse,
}));
vi.mock("@/lib/documents/rust-runtime", () => ({
executeRustBridgeQueryTransport: mockExecuteRustBridgeQueryTransport,
resolveRustBridgeQueryPlan: mockResolveRustBridgeQueryPlan,
}));
describe("/api/mnote-web/stream route", () => {
beforeEach(() => {
mockBuildMnoteWebForwardHeaders.mockReset();
mockBuildMnoteWebStreamUrl.mockReset();
mockGetAuthedConvexClient.mockReset();
mockBuildDocumentBridgeContext.mockReset();
mockBuildDocumentQueryEnvelope.mockReset();
mockExecuteRustBridgeQueryTransport.mockReset();
mockResolveRustBridgeQueryPlan.mockReset();
mockDocumentBridgeErrorResponse.mockClear();
vi.unstubAllGlobals();
});
it("透传上游 SSE 并去掉 set-cookie", async () => {
const upstreamHeaders = new Headers({
"content-type": "text/event-stream; charset=utf-8",
"cache-control": "no-cache",
"set-cookie": "secret=1",
it("直接在 3000 内生成 snapshot SSE,不再回源 mnote-web", async () => {
mockGetAuthedConvexClient.mockResolvedValue({
client: { query: vi.fn() },
});
const upstreamResponse = new Response("event: snapshot\ndata: {\"ok\":true}\n\n", {
status: 200,
headers: upstreamHeaders,
mockBuildDocumentBridgeContext.mockResolvedValue({
requestId: "req_stream_1",
traceId: "trace_stream_1",
workspaceId: "ws_1",
});
mockBuildMnoteWebForwardHeaders.mockResolvedValue(new Headers({ cookie: "a=1" }));
mockBuildMnoteWebStreamUrl.mockReturnValue(
new URL("http://127.0.0.1:3104/api/stream/events?stream=workspace&projection=sidebar_tree&workspaceId=ws_1"),
);
const fetchMock = vi.fn(async () => upstreamResponse);
vi.stubGlobal("fetch", fetchMock);
mockBuildDocumentQueryEnvelope
.mockReturnValueOnce({
name: "sidebar.dataset.list",
payload: { workspaceId: "ws_1" },
})
.mockReturnValueOnce({
name: "bridge.workspace.overview",
payload: {
workspaceId: "ws_1",
limit: 20,
cursor: null,
commandStatus: null,
eventStatus: null,
targetPageId: null,
targetBlockId: null,
aggregateType: null,
aggregateId: null,
},
});
mockResolveRustBridgeQueryPlan
.mockResolvedValueOnce({ kind: "query", functionName: "sidebar:datasetList", argsJson: { workspaceId: "ws_1" } })
.mockResolvedValueOnce({ kind: "query", functionName: "bridgeLogs:listWorkspaceOverview", argsJson: { workspaceId: "ws_1" } });
mockExecuteRustBridgeQueryTransport
.mockResolvedValueOnce({
active_workspace_id: "ws_1",
workspaces: [],
documents: [
{
id: "page_root",
workspace_id: "ws_1",
title: "工作区首页",
parent_id: null,
sort_order: 0,
is_starred: true,
is_template: false,
created_at: "2026-04-22T00:00:00Z",
updated_at: "2026-04-22T00:00:00Z",
},
],
trashed_documents: [],
media_assets: [],
trashed_media_assets: [],
mindmap_assets: [],
trashed_mindmap_assets: [],
table_assets: [],
trashed_table_assets: [],
mindmap_docs: [],
mindmap_asset_children: {},
})
.mockResolvedValueOnce({
workspace_id: "ws_1",
command_logs: [],
domain_events: [],
counts: { command_logs: 0, domain_events: 0 },
filters: null,
next_cursor: null,
has_more: false,
generated_at: "2026-04-22T00:00:00Z",
});
const { GET } = await import("./route");
const response = await GET(
@@ -55,19 +116,15 @@ describe("/api/mnote-web/stream route", () => {
}),
);
expect(fetchMock).toHaveBeenCalledTimes(1);
expect(fetchMock).toHaveBeenCalledWith(
"http://127.0.0.1:3104/api/stream/events?stream=workspace&projection=sidebar_tree&workspaceId=ws_1",
expect.objectContaining({
method: "GET",
cache: "no-store",
redirect: "follow",
headers: expect.any(Headers),
}),
);
expect(response.status).toBe(200);
expect(response.headers.get("content-type")).toContain("text/event-stream");
expect(response.headers.get("set-cookie")).toBeNull();
await expect(response.text()).resolves.toContain("event: snapshot");
const text = await response.text();
expect(text).toContain("event: snapshot");
expect(text).toContain('"kind":"snapshot"');
expect(text).toContain('"projection":"sidebar_tree"');
expect(text).toContain('"workspaceId":"ws_1"');
expect(text).toContain('"activeWorkspaceId":"ws_1"');
expect(mockResolveRustBridgeQueryPlan).toHaveBeenCalledTimes(2);
expect(mockExecuteRustBridgeQueryTransport).toHaveBeenCalledTimes(2);
});
});
@@ -1,11 +1,21 @@
import { documentBridgeErrorResponse } from "@/lib/documents/bridge";
import { getAuthedConvexClient } from "@/lib/convex/route";
import {
buildMnoteWebForwardHeaders,
buildMnoteWebStreamUrl,
} from "@/lib/server/mnote-web";
buildDocumentBridgeContext,
buildDocumentQueryEnvelope,
documentBridgeErrorResponse,
} from "@/lib/documents/bridge";
import {
executeRustBridgeQueryTransport,
resolveRustBridgeQueryPlan,
} from "@/lib/documents/rust-runtime";
import { mapSidebarDatasetListQueryResultToInitialData } from "@/lib/sidebar-data";
export const dynamic = "force-dynamic";
function toSseFrame(event: string, data: unknown) {
return `event: ${event}\ndata: ${JSON.stringify(data ?? null)}\n\n`;
}
export async function GET(request: Request) {
try {
const requestUrl = new URL(request.url);
@@ -16,26 +26,73 @@ export async function GET(request: Request) {
return Response.json({ error: "缺少 workspaceId" }, { status: 400 });
}
const targetUrl = buildMnoteWebStreamUrl({ workspaceId, cursor });
const headers = await buildMnoteWebForwardHeaders(request);
headers.set("accept", "text/event-stream");
headers.set("x-mnote-workspace-id", workspaceId);
const upstream = await fetch(targetUrl.toString(), {
method: "GET",
headers,
cache: "no-store",
redirect: "follow",
const { client } = await getAuthedConvexClient();
const context = await buildDocumentBridgeContext({
request,
workspaceId,
});
const responseHeaders = new Headers(upstream.headers);
responseHeaders.delete("set-cookie");
responseHeaders.set("cache-control", "no-store");
const sidebarEnvelope = buildDocumentQueryEnvelope({
name: "sidebar.dataset.list",
payload: {
workspaceId,
},
});
const sidebarPlan = await resolveRustBridgeQueryPlan({
context,
envelope: sidebarEnvelope,
});
const sidebarDataset = await executeRustBridgeQueryTransport({
client,
plan: sidebarPlan,
});
return new Response(upstream.body, {
status: upstream.status,
statusText: upstream.statusText,
headers: responseHeaders,
const overviewEnvelope = buildDocumentQueryEnvelope({
name: "bridge.workspace.overview",
payload: {
workspaceId,
limit: 20,
cursor,
commandStatus: null,
eventStatus: null,
targetPageId: null,
targetBlockId: null,
aggregateType: null,
aggregateId: null,
},
});
const overviewPlan = await resolveRustBridgeQueryPlan({
context,
envelope: overviewEnvelope,
});
const overview = await executeRustBridgeQueryTransport({
client,
plan: overviewPlan,
});
const payload = {
kind: "snapshot",
stream: "workspace",
projection: "sidebar_tree",
workspaceId,
rootNodeId: null,
cursor,
requestId: context.requestId,
traceId: context.traceId,
data: mapSidebarDatasetListQueryResultToInitialData(sidebarDataset),
snapshot: {
dataset: sidebarDataset,
tree: sidebarDataset.kernel_sidebar_projection ?? sidebarDataset.kernelSidebarProjection ?? null,
},
overview,
};
return new Response(toSseFrame("snapshot", payload), {
status: 200,
headers: {
"content-type": "text/event-stream; charset=utf-8",
"cache-control": "no-store",
},
});
} catch (error) {
return documentBridgeErrorResponse(error);