feat: 收口文档桥接与 OnlyOffice/Sidebar 回归

- 为 documents.save/meta/content、blocks.patch 与 sidebar.dataset.list 补齐 Rust 协议映射、共享契约与桥接执行器

- 对齐 BlockNote、Mindmap、OnlyOffice 的保存/路由元信息,并补真实浏览器回归脚本与 OnlyOffice 部署基线

- 忽略 Rust 本地构建产物与 Harness 调试状态文件,避免临时产物进入仓库历史
This commit is contained in:
lix-2026
2026-04-15 03:06:29 +08:00
parent 84a8454fa9
commit b33ffb99e7
51 changed files with 3260 additions and 379 deletions
@@ -1,18 +1,27 @@
import { useMemo } from "react";
import { useQuery, useConvexAuth } from "convex/react";
import type { FunctionReference } from "convex/server";
import type { SidebarInitialData } from "@/components/sidebar/types";
import type { MediaAsset } from "@/types/media";
import { api } from "@/lib/convex/api";
import { buildSidebarInitialData } from "@/lib/sidebar-data";
import {
mapSidebarDatasetListQueryResultToInitialData,
type SidebarDatasetListQueryResult,
} from "@/lib/sidebar-data";
import { toBrowserAccessibleUrl } from "@/lib/url/browser-file-url";
type CurrentUserRecord = {
_id?: string;
};
/**
* Convex 模式下的侧边栏数据 hook
* 使用 Convex 的 useQuery 实现实时订阅,无需手动 refetch
const sidebarDatasetListQuery = ((api as unknown as Record<string, unknown>).sidebar as
| Record<string, unknown>
| undefined)?.datasetList as FunctionReference<
"query",
"public",
{ workspaceId: string },
SidebarDatasetListQueryResult
>;
/**
* Convex 模式下的侧边栏数据 hook
* 使用 Convex 的 useQuery 实现实时订阅,无需手动 refetch
*
* 注意:此 hook 仅应在 Convex 模式下使用
* 后端从 ctx.auth.getUserIdentity() 获取用户身份,前端无需传入 userId
@@ -27,51 +36,9 @@ export function useConvexSidebarData(workspaceId: string): {
// 显式转为 boolean,确保类型正确
const shouldFetch = Boolean(isAuthenticated && workspaceId);
const currentUser = useQuery(api.users.currentUser, shouldFetch ? {} : "skip");
const currentUserRecord =
currentUser && typeof currentUser === "object" ? (currentUser as CurrentUserRecord) : null;
const userId =
currentUserRecord && typeof currentUserRecord._id === "string"
? currentUserRecord._id
: "";
const shouldFetchAuthed = Boolean(shouldFetch && userId);
// 使用 Convex 的 useQuery,自动订阅实时更新
// 后端从 ctx.auth.getUserIdentity() 获取用户身份,无需前端传入 userId
const documents = useQuery(
api.documents.listByWorkspace,
shouldFetch ? { workspaceId } : "skip"
);
const trashedDocuments = useQuery(
api.documents.listTrashedByWorkspace,
shouldFetch ? { workspaceId } : "skip"
);
const mindmaps = useQuery(
api.mindmaps.listByWorkspace,
shouldFetch ? { workspaceId, includeDeleted: true } : "skip"
);
const mediaAssets = useQuery(
api.mediaAssets.listByWorkspace,
shouldFetchAuthed ? { userId, workspaceId, limit: 200 } : "skip",
);
const trashedMediaAssets = useQuery(
api.mediaAssets.listDeletedByWorkspace,
shouldFetchAuthed ? { userId, workspaceId, limit: 2000 } : "skip",
);
const tables = useQuery(
api.tables.listByWorkspaceForSearch,
shouldFetchAuthed ? { userId, workspaceId, includeArchived: true, limit: 3000 } : "skip",
);
const workspacesResult = useQuery(
api.workspaces.fetchWorkspaceSummaries,
shouldFetch ? {} : "skip",
const sidebarDataset = useQuery(
sidebarDatasetListQuery,
shouldFetch ? { workspaceId } : "skip",
);
const normalizeAssetUrls = (asset: MediaAsset): MediaAsset => {
@@ -80,54 +47,39 @@ export function useConvexSidebarData(workspaceId: string): {
return { ...asset, file_url: fileUrl, thumbnail_url: thumbUrl };
};
// 组合数据,格式与 SidebarInitialData 一致
const data: SidebarInitialData | null = useMemo(() => {
// 当 skip 时,返回值是 undefined
if (currentUser === undefined ||
documents === undefined ||
trashedDocuments === undefined ||
mindmaps === undefined ||
mediaAssets === undefined ||
trashedMediaAssets === undefined ||
tables === undefined ||
workspacesResult === undefined) {
const normalizedSidebarDataset = useMemo<SidebarDatasetListQueryResult | null>(() => {
if (sidebarDataset === undefined || !sidebarDataset) {
return null;
}
return buildSidebarInitialData({
activeWorkspaceId: workspacesResult.activeWorkspaceId || workspaceId,
workspaces: workspacesResult.workspaces,
documents,
trashedDocuments,
mindmaps,
mediaAssets: ((mediaAssets ?? []) as MediaAsset[]).map(normalizeAssetUrls),
trashedMediaAssets: ((trashedMediaAssets ?? []) as MediaAsset[]).map(normalizeAssetUrls),
tables,
});
}, [
currentUser,
documents,
trashedDocuments,
mindmaps,
mediaAssets,
trashedMediaAssets,
tables,
workspacesResult,
workspaceId,
]);
return {
...sidebarDataset,
media_assets: ((sidebarDataset.media_assets ?? []) as MediaAsset[]).map(normalizeAssetUrls),
trashed_media_assets: ((sidebarDataset.trashed_media_assets ?? []) as MediaAsset[]).map(
normalizeAssetUrls,
),
mindmap_assets: ((sidebarDataset.mindmap_assets ?? []) as MediaAsset[]).map(normalizeAssetUrls),
trashed_mindmap_assets: ((sidebarDataset.trashed_mindmap_assets ?? []) as MediaAsset[]).map(
normalizeAssetUrls,
),
table_assets: ((sidebarDataset.table_assets ?? []) as MediaAsset[]).map(normalizeAssetUrls),
trashed_table_assets: ((sidebarDataset.trashed_table_assets ?? []) as MediaAsset[]).map(
normalizeAssetUrls,
),
};
}, [sidebarDataset]);
// 组合数据,格式与 SidebarInitialData 一致
const data: SidebarInitialData | null = useMemo(() => {
if (!normalizedSidebarDataset) {
return null;
}
return mapSidebarDatasetListQueryResultToInitialData(normalizedSidebarDataset);
}, [normalizedSidebarDataset]);
// loading 状态:只有当 shouldFetch 为 true 且数据未加载时才算 loading
// 使用 === undefined 判断,因为 skip 时返回 undefined
const isLoading = shouldFetch && (
currentUser === undefined ||
documents === undefined ||
trashedDocuments === undefined ||
mindmaps === undefined ||
mediaAssets === undefined ||
trashedMediaAssets === undefined ||
tables === undefined ||
workspacesResult === undefined
);
const isLoading = shouldFetch && sidebarDataset === undefined;
const error = null;
// Convex 模式下数据自动实时同步,refetch 是空操作