0.2.1 onlyoffice修复

This commit is contained in:
liaibo
2026-01-17 10:12:53 +08:00
parent 94957dc361
commit 19907bccdc
102 changed files with 7188 additions and 186 deletions
@@ -162,10 +162,28 @@ const saveDocumentBlocks = async (supabase: DocSupabaseClient, ctx: DocToolConte
};
export const createDocServerTools = (args: {
supabase: DocSupabaseClient;
supabase?: DocSupabaseClient;
ctx: DocToolContext;
allowedToolIds: Set<string>;
// 说明:Convex 迁移阶段用于“去 Supabase 化”。如果提供该能力,则完全不依赖 Supabase。
loadBlocks?: () => Promise<{ blocks: unknown[]; source: string }>;
saveBlocks?: (blocks: unknown[]) => Promise<void>;
}) => {
const loadBlocks = async () => {
if (args.loadBlocks) return await args.loadBlocks();
if (!args.supabase) throw new Error("缺少 Supabase 数据源(请在 Convex 模式下传入 loadBlocks/saveBlocks");
return await loadDocumentBlocks(args.supabase, args.ctx);
};
const saveBlocks = async (blocks: unknown[]) => {
if (args.saveBlocks) {
await args.saveBlocks(blocks);
return;
}
if (!args.supabase) throw new Error("缺少 Supabase 数据源(请在 Convex 模式下传入 loadBlocks/saveBlocks");
await saveDocumentBlocks(args.supabase, args.ctx, blocks);
};
const run = async (toolId: string, toolArgs: Record<string, unknown>) => {
if (!args.allowedToolIds.has(toolId)) {
throw new Error(`工具未被允许:${toolId}`);
@@ -174,7 +192,7 @@ export const createDocServerTools = (args: {
if (toolId === "doc_get") {
const maxNodesRaw = Number(toolArgs.maxBlocks ?? 80);
const maxBlocks = Math.max(10, Math.min(240, Number.isFinite(maxNodesRaw) ? Math.floor(maxNodesRaw) : 80));
const { blocks, source } = await loadDocumentBlocks(args.supabase, args.ctx);
const { blocks, source } = await loadBlocks();
const summary = walkSummaries(blocks, maxBlocks);
return { ok: true, source, totalTopLevelBlocks: blocks.length, blocks: summary };
}
@@ -184,7 +202,7 @@ export const createDocServerTools = (args: {
if (!query) throw new Error("缺少 query");
const maxRaw = Number(toolArgs.maxResults ?? 8);
const maxResults = Math.max(1, Math.min(30, Number.isFinite(maxRaw) ? Math.floor(maxRaw) : 8));
const { blocks, source } = await loadDocumentBlocks(args.supabase, args.ctx);
const { blocks, source } = await loadBlocks();
const summary = walkSummaries(blocks, 400);
const q = query.toLowerCase();
const hits = summary.filter((x) => x.text.toLowerCase().includes(q)).slice(0, maxResults);
@@ -207,7 +225,7 @@ export const createDocServerTools = (args: {
const created = specs.map(buildBlockFromSpec);
const { blocks, source } = await loadDocumentBlocks(args.supabase, args.ctx);
const { blocks, source } = await loadBlocks();
const targetId = beforeBlockId || afterBlockId;
const found = targetId ? findContainerById(blocks, targetId) : null;
if (targetId && !found) {
@@ -221,7 +239,7 @@ export const createDocServerTools = (args: {
found.container.splice(insertAt, 0, ...created);
}
await saveDocumentBlocks(args.supabase, args.ctx, blocks);
await saveBlocks(blocks);
return {
ok: true,
source,
@@ -238,7 +256,7 @@ export const createDocServerTools = (args: {
const modeRaw = String(toolArgs.mode ?? "replace").trim();
const mode = modeRaw === "append" || modeRaw === "prepend" ? modeRaw : "replace";
const { blocks, source } = await loadDocumentBlocks(args.supabase, args.ctx);
const { blocks, source } = await loadBlocks();
const found = findContainerById(blocks, blockId);
if (!found) throw new Error(`未找到 blockId${blockId}`);
const block = found.container[found.index];
@@ -247,7 +265,7 @@ export const createDocServerTools = (args: {
const nextText = mode === "append" ? `${prevText}${text}` : mode === "prepend" ? `${text}${prevText}` : text;
found.container[found.index] = { ...block, content: createTextContent(nextText) };
await saveDocumentBlocks(args.supabase, args.ctx, blocks);
await saveBlocks(blocks);
return { ok: true, source, blockId, mode, data: blocks };
}
@@ -45,9 +45,42 @@ const loadWorkspaceIds = async (supabase: DocsSupabaseClient, userId: string) =>
};
export const createDocsServerTools = (args: {
supabase: DocsSupabaseClient;
supabase?: DocsSupabaseClient;
ctx: DocsToolContext;
allowedToolIds: Set<string>;
// 说明:Convex 迁移阶段用于“去 Supabase 化”。如果提供该能力,则完全不依赖 Supabase。
searchDocs?: (args: {
userId: string;
query: string;
limit: number;
workspaceId: string | null;
includeDeleted: boolean;
}) => Promise<
Array<{
id: string;
title: string;
workspaceId: string;
parentId: string | null;
updatedAt: unknown;
snippet: string;
}>
>;
readDoc?: (args: {
userId: string;
documentId: string;
maxChars: number;
includeContent: boolean;
}) => Promise<{
ok: true;
documentId: string;
title: string;
workspaceId: string;
parentId: string | null;
updatedAt: unknown;
rawTextLength: number;
rawText: string;
content?: unknown;
}>;
}) => {
const run = async (toolId: string, toolArgs: Record<string, unknown>) => {
if (!args.allowedToolIds.has(toolId)) {
@@ -62,6 +95,21 @@ export const createDocsServerTools = (args: {
const workspaceId = String(toolArgs.workspaceId ?? "").trim() || null;
const includeDeleted = Boolean(toolArgs.includeDeleted ?? false);
if (args.searchDocs) {
const results = await args.searchDocs({
userId: args.ctx.userId,
query,
limit,
workspaceId,
includeDeleted,
});
return { ok: true, query, results };
}
if (!args.supabase) {
throw new Error("缺少 Supabase 数据源(请在 Convex 模式下传入 searchDocs/readDoc");
}
const wsIds = await loadWorkspaceIds(args.supabase, args.ctx.userId);
const wsFilter = workspaceId ? [workspaceId] : wsIds;
if (wsFilter.length === 0) return { ok: true, query, results: [] };
@@ -108,6 +156,19 @@ export const createDocsServerTools = (args: {
const maxChars = Math.max(200, Math.min(20_000, Number.isFinite(maxCharsRaw) ? Math.floor(maxCharsRaw) : 2500));
const includeContent = Boolean(toolArgs.includeContent ?? false);
if (args.readDoc) {
return await args.readDoc({
userId: args.ctx.userId,
documentId,
maxChars,
includeContent,
});
}
if (!args.supabase) {
throw new Error("缺少 Supabase 数据源(请在 Convex 模式下传入 searchDocs/readDoc");
}
const { data, error } = await args.supabase
.from("documents")
.select(includeContent ? "id,title,raw_text,content,workspace_id,parent_id,updated_at" : "id,title,raw_text,workspace_id,parent_id,updated_at")
@@ -30,9 +30,12 @@ const resolveAttachment = (ctx: MediaToolContext, ref: string): ResolvedAttachme
const pick = (obj: unknown, key: string) => (isRecord(obj) ? obj[key] : undefined);
export const createMediaServerTools = (args: {
supabase: MediaSupabaseClient;
supabase?: MediaSupabaseClient;
ctx: MediaToolContext;
allowedToolIds: Set<string>;
// 说明:Convex 迁移阶段用于“去 Supabase 化”。如果提供该能力,则完全不依赖 Supabase。
loadById?: (id: string) => Promise<unknown | null>;
loadByFileUrl?: (fileUrl: string) => Promise<unknown | null>;
}) => {
const run = async (toolId: string, toolArgs: Record<string, unknown>) => {
if (!args.allowedToolIds.has(toolId)) throw new Error(`工具未被允许:${toolId}`);
@@ -48,27 +51,37 @@ export const createMediaServerTools = (args: {
let row: unknown = null;
if (targetAssetId) {
const { data, error } = await args.supabase
.from("media_assets")
.select("id,file_name,file_url,mime_type,ocr_text,ocr_status,ocr_payload,storage_path,bucket,document_id,workspace_id,deleted_at,purged_at,updated_at")
.eq("id", targetAssetId)
.single();
if (error) {
throw new Error(isRecord(error) && typeof error.message === "string" ? error.message : "读取图片失败");
if (args.loadById) {
row = await args.loadById(targetAssetId);
} else {
if (!args.supabase) throw new Error("缺少 Supabase 数据源(请在 Convex 模式下传入 loadById/loadByFileUrl");
const { data, error } = await args.supabase
.from("media_assets")
.select("id,file_name,file_url,mime_type,ocr_text,ocr_status,ocr_payload,storage_path,bucket,document_id,workspace_id,deleted_at,purged_at,updated_at")
.eq("id", targetAssetId)
.single();
if (error) {
throw new Error(isRecord(error) && typeof error.message === "string" ? error.message : "读取图片失败");
}
row = data;
}
row = data;
} else if (targetUrl) {
const { data, error } = await args.supabase
.from("media_assets")
.select("id,file_name,file_url,mime_type,ocr_text,ocr_status,ocr_payload,storage_path,bucket,document_id,workspace_id,deleted_at,purged_at,updated_at")
.eq("file_url", targetUrl)
.order("updated_at", { ascending: false })
.limit(1)
.maybeSingle();
if (error) {
throw new Error(isRecord(error) && typeof error.message === "string" ? error.message : "读取图片失败");
if (args.loadByFileUrl) {
row = await args.loadByFileUrl(targetUrl);
} else {
if (!args.supabase) throw new Error("缺少 Supabase 数据源(请在 Convex 模式下传入 loadById/loadByFileUrl");
const { data, error } = await args.supabase
.from("media_assets")
.select("id,file_name,file_url,mime_type,ocr_text,ocr_status,ocr_payload,storage_path,bucket,document_id,workspace_id,deleted_at,purged_at,updated_at")
.eq("file_url", targetUrl)
.order("updated_at", { ascending: false })
.limit(1)
.maybeSingle();
if (error) {
throw new Error(isRecord(error) && typeof error.message === "string" ? error.message : "读取图片失败");
}
row = data;
}
row = data;
} else {
throw new Error("缺少 assetId / fileUrl / attachmentRef");
}
@@ -109,4 +122,3 @@ export const createMediaServerTools = (args: {
return { run };
};
@@ -188,13 +188,25 @@ const sanitizeAddChildOps = (args: {
};
export const createMindmapServerTools = (args: {
supabase: SupabaseRouteClient;
supabase?: SupabaseRouteClient;
ctx: MindmapToolContext;
cfg: OpenAiCompatibleChatOptions;
allowedToolIds: Set<string>;
// 说明:Convex 迁移阶段用于“去 Supabase 化”。如果提供该能力,则完全不依赖 Supabase/local 文件。
loadMindmap?: () => Promise<{
doc: { id: string; title: string | null; workspace_id: string | null };
base: MindmapTreeNode;
}>;
saveMindmap?: (args: {
doc: { id: string; title: string | null; workspace_id: string | null };
data: MindmapTreeNode;
}) => Promise<void>;
}) => {
const loadDoc = async () => {
const { documentId, userId } = args.ctx;
if (!args.supabase) {
throw new Error("缺少 Supabase 数据源(请在 Convex 模式下传入 loadMindmap/saveMindmap");
}
const query = args.supabase
.from("documents")
.select("id,title,workspace_id,mindmap_data")
@@ -210,6 +222,11 @@ export const createMindmapServerTools = (args: {
};
const loadMindmap = async () => {
if (args.loadMindmap) {
const loaded = await args.loadMindmap();
ensureMindmapUids(loaded.base);
return loaded;
}
const doc = await loadDoc();
const local = await readMindmapLocal(args.ctx.documentId, args.ctx.mindmapId);
const base = (local.ok ? local.data : (doc.mindmap_data ?? defaultMindmapData)) as MindmapTreeNode;
@@ -217,6 +234,14 @@ export const createMindmapServerTools = (args: {
return { doc, base };
};
const persistMindmap = async (doc: { id: string; title: string | null; workspace_id: string | null }, nextData: MindmapTreeNode) => {
if (args.saveMindmap) {
await args.saveMindmap({ doc, data: nextData });
return;
}
await writeMindmapLocal(args.ctx.documentId, args.ctx.mindmapId, nextData, doc.title ?? "无标题");
};
const mindmap_get = async (toolArgs: Record<string, unknown>) => {
const maxNodes = Number(toolArgs.maxNodes ?? 120);
const { base } = await loadMindmap();
@@ -313,7 +338,7 @@ export const createMindmapServerTools = (args: {
const { doc, base } = await loadMindmap();
const { data: nextData, applied, errors } = applyMindmapOps(base, normalized);
await writeMindmapLocal(args.ctx.documentId, args.ctx.mindmapId, nextData, doc.title ?? "无标题");
await persistMindmap(doc, nextData);
return { ok: true, applied, errors, data: nextData, meta: { reason } };
};
@@ -334,7 +359,7 @@ export const createMindmapServerTools = (args: {
node: { text, ...(hyperlink ? { hyperlink } : {}), ...(note ? { note } : {}), ...(refs.length ? { refs } : {}) },
};
const { data: nextData, applied, errors } = applyMindmapOps(base, [op]);
await writeMindmapLocal(args.ctx.documentId, args.ctx.mindmapId, nextData, doc.title ?? "无标题");
await persistMindmap(doc, nextData);
return { ok: true, applied, errors, data: nextData, meta: { reason } };
};
@@ -355,7 +380,7 @@ export const createMindmapServerTools = (args: {
node: { text, ...(hyperlink ? { hyperlink } : {}), ...(note ? { note } : {}), ...(refs.length ? { refs } : {}) },
};
const { data: nextData, applied, errors } = applyMindmapOps(base, [op]);
await writeMindmapLocal(args.ctx.documentId, args.ctx.mindmapId, nextData, doc.title ?? "无标题");
await persistMindmap(doc, nextData);
return { ok: true, applied, errors, data: nextData, meta: { reason } };
};
@@ -368,7 +393,7 @@ export const createMindmapServerTools = (args: {
const { doc, base } = await loadMindmap();
const op: MindmapOp = { op: "updateText", uid, text };
const { data: nextData, applied, errors } = applyMindmapOps(base, [op]);
await writeMindmapLocal(args.ctx.documentId, args.ctx.mindmapId, nextData, doc.title ?? "无标题");
await persistMindmap(doc, nextData);
return { ok: true, applied, errors, data: nextData, meta: { reason } };
};
@@ -384,7 +409,7 @@ export const createMindmapServerTools = (args: {
const { doc, base } = await loadMindmap();
const op: MindmapOp = { op: "setHyperlink", uid, hyperlink: hyperlinkRaw === null ? null : hyperlink };
const { data: nextData, applied, errors } = applyMindmapOps(base, [op]);
await writeMindmapLocal(args.ctx.documentId, args.ctx.mindmapId, nextData, doc.title ?? "无标题");
await persistMindmap(doc, nextData);
return { ok: true, applied, errors, data: nextData, meta: { reason } };
};
@@ -397,7 +422,7 @@ export const createMindmapServerTools = (args: {
const { doc, base } = await loadMindmap();
const op: MindmapOp = { op: "appendNote", uid, markdown };
const { data: nextData, applied, errors } = applyMindmapOps(base, [op]);
await writeMindmapLocal(args.ctx.documentId, args.ctx.mindmapId, nextData, doc.title ?? "无标题");
await persistMindmap(doc, nextData);
return { ok: true, applied, errors, data: nextData, meta: { reason } };
};
@@ -410,7 +435,7 @@ export const createMindmapServerTools = (args: {
const { doc, base } = await loadMindmap();
const op: MindmapOp = { op: "setRefs", uid, refs };
const { data: nextData, applied, errors } = applyMindmapOps(base, [op]);
await writeMindmapLocal(args.ctx.documentId, args.ctx.mindmapId, nextData, doc.title ?? "无标题");
await persistMindmap(doc, nextData);
return { ok: true, applied, errors, data: nextData, meta: { reason } };
};
@@ -421,7 +446,7 @@ export const createMindmapServerTools = (args: {
const { doc, base } = await loadMindmap();
const op: MindmapOp = { op: "deleteNode", uid };
const { data: nextData, applied, errors } = applyMindmapOps(base, [op]);
await writeMindmapLocal(args.ctx.documentId, args.ctx.mindmapId, nextData, doc.title ?? "无标题");
await persistMindmap(doc, nextData);
return { ok: true, applied, errors, data: nextData, meta: { reason } };
};
@@ -493,7 +518,7 @@ export const createMindmapServerTools = (args: {
const nextRefs = mode === "replace" ? [ref] : mergeRefsUnique(prevRefs, [ref]);
const op: MindmapOp = { op: "setRefs", uid, refs: nextRefs };
const { data: nextData, applied, errors } = applyMindmapOps(base, [op]);
await writeMindmapLocal(args.ctx.documentId, args.ctx.mindmapId, nextData, doc.title ?? "无标题");
await persistMindmap(doc, nextData);
return { ok: true, applied, errors, data: nextData, meta: { reason } };
};
@@ -546,7 +571,7 @@ export const createMindmapServerTools = (args: {
node: { text, ...(hyperlink ? { hyperlink } : {}), refs: [ref], ...(note ? { note } : {}) },
};
const { data: nextData, applied, errors } = applyMindmapOps(base, [op]);
await writeMindmapLocal(args.ctx.documentId, args.ctx.mindmapId, nextData, doc.title ?? "无标题");
await persistMindmap(doc, nextData);
return { ok: true, applied, errors, data: nextData, meta: { reason } };
};
@@ -579,7 +604,7 @@ export const createMindmapServerTools = (args: {
const { doc, base } = await loadMindmap();
const { data: nextData, applied, errors } = applyMindmapOps(base, [op]);
await writeMindmapLocal(args.ctx.documentId, args.ctx.mindmapId, nextData, doc.title ?? "无标题");
await persistMindmap(doc, nextData);
return { ok: true, applied, errors, data: nextData, meta: { reason } };
};
@@ -600,7 +625,7 @@ export const createMindmapServerTools = (args: {
const { doc, base } = await loadMindmap();
const op: MindmapOp = { op: "appendNote", uid, markdown };
const { data: nextData, applied, errors } = applyMindmapOps(base, [op]);
await writeMindmapLocal(args.ctx.documentId, args.ctx.mindmapId, nextData, doc.title ?? "无标题");
await persistMindmap(doc, nextData);
return {
ok: true,
applied,
@@ -701,7 +726,7 @@ export const createMindmapServerTools = (args: {
const fixed = sanitizeAddChildOps({ targetUid, currentChildren, ops, searxResults });
const { data: nextData, applied, errors } = applyMindmapOps(base, fixed);
await writeMindmapLocal(args.ctx.documentId, args.ctx.mindmapId, nextData, doc.title ?? "无标题");
await persistMindmap(doc, nextData);
return {
ok: true,
@@ -63,9 +63,27 @@ const parseSlash = (text: string): ParsedSlash => {
};
export const createSlashServerTools = (args: {
supabase: SlashSupabaseClient;
supabase?: SlashSupabaseClient;
ctx: SlashToolContext;
allowedToolIds: Set<string>;
// 说明:Convex 迁移阶段用于“去 Supabase 化”。如果提供该能力,则完全不依赖 Supabase。
loadWorkspaceIds?: (userId: string) => Promise<string[]>;
inferWorkspaceIdFromDoc?: (documentId: string) => Promise<string | null>;
createDoc?: (args: { userId: string; workspaceId: string; parentId: string | null; title: string }) => Promise<{
id: string;
title: string;
workspaceId: string;
parentId: string | null;
createdAt: unknown;
updatedAt: unknown;
}>;
renameDoc?: (args: { userId: string; documentId: string; title: string }) => Promise<{
id: string;
title: string;
workspaceId: string;
parentId: string | null;
updatedAt: unknown;
}>;
}) => {
const run = async (toolId: string, toolArgs: Record<string, unknown>) => {
if (!args.allowedToolIds.has(toolId)) throw new Error(`工具未被允许:${toolId}`);
@@ -106,11 +124,35 @@ export const createSlashServerTools = (args: {
const workspaceIdFromParams = parsed.params.workspaceId ? String(parsed.params.workspaceId) : null;
const workspaceId =
workspaceIdFromParams ||
(args.ctx.currentDocumentId ? await inferWorkspaceIdFromDoc(args.supabase, args.ctx.currentDocumentId) : null) ||
(await loadWorkspaceIds(args.supabase, args.ctx.userId))[0] ||
(args.ctx.currentDocumentId
? args.inferWorkspaceIdFromDoc
? await args.inferWorkspaceIdFromDoc(args.ctx.currentDocumentId)
: args.supabase
? await inferWorkspaceIdFromDoc(args.supabase, args.ctx.currentDocumentId)
: null
: null) ||
((args.loadWorkspaceIds
? (await args.loadWorkspaceIds(args.ctx.userId))[0]
: args.supabase
? (await loadWorkspaceIds(args.supabase, args.ctx.userId))[0]
: null) ?? null) ||
null;
if (!workspaceId) throw new Error("无法推断 workspaceId(请在 params.workspaceId 指定)");
if (args.createDoc) {
const doc = await args.createDoc({
userId: args.ctx.userId,
workspaceId,
parentId,
title,
});
return { ok: true, command: "new_doc", document: doc };
}
if (!args.supabase) {
throw new Error("缺少 Supabase 数据源(请在 Convex 模式下传入 createDoc/renameDoc");
}
const payload = {
workspace_id: workspaceId,
user_id: args.ctx.userId,
@@ -142,6 +184,15 @@ export const createSlashServerTools = (args: {
const title = String(parsed.params.title ?? "").trim();
if (!documentId || !title) throw new Error("缺少 documentId 或 title");
if (args.renameDoc) {
const doc = await args.renameDoc({ userId: args.ctx.userId, documentId, title });
return { ok: true, command: "rename_doc", document: doc };
}
if (!args.supabase) {
throw new Error("缺少 Supabase 数据源(请在 Convex 模式下传入 createDoc/renameDoc");
}
const { data, error } = await args.supabase
.from("documents")
.update({ title })
@@ -172,4 +223,3 @@ export const createSlashServerTools = (args: {
return { run };
};