// ==UserScript== // @name 智填助手 Pro // @namespace http://tampermonkey.net/ // @version 2.0.0 // @description AI 驱动的智能表单填写工具 - 支持圈选区域 + 样式隔离 // @author laosuye // @match *://*/* // @grant GM_setValue // @grant GM_getValue // @grant GM_xmlhttpRequest // @grant GM_addStyle // @grant GM_registerMenuCommand // @connect api.openai.com // @connect api.anthropic.com // @connect dashscope.aliyuncs.com // @connect open.bigmodel.cn // @connect api.deepseek.com // @connect * // @license free // @downloadURL https://update.greasyfork.icu/scripts/564419/%E6%99%BA%E5%A1%AB%E5%8A%A9%E6%89%8B%20Pro.user.js // @updateURL https://update.greasyfork.icu/scripts/564419/%E6%99%BA%E5%A1%AB%E5%8A%A9%E6%89%8B%20Pro.meta.js // ==/UserScript== (function () { 'use strict'; // ========== 常量定义 ========== const STORAGE_KEYS = { CONFIG: 'aff_config', POSITION: 'aff_position' }; const DEFAULT_PROVIDERS = { openai: { name: 'OpenAI', baseUrl: 'https://api.openai.com/v1', model: 'gpt-3.5-turbo', compatible: 'openai' }, claude: { name: 'Claude', baseUrl: 'https://api.anthropic.com', model: 'claude-3-haiku-20240307', compatible: 'claude' }, qwen: { name: '通义千问', baseUrl: 'https://dashscope.aliyuncs.com/compatible-mode/v1', model: 'qwen-turbo', compatible: 'openai' }, zhipu: { name: '智谱AI', baseUrl: 'https://open.bigmodel.cn/api/paas/v4', model: 'glm-4-flash', compatible: 'openai' }, deepseek: { name: 'DeepSeek', baseUrl: 'https://api.deepseek.com/v1', model: 'deepseek-chat', compatible: 'openai' } }; const LOCALES = { 'zh-CN': '中文(中国)', 'zh-TW': '中文(台湾)', 'en-US': 'English (US)', 'en-GB': 'English (UK)', 'ja-JP': '日本語', 'ko-KR': '한국어' }; const PROMPT_TEMPLATE = `你是一个测试数据生成助手。请根据以下表单字段信息,生成合理的测试数据。 要求: 1. 语言/地区:{locale} 2. 数据要符合字段的验证规则(如有) 3. 数据要看起来真实合理 4. 仅返回 JSON 格式,不要有其他文字:{"0": "值1", "1": "值2", ...} 5. 对于 select 类型,返回选项的值(value)或文本 6. 对于 radio/checkbox 类型,返回要选中的值 7. 密码字段生成包含大小写字母、数字的 8-12 位密码 表单字段: {fields_json}`; // ========== 配置管理模块 ========== const ConfigManager = { _encrypt: str => str ? btoa(encodeURIComponent(str.split('').reverse().join(''))) : '', _decrypt(str) { if (!str) return ''; try { return decodeURIComponent(atob(str)).split('').reverse().join(''); } catch { return ''; } }, getConfig() { const config = GM_getValue(STORAGE_KEYS.CONFIG, null); if (config) config.apiKey = this._decrypt(config.apiKey); return config; }, setConfig(config) { GM_setValue(STORAGE_KEYS.CONFIG, { ...config, apiKey: this._encrypt(config.apiKey) }); }, getPosition: () => GM_getValue(STORAGE_KEYS.POSITION, { x: null, y: null }), setPosition: pos => GM_setValue(STORAGE_KEYS.POSITION, pos), getProviderDefaults: provider => DEFAULT_PROVIDERS[provider] || DEFAULT_PROVIDERS.openai, isConfigValid() { const config = this.getConfig(); return config && config.apiKey && config.baseUrl; } }; // ========== AI 服务适配层 ========== const AIService = { async generateFormData(fields, locale) { const config = ConfigManager.getConfig(); if (!config?.apiKey) throw new Error('请先配置 AI 服务'); const fieldsInfo = fields.map(f => ({ index: f.index, type: f.type, name: f.name, label: f.label, placeholder: f.placeholder, required: f.required, pattern: f.pattern, options: f.options, minLength: f.minLength, maxLength: f.maxLength })); const prompt = PROMPT_TEMPLATE .replace('{locale}', LOCALES[locale] || locale) .replace('{fields_json}', JSON.stringify(fieldsInfo, null, 2)); return this._callAPI(config, prompt); }, _callAPI(config, prompt, isTest = false) { return new Promise((resolve, reject) => { const providerDefaults = ConfigManager.getProviderDefaults(config.provider); const isClaude = providerDefaults.compatible === 'claude'; const url = isClaude ? `${config.baseUrl.replace(/\/$/, '')}/v1/messages` : `${config.baseUrl.replace(/\/$/, '')}/chat/completions`; const headers = isClaude ? { 'Content-Type': 'application/json', 'x-api-key': config.apiKey, 'anthropic-version': '2023-06-01' } : { 'Content-Type': 'application/json', 'Authorization': `Bearer ${config.apiKey}` }; const body = isClaude ? { model: config.model, max_tokens: 1024, messages: [{ role: 'user', content: prompt }] } : { model: config.model, messages: [{ role: 'user', content: prompt }], temperature: 0.7 }; GM_xmlhttpRequest({ method: 'POST', url, headers, data: JSON.stringify(body), timeout: 30000, onload: response => { try { if (response.status >= 200 && response.status < 300) { const data = JSON.parse(response.responseText); const content = isClaude ? data.content[0].text : data.choices[0].message.content; if (isTest) return resolve({ test: true, content }); const jsonMatch = content.match(/\{[\s\S]*\}/); jsonMatch ? resolve(JSON.parse(jsonMatch[0])) : reject(new Error('AI 返回格式错误')); } else { let errMsg = `请求失败: ${response.status}`; try { const errData = JSON.parse(response.responseText); errMsg = errData.error?.message || errData.message || errMsg; } catch { errMsg = response.responseText || errMsg; } reject(new Error(errMsg)); } } catch { reject(new Error('解析响应失败')); } }, onerror: () => reject(new Error('网络请求失败')), ontimeout: () => reject(new Error('请求超时')) }); }); }, async testConnection(config) { try { await this._callAPI(config, '请回复 ok', true); return { success: true, message: '连接成功' }; } catch (e) { return { success: false, message: e.message }; } } }; // ========== 🆕 区域选择器(智能悬停模式)========== const AreaSelector = { overlay: null, selectionBox: null, confirmButtons: null, topButtons: null, currentElement: null, selectedElement: null, hoveredElement: null, // 查找包含表单的父元素 findFormContainer(element) { let current = element; while (current && current !== document.body) { // 检查当前元素或其子元素是否包含表单字段 const hasFormFields = current.querySelectorAll('input, textarea, select').length > 0; if (hasFormFields) { // 优先选择 form 标签或常见的表单容器 if (current.tagName === 'FORM' || current.classList.contains('form') || current.classList.contains('ant-form') || current.classList.contains('el-form') || current.closest('form, [class*="form"], [class*="modal"], [class*="dialog"]') === current) { return current; } } current = current.parentElement; } // 如果没找到合适的容器,返回最近的包含表单字段的元素 current = element; while (current && current !== document.body) { if (current.querySelectorAll('input, textarea, select').length > 0) { return current; } current = current.parentElement; } return element; }, createOverlay() { this.overlay = document.createElement('div'); this.overlay.style.cssText = ` position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0, 0, 0, 0.3); z-index: 9999998; cursor: crosshair; `; this.selectionBox = document.createElement('div'); this.selectionBox.style.cssText = ` position: fixed; border: 3px solid #6366f1; background: rgba(99, 102, 241, 0.15); pointer-events: none; z-index: 9999999; display: none; transition: all 0.15s ease; box-shadow: 0 0 0 2px rgba(99, 102, 241, 0.3); `; // 创建确认按钮容器(右下角) this.confirmButtons = document.createElement('div'); this.confirmButtons.style.cssText = ` position: fixed; display: none; gap: 8px; z-index: 10000000; pointer-events: auto; `; this.confirmButtons.innerHTML = ` `; // 创建顶部按钮容器(右上角) this.topButtons = document.createElement('div'); this.topButtons.style.cssText = ` position: fixed; display: none; gap: 8px; z-index: 10000000; pointer-events: auto; `; this.topButtons.innerHTML = ` `; const hint = document.createElement('div'); hint.style.cssText = ` position: fixed; top: 50%; left: 50%; transform: translate(-50%, -50%); background: white; padding: 20px 30px; border-radius: 12px; box-shadow: 0 10px 40px rgba(0,0,0,0.2); font-size: 16px; color: #475569; z-index: 10000000; pointer-events: none; `; hint.textContent = '🖱️ 移动鼠标到表单区域,点击选择(按 ESC 取消)'; document.body.appendChild(this.overlay); document.body.appendChild(this.selectionBox); document.body.appendChild(this.confirmButtons); document.body.appendChild(this.topButtons); this.overlay.appendChild(hint); this.bindEvents(); }, updateSelectionBox(element) { if (!element) return; const rect = element.getBoundingClientRect(); this.selectionBox.style.display = 'block'; this.selectionBox.style.left = rect.left + 'px'; this.selectionBox.style.top = rect.top + 'px'; this.selectionBox.style.width = rect.width + 'px'; this.selectionBox.style.height = rect.height + 'px'; }, showConfirmButtons(element) { const rect = element.getBoundingClientRect(); // 右下角按钮 this.confirmButtons.style.display = 'flex'; this.confirmButtons.style.left = (rect.right - 90) + 'px'; this.confirmButtons.style.top = (rect.bottom + 10) + 'px'; // 右上角按钮 this.topButtons.style.display = 'flex'; this.topButtons.style.left = (rect.right - 90) + 'px'; this.topButtons.style.top = (rect.top - 46) + 'px'; }, hideConfirmButtons() { this.confirmButtons.style.display = 'none'; this.topButtons.style.display = 'none'; }, bindEvents() { // 鼠标移动时高亮元素 const mouseMoveHandler = e => { if (this.selectedElement) return; // 已选中时不再响应悬停 // 获取鼠标下的元素(忽略遮罩层) this.overlay.style.pointerEvents = 'none'; const element = document.elementFromPoint(e.clientX, e.clientY); this.overlay.style.pointerEvents = 'auto'; if (!element || element === this.hoveredElement) return; this.hoveredElement = element; const container = this.findFormContainer(element); this.currentElement = container; this.updateSelectionBox(container); }; this.overlay.addEventListener('mousemove', mouseMoveHandler); // 点击选中元素 this.overlay.addEventListener('click', e => { if (this.selectedElement) return; // 已选中时不响应点击 if (this.currentElement) { this.selectedElement = this.currentElement; this.selectionBox.style.border = '3px solid #10b981'; this.selectionBox.style.background = 'rgba(16, 185, 129, 0.15)'; this.showConfirmButtons(this.selectedElement); // 移除提示文字 const hint = this.overlay.querySelector('div'); if (hint) hint.remove(); } }); // 确认和取消按钮事件处理 const handleConfirm = () => { if (this.selectedElement) { const rect = this.selectedElement.getBoundingClientRect(); this.onAreaSelected?.(rect, this.selectedElement); this.destroy(); } }; const handleCancel = () => { this.selectedElement = null; this.hideConfirmButtons(); this.selectionBox.style.border = '3px solid #6366f1'; this.selectionBox.style.background = 'rgba(99, 102, 241, 0.15)'; }; // 绑定底部按钮 const bottomConfirmBtn = this.confirmButtons.querySelector('.aff-confirm-select'); const bottomCancelBtn = this.confirmButtons.querySelector('.aff-cancel-select'); bottomConfirmBtn.addEventListener('mouseenter', () => { bottomConfirmBtn.style.transform = 'scale(1.1)'; }); bottomConfirmBtn.addEventListener('mouseleave', () => { bottomConfirmBtn.style.transform = 'scale(1)'; }); bottomConfirmBtn.addEventListener('click', handleConfirm); bottomCancelBtn.addEventListener('mouseenter', () => { bottomCancelBtn.style.transform = 'scale(1.1)'; }); bottomCancelBtn.addEventListener('mouseleave', () => { bottomCancelBtn.style.transform = 'scale(1)'; }); bottomCancelBtn.addEventListener('click', handleCancel); // 绑定顶部按钮 const topConfirmBtn = this.topButtons.querySelector('.aff-confirm-select'); const topCancelBtn = this.topButtons.querySelector('.aff-cancel-select'); topConfirmBtn.addEventListener('mouseenter', () => { topConfirmBtn.style.transform = 'scale(1.1)'; }); topConfirmBtn.addEventListener('mouseleave', () => { topConfirmBtn.style.transform = 'scale(1)'; }); topConfirmBtn.addEventListener('click', handleConfirm); topCancelBtn.addEventListener('mouseenter', () => { topCancelBtn.style.transform = 'scale(1.1)'; }); topCancelBtn.addEventListener('mouseleave', () => { topCancelBtn.style.transform = 'scale(1)'; }); topCancelBtn.addEventListener('click', handleCancel); // ESC 取消选择 const escHandler = e => { if (e.key === 'Escape') { this.destroy(); document.removeEventListener('keydown', escHandler); } }; document.addEventListener('keydown', escHandler); }, destroy() { this.overlay?.remove(); this.selectionBox?.remove(); this.confirmButtons?.remove(); this.topButtons?.remove(); this.currentElement = null; this.selectedElement = null; this.hoveredElement = null; }, async selectArea() { return new Promise(resolve => { this.onAreaSelected = (rect, element) => resolve({ rect, element }); this.createOverlay(); }); } }; // ========== 表单识别器(增强版)========== const FormDetector = { targetArea: null, // 🆕 目标区域 _getLabel(element) { // 1. 通过 id 查找 label[for] if (element.id) { const label = document.querySelector(`label[for="${element.id}"]`); if (label) { const text = label.textContent.trim(); if (text) return text; } } // 2. 查找父级 label 标签 const parentLabel = element.closest('label'); if (parentLabel) { const clone = parentLabel.cloneNode(true); clone.querySelectorAll('input, select, textarea, button').forEach(i => i.remove()); const text = clone.textContent.trim(); if (text) return text; } // 3. 查找表单项容器(支持多种UI框架) const formItem = element.closest( '.el-form-item, .ant-form-item, .form-item, .form-group, ' + '.field, .input-group, [class*="form-item"], [class*="field"]' ); if (formItem) { // 查找标签元素 const labelEl = formItem.querySelector( '.el-form-item__label, .ant-form-item-label label, .ant-form-item-label, ' + '.form-label, .field-label, label, [class*="label"]' ); if (labelEl) { const clone = labelEl.cloneNode(true); clone.querySelectorAll('input, select, textarea, button, .required, .optional').forEach(i => i.remove()); const text = clone.textContent.trim().replace(/[*::]+$/, '').trim(); if (text) return text; } } // 4. 查找前置兄弟元素中的文本 const parent = element.parentElement; if (parent) { let sibling = element.previousElementSibling; let attempts = 0; while (sibling && attempts < 3) { attempts++; if (sibling.tagName === 'LABEL' || sibling.classList.contains('label') || sibling.className.includes('label')) { const clone = sibling.cloneNode(true); clone.querySelectorAll('input, select, textarea, button').forEach(i => i.remove()); const text = clone.textContent.trim().replace(/[*::]+$/, '').trim(); if (text && text.length < 100) return text; } // 检查纯文本节点 const text = sibling.textContent.trim().replace(/[*::]+$/, '').trim(); if (text && text.length > 0 && text.length < 50 && !sibling.querySelector('input, select, textarea')) { return text; } sibling = sibling.previousElementSibling; } } // 5. 查找父元素中的标签类元素 if (parent) { const labelLike = parent.querySelector( 'label, .label, .form-label, .field-label, ' + '[class*="label"]:not([class*="input"]):not([class*="control"])' ); if (labelLike && !labelLike.contains(element)) { const clone = labelLike.cloneNode(true); clone.querySelectorAll('input, select, textarea, button').forEach(i => i.remove()); const text = clone.textContent.trim().replace(/[*::]+$/, '').trim(); if (text && text.length < 100) return text; } } // 6. 使用 placeholder 或 title 作为后备 if (element.placeholder && element.placeholder.length < 50) { return element.placeholder; } if (element.title && element.title.length < 50) { return element.title; } // 7. 使用 name 或 id 作为最后的后备 if (element.name && element.name.length < 50) { return element.name.replace(/[-_]/g, ' ').replace(/([A-Z])/g, ' $1').trim(); } if (element.id && element.id.length < 50) { return element.id.replace(/[-_]/g, ' ').replace(/([A-Z])/g, ' $1').trim(); } return ''; }, _getSelectOptions: el => [...el.options].filter(o => o.value).map(o => ({ value: o.value, text: o.textContent.trim() })), _getRadioCheckboxOptions(element) { if (!element.name) return []; const selector = `input[name="${element.name}"]`; // targetArea is a DOMRect, not a DOM element, so we need to query from document // and filter by position let elements = [...document.querySelectorAll(selector)]; // If targetArea is set, filter elements by position if (this.targetArea) { elements = elements.filter(el => this._isInTargetArea(el)); } return elements.map(el => ({ value: el.value, text: this._getLabel(el) || el.value })); }, _getCurrentValue(element) { try { if (element.tagName === 'SELECT') { return element.options[element.selectedIndex]?.value || ''; } if (element.type === 'radio') { const checked = document.querySelector(`input[type="radio"][name="${element.name}"]:checked`); // If targetArea is set, verify the checked element is in the area if (checked && this.targetArea && !this._isInTargetArea(checked)) { return ''; } return checked?.value || ''; } if (element.type === 'checkbox') { return element.checked ? (element.value || 'true') : ''; } return element.value?.trim() || ''; } catch { return ''; } }, // 🆕 检查元素是否在目标区域内 _isInTargetArea(element) { if (!this.targetArea) return true; const rect = element.getBoundingClientRect(); const area = this.targetArea; return rect.left >= area.left && rect.right <= area.right && rect.top >= area.top && rect.bottom <= area.bottom; }, // 🆕 查找所有表单容器 _findFormContainers(targetArea) { if (!targetArea) return [document.body]; const containers = []; const centerElement = document.elementFromPoint( targetArea.left + targetArea.width / 2, targetArea.top + targetArea.height / 2 ); if (!centerElement) return [document.body]; // 查找所有可能的表单容器 const formSelectors = [ 'form', '[class*="form"]', '[class*="modal"]', '[class*="dialog"]', '[class*="drawer"]', '[class*="panel"]' ]; // 从中心元素向上查找所有表单容器 let current = centerElement; const foundContainers = new Set(); while (current && current !== document.body) { for (const selector of formSelectors) { if (current.matches(selector)) { const rect = current.getBoundingClientRect(); // 检查容器是否与目标区域有重叠 if (this._hasOverlap(rect, targetArea)) { foundContainers.add(current); } } } current = current.parentElement; } // 如果找到了容器,返回它们;否则返回目标区域内的所有元素 if (foundContainers.size > 0) { return Array.from(foundContainers); } // 如果没有找到明确的表单容器,返回中心元素的最近父容器 return [centerElement.closest('div, section, main, article') || document.body]; }, // 🆕 检查两个矩形是否有重叠 _hasOverlap(rect1, rect2) { return !(rect1.right < rect2.left || rect1.left > rect2.right || rect1.bottom < rect2.top || rect1.top > rect2.bottom); }, scanFormFields(targetArea = null) { this.targetArea = targetArea; const fields = []; const processedElements = new Set(); // 改用元素集合去重 const processedNames = new Set(); // 🆕 查找所有相关的表单容器 const containers = this._findFormContainers(targetArea); // 遍历所有容器,收集表单字段 containers.forEach(container => { container.querySelectorAll('input, textarea, select').forEach((element, idx) => { // 避免重复处理同一个元素 if (processedElements.has(element)) return; // 🆕 区域过滤 if (targetArea && !this._isInTargetArea(element)) return; // 跳过不可见、禁用、只读元素 if (element.type === 'hidden') return; const style = window.getComputedStyle(element); if (style.display === 'none' || style.visibility === 'hidden') return; if (['submit', 'button', 'reset', 'image'].includes(element.type)) return; if (element.readOnly || element.disabled) return; // 跳过复杂组件 if (element.closest('.ant-picker, .el-date-editor, .el-time-picker, .th-selectInput, .th-selectInputContainer')) return; // 跳过搜索区域(弹窗内的除外) if (element.closest('.search-form, .filter-form, .query-form, [class*="search"], [class*="filter"], [class*="query"]')) { if (!element.closest('.ant-modal, .ant-drawer, .el-dialog, [class*="modal"], [class*="dialog"]')) return; } const type = element.type || element.tagName.toLowerCase(); const name = element.name || element.id || `field_${fields.length}`; // radio/checkbox 按 name 去重 if ((type === 'radio' || type === 'checkbox') && element.name) { if (processedNames.has(element.name)) return; processedNames.add(element.name); } // 标记元素已处理 processedElements.add(element); // 获取 label - 优先使用表单项容器中的标签 let label = ''; const formItem = element.closest( '.el-form-item, .ant-form-item, .form-item, .form-group, ' + '.field, .input-group, [class*="form-item"], [class*="field"]' ); if (formItem) { const labelEl = formItem.querySelector( '.el-form-item__label, .ant-form-item-label label, .ant-form-item-label, ' + '.form-label, .field-label, label, [class*="label"]' ); if (labelEl) { const clone = labelEl.cloneNode(true); clone.querySelectorAll('input, select, textarea, button, .required, .optional').forEach(i => i.remove()); label = clone.textContent.trim().replace(/[*::]+$/, '').trim(); } } if (!label) label = this._getLabel(element); const field = { index: fields.length, element, type: type === 'select' || element.tagName.toLowerCase() === 'select' ? 'select' : type, name, label, placeholder: element.placeholder || '', required: element.required, pattern: element.pattern || null, minLength: element.minLength > 0 ? element.minLength : null, maxLength: element.maxLength > 0 ? element.maxLength : null, options: null, currentValue: this._getCurrentValue(element), container: container // 记录所属容器,便于调试 }; // 获取选项 if (field.type === 'select') { field.options = this._getSelectOptions(element); } else if (type === 'radio' || type === 'checkbox') { field.options = this._getRadioCheckboxOptions(element); } fields.push(field); }); }); console.log(`[FormDetector] 扫描完成: 找到 ${containers.length} 个容器, ${fields.length} 个字段`); return fields; } }; // ========== 数据填充引擎 ========== const FormFiller = { _triggerEvents(el) { ['input', 'change', 'blur'].forEach(e => el.dispatchEvent(new Event(e, { bubbles: true }))); }, _simulateInput(element, value) { element.focus(); const setter = Object.getOwnPropertyDescriptor( element.tagName === 'TEXTAREA' ? HTMLTextAreaElement.prototype : HTMLInputElement.prototype, 'value' )?.set; setter ? setter.call(element, value) : (element.value = value); this._triggerEvents(element); }, _fillSelect(element, value) { for (let i = 0; i < element.options.length; i++) { if (element.options[i].value === value || element.options[i].text === value) { element.selectedIndex = i; this._triggerEvents(element); return true; } } for (let i = 0; i < element.options.length; i++) { if (element.options[i].text.includes(value) || value.includes(element.options[i].text)) { element.selectedIndex = i; this._triggerEvents(element); return true; } } if (element.options.length > 1) { element.selectedIndex = 1; this._triggerEvents(element); return true; } return false; }, _fillRadio(element, value) { const radios = document.querySelectorAll(`input[type="radio"][name="${element.name}"]`); for (const radio of radios) { const label = FormDetector._getLabel(radio) || radio.value; if (radio.value === value || label === value || label.includes(value) || value.includes(label)) { radio.checked = true; radio.dispatchEvent(new Event('change', { bubbles: true })); return true; } } if (radios.length > 0) { radios[0].checked = true; radios[0].dispatchEvent(new Event('change', { bubbles: true })); return true; } return false; }, _fillCheckbox(element, value) { const shouldCheck = value === true || value === 'true' || value === '1' || value === element.value; if (element.checked !== shouldCheck) { element.checked = shouldCheck; element.dispatchEvent(new Event('change', { bubbles: true })); } return true; }, async fillField(field, value) { if (value === undefined || value === null) return false; if (field.currentValue?.trim()) return true; try { switch (field.type) { case 'select': return this._fillSelect(field.element, value); case 'radio': return this._fillRadio(field.element, value); case 'checkbox': return this._fillCheckbox(field.element, value); default: this._simulateInput(field.element, String(value)); return true; } } catch { return false; } }, async fillForm(fields, data, onProgress) { let filled = 0, failed = 0; for (const field of fields) { const value = data[field.index] ?? data[String(field.index)]; (await this.fillField(field, value)) ? filled++ : failed++; onProgress?.(filled + failed, fields.length); await new Promise(r => setTimeout(r, 50)); } return { filled, failed, total: fields.length }; } }; // ========== 🆕 Shadow DOM 样式隔离的浮动面板 ========== const UIPanel = { shadowHost: null, shadowRoot: null, panel: null, settingsModal: null, dragState: null, ICONS: { magic: ``, loading: ``, check: ``, error: ``, select: ``, collapse: ``, expand: `` }, // 🆕 在 Shadow DOM 内部注入样式 getStyles() { return ` * { box-sizing: border-box; } #aff-panel { position: fixed; z-index: 999999; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; user-select: none; transition: all 0.3s ease; } #aff-panel.collapsed { transform: translateX(calc(100% - 50px)); } .aff-btn-group { display: flex; gap: 8px; background: white; padding: 8px; border-radius: 50px; box-shadow: 0 4px 20px rgba(0,0,0,0.15); transition: all 0.3s ease; } #aff-panel.collapsed .aff-btn-group { border-radius: 50px 0 0 50px; } .aff-icon-btn { width: 44px; height: 44px; border: none; border-radius: 50%; background: linear-gradient(135deg, #6366f1 0%, #8b5cf6 50%, #a855f7 100%); color: white; cursor: pointer; transition: all 0.3s ease; display: flex; align-items: center; justify-content: center; position: relative; } .aff-icon-btn:hover { transform: translateY(-2px) scale(1.05); box-shadow: 0 6px 25px rgba(99, 102, 241, 0.5); } .aff-icon-btn:active { transform: translateY(0) scale(0.95); } .aff-icon-btn:disabled { opacity: 0.6; cursor: not-allowed; transform: none; } .aff-icon-btn .icon { display: flex; align-items: center; justify-content: center; } .aff-collapse-btn { width: 32px; height: 32px; background: linear-gradient(135deg, #94a3b8, #64748b); } .aff-collapse-btn:hover { transform: scale(1.05); box-shadow: 0 4px 15px rgba(100, 116, 139, 0.4); } .aff-tooltip { position: absolute; bottom: -35px; left: 50%; transform: translateX(-50%); background: rgba(0,0,0,0.8); color: white; padding: 6px 12px; border-radius: 6px; font-size: 12px; white-space: nowrap; opacity: 0; pointer-events: none; transition: opacity 0.2s; } .aff-icon-btn:hover .aff-tooltip { opacity: 1; } .aff-modal-overlay { position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0,0,0,0.5); backdrop-filter: blur(4px); z-index: 9999999; display: flex; align-items: center; justify-content: center; } .aff-modal { background: white; border-radius: 16px; box-shadow: 0 20px 60px rgba(0,0,0,0.3); max-width: 420px; width: 90%; max-height: 80vh; overflow: auto; } .aff-modal-header { display: flex; align-items: center; justify-content: space-between; padding: 18px 24px; border-bottom: 1px solid #f0f0f0; background: linear-gradient(135deg, #f8fafc, #f1f5f9); border-radius: 16px 16px 0 0; } .aff-modal-header h3 { margin: 0; font-size: 17px; font-weight: 600; background: linear-gradient(135deg, #6366f1, #a855f7); -webkit-background-clip: text; -webkit-text-fill-color: transparent; background-clip: text; } .aff-modal-close { cursor: pointer; font-size: 22px; color: #94a3b8; transition: color 0.2s; line-height: 1; } .aff-modal-close:hover { color: #475569; } .aff-modal-body { padding: 24px; } .aff-form-group { margin-bottom: 18px; } .aff-form-group label { display: block; margin-bottom: 6px; font-size: 13px; font-weight: 500; color: #475569; } .aff-form-group input, .aff-form-group select { width: 100%; padding: 11px 14px; border: 1px solid #e2e8f0; border-radius: 10px; font-size: 14px; box-sizing: border-box; transition: all 0.2s; background: #f8fafc; } .aff-form-group input:focus, .aff-form-group select:focus { outline: none; border-color: #8b5cf6; box-shadow: 0 0 0 3px rgba(139,92,246,0.15); background: white; } .aff-form-actions { display: flex; gap: 12px; margin-top: 24px; } .aff-btn { padding: 11px 22px; border: none; border-radius: 10px; font-size: 14px; font-weight: 500; cursor: pointer; transition: all 0.2s; } .aff-btn-primary { background: linear-gradient(135deg, #6366f1, #8b5cf6); color: white; } .aff-btn-primary:hover { box-shadow: 0 4px 12px rgba(99,102,241,0.4); transform: translateY(-1px); } .aff-btn-secondary { background: #f1f5f9; color: #475569; } .aff-btn-secondary:hover { background: #e2e8f0; } `; }, // 🆕 创建 Shadow DOM createShadowRoot() { this.shadowHost = document.createElement('div'); this.shadowHost.id = 'aff-shadow-host'; this.shadowHost.style.cssText = 'all: initial; position: fixed; z-index: 2147483647;'; document.body.appendChild(this.shadowHost); this.shadowRoot = this.shadowHost.attachShadow({ mode: 'open' }); // 注入样式 const style = document.createElement('style'); style.textContent = this.getStyles(); this.shadowRoot.appendChild(style); }, createPanel() { const position = ConfigManager.getPosition(); this.panel = document.createElement('div'); this.panel.id = 'aff-panel'; this.panel.innerHTML = `
`; this.shadowRoot.appendChild(this.panel); // 默认定位在右侧 if (position.x !== null && position.y !== null) { this.shadowHost.style.left = position.x + 'px'; this.shadowHost.style.top = position.y + 'px'; } else { this.shadowHost.style.right = '20px'; this.shadowHost.style.top = '50%'; this.shadowHost.style.transform = 'translateY(-50%)'; } this.bindEvents(); // 应用初始收起状态 if (this.isCollapsed) { const selectBtn = this.shadowRoot.getElementById('aff-select-btn'); const fillBtn = this.shadowRoot.getElementById('aff-fill-btn'); selectBtn.style.display = 'none'; fillBtn.style.display = 'none'; this.panel.style.width = '48px'; } }, bindEvents() { const selectBtn = this.shadowRoot.getElementById('aff-select-btn'); const fillBtn = this.shadowRoot.getElementById('aff-fill-btn'); const collapseBtn = this.shadowRoot.getElementById('aff-collapse-btn'); const DRAG_THRESHOLD = 5; // 拖拽逻辑 this.panel.addEventListener('mousedown', e => { if (e.target.closest('button')) return; // 点击按钮时不拖拽 const rect = this.shadowHost.getBoundingClientRect(); this.dragState = { startX: e.clientX, startY: e.clientY, offsetX: e.clientX - rect.left, offsetY: e.clientY - rect.top, moved: false }; this.shadowHost.style.right = 'auto'; }); document.addEventListener('mousemove', e => { if (!this.dragState) return; const dx = e.clientX - this.dragState.startX; const dy = e.clientY - this.dragState.startY; if (!this.dragState.moved && (Math.abs(dx) > DRAG_THRESHOLD || Math.abs(dy) > DRAG_THRESHOLD)) { this.dragState.moved = true; } if (this.dragState.moved) { this.shadowHost.style.left = (e.clientX - this.dragState.offsetX) + 'px'; this.shadowHost.style.top = (e.clientY - this.dragState.offsetY) + 'px'; } }); document.addEventListener('mouseup', () => { if (!this.dragState) return; if (this.dragState.moved) { const rect = this.shadowHost.getBoundingClientRect(); ConfigManager.setPosition({ x: rect.left, y: rect.top }); } this.dragState = null; }); // 🆕 圈选按钮 selectBtn.addEventListener('click', () => this.handleSelectArea()); // 填写按钮 fillBtn.addEventListener('click', () => this.handleFill()); // 收起/展开按钮 collapseBtn.addEventListener('click', () => this.toggleCollapse()); }, // 🆕 圈选区域处理 async handleSelectArea() { try { this.updateButtonStatus('select', '圈选中...', 'loading'); const result = await AreaSelector.selectArea(); if (result && result.rect) { this.updateButtonStatus('select', '✓', 'success'); setTimeout(() => this.resetButton('select'), 1000); // 显示字段选择弹窗 setTimeout(() => this.showFieldSelector(result.rect, result.element), 1200); } } catch (e) { this.updateButtonStatus('select', '✗', 'error'); setTimeout(() => this.resetButton('select'), 2000); } }, // 🆕 显示字段选择弹窗 showFieldSelector(targetArea, targetElement) { // 扫描表单字段 const fields = FormDetector.scanFormFields(targetArea); if (!fields.length) { alert('未在选中区域找到表单字段'); return; } // 创建弹窗 const overlay = document.createElement('div'); overlay.style.cssText = ` position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0,0,0,0.5); backdrop-filter: blur(4px); z-index: 2147483646; display: flex; align-items: center; justify-content: center; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; `; const fieldsList = fields.map((field, idx) => { const displayLabel = field.label || field.placeholder || field.name || `字段 ${idx + 1}`; const typeLabel = { 'text': '文本', 'email': '邮箱', 'tel': '电话', 'number': '数字', 'password': '密码', 'textarea': '多行文本', 'select': '下拉选择', 'radio': '单选', 'checkbox': '复选' }[field.type] || field.type; return ` `; }).join(''); overlay.innerHTML = `

