Page Spec Generator — 页面功能规格说明书生成器 (v3.3)
基于 Browser Harness (CDP 协议) 自动探索任意网站, 生成结构化、多角色可读的完整页面功能规格说明书。
> 📋 版本更新记录、规划路线图、完整代码模板见 reference.md。
适用场景
| 场景 | 说明 |
|---|---|
| 新项目接手 | 快速了解目标网站的全貌 (路由/交互/API) |
| 竞品分析 | 结构化记录竞品功能模块和交互设计 |
| 回归测试基线 | 提供完整的交互元素目录作为测试用例基线 |
| 产品文档化 | 为缺乏文档的遗留系统补全前端规格 |
| 无障碍审计 | 自动检测 landmark/heading/alt-text 缺失 |
前置条件与强制约束
硬性约束
| 约束 | 说明 |
|---|---|
| ✅ 必须使用 Browser Harness | 本 Skill 的所有浏览器操作通过 Browser Harness (CDP 协议) 执行。generate_page_spec(), page_info(), js(), capture_screenshot(), cdp() 等所有 helper 均依赖 Browser Harness |
| ❌ 禁止使用 Playwright | 除非用户明确说"用 Playwright", 否则禁止使用 Playwright/Puppeteer/Selenium 等第三方浏览器驱动。Browser Harness 直接通过 CDP 连接用户正在运行的 Chrome, 继承登录态/扩展/书签, 无需额外配置 |
| 🔧 未安装时询问用户 | 如果 browser-harness --doctor 失败 (command not found), 停止并询问用户是否需要自动安装。禁止未经用户许可自动执行安装命令 |
| 🚫 禁止替用户操作 Chrome 设置 | 当 daemon 连接失败时,引导用户自己打开 chrome://inspect/#remote-debugging 并点击 Allow。Agent 端的浏览器无法控制用户的 Chrome 设置,不要尝试替用户打开该页面 |
安装检查流程
执行 browser-harness --doctor
├── [ok] chrome running + daemon alive → 继续
├── [FAIL] command not found → 询问用户
└── [FAIL] daemon/connection → 按 install.md 排查
Browser Harness 未安装时的处理
如果 browser-harness 命令不可用:
-
告知用户 — 说明 Browser Harness 是本 Skill 的必需依赖:
> "Page Spec Generator 依赖 Browser Harness 进行浏览器控制。检测到你的环境中未安装 Browser Harness。" -
提供安装信息 — 引用官方安装文档:
- 安装指南: https://github.com/browser-use/browser-harness/blob/main/install.md
- 项目说明: https://github.com/browser-use/browser-harness/blob/main/README.md -
询问用户 — 使用
AskUserQuestion工具询问:
> "是否需要我自动安装 Browser Harness?安装命令为:
>bash > git clone https://github.com/browser-use/browser-harness ~/Developer/browser-harness > cd ~/Developer/browser-harness > uv tool install -e . >
> 你也可以手动执行以上命令后重新运行本 Skill。" -
等待用户确认 — 仅在用户明确同意后才执行安装。禁止自动执行。
首次使用验证
在正式开始分析前,先跑 5 行验证脚本确认环境正常:
browser-harness <<'PY'
new_tab("https://www.baidu.com")
print(page_info())
PY
预期输出:
{'url': 'https://www.baidu.com/', 'title': '百度一下,你就知道',
'w': 1536, 'h': 738, ...}
| 验证项 | 通过标志 |
|---|---|
| daemon 启动 + CDP 连接 | page_info() 返回 dict 非报错 |
| 页面导航 | url 字段为目标 URL |
| JS 执行 | title 字段包含中文 |
验证失败对照:
| 错误信息 | 原因 | 解决 |
|---|---|---|
fatal: CDP WS handshake failed |
Chrome 远程调试未授权 | chrome://inspect 重勾 Allow |
FileNotFoundError |
截图目录不存在 | 先 mkdir -p 创建目录 |
Connection refused |
Chrome 未启动 | 启动 Chrome 后重试 |
TimeoutError |
网络不通或页面太慢 | 换 https://www.baidu.com (国内最快) |
UnicodeEncodeError |
Windows GBK 编码 | encoding="utf-8" 写文件 |
Target ... not found |
标签页关闭/刷新/导航到内部页后 session 失效 | 调用 ensure_real_tab() 重新附着到用户最后一个真实页面 |
验证通过后即可开始 Phase 0 全站分析。
其他前置条件
- Chrome 浏览器已启动,两种方式任选其一:
- Way 1 (推荐):
chrome://inspect/#remote-debugging勾选 Allow — 继承日常登录态,适合大多数场景 - Way 2 (无人值守/反自动化站点): 启动 Chrome 时加
--remote-debugging-port=9222 --user-data-dir=<非默认路径>,然后export BU_CDP_URL=http://127.0.0.1:9222。Windows 示例:
bash "C:\Program Files\Google\Chrome\Application\chrome.exe" --remote-debugging-port=9222 --user-data-dir=C:\temp\chrome-dev agent_helpers.py已加载 (含page_*()函数, Browser Harness 安装后自动可用)- (可选)
BROWSER_USE_API_KEY环境变量 — 仅云端浏览器需要
方法论 / 8 阶段流水线
Phase 0 ── 初始扫描 (generate_page_spec)
│
Phase 1 ── 导航爬取 (page_nav_all_links)
│
Phase 2 ── 穷举交互 (逐个点击/输入/切换 + 行为推理 + 弹窗递归 + 缺口标记)
│
Phase 3 ── API 监控 (Performance API + fetch 包装器)
│
Phase 4 ── 状态机测试 (登录/错误/空/加载态 + 会话管理)
│
Phase 5 ── 响应式检查 (多断点 + CDP 仿真)
│
Phase 6 ── 编译文档 (14+ 章节 + 截图规范 + 缺口清单)
分析模式选择
本 Skill 支持两种分析模式, 根据用户意图自动选择:
用户输入 URL + 分析需求
│
├── 用户指定了具体模块 (如"分析酒店的搜索功能")
│ → 🔬 模块深度分析模式 (§M)
│
├── 用户说"全站"/"整个网站"/未指定模块
│ → 🌐 全站分析模式 (§0-7)
│ → 先执行 Phase 0 全站扫描
│ → 展示模块列表
│ → 询问用户: "发现 N 个功能模块, 是否需要针对某个模块深度分析?"
│
└── 用户说"先全站再深入某个模块"
→ 🌐 全站分析 → 产出全景文档
→ 🔬 再对指定模块执行深度分析
| 模式 | 适用场景 | 深度 | 产出章节 |
|---|---|---|---|
| 🌐 全站分析 (§0-7) | 新项目接手、竞品全貌、快速了解 | 广度优先 | 14+ 章 |
| 🔬 模块深度分析 (§M) | 深入某个功能、测试用例基线、分支覆盖 | 深度优先 | 8 章 (每章更密集) |
建议策略: 先用全站模式获得全景图, 再对核心模块逐一深度分析。模块分析结果可合并到全站文档中作为独立章节。
Phase 0 — 站点分类与初始扫描
0.1 站点类型分类
在开始扫描前, 先判断站点类型以调整分析深度:
def classify_site(url):
"""根据 URL 和首页内容推断站点类型。"""
goto_url(url)
time.sleep(3)
title = (js("document.title") or "").lower()
body = (js("document.body.innerText") or "").lower()[:500]
if any(kw in title for kw in ["docs", "documentation", "指南", "api", "reference"]):
return "documentation" # 文档站: 侧重导航/搜索/侧栏目录
if any(kw in title for kw in ["market", "skill", "shop", "store"]):
return "marketplace" # 市场/电商: 侧重分类/筛选/搜索/支付
if any(kw in title for kw in ["community", "forum", "blog", "技术", "开发者"]):
return "community" # 社区: 侧重内容流/评论/认证/广告
if any(kw in title for kw in ["admin", "dashboard", "console", "管理"]):
return "saas" # SaaS: 侧重数据表格/CRUD/权限
return "portal" # 门户: 综合, 按默认深度分析
各类型分析侧重点:
| 类型 | 侧重模块 | 跳过/轻量 |
|---|---|---|
| documentation | 导航层级, 搜索类型(本地/Algolia), 侧栏目录, API 列表 | 广告分析, 登录态 |
| marketplace | 分类页, 筛选/排序, 搜索, 商品列表 | 用户中心, 后台 |
| community | 内容流(无限滚动), 评论, 认证, 广告密度 | 支付流, 后台管理 |
| saas | 数据表格, 表单验证, 权限, CRUD | 首页营销内容 |
| portal | 全量分析, 均衡覆盖 | 无跳过 |
0.2 初始扫描
# 基础扫描
result = generate_page_spec(
"https://target-site.com",
scroll_pause=1.5,
scroll_steps=5,
explore_tabs=True,
deep=True,
)
summary = result.get('summary', {})
spec_path = summary.get('spec_file')
> ⚠️ SPA 兼容性警告: generate_page_spec() 内部使用 CDP Input.dispatchMouseEvent (mouseWheel) 实现滚动。这是 Chromium 已知 Bug (crbug.com/444929150) — SPA / 重 JS 页面 / Docker 环境下 CDP wheel 事件可能超时。Puppeteer #13317 和 Playwright #1115 均有相同问题。
> 降级方案: 如果 generate_page_spec() 超时,跳过滚动,改用 JS 原生滚动 + 手动采集:
> python
> # 检测页面是否 SPA (page_height ≈ viewport_height)
> info = page_info()
> if info.get("ph", 0) <= info.get("h", 900):
> # SPA: 用 window.scrollBy 替代 CDP mouseWheel
> wait_for_network_idle(timeout=15, idle_ms=1500)
> time.sleep(5)
> for _ in range(5):
> js("window.scrollBy(0, 300)")
> time.sleep(1.5)
> js("window.scrollTo(0, 0)")
>
> # 手动执行 generate_page_spec 内部的采集函数
> elements = page_interactive_elements()
> structure = page_semantic_structure()
> forms = page_forms()
> media = page_media()
> # 这些函数不依赖滚动,SPA 上已验证可用
>
0.3 输出文件管理
generate_page_spec() 的输出文件可能位于 temp 目录。必须将其移至项目目录:
import pathlib, shutil
spec_path = summary.get('spec_file')
if spec_path and pathlib.Path(spec_path).exists():
target_dir = pathlib.Path("agent-workspace/domain-skills/page-exploration")
target_dir.mkdir(parents=True, exist_ok=True)
target_file = target_dir / f"page-spec-{hostname}.md"
shutil.copy2(spec_path, target_file)
print(f"Spec relocated: {spec_path} → {target_file}")
Phase 1 — 导航爬取与路由全景
1.1 枚举所有导航链接
nav = json.loads(js("""
(function(){
var items = [];
var seen = new Set();
document.querySelectorAll('a[href]').forEach(function(a){
var text = (a.innerText || a.textContent || '').trim();
if (!text || text.length > 40) return;
var href = a.getAttribute('href') || '';
if (!href || href === '#' || href.startsWith('javascript:')) return;
var key = text + '|' + href;
if (seen.has(key)) return; seen.add(key);
items.push({text: text.slice(0,35), href: href.slice(0,120)});
});
return JSON.stringify(items);
})();
"""))
1.2 URL 归一化
import re
from urllib.parse import urlparse, urljoin
def normalize_url(href, base_domain):
"""归一化 URL: 提取 path 部分, 处理子域名和 CDN。"""
if href.startswith(('mailto:', 'tel:', 'javascript:')):
return None
if href.startswith('/'):
return href.split('?')[0].split('#')[0]
if href.startswith(('http://', 'https://', '//')):
parsed = urlparse(href if '//' in href else 'https:' + href)
host = parsed.hostname or ''
if host.endswith('.' + base_domain) or host == base_domain:
return parsed.path.split('?')[0].split('#')[0] or '/'
else:
return 'external'
if not href.startswith(('http', '//')):
return href.split('?')[0].split('#')[0]
return None
1.3 分类、去重与广告检测
from collections import defaultdict
internal = []
external = []
ad_links = []
ad_keywords = ['ad', 'ads', 'promo', 'sponsor', 'click', 'track', 'utm_', 'banner']
for n in nav:
href = n['href']
path = normalize_url(href, base_domain)
if path is None:
continue
if path == 'external':
is_ad = any(kw in href.lower() for kw in ad_keywords)
if is_ad:
ad_links.append(n)
else:
external.append(n)
else:
n['_path'] = path
internal.append(n)
by_prefix = defaultdict(list)
for n in internal:
p = n['_path']
prefix = p.split('/')[1] if p.count('/') > 1 else 'root'
by_prefix[prefix].append(n)
print(f"Internal: {len(internal)}, External: {len(external)}, Ad/Track: {len(ad_links)}")
1.4 SPA 路由枚举
对于多域名 SPA 架构, 手动枚举子域名:
subdomains = set()
for n in internal:
for match in ['live.', 'search.', 'space.', 'member.', 'show.', 'manga.']:
if match in n['href']:
subdomains.add(match.rstrip('.'))
for sub in subdomains:
generate_page_spec(f"https://{sub}.target.com")
Phase 2 — 深度交互分析
> 核心原则: 页面上每一个可操作元素, 都必须被操作一次, 观察反应, 记录行为。不是"知道有什么元素", 而是"每个元素点了之后会发生什么"。
交互策略
- 枚举所有可交互元素 — 使用综合选择器覆盖: 标准 HTML 元素 + ARIA role + 常见交互 class 模式。跳过不可见元素 (display:none / 0×0 尺寸),去重用 tag+text+class+rect 联合键
- 逐个执行交互 — 每种元素类型用对应操作: button/link→click、input/textarea→输入测试值、select→切换选项、checkbox/radio→toggle、slider→模拟拖动
- 记录操作前后状态 — 截图 + DOM hash + URL 对比,判断效果 (导航/弹窗/DOM变化/无变化)
- 恢复状态 — 弹窗尽量关闭 (Escape+关闭按钮)、toggle 再次点击还原、页面导航用 go_back
- 递归弹窗 — 弹窗/下拉面板内的元素纳入下一轮交互,max_depth=3
> 📝 完整 JS 选择器代码、交互执行器、效果分类函数见 reference.md §Phase 2。
2.2 交互元素目录分类
对每个关键页面, 枚举所有可交互元素 (作为穷举交互的补充信息目录):
def analyze_page_interactions(url):
"""返回页面上所有按钮/输入框/链接/Tab/表单/开关的详细目录。"""
# 穷举交互已经包含了元素发现, 此函数仅作结构化分类供文档引用
goto_url(url)
time.sleep(4)
wait_for_load()
wait_for_network_idle(timeout=12.0, idle_ms=1000)
auth = detect_auth_forms()
catalog = json.loads(js("""
(function(){
var items = {buttons:[], inputs:[], links:[], tabs:[], forms:[], toggles:[]};
var seen = new Set();
document.querySelectorAll('button, [role="button"], [class*="btn"], [class*="button"]').forEach(function(el){
var text = (el.innerText || el.textContent || '').trim().replace(/\\s+/g,' ').slice(0,40);
if (!text || seen.has('btn_'+text)) return; seen.add('btn_'+text);
items.buttons.push({
text: text, tag: el.tagName, cls: (el.className||'').slice(0,60),
disabled: el.hasAttribute('disabled'),
href: (el.getAttribute('href')||'').slice(0,60)
});
});
document.querySelectorAll('input:not([type=hidden]):not([type=submit]), textarea, select').forEach(function(el){
var type = el.getAttribute('type')||el.tagName;
items.inputs.push({
type: type,
isPassword: type === 'password',
placeholder: (el.getAttribute('placeholder')||'').slice(0,40),
name: el.getAttribute('name')||'',
maxlength: el.getAttribute('maxlength')||'',
minlength: el.getAttribute('minlength')||'',
required: el.hasAttribute('required'),
pattern: (el.getAttribute('pattern')||'').slice(0,60),
readonly: el.hasAttribute('readonly')
});
});
document.querySelectorAll('a[href]:not([href="#"]):not([href^="javascript:"])').forEach(function(a){
var text = (a.innerText||a.textContent||'').trim().replace(/\\s+/g,' ').slice(0,50);
var href = a.getAttribute('href')||'';
if (!text || text.length > 40) return;
items.links.push({text: text, href: href.slice(0,100), target: a.getAttribute('target')||''});
});
var tabCandidates = document.querySelectorAll(
'[role="tab"], [class*="tab"]:not(a):not(button):not([class*="table"]), ' +
'button[class*="tab"], a[class*="tab"], ' +
'[class*="sort"], [class*="filter"], [class*="category"]'
);
tabCandidates.forEach(function(el){
var text = (el.innerText||el.textContent||'').trim();
if (!text || text.length > 25) return;
if (/^\\d+$/.test(text.trim()) && parseInt(text) < 100) return;
items.tabs.push({
text: text,
badge: text.match(/\\d+/)?.[0] || '',
active: el.classList.contains('active')||el.getAttribute('aria-selected')==='true'
});
});
document.querySelectorAll('form').forEach(function(f){
items.forms.push({
action: (f.getAttribute('action')||'').slice(0,60),
method: f.getAttribute('method')||'get',
inputs: f.querySelectorAll('input,textarea,select').length,
hasSubmit: !!f.querySelector('button,[type="submit"]'),
hasPassword: !!f.querySelector('input[type="password"]')
});
});
return JSON.stringify(items);
})();
"""))
catalog['_auth'] = auth
return catalog
def detect_auth_forms():
auth = json.loads(js("""
(function(){
var result = {hasLoginForm: false, hasPassword: false, hasCaptcha: false};
var inputs = document.querySelectorAll('input[type="password"]');
if (inputs.length > 0) {
result.hasPassword = true;
result.hasLoginForm = true;
}
if (document.querySelector('img[src*="captcha"], [class*="captcha"], [id*="captcha"]')) {
result.hasCaptcha = true;
}
return JSON.stringify(result);
})();
"""))
return auth
2.3 行为规格描述规则
对每个元素类型, 在文档中记录以下内容:
| 元素类型 | 必记录字段 |
|---|---|
| 按钮 | 文本、CSS class、disabled 状态、href/onclick、点击行为(页面跳转/弹窗/状态切换)、再次点击同一按钮的效果(幂等/取消/无操作) |
| 输入框 | placeholder、type、maxlength、minlength、required、pattern(正则)、readonly、Enter 触发行为、防抖延迟(若有) |
| 链接 | 文本、href、target、所属分类(导航/内容/功能)、是否通过重定向 URL |
| Tab | 文本、active 状态、切换后内容变化、切换后页码重置行为、点击已选中 Tab 的行为 |
| 表单 | action、method、input 数量、submit 按钮、验证规则、隐私协议勾选 |
| 开关/复选 | checked 状态、label、切换效果 |
| 统计徽标 | 是否可点击(display-only vs clickable filter)、点击后与其他筛选的组合规则 |
| 分类筛选 | 单选/多选、取消选中方式、全部清除入口 |
2.4 搜索深度分析
搜索是文档站/市场的核心功能, 需区分两种类型:
类型 A: 本地索引搜索
- 搜索索引在构建时生成, 离线仍可用
- 无网络请求, 纯客户端过滤
- 适用于 SSG 静态文档站
类型 B: 云搜索 (如 Algolia DocSearch)
- 每次查询实时请求云 API
- 离线不可用
- 需收集:
appId,apiKey,indexName,facetFilters,placeholder - 需检查: 快捷键(Ctrl+K/Cmd+K)、键盘导航(↑↓→Enter/Esc)、结果分组、摘要长度、缺失结果链接
搜索行为验证清单
def analyze_search_behavior(url):
"""分析搜索行为: 触发方式、防抖、结果展示、边界条件。"""
# 1. 判断搜索类型: 本地 vs 云
search_type = "local" # 默认假设
algolia_config = json.loads(js("""
(function(){
// 检查 Algolia DocSearch
var ds = window.docsearch || window.__DOCSEARCH__;
if (ds) return JSON.stringify({type:'algolia', config: ds});
// 检查 localStorage 中的 DocSearch 键
var keys = Object.keys(localStorage).filter(k => k.includes('DOCSEARCH'));
if (keys.length > 0) return JSON.stringify({type:'algolia', lsKeys: keys});
return JSON.stringify({type:'local'});
})();
"""))
# 2. 测试触发方式: Enter vs 实时防抖
# 在网络面板打开的情况下, 在搜索框逐字输入, 观察 API 请求
# 若有递增的搜索请求 → 实时防抖; 仅 Enter 时触发 → 手动触发
# 3. 边界条件
return {
"search_type": search_type,
"config": algolia_config,
"gaps": [
"搜索触发方式: Enter-only vs 实时防抖? 需实机验证",
"搜索结果为空时: 显示什么? 引导文案?",
"搜索特殊字符 (<script> 等) 的 XSS 防护表现",
"搜索输入是否有 min/max 字符限制?",
"搜索历史/自动补全是否存在?"
]
}
2.5 行为缺口标记 (Gap Marking)
在编写每个交互元素的行为描述时, 对不确定的交互标记 Gap:
格式: ⚠️ Gap: {描述} — 需实机验证 {具体操作}
示例:
- ⚠️ Gap: 再次点击已选中的 Tab 是否触发重复请求? 还是被前端幂等拦截?
- ⚠️ Gap: 搜索是回车触发还是基于防抖的实时搜索?
- ⚠️ Gap: 分类筛选是否支持 Ctrl+点击多选?
- ⚠️ Gap: 快速切换 Tab 时是否存在竞态条件?
Gap 优先级定义:
| 级别 | 说明 | 行动要求 |
|------|------|----------|
| P0 | 核心行为冲突 (文档断言 vs 实际行为矛盾) | 立即实机验证并修正文档 |
| P1 | 行为未验证但关键 (影响功能完整性理解) | 安排实机验证 |
| P2 | 细节确认 (加载态样式、滚动位置、动画) | 可选验证 |
Phase 3 — API 数据流监控
采集策略
- Performance API 快速扫 —
performance.getEntriesByType('resource')提取 API URL 列表,过滤静态资源。⚠️ 只捕获页面加载后的请求 - 三步深度采集 — (1) 提取真实 URL+查询参数 (2) 浏览器内
fetch()携带 Cookie 获取响应体 (3) 校验错误反向推断 POST 入参格式 - 关键教训:
js()大返回值分步采集、http_get()不带 Cookie 必须用浏览器内 fetch、POST 端点靠 ValidationError 推断参数、每次 fetch 后time.sleep(0.5)防反爬 - 校验错误解读:
"xxx should not be empty"→必填缺失 /"must be one of: A,B,C"→枚举不合法 /"must be a number"→类型错误 /"Request head is null"→需认证
API 文档格式
每个端点文档必须包含: 完整 URL、HTTP 方法、触发时机与行为、调用方模块、认证要求、请求参数表、响应 JSON、响应字段说明表、错误码与解决方法。
> 📝 完整三步法代码、OpenAPI Schema 生成、校验错误推断示例见 reference.md §Phase 3。
Phase 4 — 状态机测试
测试场景
| 场景 | 测试 URL | 检查点 |
|---|---|---|
| 404 路由 | /nonexistent-page |
标题含"404"? 导航保留? 搜索可用? 友好消息? 推广内容? |
| 空搜索 | ?keyword=__empty__ |
0 结果 Tab |
| 未登录受限 | /history (无登录) |
登录拦截 / 空数据 |
| 无效 ID | /resource/INVALID |
错误提示 / 空状态 |
登录状态机
从 JS 源码提取错误码分支: 搜索页面文本中的 "10\d{2}" / "12\d{2}" 模式,常见: 1042(冻结)、1232(异地限流)、1236(验证码)、1068(二次认证)。
会话管理
收集 Cookie 中的认证键 (CASTGC, UserName, AU, access-token 等) 和 localStorage 中的 token/login/auth/session 键。
内容流
检测加载指示器 (spinner/skeleton/load-more),推断批量大小和结束态。
权限矩阵
按未登录→已登录→VIP→超级VIP 四级记录功能差异。
> 📝 完整代码模板 (404检测、登录错误码、会话分析、无限滚动、图片密度) 见 reference.md §Phase 4。
Phase 5 — 响应式断点
| 断点 | 常见行为 |
|---|---|
| 768px | 侧栏切换点: ≥768 可见, <768 汉堡菜单 |
| 1280px | 宽屏大纲/粘性导航生效 |
| 1024px | 列数切换 |
- JS 赋值的
window.innerWidth=在现代 SPA 中不触发真正的 CSS 媒体查询重排 - 推荐用 CDP Emulation:
cdp("Emulation.setDeviceMetricsOverride", width=768, height=1024, deviceScaleFactor=2, mobile=True)
> 📝 完整响应式检查代码见 reference.md §Phase 5。
Phase 6 — 编译文档
截图规范
所有 Phase 中采集的截图统一按以下规范管理:
SHOTS = pathlib.Path("agent-workspace/domain-skills/page-exploration/screenshots-{site}")
SHOTS.mkdir(parents=True, exist_ok=True)
def screenshot_page(name, url):
goto_url(url)
time.sleep(4)
wait_for_load()
wait_for_network_idle(timeout=10.0, idle_ms=1000)
shot_path = str(SHOTS / f"{name}.png")
capture_screenshot(path=shot_path)
return shot_path
截图命名规则: {module}_{state}.png
- home.png — 首页默认态
- search.png — 搜索结果页
- login_modal.png — 登录模态框
- error_404.png — 404 错误页
- empty_state.png — 空状态
7.1 文档结构模板 (14+ 章)
# 页面功能规格说明书 — {站点名称}
- 版本: 3.x (行为规格版)
- 目标站点: {URL}
- **站点类型**: {documentation/marketplace/community/saas/portal}
- **适用对象**: 前端开发 · 后端开发 · 测试/QA · 产品经理 · 部门领导
- **分析基础**: {数据来源描述, 如 "基于 Browser Harness 采集 + Agent 团队行为分析"}
## 目录
1. [功能模块 A](#1-功能模块A)
2. [功能模块 B](#2-功能模块B)
...
N. [全局搜索](#N-全局搜索) (区分本地/云端)
N+1. [用户认证与状态机](#N+1-用户认证)
N+2. [会话管理](#N+2-会话管理)
N+3. [权限边界矩阵](#N+3-权限边界矩阵)
N+4. [输入验证规则汇总](#N+4-输入验证)
N+5. [API 数据流与网络请求](#N+5-api-数据流)
N+6. [键盘快捷键与无障碍](#N+6-键盘快捷键)
N+7. [响应式设计与断点](#N+7-响应式设计)
N+8. [行为缺口清单](#N+8-行为缺口清单)
N+9. [架构总览与技术债评估](#N+9-架构评估)
### 各章节内容规范:
## {N}. 用户认证与状态机
### 状态流转图 (含错误码分支)
状态: logged_out → logging_in → verification_required → logged_in
→ remote_login_verify → sign_page
→ frozen (1042)
→ rate_limited (1232)
→ captcha_required (1236/1237)
迁移条件:
logged_out → 点击登录 → logging_in (模态框弹出)
logging_in → 提交凭据 → API 调用
├── 成功 → logged_in (模态框关闭, 导航更新)
├── 1042 → 显示 "账号已被冻结"
├── 1232 → 显示 "异地登录限制, 今日禁止"
├── 1233 → redirect to /sign (异地验证)
├── 1236/1237 → verification_required (验证码输入)
└── 网络错误 → 显示 "网络异常, 请重试"
### 验证规则表
| 字段 | 类型 | 必填 | pattern | 前端阻断条件 |
|------|------|------|---------|-------------|
| 账号 | text/email | 是 | 无 | 空时按钮 disabled |
| 密码 | password | 是 | 无 | 空时按钮 disabled |
| 隐私协议 | checkbox | 是 | — | 未勾选时阻止提交 |
### 会话管理
| 机制 | 详情 |
|------|------|
| Cookie | CASTGC, UserName, AU, access-token 等 |
| localStorage | loginType, loginSuccess, sourceFrom |
| Token 交换 | POST /v1/login/exchange/ticket |
| 自动登录 | Cookie 自动恢复会话 |
| 过期行为 | API 401 → 清除登录态 → 导航恢复未登录 |
## {N+5}. API 数据流与网络请求 (v3.1 增强)
> **编写原则**: 每个 API 端点必须包含"前端如何调用、何时调用、入参是什么、返回值是什么"四个要素。禁止只列 URL 和用途。参考 §3.5 的三步采集法获取完整数据。
### API 全景表
| # | API 端点 | 方法 | 服务号 | 触发时机 | 认证 |
|---|----------|------|--------|----------|------|
### {API-1 名称} (HTTP 方法)
| 属性 | 详情 |
|------|------|
| **完整 URL** | https://m.{host}/restapi/soa2/{serviceId}/{path} |
| **HTTP 方法** | GET / POST |
| **Content-Type** | application/json (POST) |
| **触发时机** | 页面加载时 / 用户点击 {按钮} 时 / 输入搜索词时 |
| **触发行为** | 具体描述: 用户做了什么动作导致此 API 被调用 |
| **调用方** | 哪个 MFE 模块 / React 组件 / 事件处理函数 |
| **认证要求** | 无需 / 需要 {cookieName} Cookie / 需要 Bearer Token |
| **查询参数** | _fxpcqlniredt (去重), x-traceID (链路追踪) — 自动携带 |
**前端调用方式** (实际 JS 代码):
\`\`\`javascript
fetch('https://m.{host}/restapi/soa2/{id}/{path}', {
method: 'POST',
credentials: 'include',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
// 实际请求参数
})
})
\`\`\`
**请求参数**:
| 参数 | 类型 | 必填 | 说明 |
|------|------|------|------|
| param1 | string | ✅ | 说明 |
| param2 | int | ❌ | 说明 (默认值) |
**响应体** (实际捕获, 截断至 3000 字符):
\`\`\`json
{ "ResponseStatus": {...}, "data": {...} }
\`\`\`
**响应字段说明**:
| 字段 | 类型 | 说明 |
|------|------|------|
| ResponseStatus.Ack | string | "Success" / "Failure" |
| data.xxx | type | 说明 |
**错误码** (从实际响应提取):
| 错误 | 原因 | 解决方案 |
|------|------|----------|
| resultCode: -1, resultMsg: "Cver error" | 缺少客户端版本号 | body 中补充 cver 字段 |
| returnCode: "6", returnMsg: "appId is Empty" | 缺少 appId 参数 | 查询参数中补充 appId= |
### API 通用模式
**请求规范**:
- 所有 API 自动携带 Cookie (\`credentials: 'include'\`)
- 链路追踪: \`_fxpcqlniredt\` + \`x-traceID\` 查询参数
- POST 统一 \`Content-Type: application/json\`
- 平台标识通过 \`head.platform\` / \`head.group\` 区分
**响应规范**:
- 统一信封: \`{ ResponseStatus: { Ack, Errors[], Extension[] } }\`
- 错误分类: \`FrameworkError\` (系统), \`ValidationError\` (校验), \`AuthException\` (认证)
- 时间戳: Microsoft JSON Date (\`/Date(UnixTimestampMs+Offset)/\`)
- 全链路追踪: \`CLOGGING_TRACE_ID\` + \`RootMessageId\` + \`XRequestId\`
## {N+1}. 行为缺口清单
| 编号 | 模块 | 缺口描述 | 优先级 | 涉及章节 |
|------|------|----------|--------|----------|
| GAP-01 | 搜索 | 触发方式不确定: Enter-only vs 实时防抖? | P0 (冲突) | §2 |
| GAP-02 | 筛选 | 是否支持 Ctrl+点击多选? | P1 (关键) | §3 |
| GAP-03 | 分页 | 分页状态是否同步到 URL? 刷新后保持? | P1 (关键) | §2 |
| GAP-04 | 搜索 | XSS 注入 `<script>alert(1)</script>` 表现 | P1 (安全) | §4 |
| GAP-05 | 加载 | 加载指示器具体形态 (骨架屏 vs Spinner) | P2 (细节) | §2 |
7.2 导航流图生成 (v3.1 新增)
在文档开头生成 Mermaid 导航流图, 可视化页面间流转关系:
def generate_navigation_flow(hostname, internal_links):
"""生成 Mermaid 流程图的 Markdown 代码。"""
import json
# 按前缀分组组织节点
groups = {}
for link in internal_links:
path = link.get('_path', '/')
prefix = path.split('/')[1] if path.count('/') > 1 else 'root'
if prefix not in groups:
groups[prefix] = []
groups[prefix].append(link['text'])
mermaid_lines = ["```mermaid", "graph TD"]
seen = set()
# 首页作为起点
mermaid_lines.append(f" Home[\"首页 /\"])")
seen.add('root')
# 为每个分组生成子图
for prefix, links in sorted(groups.items()):
if prefix not in seen:
mermaid_lines.append(f" subgraph {prefix}")
for link_text in links[:8]: # 每分组最多 8 个
safe_text = link_text.replace('"', "'")
mermaid_lines.append(f" {prefix}_{hash(link_text)%1000}[{safe_text}]")
mermaid_lines.append(" end")
seen.add(prefix)
# 连线: Home → 分组
mermaid_lines.append(f" Home --> {prefix}")
mermaid_lines.append("```")
return "\n".join(mermaid_lines)
输出示例:
graph TD
Home["首页 /"]
subgraph usage
usage_42[用量信息]
end
subgraph api_keys
api_keys_7[API Keys]
end
subgraph top_up
top_up_91[充值]
end
Home --> usage
Home --> api_keys
Home --> top_up
对于更复杂的跨页面流程 (如登录 → 首页 → 功能页), 生成全站级导航流图:
def generate_site_flow():
"""生成全站级导航流程 Mermaid 图。"""
return """```mermaid
flowchart TD
A["首页 /"] --> B["登录页 /login"]
A --> C["功能页 /features"]
B --> D{"认证成功?"}
D -->|是| E["工作台 /dashboard"]
D -->|否| B
E --> F["个人设置 /profile"]
E --> G["数据管理 /data"]
E --> H["退出登录"]
H --> A
```"""
流图放置位置: 在文档 ## 1. 功能总览 章节之后立即插入, 作为"页面导航全景图"。
7.3 组件状态模型 (v3.1 新增)
每个交互元素需记录完整状态矩阵, 而非仅默认态:
### {组件名} 状态矩阵
| 状态 | 视觉表现 | 行为 |
|------|----------|------|
| **default** | 正常显示 | 可点击/可交互 |
| **hover** | 背景色变化 / 下划线 | - |
| **focus-visible** | outline / 边框高亮 | 键盘 Tab 可到达 |
| **active/pressed** | 缩放/颜色加深 | 触发动作 |
| **disabled** | 灰色 / 低透明度 | 点击无响应, cursor: not-allowed |
| **loading** | Spinner / 骨架屏 | 用户不可重复点击 |
| **error** | 红色边框 / 错误提示 | 显示验证失败信息 |
| **empty** | 占位符 / 空态插画 | 引导用户操作 |
| **selected** | 高亮 / 勾选图标 | 表示当前选项 |
示例 — 搜索输入框状态矩阵:
## 搜索输入框 (search-input)
| 状态 | 视觉 | 行为 |
|------|------|------|
| default | placeholder="搜索..." | 等待输入 |
| focus | 边框蓝色 1px | 键盘输入, 防抖 300ms 后触发搜索 |
| typing | 显示清除按钮(×) | 实时搜索建议下拉 |
| empty-input | 显示搜索图标 | 搜索按钮 disabled |
| loading | 右侧 Spinner | 请求进行中, 防抖重置 |
| error | 红色边框 + Toast | 网络错误提示 |
| results-shown | 下拉结果面板 | ↑↓ 导航, Enter 选中 |
| no-results | 面板显示"未找到相关结果" | 引导修改关键词 |
记录要求:
- 每个组件类型至少记录 4 种状态 (default / disabled / loading / error)
- 交互式组件 (按钮/输入/切换) 需覆盖所有适用状态
- 状态迁移条件需用自然语言描述
7.4 无障碍深度报告模板 (v3.1 新增)
除基础的 landmark/heading 检测外, 扩展为完整的 WCAG 合规检查:
## 无障碍评估报告
### 1. 键盘可访问性
| 检查项 | 结果 | 严重度 |
|--------|------|--------|
| Tab 顺序是否合理 (visual order = DOM order) | ❌/✅ | 高 |
| 焦点指示器是否可见 (outline 未被 `:focus { outline: none }` 清除) | ❌/✅ | 高 |
| Escape 键是否关闭弹窗/下拉/模态框 | ❌/✅ | 高 |
| 模态框打开时是否焦点陷阱 (Tab 循环在框内) | ❌/✅ | 高 |
| 长页面是否有"跳过导航"链接 (Skip to Content) | ❌/✅ | 中 |
| 自定义组件 (滑条/树/网格) 是否支持箭头键导航 | ❌/✅ | 中 |
### 2. ARIA 使用检查
| 检查项 | 结果 | 严重度 |
|--------|------|--------|
| 动态内容区域使用 aria-live (polite/assertive) | ❌/✅ | 高 |
| aria-expanded 与展开状态同步 | ❌/✅ | 中 |
| aria-current 标记当前导航项 | ❌/✅ | 中 |
| aria-label 覆盖无文字图标的按钮 | ❌/✅ | 高 |
| role="alert" 用于错误消息 | ❌/✅ | 中 |
### 3. 色彩与对比度
| 检查项 | 结果 |
|--------|------|
| 正文文本对比度 ≥ 4.5:1 (AA) / ≥ 7:1 (AAA) | ❌/✅ |
| 大号文本 (≥18px bold / ≥24px) 对比度 ≥ 3:1 | ❌/✅ |
| 非文本元素 (图标/边框) 对比度 ≥ 3:1 | ❌/✅ |
| 焦点指示器对比度 ≥ 3:1 | ❌/✅ |
| 链接颜色与周围文本的区别 (不仅是颜色) | ❌/✅ |
### 4. 屏幕阅读器
| 检查项 | 结果 |
|--------|------|
| 页面标题 (`<title>`) 是否描述当前页面内容 | ❌/✅ |
| 标题层级 (`<h1>-<h6>`) 是否合理嵌套 | ❌/✅ |
| 图片 alt 文本缺失数 | N |
| aria-hidden 是否误用于可交互元素 | ❌/✅ |
| 表单输入是否有关联 `<label>` | ❌/✅ |
| 错误消息是否通过 aria-describedby 关联到输入框 | ❌/✅ |
### 5. 触控目标 (移动端)
| 检查项 | 标准 | 结果 |
|--------|------|------|
| 所有交互目标 ≥ 44×44px (WCAG 2.5.8) | ≥44px | ✅/❌ |
| 相邻目标间距 ≥ 8px | ≥8px | ✅/❌ |
7.5 文档编写原则
- 聚焦行为, 而非结构: 重点写"点击按钮后会发生什么(分支 A/B/C)", 而非"有一个按钮"
- 标记不确定性: 所有推测行为添加
⚠️ Gap:前缀 - 保留矛盾证据: 如果文档 v2.0 断言 A 但新证据指向 B, 标记冲突而非单方面覆盖
- 自然语言叙述: 流程图 + 段落描述, 避免纯表格罗列
- 多角色可读: 前端团队看到技术细节, 产品/测试看到行为分支
7.6 文档质量检查清单
| 检查项 | 标准 |
|---|---|
| 穷举交互覆盖率 | 每个可交互元素都被操作过(点击/输入/切换), 非仅枚举 |
| 按钮规格 | 每个按钮: 文本/class/disabled/click 行为/幂等性/点击效果 |
| 输入框规格 | 每个输入框: 类型/placeholder/maxlength/required/pattern/触发行为/防抖 |
| Tab 筛选 | 列出所有 Tab, 切换后页码重置/滚动位置/幂等性 |
| 搜索类型 | 明确标注本地索引或云端搜索, 提供搜索配置 |
| 导航路由 | 按域名/前缀分组的完整路由表 |
| API 端点 | 按功能域组织 |
| 登录状态机 | 含错误码分支 (如有) |
| 会话管理 | Cookie/localStorage/Token 交换机制 |
| 权限矩阵 | 未登录/已登录/VIP 差异表 |
| 错误/空态 | 至少 4 种错误场景测试 |
| 图片密度 | 超 50 张标记"偏高", 超 100 张标记"严重" |
| 响应式 | 关键断点 (768px/1280px) 行为 |
| 缺口清单 | 所有 ⚠️ Gap 汇总, 标注优先级 |
| 技术债 | 至少 5 项评级 |
模块深度分析模式 (§M) — 分模块逐分支穷举 (v3.2)
当用户指定某个模块(导航标签/功能区域)进行深度分析时,执行此 5 阶段流水线。
触发条件
用户说"分析 XX 模块" / "深入分析 XX" / 指定具体导航标签名称 / 全站 Phase 0 后选择某模块深入。
5 阶段流水线
- M-Phase 0 — 模块发现: 从主导航提取模块列表,展示给用户选择
- M-Phase 1 — 边界映射: 点击导航进入模块 → 枚举子路由/Tab/可交互元素/表单,产出边界 JSON
- M-Phase 2 — 穷举分支测试: 输入框 12 种分支(空值/XSS/Emoji/边界值/粘贴…)、下拉 4 种、日期 6 种(跨月/非法范围/键盘输入)、按钮 5 种(双击/loading)、筛选 7 种(多选组合/0结果)、分页 7 种(末页/越界/URL同步)
- M-Phase 3 — API 分支覆盖: 每个端点测试 200+数据/200+空列表/400参数错误/401未登录/403无权限/500+Timeout 六种响应
- M-Phase 4 — 状态机枚举: default→loading→data_loaded→empty/error→modal_open→submitting→success
- M-Phase 5 — 编译模块文档: 8 章 (概述→路由→分支矩阵→API→状态机→验证规则→错误态→缺口清单)
注意事项
独立运行不依赖全站分析;分支爆炸优先覆盖表单/搜索/筛选;每个分支测试后恢复基准状态;截图按 {element}_{branch}_before/after.png 命名。
> 📝 完整代码模板 (模块发现、边界映射、分支执行器、API 覆盖、状态枚举) 见 reference.md §模块深度分析。
如果用户未明确指定模块名,先执行全站 Phase 0 初始扫描,提取导航标签列表:
def discover_modules(url):
"""从当前页面提取所有可分析的模块列表。"""
goto_url(url)
time.sleep(3)
wait_for_load()
modules = json.loads(js("""
(function(){
var items = [];
var seen = new Set();
// 从主导航提取模块
document.querySelectorAll(
'nav a, [role="navigation"] a, [class*="nav"] a, ' +
'[class*="tab"]:not([class*="table"]), [role="tab"]'
).forEach(function(el){
var text = (el.innerText || el.textContent || '').trim().replace(/\\s+/g,' ');
if (!text || text.length > 25 || seen.has(text)) return;
seen.add(text);
var href = el.getAttribute('href') || '';
items.push({name: text, href: href.slice(0,120)});
});
return JSON.stringify(items.slice(0, 20));
})();
"""))
print("发现以下功能模块:")
for i, m in enumerate(modules):
print(f" [{i+1}] {m['name']} ({m['href']})")
return modules
将模块列表展示给用户,格式:
发现 14 个功能模块:
核心业务:
[1] 酒店 [2] 机票 [3] 火车票
[4] 旅游 [5] 门票·活动 [6] 汽车·船票
[7] 用车
特色功能:
[8] AI行程助手 [9] 攻略·景点 [10] 旅游地图
[11] 全球购 [12] 礼品卡
其他:
[13] 携程金融 [14] 企业商旅
请选择要深度分析的模块 (输入编号或名称, 支持多选):
> 📝 模块深度分析的完整代码模板 (M-Phase 0-5 所有实现) 见 reference.md §模块深度分析。
源码增强插件 (+src v3.3)
当项目目录下有 src/ 或 package.json 时,可选启用 4 个源码插件,对现有 Phase 注入源码级增强。
设计原则: 可选注入、降级容错 (类名→testid→标签→坐标)、emoji 标注来源 (🌐📂⚠️📝)、嵌入而非膨胀、先评估再增强 (覆盖率<30%警告)。
四个插件
- +src:router: grep 路由文件 → 提取路由树 (path/name/component/meta/children/guards) → 与浏览器爬虫交叉验证 → 差异标注
- +src:api: grep API 文件 → 提取函数签名 + 接口定义 (仅展开一层) → 与浏览器调用交叉验证 → 四等级覆盖度报告 (已验证/已定义未触发/需特定状态/无法判断)
- +src:component: CSS 类名 grep 反向映射 (三级降级) → 读取源文件 → 提取 Props/条件分支/事件 → 补充交互分支矩阵
- +src:store: 定位 Pinia/Vuex/Redux/Zustand 文件 → 提取 State/Actions/持久化配置 → 浏览器
window.__STORE__交叉验证
注意事项
覆盖率诚实标注、不阻塞现有流程、多仓库需逐个分析、敏感字段不写入文档、TypeScript 类型仅展开一层。
> 📝 完整源码插件代码 (评估函数、四种插件详细流程、输出标注格式) 见 reference.md §源码增强插件。
常见问题
Q: 页面受登录态影响怎么办?
部分页面 (如 B 站首页 /) 会因登录态不同而行为不同。策略:
1. 分别测试已登录和未登录状态的路由行为
2. 在文档中标注:"当前测试态: 已登录/未登录"
3. 使用 page_auth_ui() 检测登录态
4. 对认证系统的 JS 源码进行静态分析提取错误码
Q: SPA 页面导航链接不完整?
SPA 使用客户端路由, 初始 DOM 可能未包含所有链接。解决方案:
1. 先滚动到底部触发懒加载
2. 使用 page_nav_all_links() (agent_helpers.py 内建) 逐一点击侧栏导航
3. 对每个子路由重新执行 Phase 0-2
Q: 页面有反爬机制?
- CDP 连接: 使用真实 Chrome, 非 headless
- 登录态: 继承用户 Chrome 配置文件中的 cookie
- 频率限制: 在请求间增加
time.sleep(2-5) - 验证码: 如遇验证码, 停止并提示用户手动完成
Q: API 监控捕获不完整?
performance.getEntriesByType('resource') 可能不捕获同域 API 请求。替代方案:
js("""
(function(){
var origFetch = window.fetch;
window.fetch = function(url, opts){
console.log('[API] fetch:', url);
return origFetch.apply(this, arguments);
};
})();
""")
Q: Agent 分析结果不一致怎么办?
不同的 Agent 可能对同一行为有不同的推断。策略:
1. 保留矛盾证据, 标记为 P0 冲突缺口
2. 将冲突作为"行为缺口"记录, 安排实机测试解决
3. 取交集: 所有 Agent 都确认的行为作为确定事实, 有分歧的作为 Gap
Q: 全站分析和模块深度分析如何选择?
- 全站模式: 接手新项目、竞品全貌了解、快速浏览。广度优先,14 章
- 模块模式: 深入某个功能、编写测试用例基线、分支覆盖。深度优先,8 章但每章更密集
- 建议: 先用全站模式获得全景图(1-2 小时),再对核心业务模块逐一深度分析(每个模块 1-3 小时)
Q: 模块深度分析的"分支爆炸"如何控制?
模块内可能有 50+ 可交互元素,每个元素 5-12 个分支,总分支数可能上千。
控制策略:
1. 优先级排序: 表单字段 > 搜索/筛选 > 按钮 > 链接。纯展示类链接降级为仅记录
2. 采样策略: 同类元素(如酒店列表中的 20 个酒店卡片链接)取前 3 个深度测试,其余记录为 Gap
3. 用户指定: 让用户指定最关心的 3-5 个子功能,其余标记为 P2 Gap
4. 时间控制: 单个模块深度分析不超过 3 小时
输出产物
| 产物 | 路径示例 | 说明 |
|---|---|---|
| 主规格文档 | page-spec-{hostname}.md |
完整 14+ 章 Markdown |
| 截图目录 | screenshots-{hostname}/ |
PNG 截图, 文档内联引用 |
| API 响应数据 | {site}_data/api_full_responses.json |
API 完整请求/响应 (v3.1) |
| API 结构数据 | {site}_data/api_response_structures.json |
浏览器内 fetch 获取的响应结构 |
| API 时序数据 | {site}_data/api_timing.json |
API 调用时序 (Performance API) |
| 链接数据 | {site}_data/links.json |
全量链接与分类 |
| 交互元素数据 | {site}_data/elements.json |
交互元素详情 |
| 端点数据 | {site}_data/endpoints.json |
API 端点 + 网络请求 |
| Agent 报告 | {site}_agent_{name}.md |
各 Agent 的完整分析报告 (调试用) |
| OpenAPI Schema | page-spec-{hostname}-openapi.json |
OpenAPI 3.0 规范 (v3.1+) |
| 导航流图 | 内嵌于规格文档 | Mermaid 页面流转图 (v3.1+) |
| 模块规格文档 | module-spec-{module_name}.md |
单模块 8 章深度规格 (v3.2) |
| 模块边界数据 | {site}_data/{module}_boundary.json |
模块内全量可交互元素清单 |
| 分支测试日志 | {site}_data/{module}_branch_log.json |
每个元素×每个分支的测试记录 |
| 模块截图 | screenshots-{module}/ |
按元素+分支命名的详细截图 |
| 源码评估报告 | {site}_data/source-assessment.json |
源码覆盖率、框架识别、置信度评分 (v3.3) |
| 路由对比报告 | {site}_data/router-diff.json |
源码路由树 vs 浏览器爬虫结果 (v3.3) |
| API 覆盖度报告 | {site}_data/api-coverage.json |
四等级覆盖度 (已验证/已定义未触发/需特定状态/无法判断) (v3.3) |
| 组件映射表 | {site}_data/component-map.json |
CSS 类名 → 组件源文件映射 (v3.3) |
| Store 结构数据 | {site}_data/store-snapshot.json |
源码 Store 定义 + 浏览器运行时快照 (v3.3) |
示例输出 (v3.x 实际数据)
| 网站 | 类型 | 文档大小 | Agent 数 | 缺口数 |
|---|---|---|---|---|
| skillhub.cn | 市场 | ~18KB | 1 | 18 |
| cn.vuejs.org | 文档 | ~20KB | 1 | 5 |
| www.csdn.net | 社区 | ~28KB | 2 | 12 |
| deepseek-transactions | SaaS | ~11KB | — | — |
| element.eleme.cn | 文档 | ~23KB | — | — |
| juejin.cn | 社区 | ~22KB | — | — |
| www.bilibili.com | 社区 | ~45KB | — | — |
| www.ctrip.com (全站) | OTA 市场 | ~32KB | — | 21 |
| www.ctrip.com (酒店模块) | OTA 模块 | ~25KB | — | 35+ |
实际使用经验总结
| 挑战 | 解决方法 |
|---|---|
| 文档站无动态 API | 静态站点适合侧重导航和搜索分析 |
| 图片过多导致性能问题 | 在文档中标记图片密度等级, 提出优化建议 |
| 登录模态框包含密码字段 | 识别但不填充, 在文档中标注认证流程和错误码 |
| 404 页面仍渲染完整导航 | 标题检查比 body 文本检查更可靠; 需同时检查友好消息 |
| 外部链接远多于内部链接 | 分离归类为广告/追踪/合作/社交 |
| 响应式 JS 模拟不触发重排 | 标注测试局限性, 建议 CDP 方式 |
| 编译文档时模板与真实数据不符 | 在提示词中明确禁止使用模板, 仅使用收集的数据 |
| 搜索类型误判 | 默认假设后验证: 检查 Algolia 配置 (window.docsearch, localStorage 中的 DOCSEARCH 键) |
| 404 友好消息漏检 | 404 页面可能同时有标题"404"和情感化消息, 不能仅靠 title 判断 |
| 文档冲突管理 | 旧版本断言 vs 新证据矛盾时, 保留双方证据并标记 P0 Gap |
| Agent 输出合并 | 多 Agent 分析的取交集原则: 共同确认的写为事实, 有分歧的写为 Gap |
http_get() 不带 Cookie |
API 返回空响应 — 改用浏览器内 fetch() 自动携带 Cookie |
js() 大返回值为 None |
分步采集: 先提取 URL 列表 (小), 再逐个 fetch 响应体 (每个几 KB) |
| POST 端点无文档 | 空 body 试 → 从 ValidationError 提取必填参数和枚举值 → 修正后重试 |
| API 响应截断 | responseBody.slice(0, 3000) 限制, 长列表数据需分页参数获取 |
| 截图未嵌入文档 | 截图后立即在对应章节用  嵌入 |
| 截图超时导致流程中断 | 截图包裹 try/except,失败降级为文本断言 (js("document.body.innerText")) 继续执行,不抛异常 |
| React/Vue 输入框赋值后状态未更新 | 用原生 HTMLInputElement.prototype.value setter + dispatchEvent(new Event('input')) + dispatchEvent(new Event('change')),勿直接 .value = |
| SPA 页面操作后内容未渲染 | wait_for_load() 后追加 time.sleep(5+) + wait_for_network_idle(idle_ms=1000) + 滚动触达目标区域触发懒加载 |
generate_page_spec() 在 SPA 上超时 |
CDP mouseWheel 是 Chromium 已知 Bug (crbug.com/444929150)。超时时降级为 js("window.scrollBy(0,300)") + 手动 page_interactive_elements() 等采集函数 |
| 截图前目录不存在 → FileNotFoundError | 每次截图前先 pathlib.Path(path).parent.mkdir(parents=True, exist_ok=True) |
| Windows 中文报告/数据文件乱码 | 所有文件写入用 open(path, "w", encoding="utf-8"),禁止依赖系统默认 GBK 编码 |
js() 无法引用 Python 变量 |
js() 代码在 Chrome 页面上下文执行,看不见 Python 变量。需传参时用 f-string 拼接: js(f"el.value = {json.dumps(val)}") |
| CDP 调用失败无自动重试 | CDP 层没有重试机制。任何 capture_screenshot()/js()/click_at_xy() 失败时原文报告给用户,不要自动重试超过 2 次 — 多次失败意味着环境问题而非临时抖动 |
| 分析完成后 daemon 未关闭 | 8 阶段跑完后 daemon 仍在后台持有 CDP 连接。建议任务结束时执行 browser-harness --reload 释放资源(下次需要时自动重启) |
| 标签页意外切换导致命令失效 | 用户手动切标签/页面刷新/跳转 chrome-error:// 后 CDP session 失效 → 调用 ensure_real_tab() 自动恢复到用户最后一个真实页面 |
本 Skill 基于 Browser Harness 页面探索代理流程总结。版本: 3.3 — 源码增强插件(4 个可选 +src 插件: router/api/component/store)、模块深度分析模式(分模块逐分支穷举)、API 深度分析引擎(请求体/响应体完整捕获)、穷举交互引擎(每个元素逐一点击/输入/切换)、导航流图(Mermaid)、组件状态矩阵、无障碍深度报告(WCAG)、OpenAPI 3.0 schema 生成、P0/P1/P2 优化路线图。