kuaifan 88fed0744c feat(ai-assistant): 页面操作穿透到当前微应用 iframe 内部
- 新增 active-context.js:按 microApps 解析最前的同源微应用 iframe(src 匹配 + contentDocument 同源探测),给出 doc/frameKey
- 采集器/执行器从只认主 document 泛化为按 el.ownerDocument 自适应,覆盖主文档与 iframe;视口与事件构造器取元素所在 window
- operation-module 加 scope(auto/main/app)、跨源/未就绪降级、frame 标注,并存活动上下文供失效守卫
- ai-kb 同步 page-action / page-context-tool / element-action 三 chunk

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-12 07:02:35 +00:00

358 lines
12 KiB
JavaScript
Vendored
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* 操作执行器
*
* 执行来自 MCP 工具的操作请求,包括:
* - 导航操作(打开任务、切换项目、跳转页面等)
* - 元素级操作(点击、输入等,作为兜底)
*
* 注意:数据操作(创建任务、发送消息等)应使用 MCP DooTask 工具直接调用 API
* 本模块只负责前端导航和 UI 操作。
*/
import { findElementByRef } from './page-context-collector';
import { resolveActiveContext } from './active-context';
/**
* 创建操作执行器
* @param {Object} store - Vuex store 实例
* @param {Object} router - Vue Router 实例
* @returns {Object} 执行器实例
*/
export function createActionExecutor(store, router) {
return new ActionExecutor(store, router);
}
class ActionExecutor {
constructor(store, router) {
this.store = store;
this.router = router;
// 导航操作注册表
this.actionHandlers = {
// 打开资源详情(通过 Vuex action
open_task: this.openTask.bind(this),
open_dialog: this.openDialog.bind(this),
// 页面导航(通过 goForward
open_project: this.openProject.bind(this),
open_file: this.openFile.bind(this),
open_folder: this.openFolder.bind(this),
// 功能页面导航
navigate_to_dashboard: this.navigateToDashboard.bind(this),
navigate_to_messenger: this.navigateToMessenger.bind(this),
navigate_to_calendar: this.navigateToCalendar.bind(this),
navigate_to_files: this.navigateToFiles.bind(this),
// 别名支持
goto_task: this.openTask.bind(this),
goto_project: this.openProject.bind(this),
goto_dialog: this.openDialog.bind(this),
navigate_to_task: this.openTask.bind(this),
navigate_to_project: this.openProject.bind(this),
navigate_to_dialog: this.openDialog.bind(this),
};
}
/**
* 执行导航操作
* @param {string} actionName - 操作名称
* @param {Object} params - 操作参数
* @returns {Promise<Object>} 执行结果
*/
async executeAction(actionName, params = {}) {
// 智能解析操作名,支持 open_task_358 这样的格式
const { normalizedAction, extractedParams } = this.parseActionName(actionName);
const mergedParams = { ...extractedParams, ...params };
const handler = this.actionHandlers[normalizedAction];
if (!handler) {
throw new Error(`不支持的操作: ${actionName}。支持的操作: ${Object.keys(this.actionHandlers).join(', ')}`);
}
try {
const result = await handler(mergedParams);
return {
success: true,
action: normalizedAction,
result,
};
} catch (error) {
throw new Error(`执行操作失败: ${error.message}`);
}
}
/**
* 解析操作名,提取嵌入的参数
* 支持格式: open_task_358 -> { normalizedAction: 'open_task', extractedParams: { task_id: 358 } }
*/
parseActionName(actionName) {
const patterns = [
{ regex: /^(open_task|goto_task|navigate_to_task)_(\d+)$/, paramName: 'task_id' },
{ regex: /^(open_project|goto_project|navigate_to_project)_(\d+)$/, paramName: 'project_id' },
{ regex: /^(open_dialog|goto_dialog|navigate_to_dialog)_(\d+)$/, paramName: 'dialog_id' },
{ regex: /^(open_file)_(\d+)$/, paramName: 'file_id' },
{ regex: /^(open_folder)_(\d+)$/, paramName: 'folder_id' },
];
for (const { regex, paramName } of patterns) {
const match = actionName.match(regex);
if (match) {
return {
normalizedAction: match[1],
extractedParams: { [paramName]: parseInt(match[2], 10) },
};
}
}
return { normalizedAction: actionName, extractedParams: {} };
}
// ========== 打开资源详情 ==========
/**
* 打开任务详情
*/
async openTask(params) {
const taskId = params.task_id;
if (!taskId) {
throw new Error('缺少 task_id 参数');
}
this.store.dispatch('openTask', taskId);
return { opened: true, task_id: taskId };
}
/**
* 打开对话
*/
async openDialog(params) {
const dialogId = params.dialog_id;
if (!dialogId) {
throw new Error('缺少 dialog_id 参数');
}
// 支持高级参数:跳转到特定消息
const dialogParams = params.msg_id
? { dialog_id: dialogId, search_msg_id: params.msg_id }
: dialogId;
this.store.dispatch('openDialog', dialogParams);
return { opened: true, dialog_id: dialogId };
}
// ========== 页面导航 ==========
/**
* 打开/切换到项目
*/
async openProject(params) {
const projectId = params.project_id;
if (!projectId) {
throw new Error('缺少 project_id 参数');
}
window.$A.goForward({ name: 'manage-project', params: { projectId } });
return { navigated: true, project_id: projectId };
}
/**
* 打开文件预览
*/
async openFile(params) {
const fileId = params.file_id;
if (!fileId) {
throw new Error('缺少 file_id 参数');
}
window.$A.goForward({ name: 'manage-file', params: { fileId } });
return { navigated: true, file_id: fileId };
}
/**
* 打开文件夹
*/
async openFolder(params) {
const folderId = params.folder_id;
if (!folderId) {
throw new Error('缺少 folder_id 参数');
}
window.$A.goForward({ name: 'manage-file', params: { folderId, fileId: null } });
return { navigated: true, folder_id: folderId };
}
/**
* 导航到仪表盘
*/
async navigateToDashboard() {
window.$A.goForward({ name: 'manage-dashboard' });
return { navigated: true, page: 'dashboard' };
}
/**
* 导航到消息页面
*/
async navigateToMessenger() {
window.$A.goForward({ name: 'manage-messenger' });
return { navigated: true, page: 'messenger' };
}
/**
* 导航到日历页面
*/
async navigateToCalendar() {
window.$A.goForward({ name: 'manage-calendar' });
return { navigated: true, page: 'calendar' };
}
/**
* 导航到文件管理页面
*/
async navigateToFiles() {
window.$A.goForward({ name: 'manage-file' });
return { navigated: true, page: 'files' };
}
// ========== 元素级操作 ==========
/**
* 设置当前的 refMap 与活动上下文(由 operation-module 在获取上下文后调用)
*/
setRefMap(refMap, context = null) {
this.currentRefMap = refMap;
this.currentContext = context;
}
/**
* 解析当前应执行元素操作的文档,并做失效守卫校验。
* 当上次采集发生在某个微应用 iframe 内时,执行前重新解析最前上下文,
* 若 frameKey 不一致(用户切了应用 / 重开过 / 刷新了页面)则拒绝,避免误操作。
* @returns {Document}
*/
resolveContextDoc() {
const saved = this.currentContext;
// 无上下文信息或主文档:直接用主文档
if (!saved || saved.kind === 'main' || saved.frameKey === 'main') {
return document;
}
const now = resolveActiveContext(this.store, saved.scope || 'auto');
if (now.frameKey !== saved.frameKey || !now.reachable || !now.doc) {
throw new Error('页面上下文已变更(用户切换了应用或刷新了页面),请重新获取页面上下文后再操作');
}
return now.doc;
}
/**
* 执行元素级操作
* @param {string} elementUid - 元素标识 (e1, e2, ... 或选择器)
* @param {string} action - 操作类型
* @param {string} value - 操作值
* @returns {Promise<Object>} 执行结果
*/
async executeElementAction(elementUid, action, value) {
const doc = this.resolveContextDoc();
const element = this.findElement(elementUid, doc);
if (!element) {
throw new Error(`找不到元素: ${elementUid}`);
}
// 元素所在 window主文档或微应用 iframe事件需用它的构造器才被框架信任
const win = element.ownerDocument.defaultView || window;
switch (action) {
case 'click':
element.click();
return { success: true, action: 'click', element: elementUid };
case 'type':
if (element.tagName === 'INPUT' || element.tagName === 'TEXTAREA' || element.contentEditable === 'true') {
element.focus();
if (element.contentEditable === 'true') {
element.textContent = value || '';
} else {
element.value = value || '';
}
element.dispatchEvent(new win.Event('input', { bubbles: true }));
element.dispatchEvent(new win.Event('change', { bubbles: true }));
return { success: true, action: 'type', value, element: elementUid };
}
throw new Error('元素不支持输入操作');
case 'select':
if (element.tagName === 'SELECT') {
element.value = value;
element.dispatchEvent(new win.Event('change', { bubbles: true }));
return { success: true, action: 'select', value, element: elementUid };
}
// iView Select 组件 - 先点击打开下拉
element.click();
await this.delay(200);
const options = element.ownerDocument.querySelectorAll('.ivu-select-dropdown-list .ivu-select-item');
for (const option of options) {
if (option.textContent.trim().includes(value)) {
option.click();
return { success: true, action: 'select', value, element: elementUid };
}
}
throw new Error(`找不到选项: ${value}`);
case 'focus':
element.focus();
return { success: true, action: 'focus', element: elementUid };
case 'scroll':
element.scrollIntoView({ behavior: 'smooth', block: 'center' });
return { success: true, action: 'scroll', element: elementUid };
case 'hover':
element.dispatchEvent(new win.MouseEvent('mouseenter', { bubbles: true }));
element.dispatchEvent(new win.MouseEvent('mouseover', { bubbles: true }));
return { success: true, action: 'hover', element: elementUid };
default:
throw new Error(`不支持的元素操作: ${action}`);
}
}
/**
* 查找元素
* 支持多种格式e1, @e1, ref=e1, CSS选择器
*/
findElement(identifier, doc = document) {
let ref = null;
if (identifier.startsWith('@')) {
ref = identifier.slice(1);
} else if (identifier.startsWith('ref=')) {
ref = identifier.slice(4);
} else if (/^e\d+$/.test(identifier)) {
ref = identifier;
}
// 如果是 ref 格式,使用 refMap 查找
if (ref && this.currentRefMap) {
const element = findElementByRef(ref, this.currentRefMap, doc);
if (element) return element;
}
// 尝试作为 CSS 选择器
try {
const element = doc.querySelector(identifier);
if (element) return element;
} catch (e) {
// 选择器无效,忽略
}
return null;
}
/**
* 延迟工具方法
*/
delay(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
}
export default createActionExecutor;