3 Commits
v2.0.0 ... main

Author SHA1 Message Date
douboer
5a3e821ef4 update at 2025-11-11 17:44:50 2025-11-11 17:44:50 +08:00
douboer
052f2f340e update at 2025-11-01 17:57:04 2025-11-01 17:57:04 +08:00
douboer
d66245f767 update at 2025-10-16 12:51:04 2025-10-16 12:51:04 +08:00
5 changed files with 187 additions and 23 deletions

View File

@@ -3,7 +3,10 @@
> **版本1.0.0** | 基础版本 + 最新改进
作者Gavin Chan
基于 **Vue 3** 和 **MCP 协议**构建的现代化 MCP 客户端界面支持HTTP和SSE双传输协议。
比较粗糙,但跑通了模型 API 的对接使用,和 MCP server 的调用。
## 🎉 最新改进(开发中)
@@ -21,7 +24,7 @@
- 🔧 **双协议支持** - HTTP 和 SSE 传输模式
- 🔄 **自动重连** - 页面刷新后自动恢复连接状态
- ⚡ **实时状态** - 连接状态实时监控和显示
- 📝 **完整管理** - 服务器配置、编辑、测试一体化
- <EFBFBD> **完整管理** - 服务器配置、编辑、测试一体化
- 📱 **响应式设计** - 适配桌面和移动设备
## 🏗️ 项目架构
@@ -73,7 +76,7 @@ npx vite --port 5174
```
名称: XHS HTTP Server
类型: http
URL: http://localhost:3100
URL: http://localhost:3100/mcp
描述: HTTP传输模式
```

1
node_modules Symbolic link
View File

@@ -0,0 +1 @@
/Users/gavin/lib/node_modules

View File

@@ -27,10 +27,10 @@ sk-2546da09b6d9471894aeb95278f96c11
## 优化
该项目经过反复重构,重构过程关注功能实现,没有关注性能、结构合理性、和实现的优雅性。全量分析,提供优化点及思路。
1. 该项目经过反复重构,重构过程关注功能实现,没有关注性能、结构合理性、和实现的优雅性。全量分析,提供优化点及思路。
**先优化,在做数据库改造**。
### 重构进度 (2024-01-XX)
### 重构进度 (2025-10-16)
#### Phase 1: 核心服务拆分 (Day 1-2) ✅ 已完成
- ✅ Step 1: 创建服务目录结构 `/web/src/services/chat/`
@@ -82,3 +82,16 @@ Pinia store、localStorage、内存状态三处保存数据
使用sqlite3 vs. better-sqlite3持久化性能开销
没有统一的数据源?
5. 当前实现client参数重带图片pathserver收到后按path发布图片。目前client/server部署在同一个服务器测试没问题因为server可以从path找到图片。
但问题是server部署如果部署在远程服务器上用户是client需要使用mcp server发布文章图片在client侧处理好需要送到远程服务器上否则server找不到图片。在多client用户使用mcp server下进一步需要考虑几个问题
- 图片通过什么方式传送到远程服务器?
- 用户publish content时需要等待图片上传完成等待时间根据网络状态可能会很长
本来用户发布文章到xhs本地之间上传图片到xhs现在多了一个环节图片上传mcp servermcp server在上传图片到xhs。
- 图片上传和发布文章能不能解耦比如用户先传送图片缓存到mcp server。需要的时候再发布文章。
但这样,用户操作会很繁琐。
- 上传图片到mcp server还有一个存放位置问题。client的path参数用什么上传到哪个目录发布时从哪个目录寻找
如果上传图片最好约定一套策略path中只要填文件名mcp server的路径不需要client考虑。
- 如果用上传图片方式大量client接入的排队机制怎么处理client采用异步方式递交点击发送/发布,可以去喝茶了,不必考虑多久完成。
- mcp server侧需要考虑的机制。

View File

