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
+72 -7
View File
@@ -445,14 +445,32 @@ export const getContent = query({
}
if (doc.user_id === userId) {
return { content: doc.content ?? null };
return {
content: doc.content ?? null,
revision: doc.content_revision ?? 0,
conflict_detection_key:
doc.content_conflict_key ??
`${args.id}:${doc.content_revision ?? 0}`,
};
}
if (doc.access_scope === "public") {
return { content: doc.content ?? null };
return {
content: doc.content ?? null,
revision: doc.content_revision ?? 0,
conflict_detection_key:
doc.content_conflict_key ??
`${args.id}:${doc.content_revision ?? 0}`,
};
}
const perm = (await resolveSharePermission(ctx, doc, userId)) ?? (await resolveGroupSharePermission(ctx, doc, userId));
if (!perm) return null;
return { content: doc.content ?? null };
return {
content: doc.content ?? null,
revision: doc.content_revision ?? 0,
conflict_detection_key:
doc.content_conflict_key ??
`${args.id}:${doc.content_revision ?? 0}`,
};
},
});
@@ -462,7 +480,13 @@ export const getContentForIngest = internalQuery({
const doc = await getCanonicalDocumentByBusinessId(ctx, args.id);
if (!doc) return null;
if (doc.user_id !== args.userId) return null;
return { content: doc.content ?? null };
return {
content: doc.content ?? null,
revision: doc.content_revision ?? 0,
conflict_detection_key:
doc.content_conflict_key ??
`${args.id}:${doc.content_revision ?? 0}`,
};
},
});
@@ -791,6 +815,8 @@ export const create = mutation({
parent_id: args.parentId,
title,
content,
content_revision: 0,
content_conflict_key: `${args.id}:0`,
raw_text: rawText,
access_scope: args.accessScope,
sort_order: sortOrder,
@@ -837,7 +863,12 @@ export const create = mutation({
});
export const updateContent = mutation({
args: { id: v.string(), content: v.any() },
args: {
id: v.string(),
content: v.any(),
expectedRevision: v.optional(v.union(v.number(), v.null())),
conflictDetectionKey: v.optional(v.union(v.string(), v.null())),
},
handler: async (ctx, args) => {
const userId = await requireUserId(ctx);
@@ -851,14 +882,48 @@ export const updateContent = mutation({
const perm = (await resolveSharePermission(ctx, doc, userId)) ?? (await resolveGroupSharePermission(ctx, doc, userId));
if (perm !== "edit") throw new Error("无权限");
}
const currentRevision = doc.content_revision ?? 0;
const currentConflictDetectionKey =
doc.content_conflict_key ??
`${args.id}:${currentRevision}`;
if (
typeof args.expectedRevision === "number" &&
Number.isInteger(args.expectedRevision) &&
args.expectedRevision >= 0 &&
args.expectedRevision !== currentRevision
) {
throw new Error("正文内容已变更,请刷新后重试");
}
if (
typeof args.conflictDetectionKey === "string" &&
args.conflictDetectionKey.trim() &&
args.conflictDetectionKey.trim() !== currentConflictDetectionKey
) {
throw new Error("正文冲突检测失败,请刷新后重试");
}
const ts = nowIso();
const rawText = extractTextFromDocumentContent(args.content);
await ctx.db.patch(doc._id, { content: args.content, raw_text: rawText, updated_at: ts });
const nextRevision = currentRevision + 1;
const nextConflictDetectionKey = `${args.id}:${nextRevision}`;
await ctx.db.patch(doc._id, {
content: args.content,
content_revision: nextRevision,
content_conflict_key: nextConflictDetectionKey,
raw_text: rawText,
updated_at: ts,
});
// 说明:在 Convex 模式下,把"自动入库/LightRAG 触发"迁到 Convex jobs/actions。
// 采用 debounce,避免频繁保存时触发过多任务。
await enqueueIngestDocumentJob(ctx, { userId: doc.user_id, documentId: args.id, debounceMs: 1500 });
return { ok: true, updated_at: ts };
return {
ok: true,
updated_at: ts,
revision: nextRevision,
conflict_detection_key: nextConflictDetectionKey,
};
},
});