/**
 * cdp_client.js
 * Chrome DevTools Protocol wrapper for browser automation.
 *
 * Runs in the extension service worker and exposes a compact browser.* tool
 * surface through BROWSER_* runtime messages.
 */
(function () {
  'use strict';

  const CDP_VERSION = '1.3';
  const attachedTabs = new Set();
  const networkCaptures = new Map();
  let networkListenerRegistered = false;

  function chromeCall(api, ...args) {
    return new Promise((resolve, reject) => {
      api(...args, (result) => {
        const err = chrome.runtime.lastError;
        if (err) reject(new Error(err.message));
        else resolve(result);
      });
    });
  }

  function normalizeUrl(rawUrl = '') {
    const value = String(rawUrl || '').trim();
    if (!/^https?:\/\//i.test(value)) {
      throw new Error('URL 必须以 http:// 或 https:// 开头');
    }
    return new URL(value).toString();
  }

  function isBlockedUrl(url = '') {
    return /^(chrome|chrome-extension|edge|about|devtools):/i.test(String(url || ''));
  }

  async function getTab(tabId) {
    if (!tabId) throw new Error('缺少 tabId');
    return await chromeCall(chrome.tabs.get, tabId);
  }

  async function getTargetTab(tabId) {
    if (tabId) return await getTab(tabId);
    if (globalThis._automationTabId) return await getTab(globalThis._automationTabId);
    const tabs = await chromeCall(chrome.tabs.query, { active: true, currentWindow: true });
    const tab = tabs?.[0];
    if (!tab?.id) throw new Error('未找到可操作的 Tab');
    return tab;
  }

  async function ensureAttached(tabId) {
    const tab = await getTab(tabId);
    if (isBlockedUrl(tab.url)) throw new Error('浏览器内部页面不支持自动化操作');
    if (!attachedTabs.has(tabId)) {
      try {
        await chromeCall(chrome.debugger.attach, { tabId }, CDP_VERSION);
      } catch (e) {
        if (!/already attached/i.test(e.message || '')) throw e;
      }
      attachedTabs.add(tabId);
    }
    await sendCommand(tabId, 'Runtime.enable').catch(() => {});
    await sendCommand(tabId, 'Page.enable').catch(() => {});
    await sendCommand(tabId, 'DOM.enable').catch(() => {});
    await sendCommand(tabId, 'Network.enable').catch(() => {});
    ensureNetworkListener();
  }

  async function detach(tabId) {
    if (!tabId || !attachedTabs.has(tabId)) return { detached: false };
    try {
      await chromeCall(chrome.debugger.detach, { tabId });
    } finally {
      attachedTabs.delete(tabId);
    }
    return { detached: true, tabId };
  }

  async function sendCommand(tabId, method, params = {}) {
    return await chromeCall(chrome.debugger.sendCommand, { tabId }, method, params);
  }

  function ensureNetworkListener() {
    if (networkListenerRegistered || !chrome.debugger?.onEvent) return;
    chrome.debugger.onEvent.addListener((source, method, params = {}) => {
      const tabId = source?.tabId;
      if (!tabId) return;
      const state = networkCaptures.get(tabId);
      if (!state?.active) return;
      try {
        handleNetworkEvent(tabId, method, params);
      } catch (e) {
        console.warn('[cdp-network] event failed:', method, e.message);
      }
    });
    networkListenerRegistered = true;
  }

  function redactHeaderValue(name, value, includeSensitive = false) {
    if (includeSensitive) return value;
    if (/^(authorization|cookie|set-cookie|x-api-key|proxy-authorization)$/i.test(String(name || ''))) {
      return '[redacted]';
    }
    return value;
  }

  function sanitizeHeaders(headers = {}, includeSensitive = false) {
    const out = {};
    for (const [name, value] of Object.entries(headers || {})) {
      out[name] = redactHeaderValue(name, value, includeSensitive);
    }
    return out;
  }

  function clipText(value, maxChars) {
    const text = String(value ?? '');
    if (text.length <= maxChars) return { text, truncated: false };
    return { text: text.slice(0, maxChars) + '\n...[truncated]', truncated: true };
  }

  function shouldKeepResource(state, resourceType) {
    if (state.captureAll) return true;
    return state.resourceTypes.has(String(resourceType || '').toLowerCase());
  }

  function shouldTryResponseBody(entry) {
    const mime = String(entry.mimeType || '').toLowerCase();
    if (!mime) return true;
    return /json|text|xml|javascript|x-www-form-urlencoded|graphql|event-stream|html/.test(mime);
  }

  function decodeBase64Utf8(base64) {
    try {
      const binary = atob(base64);
      const bytes = new Uint8Array(binary.length);
      for (let i = 0; i < binary.length; i += 1) bytes[i] = binary.charCodeAt(i);
      return new TextDecoder('utf-8', { fatal: false }).decode(bytes);
    } catch (_) {
      return '';
    }
  }

  function getOrCreateNetworkState(tabId, args = {}) {
    let state = networkCaptures.get(tabId);
    if (!state || args.reset) {
      state = {
        active: false,
        entries: new Map(),
        order: [],
        startedAt: Date.now(),
        maxEntries: Number(args.maxEntries || 300),
        maxBodyChars: Number(args.maxBodyChars || 12000),
        includeBodies: args.includeBodies !== false,
        includeSensitiveHeaders: args.includeSensitiveHeaders === true,
        includePostData: args.includePostData !== false,
        captureAll: args.captureAll === true,
        resourceTypes: new Set((args.resourceTypes || args.types || ['XHR', 'Fetch'])
          .map(x => String(x || '').toLowerCase()))
      };
      networkCaptures.set(tabId, state);
    }
    return state;
  }

  function rememberEntry(state, requestId, entry) {
    if (!state.entries.has(requestId)) state.order.push(requestId);
    state.entries.set(requestId, entry);
    while (state.order.length > state.maxEntries) {
      const oldId = state.order.shift();
      state.entries.delete(oldId);
    }
  }

  function summarizeInitiator(initiator = {}) {
    const frame = initiator.stack?.callFrames?.[0] || initiator.asyncStackTrace?.callFrames?.[0];
    return {
      type: initiator.type || '',
      url: frame?.url || initiator.url || '',
      lineNumber: Number.isFinite(frame?.lineNumber) ? frame.lineNumber + 1 : undefined,
      columnNumber: Number.isFinite(frame?.columnNumber) ? frame.columnNumber + 1 : undefined
    };
  }

  function handleNetworkEvent(tabId, method, params = {}) {
    const state = networkCaptures.get(tabId);
    if (!state?.active) return;
    const requestId = params.requestId;
    if (!requestId) return;

    if (method === 'Network.requestWillBeSent') {
      const req = params.request || {};
      if (!shouldKeepResource(state, params.type)) return;
      const current = state.entries.get(requestId) || {};
      const post = clipText(req.postData || '', state.maxBodyChars);
      const entry = {
        ...current,
        requestId,
        tabId,
        url: req.url || params.documentURL || '',
        method: req.method || 'GET',
        resourceType: params.type || current.resourceType || '',
        timestamp: params.timestamp || current.timestamp || 0,
        wallTime: params.wallTime ? new Date(params.wallTime * 1000).toISOString() : current.wallTime,
        requestHeaders: sanitizeHeaders(req.headers || current.requestHeaders || {}, state.includeSensitiveHeaders),
        hasPostData: !!req.hasPostData || !!req.postData,
        postData: state.includePostData ? post.text : '',
        postDataTruncated: post.truncated,
        initiator: summarizeInitiator(params.initiator || {}),
        redirects: current.redirects || []
      };
      if (params.redirectResponse) {
        entry.redirects.push({
          status: params.redirectResponse.status,
          url: params.redirectResponse.url,
          headers: sanitizeHeaders(params.redirectResponse.headers || {}, state.includeSensitiveHeaders)
        });
      }
      rememberEntry(state, requestId, entry);
      if (entry.hasPostData && !entry.postData && state.includePostData) {
        capturePostData(tabId, requestId);
      }
      return;
    }

    const entry = state.entries.get(requestId);
    if (!entry) return;

    if (method === 'Network.requestWillBeSentExtraInfo') {
      entry.requestExtraHeaders = sanitizeHeaders(params.headers || {}, state.includeSensitiveHeaders);
      return;
    }

    if (method === 'Network.responseReceived') {
      const resp = params.response || {};
      entry.status = resp.status;
      entry.statusText = resp.statusText || '';
      entry.mimeType = resp.mimeType || '';
      entry.responseUrl = resp.url || entry.url;
      entry.responseHeaders = sanitizeHeaders(resp.headers || {}, state.includeSensitiveHeaders);
      entry.remoteIPAddress = resp.remoteIPAddress || '';
      entry.protocol = resp.protocol || '';
      entry.fromDiskCache = !!resp.fromDiskCache;
      entry.fromServiceWorker = !!resp.fromServiceWorker;
      return;
    }

    if (method === 'Network.responseReceivedExtraInfo') {
      entry.responseExtraHeaders = sanitizeHeaders(params.headers || {}, state.includeSensitiveHeaders);
      entry.status = entry.status || params.statusCode;
      return;
    }

    if (method === 'Network.loadingFinished') {
      entry.finishedAt = params.timestamp || 0;
      entry.encodedDataLength = params.encodedDataLength || 0;
      entry.durationMs = entry.timestamp && entry.finishedAt
        ? Math.max(0, Math.round((entry.finishedAt - entry.timestamp) * 1000))
        : undefined;
      entry.finished = true;
      if (state.includeBodies) captureResponseBody(tabId, requestId);
      return;
    }

    if (method === 'Network.loadingFailed') {
      entry.failed = true;
      entry.errorText = params.errorText || '';
      entry.canceled = !!params.canceled;
      entry.finished = true;
    }
  }

  async function capturePostData(tabId, requestId) {
    const state = networkCaptures.get(tabId);
    const entry = state?.entries.get(requestId);
    if (!state?.active || !entry) return;
    try {
      const result = await sendCommand(tabId, 'Network.getRequestPostData', { requestId });
      const clipped = clipText(result.postData || '', state.maxBodyChars);
      entry.postData = clipped.text;
      entry.postDataTruncated = clipped.truncated;
    } catch (e) {
      entry.postDataError = e.message;
    }
  }

  async function captureResponseBody(tabId, requestId) {
    const state = networkCaptures.get(tabId);
    const entry = state?.entries.get(requestId);
    if (!state?.active || !entry || entry.bodyCaptured || entry.bodyError) return;
    if (!shouldTryResponseBody(entry)) {
      entry.bodySkipped = 'non-text response';
      return;
    }
    try {
      const result = await sendCommand(tabId, 'Network.getResponseBody', { requestId });
      const raw = result.base64Encoded ? decodeBase64Utf8(result.body || '') : String(result.body || '');
      if (!raw && result.base64Encoded) {
        entry.bodySkipped = 'base64 binary response';
        return;
      }
      const clipped = clipText(raw, state.maxBodyChars);
      entry.responseBody = clipped.text;
      entry.responseBodyTruncated = clipped.truncated;
      entry.responseBodyBase64Encoded = !!result.base64Encoded;
      entry.bodyCaptured = true;
    } catch (e) {
      entry.bodyError = e.message;
    }
  }

  function buildCurl(entry) {
    const lines = [`curl ${JSON.stringify(entry.responseUrl || entry.url || '')}`];
    if (entry.method && entry.method !== 'GET') lines.push(`  -X ${entry.method}`);
    const headers = { ...(entry.requestHeaders || {}), ...(entry.requestExtraHeaders || {}) };
    for (const [name, value] of Object.entries(headers)) {
      if (/^(cookie|content-length|host)$/i.test(name)) continue;
      lines.push(`  -H ${JSON.stringify(`${name}: ${value}`)}`);
    }
    if (entry.postData) lines.push(`  --data-raw ${JSON.stringify(entry.postData.replace(/\n\.\.\.\[truncated\]$/, ''))}`);
    return lines.join(' \\\n');
  }

  function entryToHar(entry) {
    const startedDateTime = entry.wallTime || new Date().toISOString();
    const requestHeaders = Object.entries(entry.requestHeaders || {}).map(([name, value]) => ({ name, value: String(value) }));
    const responseHeaders = Object.entries(entry.responseHeaders || {}).map(([name, value]) => ({ name, value: String(value) }));
    return {
      startedDateTime,
      time: entry.durationMs || 0,
      request: {
        method: entry.method || 'GET',
        url: entry.url || '',
        httpVersion: 'HTTP/2',
        headers: requestHeaders,
        queryString: [],
        cookies: [],
        headersSize: -1,
        bodySize: entry.postData ? entry.postData.length : 0,
        postData: entry.postData ? { mimeType: entry.requestHeaders?.['content-type'] || '', text: entry.postData } : undefined
      },
      response: {
        status: entry.status || 0,
        statusText: entry.statusText || '',
        httpVersion: entry.protocol || 'HTTP/2',
        headers: responseHeaders,
        cookies: [],
        content: {
          size: entry.responseBody ? entry.responseBody.length : Number(entry.encodedDataLength || 0),
          mimeType: entry.mimeType || '',
          text: entry.responseBody || undefined
        },
        redirectURL: '',
        headersSize: -1,
        bodySize: Number(entry.encodedDataLength || -1)
      },
      cache: {},
      timings: { send: 0, wait: entry.durationMs || 0, receive: 0 }
    };
  }

  function listCapturedEntries(tabId, args = {}) {
    const state = networkCaptures.get(tabId);
    if (!state) return { tabId, active: false, count: 0, requests: [] };
    const limit = Math.min(Number(args.limit || 50), 300);
    const urlIncludes = String(args.urlIncludes || args.url || '').toLowerCase();
    const method = String(args.method || '').toUpperCase();
    const types = new Set((args.resourceTypes || args.types || []).map(x => String(x || '').toLowerCase()));
    let requests = state.order.map(id => state.entries.get(id)).filter(Boolean);
    if (urlIncludes) requests = requests.filter(x => String(x.url || x.responseUrl || '').toLowerCase().includes(urlIncludes));
    if (method) requests = requests.filter(x => String(x.method || '').toUpperCase() === method);
    if (types.size > 0) requests = requests.filter(x => types.has(String(x.resourceType || '').toLowerCase()));
    requests = requests.slice(-limit).map(entry => {
      const item = {
        ...entry,
        curl: args.includeCurl === false ? undefined : buildCurl(entry)
      };
      if (args.includeBodies === false) {
        delete item.responseBody;
        delete item.postData;
      }
      return item;
    });
    if (args.format === 'har') {
      return {
        tabId,
        active: state.active,
        count: requests.length,
        har: {
          log: {
            version: '1.2',
            creator: { name: '詹老师的AIPROS CDP Network', version: '0.4.0' },
            entries: requests.map(entryToHar)
          }
        }
      };
    }
    return {
      tabId,
      active: state.active,
      startedAt: new Date(state.startedAt).toISOString(),
      count: requests.length,
      totalCaptured: state.order.length,
      requests
    };
  }

  async function evaluate(tabId, expression) {
    await ensureAttached(tabId);
    const result = await sendCommand(tabId, 'Runtime.evaluate', {
      expression,
      awaitPromise: true,
      returnByValue: true,
      userGesture: true
    });
    if (result.exceptionDetails) {
      const message = result.exceptionDetails.exception?.description ||
        result.exceptionDetails.text ||
        '页面脚本执行失败';
      throw new Error(message);
    }
    return result.result?.value;
  }

  async function waitForTabLoad(tabId, timeout = 15000) {
    const current = await getTab(tabId).catch(() => null);
    if (current?.status === 'complete') return current;
    return await new Promise((resolve, reject) => {
      let done = false;
      const timer = setTimeout(async () => {
        if (done) return;
        done = true;
        chrome.tabs.onUpdated.removeListener(listener);
        try {
          resolve(await getTab(tabId));
        } catch (e) {
          reject(e);
        }
      }, timeout);
      const listener = (updatedTabId, changeInfo, tab) => {
        if (updatedTabId !== tabId) return;
        if (changeInfo.status !== 'complete') return;
        if (done) return;
        done = true;
        clearTimeout(timer);
        chrome.tabs.onUpdated.removeListener(listener);
        resolve(tab);
      };
      chrome.tabs.onUpdated.addListener(listener);
    });
  }

  async function open(args = {}) {
    const url = normalizeUrl(args.url);
    const newTab = args.newTab !== false;
    let tab;
    if (newTab || !globalThis._automationTabId) {
      tab = await chromeCall(chrome.tabs.create, { url, active: args.active !== false });
    } else {
      tab = await chromeCall(chrome.tabs.update, globalThis._automationTabId, { url, active: args.active !== false });
    }
    globalThis._automationTabId = tab.id;
    const loaded = await waitForTabLoad(tab.id, args.timeout || 20000);
    await ensureAttached(loaded.id);
    const summary = await getText({ tabId: loaded.id, maxLength: 2000 }).catch(() => ({}));
    return {
      tabId: loaded.id,
      url: loaded.url || url,
      title: loaded.title || summary.title || '',
      text: summary.text || ''
    };
  }

  async function navigate(args = {}) {
    const tab = await getTargetTab(args.tabId);
    const url = normalizeUrl(args.url);
    await chromeCall(chrome.tabs.update, tab.id, { url, active: args.active !== false });
    const loaded = await waitForTabLoad(tab.id, args.timeout || 20000);
    globalThis._automationTabId = loaded.id;
    await ensureAttached(loaded.id);
    return { tabId: loaded.id, url: loaded.url || url, title: loaded.title || '' };
  }

  function ensureSearchSelector(args = {}) {
    if (args.selector) return String(args.selector);
    const cue = [
      args.intent,
      args.purpose,
      args.textTarget,
      args.label,
      args.placeholder,
      args.name,
      args.role,
      args.site
    ].map(v => String(v || '')).join(' ');
    if (args.search !== true && !/(搜索|搜|查询|检索|search|query|百度|google|bing|duckduckgo)/i.test(cue)) return '';
    return [
      'input[type="search"]',
      '[role="searchbox"]',
      'textarea[name="q"]',
      'input[name="q"]',
      'input[name="wd"]',
      'input[name="word"]',
      'input[name="keyword"]',
      '#kw',
      '#searchInput',
      '#search-input',
      'input[placeholder*="搜索"]',
      'input[placeholder*="查询"]',
      'input[placeholder*="Search" i]',
      'textarea[placeholder*="搜索"]',
      'textarea[placeholder*="Search" i]'
    ].join(',');
  }

  function elementLocatorExpression(args = {}) {
    const payload = {
      ...args,
      selector: args.selector || '',
      searchSelector: ensureSearchSelector(args)
    };
    return `
(() => {
  const args = ${JSON.stringify(payload)};
  const normalize = value => String(value || '').replace(/\\s+/g, ' ').trim();
  const lower = value => normalize(value).toLowerCase();
  const tagOf = el => (el?.tagName || '').toUpperCase();
  const viewOf = el => el?.ownerDocument?.defaultView || window;
  const selectorInput = args.selector || args.searchSelector || '';
  const needles = (Array.isArray(args.texts) ? args.texts : [args.text, args.textTarget, args.label])
    .map(normalize)
    .filter(Boolean);
  const intent = normalize(args.intent || args.action || args.purpose);
  const intentNeedles = intent === 'approval-submit'
    ? ['提交审批', '提交', '确认提交', '确定', '确认', '同意', '通过', '批准', '办理', '完成']
    : intent === 'approval-reject'
      ? ['驳回', '退回', '拒绝', '不同意', '提交', '确定', '确认']
      : /search|搜索|查询|检索/i.test(intent)
        ? ['搜索', '百度一下', 'Search', 'Google Search', 'Bing Search']
        : [];
  const allNeedles = [...new Set([...needles, ...intentNeedles].filter(Boolean))];
  const hasExplicitTarget = !!(selectorInput || allNeedles.length || args.role || args.name || args.placeholder || Number.isFinite(Number(args.index)) || intent);

  function cssEscape(value) {
    if (window.CSS?.escape) return window.CSS.escape(String(value));
    return String(value).replace(/["\\\\#.:,[\\]>+~*^$|=\\s]/g, '\\\\$&');
  }

  function visibleEnough(el) {
    if (!el || el.nodeType !== 1) return false;
    const win = viewOf(el);
    const s = win.getComputedStyle(el);
    const r = el.getBoundingClientRect();
    return s.display !== 'none' && s.visibility !== 'hidden' && s.opacity !== '0' && r.width > 1 && r.height > 1;
  }

  function enabledEnough(el) {
    return !el.disabled && !el.readOnly && el.getAttribute?.('aria-disabled') !== 'true' && !el.closest?.('[aria-disabled="true"],[disabled]');
  }

  function textOf(el) {
    return normalize([
      el.innerText,
      el.textContent,
      el.value,
      el.getAttribute?.('aria-label'),
      el.getAttribute?.('placeholder'),
      el.getAttribute?.('name'),
      el.getAttribute?.('id'),
      el.getAttribute?.('title'),
      el.labels?.[0]?.innerText,
      el.closest?.('label')?.innerText,
      el.closest?.('[aria-label]')?.getAttribute?.('aria-label')
    ].filter(Boolean).join(' '));
  }

  function roleOf(el) {
    const explicit = el.getAttribute?.('role');
    if (explicit) return explicit;
    const tag = tagOf(el);
    const type = String(el.type || '').toLowerCase();
    if (tag === 'BUTTON' || (tag === 'INPUT' && /button|submit|reset/.test(type))) return 'button';
    if (tag === 'A' && el.href) return 'link';
    if (tag === 'SELECT') return 'combobox';
    if (tag === 'TEXTAREA') return 'textbox';
    if (tag === 'INPUT') {
      if (type === 'search') return 'searchbox';
      if (/checkbox|radio|range/.test(type)) return type;
      return 'textbox';
    }
    if (el.isContentEditable) return 'textbox';
    return '';
  }

  function isInteractive(el) {
    if (!visibleEnough(el) || !enabledEnough(el)) return false;
    const tag = tagOf(el);
    const type = String(el.type || '').toLowerCase();
    if (['BUTTON', 'A', 'SELECT', 'TEXTAREA', 'SUMMARY'].includes(tag)) return true;
    if (tag === 'INPUT' && !['hidden', 'file', 'image'].includes(type)) return true;
    if (el.isContentEditable || el.getAttribute?.('contenteditable') === 'true') return true;
    if (el.getAttribute?.('role')) return true;
    if (el.onclick || el.getAttribute?.('onclick')) return true;
    if (Number.isFinite(Number(el.getAttribute?.('tabindex')))) return true;
    const cls = String(el.className || '').toLowerCase();
    return /\\b(btn|button|link|tab|menu|select|input|search)\\b/.test(cls);
  }

  function queryDeepAll(root, selector, out = []) {
    if (!root || !selector) return out;
    try {
      out.push(...Array.from(root.querySelectorAll?.(selector) || []));
    } catch (_) {}
    let all = [];
    try { all = Array.from(root.querySelectorAll?.('*') || []); } catch (_) {}
    for (const el of all) {
      if (el.shadowRoot) queryDeepAll(el.shadowRoot, selector, out);
      if ((tagOf(el) === 'IFRAME' || tagOf(el) === 'FRAME') && el.contentDocument) {
        try { queryDeepAll(el.contentDocument, selector, out); } catch (_) {}
      }
    }
    return [...new Set(out)];
  }

  function collectDeep(root, out = []) {
    if (!root) return out;
    let all = [];
    try { all = Array.from(root.querySelectorAll?.('*') || []); } catch (_) {}
    for (const el of all) {
      out.push(el);
      if (el.shadowRoot) collectDeep(el.shadowRoot, out);
      if ((tagOf(el) === 'IFRAME' || tagOf(el) === 'FRAME') && el.contentDocument) {
        try { collectDeep(el.contentDocument, out); } catch (_) {}
      }
    }
    return out;
  }

  function globalRect(el) {
    const rect = el.getBoundingClientRect();
    let left = rect.left;
    let top = rect.top;
    let win = viewOf(el);
    while (win && win !== win.top) {
      try {
        const frame = win.frameElement;
        if (!frame) break;
        const frameRect = frame.getBoundingClientRect();
        left += frameRect.left;
        top += frameRect.top;
        win = win.parent;
      } catch (_) {
        break;
      }
    }
    return { left, top, width: rect.width, height: rect.height, right: left + rect.width, bottom: top + rect.height };
  }

  function canReceivePointer(el) {
    const doc = el.ownerDocument || document;
    const r = el.getBoundingClientRect();
    const x = Math.max(0, Math.min(doc.documentElement.clientWidth - 1, r.left + r.width / 2));
    const y = Math.max(0, Math.min(doc.documentElement.clientHeight - 1, r.top + r.height / 2));
    const root = el.getRootNode?.();
    const hit = root?.elementFromPoint?.(x, y) || doc.elementFromPoint?.(x, y);
    const docHit = doc.elementFromPoint?.(x, y);
    return !!hit && (hit === el || el.contains(hit) || hit.contains?.(el) || docHit === el || el.contains(docHit));
  }

  function selectorFor(el) {
    const tag = tagOf(el).toLowerCase();
    const id = el.getAttribute?.('id');
    if (id) return '#' + cssEscape(id);
    const name = el.getAttribute?.('name');
    if (name) return tag + '[name="' + String(name).replace(/"/g, '\\\\"') + '"]';
    const aria = el.getAttribute?.('aria-label');
    if (aria) return tag + '[aria-label="' + String(aria).replace(/"/g, '\\\\"') + '"]';
    const placeholder = el.getAttribute?.('placeholder');
    if (placeholder) return tag + '[placeholder="' + String(placeholder).replace(/"/g, '\\\\"') + '"]';
    const parts = [];
    let node = el;
    while (node && node.nodeType === 1 && parts.length < 4) {
      let part = tagOf(node).toLowerCase();
      const parent = node.parentElement;
      if (parent) {
        const same = Array.from(parent.children).filter(child => tagOf(child) === tagOf(node));
        if (same.length > 1) part += ':nth-of-type(' + (same.indexOf(node) + 1) + ')';
      }
      parts.unshift(part);
      node = parent;
    }
    return parts.join(' > ');
  }

  function scoreCandidate(el, index) {
    const text = textOf(el);
    const role = roleOf(el);
    const lowerClass = String(el.className || '').toLowerCase();
    const tag = tagOf(el);
    let score = 0;
    if (selectorInput && queryDeepAll(document, selectorInput).includes(el)) score += 1200;
    for (const needle of allNeedles) {
      if (!needle) continue;
      const a = lower(text);
      const b = lower(needle);
      if (a === b) score = Math.max(score, 1000);
      else if (a.startsWith(b)) score = Math.max(score, 850);
      else if (a.includes(b)) score = Math.max(score, 650);
    }
    if (args.role && lower(role) === lower(args.role)) score += 260;
    if (args.name && lower(text).includes(lower(args.name))) score += 220;
    if (args.placeholder && lower(text).includes(lower(args.placeholder))) score += 220;
    if (tag === 'BUTTON') score += 140;
    if (role === 'button') score += 120;
    if (role === 'textbox' || role === 'searchbox') score += selectorInput ? 120 : 20;
    if (tag === 'INPUT' && /button|submit/i.test(el.type || '')) score += 120;
    if (/primary|submit|confirm|ok|agree|approve|success/.test(lowerClass)) score += 90;
    if (/disabled|loading/.test(lowerClass)) score -= 300;
    if (canReceivePointer(el)) score += 80;
    const rect = el.getBoundingClientRect();
    if (rect.width > 260 || rect.height > 80) score -= 80;
    return { item: el, score, text, role, selector: selectorFor(el), index };
  }

  if (!hasExplicitTarget) return { ok: false, error: '请提供 selector、text、label、placeholder、role、index 或 x/y 坐标' };
  let candidates = [];
  if (selectorInput) candidates = queryDeepAll(document, selectorInput).filter(el => visibleEnough(el) && enabledEnough(el));
  if (!candidates.length) candidates = collectDeep(document).filter(isInteractive);
  let scored = candidates.map(scoreCandidate).filter(x => x.score > 0).sort((a, b) => b.score - a.score || a.index - b.index);
  if (Number.isFinite(Number(args.index))) scored = scored.filter((_, i) => i === Number(args.index));
  const best = scored[0] || (selectorInput && candidates[0] ? scoreCandidate(candidates[0], 0) : null);
  const el = best?.item;
  if (!el) return { ok: false, error: '未找到可点击目标: ' + (selectorInput || allNeedles.join('/') || intent || '未提供目标') };
  el.scrollIntoView({ block: 'center', inline: 'center', behavior: 'instant' });
  const rect = globalRect(el);
  const canPoint = canReceivePointer(el);
  if (!canPoint && args.force !== true) return { ok: false, error: '目标被遮挡或无法接收点击事件: ' + (best.text || best.selector || el.tagName) };
  return {
    ok: true,
    x: rect.left + rect.width / 2,
    y: rect.top + rect.height / 2,
    tagName: tagOf(el).toLowerCase(),
    role: best.role,
    selector: best.selector,
    text: best.text.slice(0, 200),
    score: best.score,
    actionability: { visible: true, enabled: true, receivesPointer: canPoint }
  };
})()`;
  }

  async function observe(args = {}) {
    const tab = await getTargetTab(args.tabId);
    await ensureAttached(tab.id);
    const limit = Math.max(1, Math.min(200, Number(args.limit || 80)));
    const maxText = Math.max(500, Math.min(20000, Number(args.maxText || args.maxLength || 4000)));
    const value = await evaluate(tab.id, `
(() => {
  const limit = ${limit};
  const maxText = ${maxText};
  const normalize = value => String(value || '').replace(/\\s+/g, ' ').trim();
  const tagOf = el => (el?.tagName || '').toUpperCase();
  const viewOf = el => el?.ownerDocument?.defaultView || window;

  function cssEscape(value) {
    if (window.CSS?.escape) return window.CSS.escape(String(value));
    return String(value).replace(/["\\\\#.:,[\\]>+~*^$|=\\s]/g, '\\\\$&');
  }

  function visibleEnough(el) {
    if (!el || el.nodeType !== 1) return false;
    const win = viewOf(el);
    const style = win.getComputedStyle(el);
    if (style.display === 'none' || style.visibility === 'hidden' || style.opacity === '0') return false;
    const rect = el.getBoundingClientRect();
    return rect.width > 1 && rect.height > 1;
  }

  function enabledEnough(el) {
    return !el.disabled && !el.readOnly && el.getAttribute?.('aria-disabled') !== 'true' && !el.closest?.('[aria-disabled="true"],[disabled]');
  }

  function roleOf(el) {
    const explicit = el.getAttribute?.('role');
    if (explicit) return explicit;
    const tag = tagOf(el);
    const type = String(el.type || '').toLowerCase();
    if (tag === 'BUTTON' || (tag === 'INPUT' && /button|submit|reset/.test(type))) return 'button';
    if (tag === 'A' && el.href) return 'link';
    if (tag === 'SELECT') return 'combobox';
    if (tag === 'TEXTAREA') return 'textbox';
    if (tag === 'INPUT') {
      if (type === 'search') return 'searchbox';
      if (/checkbox|radio|range/.test(type)) return type;
      return 'textbox';
    }
    if (el.isContentEditable) return 'textbox';
    return '';
  }

  function labelOf(el) {
    return normalize([
      el.getAttribute?.('aria-label'),
      el.getAttribute?.('placeholder'),
      el.getAttribute?.('name'),
      el.getAttribute?.('id'),
      el.getAttribute?.('title'),
      el.labels?.[0]?.innerText,
      el.closest?.('label')?.innerText,
      el.closest?.('[aria-label]')?.getAttribute?.('aria-label'),
      el.innerText,
      el.textContent,
      el.value
    ].filter(Boolean).join(' ')).slice(0, 180);
  }

  function selectorFor(el) {
    const tag = tagOf(el).toLowerCase();
    const id = el.getAttribute?.('id');
    if (id) return '#' + cssEscape(id);
    const name = el.getAttribute?.('name');
    if (name) return tag + '[name="' + String(name).replace(/"/g, '\\\\"') + '"]';
    const aria = el.getAttribute?.('aria-label');
    if (aria) return tag + '[aria-label="' + String(aria).replace(/"/g, '\\\\"') + '"]';
    const placeholder = el.getAttribute?.('placeholder');
    if (placeholder) return tag + '[placeholder="' + String(placeholder).replace(/"/g, '\\\\"') + '"]';
    const parts = [];
    let node = el;
    while (node && node.nodeType === 1 && parts.length < 4) {
      let part = tagOf(node).toLowerCase();
      const parent = node.parentElement;
      if (parent) {
        const same = Array.from(parent.children).filter(child => tagOf(child) === tagOf(node));
        if (same.length > 1) part += ':nth-of-type(' + (same.indexOf(node) + 1) + ')';
      }
      parts.unshift(part);
      node = parent;
    }
    return parts.join(' > ');
  }

  function globalRect(el) {
    const rect = el.getBoundingClientRect();
    let left = rect.left;
    let top = rect.top;
    let win = viewOf(el);
    while (win && win !== win.top) {
      try {
        const frame = win.frameElement;
        if (!frame) break;
        const frameRect = frame.getBoundingClientRect();
        left += frameRect.left;
        top += frameRect.top;
        win = win.parent;
      } catch (_) {
        break;
      }
    }
    return {
      x: Math.round(left),
      y: Math.round(top),
      width: Math.round(rect.width),
      height: Math.round(rect.height)
    };
  }

  function canReceivePointer(el) {
    const doc = el.ownerDocument || document;
    const rect = el.getBoundingClientRect();
    const x = Math.max(0, Math.min(doc.documentElement.clientWidth - 1, rect.left + rect.width / 2));
    const y = Math.max(0, Math.min(doc.documentElement.clientHeight - 1, rect.top + rect.height / 2));
    const root = el.getRootNode?.();
    const hit = root?.elementFromPoint?.(x, y) || doc.elementFromPoint?.(x, y);
    const docHit = doc.elementFromPoint?.(x, y);
    return !!hit && (hit === el || el.contains(hit) || hit.contains?.(el) || docHit === el || el.contains(docHit));
  }

  function queryDeepAll(root, selector, out = []) {
    if (!root || !selector) return out;
    try { out.push(...Array.from(root.querySelectorAll?.(selector) || [])); } catch (_) {}
    let all = [];
    try { all = Array.from(root.querySelectorAll?.('*') || []); } catch (_) {}
    for (const el of all) {
      if (el.shadowRoot) queryDeepAll(el.shadowRoot, selector, out);
      if ((tagOf(el) === 'IFRAME' || tagOf(el) === 'FRAME') && el.contentDocument) {
        try { queryDeepAll(el.contentDocument, selector, out); } catch (_) {}
      }
    }
    return [...new Set(out)];
  }

  const interactiveSelector = [
    'button', 'a[href]', 'summary', 'select', 'textarea',
    'input:not([type="hidden"])',
    '[contenteditable="true"]',
    '[role="button"]', '[role="link"]', '[role="textbox"]', '[role="searchbox"]',
    '[role="checkbox"]', '[role="radio"]', '[role="switch"]', '[role="tab"]',
    '[role="menuitem"]', '[role="option"]', '[onclick]', '[tabindex]'
  ].join(',');
  const elements = queryDeepAll(document, interactiveSelector)
    .filter(el => visibleEnough(el) && enabledEnough(el))
    .slice(0, limit)
    .map((el, index) => {
      const rect = globalRect(el);
      return {
        index,
        role: roleOf(el),
        tagName: tagOf(el).toLowerCase(),
        selector: selectorFor(el),
        label: labelOf(el),
        value: 'value' in el ? String(el.value || '').slice(0, 120) : '',
        rect,
        center: { x: Math.round(rect.x + rect.width / 2), y: Math.round(rect.y + rect.height / 2) },
        receivesPointer: canReceivePointer(el)
      };
    });
  return {
    title: document.title,
    url: location.href,
    viewport: { width: innerWidth, height: innerHeight, scrollX, scrollY },
    text: normalize(document.body?.innerText || '').slice(0, maxText),
    elements
  };
})()`);
    return { tabId: tab.id, ...value };
  }

  async function click(args = {}) {
    const tab = await getTargetTab(args.tabId);
    await ensureAttached(tab.id);
    let point = null;
    if (Number.isFinite(Number(args.x)) && Number.isFinite(Number(args.y))) {
      point = { ok: true, x: Number(args.x), y: Number(args.y), text: '坐标点击' };
    } else {
      const expr = elementLocatorExpression(args);
      if (!expr) throw new Error('请提供 selector、text 或 x/y 坐标');
      point = await evaluate(tab.id, expr);
    }
    if (!point?.ok) throw new Error(point?.error || '未找到可点击目标');
    const button = args.button || 'left';
    const clickCount = args.clickCount || 1;
    await sendCommand(tab.id, 'Input.dispatchMouseEvent', { type: 'mouseMoved', x: point.x, y: point.y, button: 'none' });
    await sendCommand(tab.id, 'Input.dispatchMouseEvent', { type: 'mousePressed', x: point.x, y: point.y, button, clickCount });
    await sendCommand(tab.id, 'Input.dispatchMouseEvent', { type: 'mouseReleased', x: point.x, y: point.y, button, clickCount });
    if (args.waitAfter !== false) await new Promise(r => setTimeout(r, Number(args.waitAfter || 600)));
    return { clicked: true, tabId: tab.id, target: point };
  }

  async function type(args = {}) {
    const tab = await getTargetTab(args.tabId);
    await ensureAttached(tab.id);
    const selector = args.selector || ensureSearchSelector(args);
    if (selector || args.textTarget || args.label || args.placeholder || args.name || args.role) {
      await click({
        tabId: tab.id,
        selector,
        text: args.textTarget,
        label: args.label,
        placeholder: args.placeholder,
        name: args.name,
        role: args.role || (selector ? '' : 'textbox'),
        intent: args.intent || args.purpose,
        search: args.search,
        waitAfter: 100
      });
    }
    if (args.clear !== false) {
      await evaluate(tab.id, `
(() => {
  const el = document.activeElement;
  if (!el) return false;
  if ('value' in el) {
    el.value = '';
    el.dispatchEvent(new Event('input', { bubbles: true }));
    el.dispatchEvent(new Event('change', { bubbles: true }));
    return true;
  }
  if (el.isContentEditable) {
    el.textContent = '';
    el.dispatchEvent(new InputEvent('input', { bubbles: true, inputType: 'deleteContentBackward' }));
    return true;
  }
  return false;
})()`).catch(() => {});
    }
    await sendCommand(tab.id, 'Input.insertText', { text: String(args.value ?? args.text ?? '') });
    if (args.waitAfter !== false) await new Promise(r => setTimeout(r, Number(args.waitAfter || 300)));
    return { typed: true, tabId: tab.id, length: String(args.value ?? args.text ?? '').length };
  }

  async function fill(args = {}) {
    const tab = await getTargetTab(args.tabId);
    const value = String(args.value ?? args.text ?? args.content ?? '');
    const payload = {
      selector: args.selector || ensureSearchSelector(args) || '',
      value,
      clear: args.clear !== false,
      intent: args.intent || args.purpose || '',
      textTarget: args.textTarget || '',
      label: args.label || '',
      placeholder: args.placeholder || '',
      name: args.name || '',
      role: args.role || ''
    };
    const result = await evaluate(tab.id, `
(() => {
  const args = ${JSON.stringify(payload)};
  const keywords = ['意见', '建议', '备注', '说明', '评论', '审批', 'opinion', 'comment', 'remark', 'advice', 'suggest', 'note'];
  const preferredSelectors = [
    'textarea[name*="opinion" i]', 'textarea[name*="comment" i]', 'textarea[name*="remark" i]',
    'textarea[name*="advice" i]', 'textarea[name*="suggest" i]',
    'textarea[placeholder*="意见"]', 'textarea[placeholder*="建议"]', 'textarea[placeholder*="备注"]',
    'textarea[placeholder*="说明"]', 'textarea[aria-label*="意见"]', 'textarea[aria-label*="备注"]',
    'input[name*="opinion" i]', 'input[name*="comment" i]', 'input[name*="remark" i]',
    'input[placeholder*="意见"]', 'input[placeholder*="建议"]', 'input[placeholder*="备注"]',
    '[contenteditable="true"][aria-label*="意见"]', '[contenteditable="true"][aria-label*="备注"]',
    '[role="textbox"][aria-label*="意见"]', '[role="textbox"][aria-label*="备注"]',
    '[role="searchbox"]', 'input[type="search"]', 'input[name="q"]', 'textarea[name="q"]',
    'input[name="wd"]', 'input[name="keyword"]', '#kw',
    'input[placeholder*="搜索"]', 'input[placeholder*="查询"]', 'input[placeholder*="Search" i]',
    '.ProseMirror', '[contenteditable="true"]', '[role="textbox"]',
    'textarea', 'input[type="text"]', 'input:not([type])'
  ];

  const uniq = (items) => Array.from(new Set(items.filter(Boolean)));
  const viewOf = (el) => el?.ownerDocument?.defaultView || window;
  const tagOf = (el) => (el?.tagName || '').toUpperCase();
  const textOf = (el) => [
    el.getAttribute?.('aria-label'),
    el.getAttribute?.('placeholder'),
    el.getAttribute?.('name'),
    el.getAttribute?.('id'),
    el.getAttribute?.('title'),
    el.labels?.[0]?.innerText,
    el.closest?.('label')?.innerText,
    el.closest?.('[aria-label]')?.getAttribute?.('aria-label')
  ].filter(Boolean).join(' ');

  function visibleEnough(el) {
    if (!el || el.nodeType !== 1) return false;
    const win = viewOf(el);
    const style = win.getComputedStyle(el);
    if (style.display === 'none' || style.visibility === 'hidden' || style.opacity === '0') return false;
    const rect = el.getBoundingClientRect();
    return rect.width > 1 && rect.height > 1;
  }

  function isEditable(el) {
    if (!el || !visibleEnough(el)) return false;
    if (el.disabled || el.readOnly || el.getAttribute?.('aria-disabled') === 'true') return false;
    const tag = tagOf(el);
    if (tag === 'TEXTAREA' || tag === 'SELECT') return true;
    if (tag === 'INPUT') {
      const type = String(el.type || 'text').toLowerCase();
      return !['button', 'checkbox', 'color', 'file', 'hidden', 'image', 'radio', 'range', 'reset', 'submit'].includes(type);
    }
    return el.isContentEditable || el.getAttribute?.('contenteditable') === 'true' || el.getAttribute?.('role') === 'textbox';
  }

  function queryDeep(root, selector) {
    if (!root) return null;
    try {
      const direct = root.querySelector?.(selector);
      if (direct) return direct;
    } catch (_) {
      return null;
    }
    let all = [];
    try { all = Array.from(root.querySelectorAll?.('*') || []); } catch (_) {}
    for (const el of all) {
      if (el.shadowRoot) {
        const found = queryDeep(el.shadowRoot, selector);
        if (found) return found;
      }
      if ((tagOf(el) === 'IFRAME' || tagOf(el) === 'FRAME') && el.contentDocument) {
        try {
          const found = queryDeep(el.contentDocument, selector);
          if (found) return found;
        } catch (_) {}
      }
    }
    return null;
  }

  function collectDeep(root, out = []) {
    if (!root) return out;
    try {
      for (const el of Array.from(root.querySelectorAll?.('*') || [])) {
        out.push(el);
        if (el.shadowRoot) collectDeep(el.shadowRoot, out);
        if ((tagOf(el) === 'IFRAME' || tagOf(el) === 'FRAME') && el.contentDocument) {
          try { collectDeep(el.contentDocument, out); } catch (_) {}
        }
      }
    } catch (_) {}
    return out;
  }

  function findTarget() {
    if (args.selector) return queryDeep(document, args.selector);
    const active = document.activeElement;
    if (isEditable(active)) return active;
    const preferred = [];
    for (const selector of preferredSelectors) {
      const found = queryDeep(document, selector);
      if (isEditable(found)) preferred.push(found);
    }
    const all = collectDeep(document).filter(isEditable);
    const candidates = uniq([...preferred, ...all]);
    candidates.sort((a, b) => scoreTarget(b) - scoreTarget(a));
    return candidates[0] || null;
  }

  function scoreTarget(el) {
    let score = 0;
    const tag = tagOf(el);
    const meta = textOf(el).toLowerCase();
    const wanted = [args.textTarget, args.label, args.placeholder, args.name].map(v => String(v || '').trim().toLowerCase()).filter(Boolean);
    if (tag === 'TEXTAREA') score += 60;
    if (tag === 'INPUT') score += 40;
    if (el.isContentEditable || el.getAttribute?.('role') === 'textbox') score += 45;
    if (document.activeElement === el) score += 80;
    if (keywords.some(k => meta.includes(k.toLowerCase()))) score += 120;
    if (args.role && String(el.getAttribute?.('role') || '').toLowerCase() === String(args.role).toLowerCase()) score += 80;
    if (wanted.some(k => meta === k)) score += 260;
    if (wanted.some(k => meta.includes(k))) score += 180;
    if (/opinion|comment|remark|advice|suggest/i.test(meta)) score += 80;
    if (/搜索|查询|search|query/.test(String(args.intent || '').toLowerCase()) && /搜索|查询|search|query|q|wd|keyword/.test(meta)) score += 160;
    const rect = el.getBoundingClientRect();
    score += Math.min(30, Math.round((rect.width * rect.height) / 5000));
    return score;
  }

  function fireInputEvents(el, data) {
    const win = viewOf(el);
    try { el.dispatchEvent(new win.InputEvent('beforeinput', { bubbles: true, cancelable: true, inputType: 'insertText', data })); } catch (_) {}
    try { el.dispatchEvent(new win.InputEvent('input', { bubbles: true, inputType: 'insertText', data })); } catch (_) { el.dispatchEvent(new win.Event('input', { bubbles: true })); }
    el.dispatchEvent(new win.Event('change', { bubbles: true }));
    try { el.dispatchEvent(new win.KeyboardEvent('keyup', { bubbles: true, key: 'Unidentified' })); } catch (_) {}
  }

  function setControlValue(el, value) {
    const win = viewOf(el);
    const tag = tagOf(el);
    const finalValue = args.clear ? value : String(el.value || '') + value;
    const proto = tag === 'TEXTAREA' ? win.HTMLTextAreaElement?.prototype : win.HTMLInputElement?.prototype;
    const setter = Object.getOwnPropertyDescriptor(proto || {}, 'value')?.set;
    if (setter) setter.call(el, finalValue);
    else el.value = finalValue;
    fireInputEvents(el, value);
  }

  function setSelectValue(el, value) {
    const wanted = String(value).trim();
    const options = Array.from(el.options || []);
    const option = options.find(o => o.value === wanted) ||
      options.find(o => (o.textContent || '').trim() === wanted) ||
      options.find(o => (o.textContent || '').includes(wanted));
    if (!option) throw new Error('未找到匹配的下拉选项: ' + wanted);
    el.value = option.value;
    fireInputEvents(el, option.value);
  }

  function setEditableText(el, value) {
    const doc = el.ownerDocument || document;
    const win = doc.defaultView || window;
    let inserted = false;
    try {
      const selection = win.getSelection();
      const range = doc.createRange();
      range.selectNodeContents(el);
      if (!args.clear) range.collapse(false);
      selection.removeAllRanges();
      selection.addRange(range);
      inserted = !!doc.execCommand?.('insertText', false, value);
    } catch (_) {}
    if (!inserted) {
      if (args.clear) el.textContent = value;
      else el.textContent = (el.textContent || '') + value;
    }
    fireInputEvents(el, value);
  }

  const target = findTarget();
  if (!target) return { ok: false, error: '未找到可填写的输入框' };
  target.scrollIntoView?.({ block: 'center', inline: 'nearest', behavior: 'instant' });
  target.focus?.({ preventScroll: true });
  const tag = tagOf(target);
  if (tag === 'SELECT') setSelectValue(target, args.value);
  else if (tag === 'INPUT' || tag === 'TEXTAREA') setControlValue(target, args.value);
  else if (target.isContentEditable || target.getAttribute?.('contenteditable') === 'true' || target.getAttribute?.('role') === 'textbox') setEditableText(target, args.value);
  else if ('value' in target) setControlValue(target, args.value);
  else return { ok: false, error: '目标元素不是可填写控件' };
  return {
    ok: true,
    filled: true,
    tagName: tag.toLowerCase(),
    selector: args.selector || '',
    label: textOf(target).slice(0, 120),
    length: args.value.length,
    preview: args.value.slice(0, 80)
  };
})()`);
    if (!result?.ok) throw new Error(result?.error || '填写失败');
    if (args.waitAfter !== false) await new Promise(r => setTimeout(r, Number(args.waitAfter || 300)));
    return { ...result, tabId: tab.id };
  }

  async function press(args = {}) {
    const tab = await getTargetTab(args.tabId);
    await ensureAttached(tab.id);
    const key = args.key || 'Enter';
    const modifiers = Number(args.modifiers || 0);
    await sendCommand(tab.id, 'Input.dispatchKeyEvent', { type: 'keyDown', key, code: args.code || key, modifiers });
    await sendCommand(tab.id, 'Input.dispatchKeyEvent', { type: 'keyUp', key, code: args.code || key, modifiers });
    return { pressed: key, tabId: tab.id };
  }

  async function waitFor(args = {}) {
    const tab = await getTargetTab(args.tabId);
    const selector = args.selector;
    if (!selector) throw new Error('缺少 selector');
    const timeout = Number(args.timeout || 8000);
    const started = Date.now();
    while (Date.now() - started < timeout) {
      const found = await evaluate(tab.id, `
(() => {
  const el = document.querySelector(${JSON.stringify(selector)});
  if (!el) return false;
  const s = getComputedStyle(el);
  const r = el.getBoundingClientRect();
  return s.display !== 'none' && s.visibility !== 'hidden' && r.width > 1 && r.height > 1;
})()`).catch(() => false);
      if (found) return { found: true, selector, elapsedMs: Date.now() - started, tabId: tab.id };
      await new Promise(r => setTimeout(r, 250));
    }
    return { found: false, selector, elapsedMs: Date.now() - started, tabId: tab.id };
  }

  async function getText(args = {}) {
    const tab = await getTargetTab(args.tabId);
    const selector = args.selector || '';
    const maxLength = Number(args.maxLength || args.max_length || 8000);
    const value = await evaluate(tab.id, `
(() => {
  const el = ${selector ? `document.querySelector(${JSON.stringify(selector)})` : 'document.body'};
  if (!el) return { text: '', error: '元素未找到' };
  return {
    title: document.title,
    url: location.href,
    text: (el.innerText || el.textContent || '').slice(0, ${Math.max(100, maxLength)})
  };
})()`);
    return { tabId: tab.id, ...value };
  }

  async function screenshot(args = {}) {
    const tab = await getTargetTab(args.tabId);
    await ensureAttached(tab.id);
    let params = { format: args.format || 'png', fromSurface: true };
    if (params.format === 'jpeg') params.quality = Number(args.quality || 85);
    if (args.fullPage) {
      const metrics = await sendCommand(tab.id, 'Page.getLayoutMetrics');
      const size = metrics.contentSize;
      params.captureBeyondViewport = true;
      params.clip = { x: 0, y: 0, width: size.width, height: size.height, scale: 1 };
    }
    const result = await sendCommand(tab.id, 'Page.captureScreenshot', params);
    return {
      tabId: tab.id,
      mime: params.format === 'jpeg' ? 'image/jpeg' : 'image/png',
      dataUrl: `data:${params.format === 'jpeg' ? 'image/jpeg' : 'image/png'};base64,${result.data}`,
      bytesBase64: result.data?.length || 0
    };
  }

  async function pdf(args = {}) {
    const tab = await getTargetTab(args.tabId);
    await ensureAttached(tab.id);
    const result = await sendCommand(tab.id, 'Page.printToPDF', {
      printBackground: args.printBackground !== false,
      landscape: !!args.landscape
    });
    return { tabId: tab.id, mime: 'application/pdf', dataBase64: result.data, bytesBase64: result.data?.length || 0 };
  }

  async function networkStart(args = {}) {
    const tab = await getTargetTab(args.tabId);
    await ensureAttached(tab.id);
    const state = getOrCreateNetworkState(tab.id, { ...args, reset: args.reset !== false });
    state.active = true;
    state.startedAt = Date.now();
    state.maxEntries = Number(args.maxEntries || state.maxEntries || 300);
    state.maxBodyChars = Number(args.maxBodyChars || state.maxBodyChars || 12000);
    state.includeBodies = args.includeBodies !== false;
    state.includePostData = args.includePostData !== false;
    state.includeSensitiveHeaders = args.includeSensitiveHeaders === true;
    state.captureAll = args.captureAll === true;
    state.resourceTypes = new Set((args.resourceTypes || args.types || ['XHR', 'Fetch'])
      .map(x => String(x || '').toLowerCase()));
    await sendCommand(tab.id, 'Network.enable', {
      maxTotalBufferSize: Number(args.maxTotalBufferSize || 20 * 1024 * 1024),
      maxResourceBufferSize: Number(args.maxResourceBufferSize || 5 * 1024 * 1024),
      maxPostDataSize: Number(args.maxPostDataSize || 256 * 1024)
    }).catch(() => sendCommand(tab.id, 'Network.enable').catch(() => {}));
    return {
      tabId: tab.id,
      active: true,
      captureAll: state.captureAll,
      resourceTypes: Array.from(state.resourceTypes),
      includeBodies: state.includeBodies,
      maxEntries: state.maxEntries,
      msg: 'Network 捕获已启动。请在页面上执行查询、提交、保存等动作，然后调用 browser.network.get 读取 XHR/fetch 请求。'
    };
  }

  async function networkGet(args = {}) {
    const tab = await getTargetTab(args.tabId);
    await ensureAttached(tab.id);
    return listCapturedEntries(tab.id, args);
  }

  async function networkClear(args = {}) {
    const tab = await getTargetTab(args.tabId);
    const state = getOrCreateNetworkState(tab.id, {});
    const cleared = state.order.length;
    state.entries.clear();
    state.order = [];
    state.startedAt = Date.now();
    return { tabId: tab.id, cleared };
  }

  async function networkStop(args = {}) {
    const tab = await getTargetTab(args.tabId);
    const state = networkCaptures.get(tab.id);
    const summary = listCapturedEntries(tab.id, { limit: Number(args.limit || 20), includeBodies: false, includeCurl: false });
    if (state) state.active = false;
    return { ...summary, active: false, stopped: true };
  }

  async function acceptanceCheck(args = {}) {
    let tab = args.tabId ? await getTargetTab(args.tabId) : null;
    if (args.url) {
      const opened = args.newTab === false && tab
        ? await navigate({ tabId: tab.id, url: args.url, wait: true })
        : await open({ url: args.url, newTab: args.newTab !== false, wait: true });
      tab = await getTargetTab(opened.tabId);
    } else {
      tab = await getTargetTab(args.tabId);
      await ensureAttached(tab.id);
    }

    if (args.startNetwork) {
      await networkStart({
        tabId: tab.id,
        reset: true,
        resourceTypes: args.resourceTypes || args.types || ['XHR', 'Fetch'],
        includeBodies: args.includeBodies !== false,
        includePostData: args.includePostData !== false,
        maxEntries: args.maxEntries || 300
      });
    }

    const waitMs = Math.max(0, Math.min(30000, Number(args.waitMs || args.waitAfter || 0)));
    if (waitMs) await new Promise(resolve => setTimeout(resolve, waitMs));

    const checks = Array.isArray(args.checks) ? args.checks : [];
    const results = [];
    for (const check of checks) {
      const type = check.type || (check.selector ? 'selector' : check.text ? 'text' : check.expression ? 'js' : 'unknown');
      try {
        if (type === 'selector') {
          const selector = check.selector;
          const value = await evaluate(tab.id, `
(() => {
  const el = document.querySelector(${JSON.stringify(selector)});
  if (!el) return { exists: false, visible: false, text: '' };
  const style = getComputedStyle(el);
  const rect = el.getBoundingClientRect();
  return {
    exists: true,
    visible: style.display !== 'none' && style.visibility !== 'hidden' && rect.width > 0 && rect.height > 0,
    text: (el.innerText || el.textContent || '').slice(0, 1000)
  };
})()`);
          const textOk = check.text ? String(value.text || '').includes(String(check.text)) : true;
          const visibleOk = check.visible === false ? value.exists : value.visible;
          results.push({ type, selector, pass: !!value.exists && visibleOk && textOk, value });
        } else if (type === 'text') {
          const value = await getText({ tabId: tab.id, selector: check.selector || '', maxLength: check.maxLength || 12000 });
          const expected = String(check.text || check.includes || '');
          results.push({ type, text: expected, pass: String(value.text || '').includes(expected), value: { title: value.title, url: value.url } });
        } else if (type === 'network') {
          const captured = listCapturedEntries(tab.id, {
            limit: check.limit || 100,
            urlIncludes: check.urlIncludes || check.url || '',
            method: check.method || '',
            resourceTypes: check.resourceTypes || check.types || [],
            includeBodies: check.includeBodies !== false,
            includeCurl: check.includeCurl !== false
          });
          const minCount = Number(check.minCount || 1);
          results.push({ type, pass: captured.count >= minCount, count: captured.count, minCount, requests: captured.requests });
        } else if (type === 'js') {
          const value = await evaluate(tab.id, String(check.expression || 'false'));
          results.push({ type, pass: !!value, value });
        } else {
          results.push({ type, pass: false, error: '未知验收类型' });
        }
      } catch (e) {
        results.push({ type, pass: false, error: e.message || String(e) });
      }
    }

    const screenshotResult = args.screenshot ? await screenshot({ tabId: tab.id, fullPage: !!args.fullPage }).catch(e => ({ error: e.message })) : null;
    const passed = results.filter(item => item.pass).length;
    const failed = results.length - passed;
    return {
      ok: failed === 0,
      tabId: tab.id,
      url: tab.url,
      title: tab.title,
      passed,
      failed,
      checks: results,
      screenshot: screenshotResult
    };
  }

  async function listTabs() {
    const tabs = await chromeCall(chrome.tabs.query, { currentWindow: true });
    return {
      tabs: tabs.map(t => ({
        id: t.id,
        title: (t.title || '').slice(0, 120),
        url: (t.url || '').slice(0, 300),
        active: !!t.active,
        automation: t.id === globalThis._automationTabId,
        cdpAttached: attachedTabs.has(t.id)
      })),
      automationTabId: globalThis._automationTabId || null
    };
  }

  async function close(args = {}) {
    const tab = await getTargetTab(args.tabId);
    await detach(tab.id).catch(() => {});
    await chromeCall(chrome.tabs.remove, tab.id);
    if (globalThis._automationTabId === tab.id) globalThis._automationTabId = null;
    return { closed: true, tabId: tab.id };
  }

  function mapMessage(message = {}) {
    const args = { ...message };
    delete args.type;
    switch (message.type) {
      case 'BROWSER_OPEN': return open(args);
      case 'BROWSER_NAVIGATE': return navigate(args);
      case 'BROWSER_CLICK': return click(args);
      case 'BROWSER_FILL': return fill(args);
      case 'BROWSER_TYPE': return type(args);
      case 'BROWSER_PRESS': return press(args);
      case 'BROWSER_OBSERVE': return observe(args);
      case 'BROWSER_WAIT': return waitFor(args);
      case 'BROWSER_GET_TEXT': return getText(args);
      case 'BROWSER_SCREENSHOT': return screenshot(args);
      case 'BROWSER_PDF': return pdf(args);
      case 'BROWSER_NETWORK_START': return networkStart(args);
      case 'BROWSER_NETWORK_GET': return networkGet(args);
      case 'BROWSER_NETWORK_CLEAR': return networkClear(args);
      case 'BROWSER_NETWORK_STOP': return networkStop(args);
      case 'BROWSER_ACCEPTANCE_CHECK': return acceptanceCheck(args);
      case 'BROWSER_LIST_TABS': return listTabs(args);
      case 'BROWSER_CLOSE': return close(args);
      case 'BROWSER_DETACH': return detach(args.tabId || globalThis._automationTabId);
      default: throw new Error('未知浏览器工具: ' + message.type);
    }
  }

  chrome.tabs?.onRemoved?.addListener((tabId) => {
    attachedTabs.delete(tabId);
    if (globalThis._automationTabId === tabId) globalThis._automationTabId = null;
  });

  globalThis.AiscCdp = {
    open,
    navigate,
    click,
    type,
    fill,
    press,
    observe,
    waitFor,
    getText,
    screenshot,
    pdf,
    networkStart,
    networkGet,
    networkClear,
    networkStop,
    acceptanceCheck,
    listTabs,
    close,
    detach,
    handleMessage: mapMessage
  };
})();
