kuaifan 0ac4b546ba feat(ai-assistant): 实现 AI 前端操作能力
新增三个 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 监听
2026-01-18 01:35:13 +00:00

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;