feat: cut over rust web main shell

This commit is contained in:
lix-2026
2026-04-29 12:24:44 +08:00
parent 7965c6c107
commit 048fe28a4d
97 changed files with 9396 additions and 1263 deletions
@@ -1,5 +1,5 @@
import { describe, expect, it, vi } from "vitest";
import { runAiAgent } from "./runAgent";
import { AI_AGENT_LEGACY_RUNTIME_CONTRACT, runAiAgent } from "./runAgent";
import type { OpenAiCompatibleChatMessage } from "@/lib/ai/openaiCompatibleChat";
describe("runAiAgent", () => {
@@ -172,3 +172,14 @@ describe("runAiAgent", () => {
]);
});
});
describe("AI_AGENT_LEGACY_RUNTIME_CONTRACT", () => {
it("legacy runAiAgent 明确降为 Hermes 主桥后的兼容 runtime", () => {
expect(AI_AGENT_LEGACY_RUNTIME_CONTRACT).toEqual({
runtimeRole: "legacy_compat",
mainBridgeOwner: "rust-web-hermes",
legacyEndpoint: "/api/ai-agent/run",
});
});
});
@@ -3,6 +3,12 @@ import type { OpenAiCompatibleChatMessage, OpenAiCompatibleChatOptions } from "@
import { openAiCompatibleChat } from "@/lib/ai/openaiCompatibleChat";
import { parseToolTagCalls, formatToolResultTag } from "../protocol/toolTagProtocol";
export const AI_AGENT_LEGACY_RUNTIME_CONTRACT = {
runtimeRole: "legacy_compat",
mainBridgeOwner: "rust-web-hermes",
legacyEndpoint: "/api/ai-agent/run",
} as const;
export type AgentChatFn = (
messages: OpenAiCompatibleChatMessage[],
cfg: OpenAiCompatibleChatOptions,
@@ -0,0 +1,31 @@
import { describe, expect, it } from "vitest";
import { RUST_WEB_MAIN_EXECUTION_BOUNDARY } from "./rust-web-main-execution-boundary";
describe("RUST_WEB_MAIN_EXECUTION_BOUNDARY", () => {
it("固定 3000 公开入口由 mnote-web 拥有,Next 只能作为 legacy compat", () => {
expect(RUST_WEB_MAIN_EXECUTION_BOUNDARY.publicEntry).toBe("3000");
expect(RUST_WEB_MAIN_EXECUTION_BOUNDARY.mainWebOwner).toBe("mnote-web");
expect(RUST_WEB_MAIN_EXECUTION_BOUNDARY.legacyCompatOwner).toBe("next-app-router");
expect(RUST_WEB_MAIN_EXECUTION_BOUNDARY.nextAllowedDuties).not.toContain("main_web_owner");
expect(RUST_WEB_MAIN_EXECUTION_BOUNDARY.forbiddenNextSemantics).toContain("主 Web gateway owner");
});
it("把主 Web 执行面和允许保留的 React island / legacy bundle 明确分层", () => {
expect(RUST_WEB_MAIN_EXECUTION_BOUNDARY.rustOwnedPlanes).toEqual(
expect.arrayContaining([
"web_gateway",
"document_shell",
"page_aggregate_transport",
"tree_realtime_stream",
"search_shell",
"ai_bridge_transport",
"mindmap_object_shell",
]),
);
expect(RUST_WEB_MAIN_EXECUTION_BOUNDARY.nextAllowedDuties).toEqual([
"legacy_react_bundle",
"legacy_app_router_compat",
"island_asset_source_until_migrated",
]);
});
});
@@ -0,0 +1,35 @@
export const RUST_WEB_MAIN_EXECUTION_BOUNDARY = {
schema: "mnote.rust_web.main_execution_boundary.v1",
publicEntry: "3000",
mainWebOwner: "mnote-web",
legacyCompatOwner: "next-app-router",
rustOwnedPlanes: [
"web_gateway",
"runtime_config",
"auth_session_handoff",
"document_shell",
"page_aggregate_transport",
"tree_projection_transport",
"tree_command_transport",
"tree_realtime_stream",
"search_shell",
"ai_bridge_transport",
"mindmap_object_shell",
],
nextAllowedDuties: [
"legacy_react_bundle",
"legacy_app_router_compat",
"island_asset_source_until_migrated",
],
forbiddenNextSemantics: [
"主 Web gateway owner",
"页面事实源",
"树排序 canonical plan",
"Page Aggregate command family owner",
"tree realtime event contract owner",
"搜索事实源",
"AI tool protocol owner",
],
} as const;
export type RustWebMainExecutionBoundary = typeof RUST_WEB_MAIN_EXECUTION_BOUNDARY;
+41 -1
View File
@@ -1,5 +1,5 @@
import { describe, expect, it } from "vitest";
import { buildSearchRequest } from "./request";
import { SEARCH_REQUEST_TRANSPORT_CONTRACT, buildSearchRequest } from "./request";
describe("buildSearchRequest", () => {
it("combines标题过滤与时间范围", () => {
@@ -42,4 +42,44 @@ describe("buildSearchRequest", () => {
expect(payload?.filters.onlyCurrentPage).toBe(false);
expect(payload?.documentId).toBeUndefined();
});
it("固定搜索请求将由 Rust Web search.documents transport 拥有", () => {
const payload = buildSearchRequest({
workspaceId: "ws-3",
activeDocumentId: "doc-3",
query: "Rust Web",
filters: {
titleOnly: false,
exact: true,
onlyCurrentPage: true,
includeOcr: true,
timeField: "updated",
customRange: undefined,
},
timeRange: "any",
});
expect(payload).toMatchObject({
workspaceId: "ws-3",
documentId: "doc-3",
query: "Rust Web",
filters: {
exact: true,
includeOcr: true,
onlyCurrentPage: true,
},
});
});
});
describe("SEARCH_REQUEST_TRANSPORT_CONTRACT", () => {
it("搜索请求 contract 固定为 Rust Web transport", () => {
expect(SEARCH_REQUEST_TRANSPORT_CONTRACT).toEqual({
shellOwner: "mnote-web",
queryName: "search.documents",
endpoint: "/api/search/documents",
runtimeRole: "rust_web_transport",
});
});
});
+7
View File
@@ -4,6 +4,13 @@ import type {
DocumentSearchTimeRange,
} from "@/types/search";
export const SEARCH_REQUEST_TRANSPORT_CONTRACT = {
shellOwner: "mnote-web",
queryName: "search.documents",
endpoint: "/api/search/documents",
runtimeRole: "rust_web_transport",
} as const;
interface BuildSearchRequestArgs {
workspaceId: string | null;
activeDocumentId: string | null;
@@ -2,7 +2,7 @@ import { describe, expect, it } from "vitest";
import { TREE_3000_ROUTE_BOUNDARY_MANIFEST } from "./tree-route-boundary";
describe("TREE_3000_ROUTE_BOUNDARY_MANIFEST", () => {
it("固定 3000 route 的 thin proxy 与 compat pending 边界", () => {
it("固定 3000 route 的 Rust Web gateway owner 与 legacy compat 边界", () => {
expect(TREE_3000_ROUTE_BOUNDARY_MANIFEST).toMatchObject({
schema: "mnote.tree.3000_route_boundary",
schemaVersion: 1,
@@ -13,28 +13,34 @@ describe("TREE_3000_ROUTE_BOUNDARY_MANIFEST", () => {
expect.arrayContaining([
expect.objectContaining({
id: "tree.commands",
role: "next-thin-proxy",
role: "rust-owned",
route: "/api/tree/commands",
owner: "rust-web-gateway",
}),
expect.objectContaining({
id: "tree.stream",
role: "next-thin-proxy",
role: "rust-owned",
route: "/api/tree/stream",
owner: "rust-web-gateway",
}),
expect.objectContaining({
id: "tree.shell.debug",
role: "compat-pending",
route: "/api/tree/shell",
owner: "next-app-router-legacy",
}),
]),
);
expect(TREE_3000_ROUTE_BOUNDARY_MANIFEST.rustOwnedSemantics).toContain("tree.subtree.move");
expect(TREE_3000_ROUTE_BOUNDARY_MANIFEST.nextThinProxyDuties).toContain(
"Rust command result 回包整形",
"legacy route alias 与 header 透传",
);
expect(TREE_3000_ROUTE_BOUNDARY_MANIFEST.browserSubstrateDuties).toContain(
"新页面 scaffold 文件创建",
);
expect(TREE_3000_ROUTE_BOUNDARY_MANIFEST.forbiddenNextSemantics).toContain("树排序 canonical plan");
expect(TREE_3000_ROUTE_BOUNDARY_MANIFEST.forbiddenNextSemantics).toContain(
"主 tree command transport owner",
);
});
});
+12 -11
View File
@@ -1,6 +1,7 @@
export type TreeRouteBoundaryRole =
| "rust-owned"
| "next-thin-proxy"
| "legacy-thin-proxy"
| "browser-substrate"
| "compat-pending";
@@ -8,7 +9,7 @@ export type TreeRouteBoundaryItem = {
id: string;
role: TreeRouteBoundaryRole;
route: string;
owner: "rust-runtime" | "next-3000" | "convex-substrate";
owner: "rust-runtime" | "rust-web-gateway" | "next-app-router-legacy" | "convex-substrate";
description: string;
};
@@ -19,25 +20,25 @@ export const TREE_3000_ROUTE_BOUNDARY_MANIFEST = {
routes: [
{
id: "tree.commands",
role: "next-thin-proxy",
role: "rust-owned",
route: "/api/tree/commands",
owner: "next-3000",
owner: "rust-web-gateway",
description:
"浏览器公开树命令入口,只负责认证、payload envelope、Rust command transport、artifact writer 调用与必要副作用调度。",
"浏览器公开树命令入口由 Rust Web gateway 拥有;legacy Next 仅保留 cookie/header compat 和浏览器 substrate 能力。",
},
{
id: "tree.stream",
role: "next-thin-proxy",
role: "rust-owned",
route: "/api/tree/stream",
owner: "next-3000",
owner: "rust-web-gateway",
description:
"浏览器 SSE/polling 入口,只转发 bridge log/domain event cursor 与 Rust 产出的 streamDelta,缺少稳定 delta 时保守 resync。",
"浏览器 SSE/polling 入口由 Rust Web gateway 拥有,legacy Next 只作为迁移期 alias。",
},
{
id: "tree.shell.debug",
role: "compat-pending",
route: "/api/tree/shell",
owner: "next-3000",
owner: "next-app-router-legacy",
description:
"仅服务显式 debug/internal runtime 验证;3000 主路径使用 same-origin inline host,不应默认请求 mnote-web:3104。",
},
@@ -55,9 +56,7 @@ export const TREE_3000_ROUTE_BOUNDARY_MANIFEST = {
nextThinProxyDuties: [
"cookie/auth 读取与 Convex client 获取",
"workspace bootstrap",
"CommandEnvelope 构造与 Rust runtime transport",
"Rust command result 回包整形",
"Rust artifact writer 调用",
"legacy route alias 与 header 透传",
"tree.subtree.move 的 sidebar snapshot preflight 数据采集",
],
browserSubstrateDuties: [
@@ -73,5 +72,7 @@ export const TREE_3000_ROUTE_BOUNDARY_MANIFEST = {
"长期 streamDelta 主语义拼装",
"tree shell renderer runtime",
"tree.node.embed 的 pageReference block 结构与插入位置语义",
"主 tree command transport owner",
"主 tree stream transport owner",
],
} as const;
@@ -4,7 +4,7 @@ import {
type SidebarDatasetListQueryResult,
} from "@/lib/sidebar-data";
export type TreeStreamKind = "snapshot" | "delta" | "resync";
export type TreeStreamKind = "snapshot" | "delta" | "resync" | "heartbeat";
export interface TreeStreamEnvelope {
stream: string;
@@ -16,7 +16,7 @@ export interface TreeStreamEnvelope {
data: unknown;
}
const TREE_STREAM_KINDS = new Set<TreeStreamKind>(["snapshot", "delta", "resync"]);
const TREE_STREAM_KINDS = new Set<TreeStreamKind>(["snapshot", "delta", "resync", "heartbeat"]);
function isRecord(value: unknown): value is Record<string, unknown> {
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
@@ -69,7 +69,7 @@ export function buildWorkspaceTreeStreamUrl(
): string {
const baseOrigin =
typeof window !== "undefined" ? window.location.origin : "http://127.0.0.1:3000";
const url = new URL("/api/mnote-web/stream", baseOrigin);
const url = new URL("/api/tree/events", baseOrigin);
url.searchParams.set("workspaceId", workspaceId.trim());
if (typeof cursor === "string" && cursor.trim()) {
url.searchParams.set("cursor", cursor.trim());
@@ -21,7 +21,7 @@ describe("tree-stream/protocol", () => {
expect(
buildWorkspaceTreeStreamUrl(" ws_1 ", "evt_9"),
).toBe(
"http://127.0.0.1:3000/api/mnote-web/stream?workspaceId=ws_1&cursor=evt_9",
"http://127.0.0.1:3000/api/tree/events?workspaceId=ws_1&cursor=evt_9",
);
});
@@ -35,7 +35,7 @@ describe("tree-stream/protocol", () => {
});
expect(
buildWorkspaceTreeStreamUrl("ws_1", null),
).toBe("http://127.0.0.1:3000/api/mnote-web/stream?workspaceId=ws_1");
).toBe("http://127.0.0.1:3000/api/tree/events?workspaceId=ws_1");
});
it("解析 snapshot / delta / resync 协议消息", () => {
@@ -85,6 +85,21 @@ describe("tree-stream/protocol", () => {
projection: "sidebar_tree",
});
expect(
parseTreeStreamMessage({
eventType: "heartbeat",
rawData: JSON.stringify({
kind: "heartbeat",
workspaceId: "ws_1",
cursor: "evt_3",
}),
}),
).toMatchObject({
kind: "heartbeat",
workspaceId: "ws_1",
cursor: "evt_3",
});
expect(
parseTreeStreamMessage({
eventType: "delta",