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

146 lines
4.7 KiB
JavaScript
Vendored
Raw Permalink 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.

/**
* AI 助手活动上下文解析
*
* 页面操作(采集 / 元素操作)默认作用于"用户当前最前面看到的文档"
* 当有微应用插件以 iframe 形式打开并在最前时,操作其 iframe 内部 DOM否则操作主文档。
*
* 仅支持同源 iframe应用商店插件反代到主站同源路径contentDocument 可达);
* 跨源或尚未就绪的应用返回 reachable=false由上层operation-module优雅降级。
*
* micro-app@micro-zoe with 沙箱)类型的子应用 DOM 本就渲染在主文档里,
* 现采集器走主文档即可扫到,这里按主界面处理、不特殊穿透。
*/
// iframe 元素选择器(见 MicroApps/iframe.vue
const APP_IFRAME_SELECTOR = 'iframe.micro-app-iframe-container';
/**
* 是否 iframe 类型微应用(与 MicroApps/index.vue::isIframe 对齐)
* @param {string} type
* @returns {boolean}
*/
function isIframeType(type) {
return typeof type === 'string' && /^iframe/i.test(type);
}
/**
* 取最前打开的微应用isOpen 且 lastOpenAt 最大)
* @param {Object} store - Vuex store
* @returns {Object|null}
*/
function frontmostApp(store) {
const apps = (store?.state?.microApps || []).filter(a => a && a.isOpen);
if (!apps.length) {
return null;
}
return apps.reduce((a, b) => ((b.lastOpenAt || 0) > (a.lastOpenAt || 0) ? b : a));
}
/**
* 定位某微应用对应的 iframe DOM 元素
* @param {Object} app - microApps 中的 app 对象(含注入运行时变量后的 url
* @returns {HTMLIFrameElement|null}
*/
function findAppIframe(app) {
if (!app || !app.url) {
return null;
}
const frames = [...document.querySelectorAll(APP_IFRAME_SELECTOR)];
if (!frames.length) {
return null;
}
// 1. 精确匹配 src === app.url
let hit = frames.find(f => f.src === app.url);
if (hit) {
return hit;
}
// 2. URL 规范化匹配(容忍尾斜杠/编码差异)
try {
const target = new URL(app.url, location.href).href;
hit = frames.find(f => {
try {
return new URL(f.src, location.href).href === target;
} catch (e) {
return false;
}
});
if (hit) {
return hit;
}
} catch (e) {
// ignore
}
// 3. 仅一个微应用 iframe 时直接用
return frames.length === 1 ? frames[0] : null;
}
/**
* 解析当前活动上下文
* @param {Object} store - Vuex store 实例
* @param {string} scope - 'auto'(默认,有同源微应用在最前就采它)| 'main'(强制主界面)| 'app'(强制最前微应用)
* @returns {Object} 上下文:
* { kind:'main', scope, doc:document, appName, label, reachable:true, frameKey:'main' }
* { kind:'app', scope:'app', doc:iframeDoc, appName, appTitle, label, reachable:true, frameKey }
* { kind:'app', scope:'app', appName, appTitle, reachable:false, reason:'cross_origin'|'not_ready'|'no_app', frameKey }
*/
export function resolveActiveContext(store, scope = 'auto') {
const mainCtx = {
kind: 'main',
scope: 'main',
doc: document,
appName: null,
label: '主界面',
reachable: true,
frameKey: 'main',
};
if (scope === 'main') {
return mainCtx;
}
const app = frontmostApp(store);
if (!app) {
// 显式要求 app 但没有打开任何微应用
if (scope === 'app') {
return {
kind: 'app', scope: 'app', appName: null, appTitle: '',
reachable: false, reason: 'no_app', frameKey: null,
};
}
// auto 下无微应用 → 主界面
return mainCtx;
}
// 非 iframe 类型micro-app with 沙箱DOM 在主文档,按主界面处理
if (!isIframeType(app.type)) {
return { ...mainCtx, appName: app.name, appTitle: app.title || app.name };
}
const base = {
kind: 'app',
scope: 'app',
appName: app.name,
appTitle: app.title || app.name,
frameKey: `app:${app.name}@${app.lastOpenAt || 0}`,
};
const iframe = findAppIframe(app);
if (!iframe) {
return { ...base, reachable: false, reason: 'not_ready' };
}
try {
const doc = iframe.contentDocument;
if (doc && doc.body && doc.body.children.length > 0) {
return { ...base, reachable: true, doc, label: `微应用「${base.appTitle}` };
}
return { ...base, reachable: false, reason: 'not_ready' };
} catch (e) {
// 跨源 iframecontentDocument 抛异常
return { ...base, reachable: false, reason: 'cross_origin' };
}
}
export default resolveActiveContext;