0.2.1 onlyoffice修复
This commit is contained in:
@@ -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 };
|
||||
};
|
||||
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
import type { AuthContext } from "@/lib/auth/types";
|
||||
import { getDevUser, isDevAuthEnabled } from "@/lib/auth/devUser";
|
||||
|
||||
export class HttpError extends Error {
|
||||
status: number;
|
||||
|
||||
constructor(status: number, message: string) {
|
||||
super(message);
|
||||
this.name = "HttpError";
|
||||
this.status = status;
|
||||
}
|
||||
}
|
||||
|
||||
export function getAuthContext(): AuthContext {
|
||||
if (isDevAuthEnabled()) return getDevUser();
|
||||
// 说明:后续接入真实鉴权时,在这里替换为 Supabase/Convex Auth 的校验逻辑。
|
||||
throw new Error("Auth is not configured");
|
||||
}
|
||||
|
||||
export function requireAuthContext(): AuthContext {
|
||||
try {
|
||||
return getAuthContext();
|
||||
} catch (err) {
|
||||
const message = err instanceof Error ? err.message : "Unauthorized";
|
||||
throw new HttpError(401, message);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
import type { AuthContext } from "@/lib/auth/types";
|
||||
|
||||
function _readEnv(key: string): string | undefined {
|
||||
const value = process.env[key];
|
||||
if (!value) return undefined;
|
||||
const trimmed = value.trim();
|
||||
return trimmed ? trimmed : undefined;
|
||||
}
|
||||
|
||||
export function getDevUser(): AuthContext {
|
||||
// 说明:第三阶段先用固定用户跑通迁移链路,后续接入真实鉴权时再替换这一层。
|
||||
const userId = _readEnv("DEV_USER_ID") ?? "dev-user";
|
||||
const email = _readEnv("DEV_USER_EMAIL") ?? "dev@mnote.local";
|
||||
const name = _readEnv("DEV_USER_NAME") ?? "开发用户";
|
||||
return { userId, email, name };
|
||||
}
|
||||
|
||||
export function isDevAuthEnabled(): boolean {
|
||||
// 说明:目前只要启用了 USE_CONVEX,就默认启用固定用户鉴权(便于迁移与测试)。
|
||||
return process.env.USE_CONVEX === "1";
|
||||
}
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
export type AuthContext = {
|
||||
userId: string;
|
||||
email?: string;
|
||||
name?: string;
|
||||
};
|
||||
|
||||
@@ -0,0 +1,114 @@
|
||||
import type { Json } from "@/types/supabase";
|
||||
|
||||
type BlockLike = {
|
||||
id: string;
|
||||
type?: string;
|
||||
props?: Record<string, unknown>;
|
||||
content?: unknown;
|
||||
children?: BlockLike[];
|
||||
};
|
||||
|
||||
const asBlockArray = (value: unknown): BlockLike[] => {
|
||||
if (!Array.isArray(value)) return [];
|
||||
return value.filter((b) => b && typeof b === "object" && typeof (b as { id?: unknown }).id === "string") as BlockLike[];
|
||||
};
|
||||
|
||||
export const findBlockInTree = (
|
||||
blocks: BlockLike[],
|
||||
blockId: string,
|
||||
): { block: BlockLike; parent: BlockLike | null; index: number } | null => {
|
||||
const stack: Array<{ list: BlockLike[]; parent: BlockLike | null }> = [{ list: blocks, parent: null }];
|
||||
while (stack.length) {
|
||||
const item = stack.pop()!;
|
||||
const list = item.list;
|
||||
for (let i = 0; i < list.length; i += 1) {
|
||||
const b = list[i]!;
|
||||
if (b.id === blockId) {
|
||||
return { block: b, parent: item.parent, index: i };
|
||||
}
|
||||
if (Array.isArray(b.children) && b.children.length > 0) {
|
||||
stack.push({ list: b.children, parent: b });
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
const cloneBlock = (block: BlockLike): BlockLike => {
|
||||
return {
|
||||
...block,
|
||||
props: block.props ? { ...block.props } : undefined,
|
||||
content: Array.isArray(block.content) ? [...block.content] : block.content,
|
||||
children: Array.isArray(block.children) ? block.children.map(cloneBlock) : block.children,
|
||||
};
|
||||
};
|
||||
|
||||
export const removeBlockSubtree = (
|
||||
blocks: BlockLike[],
|
||||
blockId: string,
|
||||
): { removed: BlockLike | null; nextBlocks: BlockLike[] } => {
|
||||
// 说明:这里不直接修改入参 blocks,返回新的 nextBlocks。
|
||||
const nextTop = blocks.map(cloneBlock);
|
||||
const hit = findBlockInTree(nextTop, blockId);
|
||||
if (!hit) return { removed: null, nextBlocks: nextTop };
|
||||
|
||||
if (hit.parent) {
|
||||
const parent = hit.parent;
|
||||
const nextChildren = asBlockArray(parent.children).map(cloneBlock);
|
||||
const removed = nextChildren.splice(hit.index, 1)[0] ?? null;
|
||||
parent.children = nextChildren;
|
||||
return { removed, nextBlocks: nextTop };
|
||||
}
|
||||
|
||||
const removed = nextTop.splice(hit.index, 1)[0] ?? null;
|
||||
return { removed, nextBlocks: nextTop };
|
||||
};
|
||||
|
||||
export const replaceBlockInTree = (
|
||||
blocks: BlockLike[],
|
||||
blockId: string,
|
||||
nextBlock: BlockLike,
|
||||
): { ok: boolean; nextBlocks: BlockLike[] } => {
|
||||
const nextTop = blocks.map(cloneBlock);
|
||||
const hit = findBlockInTree(nextTop, blockId);
|
||||
if (!hit) return { ok: false, nextBlocks: nextTop };
|
||||
|
||||
const normalized = cloneBlock({ ...nextBlock, id: blockId });
|
||||
if (hit.parent) {
|
||||
const parent = hit.parent;
|
||||
const nextChildren = asBlockArray(parent.children).map(cloneBlock);
|
||||
nextChildren[hit.index] = normalized;
|
||||
parent.children = nextChildren;
|
||||
return { ok: true, nextBlocks: nextTop };
|
||||
}
|
||||
|
||||
nextTop[hit.index] = normalized;
|
||||
return { ok: true, nextBlocks: nextTop };
|
||||
};
|
||||
|
||||
export const extractBlockText = (block: BlockLike): string => {
|
||||
const inline = Array.isArray(block.content) ? (block.content as Array<{ text?: unknown }>) : [];
|
||||
const text = inline.map((n) => (typeof n?.text === "string" ? n.text : "")).join("");
|
||||
return text.trim();
|
||||
};
|
||||
|
||||
export const getBlocksFromDocumentContent = (content: unknown): BlockLike[] => {
|
||||
if (Array.isArray(content)) return asBlockArray(content);
|
||||
if (content && typeof content === "object") {
|
||||
const blocks = (content as { blocks?: unknown }).blocks;
|
||||
return asBlockArray(blocks);
|
||||
}
|
||||
return [];
|
||||
};
|
||||
|
||||
export const withBlocksWrittenBack = (content: unknown, blocks: BlockLike[]): Json => {
|
||||
// 复用现有结构:数组或 {blocks: []}
|
||||
if (Array.isArray(content)) {
|
||||
return blocks as unknown as Json;
|
||||
}
|
||||
if (content && typeof content === "object") {
|
||||
return { ...(content as Record<string, unknown>), blocks } as Json;
|
||||
}
|
||||
return { blocks } as Json;
|
||||
};
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
export { api, internal } from "../../../convex/_generated/api";
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
export function isConvexEnabled(): boolean {
|
||||
return process.env.USE_CONVEX === "1";
|
||||
}
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
import type { ConvexHttpClient } from "convex/browser";
|
||||
import type { AuthContext } from "@/lib/auth/types";
|
||||
import { requireAuthContext } from "@/lib/auth/authContext";
|
||||
import { getConvexHttpClient } from "@/lib/convex/server";
|
||||
|
||||
export function getAuthedConvexClient(): { auth: AuthContext; client: ConvexHttpClient } {
|
||||
const auth = requireAuthContext();
|
||||
const client = getConvexHttpClient();
|
||||
return { auth, client };
|
||||
}
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
import { ConvexHttpClient } from "convex/browser";
|
||||
|
||||
let cached: ConvexHttpClient | null = null;
|
||||
|
||||
export function getConvexHttpClient(): ConvexHttpClient {
|
||||
if (cached) return cached;
|
||||
|
||||
const url = process.env.CONVEX_SELF_HOSTED_URL ?? process.env.NEXT_PUBLIC_CONVEX_URL;
|
||||
if (!url) {
|
||||
throw new Error("缺少 CONVEX_SELF_HOSTED_URL / NEXT_PUBLIC_CONVEX_URL 配置");
|
||||
}
|
||||
|
||||
const client = new ConvexHttpClient(url);
|
||||
const adminKey = process.env.CONVEX_SELF_HOSTED_ADMIN_KEY;
|
||||
if (adminKey) {
|
||||
client.setAdminAuth(adminKey);
|
||||
}
|
||||
|
||||
cached = client;
|
||||
return client;
|
||||
}
|
||||
|
||||
@@ -1,4 +1,9 @@
|
||||
export type MnoteRuntimeConfig = {
|
||||
/**
|
||||
* 是否启用 Convex(自部署)链路。
|
||||
* 说明:该字段由服务端在运行期注入到 window.__MNOTE_RUNTIME_CONFIG__,用于客户端按需关闭 Supabase 相关能力。
|
||||
*/
|
||||
useConvex?: boolean;
|
||||
supabaseUrl?: string;
|
||||
/**
|
||||
* 服务端/本机回源用的 Supabase 地址(通常是 HTTP),用于避免 FRP/自签证书导致 Node 侧 TLS 校验失败。
|
||||
@@ -39,10 +44,11 @@ declare global {
|
||||
}
|
||||
|
||||
const readFromEnv = (): MnoteRuntimeConfig => ({
|
||||
useConvex: process.env.USE_CONVEX === "1",
|
||||
supabaseUrl: process.env.NEXT_PUBLIC_SUPABASE_URL,
|
||||
supabaseInternalUrl: process.env.SUPABASE_INTERNAL_URL,
|
||||
supabaseAnonKey: process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY,
|
||||
backendUrl: process.env.NEXT_PUBLIC_BACKEND_URL ?? process.env.BACKEND_URL,
|
||||
backendUrl: process.env.NEXT_PUBLIC_BACKEND_URL ?? process.env.BACKEND_URL,
|
||||
onlyofficeBaseUrl: process.env.NEXT_PUBLIC_ONLYOFFICE_BASE_URL,
|
||||
onlyofficeStorageHostOverride: process.env.NEXT_PUBLIC_ONLYOFFICE_STORAGE_HOST_OVERRIDE,
|
||||
onlyofficeProxyOrigin: process.env.NEXT_PUBLIC_ONLYOFFICE_PROXY_ORIGIN,
|
||||
|
||||
Reference in New Issue
Block a user