Loading...
Loading...
Compare original and translation side by side
.cjs.cjsUser request
│
├─► actionbook search "<site> <intent>"
│ ├─ Results with Health Score ≥ 70% ──► actionbook get "<ID>" ──► use selectors
│ └─ No results / low score ──► Fallback
│
└─► Fallback: actionbook browser open <url>
├─ actionbook browser snapshot (accessibility tree → find selectors)
├─ actionbook browser screenshot (visual confirmation)
└─ manual selector discovery via DOM inspection| Priority | Source | When |
|---|---|---|
| 1 | | Site is indexed, health score ≥ 70% |
| 2 | | Not indexed or selectors outdated |
| 3 | DOM inspection via screenshot + snapshot | Complex SPA / dynamic content |
search + getgetsnapshotscreenshotsnapshotscreenshotUser request
│
├─► actionbook search "<site> <intent>"
│ ├─ Results with Health Score ≥ 70% ──► actionbook get "<ID>" ──► use selectors
│ └─ No results / low score ──► Fallback
│
└─► Fallback: actionbook browser open <url>
├─ actionbook browser snapshot (accessibility tree → find selectors)
├─ actionbook browser screenshot (visual confirmation)
└─ manual selector discovery via DOM inspection| 优先级 | 来源 | 使用场景 |
|---|---|---|
| 1 | | 网站已被索引,健康评分≥70% |
| 2 | | 未被索引或选择器已过时 |
| 3 | 通过截图+snapshot进行DOM检查 | 复杂SPA / 动态内容 |
search + getgetsnapshotscreenshot snapshotscreenshot// Wait for hydration to complete — not just DOMContentLoaded
await page.waitForSelector('[data-item]', { state: 'attached' });
await page.waitForFunction(() => {
const items = document.querySelectorAll('[data-item]');
return items.length > 0 && !document.querySelector('[data-pending]');
});data-reactroot__NEXT_DATA__actionbook browser text "<selector>"// Wait for hydration to complete — not just DOMContentLoaded
await page.waitForSelector('[data-item]', { state: 'attached' });
await page.waitForFunction(() => {
const items = document.querySelectorAll('[data-item]');
return items.length > 0 && !document.querySelector('[data-pending]');
});data-reactroot__NEXT_DATA__actionbook browser text "<selector>"// Scroll-and-collect loop for virtualized lists (scroll container aware)
const allItems = [];
const maxScrolls = 50;
let scrolls = 0;
const container = await page.$('<scroll-container-selector>');
if (!container) throw new Error('Scroll container not found');
let previousTop = await container.evaluate(el => el.scrollTop);
while (scrolls < maxScrolls) {
const items = await page.$$eval('[data-row]', rows =>
rows.map(r => ({ text: r.textContent.trim() }))
);
for (const item of items) {
if (!allItems.find(i => i.text === item.text)) allItems.push(item);
}
await container.evaluate(el => el.scrollBy(0, 600));
await page.waitForTimeout(300);
const currentTop = await container.evaluate(el => el.scrollTop);
if (currentTop === previousTop) break;
previousTop = currentTop;
scrolls += 1;
}overflow: auto/scrolltransform: translateY(...)position: absolute; top: ...px// Scroll-and-collect loop for virtualized lists (scroll container aware)
const allItems = [];
const maxScrolls = 50;
let scrolls = 0;
const container = await page.$('<scroll-container-selector>');
if (!container) throw new Error('Scroll container not found');
let previousTop = await container.evaluate(el => el.scrollTop);
while (scrolls < maxScrolls) {
const items = await page.$$eval('[data-row]', rows =>
rows.map(r => ({ text: r.textContent.trim() }))
);
for (const item of items) {
if (!allItems.find(i => i.text === item.text)) allItems.push(item);
}
await container.evaluate(el => el.scrollBy(0, 600));
await page.waitForTimeout(300);
const currentTop = await container.evaluate(el => el.scrollTop);
if (currentTop === previousTop) break;
previousTop = currentTop;
scrolls += 1;
}overflow: auto/scrolltransform: translateY(...)position: absolute; top: ...px// Scroll to bottom until no new content loads (with no-growth tolerance)
let itemCount = 0;
let noGrowthStreak = 0;
const maxScrolls = 80;
let scrolls = 0;
while (scrolls < maxScrolls && noGrowthStreak < 3) {
await page.evaluate(() => window.scrollTo(0, document.body.scrollHeight));
await page.waitForTimeout(1200);
const newCount = await page.$$eval('.item', els => els.length);
if (newCount > itemCount) {
itemCount = newCount;
noGrowthStreak = 0;
} else {
noGrowthStreak += 1;
}
scrolls += 1;
}// Scroll to bottom until no new content loads (with no-growth tolerance)
let itemCount = 0;
let noGrowthStreak = 0;
const maxScrolls = 80;
let scrolls = 0;
while (scrolls < maxScrolls && noGrowthStreak < 3) {
await page.evaluate(() => window.scrollTo(0, document.body.scrollHeight));
await page.waitForTimeout(1200);
const newCount = await page.$$eval('.item', els => els.length);
if (newCount > itemCount) {
itemCount = newCount;
noGrowthStreak = 0;
} else {
noGrowthStreak += 1;
}
scrolls += 1;
}// Click-through pagination (navigation-aware, SPA-safe)
const allData = [];
const maxPages = 50;
let pageIndex = 0;
while (pageIndex < maxPages) {
const pageData = await page.$$eval('.result-item', items =>
items.map(el => ({ title: el.querySelector('h3')?.textContent?.trim() }))
);
allData.push(...pageData);
const nextBtn = await page.$('a.next-page:not([disabled])');
if (!nextBtn) break;
const previousUrl = page.url();
const previousFirstItem = await page
.$eval('.result-item', el => el.textContent?.trim() || '')
.catch(() => '');
await nextBtn.click();
// Post-click detection only: advance must be caused by this click
const advanced = await Promise.any([
page
.waitForURL(url => url.toString() !== previousUrl, { timeout: 5000 })
.then(() => true),
page
.waitForFunction(
prev => {
const first = document.querySelector('.result-item');
return !!first && (first.textContent || '').trim() !== prev;
},
previousFirstItem,
{ timeout: 5000 }
)
.then(() => true),
]).catch(() => false);
if (!advanced) break;
await page.waitForLoadState('networkidle').catch(() => {});
pageIndex += 1;
}// Click-through pagination (navigation-aware, SPA-safe)
const allData = [];
const maxPages = 50;
let pageIndex = 0;
while (pageIndex < maxPages) {
const pageData = await page.$$eval('.result-item', items =>
items.map(el => ({ title: el.querySelector('h3')?.textContent?.trim() }))
);
allData.push(...pageData);
const nextBtn = await page.$('a.next-page:not([disabled])');
if (!nextBtn) break;
const previousUrl = page.url();
const previousFirstItem = await page
.$eval('.result-item', el => el.textContent?.trim() || '')
.catch(() => '');
await nextBtn.click();
// Post-click detection only: advance must be caused by this click
const advanced = await Promise.any([
page
.waitForURL(url => url.toString() !== previousUrl, { timeout:<[BOS_never_used_51bce0c785ca2f68081bfa7d91973934]>5000 })
.then(() => true),
page
.waitForFunction(
prev => {
const first = document.querySelector('.result-item');
return !!first && (first.textContent || '').trim() !== prev;
},
previousFirstItem,
{ timeout: 5000 }
)
.then(() => true),
]).catch(() => false);
if (!advanced) break;
await page.waitForLoadState('networkidle').catch(() => {});
pageIndex += 1;
}undefinedundefined
Use this routing strictly:
- **Path A (default when `get` is good):** requested fields are covered by `get` selectors and quality is acceptable.
- Start from `get` selectors and move to script draft quickly.
- You may run lightweight mechanism probes (`browser text`, quick scroll checks) before finalizing script strategy.
- **Do not run full fallback (`snapshot` / `screenshot`) before first draft unless probe/sample validation shows mismatch.**
- Field mapping must default to `get` selectors and mark source as `actionbook_get`.
- **Path B (partial / unstable):** `get` exists but required fields are missing, selector resolves 0 elements, or validation fails.
- Run targeted fallback only for failed fields/steps.
- **Path C (no usable coverage):** search/get has no usable result.
- Run full fallback discovery.
严格遵循以下路由规则:
- **路径A(默认,当`get`结果良好时):** 请求的字段已被`get`选择器覆盖,且质量符合要求。
- 从`get`选择器开始,快速生成脚本草稿。
- 在最终确定脚本策略前,可运行轻量级机制探测(`browser text`、快速滚动检查)。
- **除非探测/样本验证显示不匹配,否则在生成第一版草稿前不要运行完整回退方案(`snapshot` / `screenshot`)。**
- 字段映射默认使用`get`选择器,并标记来源为`actionbook_get`。
- **路径B(部分覆盖/不稳定):** `get`结果存在,但所需字段缺失、选择器匹配0个元素或验证失败。
- 仅针对失败的字段/步骤运行定向回退方案。
- **路径C(无可用覆盖):** search/get无可用结果。
- 运行完整回退发现流程。actionbook browser open "<url>"actionbook browser open "<url>" # if not already open
actionbook browser snapshot # focus on failed field/container mappingactionbook browser open "<url>"actionbook browser open "<url>" # 如果尚未打开
actionbook browser snapshot # 聚焦于失败的字段/容器映射
**Path C full fallback (no usable coverage):**
```bash
actionbook browser open "<url>"
actionbook browser snapshot
actionbook browser screenshotundefined
**路径C:完整回退(无可用覆盖):**
```bash
actionbook browser open "<url>"
actionbook browser snapshot
actionbook browser screenshotundefined
Fallback trigger conditions:
- `actionbook get` cannot map all required fields.
- `actionbook get` selectors return empty/unstable values in sample run.
- Runtime behavior conflicts with expected mechanism (e.g., virtualized container, delayed hydration).
回退触发条件:
- `actionbook get`无法映射所有所需字段。
- `actionbook get`选择器返回空值/不稳定值。
- 运行时行为与预期机制冲突(例如,虚拟化容器、延迟水合)。extract_<domain>_<slug>.cjsloadJSON.stringifymaxPagesmaxScrolls// extract_<domain>_<slug>.cjs
// Generated by Actionbook extract skill
// Usage: node extract_<domain>_<slug>.cjs
const { chromium } = require('playwright');
(async () => {
const browser = await chromium.launch();
const page = await browser.newPage();
await page.goto('<URL>', { waitUntil: 'domcontentloaded' });
// -- wait for readiness --
await page.waitForSelector('<container>', { state: 'visible' });
// -- extract --
const data = await page.$$eval('<item-selector>', items =>
items.map(el => ({
// fields mapped from user request
}))
);
// -- output --
const fs = require('fs');
fs.writeFileSync('output.json', JSON.stringify(data, null, 2));
console.log(`Extracted ${data.length} items → output.json`);
await browser.close();
})();extract_<domain>_<slug>.cjsloadJSON.stringifymaxPagesmaxScrolls// extract_<domain>_<slug>.cjs
// Generated by Actionbook extract skill
// Usage: node extract_<domain>_<slug>.cjs
const { chromium } = require('playwright');
(async () => {
const browser = await chromium.launch();
const page = await browser.newPage();
await page.goto('<URL>', { waitUntil: 'domcontentloaded' });
// -- wait for readiness --
await page.waitForSelector('<container>', { state: 'visible' });
// -- extract --
const data = await page.$$eval('<item-selector>', items =>
items.map(el => ({
// fields mapped from user request
}))
);
// -- output --
const fs = require('fs');
fs.writeFileSync('output.json', JSON.stringify(data, null, 2));
console.log(`Extracted ${data.length} items → output.json`);
await browser.close();
})();node extract_<domain>_<slug>.cjs| Check | Pass condition |
|---|---|
| Script exits 0 | No runtime errors |
| Output file exists | Non-empty file written |
| Record count > 0 | At least one item extracted |
| No null/empty fields | Every declared field has a value in ≥ 90% of records |
| Data matches page | Spot-check first and last record against |
node extract_<domain>_<slug>.cjs| 检查项 | 通过条件 |
|---|---|
| 脚本正常退出 | 无运行时错误 |
| 输出文件存在 | 生成非空文件 |
| 记录数>0 | 至少提取到一个条目 |
| 无空值/缺失字段 | 每个声明的字段在≥90%的记录中都有值 |
| 数据与页面匹配 | 抽查第一条和最后一条记录,与 |
.cjs.cjsextract| Artifact | Path | Format |
|---|---|---|
| Playwright script | | Standalone Node.js script using |
| Extracted data | | JSON array of objects (default), CSV, or user-specified |
extract| 成果 | 路径 | 格式 |
|---|---|---|
| Playwright脚本 | | 独立Node.js脚本,使用 |
| 提取的数据 | | JSON对象数组(默认)、CSV或用户指定格式 |
actionbook get| Priority | Type | Reason |
|---|---|---|
| 1 | | Stable, test-oriented, rarely changes |
| 2 | | Accessibility-driven, semantically meaningful |
| 3 | CSS selector | Structural, may break on redesign |
| 4 | XPath | Last resort, most brittle |
actionbook get| 优先级 | 类型 | 原因 |
|---|---|---|
| 1 | | 稳定、面向测试、极少变更 |
| 2 | | 基于无障碍设计、语义明确 |
| 3 | CSS选择器 | 结构化,可能会因网站改版而失效 |
| 4 | XPath | 最后选择,最脆弱 |
| Error | Action |
|---|---|
| Fall back to |
| Selector returns 0 elements | Re-snapshot, compare with screenshot, update selector |
| Script times out | Add longer |
| Partial data (some fields empty) | Check if content is lazy-loaded; add scroll/wait |
| Anti-bot / CAPTCHA | Inform user; suggest running with |
| 错误类型 | 处理措施 |
|---|---|
| 回退到 |
| 选择器匹配0个元素 | 重新生成snapshot,与截图对比,更新选择器 |
| 脚本超时 | 增加 |
| 数据不完整(部分字段为空) | 检查内容是否为懒加载;添加滚动/等待逻辑 |
| 反爬 / CAPTCHA | 通知用户;建议使用 |