feat: cut over rust web main shell
This commit is contained in:
@@ -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;
|
||||
@@ -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",
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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",
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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",
|
||||
|
||||
Reference in New Issue
Block a user