From 672d0b7ae0507d116f8027e4a844cfd348c0bcd3 Mon Sep 17 00:00:00 2001
From: duanfuxiang
Date: Mon, 9 Jun 2025 02:19:41 +0800
Subject: [PATCH] update mcp tools
---
CHANGELOG.yaml | 5 +
manifest.json | 2 +-
package.json | 2 +-
src/components/chat-view/McpHubView.tsx | 6 +-
src/components/chat-view/QueryProgress.tsx | 4 +-
src/database/json/constants.ts | 1 +
.../json/convert-data/ConvertDataManager.ts | 185 +++++++++++++++++
src/database/json/convert-data/index.ts | 2 +
src/database/json/convert-data/types.ts | 33 +++
src/lang/locale/zh-cn.ts | 2 +-
src/utils/prompt-generator.ts | 192 ++++++++++++++----
11 files changed, 386 insertions(+), 48 deletions(-)
create mode 100644 src/database/json/convert-data/ConvertDataManager.ts
create mode 100644 src/database/json/convert-data/index.ts
create mode 100644 src/database/json/convert-data/types.ts
diff --git a/CHANGELOG.yaml b/CHANGELOG.yaml
index 7667fe3..0cc3945 100644
--- a/CHANGELOG.yaml
+++ b/CHANGELOG.yaml
@@ -1,4 +1,9 @@
releases:
+ - version: "0.6.0"
+ features:
+ - "Added Infio built-in MCP (Model Control Protocol) support"
+ - "Optimized startup performance for faster loading times"
+ - "Support Infio provider MCP tools"
- version: "0.5.2"
features:
- "Added Infio provider"
diff --git a/manifest.json b/manifest.json
index af558fb..bd6f4ed 100644
--- a/manifest.json
+++ b/manifest.json
@@ -1,7 +1,7 @@
{
"id": "infio-copilot",
"name": "Infio Copilot",
- "version": "0.5.2",
+ "version": "0.6.0",
"minAppVersion": "0.15.0",
"description": "A Cursor-inspired AI assistant for notes that offers smart autocomplete and interactive chat with your selected notes",
"author": "Felix.D",
diff --git a/package.json b/package.json
index e1df92c..bfee0ce 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "obsidian-infio-copilot",
- "version": "0.5.2",
+ "version": "0.6.0",
"description": "A Cursor-inspired AI assistant that offers smart autocomplete and interactive chat with your selected notes",
"main": "main.js",
"scripts": {
diff --git a/src/components/chat-view/McpHubView.tsx b/src/components/chat-view/McpHubView.tsx
index 4ebbd60..203a4f8 100644
--- a/src/components/chat-view/McpHubView.tsx
+++ b/src/components/chat-view/McpHubView.tsx
@@ -128,11 +128,11 @@ const McpHubView = () => {
- {tool.name}
+ {tool.name.replace('COMPOSIO_SEARCH_', '')}
{tool.description && (
-
{tool.description}
+
{tool.description.replace('composio', '')}
)}
{(tool.inputSchema && (() => {
const schema = tool.inputSchema;
@@ -285,7 +285,7 @@ const McpHubView = () => {
{isExpanded ?
:
}
- {server.name}
+ {server.name.replace('infio-builtin-server', 'builtin')}
e.stopPropagation()}>
diff --git a/src/components/chat-view/QueryProgress.tsx b/src/components/chat-view/QueryProgress.tsx
index b6d2777..ac4e4b4 100644
--- a/src/components/chat-view/QueryProgress.tsx
+++ b/src/components/chat-view/QueryProgress.tsx
@@ -87,7 +87,7 @@ export default function QueryProgress({
{t('chat.queryProgress.readingFilesDone')}
- {t('chat.queryProgress.filesLoaded', { count: state.fileContents.length })}
+ {t('chat.queryProgress.filesLoaded').replace('{count}', state.fileContents.length.toString())}
)
@@ -115,7 +115,7 @@ export default function QueryProgress({
{t('chat.queryProgress.readingWebsitesDone')}
- {t('chat.queryProgress.websitesLoaded', { count: state.websiteContents.length })}
+ {t('chat.queryProgress.websitesLoaded').replace('{count}', state.websiteContents.length.toString())}
)
diff --git a/src/database/json/constants.ts b/src/database/json/constants.ts
index 02ab272..f9576bc 100644
--- a/src/database/json/constants.ts
+++ b/src/database/json/constants.ts
@@ -2,4 +2,5 @@ export const ROOT_DIR = '.infio_json_db'
export const COMMAND_DIR = 'commands'
export const CHAT_DIR = 'chats'
export const CUSTOM_MODE_DIR = 'custom_modes'
+export const CONVERT_DATA_DIR = 'convert_data'
export const INITIAL_MIGRATION_MARKER = '.initial_migration_completed'
diff --git a/src/database/json/convert-data/ConvertDataManager.ts b/src/database/json/convert-data/ConvertDataManager.ts
new file mode 100644
index 0000000..8e1e226
--- /dev/null
+++ b/src/database/json/convert-data/ConvertDataManager.ts
@@ -0,0 +1,185 @@
+import { createHash } from 'crypto'
+
+import { App } from 'obsidian'
+import { v4 as uuidv4 } from 'uuid'
+
+
+import { AbstractJsonRepository } from '../base'
+import { CONVERT_DATA_DIR, ROOT_DIR } from '../constants'
+
+import {
+ CONVERT_DATA_SCHEMA_VERSION,
+ ConvertData,
+ ConvertDataMetadata,
+ ConvertType
+} from './types'
+
+export class ConvertDataManager extends AbstractJsonRepository<
+ ConvertData,
+ ConvertDataMetadata
+> {
+ constructor(app: App) {
+ super(app, `${ROOT_DIR}/${CONVERT_DATA_DIR}`)
+ }
+
+ protected parseFileName(fileName: string): ConvertDataMetadata | null {
+ // Check if filename is a valid MD5 hash (32 hex characters)
+ const match = fileName.match(
+ new RegExp(`^([a-f0-9]{32})\\.json$`),
+ )
+ if (!match) return null
+
+ return {
+ id: match[1],
+ md5Hash: match[1],
+ name: '',
+ type: '',
+ source: '',
+ createdAt: 0,
+ updatedAt: 0,
+ schemaVersion: 0,
+ }
+ }
+
+ protected generateFileName(data: ConvertData): string {
+ // Format: {md5Hash}.json
+ return `${data.md5Hash}.json`
+ }
+
+ /**
+ * 生成源的MD5哈希值
+ */
+ public static generateSourceHash(source: string): string {
+ return createHash('md5').update(source).digest('hex')
+ }
+
+ /**
+ * 创建转换数据记录
+ */
+ public async createConvertData(
+ convertData: Omit<
+ ConvertData,
+ 'id' | 'md5Hash' | 'createdAt' | 'updatedAt' | 'schemaVersion'
+ >,
+ ): Promise {
+ const md5Hash = ConvertDataManager.generateSourceHash(convertData.source)
+ const now = Date.now()
+
+ const newConvertData: ConvertData = {
+ id: uuidv4(),
+ md5Hash,
+ ...convertData,
+ createdAt: now,
+ updatedAt: now,
+ schemaVersion: CONVERT_DATA_SCHEMA_VERSION,
+ }
+
+ await this.create(newConvertData)
+ return newConvertData
+ }
+
+ /**
+ * 根据源查找转换数据
+ */
+ public async findBySource(source: string): Promise {
+ const md5Hash = ConvertDataManager.generateSourceHash(source)
+ return this.findByHash(md5Hash)
+ }
+
+ /**
+ * 根据MD5哈希查找转换数据
+ */
+ public async findByHash(md5Hash: string): Promise {
+ const fileName = `${md5Hash}.json`
+
+ try {
+ return await this.read(fileName)
+ } catch {
+ return null
+ }
+ }
+
+ /**
+ * 根据ID查找转换数据
+ */
+ public async findById(id: string): Promise {
+ const allMetadata = await this.listMetadata()
+ const targetMetadata = allMetadata.find((meta) => meta?.id === id)
+
+ if (!targetMetadata) return null
+
+ const fileName = `${targetMetadata.md5Hash}.json`
+
+ try {
+ return await this.read(fileName)
+ } catch {
+ return null
+ }
+ }
+
+ /**
+ * 更新转换数据
+ */
+ public async updateConvertData(
+ id: string,
+ updates: Partial<
+ Omit
+ >,
+ ): Promise {
+ const convertData = await this.findById(id)
+ if (!convertData) return null
+
+ const updatedConvertData: ConvertData = {
+ ...convertData,
+ ...updates,
+ updatedAt: Date.now(),
+ }
+
+ await this.update(convertData, updatedConvertData)
+ return updatedConvertData
+ }
+
+ /**
+ * 删除转换数据
+ */
+ public async deleteConvertData(id: string): Promise {
+ const convertData = await this.findById(id)
+ if (!convertData) return false
+
+ const fileName = this.generateFileName(convertData)
+ await this.delete(fileName)
+ return true
+ }
+
+ /**
+ * 列出所有转换数据
+ */
+ public async listConvertData(): Promise {
+ const allMetadata = await this.listMetadata()
+ const allConvertData = await Promise.all(
+ allMetadata.map(async (meta) => {
+ if (!meta) return null
+
+ const fileName = `${meta.md5Hash}.json`
+
+ try {
+ return await this.read(fileName)
+ } catch {
+ return null
+ }
+ })
+ )
+
+ return allConvertData
+ .filter((data): data is ConvertData => data !== null)
+ .sort((a, b) => b.updatedAt - a.updatedAt)
+ }
+
+ /**
+ * 根据类型列出转换数据
+ */
+ public async listByType(type: ConvertType): Promise {
+ const allData = await this.listConvertData()
+ return allData.filter((data) => data.type === type)
+ }
+}
diff --git a/src/database/json/convert-data/index.ts b/src/database/json/convert-data/index.ts
new file mode 100644
index 0000000..1d5308b
--- /dev/null
+++ b/src/database/json/convert-data/index.ts
@@ -0,0 +1,2 @@
+export * from './types'
+export * from './ConvertDataManager'
diff --git a/src/database/json/convert-data/types.ts b/src/database/json/convert-data/types.ts
new file mode 100644
index 0000000..1cbd1fe
--- /dev/null
+++ b/src/database/json/convert-data/types.ts
@@ -0,0 +1,33 @@
+import { z } from "zod"
+
+export const CONVERT_DATA_SCHEMA_VERSION = 1
+
+export const convertTypeSchema = z.enum(['CONVERT_VIDEO', 'CONVERT_DOCUMENT'])
+
+export type ConvertType = z.infer
+
+export const convertDataSchema = z.object({
+ id: z.string().uuid("Invalid ID"),
+ md5Hash: z.string().min(32, "MD5 hash must be 32 characters"), // 用于查询的md5
+ name: z.string().min(1, "Name is required"), // url标题或文件名称
+ type: convertTypeSchema, // CONVERT_VIDEO 或 CONVERT_DOCUMENT
+ source: z.string().min(1, "Source is required"), // 转换源(视频链接或文件路径)
+ contentPath: z.string().optional(), // 转换后存储的md文件路径,可能被移动
+ content: z.string().min(1, "Content is required"), // 转换后的markdown文本
+ createdAt: z.number().int().positive(),
+ updatedAt: z.number().int().positive(),
+ schemaVersion: z.literal(CONVERT_DATA_SCHEMA_VERSION),
+})
+
+export type ConvertData = z.infer
+
+export type ConvertDataMetadata = {
+ id: string
+ md5Hash: string
+ name: string
+ type: string
+ source: string
+ createdAt: number
+ updatedAt: number
+ schemaVersion: number
+}
diff --git a/src/lang/locale/zh-cn.ts b/src/lang/locale/zh-cn.ts
index c2f8dfb..baf80f9 100644
--- a/src/lang/locale/zh-cn.ts
+++ b/src/lang/locale/zh-cn.ts
@@ -409,7 +409,7 @@ export default {
noErrors: "没有错误记录",
parameters: "参数",
toolNoDescription: "无描述",
- useMcpToolFrom: "Use MCP tool from",
+ useMcpToolFrom: "使用来自以下的 MCP 工具:",
}
}
};
diff --git a/src/utils/prompt-generator.ts b/src/utils/prompt-generator.ts
index 7194858..f91c1c3 100644
--- a/src/utils/prompt-generator.ts
+++ b/src/utils/prompt-generator.ts
@@ -6,6 +6,8 @@ import { DiffStrategy } from '../core/diff/DiffStrategy'
import { McpHub } from '../core/mcp/McpHub'
import { SystemPrompt } from '../core/prompts/system'
import { RAGEngine } from '../core/rag/rag-engine'
+import { ConvertDataManager } from '../database/json/convert-data/ConvertDataManager'
+import { ConvertType } from '../database/json/convert-data/types'
import { SelectVector } from '../database/schema'
import { ChatMessage, ChatUserMessage } from '../types/chat'
import { ContentPart, RequestMessage } from '../types/llm/request'
@@ -141,6 +143,7 @@ export class PromptGenerator {
private customModePrompts: CustomModePrompts | null = null
private customModeList: ModeConfig[] | null = null
private getMcpHub: () => Promise | null = null
+ private convertDataManager: ConvertDataManager
private static readonly EMPTY_ASSISTANT_MESSAGE: RequestMessage = {
role: 'assistant',
content: '',
@@ -163,6 +166,7 @@ export class PromptGenerator {
this.customModePrompts = customModePrompts ?? null
this.customModeList = customModeList ?? null
this.getMcpHub = getMcpHub ?? null
+ this.convertDataManager = new ConvertDataManager(app)
}
public async generateRequestMessages({
@@ -388,14 +392,22 @@ export class PromptGenerator {
completedFiles: completedFiles
})
- const content = await getFileOrFolderContent(
- file,
- this.app.vault,
- this.app
- )
+ // 如果文件不是 md 文件且 mcpHub 存在,使用 MCP 工具转换
+ const mcpHub = await this.getMcpHub?.()
+ let content: string
+ let markdownFilePath = ''
+ if (file.extension !== 'md' && mcpHub?.isBuiltInServerAvailable()) {
+ [content, markdownFilePath] = await this.callMcpToolConvertDocument(file, mcpHub)
+ } else {
+ content = await getFileOrFolderContent(
+ file,
+ this.app.vault,
+ this.app
+ )
+ }
// 创建Markdown文件
- const markdownFilePath = await this.createMarkdownFileForContent(
+ markdownFilePath = markdownFilePath || await this.createMarkdownFileForContent(
file.path,
content,
false
@@ -522,13 +534,11 @@ export class PromptGenerator {
completedUrls: completedUrls
})
- const content = await this.getWebsiteContent(url, mcpHub)
-
+ const [content, mcpContentPath] = await this.getWebsiteContent(url, mcpHub)
// 从内容中提取标题
const websiteTitle = this.extractTitleFromWebsiteContent(content, url)
- // 为网页内容创建Markdown文件
- const markdownFilePath = await this.createMarkdownFileForContent(
+ const contentPath = mcpContentPath || await this.createMarkdownFileForContent(
url,
content,
true,
@@ -536,8 +546,8 @@ export class PromptGenerator {
)
completedUrls++
- urlContents.push({ url: markdownFilePath, content }) // 这里url改为markdownFilePath
- allWebsiteReadResults.push({ url: markdownFilePath, content }) // 同样这里也改为markdownFilePath
+ urlContents.push({ url: contentPath, content })
+ allWebsiteReadResults.push({ url: contentPath, content })
}
// 网页读取完成
@@ -574,8 +584,11 @@ export class PromptGenerator {
// 如果当前文件不是 md 文件且 mcpHub 存在,使用 MCP 工具转换
const mcpHub = await this.getMcpHub?.()
+ let currentMarkdownFilePath = ''
if (currentFile.file.extension !== 'md' && mcpHub?.isBuiltInServerAvailable()) {
- currentFileContent = await this.callMcpToolConvertDocument(currentFile.file, mcpHub)
+ const [mcpCurrFileContent, mcpCurrFileContentPath] = await this.callMcpToolConvertDocument(currentFile.file, mcpHub)
+ currentFileContent = mcpCurrFileContent
+ currentMarkdownFilePath = mcpCurrFileContentPath
} else {
currentFileContent = await getFileOrFolderContent(
currentFile.file,
@@ -585,7 +598,7 @@ export class PromptGenerator {
}
// 为当前文件创建Markdown文件
- const currentMarkdownFilePath = await this.createMarkdownFileForContent(
+ currentMarkdownFilePath = currentMarkdownFilePath || await this.createMarkdownFileForContent(
currentFile.file.path,
currentFileContent,
false
@@ -966,22 +979,18 @@ When writing out new markdown blocks, remember not to include "line_number|" at
}
- private async getPdfContent(file: TFile): Promise {
- return await parsePdfContent(file, this.app)
- }
-
-
/**
* TODO: Improve markdown conversion logic
* - filter visually hidden elements
* ...
*/
- private async getWebsiteContent(url: string, mcpHub: McpHub | null): Promise {
+ private async getWebsiteContent(url: string, mcpHub: McpHub | null): Promise<[string, string]> {
const mcpHubAvailable = mcpHub?.isBuiltInServerAvailable()
if (mcpHubAvailable && isVideoUrl(url)) {
- return this.callMcpToolConvertVideo(url, mcpHub)
+ const [md, mdPath] = await this.callMcpToolConvertVideo(url, mcpHub)
+ return [md, mdPath]
}
if (isYoutubeUrl(url)) {
@@ -989,17 +998,28 @@ When writing out new markdown blocks, remember not to include "line_number|" at
const { title, transcript } =
await YoutubeTranscript.fetchTranscriptAndMetadata(url)
- return `Title: ${title}
+ return [
+ `Title: ${title}
Video Transcript:
-${transcript.map((t) => `${t.offset}: ${t.text}`).join('\n')}`
+${transcript.map((t) => `${t.offset}: ${t.text}`).join('\n')}`,
+ ''
+ ]
}
const response = await requestUrl({ url })
- return htmlToMarkdown(response.text)
+ return [htmlToMarkdown(response.text), '']
}
- private async callMcpToolConvertVideo(url: string, mcpHub: McpHub): Promise {
+ private async callMcpToolConvertVideo(url: string, mcpHub: McpHub): Promise<[string, string]> {
+ // 首先检查缓存
+ const cachedData = await this.convertDataManager.findBySource(url)
+ if (cachedData) {
+ console.log(`Using cached video conversion for: ${url}`)
+ return [cachedData.content, cachedData.contentPath]
+ }
+
+ // 如果没有缓存,进行转换
const response = await mcpHub.callTool(
'infio-builtin-server',
'CONVERT_VIDEO',
@@ -1007,23 +1027,41 @@ ${transcript.map((t) => `${t.offset}: ${t.text}`).join('\n')}`
)
// 处理图片内容并获取图片引用
- const imageReferences = await this.processImagesInResponse(response.content)
+ // @ts-ignore
+ await this.processImagesInResponse(response.content)
const textContent = response.content.find((c) => c.type === 'text')
- let result = textContent?.text || ''
+ // @ts-ignore
+ const md = textContent?.text as string || ''
- // 在文本内容末尾添加图片引用
- if (imageReferences.length > 0) {
- result += '\n\n## 图片内容\n\n'
- imageReferences.forEach(imagePath => {
- result += `\n\n`
- })
- }
+ // 创建Markdown文件
+ const websiteTitle = this.extractTitleFromWebsiteContent(md, url)
- return result
+ // 为网页内容创建Markdown文件
+ const mdPath = await this.createMarkdownFileForContent(
+ url,
+ md,
+ true,
+ websiteTitle,
+ )
+
+ // 异步保存到缓存(不等待,避免阻塞)
+ this.saveConvertDataToCache(url, 'CONVERT_VIDEO', md, mdPath, url).catch(error => {
+ console.error('Failed to save video conversion to cache:', error)
+ })
+
+ return [md, mdPath]
}
- private async callMcpToolConvertDocument(file: TFile, mcpHub: McpHub): Promise {
+ private async callMcpToolConvertDocument(file: TFile, mcpHub: McpHub): Promise<[string, string]> {
+ // 首先检查缓存
+ const cachedData = await this.convertDataManager.findBySource(file.path)
+ if (cachedData) {
+ console.log(`Using cached document conversion for: ${file.path}`)
+ return [cachedData.content, cachedData.contentPath]
+ }
+
+ // 如果没有缓存,进行转换
// 读取文件的二进制内容并转换为Base64
const fileBuffer = await this.app.vault.readBinary(file)
@@ -1052,12 +1090,23 @@ ${transcript.map((t) => `${t.offset}: ${t.text}`).join('\n')}`
)
// 处理图片内容并获取图片引用
+ // @ts-ignore
await this.processImagesInResponse(response.content)
- const textContent = response.content.find((c) => c.type === 'text')
- const result = textContent?.text as string || ''
+ // @ts-ignore
+ const textContent = response.content.find((c: { type: string; text?: string }) => c.type === 'text')
+ // @ts-ignore
+ const md = textContent?.text as string || ''
- return result
+ // 创建Markdown文件
+ const mdPath = await this.createMarkdownFileForContent(file.path, md, false, file.name)
+
+ // 异步保存到缓存
+ this.saveConvertDataToCache(file.path, 'CONVERT_DOCUMENT', md, mdPath, file.name).catch(error => {
+ console.error('Failed to save document conversion to cache:', error)
+ })
+
+ return [md, mdPath]
}
/**
@@ -1107,7 +1156,7 @@ ${transcript.map((t) => `${t.offset}: ${t.text}`).join('\n')}`
return file.path
} catch (error) {
console.error('Failed to create markdown file:', error)
- return originalPath // 如果创建失败,返回原路径
+ return ""
}
}
@@ -1188,11 +1237,74 @@ ${transcript.map((t) => `${t.offset}: ${t.text}`).join('\n')}`
return savedImagePaths
}
+ /**
+ * 根据 MIME 类型获取图片扩展名
+ */
+ private getImageExtensionFromMimeType(mimeType?: string): string {
+ if (!mimeType) return 'png'
+
+ const extensionMap: Record = {
+ 'image/jpeg': 'jpg',
+ 'image/jpg': 'jpg',
+ 'image/png': 'png',
+ 'image/gif': 'gif',
+ 'image/webp': 'webp',
+ 'image/svg+xml': 'svg',
+ }
+
+ return extensionMap[mimeType.toLowerCase()] || 'png'
+ }
+
+ /**
+ * 保存转换数据到缓存
+ */
+ private async saveConvertDataToCache(
+ source: string,
+ type: ConvertType,
+ content: string,
+ contentPath: string,
+ name?: string
+ ): Promise {
+ try {
+ // 生成名称
+ let displayName = name
+ if (!displayName) {
+ if (type === 'CONVERT_VIDEO') {
+ // 从URL提取名称
+ try {
+ const url = new URL(source)
+ displayName = url.hostname + url.pathname
+ } catch {
+ displayName = source
+ }
+ } else {
+ // 从文件路径提取名称
+ displayName = source.split('/').pop() || source
+ }
+ }
+
+ // 保存到数据库
+ await this.convertDataManager.createConvertData({
+ name: displayName,
+ type,
+ source,
+ contentPath,
+ content,
+ })
+
+ console.log(`Saved conversion data to cache: ${source}`)
+ } catch (error) {
+ console.error('Failed to save conversion data to cache:', error)
+ throw error
+ }
+ }
+
/**
* 将base64图片数据保存为文件到Obsidian资源目录
*/
private async saveImageFromBase64(base64Data: string, filename: string, mimeType?: string): Promise {
// 获取默认资源目录
+ // @ts-ignore
const staticResourceDir = this.app.vault.getConfig("attachmentFolderPath")
// 构建完整的文件路径