update , add mcp server stdio and sse

This commit is contained in:
duanfuxiang
2025-06-02 20:38:40 +08:00
parent 8ca5216b71
commit b1315aa6b1
30 changed files with 2639 additions and 955 deletions

25
src/utils/config.ts Normal file
View File

@@ -0,0 +1,25 @@
/**
* Deeply injects environment variables into a configuration object/string/json
*
* Uses VSCode env:name pattern: https://code.visualstudio.com/docs/reference/variables-reference#_environment-variables
*
* Does not mutate original object
*/
export async function injectEnv<C extends string | Record<PropertyKey, any>>(config: C, notFoundValue: any = "") {
// Use simple regex replace for now, will see if object traversal and recursion is needed here (e.g: for non-serializable objects)
const isObject = typeof config === "object"
let _config: string = isObject ? JSON.stringify(config) : config
_config = _config.replace(/\$\{env:([\w]+)\}/g, (_, name) => {
// Check if null or undefined
// intentionally using == to match null | undefined
if (process.env[name] == null) {
console.warn(`[injectEnv] env variable ${name} referenced but not found in process.env`)
}
return process.env[name] ?? notFoundValue
})
return (isObject ? JSON.parse(_config) : _config) as C extends string ? string : C
}

View File

@@ -79,13 +79,13 @@ export function getToolsForMode(groups: readonly GroupEntry[]): string[] {
}
// Main modes configuration as an ordered array
export const modes: readonly ModeConfig[] = [
export const defaultModes: readonly ModeConfig[] = [
{
slug: "ask",
name: "Ask",
roleDefinition:
"You are Infio, a versatile assistant dedicated to providing informative responses, thoughtful explanations, and practical guidance on virtually any topic or challenge you face.",
groups: ["read"],
groups: ["read", "mcp"],
customInstructions:
"You can analyze information, explain concepts across various domains, and access external resources when helpful. Make sure to address the user's questions thoroughly with thoughtful explanations and practical guidance. Use visual aids like Mermaid diagrams when they help make complex topics clearer. Offer solutions to challenges from diverse fields, not just technical ones, and provide context that helps users better understand the subject matter.",
},
@@ -94,7 +94,7 @@ export const modes: readonly ModeConfig[] = [
name: "Write",
roleDefinition:
"You are Infio, a versatile content creator skilled in composing, editing, and organizing various text-based documents. You excel at structuring information clearly, creating well-formatted content, and helping users express their ideas effectively.",
groups: ["read", "edit"],
groups: ["read", "edit", "mcp"],
customInstructions:
"You can create and modify any text-based files, with particular expertise in Markdown formatting. Help users organize their thoughts, create documentation, take notes, or draft any written content they need. When appropriate, suggest structural improvements and formatting enhancements that make content more readable and accessible. Consider the purpose and audience of each document to provide the most relevant assistance."
},
@@ -103,14 +103,14 @@ export const modes: readonly ModeConfig[] = [
name: "Research",
roleDefinition:
"You are Infio, an advanced research assistant specialized in comprehensive investigation and analytical thinking. You excel at breaking down complex questions, exploring multiple perspectives, and synthesizing information to provide well-reasoned conclusions.",
groups: ["research"],
groups: ["research", "mcp"],
customInstructions:
"You can conduct thorough research by analyzing available information, connecting related concepts, and applying structured reasoning methods. Help users explore topics in depth by considering multiple angles, identifying relevant evidence, and evaluating the reliability of sources. Use step-by-step analysis when tackling complex problems, explaining your thought process clearly. Create visual representations like Mermaid diagrams when they help clarify relationships between ideas. Use Markdown tables to present statistical data or comparative information when appropriate. Present balanced viewpoints while highlighting the strength of evidence behind different conclusions.",
},
] as const
// Export the default mode slug
export const defaultModeSlug = modes[0].slug
export const defaultModeSlug = defaultModes[0].slug
// Helper functions
export function getModeBySlug(slug: string, customModes?: ModeConfig[]): ModeConfig | undefined {
@@ -120,7 +120,7 @@ export function getModeBySlug(slug: string, customModes?: ModeConfig[]): ModeCon
return customMode
}
// Then check built-in modes
return modes.find((mode) => mode.slug === slug)
return defaultModes.find((mode) => mode.slug === slug)
}
export function getModeConfig(slug: string, customModes?: ModeConfig[]): ModeConfig {
@@ -134,11 +134,11 @@ export function getModeConfig(slug: string, customModes?: ModeConfig[]): ModeCon
// Get all available modes, with custom modes overriding built-in modes
export function getAllModes(customModes?: ModeConfig[]): ModeConfig[] {
if (!customModes?.length) {
return [...modes]
return [...defaultModes]
}
// Start with built-in modes
const allModes = [...modes]
const allModes = [...defaultModes]
// Process custom modes
customModes.forEach((customMode) => {
@@ -239,7 +239,7 @@ export function isToolAllowedForMode(
// Create the mode-specific default prompts
export const defaultPrompts: Readonly<CustomModePrompts> = Object.freeze(
Object.fromEntries(
modes.map((mode) => [
defaultModes.map((mode) => [
mode.slug,
{
roleDefinition: mode.roleDefinition,
@@ -275,7 +275,7 @@ export async function getFullModeDetails(
},
): Promise<ModeConfig> {
// First get the base mode config from custom modes or built-in modes
const baseMode = getModeBySlug(modeSlug, customModes) || modes.find((m) => m.slug === modeSlug) || modes[0]
const baseMode = getModeBySlug(modeSlug, customModes) || defaultModes.find((m) => m.slug === modeSlug) || defaultModes[0]
// Check for any prompt component overrides
const promptComponent = customModePrompts?.[modeSlug]

View File

@@ -82,6 +82,15 @@ export type ParsedMsgBlock =
mode: string
reason: string
finish: boolean
} | {
type: 'use_mcp_tool'
server_name: string
tool_name: string
parameters: Record<string, unknown>,
finish: boolean
} | {
type: 'tool_result'
content: string
}
export function parseMsgBlocks(
@@ -569,6 +578,85 @@ export function parseMsgBlocks(
finish: node.sourceCodeLocation.endTag !== undefined
})
lastEndOffset = endOffset
} else if (node.nodeName === 'use_mcp_tool') {
if (!node.sourceCodeLocation) {
throw new Error('sourceCodeLocation is undefined')
}
const startOffset = node.sourceCodeLocation.startOffset
const endOffset = node.sourceCodeLocation.endOffset
if (startOffset > lastEndOffset) {
parsedResult.push({
type: 'string',
content: input.slice(lastEndOffset, startOffset),
})
}
let server_name: string = ''
let tool_name: string = ''
let parameters: Record<string, unknown> = {}
for (const childNode of node.childNodes) {
if (childNode.nodeName === 'server_name' && childNode.childNodes.length > 0) {
// @ts-expect-error - 忽略 value 属性的类型错误
server_name = childNode.childNodes[0].value
} else if (childNode.nodeName === 'tool_name' && childNode.childNodes.length > 0) {
// @ts-expect-error - 忽略 value 属性的类型错误
tool_name = childNode.childNodes[0].value
} else if ((childNode.nodeName === 'parameters'
|| childNode.nodeName === 'input'
|| childNode.nodeName === 'arguments')
&& childNode.childNodes.length > 0) {
try {
// @ts-expect-error - 忽略 value 属性的类型错误
const parametersJson = childNode.childNodes[0].value
parameters = JSON5.parse(parametersJson)
} catch (error) {
console.debug('Failed to parse parameters JSON', error)
}
}
}
parsedResult.push({
type: 'use_mcp_tool',
server_name,
tool_name,
parameters,
finish: node.sourceCodeLocation.endTag !== undefined
})
lastEndOffset = endOffset
} else if (node.nodeName === 'tool_result') {
if (!node.sourceCodeLocation) {
throw new Error('sourceCodeLocation is undefined')
}
const startOffset = node.sourceCodeLocation.startOffset
const endOffset = node.sourceCodeLocation.endOffset
if (startOffset > lastEndOffset) {
parsedResult.push({
type: 'string',
content: input.slice(lastEndOffset, startOffset),
})
}
const children = node.childNodes
if (children.length === 0) {
parsedResult.push({
type: 'tool_result',
content: '',
})
} else {
const innerContentStartOffset =
children[0].sourceCodeLocation?.startOffset
const innerContentEndOffset =
children[children.length - 1].sourceCodeLocation?.endOffset
if (!innerContentStartOffset || !innerContentEndOffset) {
throw new Error('sourceCodeLocation is undefined')
}
parsedResult.push({
type: 'tool_result',
content: input.slice(innerContentStartOffset, innerContentEndOffset),
})
}
lastEndOffset = endOffset
}
}

View File

@@ -3,10 +3,11 @@ import { App, MarkdownView, TAbstractFile, TFile, TFolder, Vault, getLanguage, h
import { editorStateToPlainText } from '../components/chat-view/chat-input/utils/editor-state-to-plain-text'
import { QueryProgressState } from '../components/chat-view/QueryProgress'
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 { SelectVector } from '../database/schema'
import { ChatMessage, ChatUserMessage } from '../types/chat'
import { ChatAssistantMessage, ChatMessage, ChatUserMessage } from '../types/chat'
import { ContentPart, RequestMessage } from '../types/llm/request'
import {
MentionableBlock,
@@ -118,6 +119,7 @@ export class PromptGenerator {
private systemPrompt: SystemPrompt
private customModePrompts: CustomModePrompts | null = null
private customModeList: ModeConfig[] | null = null
private getMcpHub: () => Promise<McpHub> | null = null
private static readonly EMPTY_ASSISTANT_MESSAGE: RequestMessage = {
role: 'assistant',
content: '',
@@ -130,6 +132,7 @@ export class PromptGenerator {
diffStrategy?: DiffStrategy,
customModePrompts?: CustomModePrompts,
customModeList?: ModeConfig[],
getMcpHub?: () => Promise<McpHub>,
) {
this.getRagEngine = getRagEngine
this.app = app
@@ -138,6 +141,7 @@ export class PromptGenerator {
this.systemPrompt = new SystemPrompt(this.app)
this.customModePrompts = customModePrompts ?? null
this.customModeList = customModeList ?? null
this.getMcpHub = getMcpHub ?? null
}
public async generateRequestMessages({
@@ -188,7 +192,9 @@ export class PromptGenerator {
const requestMessages: RequestMessage[] = [
systemMessage,
...compiledMessages.slice(-19).map((message): RequestMessage => {
...compiledMessages.slice(-19)
.filter((message) => !(message.role === 'assistant' && message.isToolResult))
.map((message): RequestMessage => {
if (message.role === 'user') {
return {
role: 'user',
@@ -473,6 +479,7 @@ export class PromptGenerator {
}
public async getSystemMessageNew(mode: Mode, filesSearchMethod: string, preferredLanguage: string): Promise<RequestMessage> {
const mcpHub = await this.getMcpHub?.()
const prompt = await this.systemPrompt.getSystemPrompt(
this.app.vault.getRoot().path,
false,
@@ -482,6 +489,7 @@ export class PromptGenerator {
this.diffStrategy,
this.customModePrompts,
this.customModeList,
mcpHub,
)
return {
@@ -627,14 +635,14 @@ ${fileContent}
const fileContent = await readTFileContent(currentFile, this.app.vault);
const lines = fileContent.split('\n');
// 计算上下文范围,并处理边界情况
const contextStartLine = Math.max(1, startLine - 20);
const contextEndLine = Math.min(lines.length, endLine + 20);
// 提取上下文行
const contextLines = lines.slice(contextStartLine - 1, contextEndLine);
// 返回带行号的上下文内容
return addLineNumbers(contextLines.join('\n'), contextStartLine);
}
@@ -653,10 +661,10 @@ ${fileContent}
endLine: number
}): Promise<RequestMessage[]> {
const systemMessage = this.getSystemMessage(false, 'edit');
// 获取适当大小的上下文
const context = await this.getContextForEdit(currentFile, startLine, endLine);
let userPrompt = `<task>\n${instruction}\n</task>\n\n
<selected_content location="${currentFile.path}#L${startLine}-${endLine}">\n${selectedContent}\n</selected_content>`;

View File

@@ -20,6 +20,72 @@ interface SearchResponse {
organic_results?: SearchResult[];
}
export interface EventProps {
[key: string]: string | number | boolean
}
export async function onEnt(
N: string,
props?: EventProps,
): Promise<void> {
return new Promise<void>((resolve) => {
try {
const eventUrl = `obsidian://plugin/infio-copilot/${N}`
const payload = {
name: N,
url: eventUrl,
domain: "copilot.infio.app",
...(props && Object.keys(props).length > 0 && { props })
}
const postData = JSON.stringify(payload)
const apiUrl = new URL(`https://api.infio.com/e1/api/event`)
const options = {
hostname: apiUrl.hostname,
port: apiUrl.port || 443,
path: apiUrl.pathname,
method: 'POST',
rejectUnauthorized: false,
headers: {
'User-Agent': navigator.userAgent,
'X-Forwarded-For': '127.0.0.1',
'Content-Type': 'application/json',
'Content-Length': Buffer.byteLength(postData),
'X-Debug-Request': 'true'
}
}
const req = https.request(options, (res) => {
let data = ''
res.on('data', (chunk) => { data += chunk })
res.on('end', () => {
if (res.statusCode && res.statusCode >= 200 && res.statusCode < 300) {
// console.log(`✅ successfully: ${N}`)
} else {
console.error(`❌ (${res.statusCode}):`, data)
}
resolve()
})
})
req.on('error', (error) => {
console.error('❌ Failed:', error)
resolve()
})
req.write(postData)
req.end()
} catch (error) {
console.error('❌ Failed:', error)
resolve()
}
})
}
// 添加余弦相似度计算函数
function cosineSimilarity(vecA: number[], vecB: number[]): number {
const dotProduct = vecA.reduce((sum, a, i) => sum + a * vecB[i], 0);