webMcpAgentService.ts 23 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556
  1. import { executeWebMcpTool, getWebMcpTool, type WebMcpToolResult } from './webMcpService';
  2. import {
  3. matchWebMcpChatTemplate,
  4. webMcpTemplateHelp,
  5. type WebMcpChatTemplate,
  6. } from './webMcpChatTemplates';
  7. import { getWebMcpTools } from './webMcpService';
  8. import { translateWebMcpInput } from '../share/webmcp';
  9. import { listDocuments } from './documentService';
  10. import { blockService } from './blockService';
  11. export interface WebMcpChatResult {
  12. handled: boolean;
  13. response?: string;
  14. toolName?: string;
  15. requiresConfirmation?: boolean;
  16. template?: WebMcpChatTemplate;
  17. input?: Record<string, unknown>;
  18. result?: WebMcpToolResult;
  19. }
  20. interface ExportCandidate {
  21. documentId: string;
  22. title: string;
  23. aliases?: string[];
  24. recordId?: string;
  25. }
  26. interface LocalDocumentCommand {
  27. toolName: string;
  28. input: Record<string, unknown>;
  29. documentTitle: string;
  30. }
  31. const READ_ONLY_TOOLS = new Set([
  32. 'list_documents',
  33. 'get_document',
  34. 'search_document',
  35. 'get_block',
  36. 'get_document_toc',
  37. 'get_document_stats',
  38. 'list_export_records',
  39. ]);
  40. const DOCUMENT_CANDIDATE_CACHE_TTL = 15_000;
  41. let documentCandidatesCache: { value: ExportCandidate[]; expiresAt: number } | undefined;
  42. let documentCandidatesRequest: Promise<ExportCandidate[]> | undefined;
  43. const stringifyResult = (result: WebMcpToolResult): string => {
  44. if (!result.ok) return `WebMCP 执行失败:${result.error || '未知错误'}`;
  45. return `WebMCP 执行结果:\n${JSON.stringify(result.data, null, 2)}`;
  46. };
  47. const getDocumentCandidates = async (): Promise<ExportCandidate[]> => {
  48. if (documentCandidatesCache && documentCandidatesCache.expiresAt > Date.now()) {
  49. return documentCandidatesCache.value;
  50. }
  51. if (documentCandidatesRequest) return documentCandidatesRequest;
  52. documentCandidatesRequest = loadDocumentCandidates();
  53. try {
  54. const value = await documentCandidatesRequest;
  55. documentCandidatesCache = { value, expiresAt: Date.now() + DOCUMENT_CANDIDATE_CACHE_TTL };
  56. return value;
  57. } finally {
  58. documentCandidatesRequest = undefined;
  59. }
  60. };
  61. const loadDocumentCandidates = async (): Promise<ExportCandidate[]> => {
  62. const unique = new Map<string, ExportCandidate>();
  63. try {
  64. const documentResult = await listDocuments({
  65. userId: 'default-user',
  66. page: 1,
  67. pageSize: 100,
  68. sortBy: 'updated_at',
  69. sortOrder: 'desc',
  70. });
  71. await Promise.all(documentResult.documents.map(async (document) => {
  72. const detail = await executeWebMcpTool('get_document', { documentId: document.id });
  73. const blocks = detail.ok && detail.data && typeof detail.data === 'object'
  74. ? (detail.data as { blocks?: unknown }).blocks
  75. : undefined;
  76. if (!Array.isArray(blocks)) return;
  77. const firstText = blocks
  78. .map((block) => {
  79. if (!block || typeof block !== 'object') return '';
  80. const content = (block as { content?: unknown }).content;
  81. if (typeof content === 'string') return content.trim();
  82. if (Array.isArray(content)) {
  83. return content
  84. .map((part) => part && typeof part === 'object' && typeof (part as { text?: unknown }).text === 'string'
  85. ? (part as { text: string }).text
  86. : '')
  87. .join('')
  88. .trim();
  89. }
  90. return '';
  91. })
  92. .find(Boolean);
  93. unique.set(document.id, { documentId: document.id, title: firstText || document.id });
  94. }));
  95. } catch (error) {
  96. console.warn('[WebMCP] 获取文档名称失败,将继续使用导出记录:', error);
  97. }
  98. const exportResult = await executeWebMcpTool('list_export_records', { page: 1, pageSize: 100 });
  99. if (exportResult.ok && exportResult.data && typeof exportResult.data === 'object') {
  100. const records = (exportResult.data as { records?: unknown }).records;
  101. if (Array.isArray(records)) {
  102. for (const record of records) {
  103. if (!record || typeof record !== 'object') continue;
  104. const item = record as Record<string, unknown>;
  105. if (typeof item.documentId !== 'string' || typeof item.fileName !== 'string') continue;
  106. const candidate = unique.get(item.documentId) || {
  107. documentId: item.documentId,
  108. title: item.fileName.replace(/\.(docx?|DOCX?)$/i, '').replace(/[_-]\d{8,}$/, '').trim(),
  109. };
  110. const exportTitle = item.fileName.replace(/\.(docx?|DOCX?)$/i, '').replace(/[_-]\d{8,}$/, '').trim();
  111. if (exportTitle && exportTitle !== candidate.title) {
  112. candidate.aliases = [...new Set([...(candidate.aliases || []), exportTitle])];
  113. }
  114. if (!candidate.recordId && typeof item.recordId === 'string') candidate.recordId = item.recordId;
  115. unique.set(item.documentId, candidate);
  116. }
  117. }
  118. }
  119. return [...unique.values()];
  120. };
  121. const normalizeDocumentText = (value: string): string =>
  122. value
  123. .toLocaleLowerCase()
  124. .replace(/\.(docx?|DOCX?)$/i, '')
  125. .replace(/(?:文档|文件)$/i, '')
  126. .replace(/[“”"'「」《》]/g, '')
  127. .replace(/\s+/g, '')
  128. .trim();
  129. const findDocumentCandidate = (
  130. title: string,
  131. candidates: ExportCandidate[]
  132. ): { candidate?: ExportCandidate; ambiguous: boolean } => {
  133. const normalizedTitle = normalizeDocumentText(title);
  134. const matches = candidates.filter((candidate) => {
  135. const candidateTitles = [candidate.title, ...(candidate.aliases || [])].map(normalizeDocumentText);
  136. return candidate.documentId === title || candidate.recordId === title || candidateTitles.some((candidateTitle) =>
  137. candidateTitle === normalizedTitle || candidateTitle.includes(normalizedTitle) || normalizedTitle.includes(candidateTitle)
  138. );
  139. });
  140. return { candidate: matches.length === 1 ? matches[0] : undefined, ambiguous: matches.length > 1 };
  141. };
  142. const getActionHelp = (action: string): string => {
  143. if (action === 'download_export_record') return '请告诉我要下载的文档名称,例如:下载产品说明文档。';
  144. if (action === 'export_document') return '请告诉我要导出的文档名称,例如:导出产品说明文档。';
  145. if (action === 'open_document') return '请告诉我要打开的文档名称,例如:打开产品说明文档。';
  146. if (action === 'get_document') return '请告诉我要读取的文档名称,例如:读取产品说明文档。';
  147. return '请告诉我要操作的文档名称和位置,例如:删除产品说明文档第一行、末行,或修改产品说明文档第二段为:新的内容。';
  148. };
  149. const chineseNumerals: Record<string, number> = {
  150. 零: 0,
  151. 〇: 0,
  152. 一: 1,
  153. 二: 2,
  154. 两: 2,
  155. 三: 3,
  156. 四: 4,
  157. 五: 5,
  158. 六: 6,
  159. 七: 7,
  160. 八: 8,
  161. 九: 9,
  162. 十: 10,
  163. 百: 100,
  164. };
  165. const parseChineseInteger = (value: string): number | undefined => {
  166. if (/^\d+$/.test(value)) return Number(value);
  167. let section = 0;
  168. let number = 0;
  169. for (const character of value) {
  170. const digit = chineseNumerals[character];
  171. if (digit === undefined) return undefined;
  172. if (digit === 10 || digit === 100) {
  173. section += (number || 1) * digit;
  174. number = 0;
  175. } else {
  176. number = digit;
  177. }
  178. }
  179. return section + number;
  180. };
  181. const parseBlockReference = (value: string): { reference?: string; remainder: string } => {
  182. const match = value.match(/(?:第\s*(\d+|[零〇一二两三四五六七八九十百]+)\s*(?:个)?\s*(?:行|段|块|标题|段落|条)|(?:最后|末尾|末)\s*(?:一行|一段|一个块|一块|一条|行|段|块|条)|首行|第一行)\s*$/i);
  183. if (!match) return { remainder: value };
  184. if (match[0].includes('最后') || match[0].includes('末尾') || match[0].includes('末')) {
  185. return { reference: 'last', remainder: value.slice(0, match.index).trim() };
  186. }
  187. if (match[0].includes('首行') || match[0].includes('第一行')) {
  188. return { reference: '1', remainder: value.slice(0, match.index).trim() };
  189. }
  190. const number = parseChineseInteger(match[1]);
  191. return number !== undefined && Number.isInteger(number) && number > 0
  192. ? { reference: String(number), remainder: value.slice(0, match.index).trim() }
  193. : { remainder: value };
  194. };
  195. const resolveBlockReference = async (documentId: string, reference: string): Promise<string | undefined> => {
  196. const blockResult = await blockService.getBlocks(documentId);
  197. const blocks = [...blockResult.blocks].sort((left, right) => left.block_order - right.block_order);
  198. const block = reference === 'last' ? blocks.at(-1) : blocks[Number(reference) - 1];
  199. return block?.id;
  200. };
  201. const parseLocalDocumentCommand = async (content: string): Promise<
  202. { command?: LocalDocumentCommand; response?: string; handled: boolean }
  203. > => {
  204. const text = content.trim();
  205. const actionMatch = text.match(/^(打开|查看|阅读|读取|导出|下载|删除|修改|更新|新增|插入)\s*(.*?)(?:文档|文件)?(?:这个文档|该文档)?$/i);
  206. if (!actionMatch) return { handled: false };
  207. const actionText = actionMatch[1];
  208. let remainder = actionMatch[2].trim().replace(/^(?:文档|文件)\s*/, '').trim();
  209. const isBlockAction = /^(删除|修改|更新|新增|插入)$/.test(actionText);
  210. let blockId: string | undefined;
  211. let blockReference: string | undefined;
  212. let blockContent: string | undefined;
  213. if (isBlockAction) {
  214. const blockMatch = remainder.match(/\b(block-[\w-]+)\b/i);
  215. blockId = blockMatch?.[1];
  216. blockContent = remainder.match(/(?:为|改为|内容为|插入)[::]?\s*(.+)$/)?.[1];
  217. remainder = remainder.replace(blockId || '', '').replace(/(?:为|改为|内容为|插入)[::]?\s*.+$/, '').trim();
  218. const parsedReference = parseBlockReference(remainder);
  219. blockReference = parsedReference.reference;
  220. remainder = parsedReference.remainder;
  221. remainder = remainder.replace(/(?:这个|该)?(?:文档|文件)\s*$/i, '').trim();
  222. }
  223. const toolName = actionText === '打开' ? 'open_document'
  224. : actionText === '导出' ? 'export_document'
  225. : actionText === '下载' ? 'download_export_record'
  226. : actionText === '读取' || actionText === '查看' || actionText === '阅读' ? 'get_document'
  227. : actionText === '删除' ? 'delete_block'
  228. : actionText === '新增' || actionText === '插入' ? 'insert_block'
  229. : 'update_block';
  230. if (!remainder || (isBlockAction && !blockId && !blockReference)) return { handled: true, response: getActionHelp(toolName) };
  231. const candidates = await getDocumentCandidates();
  232. const { candidate, ambiguous } = findDocumentCandidate(remainder, candidates);
  233. if (ambiguous) return { handled: true, response: `找到多份与“${remainder}”相近的文档,请提供更完整的文档名称。` };
  234. if (!candidate) return { handled: true, response: `没有找到名为“${remainder}”的文档,请确认文档名称后重试。` };
  235. if (blockReference) {
  236. try {
  237. blockId = await resolveBlockReference(candidate.documentId, blockReference);
  238. } catch (error) {
  239. console.warn('[WebMCP] 获取文档块失败:', error);
  240. }
  241. if (!blockId) {
  242. const position = blockReference === 'last' ? '末尾' : `第 ${blockReference}`;
  243. return { handled: true, response: `没有找到“${remainder}”的${position}内容块,请确认位置后重试。` };
  244. }
  245. }
  246. const input: Record<string, unknown> = toolName === 'download_export_record'
  247. ? { recordId: candidate.recordId || candidate.documentId }
  248. : { documentId: candidate.documentId };
  249. if (blockId) input.blockId = blockId;
  250. if (toolName === 'update_block' && blockContent) input.content = blockContent;
  251. if (toolName === 'insert_block' && blockContent) {
  252. input.type = 'paragraph';
  253. input.content = blockContent;
  254. }
  255. if (isBlockAction && (!input.content && toolName !== 'delete_block')) {
  256. return { handled: true, response: `请补充要${toolName === 'insert_block' ? '插入' : '修改'}的内容。` };
  257. }
  258. return { handled: true, command: { toolName, input, documentTitle: candidate.title } };
  259. };
  260. const translateInput = async (content: string) =>
  261. translateWebMcpInput(content, {
  262. availableTools: getWebMcpTools().map((tool) => tool.name),
  263. documentCandidates: await getDocumentCandidates(),
  264. locale: 'zh-CN',
  265. });
  266. const isDocumentAction = (content: string): boolean =>
  267. /(打开|查看|阅读|读取|删除|修改|更新|新增|插入|导出|下载).*(文档|文件|区块|块)/i.test(content);
  268. const documentActionHelp = (content: string): string => {
  269. if (/(打开|查看|阅读|读取)/i.test(content)) {
  270. return '我还不知道你要打开哪份文档,请告诉我文档名称,例如:打开产品说明文档。';
  271. }
  272. if (/(删除|修改|更新|新增|插入)/i.test(content)) {
  273. return '请提供文档名称和文档块 ID,例如:修改产品说明文档 block-p-50 为:新的内容。';
  274. }
  275. if (/(导出)/i.test(content)) return '请告诉我要导出的文档名称,例如:导出产品说明文档。';
  276. if (/(下载)/i.test(content)) return '请提供导出记录 ID,例如:下载导出记录 rec-abc123。';
  277. return '请补充要操作的文档名称或文档块信息。';
  278. };
  279. const formatBlockContent = (content: unknown): string => {
  280. if (typeof content === 'string') return content.trim();
  281. if (Array.isArray(content)) {
  282. return content
  283. .map((part) => {
  284. if (typeof part === 'string') return part;
  285. if (part && typeof part === 'object' && typeof (part as { text?: unknown }).text === 'string') {
  286. return (part as { text: string }).text;
  287. }
  288. return '';
  289. })
  290. .join('')
  291. .trim();
  292. }
  293. if (content && typeof content === 'object') {
  294. const title = (content as { title?: unknown }).title;
  295. if (typeof title === 'string' && title.trim()) return title.trim();
  296. try {
  297. return JSON.stringify(content);
  298. } catch {
  299. return '';
  300. }
  301. }
  302. return '';
  303. };
  304. const formatSearchResult = (documentLabel: string, data: unknown): string => {
  305. if (!data || typeof data !== 'object') return `未找到“${documentLabel}”中的匹配内容。`;
  306. const payload = data as { blocks?: unknown; total?: unknown; query?: unknown };
  307. const blocks = Array.isArray(payload.blocks) ? payload.blocks : [];
  308. const total = typeof payload.total === 'number' ? payload.total : blocks.length;
  309. const query = typeof payload.query === 'string' ? payload.query : '';
  310. if (!blocks.length) {
  311. return `在“${documentLabel}”中未找到${query ? `包含“${query}”的` : ''}内容。`;
  312. }
  313. const lines = blocks.slice(0, 20).map((block, index) => {
  314. if (!block || typeof block !== 'object') return `${index + 1}. (无法读取内容)`;
  315. const item = block as { id?: unknown; type?: unknown; content?: unknown };
  316. const id = typeof item.id === 'string' ? item.id : '未知块';
  317. const type = typeof item.type === 'string' ? item.type : '内容块';
  318. const content = formatBlockContent(item.content).replace(/\s+/g, ' ').trim();
  319. const preview = content.length > 500 ? `${content.slice(0, 500)}...` : content;
  320. return `${index + 1}. 【${type}】${preview || '(无可显示内容)'}\n 块 ID:${id}`;
  321. });
  322. const suffix = total > blocks.length ? `\n\n仅显示前 ${blocks.length} 条,共找到 ${total} 条。` : '';
  323. return `在“${documentLabel}”中找到 ${total} 条${query ? `包含“${query}”的` : ''}内容:\n${lines.join('\n')}\n${suffix}`.trim();
  324. };
  325. const getDocumentUrl = (documentId: string): string => {
  326. const url = new URL(window.location.href);
  327. url.searchParams.set('documentId', documentId);
  328. return url.toString();
  329. };
  330. const formatDocumentListResult = (data: unknown): string => {
  331. if (!data || typeof data !== 'object') return '暂无可查看的文档。';
  332. const payload = data as { documents?: unknown; pagination?: { total?: unknown } };
  333. const documents = Array.isArray(payload.documents) ? payload.documents : [];
  334. if (!documents.length) return '暂无可查看的文档。';
  335. const lines = documents.map((document, index) => {
  336. if (!document || typeof document !== 'object') return `${index + 1}. 文档信息无效`;
  337. const item = document as { id?: unknown; updatedAt?: unknown };
  338. const documentId = typeof item.id === 'string' ? item.id : '';
  339. if (!documentId) return `${index + 1}. 文档 ID 缺失`;
  340. const updatedAt = typeof item.updatedAt === 'string' ? `(更新于 ${item.updatedAt})` : '';
  341. return `${index + 1}. ${documentId}${updatedAt}\n ${getDocumentUrl(documentId)}`;
  342. });
  343. const total = typeof payload.pagination?.total === 'number' ? payload.pagination.total : documents.length;
  344. return `共找到 ${total} 个文档:\n${lines.join('\n')}`;
  345. };
  346. const friendlyResult = (
  347. toolName: string,
  348. input: Record<string, unknown>,
  349. result: WebMcpToolResult
  350. ): string => {
  351. if (!result.ok) return `操作未完成:${result.error || '未知错误'}`;
  352. const documentId = typeof input.documentId === 'string' ? input.documentId : '';
  353. const blockId = typeof input.blockId === 'string' ? input.blockId : '';
  354. const documentLabel = typeof input.documentTitle === 'string' ? input.documentTitle : documentId;
  355. if (toolName === 'list_documents') return formatDocumentListResult(result.data);
  356. if (toolName === 'open_document') return `已帮您打开“${documentLabel || '指定'}”文档。`;
  357. if (toolName === 'get_document') return `已读取“${documentLabel || documentId}”文档内容。`;
  358. if (toolName === 'search_document') return formatSearchResult(documentLabel || documentId, result.data);
  359. if (toolName === 'update_block') return `已修改“${documentLabel || documentId}”中的区块 ${blockId}。`;
  360. if (toolName === 'delete_block') return `已删除“${documentLabel || documentId}”中的区块 ${blockId}。`;
  361. if (toolName === 'insert_block') return `已向“${documentLabel || documentId}”插入新的内容块。`;
  362. if (toolName === 'export_document') return `已开始导出“${documentLabel || documentId}”文档。`;
  363. if (toolName === 'download_export_record') return `已开始下载导出记录 ${String(input.recordId || '')}。`;
  364. return stringifyResult(result);
  365. };
  366. export const isWebMcpChatCommand = (content: string): boolean =>
  367. content.trim() === '查看 WebMCP 模板' || matchWebMcpChatTemplate(content) !== null;
  368. export const parseWebMcpChatCommand = (content: string) => matchWebMcpChatTemplate(content);
  369. export const executeWebMcpChatCommand = async (content: string): Promise<WebMcpChatResult> => {
  370. if (content.trim() === '查看 WebMCP 模板') {
  371. return { handled: true, response: webMcpTemplateHelp() };
  372. }
  373. const matched = matchWebMcpChatTemplate(content);
  374. const localCommand = matched ? { handled: false as const } : await parseLocalDocumentCommand(content);
  375. if (localCommand.handled && localCommand.command) {
  376. const tool = getWebMcpTool(localCommand.command.toolName);
  377. if (!tool) return { handled: true, response: `WebMCP 工具不存在:${localCommand.command.toolName}` };
  378. const { toolName, input, documentTitle } = localCommand.command;
  379. if (tool.requiresConfirmation) {
  380. return { handled: true, toolName, input, requiresConfirmation: true, response: `即将对“${documentTitle}”执行${tool.description.replace('。需要用户确认。', '')},请确认。` };
  381. }
  382. const result = await executeWebMcpTool(toolName, input);
  383. return { handled: true, toolName, input, result, response: friendlyResult(toolName, { ...input, documentTitle }, result) };
  384. }
  385. if (localCommand.handled && localCommand.response) return { handled: true, response: localCommand.response };
  386. if (!matched || !matched.template.toolName) {
  387. try {
  388. const translated = await translateInput(content);
  389. const translation = translated.translation;
  390. if (translation.intent === 'clarify') {
  391. return { handled: true, response: translation.question || '请补充更多信息后重试。' };
  392. }
  393. if (!translation.toolCall) {
  394. return isDocumentAction(content)
  395. ? { handled: true, response: documentActionHelp(content) }
  396. : { handled: false };
  397. }
  398. const tool = getWebMcpTool(translation.toolCall.name);
  399. if (!tool) return { handled: true, response: `WebMCP 工具不存在:${translation.toolCall.name}` };
  400. const input = { ...translation.toolCall.arguments };
  401. const displayInput = { ...input };
  402. if (!displayInput.documentTitle && typeof displayInput.documentId === 'string') displayInput.documentTitle = displayInput.documentId;
  403. const requiresConfirmation = Boolean(tool.requiresConfirmation || translation.requiresConfirmation);
  404. if (requiresConfirmation) {
  405. return {
  406. handled: true,
  407. toolName: tool.name,
  408. input,
  409. requiresConfirmation: true,
  410. response: `即将${tool.description.replace('。需要用户确认。', '')},请确认。`,
  411. };
  412. }
  413. const result = await executeWebMcpTool(tool.name, input);
  414. return {
  415. handled: true,
  416. toolName: tool.name,
  417. input,
  418. requiresConfirmation: false,
  419. result,
  420. response: friendlyResult(tool.name, displayInput, result),
  421. };
  422. } catch (error) {
  423. console.warn('[WebMCP] 远端转译不可用,回退普通 AI:', error);
  424. return { handled: false };
  425. }
  426. }
  427. const toolName = matched.template.toolName;
  428. const tool = getWebMcpTool(toolName);
  429. if (!tool) return { handled: true, response: `WebMCP 工具不存在:${toolName}`, toolName };
  430. if (!READ_ONLY_TOOLS.has(toolName)) {
  431. return {
  432. handled: true,
  433. toolName,
  434. template: matched.template,
  435. input: matched.input,
  436. requiresConfirmation: true,
  437. response: `该操作需要确认:${matched.template.label}\n${JSON.stringify(matched.input, null, 2)}`,
  438. };
  439. }
  440. const result = await executeWebMcpTool(toolName, matched.input);
  441. return {
  442. handled: true,
  443. toolName,
  444. template: matched.template,
  445. input: matched.input,
  446. requiresConfirmation: false,
  447. result,
  448. response: friendlyResult(toolName, matched.input, result),
  449. };
  450. };
  451. export const confirmAndExecuteWebMcpChatCommand = async (
  452. contentOrCommand: string | Pick<WebMcpChatResult, 'toolName' | 'input' | 'template'> & { documentTitle?: string }
  453. ): Promise<WebMcpChatResult> => {
  454. if (typeof contentOrCommand !== 'string') {
  455. if (!contentOrCommand.toolName || !contentOrCommand.input) return { handled: false };
  456. const result = await executeWebMcpTool(contentOrCommand.toolName, contentOrCommand.input, { confirmed: true });
  457. return {
  458. handled: true,
  459. toolName: contentOrCommand.toolName,
  460. input: contentOrCommand.input,
  461. template: contentOrCommand.template,
  462. requiresConfirmation: false,
  463. result,
  464. response: friendlyResult(contentOrCommand.toolName, {
  465. ...contentOrCommand.input,
  466. documentTitle: contentOrCommand.documentTitle,
  467. }, result),
  468. };
  469. }
  470. const content = contentOrCommand;
  471. const matched = matchWebMcpChatTemplate(content);
  472. const localCommand = matched ? { handled: false as const } : await parseLocalDocumentCommand(content);
  473. if (localCommand.command) {
  474. const { toolName, input, documentTitle } = localCommand.command;
  475. const result = await executeWebMcpTool(toolName, input);
  476. return {
  477. handled: true,
  478. toolName,
  479. input,
  480. requiresConfirmation: false,
  481. result,
  482. response: friendlyResult(toolName, { ...input, documentTitle }, result),
  483. };
  484. }
  485. if (!matched?.template.toolName) {
  486. try {
  487. const translated = await translateInput(content);
  488. const toolCall = translated.translation.toolCall;
  489. if (!toolCall) return { handled: false };
  490. const input = { ...toolCall.arguments };
  491. const result = await executeWebMcpTool(toolCall.name, input, { confirmed: true });
  492. return {
  493. handled: true,
  494. toolName: toolCall.name,
  495. input: toolCall.arguments,
  496. requiresConfirmation: false,
  497. result,
  498. response: friendlyResult(toolCall.name, input, result),
  499. };
  500. } catch (error) {
  501. return { handled: true, response: `WebMCP 操作执行失败:${error instanceof Error ? error.message : '未知错误'}` };
  502. }
  503. }
  504. const tool = getWebMcpTool(matched.template.toolName);
  505. if (!tool) return { handled: true, response: `WebMCP 工具不存在:${matched.template.toolName}` };
  506. const result = await executeWebMcpTool(matched.template.toolName, matched.input);
  507. return {
  508. handled: true,
  509. toolName: matched.template.toolName,
  510. template: matched.template,
  511. input: matched.input,
  512. requiresConfirmation: false,
  513. result,
  514. response: friendlyResult(matched.template.toolName, matched.input, result),
  515. };
  516. };