Compare commits

...

70 Commits

Author SHA1 Message Date
Kirigaya Kazuto
e09c8beb7e
Merge pull request #43 from LSTM-Kirigaya/dev
Some checks are pending
Build / build (ubuntu-latest) (push) Waiting to run
Test / test (macos-latest) (push) Waiting to run
Test / test (ubuntu-latest) (push) Waiting to run
Test / test (windows-latest) (push) Waiting to run
update yarn
2025-06-22 17:11:51 +08:00
9abc679c8f update yarn 2025-06-22 16:59:38 +08:00
li1553770945
f9fd4db580 fix:添加vsce作为项目依赖 2025-06-22 16:49:20 +08:00
Li Yaning
eab59e4ba1
Merge pull request #42 from LSTM-Kirigaya/dev
包管理器换成yarn
2025-06-22 16:32:04 +08:00
li1553770945
d3ae4c7999 Merge branch 'dev' of https://github.com/LSTM-Kirigaya/openmcp-client into dev 2025-06-22 16:26:35 +08:00
li1553770945
ed4d5e2ed1 Merge branch 'main' into dev 2025-06-22 16:25:37 +08:00
Li Yaning
216dabec6f
Merge pull request #41 from LSTM-Kirigaya/hotfix
包管理器换成yarn
2025-06-22 16:21:44 +08:00
li1553770945
279768e320 feat:包管理器改为yarn 2025-06-22 16:15:11 +08:00
li1553770945
ef811c1e8a fix:修复依赖问题 2025-06-22 15:32:27 +08:00
li1553770945
1702f93d16 feat:添加连接测试 2025-06-22 15:23:41 +08:00
li1553770945
c96d995f11 fix:修复依赖版本问题 2025-06-22 15:17:33 +08:00
li1553770945
15f0ff91e3 fix:添加缺失依赖 2025-06-22 14:55:32 +08:00
Kirigaya Kazuto
dd7685e42c
Merge pull request #40 from LSTM-Kirigaya/dev
Dev
2025-06-22 04:21:09 +08:00
ae56771c5a finish config export 2025-06-22 04:06:21 +08:00
10ffe098c2 finish config export 2025-06-22 04:03:29 +08:00
d10c88f35e finish config export 2025-06-22 04:00:14 +08:00
Kirigaya Kazuto
22a79cfc55
Merge pull request #39 from LSTM-Kirigaya/dev
Dev
2025-06-20 21:20:37 +08:00
3e363c6dde finish sdk 0.0.8 2025-06-20 21:20:18 +08:00
4a8385d7ad task loop 2025-06-20 21:06:01 +08:00
be641197b5 add prompt reader 2025-06-20 18:19:20 +08:00
7098382317 upgrade taskloop 2025-06-20 17:23:45 +08:00
51ceacbc10 upgrade taskloop 2025-06-20 17:04:26 +08:00
50510ae647 upgrade taskloop 2025-06-20 15:49:54 +08:00
f59e27b569 fix error 2025-06-20 14:35:07 +08:00
10befe12b4 save 2025-06-20 13:54:04 +08:00
7968a0d4c5 finish new agent framework 2025-06-20 02:42:29 +08:00
f51bd364b4 finish new agent framework 2025-06-20 02:36:53 +08:00
66f9b128f8 update 2025-06-19 23:56:08 +08:00
Kirigaya Kazuto
d04c04f574
Merge pull request #38 from LSTM-Kirigaya/dev
Dev
2025-06-19 15:20:34 +08:00
a5fe596cac task loop 2025-06-19 15:19:31 +08:00
e6d1f1205a task loop 2025-06-19 14:24:43 +08:00
Meghan Morrow
1b3418f15b 修复: 添加工作区验证以防止.openmcp目录创建错误
- 在getWorkspaceConnectionConfigPath()中添加工作区路径验证
- 在getWorkspaceConnectionConfig()中添加工作区路径验证
- 当没有找到工作区时抛出描述性错误
- 解决VSCode扩展尝试创建.openmcp目录时的ENOENT错误
2025-06-19 14:24:43 +08:00
Meghan Morrow
0961f18381 refactor: 更新task-loop注释,移除过时的XML工具调用说明
- 删除handleChunkDeltaContent函数注释中关于XML指令包裹的toolcall描述
- 该功能现在在toolcalled生命周期后处理,而非chunk生命周期中处理
2025-06-19 14:24:43 +08:00
9449bdb190 support xml 2025-06-19 14:24:43 +08:00
d4ac089a9a support xml 2025-06-19 14:24:43 +08:00
0df86cfc28 change tooltip color 2025-06-19 14:24:43 +08:00
c9a2d47c85 task loop 2025-06-19 14:24:24 +08:00
Li Yaning
51d4495f88
Merge pull request #37 from LSTM-Kirigaya/main
feat:增加测试样例
2025-06-19 11:01:02 +08:00
Meghan Morrow
8446cf07dc 修复: 添加工作区验证以防止.openmcp目录创建错误
- 在getWorkspaceConnectionConfigPath()中添加工作区路径验证
- 在getWorkspaceConnectionConfig()中添加工作区路径验证
- 当没有找到工作区时抛出描述性错误
- 解决VSCode扩展尝试创建.openmcp目录时的ENOENT错误
2025-06-19 10:55:06 +08:00
li1553770945
61a82061d7 feat:增加测试样例 2025-06-19 00:26:53 +08:00
Meghan Morrow
777a17bb99 refactor: 更新task-loop注释,移除过时的XML工具调用说明
- 删除handleChunkDeltaContent函数注释中关于XML指令包裹的toolcall描述
- 该功能现在在toolcalled生命周期后处理,而非chunk生命周期中处理
2025-06-18 22:17:35 +08:00
8355e0ec66 support xml 2025-06-18 21:47:02 +08:00
2c13ce5e6d support xml 2025-06-18 20:32:04 +08:00
c2a634118b change tooltip color 2025-06-18 17:04:33 +08:00
3700857aa8 update 2025-06-17 23:32:50 +08:00
94935e6b9c update 2025-06-17 23:29:23 +08:00
50990688be update 2025-06-17 23:12:37 +08:00
li1553770945
2a8ff295b2 feat:构建工具改为esbuild 2025-06-15 23:59:39 +08:00
Kirigaya Kazuto
3cae8572e8
Merge pull request #35 from LSTM-Kirigaya/claude
Claude
2025-06-15 20:53:47 +08:00
5bfda7a893 finish ocr adjust 2025-06-15 20:53:19 +08:00
d26201c893 finish ocr adjust 2025-06-15 19:55:39 +08:00
Meghan Morrow
dad8d41e46 feat: 工具模块界面优化 + OpenRouter集成
- 优化工具列表显示:工具名称优先显示,描述悬停提示
- 集成OpenRouter支持:400+模型一键访问
- 改进模型选择界面:支持搜索过滤和分类显示
- 自动同步新提供商配置
- 版本升级至0.1.7

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-06-15 19:27:54 +08:00
596e42e110 recover task-loop package 2025-06-15 19:18:56 +08:00
e22d344647 recover task-loop package 2025-06-15 18:55:54 +08:00
li1553770945
62ea963003 feat:更新github actions 2025-06-15 00:31:09 +08:00
Meghan Morrow
2af555dd3b feat: add MCP security checks to MVP requirements
Added MCP security checks (prevent prompt injection, etc.) to the MVP roadmap in README to enhance security features.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-06-15 00:25:05 +08:00
li1553770945
0560fda4bb feat:添加测试程序(未添加测试用例) 2025-06-15 00:18:17 +08:00
dcbcf567cc update 2025-06-14 00:13:46 +08:00
Li Yaning
7e203905de
Merge pull request #33 from ZYD045692/feature/mcp-hot-update-stdio
feat(service-mcp-connect): 实现stdio连接方式的热更新
2025-06-14 00:08:24 +08:00
li1553770945
52074d1ea2 feat:增加自动构建脚本 2025-06-14 00:02:53 +08:00
Li Yaning
66e748bf01
Merge pull request #34 from LSTM-Kirigaya/hotfix
构建工具改为rollup
2025-06-13 23:28:52 +08:00
li1553770945
836fc6d7b0 Merge branch 'main' of https://github.com/LSTM-Kirigaya/openmcp-client into hotfix 2025-06-13 23:28:24 +08:00
li1553770945
786f5ea615 feat:构建工具改为rollup 2025-06-13 23:07:27 +08:00
li1553770945
e7856dd8a6 fix 2025-06-13 21:27:37 +08:00
li1553770945
bd23bfe17c fix 2025-06-13 21:22:49 +08:00
li1553770945
dfb70785e0 bugfix:尝试修复路径问题 2025-06-13 21:12:49 +08:00
aa8547c4ad fix 0.1.5 cannot launch in windows 2025-06-13 21:08:44 +08:00
54826eda62 fix 0.1.5 cannot launch in windows 2025-06-13 18:11:17 +08:00
506d33be9a fix 0.1.5 cannot launch in windows 2025-06-13 18:05:58 +08:00
ZYD045692
17cc8f7612 feat(service-mcp-connect): 实现stdio连接方式的热更新 2025-06-13 17:40:26 +08:00
106 changed files with 18692 additions and 2970 deletions

45
.github/workflows/build.yaml vendored Normal file
View File

@ -0,0 +1,45 @@
name: Build
on:
push:
branches:
- main
- dev
- hotfix
- hotfix/*
release:
types:
- published
jobs:
build:
strategy:
matrix:
os: [ubuntu-latest]
runs-on: ${{ matrix.os }}
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '22.x'
cache: 'yarn'
- name: Install dependencies
run: yarn install --frozen-lockfile
- name: Build all
run: yarn build
- name: Package VSIX
run: npx vsce package --out dist/openmcp.vsix
- name: Upload VSIX
uses: actions/upload-artifact@v4
with:
name: openmcp-${{ matrix.os }}
path: dist/openmcp.vsix

44
.github/workflows/test.yaml vendored Normal file
View File

@ -0,0 +1,44 @@
name: Test
on:
push:
branches:
- main
- dev
- hotfix
- hotfix/*
release:
types:
- published
jobs:
test:
strategy:
matrix:
os: [ubuntu-latest, windows-latest, macos-latest]
runs-on: ${{ matrix.os }}
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '22.x'
cache: 'yarn'
- name: Install dependencies
run: yarn install --frozen-lockfile
- name: Install xvfb (Linux only)
if: runner.os == 'Linux'
run: sudo apt-get update && sudo apt-get install -y xvfb
- name: Run Test with XVFB (Linux)
if: runner.os == 'Linux'
run: xvfb-run --auto-servernum yarn test
- name: Run Test (Windows/macOS)
if: runner.os != 'Linux'
run: yarn test

2
.gitignore vendored
View File

@ -16,3 +16,5 @@ resources/service
*.traineddata *.traineddata
.turbo .turbo
stats.html stats.html
.openmcp
test-vsix

View File

@ -24,3 +24,7 @@ software/**
.editorconfig .editorconfig
.gitattributes .gitattributes
*.vsix *.vsix
.turbo
.github
webpack
.openmcp

View File

@ -1,5 +1,19 @@
# Change Log # Change Log
## [main] 0.1.8
- 增加 STDIO 下的热更新,现在用户修改 mcp 代码openmcp 会自动完成一切相关功能的热更新,无需用户手动重启。
- 完成 mcpconfig.json 的导出功能,导出的 配置文件 可以通过 openmcp-sdk 框架完成低代码 agent 部署;也可以直接载入 Claude Desktop 等等 MCP 客户端中,实现 MCP 的快速部署和使用。
- 修复若干 vscode 插件端 bug
## [main] 0.1.7
- 新的构建系统
- 修复无法在 trae & cursor 中使用的 bug
## [main] 0.1.6
- 针对 0.1.5 无法在 Windows 启动的紧急修复。
- 修复环境变量中添加 token 失效的问题。
- 优化工具展示的页面布局。
## [main] 0.1.5 ## [main] 0.1.5
- 修复 gemini 获取模型列表时存在 models 前缀的问题 - 修复 gemini 获取模型列表时存在 models 前缀的问题
- 增加 web api 功能 - 增加 web api 功能

169
CLAUDE.md Normal file
View File

@ -0,0 +1,169 @@
# CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
## Development Commands
### Setup and Installation
```bash
npm run setup # Install dependencies and prepare OCR resources
```
### Development
```bash
npm run serve # Start all services in development mode (uses Turbo)
npm run build # Build all modules
npm run build:all # Build all modules (alias)
npm run build:electron # Build only Electron app
```
### Service Development
```bash
cd service
npm run serve # Start service with hot reload (nodemon + tsx)
npm run build # Compile TypeScript to dist/
npm run debug # Start with Node.js inspector
npm run typecheck # Type checking without emit
```
### Renderer Development
```bash
cd renderer
npm run serve # Start Vite dev server
npm run build # Build for production
npm run serve:website # Start in website mode
npm run type-check # Vue TypeScript checking
```
### VSCode Extension
```bash
npm run vscode:prepublish # Prepare for VSCode publishing (Rollup build)
npm run compile # Compile TypeScript
npm run watch # Watch mode compilation
vsce package # Create VSIX package for distribution
vsce publish # Publish to VSCode Marketplace (requires auth)
```
### Quality Assurance
```bash
npm run lint # ESLint for TypeScript files
npm run pretest # Run compile and lint
npm run test # Run tests
```
## Architecture Overview
### Multi-Module Structure
OpenMCP follows a **layered modular architecture** with three main deployment targets:
1. **VSCode Extension** (`src/extension.ts`) - IDE integration
2. **Service Layer** (`service/`) - Node.js backend handling MCP protocol
3. **Renderer Layer** (`renderer/`) - Vue.js frontend for UI
### Key Architectural Patterns
#### Message Bridge Communication
The system uses a **message bridge pattern** for cross-platform communication:
- **VSCode**: Uses `vscode.postMessage` API
- **Electron**: Uses IPC communication
- **Web**: Uses WebSocket connections
- **Node.js**: Uses EventEmitter for SDK mode
All communication flows through `MessageBridge` class in `renderer/src/api/message-bridge.ts`.
#### MCP Client Management
- **Connection Management**: `service/src/mcp/connect.service.ts` handles multiple MCP server connections
- **Client Pooling**: `clientMap` maintains active MCP client instances with UUID-based identification
- **Transport Abstraction**: Supports STDIO, SSE, and StreamableHTTP transports
- **Auto-reconnection**: `McpServerConnectMonitor` handles connection monitoring
#### Request/Response Flow
```
Frontend (Vue) → MessageBridge → Service Router → MCP Controllers → MCP SDK → External MCP Servers
```
### Important Service Patterns
#### Preprocessing Commands
`service/src/mcp/connect.service.ts` includes **automatic environment setup**:
- Python projects: Auto-runs `uv sync` and installs MCP CLI
- Node.js projects: Auto-runs `npm install` if node_modules missing
- Path resolution: Handles `~/` home directory expansion
#### OCR Integration
Built-in OCR using Tesseract.js:
- Images from MCP responses are automatically processed
- Base64 images saved to temp files and queued for OCR
- Results delivered via worker threads
### Frontend Architecture (Vue 3)
#### State Management
- **Panel System**: Tab-based interface in `renderer/src/components/main-panel/`
- **Reactive Connections**: MCP connection state managed reactively
- **Multi-language**: Vue i18n with support for 9 languages
#### Core Components
- **Chat Interface**: `main-panel/chat/` - LLM interaction with MCP tools
- **Tool Testing**: `main-panel/tool/` - Direct MCP tool invocation
- **Resource Browser**: `main-panel/resource/` - MCP resource exploration
- **Prompt Manager**: `main-panel/prompt/` - System prompt templates
### Build System
#### Turbo Monorepo
Uses Turbo for coordinated builds across modules:
- **Dependency ordering**: Renderer builds before Electron
- **Parallel execution**: Service and Renderer can build concurrently
- **Task caching**: Disabled for development iterations
#### Rollup Configuration
VSCode extension uses Rollup for optimal bundling:
- **ES modules**: Output as ESM format
- **External dependencies**: VSCode API marked as external
- **TypeScript**: Direct compilation without webpack
## Development Workflow
### Adding New MCP Features
1. **Service Layer**: Add controller in `service/src/mcp/`
2. **Router Registration**: Add to `ModuleControllers` in `service/src/common/router.ts`
3. **Frontend Integration**: Add API calls in `renderer/src/api/`
4. **UI Components**: Create components in `renderer/src/components/`
### Testing MCP Servers
1. **Connection**: Configure in connection panel (STDIO/SSE/HTTP)
2. **Validation**: Test tools/resources in respective panels
3. **Integration**: Verify LLM interaction in chat interface
### Packaging VSCode Extension
1. **Build Dependencies**: Run `npm run build` to build all modules
2. **Prepare Extension**: Run `npm run vscode:prepublish` to bundle extension code
3. **Create Package**: Run `vsce package` to generate `.vsix` file
4. **Install Locally**: Use `code --install-extension openmcp-x.x.x.vsix` for testing
5. **Publish**: Run `vsce publish` (requires marketplace publisher account)
### Platform-Specific Considerations
- **VSCode**: Uses webview API, limited to extension context
- **Electron**: Full desktop app capabilities, local service spawning
- **Web**: Requires external service, WebSocket limitations
- **SDK**: Embedded in other Node.js applications
## Important Files
### Configuration
- `turbo.json` - Monorepo build orchestration
- `rollup.config.js` - VSCode extension bundling
- `service/package.json` - Backend dependencies and scripts
- `renderer/package.json` - Frontend dependencies and scripts
### Core Architecture
- `src/extension.ts` - VSCode extension entry point
- `service/src/main.ts` - Service WebSocket server
- `service/src/common/router.ts` - Request routing system
- `renderer/src/api/message-bridge.ts` - Cross-platform communication
- `service/src/mcp/client.service.ts` - MCP client implementation

View File

@ -60,6 +60,7 @@ Supports multiple large models
| `service` | Cloud sync for system configuration | `MVP` | 0% | `P1` | | `service` | Cloud sync for system configuration | `MVP` | 0% | `P1` |
| `all` | System prompt management module | `Iteration` | 100% | `Done` | | `all` | System prompt management module | `Iteration` | 100% | `Done` |
| `service` | Tool-wise logging system | `MVP` | 0% | `P1` | | `service` | Tool-wise logging system | `MVP` | 0% | `P1` |
| `service` | MCP security checks (prevent prompt injection, etc.) | `MVP` | 0% | `P1` |
| `service` | Built-in OCR for character recognition | `Iteration` | 100% | `Done` | | `service` | Built-in OCR for character recognition | `Iteration` | 100% | `Done` |
## Project Concept ## Project Concept
@ -134,9 +135,8 @@ npm run setup
Start dev server: Start dev server:
```bash ```bash
npm run dev npm run serve
``` ```
Port usage: 8282 (renderer) + 8081 (service)
### Extension Development ### Extension Development
@ -151,4 +151,21 @@ Build for deployment:
```bash ```bash
npm run build npm run build
``` ```
build vscode extension:
```bash
npm run build:plugin
```
Then just press F5, いただきます (Let's begin) Then just press F5, いただきます (Let's begin)
---
## CI Pipeline
✅ npm run build
✅ npm run build:task-loop
✅ openmcp-client UT
✅ openmcp-sdk UT
✅ vscode extension UT

20
esbuild.config.js Normal file
View File

@ -0,0 +1,20 @@
// esbuild.config.js
const { build } = require('esbuild');
build({
entryPoints: ['src/extension.ts'],
bundle: true,
platform: 'node',
format: 'cjs',
outfile: 'dist/extension.cjs.js',
sourcemap: true,
external: ['vscode'],
target: ['node22'],
loader: {
'.json': 'json'
},
define: { 'import.meta.url': '_importMetaUrl' },
banner: {
js: "const _importMetaUrl=require('url').pathToFileURL(__filename)",
},
}).catch(() => process.exit(1));

7463
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -2,8 +2,9 @@
"name": "openmcp", "name": "openmcp",
"displayName": "OpenMCP", "displayName": "OpenMCP",
"description": "An all in one MCP Client/TestTool", "description": "An all in one MCP Client/TestTool",
"version": "0.1.5", "version": "0.1.8",
"publisher": "kirigaya", "publisher": "kirigaya",
"private": true,
"author": { "author": {
"name": "kirigaya", "name": "kirigaya",
"email": "1193466151@qq.com" "email": "1193466151@qq.com"
@ -19,7 +20,7 @@
"Other" "Other"
], ],
"activationEvents": [], "activationEvents": [],
"main": "./dist/extension.js", "main": "./dist/extension.cjs.js",
"icon": "icons/openmcp.png", "icon": "icons/openmcp.png",
"contributes": { "contributes": {
"configuration": { "configuration": {
@ -220,42 +221,64 @@
"renderer" "renderer"
], ],
"scripts": { "scripts": {
"setup": "npm i && npm run prepare:ocr", "setup": "yarn install && yarn prepare:ocr",
"serve": "turbo serve", "serve": "turbo serve",
"build": "turbo build", "build": "turbo build && tsc -p ./ && node esbuild.config.js",
"build:electron": "turbo build --filter=@openmcp/electron", "build:plugin": "yarn build && tsc && vsce package",
"build:all": "turbo build", "vscode:prepublish": "node esbuild.config.js",
"vscode:prepublish": "webpack --mode production",
"compile": "tsc -p ./", "compile": "tsc -p ./",
"watch": "tsc -watch -p ./", "watch": "tsc -watch -p ./",
"pretest": "npm run compile && npm run lint", "pretest": "yarn build",
"lint": "eslint src --ext ts", "lint": "eslint src --ext ts",
"test": "node ./out/test/runTest.js", "test": "node ./dist/test/e2e/runTest.js",
"prepare:ocr": "webpack --config webpack/webpack.tesseract.js", "prepare:ocr": "rollup -c rollup.tesseract.js --bundleConfigAsCjs",
"build:task-loop": "npx vite build --config webpack/vite.config.task-loop.mjs && node renderer/scripts/task-loop.build.mjs" "build:task-loop": "npx vite build --config renderer/vite.config.task-loop.mjs && node renderer/scripts/task-loop.build.mjs"
}, },
"dependencies": { "dependencies": {
"@modelcontextprotocol/sdk": "^1.12.1", "@modelcontextprotocol/sdk": "^1.12.1",
"@seald-io/nedb": "^4.1.1", "@seald-io/nedb": "^4.1.1",
"@types/glob": "^7.2.0",
"@types/node-fetch": "^2.6.4",
"abort-controller": "^3.0.0",
"agentkeepalive": "^4.2.1",
"axios": "^1.9.0", "axios": "^1.9.0",
"bson": "^6.8.0", "bson": "^6.8.0",
"form-data-encoder": "^1.7.2",
"formdata-node": "^4.3.2",
"glob": "^7.2.3",
"https-proxy-agent": "^7.0.6", "https-proxy-agent": "^7.0.6",
"mocha": "^10.8.2",
"openai": "^5.0.1", "openai": "^5.0.1",
"pako": "^2.1.0", "pako": "^2.1.0",
"pkce-challenge": "^5.0.0",
"tesseract.js": "^6.0.1", "tesseract.js": "^6.0.1",
"tslib": "^2.8.1",
"uuid": "^11.1.0", "uuid": "^11.1.0",
"ws": "^8.18.1" "ws": "^8.18.1"
}, },
"devDependencies": { "devDependencies": {
"@rollup/plugin-babel": "^6.0.4",
"@rollup/plugin-commonjs": "^28.0.5",
"@rollup/plugin-inject": "^5.0.5", "@rollup/plugin-inject": "^5.0.5",
"@rollup/plugin-json": "^6.1.0",
"@rollup/plugin-node-resolve": "^16.0.1",
"@rollup/plugin-typescript": "^12.1.2",
"@types/node": "^22.15.29", "@types/node": "^22.15.29",
"@types/pako": "^2.0.3", "@types/pako": "^2.0.3",
"@types/showdown": "^2.0.0", "@types/showdown": "^2.0.0",
"@types/sinon": "^17.0.4",
"@types/vscode": "^1.72.0", "@types/vscode": "^1.72.0",
"@vscode/test-cli": "^0.0.11",
"@vscode/test-electron": "^2.5.2",
"@vscode/vsce": "^3.5.0",
"copy-webpack-plugin": "^13.0.0", "copy-webpack-plugin": "^13.0.0",
"esbuild": "^0.25.5",
"fork-ts-checker-webpack-plugin": "^9.1.0", "fork-ts-checker-webpack-plugin": "^9.1.0",
"null-loader": "^4.0.1", "null-loader": "^4.0.1",
"rollup": "^4.43.0",
"rollup-plugin-copy": "^3.5.0",
"rollup-plugin-visualizer": "^6.0.1", "rollup-plugin-visualizer": "^6.0.1",
"sinon": "^21.0.0",
"ts-loader": "^9.5.1", "ts-loader": "^9.5.1",
"turbo": "^2.5.3", "turbo": "^2.5.3",
"typescript": "^5.4.2", "typescript": "^5.4.2",
@ -267,5 +290,5 @@
"webpack": "^5.99.5", "webpack": "^5.99.5",
"webpack-cli": "^5.1.4" "webpack-cli": "^5.1.4"
}, },
"packageManager": "npm@10.0.0" "packageManager": "yarn@1.22.22"
} }

View File

@ -33,7 +33,8 @@
"uuid": "^11.1.0", "uuid": "^11.1.0",
"vue": "^3.5.13", "vue": "^3.5.13",
"vue-i18n": "^11.1.0", "vue-i18n": "^11.1.0",
"vue-router": "^4.5.0" "vue-router": "^4.5.0",
"xml2js": "^0.6.2"
}, },
"devDependencies": { "devDependencies": {
"@babel/core": "^7.27.1", "@babel/core": "^7.27.1",
@ -47,6 +48,7 @@
"@types/markdown-it": "^14.1.2", "@types/markdown-it": "^14.1.2",
"@types/node": "^22.14.0", "@types/node": "^22.14.0",
"@types/prismjs": "^1.26.5", "@types/prismjs": "^1.26.5",
"@types/xml2js": "^0.4.14",
"@vitejs/plugin-vue": "^5.2.3", "@vitejs/plugin-vue": "^5.2.3",
"@vue/babel-plugin-jsx": "^1.4.0", "@vue/babel-plugin-jsx": "^1.4.0",
"@vue/devtools-core": "^7.7.6", "@vue/devtools-core": "^7.7.6",
@ -60,5 +62,7 @@
"vite": "^6.2.4", "vite": "^6.2.4",
"vite-plugin-vue-devtools": "^7.7.2", "vite-plugin-vue-devtools": "^7.7.2",
"vue-tsc": "^2.2.8" "vue-tsc": "^2.2.8"
} },
"main": "index.js",
"license": "MIT"
} }

View File

@ -1,8 +1,8 @@
@font-face { @font-face {
font-family: "iconfont"; /* Project id 4870215 */ font-family: "iconfont"; /* Project id 4870215 */
src: url('iconfont.woff2?t=1749572305505') format('woff2'), src: url('iconfont.woff2?t=1750532923458') format('woff2'),
url('iconfont.woff?t=1749572305505') format('woff'), url('iconfont.woff?t=1750532923458') format('woff'),
url('iconfont.ttf?t=1749572305505') format('truetype'); url('iconfont.ttf?t=1750532923458') format('truetype');
} }
.iconfont { .iconfont {
@ -13,6 +13,18 @@
-moz-osx-font-smoothing: grayscale; -moz-osx-font-smoothing: grayscale;
} }
.icon-deploy:before {
content: "\e614";
}
.icon-suffix-xml:before {
content: "\e653";
}
.icon-MCP:before {
content: "\e63c";
}
.icon-wendang:before { .icon-wendang:before {
content: "\e61b"; content: "\e61b";
} }

Binary file not shown.

View File

@ -0,0 +1,3 @@
# OpenRouter Icon Placeholder
# This would normally be an actual .ico file
# For now, using a placeholder that follows the naming convention

View File

@ -228,3 +228,4 @@ a {
.ͼo .cm-gutters { .ͼo .cm-gutters {
background-color: transparent !important; background-color: transparent !important;
} }

View File

@ -3,13 +3,13 @@ import * as fs from 'node:fs';
const targetFile = './openmcp-sdk/task-loop.js'; const targetFile = './openmcp-sdk/task-loop.js';
if (fs.existsSync(targetFile)) { if (fs.existsSync(targetFile)) {
let content = fs.readFileSync(targetFile, 'utf8'); let content = fs.readFileSync(targetFile, 'utf-8');
// Replace element-plus with ./tools.js // Replace element-plus with ./tools.js
content = content.replace(/'element-plus'/g, "'./tools.js'"); content = content.replace(/'element-plus'/g, "'./tools.mjs'");
content = content.replace(/"element-plus"/g, "\"./tools.js\""); content = content.replace(/"element-plus"/g, "\"./tools.mjs\"");
content = content.replace(/const chalk = require\("chalk"\);/g, 'const chalk = require("chalk").default;'); // content = content.replace(/const chalk = require\("chalk"\);/g, 'const chalk = require("chalk").default;');
// Replace define_window_default$number.performance with performance // Replace define_window_default$number.performance with performance
content = content.replace(/define_window_default\$\d+\.performance/g, 'performance'); content = content.replace(/define_window_default\$\d+\.performance/g, 'performance');

View File

@ -39,22 +39,22 @@ export class MessageBridge {
switch (platform) { switch (platform) {
case 'vscode': case 'vscode':
this.setupVsCodeListener(); this.setupVsCodeListener();
pinkLog('当前模式: vscode'); pinkLog('current platform: vscode');
break; break;
case 'electron': case 'electron':
this.setupElectronListener(); this.setupElectronListener();
pinkLog('当前模式: electron'); pinkLog('current platform: electron');
break; break;
case 'nodejs': case 'nodejs':
this.setupNodejsListener(); this.setupNodejsListener();
pinkLog('当前模式: nodejs'); pinkLog('current platform: nodejs');
break; break;
case 'web': case 'web':
this.setupWebSocket(); this.setupWebSocket();
pinkLog('当前模式: web'); pinkLog('current platform: web');
break; break;
} }
} }

View File

@ -1,5 +1,5 @@
import type { ToolCallContent, ToolItem } from "@/hook/type"; import type { InputSchema, ToolCallContent, ToolItem } from "@/hook/type";
import { type Ref, ref } from "vue"; import type { Ref } from "vue";
import type { OpenAI } from 'openai'; import type { OpenAI } from 'openai';
type ChatCompletionChunk = OpenAI.Chat.Completions.ChatCompletionChunk; type ChatCompletionChunk = OpenAI.Chat.Completions.ChatCompletionChunk;
@ -16,6 +16,7 @@ export enum MessageState {
Success = 'success', Success = 'success',
ParseJsonError = 'parse json error', ParseJsonError = 'parse json error',
NoToolFunction = 'no tool function', NoToolFunction = 'no tool function',
InvalidXml = 'invalid xml',
} }
export interface IExtraInfo { export interface IExtraInfo {
@ -23,6 +24,7 @@ export interface IExtraInfo {
state: MessageState, state: MessageState,
serverName: string, serverName: string,
usage?: ChatCompletionChunk['usage']; usage?: ChatCompletionChunk['usage'];
enableXmlWrapper: boolean;
[key: string]: any; [key: string]: any;
} }
@ -52,7 +54,7 @@ export interface EnableToolItem {
name: string; name: string;
description: string; description: string;
enabled: boolean; enabled: boolean;
inputSchema: any; inputSchema: InputSchema;
} }
export interface ChatSetting { export interface ChatSetting {
@ -63,6 +65,7 @@ export interface ChatSetting {
enableWebSearch: boolean enableWebSearch: boolean
contextLength: number contextLength: number
parallelToolCalls: boolean parallelToolCalls: boolean
enableXmlWrapper: boolean
} }
export interface ChatStorage { export interface ChatStorage {
@ -92,7 +95,7 @@ export type RichTextItem = PromptTextItem | ResourceTextItem | TextItem;
export interface ICommonRenderMessage { export interface ICommonRenderMessage {
role: 'user' | 'assistant/content'; role: 'user' | 'assistant/content';
content: string; content: string;
showJson?: Ref<boolean>; showJson?: any;
extraInfo: IExtraInfo; extraInfo: IExtraInfo;
} }
@ -101,13 +104,13 @@ export interface IToolRenderMessage {
content: string; content: string;
toolResults: ToolCallContent[][]; toolResults: ToolCallContent[][];
tool_calls: ToolCall[]; tool_calls: ToolCall[];
showJson?: Ref<boolean>; showJson?: any;
extraInfo: IExtraInfo; extraInfo: IExtraInfo;
} }
export type IRenderMessage = ICommonRenderMessage | IToolRenderMessage; export type IRenderMessage = ICommonRenderMessage | IToolRenderMessage;
export function getToolSchema(enableTools: EnableToolItem[]) { export function getToolSchema(enableTools: EnableToolItem[]): any[] {
const toolsSchema = []; const toolsSchema = [];
for (let i = 0; i < enableTools.length; i++) { for (let i = 0; i < enableTools.length; i++) {
const enableTool = enableTools[i]; const enableTool = enableTools[i];

View File

@ -101,8 +101,6 @@ function handleSend(newMessage?: string) {
loop.bindStreaming(streamingContent, streamingToolCalls); loop.bindStreaming(streamingContent, streamingToolCalls);
loop.registerOnError((error) => { loop.registerOnError((error) => {
console.log('error.msg');
console.log(error.msg);
const errorMessage = clearErrorMessage(error.msg); const errorMessage = clearErrorMessage(error.msg);
ElMessage.error(errorMessage); ElMessage.error(errorMessage);
@ -114,7 +112,8 @@ function handleSend(newMessage?: string) {
extraInfo: { extraInfo: {
created: Date.now(), created: Date.now(),
state: error.state, state: error.state,
serverName: llms[llmManager.currentModelIndex].id || 'unknown' serverName: llms[llmManager.currentModelIndex].id || 'unknown',
enableXmlWrapper: false
} }
}); });
} }

View File

@ -1,6 +1,6 @@
<template> <template>
<el-tooltip :content="t('context-length')" placement="top"> <el-tooltip :content="t('context-length')" placement="top" effect="light">
<div class="setting-button" @click="showContextLengthDialog = true"> <div class="setting-button width-30" @click="showContextLengthDialog = true">
<span class="iconfont icon-length"></span> <span class="iconfont icon-length"></span>
<span class="value-badge">{{ tabStorage.settings.contextLength }}</span> <span class="value-badge">{{ tabStorage.settings.contextLength }}</span>
</div> </div>
@ -10,7 +10,7 @@
<el-dialog v-model="showContextLengthDialog" :title="t('context-length') + ' ' + tabStorage.settings.contextLength" <el-dialog v-model="showContextLengthDialog" :title="t('context-length') + ' ' + tabStorage.settings.contextLength"
width="400px"> width="400px">
<div class="slider-container"> <div class="slider-container">
<el-slider v-model="tabStorage.settings.contextLength" :min="1" :max="99" :step="1" /> <el-slider v-model="tabStorage.settings.contextLength" :min="1" :max="500" :step="1" />
<div class="slider-tips"> <div class="slider-tips">
<span> 1: {{ t('single-dialog') }}</span> <span> 1: {{ t('single-dialog') }}</span>
<span> >1: {{ t('multi-dialog') }}</span> <span> >1: {{ t('multi-dialog') }}</span>
@ -39,4 +39,8 @@ const showContextLengthDialog = ref(false);
.icon-length { .icon-length {
font-size: 16px; font-size: 16px;
} }
.width-30 {
width: 30px;
}
</style> </style>

View File

@ -0,0 +1,208 @@
<template>
<el-tooltip :content="'导出 mcpconfig'" placement="top" effect="light">
<div class="setting-button" @click="toggleDialog">
<span class="iconfont icon-deploy">
</span>
</div>
</el-tooltip>
<el-dialog v-model="showDialog" width="fit-content">
<template #header>
<div>
<div class="export-file-input">
<span>{{ t('export-filename') }}</span>
<el-input
v-model="exportFileName"
style="max-width: 300px"
>
<template #append>.json</template>
</el-input>
<span class="how-to-use"
@click="gotoHowtoUse"
>
<span class="iconfont icon-info"></span>
<span>{{ t('how-to-use') }}</span>
</span>
</div>
</div>
</template>
<div class="tools-dialog-container">
<el-scrollbar height="400px" class="tools-list">
<div v-html="exportJson">
</div>
</el-scrollbar>
<!--
<el-scrollbar height="400px" class="tools-list">
<div v-html="exampleCode"></div>
</el-scrollbar> -->
</div>
<template #footer>
<el-button type="primary" @click="copyCode">{{ t('copy') }}</el-button>
<el-button type="primary" @click="exportCode">{{ t('export') }}</el-button>
</template>
</el-dialog>
</template>
<script setup lang="ts">
import { ref, computed, inject, onMounted, watch } from 'vue';
import { useI18n } from 'vue-i18n';
import { type ChatStorage, type EnableToolItem, getToolSchema } from '../chat';
import { markdownToHtml } from '@/components/main-panel/chat/markdown/markdown';
import { llmManager, llms } from '@/views/setting/llm';
import { mcpClientAdapter } from '@/views/connect/core';
import { ElMessage } from 'element-plus';
import { useMessageBridge } from '@/api/message-bridge';
const { t, locale } = useI18n();
const showDialog = ref(false);
const exportJson = ref('');
const exportFileName = ref('mcpconfig');
// toggleDialog
const toggleDialog = () => {
showDialog.value = true;
};
const generateExportData = computed(() => {
const currentLLM = llms[llmManager.currentModelIndex];
const mcpServers = {} as any;
for (const client of mcpClientAdapter.clients) {
const option = client.connectOption;
const type = option.connectionType;
if (type === 'STDIO') {
mcpServers[client.name] = {
command: option.command,
args: option.args,
description: "",
}
} else if (type === 'SSE') {
mcpServers[client.name] = {
type: 'sse',
url: option.url,
description: "",
}
} else {
mcpServers[client.name] = {
type: 'streamable_http',
url: option.url,
description: "",
}
}
}
const mcpconfig = {
version: "0.0.1",
namespace: "openmcp",
mcpServers,
defaultLLM: {
baseURL: currentLLM.baseUrl,
apiToken: currentLLM.userToken,
model: currentLLM.userModel
}
};
return JSON.stringify(mcpconfig, null, 2);
});
const innerMarkdownHtml = (code: string) => {
const rawCode = markdownToHtml(code);
const doc = new DOMParser().parseFromString(rawCode, 'text/html');
const pres = doc.querySelectorAll('pre');
if (pres.length < 2) {
return '';
}
const inner = pres[1].outerHTML;
return inner;
}
const exampleCode = computed(() => {
return innerMarkdownHtml(
'```typescript\n' +
`import { OmAgent } from 'openmcp-sdk/service/sdk';
const agent = new OmAgent();
agent.loadMcpConfig('/path/to/${exportFileName.value}.json');
const prompt = await agent.getPrompt('hacknews', { topn: '5' });
const res = await agent.ainvoke({ messages: prompt });
console.log('⚙️ Agent Response', res);
` +
'\n```'
);
});
const copyCode = async () => {
try {
await navigator.clipboard.writeText(generateExportData.value);
ElMessage.success(t('copy-success'));
} catch (error) {
ElMessage.error(t('copy-fail'));
}
}
const exportCode = async () => {
const bridge = useMessageBridge();
bridge.postMessage({
command: 'vscode/export-file',
data: {
filename: exportFileName.value,
content: generateExportData.value
}
})
}
const gotoHowtoUse = () => {
if (locale.value === 'zh') {
window.open('https://kirigaya.cn/openmcp/zh/sdk-tutorial/#%E4%BD%BF%E7%94%A8');
} else if (locale.value === 'ja') {
window.open('https://kirigaya.cn/openmcp/ja/sdk-tutorial/#%E4%BD%BF%E7%94%A8%E6%96%B9%E6%B3%95');
} else {
window.open('https://kirigaya.cn/openmcp/sdk-tutorial/#usage');
}
}
onMounted(async () => {
exportJson.value = innerMarkdownHtml(
'```json\n' + generateExportData.value + '\n```'
);
});
</script>
<style scoped>
.export-file-input {
display: flex;
gap: 10px;
align-items: center;
}
.tools-list {
border-radius: .5em;
border: 1px solid var(--main-color);
padding: 5px;
}
.how-to-use {
margin-left: 10px;
font-size: 15px;
cursor: pointer;
background-color: var(--main-color);
color: #1e1e1e;
padding: 3px 5px;
border-radius: .3em;
}
</style>

View File

@ -1,5 +1,5 @@
<template> <template>
<el-tooltip :content="t('choose-model')" placement="top"> <el-tooltip :content="t('choose-model')" placement="top" effect="light">
<div class="setting-button" @click="showModelDialog = true"> <div class="setting-button" @click="showModelDialog = true">
<span class="iconfont icon-model"> <span class="iconfont icon-model">
{{ currentServerName }}/{{ currentModelName }} {{ currentServerName }}/{{ currentModelName }}

View File

@ -1,5 +1,5 @@
<template> <template>
<el-tooltip :content="t('parallel-tool-calls')" placement="top"> <el-tooltip :content="t('parallel-tool-calls')" placement="top" effect="light">
<div class="setting-button" :class="{ 'active': tabStorage.settings.parallelToolCalls }" size="small" <div class="setting-button" :class="{ 'active': tabStorage.settings.parallelToolCalls }" size="small"
@click="toggle"> @click="toggle">
<span class="iconfont icon-parallel"></span> <span class="iconfont icon-parallel"></span>

View File

@ -1,5 +1,5 @@
<template> <template>
<el-tooltip :content="t('prompts')" placement="top"> <el-tooltip :content="t('prompts')" placement="top" effect="light">
<div class="setting-button" @click="showChoosePrompt = true; saveCursorPosition();"> <div class="setting-button" @click="showChoosePrompt = true; saveCursorPosition();">
<span class="iconfont icon-chat"></span> <span class="iconfont icon-chat"></span>
</div> </div>

View File

@ -1,5 +1,5 @@
<template> <template>
<el-tooltip :content="t('resources')" placement="top"> <el-tooltip :content="t('resources')" placement="top" effect="light">
<div class="setting-button" @click="showChooseResource = true; saveCursorPosition();"> <div class="setting-button" @click="showChooseResource = true; saveCursorPosition();">
<span class="iconfont icon-file"></span> <span class="iconfont icon-file"></span>
</div> </div>

View File

@ -8,6 +8,8 @@
<ParallelToolCalls /> <ParallelToolCalls />
<Temperature /> <Temperature />
<ContextLength /> <ContextLength />
<XmlWrapper />
<Export />
</div> </div>
</template> </template>
@ -25,6 +27,8 @@ import Resource from './resource.vue';
import ParallelToolCalls from './parallel-tool-calls.vue'; import ParallelToolCalls from './parallel-tool-calls.vue';
import Temperature from './temperature.vue'; import Temperature from './temperature.vue';
import ContextLength from './context-length.vue'; import ContextLength from './context-length.vue';
import XmlWrapper from './xml-wrapper.vue';
import Export from './export.vue';
const props = defineProps({ const props = defineProps({
modelValue: { modelValue: {
@ -58,8 +62,9 @@ if (!tabStorage.settings) {
enableTools: [], enableTools: [],
enableWebSearch: false, enableWebSearch: false,
temperature: 0.6, temperature: 0.6,
contextLength: 20, contextLength: 100,
systemPrompt: '', systemPrompt: '',
enableXmlWrapper: false,
parallelToolCalls: true parallelToolCalls: true
} as ChatSetting; } as ChatSetting;
} }

View File

@ -1,5 +1,5 @@
<template> <template>
<el-tooltip :content="t('system-prompt')" placement="top"> <el-tooltip :content="t('system-prompt')" placement="top" effect="light">
<div class="setting-button" :class="{ 'active': hasSystemPrompt }" size="small" <div class="setting-button" :class="{ 'active': hasSystemPrompt }" size="small"
@click="showSystemPromptDialog = true"> @click="showSystemPromptDialog = true">
<span class="iconfont icon-prompt"></span> <span class="iconfont icon-prompt"></span>

View File

@ -1,6 +1,6 @@
<template> <template>
<el-tooltip :content="t('temperature-parameter')" placement="top"> <el-tooltip :content="t('temperature-parameter')" placement="top" effect="light">
<div class="setting-button" @click="showTemperatureSlider = true"> <div class="setting-button width-30" @click="showTemperatureSlider = true">
<span class="iconfont icon-temperature"></span> <span class="iconfont icon-temperature"></span>
<span class="value-badge">{{ tabStorage.settings.temperature.toFixed(1) }}</span> <span class="value-badge">{{ tabStorage.settings.temperature.toFixed(1) }}</span>
</div> </div>
@ -34,9 +34,13 @@ const showTemperatureSlider = ref(false);
</script> </script>
<style> <style scoped>
.icon-temperature { .icon-temperature {
font-size: 18px; font-size: 16px;
}
.width-30 {
width: 30px;
} }
</style> </style>

View File

@ -1,5 +1,5 @@
<template> <template>
<el-tooltip :content="t('tool-use')" placement="top"> <el-tooltip :content="t('tool-use')" placement="top" effect="light">
<div class="setting-button" :class="{ 'active': availableToolsNum > 0 }" size="small" @click="toggleTools"> <div class="setting-button" :class="{ 'active': availableToolsNum > 0 }" size="small" @click="toggleTools">
<span class="iconfont icon-tool badge-outer"> <span class="iconfont icon-tool badge-outer">
<span class="badge-inner"> <span class="badge-inner">
@ -10,7 +10,19 @@
</el-tooltip> </el-tooltip>
<el-dialog v-model="showToolsDialog" title="工具管理" width="800px"> <el-dialog v-model="showToolsDialog" width="800px">
<template #header>
<div>
<span>{{ t('tool-manage') }}</span>
<el-tooltip :content="t('enable-xml-wrapper')" placement="top" effect="light">
<span class="xml-tag" :class="{
'active': tabStorage.settings.enableXmlWrapper
}" @click="tabStorage.settings.enableXmlWrapper = !tabStorage.settings.enableXmlWrapper">xml</span>
</el-tooltip>
</div>
</template>
<div class="tools-dialog-container"> <div class="tools-dialog-container">
<el-scrollbar height="400px" class="tools-list"> <el-scrollbar height="400px" class="tools-list">
<div v-for="(tool, index) in tabStorage.settings.enableTools" :key="index" class="tool-item"> <div v-for="(tool, index) in tabStorage.settings.enableTools" :key="index" class="tool-item">
@ -23,23 +35,27 @@
</el-scrollbar> </el-scrollbar>
<el-scrollbar height="400px" class="schema-viewer"> <el-scrollbar height="400px" class="schema-viewer">
<div v-html="activeToolsSchemaHTML"></div> <!-- 如果激活 xml 指令包裹则展示对应的 prompt -->
<div v-if="tabStorage.settings.enableXmlWrapper" v-html="activeToolsXmlPrompt" />
<!-- 如果是普通模式则展示普通的工具列表 -->
<div v-else v-html="activeToolsSchemaHTML" />
</el-scrollbar> </el-scrollbar>
</div> </div>
<template #footer> <template #footer>
<el-button type="primary" @click="enableAllTools">激活所有工具</el-button> <el-button type="primary" @click="enableAllTools">{{ t('enable-all-tools') }}</el-button>
<el-button type="danger" @click="disableAllTools">禁用所有工具</el-button> <el-button type="danger" @click="disableAllTools">{{ t('disable-all-tools') }}</el-button>
<el-button type="primary" @click="showToolsDialog = false">{{ t("cancel") }}</el-button> <el-button type="primary" @click="showToolsDialog = false">{{ t("cancel") }}</el-button>
</template> </template>
</el-dialog> </el-dialog>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { ref, computed, inject, onMounted } from 'vue'; import { ref, computed, inject, onMounted, watch } from 'vue';
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
import { type ChatStorage, type EnableToolItem, getToolSchema } from '../chat'; import { type ChatStorage, type EnableToolItem, getToolSchema } from '../chat';
import { markdownToHtml } from '@/components/main-panel/chat/markdown/markdown'; import { markdownToHtml } from '@/components/main-panel/chat/markdown/markdown';
import { mcpClientAdapter } from '@/views/connect/core'; import { mcpClientAdapter } from '@/views/connect/core';
import { toolSchemaToPromptDescription } from '../../core/xml-wrapper';
const { t } = useI18n(); const { t } = useI18n();
@ -48,26 +64,34 @@ const tabStorage = inject('tabStorage') as ChatStorage;
const showToolsDialog = ref(false); const showToolsDialog = ref(false);
const availableToolsNum = computed(() => { const availableToolsNum = computed(() => {
return tabStorage.settings.enableTools.filter(tool => tool.enabled).length; return tabStorage.settings.enableTools.filter(tool => tool.enabled).length;
}); });
// toggleTools // toggleTools
const toggleTools = () => { const toggleTools = () => {
showToolsDialog.value = true; showToolsDialog.value = true;
}; };
const activeToolsSchemaHTML = computed(() => {
const toolsSchema = getToolSchema(tabStorage.settings.enableTools);
const jsonString = JSON.stringify(toolsSchema, null, 2);
return markdownToHtml( const activeToolsSchemaHTML = computed(() => {
"```json\n" + jsonString + "\n```" const toolsSchema = getToolSchema(tabStorage.settings.enableTools);
); const jsonString = JSON.stringify(toolsSchema, null, 2);
return markdownToHtml(
"```json\n" + jsonString + "\n```"
);
});
const activeToolsXmlPrompt = computed(() => {
const prompt = toolSchemaToPromptDescription(tabStorage.settings.enableTools);
return markdownToHtml(
"```markdown\n" + prompt + "\n```"
);
}); });
// - // -
const enableAllTools = () => { const enableAllTools = () => {
tabStorage.settings.enableTools.forEach(tool => { tabStorage.settings.enableTools.forEach(tool => {
tool.enabled = true; tool.enabled = true;
}); });
}; };
@ -79,34 +103,64 @@ const disableAllTools = () => {
}); });
}; };
onMounted(async () => { //
const updateToolsList = async () => {
// tool tabStorage.settings.enableTools // tool tabStorage.settings.enableTools
// enable // enable
const disableToolNames = new Set<string>( const disableToolNames = new Set<string>(
tabStorage.settings.enableTools tabStorage.settings.enableTools
.filter(tool => !tool.enabled) .filter(tool => !tool.enabled)
.map(tool => tool.name) .map(tool => tool.name)
); );
const newTools: EnableToolItem[] = []; const newTools: EnableToolItem[] = [];
for (const client of mcpClientAdapter.clients) { for (const client of mcpClientAdapter.clients) {
const tools = await client.getTools(); const tools = await client.getTools({ cache: false });
for (const tool of tools.values()) { if (tools) {
const enabled = !disableToolNames.has(tool.name); for (const tool of tools.values()) {
const enabled = !disableToolNames.has(tool.name);
newTools.push({ newTools.push({
name: tool.name, name: tool.name,
description: tool.description, description: tool.description,
inputSchema: tool.inputSchema, inputSchema: tool.inputSchema,
enabled enabled
}); });
}
} }
} }
tabStorage.settings.enableTools = newTools; tabStorage.settings.enableTools = newTools;
}
onMounted(async () => {
await updateToolsList();
watch(() => mcpClientAdapter.refreshSignal.value, async () => {
await updateToolsList();
});
}); });
</script> </script>
<style></style> <style scoped>
.xml-tag {
margin-left: 10px;
border-radius: .5em;
padding: 2px 5px;
font-size: 12px;
font-weight: 900;
color: black;
background-color: var(--main-color);
opacity: 0.3;
transition: var(--animation-3s);
cursor: pointer;
}
.xml-tag.active {
opacity: 1;
transition: var(--animation-3s);
}
</style>

View File

@ -1,5 +1,5 @@
<template> <template>
<el-tooltip :content="t('websearch')" placement="top"> <el-tooltip :content="t('websearch')" placement="top" effect="light">
<div class="setting-button" :class="{ 'active': tabStorage.settings.enableWebSearch }" size="small" <div class="setting-button" :class="{ 'active': tabStorage.settings.enableWebSearch }" size="small"
@click="toggleWebSearch"> @click="toggleWebSearch">
<span class="iconfont icon-web"></span> <span class="iconfont icon-web"></span>

View File

@ -0,0 +1,25 @@
<template>
<el-tooltip :content="t('enable-xml-wrapper')" placement="top" effect="light">
<div class="setting-button" :class="{ 'active': tabStorage.settings.enableXmlWrapper }" size="small"
@click="toggle">
<span class="iconfont icon-suffix-xml"></span>
</div>
</el-tooltip>
</template>
<script setup lang="ts">
import { defineComponent, inject } from 'vue';
import type { ChatStorage } from '../chat';
import { useI18n } from 'vue-i18n';
const { t } = useI18n();
const tabStorage = inject('tabStorage') as ChatStorage;
const toggle = () => {
tabStorage.settings.enableXmlWrapper = !tabStorage.settings.enableXmlWrapper;
};
</script>
<style></style>

View File

@ -1,5 +1,5 @@
<template> <template>
<el-tooltip :content="props.messages[0].content.text" placement="top"> <el-tooltip :content="props.messages[0].content.text" placement="top" effect="light">
<span class="chat-prompt-item" contenteditable="false"> <span class="chat-prompt-item" contenteditable="false">
<span class="iconfont icon-chat"></span> <span class="iconfont icon-chat"></span>
<span class="real-text">{{ props.messages[0].content.text }}</span> <span class="real-text">{{ props.messages[0].content.text }}</span>
@ -22,9 +22,6 @@ const props = defineProps({
<style> <style>
.chat-prompt-item { .chat-prompt-item {
max-width: 80px; max-width: 80px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
display: inline-flex; display: inline-flex;
border-radius: .3em; border-radius: .3em;
align-items: center; align-items: center;
@ -34,9 +31,17 @@ const props = defineProps({
font-size: 12px; font-size: 12px;
margin-left: 3px; margin-left: 3px;
margin-right: 3px; margin-right: 3px;
user-select: none;
} }
.chat-prompt-item .iconfont { .chat-prompt-item .iconfont {
margin-right: 4px; margin-right: 4px;
} }
.chat-prompt-item .real-text {
max-width: 60px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
</style> </style>

View File

@ -1,5 +1,5 @@
<template> <template>
<el-tooltip placement="top"> <el-tooltip placement="top" effect="light">
<template #content> <template #content>
<div class="resource-chat-item-tooltip"> <div class="resource-chat-item-tooltip">
<div v-for="(item, index) of toolRenderItems" :key="index"> <div v-for="(item, index) of toolRenderItems" :key="index">

View File

@ -2,7 +2,14 @@ import type { ToolCallContent, ToolCallResponse } from "@/hook/type";
import { MessageState, type ToolCall } from "../chat-box/chat"; import { MessageState, type ToolCall } from "../chat-box/chat";
import { mcpClientAdapter } from "@/views/connect/core"; import { mcpClientAdapter } from "@/views/connect/core";
import type { BasicLlmDescription } from "@/views/setting/llm"; import type { BasicLlmDescription } from "@/views/setting/llm";
import { redLog } from "@/views/setting/util"; import type OpenAI from "openai";
export interface TaskLoopChatOption {
id?: string
proxyServer?: string
enableXmlWrapper?: boolean
}
export type ChatCompletionCreateParamsBase = OpenAI.Chat.Completions.ChatCompletionCreateParams & TaskLoopChatOption;
export interface ToolCallResult { export interface ToolCallResult {
state: MessageState; state: MessageState;
@ -60,7 +67,7 @@ function deserializeToolCallResponse(toolArgs: string) {
} }
} }
function handleToolResponse(toolResponse: ToolCallResponse) { export function handleToolResponse(toolResponse: ToolCallResponse) {
if (typeof toolResponse === 'string') { if (typeof toolResponse === 'string') {
return { return {
@ -98,36 +105,67 @@ function parseErrorObject(error: any): string {
} }
} }
function grokIndexAdapter(toolCall: ToolCall, callId2Index: Map<string, number>): IToolCallIndex {
/**
* @description ID映射为索引
* @param toolCall
* @param callId2Index ID到索引的映射表
* @returns
*/
export function idAsIndexAdapter(toolCall: ToolCall | string, callId2Index: Map<string, number>): IToolCallIndex {
// grok 采用 id 作为 index需要将 id 映射到 zero-based 的 index // grok 采用 id 作为 index需要将 id 映射到 zero-based 的 index
if (!toolCall.id) { const id = typeof toolCall === 'string' ? toolCall : toolCall.id;
if (!id) {
return 0; return 0;
} }
if (!callId2Index.has(toolCall.id)) { if (!callId2Index.has(id)) {
callId2Index.set(toolCall.id, callId2Index.size); callId2Index.set(id, callId2Index.size);
} }
return callId2Index.get(toolCall.id)!; return callId2Index.get(id)!;
} }
function geminiIndexAdapter(toolCall: ToolCall): IToolCallIndex {
/**
* @description
* @param toolCall
* @returns 0
*/
export function singleCallIndexAdapter(toolCall: ToolCall): IToolCallIndex {
// TODO: 等待后续支持 // TODO: 等待后续支持
return 0; return 0;
} }
function defaultIndexAdapter(toolCall: ToolCall): IToolCallIndex { /**
* @description
* @param toolCall
* @returns
*/
export function defaultIndexAdapter(toolCall: ToolCall): IToolCallIndex {
return toolCall.index || 0; return toolCall.index || 0;
} }
export function getToolCallIndexAdapter(llm: BasicLlmDescription) { export function getToolCallIndexAdapter(llm: BasicLlmDescription, chatData: ChatCompletionCreateParamsBase) {
// 如果是 xml 模式,那么 index adapter 必须是 idAsIndexAdapter
if (chatData.enableXmlWrapper) {
const callId2Index = new Map<string, number>();
return (toolCall: ToolCall) => idAsIndexAdapter(toolCall, callId2Index);
}
if (llm.userModel.startsWith('gemini')) { if (llm.userModel.startsWith('gemini')) {
return geminiIndexAdapter; return singleCallIndexAdapter;
} }
if (llm.userModel.startsWith('grok')) { if (llm.userModel.startsWith('grok')) {
const callId2Index = new Map<string, number>(); const callId2Index = new Map<string, number>();
return (toolCall: ToolCall) => grokIndexAdapter(toolCall, callId2Index); return (toolCall: ToolCall) => idAsIndexAdapter(toolCall, callId2Index);
} }
return defaultIndexAdapter; return defaultIndexAdapter;
} }
export function getIdAsIndexAdapter() {
const callId2Index = new Map<string, number>();
return (toolCall: ToolCall) => idAsIndexAdapter(toolCall, callId2Index);
}

View File

@ -1,6 +1,6 @@
/* eslint-disable */ /* eslint-disable */
import { ref, type Ref } from "vue"; import { ref, type Ref } from "vue";
import { type ToolCall, type ChatStorage, getToolSchema, MessageState } from "../chat-box/chat"; import { type ToolCall, type ChatStorage, getToolSchema, MessageState, type ChatMessage, type ChatSetting, type EnableToolItem } from "../chat-box/chat";
import { useMessageBridge, MessageBridge, createMessageBridge } from "@/api/message-bridge"; import { useMessageBridge, MessageBridge, createMessageBridge } from "@/api/message-bridge";
import type { OpenAI } from 'openai'; import type { OpenAI } from 'openai';
import { llmManager, llms, type BasicLlmDescription } from "@/views/setting/llm"; import { llmManager, llms, type BasicLlmDescription } from "@/views/setting/llm";
@ -13,9 +13,15 @@ import { mcpSetting } from "@/hook/mcp";
import { mcpClientAdapter } from "@/views/connect/core"; import { mcpClientAdapter } from "@/views/connect/core";
import type { ToolItem } from "@/hook/type"; import type { ToolItem } from "@/hook/type";
import chalk from 'chalk'; import chalk from 'chalk';
import { getXmlWrapperPrompt, getToolCallFromXmlString, getXmlsFromString, handleXmlWrapperToolcall, toNormaliseToolcall, getXmlResultPrompt } from "./xml-wrapper";
export type ChatCompletionChunk = OpenAI.Chat.Completions.ChatCompletionChunk; export type ChatCompletionChunk = OpenAI.Chat.Completions.ChatCompletionChunk;
export type ChatCompletionCreateParamsBase = OpenAI.Chat.Completions.ChatCompletionCreateParams & { id?: string, proxyServer?: string }; export interface TaskLoopChatOption {
id?: string
proxyServer?: string
enableXmlWrapper?: boolean
}
export type ChatCompletionCreateParamsBase = OpenAI.Chat.Completions.ChatCompletionCreateParams & TaskLoopChatOption;
export interface TaskLoopOptions { export interface TaskLoopOptions {
maxEpochs?: number; maxEpochs?: number;
maxJsonParseRetry?: number; maxJsonParseRetry?: number;
@ -28,6 +34,8 @@ export interface IErrorMssage {
msg: string msg: string
} }
export { MessageState };
export interface IDoConversationResult { export interface IDoConversationResult {
stop: boolean; stop: boolean;
} }
@ -57,8 +65,8 @@ export class TaskLoop {
}; };
constructor( constructor(
private readonly taskOptions: TaskLoopOptions = { private taskOptions: TaskLoopOptions = {
maxEpochs: 20, maxEpochs: 50,
maxJsonParseRetry: 3, maxJsonParseRetry: 3,
adapter: undefined, adapter: undefined,
verbose: 0 verbose: 0
@ -77,22 +85,58 @@ export class TaskLoop {
throw new Error('adapter is required'); throw new Error('adapter is required');
} }
// 根据 adapter 创建 nodejs 下特殊的、基于 event 的 message bridge (不占用任何端口)
createMessageBridge(adapter.emitter); createMessageBridge(adapter.emitter);
// 用于进行连接同步
this.nodejsStatus.connectionFut = mcpClientAdapter.launch(); this.nodejsStatus.connectionFut = mcpClientAdapter.launch();
} }
// web 环境下 bridge 会自动加载完成 // web 环境下 bridge 会自动加载完成
this.bridge = useMessageBridge(); this.bridge = useMessageBridge();
// 注册 HMR
mcpClientAdapter.addConnectRefreshListener();
} }
private handleChunkDeltaContent(chunk: ChatCompletionChunk) { public async waitConnection() {
await this.nodejsStatus.connectionFut;
}
public setTaskLoopOptions(taskOptions: TaskLoopOptions) {
const {
maxEpochs = 50,
maxJsonParseRetry = 3,
verbose = 1,
} = taskOptions;
this.taskOptions = {
maxEpochs,
maxJsonParseRetry,
verbose,
...this.taskOptions
};
}
/**
* @description streaming content
* @param chunk
* @param chatData
*/
private handleChunkDeltaContent(chunk: ChatCompletionChunk, chatData: ChatCompletionCreateParamsBase) {
const content = chunk.choices[0]?.delta?.content || ''; const content = chunk.choices[0]?.delta?.content || '';
if (content) { if (content) {
this.streamingContent.value += content; this.streamingContent.value += content;
} }
} }
private handleChunkDeltaToolCalls(chunk: ChatCompletionChunk, toolcallIndexAdapter: (toolCall: ToolCall) => IToolCallIndex) { /**
* @description streaming chunk tool_calls
* @param chunk
* @param chatData
* @param toolcallIndexAdapter
*/
private handleChunkDeltaToolCalls(chunk: ChatCompletionChunk, chatData: ChatCompletionCreateParamsBase, toolcallIndexAdapter: (toolCall: ToolCall) => IToolCallIndex) {
const toolCall = chunk.choices[0]?.delta?.tool_calls?.[0]; const toolCall = chunk.choices[0]?.delta?.tool_calls?.[0];
if (toolCall) { if (toolCall) {
@ -148,9 +192,15 @@ export class TaskLoop {
const { chunk } = data.msg as { chunk: ChatCompletionChunk }; const { chunk } = data.msg as { chunk: ChatCompletionChunk };
// 处理增量的 content 和 tool_calls // 处理增量的 content 和 tool_calls
this.handleChunkDeltaContent(chunk); if (chatData.enableXmlWrapper) {
this.handleChunkDeltaToolCalls(chunk, toolcallIndexAdapter); this.handleChunkDeltaContent(chunk, chatData);
this.handleChunkUsage(chunk); // no tool call in enableXmlWrapper
this.handleChunkUsage(chunk);
} else {
this.handleChunkDeltaContent(chunk, chatData);
this.handleChunkDeltaToolCalls(chunk, chatData, toolcallIndexAdapter);
this.handleChunkUsage(chunk);
}
this.consumeChunks(chunk); this.consumeChunks(chunk);
}, { once: false }); }, { once: false });
@ -207,23 +257,35 @@ export class TaskLoop {
const model = this.getLlmConfig().userModel; const model = this.getLlmConfig().userModel;
const temperature = tabStorage.settings.temperature; const temperature = tabStorage.settings.temperature;
const tools = getToolSchema(tabStorage.settings.enableTools);
const parallelToolCalls = tabStorage.settings.parallelToolCalls; const parallelToolCalls = tabStorage.settings.parallelToolCalls;
const proxyServer = mcpSetting.proxyServer || ''; const proxyServer = mcpSetting.proxyServer || '';
// 如果是 xml 模式,则 tools 为空
const enableXmlWrapper = tabStorage.settings.enableXmlWrapper;
const tools = enableXmlWrapper ? []: getToolSchema(tabStorage.settings.enableTools);
const userMessages = []; const userMessages = [];
// 尝试获取 system prompt在 api 模式下systemPrompt 就是目标提词 // 尝试获取 system prompt在 api 模式下systemPrompt 就是目标提词
// 但是在 UI 模式下systemPrompt 只是一个 index需要从后端数据库中获取真实 prompt // 但是在 UI 模式下systemPrompt 只是一个 index需要从后端数据库中获取真实 prompt
if (tabStorage.settings.systemPrompt) {
const prompt = getSystemPrompt(tabStorage.settings.systemPrompt) || tabStorage.settings.systemPrompt;
userMessages.push({ let prompt = '';
role: 'system',
content: prompt // 如果存在系统提示词,则从数据库中获取对应的数据
}); if (tabStorage.settings.systemPrompt) {
prompt += getSystemPrompt(tabStorage.settings.systemPrompt) || tabStorage.settings.systemPrompt;
} }
// 如果是 xml 模式,则在开头注入 xml
if (enableXmlWrapper) {
prompt += getXmlWrapperPrompt(tabStorage.settings.enableTools, tabStorage);
}
userMessages.push({
role: 'system',
content: prompt
});
// 如果超出了 tabStorage.settings.contextLength, 则删除最早的消息 // 如果超出了 tabStorage.settings.contextLength, 则删除最早的消息
const loadMessages = tabStorage.messages.slice(- tabStorage.settings.contextLength); const loadMessages = tabStorage.messages.slice(- tabStorage.settings.contextLength);
userMessages.push(...loadMessages); userMessages.push(...loadMessages);
@ -240,7 +302,8 @@ export class TaskLoop {
tools, tools,
parallelToolCalls, parallelToolCalls,
messages: userMessages, messages: userMessages,
proxyServer proxyServer,
enableXmlWrapper,
} as ChatCompletionCreateParamsBase; } as ChatCompletionCreateParamsBase;
return chatData; return chatData;
@ -345,7 +408,7 @@ export class TaskLoop {
if (verbose > 0) { if (verbose > 0) {
console.log( console.log(
chalk.gray(`[${new Date().toLocaleString()}]`), chalk.gray(`[${new Date().toLocaleString()}]`),
chalk.blueBright('🔧 calling tool'), chalk.blueBright('🔧 using tool'),
chalk.blueBright(toolCall.function!.name) chalk.blueBright(toolCall.function!.name)
); );
} }
@ -359,13 +422,13 @@ export class TaskLoop {
if (result.state === 'success') { if (result.state === 'success') {
console.log( console.log(
chalk.gray(`[${new Date().toLocaleString()}]`), chalk.gray(`[${new Date().toLocaleString()}]`),
chalk.green('✓ call tools okey dockey'), chalk.green('✓ use tools'),
chalk.green(result.state) chalk.green(result.state)
); );
} else { } else {
console.log( console.log(
chalk.gray(`[${new Date().toLocaleString()}]`), chalk.gray(`[${new Date().toLocaleString()}]`),
chalk.red('× fail to call tools'), chalk.red('× use tools'),
chalk.red(result.content.map(item => item.text).join(', ')) chalk.red(result.content.map(item => item.text).join(', '))
); );
} }
@ -375,7 +438,7 @@ export class TaskLoop {
private consumeEpochs() { private consumeEpochs() {
const { verbose = 0 } = this.taskOptions; const { verbose = 0 } = this.taskOptions;
if (verbose > 0) { if (verbose > 1) {
console.log( console.log(
chalk.gray(`[${new Date().toLocaleString()}]`), chalk.gray(`[${new Date().toLocaleString()}]`),
chalk.blue('task loop enters a new epoch') chalk.blue('task loop enters a new epoch')
@ -386,12 +449,14 @@ export class TaskLoop {
private consumeDones() { private consumeDones() {
const { verbose = 0 } = this.taskOptions; const { verbose = 0 } = this.taskOptions;
if (verbose > 0) {
if (verbose > 1) {
console.log( console.log(
chalk.gray(`[${new Date().toLocaleString()}]`), chalk.gray(`[${new Date().toLocaleString()}]`),
chalk.green('task loop finish a epoch') chalk.green('task loop finish a epoch')
); );
} }
return this.onDone(); return this.onDone();
} }
@ -461,6 +526,7 @@ export class TaskLoop {
// 等待连接完成 // 等待连接完成
await this.nodejsStatus.connectionFut; await this.nodejsStatus.connectionFut;
} }
const enableXmlWrapper = tabStorage.settings.enableXmlWrapper;
// 添加目前的消息 // 添加目前的消息
tabStorage.messages.push({ tabStorage.messages.push({
@ -469,13 +535,14 @@ export class TaskLoop {
extraInfo: { extraInfo: {
created: Date.now(), created: Date.now(),
state: MessageState.Success, state: MessageState.Success,
serverName: this.getLlmConfig().id || 'unknown' serverName: this.getLlmConfig().id || 'unknown',
enableXmlWrapper
} }
}); });
let jsonParseErrorRetryCount = 0; let jsonParseErrorRetryCount = 0;
const { const {
maxEpochs = 20, maxEpochs = 50,
verbose = 0 verbose = 0
} = this.taskOptions || {}; } = this.taskOptions || {};
@ -498,7 +565,7 @@ export class TaskLoop {
this.currentChatId = chatData.id!; this.currentChatId = chatData.id!;
const llm = this.getLlmConfig(); const llm = this.getLlmConfig();
const toolcallIndexAdapter = getToolCallIndexAdapter(llm); const toolcallIndexAdapter = getToolCallIndexAdapter(llm, chatData);
// 发送请求 // 发送请求
const doConverationResult = await this.doConversation(chatData, toolcallIndexAdapter); const doConverationResult = await this.doConversation(chatData, toolcallIndexAdapter);
@ -513,14 +580,15 @@ export class TaskLoop {
extraInfo: { extraInfo: {
created: Date.now(), created: Date.now(),
state: MessageState.Success, state: MessageState.Success,
serverName: this.getLlmConfig().id || 'unknown' serverName: this.getLlmConfig().id || 'unknown',
enableXmlWrapper
} }
}); });
if (verbose > 0) { if (verbose > 0) {
console.log( console.log(
chalk.gray(`[${new Date().toLocaleString()}]`), chalk.gray(`[${new Date().toLocaleString()}]`),
chalk.yellow('🤖 llm wants to call these tools'), chalk.yellow('🤖 Agent wants to use these tools'),
chalk.yellow(this.streamingToolCalls.value.map(tool => tool.function!.name || '').join(', ')) chalk.yellow(this.streamingToolCalls.value.map(tool => tool.function!.name || '').join(', '))
); );
} }
@ -550,7 +618,8 @@ export class TaskLoop {
created: Date.now(), created: Date.now(),
state: toolCallResult.state, state: toolCallResult.state,
serverName: this.getLlmConfig().id || 'unknown', serverName: this.getLlmConfig().id || 'unknown',
usage: undefined usage: undefined,
enableXmlWrapper
} }
}); });
break; break;
@ -565,7 +634,8 @@ export class TaskLoop {
created: Date.now(), created: Date.now(),
state: toolCallResult.state, state: toolCallResult.state,
serverName: this.getLlmConfig().id || 'unknown', serverName: this.getLlmConfig().id || 'unknown',
usage: this.completionUsage usage: this.completionUsage,
enableXmlWrapper
} }
}); });
} else if (toolCallResult.state === MessageState.ToolCall) { } else if (toolCallResult.state === MessageState.ToolCall) {
@ -579,7 +649,8 @@ export class TaskLoop {
created: Date.now(), created: Date.now(),
state: toolCallResult.state, state: toolCallResult.state,
serverName: this.getLlmConfig().id || 'unknown', serverName: this.getLlmConfig().id || 'unknown',
usage: this.completionUsage usage: this.completionUsage,
enableXmlWrapper
} }
}); });
} }
@ -593,10 +664,96 @@ export class TaskLoop {
created: Date.now(), created: Date.now(),
state: MessageState.Success, state: MessageState.Success,
serverName: this.getLlmConfig().id || 'unknown', serverName: this.getLlmConfig().id || 'unknown',
usage: this.completionUsage usage: this.completionUsage,
enableXmlWrapper
} }
}); });
break;
// 如果 xml 模型,需要检查内部是否含有有效的 xml 进行调用
if (tabStorage.settings.enableXmlWrapper) {
const xmls = getXmlsFromString(this.streamingContent.value);
if (xmls.length === 0) {
// 没有 xml 了,说明对话结束
break;
}
// 使用 user 作为身份来承载 xml 调用的结果
// 并且在 extra 内存储结构化信息
const fakeUserMessage = {
role: 'user',
content: '',
extraInfo: {
created: Date.now(),
state: MessageState.Success,
serverName: this.getLlmConfig().id || 'unknown',
usage: this.completionUsage,
enableXmlWrapper,
}
} as ChatMessage;
// 有 xml 了,需要检查 xml 内部是否有有效的 xml 进行调用
for (const xml of xmls) {
const toolcall = await getToolCallFromXmlString(xml);
if (!toolcall) {
continue;
}
// toolcall 事件
// 此处使用的是 xml 使用的 toolcall为了保持一致性需要转换成 openai 标准下的 toolcall
const normaliseToolcall = toNormaliseToolcall(toolcall, toolcallIndexAdapter);
this.consumeToolCalls(normaliseToolcall);
// 调用 XML 调用,其实可以考虑后续把这个循环改成 Promise.race
const toolCallResult = await handleXmlWrapperToolcall(toolcall);
// toolcalled 事件
// 因为是交付给后续进行统一消费的,所以此处的输出满足 openai 接口规范
this.consumeToolCalleds(toolCallResult);
// XML 模式下只存在 assistant 和 user 这两个角色,因此,以 user 为身份来存储
if (toolCallResult.state === MessageState.InvalidXml) {
// 如果是因为解析 XML 错误,则重新开始
tabStorage.messages.pop();
jsonParseErrorRetryCount ++;
redLog('解析 XML 错误 ' + normaliseToolcall?.function?.arguments);
// 如果因为 XML 错误而失败太多,就只能中断了
if (jsonParseErrorRetryCount >= (this.taskOptions.maxJsonParseRetry || 3)) {
const prompt = getXmlResultPrompt(toolcall.callId, `解析 XML 错误,无法继续调用工具 (累计错误次数 ${this.taskOptions.maxJsonParseRetry})`);
fakeUserMessage.content += prompt;
break;
}
} else if (toolCallResult.state === MessageState.Success) {
// TODO: xml 目前只支持 text 类型的回复
const toolCallResultString = toolCallResult.content
.filter(c => c.type === 'text')
.map(c => c.text)
.join('\n');
fakeUserMessage.content += getXmlResultPrompt(toolcall.callId, toolCallResultString);
} else if (toolCallResult.state === MessageState.ToolCall) {
// TODO: xml 目前只支持 text 类型的回复
const toolCallResultString = toolCallResult.content
.filter(c => c.type === 'text')
.map(c => c.text)
.join('\n');
fakeUserMessage.content += getXmlResultPrompt(toolcall.callId, toolCallResultString);
}
}
tabStorage.messages.push(fakeUserMessage);
} else {
// 普通对话直接结束
break;
}
} else { } else {
// 一些提示 // 一些提示
@ -609,4 +766,52 @@ export class TaskLoop {
} }
} }
} }
public async createStorage(settings?: ChatSetting): Promise<ChatStorage> {
let {
enableXmlWrapper = false,
systemPrompt = '',
temperature = 0.6,
contextLength = 100,
parallelToolCalls = true,
enableWebSearch = false,
enableTools = undefined,
} = settings || {};
if (enableTools === undefined) {
// 默认缺省的情况下使用全部工具
const tools = await this.listTools();
enableTools = tools.map(tool => ({
...tool,
enabled: true
})) as EnableToolItem[];
}
const _settings = {
enableXmlWrapper,
systemPrompt,
temperature,
contextLength,
parallelToolCalls,
enableTools,
enableWebSearch
} as ChatSetting;
return {
messages: [],
settings: _settings
}
}
public async getPrompt(promptId: string, args?: Record<string, any>) {
const prompt = await mcpClientAdapter.readPromptTemplate(promptId, args);
// transform prompt to string
const promptString = prompt.messages.map(m => m.content.text).join('\n');
return promptString;
}
public async getResource(resourceUri: string) {
const resource = await mcpClientAdapter.readResource(resourceUri);
return resource;
}
} }

View File

@ -0,0 +1,294 @@
import { parseString } from 'xml2js';
import { MessageState, type ToolCall } from '../chat-box/chat';
import { mcpClientAdapter } from '@/views/connect/core';
import { handleToolResponse, type IToolCallIndex, type ToolCallResult } from './handle-tool-calls';
import type { ChatStorage, EnableToolItem } from "../chat-box/chat";
import type { ToolCallContent } from '@/hook/type';
export interface XmlToolCall {
server: string;
name: string;
callId: string;
parameters: Record<string, string>;
}
export function toolSchemaToPromptDescription(enableTools: EnableToolItem[]) {
let prompt = '';
const tools = enableTools.filter(tool => tool.enabled);
// 无参数的工具
const noParamTools = tools.filter(tool =>
!tool.inputSchema.required || tool.inputSchema.required.length === 0
);
if (noParamTools.length > 0) {
noParamTools.forEach(tool => {
prompt += `- ${tool.name}\n`;
prompt += `**Description**: ${tool.description}\n\n`;
});
}
// 有参数的工具
const paramTools = tools.filter(tool =>
tool.inputSchema.required && tool.inputSchema.required.length > 0
);
if (paramTools.length > 0) {
paramTools.forEach(tool => {
prompt += `- ${tool.name}\n`;
prompt += `**Description**: ${tool.description}\n`;
prompt += `**Parameters**:\n`;
Object.entries(tool.inputSchema.properties).forEach(([name, prop]) => {
const required = tool.inputSchema.required?.includes(name) || false;
prompt += `- \`${name}\`: ${prop.description || 'No Description'} (${prop.type}) ${required ? '(required)' : ''}\n`;
});
prompt += '\n';
});
}
return prompt;
}
export function getXmlWrapperPrompt(tools: EnableToolItem[], tabStorage: ChatStorage) {
const toolPrompt = toolSchemaToPromptDescription(tools);
const requests = [
`ALWAYS analyze what function calls would be appropriate for the task`,
`ALWAYS format your function call usage EXACTLY as specified in the schema`,
`NEVER skip required parameters in function calls`,
`NEVER invent functions that arent available to you`,
`ALWAYS wait for function call execution results before continuing`,
`After invoking a function, wait for the output in <function_results> tag and then continue with your response`,
`NEVER mock or form <function_results> on your own, it will be provided to you after the execution`,
];
if (!tabStorage.settings.parallelToolCalls) {
requests.push(`NEVER invoke multiple functions in a single response`);
}
const requestString = requests.map((text, index) => {
return `${index + 1}. ${text}`;
}).join('\n');
return `
[Start Fresh Session from here]
<SYSTEM>
You are OpenMCP Assistant with the capabilities of invoke functions and make the best use of it during your assistance, a knowledgeable assistant focused on answering questions and providing information on any topics.
In this environment you have access to a set of tools you can use to answer the user's question.
You have access to a set of functions you can use to answer the user's question. You do NOT currently have the ability to inspect files or interact with external resources, except by invoking the below functions.
Function Call Structure:
- All function calls should be wrapped in 'xml' codeblocks tags like \`\`\`xml ... \`\`\`. This is strict requirement.
- Wrap all function calls in 'function_calls' tags
- Each function call uses 'invoke' tags with a 'name' attribute
- Parameters use 'parameter' tags with 'name' attributes
- Parameter Formatting:
- String/scalar parameters: written directly as values
- Lists/objects: must use proper JSON format
- Required parameters must always be included
- Optional parameters should only be included when needed
- If there is xml inside the parameter value, do not use CDATA for wrapping it, just give the xml directly
The instructions regarding 'invoke' specify that:
- When invoking functions, use the 'invoke' tag with a 'name' attribute specifying the function name.
- The invoke tag must be nested within an 'function_calls' block.
- Parameters for the function should be included as 'parameter' tags within the invoke tag, each with a 'name' attribute.
- Include all required parameters for each function call, while optional parameters should only be included when necessary.
- String and scalar parameters should be specified directly as values, while lists and objects should use proper JSON format.
- Do not refer to function/tool names when speaking directly to users - focus on what I'm doing rather than the tool I'm using.
- When invoking a function, ensure all necessary context is provided for the function to execute properly.
- Each 'invoke' tag should represent a single, complete function call with all its relevant parameters.
- DO not generate any <function_calls> tag in your thinking/resoning process, because those will be interpreted as a function call and executed. just formulate the correct parameters for the function call.
The instructions regarding 'call_id="$CALL_ID">
- It is a unique identifier for the function call.
- It is a number that is incremented by 1 for each new function call, starting from 1.
You can invoke one or more functions by writing a "<function_calls>" block like the following as part of your reply to the user, MAKE SURE TO INVOKE ONLY ONE FUNCTION AT A TIME, meaning only one '<function_calls>' tag in your output :
<Example>
\`\`\`xml
<function_calls>
<invoke name="$FUNCTION_NAME" call_id="$CALL_ID">
<parameter name="$PARAMETER_NAME_1">$PARAMETER_VALUE</parameter>
<parameter name="$PARAMETER_NAME_2">$PARAMETER_VALUE</parameter>
...
</invoke>
</function_calls>
\`\`\`
</Example>
String and scalar parameters should be specified as is, while lists and objects should use JSON format. Note that spaces for string values are not stripped. The output is not expected to be valid XML and is parsed with regular expressions.
When a user makes a request:
${requestString}
Answer the user's request using the relevant tool(s), if they are available. Check that all the required parameters for each tool call are provided or can reasonably be inferred from context. IF there are no relevant tools or there are missing values for required parameters, ask the user to supply these values; otherwise proceed with the tool calls. If the user provides a specific value for a parameter (for example provided in quotes), make sure to use that value EXACTLY. DO NOT make up values for or ask about optional parameters. Carefully analyze descriptive terms in the request as they may indicate required parameter values that should be included even if not explicitly quoted.
<Output Format>
<Start HERE>
## Thoughts
- User Query Elaboration:
- Thoughts:
- Observations:
- Solutions:
- Function to be used:
- call_id: $CALL_ID + 1 = $CALL_ID
\`\`\`xml
<function_calls>
<invoke name="$FUNCTION_NAME" call_id="$CALL_ID">
<parameter name="$PARAMETER_NAME_1">$PARAMETER_VALUE</parameter>
<parameter name="$PARAMETER_NAME_2">$PARAMETER_VALUE</parameter>
...
</invoke>
</function_calls>
\`\`\`
<End HERE>
</Output Format>
Do not use <Start HERE> and <End HERE> in your output, that is just output format reference to where to start and end your output.
## AVAILABLE TOOLS FOR SUPERASSISTANT
${toolPrompt}
- list_servers
**Description**: List all connected MCP servers and their capabilities
- get_server_info
**Description**: Get detailed information about a specific server
**Parameters**:
- \`serverName\`: Name of the server to get info for (string) (required)
</SYSTEM>
User Interaction Starts here:
`.trim();
}
export function getXmlResultPrompt(callId: string, result: string) {
return `
\`\`\`xml
<function_results>
<result call_id="${callId}">
${result}
</result>
</function_results>
\`\`\`
`.trim() + '\n\n';
}
export function getXmlsFromString(content: string) {
const matches = content.matchAll(/```xml\n([\s\S]*?)\n```/g);
return Array.from(matches).map(match => match[1].trim());
}
export async function getToolCallFromXmlString(xmlString: string): Promise<XmlToolCall | null> {
try {
const result = await new Promise<any>((resolve, reject) => {
parseString(xmlString, (err, result) => {
if (err) reject(err);
else resolve(result);
});
});
if (!result?.function_calls?.invoke) {
return null;
}
const invoke = result.function_calls.invoke[0].$;
const parameters: Record<string, any> = {};
if (result.function_calls.invoke[0].parameter) {
result.function_calls.invoke[0].parameter.forEach((param: any) => {
const name = param.$.name as string;
parameters[name] = param._;
});
}
// name 可能是 neo4j-mcp.executeReadOnlyCypherQuery
return {
server: '',
name: invoke.name,
callId: invoke.call_id,
parameters
};
} catch (error) {
console.error('Failed to parse function calls:', error);
return null;
}
}
export async function getToolResultFromXmlString(xmlString: string) {
try {
const result = await new Promise<any>((resolve, reject) => {
parseString(xmlString, (err, result) => {
if (err) reject(err);
else resolve(result);
});
});
if (!result?.function_results?.result) {
return null;
}
const resultData = result.function_results.result[0];
const callId = resultData.$.call_id;
// 提取所有评论文本
const toolcallContent = [] as ToolCallContent[];
const content = resultData._;
toolcallContent.push({
type: 'text',
text: content
});
return {
callId,
toolcallContent
};
} catch (error) {
console.error('Failed to parse function results:', error);
return null;
}
}
export function toNormaliseToolcall(xmlToolcall: XmlToolCall, toolcallIndexAdapter: (toolCall: ToolCall) => IToolCallIndex): ToolCall {
const toolcall = {
id: xmlToolcall.callId,
index: -1,
type: 'function',
function: {
name: xmlToolcall.name,
arguments: JSON.stringify(xmlToolcall.parameters)
}
} as ToolCall;
toolcall.index = toolcallIndexAdapter(toolcall);
return toolcall;
}
export async function handleXmlWrapperToolcall(toolcall: XmlToolCall): Promise<ToolCallResult> {
if (!toolcall) {
return {
content: [{
type: 'error',
text: 'invalid xml'
}],
state: MessageState.InvalidXml
}
}
const toolResponse = await mcpClientAdapter.callTool(toolcall.name, toolcall.parameters);
return handleToolResponse(toolResponse);
}

View File

@ -1,10 +1,10 @@
<template> <template>
<div class="chat-container" :ref="el => chatContainerRef = el"> <div class="chat-container" :ref="el => chatContainerRef = el">
<el-scrollbar ref="scrollbarRef" :height="'90%'" @scroll="handleScroll" v-if="renderMessages.length > 0 || isLoading"> <el-scrollbar ref="scrollbarRef" :height="'90%'" @scroll="handleScroll"
v-if="renderMessages.length > 0 || isLoading">
<div class="message-list" :ref="el => messageListRef = el"> <div class="message-list" :ref="el => messageListRef = el">
<div v-for="(message, index) in renderMessages" :key="index" <div v-for="(message, index) in renderMessages" :key="index"
:class="['message-item', message.role.split('/')[0], message.role.split('/')[1]]" :class="['message-item', message.role.split('/')[0], message.role.split('/')[1]]">
>
<div class="message-avatar" v-if="message.role === 'assistant/content'"> <div class="message-avatar" v-if="message.role === 'assistant/content'">
<span class="iconfont icon-robot"></span> <span class="iconfont icon-robot"></span>
</div> </div>
@ -23,10 +23,8 @@
<!-- 助手调用的工具部分 --> <!-- 助手调用的工具部分 -->
<div class="message-content" v-else-if="message.role === 'assistant/tool_calls'"> <div class="message-content" v-else-if="message.role === 'assistant/tool_calls'">
<Message.Toolcall <Message.Toolcall :message="message" :tab-id="props.tabId"
:message="message" :tab-id="props.tabId" @update:tool-result="(value, toolIndex, index) => message.toolResults[toolIndex][index] = value" />
@update:tool-result="(value, toolIndex, index) => message.toolResults[toolIndex][index] = value"
/>
</div> </div>
</div> </div>
@ -47,23 +45,22 @@
</div> </div>
</div> </div>
<ChatBox <ChatBox :ref="el => footerRef = el" :tab-id="props.tabId" />
:ref="el => footerRef = el"
:tab-id="props.tabId"
/>
</div> </div>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { ref, onMounted, defineComponent, defineProps, onUnmounted, computed, nextTick, watch, provide } from 'vue'; import { ref, defineComponent, defineProps, onUnmounted, computed, nextTick, watch, provide, watchEffect } from 'vue';
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
import { ElMessage, type ScrollbarInstance } from 'element-plus'; import { type ScrollbarInstance } from 'element-plus';
import { tabs } from '../panel'; import { tabs } from '../panel';
import type { ChatMessage, ChatStorage, IRenderMessage, ToolCall } from './chat-box/chat'; import type { ChatMessage, ChatStorage, IRenderMessage, ToolCall } from './chat-box/chat';
import { MessageState } from './chat-box/chat'; import { MessageState } from './chat-box/chat';
import * as Message from './message'; import * as Message from './message';
import ChatBox from './chat-box/index.vue'; import ChatBox from './chat-box/index.vue';
import { getToolCallFromXmlString, getToolResultFromXmlString, getXmlsFromString, toNormaliseToolcall } from './core/xml-wrapper';
import { getIdAsIndexAdapter } from './core/handle-tool-calls';
defineComponent({ name: 'chat' }); defineComponent({ name: 'chat' });
@ -85,18 +82,71 @@ if (!tabStorage.messages) {
tabStorage.messages = [] as ChatMessage[]; tabStorage.messages = [] as ChatMessage[];
} }
const renderMessages = computed(() => { function getXmlToolCalls(message: ChatMessage) {
const messages: IRenderMessage[] = []; if (message.role !== 'assistant' && message.role !== 'user') {
return [];
}
const enableXmlTools = message.extraInfo?.enableXmlWrapper ?? false;
if (!enableXmlTools) {
return [];
}
const xmls = getXmlsFromString(message.content);
return xmls || [];
}
const renderMessages = ref<IRenderMessage[]>([]);
watchEffect(async () => {
renderMessages.value = [];
for (const message of tabStorage.messages) { for (const message of tabStorage.messages) {
const indexAdapter = getIdAsIndexAdapter();
const xmls = getXmlToolCalls(message);
if (message.role === 'user') { if (message.role === 'user') {
messages.push({ if (xmls.length > 0 && message.extraInfo.enableXmlWrapper) {
role: 'user', // xml xml xml
content: message.content, // assistant/tool_calls
extraInfo: message.extraInfo const lastAssistantMessage = renderMessages.value[renderMessages.value.length - 1];
}); if (lastAssistantMessage.role === 'assistant/tool_calls') {
const toolCallResultXmls = getXmlsFromString(message.content);
for (const xml of toolCallResultXmls) {
const toolResult = await getToolResultFromXmlString(xml);
if (toolResult) {
const index = indexAdapter(toolResult.callId);
lastAssistantMessage.toolResults[index] = toolResult.toolcallContent;
if (lastAssistantMessage.extraInfo.state === MessageState.Unknown) {
lastAssistantMessage.extraInfo.state = message.extraInfo.state;
} else if (lastAssistantMessage.extraInfo.state === MessageState.Success
|| message.extraInfo.state !== MessageState.Success
) {
lastAssistantMessage.extraInfo.state = message.extraInfo.state;
}
lastAssistantMessage.extraInfo.usage = lastAssistantMessage.extraInfo.usage || message.extraInfo.usage;
}
}
}
} else {
renderMessages.value.push({
role: 'user',
content: message.content,
extraInfo: message.extraInfo
});
}
} else if (message.role === 'assistant') { } else if (message.role === 'assistant') {
if (message.tool_calls) { if (message.tool_calls) {
messages.push({ renderMessages.value.push({
role: 'assistant/tool_calls', role: 'assistant/tool_calls',
content: message.content, content: message.content,
toolResults: Array(message.tool_calls.length).fill([]), toolResults: Array(message.tool_calls.length).fill([]),
@ -108,16 +158,43 @@ const renderMessages = computed(() => {
} }
}); });
} else { } else {
messages.push({ if (xmls.length > 0 && message.extraInfo.enableXmlWrapper) {
role: 'assistant/content', // xml xml xml
content: message.content, const toolCalls = [];
extraInfo: message.extraInfo for (const xml of xmls) {
}); const xmlToolCall = await getToolCallFromXmlString(xml);
if (xmlToolCall) {
toolCalls.push(
toNormaliseToolcall(xmlToolCall, indexAdapter)
);
}
}
const renderAssistantMessage = message.content.replace(/```xml[\s\S]*?```/g, '');
renderMessages.value.push({
role: 'assistant/tool_calls',
content: renderAssistantMessage,
toolResults: Array(toolCalls.length).fill([]),
tool_calls: toolCalls,
showJson: ref(false),
extraInfo: {
...message.extraInfo,
state: MessageState.Unknown
}
});
} else {
renderMessages.value.push({
role: 'assistant/content',
content: message.content,
extraInfo: message.extraInfo
});
}
} }
} else if (message.role === 'tool') { } else if (message.role === 'tool') {
// assistant // assistant
const lastAssistantMessage = messages[messages.length - 1]; const lastAssistantMessage = renderMessages.value[renderMessages.value.length - 1];
if (lastAssistantMessage.role === 'assistant/tool_calls') { if (lastAssistantMessage.role === 'assistant/tool_calls') {
lastAssistantMessage.toolResults[message.index] = message.content; lastAssistantMessage.toolResults[message.index] = message.content;
@ -133,10 +210,9 @@ const renderMessages = computed(() => {
} }
} }
} }
return messages;
}); });
const isLoading = ref(false); const isLoading = ref(false);
const streamingContent = ref(''); const streamingContent = ref('');
@ -232,14 +308,14 @@ watch(streamingToolCalls, () => {
padding-top: 70px; padding-top: 70px;
} }
.chat-openmcp-icon > div { .chat-openmcp-icon>div {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
align-items: left; align-items: left;
font-size: 28px; font-size: 28px;
} }
.chat-openmcp-icon > div > span { .chat-openmcp-icon>div>span {
margin-bottom: 23px; margin-bottom: 23px;
} }
@ -285,7 +361,7 @@ watch(streamingToolCalls, () => {
width: 100%; width: 100%;
} }
.user .message-text > span { .user .message-text>span {
border-radius: .9em; border-radius: .9em;
background-color: var(--main-light-color); background-color: var(--main-light-color);
padding: 10px 15px; padding: 10px 15px;
@ -340,9 +416,12 @@ watch(streamingToolCalls, () => {
@keyframes spin { @keyframes spin {
0% { transform: rotate(0deg); } 0% {
100% { transform: rotate(360deg); } transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
}
} }
</style> </style>

View File

@ -1,7 +1,7 @@
<template> <template>
<div class="message-role"> <div class="message-role">
<span class="message-reminder" v-if="callingTools"> <span class="message-reminder" v-if="callingTools">
Agent 正在使用工具 Agent {{ t('using-tool') }}
<span class="tool-loading iconfont icon-double-loading"> <span class="tool-loading iconfont icon-double-loading">
</span> </span>
</span> </span>

View File

@ -7,17 +7,24 @@
<h3 class="resource-template"> <h3 class="resource-template">
<span>tools/list</span> <span>tools/list</span>
<span class="iconfont icon-restart" @click.stop="reloadTools(client, { first: false })"></span> <span class="iconfont icon-restart" @click.stop="reloadTools(client, { first: false })"></span>
<span>
<span class="cilent-name-tag">
{{ client.name }}
</span>
</span>
</h3> </h3>
</template> </template>
<!-- body --> <!-- body -->
<div class="tool-list-container-scrollbar"> <div class="tool-list-container-scrollbar">
<el-scrollbar height="500px"> <el-scrollbar height="fit-content">
<div class="tool-list-container"> <div class="tool-list-container">
<div class="item" :class="{ 'active': tabStorage.currentToolName === tool.name }" <div class="item" :class="{ 'active': tabStorage.currentToolName === tool.name }"
v-for="tool of client.tools?.values()" :key="tool.name" @click="handleClick(tool)"> v-for="tool of client.tools?.values()" :key="tool.name" @click="handleClick(tool)">
<span>{{ tool.name }}</span> <span>{{ tool.name }}</span>
<span>{{ tool.description || '' }}</span> <br>
<span class="tool-description">{{ tool.description || '' }}</span>
</div> </div>
</div> </div>
</el-scrollbar> </el-scrollbar>
@ -27,7 +34,7 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { onMounted, defineProps, ref, type Reactive } from 'vue'; import { onMounted, defineProps, type Reactive } from 'vue';
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
import type { ToolStorage } from './tools'; import type { ToolStorage } from './tools';
import { tabs } from '../panel'; import { tabs } from '../panel';
@ -99,13 +106,12 @@ onMounted(async () => {
width: 175px; width: 175px;
} }
.tool-list-container>.item { .tool-list-container > .item {
margin: 3px; margin: 3px;
padding: 5px 10px; padding: 5px 10px;
border-radius: .3em; border-radius: .3em;
user-select: none; user-select: none;
cursor: pointer; cursor: pointer;
display: flex;
align-items: center; align-items: center;
justify-content: space-between; justify-content: space-between;
transition: var(--animation-3s); transition: var(--animation-3s);
@ -122,18 +128,34 @@ onMounted(async () => {
} }
.tool-list-container>.item>span:first-child { .tool-list-container>.item>span:first-child {
max-width: 200px; min-width: 120px;
max-width: 250px;
overflow: hidden; overflow: hidden;
text-overflow: ellipsis; text-overflow: ellipsis;
white-space: nowrap; white-space: nowrap;
flex-shrink: 0;
} }
.tool-list-container>.item>span:last-child { .tool-list-container>.item>span:last-child {
min-width: 120px;
max-width: 250px;
overflow: visible;
white-space: normal;
word-wrap: break-word;
}
.resource-template .cilent-name-tag {
margin-left: 10px;
background-color: var(--main-color);
padding: 2px 5px;
border-radius: .3em;
height: fit-content;
font-size: 13px;
color: black;
}
.tool-description {
opacity: 0.6; opacity: 0.6;
font-size: 12.5px; font-size: 12.5px;
max-width: 200px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
} }
</style> </style>

View File

@ -2,7 +2,7 @@
<div v-if="!isConnecting" class="connected-status-container" id="connected-status-container" <div v-if="!isConnecting" class="connected-status-container" id="connected-status-container"
@click.stop="toggleConnectionPanel()" :class="{ 'connected': client.connectionResult.success }"> @click.stop="toggleConnectionPanel()" :class="{ 'connected': client.connectionResult.success }">
<span class="mcp-server-info"> <span class="mcp-server-info">
<el-tooltip class="extra-connect-container" effect="dark" placement="right" <el-tooltip class="extra-connect-container" effect="light" placement="right"
:content="fullDisplayServerName"> :content="fullDisplayServerName">
<span class="name">{{ displayServerName }}</span> <span class="name">{{ displayServerName }}</span>
</el-tooltip> </el-tooltip>
@ -20,7 +20,7 @@
</div> </div>
<div v-else class="connected-status-container"> <div v-else class="connected-status-container">
<span class="mcp-server-info"> <span class="mcp-server-info">
<el-tooltip class="extra-connect-container" effect="dark" placement="right" <el-tooltip class="extra-connect-container" effect="light" placement="right"
:content="fullDisplayServerName"> :content="fullDisplayServerName">
<span class="name"> <span class="name">
加载中 加载中

View File

@ -3,7 +3,7 @@
<div v-for="(item, index) of sidebarItems" :key="index" <div v-for="(item, index) of sidebarItems" :key="index"
:id="`sidebar-${item.ident}`" :id="`sidebar-${item.ident}`"
> >
<el-tooltip :content="t(item.ident)" placement="right"> <el-tooltip :content="t(item.ident)" placement="right" effect="light">
<div class="sidebar-option-item" :class="{ 'active': isActive(item.ident) }" <div class="sidebar-option-item" :class="{ 'active': isActive(item.ident) }"
@click="gotoOption(item.ident)"> @click="gotoOption(item.ident)">
<span :class="`iconfont ${item.icon}`"></span> <span :class="`iconfont ${item.icon}`"></span>

View File

@ -61,7 +61,7 @@ export async function loadPanels(client: McpClient | Reactive<McpClient>) {
panelLoaded.value = true; panelLoaded.value = true;
} }
let debounceHandler: number; let debounceHandler: NodeJS.Timeout;
export function safeSavePanels() { export function safeSavePanels() {
clearTimeout(debounceHandler); clearTimeout(debounceHandler);

View File

@ -1,189 +1,186 @@
{ {
"module": "وحدة", "module": "Module",
"signal": "إشارة", "signal": "Signal",
"search-signal": "البحث عن إشارة", "search-signal": "Rechercher un signal",
"language-setting": "اللغة", "language-setting": "Langue",
"search-setting": "البحث", "search-setting": "Recherche",
"search-case-sensitivity": "حساسية الحالة", "search-case-sensitivity": "Sensibilité à la casse",
"search-mode": "وضع البحث", "search-mode": "Mode de recherche",
"search-scope": "نطاق البحث", "search-scope": "Portée de la recherche",
"search-display-parent-only": "عرض الوحدة الرئيسية فقط", "search-display-parent-only": "Afficher uniquement le module parent",
"search-nothing": "لم يتم العثور على أي إشارات", "search-nothing": "Aucun signal trouvé",
"signal-only": "إشارة فقط", "signal-only": "Signal uniquement",
"module-only": "وحدة فقط", "module-only": "Module uniquement",
"signal-module": "إشارة + وحدة", "signal-module": "Signal + Module",
"general-setting": "عام", "general-setting": "Général",
"appearance-setting": "المظهر", "appearance-setting": "Apparence",
"display-wave-height": "ارتفاع مسار الموجة", "display-wave-height": "Hauteur de la piste d'onde",
"display-signal-info-scope": "معلومات العرض في الشريط الجانبي", "display-signal-info-scope": "Informations affichées dans la barre latérale",
"display-signal-info-scope.width": "عرض البت", "display-signal-info-scope.width": "Largeur de bit",
"display-signal-info-scope.parent": "اسم الوحدة الرئيسية", "display-signal-info-scope.parent": "Nom du module parent",
"wavecolor": "لون الموجة الافتراضي", "wavecolor": "Couleur d'onde par défaut",
"wavecolor.normal-bit": "موجة بعرض وحدة", "wavecolor.normal-bit": "Onde à largeur unique",
"wavecolor.normal-vec": "موجة بعرض متعدد البتات", "wavecolor.normal-vec": "Onde à largeur multiple",
"wavecolor.high-impedance": "موجة عالية المقاومة", "wavecolor.high-impedance": "Onde à haute impédance",
"wavecolor.unknown": "موجة بحالة غير معروفة", "wavecolor.unknown": "Onde à état inconnu",
"operation-setting": "العمليات", "operation-setting": "Opérations",
"render-setting": "العرض", "render-setting": "Rendu",
"render-animation": "تفعيل الرسوم المتحركة للعرض", "render-animation": "Activer l'animation de rendu",
"usermanual": "دليل المستخدم", "usermanual": "Manuel utilisateur",
"usermanual.left-right-scroll.caption": "التحرك لأعلى ولأسفل", "usermanual.left-right-scroll.caption": "Déplacer vers le haut et le bas",
"usermanual.up-down-scroll.caption": "التحرك لليسار ولليمين", "usermanual.up-down-scroll.caption": "Déplacer vers la gauche et la droite",
"usermanual.xscale.caption": "التكبير الأفقي", "usermanual.xscale.caption": "Zoom horizontal",
"loading": "جاري التحميل", "loading": "Chargement",
"context-menu.create-group": "إنشاء مجموعة جديدة", "context-menu.create-group": "Créer un groupe",
"context-menu.join-group": "الانضمام إلى مجموعة موجودة", "context-menu.join-group": "Rejoindre un groupe existant",
"context-menu.change-color": "تغيير اللون", "context-menu.change-color": "Changer la couleur",
"context-menu.delete": "حذف الإشارة", "context-menu.delete": "Supprimer le signal",
"context-menu.delete-all-select": "حذف جميع الإشارات المحددة", "context-menu.delete-all-select": "Supprimer tous les signaux sélectionnés",
"context-menu.signal.name": "اسم الإشارة", "context-menu.signal.name": "Nom du signal",
"context-menu.signal.type": "نوع الإشارة", "context-menu.signal.type": "Type de signal",
"context-menu.signal.width": "عرض الإشارة", "context-menu.signal.width": "Largeur du signal",
"context-menu.signal.dep": "التبعيات", "context-menu.signal.dep": "Dépendances",
"context-menu.group.cancel": "إلغاء التجميع", "context-menu.group.cancel": "Annuler le groupement",
"context-menu.group.delete": "حذف المجموعة", "context-menu.group.delete": "Supprimer le groupe",
"context-menu.group.empty": "لا توجد مجموعات متاحة", "context-menu.group.empty": "Aucun groupe disponible",
"context-menu.group.uname-group": "مجموعة بدون اسم", "context-menu.group.uname-group": "Groupe sans nom",
"toolbar.modal.common-digital": "رقمي", "toolbar.modal.common-digital": "Numérique",
"toolbar.modal.ladder-analog": "تناظري (سلم)", "toolbar.modal.ladder-analog": "Analogique (échelle)",
"toolbar.modal.line-analog": "تناظري (خط)", "toolbar.modal.line-analog": "Analogique (ligne)",
"toolbar.search.name": "الاسم", "toolbar.search.name": "Nom",
"toolbar.search.value": "القيمة", "toolbar.search.value": "Valeur",
"toolbar.format.category.base": "أساسي", "toolbar.format.category.base": "Base",
"toolbar.format.category.dec": "عشري", "toolbar.format.category.dec": "Décimal",
"toolbar.format.category.float": "فاصلة عائمة", "toolbar.format.category.float": "Virgule flottante",
"toolbar.format.signed": "موقع", "toolbar.format.signed": "Signé",
"toolbar.format.unsigned": "غير موقع", "toolbar.format.unsigned": "Non signé",
"toolbar.format.half": "نصف دقة (16 بت)", "toolbar.format.half": "Demi-précision (16 bits)",
"toolbar.format.float": "دقة واحدة (32 بت)", "toolbar.format.float": "Simple précision (32 bits)",
"toolbar.format.double": "دقة مزدوجة (64 بت)", "toolbar.format.double": "Double précision (64 bits)",
"toolbar.location.to-begin": "الانتقال إلى البداية", "toolbar.location.to-begin": "Déplacer au début",
"toolbar.location.to-end": "الانتقال إلى النهاية", "toolbar.location.to-end": "Déplacer à la fin",
"toolbar.location.to-next-change": "الانتقال إلى التغيير التالي", "toolbar.location.to-next-change": "Aller au prochain changement",
"toolbar.location.to-prev-change": "الانتقال إلى التغيير السابق", "toolbar.location.to-prev-change": "Aller au changement précédent",
"toolbar.location.make-location": "إنشاء علامة جديدة", "toolbar.location.make-location": "Créer un nouveau repère",
"toolbar.location.clear-location": "مسح جميع العلامات", "toolbar.location.clear-location": "Effacer tous les repères",
"toolbar.location.clear-location-dialog": "هل أنت متأكد أنك تريد مسح جميع العلامات؟", "toolbar.location.clear-location-dialog": "Êtes-vous sûr de vouloir effacer tous les repères?",
"context-menu.cannot-join-repeat-group": "الإشارة الحالية موجودة بالفعل في هذه المجموعة", "context-menu.cannot-join-repeat-group": "Le signal actuel est déjà dans ce groupe",
"toolbar.no-result": "لا توجد نتائج", "toolbar.no-result": "Aucun résultat",
"toolbar.search.value.already-to-head": "بالفعل في البداية", "toolbar.search.value.already-to-head": "Déjà au début",
"toolbar.search.value.already-to-tail": "بالفعل في النهاية", "toolbar.search.value.already-to-tail": "Déjà à la fin",
"toolbar.search.value.searching": "جاري البحث", "toolbar.search.value.searching": "Recherche en cours",
"pivot.context.delete": "حذف العلامة", "pivot.context.delete": "Supprimer le repère",
"pivot.context.display-axis": "إنشاء محور نسبي", "pivot.context.display-axis": "Créer un axe relatif",
"pivot.context.cancel-axis": "إلغاء المحور النسبي", "pivot.context.cancel-axis": "Annuler l'axe relatif",
"setting.appearance.pivot-color": "لون العلامة", "setting.appearance.pivot-color": "Couleur du repère",
"setting.appearance.moving-pivot": "علامة متحركة", "setting.appearance.moving-pivot": "Repère mobile",
"setting.appearance.user-pivot": "علامة المستخدم", "setting.appearance.user-pivot": "Repère utilisateur",
"setting.appearance.system-pivot": "علامة النظام", "setting.appearance.system-pivot": "Repère système",
"confirm": "تأكيد", "confirm": "Confirmer",
"cancel": "إلغاء", "cancel": "Annuler",
"tips": "نصائح", "tips": "Conseils",
"filemenu.save-view": "حفظ ملف العرض", "filemenu.save-view": "Enregistrer le fichier de vue",
"filemenu.save-as-view": "حفظ العرض كملف", "filemenu.save-as-view": "Enregistrer la vue sous",
"filemenu.load-view": "تحميل ملف العرض", "filemenu.load-view": "Charger le fichier de vue",
"filemenu.auto-save": "الحفظ التلقائي", "filemenu.auto-save": "Sauvegarde automatique",
"current-version": "الإصدار الحالي", "current-version": "Version actuelle",
"setting.language.change-dialog": "لقد قمت بتغيير اللغة إلى {0}، ونوصي بإعادة تشغيل Vcd Viewer.", "setting.language.change-dialog": "Vous avez changé la langue en {0}, nous vous recommandons de redémarrer Vcd Viewer.",
"resources": "الموارد", "resources": "Ressources",
"tools": "أدوات", "tools": "Outils",
"prompts": "المطالبات", "prompts": "Invites",
"interaction-test": "اختبار تفاعلي", "interaction-test": "Test interactif",
"setting": "إعدادات", "setting": "Paramètres",
"about": "نبذة عنا", "about": "À propos",
"connected": "متصل", "connected": "Connecté",
"disconnected": "غير متصل", "disconnected": "Déconnecté",
"debug": "تصحيح", "debug": "Déboguer",
"connect": "اتصال", "connect": "Connexion",
"setting.general-color-setting": "إعدادات الألوان العامة", "setting.general-color-setting": "Paramètres de couleur généraux",
"choose-a-project-debug": "اختر مشروعًا لتصحيحه", "choose-a-project-debug": "Sélectionnez un projet à déboguer",
"model": "النموذج", "model": "Modèle",
"server-provider": "مزود الخدمة", "server-provider": "Fournisseur de services",
"api-root-url": "مسار جذر API", "api-root-url": "Chemin racine de l'API",
"api-token": "مفتاح API", "api-token": "Clé API",
"connection-method": "طريقة الاتصال", "connection-method": "Méthode de connexion",
"command": "أمر", "command": "Commande",
"env-var": "متغيرات البيئة", "env-var": "Variables d'environnement",
"log": "سجلات", "log": "Journaux",
"warning.click-to-connect": "يرجى النقر أولاً على $1 على اليسار للاتصال", "warning.click-to-connect": "Veuillez d'abord cliquer sur $1 à gauche pour vous connecter",
"reset": "إعادة تعيين", "reset": "Réinitialiser",
"read-resource": "قراءة الموارد", "read-resource": "Lire les ressources",
"enter": "إدخال", "enter": "Entrée",
"blank-test": "اختبار فارغ", "blank-test": "Test vide",
"connect.appearance.reconnect": "إعادة الاتصال", "connect.appearance.reconnect": "Reconnexion",
"connect.appearance.connect": "اتصال", "connect.appearance.connect": "Connexion",
"response": "الاستجابة", "response": "Réponse",
"refresh": "تحديث", "refresh": "Rafraîchir",
"read-prompt": "قراءة المطالبة", "read-prompt": "Lire l'invite",
"execute-tool": "تشغيل", "execute-tool": "Exécuter",
"save": "حفظ", "save": "Enregistrer",
"send": "إرسال", "send": "Envoyer",
"server-not-support-statistic": "موفر الخدمة الذي تستخدمه لا يدعم الإحصائيات مؤقتًا", "server-not-support-statistic": "Le fournisseur que vous utilisez ne prend pas en charge les statistiques temporairement",
"answer-at": "تم الإجابة في", "answer-at": "Répondu le",
"input-token": "إدخال", "input-token": "Entrée",
"output-token": "إخراج", "output-token": "Sortie",
"total": "الإجمالي", "total": "Total",
"cache-hit-ratio": "معدل ضربات التخزين المؤقت", "cache-hit-ratio": "Taux de réussite du cache",
"success-save": "تم الحفظ بنجاح", "success-save": "Enregistré avec succès",
"confirm-delete-model": "هل تريد حذف موفر النموذج؟", "confirm-delete-model": "Êtes-vous sûr de vouloir supprimer le fournisseur de modèles ?",
"reserve-one-last-model": "احتفظ بنموذج واحد على الأقل", "reserve-one-last-model": "Conservez au moins un modèle",
"edit": "تعديل", "edit": "Modifier",
"delete": "حذف", "delete": "Supprimer",
"test": "اختبار", "test": "Test",
"add-new-server": "إضافة خدمة", "add-new-server": "Ajouter un service",
"choose-model": "اختر النموذج", "choose-model": "Sélectionner le modèle",
"system-prompt": "كلمات توجيه النظام", "system-prompt": "Invite système",
"tool-use": "استخدام الأداة", "tool-use": "Utilisation d'outils",
"websearch": "بحث على الإنترنت", "websearch": "Recherche sur Internet",
"temperature-parameter": "معامل درجة الحرارة", "temperature-parameter": "Paramètre de température",
"context-length": "طول السياق", "context-length": "Longueur du contexte",
"system-prompt.placeholder": "أدخل كلمة تلميح النظام (مثال: أنت مساعد محترف في تطوير الواجهات الأمامية، أجب باللغة العربية)", "system-prompt.placeholder": "Entrez l'invite système (par exemple : Vous êtes un assistant professionnel de développement front-end, répondez en français)",
"precise": "دقيق", "precise": "Précis",
"moderate": "توازن", "moderate": "Équilibre",
"creative": "إبداع", "creative": "Créativité",
"single-dialog": "محادثة من جولة واحدة", "single-dialog": "Dialogue en un tour",
"multi-dialog": "محادثة متعددة الجولات", "multi-dialog": "Conversation multi-tours",
"press-and-run": "اكتب سؤالاً لبدء الاختبار", "press-and-run": "Tapez une question pour commencer le test",
"connect-sigature": "توقيع الاتصال", "connect-sigature": "Signature de connexion",
"finish-refresh": "تم التحديث", "finish-refresh": "Actualisation terminée",
"add-system-prompt.name-placeholder": "عنوان prompt المخصص", "add-system-prompt.name-placeholder": "Titre de l'invite personnalisée",
"enter-message-dot": "أدخل الرسالة...", "enter-message-dot": "Entrez un message...",
"generate-answer": "جارٍ إنشاء الإجابة", "generate-answer": "Génération de la réponse",
"choose-presetting": "اختر الإعداد المسبق", "choose-presetting": "Sélectionner un préréglage",
"cwd": "دليل التنفيذ", "cwd": "Répertoire d'exécution",
"mcp-server-timeout": "أطول وقت لاستدعاء أداة MCP", "mcp-server-timeout": "Temps d'appel maximum de l'outil MCP",
"return": "عودة", "return": "Retour",
"error": "خطأ", "error": "Erreur",
"feedback": "تعليقات", "feedback": "Retour",
"waiting-mcp-server": "في انتظار استجابة خادم MCP", "waiting-mcp-server": "En attente de la réponse du serveur MCP",
"parallel-tool-calls": "السماح للنموذج باستدعاء أدوات متعددة في رد واحد", "parallel-tool-calls": "Permettre au modèle d'appeler plusieurs outils en une seule réponse",
"proxy-server": "خادم وكيل", "proxy-server": "Serveur proxy",
"update-model-list": "تحديث قائمة النماذج", "update-model-list": "Mettre à jour la liste des modèles",
"ensure-delete-connection": "هل أنت متأكد أنك تريد حذف الاتصال $1؟", "preset-env-sync.success": "Variables d'environnement prédéfinies synchronisées",
"choose-connection-type": "الرجاء اختيار نوع الاتصال", "preset-env-sync.fail": "Échec de la synchronisation des variables d'environnement prédéfinies",
"please-enter-connection-command": "الرجاء إدخال أمر الاتصال", "drag-to-fill-connect-parameters": "Faites glisser pour remplir les paramètres de connexion",
"example-mcp-run": "على سبيل المثال: mcp run main.py", "connect-success": "Connexion réussie",
"please-enter-cwd": "الرجاء إدخال دليل العمل (cwd)، اختياري", "connect-fail": "Échec de la connexion",
"please-enter-cwd-placeholder": "على سبيل المثال: /path/to/project", "preset": "Préréglage",
"please-enter-url": "الرجاء إدخال عنوان URL للاتصال", "openmcp-document": "Documentation officielle d'OpenMCP",
"example-as": "على سبيل المثال:", "star-our-project": "Star notre projet",
"enter-optional-oauth": "الرجاء إدخال رمز OAuth، اختياري", "document": "Documentation officielle",
"quick-start": "مقدمة", "join-discussion": "Rejoindre le groupe de discussion",
"read-document": "قراءة الوثائق", "comment-for-us": "Écrivez un avis pour nous !",
"report-issue": "الإبلاغ عن مشكلة", "openmcp-developed-by": "OpenMCP Client {version} développé par {author}",
"join-project": "المشاركة في المشروع", "error-parse-json": "Erreur d'analyse JSON :",
"comment-plugin": "ملحق التعليقات", "enable-xml-wrapper": "تمكين تغليف تعليمات XML",
"preset-env-sync.success": "تم مزامنة متغيرات البيئة المحددة مسبقًا", "tool-manage": "إدارة الأدوات",
"preset-env-sync.fail": "فشل مزامنة متغيرات البيئة المحددة مسبقًا", "enable-all-tools": "تفعيل جميع الأدوات",
"drag-to-fill-connect-parameters": "اسحب لملء معلمات الاتصال", "disable-all-tools": "تعطيل جميع الأدوات",
"connect-success": "تم الاتصال بنجاح", "using-tool": "جاري استخدام الأداة",
"connect-fail": "فشل الاتصال", "copy-success": "تم النسخ بنجاح",
"preset": "مسبق", "copy-fail": "فشل النسخ",
"openmcp-document": "الوثائق الرسمية لـ OpenMCP", "copy": "نسخ",
"star-our-project": "نجم مشروعنا", "export": "تصدير",
"document": "الوثائق الرسمية", "export-filename": "اسم ملف التصدير",
"join-discussion": "انضم إلى مجموعة النقاش", "how-to-use": "كيفية الاستخدام؟"
"comment-for-us": "اكتب تقييمًا لنا!",
"openmcp-developed-by": "OpenMCP Client {version} تم تطويره بواسطة {author}",
"error-parse-json": "خطأ في تحليل JSON:"
} }

View File

@ -1,189 +1,186 @@
{ {
"module": "Modul", "module": "Module",
"signal": "Signal", "signal": "Signal",
"search-signal": "Signal suchen", "search-signal": "Rechercher un signal",
"language-setting": "Sprache", "language-setting": "Langue",
"search-setting": "Suche", "search-setting": "Recherche",
"search-case-sensitivity": "Groß- und Kleinschreibung beachten", "search-case-sensitivity": "Sensibilité à la casse",
"search-mode": "Suchmodus", "search-mode": "Mode de recherche",
"search-scope": "Suchbereich", "search-scope": "Portée de la recherche",
"search-display-parent-only": "Nur übergeordnetes Modul anzeigen", "search-display-parent-only": "Afficher uniquement le module parent",
"search-nothing": "Keine Signale gefunden", "search-nothing": "Aucun signal trouvé",
"signal-only": "Nur Signal", "signal-only": "Signal uniquement",
"module-only": "Nur Modul", "module-only": "Module uniquement",
"signal-module": "Signal + Modul", "signal-module": "Signal + Module",
"general-setting": "Allgemein", "general-setting": "Général",
"appearance-setting": "Aussehen", "appearance-setting": "Apparence",
"display-wave-height": "Höhe der Wellenformspur", "display-wave-height": "Hauteur de la piste d'onde",
"display-signal-info-scope": "Informationen in der Seitenleiste anzeigen", "display-signal-info-scope": "Informations affichées dans la barre latérale",
"display-signal-info-scope.width": "Bitbreite", "display-signal-info-scope.width": "Largeur de bit",
"display-signal-info-scope.parent": "Name des übergeordneten Moduls", "display-signal-info-scope.parent": "Nom du module parent",
"wavecolor": "Standardwellenformfarbe", "wavecolor": "Couleur d'onde par défaut",
"wavecolor.normal-bit": "Einzelbitwellenform", "wavecolor.normal-bit": "Onde à largeur unique",
"wavecolor.normal-vec": "Mehrfachbitwellenform", "wavecolor.normal-vec": "Onde à largeur multiple",
"wavecolor.high-impedance": "Hohes Impedanzwellenform", "wavecolor.high-impedance": "Onde à haute impédance",
"wavecolor.unknown": "Unbekannte Zustandswellenform", "wavecolor.unknown": "Onde à état inconnu",
"operation-setting": "Operation", "operation-setting": "Opérations",
"render-setting": "Rendering", "render-setting": "Rendu",
"render-animation": "Rendering-Animation aktivieren", "render-animation": "Activer l'animation de rendu",
"usermanual": "Benutzerhandbuch", "usermanual": "Manuel utilisateur",
"usermanual.left-right-scroll.caption": "Nach oben und unten bewegen", "usermanual.left-right-scroll.caption": "Déplacer vers le haut et le bas",
"usermanual.up-down-scroll.caption": "Nach links und rechts bewegen", "usermanual.up-down-scroll.caption": "Déplacer vers la gauche et la droite",
"usermanual.xscale.caption": "Horizontal skalieren", "usermanual.xscale.caption": "Zoom horizontal",
"loading": "Laden", "loading": "Chargement",
"context-menu.create-group": "Neue Gruppe erstellen", "context-menu.create-group": "Créer un groupe",
"context-menu.join-group": "Einer bestehenden Gruppe beitreten", "context-menu.join-group": "Rejoindre un groupe existant",
"context-menu.change-color": "Farbe ändern", "context-menu.change-color": "Changer la couleur",
"context-menu.delete": "Signal löschen", "context-menu.delete": "Supprimer le signal",
"context-menu.delete-all-select": "Alle ausgewählten Signale löschen", "context-menu.delete-all-select": "Supprimer tous les signaux sélectionnés",
"context-menu.signal.name": "Signalname", "context-menu.signal.name": "Nom du signal",
"context-menu.signal.type": "Signaltyp", "context-menu.signal.type": "Type de signal",
"context-menu.signal.width": "Signalbreite", "context-menu.signal.width": "Largeur du signal",
"context-menu.signal.dep": "Abhängigkeiten", "context-menu.signal.dep": "Dépendances",
"context-menu.group.cancel": "Gruppierung abbrechen", "context-menu.group.cancel": "Annuler le groupement",
"context-menu.group.delete": "Gruppe löschen", "context-menu.group.delete": "Supprimer le groupe",
"context-menu.group.empty": "Keine verfügbaren Gruppen", "context-menu.group.empty": "Aucun groupe disponible",
"context-menu.group.uname-group": "Unbenannte Gruppe", "context-menu.group.uname-group": "Groupe sans nom",
"toolbar.modal.common-digital": "Digital", "toolbar.modal.common-digital": "Numérique",
"toolbar.modal.ladder-analog": "Analog (Treppe)", "toolbar.modal.ladder-analog": "Analogique (échelle)",
"toolbar.modal.line-analog": "Analog (Linie)", "toolbar.modal.line-analog": "Analogique (ligne)",
"toolbar.search.name": "Name", "toolbar.search.name": "Nom",
"toolbar.search.value": "Wert", "toolbar.search.value": "Valeur",
"toolbar.format.category.base": "Basis", "toolbar.format.category.base": "Base",
"toolbar.format.category.dec": "Dezimal", "toolbar.format.category.dec": "Décimal",
"toolbar.format.category.float": "Fließkomma", "toolbar.format.category.float": "Virgule flottante",
"toolbar.format.signed": "Vorzeichenbehaftet", "toolbar.format.signed": "Signé",
"toolbar.format.unsigned": "Vorzeichenlos", "toolbar.format.unsigned": "Non signé",
"toolbar.format.half": "Halbgenau (16 Bit)", "toolbar.format.half": "Demi-précision (16 bits)",
"toolbar.format.float": "Einfach genau (32 Bit)", "toolbar.format.float": "Simple précision (32 bits)",
"toolbar.format.double": "Doppelt genau (64 Bit)", "toolbar.format.double": "Double précision (64 bits)",
"toolbar.location.to-begin": "Zum Anfang bewegen", "toolbar.location.to-begin": "Déplacer au début",
"toolbar.location.to-end": "Zum Ende bewegen", "toolbar.location.to-end": "Déplacer à la fin",
"toolbar.location.to-next-change": "Zum nächsten Änderungspunkt bewegen", "toolbar.location.to-next-change": "Aller au prochain changement",
"toolbar.location.to-prev-change": "Zum vorherigen Änderungspunkt bewegen", "toolbar.location.to-prev-change": "Aller au changement précédent",
"toolbar.location.make-location": "Neuen Ankerpunkt erstellen", "toolbar.location.make-location": "Créer un nouveau repère",
"toolbar.location.clear-location": "Alle Ankerpunkte löschen", "toolbar.location.clear-location": "Effacer tous les repères",
"toolbar.location.clear-location-dialog": "Sind Sie sicher, dass Sie alle Ankerpunkte löschen möchten?", "toolbar.location.clear-location-dialog": "Êtes-vous sûr de vouloir effacer tous les repères?",
"context-menu.cannot-join-repeat-group": "Das aktuelle Signal ist bereits in dieser Gruppe", "context-menu.cannot-join-repeat-group": "Le signal actuel est déjà dans ce groupe",
"toolbar.no-result": "Kein Ergebnis", "toolbar.no-result": "Aucun résultat",
"toolbar.search.value.already-to-head": "Bereits am Anfang", "toolbar.search.value.already-to-head": "Déjà au début",
"toolbar.search.value.already-to-tail": "Bereits am Ende", "toolbar.search.value.already-to-tail": "Déjà à la fin",
"toolbar.search.value.searching": "Suche läuft", "toolbar.search.value.searching": "Recherche en cours",
"pivot.context.delete": "Ankerpunkt löschen", "pivot.context.delete": "Supprimer le repère",
"pivot.context.display-axis": "Relative Achse erstellen", "pivot.context.display-axis": "Créer un axe relatif",
"pivot.context.cancel-axis": "Relative Achse abbrechen", "pivot.context.cancel-axis": "Annuler l'axe relatif",
"setting.appearance.pivot-color": "Ankerpunktfarbe", "setting.appearance.pivot-color": "Couleur du repère",
"setting.appearance.moving-pivot": "Beweglicher Ankerpunkt", "setting.appearance.moving-pivot": "Repère mobile",
"setting.appearance.user-pivot": "Benutzerankerpunkt", "setting.appearance.user-pivot": "Repère utilisateur",
"setting.appearance.system-pivot": "Systemankerpunkt", "setting.appearance.system-pivot": "Repère système",
"confirm": "Bestätigen", "confirm": "Confirmer",
"cancel": "Abbrechen", "cancel": "Annuler",
"tips": "Tipps", "tips": "Conseils",
"filemenu.save-view": "Ansicht speichern", "filemenu.save-view": "Enregistrer le fichier de vue",
"filemenu.save-as-view": "Ansicht speichern unter", "filemenu.save-as-view": "Enregistrer la vue sous",
"filemenu.load-view": "Ansicht laden", "filemenu.load-view": "Charger le fichier de vue",
"filemenu.auto-save": "Automatisches Speichern", "filemenu.auto-save": "Sauvegarde automatique",
"current-version": "Aktuelle Version", "current-version": "Version actuelle",
"setting.language.change-dialog": "Sie haben die Sprache auf {0} geändert. Wir empfehlen Ihnen, Vcd Viewer neu zu starten.", "setting.language.change-dialog": "Vous avez changé la langue en {0}, nous vous recommandons de redémarrer Vcd Viewer.",
"resources": "Ressourcen", "resources": "Ressources",
"tools": "Werkzeuge", "tools": "Outils",
"prompts": "Eingabeaufforderungen", "prompts": "Invites",
"interaction-test": "Interaktiver Test", "interaction-test": "Test interactif",
"setting": "Einstellungen", "setting": "Paramètres",
"about": "Über", "about": "À propos",
"connected": "Verbunden", "connected": "Connecté",
"disconnected": "Getrennt", "disconnected": "Déconnecté",
"debug": "Debuggen", "debug": "Déboguer",
"connect": "Verbinden", "connect": "Connexion",
"setting.general-color-setting": "Allgemeine Farbeinstellungen", "setting.general-color-setting": "Paramètres de couleur généraux",
"choose-a-project-debug": "Wählen Sie ein Projekt zum Debuggen aus", "choose-a-project-debug": "Sélectionnez un projet à déboguer",
"model": "Modell", "model": "Modèle",
"server-provider": "Dienstanbieter", "server-provider": "Fournisseur de services",
"api-root-url": "API-Stammpfad", "api-root-url": "Chemin racine de l'API",
"api-token": "API-Schlüssel", "api-token": "Clé API",
"connection-method": "Verbindungsmethode", "connection-method": "Méthode de connexion",
"command": "Befehl", "command": "Commande",
"env-var": "Umgebungsvariablen", "env-var": "Variables d'environnement",
"log": "Protokolle", "log": "Journaux",
"warning.click-to-connect": "Bitte klicken Sie zuerst auf $1 links, um eine Verbindung herzustellen", "warning.click-to-connect": "Veuillez d'abord cliquer sur $1 à gauche pour vous connecter",
"reset": "Zurücksetzen", "reset": "Réinitialiser",
"read-resource": "Ressourcen lesen", "read-resource": "Lire les ressources",
"enter": "Eingabe", "enter": "Entrée",
"blank-test": "Leertest", "blank-test": "Test vide",
"connect.appearance.reconnect": "Neuverbindung", "connect.appearance.reconnect": "Reconnexion",
"connect.appearance.connect": "Verbindung", "connect.appearance.connect": "Connexion",
"response": "Antwort", "response": "Réponse",
"refresh": "Aktualisieren", "refresh": "Rafraîchir",
"read-prompt": "Prompt lesen", "read-prompt": "Lire l'invite",
"execute-tool": "Ausführen", "execute-tool": "Exécuter",
"save": "Speichern", "save": "Enregistrer",
"send": "Senden", "send": "Envoyer",
"server-not-support-statistic": "Der von Ihnen verwendete Anbieter unterstützt vorerst keine Statistiken", "server-not-support-statistic": "Le fournisseur que vous utilisez ne prend pas en charge les statistiques temporairement",
"answer-at": "Beantwortet am", "answer-at": "Répondu le",
"input-token": "Eingabe", "input-token": "Entrée",
"output-token": "Ausgabe", "output-token": "Sortie",
"total": "Gesamt", "total": "Total",
"cache-hit-ratio": "Cache-Trefferquote", "cache-hit-ratio": "Taux de réussite du cache",
"success-save": "Erfolgreich gespeichert", "success-save": "Enregistré avec succès",
"confirm-delete-model": "Modellanbieter wirklich löschen?", "confirm-delete-model": "Êtes-vous sûr de vouloir supprimer le fournisseur de modèles ?",
"reserve-one-last-model": "Behalten Sie mindestens ein Modell", "reserve-one-last-model": "Conservez au moins un modèle",
"edit": "Bearbeiten", "edit": "Modifier",
"delete": "Löschen", "delete": "Supprimer",
"test": "Test", "test": "Test",
"add-new-server": "Dienst hinzufügen", "add-new-server": "Ajouter un service",
"choose-model": "Modell auswählen", "choose-model": "Sélectionner le modèle",
"system-prompt": "Systemaufforderung", "system-prompt": "Invite système",
"tool-use": "Werkzeugnutzung", "tool-use": "Utilisation d'outils",
"websearch": "Internetsuche", "websearch": "Recherche sur Internet",
"temperature-parameter": "Temperaturparameter", "temperature-parameter": "Paramètre de température",
"context-length": "Kontextlänge", "context-length": "Longueur du contexte",
"system-prompt.placeholder": "Geben Sie den System-Prompt ein (z. B.: Sie sind ein professioneller Frontend-Entwicklungsassistent, antworten Sie auf Deutsch)", "system-prompt.placeholder": "Entrez l'invite système (par exemple : Vous êtes un assistant professionnel de développement front-end, répondez en français)",
"precise": "Präzise", "precise": "Précis",
"moderate": "Gleichgewicht", "moderate": "Équilibre",
"creative": "Kreativität", "creative": "Créativité",
"single-dialog": "Einzelrunden-Dialog", "single-dialog": "Dialogue en un tour",
"multi-dialog": "Mehrrundengespräch", "multi-dialog": "Conversation multi-tours",
"press-and-run": "Geben Sie eine Frage ein, um den Test zu starten", "press-and-run": "Tapez une question pour commencer le test",
"connect-sigature": "Verbindungssignatur", "connect-sigature": "Signature de connexion",
"finish-refresh": "Aktualisierung abgeschlossen", "finish-refresh": "Actualisation terminée",
"add-system-prompt.name-placeholder": "Titel für benutzerdefinierte Eingabeaufforderung", "add-system-prompt.name-placeholder": "Titre de l'invite personnalisée",
"enter-message-dot": "Nachricht eingeben...", "enter-message-dot": "Entrez un message...",
"generate-answer": "Antwort wird generiert", "generate-answer": "Génération de la réponse",
"choose-presetting": "Voreinstellung auswählen", "choose-presetting": "Sélectionner un préréglage",
"cwd": "Ausführungsverzeichnis", "cwd": "Répertoire d'exécution",
"mcp-server-timeout": "Maximale Aufrufzeit des MCP-Tools", "mcp-server-timeout": "Temps d'appel maximum de l'outil MCP",
"return": "Zurück", "return": "Retour",
"error": "Fehler", "error": "Erreur",
"feedback": "Feedback", "feedback": "Retour",
"waiting-mcp-server": "Warten auf Antwort vom MCP-Server", "waiting-mcp-server": "En attente de la réponse du serveur MCP",
"parallel-tool-calls": "Erlauben Sie dem Modell, mehrere Tools in einer einzigen Antwort aufzurufen", "parallel-tool-calls": "Permettre au modèle d'appeler plusieurs outils en une seule réponse",
"proxy-server": "Proxy-Server", "proxy-server": "Serveur proxy",
"update-model-list": "Modellliste aktualisieren", "update-model-list": "Mettre à jour la liste des modèles",
"ensure-delete-connection": "Möchten Sie die Verbindung $1 wirklich löschen?", "preset-env-sync.success": "Variables d'environnement prédéfinies synchronisées",
"choose-connection-type": "Bitte wählen Sie den Verbindungstyp", "preset-env-sync.fail": "Échec de la synchronisation des variables d'environnement prédéfinies",
"please-enter-connection-command": "Bitte geben Sie den Verbindungsbefehl ein", "drag-to-fill-connect-parameters": "Faites glisser pour remplir les paramètres de connexion",
"example-mcp-run": "Beispiel: mcp run main.py", "connect-success": "Connexion réussie",
"please-enter-cwd": "Bitte geben Sie das Arbeitsverzeichnis (cwd) ein, optional", "connect-fail": "Échec de la connexion",
"please-enter-cwd-placeholder": "Zum Beispiel: /path/to/project", "preset": "Préréglage",
"please-enter-url": "Bitte geben Sie die Verbindungs-URL ein", "openmcp-document": "Documentation officielle d'OpenMCP",
"example-as": "Zum Beispiel:", "star-our-project": "Star notre projet",
"enter-optional-oauth": "Bitte geben Sie das OAuth-Token ein, optional", "document": "Documentation officielle",
"quick-start": "Einführung", "join-discussion": "Rejoindre le groupe de discussion",
"read-document": "Dokumentation lesen", "comment-for-us": "Écrivez un avis pour nous !",
"report-issue": "Problem melden", "openmcp-developed-by": "OpenMCP Client {version} développé par {author}",
"join-project": "Am Projekt teilnehmen", "error-parse-json": "Erreur d'analyse JSON :",
"comment-plugin": "Kommentar-Plugin", "enable-xml-wrapper": "XML-Befehlsverpackung aktivieren",
"preset-env-sync.success": "Vordefinierte Umgebungsvariablen synchronisiert", "tool-manage": "Werkzeugverwaltung",
"preset-env-sync.fail": "Synchronisierung der vordefinierten Umgebungsvariablen fehlgeschlagen", "enable-all-tools": "Alle Tools aktivieren",
"drag-to-fill-connect-parameters": "Ziehen Sie die Verbindungsparameter", "disable-all-tools": "Alle Tools deaktivieren",
"connect-success": "Erfolgreich verbunden", "using-tool": "Werkzeug wird verwendet",
"connect-fail": "Verbindungsfehler", "copy-success": "Erfolgreich kopiert",
"preset": "Voreinstellung", "copy-fail": "Kopieren fehlgeschlagen",
"openmcp-document": "OpenMCP offizielle Dokumentation", "copy": "Kopieren",
"star-our-project": "Star unser Projekt", "export": "Exportieren",
"document": "Offizielle Dokumentation", "export-filename": "Exportdateiname",
"join-discussion": "Diskussionsgruppe beitreten", "how-to-use": "Wie benutzt man?"
"comment-for-us": "Schreiben Sie eine Bewertung für uns!",
"openmcp-developed-by": "OpenMCP Client {version} entwickelt von {author}",
"error-parse-json": "JSON-Parsing-Fehler:"
} }

View File

@ -1,175 +1,186 @@
{ {
"module": "Modules", "module": "Module",
"signal": "Signals", "signal": "Signal",
"search-signal": "Search Signal", "search-signal": "Rechercher un signal",
"language-setting": "Language", "language-setting": "Langue",
"search-setting": "Search", "search-setting": "Recherche",
"search-case-sensitivity": "Case Sensitivity", "search-case-sensitivity": "Sensibilité à la casse",
"search-mode": "search mode", "search-mode": "Mode de recherche",
"search-scope": "Search Scope", "search-scope": "Portée de la recherche",
"search-display-parent-only": "Display Parent Module Only", "search-display-parent-only": "Afficher uniquement le module parent",
"search-nothing": "Find Nothing", "search-nothing": "Aucun signal trouvé",
"signal-only": "Signal Only", "signal-only": "Signal uniquement",
"module-only": "Module Only", "module-only": "Module uniquement",
"signal-module": "Signal + Module", "signal-module": "Signal + Module",
"general-setting": "General", "general-setting": "Général",
"appearance-setting": "Appearance", "appearance-setting": "Apparence",
"display-wave-height": "Height of Wave Track", "display-wave-height": "Hauteur de la piste d'onde",
"display-signal-info-scope": "Info displayed in sidebar", "display-signal-info-scope": "Informations affichées dans la barre latérale",
"display-signal-info-scope.width": "width", "display-signal-info-scope.width": "Largeur de bit",
"display-signal-info-scope.parent": "parent", "display-signal-info-scope.parent": "Nom du module parent",
"wavecolor": "default color of wave", "wavecolor": "Couleur d'onde par défaut",
"wavecolor.normal-bit": "wave of one width", "wavecolor.normal-bit": "Onde à largeur unique",
"wavecolor.normal-vec": "wave of more than one width", "wavecolor.normal-vec": "Onde à largeur multiple",
"wavecolor.high-impedance": "wave of high impedance", "wavecolor.high-impedance": "Onde à haute impédance",
"wavecolor.unknown": "wave of unknown", "wavecolor.unknown": "Onde à état inconnu",
"operation-setting": "Operation", "operation-setting": "Opérations",
"render-setting": "Render", "render-setting": "Rendu",
"render-animation": "enable rendering animation", "render-animation": "Activer l'animation de rendu",
"usermanual": "User Manual", "usermanual": "Manuel utilisateur",
"usermanual.left-right-scroll.caption": "move up and down", "usermanual.left-right-scroll.caption": "Déplacer vers le haut et le bas",
"usermanual.up-down-scroll.caption": "move left and right", "usermanual.up-down-scroll.caption": "Déplacer vers la gauche et la droite",
"usermanual.xscale.caption": "scale along x axis", "usermanual.xscale.caption": "Zoom horizontal",
"loading": "loading", "loading": "Chargement",
"context-menu.create-group": "create group", "context-menu.create-group": "Créer un groupe",
"context-menu.join-group": "join created group", "context-menu.join-group": "Rejoindre un groupe existant",
"context-menu.change-color": "change color", "context-menu.change-color": "Changer la couleur",
"context-menu.delete": "delete signal", "context-menu.delete": "Supprimer le signal",
"context-menu.delete-all-select": "delete all the selected signals", "context-menu.delete-all-select": "Supprimer tous les signaux sélectionnés",
"context-menu.signal.name": "signal name", "context-menu.signal.name": "Nom du signal",
"context-menu.signal.type": "signal type", "context-menu.signal.type": "Type de signal",
"context-menu.signal.width": "signal width", "context-menu.signal.width": "Largeur du signal",
"context-menu.signal.dep": "signal dependency", "context-menu.signal.dep": "Dépendances",
"context-menu.group.cancel": "cancel group", "context-menu.group.cancel": "Annuler le groupement",
"context-menu.group.delete": "delete group", "context-menu.group.delete": "Supprimer le groupe",
"context-menu.group.empty": "No groups are currently available", "context-menu.group.empty": "Aucun groupe disponible",
"context-menu.group.uname-group": "unamed group", "context-menu.group.uname-group": "Groupe sans nom",
"toolbar.modal.common-digital": "Digital", "toolbar.modal.common-digital": "Numérique",
"toolbar.modal.ladder-analog": "Analog (Ladder)", "toolbar.modal.ladder-analog": "Analogique (échelle)",
"toolbar.modal.line-analog": "Analog (Line)", "toolbar.modal.line-analog": "Analogique (ligne)",
"toolbar.search.name": "Name", "toolbar.search.name": "Nom",
"toolbar.search.value": "Value", "toolbar.search.value": "Valeur",
"toolbar.format.category.base": "Base", "toolbar.format.category.base": "Base",
"toolbar.format.category.dec": "Decimal", "toolbar.format.category.dec": "Décimal",
"toolbar.format.category.float": "Float", "toolbar.format.category.float": "Virgule flottante",
"toolbar.format.signed": "Signed", "toolbar.format.signed": "Signé",
"toolbar.format.unsigned": "Unsigned", "toolbar.format.unsigned": "Non signé",
"toolbar.format.half": "Half (16bit)", "toolbar.format.half": "Demi-précision (16 bits)",
"toolbar.format.float": "Float (32bit)", "toolbar.format.float": "Simple précision (32 bits)",
"toolbar.format.double": "Double (64bit)", "toolbar.format.double": "Double précision (64 bits)",
"toolbar.location.to-begin": "Move to Beginning", "toolbar.location.to-begin": "Déplacer au début",
"toolbar.location.to-end": "Move to End", "toolbar.location.to-end": "Déplacer à la fin",
"toolbar.location.to-next-change": "Go to Next Change Edge", "toolbar.location.to-next-change": "Aller au prochain changement",
"toolbar.location.to-prev-change": "Go to Previous Change Edge", "toolbar.location.to-prev-change": "Aller au changement précédent",
"toolbar.location.make-location": "Create New Pivot", "toolbar.location.make-location": "Créer un nouveau repère",
"toolbar.location.clear-location": "Clear All Pivots", "toolbar.location.clear-location": "Effacer tous les repères",
"toolbar.location.clear-location-dialog": "Are you sure to clear all the pivots ?", "toolbar.location.clear-location-dialog": "Êtes-vous sûr de vouloir effacer tous les repères?",
"context-menu.cannot-join-repeat-group": "current signal is already contained in this group", "context-menu.cannot-join-repeat-group": "Le signal actuel est déjà dans ce groupe",
"toolbar.no-result": "No Result", "toolbar.no-result": "Aucun résultat",
"toolbar.search.value.already-to-head": "already to head", "toolbar.search.value.already-to-head": "Déjà au début",
"toolbar.search.value.already-to-tail": "already to tail", "toolbar.search.value.already-to-tail": "Déjà à la fin",
"toolbar.search.value.searching": "searching", "toolbar.search.value.searching": "Recherche en cours",
"pivot.context.delete": "delete pivot", "pivot.context.delete": "Supprimer le repère",
"pivot.context.display-axis": "create relative axis", "pivot.context.display-axis": "Créer un axe relatif",
"pivot.context.cancel-axis": "cancel relative axis", "pivot.context.cancel-axis": "Annuler l'axe relatif",
"setting.appearance.pivot-color": "pivot color", "setting.appearance.pivot-color": "Couleur du repère",
"setting.appearance.moving-pivot": "moving pivot", "setting.appearance.moving-pivot": "Repère mobile",
"setting.appearance.user-pivot": "user pivot", "setting.appearance.user-pivot": "Repère utilisateur",
"setting.appearance.system-pivot": "system pivot", "setting.appearance.system-pivot": "Repère système",
"confirm": "confirm", "confirm": "Confirmer",
"cancel": "cancel", "cancel": "Annuler",
"tips": "Tips", "tips": "Conseils",
"filemenu.save-view": "保存视图文件", "filemenu.save-view": "Enregistrer le fichier de vue",
"filemenu.save-as-view": "另存为视图文件", "filemenu.save-as-view": "Enregistrer la vue sous",
"filemenu.load-view": "导入视图文件", "filemenu.load-view": "Charger le fichier de vue",
"filemenu.auto-save": "自动保存", "filemenu.auto-save": "Sauvegarde automatique",
"current-version": "current version", "current-version": "Version actuelle",
"setting.language.change-dialog": "You have changed the language to {0}, we recommend restarting Vcd Viewer.", "setting.language.change-dialog": "Vous avez changé la langue en {0}, nous vous recommandons de redémarrer Vcd Viewer.",
"resources": "Resources", "resources": "Ressources",
"tools": "Tools", "tools": "Outils",
"prompts": "Prompts", "prompts": "Invites",
"interaction-test": "Interactive Test", "interaction-test": "Test interactif",
"setting": "Settings", "setting": "Paramètres",
"about": "About", "about": "À propos",
"connected": "Connected", "connected": "Connecté",
"disconnected": "Disconnected", "disconnected": "Déconnecté",
"debug": "Debug", "debug": "Déboguer",
"connect": "Connect", "connect": "Connexion",
"setting.general-color-setting": "General Color Settings", "setting.general-color-setting": "Paramètres de couleur généraux",
"choose-a-project-debug": "Select a project to debug", "choose-a-project-debug": "Sélectionnez un projet à déboguer",
"model": "Model", "model": "Modèle",
"server-provider": "Service Provider", "server-provider": "Fournisseur de services",
"api-root-url": "Base Url", "api-root-url": "Chemin racine de l'API",
"api-token": "API key", "api-token": "Clé API",
"connection-method": "Connection method", "connection-method": "Méthode de connexion",
"command": "Command", "command": "Commande",
"env-var": "Environment variables", "env-var": "Variables d'environnement",
"log": "Logs", "log": "Journaux",
"warning.click-to-connect": "Please first click on $1 on the left to connect", "warning.click-to-connect": "Veuillez d'abord cliquer sur $1 à gauche pour vous connecter",
"reset": "Reset", "reset": "Réinitialiser",
"read-resource": "Read resources", "read-resource": "Lire les ressources",
"enter": "Input", "enter": "Entrée",
"blank-test": "Blank test", "blank-test": "Test vide",
"connect.appearance.reconnect": "Reconnect", "connect.appearance.reconnect": "Reconnexion",
"connect.appearance.connect": "Connection", "connect.appearance.connect": "Connexion",
"response": "Response", "response": "Réponse",
"refresh": "Refresh", "refresh": "Rafraîchir",
"read-prompt": "Read prompt", "read-prompt": "Lire l'invite",
"execute-tool": "Run", "execute-tool": "Exécuter",
"save": "Save", "save": "Enregistrer",
"send": "Send", "send": "Envoyer",
"server-not-support-statistic": "The vendor you are using does not support statistics temporarily", "server-not-support-statistic": "Le fournisseur que vous utilisez ne prend pas en charge les statistiques temporairement",
"answer-at": "Answered on", "answer-at": "Répondu le",
"input-token": "Input", "input-token": "Entrée",
"output-token": "Output", "output-token": "Sortie",
"total": "Total", "total": "Total",
"cache-hit-ratio": "Cache hit rate", "cache-hit-ratio": "Taux de réussite du cache",
"success-save": "Successfully saved", "success-save": "Enregistré avec succès",
"confirm-delete-model": "Are you sure you want to delete the model provider?", "confirm-delete-model": "Êtes-vous sûr de vouloir supprimer le fournisseur de modèles ?",
"reserve-one-last-model": "Keep at least one model", "reserve-one-last-model": "Conservez au moins un modèle",
"edit": "Edit", "edit": "Modifier",
"delete": "Delete", "delete": "Supprimer",
"test": "Test", "test": "Test",
"add-new-server": "Add service", "add-new-server": "Ajouter un service",
"choose-model": "Select model", "choose-model": "Sélectionner le modèle",
"system-prompt": "System prompt", "system-prompt": "Invite système",
"tool-use": "Tool usage", "tool-use": "Utilisation d'outils",
"websearch": "Web search", "websearch": "Recherche sur Internet",
"temperature-parameter": "Temperature parameter", "temperature-parameter": "Paramètre de température",
"context-length": "Context length", "context-length": "Longueur du contexte",
"system-prompt.placeholder": "Enter the system prompt (e.g.: You are a professional front-end development assistant, answer in English)", "system-prompt.placeholder": "Entrez l'invite système (par exemple : Vous êtes un assistant professionnel de développement front-end, répondez en français)",
"precise": "Precise", "precise": "Précis",
"moderate": "Balance", "moderate": "Équilibre",
"creative": "Creativity", "creative": "Créativité",
"single-dialog": "Single-round dialogue", "single-dialog": "Dialogue en un tour",
"multi-dialog": "Multi-turn conversation", "multi-dialog": "Conversation multi-tours",
"press-and-run": "Type a question to start the test", "press-and-run": "Tapez une question pour commencer le test",
"connect-sigature": "Connection signature", "connect-sigature": "Signature de connexion",
"finish-refresh": "Refresh completed", "finish-refresh": "Actualisation terminée",
"add-system-prompt.name-placeholder": "Title for custom prompt", "add-system-prompt.name-placeholder": "Titre de l'invite personnalisée",
"enter-message-dot": "Enter message...", "enter-message-dot": "Entrez un message...",
"generate-answer": "Generating answer", "generate-answer": "Génération de la réponse",
"choose-presetting": "Select preset", "choose-presetting": "Sélectionner un préréglage",
"cwd": "Execution directory", "cwd": "Répertoire d'exécution",
"mcp-server-timeout": "Maximum call time of MCP tool", "mcp-server-timeout": "Temps d'appel maximum de l'outil MCP",
"return": "Back", "return": "Retour",
"error": "Error", "error": "Erreur",
"feedback": "Feedback", "feedback": "Retour",
"waiting-mcp-server": "Waiting for MCP server response", "waiting-mcp-server": "En attente de la réponse du serveur MCP",
"parallel-tool-calls": "Allow the model to call multiple tools in a single reply", "parallel-tool-calls": "Permettre au modèle d'appeler plusieurs outils en une seule réponse",
"proxy-server": "Proxy server", "proxy-server": "Serveur proxy",
"update-model-list": "Update model list", "update-model-list": "Mettre à jour la liste des modèles",
"preset-env-sync.success": "Preset environment variables synchronized", "preset-env-sync.success": "Variables d'environnement prédéfinies synchronisées",
"preset-env-sync.fail": "Preset environment variables synchronization failed", "preset-env-sync.fail": "Échec de la synchronisation des variables d'environnement prédéfinies",
"drag-to-fill-connect-parameters": "Drag to fill connection parameters", "drag-to-fill-connect-parameters": "Faites glisser pour remplir les paramètres de connexion",
"connect-success": "Connected successfully", "connect-success": "Connexion réussie",
"connect-fail": "Connection failed", "connect-fail": "Échec de la connexion",
"preset": "Preset", "preset": "Préréglage",
"openmcp-document": "OpenMCP official documentation", "openmcp-document": "Documentation officielle d'OpenMCP",
"star-our-project": "Star our project", "star-our-project": "Star notre projet",
"document": "Official documentation", "document": "Documentation officielle",
"join-discussion": "Join the discussion group", "join-discussion": "Rejoindre le groupe de discussion",
"comment-for-us": "Write a review for us!", "comment-for-us": "Écrivez un avis pour nous !",
"openmcp-developed-by": "OpenMCP Client {version} developed by {author}", "openmcp-developed-by": "OpenMCP Client {version} développé par {author}",
"error-parse-json": "JSON parsing error:" "error-parse-json": "Erreur d'analyse JSON :",
"enable-xml-wrapper": "Enable XML command wrapping",
"tool-manage": "Tool Management",
"enable-all-tools": "Activate all tools",
"disable-all-tools": "Disable all tools",
"using-tool": "Using tool",
"copy-success": "Copied successfully",
"copy-fail": "Copy failed",
"copy": "Copy",
"export": "Export",
"export-filename": "Export file name",
"how-to-use": "How to use?"
} }

View File

@ -159,20 +159,6 @@
"parallel-tool-calls": "Permettre au modèle d'appeler plusieurs outils en une seule réponse", "parallel-tool-calls": "Permettre au modèle d'appeler plusieurs outils en une seule réponse",
"proxy-server": "Serveur proxy", "proxy-server": "Serveur proxy",
"update-model-list": "Mettre à jour la liste des modèles", "update-model-list": "Mettre à jour la liste des modèles",
"ensure-delete-connection": "Êtes-vous sûr de vouloir supprimer la connexion $1 ?",
"choose-connection-type": "Veuillez sélectionner le type de connexion",
"please-enter-connection-command": "Veuillez saisir la commande de connexion",
"example-mcp-run": "Par exemple : mcp run main.py",
"please-enter-cwd": "Veuillez entrer le répertoire de travail (cwd), facultatif",
"please-enter-cwd-placeholder": "Par exemple : /path/to/project",
"please-enter-url": "Veuillez saisir l'URL de connexion",
"example-as": "Par exemple :",
"enter-optional-oauth": "Veuillez entrer le jeton OAuth, facultatif",
"quick-start": "Premiers pas",
"read-document": "Lire la documentation",
"report-issue": "Signaler un problème",
"join-project": "Participer au projet",
"comment-plugin": "Plugin de commentaires",
"preset-env-sync.success": "Variables d'environnement prédéfinies synchronisées", "preset-env-sync.success": "Variables d'environnement prédéfinies synchronisées",
"preset-env-sync.fail": "Échec de la synchronisation des variables d'environnement prédéfinies", "preset-env-sync.fail": "Échec de la synchronisation des variables d'environnement prédéfinies",
"drag-to-fill-connect-parameters": "Faites glisser pour remplir les paramètres de connexion", "drag-to-fill-connect-parameters": "Faites glisser pour remplir les paramètres de connexion",
@ -185,5 +171,16 @@
"join-discussion": "Rejoindre le groupe de discussion", "join-discussion": "Rejoindre le groupe de discussion",
"comment-for-us": "Écrivez un avis pour nous !", "comment-for-us": "Écrivez un avis pour nous !",
"openmcp-developed-by": "OpenMCP Client {version} développé par {author}", "openmcp-developed-by": "OpenMCP Client {version} développé par {author}",
"error-parse-json": "Erreur d'analyse JSON :" "error-parse-json": "Erreur d'analyse JSON :",
"enable-xml-wrapper": "Activer l'encapsulation de commande XML",
"tool-manage": "Gestion des outils",
"enable-all-tools": "Activer tous les outils",
"disable-all-tools": "Désactiver tous les outils",
"using-tool": "Utilisation de l'outil",
"copy-success": "Copié avec succès",
"copy-fail": "Échec de la copie",
"copy": "Copier",
"export": "Exporter",
"export-filename": "Nom du fichier d'exportation",
"how-to-use": "Comment utiliser ?"
} }

View File

@ -1,175 +1,186 @@
{ {
"module": "モジュール", "module": "Module",
"signal": "信号", "signal": "Signal",
"search-signal": "信号を検索", "search-signal": "Rechercher un signal",
"language-setting": "言語", "language-setting": "Langue",
"search-setting": "検索", "search-setting": "Recherche",
"search-case-sensitivity": "大文字と小文字を区別", "search-case-sensitivity": "Sensibilité à la casse",
"search-mode": "検索モード", "search-mode": "Mode de recherche",
"search-scope": "検索範囲", "search-scope": "Portée de la recherche",
"search-display-parent-only": "親モジュールのみ表示", "search-display-parent-only": "Afficher uniquement le module parent",
"search-nothing": "信号が見つかりませんでした", "search-nothing": "Aucun signal trouvé",
"signal-only": "信号のみ", "signal-only": "Signal uniquement",
"module-only": "モジュールのみ", "module-only": "Module uniquement",
"signal-module": "信号 + モジュール", "signal-module": "Signal + Module",
"general-setting": "一般", "general-setting": "Général",
"appearance-setting": "外観", "appearance-setting": "Apparence",
"display-wave-height": "波形トラックの高さ", "display-wave-height": "Hauteur de la piste d'onde",
"display-signal-info-scope": "サイドバーに表示する情報", "display-signal-info-scope": "Informations affichées dans la barre latérale",
"display-signal-info-scope.width": "ビット幅", "display-signal-info-scope.width": "Largeur de bit",
"display-signal-info-scope.parent": "所属モジュール名", "display-signal-info-scope.parent": "Nom du module parent",
"wavecolor": "デフォルトの波形色", "wavecolor": "Couleur d'onde par défaut",
"wavecolor.normal-bit": "単位幅波形", "wavecolor.normal-bit": "Onde à largeur unique",
"wavecolor.normal-vec": "複数ビット幅波形", "wavecolor.normal-vec": "Onde à largeur multiple",
"wavecolor.high-impedance": "ハイインピーダンス波形", "wavecolor.high-impedance": "Onde à haute impédance",
"wavecolor.unknown": "未知状態波形", "wavecolor.unknown": "Onde à état inconnu",
"operation-setting": "操作", "operation-setting": "Opérations",
"render-setting": "レンダリング", "render-setting": "Rendu",
"render-animation": "レンダリングアニメーションを有効にする", "render-animation": "Activer l'animation de rendu",
"usermanual": "使用説明", "usermanual": "Manuel utilisateur",
"usermanual.left-right-scroll.caption": "上下に移動", "usermanual.left-right-scroll.caption": "Déplacer vers le haut et le bas",
"usermanual.up-down-scroll.caption": "左右に移動", "usermanual.up-down-scroll.caption": "Déplacer vers la gauche et la droite",
"usermanual.xscale.caption": "横方向に拡大", "usermanual.xscale.caption": "Zoom horizontal",
"loading": "読み込み中", "loading": "Chargement",
"context-menu.create-group": "新しいグループを作成", "context-menu.create-group": "Créer un groupe",
"context-menu.join-group": "既存のグループに参加", "context-menu.join-group": "Rejoindre un groupe existant",
"context-menu.change-color": "色を変更", "context-menu.change-color": "Changer la couleur",
"context-menu.delete": "信号を削除", "context-menu.delete": "Supprimer le signal",
"context-menu.delete-all-select": "選択したすべての信号を削除", "context-menu.delete-all-select": "Supprimer tous les signaux sélectionnés",
"context-menu.signal.name": "信号名", "context-menu.signal.name": "Nom du signal",
"context-menu.signal.type": "信号タイプ", "context-menu.signal.type": "Type de signal",
"context-menu.signal.width": "信号幅", "context-menu.signal.width": "Largeur du signal",
"context-menu.signal.dep": "依存関係", "context-menu.signal.dep": "Dépendances",
"context-menu.group.cancel": "グループをキャンセル", "context-menu.group.cancel": "Annuler le groupement",
"context-menu.group.delete": "グループを削除", "context-menu.group.delete": "Supprimer le groupe",
"context-menu.group.empty": "利用可能なグループがありません", "context-menu.group.empty": "Aucun groupe disponible",
"context-menu.group.uname-group": "名前なしグループ", "context-menu.group.uname-group": "Groupe sans nom",
"toolbar.modal.common-digital": "デジタル", "toolbar.modal.common-digital": "Numérique",
"toolbar.modal.ladder-analog": "アナログ(階段)", "toolbar.modal.ladder-analog": "Analogique (échelle)",
"toolbar.modal.line-analog": "アナログ(折れ線)", "toolbar.modal.line-analog": "Analogique (ligne)",
"toolbar.search.name": "名前", "toolbar.search.name": "Nom",
"toolbar.search.value": "値", "toolbar.search.value": "Valeur",
"toolbar.format.category.base": "基本", "toolbar.format.category.base": "Base",
"toolbar.format.category.dec": "10進数", "toolbar.format.category.dec": "Décimal",
"toolbar.format.category.float": "浮動小数点数", "toolbar.format.category.float": "Virgule flottante",
"toolbar.format.signed": "符号付き", "toolbar.format.signed": "Signé",
"toolbar.format.unsigned": "符号なし", "toolbar.format.unsigned": "Non signé",
"toolbar.format.half": "半精度16ビット", "toolbar.format.half": "Demi-précision (16 bits)",
"toolbar.format.float": "単精度32ビット", "toolbar.format.float": "Simple précision (32 bits)",
"toolbar.format.double": "倍精度64ビット", "toolbar.format.double": "Double précision (64 bits)",
"toolbar.location.to-begin": "先頭に移動", "toolbar.location.to-begin": "Déplacer au début",
"toolbar.location.to-end": "末尾に移動", "toolbar.location.to-end": "Déplacer à la fin",
"toolbar.location.to-next-change": "次の変化点に移動", "toolbar.location.to-next-change": "Aller au prochain changement",
"toolbar.location.to-prev-change": "前の変化点に移動", "toolbar.location.to-prev-change": "Aller au changement précédent",
"toolbar.location.make-location": "新しいピボットを作成", "toolbar.location.make-location": "Créer un nouveau repère",
"toolbar.location.clear-location": "すべてのピボットをクリア", "toolbar.location.clear-location": "Effacer tous les repères",
"toolbar.location.clear-location-dialog": "すべてのピボットをクリアしてもよろしいですか?", "toolbar.location.clear-location-dialog": "Êtes-vous sûr de vouloir effacer tous les repères?",
"context-menu.cannot-join-repeat-group": "現在の信号はすでにこのグループに含まれています", "context-menu.cannot-join-repeat-group": "Le signal actuel est déjà dans ce groupe",
"toolbar.no-result": "結果なし", "toolbar.no-result": "Aucun résultat",
"toolbar.search.value.already-to-head": "すでに先頭にいます", "toolbar.search.value.already-to-head": "Déjà au début",
"toolbar.search.value.already-to-tail": "すでに末尾にいます", "toolbar.search.value.already-to-tail": "Déjà à la fin",
"toolbar.search.value.searching": "検索中", "toolbar.search.value.searching": "Recherche en cours",
"pivot.context.delete": "ピボットを削除", "pivot.context.delete": "Supprimer le repère",
"pivot.context.display-axis": "相対座標軸を作成", "pivot.context.display-axis": "Créer un axe relatif",
"pivot.context.cancel-axis": "相対座標軸をキャンセル", "pivot.context.cancel-axis": "Annuler l'axe relatif",
"setting.appearance.pivot-color": "ピボットの色", "setting.appearance.pivot-color": "Couleur du repère",
"setting.appearance.moving-pivot": "移動ピボット", "setting.appearance.moving-pivot": "Repère mobile",
"setting.appearance.user-pivot": "ユーザーピボット", "setting.appearance.user-pivot": "Repère utilisateur",
"setting.appearance.system-pivot": "システムピボット", "setting.appearance.system-pivot": "Repère système",
"confirm": "確認", "confirm": "Confirmer",
"cancel": "キャンセル", "cancel": "Annuler",
"tips": "ヒント", "tips": "Conseils",
"filemenu.save-view": "ビューファイルを保存", "filemenu.save-view": "Enregistrer le fichier de vue",
"filemenu.save-as-view": "ビューファイルとして保存", "filemenu.save-as-view": "Enregistrer la vue sous",
"filemenu.load-view": "ビューファイルをインポート", "filemenu.load-view": "Charger le fichier de vue",
"filemenu.auto-save": "自動保存", "filemenu.auto-save": "Sauvegarde automatique",
"current-version": "現在のバージョン", "current-version": "Version actuelle",
"setting.language.change-dialog": "言語を{0}に変更しました。Vcd Viewerを再起動することをお勧めします。", "setting.language.change-dialog": "Vous avez changé la langue en {0}, nous vous recommandons de redémarrer Vcd Viewer.",
"resources": "リソース", "resources": "Ressources",
"tools": "ツール", "tools": "Outils",
"prompts": "プロンプト", "prompts": "Invites",
"interaction-test": "インタラクティブテスト", "interaction-test": "Test interactif",
"setting": "設定", "setting": "Paramètres",
"about": "について", "about": "À propos",
"connected": "接続済み", "connected": "Connecté",
"disconnected": "切断されました", "disconnected": "Déconnecté",
"debug": "デバッグ", "debug": "Déboguer",
"connect": "接続", "connect": "Connexion",
"setting.general-color-setting": "一般的な色設定", "setting.general-color-setting": "Paramètres de couleur généraux",
"choose-a-project-debug": "デバッグするプロジェクトを選択", "choose-a-project-debug": "Sélectionnez un projet à déboguer",
"model": "モデル", "model": "Modèle",
"server-provider": "サービスプロバイダー", "server-provider": "Fournisseur de services",
"api-root-url": "APIルートパス", "api-root-url": "Chemin racine de l'API",
"api-token": "APIキー", "api-token": "Clé API",
"connection-method": "接続方法", "connection-method": "Méthode de connexion",
"command": "コマンド", "command": "Commande",
"env-var": "環境変数", "env-var": "Variables d'environnement",
"log": "ログ", "log": "Journaux",
"warning.click-to-connect": "まず左側の$1をクリックして接続してください", "warning.click-to-connect": "Veuillez d'abord cliquer sur $1 à gauche pour vous connecter",
"reset": "リセット", "reset": "Réinitialiser",
"read-resource": "リソースを読み込む", "read-resource": "Lire les ressources",
"enter": "入力", "enter": "Entrée",
"blank-test": "空白テスト", "blank-test": "Test vide",
"connect.appearance.reconnect": "再接続", "connect.appearance.reconnect": "Reconnexion",
"connect.appearance.connect": "接続", "connect.appearance.connect": "Connexion",
"response": "応答", "response": "Réponse",
"refresh": "更新", "refresh": "Rafraîchir",
"read-prompt": "プロンプトを読み取る", "read-prompt": "Lire l'invite",
"execute-tool": "実行", "execute-tool": "Exécuter",
"save": "保存", "save": "Enregistrer",
"send": "送信", "send": "Envoyer",
"server-not-support-statistic": "お使いのベンダーは一時的に統計情報をサポートしていません", "server-not-support-statistic": "Le fournisseur que vous utilisez ne prend pas en charge les statistiques temporairement",
"answer-at": "解答日", "answer-at": "Répondu le",
"input-token": "入力", "input-token": "Entrée",
"output-token": "出力", "output-token": "Sortie",
"total": "合計", "total": "Total",
"cache-hit-ratio": "キャッシュヒット率", "cache-hit-ratio": "Taux de réussite du cache",
"success-save": "正常に保存されました", "success-save": "Enregistré avec succès",
"confirm-delete-model": "このモデルプロバイダーを削除しますか?", "confirm-delete-model": "Êtes-vous sûr de vouloir supprimer le fournisseur de modèles ?",
"reserve-one-last-model": "少なくとも1つのモデルを保持してください", "reserve-one-last-model": "Conservez au moins un modèle",
"edit": "編集", "edit": "Modifier",
"delete": "削除", "delete": "Supprimer",
"test": "テスト", "test": "Test",
"add-new-server": "サービスを追加", "add-new-server": "Ajouter un service",
"choose-model": "モデルを選択", "choose-model": "Sélectionner le modèle",
"system-prompt": "システムプロンプト", "system-prompt": "Invite système",
"tool-use": "ツールの使用", "tool-use": "Utilisation d'outils",
"websearch": "ウェブ検索", "websearch": "Recherche sur Internet",
"temperature-parameter": "温度パラメータ", "temperature-parameter": "Paramètre de température",
"context-length": "コンテキストの長さ", "context-length": "Longueur du contexte",
"system-prompt.placeholder": "システムプロンプトを入力してください(例:あなたはプロのフロントエンド開発アシスタントで、日本語で答えます)", "system-prompt.placeholder": "Entrez l'invite système (par exemple : Vous êtes un assistant professionnel de développement front-end, répondez en français)",
"precise": "精密", "precise": "Précis",
"moderate": "バランス", "moderate": "Équilibre",
"creative": "創造性", "creative": "Créativité",
"single-dialog": "単一ラウンドの対話", "single-dialog": "Dialogue en un tour",
"multi-dialog": "マルチターン会話", "multi-dialog": "Conversation multi-tours",
"press-and-run": "テストを開始するには質問を入力してください", "press-and-run": "Tapez une question pour commencer le test",
"connect-sigature": "接続署名", "connect-sigature": "Signature de connexion",
"finish-refresh": "更新が完了しました", "finish-refresh": "Actualisation terminée",
"add-system-prompt.name-placeholder": "カスタムプロンプトのタイトル", "add-system-prompt.name-placeholder": "Titre de l'invite personnalisée",
"enter-message-dot": "メッセージを入力...", "enter-message-dot": "Entrez un message...",
"generate-answer": "回答を生成中", "generate-answer": "Génération de la réponse",
"choose-presetting": "プリセットを選択", "choose-presetting": "Sélectionner un préréglage",
"cwd": "実行ディレクトリ", "cwd": "Répertoire d'exécution",
"mcp-server-timeout": "MCPツールの最大呼び出し時間", "mcp-server-timeout": "Temps d'appel maximum de l'outil MCP",
"return": "戻る", "return": "Retour",
"error": "エラー", "error": "Erreur",
"feedback": "フィードバック", "feedback": "Retour",
"waiting-mcp-server": "MCPサーバーの応答を待機中", "waiting-mcp-server": "En attente de la réponse du serveur MCP",
"parallel-tool-calls": "モデルが単一の返信で複数のツールを呼び出すことを許可する", "parallel-tool-calls": "Permettre au modèle d'appeler plusieurs outils en une seule réponse",
"proxy-server": "プロキシサーバー", "proxy-server": "Serveur proxy",
"update-model-list": "モデルリストを更新", "update-model-list": "Mettre à jour la liste des modèles",
"preset-env-sync.success": "プリセット環境変数の同期が完了しました", "preset-env-sync.success": "Variables d'environnement prédéfinies synchronisées",
"preset-env-sync.fail": "プリセット環境変数の同期に失敗しました", "preset-env-sync.fail": "Échec de la synchronisation des variables d'environnement prédéfinies",
"drag-to-fill-connect-parameters": "接続パラメータを入力するためにドラッグしてください", "drag-to-fill-connect-parameters": "Faites glisser pour remplir les paramètres de connexion",
"connect-success": "接続に成功しました", "connect-success": "Connexion réussie",
"connect-fail": "接続に失敗しました", "connect-fail": "Échec de la connexion",
"preset": "プリセット", "preset": "Préréglage",
"openmcp-document": "OpenMCP公式ドキュメント", "openmcp-document": "Documentation officielle d'OpenMCP",
"star-our-project": "私たちのプロジェクトをスター", "star-our-project": "Star notre projet",
"document": "公式ドキュメント", "document": "Documentation officielle",
"join-discussion": "ディスカッショングループに参加", "join-discussion": "Rejoindre le groupe de discussion",
"comment-for-us": "私たちのためにレビューを書いてください!", "comment-for-us": "Écrivez un avis pour nous !",
"openmcp-developed-by": "OpenMCP Client {version} は {author} によって開発されました", "openmcp-developed-by": "OpenMCP Client {version} développé par {author}",
"error-parse-json": "JSON解析エラー:" "error-parse-json": "Erreur d'analyse JSON :",
"enable-xml-wrapper": "XMLコマンドラッピングを有効にする",
"tool-manage": "ツール管理",
"enable-all-tools": "すべてのツールを有効にする",
"disable-all-tools": "すべてのツールを無効にする",
"using-tool": "ツール使用中",
"copy-success": "コピーしました",
"copy-fail": "コピーに失敗しました",
"copy": "コピー",
"export": "エクスポート",
"export-filename": "エクスポートファイル名",
"how-to-use": "使い方は?"
} }

View File

@ -1,189 +1,186 @@
{ {
"module": "모듈", "module": "Module",
"signal": "신호", "signal": "Signal",
"search-signal": "신호 검색", "search-signal": "Rechercher un signal",
"language-setting": "언어", "language-setting": "Langue",
"search-setting": "검색", "search-setting": "Recherche",
"search-case-sensitivity": "대소문자 구분", "search-case-sensitivity": "Sensibilité à la casse",
"search-mode": "검색 모드", "search-mode": "Mode de recherche",
"search-scope": "검색 범위", "search-scope": "Portée de la recherche",
"search-display-parent-only": "부모 모듈만 표시", "search-display-parent-only": "Afficher uniquement le module parent",
"search-nothing": "신호를 찾을 수 없습니다", "search-nothing": "Aucun signal trouvé",
"signal-only": "신호만", "signal-only": "Signal uniquement",
"module-only": "모듈만", "module-only": "Module uniquement",
"signal-module": "신호 + 모듈", "signal-module": "Signal + Module",
"general-setting": "일반", "general-setting": "Général",
"appearance-setting": "외관", "appearance-setting": "Apparence",
"display-wave-height": "파형 트랙의 높이", "display-wave-height": "Hauteur de la piste d'onde",
"display-signal-info-scope": "사이드바에 표시할 정보", "display-signal-info-scope": "Informations affichées dans la barre latérale",
"display-signal-info-scope.width": "비트 너비", "display-signal-info-scope.width": "Largeur de bit",
"display-signal-info-scope.parent": "소속 모듈 이름", "display-signal-info-scope.parent": "Nom du module parent",
"wavecolor": "기본 파형 색상", "wavecolor": "Couleur d'onde par défaut",
"wavecolor.normal-bit": "단위 너비 파형", "wavecolor.normal-bit": "Onde à largeur unique",
"wavecolor.normal-vec": "다중 비트 너비 파형", "wavecolor.normal-vec": "Onde à largeur multiple",
"wavecolor.high-impedance": "고임피던스 파형", "wavecolor.high-impedance": "Onde à haute impédance",
"wavecolor.unknown": "알 수 없는 상태 파형", "wavecolor.unknown": "Onde à état inconnu",
"operation-setting": "작업", "operation-setting": "Opérations",
"render-setting": "렌더링", "render-setting": "Rendu",
"render-animation": "렌더링 애니메이션 활성화", "render-animation": "Activer l'animation de rendu",
"usermanual": "사용 설명서", "usermanual": "Manuel utilisateur",
"usermanual.left-right-scroll.caption": "상하 이동", "usermanual.left-right-scroll.caption": "Déplacer vers le haut et le bas",
"usermanual.up-down-scroll.caption": "좌우 이동", "usermanual.up-down-scroll.caption": "Déplacer vers la gauche et la droite",
"usermanual.xscale.caption": "가로 확대", "usermanual.xscale.caption": "Zoom horizontal",
"loading": "로딩 중", "loading": "Chargement",
"context-menu.create-group": "새 그룹 생성", "context-menu.create-group": "Créer un groupe",
"context-menu.join-group": "기존 그룹에 참여", "context-menu.join-group": "Rejoindre un groupe existant",
"context-menu.change-color": "색상 변경", "context-menu.change-color": "Changer la couleur",
"context-menu.delete": "신호 삭제", "context-menu.delete": "Supprimer le signal",
"context-menu.delete-all-select": "선택한 모든 신호 삭제", "context-menu.delete-all-select": "Supprimer tous les signaux sélectionnés",
"context-menu.signal.name": "신호 이름", "context-menu.signal.name": "Nom du signal",
"context-menu.signal.type": "신호 유형", "context-menu.signal.type": "Type de signal",
"context-menu.signal.width": "신호 너비", "context-menu.signal.width": "Largeur du signal",
"context-menu.signal.dep": "종속성", "context-menu.signal.dep": "Dépendances",
"context-menu.group.cancel": "그룹 취소", "context-menu.group.cancel": "Annuler le groupement",
"context-menu.group.delete": "그룹 삭제", "context-menu.group.delete": "Supprimer le groupe",
"context-menu.group.empty": "사용 가능한 그룹이 없습니다", "context-menu.group.empty": "Aucun groupe disponible",
"context-menu.group.uname-group": "이름 없는 그룹", "context-menu.group.uname-group": "Groupe sans nom",
"toolbar.modal.common-digital": "디지털", "toolbar.modal.common-digital": "Numérique",
"toolbar.modal.ladder-analog": "아날로그 (사다리)", "toolbar.modal.ladder-analog": "Analogique (échelle)",
"toolbar.modal.line-analog": "아날로그 (선)", "toolbar.modal.line-analog": "Analogique (ligne)",
"toolbar.search.name": "이름", "toolbar.search.name": "Nom",
"toolbar.search.value": "값", "toolbar.search.value": "Valeur",
"toolbar.format.category.base": "기본", "toolbar.format.category.base": "Base",
"toolbar.format.category.dec": "십진법", "toolbar.format.category.dec": "Décimal",
"toolbar.format.category.float": "부동 소수점", "toolbar.format.category.float": "Virgule flottante",
"toolbar.format.signed": "부호 있음", "toolbar.format.signed": "Signé",
"toolbar.format.unsigned": "부호 없음", "toolbar.format.unsigned": "Non signé",
"toolbar.format.half": "반정밀도 (16비트)", "toolbar.format.half": "Demi-précision (16 bits)",
"toolbar.format.float": "단정밀도 (32비트)", "toolbar.format.float": "Simple précision (32 bits)",
"toolbar.format.double": "배정밀도 (64비트)", "toolbar.format.double": "Double précision (64 bits)",
"toolbar.location.to-begin": "시작 부분으로 이동", "toolbar.location.to-begin": "Déplacer au début",
"toolbar.location.to-end": "끝 부분으로 이동", "toolbar.location.to-end": "Déplacer à la fin",
"toolbar.location.to-next-change": "다음 변화로 이동", "toolbar.location.to-next-change": "Aller au prochain changement",
"toolbar.location.to-prev-change": "이전 변화로 이동", "toolbar.location.to-prev-change": "Aller au changement précédent",
"toolbar.location.make-location": "새 표식 생성", "toolbar.location.make-location": "Créer un nouveau repère",
"toolbar.location.clear-location": "모든 표식 지우기", "toolbar.location.clear-location": "Effacer tous les repères",
"toolbar.location.clear-location-dialog": "모든 표식을 지우시겠습니까?", "toolbar.location.clear-location-dialog": "Êtes-vous sûr de vouloir effacer tous les repères?",
"context-menu.cannot-join-repeat-group": "현재 신호가 이미 이 그룹에 있습니다", "context-menu.cannot-join-repeat-group": "Le signal actuel est déjà dans ce groupe",
"toolbar.no-result": "결과 없음", "toolbar.no-result": "Aucun résultat",
"toolbar.search.value.already-to-head": "이미 시작 부분입니다", "toolbar.search.value.already-to-head": "Déjà au début",
"toolbar.search.value.already-to-tail": "이미 끝 부분입니다", "toolbar.search.value.already-to-tail": "Déjà à la fin",
"toolbar.search.value.searching": "검색 중", "toolbar.search.value.searching": "Recherche en cours",
"pivot.context.delete": "표식 삭제", "pivot.context.delete": "Supprimer le repère",
"pivot.context.display-axis": "상대 좌표축 생성", "pivot.context.display-axis": "Créer un axe relatif",
"pivot.context.cancel-axis": "상대 좌표축 취소", "pivot.context.cancel-axis": "Annuler l'axe relatif",
"setting.appearance.pivot-color": "표식 색상", "setting.appearance.pivot-color": "Couleur du repère",
"setting.appearance.moving-pivot": "이동 표식", "setting.appearance.moving-pivot": "Repère mobile",
"setting.appearance.user-pivot": "사용자 표식", "setting.appearance.user-pivot": "Repère utilisateur",
"setting.appearance.system-pivot": "시스템 표식", "setting.appearance.system-pivot": "Repère système",
"confirm": "확인", "confirm": "Confirmer",
"cancel": "취소", "cancel": "Annuler",
"tips": "팁", "tips": "Conseils",
"filemenu.save-view": "뷰 파일 저장", "filemenu.save-view": "Enregistrer le fichier de vue",
"filemenu.save-as-view": "뷰 파일로 저장", "filemenu.save-as-view": "Enregistrer la vue sous",
"filemenu.load-view": "뷰 파일 불러오기", "filemenu.load-view": "Charger le fichier de vue",
"filemenu.auto-save": "자동 저장", "filemenu.auto-save": "Sauvegarde automatique",
"current-version": "현재 버전", "current-version": "Version actuelle",
"setting.language.change-dialog": "언어를 {0}로 변경했습니다. Vcd Viewer를 다시 시작하는 것을 권장합니다.", "setting.language.change-dialog": "Vous avez changé la langue en {0}, nous vous recommandons de redémarrer Vcd Viewer.",
"resources": "자원", "resources": "Ressources",
"tools": "도구", "tools": "Outils",
"prompts": "프롬프트", "prompts": "Invites",
"interaction-test": "인터랙티브 테스트", "interaction-test": "Test interactif",
"setting": "설정", "setting": "Paramètres",
"about": "정보", "about": "À propos",
"connected": "연결됨", "connected": "Connecté",
"disconnected": "연결 해제됨", "disconnected": "Déconnecté",
"debug": "디버그", "debug": "Déboguer",
"connect": "연결", "connect": "Connexion",
"setting.general-color-setting": "일반 색상 설정", "setting.general-color-setting": "Paramètres de couleur généraux",
"choose-a-project-debug": "디버깅할 프로젝트 선택", "choose-a-project-debug": "Sélectionnez un projet à déboguer",
"model": "모델", "model": "Modèle",
"server-provider": "서비스 제공자", "server-provider": "Fournisseur de services",
"api-root-url": "API 루트 경로", "api-root-url": "Chemin racine de l'API",
"api-token": "API 키", "api-token": "Clé API",
"connection-method": "연결 방법", "connection-method": "Méthode de connexion",
"command": "명령", "command": "Commande",
"env-var": "환경 변수", "env-var": "Variables d'environnement",
"log": "로그", "log": "Journaux",
"warning.click-to-connect": "먼저 왼쪽의 $1을 클릭하여 연결하십시오", "warning.click-to-connect": "Veuillez d'abord cliquer sur $1 à gauche pour vous connecter",
"reset": "재설정", "reset": "Réinitialiser",
"read-resource": "리소스 읽기", "read-resource": "Lire les ressources",
"enter": "입력", "enter": "Entrée",
"blank-test": "빈 테스트", "blank-test": "Test vide",
"connect.appearance.reconnect": "재연결", "connect.appearance.reconnect": "Reconnexion",
"connect.appearance.connect": "연결", "connect.appearance.connect": "Connexion",
"response": "응답", "response": "Réponse",
"refresh": "새로 고침", "refresh": "Rafraîchir",
"read-prompt": "프롬프트 읽기", "read-prompt": "Lire l'invite",
"execute-tool": "실행", "execute-tool": "Exécuter",
"save": "저장", "save": "Enregistrer",
"send": "보내기", "send": "Envoyer",
"server-not-support-statistic": "사용 중인 공급업체는 일시적으로 통계를 지원하지 않습니다", "server-not-support-statistic": "Le fournisseur que vous utilisez ne prend pas en charge les statistiques temporairement",
"answer-at": "답변일", "answer-at": "Répondu le",
"input-token": "입력", "input-token": "Entrée",
"output-token": "출력", "output-token": "Sortie",
"total": "총계", "total": "Total",
"cache-hit-ratio": "캐시 적중률", "cache-hit-ratio": "Taux de réussite du cache",
"success-save": "성공적으로 저장됨", "success-save": "Enregistré avec succès",
"confirm-delete-model": "이 모델 공급자를 삭제하시겠습니까?", "confirm-delete-model": "Êtes-vous sûr de vouloir supprimer le fournisseur de modèles ?",
"reserve-one-last-model": "적어도 하나의 모델을 유지하세요", "reserve-one-last-model": "Conservez au moins un modèle",
"edit": "편집", "edit": "Modifier",
"delete": "삭제", "delete": "Supprimer",
"test": "테스트", "test": "Test",
"add-new-server": "서비스 추가", "add-new-server": "Ajouter un service",
"choose-model": "모델 선택", "choose-model": "Sélectionner le modèle",
"system-prompt": "시스템 프롬프트", "system-prompt": "Invite système",
"tool-use": "도구 사용", "tool-use": "Utilisation d'outils",
"websearch": "웹 검색", "websearch": "Recherche sur Internet",
"temperature-parameter": "온도 매개변수", "temperature-parameter": "Paramètre de température",
"context-length": "컨텍스트 길이", "context-length": "Longueur du contexte",
"system-prompt.placeholder": "시스템 프롬프트를 입력하세요 (예: 당신은 전문 프론트엔드 개발 어시스턴트이며, 한국어로 답변합니다)", "system-prompt.placeholder": "Entrez l'invite système (par exemple : Vous êtes un assistant professionnel de développement front-end, répondez en français)",
"precise": "정확한", "precise": "Précis",
"moderate": "균형", "moderate": "Équilibre",
"creative": "창의성", "creative": "Créativité",
"single-dialog": "단일 라운드 대화", "single-dialog": "Dialogue en un tour",
"multi-dialog": "다중 턴 대화", "multi-dialog": "Conversation multi-tours",
"press-and-run": "테스트를 시작하려면 질문을 입력하세요", "press-and-run": "Tapez une question pour commencer le test",
"connect-sigature": "연결 서명", "connect-sigature": "Signature de connexion",
"finish-refresh": "새로 고침 완료", "finish-refresh": "Actualisation terminée",
"add-system-prompt.name-placeholder": "사용자 지정 프롬프트 제목", "add-system-prompt.name-placeholder": "Titre de l'invite personnalisée",
"enter-message-dot": "메시지를 입력하세요...", "enter-message-dot": "Entrez un message...",
"generate-answer": "답변 생성 중", "generate-answer": "Génération de la réponse",
"choose-presetting": "프리셋 선택", "choose-presetting": "Sélectionner un préréglage",
"cwd": "실행 디렉터리", "cwd": "Répertoire d'exécution",
"mcp-server-timeout": "MCP 도구 최대 호출 시간", "mcp-server-timeout": "Temps d'appel maximum de l'outil MCP",
"return": "돌아가기", "return": "Retour",
"error": "오류", "error": "Erreur",
"feedback": "피드백", "feedback": "Retour",
"waiting-mcp-server": "MCP 서버 응답 대기 중", "waiting-mcp-server": "En attente de la réponse du serveur MCP",
"parallel-tool-calls": "모델이 단일 응답에서 여러 도구를 호출할 수 있도록 허용", "parallel-tool-calls": "Permettre au modèle d'appeler plusieurs outils en une seule réponse",
"proxy-server": "프록시 서버", "proxy-server": "Serveur proxy",
"update-model-list": "모델 목록 업데이트", "update-model-list": "Mettre à jour la liste des modèles",
"ensure-delete-connection": "연결 $1을(를) 삭제하시겠습니까?", "preset-env-sync.success": "Variables d'environnement prédéfinies synchronisées",
"choose-connection-type": "연결 유형을 선택해 주세요", "preset-env-sync.fail": "Échec de la synchronisation des variables d'environnement prédéfinies",
"please-enter-connection-command": "연결 명령을 입력해 주세요", "drag-to-fill-connect-parameters": "Faites glisser pour remplir les paramètres de connexion",
"example-mcp-run": "예: mcp run main.py", "connect-success": "Connexion réussie",
"please-enter-cwd": "작업 디렉토리 (cwd)를 입력하세요 (선택 사항)", "connect-fail": "Échec de la connexion",
"please-enter-cwd-placeholder": "예: /path/to/project", "preset": "Préréglage",
"please-enter-url": "연결 URL을 입력해 주세요", "openmcp-document": "Documentation officielle d'OpenMCP",
"example-as": "예를 들어:", "star-our-project": "Star notre projet",
"enter-optional-oauth": "OAuth 토큰을 입력하세요 (선택 사항)", "document": "Documentation officielle",
"quick-start": "시작하기", "join-discussion": "Rejoindre le groupe de discussion",
"read-document": "문서 읽기", "comment-for-us": "Écrivez un avis pour nous !",
"report-issue": "문제 신고", "openmcp-developed-by": "OpenMCP Client {version} développé par {author}",
"join-project": "프로젝트 참여", "error-parse-json": "Erreur d'analyse JSON :",
"comment-plugin": "댓글 플러그인", "enable-xml-wrapper": "XML 명령 래핑 활성화",
"preset-env-sync.success": "사전 설정 환경 변수 동기화 완료", "tool-manage": "도구 관리",
"preset-env-sync.fail": "사전 설정된 환경 변수 동기화 실패", "enable-all-tools": "모든 도구 활성화",
"drag-to-fill-connect-parameters": "연결 매개변수를 채우려면 드래그하세요", "disable-all-tools": "모든 도구 비활성화",
"connect-success": "성공적으로 연결되었습니다", "using-tool": "도구 사용 중",
"connect-fail": "연결 실패", "copy-success": "성공적으로 복사되었습니다",
"preset": "프리셋", "copy-fail": "복사 실패",
"openmcp-document": "OpenMCP 공식 문서", "copy": "복사",
"star-our-project": "우리 프로젝트 스타", "export": "내보내기",
"document": "공식 문서", "export-filename": "내보내기 파일 이름",
"join-discussion": "토론 그룹에 참여", "how-to-use": "사용 방법?"
"comment-for-us": "우리를 위해 리뷰를 작성해 주세요!",
"openmcp-developed-by": "OpenMCP Client {version}은 {author}에 의해 개발되었습니다",
"error-parse-json": "JSON 구문 분석 오류:"
} }

View File

@ -1,189 +1,186 @@
{ {
"module": "Модуль", "module": "Module",
"signal": "Сигнал", "signal": "Signal",
"search-signal": "Поиск сигнала", "search-signal": "Rechercher un signal",
"language-setting": "Язык", "language-setting": "Langue",
"search-setting": "Поиск", "search-setting": "Recherche",
"search-case-sensitivity": "Учитывать регистр", "search-case-sensitivity": "Sensibilité à la casse",
"search-mode": "Режим поиска", "search-mode": "Mode de recherche",
"search-scope": "Область поиска", "search-scope": "Portée de la recherche",
"search-display-parent-only": "Показывать только родительский модуль", "search-display-parent-only": "Afficher uniquement le module parent",
"search-nothing": "Сигналы не найдены", "search-nothing": "Aucun signal trouvé",
"signal-only": "Только сигнал", "signal-only": "Signal uniquement",
"module-only": "Только модуль", "module-only": "Module uniquement",
"signal-module": "Сигнал + Модуль", "signal-module": "Signal + Module",
"general-setting": "Общие", "general-setting": "Général",
"appearance-setting": "Внешний вид", "appearance-setting": "Apparence",
"display-wave-height": "Высота дорожки волны", "display-wave-height": "Hauteur de la piste d'onde",
"display-signal-info-scope": "Информация в боковой панели", "display-signal-info-scope": "Informations affichées dans la barre latérale",
"display-signal-info-scope.width": "Ширина бита", "display-signal-info-scope.width": "Largeur de bit",
"display-signal-info-scope.parent": "Имя родительского модуля", "display-signal-info-scope.parent": "Nom du module parent",
"wavecolor": "Цвет волны по умолчанию", "wavecolor": "Couleur d'onde par défaut",
"wavecolor.normal-bit": "Одиночная ширина волны", "wavecolor.normal-bit": "Onde à largeur unique",
"wavecolor.normal-vec": "Многобитная ширина волны", "wavecolor.normal-vec": "Onde à largeur multiple",
"wavecolor.high-impedance": "Высокоимпедансная волна", "wavecolor.high-impedance": "Onde à haute impédance",
"wavecolor.unknown": "Неизвестное состояние волны", "wavecolor.unknown": "Onde à état inconnu",
"operation-setting": "Операции", "operation-setting": "Opérations",
"render-setting": "Рендеринг", "render-setting": "Rendu",
"render-animation": "Включить анимацию рендеринга", "render-animation": "Activer l'animation de rendu",
"usermanual": "Руководство пользователя", "usermanual": "Manuel utilisateur",
"usermanual.left-right-scroll.caption": "Перемещение вверх и вниз", "usermanual.left-right-scroll.caption": "Déplacer vers le haut et le bas",
"usermanual.up-down-scroll.caption": "Перемещение влево и вправо", "usermanual.up-down-scroll.caption": "Déplacer vers la gauche et la droite",
"usermanual.xscale.caption": "Горизонтальное масштабирование", "usermanual.xscale.caption": "Zoom horizontal",
"loading": "Загрузка", "loading": "Chargement",
"context-menu.create-group": "Создать группу", "context-menu.create-group": "Créer un groupe",
"context-menu.join-group": "Присоединиться к существующей группе", "context-menu.join-group": "Rejoindre un groupe existant",
"context-menu.change-color": "Изменить цвет", "context-menu.change-color": "Changer la couleur",
"context-menu.delete": "Удалить сигнал", "context-menu.delete": "Supprimer le signal",
"context-menu.delete-all-select": "Удалить все выбранные сигналы", "context-menu.delete-all-select": "Supprimer tous les signaux sélectionnés",
"context-menu.signal.name": "Имя сигнала", "context-menu.signal.name": "Nom du signal",
"context-menu.signal.type": "Тип сигнала", "context-menu.signal.type": "Type de signal",
"context-menu.signal.width": "Ширина сигнала", "context-menu.signal.width": "Largeur du signal",
"context-menu.signal.dep": "Зависимости", "context-menu.signal.dep": "Dépendances",
"context-menu.group.cancel": "Отменить группировку", "context-menu.group.cancel": "Annuler le groupement",
"context-menu.group.delete": "Удалить группу", "context-menu.group.delete": "Supprimer le groupe",
"context-menu.group.empty": "Нет доступных групп", "context-menu.group.empty": "Aucun groupe disponible",
"context-menu.group.uname-group": "Безымянная группа", "context-menu.group.uname-group": "Groupe sans nom",
"toolbar.modal.common-digital": "Цифровой", "toolbar.modal.common-digital": "Numérique",
"toolbar.modal.ladder-analog": "Аналоговый (ступенчатый)", "toolbar.modal.ladder-analog": "Analogique (échelle)",
"toolbar.modal.line-analog": "Аналоговый (линейный)", "toolbar.modal.line-analog": "Analogique (ligne)",
"toolbar.search.name": "Имя", "toolbar.search.name": "Nom",
"toolbar.search.value": "Значение", "toolbar.search.value": "Valeur",
"toolbar.format.category.base": "Основной", "toolbar.format.category.base": "Base",
"toolbar.format.category.dec": "Десятичный", "toolbar.format.category.dec": "Décimal",
"toolbar.format.category.float": "Плавающая запятая", "toolbar.format.category.float": "Virgule flottante",
"toolbar.format.signed": "Знаковый", "toolbar.format.signed": "Signé",
"toolbar.format.unsigned": "Беззнаковый", "toolbar.format.unsigned": "Non signé",
"toolbar.format.half": "Полуточный (16 бит)", "toolbar.format.half": "Demi-précision (16 bits)",
"toolbar.format.float": "Одинарная точность (32 бита)", "toolbar.format.float": "Simple précision (32 bits)",
"toolbar.format.double": "Двойная точность (64 бита)", "toolbar.format.double": "Double précision (64 bits)",
"toolbar.location.to-begin": "Переместить в начало", "toolbar.location.to-begin": "Déplacer au début",
"toolbar.location.to-end": "Переместить в конец", "toolbar.location.to-end": "Déplacer à la fin",
"toolbar.location.to-next-change": "Перейти к следующему изменению", "toolbar.location.to-next-change": "Aller au prochain changement",
"toolbar.location.to-prev-change": "Перейти к предыдущему изменению", "toolbar.location.to-prev-change": "Aller au changement précédent",
"toolbar.location.make-location": "Создать новый маркер", "toolbar.location.make-location": "Créer un nouveau repère",
"toolbar.location.clear-location": "Очистить все маркеры", "toolbar.location.clear-location": "Effacer tous les repères",
"toolbar.location.clear-location-dialog": "Вы уверены, что хотите очистить все маркеры?", "toolbar.location.clear-location-dialog": "Êtes-vous sûr de vouloir effacer tous les repères?",
"context-menu.cannot-join-repeat-group": "Текущий сигнал уже находится в этой группе", "context-menu.cannot-join-repeat-group": "Le signal actuel est déjà dans ce groupe",
"toolbar.no-result": "Нет результатов", "toolbar.no-result": "Aucun résultat",
"toolbar.search.value.already-to-head": "Уже в начале", "toolbar.search.value.already-to-head": "Déjà au début",
"toolbar.search.value.already-to-tail": "Уже в конце", "toolbar.search.value.already-to-tail": "Déjà à la fin",
"toolbar.search.value.searching": "Поиск", "toolbar.search.value.searching": "Recherche en cours",
"pivot.context.delete": "Удалить маркер", "pivot.context.delete": "Supprimer le repère",
"pivot.context.display-axis": "Создать относительную ось", "pivot.context.display-axis": "Créer un axe relatif",
"pivot.context.cancel-axis": "Отменить относительную ось", "pivot.context.cancel-axis": "Annuler l'axe relatif",
"setting.appearance.pivot-color": "Цвет маркера", "setting.appearance.pivot-color": "Couleur du repère",
"setting.appearance.moving-pivot": "Движущийся маркер", "setting.appearance.moving-pivot": "Repère mobile",
"setting.appearance.user-pivot": "Пользовательский маркер", "setting.appearance.user-pivot": "Repère utilisateur",
"setting.appearance.system-pivot": "Системный маркер", "setting.appearance.system-pivot": "Repère système",
"confirm": "Подтвердить", "confirm": "Confirmer",
"cancel": "Отменить", "cancel": "Annuler",
"tips": "Советы", "tips": "Conseils",
"filemenu.save-view": "Сохранить файл представления", "filemenu.save-view": "Enregistrer le fichier de vue",
"filemenu.save-as-view": "Сохранить представление как", "filemenu.save-as-view": "Enregistrer la vue sous",
"filemenu.load-view": "Загрузить файл представления", "filemenu.load-view": "Charger le fichier de vue",
"filemenu.auto-save": "Автосохранение", "filemenu.auto-save": "Sauvegarde automatique",
"current-version": "Текущая версия", "current-version": "Version actuelle",
"setting.language.change-dialog": "Вы изменили язык на {0}, рекомендуем перезапустить Vcd Viewer.", "setting.language.change-dialog": "Vous avez changé la langue en {0}, nous vous recommandons de redémarrer Vcd Viewer.",
"resources": "Ресурсы", "resources": "Ressources",
"tools": "Инструменты", "tools": "Outils",
"prompts": "Подсказки", "prompts": "Invites",
"interaction-test": "Интерактивный тест", "interaction-test": "Test interactif",
"setting": "Настройки", "setting": "Paramètres",
"about": "О нас", "about": "À propos",
"connected": "Подключено", "connected": "Connecté",
"disconnected": "Отключено", "disconnected": "Déconnecté",
"debug": "Отладка", "debug": "Déboguer",
"connect": "Подключение", "connect": "Connexion",
"setting.general-color-setting": "Общие настройки цвета", "setting.general-color-setting": "Paramètres de couleur généraux",
"choose-a-project-debug": "Выберите проект для отладки", "choose-a-project-debug": "Sélectionnez un projet à déboguer",
"model": "Модель", "model": "Modèle",
"server-provider": "Поставщик услуг", "server-provider": "Fournisseur de services",
"api-root-url": "Корневой путь API", "api-root-url": "Chemin racine de l'API",
"api-token": "API-ключ", "api-token": "Clé API",
"connection-method": "Способ подключения", "connection-method": "Méthode de connexion",
"command": "Команда", "command": "Commande",
"env-var": "Переменные среды", "env-var": "Variables d'environnement",
"log": "Логи", "log": "Journaux",
"warning.click-to-connect": "Пожалуйста, сначала нажмите на $1 слева для подключения", "warning.click-to-connect": "Veuillez d'abord cliquer sur $1 à gauche pour vous connecter",
"reset": "Сброс", "reset": "Réinitialiser",
"read-resource": "Чтение ресурсов", "read-resource": "Lire les ressources",
"enter": "Ввод", "enter": "Entrée",
"blank-test": "Пустой тест", "blank-test": "Test vide",
"connect.appearance.reconnect": "Переподключение", "connect.appearance.reconnect": "Reconnexion",
"connect.appearance.connect": "Соединение", "connect.appearance.connect": "Connexion",
"response": "Ответ", "response": "Réponse",
"refresh": "Обновить", "refresh": "Rafraîchir",
"read-prompt": "Чтение подсказки", "read-prompt": "Lire l'invite",
"execute-tool": "Запуск", "execute-tool": "Exécuter",
"save": "Сохранить", "save": "Enregistrer",
"send": "Отправить", "send": "Envoyer",
"server-not-support-statistic": "Используемый вами поставщик временно не поддерживает статистику", "server-not-support-statistic": "Le fournisseur que vous utilisez ne prend pas en charge les statistiques temporairement",
"answer-at": "Ответ дан", "answer-at": "Répondu le",
"input-token": "Ввод", "input-token": "Entrée",
"output-token": "Вывод", "output-token": "Sortie",
"total": "Итого", "total": "Total",
"cache-hit-ratio": "Коэффициент попаданий в кэш", "cache-hit-ratio": "Taux de réussite du cache",
"success-save": "Успешно сохранено", "success-save": "Enregistré avec succès",
"confirm-delete-model": "Вы уверены, что хотите удалить поставщика моделей?", "confirm-delete-model": "Êtes-vous sûr de vouloir supprimer le fournisseur de modèles ?",
"reserve-one-last-model": "Оставьте хотя бы одну модель", "reserve-one-last-model": "Conservez au moins un modèle",
"edit": "Редактировать", "edit": "Modifier",
"delete": "Удалить", "delete": "Supprimer",
"test": "Тест", "test": "Test",
"add-new-server": "Добавить услугу", "add-new-server": "Ajouter un service",
"choose-model": "Выбрать модель", "choose-model": "Sélectionner le modèle",
"system-prompt": "Системная подсказка", "system-prompt": "Invite système",
"tool-use": "Использование инструмента", "tool-use": "Utilisation d'outils",
"websearch": "Поиск в Интернете", "websearch": "Recherche sur Internet",
"temperature-parameter": "Температурный параметр", "temperature-parameter": "Paramètre de température",
"context-length": "Длина контекста", "context-length": "Longueur du contexte",
"system-prompt.placeholder": "Введите системный запрос (например: Вы профессиональный помощник по фронтенд-разработке, отвечайте на русском)", "system-prompt.placeholder": "Entrez l'invite système (par exemple : Vous êtes un assistant professionnel de développement front-end, répondez en français)",
"precise": "Точный", "precise": "Précis",
"moderate": "Баланс", "moderate": "Équilibre",
"creative": "Творчество", "creative": "Créativité",
"single-dialog": "Однораундовый диалог", "single-dialog": "Dialogue en un tour",
"multi-dialog": "Многораундовый разговор", "multi-dialog": "Conversation multi-tours",
"press-and-run": "Введите вопрос, чтобы начать тест", "press-and-run": "Tapez une question pour commencer le test",
"connect-sigature": "Подпись соединения", "connect-sigature": "Signature de connexion",
"finish-refresh": "Обновление завершено", "finish-refresh": "Actualisation terminée",
"add-system-prompt.name-placeholder": "Заголовок пользовательского prompt", "add-system-prompt.name-placeholder": "Titre de l'invite personnalisée",
"enter-message-dot": "Введите сообщение...", "enter-message-dot": "Entrez un message...",
"generate-answer": "Генерация ответа", "generate-answer": "Génération de la réponse",
"choose-presetting": "Выбрать预设", "choose-presetting": "Sélectionner un préréglage",
"cwd": "Каталог выполнения", "cwd": "Répertoire d'exécution",
"mcp-server-timeout": "Максимальное время вызова инструмента MCP", "mcp-server-timeout": "Temps d'appel maximum de l'outil MCP",
"return": "Назад", "return": "Retour",
"error": "Ошибка", "error": "Erreur",
"feedback": "Обратная связь", "feedback": "Retour",
"waiting-mcp-server": "Ожидание ответа от сервера MCP", "waiting-mcp-server": "En attente de la réponse du serveur MCP",
"parallel-tool-calls": "Разрешить модели вызывать несколько инструментов в одном ответе", "parallel-tool-calls": "Permettre au modèle d'appeler plusieurs outils en une seule réponse",
"proxy-server": "Прокси-сервер", "proxy-server": "Serveur proxy",
"update-model-list": "Обновить список моделей", "update-model-list": "Mettre à jour la liste des modèles",
"ensure-delete-connection": "Вы уверены, что хотите удалить соединение $1?", "preset-env-sync.success": "Variables d'environnement prédéfinies synchronisées",
"choose-connection-type": "Пожалуйста, выберите тип подключения", "preset-env-sync.fail": "Échec de la synchronisation des variables d'environnement prédéfinies",
"please-enter-connection-command": "Пожалуйста, введите команду подключения", "drag-to-fill-connect-parameters": "Faites glisser pour remplir les paramètres de connexion",
"example-mcp-run": "Например: mcp run main.py", "connect-success": "Connexion réussie",
"please-enter-cwd": "Пожалуйста, введите рабочий каталог (cwd), необязательно", "connect-fail": "Échec de la connexion",
"please-enter-cwd-placeholder": "Например: /path/to/project", "preset": "Préréglage",
"please-enter-url": "Пожалуйста, введите URL-адрес подключения", "openmcp-document": "Documentation officielle d'OpenMCP",
"example-as": "Например:", "star-our-project": "Star notre projet",
"enter-optional-oauth": "Пожалуйста, введите токен OAuth, необязательно", "document": "Documentation officielle",
"quick-start": "Начало работы", "join-discussion": "Rejoindre le groupe de discussion",
"read-document": "Читать документацию", "comment-for-us": "Écrivez un avis pour nous !",
"report-issue": "Сообщить о проблеме", "openmcp-developed-by": "OpenMCP Client {version} développé par {author}",
"join-project": "Участвовать в проекте", "error-parse-json": "Erreur d'analyse JSON :",
"comment-plugin": "Плагин комментариев", "enable-xml-wrapper": "Включить обёртку XML-команд",
"preset-env-sync.success": "Предустановленные переменные среды синхронизированы", "tool-manage": "Управление инструментами",
"preset-env-sync.fail": "Не удалось синхронизировать предустановленные переменные среды", "enable-all-tools": "Активировать все инструменты",
"drag-to-fill-connect-parameters": "Перетащите, чтобы заполнить параметры подключения", "disable-all-tools": "Отключить все инструменты",
"connect-success": "Успешное подключение", "using-tool": "Использование инструмента",
"connect-fail": "Ошибка подключения", "copy-success": "Скопировано успешно",
"preset": "Предустановка", "copy-fail": "Ошибка копирования",
"openmcp-document": "Официальная документация OpenMCP", "copy": "Копировать",
"star-our-project": "Звезда нашего проекта", "export": "Экспорт",
"document": "Официальная документация", "export-filename": "Имя экспортируемого файла",
"join-discussion": "Присоединиться к дискуссионной группе", "how-to-use": "Как использовать?"
"comment-for-us": "Напишите отзыв для нас!",
"openmcp-developed-by": "OpenMCP Client {version} разработан {author}",
"error-parse-json": "Ошибка разбора JSON:"
} }

View File

@ -171,5 +171,16 @@
"join-discussion": "加入讨论群", "join-discussion": "加入讨论群",
"comment-for-us": "为我们撰写评价!", "comment-for-us": "为我们撰写评价!",
"openmcp-developed-by": "OpenMCP Client {version} 由 {author} 开发", "openmcp-developed-by": "OpenMCP Client {version} 由 {author} 开发",
"error-parse-json": "JSON 解析错误:" "error-parse-json": "JSON 解析错误:",
"enable-xml-wrapper": "开启 XML 指令包裹",
"tool-manage": "工具管理",
"enable-all-tools": "激活所有工具",
"disable-all-tools": "禁用所有工具",
"using-tool": "正在使用工具",
"copy-success": "复制成功",
"copy-fail": "复制失败",
"copy": "复制",
"export": "导出",
"export-filename": "导出文件名",
"how-to-use": "如何使用?"
} }

View File

@ -1,189 +1,186 @@
{ {
"module": "模塊", "module": "Module",
"signal": "信號", "signal": "Signal",
"search-signal": "搜索信號", "search-signal": "Rechercher un signal",
"language-setting": "語言", "language-setting": "Langue",
"search-setting": "搜索", "search-setting": "Recherche",
"search-case-sensitivity": "區分大小寫", "search-case-sensitivity": "Sensibilité à la casse",
"search-mode": "搜索模式", "search-mode": "Mode de recherche",
"search-scope": "搜索範圍", "search-scope": "Portée de la recherche",
"search-display-parent-only": "只展示父模塊", "search-display-parent-only": "Afficher uniquement le module parent",
"search-nothing": "沒有找到任何信號", "search-nothing": "Aucun signal trouvé",
"signal-only": "信號", "signal-only": "Signal uniquement",
"module-only": "模塊", "module-only": "Module uniquement",
"signal-module": "信號 + 模塊", "signal-module": "Signal + Module",
"general-setting": "通用", "general-setting": "Général",
"appearance-setting": "外觀", "appearance-setting": "Apparence",
"display-wave-height": "波形軌道的高度", "display-wave-height": "Hauteur de la piste d'onde",
"display-signal-info-scope": "側邊欄展示信息", "display-signal-info-scope": "Informations affichées dans la barre latérale",
"display-signal-info-scope.width": "位寬", "display-signal-info-scope.width": "Largeur de bit",
"display-signal-info-scope.parent": "所屬模塊名", "display-signal-info-scope.parent": "Nom du module parent",
"wavecolor": "默認波形顏色", "wavecolor": "Couleur d'onde par défaut",
"wavecolor.normal-bit": "單位寬波形", "wavecolor.normal-bit": "Onde à largeur unique",
"wavecolor.normal-vec": "多位寬波形", "wavecolor.normal-vec": "Onde à largeur multiple",
"wavecolor.high-impedance": "高阻態波形", "wavecolor.high-impedance": "Onde à haute impédance",
"wavecolor.unknown": "未知態波形", "wavecolor.unknown": "Onde à état inconnu",
"operation-setting": "操作", "operation-setting": "Opérations",
"render-setting": "渲染", "render-setting": "Rendu",
"render-animation": "開啟渲染動畫", "render-animation": "Activer l'animation de rendu",
"usermanual": "使用說明", "usermanual": "Manuel utilisateur",
"usermanual.left-right-scroll.caption": "上下移動", "usermanual.left-right-scroll.caption": "Déplacer vers le haut et le bas",
"usermanual.up-down-scroll.caption": "左右移動", "usermanual.up-down-scroll.caption": "Déplacer vers la gauche et la droite",
"usermanual.xscale.caption": "橫向縮放", "usermanual.xscale.caption": "Zoom horizontal",
"loading": "加載中", "loading": "Chargement",
"context-menu.create-group": "新建組", "context-menu.create-group": "Créer un groupe",
"context-menu.join-group": "加入已有分組", "context-menu.join-group": "Rejoindre un groupe existant",
"context-menu.change-color": "修改顏色", "context-menu.change-color": "Changer la couleur",
"context-menu.delete": "刪除信號", "context-menu.delete": "Supprimer le signal",
"context-menu.delete-all-select": "刪除所有選中信號", "context-menu.delete-all-select": "Supprimer tous les signaux sélectionnés",
"context-menu.signal.name": "信號名稱", "context-menu.signal.name": "Nom du signal",
"context-menu.signal.type": "信號類型", "context-menu.signal.type": "Type de signal",
"context-menu.signal.width": "信號寬度", "context-menu.signal.width": "Largeur du signal",
"context-menu.signal.dep": "依賴關係", "context-menu.signal.dep": "Dépendances",
"context-menu.group.cancel": "取消分組", "context-menu.group.cancel": "Annuler le groupement",
"context-menu.group.delete": "刪除分組", "context-menu.group.delete": "Supprimer le groupe",
"context-menu.group.empty": "當前沒有可用的分組", "context-menu.group.empty": "Aucun groupe disponible",
"context-menu.group.uname-group": "未命名分組", "context-menu.group.uname-group": "Groupe sans nom",
"toolbar.modal.common-digital": "數字", "toolbar.modal.common-digital": "Numérique",
"toolbar.modal.ladder-analog": "模擬(階梯)", "toolbar.modal.ladder-analog": "Analogique (échelle)",
"toolbar.modal.line-analog": "模擬(折線)", "toolbar.modal.line-analog": "Analogique (ligne)",
"toolbar.search.name": "名稱", "toolbar.search.name": "Nom",
"toolbar.search.value": "值", "toolbar.search.value": "Valeur",
"toolbar.format.category.base": "基礎", "toolbar.format.category.base": "Base",
"toolbar.format.category.dec": "十進制", "toolbar.format.category.dec": "Décimal",
"toolbar.format.category.float": "浮點數", "toolbar.format.category.float": "Virgule flottante",
"toolbar.format.signed": "有符號", "toolbar.format.signed": "Signé",
"toolbar.format.unsigned": "無符號", "toolbar.format.unsigned": "Non signé",
"toolbar.format.half": "半精度16bit", "toolbar.format.half": "Demi-précision (16 bits)",
"toolbar.format.float": "單精度32bit", "toolbar.format.float": "Simple précision (32 bits)",
"toolbar.format.double": "雙精度64bit", "toolbar.format.double": "Double précision (64 bits)",
"toolbar.location.to-begin": "移動至開頭", "toolbar.location.to-begin": "Déplacer au début",
"toolbar.location.to-end": "移動至結尾", "toolbar.location.to-end": "Déplacer à la fin",
"toolbar.location.to-next-change": "前往下一個變化的邊沿", "toolbar.location.to-next-change": "Aller au prochain changement",
"toolbar.location.to-prev-change": "前往上一個變化的邊沿", "toolbar.location.to-prev-change": "Aller au changement précédent",
"toolbar.location.make-location": "創建新的信標", "toolbar.location.make-location": "Créer un nouveau repère",
"toolbar.location.clear-location": "清除所有信標", "toolbar.location.clear-location": "Effacer tous les repères",
"toolbar.location.clear-location-dialog": "您確定要清除所有的信標嗎?", "toolbar.location.clear-location-dialog": "Êtes-vous sûr de vouloir effacer tous les repères?",
"context-menu.cannot-join-repeat-group": "當前信號已在此分組中", "context-menu.cannot-join-repeat-group": "Le signal actuel est déjà dans ce groupe",
"toolbar.no-result": "無結果", "toolbar.no-result": "Aucun résultat",
"toolbar.search.value.already-to-head": "已經到開頭了", "toolbar.search.value.already-to-head": "Déjà au début",
"toolbar.search.value.already-to-tail": "已經到結尾了", "toolbar.search.value.already-to-tail": "Déjà à la fin",
"toolbar.search.value.searching": "搜索中", "toolbar.search.value.searching": "Recherche en cours",
"pivot.context.delete": "刪除信標", "pivot.context.delete": "Supprimer le repère",
"pivot.context.display-axis": "創建相對坐標軸", "pivot.context.display-axis": "Créer un axe relatif",
"pivot.context.cancel-axis": "取消相對坐標軸", "pivot.context.cancel-axis": "Annuler l'axe relatif",
"setting.appearance.pivot-color": "信標顏色", "setting.appearance.pivot-color": "Couleur du repère",
"setting.appearance.moving-pivot": "移動信標", "setting.appearance.moving-pivot": "Repère mobile",
"setting.appearance.user-pivot": "用戶信標", "setting.appearance.user-pivot": "Repère utilisateur",
"setting.appearance.system-pivot": "系統信標", "setting.appearance.system-pivot": "Repère système",
"confirm": "確定", "confirm": "Confirmer",
"cancel": "取消", "cancel": "Annuler",
"tips": "提示", "tips": "Conseils",
"filemenu.save-view": "保存視圖文件", "filemenu.save-view": "Enregistrer le fichier de vue",
"filemenu.save-as-view": "另存為視圖文件", "filemenu.save-as-view": "Enregistrer la vue sous",
"filemenu.load-view": "導入視圖文件", "filemenu.load-view": "Charger le fichier de vue",
"filemenu.auto-save": "自動保存", "filemenu.auto-save": "Sauvegarde automatique",
"current-version": "當前版本", "current-version": "Version actuelle",
"setting.language.change-dialog": "您已將語言更改為 {0},我們建議您重新啟動 Vcd Viewer。", "setting.language.change-dialog": "Vous avez changé la langue en {0}, nous vous recommandons de redémarrer Vcd Viewer.",
"resources": "資源", "resources": "Ressources",
"tools": "工具", "tools": "Outils",
"prompts": "提示", "prompts": "Invites",
"interaction-test": "交互測試", "interaction-test": "Test interactif",
"setting": "設定", "setting": "Paramètres",
"about": "關於", "about": "À propos",
"connected": "已連線", "connected": "Connecté",
"disconnected": "已斷開連接", "disconnected": "Déconnecté",
"debug": "偵錯", "debug": "Déboguer",
"connect": "連接", "connect": "Connexion",
"setting.general-color-setting": "通用顏色設定", "setting.general-color-setting": "Paramètres de couleur généraux",
"choose-a-project-debug": "選擇一個項目進行調試", "choose-a-project-debug": "Sélectionnez un projet à déboguer",
"model": "模型", "model": "Modèle",
"server-provider": "服務提供者", "server-provider": "Fournisseur de services",
"api-root-url": "API 根路徑", "api-root-url": "Chemin racine de l'API",
"api-token": "API 密鑰", "api-token": "Clé API",
"connection-method": "連接方式", "connection-method": "Méthode de connexion",
"command": "命令", "command": "Commande",
"env-var": "環境變數", "env-var": "Variables d'environnement",
"log": "日誌", "log": "Journaux",
"warning.click-to-connect": "請先點擊左側的 $1 進行連接", "warning.click-to-connect": "Veuillez d'abord cliquer sur $1 à gauche pour vous connecter",
"reset": "重置", "reset": "Réinitialiser",
"read-resource": "讀取資源", "read-resource": "Lire les ressources",
"enter": "輸入", "enter": "Entrée",
"blank-test": "空白測試", "blank-test": "Test vide",
"connect.appearance.reconnect": "重新連線", "connect.appearance.reconnect": "Reconnexion",
"connect.appearance.connect": "連接", "connect.appearance.connect": "Connexion",
"response": "響應", "response": "Réponse",
"refresh": "重新整理", "refresh": "Rafraîchir",
"read-prompt": "讀取提示", "read-prompt": "Lire l'invite",
"execute-tool": "執行", "execute-tool": "Exécuter",
"save": "儲存", "save": "Enregistrer",
"send": "傳送", "send": "Envoyer",
"server-not-support-statistic": "你使用的供應商暫時不支持統計信息", "server-not-support-statistic": "Le fournisseur que vous utilisez ne prend pas en charge les statistiques temporairement",
"answer-at": "作答於", "answer-at": "Répondu le",
"input-token": "輸入", "input-token": "Entrée",
"output-token": "輸出", "output-token": "Sortie",
"total": "總計", "total": "Total",
"cache-hit-ratio": "緩存命中率", "cache-hit-ratio": "Taux de réussite du cache",
"success-save": "成功儲存", "success-save": "Enregistré avec succès",
"confirm-delete-model": "確定刪除該模型提供商?", "confirm-delete-model": "Êtes-vous sûr de vouloir supprimer le fournisseur de modèles ?",
"reserve-one-last-model": "至少保留一個模型", "reserve-one-last-model": "Conservez au moins un modèle",
"edit": "編輯", "edit": "Modifier",
"delete": "刪除", "delete": "Supprimer",
"test": "測試", "test": "Test",
"add-new-server": "新增服務", "add-new-server": "Ajouter un service",
"choose-model": "選擇模型", "choose-model": "Sélectionner le modèle",
"system-prompt": "系統提示詞", "system-prompt": "Invite système",
"tool-use": "工具使用", "tool-use": "Utilisation d'outils",
"websearch": "網路搜尋", "websearch": "Recherche sur Internet",
"temperature-parameter": "溫度參數", "temperature-parameter": "Paramètre de température",
"context-length": "上下文長度", "context-length": "Longueur du contexte",
"system-prompt.placeholder": "輸入系統提示詞(例如:你是一個專業的前端開發助手,用中文回答)", "system-prompt.placeholder": "Entrez l'invite système (par exemple : Vous êtes un assistant professionnel de développement front-end, répondez en français)",
"precise": "精確", "precise": "Précis",
"moderate": "平衡", "moderate": "Équilibre",
"creative": "創意", "creative": "Créativité",
"single-dialog": "單輪對話", "single-dialog": "Dialogue en un tour",
"multi-dialog": "多輪對話", "multi-dialog": "Conversation multi-tours",
"press-and-run": "輸入問題以開始測試", "press-and-run": "Tapez une question pour commencer le test",
"connect-sigature": "連接簽名", "connect-sigature": "Signature de connexion",
"finish-refresh": "刷新完成", "finish-refresh": "Actualisation terminée",
"add-system-prompt.name-placeholder": "自定義提示的標題", "add-system-prompt.name-placeholder": "Titre de l'invite personnalisée",
"enter-message-dot": "輸入訊息...", "enter-message-dot": "Entrez un message...",
"generate-answer": "正在生成答案", "generate-answer": "Génération de la réponse",
"choose-presetting": "選擇預設", "choose-presetting": "Sélectionner un préréglage",
"cwd": "執行目錄", "cwd": "Répertoire d'exécution",
"mcp-server-timeout": "MCP工具最長調用時間", "mcp-server-timeout": "Temps d'appel maximum de l'outil MCP",
"return": "返回", "return": "Retour",
"error": "錯誤", "error": "Erreur",
"feedback": "反饋", "feedback": "Retour",
"waiting-mcp-server": "等待MCP伺服器響應", "waiting-mcp-server": "En attente de la réponse du serveur MCP",
"parallel-tool-calls": "允許模型在單輪回覆中調用多個工具", "parallel-tool-calls": "Permettre au modèle d'appeler plusieurs outils en une seule réponse",
"proxy-server": "代理伺服器", "proxy-server": "Serveur proxy",
"update-model-list": "更新模型列表", "update-model-list": "Mettre à jour la liste des modèles",
"ensure-delete-connection": "確定要刪除連接 $1 嗎?", "preset-env-sync.success": "Variables d'environnement prédéfinies synchronisées",
"choose-connection-type": "請選擇連接類型", "preset-env-sync.fail": "Échec de la synchronisation des variables d'environnement prédéfinies",
"please-enter-connection-command": "請輸入連接命令", "drag-to-fill-connect-parameters": "Faites glisser pour remplir les paramètres de connexion",
"example-mcp-run": "例如: mcp run main.py", "connect-success": "Connexion réussie",
"please-enter-cwd": "請輸入工作目錄 (cwd),可選", "connect-fail": "Échec de la connexion",
"please-enter-cwd-placeholder": "例如: /path/to/project", "preset": "Préréglage",
"please-enter-url": "請輸入連接的 URL", "openmcp-document": "Documentation officielle d'OpenMCP",
"example-as": "例如:", "star-our-project": "Star notre projet",
"enter-optional-oauth": "請輸入 OAuth 令牌,可選", "document": "Documentation officielle",
"quick-start": "入門", "join-discussion": "Rejoindre le groupe de discussion",
"read-document": "閱讀文件", "comment-for-us": "Écrivez un avis pour nous !",
"report-issue": "報告問題", "openmcp-developed-by": "OpenMCP Client {version} développé par {author}",
"join-project": "參與專案", "error-parse-json": "Erreur d'analyse JSON :",
"comment-plugin": "評論插件", "enable-xml-wrapper": "開啟 XML 指令包裹",
"preset-env-sync.success": "預設環境變數同步完成", "tool-manage": "工具管理",
"preset-env-sync.fail": "預設環境變數同步失敗", "enable-all-tools": "啟用所有工具",
"drag-to-fill-connect-parameters": "拖曳以填充連接參數", "disable-all-tools": "禁用所有工具",
"connect-success": "連接成功", "using-tool": "正在使用工具",
"connect-fail": "連接失敗", "copy-success": "複製成功",
"preset": "預設", "copy-fail": "複製失敗",
"openmcp-document": "OpenMCP 官方文件", "copy": "複製",
"star-our-project": "給我們的項目加星", "export": "匯出",
"document": "官方文件", "export-filename": "匯出檔案名稱",
"join-discussion": "加入討論群", "how-to-use": "如何使用?"
"comment-for-us": "為我們撰寫評價!",
"openmcp-developed-by": "OpenMCP Client {version} 由 {author} 開發",
"error-parse-json": "JSON解析錯誤:"
} }

View File

@ -47,7 +47,7 @@ import { useI18n } from 'vue-i18n';
const { t } = useI18n(); const { t } = useI18n();
const version = '0.1.5'; const version = '0.1.8';
const author = 'LSTM-Kirigaya (锦恢)'; const author = 'LSTM-Kirigaya (锦恢)';
defineComponent({ name: 'about' }); defineComponent({ name: 'about' });

View File

@ -67,7 +67,7 @@ async function connect() {
} }
const isDraging = ref(false); const isDraging = ref(false);
let dragHandler: number; let dragHandler: NodeJS.Timeout;
function handleDragOver(event: DragEvent) { function handleDragOver(event: DragEvent) {
event.preventDefault(); event.preventDefault();

View File

@ -29,7 +29,7 @@ export const connectionSelectDataViewOption: ConnectionTypeOptionItem[] = [
function prettifyMapKeys(keys: MapIterator<string>) { function prettifyMapKeys(keys: MapIterator<string>) {
const result: string[] = []; const result: string[] = [];
for (const key of keys) { for (const key of keys) {
result.push('+ ' +key); result.push('+ ' + key);
} }
return result.join('\n'); return result.join('\n');
} }
@ -197,7 +197,7 @@ export class McpClient {
const bridge = useMessageBridge(); const bridge = useMessageBridge();
const { code, msg } = await bridge.commandRequest<ToolsListResponse>('tools/list', { clientId: this.clientId }); const { code, msg } = await bridge.commandRequest<ToolsListResponse>('tools/list', { clientId: this.clientId });
if (code!== 200) { if (code !== 200) {
return new Map<string, ToolItem>(); return new Map<string, ToolItem>();
} }
@ -227,7 +227,7 @@ export class McpClient {
const { code, msg } = await bridge.commandRequest<PromptsListResponse>('prompts/list', { clientId: this.clientId }); const { code, msg } = await bridge.commandRequest<PromptsListResponse>('prompts/list', { clientId: this.clientId });
if (code!== 200) { if (code !== 200) {
return new Map<string, PromptTemplate>(); return new Map<string, PromptTemplate>();
} }
@ -252,7 +252,7 @@ export class McpClient {
const bridge = useMessageBridge(); const bridge = useMessageBridge();
const { code, msg } = await bridge.commandRequest<ResourcesListResponse>('resources/list', { clientId: this.clientId }); const { code, msg } = await bridge.commandRequest<ResourcesListResponse>('resources/list', { clientId: this.clientId });
if (code!== 200) { if (code !== 200) {
return new Map<string, Resources>(); return new Map<string, Resources>();
} }
@ -276,7 +276,7 @@ export class McpClient {
const bridge = useMessageBridge(); const bridge = useMessageBridge();
const { code, msg } = await bridge.commandRequest<ResourceTemplatesListResponse>('resources/templates/list', { clientId: this.clientId }); const { code, msg } = await bridge.commandRequest<ResourceTemplatesListResponse>('resources/templates/list', { clientId: this.clientId });
if (code!== 200) { if (code !== 200) {
return new Map(); return new Map();
} }
this.resourceTemplates = new Map<string, ResourceTemplate>(); this.resourceTemplates = new Map<string, ResourceTemplate>();
@ -455,19 +455,62 @@ export class McpClient {
}); });
} }
} }
// 添加资源刷新方法,支持超时控制
public async refreshAllResources(timeoutMs = 30000): Promise<void> {
const controller = new AbortController();
const signal = controller.signal;
// 设置超时
const timeoutId = setTimeout(() => {
controller.abort();
console.error(`[REFRESH TIMEOUT] Client ${this.clientId}`);
}, timeoutMs);
try {
console.log(`[REFRESH START] Client ${this.clientId}`);
// 按顺序刷新资源
await this.getTools({ cache: false });
await this.getPromptTemplates({ cache: false });
await this.getResources({ cache: false });
await this.getResourceTemplates({ cache: false });
console.log(chalk.gray(`[${new Date().toLocaleString()}]`),
chalk.green(`🚀 [${this.name}] REFRESH COMPLETE`));
} catch (error) {
if (signal.aborted) {
throw new Error(`Refresh timed out after ${timeoutMs}ms`);
}
console.error(`[REFRESH ERROR] Client ${this.clientId}:`, error);
console.error(
chalk.gray(`[${new Date().toLocaleString()}]`),
chalk.red(`🚀 [${this.name}] REFRESH FAILED`),
error
);
throw error;
} finally {
clearTimeout(timeoutId);
}
}
} }
class McpClientAdapter { class McpClientAdapter {
public clients: Reactive<McpClient[]> = []; public clients: Reactive<McpClient[]> = [];
public currentClientIndex: number = 0; public currentClientIndex: number = 0;
public refreshSignal = reactive({ value: 0 });
private defaultClient: McpClient = new McpClient(); private defaultClient: McpClient = new McpClient();
public connectLogListenerCancel: (() => void) | null = null; public connectLogListenerCancel: (() => void) | null = null;
public connectrefreshListener: (() => void) | null = null;
constructor( constructor(
public platform: string public platform: string
) { } ) {
if (platform !== 'nodejs') {
this.addConnectRefreshListener();
}
}
/** /**
* @description * @description
@ -511,6 +554,50 @@ class McpClientAdapter {
}); });
} }
private findClientIndexByUuid(uuid: string): number {
// 检查客户端数组是否存在且不为空
if (!this.clients || this.clients.length === 0) {
return -1;
}
const index = this.clients.findIndex(client => client.clientId === uuid);
return index;
}
/**
* @description register HMR
*/
public addConnectRefreshListener() {
// 创建对于 connect/refresh 的监听
if (!this.connectrefreshListener) {
const bridge = useMessageBridge();
this.connectrefreshListener = bridge.addCommandListener('connect/refresh', async (message) => {
const { code, msg } = message;
console.log('refresh');
if (code === 200) {
// 查找目标客户端
const clientIndex = this.findClientIndexByUuid(msg.uuid);
if (clientIndex > -1) {
// 刷新该客户端的所有资源
console.log('clientIndex', clientIndex);
await this.clients[clientIndex].refreshAllResources();
this.refreshSignal.value++;
} else {
console.error(
chalk.gray(`[${new Date().toLocaleString()}]`),
chalk.red(`No client found with ID: ${msg.uuid}`),
);
}
}
}, { once: false });
}
}
public async launch() { public async launch() {
// 创建对于 log/output 的监听 // 创建对于 log/output 的监听
if (!this.connectLogListenerCancel) { if (!this.connectLogListenerCancel) {
@ -525,7 +612,7 @@ class McpClientAdapter {
} }
client.connectionResult.logString.push({ client.connectionResult.logString.push({
type: code === 200 ? 'info': 'error', type: code === 200 ? 'info' : 'error',
title: msg.title, title: msg.title,
message: msg.message message: msg.message
}); });
@ -599,7 +686,7 @@ class McpClientAdapter {
return msg; return msg;
} }
public async readPromptTemplate(promptId: string, args: Record<string, any>) { public async readPromptTemplate(promptId: string, args?: Record<string, any>) {
// TODO: 如果遇到不同服务器的同名 tool请拓展解决方案 // TODO: 如果遇到不同服务器的同名 tool请拓展解决方案
// 目前只找到第一个匹配 toolName 的工具进行调用 // 目前只找到第一个匹配 toolName 的工具进行调用
let clientId = this.clients[0].clientId; let clientId = this.clients[0].clientId;

View File

@ -37,6 +37,7 @@ export async function makeSimpleTalk() {
enableTools: [], enableTools: [],
enableWebSearch: false, enableWebSearch: false,
contextLength: 5, contextLength: 5,
enableXmlWrapper: false,
parallelToolCalls: true parallelToolCalls: true
} }
}; };

View File

@ -237,13 +237,22 @@ async function updateModels() {
const proxyServer = mcpSetting.proxyServer; const proxyServer = mcpSetting.proxyServer;
const bridge = useMessageBridge(); const bridge = useMessageBridge();
const { code, msg } = await bridge.commandRequest('llm/models', {
apiKey,
baseURL,
proxyServer
});
// OpenRouter
let result;
if (llm.isDynamic && llm.id === 'openrouter') {
result = await bridge.commandRequest('llm/models/openrouter', {});
} else {
result = await bridge.commandRequest('llm/models', {
apiKey,
baseURL,
proxyServer
});
}
const { code, msg } = result;
const isGemini = baseURL.includes('googleapis'); const isGemini = baseURL.includes('googleapis');
const isOpenRouter = llm.id === 'openrouter';
if (code === 200 && Array.isArray(msg)) { if (code === 200 && Array.isArray(msg)) {
const models = msg const models = msg
@ -257,9 +266,27 @@ async function updateModels() {
}); });
llm.models = models; llm.models = models;
// OpenRouter
if (isOpenRouter && !llm.userModel && models.length > 0) {
// GPT-4
const recommendedModel = models.find(model =>
model.includes('gpt-4') ||
model.includes('claude') ||
model.includes('gemini')
) || models[0];
llm.userModel = recommendedModel;
}
saveLlmSetting(); saveLlmSetting();
if (isOpenRouter) {
ElMessage.success(`已更新 ${models.length} 个 OpenRouter 模型`);
} else {
ElMessage.success('模型列表更新成功');
}
} else { } else {
ElMessage.error('模型列表更新失败' + msg); ElMessage.error('模型列表更新失败: ' + msg);
} }
updateModelLoading.value = false; updateModelLoading.value = false;
} }

View File

@ -4,18 +4,42 @@
<span class="iconfont icon-llm"></span> <span class="iconfont icon-llm"></span>
<span class="option-title">{{ t('model') }}</span> <span class="option-title">{{ t('model') }}</span>
</span> </span>
<div style="width: 160px;"> <div style="width: 240px;">
<el-select v-if="llms[llmManager.currentModelIndex]" <el-select v-if="llms[llmManager.currentModelIndex]"
name="language-setting" name="model-setting"
v-model="llms[llmManager.currentModelIndex].userModel" v-model="llms[llmManager.currentModelIndex].userModel"
@change="onmodelchange" @change="onmodelchange"
filterable
:placeholder="getPlaceholderText()"
:reserve-keyword="false"
size="default"
:popper-class="isOpenRouter ? 'openrouter-select-dropdown' : ''"
> >
<el-option <el-option
v-for="option in llms[llmManager.currentModelIndex].models" v-for="option in llms[llmManager.currentModelIndex].models"
:value="option" :value="option"
:label="option" :label="option"
:key="option" :key="option"
></el-option> >
<div v-if="isOpenRouter" class="openrouter-model-option">
<div class="model-info">
<span class="model-name">{{ getModelDisplayName(option) }}</span>
<span class="model-provider">{{ getModelProvider(option) }}</span>
</div>
<span v-if="getModelBadge(option)" class="model-badge">{{ getModelBadge(option) }}</span>
</div>
<span v-else class="regular-model-option">{{ option }}</span>
</el-option>
<!-- 当没有搜索结果时显示 -->
<el-option v-if="filteredModels.length === 0 && searchKeyword"
value=""
disabled
class="search-result-info">
<div class="search-empty">
<span>{{ `找到 0 个模型匹配 "${searchKeyword}"` }}</span>
</div>
</el-option>
</el-select> </el-select>
</div> </div>
</div> </div>
@ -46,7 +70,7 @@
<script setup lang="ts"> <script setup lang="ts">
/* eslint-disable */ /* eslint-disable */
import { defineComponent } from 'vue'; import { defineComponent, computed, ref, watch } from 'vue';
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
import { llmManager, llms } from './llm'; import { llmManager, llms } from './llm';
import { pinkLog } from './util'; import { pinkLog } from './util';
@ -57,6 +81,29 @@ defineComponent({ name: 'connect-interface-openai' });
const { t } = useI18n(); const { t } = useI18n();
//
const searchKeyword = ref('');
// OpenRouter
const isOpenRouter = computed(() => {
return llms[llmManager.currentModelIndex]?.id === 'openrouter';
});
//
const filteredModels = computed(() => {
if (!llms[llmManager.currentModelIndex]) return [];
const models = llms[llmManager.currentModelIndex].models;
if (!searchKeyword.value) return models;
const keyword = searchKeyword.value.toLowerCase();
return models.filter(model =>
model.toLowerCase().includes(keyword) ||
getModelDisplayName(model).toLowerCase().includes(keyword) ||
getModelProvider(model).toLowerCase().includes(keyword)
);
});
function saveLlmSetting() { function saveLlmSetting() {
saveSetting(() => { saveSetting(() => {
ElMessage({ ElMessage({
@ -71,7 +118,140 @@ function onmodelchange() {
saveLlmSetting(); saveLlmSetting();
} }
//
function filterModels(query: string) {
searchKeyword.value = query;
// false Element Plus 使 filteredModels
return false;
}
//
function getPlaceholderText() {
if (isOpenRouter.value) {
const modelCount = llms[llmManager.currentModelIndex]?.models?.length || 0;
return `搜索 ${modelCount} 个模型... (可输入模型名或提供商)`;
}
return '选择模型';
}
//
function getModelDisplayName(modelId: string) {
if (!modelId.includes('/')) return modelId;
return modelId.split('/')[1] || modelId;
}
//
function getModelProvider(modelId: string) {
if (!modelId.includes('/')) return '';
return modelId.split('/')[0];
}
// free
function getModelBadge(modelId: string) {
if (modelId.includes(':free')) return 'FREE';
if (modelId.includes(':thinking')) return 'THINKING';
if (modelId.includes(':beta')) return 'BETA';
if (modelId.includes('preview')) return 'PREVIEW';
if (modelId.includes('exp')) return 'EXP';
return '';
}
</script> </script>
<style> <style scoped>
/* OpenRouter 模型选项样式 */
.openrouter-model-option {
display: flex;
justify-content: space-between;
align-items: center;
width: 100%;
padding: 4px 0;
}
.model-info {
display: flex;
flex-direction: column;
flex: 1;
min-width: 0;
}
.model-name {
font-size: 14px;
font-weight: 500;
color: var(--el-text-color-primary, #303133);
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.model-provider {
font-size: 12px;
color: var(--el-text-color-regular, #606266);
margin-top: 2px;
}
.model-badge {
font-size: 10px;
padding: 2px 6px;
border-radius: 4px;
background-color: #f0f9ff;
color: #0369a1;
font-weight: 600;
margin-left: 8px;
white-space: nowrap;
}
/* 普通模型选项样式 */
.regular-model-option {
font-size: 14px;
color: var(--el-text-color-primary, #303133);
font-weight: 500;
}
/* 搜索为空时的样式 */
.search-empty {
text-align: center;
color: var(--el-text-color-secondary, #909399);
font-size: 13px;
padding: 12px 0;
}
/* 下拉框样式优化 */
:global(.openrouter-select-dropdown) {
max-height: 300px !important;
}
:global(.openrouter-select-dropdown .el-select-dropdown__item) {
height: auto !important;
padding: 8px 12px !important;
line-height: normal !important;
}
/* 搜索框样式 */
:global(.el-select .el-input .el-input__wrapper) {
transition: all 0.3s ease;
}
:global(.el-select .el-input.is-focus .el-input__wrapper) {
box-shadow: 0 0 0 1px var(--el-color-primary, #409eff) inset;
}
/* 暗色主题下的文字优化 */
@media (prefers-color-scheme: dark) {
.model-name {
color: #e5eaf3 !important;
}
.model-provider {
color: #a3a6ad !important;
}
.regular-model-option {
color: #e5eaf3 !important;
}
.search-empty {
color: #8b949e !important;
}
}
</style> </style>

View File

@ -22,6 +22,9 @@ export interface BasicLlmDescription {
website: string, website: string,
userToken: string, userToken: string,
userModel: string, userModel: string,
isDynamic?: boolean,
modelsEndpoint?: string,
supportsPricing?: boolean,
[key: string]: any [key: string]: any
} }

View File

@ -32,8 +32,8 @@ export default defineConfig({
lib: { lib: {
entry: resolve(__dirname, '..', 'renderer/src/components/main-panel/chat/core/task-loop.ts'), entry: resolve(__dirname, '..', 'renderer/src/components/main-panel/chat/core/task-loop.ts'),
name: 'TaskLoop', name: 'TaskLoop',
fileName: 'task-loop', fileName: (format) => `task-loop.js`,
formats: ['cjs'] formats: ['es']
}, },
outDir: resolve(__dirname, '..', 'openmcp-sdk'), outDir: resolve(__dirname, '..', 'openmcp-sdk'),
emptyOutDir: false, emptyOutDir: false,

View File

@ -2,123 +2,110 @@
<img src="./icons/openmcp-sdk.svg" height="200px"/> <img src="./icons/openmcp-sdk.svg" height="200px"/>
<h3>openmcp-sdk : 适用于 openmcp 的部署框架</h3> <h3>openmcp-sdk: Deployment Framework for OpenMCP</h3>
<h4>闪电般将您的 agent 从实验室部署到生产环境</h4> <h4>Lightning-fast deployment of your agent from lab to production</h4>
<a href="https://kirigaya.cn/openmcp" target="_blank" style="display: inline-block; padding: 8px 16px; background-color: #7D3FF8; color: white; border-radius: .5em; text-decoration: none;">📄 OpenMCP 官方文档</a> <a href="https://kirigaya.cn/openmcp" target="_blank" style="display: inline-block; padding: 8px 16px; background-color: #7D3FF8; color: white; border-radius: .5em; text-decoration: none;">📄 OpenMCP Official Documentation</a>
<a href="https://qm.qq.com/cgi-bin/qm/qr?k=C6ZUTZvfqWoI12lWe7L93cWa1hUsuVT0&jump_from=webapi&authKey=McW6B1ogTPjPDrCyGttS890tMZGQ1KB3QLuG4aqVNRaYp4vlTSgf2c6dMcNjMuBD" target="_blank" style="display: inline-block; padding: 8px 16px; background-color: #CB81DA; color: white; border-radius: .5em; text-decoration: none;">QQ 讨论群</a><a href="https://discord.gg/af5cfB9a" target="_blank" style="display: inline-block; padding: 8px 16px; background-color: rgb(84, 176, 84); color: white; border-radius: .5em; text-decoration: none; margin-left: 5px;">Discord频道</a> <a href="https://qm.qq.com/cgi-bin/qm/qr?k=C6ZUTZvfqWoI12lWe7L93cWa1hUsuVT0&jump_from=webapi&authKey=McW6B1ogTPjPDrCyGttS890tMZGQ1KB3QLuG4aqVNRaYp4vlTSgf2c6dMcNjMuBD" target="_blank" style="display: inline-block; padding: 8px 16px; background-color: #CB81DA; color: white; border-radius: .5em; text-decoration: none;">QQ Discussion Group</a><a href="https://discord.gg/af5cfB9a" target="_blank" style="display: inline-block; padding: 8px 16px; background-color: rgb(84, 176, 84); color: white; border-radius: .5em; text-decoration: none; margin-left: 5px;">Discord Channel</a>
</div> </div>
## Installation
## 安装
```bash ```bash
npm install openmcp-sdk npm install openmcp-sdk
``` ```
> 目前 openmcp-sdk 只支持 esm 模式的导入 > Currently, openmcp-sdk only supports ESM-style imports.
## 使用 ## Usage
文件名main.ts Filename: `main.ts`
```typescript ```typescript
import { TaskLoop } from 'openmcp-sdk/task-loop'; import { OmAgent } from 'openmcp-sdk/service/sdk';
import { TaskLoopAdapter } from 'openmcp-sdk/service';
async function main() {
// 创建适配器,负责通信和 mcp 连接
const adapter = new TaskLoopAdapter();
// 添加 mcp 服务器 // create Agent
adapter.addMcp({ const agent = new OmAgent();
connectionType: 'STDIO',
commandString: 'node index.js',
cwd: '~/projects/openmcp-tutorial/my-browser/dist'
});
// 创建事件循环驱动器, verbose 数值越高,输出的日志越详细 // Load configuration, which can be automatically generated after debugging with openmcp client
const taskLoop = new TaskLoop({ adapter, verbose: 1 }); agent.loadMcpConfig('./mcpconfig.json');
// 获取所有工具 // Read the debugged prompt
const tools = await taskLoop.getTools(); const prompt = await agent.getPrompt('hacknews', { topn: '5' });
// 配置改次事件循环使用的大模型 // Execute the task
taskLoop.setLlmConfig({ const res = await agent.ainvoke({ messages: prompt });
id: 'deepseek',
baseUrl: 'https://api.deepseek.com/v1',
userToken: process.env['DEEPSEEK_API_TOKEN'],
userModel: 'deepseek-chat'
});
// 创建当前事件循环对应的上下文,并且配置当前上下文的设置 console.log('⚙️ Agent Response', res);
const storage = { ```
messages: [],
settings: { `mcpconfig.json` can be generated from [openmcp client](https://github.com/LSTM-Kirigaya/openmcp-client) directly, you don't have to write it by yourself. Here is the example:
temperature: 0.7,
// 在本次对话使用所有工具 ```json
enableTools: tools, {
// 系统提示词 "version": "1.0.0",
systemPrompt: 'you are a clever bot', "namespace": "openmcp",
// 对话上下文的轮数 "mcpServers": {
contextLength: 20 "my-browser": {
"command": "mcp",
"args": [
"run",
"~/projects/openmcp-tutorial/crawl4ai-mcp/main.py"
],
"description": "A MCP for long-term memory support",
"prompts": [
"hacknews"
]
} }
}; },
"defaultLLM": {
// 本次发出的问题 "baseURL": "https://api.deepseek.com",
const message = 'hello world'; "apiToken": "sk-xxxxxxxxxxxxxx",
"model": "deepseek-chat"
// 开启事件循环 }
await taskLoop.start(storage, message);
// 打印上下文,最终的回答在 messages.at(-1) 中
const content = storage.messages.at(-1).content;
console.log('最终回答:', content);
} }
main();
``` ```
下面是可能的输出: Run your agent and get example output:
``` ```
[6/5/2025, 8:16:15 PM] 🚀 [my-browser] 0.1.0 connected [2025/6/20 20:47:31] 🚀 [crawl4ai-mcp] 1.9.1 connected
[6/5/2025, 8:16:15 PM] task loop enters a new epoch [2025/6/20 20:47:35] 🤖 Agent wants to use these tools get_web_markdown
[6/5/2025, 8:16:23 PM] task loop finish a epoch [2025/6/20 20:47:35] 🔧 using tool get_web_markdown
[6/5/2025, 8:16:23 PM] 🤖 llm wants to call these tools k_navigate [2025/6/20 20:47:39] ✓ use tools success
[6/5/2025, 8:16:23 PM] 🔧 calling tool k_navigate [2025/6/20 20:47:46] 🤖 Agent wants to use these tools get_web_markdown, get_web_markdown, get_web_markdown
[6/5/2025, 8:16:34 PM] × fail to call tools McpError: MCP error -32603: net::ERR_CONNECTION_RESET at https://towardsdatascience.com/tag/editors-pick/ [2025/6/20 20:47:46] 🔧 using tool get_web_markdown
[6/5/2025, 8:16:34 PM] task loop enters a new epoch [2025/6/20 20:47:48] ✓ use tools success
[6/5/2025, 8:16:40 PM] task loop finish a epoch [2025/6/20 20:47:48] 🔧 using tool get_web_markdown
[6/5/2025, 8:16:40 PM] 🤖 llm wants to call these tools k_navigate [2025/6/20 20:47:54] ✓ use tools success
[6/5/2025, 8:16:40 PM] 🔧 calling tool k_navigate [2025/6/20 20:47:54] 🔧 using tool get_web_markdown
[6/5/2025, 8:16:44 PM] ✓ call tools okey dockey success [2025/6/20 20:47:57] ✓ use tools success
[6/5/2025, 8:16:44 PM] task loop enters a new epoch
[6/5/2025, 8:16:57 PM] task loop finish a epoch ⚙️ Agent Response
[6/5/2025, 8:16:57 PM] 🤖 llm wants to call these tools k_evaluate ⌨️ Today's Tech Article Roundup
[6/5/2025, 8:16:57 PM] 🔧 calling tool k_evaluate
[6/5/2025, 8:16:57 PM] ✓ call tools okey dockey success 📌 How to Detect or Observe Passing Gravitational Waves?
[6/5/2025, 8:16:57 PM] task loop enters a new epoch Summary: This article explores the physics of gravitational waves, explaining their effects on space-time and how humans might perceive or observe this cosmic phenomenon.
[6/5/2025, 8:17:06 PM] task loop finish a epoch Author: ynoxinul
[6/5/2025, 8:17:06 PM] 🤖 llm wants to call these tools k_navigate, k_navigate Posted: 2 hours ago
[6/5/2025, 8:17:06 PM] 🔧 calling tool k_navigate Link: https://physics.stackexchange.com/questions/338912/how-would-a-passing-gravitational-wave-look-or-feel
[6/5/2025, 8:17:09 PM] ✓ call tools okey dockey success
[6/5/2025, 8:17:09 PM] 🔧 calling tool k_navigate 📌 Learn Makefile Tutorial
[6/5/2025, 8:17:12 PM] ✓ call tools okey dockey success Summary: A comprehensive Makefile tutorial for beginners and advanced users, covering basic syntax, variables, automatic rules, and advanced features to help developers manage project builds efficiently.
[6/5/2025, 8:17:12 PM] task loop enters a new epoch Author: dsego
[6/5/2025, 8:17:19 PM] task loop finish a epoch Posted: 4 hours ago
[6/5/2025, 8:17:19 PM] 🤖 llm wants to call these tools k_evaluate, k_evaluate Link: https://makefiletutorial.com/
[6/5/2025, 8:17:19 PM] 🔧 calling tool k_evaluate
[6/5/2025, 8:17:19 PM] ✓ call tools okey dockey success 📌 Hurl: Run and Test HTTP Requests in Plain Text
[6/5/2025, 8:17:19 PM] 🔧 calling tool k_evaluate Summary: Hurl is a command-line tool that allows defining and executing HTTP requests in plain text format, ideal for data fetching and HTTP session testing. It supports chained requests, value capture, and response queries, making it perfect for testing REST, SOAP, and GraphQL APIs.
[6/5/2025, 8:17:19 PM] ✓ call tools okey dockey success Author: flykespice
[6/5/2025, 8:17:19 PM] task loop enters a new epoch Posted: 8 hours ago
[6/5/2025, 8:17:45 PM] task loop finish a epoch Link: https://github.com/Orange-OpenSource/hurl
"以下是整理好的热门文章信息,并已翻译为简体中文:\n\n---\n\n### K1 标题 \n**《数据漂移并非真正问题:你的监控策略才是》** \n\n**简介** \n在机器学习领域数据漂移常被视为模型性能下降的罪魁祸首但本文作者提出了一种颠覆性的观点数据漂移只是一个信号真正的核心问题在于监控策略的不足。文章通过实际案例如电商推荐系统和金融风控模型揭示了传统统计监控的局限性并提出了一个三层监控框架 \n1. **统计监控**:快速检测数据分布变化,但仅作为初步信号。 \n2. **上下文监控**:结合业务逻辑,判断漂移是否对关键指标产生影响。 \n3. **行为监控**:追踪模型预测的实际效果,避免“无声漂移”。 \n\n亮点在于作者强调了监控系统需要与业务目标紧密结合而非单纯依赖技术指标。 \n\n**原文链接** \n[点击阅读原文](https://towardsdatascience.com/data-drift-is-not-the-actual-problem-your-monitoring-strategy-is/) \n\n---\n\n### K2 标题 \n**《从 Jupyter 到程序员的快速入门指南》** \n\n**简介** \n本文为数据科学家和初学者提供了一条从 Jupyter Notebook 过渡到专业编程的清晰路径。作者通过实际代码示例和工具推荐(如 VS Code、Git 和 Docker帮助读者摆脱 Notebook 的局限性,提升代码的可维护性和可扩展性。 \n\n亮点包括 \n- 如何将 Notebook 代码模块化为可复用的 Python 脚本。 \n- 使用版本控制和容器化技术优化开发流程。 \n- 实战案例展示如何将实验性代码转化为生产级应用。 \n\n**原文链接** \n[点击阅读原文](https://towardsdatascience.com/the-journey-from-jupyter-to-programmer-a-quick-start-guide/) \n\n---\n\n如果需要进一步优化或补充其他内容请随时告诉我"
``` ```
更多使用请看官方文档https://kirigaya.cn/openmcp/sdk-tutorial/ For more details, see the official documentation: [https://kirigaya.cn/openmcp/sdk-tutorial/](https://kirigaya.cn/openmcp/sdk-tutorial/)
star 我们的项目https://github.com/LSTM-Kirigaya/openmcp-client Star our project: [https://github.com/LSTM-Kirigaya/openmcp-client](https://github.com/LSTM-Kirigaya/openmcp-client)

View File

@ -1,5 +1,5 @@
const { TaskLoop } = require('../../openmcp-sdk/task-loop'); import { TaskLoop } from '../../openmcp-sdk/task-loop.js';
const { TaskLoopAdapter } = require('../../openmcp-sdk/service'); import { TaskLoopAdapter } from '../../openmcp-sdk/service.js';
async function main() { async function main() {
// 创建适配器,负责通信和 mcp 连接 // 创建适配器,负责通信和 mcp 连接

View File

@ -1,6 +1,7 @@
{ {
"name": "openmcp-sdk", "name": "openmcp-sdk",
"version": "0.0.7", "version": "0.0.8",
"type": "module",
"description": "openmcp-sdk", "description": "openmcp-sdk",
"scripts": { "scripts": {
"test": "echo \"Error: no test specified\" && exit 1" "test": "echo \"Error: no test specified\" && exit 1"
@ -37,6 +38,8 @@
"open": "^10.1.2", "open": "^10.1.2",
"ws": "^8.18.1", "ws": "^8.18.1",
"cli-table3": "^0.6.5", "cli-table3": "^0.6.5",
"https-proxy-agent": "^7.0.6" "https-proxy-agent": "^7.0.6",
"pino": "^9.6.0",
"pino-pretty": "^13.0.0"
} }
} }

View File

@ -4,35 +4,72 @@ import type { OpenAI } from 'openai';
export type ChatCompletionChunk = OpenAI.Chat.Completions.ChatCompletionChunk; export type ChatCompletionChunk = OpenAI.Chat.Completions.ChatCompletionChunk;
export type ChatCompletionCreateParamsBase = OpenAI.Chat.Completions.ChatCompletionCreateParams & { id?: string }; export type ChatCompletionCreateParamsBase = OpenAI.Chat.Completions.ChatCompletionCreateParams & { id?: string };
export interface TaskLoopOptions { interface SchemaProperty {
maxEpochs?: number; title: string;
maxJsonParseRetry?: number; type: string;
adapter?: any; description?: string;
verbose?: 0 | 1 | 2 | 3;
} }
export interface SchemaProperty { interface InputSchema {
title: string; type: string;
type: string; properties: Record<string, SchemaProperty>;
description?: string; required?: string[];
title?: string;
$defs?: any;
} }
export interface InputSchema { interface ToolItem {
type: string; name: string;
properties: Record<string, SchemaProperty>; description: string;
required?: string[]; inputSchema: InputSchema;
title?: string;
$defs?: any;
}
export interface ToolItem {
name: string;
description: string;
inputSchema: InputSchema;
enabled: boolean; enabled: boolean;
anyOf?: any; anyOf?: any;
} }
interface IExtraInfo {
created: number,
state: MessageState,
serverName: string,
usage?: ChatCompletionChunk['usage'];
enableXmlWrapper: boolean;
[key: string]: any;
}
interface ToolMessage {
role: 'tool';
index: number;
content: ToolCallContent[];
tool_call_id?: string
name?: string // 工具名称,当 role 为 tool
tool_calls?: ToolCall[],
extraInfo: IExtraInfo
}
interface TextMessage {
role: 'user' | 'assistant' | 'system';
content: string;
tool_call_id?: string
name?: string // 工具名称,当 role 为 tool
tool_calls?: ToolCall[],
extraInfo: IExtraInfo
}
export type ChatMessage = ToolMessage | TextMessage;
interface ChatStorage {
messages: ChatMessage[]
settings: ChatSetting
}
interface EnableToolItem {
name: string;
description: string;
enabled: boolean;
inputSchema: InputSchema;
}
export type Ref<T> = { export type Ref<T> = {
value: T; value: T;
}; };
@ -50,7 +87,7 @@ export interface ToolCall {
export interface ToolCallContent { export interface ToolCallContent {
type: string; type: string;
text: string; text: string;
[key: string]: any; [key: string]: any;
} }
export interface ToolCallResult { export interface ToolCallResult {
@ -80,68 +117,143 @@ export interface IDoConversationResult {
stop: boolean; stop: boolean;
} }
export interface TaskLoopOptions {
/**
* The maximum number of epochs (conversation rounds) to perform.
*/
maxEpochs?: number;
/**
* The maximum number of retries allowed when parsing JSON responses fails.
*/
maxJsonParseRetry?: number;
/**
* A custom adapter that can be used to modify behavior or integrate with different environments.
*/
adapter?: any;
/**
* Verbosity level for logging:
* 0 - Silent, 1 - Errors only, 2 - Warnings and errors, 3 - Full debug output.
*/
verbose?: 0 | 1 | 2 | 3;
}
interface ChatSetting {
/**
* Index of the selected language model from a list of available models.
*/
modelIndex: number;
/**
* System-level prompt used to guide the behavior of the assistant.
*/
systemPrompt: string;
/**
* List of tools that are enabled and available during the chat.
*/
enableTools: EnableToolItem[];
/**
* Sampling temperature for generating responses.
* Higher values (e.g., 0.8) make output more random; lower values (e.g., 0.2) make it more focused and deterministic.
*/
temperature: number;
/**
* Whether web search is enabled for enhancing responses with real-time information.
*/
enableWebSearch: boolean;
/**
* Maximum length of the conversation context to keep.
*/
contextLength: number;
/**
* Whether multiple tools can be called in parallel within a single message.
*/
parallelToolCalls: boolean;
/**
* Whether to wrap tool call responses in XML format.
*/
enableXmlWrapper: boolean;
}
/** /**
* @description * @description
*/ */
export class TaskLoop { export class TaskLoop {
private streamingContent;
private streamingToolCalls;
private readonly taskOptions;
private bridge;
private currentChatId;
private onError;
private onChunk;
private onDone;
private onToolCalled;
private onEpoch;
private completionUsage;
private llmConfig;
constructor(taskOptions?: TaskLoopOptions); constructor(taskOptions?: TaskLoopOptions);
private handleChunkDeltaContent;
private handleChunkDeltaToolCalls; /**
private handleChunkUsage; * @description wait for connection
private doConversation; */
waitConnection(): Promise<void>;
/**
* @description Set the task loop options
* @param taskOptions
*/
setTaskLoopOptions(taskOptions: TaskLoopOptions): void;
/**
* @description make chat data
* @param tabStorage
*/
makeChatData(tabStorage: any): ChatCompletionCreateParamsBase | undefined; makeChatData(tabStorage: any): ChatCompletionCreateParamsBase | undefined;
/**
* @description stop the task loop
*/
abort(): void; abort(): void;
/** /**
* @description error * @description Register a callback function triggered on error
* @param handler * @param handler
*/ */
registerOnError(handler: (msg: IErrorMssage) => void): void; registerOnError(handler: (msg: IErrorMssage) => void): void;
/**
* @description Register a callback function triggered on chunk
* @param handler
*/
registerOnChunk(handler: (chunk: ChatCompletionChunk) => void): void; registerOnChunk(handler: (chunk: ChatCompletionChunk) => void): void;
/** /**
* @description chat.completion * @description Register a callback function triggered at the beginning of each epoch
* @param handler * @param handler
*/ */
registerOnDone(handler: () => void): void; registerOnDone(handler: () => void): void;
/** /**
* @description epoch * @description Register a callback function triggered at the beginning of each epoch
* @param handler * @param handler
*/ */
registerOnEpoch(handler: () => void): void; registerOnEpoch(handler: () => void): void;
/** /**
* @description toolcall * @description Registers a callback function that is triggered when a tool call is completed. This method allows you to intercept and modify the output of the tool call.
* @param handler * @param handler
*/ */
registerOnToolCalled(handler: (toolCallResult: ToolCallResult) => ToolCallResult): void; registerOnToolCalled(handler: (toolCallResult: ToolCallResult) => ToolCallResult): void;
/** /**
* @description toolcall * @description Register a callback triggered after tool call finishes. You can intercept and modify the output.
* @param handler * @param handler
*/ */
registerOnToolCall(handler: (toolCall: ToolCall) => ToolCall): void; registerOnToolCall(handler: (toolCall: ToolCall) => ToolCall): void;
/** /**
* @description LLM * @description Get current LLM configuration
*/ */
getLlmConfig(): any; getLlmConfig(): any;
/** /**
* @description LLM nodejs * @description Set the current LLM configuration, for Node.js environment
* @param config * @param config
* @example * @example
* setLlmConfig({ * setLlmConfig({
@ -154,11 +266,19 @@ export class TaskLoop {
setLlmConfig(config: any): void; setLlmConfig(config: any): void;
/** /**
* @description epoch * @description Set proxy server
* @param maxEpochs * @param maxEpochs
*/ */
setMaxEpochs(maxEpochs: number): void; setMaxEpochs(maxEpochs: number): void;
/**
* @description bind streaming content and tool calls
*/
bindStreaming(content: Ref<string>, toolCalls: Ref<ToolCall[]>): void; bindStreaming(content: Ref<string>, toolCalls: Ref<ToolCall[]>): void;
/**
* @description not finish
*/
connectToService(): Promise<void>; connectToService(): Promise<void>;
/** /**
@ -168,14 +288,29 @@ export class TaskLoop {
setProxyServer(proxyServer: string): void; setProxyServer(proxyServer: string): void;
/** /**
* @description * @description Get all available tool list
*/ */
listTools(): Promise<ToolItem[]>; listTools(): Promise<ToolItem[]>;
/** /**
* @description DOM * @description Start the loop and asynchronously update the DOM
*/ */
start(tabStorage: any, userMessage: string): Promise<void>; start(tabStorage: ChatStorage, userMessage: string): Promise<void>;
/**
* @description Create single conversation context
*/
createStorage(settings?: ChatSetting): Promise<ChatStorage>;
/**
* @description Get prompt template from mcp server
*/
getPrompt(promptId: string, args?: Record<string, any>): Promise<string>;
/**
* @description Get resource template from mcp server
*/
getResource(resourceUri: string): Promise<string>;
} }
export declare const getToolSchema: any; export declare const getToolSchema: any;

37
rollup.tesseract.js Normal file
View File

@ -0,0 +1,37 @@
import path from 'path';
import { nodeResolve } from '@rollup/plugin-node-resolve';
import commonjs from '@rollup/plugin-commonjs';
import { babel } from '@rollup/plugin-babel';
import json from '@rollup/plugin-json'; // ✅ 新增
import copy from 'rollup-plugin-copy';
export default {
input: './node_modules/tesseract.js/src/worker-script/node/index.js',
output: {
file: path.resolve(__dirname, '..', 'resources', 'ocr', 'worker.js'),
format: 'cjs',
exports: 'auto'
},
plugins: [
json(), // ✅ 插入 JSON 插件
nodeResolve({
browser: false,
preferBuiltins: true
}),
commonjs(),
babel({
babelHelpers: 'bundled',
exclude: 'node_modules/**',
presets: ['@babel/preset-env']
}),
copy({
targets: [
{
src: path.resolve(__dirname, '..', 'node_modules', 'tesseract.js-core', 'tesseract*'),
dest: path.resolve(__dirname, '..', 'resources', 'ocr')
}
]
})
],
external: ['bufferutil', 'utf-8-validate']
};

View File

@ -6,13 +6,13 @@
"types": "dist/index.d.ts", "types": "dist/index.d.ts",
"type": "module", "type": "module",
"scripts": { "scripts": {
"serve": "nodemon --watch src --exec tsx src/main.ts", "serve": "tsx watch src/main.ts",
"build": "tsc", "build": "tsc",
"build:watch": "tsc --watch", "build:watch": "tsc --watch",
"postbuild": "node ./scripts/post-build.mjs", "postbuild": "node ./scripts/post-build.mjs",
"start": "node --experimental-specifier-resolution=node dist/main.js", "start": "node dist/main.js",
"start:prod": "NODE_ENV=production node --experimental-specifier-resolution=node dist/main.js", "start:prod": "NODE_ENV=production node dist/main.js",
"debug": "node --inspect --no-warnings=ExperimentalWarning -r tsx/esm src/main.ts", "debug": "tsx --inspect src/main.ts",
"clean": "rm -rf dist", "clean": "rm -rf dist",
"lint": "eslint src --ext .ts,.tsx", "lint": "eslint src --ext .ts,.tsx",
"typecheck": "tsc --noEmit" "typecheck": "tsc --noEmit"

View File

@ -22,7 +22,6 @@ export async function routeMessage(command: string, data: any, webview: PostMess
const { handler, option = {} } = handlerStore; const { handler, option = {} } = handlerStore;
try { try {
// TODO: select client based on something
const res = await handler(data, webview); const res = await handler(data, webview);
// res.code = -1 代表当前请求不需要返回发送 // res.code = -1 代表当前请求不需要返回发送

View File

@ -1,8 +1,5 @@
import { WebSocket } from 'ws'; import { WebSocket } from 'ws';
import { EventEmitter } from 'events'; import { ConnectionType } from '../mcp/client.dto.js';
import { routeMessage } from '../common/router.js';
import { ConnectionType, McpOptions } from '../mcp/client.dto.js';
import { clientMap, connectService } from '../mcp/connect.service.js';
// WebSocket 消息格式 // WebSocket 消息格式
export interface WebSocketMessage { export interface WebSocketMessage {
@ -27,6 +24,10 @@ export interface IConnectionArgs {
cwd?: string; cwd?: string;
url?: string; url?: string;
oauth?: string; oauth?: string;
env?: {
[key: string]: string;
};
[key: string]: any;
} }
// 监听器回调类型 // 监听器回调类型
@ -56,6 +57,7 @@ export class VSCodeWebViewLike {
* @param message - command args * @param message - command args
*/ */
postMessage(message: WebSocketMessage): void { postMessage(message: WebSocketMessage): void {
if (this.ws.readyState === WebSocket.OPEN) { if (this.ws.readyState === WebSocket.OPEN) {
this.ws.send(JSON.stringify(message)); this.ws.send(JSON.stringify(message));
} else { } else {
@ -75,80 +77,3 @@ export class VSCodeWebViewLike {
}; };
} }
} }
export class TaskLoopAdapter {
public emitter: EventEmitter;
private messageHandlers: Set<MessageHandler>;
private connectionOptions: IConnectionArgs[] = [];
constructor(option?: any) {
this.emitter = new EventEmitter(option);
this.messageHandlers = new Set();
this.emitter.on('message/renderer', (message: WebSocketMessage) => {
this.messageHandlers.forEach((handler) => handler(message));
});
// 默认需要将监听的消息导入到 routeMessage 中
this.onDidReceiveMessage((message) => {
const { command, data } = message;
switch (command) {
case 'nodejs/launch-signature':
this.postMessage({
command: 'nodejs/launch-signature',
data: {
code: 200,
msg: this.connectionOptions
}
})
break;
case 'nodejs/update-connection-signature':
// sdk 模式下不需要自动保存连接参数
break;
default:
routeMessage(command, data, this);
break;
}
});
}
/**
* @description
* @param message - command args
*/
public postMessage(message: WebSocketMessage): void {
this.emitter.emit('message/service', message);
}
/**
* @description
* @param callback -
* @returns {{ dispose: () => void }} -
*/
public onDidReceiveMessage(callback: MessageHandler): { dispose: () => void } {
this.messageHandlers.add(callback);
return {
dispose: () => this.messageHandlers.delete(callback),
};
}
/**
* @description mcp
* @param mcpOption
*/
public addMcp(mcpOption: IConnectionArgs) {
// 0.1.4 新版本开始,此处修改为懒加载连接
// 实际的连接移交给前端 mcpAdapter 中进行统一的调度
// 调度步骤如下:
// getLaunchSignature 先获取访问签名,访问签名通过当前函数 push 到 class 中
this.connectionOptions.push(mcpOption);
}
}

View File

@ -130,6 +130,21 @@ export const llms = [
website: 'https://kimi.moonshot.cn', website: 'https://kimi.moonshot.cn',
userToken: '', userToken: '',
userModel: 'moonshot-v1-8k' userModel: 'moonshot-v1-8k'
},
{
id: 'openrouter',
name: 'OpenRouter',
baseUrl: 'https://openrouter.ai/api/v1',
models: [], // 动态加载
provider: 'OpenRouter',
isOpenAICompatible: true,
description: '400+ AI models from multiple providers in one API',
website: 'https://openrouter.ai',
userToken: '',
userModel: '',
isDynamic: true,
modelsEndpoint: 'https://openrouter.ai/api/v1/models',
supportsPricing: true
} }
]; ];

View File

@ -0,0 +1,100 @@
export interface OpenRouterModel {
id: string;
name: string;
description?: string;
context_length: number;
pricing: {
prompt: string;
completion: string;
};
architecture?: {
input_modalities?: string[];
output_modalities?: string[];
tokenizer?: string;
};
supported_parameters?: string[];
}
export interface OpenRouterModelsResponse {
data: OpenRouterModel[];
}
// 模型缓存避免频繁API调用
let modelsCache: { models: OpenRouterModel[]; timestamp: number } | null = null;
const CACHE_DURATION = 5 * 60 * 1000; // 5分钟缓存
export async function fetchOpenRouterModels(): Promise<OpenRouterModel[]> {
const now = Date.now();
// 检查缓存
if (modelsCache && (now - modelsCache.timestamp) < CACHE_DURATION) {
return modelsCache.models;
}
try {
const response = await fetch('https://openrouter.ai/api/v1/models');
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const data: OpenRouterModelsResponse = await response.json();
const models = data.data.map(model => ({
id: model.id,
name: model.name,
description: model.description,
context_length: model.context_length,
pricing: model.pricing,
architecture: model.architecture,
supported_parameters: model.supported_parameters
}));
// 更新缓存
modelsCache = {
models,
timestamp: now
};
console.log(`Fetched ${models.length} OpenRouter models`);
return models;
} catch (error) {
console.error('Failed to fetch OpenRouter models:', error);
// 返回缓存的模型(如果有)或空数组
return modelsCache?.models || [];
}
}
export async function getOpenRouterModelsByCategory(category?: string): Promise<OpenRouterModel[]> {
try {
const url = category
? `https://openrouter.ai/api/v1/models?category=${encodeURIComponent(category)}`
: 'https://openrouter.ai/api/v1/models';
const response = await fetch(url);
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const data: OpenRouterModelsResponse = await response.json();
return data.data;
} catch (error) {
console.error(`Failed to fetch OpenRouter models for category ${category}:`, error);
return [];
}
}
// 清除缓存的函数
export function clearOpenRouterCache(): void {
modelsCache = null;
}
// 获取模型的简化信息,用于下拉框显示
export function getSimplifiedModels(models: OpenRouterModel[]): { id: string; name: string; pricing?: string }[] {
return models.map(model => ({
id: model.id,
name: model.name,
pricing: model.pricing ? `$${model.pricing.prompt}/1K` : undefined
}));
}

327
service/src/hook/sdk.ts Normal file
View File

@ -0,0 +1,327 @@
import { EventEmitter } from 'events';
import { routeMessage } from '../common/router.js';
import * as fs from 'fs';
export class TaskLoopAdapter {
public emitter: EventEmitter;
private messageHandlers: Set<MessageHandler>;
private connectionOptions: IConnectionArgs[] = [];
constructor(option?: any) {
this.emitter = new EventEmitter(option);
this.messageHandlers = new Set();
this.emitter.on('message/renderer', (message: WebSocketMessage) => {
this.messageHandlers.forEach((handler) => handler(message));
});
// 默认需要将监听的消息导入到 routeMessage 中
this.onDidReceiveMessage((message) => {
const { command, data } = message;
switch (command) {
case 'nodejs/launch-signature':
this.postMessage({
command: 'nodejs/launch-signature',
data: {
code: 200,
msg: this.connectionOptions
}
})
break;
case 'nodejs/update-connection-signature':
// sdk 模式下不需要自动保存连接参数
break;
default:
routeMessage(command, data, this);
break;
}
});
}
/**
* @description
* @param message - command args
*/
public postMessage(message: WebSocketMessage): void {
this.emitter.emit('message/service', message);
}
/**
* @description
* @param callback -
* @returns {{ dispose: () => void }} -
*/
public onDidReceiveMessage(callback: MessageHandler): { dispose: () => void } {
this.messageHandlers.add(callback);
return {
dispose: () => this.messageHandlers.delete(callback),
};
}
/**
* @description mcp
* @param mcpOption
*/
public addMcp(mcpOption: IConnectionArgs) {
// 0.1.4 新版本开始,此处修改为懒加载连接
// 实际的连接移交给前端 mcpAdapter 中进行统一的调度
// 调度步骤如下:
// getLaunchSignature 先获取访问签名,访问签名通过当前函数 push 到 class 中
this.connectionOptions.push(mcpOption);
}
}
interface StdioMCPConfig {
command: string;
args: string[];
env?: {
[key: string]: string;
};
description?: string;
prompts?: string[];
resources?: string[];
}
interface HttpMCPConfig {
url: string;
type?: string;
env?: {
[key: string]: string;
};
description?: string;
prompts?: string[];
resources?: string[];
}
export interface OmAgentConfiguration {
version: string;
mcpServers: {
[key: string]: StdioMCPConfig | HttpMCPConfig;
};
defaultLLM: {
baseURL: string;
apiToken: string;
model: string;
}
}
export interface DefaultLLM {
baseURL: string;
apiToken?: string;
model: string;
}
import {
MessageState,
type TaskLoopOptions,
type ChatMessage,
type ChatSetting,
type TaskLoop,
type TextMessage
} from '../../task-loop.js';
import { IConnectionArgs, MessageHandler, WebSocketMessage } from './adapter.js';
import { ConnectionType } from 'src/mcp/client.dto.js';
export function UserMessage(content: string): TextMessage {
return {
role: 'user',
content,
extraInfo: {
created: Date.now(),
state: MessageState.None,
serverName: '',
enableXmlWrapper: false
}
}
}
export function AssistantMessage(content: string): TextMessage {
return {
role: 'assistant',
content,
extraInfo: {
created: Date.now(),
state: MessageState.None,
serverName: '',
enableXmlWrapper: false
}
}
}
export class OmAgent {
private _adapter: TaskLoopAdapter;
private _loop?: TaskLoop;
private _defaultLLM?: DefaultLLM;
constructor() {
this._adapter = new TaskLoopAdapter();
}
/**
* @description Load MCP configuration from file.
* Supports multiple MCP backends and a default LLM model configuration.
*
* @example
* Example configuration:
* {
* "version": "1.0.0",
* "mcpServers": {
* "openmemory": {
* "command": "npx",
* "args": ["-y", "openmemory"],
* "env": {
* "OPENMEMORY_API_KEY": "YOUR_API_KEY",
* "CLIENT_NAME": "openmemory"
* },
* "description": "A MCP for long-term memory support"
* }
* },
* "defaultLLM": {
* "baseURL": "https://api.openmemory.ai",
* "apiToken": "YOUR_API_KEY",
* "model": "deepseek-chat"
* }
* }
*
* @param configPath - Path to the configuration file
*/
public loadMcpConfig(configPath: string) {
const config = JSON.parse(fs.readFileSync(configPath, 'utf-8')) as OmAgentConfiguration;
const { mcpServers, defaultLLM } = config;
// set default llm
this.setDefaultLLM(defaultLLM);
for (const key in mcpServers) {
const mcpConfig = mcpServers[key];
if ('command' in mcpConfig) {
const commandString = (
mcpConfig.command + ' ' + mcpConfig.args.join(' ')
).trim();
this._adapter.addMcp({
commandString,
connectionType: 'STDIO',
env: mcpConfig.env,
description: mcpConfig.description,
});
} else {
const connectionType: ConnectionType = mcpConfig.type === 'http' ? 'STREAMABLE_HTTP' : 'SSE';
this._adapter.addMcp({
url: mcpConfig.url,
env: mcpConfig.env,
connectionType,
description: mcpConfig.description,
});
}
}
}
/**
* @description Add MCP server
*/
public addMcpServer(connectionArgs: IConnectionArgs) {
this._adapter.addMcp(connectionArgs);
}
private async getLoop(loopOption?: TaskLoopOptions) {
if (this._loop) {
if (loopOption) {
this._loop.setTaskLoopOptions(loopOption);
}
return this._loop;
}
const {
verbose = 1,
maxEpochs = 50,
maxJsonParseRetry = 3,
} = loopOption || {}
const adapter = this._adapter;
const { TaskLoop } = await import('../../task-loop.js');
this._loop = new TaskLoop({ adapter, verbose, maxEpochs, maxJsonParseRetry });
await this._loop.waitConnection();
return this._loop;
}
public setDefaultLLM(option: DefaultLLM) {
this._defaultLLM = option;
}
public async getPrompt(promptId: string, args: Record<string, any>) {
const loop = await this.getLoop();
const prompt = await loop.getPrompt(promptId, JSON.parse(JSON.stringify(args)));
return prompt;
}
/**
* @description Asynchronous invoking agent by string or messages
* @param messages Chat message or string
* @param settings Chat setting and task loop options
* @returns
*/
public async ainvoke(
{ messages, settings }: { messages: ChatMessage[] | string; settings?: ChatSetting & Partial<TaskLoopOptions>; }
) {
if (messages.length === 0) {
throw new Error('messages is empty');
}
// detach taskloop option from settings and set default value
const {
maxEpochs = 50,
maxJsonParseRetry = 3,
verbose = 1
} = settings || {};
const loop = await this.getLoop({ maxEpochs, maxJsonParseRetry, verbose });
const storage = await loop.createStorage(settings);
// set input message
// user can invoke [UserMessage("CONTENT")] to make messages quickly
// or use string directly
let userMessage: string;
if (typeof messages === 'string') {
userMessage = messages;
} else {
const lastMessageContent = messages.at(-1)?.content;
if (typeof lastMessageContent === 'string') {
userMessage = lastMessageContent;
} else {
throw new Error('last message content is undefined');
}
}
// select correct llm config
// user can set llm config via omagent.setDefaultLLM()
// or write "defaultLLM" in mcpconfig.json to specify
if (this._defaultLLM) {
loop.setLlmConfig({
baseUrl: this._defaultLLM.baseURL,
userToken: this._defaultLLM.apiToken,
userModel: this._defaultLLM.model,
});
} else {
// throw error to user and give the suggestion
throw new Error('default LLM is not set, please set it via omagent.setDefaultLLM() or write "defaultLLM" in mcpconfig.json');
}
await loop.start(storage, userMessage);
// get response from last message in message list
const lastMessage = storage.messages.at(-1)?.content;
return lastMessage;
}
}

View File

@ -1,4 +1,4 @@
export { routeMessage } from './common/router.js'; export { routeMessage } from './common/router.js';
export { VSCodeWebViewLike, TaskLoopAdapter } from './hook/adapter.js'; export { VSCodeWebViewLike } from './hook/adapter.js';
export { setVscodeWorkspace, setRunningCWD } from './hook/setting.js'; export { setVscodeWorkspace, setRunningCWD } from './hook/setting.js';
export { clientMap } from './mcp/connect.service.js'; export { clientMap } from './mcp/connect.service.js';

View File

@ -1,11 +1,9 @@
import { RequestClientType } from "../common/index.dto.js";
import { Controller } from "../common/index.js"; import { Controller } from "../common/index.js";
import { RequestData } from "../common/index.dto.js"; import { RequestData } from "../common/index.dto.js";
import { PostMessageble } from "../hook/adapter.js"; import { PostMessageble } from "../hook/adapter.js";
import { getClient } from "../mcp/connect.service.js";
import { abortMessageService, streamingChatCompletion } from "./llm.service.js"; import { abortMessageService, streamingChatCompletion } from "./llm.service.js";
import { OpenAI } from "openai"; import { OpenAI } from "openai";
import { axiosFetch } from "src/hook/axios-fetch.js"; import { fetchOpenRouterModels, getSimplifiedModels } from "../hook/openrouter.js";
export class LlmController { export class LlmController {
@Controller('llm/chat/completions') @Controller('llm/chat/completions')
@ -57,4 +55,66 @@ export class LlmController {
msg: models.data msg: models.data
} }
} }
@Controller('llm/models/openrouter')
async getOpenRouterModels(data: RequestData, webview: PostMessageble) {
try {
const models = await fetchOpenRouterModels();
const simplifiedModels = getSimplifiedModels(models);
// 转换为标准格式与其他模型API保持一致
const standardModels = simplifiedModels.map(model => ({
id: model.id,
object: 'model',
name: model.name,
pricing: model.pricing
}));
return {
code: 200,
msg: standardModels
};
} catch (error) {
console.error('Failed to fetch OpenRouter models:', error);
return {
code: 500,
msg: `Failed to fetch OpenRouter models: ${error instanceof Error ? error.message : String(error)}`
};
}
}
@Controller('llm/models/dynamic')
async getDynamicModels(data: RequestData, webview: PostMessageble) {
const { providerId } = data;
try {
if (providerId === 'openrouter') {
const models = await fetchOpenRouterModels();
const simplifiedModels = getSimplifiedModels(models);
const standardModels = simplifiedModels.map(model => ({
id: model.id,
object: 'model',
name: model.name,
pricing: model.pricing
}));
return {
code: 200,
msg: standardModels
};
} else {
return {
code: 400,
msg: `Unsupported dynamic provider: ${providerId}`
};
}
} catch (error) {
console.error(`Failed to fetch dynamic models for ${providerId}:`, error);
return {
code: 500,
msg: `Failed to fetch models: ${error instanceof Error ? error.message : String(error)}`
};
}
}
} }

View File

@ -35,9 +35,17 @@ export async function streamingChatCompletion(
}); });
// 构建OpenRouter特定的请求头
const defaultHeaders: Record<string, string> = {};
if (baseURL && baseURL.includes('openrouter.ai')) {
defaultHeaders['HTTP-Referer'] = 'https://github.com/openmcp/openmcp-client';
defaultHeaders['X-Title'] = 'OpenMCP Client';
}
const client = new OpenAI({ const client = new OpenAI({
baseURL, baseURL,
apiKey, apiKey,
defaultHeaders: Object.keys(defaultHeaders).length > 0 ? defaultHeaders : undefined
}); });
const seriableTools = (tools.length === 0) ? undefined: tools; const seriableTools = (tools.length === 0) ? undefined: tools;

View File

@ -1,13 +1,11 @@
import { WebSocketServer } from 'ws'; import { WebSocketServer } from 'ws';
import {pino} from 'pino'; import { pino } from 'pino';
import { fileURLToPath } from 'url'; import { fileURLToPath } from 'url';
import { dirname } from 'path'; import { dirname, join } from 'path';
import { routeMessage } from './common/router.js'; import { routeMessage } from './common/router.js';
import { VSCodeWebViewLike } from './hook/adapter.js'; import { VSCodeWebViewLike } from './hook/adapter.js';
import path from 'node:path'; import fs from 'fs/promises'; // 使用 Promise API 替代回调
import * as fs from 'node:fs'; import chalk from 'chalk';
import { setRunningCWD } from './hook/setting.js';
import axios from 'axios';
export interface VSCodeMessage { export interface VSCodeMessage {
command: string; command: string;
@ -15,91 +13,80 @@ export interface VSCodeMessage {
callbackId?: string; callbackId?: string;
} }
// 统一路径变量
const devHome = join(__dirname, '..', '..');
const serverPath = join(devHome, 'servers');
const envPath = join(__dirname, '..', '.env');
const logger = pino({ const logger = pino({
transport: {
target: 'pino-pretty', // 启用 pino-pretty
options: {
colorize: true, // 开启颜色
levelFirst: true, // 先打印日志级别
translateTime: 'SYS:yyyy-mm-dd HH:MM:ss', // 格式化时间
ignore: 'pid,hostname', // 忽略部分字段
}
}
}); });
export type MessageHandler = (message: VSCodeMessage) => void; export type MessageHandler = (message: VSCodeMessage) => void;
function refreshConnectionOption(envPath: string) { async function refreshConnectionOption() {
const serverPath = path.join(__dirname, '..', '..', 'servers');
const defaultOption = { const defaultOption = {
connectionType: 'STDIO', connectionType: 'STDIO',
commandString: 'mcp run main.py', commandString: 'mcp run main.py',
cwd: serverPath cwd: serverPath
}; };
fs.writeFileSync(envPath, JSON.stringify(defaultOption, null, 4)); try {
await fs.writeFile(envPath, JSON.stringify(defaultOption, null, 4), 'utf-8');
return { items: [ defaultOption ] }; return { items: [defaultOption] };
} catch (error) {
logger.error('刷新连接配置失败:', error);
throw error;
}
} }
function acquireConnectionOption() { async function acquireConnectionOption() {
const envPath = path.join(__dirname, '..', '.env');
if (!fs.existsSync(envPath)) {
return refreshConnectionOption(envPath);
}
try { try {
const option = JSON.parse(fs.readFileSync(envPath, 'utf-8')); const data = await fs.readFile(envPath, 'utf-8');
const option = JSON.parse(data);
if (!option.items) { if (!option.items || option.items.length === 0) {
return refreshConnectionOption(envPath); return await refreshConnectionOption();
}
if (option.items && option.items.length === 0) {
return refreshConnectionOption(envPath);
} }
// 按照前端的规范,整理成 commandString 样式 // 按照前端的规范,整理成 commandString 样式
option.items = option.items.map((item: any) => { option.items = option.items.map((item: { connectionType: string; commandString: string; command: any; args: any; url: any; }) => {
if (item.connectionType === 'STDIO') { if (item.connectionType === 'STDIO') {
item.commandString = [item.command, ...item.args]?.join(' '); item.commandString = [item.command, ...item.args]?.join(' ');
} else { } else {
item.url = item.url; item.url = item.url;
} }
return item; return item;
}); });
return option; return option;
} catch (error) { } catch (error) {
logger.error('读取 .env 配置文件'); logger.error('读取 .env 配置文件失败:', error);
return refreshConnectionOption(envPath); return await refreshConnectionOption();
} }
} }
function updateConnectionOption(data: any) { async function updateConnectionOption(data: any) {
const envPath = path.join(__dirname, '..', '.env'); try {
const connection = { items: data }; await fs.writeFile(envPath, JSON.stringify({ items: data }, null, 4), 'utf-8');
fs.writeFileSync(envPath, JSON.stringify(connection, null, 4)); } catch (error) {
logger.error('更新连接配置失败:', error);
throw error;
}
} }
const __dirname = dirname(fileURLToPath(import.meta.url)); // 设置运行时路径
const devHome = path.join(__dirname, '..', '..'); import { setRunningCWD } from './hook/setting.js';
setRunningCWD(devHome); setRunningCWD(devHome);
// 启动 WebSocket 服务器
const wss = new WebSocketServer({ port: 8282 }); const wss = new WebSocketServer({ port: 8282 });
console.log('WebSocket 服务器已启动:', 'ws://localhost:8282');
console.log('listen on ws://localhost:8282'); wss.on('connection', (ws) => {
wss.on('connection', (ws: any) => {
// 仿造 webview 进行统一接口访问
const webview = new VSCodeWebViewLike(ws); const webview = new VSCodeWebViewLike(ws);
// 先发送成功建立的消息
webview.postMessage({ webview.postMessage({
command: 'hello', command: 'hello',
data: { data: {
@ -108,34 +95,36 @@ wss.on('connection', (ws: any) => {
} }
}); });
const option = acquireConnectionOption(); acquireConnectionOption().then(option => {
webview.onDidReceiveMessage(async (message) => {
console.log(
chalk.white('receive command') +
chalk.blue(` [${message.command || '未定义'}]`)
);
// 注册消息接受的管线 const { command, data } = message;
webview.onDidReceiveMessage(message => {
logger.info(`command: [${message.command || 'No Command'}]`);
const { command, data } = message;
switch (command) { switch (command) {
case 'web/launch-signature': case 'web/launch-signature':
const launchResult = { webview.postMessage({
code: 200, command: 'web/launch-signature',
msg: option.items data: {
}; code: 200,
msg: option.items
}
});
break;
webview.postMessage({ case 'web/update-connection-signature':
command: 'web/launch-signature', await updateConnectionOption(data);
data: launchResult break;
});
break; default:
routeMessage(command, data, webview);
case 'web/update-connection-signature': break;
updateConnectionOption(data); }
break; });
}).catch(error => {
default: logger.error('初始化连接配置失败:', error);
routeMessage(command, data, webview);
break;
}
}); });
}); });

View File

@ -0,0 +1,132 @@
import { McpOptions } from './client.dto.js';
import { SingleFileMonitor, FileMonitorConfig } from './file-monitor.service.js';
import { PostMessageble } from '../hook/adapter.js';
import * as fs from 'fs';
import * as path from 'path';
import { pino } from 'pino';
import chalk from 'chalk';
// 保留现有 logger 配置
const logger = pino({
});
function getFilePath(options: {
cwd?: string;
args?: string[];
}): string {
const baseDir = options.cwd || process.cwd();
const targetFile = options.args?.length ? options.args[options.args.length - 1] : '';
if (!targetFile || path.isAbsolute(targetFile)) {
return targetFile;
}
return path.resolve(baseDir, targetFile);
}
export class McpServerConnectMonitor {
private Monitor: SingleFileMonitor | undefined;
private Options: McpOptions;
private uuid: string;
private webview: PostMessageble | undefined;
private filePath: string;
constructor(uuid: string, options: McpOptions, onchange: Function, webview?: PostMessageble) {
this.Options = options;
this.webview = webview;
this.uuid = uuid;
this.filePath = getFilePath(options);
// 记录实例创建
// logger.info({ uuid, connectionType: options.connectionType }, 'Created new connection monitor instance');
switch (options.connectionType) {
case 'STDIO':
this.setupStdioMonitor(onchange);
break;
case 'SSE':
logger.info({ uuid }, 'SSE connection type configured but not implemented');
break;
case 'STREAMABLE_HTTP':
logger.info({ uuid }, 'STREAMABLE_HTTP connection type configured but not implemented');
break;
}
}
private setupStdioMonitor(onchange: Function) {
const fileConfig: FileMonitorConfig = {
filePath: this.filePath,
debounceTime: 500,
duplicateCheckTime: 500,
onChange: async (curr, prev) => {
try {
await onchange(this.uuid, this.Options);
console.log('send something');
this.sendWebviewMessage('connect/refresh', {
code: 200,
msg: {
message: 'refresh connect success',
uuid: this.uuid,
}
});
console.log(
chalk.green('Connection refresh successfully')
);
} catch (err) {
this.sendWebviewMessage('connect/refresh', {
code: 500,
msg: {
message: 'refresh connect failed',
uuid: this.uuid,
}
});
// 使用 error 级别记录错误
logger.error({ uuid: this.uuid, error: err }, 'Connection refresh failed');
}
},
onDelete: () => {
// 使用 warn 级别记录文件删除
logger.warn({ uuid: this.uuid }, 'Monitored file has been deleted');
},
onStart: () => {
// 使用 info 级别记录监控开始
// logger.info({ uuid: this.uuid, filePath: path.resolve(fileConfig.filePath) }, 'Started monitoring file');
try {
const stats = fs.statSync(fileConfig.filePath);
// 使用 debug 级别记录详细文件信息
logger.debug({
uuid: this.uuid,
size: stats.size,
ctime: new Date(stats.ctime).toLocaleString()
}, 'File information retrieved');
} catch (err) {
// 使用 error 级别记录获取文件信息失败
logger.error({ uuid: this.uuid, error: err }, 'Failed to retrieve file information');
}
},
onError: (error) => {
// 使用 error 级别记录监控错误
logger.error({ uuid: this.uuid, error }, 'Error occurred during monitoring');
}
};
this.Monitor = new SingleFileMonitor(fileConfig);
}
private sendWebviewMessage(command: string, data: any) {
// 发送消息到webview
this.webview?.postMessage({ command, data });
}
public close() {
this.Monitor?.close();
}
}

View File

@ -3,18 +3,34 @@ import { RequestClientType } from '../common/index.dto.js';
import { connect } from './client.service.js'; import { connect } from './client.service.js';
import { RestfulResponse } from '../common/index.dto.js'; import { RestfulResponse } from '../common/index.dto.js';
import { McpOptions } from './client.dto.js'; import { McpOptions } from './client.dto.js';
import { McpServerConnectMonitor } from './connect-monitor.service.js';
import * as crypto from 'node:crypto'; import * as crypto from 'node:crypto';
import path from 'node:path'; import path from 'node:path';
import fs from 'node:fs'; import fs from 'node:fs';
import * as os from 'os'; import * as os from 'os';
import { PostMessageble } from '../hook/adapter.js'; import { PostMessageble } from '../hook/adapter.js';
import Table from 'cli-table3'; import chalk from 'chalk';
export const clientMap: Map<string, RequestClientType> = new Map(); export const clientMap: Map<string, RequestClientType> = new Map();
export function getClient(clientId?: string): RequestClientType | undefined { export function getClient(clientId?: string): RequestClientType | undefined {
return clientMap.get(clientId || ''); return clientMap.get(clientId || '');
} }
export const clientMonitorMap: Map<string, McpServerConnectMonitor> = new Map();
export async function updateClientMap(uuid: string, options: McpOptions): Promise<{ res: boolean; error?: any }> {
try {
const client = await connect(options);
clientMap.set(uuid, client);
const tools = await client.listTools();
console.log(
chalk.white('update client tools'),
chalk.blue(tools.tools.map(tool => tool.name).join(','))
);
return { res: true };
} catch (error) {
console.error('[updateClientMap] error:', error);
return { res: false, error };
}
}
export function tryGetRunCommandError(command: string, args: string[] = [], cwd?: string): string | null { export function tryGetRunCommandError(command: string, args: string[] = [], cwd?: string): string | null {
try { try {
const commandString = command + ' ' + args.join(' '); const commandString = command + ' ' + args.join(' ');
@ -254,25 +270,6 @@ export async function connectService(
webview?: PostMessageble webview?: PostMessageble
): Promise<RestfulResponse> { ): Promise<RestfulResponse> {
try { try {
// 使用cli-table3创建美观的表格
const table = new Table({
head: ['Property', 'Value'],
colWidths: [20, 60],
style: {
head: ['green'],
border: ['grey']
}
});
table.push(
['Connection Type', option.connectionType],
['Command', option.command || 'N/A'],
['Arguments', option.args?.join(' ') || 'N/A'],
['Working Directory', option.cwd || 'N/A'],
['URL', option.url || 'N/A']
);
console.log(table.toString());
// 预处理字符串 // 预处理字符串
await preprocessCommand(option, webview); await preprocessCommand(option, webview);
@ -288,8 +285,14 @@ export async function connectService(
// } // }
// const client = clientMap.get(uuid)!; // const client = clientMap.get(uuid)!;
{
clientMap.get(uuid)?.disconnect();
clientMonitorMap.get(uuid)?.close();
}
const client = await connect(option); const client = await connect(option);
clientMap.set(uuid, client); clientMap.set(uuid, client);
clientMonitorMap.set(uuid, new McpServerConnectMonitor(uuid, option, updateClientMap, webview));
const versionInfo = client.getServerVersion(); const versionInfo = client.getServerVersion();

View File

@ -0,0 +1,185 @@
import * as fs from 'fs';
import * as path from 'path';
/**
*
*/
interface FileMonitorConfig {
filePath: string;
onChange?: (curr: fs.Stats, prev: fs.Stats) => void;
onDelete?: () => void;
onStart?: () => void;
onError?: (error: Error) => void;
debounceTime?: number; // 防抖时间(毫秒)
duplicateCheckTime?: number; // 去重检查时间阈值(毫秒)
}
/**
*
*/
class SingleFileMonitor {
private filePath: string;
private onChange: (curr: fs.Stats, prev: fs.Stats) => void;
private onDelete: () => void;
private onError: (error: Error) => void;
private watcher: fs.FSWatcher | null = null;
private exists: boolean = false;
private lastModified: number = 0;
private lastSize: number = 0;
private debounceTimer: NodeJS.Timeout | null = null;
private debounceTime: number;
private duplicateCheckTime: number; // 去重检查时间阈值
private lastChangeTime: number = 0;
private lastChangeSize: number = 0;
private isProcessingChange = false;
private config: FileMonitorConfig; // 添加config属性
constructor(config: FileMonitorConfig) {
this.config = config; // 保存配置
this.filePath = config.filePath;
this.onChange = config.onChange || (() => {});
this.onDelete = config.onDelete || (() => {});
this.onError = config.onError || ((error) => console.error('文件监控错误:', error));
this.debounceTime = config.debounceTime || 1000;
// 使用配置中的去重时间默认800ms
this.duplicateCheckTime = config.duplicateCheckTime !== undefined
? config.duplicateCheckTime
: 800;
this.init();
}
private init() {
// 检查文件是否存在
this.checkFileExists()
.then(exists => {
this.exists = exists;
if (exists) {
const stats = fs.statSync(this.filePath);
this.lastModified = stats.mtimeMs;
this.lastSize = stats.size;
}
this.config.onStart?.(); // 使用保存的config属性
this.startWatching();
})
.catch(this.onError);
}
private startWatching() {
try {
this.watcher = fs.watch(this.filePath, (eventType) => {
if (eventType === 'change') {
this.handleFileChange(true);
}
});
// console.log(`正在监控文件: ${this.filePath}`);
} catch (error) {
this.onError(error as Error);
}
}
private checkFileExists(): Promise<boolean> {
return new Promise(resolve => {
fs.access(this.filePath, fs.constants.F_OK, (err) => {
resolve(!err);
});
});
}
private handleFileChange(isFromWatch: boolean = false) {
if (this.isProcessingChange) return;
if (!this.exists) {
this.checkFileExists()
.then(exists => {
if (exists) {
this.exists = true;
const stats = fs.statSync(this.filePath);
this.lastModified = stats.mtimeMs;
this.lastSize = stats.size;
}
});
return;
}
let currentStats: fs.Stats;
try {
currentStats = fs.statSync(this.filePath);
} catch (error) {
this.exists = false;
this.onDelete();
return;
}
const currentMtime = currentStats.mtimeMs;
const currentSize = currentStats.size;
if (currentSize === this.lastSize && currentMtime - this.lastModified < 800) {
return;
}
const now = Date.now();
// 使用可配置的去重时间阈值
if (now - this.lastChangeTime < this.duplicateCheckTime && currentSize === this.lastChangeSize) {
return;
}
this.lastChangeTime = now;
this.lastChangeSize = currentSize;
const prevStats = fs.statSync(this.filePath);
this.lastModified = currentMtime;
this.lastSize = currentSize;
this.isProcessingChange = true;
if (this.debounceTimer) {
clearTimeout(this.debounceTimer);
}
this.debounceTimer = setTimeout(() => {
this.checkFileExists()
.then(exists => {
if (exists) {
const currStats = fs.statSync(this.filePath);
this.onChange(currStats, prevStats);
}
})
.catch(this.onError)
.finally(() => {
this.isProcessingChange = false;
this.debounceTimer = null;
});
}, this.debounceTime);
}
private checkFileStatus() {
this.checkFileExists()
.then(exists => {
if (this.exists && !exists) {
this.exists = false;
this.onDelete();
} else if (!this.exists && exists) {
this.exists = true;
const stats = fs.statSync(this.filePath);
this.lastModified = stats.mtimeMs;
this.lastSize = stats.size;
}
})
.catch(this.onError);
}
public close() {
if (this.watcher) {
// 明确指定close方法的类型解决TS2554错误
(this.watcher.close as (callback?: () => void) => void)(() => {
// console.log(`已停止监控文件: ${this.filePath}`);
});
this.watcher = null;
}
if (this.debounceTimer) {
clearTimeout(this.debounceTimer);
}
}
}
export { SingleFileMonitor, FileMonitorConfig };

1
service/src/sdk.ts Normal file
View File

@ -0,0 +1 @@
export { TaskLoopAdapter, OmAgent, OmAgentConfiguration, UserMessage, AssistantMessage } from './hook/sdk.js';

View File

@ -1,12 +1,14 @@
import { WebSocketServer } from 'ws'; import { WebSocketServer } from 'ws';
import pino from 'pino'; import pino from 'pino';
import { fileURLToPath } from 'url';
import { dirname, join } from 'path';
import { routeMessage } from './common/router.js'; import { routeMessage } from './common/router.js';
import { VSCodeWebViewLike } from './hook/adapter.js'; import { VSCodeWebViewLike } from './hook/adapter.js';
import path from 'node:path'; import fs from 'fs';
import * as fs from 'node:fs'; import path from 'path';
import { setRunningCWD } from './hook/setting.js'; import { setRunningCWD } from './hook/setting.js';
import { exit } from 'node:process'; import { exit } from 'process';
export interface VSCodeMessage { export interface VSCodeMessage {
command: string; command: string;
@ -15,22 +17,13 @@ export interface VSCodeMessage {
} }
const logger = pino.default({ const logger = pino.default({
transport: {
target: 'pino-pretty', // 启用 pino-pretty
options: {
colorize: true, // 开启颜色
levelFirst: true, // 先打印日志级别
translateTime: 'SYS:yyyy-mm-dd HH:MM:ss', // 格式化时间
ignore: 'pid,hostname', // 忽略部分字段
}
}
}); });
export type MessageHandler = (message: VSCodeMessage) => void; export type MessageHandler = (message: VSCodeMessage) => void;
function refreshConnectionOption(envPath: string) { function refreshConnectionOption(envPath: string) {
const serverPath = path.join(__dirname, '..', '..', 'servers'); const serverPath = join(__dirname, '..', '..', 'servers');
const defaultOption = { const defaultOption = {
connectionType: 'STDIO', connectionType: 'STDIO',
commandString: 'mcp run main.py', commandString: 'mcp run main.py',
@ -38,12 +31,11 @@ function refreshConnectionOption(envPath: string) {
}; };
fs.writeFileSync(envPath, JSON.stringify(defaultOption, null, 4)); fs.writeFileSync(envPath, JSON.stringify(defaultOption, null, 4));
return { items: [defaultOption] }; return { items: [defaultOption] };
} }
function acquireConnectionOption() { function acquireConnectionOption() {
const envPath = path.join(__dirname, '..', '.env'); const envPath = join(__dirname, '..', '.env');
if (!fs.existsSync(envPath)) { if (!fs.existsSync(envPath)) {
return refreshConnectionOption(envPath); return refreshConnectionOption(envPath);
@ -52,11 +44,7 @@ function acquireConnectionOption() {
try { try {
const option = JSON.parse(fs.readFileSync(envPath, 'utf-8')); const option = JSON.parse(fs.readFileSync(envPath, 'utf-8'));
if (!option.items) { if (!option.items || option.items.length === 0) {
return refreshConnectionOption(envPath);
}
if (option.items && option.items.length === 0) {
return refreshConnectionOption(envPath); return refreshConnectionOption(envPath);
} }
@ -67,7 +55,6 @@ function acquireConnectionOption() {
} else { } else {
item.url = item.url; item.url = item.url;
} }
return item; return item;
}); });
@ -79,20 +66,22 @@ function acquireConnectionOption() {
} }
} }
if (!fs.existsSync(path.join(__dirname, '..', '.env.website.local'))) { // 验证 .env.website.local 存在性
const localEnvPath = join(__dirname, '..', '.env.website.local');
if (!fs.existsSync(localEnvPath)) {
console.log('.env.website.local 不存在!'); console.log('.env.website.local 不存在!');
exit(0); exit(0);
} }
const authPassword = JSON.parse(fs.readFileSync(path.join(__dirname, '..', '.env.website.local'), 'utf-8')).password; // 读取认证密码
const authPassword = JSON.parse(fs.readFileSync(localEnvPath, 'utf-8')).password;
function updateConnectionOption(data: any) { function updateConnectionOption(data: any) {
const envPath = path.join(__dirname, '..', '.env'); const envPath = join(__dirname, '..', '.env');
const connection = { items: data }; fs.writeFileSync(envPath, JSON.stringify({ items: data }, null, 4));
fs.writeFileSync(envPath, JSON.stringify(connection, null, 4));
} }
const devHome = path.join(__dirname, '..', '..'); const devHome = join(__dirname, '..', '..');
setRunningCWD(devHome); setRunningCWD(devHome);
function verifyToken(url: string) { function verifyToken(url: string) {
@ -104,30 +93,25 @@ function verifyToken(url: string) {
} }
} }
const wss = new WebSocketServer( const wss = new WebSocketServer({
{ port: 8282,
port: 8282, verifyClient: (info, callback) => {
verifyClient: (info, callback) => { const url = info.req.url || '';
console.log(info.req.url); const ok = verifyToken(url);
const ok = verifyToken(info.req.url || '');
if (!ok) { if (!ok) {
callback(false, 401, 'Unauthorized: Invalid token'); callback(false, 401, 'Unauthorized: Invalid token');
} else { } else {
callback(true); // 允许连接 callback(true);
}
} }
}, }
); });
console.log('listen on ws://localhost:8282'); console.log('listen on ws://localhost:8282');
wss.on('connection', (ws: any) => { wss.on('connection', (ws) => {
// 仿造 webview 进行统一接口访问
const webview = new VSCodeWebViewLike(ws); const webview = new VSCodeWebViewLike(ws);
// 先发送成功建立的消息
webview.postMessage({ webview.postMessage({
command: 'hello', command: 'hello',
data: { data: {
@ -138,23 +122,19 @@ wss.on('connection', (ws: any) => {
const option = acquireConnectionOption(); const option = acquireConnectionOption();
// 注册消息接受的管线
webview.onDidReceiveMessage(message => { webview.onDidReceiveMessage(message => {
logger.info(`command: [${message.command || 'No Command'}]`); logger.info(`command: [${message.command || 'No Command'}]`);
const { command, data } = message; const { command, data } = message;
switch (command) { switch (command) {
case 'web/launch-signature': case 'web/launch-signature':
const launchResult = {
code: 200,
msg: option.items
};
webview.postMessage({ webview.postMessage({
command: 'web/launch-signature', command: 'web/launch-signature',
data: launchResult data: {
code: 200,
msg: option.items
}
}); });
break; break;
case 'web/update-connection-signature': case 'web/update-connection-signature':

View File

@ -53,9 +53,28 @@ export function loadSetting(): IConfig {
try { try {
const configData = fs.readFileSync(configPath, 'utf-8'); const configData = fs.readFileSync(configPath, 'utf-8');
const config = JSON.parse(configData) as IConfig; const config = JSON.parse(configData) as IConfig;
if (!config.LLM_INFO || (Array.isArray(config.LLM_INFO) && config.LLM_INFO.length === 0)) { if (!config.LLM_INFO || (Array.isArray(config.LLM_INFO) && config.LLM_INFO.length === 0)) {
config.LLM_INFO = llms; config.LLM_INFO = llms;
} else {
// 自动同步新的提供商:检查默认配置中是否有新的提供商未在用户配置中
const existingIds = new Set(config.LLM_INFO.map((llm: any) => llm.id));
const newProviders = llms.filter((llm: any) => !existingIds.has(llm.id));
if (newProviders.length > 0) {
console.log(`Adding ${newProviders.length} new providers:`, newProviders.map(p => p.name));
config.LLM_INFO.push(...newProviders);
// 自动保存更新后的配置
try {
fs.writeFileSync(configPath, JSON.stringify(config, null, 2), 'utf-8');
console.log('Configuration updated with new providers');
} catch (saveError) {
console.error('Failed to save updated configuration:', saveError);
}
}
} }
return config; return config;
} catch (error) { } catch (error) {
console.error('Error loading config file, creating new one:', error); console.error('Error loading config file, creating new one:', error);

324
service/task-loop.d.ts vendored Normal file
View File

@ -0,0 +1,324 @@
/* eslint-disable */
import type { OpenAI } from 'openai';
export type ChatCompletionChunk = OpenAI.Chat.Completions.ChatCompletionChunk;
export type ChatCompletionCreateParamsBase = OpenAI.Chat.Completions.ChatCompletionCreateParams & { id?: string };
interface SchemaProperty {
title: string;
type: string;
description?: string;
}
interface InputSchema {
type: string;
properties: Record<string, SchemaProperty>;
required?: string[];
title?: string;
$defs?: any;
}
interface ToolItem {
name: string;
description: string;
inputSchema: InputSchema;
enabled: boolean;
anyOf?: any;
}
interface IExtraInfo {
created: number,
state: MessageState,
serverName: string,
usage?: ChatCompletionChunk['usage'];
enableXmlWrapper: boolean;
[key: string]: any;
}
interface ToolMessage {
role: 'tool';
index: number;
content: ToolCallContent[];
tool_call_id?: string
name?: string // 工具名称,当 role 为 tool
tool_calls?: ToolCall[],
extraInfo: IExtraInfo
}
interface TextMessage {
role: 'user' | 'assistant' | 'system';
content: string;
tool_call_id?: string
name?: string // 工具名称,当 role 为 tool
tool_calls?: ToolCall[],
extraInfo: IExtraInfo
}
export type ChatMessage = ToolMessage | TextMessage;
interface ChatStorage {
messages: ChatMessage[]
settings: ChatSetting
}
interface EnableToolItem {
name: string;
description: string;
enabled: boolean;
inputSchema: InputSchema;
}
export type Ref<T> = {
value: T;
};
export interface ToolCall {
id?: string;
index?: number;
type: string;
function: {
name: string;
arguments: string;
}
}
export interface ToolCallContent {
type: string;
text: string;
[key: string]: any;
}
export interface ToolCallResult {
state: MessageState;
content: ToolCallContent[];
}
export enum MessageState {
ServerError = 'server internal error',
ReceiveChunkError = 'receive chunk error',
Timeout = 'timeout',
MaxEpochs = 'max epochs',
Unknown = 'unknown error',
Abort = 'abort',
ToolCall = 'tool call failed',
None = 'none',
Success = 'success',
ParseJsonError = 'parse json error'
}
export interface IErrorMssage {
state: MessageState;
msg: string;
}
export interface IDoConversationResult {
stop: boolean;
}
export interface TaskLoopOptions {
/**
* The maximum number of epochs (conversation rounds) to perform.
*/
maxEpochs?: number;
/**
* The maximum number of retries allowed when parsing JSON responses fails.
*/
maxJsonParseRetry?: number;
/**
* A custom adapter that can be used to modify behavior or integrate with different environments.
*/
adapter?: any;
/**
* Verbosity level for logging:
* 0 - Silent, 1 - Errors only, 2 - Warnings and errors, 3 - Full debug output.
*/
verbose?: 0 | 1 | 2 | 3;
}
interface ChatSetting {
/**
* Index of the selected language model from a list of available models.
*/
modelIndex: number;
/**
* System-level prompt used to guide the behavior of the assistant.
*/
systemPrompt: string;
/**
* List of tools that are enabled and available during the chat.
*/
enableTools: EnableToolItem[];
/**
* Sampling temperature for generating responses.
* Higher values (e.g., 0.8) make output more random; lower values (e.g., 0.2) make it more focused and deterministic.
*/
temperature: number;
/**
* Whether web search is enabled for enhancing responses with real-time information.
*/
enableWebSearch: boolean;
/**
* Maximum length of the conversation context to keep.
*/
contextLength: number;
/**
* Whether multiple tools can be called in parallel within a single message.
*/
parallelToolCalls: boolean;
/**
* Whether to wrap tool call responses in XML format.
*/
enableXmlWrapper: boolean;
}
/**
* @description
*/
export class TaskLoop {
constructor(taskOptions?: TaskLoopOptions);
/**
* @description wait for connection
*/
waitConnection(): Promise<void>;
/**
* @description Set the task loop options
* @param taskOptions
*/
setTaskLoopOptions(taskOptions: TaskLoopOptions): void;
/**
* @description make chat data
* @param tabStorage
*/
makeChatData(tabStorage: any): ChatCompletionCreateParamsBase | undefined;
/**
* @description stop the task loop
*/
abort(): void;
/**
* @description Register a callback function triggered on error
* @param handler
*/
registerOnError(handler: (msg: IErrorMssage) => void): void;
/**
* @description Register a callback function triggered on chunk
* @param handler
*/
registerOnChunk(handler: (chunk: ChatCompletionChunk) => void): void;
/**
* @description Register a callback function triggered at the beginning of each epoch
* @param handler
*/
registerOnDone(handler: () => void): void;
/**
* @description Register a callback function triggered at the beginning of each epoch
* @param handler
*/
registerOnEpoch(handler: () => void): void;
/**
* @description Registers a callback function that is triggered when a tool call is completed. This method allows you to intercept and modify the output of the tool call.
* @param handler
*/
registerOnToolCalled(handler: (toolCallResult: ToolCallResult) => ToolCallResult): void;
/**
* @description Register a callback triggered after tool call finishes. You can intercept and modify the output.
* @param handler
*/
registerOnToolCall(handler: (toolCall: ToolCall) => ToolCall): void;
/**
* @description Get current LLM configuration
*/
getLlmConfig(): any;
/**
* @description Set the current LLM configuration, for Node.js environment
* @param config
* @example
* setLlmConfig({
* id: 'openai',
* baseUrl: 'https://api.openai.com/v1',
* userToken: 'sk-xxx',
* userModel: 'gpt-3.5-turbo',
* })
*/
setLlmConfig(config: any): void;
/**
* @description Set proxy server
* @param maxEpochs
*/
setMaxEpochs(maxEpochs: number): void;
/**
* @description bind streaming content and tool calls
*/
bindStreaming(content: Ref<string>, toolCalls: Ref<ToolCall[]>): void;
/**
* @description not finish
*/
connectToService(): Promise<void>;
/**
* @description
* @param proxyServer
*/
setProxyServer(proxyServer: string): void;
/**
* @description Get all available tool list
*/
listTools(): Promise<ToolItem[]>;
/**
* @description Start the loop and asynchronously update the DOM
*/
start(tabStorage: ChatStorage, userMessage: string): Promise<void>;
/**
* @description Create single conversation context
*/
createStorage(settings?: ChatSetting): Promise<ChatStorage>;
/**
* @description Get prompt template from mcp server
*/
getPrompt(promptId: string, args?: Record<string, any>): Promise<string>;
/**
* @description Get resource template from mcp server
*/
getResource(resourceUri: string): Promise<string>;
}
export declare const getToolSchema: any;
export declare const useMessageBridge: any;
export declare const llmManager: any;
export declare const llms: any;
export declare const pinkLog: any;
export declare const redLog: any;
export declare const ElMessage: any;
export declare const handleToolCalls: any;
export declare const getPlatform: any;

View File

@ -10,7 +10,7 @@
"forceConsistentCasingInFileNames": true, "forceConsistentCasingInFileNames": true,
"outDir": "./dist", "outDir": "./dist",
"declaration": true, "declaration": true,
"declarationMap": true, "declarationMap": false,
"experimentalDecorators": true, "experimentalDecorators": true,
}, },
"paths": { "paths": {

View File

@ -1,4 +1,4 @@
const path = require('path'); import path from 'path';
module.exports = { module.exports = {
entry: './src/main.ts', entry: './src/main.ts',

View File

@ -1,10 +1,10 @@
import * as vscode from 'vscode'; import * as vscode from 'vscode';
import { registerCommands, registerTreeDataProviders } from '.'; import { registerCommands, registerTreeDataProviders } from './index.js';
import { HelpProvider } from '../sidebar/help.controller'; import { HelpProvider } from '../sidebar/help.controller.js';
import { McpWorkspaceConnectProvider } from '../sidebar/workspace.controller'; import { McpWorkspaceConnectProvider } from '../sidebar/workspace.controller.js';
import { McpInstalledConnectProvider } from '../sidebar/installed.controller'; import { McpInstalledConnectProvider } from '../sidebar/installed.controller.js';
import { WebviewController } from '../webview/webview.controller'; import { WebviewController } from '../webview/webview.controller.js';
import { HookController } from '../hook/hook.controller'; import { HookController } from '../hook/hook.controller.js';
export const InstallModules = [ export const InstallModules = [
McpWorkspaceConnectProvider, McpWorkspaceConnectProvider,

View File

@ -1,4 +1,4 @@
import { CommandHandlerDescriptor, IRegisterCommandItem, IRegisterTreeDataProviderItem, TreeDataProviderConstructor } from "./index.dto"; import { CommandHandlerDescriptor, IRegisterCommandItem, IRegisterTreeDataProviderItem, TreeDataProviderConstructor } from "./index.dto.js";
export const registerCommands = new Array<[string, IRegisterCommandItem]>(); export const registerCommands = new Array<[string, IRegisterCommandItem]>();
export const registerTreeDataProviders = new Map<string, IRegisterTreeDataProviderItem<any>>(); export const registerTreeDataProviders = new Map<string, IRegisterTreeDataProviderItem<any>>();

View File

@ -1,7 +1,7 @@
import * as vscode from 'vscode'; import * as vscode from 'vscode';
import { setRunningCWD, setVscodeWorkspace } from '../openmcp-sdk/service'; import { setRunningCWD, setVscodeWorkspace } from '../openmcp-sdk/service/index.js';
import { launch } from './common/entry'; import { launch } from './common/entry.js';
import { initialiseI18n } from './i18n'; import { initialiseI18n } from './i18n/index.js';
export function activate(context: vscode.ExtensionContext) { export function activate(context: vscode.ExtensionContext) {
console.log('activate openmcp'); console.log('activate openmcp');
@ -10,11 +10,9 @@ export function activate(context: vscode.ExtensionContext) {
// 获取当前打开的项目的路径 // 获取当前打开的项目的路径
const workspaceFolder = vscode.workspace.workspaceFolders?.[0]; const workspaceFolder = vscode.workspace.workspaceFolders?.[0];
const workspace = workspaceFolder?.uri.fsPath || ''; const workspace = workspaceFolder?.uri.fsPath || '';
setVscodeWorkspace(workspace); setVscodeWorkspace(workspace);
setRunningCWD(context.extensionPath); setRunningCWD(context.extensionPath);
initialiseI18n(context); initialiseI18n(context);
launch(context); launch(context);
} }

View File

@ -95,7 +95,13 @@ export function getConnectionConfig() {
*/ */
export function getWorkspaceConnectionConfigPath() { export function getWorkspaceConnectionConfigPath() {
const workspace = getWorkspacePath(); const workspace = getWorkspacePath();
if (!workspace) {
throw new Error('No workspace found. Please open a folder in VSCode first.');
}
const configDir = fspath.join(workspace, '.openmcp'); const configDir = fspath.join(workspace, '.openmcp');
if (!fs.existsSync(configDir)) {
fs.mkdirSync(configDir, { recursive: true }); // 递归创建目录
}
const connectionConfig = fspath.join(configDir, CONNECTION_CONFIG_NAME); const connectionConfig = fspath.join(configDir, CONNECTION_CONFIG_NAME);
return connectionConfig; return connectionConfig;
} }
@ -110,6 +116,9 @@ export function getWorkspaceConnectionConfig() {
} }
const workspace = getWorkspacePath(); const workspace = getWorkspacePath();
if (!workspace) {
throw new Error('No workspace found. Please open a folder in VSCode first.');
}
const configDir = fspath.join(workspace, '.openmcp'); const configDir = fspath.join(workspace, '.openmcp');
const connectionConfig = fspath.join(configDir, CONNECTION_CONFIG_NAME); const connectionConfig = fspath.join(configDir, CONNECTION_CONFIG_NAME);
@ -296,6 +305,7 @@ function normaliseConnectionFilePath(item: McpOptions, workspace: string) {
export function getWorkspacePath() { export function getWorkspacePath() {
const workspaceFolder = vscode.workspace.workspaceFolders?.[0]; const workspaceFolder = vscode.workspace.workspaceFolders?.[0];
console.log('getWorkspacePath: ', vscode.workspace.workspaceFolders);
return (workspaceFolder?.uri.fsPath || '').replace(/\\/g, '/'); return (workspaceFolder?.uri.fsPath || '').replace(/\\/g, '/');
} }
@ -367,3 +377,18 @@ export async function getFirstValidPathFromCommand(command: string, cwd: string)
return undefined; return undefined;
} }
export async function exportFile(filename: string, content: any) {
// 使用 vscode 的 api创建文件导出窗口询问用户
const uri = await vscode.window.showSaveDialog({
defaultUri: vscode.Uri.file(filename),
filters: {
'JSON': ['json']
}
});
if (uri) {
fs.writeFileSync(uri.fsPath, content, 'utf-8');
}
}

View File

@ -1,4 +1,4 @@
import { RegisterCommand } from "../common"; import { RegisterCommand } from "../common/index.js";
import * as vscode from 'vscode'; import * as vscode from 'vscode';
import * as path from 'path'; import * as path from 'path';
import Tesseract from 'tesseract.js'; import Tesseract from 'tesseract.js';

View File

@ -1,5 +1,5 @@
import * as vscode from 'vscode'; import * as vscode from 'vscode';
import { McpOptions } from '../global'; import { McpOptions } from '../global.js';
export class SidebarItem extends vscode.TreeItem { export class SidebarItem extends vscode.TreeItem {
constructor( constructor(

View File

@ -1,7 +1,7 @@
import * as vscode from 'vscode'; import * as vscode from 'vscode';
import { SidebarItem } from './common'; import { SidebarItem } from './common.js';
import { RegisterTreeDataProvider } from '../common'; import { RegisterTreeDataProvider } from '../common/index.js';
import { t } from '../i18n'; import { t } from '../i18n/index.js';
@RegisterTreeDataProvider('openmcp.sidebar.help') @RegisterTreeDataProvider('openmcp.sidebar.help')
export class HelpProvider implements vscode.TreeDataProvider<SidebarItem> { export class HelpProvider implements vscode.TreeDataProvider<SidebarItem> {

View File

@ -1,9 +1,9 @@
import * as vscode from 'vscode'; import * as vscode from 'vscode';
import { RegisterCommand, RegisterTreeDataProvider } from '../common'; import { RegisterCommand, RegisterTreeDataProvider } from '../common/index.js';
import { ConnectionViewItem } from './common'; import { ConnectionViewItem } from './common.js';
import { getConnectionConfig, getInstalledConnectionConfigPath, saveConnectionConfig } from '../global'; import { getConnectionConfig, getInstalledConnectionConfigPath, saveConnectionConfig } from '../global.js';
import { acquireInstalledConnection, deleteInstalledConnection } from './installed.service'; import { acquireInstalledConnection, deleteInstalledConnection } from './installed.service.js';
import { revealOpenMcpWebviewPanel } from '../webview/webview.service'; import { revealOpenMcpWebviewPanel } from '../webview/webview.service.js';
@RegisterTreeDataProvider('openmcp.sidebar.installed-connection') @RegisterTreeDataProvider('openmcp.sidebar.installed-connection')
export class McpInstalledConnectProvider implements vscode.TreeDataProvider<ConnectionViewItem> { export class McpInstalledConnectProvider implements vscode.TreeDataProvider<ConnectionViewItem> {

View File

@ -1,7 +1,7 @@
import { getConnectionConfig, panels, saveConnectionConfig, getFirstValidPathFromCommand, McpOptions } from "../global"; import { getConnectionConfig, panels, saveConnectionConfig, getFirstValidPathFromCommand, McpOptions } from "../global.js";
import { exec, spawn } from 'node:child_process'; import { exec, spawn } from 'node:child_process';
import * as vscode from 'vscode'; import * as vscode from 'vscode';
import { t } from "../i18n"; import { t } from "../i18n/index.js";
export async function deleteInstalledConnection(item: McpOptions[] | McpOptions) { export async function deleteInstalledConnection(item: McpOptions[] | McpOptions) {
// 弹出确认对话框 // 弹出确认对话框

View File

@ -1,9 +1,9 @@
import * as vscode from 'vscode'; import * as vscode from 'vscode';
import { RegisterCommand, RegisterTreeDataProvider } from '../common'; import { RegisterCommand, RegisterTreeDataProvider } from '../common/index.js';
import { getWorkspaceConnectionConfig, getWorkspaceConnectionConfigPath, getWorkspacePath, saveWorkspaceConnectionConfig } from '../global'; import { getWorkspaceConnectionConfig, getWorkspaceConnectionConfigPath, getWorkspacePath, saveWorkspaceConnectionConfig } from '../global.js';
import { ConnectionViewItem } from './common'; import { ConnectionViewItem } from './common.js';
import { revealOpenMcpWebviewPanel } from '../webview/webview.service'; import { revealOpenMcpWebviewPanel } from '../webview/webview.service.js';
import { acquireUserCustomConnection, deleteUserConnection } from './workspace.service'; import { acquireUserCustomConnection, deleteUserConnection } from './workspace.service.js';
@RegisterTreeDataProvider('openmcp.sidebar.workspace-connection') @RegisterTreeDataProvider('openmcp.sidebar.workspace-connection')
export class McpWorkspaceConnectProvider implements vscode.TreeDataProvider<ConnectionViewItem> { export class McpWorkspaceConnectProvider implements vscode.TreeDataProvider<ConnectionViewItem> {
@ -55,7 +55,7 @@ export class McpWorkspaceConnectProvider implements vscode.TreeDataProvider<Conn
public refresh(context: vscode.ExtensionContext): void { public refresh(context: vscode.ExtensionContext): void {
console.log(this); console.log(this);
this._onDidChangeTreeData.fire(); this._onDidChangeTreeData.fire(undefined);
} }
@RegisterCommand('addConnection') @RegisterCommand('addConnection')

View File

@ -1,6 +1,8 @@
import { getFirstValidPathFromCommand, getWorkspaceConnectionConfig, getWorkspacePath, McpOptions, panels, saveWorkspaceConnectionConfig } from "../global"; import { getFirstValidPathFromCommand, getWorkspaceConnectionConfig, getWorkspacePath, McpOptions, panels, saveWorkspaceConnectionConfig } from "../global.js";
import * as vscode from 'vscode'; import * as vscode from 'vscode';
import { t } from "../i18n"; import { t } from "../i18n/index.js";
import { exec } from 'child_process';
import { promisify } from 'util';
export async function deleteUserConnection(item: McpOptions[] | McpOptions) { export async function deleteUserConnection(item: McpOptions[] | McpOptions) {
// 弹出确认对话框 // 弹出确认对话框
@ -45,8 +47,7 @@ export async function deleteUserConnection(item: McpOptions[] | McpOptions) {
} }
export async function validateAndGetCommandPath(command: string, cwd?: string): Promise<string> { export async function validateAndGetCommandPath(command: string, cwd?: string): Promise<string> {
const { exec } = require('child_process');
const { promisify } = require('util');
const execAsync = promisify(exec); const execAsync = promisify(exec);
try { try {

27
src/test/e2e/runTest.ts Normal file
View File

@ -0,0 +1,27 @@
import * as path from 'path';
import { runTests } from '@vscode/test-electron';
async function main() {
try {
// The folder containing the Extension Manifest package.json
// Passed to `--extensionDevelopmentPath`
const extensionDevelopmentPath = path.resolve(__dirname, '../../../');
console.log('Extension Path:', extensionDevelopmentPath); // 添加日志验证路径
// The path to the extension test script
// Passed to --extensionTestsPath
const extensionTestsPath = path.resolve(__dirname, './suite/index.js');
// Download VS Code, unzip it and run the integration test
await runTests({
extensionDevelopmentPath: extensionDevelopmentPath,
extensionTestsPath: extensionTestsPath,
});
} catch (err) {
console.error('Failed to run tests');
console.error(err);
process.exit(1);
}
}
main();

View File

@ -0,0 +1,31 @@
import * as assert from 'assert';
import * as vscode from 'vscode';
suite('测试基础插件激活和命令注册', () => {
vscode.window.showInformationMessage('开始测试基础插件激活和命令注册');
setup(async () => {
await vscode.commands.executeCommand('workbench.view.extension.openmcp-sidebar');
});
test('测试的测试', () => {
assert.strictEqual([1, 2, 3].indexOf(5), -1);
assert.strictEqual([1, 2, 3].indexOf(0), -1);
});
test('插件存在测试', async () => {
const ext = vscode.extensions.getExtension('kirigaya.openmcp');
assert.ok(ext, '插件未找到');
});
test('插件激活测试', async () => {
const ext = vscode.extensions.getExtension('kirigaya.openmcp');
await ext?.activate();
assert.ok(ext?.isActive, '插件未激活');
});
test('命令 openmcp.showOpenMCP 注册测试', async () => {
const commands = await vscode.commands.getCommands(true);
assert.ok(commands.includes('openmcp.showOpenMCP'), '命令未注册');
});
//
});

View File

@ -0,0 +1,44 @@
import * as assert from 'assert';
import * as vscode from 'vscode';
import * as sinon from 'sinon';
suite('连接管理测试', () => {
vscode.window.showInformationMessage('开始测试连接管理');
let inputBoxStub: sinon.SinonStub;
let quickPickStub: sinon.SinonStub;
setup(async () => {
// mock showQuickPick
// quickPickStub = sinon.stub(vscode.window, 'showQuickPick');
// // mock showInputBox
// inputBoxStub = sinon.stub(vscode.window, 'showInputBox');
await vscode.commands.executeCommand('workbench.view.extension.openmcp-sidebar');
});
teardown(() => {
sinon.restore();
});
test('新建STDIO连接', async function () {
this.timeout(15000);
// await vscode.commands.executeCommand('openmcp.sidebar.workspace-connection.addConnection');
// quickPickStub.onFirstCall().resolves('STDIO');
// await new Promise(resolve => setTimeout(resolve, 5000));
// inputBoxStub.onFirstCall().resolves('echo'); // command
// await new Promise(resolve => setTimeout(resolve, 5000));
// inputBoxStub.onSecondCall().resolves(''); // cwd
await vscode.commands.executeCommand('openmcp.sidebar.workspace-connection.addConnection');
});
test('等待以便观察窗口', async function () {
this.timeout(15000);
await new Promise(resolve => setTimeout(resolve, 10000));
});
});

View File

@ -0,0 +1,37 @@
import * as path from 'path';
import Mocha from 'mocha';
import glob from 'glob';
export function run(): Promise<void> {
// Create the mocha test
const mocha = new Mocha({
ui: 'tdd'
});
const testsRoot = path.resolve(__dirname, '..');
return new Promise((c, e) => {
glob('**/**.test.js', { cwd: testsRoot }, (err: Error | null, files: string[]) => {
if (err) {
return e(err);
}
// Add files to the test suite
files.forEach(f => mocha.addFile(path.resolve(testsRoot, f)));
try {
// Run the mocha test
mocha.run(failures => {
if (failures > 0) {
e(new Error(`${failures} tests failed.`));
} else {
c();
}
});
} catch (err) {
console.error(err);
e(err);
}
});
});
}

View File

@ -1,7 +1,7 @@
import * as vscode from 'vscode'; import * as vscode from 'vscode';
import { RegisterCommand } from "../common"; import { RegisterCommand } from "../common/index.js";
import { getDefaultLanunchSignature, getWorkspacePath, revealOpenMcpWebviewPanel } from './webview.service'; import { getDefaultLanunchSignature, getWorkspacePath, revealOpenMcpWebviewPanel } from './webview.service.js';
import { getWorkspaceConnectionConfigItemByPath } from '../global'; import { getWorkspaceConnectionConfigItemByPath } from '../global.js';
import path from 'path'; import path from 'path';
export class WebviewController { export class WebviewController {

View File

@ -1,8 +1,8 @@
import * as vscode from 'vscode'; import * as vscode from 'vscode';
import * as fs from 'fs'; import * as fs from 'fs';
import * as fspath from 'path'; import * as fspath from 'path';
import { ConnectionType, McpOptions, panels, updateInstalledConnectionConfig, updateWorkspaceConnectionConfig } from '../global'; import { ConnectionType, exportFile, McpOptions, panels, updateInstalledConnectionConfig, updateWorkspaceConnectionConfig } from '../global.js';
import { routeMessage } from '../../openmcp-sdk/service'; import { routeMessage } from '../../openmcp-sdk/service/index.js';
export function getWebviewContent(context: vscode.ExtensionContext, panel: vscode.WebviewPanel): string | undefined { export function getWebviewContent(context: vscode.ExtensionContext, panel: vscode.WebviewPanel): string | undefined {
const viewRoot = fspath.join(context.extensionPath, 'openmcp-sdk', 'renderer'); const viewRoot = fspath.join(context.extensionPath, 'openmcp-sdk', 'renderer');
@ -45,7 +45,7 @@ export function revealOpenMcpWebviewPanel(
// 对老版本的 option 进行调整 // 对老版本的 option 进行调整
option = Array.isArray(option)? option : [option]; option = Array.isArray(option)? option : [option];
option.forEach(item => { option.forEach((item: McpOptions) => {
const itemType = (item.type || item.connectionType).toUpperCase() as ConnectionType; const itemType = (item.type || item.connectionType).toUpperCase() as ConnectionType;
item.type = undefined; item.type = undefined;
item.connectionType = itemType; item.connectionType = itemType;
@ -101,6 +101,10 @@ export function revealOpenMcpWebviewPanel(
} }
break; break;
case 'vscode/export-file':
exportFile(data.filename, data.content);
break;
case 'vscode/clipboard/writeText': case 'vscode/clipboard/writeText':
vscode.env.clipboard.writeText(data.text); vscode.env.clipboard.writeText(data.text);
break; break;

Some files were not shown because too many files have changed in this diff Show More