update mcp tools
This commit is contained in:
parent
4053214078
commit
672d0b7ae0
@ -1,4 +1,9 @@
|
|||||||
releases:
|
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"
|
- version: "0.5.2"
|
||||||
features:
|
features:
|
||||||
- "Added Infio provider"
|
- "Added Infio provider"
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"id": "infio-copilot",
|
"id": "infio-copilot",
|
||||||
"name": "Infio Copilot",
|
"name": "Infio Copilot",
|
||||||
"version": "0.5.2",
|
"version": "0.6.0",
|
||||||
"minAppVersion": "0.15.0",
|
"minAppVersion": "0.15.0",
|
||||||
"description": "A Cursor-inspired AI assistant for notes that offers smart autocomplete and interactive chat with your selected notes",
|
"description": "A Cursor-inspired AI assistant for notes that offers smart autocomplete and interactive chat with your selected notes",
|
||||||
"author": "Felix.D",
|
"author": "Felix.D",
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "obsidian-infio-copilot",
|
"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",
|
"description": "A Cursor-inspired AI assistant that offers smart autocomplete and interactive chat with your selected notes",
|
||||||
"main": "main.js",
|
"main": "main.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|||||||
@ -128,11 +128,11 @@ const McpHubView = () => {
|
|||||||
<div className="infio-mcp-tool-row">
|
<div className="infio-mcp-tool-row">
|
||||||
<div className="infio-mcp-tool-row-header">
|
<div className="infio-mcp-tool-row-header">
|
||||||
<div className="infio-mcp-tool-name-section">
|
<div className="infio-mcp-tool-name-section">
|
||||||
<span className="infio-mcp-tool-name">{tool.name}</span>
|
<span className="infio-mcp-tool-name">{tool.name.replace('COMPOSIO_SEARCH_', '')}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{tool.description && (
|
{tool.description && (
|
||||||
<p className="infio-mcp-item-description">{tool.description}</p>
|
<p className="infio-mcp-item-description">{tool.description.replace('composio', '')}</p>
|
||||||
)}
|
)}
|
||||||
{(tool.inputSchema && (() => {
|
{(tool.inputSchema && (() => {
|
||||||
const schema = tool.inputSchema;
|
const schema = tool.inputSchema;
|
||||||
@ -285,7 +285,7 @@ const McpHubView = () => {
|
|||||||
{isExpanded ? <ChevronDown size={16} /> : <ChevronRight size={16} />}
|
{isExpanded ? <ChevronDown size={16} /> : <ChevronRight size={16} />}
|
||||||
</div>
|
</div>
|
||||||
<span className={`infio-mcp-hub-status-indicator ${server.status === 'connected' ? 'connected' : server.status === 'connecting' ? 'connecting' : 'disconnected'} ${server.disabled ? 'disabled' : ''}`}></span>
|
<span className={`infio-mcp-hub-status-indicator ${server.status === 'connected' ? 'connected' : server.status === 'connecting' ? 'connecting' : 'disconnected'} ${server.disabled ? 'disabled' : ''}`}></span>
|
||||||
<h3 className="infio-mcp-hub-name">{server.name}</h3>
|
<h3 className="infio-mcp-hub-name">{server.name.replace('infio-builtin-server', 'builtin')}</h3>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="infio-mcp-hub-actions" onClick={(e) => e.stopPropagation()}>
|
<div className="infio-mcp-hub-actions" onClick={(e) => e.stopPropagation()}>
|
||||||
|
|||||||
@ -87,7 +87,7 @@ export default function QueryProgress({
|
|||||||
{t('chat.queryProgress.readingFilesDone')}
|
{t('chat.queryProgress.readingFilesDone')}
|
||||||
</p>
|
</p>
|
||||||
<p className="infio-query-progress-detail">
|
<p className="infio-query-progress-detail">
|
||||||
{t('chat.queryProgress.filesLoaded', { count: state.fileContents.length })}
|
{t('chat.queryProgress.filesLoaded').replace('{count}', state.fileContents.length.toString())}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
@ -115,7 +115,7 @@ export default function QueryProgress({
|
|||||||
{t('chat.queryProgress.readingWebsitesDone')}
|
{t('chat.queryProgress.readingWebsitesDone')}
|
||||||
</p>
|
</p>
|
||||||
<p className="infio-query-progress-detail">
|
<p className="infio-query-progress-detail">
|
||||||
{t('chat.queryProgress.websitesLoaded', { count: state.websiteContents.length })}
|
{t('chat.queryProgress.websitesLoaded').replace('{count}', state.websiteContents.length.toString())}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|||||||
@ -2,4 +2,5 @@ export const ROOT_DIR = '.infio_json_db'
|
|||||||
export const COMMAND_DIR = 'commands'
|
export const COMMAND_DIR = 'commands'
|
||||||
export const CHAT_DIR = 'chats'
|
export const CHAT_DIR = 'chats'
|
||||||
export const CUSTOM_MODE_DIR = 'custom_modes'
|
export const CUSTOM_MODE_DIR = 'custom_modes'
|
||||||
|
export const CONVERT_DATA_DIR = 'convert_data'
|
||||||
export const INITIAL_MIGRATION_MARKER = '.initial_migration_completed'
|
export const INITIAL_MIGRATION_MARKER = '.initial_migration_completed'
|
||||||
|
|||||||
185
src/database/json/convert-data/ConvertDataManager.ts
Normal file
185
src/database/json/convert-data/ConvertDataManager.ts
Normal file
@ -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<ConvertData> {
|
||||||
|
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<ConvertData | null> {
|
||||||
|
const md5Hash = ConvertDataManager.generateSourceHash(source)
|
||||||
|
return this.findByHash(md5Hash)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据MD5哈希查找转换数据
|
||||||
|
*/
|
||||||
|
public async findByHash(md5Hash: string): Promise<ConvertData | null> {
|
||||||
|
const fileName = `${md5Hash}.json`
|
||||||
|
|
||||||
|
try {
|
||||||
|
return await this.read(fileName)
|
||||||
|
} catch {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据ID查找转换数据
|
||||||
|
*/
|
||||||
|
public async findById(id: string): Promise<ConvertData | null> {
|
||||||
|
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<ConvertData, 'id' | 'md5Hash' | 'createdAt' | 'updatedAt' | 'schemaVersion'>
|
||||||
|
>,
|
||||||
|
): Promise<ConvertData | null> {
|
||||||
|
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<boolean> {
|
||||||
|
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<ConvertData[]> {
|
||||||
|
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<ConvertData[]> {
|
||||||
|
const allData = await this.listConvertData()
|
||||||
|
return allData.filter((data) => data.type === type)
|
||||||
|
}
|
||||||
|
}
|
||||||
2
src/database/json/convert-data/index.ts
Normal file
2
src/database/json/convert-data/index.ts
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
export * from './types'
|
||||||
|
export * from './ConvertDataManager'
|
||||||
33
src/database/json/convert-data/types.ts
Normal file
33
src/database/json/convert-data/types.ts
Normal file
@ -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<typeof convertTypeSchema>
|
||||||
|
|
||||||
|
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<typeof convertDataSchema>
|
||||||
|
|
||||||
|
export type ConvertDataMetadata = {
|
||||||
|
id: string
|
||||||
|
md5Hash: string
|
||||||
|
name: string
|
||||||
|
type: string
|
||||||
|
source: string
|
||||||
|
createdAt: number
|
||||||
|
updatedAt: number
|
||||||
|
schemaVersion: number
|
||||||
|
}
|
||||||
@ -409,7 +409,7 @@ export default {
|
|||||||
noErrors: "没有错误记录",
|
noErrors: "没有错误记录",
|
||||||
parameters: "参数",
|
parameters: "参数",
|
||||||
toolNoDescription: "无描述",
|
toolNoDescription: "无描述",
|
||||||
useMcpToolFrom: "Use MCP tool from",
|
useMcpToolFrom: "使用来自以下的 MCP 工具:",
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@ -6,6 +6,8 @@ import { DiffStrategy } from '../core/diff/DiffStrategy'
|
|||||||
import { McpHub } from '../core/mcp/McpHub'
|
import { McpHub } from '../core/mcp/McpHub'
|
||||||
import { SystemPrompt } from '../core/prompts/system'
|
import { SystemPrompt } from '../core/prompts/system'
|
||||||
import { RAGEngine } from '../core/rag/rag-engine'
|
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 { SelectVector } from '../database/schema'
|
||||||
import { ChatMessage, ChatUserMessage } from '../types/chat'
|
import { ChatMessage, ChatUserMessage } from '../types/chat'
|
||||||
import { ContentPart, RequestMessage } from '../types/llm/request'
|
import { ContentPart, RequestMessage } from '../types/llm/request'
|
||||||
@ -141,6 +143,7 @@ export class PromptGenerator {
|
|||||||
private customModePrompts: CustomModePrompts | null = null
|
private customModePrompts: CustomModePrompts | null = null
|
||||||
private customModeList: ModeConfig[] | null = null
|
private customModeList: ModeConfig[] | null = null
|
||||||
private getMcpHub: () => Promise<McpHub> | null = null
|
private getMcpHub: () => Promise<McpHub> | null = null
|
||||||
|
private convertDataManager: ConvertDataManager
|
||||||
private static readonly EMPTY_ASSISTANT_MESSAGE: RequestMessage = {
|
private static readonly EMPTY_ASSISTANT_MESSAGE: RequestMessage = {
|
||||||
role: 'assistant',
|
role: 'assistant',
|
||||||
content: '',
|
content: '',
|
||||||
@ -163,6 +166,7 @@ export class PromptGenerator {
|
|||||||
this.customModePrompts = customModePrompts ?? null
|
this.customModePrompts = customModePrompts ?? null
|
||||||
this.customModeList = customModeList ?? null
|
this.customModeList = customModeList ?? null
|
||||||
this.getMcpHub = getMcpHub ?? null
|
this.getMcpHub = getMcpHub ?? null
|
||||||
|
this.convertDataManager = new ConvertDataManager(app)
|
||||||
}
|
}
|
||||||
|
|
||||||
public async generateRequestMessages({
|
public async generateRequestMessages({
|
||||||
@ -388,14 +392,22 @@ export class PromptGenerator {
|
|||||||
completedFiles: completedFiles
|
completedFiles: completedFiles
|
||||||
})
|
})
|
||||||
|
|
||||||
const content = await getFileOrFolderContent(
|
// 如果文件不是 md 文件且 mcpHub 存在,使用 MCP 工具转换
|
||||||
file,
|
const mcpHub = await this.getMcpHub?.()
|
||||||
this.app.vault,
|
let content: string
|
||||||
this.app
|
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文件
|
// 创建Markdown文件
|
||||||
const markdownFilePath = await this.createMarkdownFileForContent(
|
markdownFilePath = markdownFilePath || await this.createMarkdownFileForContent(
|
||||||
file.path,
|
file.path,
|
||||||
content,
|
content,
|
||||||
false
|
false
|
||||||
@ -522,13 +534,11 @@ export class PromptGenerator {
|
|||||||
completedUrls: completedUrls
|
completedUrls: completedUrls
|
||||||
})
|
})
|
||||||
|
|
||||||
const content = await this.getWebsiteContent(url, mcpHub)
|
const [content, mcpContentPath] = await this.getWebsiteContent(url, mcpHub)
|
||||||
|
|
||||||
// 从内容中提取标题
|
// 从内容中提取标题
|
||||||
const websiteTitle = this.extractTitleFromWebsiteContent(content, url)
|
const websiteTitle = this.extractTitleFromWebsiteContent(content, url)
|
||||||
|
|
||||||
// 为网页内容创建Markdown文件
|
const contentPath = mcpContentPath || await this.createMarkdownFileForContent(
|
||||||
const markdownFilePath = await this.createMarkdownFileForContent(
|
|
||||||
url,
|
url,
|
||||||
content,
|
content,
|
||||||
true,
|
true,
|
||||||
@ -536,8 +546,8 @@ export class PromptGenerator {
|
|||||||
)
|
)
|
||||||
|
|
||||||
completedUrls++
|
completedUrls++
|
||||||
urlContents.push({ url: markdownFilePath, content }) // 这里url改为markdownFilePath
|
urlContents.push({ url: contentPath, content })
|
||||||
allWebsiteReadResults.push({ url: markdownFilePath, content }) // 同样这里也改为markdownFilePath
|
allWebsiteReadResults.push({ url: contentPath, content })
|
||||||
}
|
}
|
||||||
|
|
||||||
// 网页读取完成
|
// 网页读取完成
|
||||||
@ -574,8 +584,11 @@ export class PromptGenerator {
|
|||||||
|
|
||||||
// 如果当前文件不是 md 文件且 mcpHub 存在,使用 MCP 工具转换
|
// 如果当前文件不是 md 文件且 mcpHub 存在,使用 MCP 工具转换
|
||||||
const mcpHub = await this.getMcpHub?.()
|
const mcpHub = await this.getMcpHub?.()
|
||||||
|
let currentMarkdownFilePath = ''
|
||||||
if (currentFile.file.extension !== 'md' && mcpHub?.isBuiltInServerAvailable()) {
|
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 {
|
} else {
|
||||||
currentFileContent = await getFileOrFolderContent(
|
currentFileContent = await getFileOrFolderContent(
|
||||||
currentFile.file,
|
currentFile.file,
|
||||||
@ -585,7 +598,7 @@ export class PromptGenerator {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 为当前文件创建Markdown文件
|
// 为当前文件创建Markdown文件
|
||||||
const currentMarkdownFilePath = await this.createMarkdownFileForContent(
|
currentMarkdownFilePath = currentMarkdownFilePath || await this.createMarkdownFileForContent(
|
||||||
currentFile.file.path,
|
currentFile.file.path,
|
||||||
currentFileContent,
|
currentFileContent,
|
||||||
false
|
false
|
||||||
@ -966,22 +979,18 @@ When writing out new markdown blocks, remember not to include "line_number|" at
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
private async getPdfContent(file: TFile): Promise<string> {
|
|
||||||
return await parsePdfContent(file, this.app)
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* TODO: Improve markdown conversion logic
|
* TODO: Improve markdown conversion logic
|
||||||
* - filter visually hidden elements
|
* - filter visually hidden elements
|
||||||
* ...
|
* ...
|
||||||
*/
|
*/
|
||||||
private async getWebsiteContent(url: string, mcpHub: McpHub | null): Promise<string> {
|
private async getWebsiteContent(url: string, mcpHub: McpHub | null): Promise<[string, string]> {
|
||||||
|
|
||||||
const mcpHubAvailable = mcpHub?.isBuiltInServerAvailable()
|
const mcpHubAvailable = mcpHub?.isBuiltInServerAvailable()
|
||||||
|
|
||||||
if (mcpHubAvailable && isVideoUrl(url)) {
|
if (mcpHubAvailable && isVideoUrl(url)) {
|
||||||
return this.callMcpToolConvertVideo(url, mcpHub)
|
const [md, mdPath] = await this.callMcpToolConvertVideo(url, mcpHub)
|
||||||
|
return [md, mdPath]
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isYoutubeUrl(url)) {
|
if (isYoutubeUrl(url)) {
|
||||||
@ -989,17 +998,28 @@ When writing out new markdown blocks, remember not to include "line_number|" at
|
|||||||
const { title, transcript } =
|
const { title, transcript } =
|
||||||
await YoutubeTranscript.fetchTranscriptAndMetadata(url)
|
await YoutubeTranscript.fetchTranscriptAndMetadata(url)
|
||||||
|
|
||||||
return `Title: ${title}
|
return [
|
||||||
|
`Title: ${title}
|
||||||
Video Transcript:
|
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 })
|
const response = await requestUrl({ url })
|
||||||
|
|
||||||
return htmlToMarkdown(response.text)
|
return [htmlToMarkdown(response.text), '']
|
||||||
}
|
}
|
||||||
|
|
||||||
private async callMcpToolConvertVideo(url: string, mcpHub: McpHub): Promise<string> {
|
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(
|
const response = await mcpHub.callTool(
|
||||||
'infio-builtin-server',
|
'infio-builtin-server',
|
||||||
'CONVERT_VIDEO',
|
'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')
|
const textContent = response.content.find((c) => c.type === 'text')
|
||||||
let result = textContent?.text || ''
|
// @ts-ignore
|
||||||
|
const md = textContent?.text as string || ''
|
||||||
|
|
||||||
// 在文本内容末尾添加图片引用
|
// 创建Markdown文件
|
||||||
if (imageReferences.length > 0) {
|
const websiteTitle = this.extractTitleFromWebsiteContent(md, url)
|
||||||
result += '\n\n## 图片内容\n\n'
|
|
||||||
imageReferences.forEach(imagePath => {
|
|
||||||
result += `\n\n`
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
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<string> {
|
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
|
// 读取文件的二进制内容并转换为Base64
|
||||||
const fileBuffer = await this.app.vault.readBinary(file)
|
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)
|
await this.processImagesInResponse(response.content)
|
||||||
|
|
||||||
const textContent = response.content.find((c) => c.type === 'text')
|
// @ts-ignore
|
||||||
const result = textContent?.text as string || ''
|
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
|
return file.path
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to create markdown file:', 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
|
return savedImagePaths
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据 MIME 类型获取图片扩展名
|
||||||
|
*/
|
||||||
|
private getImageExtensionFromMimeType(mimeType?: string): string {
|
||||||
|
if (!mimeType) return 'png'
|
||||||
|
|
||||||
|
const extensionMap: Record<string, string> = {
|
||||||
|
'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<void> {
|
||||||
|
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资源目录
|
* 将base64图片数据保存为文件到Obsidian资源目录
|
||||||
*/
|
*/
|
||||||
private async saveImageFromBase64(base64Data: string, filename: string, mimeType?: string): Promise<string> {
|
private async saveImageFromBase64(base64Data: string, filename: string, mimeType?: string): Promise<string> {
|
||||||
// 获取默认资源目录
|
// 获取默认资源目录
|
||||||
|
// @ts-ignore
|
||||||
const staticResourceDir = this.app.vault.getConfig("attachmentFolderPath")
|
const staticResourceDir = this.app.vault.getConfig("attachmentFolderPath")
|
||||||
|
|
||||||
// 构建完整的文件路径
|
// 构建完整的文件路径
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user