From 31fa5ead4f1c2a9d12606ccd21c762843f19c6cc Mon Sep 17 00:00:00 2001 From: Kirigaya <1193466151@qq.com> Date: Sun, 27 Apr 2025 19:11:24 +0800 Subject: [PATCH] =?UTF-8?q?=E9=87=8D=E6=9E=84=20OpenMCPService=EF=BC=8C?= =?UTF-8?q?=E9=87=87=E7=94=A8=E6=96=B0=E7=9A=84=E6=9E=B6=E6=9E=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- renderer/src/views/connect/connection.ts | 6 +- service/src/common/index.dto.ts | 33 ++ service/src/common/index.ts | 17 ++ service/src/common/router.ts | 41 +++ service/src/controller/index.ts | 97 ------ service/src/hook/llm.ts | 56 ---- service/src/hook/setting.ts | 156 ---------- service/src/index.ts | 4 +- service/src/llm/llm.controller.ts | 29 ++ service/src/llm/llm.dto.ts | 9 + service/src/llm/llm.service.ts | 146 +++++++++ service/src/main.ts | 6 +- service/src/mcp/client.controller.ts | 138 +++++++++ service/src/mcp/client.dto.ts | 37 +++ .../{hook/client.ts => mcp/client.service.ts} | 32 +- service/src/mcp/connect.controller.ts | 39 +++ .../connect.ts => mcp/connect.service.ts} | 27 +- service/src/panel/panel.controller.ts | 28 ++ service/src/panel/panel.dto.ts | 12 + service/src/panel/panel.service.ts | 68 +++++ service/src/service/env-var.ts | 27 -- service/src/service/llm.ts | 120 -------- service/src/service/mcp-server.ts | 289 ------------------ service/src/service/ocr.ts | 18 -- service/src/service/panel.ts | 55 ---- service/src/service/setting.ts | 60 ---- service/src/service/util.ts | 25 -- service/src/setting/setting.controller.ts | 27 ++ service/src/setting/setting.dto.ts | 4 + service/src/setting/setting.service.ts | 77 +++++ 30 files changed, 734 insertions(+), 949 deletions(-) create mode 100644 service/src/common/index.dto.ts create mode 100644 service/src/common/index.ts create mode 100644 service/src/common/router.ts delete mode 100644 service/src/controller/index.ts create mode 100644 service/src/llm/llm.controller.ts create mode 100644 service/src/llm/llm.dto.ts create mode 100644 service/src/llm/llm.service.ts create mode 100644 service/src/mcp/client.controller.ts create mode 100644 service/src/mcp/client.dto.ts rename service/src/{hook/client.ts => mcp/client.service.ts} (81%) create mode 100644 service/src/mcp/connect.controller.ts rename service/src/{service/connect.ts => mcp/connect.service.ts} (71%) create mode 100644 service/src/panel/panel.controller.ts create mode 100644 service/src/panel/panel.dto.ts create mode 100644 service/src/panel/panel.service.ts delete mode 100644 service/src/service/env-var.ts delete mode 100644 service/src/service/llm.ts delete mode 100644 service/src/service/mcp-server.ts delete mode 100644 service/src/service/ocr.ts delete mode 100644 service/src/service/panel.ts delete mode 100644 service/src/service/setting.ts delete mode 100644 service/src/service/util.ts create mode 100644 service/src/setting/setting.controller.ts create mode 100644 service/src/setting/setting.dto.ts create mode 100644 service/src/setting/setting.service.ts diff --git a/renderer/src/views/connect/connection.ts b/renderer/src/views/connect/connection.ts index 989e6c1..a852a7b 100644 --- a/renderer/src/views/connect/connection.ts +++ b/renderer/src/views/connect/connection.ts @@ -55,7 +55,7 @@ export function makeEnv() { type ConnectionType = 'STDIO' | 'SSE'; // 定义命令行参数接口 -export interface MCPOptions { +export interface McpOptions { connectionType: ConnectionType; // STDIO 特定选项 command?: string; @@ -70,7 +70,7 @@ export interface MCPOptions { } export function doConnect() { - let connectOption: MCPOptions; + let connectOption: McpOptions; const bridge = useMessageBridge(); const env = makeEnv(); @@ -313,7 +313,7 @@ async function launchSSE() { resolve(void 0); }, { once: true }); - const connectOption: MCPOptions = { + const connectOption: McpOptions = { connectionType: 'SSE', url: connectionArgs.urlString, clientName: 'openmcp.connect.sse', diff --git a/service/src/common/index.dto.ts b/service/src/common/index.dto.ts new file mode 100644 index 0000000..e6ab551 --- /dev/null +++ b/service/src/common/index.dto.ts @@ -0,0 +1,33 @@ +import { PostMessageble } from "../hook/adapter"; +import { McpClient } from "../mcp/client.service"; + +export type RequestClientType = McpClient | undefined; + +export type RequestHandler = ( + client: RequestClientType, + data: T, + webview: PostMessageble +) => Promise; + +export interface RequestHandlerStore { + handler: RequestHandler + option?: ControllerOption; +} + +export interface MapperDescriptor { + configurable?: boolean; + enumerable?: boolean; + value?: RequestHandler; + writable?: boolean; + get?(): any; + set?(v: any): void; +} + +export interface RestfulResponse { + code: number; + msg: any; +} + +export interface ControllerOption { + [key: string]: any; +} \ No newline at end of file diff --git a/service/src/common/index.ts b/service/src/common/index.ts new file mode 100644 index 0000000..67dc21a --- /dev/null +++ b/service/src/common/index.ts @@ -0,0 +1,17 @@ +import { MapperDescriptor, RequestHandler, RequestClientType, RestfulResponse, ControllerOption, RequestHandlerStore } from "./index.dto"; +export { MapperDescriptor, RequestHandler, RequestClientType }; + +export const requestHandlerStorage = new Map>(); + +export function Controller(command: string, option: ControllerOption = {}) { + return function(target: any, propertykey: string, descriptor: MapperDescriptor) { + const handler = descriptor.value; + if (requestHandlerStorage.has(command)) { + throw new Error(`Duplicate request handler for ${command}`); + } + if (handler) { + requestHandlerStorage.set(command, { handler, option }); + } + return descriptor; + } +} diff --git a/service/src/common/router.ts b/service/src/common/router.ts new file mode 100644 index 0000000..acfe0cb --- /dev/null +++ b/service/src/common/router.ts @@ -0,0 +1,41 @@ +import { requestHandlerStorage } from "."; +import { PostMessageble } from "../hook/adapter"; +import { LlmController } from "../llm/llm.controller"; +import { ClientController } from "../mcp/client.controller"; +import { ConnectController } from "../mcp/connect.controller"; +import { client } from "../mcp/connect.service"; +import { PanelController } from "../panel/panel.controller"; +import { SettingController } from "../setting/setting.controller"; + +export const ModuleControllers = [ + ConnectController, + ClientController, + LlmController, + PanelController, + SettingController +]; + +export async function routeMessage(command: string, data: any, webview: PostMessageble) { + const handlerStore = requestHandlerStorage.get(command); + if (handlerStore) { + const { handler, option = {} } = handlerStore; + + try { + // TODO: select client based on something + const res = await handler(client, data, webview); + + // res.code = -1 代表当前请求不需要返回发送 + if (res.code >= 0) { + webview.postMessage({ command, data: res }); + } + } catch (error) { + webview.postMessage({ + command, data: { + code: 500, + msg: (error as any).toString() + } + }); + } + } + return +} \ No newline at end of file diff --git a/service/src/controller/index.ts b/service/src/controller/index.ts deleted file mode 100644 index 1562ef0..0000000 --- a/service/src/controller/index.ts +++ /dev/null @@ -1,97 +0,0 @@ - -import { PostMessageble } from '../hook/adapter'; -import { lookupEnvVarService } from '../service/env-var'; - -import { - callToolService, - getPromptService, - getServerVersionService, - listPromptsService, - listResourcesService, - listResourceTemplatesService, - listToolsService, - readResourceService -} from '../service/mcp-server'; - -import { abortMessageService, chatCompletionService } from '../service/llm'; -import { panelLoadService, panelSaveService } from '../service/panel'; -import { settingLoadService, settingSaveService } from '../service/setting'; -import { pingService } from '../service/util'; -import { client, connectService } from '../service/connect'; - - - -export function messageController(command: string, data: any, webview: PostMessageble) { - switch (command) { - case 'connect': - connectService(client, data, webview); - break; - - case 'server/version': - getServerVersionService(client, data, webview); - break; - - case 'prompts/list': - listPromptsService(client, data, webview); - break; - - case 'prompts/get': - getPromptService(client, data, webview); - break; - - case 'resources/list': - listResourcesService(client, data, webview); - break; - - case 'resources/templates/list': - listResourceTemplatesService(client, data, webview); - break; - - case 'resources/read': - readResourceService(client, data, webview); - break; - - case 'tools/list': - listToolsService(client, data, webview); - break; - - case 'tools/call': - callToolService(client, data, webview); - break; - - case 'ping': - pingService(client, data, webview); - break; - - case 'setting/save': - settingSaveService(client, data, webview); - break; - - case 'setting/load': - settingLoadService(client, data, webview); - break; - - case 'panel/save': - panelSaveService(client, data, webview); - break; - - case 'panel/load': - panelLoadService(client, data, webview); - break; - - case 'llm/chat/completions': - chatCompletionService(client, data, webview); - break; - - case 'llm/chat/completions/abort': - abortMessageService(client, data, webview); - break; - - case 'lookup-env-var': - lookupEnvVarService(client, data, webview); - break; - - default: - break; - } -} \ No newline at end of file diff --git a/service/src/hook/llm.ts b/service/src/hook/llm.ts index 3d53f25..fbc061d 100644 --- a/service/src/hook/llm.ts +++ b/service/src/hook/llm.ts @@ -1,5 +1,3 @@ -import { OpenAI } from 'openai'; - export const llms = [ { id: 'deepseek', @@ -86,57 +84,3 @@ export const llms = [ userModel: 'moonshot-v1-8k' } ]; - -type MyMessageType = OpenAI.Chat.ChatCompletionMessageParam & { - extraInfo?: any; -} - -type MyToolMessageType = OpenAI.Chat.ChatCompletionToolMessageParam & { - extraInfo?: any; -} - -function postProcessToolMessages(message: MyToolMessageType) { - if (typeof message.content === 'string') { - return; - } - - for (const content of message.content) { - const contentType = content.type as string; - const rawContent = content as any; - - if (contentType === 'image') { - delete rawContent._meta; - - rawContent.type = 'text'; - - // 从缓存中提取图像数据 - rawContent.text = '图片已被删除'; - } - } - - message.content = JSON.stringify(message.content); -} - -export function postProcessMessages(messages: MyMessageType[]) { - for (const message of messages) { - // 去除 extraInfo 属性 - delete message.extraInfo; - - switch (message.role) { - case 'user': - break; - case 'assistant': - break; - - case 'system': - - break; - - case 'tool': - postProcessToolMessages(message); - break; - default: - break; - } - } -} \ No newline at end of file diff --git a/service/src/hook/setting.ts b/service/src/hook/setting.ts index 247ea04..a991252 100644 --- a/service/src/hook/setting.ts +++ b/service/src/hook/setting.ts @@ -1,161 +1,5 @@ -import * as fs from 'fs'; -import * as path from 'path'; -import * as os from 'os'; -import { llms } from './llm'; -import { IServerVersion } from './client'; - export let VSCODE_WORKSPACE = ''; export function setVscodeWorkspace(workspace: string) { VSCODE_WORKSPACE = workspace; -} - -function getConfigurationPath() { - // 如果是 vscode 插件下,则修改为 ~/.openmcp/config.json - if (VSCODE_WORKSPACE) { - // 在 VSCode 插件环境下 - const homeDir = os.homedir(); - const configDir = path.join(homeDir, '.openmcp'); - if (!fs.existsSync(configDir)) { - fs.mkdirSync(configDir, { recursive: true }); - } - return path.join(configDir, 'setting.json'); - } - return 'setting.json'; -} - -function getTabSavePath(serverInfo: IServerVersion) { - const { name = 'untitle', version = '0.0.0' } = serverInfo || {}; - const tabSaveName = `tabs.${name}.json`; - - // 如果是 vscode 插件下,则修改为 ~/.vscode/openmcp.json - if (VSCODE_WORKSPACE) { - // 在 VSCode 插件环境下 - const configDir = path.join(VSCODE_WORKSPACE, '.vscode'); - if (!fs.existsSync(configDir)) { - fs.mkdirSync(configDir, { recursive: true }); - } - return path.join(configDir, tabSaveName); - } - return tabSaveName; -} - -function getDefaultLanguage() { - if (process.env.VSCODE_PID) { - // TODO: 获取 vscode 内部的语言 - - } - return 'zh'; -} - -interface IConfig { - MODEL_INDEX: number; - [key: string]: any; -} - -const DEFAULT_CONFIG: IConfig = { - MODEL_INDEX: 0, - LLM_INFO: llms, - LANG: getDefaultLanguage() -}; - -interface SaveTabItem { - name: string; - icon: string; - type: string; - componentIndex: number; - storage: Record; -} - -interface SaveTab { - tabs: SaveTabItem[] - currentIndex: number -} - -const DEFAULT_TABS: SaveTab = { - tabs: [], - currentIndex: -1 -} - -function createConfig(): IConfig { - const configPath = getConfigurationPath(); - const configDir = path.dirname(configPath); - - // 确保配置目录存在 - if (configDir && !fs.existsSync(configDir)) { - fs.mkdirSync(configDir, { recursive: true }); - } - - // 写入默认配置 - fs.writeFileSync(configPath, JSON.stringify(DEFAULT_CONFIG, null, 2), 'utf-8'); - return DEFAULT_CONFIG; -} - -function createSaveTabConfig(serverInfo: IServerVersion): SaveTab { - const configPath = getTabSavePath(serverInfo); - const configDir = path.dirname(configPath); - - // 确保配置目录存在 - if (configDir && !fs.existsSync(configDir)) { - fs.mkdirSync(configDir, { recursive: true }); - } - - // 写入默认配置 - fs.writeFileSync(configPath, JSON.stringify(DEFAULT_TABS, null, 2), 'utf-8'); - return DEFAULT_TABS; -} - -export function loadConfig(): IConfig { - const configPath = getConfigurationPath(); - - if (!fs.existsSync(configPath)) { - return createConfig(); - } - - try { - const configData = fs.readFileSync(configPath, 'utf-8'); - return JSON.parse(configData) as IConfig; - } catch (error) { - console.error('Error loading config file, creating new one:', error); - return createConfig(); - } -} - -export function saveConfig(config: Partial): void { - const configPath = getConfigurationPath(); - console.log('save to ' + configPath); - - try { - fs.writeFileSync(configPath, JSON.stringify(config, null, 2), 'utf-8'); - } catch (error) { - console.error('Error saving config file:', error); - throw error; - } -} - -export function loadTabSaveConfig(serverInfo: IServerVersion): SaveTab { - const tabSavePath = getTabSavePath(serverInfo); - - if (!fs.existsSync(tabSavePath)) { - return createSaveTabConfig(serverInfo); - } - - try { - const configData = fs.readFileSync(tabSavePath, 'utf-8'); - return JSON.parse(configData) as SaveTab; - } catch (error) { - console.error('Error loading config file, creating new one:', error); - return createSaveTabConfig(serverInfo); - } -} - -export function saveTabSaveConfig(serverInfo: IServerVersion, config: Partial): void { - const tabSavePath = getTabSavePath(serverInfo); - - try { - fs.writeFileSync(tabSavePath, JSON.stringify(config, null, 2), 'utf-8'); - } catch (error) { - console.error('Error saving config file:', error); - throw error; - } } \ No newline at end of file diff --git a/service/src/index.ts b/service/src/index.ts index ee3fd15..5cc80bd 100644 --- a/service/src/index.ts +++ b/service/src/index.ts @@ -1,5 +1,5 @@ -export { messageController } from './controller'; +export { routeMessage } from './common/router'; export { VSCodeWebViewLike } from './hook/adapter'; export { setVscodeWorkspace } from './hook/setting'; // TODO: 更加规范 -export { client } from './service/connect'; \ No newline at end of file +export { client } from './mcp/connect.service'; \ No newline at end of file diff --git a/service/src/llm/llm.controller.ts b/service/src/llm/llm.controller.ts new file mode 100644 index 0000000..ab945b7 --- /dev/null +++ b/service/src/llm/llm.controller.ts @@ -0,0 +1,29 @@ +import { Controller, RequestClientType } from "../common"; +import { PostMessageble } from "../hook/adapter"; +import { abortMessageService, streamingChatCompletion } from "./llm.service"; + +export class LlmController { + + @Controller('llm/chat/completions') + async chatCompletion(client: RequestClientType, data: any, webview: PostMessageble) { + if (!client) { + return { + code: 501, + msg:'mcp client 尚未连接' + }; + } + + await streamingChatCompletion(data, webview); + + return { + code: -1, + msg: 'terminate' + }; + } + + @Controller('llm/chat/completions/abort') + async abortChatCompletion(client: RequestClientType, data: any, webview: PostMessageble) { + return abortMessageService(data, webview); + } + +} \ No newline at end of file diff --git a/service/src/llm/llm.dto.ts b/service/src/llm/llm.dto.ts new file mode 100644 index 0000000..2514b92 --- /dev/null +++ b/service/src/llm/llm.dto.ts @@ -0,0 +1,9 @@ +import { OpenAI } from "openai"; + +export type MyMessageType = OpenAI.Chat.ChatCompletionMessageParam & { + extraInfo?: any; +} + +export type MyToolMessageType = OpenAI.Chat.ChatCompletionToolMessageParam & { + extraInfo?: any; +} \ No newline at end of file diff --git a/service/src/llm/llm.service.ts b/service/src/llm/llm.service.ts new file mode 100644 index 0000000..5bde736 --- /dev/null +++ b/service/src/llm/llm.service.ts @@ -0,0 +1,146 @@ +import { PostMessageble } from "../hook/adapter"; +import { OpenAI } from "openai"; +import { MyMessageType, MyToolMessageType } from "./llm.dto"; +import { RestfulResponse } from "../common/index.dto"; + +export let currentStream: AsyncIterable | null = null; + +export async function streamingChatCompletion( + data: any, + webview: PostMessageble +) { + let { baseURL, apiKey, model, messages, temperature, tools = [] } = data; + + const client = new OpenAI({ + baseURL, + apiKey + }); + + if (tools.length === 0) { + tools = undefined; + } + + postProcessMessages(messages); + + const stream = await client.chat.completions.create({ + model, + messages, + temperature, + tools, + tool_choice: 'auto', + web_search_options: {}, + stream: true + }); + + // 存储当前的流式传输对象 + currentStream = stream; + + // 流式传输结果 + for await (const chunk of stream) { + if (!currentStream) { + // 如果流被中止,则停止循环 + // TODO: 为每一个标签页设置不同的 currentStream 管理器 + stream.controller.abort(); + // 传输结束 + webview.postMessage({ + command: 'llm/chat/completions/done', + data: { + code: 200, + msg: { + success: true, + stage: 'abort' + } + } + }); + break; + } + + if (chunk.choices) { + const chunkResult = { + code: 200, + msg: { + chunk + } + }; + + webview.postMessage({ + command: 'llm/chat/completions/chunk', + data: chunkResult + }); + } + } + + // 传输结束 + webview.postMessage({ + command: 'llm/chat/completions/done', + data: { + code: 200, + msg: { + success: true, + stage: 'done' + } + } + }); +} + + +// 处理中止消息的函数 +export function abortMessageService(data: any, webview: PostMessageble): RestfulResponse { + if (currentStream) { + // 标记流已中止 + currentStream = null; + } + + return { + code: 200, + msg: { + success: true + } + } +} + +function postProcessToolMessages(message: MyToolMessageType) { + if (typeof message.content === 'string') { + return; + } + + for (const content of message.content) { + const contentType = content.type as string; + const rawContent = content as any; + + if (contentType === 'image') { + delete rawContent._meta; + + rawContent.type = 'text'; + + // 从缓存中提取图像数据 + rawContent.text = '图片已被删除'; + } + } + + message.content = JSON.stringify(message.content); +} + +export function postProcessMessages(messages: MyMessageType[]) { + for (const message of messages) { + // 去除 extraInfo 属性 + delete message.extraInfo; + + switch (message.role) { + case 'user': + break; + case 'assistant': + break; + + case 'system': + + break; + + case 'tool': + postProcessToolMessages(message); + break; + default: + break; + } + } +} \ No newline at end of file diff --git a/service/src/main.ts b/service/src/main.ts index 41f6f2d..f70218b 100644 --- a/service/src/main.ts +++ b/service/src/main.ts @@ -1,7 +1,7 @@ import WebSocket from 'ws'; import pino from 'pino'; -import { messageController } from './controller'; +import { routeMessage } from './common/router'; import { VSCodeWebViewLike } from './hook/adapter'; export interface VSCodeMessage { @@ -23,7 +23,7 @@ const logger = pino({ }); export type MessageHandler = (message: VSCodeMessage) => void; -const wss = new WebSocket.Server({ port: 8080 }); +const wss = new (WebSocket as any).Server({ port: 8080 }); wss.on('connection', ws => { @@ -44,6 +44,6 @@ wss.on('connection', ws => { logger.info(`command: [${message.command || 'No Command'}]`); const { command, data } = message; - messageController(command, data, webview); + routeMessage(command, data, webview); }); }); \ No newline at end of file diff --git a/service/src/mcp/client.controller.ts b/service/src/mcp/client.controller.ts new file mode 100644 index 0000000..845826b --- /dev/null +++ b/service/src/mcp/client.controller.ts @@ -0,0 +1,138 @@ +import { Controller, RequestClientType } from "../common"; +import { PostMessageble } from "../hook/adapter"; + +export class ClientController { + + @Controller('server/version') + async getServerVersion(client: RequestClientType, data: any, webview: PostMessageble) { + if (!client) { + return { + code: 501, + msg:'mcp client 尚未连接' + }; + } + + const version = client.getServerVersion(); + return { + code: 200, + msg: version + }; + } + + @Controller('prompts/list') + async listPrompts(client: RequestClientType, data: any, webview: PostMessageble) { + if (!client) { + const connectResult = { + code: 501, + msg: 'mcp client 尚未连接' + }; + return connectResult; + } + + const prompts = await client.listPrompts(); + const result = { + code: 200, + msg: prompts + }; + return result; + } + + @Controller('prompts/get') + async getPrompt(client: RequestClientType, option: any, webview: PostMessageble) { + if (!client) { + return { + code: 501, + msg: 'mcp client 尚未连接' + }; + } + + const prompt = await client.getPrompt(option.promptId, option.args || {}); + return { + code: 200, + msg: prompt + }; + } + + @Controller('resources/list') + async listResources(client: RequestClientType, data: any, webview: PostMessageble) { + if (!client) { + return { + code: 501, + msg: 'mcp client 尚未连接' + }; + } + + const resources = await client.listResources(); + return { + code: 200, + msg: resources + }; + } + + @Controller('resources/templates/list') + async listResourceTemplates(client: RequestClientType, data: any, webview: PostMessageble) { + if (!client) { + return { + code: 501, + msg: 'mcp client 尚未连接' + }; + } + + const resources = await client.listResourceTemplates(); + return { + code: 200, + msg: resources + }; + } + + @Controller('resources/read') + async readResource(client: RequestClientType, option: any, webview: PostMessageble) { + if (!client) { + return { + code: 501, + msg: 'mcp client 尚未连接' + }; + } + + const resource = await client.readResource(option.resourceUri); + return { + code: 200, + msg: resource + }; + } + + @Controller('tools/list') + async listTools(client: RequestClientType, data: any, webview: PostMessageble) { + if (!client) { + return { + code: 501, + msg: 'mcp client 尚未连接' + }; + } + + const tools = await client.listTools(); + return { + code: 200, + msg: tools + }; + } + + @Controller('tools/call') + async callTool(client: RequestClientType, option: any, webview: PostMessageble) { + if (!client) { + return { + code: 501, + msg: 'mcp client 尚未连接' + }; + } + + const toolResult = await client.callTool({ + name: option.toolName, + arguments: option.toolArgs + }); + return { + code: 200, + msg: toolResult + }; + } +} \ No newline at end of file diff --git a/service/src/mcp/client.dto.ts b/service/src/mcp/client.dto.ts new file mode 100644 index 0000000..1025588 --- /dev/null +++ b/service/src/mcp/client.dto.ts @@ -0,0 +1,37 @@ +import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js"; +import { SSEClientTransport } from "@modelcontextprotocol/sdk/client/sse.js"; +import { Implementation } from "@modelcontextprotocol/sdk/types"; + +export interface GetPromptOption { + promptId: string; + args?: Record; +} + +export interface ReadResourceOption { + resourceUri: string; +} + +export interface CallToolOption { + toolName: string; + toolArgs: Record; +} + +// 定义连接类型 +export type ConnectionType = 'STDIO' | 'SSE'; + +export type McpTransport = StdioClientTransport | SSEClientTransport; +export type IServerVersion = Implementation | undefined; + +// 定义命令行参数接口 +export interface McpOptions { + connectionType: ConnectionType; + // STDIO 特定选项 + command?: string; + args?: string[]; + // SSE 特定选项 + url?: string; + cwd?: string; + // 通用客户端选项 + clientName?: string; + clientVersion?: string; +} \ No newline at end of file diff --git a/service/src/hook/client.ts b/service/src/mcp/client.service.ts similarity index 81% rename from service/src/hook/client.ts rename to service/src/mcp/client.service.ts index d23b3ae..d1257c1 100644 --- a/service/src/hook/client.ts +++ b/service/src/mcp/client.service.ts @@ -2,38 +2,18 @@ import { Client } from "@modelcontextprotocol/sdk/client/index.js"; import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js"; import { SSEClientTransport } from "@modelcontextprotocol/sdk/client/sse.js"; -import { Implementation } from "@modelcontextprotocol/sdk/types"; - -// 定义连接类型 -type ConnectionType = 'STDIO' | 'SSE'; - -type McpTransport = StdioClientTransport | SSEClientTransport; -export type IServerVersion = Implementation | undefined; - -// 定义命令行参数接口 -export interface MCPOptions { - connectionType: ConnectionType; - // STDIO 特定选项 - command?: string; - args?: string[]; - // SSE 特定选项 - url?: string; - cwd?: string; - // 通用客户端选项 - clientName?: string; - clientVersion?: string; -} +import { McpOptions, McpTransport, IServerVersion } from './client.dto'; // 增强的客户端类 -export class MCPClient { +export class McpClient { private client: Client; private transport?: McpTransport; - private options: MCPOptions; + private options: McpOptions; private serverVersion: IServerVersion; private transportStdErr: string = ''; - constructor(options: MCPOptions) { + constructor(options: McpOptions) { this.options = options; this.serverVersion = undefined; @@ -144,8 +124,8 @@ export class MCPClient { } // Connect 函数实现 -export async function connect(options: MCPOptions): Promise { - const client = new MCPClient(options); +export async function connect(options: McpOptions): Promise { + const client = new McpClient(options); await client.connect(); return client; } diff --git a/service/src/mcp/connect.controller.ts b/service/src/mcp/connect.controller.ts new file mode 100644 index 0000000..6f979bc --- /dev/null +++ b/service/src/mcp/connect.controller.ts @@ -0,0 +1,39 @@ +import { Controller, RequestClientType } from '../common'; +import { PostMessageble } from '../hook/adapter'; +import { connectService } from './connect.service'; + +export class ConnectController { + + @Controller('connect') + async connect(client: RequestClientType, data: any, webview: PostMessageble) { + const res = await connectService(client, data); + return res; + } + + @Controller('lookup-env-var') + async lookupEnvVar(client: RequestClientType, data: any, webview: PostMessageble) { + const { keys } = data; + const values = keys.map((key: string) => process.env[key] || ''); + + return { + code: 200, + msg: values + } + } + + @Controller('ping') + async ping(client: RequestClientType, data: any, webview: PostMessageble) { + if (!client) { + const connectResult = { + code: 501, + msg:'mcp client 尚未连接' + }; + return connectResult; + } + + return { + code: 200, + msg: {} + } + } +} \ No newline at end of file diff --git a/service/src/service/connect.ts b/service/src/mcp/connect.service.ts similarity index 71% rename from service/src/service/connect.ts rename to service/src/mcp/connect.service.ts index 8868dfd..e2891cc 100644 --- a/service/src/service/connect.ts +++ b/service/src/mcp/connect.service.ts @@ -1,12 +1,14 @@ - -import { PostMessageble } from '../hook/adapter'; -import { connect, MCPClient, type MCPOptions } from '../hook/client'; import { spawnSync } from 'node:child_process'; +import { RequestClientType } from '../common'; +import { connect } from './client.service'; +import { RestfulResponse } from '../common/index.dto'; +import { McpOptions } from './client.dto'; -// TODO: 支持更多的 client -export let client: MCPClient | undefined = undefined; -function tryGetRunCommandError(command: string, args: string[] = [], cwd?: string): string | null { +// TODO: 更多的 client +export let client: RequestClientType = undefined; + +export function tryGetRunCommandError(command: string, args: string[] = [], cwd?: string): string | null { try { console.log('current command', command); console.log('current args', args); @@ -30,10 +32,9 @@ function tryGetRunCommandError(command: string, args: string[] = [], cwd?: strin } export async function connectService( - _client: MCPClient | undefined, - option: MCPOptions, - webview: PostMessageble -) { + _client: RequestClientType, + option: McpOptions +): Promise { try { console.log('ready to connect', option); @@ -42,7 +43,8 @@ export async function connectService( code: 200, msg: 'Connect to OpenMCP successfully\nWelcome back, Kirigaya' }; - webview.postMessage({ command: 'connect', data: connectResult }); + + return connectResult; } catch (error) { // TODO: 这边获取到的 error 不够精致,如何才能获取到更加精准的错误 @@ -61,6 +63,7 @@ export async function connectService( code: 500, msg: errorMsg }; - webview.postMessage({ command: 'connect', data: connectResult }); + + return connectResult; } } diff --git a/service/src/panel/panel.controller.ts b/service/src/panel/panel.controller.ts new file mode 100644 index 0000000..facf750 --- /dev/null +++ b/service/src/panel/panel.controller.ts @@ -0,0 +1,28 @@ +import { Controller, RequestClientType } from "../common"; +import { PostMessageble } from "../hook/adapter"; +import { loadTabSaveConfig, saveTabSaveConfig } from "./panel.service"; + +export class PanelController { + @Controller('panel/save') + async savePanel(client: RequestClientType, data: any, webview: PostMessageble) { + const serverInfo = client?.getServerVersion(); + saveTabSaveConfig(serverInfo, data); + + return { + code: 200, + msg: 'Settings saved successfully' + }; + } + + + @Controller('panel/load') + async loadPanel(client: RequestClientType, data: any, webview: PostMessageble) { + const serverInfo = client?.getServerVersion(); + const config = loadTabSaveConfig(serverInfo); + + return { + code: 200, + msg: config + }; + } +} \ No newline at end of file diff --git a/service/src/panel/panel.dto.ts b/service/src/panel/panel.dto.ts new file mode 100644 index 0000000..2a53498 --- /dev/null +++ b/service/src/panel/panel.dto.ts @@ -0,0 +1,12 @@ +export interface SaveTabItem { + name: string; + icon: string; + type: string; + componentIndex: number; + storage: Record; +} + +export interface SaveTab { + tabs: SaveTabItem[] + currentIndex: number +} \ No newline at end of file diff --git a/service/src/panel/panel.service.ts b/service/src/panel/panel.service.ts new file mode 100644 index 0000000..482ca83 --- /dev/null +++ b/service/src/panel/panel.service.ts @@ -0,0 +1,68 @@ +import * as fs from 'fs'; +import * as path from 'path'; +import { VSCODE_WORKSPACE } from '../hook/setting'; +import { IServerVersion } from '../mcp/client.dto'; +import { SaveTab } from './panel.dto'; +import { IConfig } from '../setting/setting.dto'; + +const DEFAULT_TABS: SaveTab = { + tabs: [], + currentIndex: -1 +} + +function getTabSavePath(serverInfo: IServerVersion) { + const { name = 'untitle', version = '0.0.0' } = serverInfo || {}; + const tabSaveName = `tabs.${name}.json`; + + // 如果是 vscode 插件下,则修改为 ~/.vscode/openmcp.json + if (VSCODE_WORKSPACE) { + // 在 VSCode 插件环境下 + const configDir = path.join(VSCODE_WORKSPACE, '.vscode'); + if (!fs.existsSync(configDir)) { + fs.mkdirSync(configDir, { recursive: true }); + } + return path.join(configDir, tabSaveName); + } + return tabSaveName; +} + +function createSaveTabConfig(serverInfo: IServerVersion): SaveTab { + const configPath = getTabSavePath(serverInfo); + const configDir = path.dirname(configPath); + + // 确保配置目录存在 + if (configDir && !fs.existsSync(configDir)) { + fs.mkdirSync(configDir, { recursive: true }); + } + + // 写入默认配置 + fs.writeFileSync(configPath, JSON.stringify(DEFAULT_TABS, null, 2), 'utf-8'); + return DEFAULT_TABS; +} + +export function loadTabSaveConfig(serverInfo: IServerVersion): SaveTab { + const tabSavePath = getTabSavePath(serverInfo); + + if (!fs.existsSync(tabSavePath)) { + return createSaveTabConfig(serverInfo); + } + + try { + const configData = fs.readFileSync(tabSavePath, 'utf-8'); + return JSON.parse(configData) as SaveTab; + } catch (error) { + console.error('Error loading config file, creating new one:', error); + return createSaveTabConfig(serverInfo); + } +} + +export function saveTabSaveConfig(serverInfo: IServerVersion, config: Partial): void { + const tabSavePath = getTabSavePath(serverInfo); + + try { + fs.writeFileSync(tabSavePath, JSON.stringify(config, null, 2), 'utf-8'); + } catch (error) { + console.error('Error saving config file:', error); + throw error; + } +} \ No newline at end of file diff --git a/service/src/service/env-var.ts b/service/src/service/env-var.ts deleted file mode 100644 index b283b43..0000000 --- a/service/src/service/env-var.ts +++ /dev/null @@ -1,27 +0,0 @@ -import { PostMessageble } from "../hook/adapter"; -import { MCPClient } from "../hook/client"; - - -export async function lookupEnvVarService(client: MCPClient | undefined, data: any, webview: PostMessageble) { - try { - const { keys } = data; - - const values = keys.map((key: string) => process.env[key] || ''); - - webview.postMessage({ - command: 'lookup-env-var', - data: { - code: 200, - msg: values - } - }); - } catch (error) { - webview.postMessage({ - command: 'lookup-env-var', - data: { - code: 500, - msg: `Failed to lookup env vars: ${(error as Error).message}` - } - }); - } -} \ No newline at end of file diff --git a/service/src/service/llm.ts b/service/src/service/llm.ts deleted file mode 100644 index f34f93f..0000000 --- a/service/src/service/llm.ts +++ /dev/null @@ -1,120 +0,0 @@ -import { OpenAI } from 'openai'; -import { MCPClient } from '../hook/client'; -import { PostMessageble } from '../hook/adapter'; -import { postProcessMessages } from '../hook/llm'; - -let currentStream: AsyncIterable | null = null; - -export async function chatCompletionService(client: MCPClient | undefined, data: any, webview: PostMessageble) { - if (!client) { - const connectResult = { - code: 501, - msg: 'mcp client 尚未连接' - }; - webview.postMessage({ command: 'ping', data: connectResult }); - return; - } - - - let { baseURL, apiKey, model, messages, temperature, tools = [] } = data; - - try { - const client = new OpenAI({ - baseURL, - apiKey - }); - - if (tools.length === 0) { - tools = undefined; - } - - postProcessMessages(messages); - - const stream = await client.chat.completions.create({ - model, - messages, - temperature, - tools, - tool_choice: 'auto', - web_search_options: {}, - stream: true - }); - - // 存储当前的流式传输对象 - currentStream = stream; - - // 流式传输结果 - for await (const chunk of stream) { - if (!currentStream) { - // 如果流被中止,则停止循环 - // TODO: 为每一个标签页设置不同的 currentStream 管理器 - stream.controller.abort(); - // 传输结束 - webview.postMessage({ - command: 'llm/chat/completions/done', - data: { - code: 200, - msg: { - success: true, - stage: 'abort' - } - } - }); - break; - } - - if (chunk.choices) { - const chunkResult = { - code: 200, - msg: { - chunk - } - }; - - webview.postMessage({ - command: 'llm/chat/completions/chunk', - data: chunkResult - }); - } - } - - // 传输结束 - webview.postMessage({ - command: 'llm/chat/completions/done', - data: { - code: 200, - msg: { - success: true, - stage: 'done' - } - } - }); - - } catch (error) { - webview.postMessage({ - command: 'llm/chat/completions/chunk', - data: { - code: 500, - msg: `OpenAI API error: ${(error as Error).message}` - } - }); - } -} - -// 处理中止消息的函数 -export function abortMessageService(client: MCPClient | undefined, data: any, webview: PostMessageble) { - if (currentStream) { - // 标记流已中止 - currentStream = null; - } - - webview.postMessage({ - command: 'llm/chat/completions/abort', - data: { - code: 200, - msg: { - success: true - } - } - }); -} \ No newline at end of file diff --git a/service/src/service/mcp-server.ts b/service/src/service/mcp-server.ts deleted file mode 100644 index 5862729..0000000 --- a/service/src/service/mcp-server.ts +++ /dev/null @@ -1,289 +0,0 @@ -import { PostMessageble } from "../hook/adapter"; -import { MCPClient } from "../hook/client"; - -// ==================== 接口定义 ==================== -export interface GetPromptOption { - promptId: string; - args?: Record; -} - -export interface ReadResourceOption { - resourceUri: string; -} - -export interface CallToolOption { - toolName: string; - toolArgs: Record; -} - -// ==================== 函数实现 ==================== - -/** - * @description 列出所有 prompts - */ -export async function listPromptsService( - client: MCPClient | undefined, - data: any, - webview: PostMessageble -) { - if (!client) { - const connectResult = { - code: 501, - msg: 'mcp client 尚未连接' - }; - webview.postMessage({ command: 'prompts/list', data: connectResult }); - return; - } - - try { - const prompts = await client.listPrompts(); - const result = { - code: 200, - msg: prompts - }; - webview.postMessage({ command: 'prompts/list', data: result }); - } catch (error) { - const result = { - code: 500, - msg: (error as any).toString() - }; - webview.postMessage({ command: 'prompts/list', data: result }); - } -} - -/** - * @description 获取特定 prompt - */ -export async function getPromptService( - client: MCPClient | undefined, - option: GetPromptOption, - webview: PostMessageble -) { - if (!client) { - const connectResult = { - code: 501, - msg: 'mcp client 尚未连接' - }; - webview.postMessage({ command: 'prompts/get', data: connectResult }); - return; - } - - try { - const prompt = await client.getPrompt(option.promptId, option.args || {}); - const result = { - code: 200, - msg: prompt - }; - webview.postMessage({ command: 'prompts/get', data: result }); - } catch (error) { - const result = { - code: 500, - msg: (error as any).toString() - }; - webview.postMessage({ command: 'prompts/get', data: result }); - } -} - -/** - * @description 列出所有resources - */ -export async function listResourcesService( - client: MCPClient | undefined, - data: any, - webview: PostMessageble -) { - if (!client) { - const connectResult = { - code: 501, - msg: 'mcp client 尚未连接' - }; - webview.postMessage({ command: 'resources/list', data: connectResult }); - return; - } - - try { - const resources = await client.listResources(); - const result = { - code: 200, - msg: resources - }; - webview.postMessage({ command: 'resources/list', data: result }); - } catch (error) { - const result = { - code: 500, - msg: (error as any).toString() - }; - webview.postMessage({ command: 'resources/list', data: result }); - } -} - - -/** - * @description 列出所有resources - */ -export async function listResourceTemplatesService( - client: MCPClient | undefined, - data: any, - webview: PostMessageble -) { - if (!client) { - const connectResult = { - code: 501, - msg: 'mcp client 尚未连接' - }; - webview.postMessage({ command: 'resources/templates/list', data: connectResult }); - return; - } - - try { - const resources = await client.listResourceTemplates(); - const result = { - code: 200, - msg: resources - }; - webview.postMessage({ command: 'resources/templates/list', data: result }); - } catch (error) { - const result = { - code: 500, - msg: (error as any).toString() - }; - webview.postMessage({ command: 'resources/templates/list', data: result }); - } -} - - -/** - * @description 读取特定resource - */ -export async function readResourceService( - client: MCPClient | undefined, - option: ReadResourceOption, - webview: PostMessageble -) { - if (!client) { - const connectResult = { - code: 501, - msg: 'mcp client 尚未连接' - }; - webview.postMessage({ command: 'resources/read', data: connectResult }); - return; - } - - try { - const resource = await client.readResource(option.resourceUri); - const result = { - code: 200, - msg: resource - }; - webview.postMessage({ command: 'resources/read', data: result }); - } catch (error) { - const result = { - code: 500, - msg: (error as any).toString() - }; - webview.postMessage({ command: 'resources/read', data: result }); - } -} - -/** - * @description 获取工具列表 - */ -export async function listToolsService( - client: MCPClient | undefined, - data: any, - webview: PostMessageble -) { - if (!client) { - const connectResult = { - code: 501, - msg: 'mcp client 尚未连接' - }; - webview.postMessage({ command: 'tools/list', data: connectResult }); - return; - } - - try { - const tools = await client.listTools(); - - const result = { - code: 200, - msg: tools - }; - - webview.postMessage({ command: 'tools/list', data: result }); - } catch (error) { - const result = { - code: 500, - msg: (error as any).toString() - }; - webview.postMessage({ command: 'tools/list', data: result }); - } -} - - -/** - * @description 调用工具 - */ -export async function callToolService( - client: MCPClient | undefined, - option: CallToolOption, - webview: PostMessageble -) { - if (!client) { - const connectResult = { - code: 501, - msg: 'mcp client 尚未连接' - }; - webview.postMessage({ command: 'tools/call', data: connectResult }); - return; - } - - try { - const toolResult = await client.callTool({ - name: option.toolName, - arguments: option.toolArgs - }); - - const result = { - code: 200, - msg: toolResult - }; - webview.postMessage({ command: 'tools/call', data: result }); - } catch (error) { - const result = { - code: 500, - msg: (error as any).toString() - }; - webview.postMessage({ command: 'tools/call', data: result }); - } -} - -export async function getServerVersionService( - client: MCPClient | undefined, - data: any, - webview: PostMessageble -) { - if (!client) { - const connectResult = { - code: 501, - msg:'mcp client 尚未连接' - }; - webview.postMessage({ command: 'server/version', data: connectResult }); - return; - } - - try { - const version = client.getServerVersion(); - const result = { - code: 200, - msg: version - }; - webview.postMessage({ command:'server/version', data: result }); - } catch (error) { - const result = { - code: 500, - msg: (error as any).toString() - }; - webview.postMessage({ command:'server/version', data: result }); - } -} \ No newline at end of file diff --git a/service/src/service/ocr.ts b/service/src/service/ocr.ts deleted file mode 100644 index 032b3a6..0000000 --- a/service/src/service/ocr.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { PostMessageble } from "../hook/adapter"; -import { MCPClient } from "../hook/client"; - -export function ocrService( - client: MCPClient | undefined, - data: any, - webview: PostMessageble -) { - - - webview.postMessage({ - command: 'ping', - data: { - code: 200, - msg: {} - } - }); -} \ No newline at end of file diff --git a/service/src/service/panel.ts b/service/src/service/panel.ts deleted file mode 100644 index 2263598..0000000 --- a/service/src/service/panel.ts +++ /dev/null @@ -1,55 +0,0 @@ -import { PostMessageble } from '../hook/adapter'; -import { loadConfig, loadTabSaveConfig, saveConfig, saveTabSaveConfig } from '../hook/setting'; -import { MCPClient } from '../hook/client'; - -export async function panelSaveService(client: MCPClient | undefined, data: any, webview: PostMessageble) { - try { - // 保存配置 - const serverInfo = client?.getServerVersion(); - saveTabSaveConfig(serverInfo, data); - - webview.postMessage({ - command: 'panel/save', - data: { - code: 200, - msg: 'Settings saved successfully' - } - }); - } catch (error) { - webview.postMessage({ - command: 'panel/save', - data: { - code: 500, - msg: `Failed to save settings: ${(error as Error).message}` - } - }); - } -} - -export async function panelLoadService( - client: MCPClient | undefined, - data: any, - webview: PostMessageble -) { - try { - // 加载配置 - const serverInfo = client?.getServerVersion(); - const config = loadTabSaveConfig(serverInfo); - - webview.postMessage({ - command: 'panel/load', - data: { - code: 200, - msg: config // 直接返回配置对象 - } - }); - } catch (error) { - webview.postMessage({ - command: 'panel/load', - data: { - code: 500, - msg: `Failed to load settings: ${(error as Error).message}` - } - }); - } -} \ No newline at end of file diff --git a/service/src/service/setting.ts b/service/src/service/setting.ts deleted file mode 100644 index f720a43..0000000 --- a/service/src/service/setting.ts +++ /dev/null @@ -1,60 +0,0 @@ -import { PostMessageble } from '../hook/adapter'; -import { loadConfig, saveConfig } from '../hook/setting'; -import { MCPClient } from '../hook/client'; - -export async function settingSaveService( - client: MCPClient | undefined, - data: any, - webview: PostMessageble -) { - try { - // 保存配置 - saveConfig(data); - console.log('Settings saved successfully'); - - webview.postMessage({ - command: 'setting/save', - data: { - code: 200, - msg: 'Settings saved successfully' - } - }); - } catch (error) { - console.log('Setting save failed:', error); - - webview.postMessage({ - command: 'setting/save', - data: { - code: 500, - msg: `Failed to save settings: ${(error as Error).message}` - } - }); - } -} - -export async function settingLoadService( - client: MCPClient | undefined, - data: any, - webview: PostMessageble -) { - try { - // 加载配置 - const config = loadConfig(); - - webview.postMessage({ - command: 'setting/load', - data: { - code: 200, - msg: config // 直接返回配置对象 - } - }); - } catch (error) { - webview.postMessage({ - command: 'setting/load', - data: { - code: 500, - msg: `Failed to load settings: ${(error as Error).message}` - } - }); - } -} \ No newline at end of file diff --git a/service/src/service/util.ts b/service/src/service/util.ts deleted file mode 100644 index f874665..0000000 --- a/service/src/service/util.ts +++ /dev/null @@ -1,25 +0,0 @@ -import { PostMessageble } from "../hook/adapter"; -import { MCPClient } from "../hook/client"; - -export function pingService( - client: MCPClient | undefined, - data: any, - webview: PostMessageble -) { - if (!client) { - const connectResult = { - code: 501, - msg: 'mcp client 尚未连接' - }; - webview.postMessage({ command: 'ping', data: connectResult }); - return; - } - - webview.postMessage({ - command: 'ping', - data: { - code: 200, - msg: {} - } - }); -} \ No newline at end of file diff --git a/service/src/setting/setting.controller.ts b/service/src/setting/setting.controller.ts new file mode 100644 index 0000000..e51e236 --- /dev/null +++ b/service/src/setting/setting.controller.ts @@ -0,0 +1,27 @@ +import { Controller, RequestClientType } from "../common"; +import { PostMessageble } from "../hook/adapter"; +import { loadSetting, saveSetting } from "./setting.service"; + +export class SettingController { + + @Controller('setting/save') + async saveSetting(client: RequestClientType, data: any, webview: PostMessageble) { + saveSetting(data); + console.log('Settings saved successfully'); + + return { + code: 200, + msg: 'Settings saved successfully' + }; + } + + @Controller('setting/load') + async loadSetting(client: RequestClientType, data: any, webview: PostMessageble) { + const config = loadSetting(); + return { + code: 200, + msg: config + } + } + +} \ No newline at end of file diff --git a/service/src/setting/setting.dto.ts b/service/src/setting/setting.dto.ts new file mode 100644 index 0000000..1cd3ed8 --- /dev/null +++ b/service/src/setting/setting.dto.ts @@ -0,0 +1,4 @@ +export interface IConfig { + MODEL_INDEX: number; + [key: string]: any; +} \ No newline at end of file diff --git a/service/src/setting/setting.service.ts b/service/src/setting/setting.service.ts new file mode 100644 index 0000000..83dd4ef --- /dev/null +++ b/service/src/setting/setting.service.ts @@ -0,0 +1,77 @@ +import * as fs from 'fs'; +import * as os from 'os'; +import * as path from 'path'; +import { VSCODE_WORKSPACE } from '../hook/setting'; +import { IConfig } from './setting.dto'; +import { llms } from '../hook/llm'; + +function getConfigurationPath() { + // 如果是 vscode 插件下,则修改为 ~/.openmcp/config.json + if (VSCODE_WORKSPACE) { + // 在 VSCode 插件环境下 + const homeDir = os.homedir(); + const configDir = path.join(homeDir, '.openmcp'); + if (!fs.existsSync(configDir)) { + fs.mkdirSync(configDir, { recursive: true }); + } + return path.join(configDir, 'setting.json'); + } + return 'setting.json'; +} + +function getDefaultLanguage() { + if (process.env.VSCODE_PID) { + // TODO: 获取 vscode 内部的语言 + + } + return 'zh'; +} + +const DEFAULT_CONFIG: IConfig = { + MODEL_INDEX: 0, + LLM_INFO: llms, + LANG: getDefaultLanguage() +}; + + +function createConfig(): IConfig { + const configPath = getConfigurationPath(); + const configDir = path.dirname(configPath); + + // 确保配置目录存在 + if (configDir && !fs.existsSync(configDir)) { + fs.mkdirSync(configDir, { recursive: true }); + } + + // 写入默认配置 + fs.writeFileSync(configPath, JSON.stringify(DEFAULT_CONFIG, null, 2), 'utf-8'); + return DEFAULT_CONFIG; +} + +export function loadSetting(): IConfig { + const configPath = getConfigurationPath(); + + if (!fs.existsSync(configPath)) { + return createConfig(); + } + + try { + const configData = fs.readFileSync(configPath, 'utf-8'); + return JSON.parse(configData) as IConfig; + } catch (error) { + console.error('Error loading config file, creating new one:', error); + return createConfig(); + } +} + +export function saveSetting(config: Partial): void { + const configPath = getConfigurationPath(); + console.log('save to ' + configPath); + + try { + fs.writeFileSync(configPath, JSON.stringify(config, null, 2), 'utf-8'); + } catch (error) { + console.error('Error saving config file:', error); + throw error; + } +}