update , add mcp server stdio and sse
This commit is contained in:
25
src/utils/config.ts
Normal file
25
src/utils/config.ts
Normal 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
|
||||
}
|
||||
@@ -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]
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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>`;
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user