@@ -242,7 +242,7 @@
<div class="progress-info">
<p>
<strong>当前进度:</strong>
{{ healthCheckResult.progress.current }} / {{ healthCheckResult.progress.total }}
{{ healthCheckResult.progress.current }} / {{ healthCheckResult.progress.total }}
</p>
<p><strong>当前模型:</strong> {{ healthCheckResult.progress.modelId }}</p>
</div>
@@ -250,8 +250,13 @@
type="line"
:percentage="healthCheckResult.progress.total > 0 ?
(healthCheckResult.progress.current / healthCheckResult.progress.total * 100) : 0"
:show-indicator="true"
:show-indicator="false"
/>
<div class="progress-details">
<span>{{ healthCheckResult.progress.current }}</span>
<span>{{ healthCheckResult.progress.total }}</span>
<span>{{ healthCheckResult.progress.current > 0 ? Math.round((healthCheckResult.progress.current / healthCheckResult.progress.total) * 100) : 0 }}%</span>
</div>
</div>
<div v-else-if="healthCheckResult.status === 'success'" class="check-success">
@@ -262,7 +267,7 @@
<div class="summary-card available">
<div class="card-icon"></div>
<div class="card-info">
<div class="card-number">{{ healthCheckResult.availableModels.length }}</div>
<div class="card-number">{{ healthCheckResult.availableModels.length }}</div>
<div class="card-label">可用模型</div>
</div>
</div>
@@ -270,7 +275,7 @@
<div class="summary-card unavailable">
<div class="card-icon"></div>
<div class="card-info">
<div class="card-number">{{ healthCheckResult.unavailableModels.length }}</div>
<div class="card-number">{{ healthCheckResult.unavailableModels.length }}</div>
<div class="card-label">不可用模型</div>
</div>
</div>
@@ -279,7 +284,7 @@
<!-- 详细结果 -->
<div class="detailed-results">
<div v-if="healthCheckResult.availableModels.length > 0" class="result-section">
<h4> 可用模型 ({{ healthCheckResult.availableModels.length }})</h4>
<h4> 可用模型 ({{ healthCheckResult.availableModels.length }})</h4>
<div class="model-list">
<n-tag
v-for="result in healthCheckResult.results.filter(r => r.available)"
@@ -295,7 +300,7 @@
</div>
<div v-if="healthCheckResult.unavailableModels.length > 0" class="result-section">
<h4> 不可用模型 ({{ healthCheckResult.unavailableModels.length }})</h4>
<h4> 不可用模型 ({{ healthCheckResult.unavailableModels.length }})</h4>
<div class="model-list">
<n-tag
v-for="result in healthCheckResult.results.filter(r => !r.available)"
@@ -539,6 +544,7 @@ const healthCheckModels = async (service: ModelService) => {
const result = await modelServiceManager.healthCheckAllModels(
service,
(current, total, modelId) => {
// 直接更新进度,不使用防抖
healthCheckResult.progress = { current, total, modelId }
}
)
@@ -1104,6 +1110,21 @@ onMounted(() => {
font-size: 14px;
}
.progress-details {
display: flex;
justify-content: space-between;
margin-top: 8px;
padding: 8px 12px;
background: #f0f0f0;
border-radius: 4px;
font-size: 14px;
color: #333;
}
.progress-details span {
font-weight: 500;
}
.check-success h3 {
margin: 16px 0;
color: #18a058;

View File

@@ -919,6 +919,124 @@ export class ModelServiceManager {
}
}
// 带自定义超时的聊天请求(用于健康检测)
private async makeChatRequestWithTimeout(service: ModelService, messages: any[], model: string, timeoutMs: number): Promise<any> {
const controller = new AbortController()
const timeoutId = setTimeout(() => controller.abort(), timeoutMs)
const headers: HeadersInit = {
'Content-Type': 'application/json'
}
let url = ''
let body: any = {}
// 构建请求与makeChatRequest相同的逻辑
switch (service.type) {
case 'openai':
case 'local':
headers['Authorization'] = `Bearer ${service.apiKey}`
url = `${service.url}/chat/completions`
body = {
model,
messages,
stream: false
}
break
case 'dashscope':
headers['Authorization'] = `Bearer ${service.apiKey}`
url = `${service.url}/chat/completions`
body = {
model,
messages,
stream: false,
parameters: {}
}
break
case 'volcengine':
headers['Authorization'] = `Bearer ${service.apiKey}`
url = `${service.url}/chat/completions`
body = {
model,
messages,
stream: false
}
break
case 'claude':
headers['x-api-key'] = service.apiKey
headers['anthropic-version'] = '2023-06-01'
url = `${service.url}/messages`
body = {
model,
messages: this.convertToClaudeFormat(messages),
max_tokens: 4096
}
break
case 'gemini':
url = `${service.url}/models/${model}:generateContent?key=${service.apiKey}`
body = {
contents: this.convertToGeminiFormat(messages)
}
break
case 'azure':
headers['api-key'] = service.apiKey
url = `${service.url}/openai/deployments/${model}/chat/completions?api-version=2023-12-01-preview`
body = {
messages,
stream: false
}
break
case 'custom':
try {
const config = JSON.parse(service.customConfig || '{}')
Object.assign(headers, config.headers || {})
} catch (e) {
console.warn('自定义配置解析失败:', e)
}
url = `${service.url}/chat/completions`
body = {
model,
messages,
stream: false
}
break
default:
throw new Error(`不支持的服务类型: ${service.type}`)
}
try {
const response = await fetch(url, {
method: 'POST',
headers,
body: JSON.stringify(body),
signal: controller.signal
})
clearTimeout(timeoutId)
if (!response.ok) {
const errorText = await response.text()
throw new Error(`HTTP ${response.status}: ${errorText}`)
}
const result = await response.json()
return result
} catch (error) {
clearTimeout(timeoutId)
if (error instanceof Error && error.name === 'AbortError') {
throw new Error(`检测超时(${timeoutMs / 1000}秒)`)
}
throw error
}
}
// 健康检测 - 测试单个模型是否可用
async testModelHealth(service: ModelService, modelId: string): Promise<{
modelId: string
@@ -929,14 +1047,10 @@ export class ModelServiceManager {
const startTime = Date.now()
try {
// 发送一个最小的测试请求
const result = await this.sendChatRequest(service.id, [
// 使用3秒超时进行健康检测简化版
await this.makeChatRequestWithTimeout(service, [
{ role: 'user', content: 'hi' }
], modelId)
if (!result.success) {
throw new Error(result.error || '测试失败')
}
], modelId, 3000)
const latency = Date.now() - startTime
return {
@@ -945,9 +1059,11 @@ export class ModelServiceManager {
latency
}
} catch (error) {
const latency = Date.now() - startTime
return {
modelId,
available: false,
latency,
error: error instanceof Error ? error.message : '测试失败'
}
}
@@ -975,24 +1091,34 @@ export class ModelServiceManager {
error?: string
}> = []
// 初始进度
if (onProgress) {
onProgress(0, models.length, '准备开始检测...')
}
for (let i = 0; i < models.length; i++) {
const modelId = models[i]
// 通知进度
// 开始检测当前模型时通知进度
if (onProgress) {
onProgress(i + 1, models.length, modelId)
onProgress(i, models.length, `正在检测: ${modelId}`)
}
// 测试模型健康状态
const result = await this.testModelHealth(service, modelId)
results.push(result)
// 添加小延迟避免过快请求
if (i < models.length - 1) {
await new Promise(resolve => setTimeout(resolve, 200))
// 检测完成后更新进度
if (onProgress) {
onProgress(i + 1, models.length, `已完成: ${modelId}`)
}
}
// 最终进度
if (onProgress) {
onProgress(models.length, models.length, '检测完成')
}
// 统计结果
const availableModels = results.filter(r => r.available).map(r => r.modelId)
const unavailableModels = results.filter(r => !r.available).map(r => r.modelId)