mirror of
https://github.com/kuaifan/dootask.git
synced 2026-01-22 17:58:11 +00:00
新增三个 MCP 工具的前端支持: - get_page_context: 基于 ARIA 角色收集页面元素,支持分页和区域筛选 - execute_action: 执行导航操作(打开任务/对话、切换项目/页面) - execute_element_action: 元素级操作(click/type/select/focus/scroll/hover) 新增文件: - operation-client.js: WebSocket 客户端,处理与 MCP Server 的通信 - page-context-collector.js: 页面上下文收集器,ref 系统和 cursor:pointer 扫描 - action-executor.js: 操作执行器,支持智能解析如 open_task_123 - operation-module.js: 模块编排,整合上述模块 修改文件: - float-button.vue: 集成 operation-module,AI 助手打开时启用 - index.vue: 发射关闭事件供 float-button 监听
232 lines
5.6 KiB
JavaScript
Vendored
232 lines
5.6 KiB
JavaScript
Vendored
/**
|
|
* AI 助手前端操作 WebSocket 客户端
|
|
*
|
|
* 负责与 MCP Server 建立 WebSocket 连接,
|
|
* 接收来自 MCP 工具的请求并返回响应。
|
|
*/
|
|
|
|
const WS_PATH = '/apps/mcp_server/mcp/operation';
|
|
const RECONNECT_DELAY = 3000;
|
|
const MAX_RECONNECT_ATTEMPTS = 5;
|
|
|
|
/**
|
|
* 前端操作客户端
|
|
*/
|
|
export class OperationClient {
|
|
/**
|
|
* @param {Object} options
|
|
* @param {Function} options.getToken - 获取用户 token 的函数
|
|
* @param {Function} options.onRequest - 处理请求的回调函数
|
|
* @param {Function} options.onConnected - 连接成功回调
|
|
* @param {Function} options.onDisconnected - 断开连接回调
|
|
* @param {Function} options.onError - 错误回调
|
|
*/
|
|
constructor(options = {}) {
|
|
this.getToken = options.getToken;
|
|
this.onRequest = options.onRequest;
|
|
this.onConnected = options.onConnected;
|
|
this.onDisconnected = options.onDisconnected;
|
|
this.onError = options.onError;
|
|
|
|
this.ws = null;
|
|
this.sessionId = null;
|
|
this.expiresAt = null;
|
|
this.reconnectAttempts = 0;
|
|
this.reconnectTimer = null;
|
|
this.isConnecting = false;
|
|
this.isManualClose = false;
|
|
}
|
|
|
|
/**
|
|
* 建立 WebSocket 连接
|
|
*/
|
|
connect() {
|
|
if (this.ws && (this.ws.readyState === WebSocket.CONNECTING || this.ws.readyState === WebSocket.OPEN)) {
|
|
return;
|
|
}
|
|
|
|
if (this.isConnecting) {
|
|
return;
|
|
}
|
|
|
|
this.isConnecting = true;
|
|
this.isManualClose = false;
|
|
|
|
const token = this.getToken?.();
|
|
if (!token) {
|
|
this.isConnecting = false;
|
|
this.onError?.('未登录或 token 不可用');
|
|
return;
|
|
}
|
|
|
|
const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
|
|
const host = window.location.host;
|
|
const url = `${protocol}//${host}${WS_PATH}?token=${encodeURIComponent(token)}`;
|
|
|
|
try {
|
|
this.ws = new WebSocket(url);
|
|
this.setupEventHandlers();
|
|
} catch (error) {
|
|
this.isConnecting = false;
|
|
this.onError?.(error.message);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 设置 WebSocket 事件处理器
|
|
*/
|
|
setupEventHandlers() {
|
|
this.ws.onopen = () => {
|
|
this.isConnecting = false;
|
|
this.reconnectAttempts = 0;
|
|
};
|
|
|
|
this.ws.onmessage = (event) => {
|
|
this.handleMessage(event.data);
|
|
};
|
|
|
|
this.ws.onclose = (event) => {
|
|
this.isConnecting = false;
|
|
this.sessionId = null;
|
|
this.onDisconnected?.();
|
|
|
|
if (!this.isManualClose && this.reconnectAttempts < MAX_RECONNECT_ATTEMPTS) {
|
|
this.scheduleReconnect();
|
|
}
|
|
};
|
|
|
|
this.ws.onerror = () => {
|
|
this.isConnecting = false;
|
|
this.onError?.('WebSocket 连接错误');
|
|
};
|
|
}
|
|
|
|
/**
|
|
* 处理收到的消息
|
|
*/
|
|
handleMessage(data) {
|
|
let msg;
|
|
try {
|
|
msg = JSON.parse(data);
|
|
} catch {
|
|
return;
|
|
}
|
|
|
|
switch (msg.type) {
|
|
case 'connected':
|
|
this.sessionId = msg.session_id;
|
|
this.expiresAt = msg.expires_at;
|
|
this.onConnected?.(this.sessionId);
|
|
break;
|
|
|
|
case 'request':
|
|
this.handleRequest(msg);
|
|
break;
|
|
|
|
case 'pong':
|
|
// 心跳响应
|
|
break;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 处理来自 MCP 的请求
|
|
*/
|
|
async handleRequest(msg) {
|
|
const { id, action, payload } = msg;
|
|
|
|
if (!this.onRequest) {
|
|
this.sendResponse(id, false, null, '请求处理器未配置');
|
|
return;
|
|
}
|
|
|
|
try {
|
|
const result = await this.onRequest(action, payload);
|
|
this.sendResponse(id, true, result, null);
|
|
} catch (error) {
|
|
const errorMsg = error.message || '操作执行失败';
|
|
this.sendResponse(id, false, null, errorMsg);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 发送响应
|
|
*/
|
|
sendResponse(id, success, data, error) {
|
|
if (!this.ws || this.ws.readyState !== WebSocket.OPEN) {
|
|
return;
|
|
}
|
|
|
|
const response = {
|
|
type: 'response',
|
|
id,
|
|
success,
|
|
data,
|
|
error,
|
|
};
|
|
|
|
this.ws.send(JSON.stringify(response));
|
|
}
|
|
|
|
/**
|
|
* 发送心跳
|
|
*/
|
|
ping() {
|
|
if (this.ws && this.ws.readyState === WebSocket.OPEN) {
|
|
this.ws.send(JSON.stringify({ type: 'ping' }));
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 安排重连
|
|
*/
|
|
scheduleReconnect() {
|
|
if (this.reconnectTimer) {
|
|
clearTimeout(this.reconnectTimer);
|
|
}
|
|
|
|
this.reconnectAttempts++;
|
|
const delay = RECONNECT_DELAY * this.reconnectAttempts;
|
|
|
|
this.reconnectTimer = setTimeout(() => {
|
|
this.connect();
|
|
}, delay);
|
|
}
|
|
|
|
/**
|
|
* 断开连接
|
|
*/
|
|
disconnect() {
|
|
this.isManualClose = true;
|
|
|
|
if (this.reconnectTimer) {
|
|
clearTimeout(this.reconnectTimer);
|
|
this.reconnectTimer = null;
|
|
}
|
|
|
|
if (this.ws) {
|
|
this.ws.close();
|
|
this.ws = null;
|
|
}
|
|
|
|
this.sessionId = null;
|
|
this.reconnectAttempts = 0;
|
|
}
|
|
|
|
/**
|
|
* 获取当前 session ID
|
|
*/
|
|
getSessionId() {
|
|
return this.sessionId;
|
|
}
|
|
|
|
/**
|
|
* 检查是否已连接
|
|
*/
|
|
isConnected() {
|
|
return this.ws && this.ws.readyState === WebSocket.OPEN && this.sessionId;
|
|
}
|
|
}
|
|
|
|
export default OperationClient;
|