📝 选择要填充的字段 (${fields.length})

已选: ${fields.length} / ${fields.length}
${fieldsList}
`; document.body.appendChild(overlay); // 更新选中计数 const updateCount = () => { const checked = overlay.querySelectorAll('input[type="checkbox"]:checked').length; overlay.querySelector('#aff-selected-count').textContent = checked; }; // 绑定事件 overlay.querySelectorAll('input[type="checkbox"]').forEach(cb => { cb.addEventListener('change', updateCount); }); overlay.querySelector('#aff-select-all').addEventListener('click', () => { overlay.querySelectorAll('input[type="checkbox"]').forEach(cb => cb.checked = true); updateCount(); }); overlay.querySelector('#aff-deselect-all').addEventListener('click', () => { overlay.querySelectorAll('input[type="checkbox"]').forEach(cb => cb.checked = false); updateCount(); }); // 确认填充的处理函数 const handleConfirm = async () => { const selectedIndexes = Array.from(overlay.querySelectorAll('input[type="checkbox"]:checked')) .map(cb => parseInt(cb.dataset.fieldIndex)); if (selectedIndexes.length === 0) { alert('请至少选择一个字段'); return; } const selectedFields = fields.filter(f => selectedIndexes.includes(f.index)); overlay.remove(); // 调用填充 await this.handleFillWithFields(selectedFields, targetArea); }; // 取消的处理函数 const handleCancel = () => overlay.remove(); // 绑定顶部按钮事件 overlay.querySelector('#aff-top-confirm-fill').addEventListener('click', handleConfirm); overlay.querySelector('#aff-top-cancel-fill').addEventListener('click', handleCancel); // 绑定底部按钮事件 overlay.querySelector('#aff-cancel-fill').addEventListener('click', handleCancel); overlay.querySelector('#aff-confirm-fill').addEventListener('click', handleConfirm); // 点击背景关闭 overlay.addEventListener('click', e => { if (e.target === overlay) overlay.remove(); }); }, // 🆕 使用选中的字段进行填充 async handleFillWithFields(selectedFields, targetArea) { if (!ConfigManager.isConfigValid()) return this.showSettings(); this.updateButtonStatus('fill', '生成中...', 'loading'); try { this.updateButtonStatus('fill', `生成${selectedFields.length}`, 'loading'); const config = ConfigManager.getConfig(); const data = await AIService.generateFormData(selectedFields, config.locale || 'zh-CN'); this.updateButtonStatus('fill', '填写中', 'loading'); const result = await FormFiller.fillForm(selectedFields, data, (cur, total) => { this.updateButtonStatus('fill', `${cur}/${total}`, 'loading'); }); this.updateButtonStatus( 'fill', result.failed === 0 ? '✓' : `${result.filled}/${result.total}`, result.failed === 0 ? 'success' : 'error' ); setTimeout(() => this.resetButton('fill'), 2000); } catch (e) { this.updateButtonStatus('fill', '✗', 'error'); setTimeout(() => this.resetButton('fill'), 3000); console.error('[智填助手]', e); } }, updateButtonStatus(btnId, text, type = '') { const btn = this.shadowRoot.getElementById(`aff-${btnId}-btn`); const icons = { loading: this.ICONS.loading, success: this.ICONS.check, error: this.ICONS.error }; const icon = icons[type] || (btnId === 'select' ? this.ICONS.select : this.ICONS.magic); btn.innerHTML = `${icon}`; btn.disabled = type === 'loading'; }, resetButton(btnId) { const btn = this.shadowRoot.getElementById(`aff-${btnId}-btn`); const icon = btnId === 'select' ? this.ICONS.select : this.ICONS.magic; const tooltip = btnId === 'select' ? '圈选区域' : '智能填写'; btn.innerHTML = `${icon}${tooltip}`; btn.disabled = false; }, toggleCollapse() { const btnGroup = this.shadowRoot.querySelector('.aff-btn-group'); const collapseBtn = this.shadowRoot.getElementById('aff-collapse-btn'); const selectBtn = this.shadowRoot.getElementById('aff-select-btn'); const fillBtn = this.shadowRoot.getElementById('aff-fill-btn'); this.isCollapsed = !this.isCollapsed; if (this.isCollapsed) { // 收起状态 selectBtn.style.display = 'none'; fillBtn.style.display = 'none'; collapseBtn.innerHTML = `${this.ICONS.expand}展开`; collapseBtn.title = '展开'; this.panel.style.width = '48px'; } else { // 展开状态 selectBtn.style.display = 'flex'; fillBtn.style.display = 'flex'; collapseBtn.innerHTML = `${this.ICONS.collapse}收起`; collapseBtn.title = '收起'; this.panel.style.width = 'auto'; } }, async handleFill(targetArea = null) { if (!ConfigManager.isConfigValid()) return this.showSettings(); this.updateButtonStatus('fill', '识别...', 'loading'); try { const fields = FormDetector.scanFormFields(targetArea); if (!fields.length) { this.updateButtonStatus('fill', '无表单', 'error'); setTimeout(() => this.resetButton('fill'), 2000); return; } this.updateButtonStatus('fill', `生成${fields.length}`, 'loading'); const config = ConfigManager.getConfig(); const data = await AIService.generateFormData(fields, config.locale || 'zh-CN'); this.updateButtonStatus('fill', '填写中', 'loading'); const result = await FormFiller.fillForm(fields, data, (cur, total) => { this.updateButtonStatus('fill', `${cur}/${total}`, 'loading'); }); this.updateButtonStatus( 'fill', result.failed === 0 ? '✓' : `${result.filled}/${result.total}`, result.failed === 0 ? 'success' : 'error' ); setTimeout(() => this.resetButton('fill'), 2000); } catch (e) { this.updateButtonStatus('fill', '✗', 'error'); setTimeout(() => this.resetButton('fill'), 3000); console.error('[智填助手]', e); } }, showSettings() { // 设置弹窗需要在主文档中创建(因为需要覆盖整个页面) this.settingsModal?.remove(); const config = ConfigManager.getConfig() || {}; const currentProvider = config.provider || 'openai'; const defaults = ConfigManager.getProviderDefaults(currentProvider); const overlay = document.createElement('div'); overlay.className = 'aff-modal-overlay'; overlay.style.cssText = ` position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0,0,0,0.5); backdrop-filter: blur(4px); z-index: 2147483646; display: flex; align-items: center; justify-content: center; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; `; overlay.innerHTML = `

