Files
HuLa-MCP/test-client.html
2025-04-01 03:34:05 +08:00

455 lines
18 KiB
HTML

<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>HuLa MCP 测试客户端</title>
<style>
body {
font-family: 'Microsoft YaHei', Arial, sans-serif;
max-width: 800px;
margin: 0 auto;
padding: 20px;
line-height: 1.6;
}
h1, h2 {
color: #333;
}
.container {
display: flex;
flex-direction: column;
gap: 20px;
}
.card {
border: 1px solid #ddd;
border-radius: 8px;
padding: 15px;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
}
.log-container {
background-color: #f5f5f5;
border: 1px solid #ddd;
border-radius: 4px;
padding: 10px;
height: 300px;
overflow-y: auto;
font-family: monospace;
margin-top: 10px;
}
.log-entry {
margin: 5px 0;
word-break: break-all;
}
.log-entry.request {
color: #0066cc;
}
.log-entry.response {
color: #009900;
}
.log-entry.error {
color: #cc0000;
}
button {
background-color: #4CAF50;
color: white;
border: none;
padding: 10px 15px;
text-align: center;
text-decoration: none;
display: inline-block;
font-size: 16px;
margin: 4px 2px;
cursor: pointer;
border-radius: 4px;
}
button:hover {
background-color: #45a049;
}
input, select {
padding: 8px;
margin: 5px 0;
border: 1px solid #ddd;
border-radius: 4px;
width: 100%;
}
.form-group {
margin-bottom: 10px;
}
.form-group label {
display: block;
margin-bottom: 5px;
font-weight: bold;
}
.json-viewer {
background-color: #f8f8f8;
border: 1px solid #ddd;
border-radius: 4px;
padding: 10px;
margin-top: 10px;
max-height: 200px;
overflow-y: auto;
font-family: monospace;
white-space: pre-wrap;
}
</style>
</head>
<body>
<h1>HuLa MCP 测试客户端</h1>
<div class="container">
<div class="card">
<h2>连接状态</h2>
<div>
<span id="connection-status">未连接</span>
<button id="connect-btn">连接到MCP服务</button>
<button id="disconnect-btn" disabled>断开连接</button>
</div>
</div>
<div class="card">
<h2>资源测试</h2>
<div class="form-group">
<label for="resource-select">选择资源:</label>
<select id="resource-select">
<option value="users://list">用户列表 (users://list)</option>
<option value="users://online">在线用户 (users://online)</option>
<option value="groups://list">群组列表 (groups://list)</option>
<option value="users://user-1">用户详情 (users://user-1)</option>
<option value="groups://group-1">群组详情 (groups://group-1)</option>
<option value="users://user-1/conversations">用户会话 (users://user-1/conversations)</option>
<option value="groups://group-1/members">群组成员 (groups://group-1/members)</option>
<option value="users://user-1/unread">未读消息数 (users://user-1/unread)</option>
</select>
</div>
<button id="fetch-resource-btn">获取资源</button>
<div id="resource-result" class="json-viewer" style="display: none;"></div>
</div>
<div class="card">
<h2>工具测试</h2>
<div class="form-group">
<label for="tool-select">选择工具:</label>
<select id="tool-select">
<option value="send-message">发送消息 (send-message)</option>
<option value="mark-message-read">标记消息已读 (mark-message-read)</option>
<option value="update-user-status">更新用户状态 (update-user-status)</option>
<option value="search-messages">搜索消息 (search-messages)</option>
</select>
</div>
<div id="tool-params-container">
<!-- 动态生成的参数输入框将显示在这里 -->
</div>
<button id="call-tool-btn">调用工具</button>
<div id="tool-result" class="json-viewer" style="display: none;"></div>
</div>
<div class="card">
<h2>日志</h2>
<div class="log-container" id="log-container"></div>
<button id="clear-log-btn">清除日志</button>
</div>
</div>
<script>
// 服务器配置
const SERVER_URL = window.location.origin; // 自动获取当前服务器URL
const SSE_ENDPOINT = `${SERVER_URL}/sse`;
const MESSAGES_ENDPOINT = `${SERVER_URL}/messages`;
// 全局变量
let sessionId = null;
let eventSource = null;
// DOM元素
const connectBtn = document.getElementById('connect-btn');
const disconnectBtn = document.getElementById('disconnect-btn');
const connectionStatus = document.getElementById('connection-status');
const resourceSelect = document.getElementById('resource-select');
const fetchResourceBtn = document.getElementById('fetch-resource-btn');
const resourceResult = document.getElementById('resource-result');
const toolSelect = document.getElementById('tool-select');
const toolParamsContainer = document.getElementById('tool-params-container');
const callToolBtn = document.getElementById('call-tool-btn');
const toolResult = document.getElementById('tool-result');
const logContainer = document.getElementById('log-container');
const clearLogBtn = document.getElementById('clear-log-btn');
// 工具参数定义
const toolParams = {
'send-message': [
{ name: 'content', type: 'text', label: '消息内容', required: true },
{ name: 'senderId', type: 'text', label: '发送者ID', required: true, default: 'user-1' },
{ name: 'receiverId', type: 'text', label: '接收者ID', required: true, default: 'user-2' },
{ name: 'type', type: 'select', label: '消息类型', options: ['text', 'image', 'file', 'audio', 'video'], default: 'text' },
{ name: 'isGroup', type: 'checkbox', label: '是否群组消息', default: false }
],
'mark-message-read': [
{ name: 'messageId', type: 'text', label: '消息ID', required: true, default: 'msg-3' },
{ name: 'userId', type: 'text', label: '用户ID', required: true, default: 'user-1' }
],
'update-user-status': [
{ name: 'userId', type: 'text', label: '用户ID', required: true, default: 'user-1' },
{ name: 'status', type: 'select', label: '状态', options: ['online', 'offline', 'away'], default: 'online' }
],
'search-messages': [
{ name: 'query', type: 'text', label: '搜索关键词', required: true },
{ name: 'userId', type: 'text', label: '用户ID', required: true, default: 'user-1' },
{ name: 'limit', type: 'number', label: '结果数量限制', default: 10 }
]
};
// 格式化JSON
function formatJSON(obj) {
return JSON.stringify(obj, null, 2);
}
// 日志函数
function log(message, type = 'info') {
const entry = document.createElement('div');
entry.className = `log-entry ${type}`;
entry.textContent = `[${new Date().toLocaleTimeString()}] ${message}`;
logContainer.appendChild(entry);
logContainer.scrollTop = logContainer.scrollHeight;
}
// 连接到MCP服务
connectBtn.addEventListener('click', () => {
if (eventSource) {
eventSource.close();
}
connectionStatus.textContent = '正在连接...';
log('正在连接到MCP服务...');
log(`SSE端点: ${SSE_ENDPOINT}`, 'request');
try {
// 创建SSE连接
eventSource = new EventSource(SSE_ENDPOINT);
eventSource.addEventListener('open', () => {
log('SSE连接已建立', 'response');
});
eventSource.addEventListener('error', (event) => {
log(`SSE连接错误`, 'error');
connectionStatus.textContent = '连接失败';
connectBtn.disabled = false;
disconnectBtn.disabled = true;
if (eventSource.readyState === EventSource.CLOSED) {
log('SSE连接已关闭', 'error');
eventSource = null;
}
});
eventSource.addEventListener('message', (event) => {
try {
const data = JSON.parse(event.data);
log(`收到消息: ${JSON.stringify(data)}`, 'response');
// 如果收到sessionId,保存它
if (data.type === 'hello' && data.sessionId) {
sessionId = data.sessionId;
connectionStatus.textContent = `已连接 (会话ID: ${sessionId})`;
connectBtn.disabled = true;
disconnectBtn.disabled = false;
log(`已获取会话ID: ${sessionId}`, 'response');
}
} catch (error) {
log(`解析消息失败: ${error.message}`, 'error');
}
});
} catch (error) {
log(`创建SSE连接失败: ${error.message}`, 'error');
connectionStatus.textContent = '连接失败';
}
});
// 断开连接
disconnectBtn.addEventListener('click', () => {
if (eventSource) {
log('正在断开连接...');
eventSource.close();
eventSource = null;
sessionId = null;
connectionStatus.textContent = '已断开连接';
connectBtn.disabled = false;
disconnectBtn.disabled = true;
log('已断开与MCP服务的连接');
}
});
// 获取资源
fetchResourceBtn.addEventListener('click', async () => {
if (!sessionId) {
log('请先连接到MCP服务', 'error');
return;
}
const resourceUri = resourceSelect.value;
log(`正在获取资源: ${resourceUri}`, 'request');
resourceResult.style.display = 'none';
try {
const response = await fetch(`${MESSAGES_ENDPOINT}?sessionId=${sessionId}`, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
type: 'getResource',
uri: resourceUri
})
});
if (!response.ok) {
throw new Error(`HTTP错误: ${response.status}`);
}
const data = await response.json();
log(`资源响应已接收`, 'response');
// 显示结果
resourceResult.textContent = formatJSON(data);
resourceResult.style.display = 'block';
} catch (error) {
log(`获取资源失败: ${error.message}`, 'error');
}
});
// 生成工具参数输入框
toolSelect.addEventListener('change', () => {
const selectedTool = toolSelect.value;
toolParamsContainer.innerHTML = '';
toolResult.style.display = 'none';
if (toolParams[selectedTool]) {
toolParams[selectedTool].forEach(param => {
const formGroup = document.createElement('div');
formGroup.className = 'form-group';
const label = document.createElement('label');
label.textContent = param.label + (param.required ? ' *' : '');
label.htmlFor = `param-${param.name}`;
formGroup.appendChild(label);
if (param.type === 'select') {
const select = document.createElement('select');
select.id = `param-${param.name}`;
select.name = param.name;
param.options.forEach(option => {
const optionElement = document.createElement('option');
optionElement.value = option;
optionElement.textContent = option;
if (param.default === option) {
optionElement.selected = true;
}
select.appendChild(optionElement);
});
formGroup.appendChild(select);
} else if (param.type === 'checkbox') {
const checkbox = document.createElement('input');
checkbox.type = 'checkbox';
checkbox.id = `param-${param.name}`;
checkbox.name = param.name;
checkbox.checked = param.default || false;
formGroup.appendChild(checkbox);
} else {
const input = document.createElement('input');
input.type = param.type;
input.id = `param-${param.name}`;
input.name = param.name;
if (param.default !== undefined) {
input.value = param.default;
}
formGroup.appendChild(input);
}
toolParamsContainer.appendChild(formGroup);
});
}
});
// 初始化工具参数输入框
toolSelect.dispatchEvent(new Event('change'));
// 调用工具
callToolBtn.addEventListener('click', async () => {
if (!sessionId) {
log('请先连接到MCP服务', 'error');
return;
}
const selectedTool = toolSelect.value;
const params = {};
let hasError = false;
if (toolParams[selectedTool]) {
toolParams[selectedTool].forEach(param => {
const element = document.getElementById(`param-${param.name}`);
if (param.type === 'checkbox') {
params[param.name] = element.checked;
} else if (param.type === 'number') {
params[param.name] = Number(element.value);
} else {
params[param.name] = element.value;
}
if (param.required && !params[param.name] && params[param.name] !== false) {
log(`参数 ${param.name} 是必填的`, 'error');
hasError = true;
}
});
}
if (hasError) return;
toolResult.style.display = 'none';
log(`正在调用工具: ${selectedTool},参数: ${JSON.stringify(params)}`, 'request');
try {
const response = await fetch(`${MESSAGES_ENDPOINT}?sessionId=${sessionId}`, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
type: 'callTool',
name: selectedTool,
params: params
})
});
if (!response.ok) {
throw new Error(`HTTP错误: ${response.status}`);
}
const data = await response.json();
log(`工具响应已接收`, 'response');
// 显示结果
toolResult.textContent = formatJSON(data);
toolResult.style.display = 'block';
} catch (error) {
log(`调用工具失败: ${error.message}`, 'error');
}
});
// 清除日志
clearLogBtn.addEventListener('click', () => {
logContainer.innerHTML = '';
log('日志已清除');
});
// 初始化
log(`服务器URL: ${SERVER_URL}`);
log(`SSE端点: ${SSE_ENDPOINT}`);
log(`消息端点: ${MESSAGES_ENDPOINT}`);
log('测试客户端已准备就绪,请点击"连接到MCP服务"按钮开始测试');
</script>
</body>
</html>