# Chromium WebDriver 外部浏览器自动化技能包
<!-- autos.skill.namespace: autos.skills.chromiumWebDriver -->
<!-- autos.skill.description: 基于 chrome.driver（基于 WebDriver/CDP 等接口） 操作 Chrome、Edge 等 Chromium 内核的外部真实浏览器。 -->
<!-- autos.skill.version: 0.1.0 -->
<!-- autos.skill.minAutos: 3.5 -->

## 定位与边界

本技能包面向 **aardio 标准库 `chrome.driver`**：通过 ChromeDriver / EdgeDriver 的 WebDriver HTTP 协议控制真实的外部 Chromium 浏览器。

适用：

- 启动并控制 Edge、Chrome、Supermium 等 `chrome.driver` 支持的 Chromium 内核浏览器。
- 附加到已使用 `--remote-debugging-port` 启动的 Chromium 外部浏览器。
- 用 WebDriver 查询/点击/输入 DOM，切换窗口/标签页，读写 cookie，调用 CDP，抓取页面信息或截图。

不适用：

- 不处理 Firefox、IE、Safari 等非 `chrome.driver` 支持的浏览器。
- 不替代 `web.view`。若任务是创建/控制 aardio 内嵌 WebView2 界面，优先用 `web.view`，不是本技能包。
- 不封装完整 Selenium API。需要细节时直接查 `lookup_library_reference("chrome.driver")` 或源码，继续使用原生 `chrome.driver` 对象。

## 技术来源与对象类型

```aardio
import autos.skills.chromiumWebDriver;
var wd = autos.skills.chromiumWebDriver;
```

- `wd` 是 `autos.skills.chromiumWebDriver` 的短别名。
- 本技能包主库底层导入并调用：`chrome.driver`、`chrome.path`、`process`、`wsock`。
- `chrome.driver` 对象的 `client` 属性是一个 `web.rest.jsonClient` 对象，chrome.driver 的关键技术是基于  web.rest.jsonClient 将 WebDriver 协议的接口端点自动转换为了 aardio 函数。
- `wd.createDriver(opt)` 返回原生 **chromeDriverObject**，等价于 `chrome.driver(...)` 创建的对象，可继续调用 `addArguments`、`setProxy`、`startBrowser`、`attach`、`close` 等原生方法。
- `wd.startBrowser(url,opt)` 返回 `browser,driver`：
  - `browser` 是原生 **chromeDriverSesObject**，可调用 `go`、`waitEle`、`querySelector`、`doScript`、`cdp`、`cookie`、`eachWindow` 等。
  - `driver` 是原生 **chromeDriverObject**，结束时用 `driver.close()` 关闭 driver 服务端。
- `wd.attach(port,opt)` 返回 `browser,driver`，附加到已开启远程调试端口的外部浏览器。
- `wd.launchDebugBrowser(opt)` 用 `process` 启动外部 Chromium，并返回 `{process;debuggerPort;browserPath;userDataDir;arguments;...}`。
- `wd.startAndAttach(url,opt)` 先启动带远程调试端口的外部 Chromium，再用 `chrome.driver.attach` 附加，返回 `browser,driver,launched`。

关键原则：**封装只是入口与脚手架，不是黑盒**。拿到 `browser` / `driver` 后继续按 `chrome.driver` 文档和 WebDriver/CDP 知识操作。



## 首选路线

### 1. 自动启动一个干净浏览器会话

这是最常用、最稳的路线。`chrome.driver` 默认优先使用系统 Edge（Chromium）；如需优先 Chrome，传 `preferChrome=true`。