✨ 智填助手设置

×
`; document.body.appendChild(overlay); this.settingsModal = overlay; // 事件绑定 const $ = id => document.getElementById(id); overlay.querySelector('.aff-modal-close').addEventListener('click', () => overlay.remove()); overlay.addEventListener('click', e => e.target === overlay && overlay.remove()); $('aff-provider').addEventListener('change', function () { const d = ConfigManager.getProviderDefaults(this.value); $('aff-baseurl').value = d.baseUrl; $('aff-model').value = d.model; }); $('aff-test-conn').addEventListener('click', async () => { const btn = $('aff-test-conn'), result = $('aff-test-result'); btn.textContent = '测试中...'; btn.disabled = true; result.style.display = 'none'; const testResult = await AIService.testConnection({ provider: $('aff-provider').value, baseUrl: $('aff-baseurl').value, apiKey: $('aff-apikey').value, model: $('aff-model').value }); btn.textContent = testResult.success ? '✓ 成功' : '✗ 失败'; result.style.display = 'block'; result.style.background = testResult.success ? 'linear-gradient(135deg,#ecfdf5,#d1fae5)' : 'linear-gradient(135deg,#fef2f2,#fee2e2)'; result.style.color = testResult.success ? '#065f46' : '#991b1b'; result.textContent = testResult.success ? '✅ 连接成功!' : '❌ 连接失败:' + testResult.message; btn.disabled = false; setTimeout(() => btn.textContent = '测试连接', 2000); }); $('aff-save-config').addEventListener('click', () => { ConfigManager.setConfig({ provider: $('aff-provider').value, baseUrl: $('aff-baseurl').value, apiKey: $('aff-apikey').value, model: $('aff-model').value, locale: $('aff-locale').value }); overlay.remove(); this.updateButtonStatus('fill', '✓', 'success'); setTimeout(() => this.resetButton('fill'), 1500); }); }, init() { this.isCollapsed = true; // 默认收起 this.createShadowRoot(); this.createPanel(); GM_registerMenuCommand('⚙️ 智填助手设置', () => this.showSettings()); if (!ConfigManager.isConfigValid()) { setTimeout(() => this.showSettings(), 500); } } }; // ========== 主入口 ========== if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', () => UIPanel.init()); } else { UIPanel.init(); } })();