0.1.14 上线前更改
This commit is contained in:
@@ -0,0 +1,384 @@
|
||||
import supabaseAdmin from "@/lib/supabase/admin";
|
||||
import { applyMindmapOps, ensureMindmapUids, type MindmapOp, type MindmapTreeNode, type NodeRef } from "@/lib/mindmap/mindmapOps";
|
||||
import { readMindmapLocal, writeMindmapLocal } from "@/lib/mindmap/mindmapLocalStore";
|
||||
|
||||
type SupabaseRouteClient = {
|
||||
from: (table: string) => any;
|
||||
};
|
||||
|
||||
export type OnlyOfficeSupabaseClient = SupabaseRouteClient;
|
||||
|
||||
export type OnlyOfficeToolContext = {
|
||||
userId: string;
|
||||
documentId?: string;
|
||||
// 来自前端(@ 选择/上传)的附件列表,优先使用(避免额外查询)
|
||||
attachments?: Array<{ id: string; title: string; fileUrl: string; mimeType?: string | null }>;
|
||||
};
|
||||
|
||||
const isRecord = (v: unknown): v is Record<string, unknown> =>
|
||||
typeof v === "object" && v !== null && !Array.isArray(v);
|
||||
|
||||
type ResolvedAttachment = { id: string; title: string; fileUrl: string; mimeType?: string | null };
|
||||
|
||||
const resolveAttachment = (ctx: OnlyOfficeToolContext, ref: string): ResolvedAttachment | null => {
|
||||
const s = String(ref || "").trim();
|
||||
if (!s) return null;
|
||||
const list = Array.isArray(ctx.attachments) ? ctx.attachments : [];
|
||||
const byId = list.find((a) => String(a.id) === s);
|
||||
if (byId) return { id: String(byId.id), title: String(byId.title || byId.id), fileUrl: String(byId.fileUrl || ""), mimeType: byId.mimeType ?? null };
|
||||
const exactTitle = list.find((a) => String(a.title) === s);
|
||||
if (exactTitle)
|
||||
return { id: String(exactTitle.id), title: String(exactTitle.title || exactTitle.id), fileUrl: String(exactTitle.fileUrl || ""), mimeType: exactTitle.mimeType ?? null };
|
||||
const fuzzy = list.find((a) => String(a.title || "").includes(s) || String(a.fileUrl || "").includes(s));
|
||||
if (fuzzy) return { id: String(fuzzy.id), title: String(fuzzy.title || fuzzy.id), fileUrl: String(fuzzy.fileUrl || ""), mimeType: fuzzy.mimeType ?? null };
|
||||
return null;
|
||||
};
|
||||
|
||||
const parseStoragePath = (fileUrl: string) => {
|
||||
try {
|
||||
const url = new URL(fileUrl);
|
||||
const segments = url.pathname.split("/").filter(Boolean);
|
||||
const objectIdx = segments.findIndex((seg) => seg === "object");
|
||||
if (objectIdx === -1 || objectIdx + 2 >= segments.length) return null;
|
||||
if (segments[objectIdx + 1] === "public") {
|
||||
const bucket = segments[objectIdx + 2];
|
||||
const path = segments.slice(objectIdx + 3).join("/");
|
||||
return { bucket, path };
|
||||
}
|
||||
if (segments[objectIdx + 1] === "sign") {
|
||||
const bucket = segments[objectIdx + 2];
|
||||
const path = segments.slice(objectIdx + 3).join("/");
|
||||
return { bucket, path };
|
||||
}
|
||||
return null;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
const guessExt = (fileNameOrUrl: string) => {
|
||||
const s = String(fileNameOrUrl || "").trim();
|
||||
if (!s) return "";
|
||||
const cleaned = s.split("?")[0].split("#")[0];
|
||||
const parts = cleaned.split(".");
|
||||
if (parts.length < 2) return "";
|
||||
return String(parts[parts.length - 1] || "").toLowerCase();
|
||||
};
|
||||
|
||||
const isPdf = (mimeType: string, fileName: string) => {
|
||||
const m = String(mimeType || "").toLowerCase();
|
||||
if (m.includes("pdf")) return true;
|
||||
return guessExt(fileName) === "pdf";
|
||||
};
|
||||
|
||||
const signForDownload = async (row: Record<string, unknown>) => {
|
||||
const bucket = typeof row.bucket === "string" ? row.bucket : "";
|
||||
const storagePath = typeof row.storage_path === "string" ? row.storage_path : "";
|
||||
const fileUrl = typeof row.file_url === "string" ? row.file_url : "";
|
||||
const fileName = typeof row.file_name === "string" ? row.file_name : undefined;
|
||||
|
||||
if (bucket && storagePath) {
|
||||
const { data, error } = await supabaseAdmin.storage.from(bucket).createSignedUrl(storagePath, 60 * 60, { download: fileName });
|
||||
if (error || !data?.signedUrl) throw new Error(error?.message ?? "生成签名 URL 失败");
|
||||
return data.signedUrl;
|
||||
}
|
||||
|
||||
const parsed = fileUrl ? parseStoragePath(fileUrl) : null;
|
||||
if (!parsed) return fileUrl;
|
||||
|
||||
const { data, error } = await supabaseAdmin.storage.from(parsed.bucket).createSignedUrl(parsed.path, 60 * 60, { download: fileName });
|
||||
if (error || !data?.signedUrl) throw new Error(error?.message ?? "生成签名 URL 失败");
|
||||
return data.signedUrl;
|
||||
};
|
||||
|
||||
const downloadBytes = async (url: string) => {
|
||||
const resp = await fetch(url);
|
||||
if (!resp.ok) throw new Error(`下载失败:${resp.status} ${resp.statusText}`);
|
||||
return await resp.arrayBuffer();
|
||||
};
|
||||
|
||||
const mineruParseContentList = async (args: { bytes: ArrayBuffer; filename: string; mimeType: string }) => {
|
||||
const endpoint = (process.env.MINERU_ENDPOINT || "").trim().replace(/\/$/, "");
|
||||
if (!endpoint) throw new Error("缺少 MINERU_ENDPOINT,无法解析 PDF 结构");
|
||||
|
||||
const form = new FormData();
|
||||
form.append("files", new Blob([args.bytes], { type: args.mimeType || "application/octet-stream" }), args.filename || "file.pdf");
|
||||
form.append("output_dir", "./output");
|
||||
form.append("lang_list", "ch");
|
||||
form.append("backend", "pipeline");
|
||||
form.append("parse_method", "auto");
|
||||
form.append("return_md", "false");
|
||||
form.append("return_middle_json", "false");
|
||||
form.append("return_model_output", "false");
|
||||
form.append("return_content_list", "true");
|
||||
form.append("return_images", "false");
|
||||
form.append("response_format_zip", "false");
|
||||
|
||||
const resp = await fetch(`${endpoint}/file_parse`, { method: "POST", body: form });
|
||||
if (!resp.ok) {
|
||||
const text = await resp.text().catch(() => "");
|
||||
throw new Error(`MinerU 解析失败:${resp.status} ${resp.statusText}${text ? `,${text.slice(0, 300)}` : ""}`);
|
||||
}
|
||||
const payload = (await resp.json().catch(() => null)) as unknown;
|
||||
if (!isRecord(payload) || !isRecord(payload.results)) throw new Error("MinerU 返回格式不正确");
|
||||
const keys = Object.keys(payload.results);
|
||||
if (!keys.length) throw new Error("MinerU 返回为空");
|
||||
const first = payload.results[keys[0]];
|
||||
if (!isRecord(first)) throw new Error("MinerU 返回格式不正确(results.*)");
|
||||
const raw = first.content_list;
|
||||
if (typeof raw !== "string" || !raw.trim()) return [];
|
||||
const parsed = JSON.parse(raw) as unknown;
|
||||
if (!Array.isArray(parsed)) return [];
|
||||
return parsed as Array<Record<string, unknown>>;
|
||||
};
|
||||
|
||||
type ExtractedOutlineItem = { level: number; title: string; page?: number };
|
||||
|
||||
const outlineFromContentList = (list: Array<Record<string, unknown>>) => {
|
||||
const out: ExtractedOutlineItem[] = [];
|
||||
const seen = new Set<string>();
|
||||
for (const item of list) {
|
||||
const levelRaw = item.text_level;
|
||||
const textRaw = item.text;
|
||||
const pageIdxRaw = item.page_idx;
|
||||
if (typeof levelRaw !== "number" || !Number.isFinite(levelRaw)) continue;
|
||||
const level = Math.max(1, Math.min(6, Math.floor(levelRaw)));
|
||||
const title = String(textRaw ?? "").replace(/\s+/g, " ").trim();
|
||||
if (!title) continue;
|
||||
const pageIdx = typeof pageIdxRaw === "number" && Number.isFinite(pageIdxRaw) ? Math.max(0, Math.floor(pageIdxRaw)) : null;
|
||||
const page = pageIdx === null ? undefined : pageIdx + 1;
|
||||
const key = `${level}|${page ?? ""}|${title}`;
|
||||
if (seen.has(key)) continue;
|
||||
seen.add(key);
|
||||
out.push({ level, title, page });
|
||||
}
|
||||
return out;
|
||||
};
|
||||
|
||||
const findNodeByUid = (root: MindmapTreeNode, uid: string): MindmapTreeNode | null => {
|
||||
const target = String(uid || "");
|
||||
if (!target) return null;
|
||||
const walk = (n: MindmapTreeNode): MindmapTreeNode | null => {
|
||||
if (String(n?.data?.uid || "") === target) return n;
|
||||
const children = Array.isArray(n.children) ? n.children : [];
|
||||
for (const c of children) {
|
||||
const hit = walk(c);
|
||||
if (hit) return hit;
|
||||
}
|
||||
return null;
|
||||
};
|
||||
return walk(root);
|
||||
};
|
||||
|
||||
const createUid = () => {
|
||||
const anyCrypto = globalThis.crypto as unknown as { randomUUID?: () => string } | undefined;
|
||||
if (anyCrypto?.randomUUID) return anyCrypto.randomUUID();
|
||||
return Math.random().toString(36).slice(2);
|
||||
};
|
||||
|
||||
const buildOutlineOps = (args: { parentUid: string; items: ExtractedOutlineItem[]; attachment: { assetId: string; fileUrl: string; title: string; mimeType?: string | null } }) => {
|
||||
const ops: MindmapOp[] = [];
|
||||
const parents: Record<number, string> = { 0: args.parentUid };
|
||||
|
||||
for (const item of args.items) {
|
||||
const level = Math.max(1, Math.min(6, Math.floor(item.level)));
|
||||
const parent = parents[level - 1] || args.parentUid;
|
||||
const uid = createUid();
|
||||
const page = item.page;
|
||||
|
||||
const refs: NodeRef[] = [
|
||||
{
|
||||
kind: "pdf",
|
||||
assetId: args.attachment.assetId,
|
||||
fileUrl: args.attachment.fileUrl,
|
||||
page,
|
||||
title: args.attachment.title,
|
||||
snippet: page ? `第 ${page} 页` : undefined,
|
||||
},
|
||||
];
|
||||
|
||||
const hyperlink = (() => {
|
||||
const base = String(args.attachment.fileUrl || "").trim();
|
||||
if (!base) return undefined;
|
||||
if (page && isPdf(String(args.attachment.mimeType ?? ""), base)) return `${base}#page=${page}`;
|
||||
return base;
|
||||
})();
|
||||
|
||||
ops.push({
|
||||
op: "addChild",
|
||||
parentUid: parent,
|
||||
node: {
|
||||
uid,
|
||||
text: item.title,
|
||||
hyperlink,
|
||||
refs,
|
||||
},
|
||||
});
|
||||
|
||||
parents[level] = uid;
|
||||
for (let d = level + 1; d <= 10; d += 1) delete parents[d];
|
||||
}
|
||||
|
||||
return ops;
|
||||
};
|
||||
|
||||
export const createOnlyOfficeServerTools = (args: {
|
||||
supabase: OnlyOfficeSupabaseClient;
|
||||
ctx: OnlyOfficeToolContext;
|
||||
allowedToolIds: Set<string>;
|
||||
}) => {
|
||||
const asset_extract_outline = async (toolArgs: Record<string, unknown>) => {
|
||||
const assetId = String(toolArgs.assetId ?? "").trim();
|
||||
const attachmentRef = String(toolArgs.attachmentRef ?? "").trim();
|
||||
const resolved = attachmentRef ? resolveAttachment(args.ctx, attachmentRef) : null;
|
||||
const targetAssetId = assetId || resolved?.id || "";
|
||||
if (!targetAssetId) throw new Error("缺少 assetId / attachmentRef");
|
||||
|
||||
const { data, error } = await args.supabase
|
||||
.from("media_assets")
|
||||
.select("id,file_name,file_url,mime_type,ocr_text,ocr_status,storage_path,bucket,updated_at,deleted_at,purged_at")
|
||||
.eq("id", targetAssetId)
|
||||
.is("deleted_at", null)
|
||||
.limit(1)
|
||||
.maybeSingle();
|
||||
if (error) throw new Error(isRecord(error) && typeof error.message === "string" ? error.message : "读取附件失败");
|
||||
const row = (data && typeof data === "object" ? (data as Record<string, unknown>) : null) ?? null;
|
||||
if (!row) return { ok: true, found: false };
|
||||
|
||||
const fileName = String(row.file_name ?? "");
|
||||
const fileUrl = String(row.file_url ?? "");
|
||||
const mimeType = String(row.mime_type ?? "");
|
||||
const ocrText = String(row.ocr_text ?? "");
|
||||
const ocrStatus = String(row.ocr_status ?? "");
|
||||
const textPreview = ocrText.trim().slice(0, 3000);
|
||||
|
||||
if (!isPdf(mimeType, fileName || fileUrl)) {
|
||||
return {
|
||||
ok: true,
|
||||
found: true,
|
||||
supported: false,
|
||||
note: "当前仅优先支持 PDF(M3:PDF→大纲→导图)。Word/PPT 将在后续阶段通过 ONLYOFFICE /converter 或其它解析策略补齐。",
|
||||
asset: { id: String(row.id ?? ""), fileName, fileUrl, mimeType, ocrStatus, updatedAt: row.updated_at ?? null },
|
||||
hasOcrText: Boolean(ocrText.trim()),
|
||||
textPreview,
|
||||
};
|
||||
}
|
||||
|
||||
// M3:PDF 优先使用 MinerU 的 content_list(含 page_idx + text_level),用于做可跳转大纲
|
||||
let outline: ExtractedOutlineItem[] = [];
|
||||
let strategy: "mineru_content_list" | "fallback_ocr_text" = "mineru_content_list";
|
||||
|
||||
try {
|
||||
const signed = await signForDownload(row);
|
||||
const bytes = await downloadBytes(signed);
|
||||
const list = await mineruParseContentList({ bytes, filename: fileName || "file.pdf", mimeType: mimeType || "application/pdf" });
|
||||
outline = outlineFromContentList(list);
|
||||
} catch (e) {
|
||||
// 兜底:返回 OCR 文本的一部分,至少让模型能“基于文本生成大纲”(但没有页码保障)
|
||||
strategy = "fallback_ocr_text";
|
||||
outline = [];
|
||||
if (textPreview) outline.push({ level: 1, title: "(未解析到结构化标题:请结合 textPreview 自行生成提纲)" });
|
||||
}
|
||||
|
||||
return {
|
||||
ok: true,
|
||||
found: true,
|
||||
supported: true,
|
||||
strategy,
|
||||
asset: {
|
||||
id: String(row.id ?? ""),
|
||||
fileName,
|
||||
fileUrl,
|
||||
mimeType,
|
||||
ocrStatus,
|
||||
updatedAt: row.updated_at ?? null,
|
||||
},
|
||||
hasOcrText: Boolean(ocrText.trim()),
|
||||
textPreview,
|
||||
outline,
|
||||
note:
|
||||
outline.length > 0
|
||||
? `已提取大纲条目:${outline.length}。`
|
||||
: strategy === "fallback_ocr_text"
|
||||
? "未能从 MinerU 获取结构化标题;请确认 MINERU_ENDPOINT 正常,或稍后重试。"
|
||||
: "未提取到标题(该 PDF 可能没有明显标题结构)。",
|
||||
};
|
||||
};
|
||||
|
||||
const asset_to_mindmap = async (toolArgs: Record<string, unknown>) => {
|
||||
const mindmapId = String(toolArgs.mindmapId ?? "").trim();
|
||||
if (!mindmapId) throw new Error("缺少 mindmapId");
|
||||
const parentUidArg = String(toolArgs.parentUid ?? "").trim();
|
||||
const maxItemsRaw = Number(toolArgs.maxItems ?? 120);
|
||||
const maxItems = Number.isFinite(maxItemsRaw) ? Math.max(10, Math.min(600, Math.floor(maxItemsRaw))) : 120;
|
||||
|
||||
const documentId = String(args.ctx.documentId ?? "").trim();
|
||||
if (!documentId) throw new Error("缺少 documentId 上下文(OnlyOffice 工具需要落盘到指定文档)");
|
||||
|
||||
const outlineResult = (await asset_extract_outline(toolArgs)) as unknown;
|
||||
if (!isRecord(outlineResult) || outlineResult.ok !== true) throw new Error("提取大纲失败");
|
||||
if (outlineResult.found !== true) throw new Error("未找到附件");
|
||||
if (outlineResult.supported !== true) throw new Error(String(outlineResult.note ?? "附件类型暂不支持"));
|
||||
|
||||
const outline = Array.isArray(outlineResult.outline) ? (outlineResult.outline as unknown[]) : [];
|
||||
const items = outline
|
||||
.map((x) =>
|
||||
isRecord(x)
|
||||
? {
|
||||
level: Number(x.level ?? 1),
|
||||
title: String(x.title ?? ""),
|
||||
...(typeof x.page === "number" ? { page: x.page } : {}),
|
||||
}
|
||||
: null,
|
||||
)
|
||||
.filter((x): x is ExtractedOutlineItem => x !== null && x.title.trim().length > 0)
|
||||
.slice(0, maxItems);
|
||||
if (!items.length) throw new Error("未提取到可用大纲条目");
|
||||
|
||||
const asset = isRecord(outlineResult.asset) ? outlineResult.asset : null;
|
||||
const attachment = {
|
||||
assetId: String(asset?.id ?? ""),
|
||||
title: String(asset?.fileName ?? "附件"),
|
||||
fileUrl: String(asset?.fileUrl ?? ""),
|
||||
mimeType: isRecord(asset) ? (asset.mimeType as string | null | undefined) : null,
|
||||
};
|
||||
if (!attachment.assetId) throw new Error("缺少附件 id");
|
||||
|
||||
const { data: baseRaw } = await readMindmapLocal(documentId, mindmapId);
|
||||
const base = (baseRaw && typeof baseRaw === "object" ? (baseRaw as MindmapTreeNode) : null) ?? { data: { text: "中心主题" }, children: [] };
|
||||
ensureMindmapUids(base);
|
||||
|
||||
const rootUid = String(base?.data?.uid || "");
|
||||
const parentUid = parentUidArg || rootUid;
|
||||
if (!parentUid) throw new Error("无法确定 parentUid(mindmap 根节点缺少 uid)");
|
||||
if (!findNodeByUid(base, parentUid)) throw new Error("未找到 parentUid 对应节点");
|
||||
|
||||
const ops = buildOutlineOps({ parentUid, items, attachment });
|
||||
const { data: nextData, applied, errors } = applyMindmapOps(base, ops);
|
||||
await writeMindmapLocal(documentId, mindmapId, nextData, "OnlyOffice 生成导图");
|
||||
|
||||
return {
|
||||
ok: true,
|
||||
mindmapId,
|
||||
parentUid,
|
||||
applied,
|
||||
errors,
|
||||
ops,
|
||||
meta: {
|
||||
assetId: attachment.assetId,
|
||||
fileName: attachment.title,
|
||||
items: items.length,
|
||||
strategy: String(outlineResult.strategy ?? ""),
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
const run = async (toolId: string, toolArgs: Record<string, unknown>) => {
|
||||
if (!args.allowedToolIds.has(toolId)) throw new Error(`工具未被允许:${toolId}`);
|
||||
if (toolId === "asset_extract_outline") return await asset_extract_outline(toolArgs);
|
||||
if (toolId === "asset_to_mindmap") return await asset_to_mindmap(toolArgs);
|
||||
throw new Error(`未知工具:${toolId}`);
|
||||
};
|
||||
|
||||
return { run };
|
||||
};
|
||||
@@ -239,6 +239,77 @@ export const builtinTools: AiAgentTool[] = [
|
||||
requiresConfirmation: true,
|
||||
isWriteTool: true,
|
||||
},
|
||||
{
|
||||
id: "asset_extract_outline",
|
||||
displayName: "提取附件大纲(PDF 优先)",
|
||||
modelDescription:
|
||||
"从附件(当前优先 PDF)提取结构化大纲(含层级与页码)。用于“文档驱动导图/可跳转引用”(不写入)。",
|
||||
inputSchemaText: `{ "assetId?": "string", "attachmentRef?": "string (可用附件 id/title/url 片段)" }`,
|
||||
source: "builtin",
|
||||
requiresConfirmation: false,
|
||||
isWriteTool: false,
|
||||
},
|
||||
{
|
||||
id: "asset_to_mindmap",
|
||||
displayName: "附件生成思维导图(写入)",
|
||||
modelDescription:
|
||||
"把附件大纲转为思维导图节点并落盘(PDF 页码会写入 refs,并尽量生成可跳转 hyperlink)。这是写工具,执行前必须确认。",
|
||||
inputSchemaText:
|
||||
`{ "mindmapId": "string", "parentUid?": "string (默认根节点)", "assetId?": "string", "attachmentRef?": "string", "maxItems?": "number (10~600, 默认120)", "reason?": "string" }`,
|
||||
source: "builtin",
|
||||
requiresConfirmation: true,
|
||||
isWriteTool: true,
|
||||
},
|
||||
{
|
||||
id: "oo_get_selection",
|
||||
displayName: "读取选区(OnlyOffice)",
|
||||
modelDescription:
|
||||
"读取 OnlyOffice 当前选区(用于增/删/改/查的基础能力:查/改前先读)。注意:该工具在客户端插件内执行。",
|
||||
inputSchemaText: `{ "format?": "\"text\"|\"html\" (默认 text)" }`,
|
||||
source: "builtin",
|
||||
requiresConfirmation: false,
|
||||
isWriteTool: false,
|
||||
},
|
||||
{
|
||||
id: "oo_replace_selection",
|
||||
displayName: "替换选区(OnlyOffice)",
|
||||
modelDescription:
|
||||
"用文本/HTML 替换 OnlyOffice 当前选区(基础改/删:删=传空字符串)。注意:该工具在客户端插件内执行。",
|
||||
inputSchemaText: `{ "text": "string", "format?": "\"text\"|\"html\" (默认 text)", "reason?": "string" }`,
|
||||
source: "builtin",
|
||||
requiresConfirmation: true,
|
||||
isWriteTool: true,
|
||||
},
|
||||
{
|
||||
id: "oo_insert_text",
|
||||
displayName: "插入文本(OnlyOffice)",
|
||||
modelDescription:
|
||||
"在 OnlyOffice 光标/选区位置插入文本(若存在选区通常会覆盖选区)。注意:该工具在客户端插件内执行。",
|
||||
inputSchemaText: `{ "text": "string", "reason?": "string" }`,
|
||||
source: "builtin",
|
||||
requiresConfirmation: true,
|
||||
isWriteTool: true,
|
||||
},
|
||||
{
|
||||
id: "oo_insert_html",
|
||||
displayName: "插入 HTML(OnlyOffice)",
|
||||
modelDescription:
|
||||
"在 OnlyOffice 光标/选区位置插入 HTML(用于保留简单格式)。注意:该工具在客户端插件内执行。",
|
||||
inputSchemaText: `{ "html": "string", "reason?": "string" }`,
|
||||
source: "builtin",
|
||||
requiresConfirmation: true,
|
||||
isWriteTool: true,
|
||||
},
|
||||
{
|
||||
id: "oo_insert_image",
|
||||
displayName: "插入图片(OnlyOffice)",
|
||||
modelDescription:
|
||||
"在 OnlyOffice 光标/选区位置插入图片(imageRef 可为附件 id 或 URL)。注意:该工具在客户端插件内执行。",
|
||||
inputSchemaText: `{ "imageRef": "string (attachmentId 或 URL)", "width?": "number", "height?": "number", "reason?": "string" }`,
|
||||
source: "builtin",
|
||||
requiresConfirmation: true,
|
||||
isWriteTool: true,
|
||||
},
|
||||
];
|
||||
|
||||
export const builtinToolSets: AiAgentToolSet[] = [
|
||||
@@ -262,6 +333,27 @@ export const builtinToolSets: AiAgentToolSet[] = [
|
||||
displayName: "媒体读取(OCR/元信息)",
|
||||
toolIds: ["image_read"],
|
||||
},
|
||||
{
|
||||
id: "toolset.onlyoffice_read",
|
||||
displayName: "OnlyOffice/附件结构化读取(M3)",
|
||||
toolIds: ["asset_extract_outline"],
|
||||
},
|
||||
{
|
||||
id: "toolset.onlyoffice_write",
|
||||
displayName: "OnlyOffice→思维导图(写入,M3)",
|
||||
toolIds: ["asset_to_mindmap"],
|
||||
},
|
||||
{
|
||||
id: "toolset.onlyoffice_editor",
|
||||
displayName: "OnlyOffice 编辑器(选区读写)",
|
||||
toolIds: [
|
||||
"oo_get_selection",
|
||||
"oo_replace_selection",
|
||||
"oo_insert_text",
|
||||
"oo_insert_html",
|
||||
"oo_insert_image",
|
||||
],
|
||||
},
|
||||
{
|
||||
id: "toolset.slash_write",
|
||||
displayName: "斜杠命令(写入)",
|
||||
|
||||
Reference in New Issue
Block a user