```aardio
import autos.skills.chromiumWebDriver;
var wd = autos.skills.chromiumWebDriver;

var browser,driverOrErr = wd.startBrowser("https://example.com",{
    startMaximized = true;
    // preferChrome = true;   // 优先 Chrome，否则默认优先 Edge
    // headless = true;       // 无界面模式
    // incognito = true;      // 无痕模式
    // arguments = ["--disable-extensions"];
});
if(!browser) return null,driverOrErr;

var h1 = browser.waitEle("h1",5000);

/*
h1.innerText() 是对 h1.innerText.get() 的封装，
而 h1.innerText 是由 web.res.jsonClient 创建的远程 API 对象（用法与规则参考基类 web.rest.client 库参考与《用 web.rest 快速开发 HTTP 客户端》），h1.innerText.get() 表示向 WebDriver 的 `/session/:sessionId/element/:id/text` 端点发送 GET 请求。
*/
var text = h1 ? h1.innerText();

browser.closeAll(); // 关闭该会话创建的所有浏览器窗口
// 或 browser.close(); 只关闭当前标签页/窗口

driverOrErr.close(); // 关闭 ChromeDriver / EdgeDriver 服务端
return text;
```

### 2. 启动“外部浏览器”并附加

如果用户强调“外部浏览器”，但还没有可附加的实例，推荐由脚手架启动一个带远程调试端口、独立用户数据目录的外部 Chromium，然后附加。

```aardio
import autos.skills.chromiumWebDriver;
var wd = autos.skills.chromiumWebDriver;

var browser,driver,launchedOrErr = wd.startAndAttach("https://example.com",{
    // debuggerPort = 9222; // 不指定则自动选择空闲端口
    // userDataDir = "C:\\Temp\\edge-webdriver-profile"; // 建议使用独立 profile，避免默认 profile 被占用
    startMaximized = true;
});
if(!browser) return null,driver;

var title = browser.getCurrentTitle();

browser.closeAll();
driver.close();
// 如果 browser.closeAll 未能退出由脚手架启动的浏览器，可按需：launchedOrErr.process.terminate();
return title;
```

### 3. 附加到已经手工启动的远程调试浏览器

普通已运行浏览器通常**不能直接附加**。必须是启动时带了 `--remote-debugging-port=端口` 的 Chromium 实例，并且最好使用独立 `--user-data-dir`。

手工启动示意（也可用 `wd.launchDebugBrowser`）：

```text
msedge.exe --remote-debugging-port=9222 --user-data-dir=C:\Temp\edge-debug-profile https://example.com
```

附加：

```aardio
import autos.skills.chromiumWebDriver;
var wd = autos.skills.chromiumWebDriver;

var browser,driverOrErr = wd.attach(9222,{
    // browserPath = "C:\\Program Files (x86)\\Microsoft\\Edge\\Application\\msedge.exe";
    // driverPath = "D:\\tools\\msedgedriver.exe"; // 版本不兼容时可显式指定
});
if(!browser) return null,driverOrErr;

return browser.getCurrentUrl();
```

## DOM、输入与等待

`waitEle` / `querySelector` 使用 CSS 选择器。XPath 可走 `query("xpath"=...)`。

```aardio
var input = browser.waitEle("#search-input",5000);
if(!input) return null,"未找到输入框";

// sendKeys 会把 ENTER 等键名映射为 chrome.driver.KEYS 中的特殊按键。
input.sendKeys("aardio","ENTER");

// 如果你确实要输入普通文本 "ENTER"，用 setValue。
// input.setValue("ChromeDriver","ENTER");

var btn = browser.querySelector("button[type=submit]");
if(btn) btn.click();

// XPath 查询：
var link = browser.query("xpath"=`//a[contains(., "下载")]`);
if(link) link.click();
```

注意：

- `waitEle(css,timeout,interval)` 仅等 CSS 选择器；超时单位毫秒。
- `waitUrl(pattern,timeout)` / `waitTitle(pattern,timeout)` 的 `pattern` 是 aardio 模式匹配，不是 PCRE 正则。
- `querySelectorAll` 返回数组，遍历时第 1 个变量是索引，第 2 个才是元素：

```aardio
for i,ele in browser.querySelectorAll("a") {
    var text = ele.innerText();
}
```

## 执行 JavaScript 与返回 DOM 节点

`browser.doScript(js,...)` 会把 JS 放进匿名函数中执行，aardio 传入的参数用 JS 的 `arguments` 访问。JS 返回 DOM 节点时，`chrome.driver` 会转换为可继续点击/取属性的 `chromeDriverEleObject`。

```aardio
var body = browser.querySelector("body");
var btn = browser.doScript(`
    return arguments[0].querySelector("button");
`,body);

