| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556 |
- import { executeWebMcpTool, getWebMcpTool, type WebMcpToolResult } from './webMcpService';
- import {
- matchWebMcpChatTemplate,
- webMcpTemplateHelp,
- type WebMcpChatTemplate,
- } from './webMcpChatTemplates';
- import { getWebMcpTools } from './webMcpService';
- import { translateWebMcpInput } from '../share/webmcp';
- import { listDocuments } from './documentService';
- import { blockService } from './blockService';
- export interface WebMcpChatResult {
- handled: boolean;
- response?: string;
- toolName?: string;
- requiresConfirmation?: boolean;
- template?: WebMcpChatTemplate;
- input?: Record<string, unknown>;
- result?: WebMcpToolResult;
- }
- interface ExportCandidate {
- documentId: string;
- title: string;
- aliases?: string[];
- recordId?: string;
- }
- interface LocalDocumentCommand {
- toolName: string;
- input: Record<string, unknown>;
- documentTitle: string;
- }
- const READ_ONLY_TOOLS = new Set([
- 'list_documents',
- 'get_document',
- 'search_document',
- 'get_block',
- 'get_document_toc',
- 'get_document_stats',
- 'list_export_records',
- ]);
- const DOCUMENT_CANDIDATE_CACHE_TTL = 15_000;
- let documentCandidatesCache: { value: ExportCandidate[]; expiresAt: number } | undefined;
- let documentCandidatesRequest: Promise<ExportCandidate[]> | undefined;
- const stringifyResult = (result: WebMcpToolResult): string => {
- if (!result.ok) return `WebMCP 执行失败:${result.error || '未知错误'}`;
- return `WebMCP 执行结果:\n${JSON.stringify(result.data, null, 2)}`;
- };
- const getDocumentCandidates = async (): Promise<ExportCandidate[]> => {
- if (documentCandidatesCache && documentCandidatesCache.expiresAt > Date.now()) {
- return documentCandidatesCache.value;
- }
- if (documentCandidatesRequest) return documentCandidatesRequest;
- documentCandidatesRequest = loadDocumentCandidates();
- try {
- const value = await documentCandidatesRequest;
- documentCandidatesCache = { value, expiresAt: Date.now() + DOCUMENT_CANDIDATE_CACHE_TTL };
- return value;
- } finally {
- documentCandidatesRequest = undefined;
- }
- };
- const loadDocumentCandidates = async (): Promise<ExportCandidate[]> => {
- const unique = new Map<string, ExportCandidate>();
- try {
- const documentResult = await listDocuments({
- userId: 'default-user',
- page: 1,
- pageSize: 100,
- sortBy: 'updated_at',
- sortOrder: 'desc',
- });
- await Promise.all(documentResult.documents.map(async (document) => {
- const detail = await executeWebMcpTool('get_document', { documentId: document.id });
- const blocks = detail.ok && detail.data && typeof detail.data === 'object'
- ? (detail.data as { blocks?: unknown }).blocks
- : undefined;
- if (!Array.isArray(blocks)) return;
- const firstText = blocks
- .map((block) => {
- if (!block || typeof block !== 'object') return '';
- const content = (block as { content?: unknown }).content;
- if (typeof content === 'string') return content.trim();
- if (Array.isArray(content)) {
- return content
- .map((part) => part && typeof part === 'object' && typeof (part as { text?: unknown }).text === 'string'
- ? (part as { text: string }).text
- : '')
- .join('')
- .trim();
- }
- return '';
- })
- .find(Boolean);
- unique.set(document.id, { documentId: document.id, title: firstText || document.id });
- }));
- } catch (error) {
- console.warn('[WebMCP] 获取文档名称失败,将继续使用导出记录:', error);
- }
- const exportResult = await executeWebMcpTool('list_export_records', { page: 1, pageSize: 100 });
- if (exportResult.ok && exportResult.data && typeof exportResult.data === 'object') {
- const records = (exportResult.data as { records?: unknown }).records;
- if (Array.isArray(records)) {
- for (const record of records) {
- if (!record || typeof record !== 'object') continue;
- const item = record as Record<string, unknown>;
- if (typeof item.documentId !== 'string' || typeof item.fileName !== 'string') continue;
- const candidate = unique.get(item.documentId) || {
- documentId: item.documentId,
- title: item.fileName.replace(/\.(docx?|DOCX?)$/i, '').replace(/[_-]\d{8,}$/, '').trim(),
- };
- const exportTitle = item.fileName.replace(/\.(docx?|DOCX?)$/i, '').replace(/[_-]\d{8,}$/, '').trim();
- if (exportTitle && exportTitle !== candidate.title) {
- candidate.aliases = [...new Set([...(candidate.aliases || []), exportTitle])];
- }
- if (!candidate.recordId && typeof item.recordId === 'string') candidate.recordId = item.recordId;
- unique.set(item.documentId, candidate);
- }
- }
- }
- return [...unique.values()];
- };
- const normalizeDocumentText = (value: string): string =>
- value
- .toLocaleLowerCase()
- .replace(/\.(docx?|DOCX?)$/i, '')
- .replace(/(?:文档|文件)$/i, '')
- .replace(/[“”"'「」《》]/g, '')
- .replace(/\s+/g, '')
- .trim();
- const findDocumentCandidate = (
- title: string,
- candidates: ExportCandidate[]
- ): { candidate?: ExportCandidate; ambiguous: boolean } => {
- const normalizedTitle = normalizeDocumentText(title);
- const matches = candidates.filter((candidate) => {
- const candidateTitles = [candidate.title, ...(candidate.aliases || [])].map(normalizeDocumentText);
- return candidate.documentId === title || candidate.recordId === title || candidateTitles.some((candidateTitle) =>
- candidateTitle === normalizedTitle || candidateTitle.includes(normalizedTitle) || normalizedTitle.includes(candidateTitle)
- );
- });
- return { candidate: matches.length === 1 ? matches[0] : undefined, ambiguous: matches.length > 1 };
- };
- const getActionHelp = (action: string): string => {
- if (action === 'download_export_record') return '请告诉我要下载的文档名称,例如:下载产品说明文档。';
- if (action === 'export_document') return '请告诉我要导出的文档名称,例如:导出产品说明文档。';
- if (action === 'open_document') return '请告诉我要打开的文档名称,例如:打开产品说明文档。';
- if (action === 'get_document') return '请告诉我要读取的文档名称,例如:读取产品说明文档。';
- return '请告诉我要操作的文档名称和位置,例如:删除产品说明文档第一行、末行,或修改产品说明文档第二段为:新的内容。';
- };
- const chineseNumerals: Record<string, number> = {
- 零: 0,
- 〇: 0,
- 一: 1,
- 二: 2,
- 两: 2,
- 三: 3,
- 四: 4,
- 五: 5,
- 六: 6,
- 七: 7,
- 八: 8,
- 九: 9,
- 十: 10,
- 百: 100,
- };
- const parseChineseInteger = (value: string): number | undefined => {
- if (/^\d+$/.test(value)) return Number(value);
- let section = 0;
- let number = 0;
- for (const character of value) {
- const digit = chineseNumerals[character];
- if (digit === undefined) return undefined;
- if (digit === 10 || digit === 100) {
- section += (number || 1) * digit;
- number = 0;
- } else {
- number = digit;
- }
- }
- return section + number;
- };
- const parseBlockReference = (value: string): { reference?: string; remainder: string } => {
- const match = value.match(/(?:第\s*(\d+|[零〇一二两三四五六七八九十百]+)\s*(?:个)?\s*(?:行|段|块|标题|段落|条)|(?:最后|末尾|末)\s*(?:一行|一段|一个块|一块|一条|行|段|块|条)|首行|第一行)\s*$/i);
- if (!match) return { remainder: value };
- if (match[0].includes('最后') || match[0].includes('末尾') || match[0].includes('末')) {
- return { reference: 'last', remainder: value.slice(0, match.index).trim() };
- }
- if (match[0].includes('首行') || match[0].includes('第一行')) {
- return { reference: '1', remainder: value.slice(0, match.index).trim() };
- }
- const number = parseChineseInteger(match[1]);
- return number !== undefined && Number.isInteger(number) && number > 0
- ? { reference: String(number), remainder: value.slice(0, match.index).trim() }
- : { remainder: value };
- };
- const resolveBlockReference = async (documentId: string, reference: string): Promise<string | undefined> => {
- const blockResult = await blockService.getBlocks(documentId);
- const blocks = [...blockResult.blocks].sort((left, right) => left.block_order - right.block_order);
- const block = reference === 'last' ? blocks.at(-1) : blocks[Number(reference) - 1];
- return block?.id;
- };
- const parseLocalDocumentCommand = async (content: string): Promise<
- { command?: LocalDocumentCommand; response?: string; handled: boolean }
- > => {
- const text = content.trim();
- const actionMatch = text.match(/^(打开|查看|阅读|读取|导出|下载|删除|修改|更新|新增|插入)\s*(.*?)(?:文档|文件)?(?:这个文档|该文档)?$/i);
- if (!actionMatch) return { handled: false };
- const actionText = actionMatch[1];
- let remainder = actionMatch[2].trim().replace(/^(?:文档|文件)\s*/, '').trim();
- const isBlockAction = /^(删除|修改|更新|新增|插入)$/.test(actionText);
- let blockId: string | undefined;
- let blockReference: string | undefined;
- let blockContent: string | undefined;
- if (isBlockAction) {
- const blockMatch = remainder.match(/\b(block-[\w-]+)\b/i);
- blockId = blockMatch?.[1];
- blockContent = remainder.match(/(?:为|改为|内容为|插入)[::]?\s*(.+)$/)?.[1];
- remainder = remainder.replace(blockId || '', '').replace(/(?:为|改为|内容为|插入)[::]?\s*.+$/, '').trim();
- const parsedReference = parseBlockReference(remainder);
- blockReference = parsedReference.reference;
- remainder = parsedReference.remainder;
- remainder = remainder.replace(/(?:这个|该)?(?:文档|文件)\s*$/i, '').trim();
- }
- const toolName = actionText === '打开' ? 'open_document'
- : actionText === '导出' ? 'export_document'
- : actionText === '下载' ? 'download_export_record'
- : actionText === '读取' || actionText === '查看' || actionText === '阅读' ? 'get_document'
- : actionText === '删除' ? 'delete_block'
- : actionText === '新增' || actionText === '插入' ? 'insert_block'
- : 'update_block';
- if (!remainder || (isBlockAction && !blockId && !blockReference)) return { handled: true, response: getActionHelp(toolName) };
- const candidates = await getDocumentCandidates();
- const { candidate, ambiguous } = findDocumentCandidate(remainder, candidates);
- if (ambiguous) return { handled: true, response: `找到多份与“${remainder}”相近的文档,请提供更完整的文档名称。` };
- if (!candidate) return { handled: true, response: `没有找到名为“${remainder}”的文档,请确认文档名称后重试。` };
- if (blockReference) {
- try {
- blockId = await resolveBlockReference(candidate.documentId, blockReference);
- } catch (error) {
- console.warn('[WebMCP] 获取文档块失败:', error);
- }
- if (!blockId) {
- const position = blockReference === 'last' ? '末尾' : `第 ${blockReference}`;
- return { handled: true, response: `没有找到“${remainder}”的${position}内容块,请确认位置后重试。` };
- }
- }
- const input: Record<string, unknown> = toolName === 'download_export_record'
- ? { recordId: candidate.recordId || candidate.documentId }
- : { documentId: candidate.documentId };
- if (blockId) input.blockId = blockId;
- if (toolName === 'update_block' && blockContent) input.content = blockContent;
- if (toolName === 'insert_block' && blockContent) {
- input.type = 'paragraph';
- input.content = blockContent;
- }
- if (isBlockAction && (!input.content && toolName !== 'delete_block')) {
- return { handled: true, response: `请补充要${toolName === 'insert_block' ? '插入' : '修改'}的内容。` };
- }
- return { handled: true, command: { toolName, input, documentTitle: candidate.title } };
- };
- const translateInput = async (content: string) =>
- translateWebMcpInput(content, {
- availableTools: getWebMcpTools().map((tool) => tool.name),
- documentCandidates: await getDocumentCandidates(),
- locale: 'zh-CN',
- });
- const isDocumentAction = (content: string): boolean =>
- /(打开|查看|阅读|读取|删除|修改|更新|新增|插入|导出|下载).*(文档|文件|区块|块)/i.test(content);
- const documentActionHelp = (content: string): string => {
- if (/(打开|查看|阅读|读取)/i.test(content)) {
- return '我还不知道你要打开哪份文档,请告诉我文档名称,例如:打开产品说明文档。';
- }
- if (/(删除|修改|更新|新增|插入)/i.test(content)) {
- return '请提供文档名称和文档块 ID,例如:修改产品说明文档 block-p-50 为:新的内容。';
- }
- if (/(导出)/i.test(content)) return '请告诉我要导出的文档名称,例如:导出产品说明文档。';
- if (/(下载)/i.test(content)) return '请提供导出记录 ID,例如:下载导出记录 rec-abc123。';
- return '请补充要操作的文档名称或文档块信息。';
- };
- const formatBlockContent = (content: unknown): string => {
- if (typeof content === 'string') return content.trim();
- if (Array.isArray(content)) {
- return content
- .map((part) => {
- if (typeof part === 'string') return part;
- if (part && typeof part === 'object' && typeof (part as { text?: unknown }).text === 'string') {
- return (part as { text: string }).text;
- }
- return '';
- })
- .join('')
- .trim();
- }
- if (content && typeof content === 'object') {
- const title = (content as { title?: unknown }).title;
- if (typeof title === 'string' && title.trim()) return title.trim();
- try {
- return JSON.stringify(content);
- } catch {
- return '';
- }
- }
- return '';
- };
- const formatSearchResult = (documentLabel: string, data: unknown): string => {
- if (!data || typeof data !== 'object') return `未找到“${documentLabel}”中的匹配内容。`;
- const payload = data as { blocks?: unknown; total?: unknown; query?: unknown };
- const blocks = Array.isArray(payload.blocks) ? payload.blocks : [];
- const total = typeof payload.total === 'number' ? payload.total : blocks.length;
- const query = typeof payload.query === 'string' ? payload.query : '';
- if (!blocks.length) {
- return `在“${documentLabel}”中未找到${query ? `包含“${query}”的` : ''}内容。`;
- }
- const lines = blocks.slice(0, 20).map((block, index) => {
- if (!block || typeof block !== 'object') return `${index + 1}. (无法读取内容)`;
- const item = block as { id?: unknown; type?: unknown; content?: unknown };
- const id = typeof item.id === 'string' ? item.id : '未知块';
- const type = typeof item.type === 'string' ? item.type : '内容块';
- const content = formatBlockContent(item.content).replace(/\s+/g, ' ').trim();
- const preview = content.length > 500 ? `${content.slice(0, 500)}...` : content;
- return `${index + 1}. 【${type}】${preview || '(无可显示内容)'}\n 块 ID:${id}`;
- });
- const suffix = total > blocks.length ? `\n\n仅显示前 ${blocks.length} 条,共找到 ${total} 条。` : '';
- return `在“${documentLabel}”中找到 ${total} 条${query ? `包含“${query}”的` : ''}内容:\n${lines.join('\n')}\n${suffix}`.trim();
- };
- const getDocumentUrl = (documentId: string): string => {
- const url = new URL(window.location.href);
- url.searchParams.set('documentId', documentId);
- return url.toString();
- };
- const formatDocumentListResult = (data: unknown): string => {
- if (!data || typeof data !== 'object') return '暂无可查看的文档。';
- const payload = data as { documents?: unknown; pagination?: { total?: unknown } };
- const documents = Array.isArray(payload.documents) ? payload.documents : [];
- if (!documents.length) return '暂无可查看的文档。';
- const lines = documents.map((document, index) => {
- if (!document || typeof document !== 'object') return `${index + 1}. 文档信息无效`;
- const item = document as { id?: unknown; updatedAt?: unknown };
- const documentId = typeof item.id === 'string' ? item.id : '';
- if (!documentId) return `${index + 1}. 文档 ID 缺失`;
- const updatedAt = typeof item.updatedAt === 'string' ? `(更新于 ${item.updatedAt})` : '';
- return `${index + 1}. ${documentId}${updatedAt}\n ${getDocumentUrl(documentId)}`;
- });
- const total = typeof payload.pagination?.total === 'number' ? payload.pagination.total : documents.length;
- return `共找到 ${total} 个文档:\n${lines.join('\n')}`;
- };
- const friendlyResult = (
- toolName: string,
- input: Record<string, unknown>,
- result: WebMcpToolResult
- ): string => {
- if (!result.ok) return `操作未完成:${result.error || '未知错误'}`;
- const documentId = typeof input.documentId === 'string' ? input.documentId : '';
- const blockId = typeof input.blockId === 'string' ? input.blockId : '';
- const documentLabel = typeof input.documentTitle === 'string' ? input.documentTitle : documentId;
- if (toolName === 'list_documents') return formatDocumentListResult(result.data);
- if (toolName === 'open_document') return `已帮您打开“${documentLabel || '指定'}”文档。`;
- if (toolName === 'get_document') return `已读取“${documentLabel || documentId}”文档内容。`;
- if (toolName === 'search_document') return formatSearchResult(documentLabel || documentId, result.data);
- if (toolName === 'update_block') return `已修改“${documentLabel || documentId}”中的区块 ${blockId}。`;
- if (toolName === 'delete_block') return `已删除“${documentLabel || documentId}”中的区块 ${blockId}。`;
- if (toolName === 'insert_block') return `已向“${documentLabel || documentId}”插入新的内容块。`;
- if (toolName === 'export_document') return `已开始导出“${documentLabel || documentId}”文档。`;
- if (toolName === 'download_export_record') return `已开始下载导出记录 ${String(input.recordId || '')}。`;
- return stringifyResult(result);
- };
- export const isWebMcpChatCommand = (content: string): boolean =>
- content.trim() === '查看 WebMCP 模板' || matchWebMcpChatTemplate(content) !== null;
- export const parseWebMcpChatCommand = (content: string) => matchWebMcpChatTemplate(content);
- export const executeWebMcpChatCommand = async (content: string): Promise<WebMcpChatResult> => {
- if (content.trim() === '查看 WebMCP 模板') {
- return { handled: true, response: webMcpTemplateHelp() };
- }
- const matched = matchWebMcpChatTemplate(content);
- const localCommand = matched ? { handled: false as const } : await parseLocalDocumentCommand(content);
- if (localCommand.handled && localCommand.command) {
- const tool = getWebMcpTool(localCommand.command.toolName);
- if (!tool) return { handled: true, response: `WebMCP 工具不存在:${localCommand.command.toolName}` };
- const { toolName, input, documentTitle } = localCommand.command;
- if (tool.requiresConfirmation) {
- return { handled: true, toolName, input, requiresConfirmation: true, response: `即将对“${documentTitle}”执行${tool.description.replace('。需要用户确认。', '')},请确认。` };
- }
- const result = await executeWebMcpTool(toolName, input);
- return { handled: true, toolName, input, result, response: friendlyResult(toolName, { ...input, documentTitle }, result) };
- }
- if (localCommand.handled && localCommand.response) return { handled: true, response: localCommand.response };
- if (!matched || !matched.template.toolName) {
- try {
- const translated = await translateInput(content);
- const translation = translated.translation;
- if (translation.intent === 'clarify') {
- return { handled: true, response: translation.question || '请补充更多信息后重试。' };
- }
- if (!translation.toolCall) {
- return isDocumentAction(content)
- ? { handled: true, response: documentActionHelp(content) }
- : { handled: false };
- }
- const tool = getWebMcpTool(translation.toolCall.name);
- if (!tool) return { handled: true, response: `WebMCP 工具不存在:${translation.toolCall.name}` };
- const input = { ...translation.toolCall.arguments };
- const displayInput = { ...input };
- if (!displayInput.documentTitle && typeof displayInput.documentId === 'string') displayInput.documentTitle = displayInput.documentId;
- const requiresConfirmation = Boolean(tool.requiresConfirmation || translation.requiresConfirmation);
- if (requiresConfirmation) {
- return {
- handled: true,
- toolName: tool.name,
- input,
- requiresConfirmation: true,
- response: `即将${tool.description.replace('。需要用户确认。', '')},请确认。`,
- };
- }
- const result = await executeWebMcpTool(tool.name, input);
- return {
- handled: true,
- toolName: tool.name,
- input,
- requiresConfirmation: false,
- result,
- response: friendlyResult(tool.name, displayInput, result),
- };
- } catch (error) {
- console.warn('[WebMCP] 远端转译不可用,回退普通 AI:', error);
- return { handled: false };
- }
- }
- const toolName = matched.template.toolName;
- const tool = getWebMcpTool(toolName);
- if (!tool) return { handled: true, response: `WebMCP 工具不存在:${toolName}`, toolName };
- if (!READ_ONLY_TOOLS.has(toolName)) {
- return {
- handled: true,
- toolName,
- template: matched.template,
- input: matched.input,
- requiresConfirmation: true,
- response: `该操作需要确认:${matched.template.label}\n${JSON.stringify(matched.input, null, 2)}`,
- };
- }
- const result = await executeWebMcpTool(toolName, matched.input);
- return {
- handled: true,
- toolName,
- template: matched.template,
- input: matched.input,
- requiresConfirmation: false,
- result,
- response: friendlyResult(toolName, matched.input, result),
- };
- };
- export const confirmAndExecuteWebMcpChatCommand = async (
- contentOrCommand: string | Pick<WebMcpChatResult, 'toolName' | 'input' | 'template'> & { documentTitle?: string }
- ): Promise<WebMcpChatResult> => {
- if (typeof contentOrCommand !== 'string') {
- if (!contentOrCommand.toolName || !contentOrCommand.input) return { handled: false };
- const result = await executeWebMcpTool(contentOrCommand.toolName, contentOrCommand.input, { confirmed: true });
- return {
- handled: true,
- toolName: contentOrCommand.toolName,
- input: contentOrCommand.input,
- template: contentOrCommand.template,
- requiresConfirmation: false,
- result,
- response: friendlyResult(contentOrCommand.toolName, {
- ...contentOrCommand.input,
- documentTitle: contentOrCommand.documentTitle,
- }, result),
- };
- }
- const content = contentOrCommand;
- const matched = matchWebMcpChatTemplate(content);
- const localCommand = matched ? { handled: false as const } : await parseLocalDocumentCommand(content);
- if (localCommand.command) {
- const { toolName, input, documentTitle } = localCommand.command;
- const result = await executeWebMcpTool(toolName, input);
- return {
- handled: true,
- toolName,
- input,
- requiresConfirmation: false,
- result,
- response: friendlyResult(toolName, { ...input, documentTitle }, result),
- };
- }
- if (!matched?.template.toolName) {
- try {
- const translated = await translateInput(content);
- const toolCall = translated.translation.toolCall;
- if (!toolCall) return { handled: false };
- const input = { ...toolCall.arguments };
- const result = await executeWebMcpTool(toolCall.name, input, { confirmed: true });
- return {
- handled: true,
- toolName: toolCall.name,
- input: toolCall.arguments,
- requiresConfirmation: false,
- result,
- response: friendlyResult(toolCall.name, input, result),
- };
- } catch (error) {
- return { handled: true, response: `WebMCP 操作执行失败:${error instanceof Error ? error.message : '未知错误'}` };
- }
- }
- const tool = getWebMcpTool(matched.template.toolName);
- if (!tool) return { handled: true, response: `WebMCP 工具不存在:${matched.template.toolName}` };
- const result = await executeWebMcpTool(matched.template.toolName, matched.input);
- return {
- handled: true,
- toolName: matched.template.toolName,
- template: matched.template,
- input: matched.input,
- requiresConfirmation: false,
- result,
- response: friendlyResult(matched.template.toolName, matched.input, result),
- };
- };
|