fix: 优化浏览器skill
This commit is contained in:
parent
7f9e36160e
commit
a95f840d8b
@ -19,6 +19,7 @@ argument-hint: "无需参数,直接调用即可"
|
||||
1. 使用 `web-page` 这个网页内容读取、自动化交互和截图技能,访问 `https://quote.eastmoney.com/stockhotmap/` 这个网页。
|
||||
2. 网页上有这个类名(`topzs`)的 div 元素,展示了`上证指数`、`深证成指`、`创业板指`、`科创综指`、`北证50`的涨跌情况。你需要这些信息的时候,就从这个元素里提取出来。
|
||||
3. 网页上有这个类名(`stockmap`)的 div 元素,展示了各个板块的涨跌情况。你需要这些信息的时候,就从这个元素里提取出来。
|
||||
4. 这个网页是 ajax 动态渲染的,访问后等待 3 秒钟再提取内容。
|
||||
|
||||
## 回复要求
|
||||
|
||||
|
||||
@ -16,7 +16,7 @@ argument-hint: "需要 url;mode 可为 content 或 screenshot;自动化可
|
||||
|
||||
- 用户发来网页链接,并问「这个网页说了什么」「帮我看看这个链接」「总结一下这个页面」。
|
||||
- 用户要求读取网页正文、标题、描述、主要内容或页面中的链接。
|
||||
- 用户要求打开网页后点击按钮/链接、填写输入框、选择下拉框、勾选复选框/单选框、提交表单、等待某个元素出现或滚动页面。
|
||||
- 用户要求打开网页后点击按钮/链接、填写输入框、选择下拉框、勾选复选框/单选框、提交表单、删除某个 DOM 元素、等待某个元素出现或滚动页面。
|
||||
- 用户要求「截图这个网页」「截整个页面」「截当前可视区域」「截页面里某个区域」。
|
||||
- 用户提供 CSS 选择器并要求截取对应元素,例如「截取 `.article` 这一块」。
|
||||
|
||||
@ -114,7 +114,7 @@ argument-hint: "需要 url;mode 可为 content 或 screenshot;自动化可
|
||||
|
||||
- `type`:动作类型,必填。
|
||||
- `selector`:CSS 选择器。多数表单动作必填。
|
||||
- `text`:按可见文本、`aria-label`、`title`、`placeholder` 等匹配元素。`click` 和 `scroll_to` 可用。
|
||||
- `text`:按可见文本、`aria-label`、`title`、`placeholder` 等匹配元素。`click`、`remove` 和 `scroll_to` 可用。
|
||||
- `exact`:文本是否精确匹配,默认 `false`。
|
||||
- `index`:匹配到多个元素时使用第几个,从 `0` 开始,默认 `0`。
|
||||
- `timeout_ms`:当前动作超时时间,默认使用 `action_timeout_ms`。
|
||||
@ -129,6 +129,7 @@ argument-hint: "需要 url;mode 可为 content 或 screenshot;自动化可
|
||||
- `press`:按键。需要 `key`,例如 `Enter`、`Tab`、`Escape`、`Backspace`、`ArrowDown`。
|
||||
- `select`:设置 `<select>` 的值。需要 `selector` 和 `value`。
|
||||
- `check` / `uncheck`:勾选或取消勾选复选框/单选框。需要 `selector`。
|
||||
- `remove`:删除匹配到的 DOM 元素。需要 `selector` 或 `text`,可用 `index` 选择第几个匹配项。
|
||||
- `wait`:固定等待。需要 `ms`。
|
||||
- `wait_for_selector`:等待元素状态。需要 `selector`,可选 `state` 为 `attached`、`visible`、`hidden`、`detached`,默认 `visible`。
|
||||
- `scroll`:按偏移滚动页面。可选 `x`、`y`,默认 `x=0,y=600`。
|
||||
@ -163,6 +164,15 @@ tsx scripts/web_page.ts \
|
||||
--actions '[{"type":"scroll_to","selector":"body"}]'
|
||||
```
|
||||
|
||||
示例:截图前删除页面遮罩或弹窗。
|
||||
|
||||
```bash
|
||||
tsx scripts/web_page.ts \
|
||||
--url 'https://example.com' \
|
||||
--mode screenshot \
|
||||
--actions '[{"type":"remove","selector":".modal-backdrop"},{"type":"remove","text":"Accept cookies"}]'
|
||||
```
|
||||
|
||||
## 执行步骤
|
||||
|
||||
### 读取网页内容
|
||||
@ -179,7 +189,7 @@ tsx scripts/web_page.ts --url 'https://example.com' --mode content
|
||||
|
||||
### 自动化后读取或截图
|
||||
|
||||
1. 当用户要求先点击、填写、提交、滚动或等待页面变化时,构造 `actions`。
|
||||
1. 当用户要求先点击、填写、提交、删除页面元素、滚动或等待页面变化时,构造 `actions`。
|
||||
2. 如果动作会触发页面跳转,给对应动作设置 `wait_for_navigation: true`。
|
||||
3. 如果需要等待异步渲染结果,使用 `wait_for_selector` 或设置更长的 `wait_ms_after`。
|
||||
4. 自动化动作完成后,继续按 `mode=content` 输出页面内容,或按 `mode=screenshot` 输出/发送截图。
|
||||
@ -224,7 +234,7 @@ tsx scripts/web_page.ts --url 'https://example.com' --mode screenshot --screensh
|
||||
- `actions` 必须是 JSON 数组,或包含 `actions` 数组的 JSON 对象。
|
||||
- `actions_file` 必须是可读取的本地 JSON 文件。
|
||||
- `action_timeout_ms` 和单个动作的 `timeout_ms` 必须大于 0。
|
||||
- 自动化动作必须符合对应类型的必填字段要求,例如 `fill` 必须传 `selector` 和 `value`,`click` 必须传 `selector` 或 `text`。
|
||||
- 自动化动作必须符合对应类型的必填字段要求,例如 `fill` 必须传 `selector` 和 `value`,`click` / `remove` 必须传 `selector` 或 `text`。
|
||||
- Chromium 路径优先读取 `CHROME_BIN`,其次读取 `CHROME_PATH`,再回退到常见系统路径。
|
||||
|
||||
## 回复要求
|
||||
|
||||
@ -25,11 +25,11 @@ const ACTION_TYPES = new Set<string>([
|
||||
"select",
|
||||
"check",
|
||||
"uncheck",
|
||||
"remove",
|
||||
"wait",
|
||||
"wait_for_selector",
|
||||
"scroll",
|
||||
"scroll_to",
|
||||
"captcha_check",
|
||||
]);
|
||||
|
||||
type ActionType =
|
||||
@ -40,11 +40,11 @@ type ActionType =
|
||||
| "select"
|
||||
| "check"
|
||||
| "uncheck"
|
||||
| "remove"
|
||||
| "wait"
|
||||
| "wait_for_selector"
|
||||
| "scroll"
|
||||
| "scroll_to"
|
||||
| "captcha_check";
|
||||
| "scroll_to";
|
||||
|
||||
type RawArgs = Record<string, string>;
|
||||
|
||||
@ -83,7 +83,6 @@ interface Params {
|
||||
maxFullHeight: number;
|
||||
actionTimeoutMs: number;
|
||||
actions: NormalizedAction[];
|
||||
captchaStrategy: "detect" | "ignore";
|
||||
output: string;
|
||||
send: "auto" | "true" | "false";
|
||||
}
|
||||
@ -100,11 +99,6 @@ interface ElementPoint {
|
||||
description: string;
|
||||
}
|
||||
|
||||
interface CaptchaResult {
|
||||
found: boolean;
|
||||
signals: string[];
|
||||
}
|
||||
|
||||
interface ExtractedLink {
|
||||
text: string;
|
||||
href: string;
|
||||
@ -308,6 +302,9 @@ function normalizeAction(
|
||||
if (type === "press" && !action.key) {
|
||||
throw new Error(`第 ${index + 1} 个 press action 必须提供 key`);
|
||||
}
|
||||
if (type === "remove" && !action.selector && !action.text) {
|
||||
throw new Error(`第 ${index + 1} 个 remove action 必须提供 selector 或 text`);
|
||||
}
|
||||
if (type === "wait_for_selector" && !action.selector) {
|
||||
throw new Error("wait_for_selector action 必须提供 selector");
|
||||
}
|
||||
@ -396,8 +393,6 @@ function normalizeParams(raw: RawArgs): Params {
|
||||
DEFAULT_ACTION_TIMEOUT_MS,
|
||||
);
|
||||
const actions = parseActions(raw, actionTimeoutMs);
|
||||
const captchaStrategy =
|
||||
raw.captcha_strategy || (actions.length ? "detect" : "ignore");
|
||||
|
||||
if (viewportWidth <= 0 || viewportHeight <= 0) {
|
||||
throw new Error("视口 width 和 height 必须大于 0");
|
||||
@ -417,9 +412,6 @@ function normalizeParams(raw: RawArgs): Params {
|
||||
if (actionTimeoutMs <= 0) {
|
||||
throw new Error("action_timeout_ms 必须大于 0");
|
||||
}
|
||||
if (!["detect", "ignore"].includes(captchaStrategy)) {
|
||||
throw new Error("captcha_strategy 只能是 detect 或 ignore");
|
||||
}
|
||||
|
||||
const send = raw.send || "auto";
|
||||
if (!["auto", "true", "false"].includes(send)) {
|
||||
@ -450,7 +442,6 @@ function normalizeParams(raw: RawArgs): Params {
|
||||
maxFullHeight,
|
||||
actionTimeoutMs,
|
||||
actions,
|
||||
captchaStrategy: captchaStrategy as Params["captchaStrategy"],
|
||||
output: raw.output || "",
|
||||
send: send as Params["send"],
|
||||
};
|
||||
@ -1147,6 +1138,47 @@ async function setChecked(
|
||||
}
|
||||
}
|
||||
|
||||
async function removeElement(
|
||||
client: CdpClient,
|
||||
sessionId: string,
|
||||
action: NormalizedAction,
|
||||
): Promise<void> {
|
||||
const encodedAction = JSON.stringify(action);
|
||||
const ok = await evaluate<boolean>(
|
||||
client,
|
||||
sessionId,
|
||||
`(() => {
|
||||
const action = ${encodedAction};
|
||||
const normalize = (value) => String(value || '').replace(/\s+/g, ' ').trim();
|
||||
const textOf = (el) => normalize([
|
||||
el.innerText,
|
||||
el.value,
|
||||
el.getAttribute('aria-label'),
|
||||
el.getAttribute('title'),
|
||||
el.getAttribute('alt'),
|
||||
el.getAttribute('placeholder'),
|
||||
].filter(Boolean).join(' '));
|
||||
const matchesText = (el) => {
|
||||
if (!action.text) return true;
|
||||
const haystack = textOf(el);
|
||||
return action.exact ? haystack === action.text : haystack.includes(action.text);
|
||||
};
|
||||
const candidates = action.selector
|
||||
? Array.from(document.querySelectorAll(action.selector))
|
||||
: Array.from(document.querySelectorAll('body *'));
|
||||
const matches = candidates.filter((el) => matchesText(el));
|
||||
const el = matches[action.index || 0];
|
||||
if (!el) return false;
|
||||
el.remove();
|
||||
return true;
|
||||
})()`,
|
||||
);
|
||||
if (!ok) {
|
||||
const target = action.selector || `text=${action.text}`;
|
||||
throw new Error(`未找到可删除元素: ${target}`);
|
||||
}
|
||||
}
|
||||
|
||||
async function waitForSelector(
|
||||
client: CdpClient,
|
||||
sessionId: string,
|
||||
@ -1238,53 +1270,6 @@ async function scrollToTarget(
|
||||
}
|
||||
}
|
||||
|
||||
async function detectCaptcha(
|
||||
client: CdpClient,
|
||||
sessionId: string,
|
||||
): Promise<CaptchaResult> {
|
||||
return await evaluate<CaptchaResult>(
|
||||
client,
|
||||
sessionId,
|
||||
`(() => {
|
||||
const signals = [];
|
||||
const add = (value) => {
|
||||
if (value && !signals.includes(value)) signals.push(value);
|
||||
};
|
||||
const text = (document.body && document.body.innerText || '').replace(/\s+/g, ' ').slice(0, 20000);
|
||||
const lowerText = text.toLowerCase();
|
||||
if (/captcha|recaptcha|hcaptcha|turnstile|verify you are human|security check/.test(lowerText)) add('页面文本疑似包含人机验证');
|
||||
if (/验证码|人机验证|安全验证|滑块|拖动滑块|请完成验证|行为验证/.test(text)) add('页面文本疑似包含中文验证码提示');
|
||||
for (const el of Array.from(document.querySelectorAll('iframe, input, div, canvas'))) {
|
||||
const value = [
|
||||
el.getAttribute('src'),
|
||||
el.getAttribute('title'),
|
||||
el.getAttribute('name'),
|
||||
el.getAttribute('id'),
|
||||
el.getAttribute('class'),
|
||||
el.getAttribute('aria-label'),
|
||||
el.getAttribute('data-sitekey'),
|
||||
].filter(Boolean).join(' ').toLowerCase();
|
||||
if (/captcha|recaptcha|hcaptcha|turnstile|geetest|aliyun|tencent|cloudflare/.test(value)) {
|
||||
add('页面元素疑似验证码组件');
|
||||
}
|
||||
}
|
||||
return { found: signals.length > 0, signals };
|
||||
})()`,
|
||||
);
|
||||
}
|
||||
|
||||
async function assertNoCaptcha(
|
||||
client: CdpClient,
|
||||
sessionId: string,
|
||||
): Promise<void> {
|
||||
const result = await detectCaptcha(client, sessionId);
|
||||
if (result && result.found) {
|
||||
throw new Error(
|
||||
`页面疑似出现验证码/人机验证,已停止自动化操作: ${(result.signals || []).join(";") || "未知信号"}。请改用人工验证后的页面、降低访问频率,或设置 captcha_strategy=ignore 仅继续非验证码相关操作。`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
async function runSingleAction(
|
||||
client: CdpClient,
|
||||
sessionId: string,
|
||||
@ -1312,6 +1297,9 @@ async function runSingleAction(
|
||||
case "uncheck":
|
||||
await setChecked(client, sessionId, action, false);
|
||||
return;
|
||||
case "remove":
|
||||
await removeElement(client, sessionId, action);
|
||||
return;
|
||||
case "wait":
|
||||
await wait(action.ms ?? 0);
|
||||
return;
|
||||
@ -1324,9 +1312,6 @@ async function runSingleAction(
|
||||
case "scroll_to":
|
||||
await scrollToTarget(client, sessionId, action);
|
||||
return;
|
||||
case "captcha_check":
|
||||
await assertNoCaptcha(client, sessionId);
|
||||
return;
|
||||
default:
|
||||
throw new Error(`不支持的 action.type: ${String(action.type)}`);
|
||||
}
|
||||
@ -1344,12 +1329,6 @@ async function runActions(
|
||||
for (let index = 0; index < params.actions.length; index += 1) {
|
||||
const action = params.actions[index];
|
||||
try {
|
||||
if (
|
||||
params.captchaStrategy === "detect" &&
|
||||
action.type !== "captcha_check"
|
||||
) {
|
||||
await assertNoCaptcha(client, sessionId);
|
||||
}
|
||||
const loadEvent = action.waitForNavigation
|
||||
? client
|
||||
.waitForEvent("Page.loadEventFired", sessionId, action.timeoutMs)
|
||||
@ -1366,9 +1345,6 @@ async function runActions(
|
||||
if (action.waitMsAfter > 0) {
|
||||
await wait(action.waitMsAfter);
|
||||
}
|
||||
if (params.captchaStrategy === "detect") {
|
||||
await assertNoCaptcha(client, sessionId);
|
||||
}
|
||||
} catch (error) {
|
||||
throw new Error(
|
||||
`第 ${index + 1} 个 action(${action.type}) 执行失败: ${errorMessage(error)}`,
|
||||
|
||||
Loading…
Reference in New Issue
Block a user