commit
3cae8572e8
169
CLAUDE.md
Normal file
169
CLAUDE.md
Normal 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
|
@ -151,4 +151,11 @@ 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)
|
||||||
|
909
package-lock.json
generated
909
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
11
package.json
11
package.json
@ -2,7 +2,7 @@
|
|||||||
"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.6",
|
"version": "0.1.7",
|
||||||
"publisher": "kirigaya",
|
"publisher": "kirigaya",
|
||||||
"author": {
|
"author": {
|
||||||
"name": "kirigaya",
|
"name": "kirigaya",
|
||||||
@ -224,15 +224,14 @@
|
|||||||
"setup": "npm i && npm run prepare:ocr",
|
"setup": "npm i && npm run prepare:ocr",
|
||||||
"serve": "turbo serve",
|
"serve": "turbo serve",
|
||||||
"build": "turbo build && tsc -p ./",
|
"build": "turbo build && tsc -p ./",
|
||||||
"build:electron": "turbo build --filter=@openmcp/electron",
|
"build:plugin": "npm run build && tsc && vsce package",
|
||||||
"build:all": "turbo build",
|
|
||||||
"vscode:prepublish": "rollup --config rollup.config.js",
|
"vscode:prepublish": "rollup --config rollup.config.js",
|
||||||
"compile": "tsc -p ./",
|
"compile": "tsc -p ./",
|
||||||
"watch": "tsc -watch -p ./",
|
"watch": "tsc -watch -p ./",
|
||||||
"pretest": "npm run build",
|
"pretest": "npm run build",
|
||||||
"lint": "eslint src --ext ts",
|
"lint": "eslint src --ext ts",
|
||||||
"test": "node ./dist/test/e2e/runTest.js",
|
"test": "node ./dist/test/e2e/runTest.js",
|
||||||
"prepare:ocr": "rollup --config rollup.tesseract.js",
|
"prepare:ocr": "rollup -c rollup.tesseract.js --bundleConfigAsCjs",
|
||||||
"build:task-loop": "npx vite build --config renderer/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": {
|
||||||
@ -249,7 +248,8 @@
|
|||||||
"ws": "^8.18.1"
|
"ws": "^8.18.1"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@rollup/plugin-commonjs": "^28.0.3",
|
"@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-json": "^6.1.0",
|
||||||
"@rollup/plugin-node-resolve": "^16.0.1",
|
"@rollup/plugin-node-resolve": "^16.0.1",
|
||||||
@ -264,6 +264,7 @@
|
|||||||
"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": "^4.43.0",
|
||||||
|
"rollup-plugin-copy": "^3.5.0",
|
||||||
"rollup-plugin-visualizer": "^6.0.1",
|
"rollup-plugin-visualizer": "^6.0.1",
|
||||||
"ts-loader": "^9.5.1",
|
"ts-loader": "^9.5.1",
|
||||||
"turbo": "^2.5.3",
|
"turbo": "^2.5.3",
|
||||||
|
3
renderer/public/images/openrouter.ai.ico
Normal file
3
renderer/public/images/openrouter.ai.ico
Normal 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
|
@ -17,7 +17,13 @@
|
|||||||
<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>
|
<el-tooltip
|
||||||
|
:content="tool.description || ''"
|
||||||
|
:disabled="!tool.description || tool.description.length <= 30"
|
||||||
|
placement="top"
|
||||||
|
:show-after="500">
|
||||||
|
<span class="tool-description">{{ tool.description || '' }}</span>
|
||||||
|
</el-tooltip>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</el-scrollbar>
|
</el-scrollbar>
|
||||||
@ -27,7 +33,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';
|
||||||
@ -122,16 +128,18 @@ 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-description {
|
||||||
opacity: 0.6;
|
opacity: 0.6;
|
||||||
font-size: 12.5px;
|
font-size: 12.5px;
|
||||||
max-width: 200px;
|
max-width: 150px;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
|
@ -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', {
|
|
||||||
|
// 检查是否为动态模型加载(如OpenRouter)
|
||||||
|
let result;
|
||||||
|
if (llm.isDynamic && llm.id === 'openrouter') {
|
||||||
|
result = await bridge.commandRequest('llm/models/openrouter', {});
|
||||||
|
} else {
|
||||||
|
result = await bridge.commandRequest('llm/models', {
|
||||||
apiKey,
|
apiKey,
|
||||||
baseURL,
|
baseURL,
|
||||||
proxyServer
|
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 {
|
} else {
|
||||||
ElMessage.error('模型列表更新失败' + msg);
|
ElMessage.success('模型列表更新成功');
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
ElMessage.error('模型列表更新失败: ' + msg);
|
||||||
}
|
}
|
||||||
updateModelLoading.value = false;
|
updateModelLoading.value = false;
|
||||||
}
|
}
|
||||||
|
@ -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>
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,15 +1,37 @@
|
|||||||
import resolve from '@rollup/plugin-node-resolve';
|
import path from 'path';
|
||||||
|
import { nodeResolve } from '@rollup/plugin-node-resolve';
|
||||||
import commonjs from '@rollup/plugin-commonjs';
|
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 {
|
export default {
|
||||||
input: 'src/ocr-worker.ts',
|
input: './node_modules/tesseract.js/src/worker-script/node/index.js',
|
||||||
output: {
|
output: {
|
||||||
file: 'dist/ocr-worker.js',
|
file: path.resolve(__dirname, '..', 'resources', 'ocr', 'worker.js'),
|
||||||
format: 'es'
|
format: 'cjs',
|
||||||
|
exports: 'auto'
|
||||||
},
|
},
|
||||||
plugins: [
|
plugins: [
|
||||||
resolve(),
|
json(), // ✅ 插入 JSON 插件
|
||||||
commonjs()
|
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: ['tesseract.js'] // 显式排除 Tesseract
|
external: ['bufferutil', 'utf-8-validate']
|
||||||
};
|
};
|
@ -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
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
|
||||||
|
100
service/src/hook/openrouter.ts
Normal file
100
service/src/hook/openrouter.ts
Normal 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
|
||||||
|
}));
|
||||||
|
}
|
@ -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)}`
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
@ -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;
|
||||||
|
@ -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);
|
||||||
|
@ -1,32 +0,0 @@
|
|||||||
import * as path from 'path';
|
|
||||||
|
|
||||||
import { runTests } from '@vscode/test-electron';
|
|
||||||
import { fileURLToPath } from 'url';
|
|
||||||
|
|
||||||
// 将 import.meta.url 转换为文件路径
|
|
||||||
const __filename = fileURLToPath(import.meta.url);
|
|
||||||
const __dirname = path.dirname(__filename);
|
|
||||||
|
|
||||||
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();
|
|
@ -1,15 +0,0 @@
|
|||||||
import * as assert from 'assert';
|
|
||||||
|
|
||||||
// You can import and use all API from the 'vscode' module
|
|
||||||
// as well as import your extension to test it
|
|
||||||
import * as vscode from 'vscode';
|
|
||||||
// import * as myExtension from '../../extension';
|
|
||||||
|
|
||||||
suite('Extension Test Suite', () => {
|
|
||||||
// vscode.window.showInformationMessage('Start all tests.');
|
|
||||||
console.log("Running sample test")
|
|
||||||
test('Sample test', () => {
|
|
||||||
assert.strictEqual([1, 2, 3].indexOf(5), -1);
|
|
||||||
assert.strictEqual([1, 2, 3].indexOf(0), -1);
|
|
||||||
});
|
|
||||||
});
|
|
@ -1,43 +0,0 @@
|
|||||||
|
|
||||||
import * as path from 'path';
|
|
||||||
import Mocha from 'mocha';
|
|
||||||
import {glob} from 'glob';
|
|
||||||
import { fileURLToPath } from 'url';
|
|
||||||
|
|
||||||
const __filename = fileURLToPath(import.meta.url);
|
|
||||||
const __dirname = path.dirname(__filename);
|
|
||||||
|
|
||||||
|
|
||||||
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 }).then((files: string[]) => {
|
|
||||||
// Add files to the test suite
|
|
||||||
console.log('Test files found:', files); // Log the found test files
|
|
||||||
files.forEach((f: string) => mocha.addFile(path.resolve(testsRoot, f)));
|
|
||||||
|
|
||||||
try {
|
|
||||||
// Run the mocha test
|
|
||||||
console.log("Running test:", files)
|
|
||||||
mocha.run(failures => {
|
|
||||||
if (failures > 0) {
|
|
||||||
e(new Error(`${failures} tests failed.`));
|
|
||||||
} else {
|
|
||||||
c();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
} catch (err) {
|
|
||||||
console.error(err);
|
|
||||||
e(err);
|
|
||||||
}
|
|
||||||
}).catch(err => {
|
|
||||||
e(err);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
@ -9,6 +9,8 @@
|
|||||||
"esModuleInterop": true,
|
"esModuleInterop": true,
|
||||||
"skipLibCheck": true,
|
"skipLibCheck": true,
|
||||||
"forceConsistentCasingInFileNames": true,
|
"forceConsistentCasingInFileNames": true,
|
||||||
|
"importHelpers": false,
|
||||||
|
"noEmitHelpers": false,
|
||||||
"experimentalDecorators": true,
|
"experimentalDecorators": true,
|
||||||
// 允许访问 openmcp-sdk 目录
|
// 允许访问 openmcp-sdk 目录
|
||||||
"paths": {
|
"paths": {
|
||||||
|
Loading…
x
Reference in New Issue
Block a user