if(btn){
    btn.click();
}

var data = browser.doScript(`
    return {
        title: document.title,
        url: location.href,
        count: document.querySelectorAll("a").length
    };
`);
```

## CDP、cookie、日志、截图

```aardio
// 创建会话时打开浏览器日志/性能日志能力。
var browser,driverOrErr = wd.startBrowser("https://httpbin.org/cookies",{
    desiredCapabilities = wd.loggingCapabilities(true);
});

// 写 cookie。
browser.cookie({
    cookie = { name="GUID"; value="09031171412667840400"; domain="httpbin.org" }
});

// 调用 CDP。Edge / Chrome 会自动使用 ms 或 goog 自定义命令路径。
var ret,err = browser.cdp("Network.getCookies",{ urls = ["https://httpbin.org"] });
var cookies = ret[["value"]];

// 获取可用日志类型 / 性能日志。
var logTypes = browser.se.log.types.get();
var perfLog = browser.se.log(type="performance");

// 截图接口返回 WebDriver 响应对象，value 通常是 base64 PNG。
var shot = browser.screenshot.get();
var pngBase64 = shot[["value"]];
```

## 原生 chrome.driver 入口仍然可用

需要完全自定义时，不必走脚手架：

```aardio
import chrome.driver;

var driver = chrome.driver();
driver.addArguments("--start-maximized");
driver.addArguments("--incognito");
// driver.removeArguments("--enable-automation");
// driver.setProxy(proxyType="manual"; httpProxy="127.0.0.1:12043");

var browser = driver.startBrowser();
browser.go("https://www.aardio.com/zh-cn/docs/");
```

## 常见坑

- **不能附加普通已运行浏览器**：必须提前用 `--remote-debugging-port` 启动。默认用户 profile 已被正常浏览器占用时，应使用独立 `--user-data-dir`。
- **浏览器与 driver 版本要匹配**：`chrome.driver` 会优先找浏览器同目录下的 driver，否则根据浏览器版本自动获取匹配 driver。下载地址失效、网络不可用或版本特殊时，显式传 `driverPath` 或版本号。
- **默认优先 Edge**：`chrome.driver()` / `chrome.path()` 默认优先系统 Edge（Chromium）。如需优先 Chrome，用 `chrome.driver(true)` 或本技能包选项 `preferChrome=true`。
- **启动参数要分开写**：`driver.addArguments("--start-maximized","--incognito")` 或传数组；不要把多个参数拼成一个带空格的大字符串，也不需要自己加引号。
- **`sendKeys` 的键名会被当成特殊按键**：例如 `"ENTER"`。要输入普通文本 `ENTER`，用 `setValue`。
- **`waitEle` 是 CSS 选择器**；XPath 用 `query("xpath"=...)` 或自己轮询。
- **及时清理资源**：自动启动的会话用 `browser.closeAll()` 关窗口，用 `driver.close()` 关 driver 服务端；附加到用户自己的浏览器时不要随意 `process.terminate()`。
- **CDP 调用成功不等于命令业务成功**：`browser.cdp` 第一个返回值可能仍包含 CDP 错误信息，要检查返回表内容。

## 默认工作流

1. 判断任务是内嵌 WebView2 还是外部 Chromium；外部浏览器才加载本技能包。
2. 选择路线：新建自动化会话用 `wd.startBrowser`；需要真实外部进程/远程调试用 `wd.startAndAttach` 或 `wd.attach`。
3. 设置启动参数、代理、headless、日志能力；确认浏览器/driver 版本匹配。
4. 打开网址，优先用 `waitEle` / `waitUrl` 做显式等待，避免固定 `sleep`。
5. 用 DOM API、`doScript`、CDP 完成操作；必要时读取 `lookup_library_reference("chrome.driver")` 查原生接口。
6. 清理：关闭窗口/会话、关闭 driver 服务端；仅对自己启动的浏览器进程考虑终止。
