diff --git a/Makefile b/Makefile index b8f4e7cc2..ea8d9bdda 100644 --- a/Makefile +++ b/Makefile @@ -211,3 +211,8 @@ webmainbench-assemble: runtime-check .PHONY: test-native-adapters test-native-adapters: node --test tests/test_public_dns.mjs tests/test_dsh_output.mjs + +.PHONY: bump-lexbench +bump-lexbench: + @test -n "$(REV)" || (echo 'REV= is required' >&2; exit 2) + $(PY) scripts/bump_lexbench.py --revision "$(REV)" --uv "$(UV)" diff --git a/README.md b/README.md index 06e25e626..98a3ab284 100644 --- a/README.md +++ b/README.md @@ -193,3 +193,9 @@ If you use the harness in published work, cite this repository and record the ta ``` Cite upstream benchmarks and evaluators separately when using their data or methods; this repository citation does not replace their attribution requirements. + +Update the pinned LexBench evaluator with `make bump-lexbench REV=`. The command verifies upstream main ancestry, regenerates the lock and definition hashes, and prints changed bindings. Historical report certificates retain their original evaluator identities. + +Web Fetch method descriptions, interfaces, account requirements, capability sources, and their English/Chinese text share one registry: `harness/web_fetch/data/report-methods.json`. Register a new product there; executable versions and run parameters come from saved evidence. See [method metadata](harness/web_fetch/README.md#report-method-metadata) for the distinction between registered examples and measured calls. + +For the exact configuration and code steps for a new product or release, see [Adding a method or release](harness/web_fetch/README.md#adding-a-method-or-release). diff --git a/configs/providers.headless.json b/configs/providers.headless.json index 65fc4063d..ef48847f7 100644 --- a/configs/providers.headless.json +++ b/configs/providers.headless.json @@ -46,7 +46,6 @@ "type": "moli_webfetch", "engine": "moli", "label": "Moli release build", - "source_url": "https://github.com/lexmount/moli/releases", "binary": "${MOLI_RELEASE_BINARY}", "expected_reported_version": "${MOLI_RELEASE_REPORTED_VERSION}", "expected_binary_sha256": "${MOLI_RELEASE_BINARY_SHA256}" @@ -55,10 +54,10 @@ "type": "moli_webfetch", "engine": "moli", "label": "Moli candidate build", - "source_url": "https://github.com/lexmount/moli", "binary": "${MOLI_CANDIDATE_BINARY}", "expected_reported_version": "${MOLI_CANDIDATE_REPORTED_VERSION}", - "expected_binary_sha256": "${MOLI_CANDIDATE_BINARY_SHA256}" + "expected_binary_sha256": "${MOLI_CANDIDATE_BINARY_SHA256}", + "release_channel": "candidate" }, "moli-1.1.3": { "type": "headless_markdown", diff --git a/docs/README.zh-CN.md b/docs/README.zh-CN.md index 803a5232a..59323af16 100644 --- a/docs/README.zh-CN.md +++ b/docs/README.zh-CN.md @@ -193,3 +193,9 @@ make ci ``` 使用上游 benchmark 的数据、评估器或方法时,还应分别引用原项目;引用本仓库不能代替上游的署名要求。 + +更新锁定的 LexBench 评分库时,运行 `make bump-lexbench REV=<已合入提交的完整哈希>`。命令会核对提交是否属于上游 main、重新生成锁文件与定义文件摘要,并列出变化。历史报告证书保留当时的评分库标识。 + +Web Fetch 的方法介绍、接口、账号要求、能力来源及中英文文案统一维护在 `harness/web_fetch/data/report-methods.json`。新增产品时修改这份资料;程序版本和运行参数读取保存的证据。登记示例与实测调用的区别见[方法资料说明](../harness/web_fetch/README.md#report-method-metadata)。 + +新增产品或更换版本需要修改哪些配置和代码,见[新增方法或版本步骤](../harness/web_fetch/README.md#adding-a-method-or-release)。 diff --git a/evaluation/lexbench-webfetch.json b/evaluation/lexbench-webfetch.json index 2c32ac205..fcd844a55 100644 --- a/evaluation/lexbench-webfetch.json +++ b/evaluation/lexbench-webfetch.json @@ -2,7 +2,7 @@ "schema_version": 1, "kind": "external_benchmark_binding", "repository": "https://github.com/lexmount/lexbench-webfetch", - "revision": "84c177d97da3b82567d9881e377aa728929cf129", + "revision": "5ac291a592fec849d86bbb791a80579cc7a6c2c6", "ownership": { "benchmark": "Task populations, task model, acceptance scoring, file checks, compiler and frozen definitions", "harness": "Execution, reference lifecycle, evidence and reports" diff --git a/harness/lexbench_pin.py b/harness/lexbench_pin.py new file mode 100644 index 000000000..bec0a4b8d --- /dev/null +++ b/harness/lexbench_pin.py @@ -0,0 +1,149 @@ +"""Maintain the current evaluator pin without rewriting historical evidence.""" +from __future__ import annotations + +import argparse +import copy +import hashlib +import json +import re +import shutil +import subprocess +import tempfile +import tomllib +from pathlib import Path +from urllib.parse import parse_qs, urlsplit + +NAME = "lexbench-webfetch" +HINT = "Run make bump-lexbench REV=." +ROOT = Path(__file__).resolve().parents[1] + + +def project_pin(project: dict) -> tuple[str, str]: + matches = [item for item in project["project"]["dependencies"] + if re.match(r"^lexbench-webfetch\s*@", item)] + if len(matches) != 1: + raise ValueError("Expected one direct LexBench Git dependency. " + HINT) + match = re.fullmatch(r"lexbench-webfetch\s*@\s*git\+(.+)@([0-9a-f]{40})", matches[0]) + if not match: + raise ValueError("LexBench must use a full immutable Git commit. " + HINT) + return match.group(1), match.group(2) + + +def validate_binding(root: Path) -> str: + repository, revision = project_pin(tomllib.loads((root / "pyproject.toml").read_text())) + lock = tomllib.loads((root / "uv.lock").read_text()) + packages = [item for item in lock["package"] if item["name"] == NAME] + binding = json.loads((root / "evaluation/lexbench-webfetch.json").read_text()) + if len(packages) != 1: + raise ValueError("Expected one locked LexBench package. " + HINT) + source = urlsplit(packages[0].get("source", {}).get("git", "")) + locked_repository = source._replace(query="", fragment="").geturl() + if (locked_repository != repository or source.fragment != revision + or parse_qs(source.query).get("rev") != [revision] + or binding["revision"] != revision + or binding["repository"].removesuffix(".git") != repository.removesuffix(".git")): + raise ValueError("LexBench project, lock and definition binding disagree. " + HINT) + return revision + + +def refresh_binding(binding: dict, revision: str, definitions: Path) -> dict: + updated = copy.deepcopy(binding) + updated["revision"] = revision + + def read(relative: str) -> bytes: + path = definitions / relative + if not path.resolve().is_relative_to(definitions.resolve()) or path.is_symlink(): + raise ValueError("Definition path escapes installed package: " + relative) + return path.read_bytes() + + for relative in updated["definition_files"]: + updated["definition_files"][relative] = hashlib.sha256(read(relative)).hexdigest() + for suite in updated["suites"]: + directory = suite["historical_directory"] + for name in suite["files"]: + suite["files"][name] = hashlib.sha256(read(directory + "/" + name)).hexdigest() + tasks = json.loads(read(directory + "/tasks.json")) + suite["dataset_id"] = tasks["id"] + suite["case_count"] = len(tasks["cases"]) + return updated + + +def binding_changes(before: dict, after: dict) -> list[str]: + changes = [] + def visit(old, new, path): + if isinstance(old, dict) and isinstance(new, dict): + for key in sorted(old.keys() | new.keys()): + visit(old.get(key), new.get(key), path + "/" + key) + elif isinstance(old, list) and isinstance(new, list): + for index, (left, right) in enumerate(zip(old, new, strict=True)): + visit(left, right, path + "/" + str(index)) + elif old != new: + changes.append(f"{path}: {old} -> {new}") + visit(before, after, "binding") + return changes + + +def run(command: list[str], cwd: Path) -> str: + return subprocess.run(command, cwd=cwd, check=True, text=True, + stdout=subprocess.PIPE).stdout.strip() + + +def require_merged_revision(repository: str, revision: str, directory: Path) -> None: + if not re.fullmatch(r"[0-9a-f]{40}", revision): + raise ValueError("REV must be a full lowercase Git commit hash") + run(["git", "init", "--quiet", str(directory)], directory.parent) + run(["git", "fetch", "--quiet", repository, "refs/heads/main"], directory) + if subprocess.run(["git", "merge-base", "--is-ancestor", revision, "FETCH_HEAD"], + cwd=directory, capture_output=True).returncode != 0: + raise ValueError("Requested LexBench revision is not in upstream main history") + + +def bump(root: Path, revision: str, *, uv: str = "uv") -> list[str]: + project_path = root / "pyproject.toml" + text = project_path.read_text() + repository, old_revision = project_pin(tomllib.loads(text)) + binding_path = root / "evaluation/lexbench-webfetch.json" + before = json.loads(binding_path.read_text()) + with tempfile.TemporaryDirectory(prefix="browser-eval-dependency-") as temporary: + staging = Path(temporary) + require_merged_revision(repository, revision, staging / "upstream") + project = staging / "project" + project.mkdir() + (project / "evaluation").mkdir() + for name in ("uv.lock", ".python-version", "README.md", "LICENSE"): + shutil.copyfile(root / name, project / name) + old_url = f"git+{repository}@{old_revision}" + if text.count(old_url) != 1: + raise ValueError("Ambiguous dependency declaration") + (project / "pyproject.toml").write_text(text.replace(old_url, f"git+{repository}@{revision}")) + run([uv, "lock", "--project", str(project)], project) + run([uv, "sync", "--frozen", "--no-install-project", "--project", str(project)], project) + definitions = Path(run([ + uv, "run", "--frozen", "--no-sync", "python", "-c", + "from lexbench_webfetch.inputs import definition_path; print(definition_path('evaluation').parent)", + ], project)) + after = refresh_binding(before, revision, definitions) + (project / "evaluation/lexbench-webfetch.json").write_text( + json.dumps(after, ensure_ascii=False, indent=2) + "\n") + validate_binding(project) + # Network, installation and package validation all finish before changing the checkout. + destinations = ("pyproject.toml", "uv.lock", "evaluation/lexbench-webfetch.json") + originals = {name: (root / name).read_bytes() for name in destinations} + try: + for name in destinations: + (root / name).write_bytes((project / name).read_bytes()) + except OSError: + for name, raw in originals.items(): + (root / name).write_bytes(raw) + raise + return binding_changes(before, after) + + +def main(argv=None): + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--revision", required=True) + parser.add_argument("--uv", default="uv") + args = parser.parse_args(argv) + for change in bump(ROOT, args.revision, uv=args.uv): + print(change) + print("Verified current dependency binding. Historical evidence was not changed.") diff --git a/harness/numbers.py b/harness/numbers.py new file mode 100644 index 000000000..cc7c72cc2 --- /dev/null +++ b/harness/numbers.py @@ -0,0 +1,14 @@ +"""Shared validation for finite numeric inputs; booleans are not measurements.""" +import math + + +def finite_number(value): + return isinstance(value, (int, float)) and not isinstance(value, bool) and math.isfinite(value) + + +def positive_number(value): + return finite_number(value) and value > 0 + + +def nonnegative_number(value): + return finite_number(value) and value >= 0 diff --git a/harness/reporting/article_assets.py b/harness/reporting/article_assets.py deleted file mode 100644 index 75e891ff8..000000000 --- a/harness/reporting/article_assets.py +++ /dev/null @@ -1,76 +0,0 @@ -"""Narrow bilingual parity normalization for manifest-bound article images.""" - -import base64 -import hashlib -import json -from pathlib import Path -from urllib.parse import urlsplit - -from lexbench_webfetch.paths import contained_path, safe_identifier - - -class ArticleAssetParity: - def __init__(self, root, manifest): - self.root = Path(root).resolve() - self.figures = {} - seen = set() - for figure in json.loads(Path(manifest).read_text())["figures"]: - slot = safe_identifier(figure["slot"]) - if slot in seen: - raise ValueError("Duplicate article figure slot") - seen.add(slot) - assets = figure["assets"] - if "en" not in assets: - continue - variants = {} - for locale in ("zh", "en"): - asset = assets[locale] - path = contained_path( - self.root, Path("assets/editorial") / asset["file"], reject_symlinks=True - ) - raw = path.read_bytes() - if hashlib.sha256(raw).hexdigest() != asset["sha256"]: - raise ValueError( - "Localized article image differs from manifest: " + asset["file"] - ) - variants[locale] = { - str(path), - "data:image/png;base64," + base64.b64encode(raw).decode("ascii"), - } - self.figures[figure["slot"]] = variants - - def rewrite(self, page, figure, tag, attrs): - """Normalize only declared URLs inside their declared article figure.""" - page = Path(page).resolve() - relative = page.relative_to(self.root).as_posix() - if relative not in { - "guide/index.html", - "guide/article.html", - "en/guide/index.html", - "en/guide/article.html", - }: - return attrs - variants = self.figures.get(figure) - attribute = {"a": "href", "img": "src"}.get(tag) - if not variants or not attribute: - return attrs - - def identify(value): - if not value: - return None - parsed = urlsplit(value) - if not parsed.scheme and not parsed.netloc and not parsed.query and not parsed.fragment: - value = str((page.parent / parsed.path).resolve()) - return next((locale for locale, known in variants.items() if value in known), None) - - values = dict(attrs) - locale = identify(values.get(attribute)) - if locale != ("en" if relative.startswith("en/") else "zh"): - return attrs - alternate = "data-en-" + attribute - valid_alternate = locale == "zh" and identify(values.get(alternate)) == "en" - return [ - (name, "#editorial-asset-" + figure if name == attribute else value) - for name, value in attrs - if not (name == alternate and valid_alternate) - ] diff --git a/harness/web_fetch/README.md b/harness/web_fetch/README.md index e280571e1..72d1ab6cf 100644 --- a/harness/web_fetch/README.md +++ b/harness/web_fetch/README.md @@ -903,6 +903,22 @@ notice. Bilingual release checks require matching routes and values. English repair content must be present in the bound asset set for an English build. See [report languages](../../docs/report-hosting.md#report-languages) for all builders. +WCXB findings live in the external, hash-bound `report-content.json` guide editions. +The home page summarizes measured values and links to that interpretation; it must +not duplicate case-specific findings in Python. Release checks verify the bound +article, current profile populations and participant rows. Optional-feature receipt +paths come from the content binding, and their case must belong to the current +population; the checks do not require a particular outcome. Illustrations and +examples are validated against the supplied article rather than a fixed inventory. + +WCXB display routes use recorded interface metadata and the shared method registry; +registered request examples are labeled separately from measured routes. Missing +route evidence stays unavailable. The builder, renderer and release checker reuse +`wcxb_scoring.paired_latency_methods` to join comparison roles through the profile's +binary hashes and certified identities. Participant names are not role bindings; +a missing or ambiguous binary match is rejected. Version aliases require complete +participant evidence, not another renderer entry. + `wcxb_latency.py` owns paired-call collection, receipt validation and certification. The collector uses the profile's complete qualified population, timeout and binary hashes. It runs two serial rounds, reverses order per task and selects the final @@ -1280,3 +1296,134 @@ there is no separate bind command. `archive` and `plan_path` are `Path` objects for the prepared main archive and parts manifest; `publications` maps each part identifier to its committed publication receipt. This step binds existing uploads; it does not upload attachments. + +## Report method metadata + +`data/report-methods.json` is the shared, packaged catalog for product labels, +method profiles, adapter contracts, interface examples, access requirements, +and capability sources. Keep Chinese and English author text together as +`{"zh": "...", "en": "..."}`. A versioned execution participant resolves to its +recorded engine or the engine in `configs/providers.headless.json`; adding a +release of an existing engine does not require another catalog entry. + +Saved `reported_version` takes precedence over hash-bound implementation +provenance and other recorded version fields. A native engine's four-part CDP +compatibility version is not an executable release. Missing metadata remains +unavailable; an unregistered method retains its outputs and explicitly reports +missing descriptive metadata instead of crashing the report. + +Interface requests in this catalog are labeled examples. Saved public call logs +own measured request fields; frozen run contracts own budgets and method scope. +Do not copy a cohort's task count, timing, or result conclusion into product +metadata. Source verification dates belong to each source entry, not to a global +report date. `checked_version` binds a source to the product version that was +actually checked; use `null` when that identity was not recorded. Reports disclose +missing or mismatching version coverage without changing measured results. + +Each method has one `access` object (`type`, `account`, `configuration`, `links`, +and `env_vars`). Hosted checklist entries may additionally declare `priority` +and `required`. Entries with `status: planned` document account preparation but +are not executable providers and are not added to reports unless measured. +Product documentation URLs belong to the method catalog. Execution participants +may have distinct labels and an explicit `release_channel`; they do not override +the product documentation source. Saved binary hashes and call records own build +provenance. A shared adapter is a transport, not necessarily one product: when +multiple products register an adapter type, the record must name its engine. + +`observed_routes` matches saved metadata to descriptions; `interface_variants` +selects request examples from that same evidence. An adapter's `interface_method` +selects its catalog interface owner. `runtime_parameters` matches recorded policies +to parameter descriptions; optional numeric fields must be positive and finite. +`task_interfaces` describes planned routes and waits. The execution selector +`output_mode_for_case` owns the mode; descriptions do not redefine it. +Measured call routes and waits override those examples, including missing values. +`task_capabilities` describes the selected route boundaries without turning task +success into a product capability claim. Run budgets, delays and retry limits +come from execution manifests, not this catalog. + +Read colocated translations by language and field identity. Keep both languages +in the owning object rather than copying them into legacy translation maps. + +Historical WCXB and WebMainBench entry points remain supported. The legacy +75 + 25 audit reader validates its explicitly selected frozen evidence contract; +new matrices use `uniform_native_local_measurement`. These are not unused code +merely because their contracts describe older evidence. + +### Adding a method or release + +1. For another release of an existing engine, add or select its execution entry in + `configs/providers.headless.json` (or an external provider configuration), bind + its executable and expected identity, and collect new evidence. No new product + description or renderer branch is needed. +2. For a new product, add its bilingual presentation and account requirements to + `data/report-methods.json`. A method may `extends` a named `templates` entry; + dictionaries merge, lists replace, and missing or cyclic parents are rejected. + Register its recorded route descriptions and native output contracts. +3. Configure its executable provider separately. Reusing a transport does not + automatically add a new browser implementation to `providers/headless.py`: + a new engine needs execution support and tests there. Existing supported + adapters can use an external provider configuration without another adapter. + New task modes belong in the execution selector; catalog rules only describe + the resulting route and any task-specific limitations. +4. Run the method registry tests and the public build/check commands against a + synthetic matrix containing the new participant, then run `make ci`. + +Catalog JSON alone supplies presentation, not executable integration. Adapter +`interface_method` chooses a request-example owner; method `adapter_types` +resolves product identity only when unambiguous. Known old participants may +resolve through the execution registry without recorded `engine`; an unknown +participant sharing an ambiguous adapter must record its engine explicitly. + +Execution configuration `source_url` no longer owns documentation links. Move +that value to the product's catalog `source_url`; old external configuration keys +are ignored for report links. Binary hashes and saved call receipts remain the +source of execution provenance. + +Hosted examples describe registered defaults, not universal measured settings: +Tavily `extract_depth` defaults to `advanced`, Exa `text_verbosity` to `full` and +`max_age_hours` to zero; these can be overridden by configuration. Read saved +public request logs for the actual call. Adding a report does not require editing +these defaults. Runtime parameter rules can fill request fields, replace or append +parameter descriptions, and validate allowed values, patterns and finite numbers. + +The `kind` field is a report grouping (native browser or adapter), not a complete +product taxonomy. It does not select transport or credentials. Keep these concerns +in their existing execution and access owners rather than duplicating them. + +An adapter may declare `task_mode_provider_type` when its planned task modes +use an existing execution selector contract under a different transport name. +For example, the Moli WebFetch transport uses the native Markdown task-mode +contract. This mapping does not override recorded output routes or waits. + + +### Native resource projection versions + +New `html_attribute_urls` declarations record `srcset_parser: html-candidates-v1`. +This keeps commas inside URLs, recognizes candidate descriptors and preserves +`data:` URLs. Changing these semantics requires a new parser identity; keep +previous identities reproducible. New calls and offline correction requests +must declare their parser instead of inheriting whichever dependency is installed. + +Historical declarations without a parser can replay `srcset` only when their +bound scoring-source identity is registered in +`harness/web_fetch/data/native-projection-history.json`. Those historical scorers +included the projection implementation in their source hash. The registry maps +each proven source hash to its parser, including versions +that already used the current parser but omitted its declaration. It names +the exact Git revision, projection source path and source SHA-256; it contains +no task IDs, measured outputs or result-dependent exceptions. The retained +comma-splitting behavior is for verifying historical receipts, not new calls. +Unknown identities and conflicting explicit parser declarations fail closed. +URL attributes other than `srcset` do not depend on this parser selection. + +Revision-chain validation uses the correction receipt's scoring-source identity, +not a later rescore's identity. Saved-answer validation uses the record's bound +scoring identity. Both compare the complete reconstructed answer with the saved +answer; neither tries another parser after a mismatch. No historical results, +raw responses, scores or receipts are rewritten by this compatibility reader. + +Before registering another historical identity, recompute its scoring-source +hash from the exact ordered source list in that revision's `scoring.py` and +verify the projection source hash. A matching saved output alone is insufficient. +The source-bound replay and rejection controls are in +`tests/test_native_answer_projection.py`. diff --git a/harness/web_fetch/accounts.py b/harness/web_fetch/accounts.py index 15ffbcdad..2063307e9 100644 --- a/harness/web_fetch/accounts.py +++ b/harness/web_fetch/accounts.py @@ -8,342 +8,15 @@ from typing import Any -ACCOUNT_REGISTRY: list[dict[str, Any]] = [ - { - "id": "lexmount-global", - "provider": "Lexmount Global", - "priority": "A · 核心评测", - "registration_url": "https://browser.lexmount.com/settings/api-keys", - "key_url": "https://browser.lexmount.com/settings/api-keys", - "env_vars": ["LEXMOUNT_API_KEY", "LEXMOUNT_PROJECT_ID"], - "required": True, - "implemented": True, - "free_note": "内测准入;API key 与 Project ID 必须来自同一个 Global 项目。", - "steps": "登录后打开 Settings → API Keys,创建或复制 API key,并复制对应 Project ID;本评测使用 api.lexmount.com。", - }, - { - "id": "firecrawl", - "provider": "Firecrawl", - "priority": "A · 核心评测", - "registration_url": "https://www.firecrawl.dev/signin", - "key_url": "https://www.firecrawl.dev/app/api-keys", - "env_vars": ["FIRECRAWL_API_KEY"], - "required": True, - "implemented": True, - "free_note": "存在无 key 的限流层;可复现评测应使用免费账号。", - "steps": "注册账号,打开 Firecrawl 控制台 API Keys 页面,创建以 fc- 开头的 key。", - }, - { - "id": "lexmount-cn", - "provider": "Lexmount 中国区", - "priority": "A · 核心评测", - "registration_url": "https://browser.lexmount.cn/", - "key_url": "https://browser.lexmount.cn/", - "env_vars": ["LEXMOUNT_CN_API_KEY", "LEXMOUNT_CN_PROJECT_ID"], - "required": True, - "implemented": True, - "free_note": "API key 与 Project ID 必须来自同一个中国区项目。", - "steps": "登录中国区控制台,复制同一项目下的 API key 与 Project ID;本评测使用 api.lexmount.cn。", - }, - { - "id": "tavily", - "provider": "Tavily Extract", - "priority": "B · 已知 URL 抽取", - "registration_url": "https://app.tavily.com", - "key_url": "https://app.tavily.com/home", - "env_vars": ["TAVILY_API_KEY"], - "required": True, - "implemented": True, - "free_note": "官方快速开始说明每月有 1,000 免费 API credits,无需绑卡。", - "steps": "注册或登录 Tavily Platform,从控制台复制 API key。", - }, - { - "id": "exa", - "provider": "Exa Contents", - "priority": "B · 已知 URL 抽取", - "registration_url": "https://dashboard.exa.ai/", - "key_url": "https://dashboard.exa.ai/api-keys", - "env_vars": ["EXA_API_KEY"], - "required": True, - "implemented": True, - "free_note": "需要控制台账号;用量由 credits/预算控制。", - "steps": "注册或登录 Exa 控制台,打开 API Keys,创建 key;如系统要求则设置预算。", - }, - { - "id": "browserbase", - "provider": "Browserbase Fetch", - "priority": "B · 已知 URL 抓取候选", - "registration_url": "https://www.browserbase.com/sign-up", - "key_url": "https://www.browserbase.com/overview", - "env_vars": ["BROWSERBASE_API_KEY", "BROWSERBASE_PROJECT_ID"], - "required": True, - "implemented": False, - "free_note": "官方页面存在契约冲突;打分前必须先以账号内实际接口核验。", - "steps": "注册后创建或选择项目,从 Overview 控制台复制 API key 与 Project ID。", - }, - { - "id": "cloudflare-browser-rendering", - "provider": "Cloudflare Browser Run /markdown、/scrape 与 Workers AI /ai/tomarkdown", - "priority": "B · 已知 URL 抽取", - "registration_url": "https://dash.cloudflare.com/sign-up", - "key_url": "https://dash.cloudflare.com/profile/api-tokens", - "env_vars": ["CLOUDFLARE_API_TOKEN", "CLOUDFLARE_ACCOUNT_ID"], - "required": True, - "implemented": True, - "free_note": "Browser Run 可用 Workers Free 额度。Browser Run 需要 Browser Rendering Edit;文件转换 API 接受 Workers AI Read 或 Edit。两类产品的配额和计费分开。", - "steps": "注册 Cloudflare,复制 Account ID;创建覆盖 Account → Browser Rendering → Edit 与 Account → Workers AI → Read 的自定义 token。官方 Workers AI REST 入门建议 Read 和 Edit 都授予;运行前用 GET /client/v4/user/tokens/verify 验证。", - }, - { - "id": "bocha-web-search", - "provider": "Bocha Web Search", - "priority": "D · 查询到搜索结果", - "registration_url": "https://open.bochaai.com/", - "key_url": "https://open.bochaai.com/", - "env_vars": ["BOCHA_API_KEY"], - "required": True, - "implemented": False, - "free_note": "搜索/摘要产品,不是任意已知 URL 抽取;应使用独立查询数据集。", - "steps": "在博查开放平台注册,在控制台创建 API key,并填入 BOCHA_API_KEY。", - }, - { - "id": "baidu-ai-search", - "provider": "Baidu Qianfan AI Search", - "priority": "D · 查询到搜索结果", - "registration_url": "https://console.bce.baidu.com/qianfan/ais/console/apiKey", - "key_url": "https://console.bce.baidu.com/qianfan/ais/console/apiKey", - "env_vars": ["QIANFAN_API_KEY"], - "required": True, - "implemented": False, - "free_note": "Web Search 按查询返回摘要、URL 与片段,不是任意 URL 抓取 API。", - "steps": "注册并完成百度智能云认证,打开千帆 API Key 管理并创建或复制 key。", - }, - { - "id": "tencent-wsa", - "provider": "Tencent Cloud Web Search API", - "priority": "D · 查询到搜索结果", - "registration_url": "https://console.cloud.tencent.com/wsa", - "key_url": "https://console.cloud.tencent.com/wsa", - "env_vars": ["TENCENT_WSA_API_KEY"], - "required": True, - "implemented": False, - "free_note": "返回标题、片段与来源 URL 的搜索 API,应与已知 URL 抽取分开评测。", - "steps": "注册并完成认证,开通 Web Search API,再在控制台创建服务 API key。", - }, - { - "id": "aliyun-model-studio", - "provider": "Alibaba Cloud Model Studio web_search + web_extractor", - "priority": "E · 模型绑定检索", - "registration_url": "https://bailian.console.aliyun.com/", - "key_url": "https://bailian.console.aliyun.com/?tab=model#/api-key", - "env_vars": ["DASHSCOPE_API_KEY"], - "required": True, - "implemented": False, - "free_note": "web_extractor 当前随 web_search 出现在模型响应中;模型 token 与工具调用都计入成本。", - "steps": "注册并完成阿里云认证,开通百炼 Model Studio,在 API Key 管理创建 key。", - }, - { - "id": "gemini-url-context", - "provider": "Google Gemini URL Context", - "priority": "E · 模型绑定检索", - "registration_url": "https://aistudio.google.com/", - "key_url": "https://aistudio.google.com/apikey", - "env_vars": ["GEMINI_API_KEY"], - "required": True, - "implemented": False, - "free_note": "模型把检索到的 URL 内容作为输入 token,返回答案/引用,而不是独立原始 Markdown。", - "steps": "登录 Google AI Studio,创建 API key 并填入 GEMINI_API_KEY。", - }, - { - "id": "brightdata", - "provider": "Bright Data Web Unlocker", - "priority": "C · 解锁场景数据集", - "registration_url": "https://brightdata.com/cp/start", - "key_url": "https://brightdata.com/cp/zones", - "env_vars": ["BRIGHTDATA_API_TOKEN", "BRIGHTDATA_UNLOCKER_ZONE"], - "required": True, - "implemented": False, - "free_note": "需要账号、计费或试用资格,以及 Web Unlocker zone。", - "steps": "注册账号,创建 Web Unlocker zone,再复制 API token 与 zone 名称。", - }, - { - "id": "scrapingbee", - "provider": "ScrapingBee", - "priority": "C · 抓取 API 对比", - "registration_url": "https://app.scrapingbee.com/account/register", - "key_url": "https://app.scrapingbee.com/account/manage/api_key", - "env_vars": ["SCRAPINGBEE_API_KEY"], - "required": True, - "implemented": False, - "free_note": "受试用/账户限额约束;生产套餐有月度最低消费。", - "steps": "注册后打开 Account → API Key,复制 key。", - }, - { - "id": "zyte", - "provider": "Zyte API", - "priority": "C · 抓取 API 对比", - "registration_url": "https://app.zyte.com/", - "key_url": "https://app.zyte.com/", - "env_vars": ["ZYTE_API_KEY"], - "required": True, - "implemented": False, - "free_note": "官方文档说明首月有 5 美元初始 credit。", - "steps": "注册后在控制台打开 Zyte API,创建或复制 API key。", - }, - { - "id": "zenrows", - "provider": "ZenRows", - "priority": "C · 抓取 API 对比", - "registration_url": "https://app.zenrows.com/register", - "key_url": "https://app.zenrows.com/builder", - "env_vars": ["ZENROWS_API_KEY"], - "required": True, - "implemented": False, - "free_note": "受账号/试用限额约束。", - "steps": "注册并复制控制台或 request builder 中显示的 API key。", - }, - { - "id": "scrapfly", - "provider": "Scrapfly", - "priority": "C · 抓取 API 对比", - "registration_url": "https://scrapfly.io/register", - "key_url": "https://scrapfly.io/dashboard", - "env_vars": ["SCRAPFLY_API_KEY"], - "required": True, - "implemented": False, - "free_note": "受账号/试用限额约束。", - "steps": "注册后打开控制台,复制项目 API key。", - }, - { - "id": "oxylabs", - "provider": "Oxylabs Web Scraper API", - "priority": "C · 抓取 API 对比", - "registration_url": "https://dashboard.oxylabs.io/en/registration", - "key_url": "https://dashboard.oxylabs.io/", - "env_vars": ["OXYLABS_USERNAME", "OXYLABS_PASSWORD"], - "required": True, - "implemented": False, - "free_note": "所选产品可能需要试用或销售审批。", - "steps": "注册或申请试用,开通 Web Scraper API 并复制用户凭据。", - }, -] +from .methods import report_method_catalog -REPORT_PROVIDER_ACCOUNT_IDS = { - "cloudflare-markdown": "cloudflare-browser-rendering", - "exa": "exa", - "firecrawl": "firecrawl", - "lexmount-cn": "lexmount-cn", - "lexmount-global": "lexmount-global", - "tavily": "tavily", -} -REPORT_LOCAL_ACCESS: dict[str, dict[str, Any]] = { - "fixture": { - "provider": "Fixture 测试适配器", - "registration_url": "https://github.com/lexmount/browser-eval", - "key_url": "https://github.com/lexmount/browser-eval", - "env_vars": [], - "steps": "仓库内测试适配器;只用于确定性回归测试。", - "free_note": "不需要账号或环境变量。", - }, - "moli": { - "provider": "Moli 1.1.5", - "registration_url": "https://github.com/lexmount/moli/releases/tag/v1.1.5", - "key_url": "https://github.com/lexmount/moli/releases/tag/v1.1.5", - "env_vars": ["MOLI_BINARY"], - "steps": "安装并固定官方 v1.1.5 二进制;不需要供应商账号或 API Key。", - "free_note": "本地 CLI。", - }, - "moli-1.1.7": { - "provider": "Moli 1.1.7", - "registration_url": "https://github.com/lexmount/moli/releases/tag/v1.1.7", - "key_url": "https://github.com/lexmount/moli/releases/tag/v1.1.7", - "env_vars": ["MOLI_1_1_7_BINARY"], - "steps": "安装并固定官方 v1.1.7 二进制;不需要供应商账号或 API Key。", - "free_note": "本地 CLI。", - }, - "moli-1.1.11": { - "provider": "Moli 1.1.11", - "registration_url": "https://github.com/lexmount/moli/releases/tag/v1.1.11", - "key_url": "https://github.com/lexmount/moli/releases/tag/v1.1.11", - "env_vars": ["MOLI_1_1_11_BINARY"], - "steps": "安装并固定官方 v1.1.11 二进制;不需要供应商账号或 API Key。", - "free_note": "本地 CLI。", - }, - "moli-webfetch": { - "provider": "Moli WebFetch", - "registration_url": "https://github.com/lexmount/moli/tree/main/skills/moli-webfetch", - "key_url": "https://github.com/lexmount/moli/releases", - "env_vars": ["MOLI_BINARY"], - "steps": "安装并固定本轮 Moli 可执行文件;不需要供应商账号或 API Key。", - "free_note": "本地 CLI;二进制路径和版本属于运行证据。", - }, - "cowagent-built-in": { - "provider": "CowAgent 内置 Web Fetch", - "registration_url": "https://github.com/zhayujie/CowAgent", - "key_url": "https://github.com/zhayujie/CowAgent", - "env_vars": ["COWAGENT_REPO", "COWAGENT_BENCHMARK_WORKSPACE"], - "steps": "检出并固定 CowAgent 源码,配置仓库路径和隔离工作目录;不需要供应商 API Key。", - "free_note": "本地项目内置实现。", - }, - "openclaw-built-in": { - "provider": "OpenClaw 内置 Web Fetch", - "registration_url": "https://github.com/openclaw/openclaw", - "key_url": "https://github.com/openclaw/openclaw/blob/main/docs/tools/web-fetch.md", - "env_vars": ["OPENCLAW_REPO", "OPENCLAW_NODE", "OPENCLAW_PNPM"], - "steps": "检出并固定 OpenClaw 源码、Node 与 pnpm;本轮关闭 Firecrawl 回退。", - "free_note": "本地项目内置实现;不需要云端抓取 Key。", - }, - "deepseek-harness-built-in": { - "provider": "DeepSeek Harness 内置 Web Fetch", - "registration_url": "https://github.com/deepseek-ai/deepseek-harness", - "key_url": "https://github.com/deepseek-ai/deepseek-harness/tree/master/packages/web", - "env_vars": ["DEEPSEEK_HARNESS_REPO", "OPENCLAW_NODE", "DEEPSEEK_PNPM"], - "steps": "检出并固定 DeepSeek Harness 源码、Node、pnpm 与构建依赖;本轮不调用聊天模型。", - "free_note": "本地项目内置实现;不需要模型 API Key。", - }, - "langchain-webbase": { - "provider": "LangChain WebBaseLoader", - "registration_url": "https://reference.langchain.com/python/langchain-community/document_loaders/web_base/WebBaseLoader", - "key_url": "https://python.langchain.com/docs/how_to/document_loader_web/", - "env_vars": ["OSS_PYTHON"], - "steps": "使用仓库冻结的 Python 环境运行;OSS_PYTHON 只在显式覆盖 worker 解释器时设置。", - "free_note": "本地开源库;不需要供应商账号。", - }, - "crawl4ai": { - "provider": "Crawl4AI", - "registration_url": "https://github.com/unclecode/crawl4ai", - "key_url": "https://docs.crawl4ai.com/core/installation/", - "env_vars": ["OSS_PYTHON"], - "steps": "使用仓库冻结的 Python 环境及匹配的浏览器运行;OSS_PYTHON 只在显式覆盖 worker 解释器时设置。", - "free_note": "本地开源库;不需要供应商账号。", - }, -} - - -def report_access_registry( - providers: set[str], *, fixture_providers: set[str] | None = None -) -> list[dict[str, Any]]: - """Return access requirements only for methods measured in one report.""" - account_by_id = {item["id"]: item for item in ACCOUNT_REGISTRY} - rows: list[dict[str, Any]] = [] - missing: list[str] = [] - fixture_providers = fixture_providers or set() - for provider in sorted(providers): - account_id = REPORT_PROVIDER_ACCOUNT_IDS.get(provider) - if account_id: - if account_id not in account_by_id: - missing.append(provider) - continue - rows.append(account_by_id[account_id]) - elif provider in REPORT_LOCAL_ACCESS: - rows.append(REPORT_LOCAL_ACCESS[provider]) - elif provider in fixture_providers: - rows.append({**REPORT_LOCAL_ACCESS["fixture"], "provider": provider}) - else: - missing.append(provider) - if missing: - raise ValueError(f"missing report access metadata for providers: {', '.join(missing)}") - return rows +def account_checklist() -> list[dict[str, Any]]: + """Hosted setup candidates; planned products remain explicitly marked.""" + return [dict(access, id=name, provider=method['label']['zh'], + implemented=method.get('status') == 'implemented') + for name, method in report_method_catalog()['methods'].items() + if (access := method.get('access', {})).get('priority')] def credential_state(item: dict[str, Any]) -> str: @@ -357,7 +30,7 @@ def credential_state(item: dict[str, Any]) -> str: def env_template() -> str: names: list[str] = [] - for item in ACCOUNT_REGISTRY: + for item in account_checklist(): for name in item["env_vars"]: if name not in names: names.append(name) @@ -367,15 +40,15 @@ def env_template() -> str: def print_account_checklist() -> None: print("WEB FETCH ACCOUNT CHECKLIST") print("=" * 72) - for item in ACCOUNT_REGISTRY: + for item in account_checklist(): state = credential_state(item) support = "适配器已实现" if item["implemented"] else "适配器待实现" print(f"[{state}] {item['provider']} — {item['priority']} — {support}") - print(f" 注册入口:{item['registration_url']}") - print(f" 凭据入口:{item['key_url']}") + for label, url in item["links"]: + print(f" {label}:{url}") print(f" 环境变量:{', '.join(item['env_vars'])}") - print(f" 获取步骤:{item['steps']}") - print(f" 准入说明:{item['free_note']}") + print(f" 获取步骤:{item['configuration']}") + print(f" 准入说明:{item['account']}") print() print("COPY INTO .env AND FILL VALUES") print("-" * 72) @@ -383,7 +56,7 @@ def print_account_checklist() -> None: HOSTED_KEYS = tuple(dict.fromkeys( - key for account in ACCOUNT_REGISTRY + key for account in account_checklist() if account.get("implemented") and account.get("required") for key in account.get("env_vars", []) )) diff --git a/harness/web_fetch/cli.py b/harness/web_fetch/cli.py index fc443a5ab..a01c3b00e 100644 --- a/harness/web_fetch/cli.py +++ b/harness/web_fetch/cli.py @@ -5,7 +5,7 @@ from pathlib import Path from .accounts import print_account_checklist -from .config import load_dataset, load_provider_config, load_runtime_env, select_diagnostic_scope, add_failure_retry_argument +from .config import load_dataset, load_provider_config, load_runtime_env, select_diagnostic_scope, add_failure_retry_argument, DEFAULT_CALL_TIMEOUT_SECONDS from lexbench_webfetch.metrics import task_passed from lexbench_webfetch.models import TASK_KINDS from .providers import build_provider @@ -119,7 +119,7 @@ def build_parser() -> argparse.ArgumentParser: run.add_argument("--task-kind", choices=TASK_KINDS, help="Select one task kind; intersects with --case-ids") run.add_argument("--runs", type=int, default=1) - run.add_argument("--timeout", type=float, default=45) + run.add_argument("--timeout", type=float, default=DEFAULT_CALL_TIMEOUT_SECONDS) run.add_argument("--delay", type=float, default=0.5) run.add_argument( "--provider-concurrency", diff --git a/harness/web_fetch/data/native-projection-history.json b/harness/web_fetch/data/native-projection-history.json new file mode 100644 index 000000000..a5a403492 --- /dev/null +++ b/harness/web_fetch/data/native-projection-history.json @@ -0,0 +1,35 @@ +{ + "schema_version": 2, + "scoring_sources": { + "b17fc68566409232d3840f69aa4511c6dcdfd154c774eb169ea43787659496f7": { + "parser": "legacy-comma-split", + "repository": "https://github.com/lexmount/browser-eval", + "revision": "c6c004f7840f3ed10ed370d2abc2ce621eec429c", + "projection_source": "harness/web_fetch/response_input.py", + "projection_source_sha256": "2ff5b1ab1a879b77eee521dd21aae810f590f9f511b9928dce893e5c2d6d5695" + }, + "bd08ad5ba4a6ef6962391ef0ea6f3ed7cccaac3294708de87cfd1b9e95d6969c": { + "parser": "legacy-comma-split", + "repository": "https://github.com/lexmount/browser-eval", + "revision": "ec71b5252eaf164754d0f852d3a62ca4cbb1196e", + "projection_source": "harness/web_fetch/response_input.py", + "projection_source_sha256": "a9e9e87fe002e5774775b6b6cccd1aadd400903d5afce262c003f02b086e2871" + }, + "7e1b468e981cfe1da89d443047aff7facd841cd03f00bfc4b016ed67ba292da3": { + "parser": "html-candidates-v1", + "repository": "https://github.com/lexmount/lexbench-webfetch", + "revision": "84c177d97da3b82567d9881e377aa728929cf129", + "projection_source": "src/lexbench_webfetch/resource_urls.py", + "projection_source_sha256": "d129031a368a5f4031af277b18ff64702dbab086a98f3aaef05e8d7beb254b07", + "caller_revision": "28534e24ea3b6f42f19e360d6ba2571bc9a81d60", + "caller_source_sha256": "bc16cfb672019597c56b4b976623bb44feb5fa5c3ca7d3eab9bda8970630db60" + }, + "d042394ea0e8c3aa7aed5851663eaf73772fc4357781999e20a87f48e857e00a": { + "parser": "legacy-comma-split", + "repository": "https://github.com/lexmount/browser-eval", + "revision": "4ef472ff151274e9c526be58bf0e056a1591f420", + "projection_source": "harness/web_fetch/response_input.py", + "projection_source_sha256": "a9e9e87fe002e5774775b6b6cccd1aadd400903d5afce262c003f02b086e2871" + } + } +} diff --git a/harness/web_fetch/data/report-methods.json b/harness/web_fetch/data/report-methods.json new file mode 100644 index 000000000..f28540939 --- /dev/null +++ b/harness/web_fetch/data/report-methods.json @@ -0,0 +1,6101 @@ +{ + "schema_version": 1, + "adapters": { + "http": { + "endpoint": "HTTP GET", + "contract": { + "zh": "已知 URL → HTML 文本或文件任务的原始字节", + "en": "Known URL → HTML text or original bytes for file tasks" + }, + "output": { + "zh": "HTML 提取文本;文件任务保留响应字节", + "en": "Extracted HTML text; file tasks retain response bytes" + }, + "freshness": { + "zh": "每次独立请求,不执行页面 JavaScript", + "en": "Independent requests without executing page JavaScript" + }, + "leaderboard": { + "zh": "独立 HTTP 对照", + "en": "Independent HTTP control" + } + }, + "headless_html": { + "endpoint": "browser HTML → node-html-markdown", + "contract": { + "zh": "已知 URL → DOM HTML → Markdown", + "en": "Known URL → DOM HTML → Markdown" + }, + "output": { + "zh": "Markdown;独立保存 HTML 和诊断", + "en": "Markdown; HTML and diagnostics are retained separately" + }, + "freshness": { + "zh": "每次新进程/会话;domcontentloaded;外层预算见运行协议", + "en": "Fresh process/session; domcontentloaded; outer budget from the run contract" + }, + "leaderboard": { + "zh": "按声明的任务类型分别验收", + "en": "Acceptance is evaluated separately for each declared task family" + }, + "report": { + "observed_routes": true + }, + "browser_cohort_role": "primary" + }, + "headless_markdown": { + "endpoint": { + "zh": "各方法预先声明的原生产物路径", + "en": "Declared native output route for each method" + }, + "contract": { + "zh": "已知 URL → 正文 Markdown/文本,或文件任务的原始字节", + "en": "Known URL → body Markdown/text, or original bytes for file tasks" + }, + "output": { + "zh": "正文 Markdown/文本;文件任务为原始字节;保留接口与阶段诊断", + "en": "Body Markdown/text; original bytes for file tasks; interface and stage diagnostics retained" + }, + "freshness": { + "zh": "每次新进程/会话;超时预算见保存的运行配置", + "en": "Fresh process/session for each call; timeout budget is recorded in the saved run configuration" + }, + "leaderboard": { + "zh": "按声明的任务类型分别验收", + "en": "Acceptance is evaluated separately for each declared task family" + }, + "report": { + "observed_routes": true + }, + "browser_cohort_role": "primary" + }, + "moli_webfetch": { + "endpoint": { + "zh": "moli fetch(网页、文档正文、结构与网页内嵌媒体用原生 Markdown,直接媒体文件用原始响应)", + "en": "moli fetch (native Markdown for page/document text, structure and embedded media; raw responses for direct media files)" + }, + "contract": { + "zh": "已知 URL → 原生 Markdown 或原文件字节", + "en": "Known URL → native Markdown or original file bytes" + }, + "output": { + "zh": "stdout Markdown/字节;stderr 独立诊断", + "en": "stdout Markdown/bytes; separate stderr diagnostics" + }, + "freshness": { + "zh": "每次启动独立进程;结构完成条件只控制官方等待参数", + "en": "A new process for each call; completion conditions only control official wait parameters" + }, + "leaderboard": { + "zh": "按声明的任务类型分别验收", + "en": "Acceptance is evaluated separately for each declared task family" + }, + "report": { + "profiles": [ + { + "metadata_equals": { + "dump": "markdown", + "route": "dynamic_native_markdown", + "wait_until": "domstable", + "wait.strategy": "native_task_family_completion", + "layout": true, + "with_frames": true + }, + "endpoint": "moli fetch --dump markdown --layout --with-frames --wait-until domstable URL", + "freshness": { + "zh": "每例启动一个独立进程;动态网站正文统一等待 Moli 原生 DOM 稳定信号,并启用原生页面布局和已加载子文档。", + "en": "Each case starts a separate process. Every dynamic-body task waits for Moli's native DOM-stability signal and enables native page layout and loaded child documents." + }, + "configuration": { + "zh": "本轮 {record_count} 个动态网站正文任务统一使用 Moli 原生 Markdown、--layout、--with-frames 和 domstable。除目标 URL 外,没有逐题等待元素、延迟、提取脚本或特殊参数,也没有启用理解模型、格式回退或付费后备方法。", + "en": "All {record_count} dynamic-body tasks use Moli's native Markdown, --layout, --with-frames, and domstable mode. Apart from the target URL, there are no per-task element waits, delays, extraction scripts, or special parameters. No interpretation model, format fallback, or paid backup method is enabled." + } + }, + { + "metadata_equals": { + "dump": "markdown", + "route": "dynamic_native_markdown", + "wait_until": "done", + "wait.strategy": "native_default_completion" + }, + "endpoint": "moli fetch --dump markdown --wait-until done URL", + "freshness": { + "zh": "每例启动一个独立进程;由 Moli 的默认 done 完成方式决定何时交付内容。", + "en": "Each case starts a separate process; Moli's default done completion mode decides when to deliver the content." + }, + "configuration": { + "zh": "本轮 {record_count} 例统一使用 Moli 原生 Markdown 和默认 done 完成方式。除目标 URL 外,没有逐例等待元素、额外等待时间、提取脚本或特殊参数,也没有启用理解模型、格式回退或付费后备方法。", + "en": "All {record_count} records use Moli native Markdown and default done completion. Apart from the target URL, no per-task selector, added delay, extraction script or special parameter is used. No understanding model, format fallback or paid backup method is enabled." + } + } + ] + }, + "browser_cohort_role": "companion", + "interface_method": "moli-webfetch", + "task_mode_provider_type": "headless_markdown" + }, + "cowagent_builtin": { + "endpoint": { + "zh": "仓库内 WebFetch.execute", + "en": "Repository WebFetch.execute" + }, + "contract": { + "zh": "已知 URL → 可读文本/文档文字", + "en": "Known URL → readable text or document text" + }, + "output": { + "zh": "纯文本", + "en": "Plain text" + }, + "freshness": { + "zh": "实时请求", + "en": "Real-time request" + }, + "leaderboard": { + "zh": "可读正文", + "en": "Readable body text" + } + }, + "lexmount": { + "endpoint": "POST https://api.lexmount.com/v1/extract", + "contract": { + "zh": "已知 URL → 主正文 + 元数据", + "en": "Known URL → main text + metadata" + }, + "output": "main_text", + "freshness": { + "zh": "托管实时抓取", + "en": "Managed real-time retrieval" + }, + "leaderboard": { + "zh": "可读正文", + "en": "Readable body text" + } + }, + "lexmount_cn": { + "endpoint": "POST https://api.lexmount.cn/v1/extract", + "contract": { + "zh": "已知 URL → 主正文 + 元数据", + "en": "Known URL → main text + metadata" + }, + "output": "main_text", + "freshness": { + "zh": "中国区托管实时抓取", + "en": "Managed real-time retrieval in the China region" + }, + "leaderboard": { + "zh": "可读正文", + "en": "Readable body text" + } + }, + "firecrawl": { + "endpoint": "POST https://api.firecrawl.dev/v2/scrape", + "contract": { + "zh": "已知 URL → Markdown/资源地址,或原文件字节", + "en": "Known URL → Markdown/resource addresses, or original-file bytes" + }, + "output": { + "zh": "Markdown/资源地址;视频资源题另请求 video;文件题为 rawBase64 解码字节", + "en": "Markdown/resource URLs; video-resource tasks also request video; file tasks decode rawBase64 bytes" + }, + "freshness": { + "zh": "maxAge=0;动态题使用预声明 Actions 等待", + "en": "maxAge=0; dynamic tasks use predeclared Actions waits" + }, + "leaderboard": { + "zh": "按声明的任务类型分别验收", + "en": "Acceptance is evaluated separately for each declared task family" + }, + "report": { + "pricing_anchor": "pricing-firecrawl", + "configuration": { + "zh": "正文与资源题请求 Markdown、links、images;原文件题改用唯一 rawBase64 格式并验收解码字节。动态顶层选择器使用 wait action,数量与 iframe 条件明确标为未完整执行。未启用额外的理解模型或付费后备方法。", + "en": "Body and resource tasks request Markdown, links, and images; original-file tasks use rawBase64 as the sole format and validate the decoded bytes. Dynamic top-level selectors use a wait action, while count and iframe conditions are explicitly marked as not fully enforced.No additional understanding model or paid fallback was enabled." + } + } + }, + "tavily_extract": { + "endpoint": "POST https://api.tavily.com/extract", + "contract": { + "zh": "已知 URL → raw_content", + "en": "Known URL → raw_content" + }, + "output": "Markdown(advanced)", + "freshness": { + "zh": "供应商实时抽取", + "en": "Provider-hosted real-time extraction" + }, + "leaderboard": { + "zh": "可读正文", + "en": "Readable body text" + }, + "report": { + "pricing_anchor": "pricing-tavily", + "configuration": { + "zh": "本轮请求 Markdown;图片资源仅来自同一 Extract 响应的 images 字段。未启用额外的理解模型或付费后备方法。", + "en": "This run requests Markdown; image resources come only from the images field in the same Extract response.No additional understanding model or paid fallback was enabled." + } + } + }, + "exa_contents": { + "endpoint": "POST https://api.exa.ai/contents", + "contract": { + "zh": "已知 URL → 正文文本", + "en": "Known URL → body text" + }, + "output": { + "zh": "纯文本", + "en": "Plain text" + }, + "freshness": "maxAgeHours=0", + "leaderboard": { + "zh": "可读正文", + "en": "Readable body text" + }, + "report": { + "pricing_anchor": "pricing-exa", + "configuration": { + "zh": "本轮请求正文文本;资源链接仅来自同一 Contents 响应的 extras 字段。未启用额外的理解模型或付费后备方法。", + "en": "This run requests body text; resource links come only from the extras field in the same Contents response.No additional understanding model or paid fallback was enabled." + } + } + }, + "cloudflare_markdown": { + "endpoint": { + "zh": "POST /accounts/{id}/browser-rendering/markdown 或 /scrape;直接文档 POST /accounts/{id}/ai/tomarkdown", + "en": "POST /accounts/{id}/browser-rendering/markdown or /scrape; direct documents POST /accounts/{id}/ai/tomarkdown" + }, + "contract": { + "zh": "已知 URL → 网页正文、页面资源地址或直接文档文字", + "en": "Known URL → page body, resource URLs, or direct document text" + }, + "output": { + "zh": "Browser Run Markdown / DOM 资源地址;Workers AI 文档 Markdown", + "en": "Browser Run Markdown / DOM resource URLs; Workers AI document Markdown" + }, + "freshness": { + "zh": "Quick Actions 默认最多缓存 5 秒;waitUntil=networkidle2;文档下载与转换同一预算", + "en": "Quick Actions caches for up to 5 seconds by default; waitUntil=networkidle2; document download and conversion share one time budget" + }, + "leaderboard": { + "zh": "按声明的任务类型分别验收", + "en": "Acceptance is evaluated separately for each declared task family" + }, + "report": { + "pricing_anchor": "pricing-cloudflare-markdown" + } + }, + "cloudflare_content": { + "endpoint": "POST /accounts/{id}/browser-rendering/content", + "contract": { + "zh": "已知 URL → 含 head 的完整渲染 HTML", + "en": "Known URL → complete rendered HTML including head" + }, + "output": { + "zh": "渲染 HTML", + "en": "Rendered HTML" + }, + "freshness": { + "zh": "Quick Actions 默认最多缓存 5 秒;waitUntil=networkidle2", + "en": "Quick Actions uses its default cache for up to 5 seconds; waitUntil=networkidle2" + }, + "leaderboard": { + "zh": "HTML 诊断", + "en": "HTML diagnostics" + } + }, + "openclaw_builtin": { + "endpoint": "OpenClaw createWebFetchTool", + "contract": { + "zh": "HTTP GET + Readability 正文抽取(关闭 Firecrawl 回退)", + "en": "HTTP GET + Readability body extraction (Firecrawl fallback disabled)" + }, + "output": "Markdown", + "freshness": "cacheTtlMinutes=0", + "leaderboard": { + "zh": "可读正文", + "en": "Readable body text" + } + }, + "deepseek_harness_builtin": { + "endpoint": "DeepSeek Harness ctx.tools.execute(web_fetch)", + "contract": { + "zh": "匿名 HTTP provider + Turndown", + "en": "Anonymous HTTP provider + Turndown" + }, + "output": "Markdown", + "freshness": { + "zh": "实时请求", + "en": "Real-time request" + }, + "leaderboard": { + "zh": "可读正文", + "en": "Readable body text" + }, + "report": { + "profiles": [ + { + "metadata_equals": { + "output_projection": "declared_body" + }, + "endpoint": "DeepSeek Harness ctx.tools.execute(web_fetch) → value.body.content", + "contract": { + "zh": "已知 URL → 原生声明 HTML/文本正文 → 共有解析", + "en": "Known URL → native declared HTML/text body → shared parsing" + }, + "output": { + "zh": "声明正文;完整工具回包和展示文本独立保存", + "en": "Declared body; the complete tool response and display text are saved separately" + } + } + ] + } + }, + "langchain_webbase": { + "endpoint": "LangChain WebBaseLoader.load()", + "contract": { + "zh": "HTTP + BeautifulSoup 网页文本加载器", + "en": "HTTP + BeautifulSoup page-text loader" + }, + "output": { + "zh": "纯文本", + "en": "Plain text" + }, + "freshness": { + "zh": "实时请求", + "en": "Real-time request" + }, + "leaderboard": { + "zh": "可读正文", + "en": "Readable body text" + } + }, + "crawl4ai": { + "endpoint": "Crawl4AI AsyncWebCrawler.arun()", + "contract": { + "zh": "本地浏览器渲染 + Markdown 抽取", + "en": "Local browser rendering + Markdown extraction" + }, + "output": "Markdown", + "freshness": "cache_mode=BYPASS", + "leaderboard": { + "zh": "可读正文", + "en": "Readable body text" + } + }, + "fixture": { + "endpoint": { + "zh": "本地 fixture", + "en": "Local fixture" + }, + "contract": { + "zh": "确定性安装测试", + "en": "Deterministic installation test" + }, + "output": { + "zh": "纯文本", + "en": "Plain text" + }, + "freshness": { + "zh": "固定", + "en": "Fixed" + }, + "leaderboard": { + "zh": "离线诊断", + "en": "Offline diagnostics" + } + } + }, + "route_labels": { + "cloudflare_native_markdown": { + "zh": "Cloudflare Browser Run /markdown 直接返回 Markdown", + "en": "Markdown returned directly by Cloudflare Browser Run /markdown" + }, + "lightpanda_native_markdown": { + "zh": "Lightpanda CLI 直接返回 Markdown", + "en": "Markdown returned directly by the Lightpanda CLI" + }, + "obscura_native_markdown": { + "zh": "Obscura CLI 直接返回 Markdown", + "en": "Markdown returned directly by the Obscura CLI" + }, + "moli_native_markdown": { + "zh": "Moli CLI 直接返回 Markdown", + "en": "Markdown returned directly by the Moli CLI" + }, + "node-html-markdown": { + "zh": "浏览器渲染 DOM → node-html-markdown", + "en": "Rendered browser DOM → node-html-markdown" + } + }, + "methods": { + "chromium": { + "label": { + "zh": "Chromium", + "en": "Chromium" + }, + "kind": "browser", + "source_url": "https://playwright.dev/python/docs/browsers", + "profile": { + "kind": { + "zh": "本地浏览器 CDP", + "en": "Local browser over CDP" + }, + "description": { + "zh": "Playwright 启动 Chromium,取完整渲染 DOM,再由 node-html-markdown 生成 Markdown。", + "en": "Playwright starts Chromium and captures the complete rendered DOM, which node-html-markdown converts to Markdown." + } + }, + "adapter_types": [], + "label_template": "{label} {version}", + "interface": { + "entry": "Playwright · Chromium → DOM HTML → node-html-markdown", + "auth": { + "zh": "本地独立浏览器;不需要 Google 账号,不加载个人会话", + "en": "Independent local browser; no Google account and no personal session" + }, + "params": [ + [ + "url", + "string", + { + "zh": "唯一任务输入", + "en": "Only task input" + } + ], + [ + "wait_until", + "string", + "load" + ], + [ + "timeout", + "seconds", + { + "zh": "端到端截止取自运行配置", + "en": "End-to-end deadline from run configuration" + } + ], + [ + "converter", + "string", + { + "zh": "node-html-markdown;版本取自调用记录", + "en": "node-html-markdown; version from call records" + } + ] + ], + "fields": [ + [ + "content", + "string", + { + "zh": "转换后的 Markdown", + "en": "Converted Markdown" + } + ], + [ + "rendered_html_path / sha256", + "string", + { + "zh": "转换前 DOM HTML 证据", + "en": "Pre-conversion DOM HTML evidence" + } + ], + [ + "fetch_html_ms / conversion_ms", + "number", + { + "zh": "分阶段耗时", + "en": "Stage-level latency" + } + ], + [ + "browser_stderr", + "string", + { + "zh": "浏览器诊断;不进入正文", + "en": "Browser diagnostics; excluded from body text" + } + ] + ], + "request": { + "url": "https://example.com/", + "engine": "chromium", + "timeout": "", + "wait_until": "load", + "dump": "html", + "converter": "" + }, + "source": "harness/web_fetch/providers/headless_worker.py", + "notes": { + "zh": "所有页面类任务都取得完整渲染 DOM,再使用 已记录版本的 node-html-markdown 转为 Markdown;不运行资源提取脚本,不读取 PDF.js 等查看器私有对象,也不使用外部文档解析器。页面获取、DOM 转换与原始文件读取均计入耗时;原始字节、HTML 与转换器哈希均保留。", + "en": "Every webpage task obtains the complete rendered DOM and converts it to Markdown with node-html-markdown (version from call evidence). No resource-extraction script, private viewer object such as PDF.js, or external document parser is used. Page retrieval, DOM conversion, and original-file reads all count toward latency. Original bytes, HTML, and converter hashes are retained." + } + }, + "capabilities": { + "scope": { + "zh": "Chromium DOM HTML 与已记录版本的 node-html-markdown", + "en": "Chromium DOM HTML and the recorded node-html-markdown version" + }, + "sources": [ + "headless-chromium" + ], + "web": { + "status": { + "zh": "支持", + "en": "Supported" + }, + "detail": { + "zh": "渲染 DOM 转 Markdown", + "en": "Rendered DOM to Markdown" + } + }, + "document": { + "status": { + "zh": "未声明", + "en": "Not declared" + }, + "detail": { + "zh": "登记示例中适配器没有 PDF / Office 解析器,不据此推断产品能力", + "en": "Registered example: the adapter has no PDF / Office parser; no broader product limitation is inferred" + } + }, + "image": { + "status": { + "zh": "仅链接", + "en": "Link only" + }, + "detail": { + "zh": "取决于 DOM 与转换器保留", + "en": "Depends on preservation by the DOM and converter" + } + }, + "av": { + "status": { + "zh": "未声明", + "en": "Not declared" + }, + "detail": { + "zh": "登记示例中没有媒体解析,不据此推断产品能力", + "en": "Registered example: the adapter performs no media parsing; no broader product limitation is inferred" + } + }, + "file": { + "status": { + "zh": "未声明", + "en": "Not declared" + }, + "detail": { + "zh": "登记示例中异常二进制输出仅保留证据", + "en": "Anomalous binary output from this registered example is retained as evidence only" + } + }, + "js": { + "status": { + "zh": "支持", + "en": "Supported" + }, + "detail": { + "zh": "兼容范围看逐例结果", + "en": "See individual results for compatibility coverage" + } + }, + "input": { + "zh": "给定公开 HTTP(S) URL;不登录、不点击、不滚动。", + "en": "A public HTTP(S) URL; no login, clicks, or scrolling." + }, + "structure": { + "zh": "统一 Markdown;不要求引擎按评分答案生成结构。", + "en": "Normalized Markdown; engines are not required to generate structures tailored to the scoring answer." + }, + "access": { + "zh": "每次新会话;Kitesurf 使用 Cloudflare 账号版远程 Browser Run API,其余为本地进程。", + "en": "A fresh session for every call; Kitesurf uses the account-level remote Cloudflare Browser Run API, while the others use local processes." + }, + "notes": { + "zh": "这里只声明登记示例中调用模式。没有启用文档解析、截图、下载、OCR 等模式,不能从登记示例中失败推断产品没有这些能力。", + "en": "Only the invocation mode used in this registered example is described. Document parsing, screenshots, downloads, OCR, and other modes were not enabled, so failures here do not establish that the product lacks those capabilities." + }, + "tested": { + "zh": "等待条件和预算取自调用记录;完整 DOM 经已记录版本的 node-html-markdown 转换。", + "en": "Wait conditions and deadline come from call records; the complete DOM is converted with the recorded version of node-html-markdown." + } + }, + "access": { + "account": { + "zh": "不需要 Google 账号或 API Key;不加载个人登录会话。", + "en": "No Google account or API key is required; personal login sessions are not loaded." + }, + "configuration": { + "zh": "安装与本报告版本和 SHA-256 一致的浏览器,配置 CHROMIUM_BINARY;详见“环境与复现”。", + "en": "Install the browser matching this report's version and SHA-256, and configure CHROMIUM_BINARY; see Environment & reproduction." + }, + "env_vars": [ + "CHROMIUM_BINARY" + ], + "links": [ + [ + "Chrome for Testing", + "https://googlechromelabs.github.io/chrome-for-testing/" + ], + [ + { + "zh": "官方浏览器安装说明", + "en": "Official browser installation guide" + }, + "https://playwright.dev/python/docs/browsers" + ] + ], + "type": "local" + }, + "routes": { + "capabilities": { + "zh": "没有直接输出 Markdown 的原生接口;取得渲染后的 DOM,再使用共同的 HTML 转 Markdown 转换器。", + "en": "No native Markdown output interface; rendered DOM is converted by the shared HTML-to-Markdown converter." + }, + "markdown": { + "zh": "Playwright/CDP 导航 → 最终 DOM → 共同转换 → Markdown", + "en": "Playwright/CDP navigation → final DOM → shared conversion → Markdown" + }, + "original": { + "zh": "Playwright/CDP 原始响应或原生下载", + "en": "Playwright/CDP raw response or native download" + }, + "resource": { + "zh": "Playwright/CDP 取得完整渲染 HTML → 共同 HTML 转 Markdown 转换 → 评分器核对资源地址", + "en": "Playwright/CDP rendered HTML → shared HTML-to-Markdown conversion → resource URL checks" + }, + "runtime": "Playwright/CDP", + "waits": { + "zh": "页面提取等待 load", + "en": "Page extraction waits for load" + } + }, + "status": "implemented", + "observed_routes": [ + { + "metadata_equals": { + "extractor": "node-html-markdown" + }, + "text": { + "zh": "Chromium 渲染 HTML → node-html-markdown{version} → Markdown", + "en": "Chromium rendered HTML → node-html-markdown{version} → Markdown" + }, + "fragments": { + "version": [ + { + "metadata_equals": {}, + "text": { + "zh": " {version}", + "en": " {version}" + }, + "fields": { + "version": "extractor_version" + } + } + ] + }, + "compact_text": { + "zh": "渲染 HTML → node-html-markdown → Markdown", + "en": "Rendered HTML → node-html-markdown → Markdown" + }, + "mode": "markdown" + }, + { + "metadata_equals": { + "output_contract": "original_file_bytes", + "extractor": "browser_native_response_or_download" + }, + "text": { + "zh": "Chromium 原始响应或原生下载 → 原始文件字节", + "en": "Chromium original response or native download → original file bytes" + }, + "compact_text": { + "zh": "Chromium 原始响应或原生下载 → 文件字节", + "en": "Chromium original response or native download → file bytes" + }, + "mode": "file" + } + ], + "task_capabilities": { + "web": { + "status": { + "zh": "支持", + "en": "Supported" + }, + "detail": { + "zh": "原生 DOM 加共同 HTML 转换", + "en": "Native DOM with shared HTML conversion" + } + }, + "document": { + "status": { + "zh": "共同转换", + "en": "Shared conversion" + }, + "detail": { + "zh": "使用浏览器渲染后的完整 HTML,再由固定 HTML-to-Markdown 包转换;不读取查看器私有对象,不使用外部 PDF/DOCX 解析器", + "en": "Full browser-rendered HTML is converted by the pinned HTML-to-Markdown package; no private viewer objects or external PDF/DOCX parsers are used" + } + }, + "image": { + "status": { + "zh": "资源地址 / 字节", + "en": "Resource URLs / bytes" + }, + "detail": { + "zh": "完整渲染 HTML 经过共同转换,评分器从 Markdown 核对资源地址;直接文件通过原生响应取得", + "en": "Full rendered HTML passes through shared conversion; the scorer checks resource URLs in Markdown, and direct files use the native response" + } + }, + "av": { + "status": { + "zh": "资源与元数据", + "en": "Resources and metadata" + }, + "detail": { + "zh": "完整渲染 HTML 经过共同转换,评分器从 Markdown 核对地址或页面信息;直接文件读取原始字节", + "en": "Full rendered HTML passes through shared conversion; the scorer checks URLs or page information in Markdown, and direct files retain original bytes" + } + }, + "file": { + "status": { + "zh": "原生获取", + "en": "Native retrieval" + }, + "detail": { + "zh": "使用该浏览器原始响应或附件接口,按固定规则验收字节", + "en": "Use the browser original-response or attachment interface; check bytes against fixed acceptance rules" + } + }, + "structure": { + "zh": "按任务交付 Markdown、文档正文、资源地址、媒体元数据或原始文件。", + "en": "Deliver Markdown, document text, resource URLs, media metadata or original files according to the task." + }, + "notes": { + "zh": "Chromium 负责访问和渲染;所有页面类任务都取得完整 HTML,再由固定标准包转成 Markdown。评测端不运行资源提取脚本,也不读取文档查看器私有对象。完整分类及边界见报告解读的接口表。", + "en": "Chromium handles access and rendering. All page tasks return full HTML for conversion to Markdown by the pinned standard package. The harness runs no resource-extraction scripts and reads no private document-viewer objects. See the interpretation interface table for the full scope and boundaries." + } + }, + "extends": "browser_tasks", + "runtime_parameters": [ + { + "parameter": "wait_until", + "rules": [ + { + "description": { + "text": "{wait}", + "fields": { + "wait": "navigation_wait_until" + }, + "field_values": { + "wait": [ + "load", + "domcontentloaded", + "networkidle", + "commit" + ] + } + }, + "request_fields": { + "wait_until": { + "text": "{wait}", + "fields": { + "wait": "navigation_wait_until" + }, + "field_values": { + "wait": [ + "load", + "domcontentloaded", + "networkidle", + "commit" + ] + } + } + } + } + ] + }, + { + "parameter": "converter", + "rules": [ + { + "description": { + "text": "node-html-markdown {version}", + "fields": { + "version": "extractor_version" + }, + "metadata_equals": { + "extractor": "node-html-markdown" + } + }, + "request_fields": { + "converter": { + "text": "node-html-markdown {version}", + "fields": { + "version": "extractor_version" + }, + "metadata_equals": { + "extractor": "node-html-markdown" + } + } + } + } + ] + }, + { + "parameter": "converter", + "append": true, + "rules": [ + { + "description": { + "text": { + "zh": ";包装脚本 SHA-256 {sha}", + "en": "; wrapper script SHA-256 {sha}" + }, + "fields": { + "sha": "converter_sha256" + }, + "field_patterns": { + "sha": "[0-9a-f]{64}" + } + }, + "request_fields": { + "converter_sha256": { + "text": "{sha}", + "fields": { + "sha": "converter_sha256" + }, + "field_patterns": { + "sha": "[0-9a-f]{64}" + } + } + }, + "note": { + "zh": "转换包装还负责代码块围栏与语言标签;本轮以完整脚本 SHA-256 绑定,不把它写成未修改的包默认行为。", + "en": "The conversion wrapper also handles code fences and language labels. Its full script SHA-256 is bound; this is not described as unmodified package behavior." + } + } + ] + } + ] + }, + "cloudflare-markdown": { + "label": { + "zh": "cloudflare-markdown", + "en": "cloudflare-markdown" + }, + "kind": "adapter", + "source_url": "https://developers.cloudflare.com/browser-run/quick-actions/markdown-endpoint/", + "profile": { + "kind": { + "zh": "托管 API", + "en": "Managed API" + }, + "description": { + "zh": "云端浏览器加载网页后转换为 Markdown。", + "en": "A cloud browser loads the page and converts it to Markdown." + } + }, + "adapter_types": [ + "cloudflare_markdown" + ], + "label_template": "{label} {version}", + "interface": { + "entry": "POST https://api.cloudflare.com/client/v4/accounts//browser-rendering/{markdown|scrape}; direct files POST …/ai/tomarkdown", + "auth": "Authorization: Bearer ", + "params": [ + [ + "url", + { + "zh": "string · 必填", + "en": "string · required" + }, + { + "zh": "目标页面;直接文件先下载公开 URL 的原始字节再上传", + "en": "Target page; for a direct file, first download bytes from its public URL before uploading" + } + ], + [ + "gotoOptions.waitUntil", + "string", + { + "zh": "网页和资源页使用 networkidle2;动态题使用预声明就绪信号", + "en": "Web and resource pages use networkidle2; dynamic tasks use their predeclared readiness signal" + } + ], + [ + "elements", + { + "zh": "array · 资源题", + "en": "array · resource tasks" + }, + { + "zh": "/scrape 按预声明 CSS 选择器取图片或媒体属性", + "en": "/scrape reads image or media attributes through predeclared CSS selectors" + } + ], + [ + "files", + { + "zh": "multipart · 直接文档", + "en": "multipart · direct documents" + }, + { + "zh": "/ai/tomarkdown 接受 PDF / DOCX 等受支持文件;输入字节由客户端取得", + "en": "/ai/tomarkdown accepts supported files such as PDF and DOCX; the client obtains input bytes" + } + ], + [ + "gotoOptions.timeout / actionTimeout", + "number", + { + "zh": "网页和资源页的客户端内层截止以保存的请求参数为准", + "en": "Client inner deadlines for web and resource pages come from saved request parameters" + } + ] + ], + "fields": [ + [ + "success", + "boolean", + { + "zh": "接口是否成功", + "en": "Whether the interface succeeded" + } + ], + [ + "result", + "string / array", + { + "zh": "/markdown 网页正文;/scrape 元素属性;/ai/tomarkdown 文档 Markdown", + "en": "/markdown page text; /scrape element attributes; /ai/tomarkdown document Markdown" + } + ], + [ + "errors[]", + "array", + { + "zh": "接口错误代码与信息", + "en": "Interface error code and message" + } + ], + [ + "meta", + { + "zh": "object · 若提供", + "en": "object · when provided" + }, + { + "zh": "Browser Run 最终 URL、标题、源站状态等", + "en": "Browser Run final URL, title, origin status, and related values" + } + ], + [ + "source_sha256", + { + "zh": "string · 直接文档", + "en": "string · direct documents" + }, + { + "zh": "上传文件的输入字节摘要;下载与转换均计入同一次耗时", + "en": "Hash of uploaded input bytes; both download and conversion count toward the same call latency" + } + ] + ], + "request": { + "web": { + "endpoint": "/browser-rendering/markdown", + "url": "https://example.com/", + "gotoOptions": { + "waitUntil": "networkidle2", + "timeout": 40500 + }, + "actionTimeout": 40500 + }, + "image_or_video_resource": { + "endpoint": "/browser-rendering/scrape", + "url": "https://example.com/", + "elements": [ + { + "selector": "img" + }, + { + "selector": "video" + } + ] + }, + "video_detail_link": { + "endpoint": "/browser-rendering/scrape", + "url": "https://example.com/", + "elements": [ + { + "selector": "a[href]" + } + ] + }, + "direct_pdf_or_docx": { + "endpoint": "/ai/tomarkdown", + "files": "" + } + }, + "source": "harness/web_fetch/providers/cloudflare.py", + "notes": { + "zh": "按任务预选官方接口:网页正文用 Browser Run /markdown;页面图片与视频地址用 /scrape 的 DOM 属性;直接 PDF / DOCX 使用 Workers AI 文件转换。/pdf 是生成网页 PDF,/links 只列普通链接,/crawl 会跟随其他页面,均不满足这里的文档文字或目标资源合同。直接文档先在客户端下载,下载与转换共享预算;该下载不是 Cloudflare 的独立成绩。浏览器等待计时器彼此独立,账户配额或转换权限错误单列。", + "en": "The official route is chosen before each task: Browser Run /markdown for page text, /scrape DOM attributes for page image and video URLs, and Workers AI file conversion for direct PDF/DOCX text. /pdf prints a webpage, /links lists ordinary links, and /crawl follows other pages; none fulfills the document-text or target-resource contract here. A direct file is downloaded by the client, then converted within the same deadline; that download is not an independent Cloudflare score. Browser wait timers are independent; quota and conversion-permission errors are separate failure categories." + } + }, + "capabilities": { + "scope": { + "zh": "Browser Run /markdown、/scrape;直接文档 Workers AI /ai/tomarkdown", + "en": "Browser Run /markdown and /scrape; Workers AI /ai/tomarkdown for direct documents" + }, + "sources": [ + "cf", + "cf-scrape", + "cf-files", + "cf-formats" + ], + "web": { + "status": { + "zh": "支持", + "en": "Supported" + }, + "detail": { + "zh": "网页 → Markdown", + "en": "Webpage → Markdown" + } + }, + "document": { + "status": { + "zh": "需配置", + "en": "Configuration required" + }, + "detail": { + "zh": "上传直接 PDF / DOCX 后转换为 Markdown", + "en": "Upload direct PDF/DOCX files and convert to Markdown" + } + }, + "image": { + "status": { + "zh": "仅链接", + "en": "Link only" + }, + "detail": { + "zh": "/scrape 读取 DOM 图片属性", + "en": "/scrape reads DOM image attributes" + } + }, + "av": { + "status": { + "zh": "仅链接", + "en": "Link only" + }, + "detail": { + "zh": "/scrape 读取 DOM 媒体属性", + "en": "/scrape reads DOM media attributes" + } + }, + "file": { + "status": { + "zh": "无此能力", + "en": "Not supported" + }, + "detail": { + "zh": "文档上传后返回文字,不交付原文件", + "en": "Document upload returns text, not the original file" + } + }, + "js": { + "status": { + "zh": "支持", + "en": "Supported" + }, + "detail": { + "zh": "Browser Run 浏览器 + 等待参数", + "en": "Browser Run browser with wait settings" + } + }, + "input": { + "zh": "网页输入为 URL;直接文档由客户端下载原始字节后上传 Workers AI。", + "en": "Pages are submitted as URLs; the client downloads direct-document bytes before uploading them to Workers AI." + }, + "structure": { + "zh": "/markdown 返回网页文本,/scrape 返回元素属性,/ai/tomarkdown 返回文档文字。", + "en": "/markdown returns page text, /scrape element attributes, and /ai/tomarkdown document text." + }, + "access": { + "zh": "Browser Rendering Edit 与 Workers AI 权限分别控制两类端点;浏览器访问仍可能被识别和拦截。", + "en": "Browser Rendering Edit and Workers AI permissions govern different endpoints; browser visits can still be identified and blocked." + }, + "notes": { + "zh": "/pdf 打印网页,不是读取已有 PDF;/links 只列页面链接,不能代替图片和媒体 DOM 属性。直接文档下载与转换共享同一次耗时;客户端下载不是 Cloudflare 独立成绩。/scrape 交付资源地址,不保证取得媒体字节或理解画面。", + "en": "/pdf prints a webpage rather than reading an existing PDF. /links lists page links but does not replace image and media DOM attributes. A direct-document download and its conversion share one measured call; the client download is not an independent Cloudflare score. /scrape provides URLs, not a guarantee of media bytes or visual understanding." + }, + "tested": { + "zh": "网页正文与动态页使用 /markdown;直接 PDF / DOCX 使用 /ai/tomarkdown;页面图片和视频资源使用 /scrape。未提供源站登录 Cookie,不用 Search/Crawl、截图、浏览器控制或失败后接口回退。", + "en": "Page text and dynamic tasks use /markdown; direct PDF/DOCX tasks use /ai/tomarkdown; page image and video resources use /scrape. No source-site login Cookie, Search/Crawl, screenshot, browser-control route, or post-failure interface fallback was used." + } + }, + "access": { + "type": "hosted", + "env_vars": [ + "CLOUDFLARE_API_TOKEN", + "CLOUDFLARE_ACCOUNT_ID" + ], + "links": [ + [ + { + "zh": "注册 / 登录", + "en": "Register / sign in" + }, + "https://dash.cloudflare.com/sign-up" + ], + [ + { + "zh": "获取凭据", + "en": "Get credentials" + }, + "https://dash.cloudflare.com/profile/api-tokens" + ] + ], + "account": { + "zh": "Browser Run 可用 Workers Free 额度。Browser Run 需要 Browser Rendering Edit;文件转换 API 接受 Workers AI Read 或 Edit。两类产品的配额和计费分开。", + "en": "Browser Run has a Workers Free allowance. It requires Browser Rendering Edit; file conversion accepts Workers AI Read or Edit. The products have separate quotas and billing." + }, + "configuration": { + "zh": "注册 Cloudflare,复制 Account ID;创建覆盖 Account → Browser Rendering → Edit 与 Account → Workers AI → Read 的自定义 token。官方 Workers AI REST 入门建议 Read 和 Edit 都授予;运行前用 GET /client/v4/user/tokens/verify 验证。", + "en": "Register with Cloudflare and copy the Account ID. Create a custom token covering Account → Browser Rendering → Edit and Account → Workers AI → Read. The Workers AI REST getting-started guide recommends both Read and Edit; verify the token at GET /client/v4/user/tokens/verify before running." + }, + "required": true, + "priority": { + "zh": "B · 已知 URL 抽取", + "en": "B · Known-URL extraction" + } + }, + "status": "implemented" + }, + "cowagent-built-in": { + "label": { + "zh": "cowagent-built-in", + "en": "cowagent-built-in" + }, + "kind": "adapter", + "source_url": "https://github.com/zhayujie/CowAgent/blob/main/agent/tools/web_fetch/web_fetch.py", + "profile": { + "kind": { + "zh": "项目内置", + "en": "Project built-in" + }, + "description": { + "zh": "CowAgent 内的 HTTP 抓取和文本、文档解析工具。", + "en": "HTTP retrieval and text and document parsing tools within CowAgent." + } + }, + "adapter_types": [ + "cowagent_builtin" + ], + "label_template": "{label} {version}", + "interface": { + "entry": "Python · WebFetch({\"cwd\": workspace}).execute(args)", + "auth": { + "zh": "本地内置工具;无供应商 API Key", + "en": "Local built-in tool; no provider API key" + }, + "params": [ + [ + "url", + { + "zh": "string · 必填", + "en": "string · required" + }, + { + "zh": "公开 HTTP/HTTPS 网页或文档 URL", + "en": "Public HTTP/HTTPS web-page or document URL" + } + ], + [ + "cwd", + { + "zh": "构造配置", + "en": "Construction configuration" + }, + { + "zh": "解析文档时使用的本地工作目录,不是工具输入参数", + "en": "Local working directory used for document parsing, not a tool input" + } + ] + ], + "fields": [ + [ + "ToolResult.status", + "string", + { + "zh": "success / error 等工具状态", + "en": "Tool status such as success / error" + } + ], + [ + "ToolResult.result", + "string", + { + "zh": "提取文字或错误说明 → content", + "en": "Extracted text or error explanation → content" + } + ] + ], + "request": { + "url": "https://example.com/" + }, + "source": "harness/web_fetch/workers/cowagent_worker.py", + "notes": { + "zh": "底层请求超时和外层评测预算分别以调用记录为准。网页与 PDF/DOCX 走不同解析路径。", + "en": "The underlying request timeout and outer benchmark deadline are recorded separately. Web pages and PDF/DOCX inputs use different parsing paths." + } + }, + "capabilities": { + "scope": "WebFetch.execute", + "sources": [ + "cow" + ], + "web": { + "status": { + "zh": "支持", + "en": "Supported" + }, + "detail": { + "zh": "HTML → 纯文本", + "en": "HTML → plain text" + } + }, + "document": { + "status": { + "zh": "支持", + "en": "Supported" + }, + "detail": { + "zh": "PDF / DOCX / TXT 等", + "en": "PDF / DOCX / TXT, etc." + } + }, + "image": { + "status": { + "zh": "无此能力", + "en": "Not supported" + }, + "detail": { + "zh": "不提取图片属性", + "en": "Does not extract image attributes" + } + }, + "av": { + "status": { + "zh": "无此能力", + "en": "Not supported" + }, + "detail": { + "zh": "无媒体提取器", + "en": "No media extractor" + } + }, + "file": { + "status": { + "zh": "有限支持", + "en": "Limited support" + }, + "detail": { + "zh": "受支持文档落盘", + "en": "Supported documents saved to disk" + } + }, + "js": { + "status": { + "zh": "无此能力", + "en": "Not supported" + }, + "detail": { + "zh": "HTTP,不运行 JS", + "en": "HTTP without JavaScript" + } + }, + "input": { + "zh": "HTTP(S) URL;不是本地文件上传接口。", + "en": "HTTP(S) URL; not a local file-upload interface." + }, + "structure": { + "zh": "文本、文档标题和保存路径;没有通用 JSON schema 抽取。", + "en": "Text, document title, and saved path; no generic JSON schema extraction." + }, + "access": { + "zh": "当前工具未提供站点 Cookie 参数;不是登录浏览器。", + "en": "The current tool provides no site Cookie parameter; it is not a logged-in browser." + }, + "notes": { + "zh": "PDF 使用 pypdf 读取文字层,不含 OCR;DOCX 读取段落。另有 XLSX、PPTX 解析分支并依赖相应库。虽然扩展名列表包含 XLS/PPT,实际采用 openpyxl/python-pptx,不能据此保证旧二进制格式可解析。文档下载成功可返回本地路径;不是任意图片或视频下载器。", + "en": "PDF uses pypdf to read the text layer, without OCR; DOCX reads paragraphs. Separate XLSX and PPTX parsing branches depend on their respective libraries. Although the extension list includes XLS/PPT, the implementation uses openpyxl/python-pptx, so this does not guarantee support for legacy binary formats. A successfully downloaded document can return a local path; this is not a general-purpose image or video downloader." + }, + "tested": { + "zh": "登记示例中调用同一 WebFetch;本机是否安装解析依赖、文件是否能解析仍看实测,支持声明不等于必然成功。", + "en": "Registered example: calls the same WebFetch; whether parsing dependencies are installed locally and whether a file can be parsed still depends on measured behavior. Declared support does not guarantee success." + } + }, + "access": { + "type": "local", + "env_vars": [ + "COWAGENT_REPO", + "COWAGENT_BENCHMARK_WORKSPACE" + ], + "links": [ + [ + { + "zh": "注册 / 登录", + "en": "Register / sign in" + }, + "https://github.com/zhayujie/CowAgent" + ] + ], + "account": { + "zh": "本地项目内置实现。", + "en": "Built into the local project." + }, + "configuration": { + "zh": "检出并固定 CowAgent 源码,配置仓库路径和隔离工作目录;不需要供应商 API Key。", + "en": "Check out and pin the CowAgent source, then configure its repository path and isolated working directory; no provider API key is required." + } + }, + "status": "implemented" + }, + "crawl4ai": { + "label": { + "zh": "crawl4ai", + "en": "crawl4ai" + }, + "kind": "adapter", + "source_url": "https://docs.crawl4ai.com/", + "profile": { + "kind": { + "zh": "本地开源库", + "en": "Local open-source library" + }, + "description": { + "zh": "本地浏览器渲染,再抽取 Markdown、媒体及链接信息。", + "en": "Renders in a local browser, then extracts Markdown, media, and link information." + } + }, + "adapter_types": [ + "crawl4ai" + ], + "label_template": "{label} {version}", + "interface": { + "entry": "Python · await AsyncWebCrawler(...).arun(url, config)", + "auth": { + "zh": "本地 Chromium;无云端抓取 Key", + "en": "Local Chromium; no cloud-retrieval key" + }, + "params": [ + [ + "url", + { + "zh": "string · 必填", + "en": "string · required" + }, + { + "zh": "目标网址", + "en": "Target URL" + } + ], + [ + "cache_mode", + "CacheMode", + { + "zh": "登记示例中 BYPASS", + "en": "BYPASS in this registered example" + } + ], + [ + "page_timeout", + "number", + { + "zh": "page_timeout 参数,单位毫秒", + "en": "page_timeout parameter in milliseconds" + } + ], + [ + "wait_until", + "string", + { + "zh": "非动态网页为 domcontentloaded;动态任务使用预声明值", + "en": "domcontentloaded for non-dynamic pages; the predeclared value for dynamic tasks" + } + ], + [ + "process_iframes / remove_overlay_elements", + "boolean", + { + "zh": "登记示例中均为 true", + "en": "true for every call in this registered example" + } + ] + ], + "fields": [ + [ + "result.success", + "boolean", + { + "zh": "爬取是否成功", + "en": "Whether crawling succeeded" + } + ], + [ + "result.markdown.raw_markdown", + "string", + "Markdown → content" + ], + [ + "result.media", + "dict", + "images、videos、audios" + ], + [ + "result.links", + "dict", + "internal / external" + ], + [ + "result.status_code / error_message", + "number / string", + { + "zh": "源站状态或失败信息", + "en": "Origin status or failure information" + } + ] + ], + "request": "config = CrawlerRunConfig(\n cache_mode=CacheMode.BYPASS,\n page_timeout=PAGE_TIMEOUT_MS,\n wait_until=\"domcontentloaded\",\n process_iframes=True,\n remove_overlay_elements=True,\n)\nresult = await crawler.arun(\n url=\"https://example.com/\", config=config\n)", + "source": "harness/web_fetch/workers/crawl4ai_worker.py", + "notes": { + "zh": "非动态网页使用本地无头 Chrome,并在 DOMContentLoaded 后提取;动态任务仍按预声明完成条件等待。直接 PDF 正文在调用前选择 PDFCrawlerStrategy 与 PDFContentScrapingStrategy,文件交付任务选择 AsyncHTTPCrawlerStrategy 并统一验收 downloaded_files。媒体链接不等于下载;文件字节校验不等于理解媒体。", + "en": "Non-dynamic pages use local headless Chrome and extract after DOMContentLoaded; dynamic tasks still wait for their predeclared completion condition. Direct PDF body tasks preselect PDFCrawlerStrategy with PDFContentScrapingStrategy, while file-delivery tasks use AsyncHTTPCrawlerStrategy and validate downloaded_files with the shared checks. A media link is not a download, and byte validation is not media understanding." + } + }, + "capabilities": { + "scope": { + "zh": "正文 AsyncWebCrawler;文件 AsyncHTTPCrawlerStrategy", + "en": "Body content: AsyncWebCrawler; files: AsyncHTTPCrawlerStrategy" + }, + "sources": [ + "c4-media", + "c4-pdf", + "c4-download", + "c4-config" + ], + "web": { + "status": { + "zh": "支持", + "en": "Supported" + }, + "detail": { + "zh": "渲染 HTML → Markdown", + "en": "Rendered HTML → Markdown" + } + }, + "document": { + "status": { + "zh": "需配置", + "en": "Configuration required" + }, + "detail": { + "zh": "PDF 专用策略", + "en": "PDF-specific strategy" + } + }, + "image": { + "status": { + "zh": "仅链接", + "en": "Link only" + }, + "detail": { + "zh": "media.images + 元数据", + "en": "media.images + metadata" + } + }, + "av": { + "status": { + "zh": "仅链接", + "en": "Link only" + }, + "detail": "media.videos / audios" + }, + "file": { + "status": { + "zh": "需配置", + "en": "Configuration required" + }, + "detail": { + "zh": "原生 HTTP 文件策略或浏览器下载", + "en": "Native HTTP file strategy or browser download" + } + }, + "js": { + "status": { + "zh": "支持", + "en": "Supported" + }, + "detail": { + "zh": "浏览器 + JS 动作", + "en": "Browser plus JavaScript actions" + } + }, + "input": { + "zh": "网页 URL;远程或本地 PDF 需专用策略。", + "en": "Webpage URL; remote or local PDF requires a dedicated strategy." + }, + "structure": { + "zh": "可选 CSS/JSON 或 LLM 提取策略;media/links 是结构化资源信息。", + "en": "Optional CSS/JSON or LLM extraction strategies; media/links provides structured resource information." + }, + "access": { + "zh": "可配置持久会话、Cookie 和页面动作;并非保证突破每个站点限制。", + "en": "Configurable persistent sessions, cookies, and page actions; does not guarantee bypassing every site's restrictions." + }, + "notes": { + "zh": "PDF 需 PDFCrawlerStrategy + PDFContentScrapingStrategy,可选提取并保存内嵌图片,默认不含扫描件 OCR。浏览器下载需 accept_downloads 并触发下载;原生 HTTP 文件策略直接将非 HTML 响应保存到 downloaded_files。网页截图和打印 PDF 是新生成的文件,不是目标原文件。", + "en": "PDF requires PDFCrawlerStrategy + PDFContentScrapingStrategy and can optionally extract and save embedded images; scanned-document OCR is not included by default. Browser downloads require accept_downloads and a triggered download; the native HTTP file strategy saves non-HTML responses directly to downloaded_files. Webpage screenshots and printed PDFs are newly generated files, not the original target files." + }, + "tested": { + "zh": "非动态网页使用通用浏览器策略、domcontentloaded 与无缓存;动态任务使用预声明的完成条件。直接 PDF 正文在调用前选择 PDFCrawlerStrategy 与 PDFContentScrapingStrategy;文件任务选择原生 HTTP 文件策略并统一校验。未启用截图、OCR 或 LLM 提取;媒体字段不代表内容理解。", + "en": "Non-dynamic pages use the general browser strategy with domcontentloaded and no cache; dynamic tasks use their predeclared completion condition. Direct PDF body tasks preselect PDFCrawlerStrategy with PDFContentScrapingStrategy; file tasks select the native HTTP file strategy and shared validation. Screenshots, OCR, and LLM extraction are not enabled, and media fields do not imply content understanding." + } + }, + "access": { + "type": "local", + "env_vars": [ + "OSS_PYTHON" + ], + "links": [ + [ + { + "zh": "注册 / 登录", + "en": "Register / sign in" + }, + "https://github.com/unclecode/crawl4ai" + ], + [ + { + "zh": "获取凭据", + "en": "Get credentials" + }, + "https://docs.crawl4ai.com/core/installation/" + ] + ], + "account": { + "zh": "本地开源库;不需要供应商账号。", + "en": "Local open-source library; no provider account is required." + }, + "configuration": { + "zh": "使用仓库冻结的 Python 环境及匹配的浏览器运行;OSS_PYTHON 只在显式覆盖 worker 解释器时设置。", + "en": "Run with the repository's frozen Python environment and matching browser; set OSS_PYTHON only when explicitly overriding the worker interpreter." + } + }, + "status": "implemented" + }, + "deepseek-harness-built-in": { + "label": { + "zh": "deepseek-harness-built-in", + "en": "deepseek-harness-built-in" + }, + "kind": "adapter", + "source_url": "https://github.com/deepseek-ai/deepseek-harness/tree/master/packages/web", + "profile": { + "kind": { + "zh": "项目内置", + "en": "Project built-in" + }, + "description": { + "zh": "登记示例中使用匿名 HTTP provider 与 Turndown 转换。", + "en": "Registered example: uses the anonymous HTTP provider with Turndown conversion." + } + }, + "adapter_types": [ + "deepseek_harness_builtin" + ], + "label_template": "{label} {version}", + "interface": { + "entry": "TypeScript · ctx.tools.execute({name: \"web_fetch\", arguments, ...})", + "auth": { + "zh": "本地 HTTP fetch provider;无云端抓取 Key", + "en": "Local HTTP fetch provider; no cloud-retrieval key" + }, + "params": [ + [ + "name", + "string", + "web_fetch" + ], + [ + "arguments.url", + { + "zh": "string · 必填", + "en": "string · required" + }, + { + "zh": "目标网址", + "en": "Target URL" + } + ], + [ + "callId / signal", + { + "zh": "运行时参数", + "en": "Runtime parameters" + }, + { + "zh": "调用标识和取消信号;示例省略初始化", + "en": "Call identifier and cancellation signal; initialization omitted from the example" + } + ] + ], + "fields": [ + [ + "isError", + "boolean", + { + "zh": "工具执行是否报错", + "en": "Whether tool execution raised an error" + } + ], + [ + "value.body.content", + "string", + { + "zh": "接口声明的 HTML/文本正文;声明为空时保持为空", + "en": "Declared HTML/text body; an empty declared body stays empty" + } + ], + [ + "content[].text", + "string", + { + "zh": "模型展示文本;与声明正文分别保存", + "en": "Model-facing display text; saved separately from the declared body" + } + ], + [ + "value.statusCode / value.url", + "number / string", + { + "zh": "成功态的规范源站状态与最终 URL", + "en": "Canonical origin status and final URL for a successful tool result" + } + ], + [ + "meta", + { + "zh": "object · 若提供", + "en": "object · when provided" + }, + { + "zh": "展示投影;仅在规范值缺失时补充状态或 URL", + "en": "Display projection; supplies status or URL only when the canonical value is absent" + } + ], + [ + "error.info.code", + { + "zh": "string · 若提供", + "en": "string · when provided" + }, + { + "zh": "工具错误类型", + "en": "Tool error type" + } + ] + ], + "request": { + "name": "web_fetch", + "arguments": { + "url": "https://example.com/" + } + }, + "source": "harness/web_fetch/workers/dsh_webfetch_worker.mts", + "notes": { + "zh": "通过内置 HTTP provider 返回声明正文及展示文本;调用预算和读取字段以保留证据为准。不是 DeepSeek 聊天 API,也没有调用模型生成答案。", + "en": "The built-in HTTP provider returns the declared body and display text; retained evidence identifies the call budget and selected fields. This is not the DeepSeek chat API, and no model generated the answer." + } + }, + "capabilities": { + "scope": "web_fetch · HTTP provider", + "sources": [ + "dsh", + "dsh-text" + ], + "web": { + "status": { + "zh": "支持", + "en": "Supported" + }, + "detail": "HTML → Markdown" + }, + "document": { + "status": { + "zh": "无此能力", + "en": "Not supported" + }, + "detail": { + "zh": "拒绝 PDF / Office MIME", + "en": "Rejects PDF / Office MIME types" + } + }, + "image": { + "status": { + "zh": "有限支持", + "en": "Limited support" + }, + "detail": { + "zh": "HTML 中的 Markdown 引用", + "en": "Markdown references in HTML" + } + }, + "av": { + "status": { + "zh": "有限支持", + "en": "Limited support" + }, + "detail": { + "zh": "页面中的普通超链接", + "en": "Ordinary hyperlinks on the page" + } + }, + "file": { + "status": { + "zh": "无此能力", + "en": "Not supported" + }, + "detail": { + "zh": "拒绝二进制 MIME", + "en": "Rejects binary MIME types" + } + }, + "js": { + "status": { + "zh": "无此能力", + "en": "Not supported" + }, + "detail": { + "zh": "匿名 HTTP 获取", + "en": "Anonymous HTTP retrieval" + } + }, + "input": { + "zh": "HTTP(S) URL;只接受 HTML、text/*、JSON/XML 等文字 MIME。", + "en": "HTTP(S) URL; accepts only textual MIME types such as HTML, text/*, and JSON/XML." + }, + "structure": { + "zh": "Turndown 转换正文和表格,返回工具文本与状态。", + "en": "Turndown converts body content and tables, returning tool text and status." + }, + "access": { + "zh": "拒绝 URL 内嵌凭据;跨源重定向需独立调用,不是登录会话浏览器。", + "en": "Rejects credentials embedded in URLs; cross-origin redirects require a separate call, and this is not a logged-in session browser." + }, + "notes": { + "zh": "依据登记示例中固定源码的内容类型白名单;PDF、image/*、video/* 不在可解码类型中。HTML 内出现媒体链接不意味着可直接抓取对应二进制 URL。", + "en": "Based on the content-type allowlist in the source fixed for this registered example; PDF, image/*, and video/* are not decodable types. Media links appearing in HTML do not imply that the corresponding binary URLs can be fetched directly." + }, + "tested": { + "zh": "仅匿名 HTTP provider;不是 DeepSeek 模型 API,也没有使用模型作评分。", + "en": "Anonymous HTTP provider only; this is not the DeepSeek model API, and no model was used for scoring." + } + }, + "access": { + "type": "local", + "env_vars": [ + "DEEPSEEK_HARNESS_REPO", + "OPENCLAW_NODE", + "DEEPSEEK_PNPM" + ], + "links": [ + [ + { + "zh": "注册 / 登录", + "en": "Register / sign in" + }, + "https://github.com/deepseek-ai/deepseek-harness" + ], + [ + { + "zh": "获取凭据", + "en": "Get credentials" + }, + "https://github.com/deepseek-ai/deepseek-harness/tree/master/packages/web" + ] + ], + "account": { + "zh": "本地项目内置实现;不需要模型 API Key。", + "en": "Built into the local project; no model API key is required." + }, + "configuration": { + "zh": "检出并固定 DeepSeek Harness 源码、Node、pnpm 与构建依赖;登记示例中不调用聊天模型。", + "en": "Check out and pin the DeepSeek Harness source, Node, pnpm, and build dependencies; this registered example does not call a chat model." + } + }, + "status": "implemented" + }, + "exa": { + "label": { + "zh": "exa", + "en": "exa" + }, + "kind": "adapter", + "source_url": "https://exa.ai/docs/reference/contents-retrieval", + "profile": { + "kind": { + "zh": "托管 API", + "en": "Managed API" + }, + "description": { + "zh": "登记示例中调用 Contents 取得指定 URL 的正文,不测试搜索。", + "en": "Registered example: calls Contents to retrieve body text from the specified URL; search was not tested." + } + }, + "adapter_types": [ + "exa_contents" + ], + "label_template": "{label} {version}", + "interface": { + "entry": "POST https://api.exa.ai/contents", + "auth": "x-api-key: ", + "params": [ + [ + "urls", + { + "zh": "array · 必填", + "en": "array · required" + }, + { + "zh": "登记示例中每次一个目标 URL,不调用搜索", + "en": "One target URL per call in this registered example; search is not invoked" + } + ], + [ + "text.verbosity", + "string", + { + "zh": "登记示例中 full,要求完整正文而非 compact 默认值", + "en": "full in this registered example, requesting complete body text instead of the compact default" + } + ], + [ + "maxAgeHours", + "number", + { + "zh": "登记示例中 0", + "en": "0 in this registered example" + } + ], + [ + "livecrawlTimeout", + "integer", + { + "zh": "livecrawlTimeout 参数,单位毫秒;与客户端截止分别记录", + "en": "livecrawlTimeout parameter in milliseconds, recorded separately from the client deadline" + } + ], + [ + "extras", + "object", + { + "zh": "正文配置不传;资源配置 links:20、imageLinks:20;具名列表配置 richLinks:100", + "en": "Omitted for body tasks; resource tasks use links:20 and imageLinks:20; named-list tasks use richLinks:100" + } + ] + ], + "fields": [ + [ + "results[].text", + "string", + { + "zh": "正文 → content", + "en": "Body text → content" + } + ], + [ + "results[].title / url", + "string", + { + "zh": "标题与结果网址", + "en": "Title and result URL" + } + ], + [ + "results[].extras.richLinks", + "array", + { + "zh": "明确的 anchor—URL 对 → 具名详情入口", + "en": "Explicit anchor–URL pairs → named detail links" + } + ], + [ + "results[].extras.links / imageLinks", + "array", + { + "zh": "不带名称归属的链接和图片地址", + "en": "Links and image addresses without name ownership" + } + ], + [ + "statuses[]", + "array", + { + "zh": "逐网址的成功状态或错误", + "en": "Per-URL success status or error" + } + ], + [ + "costDollars", + "object", + { + "zh": "费用信息", + "en": "Cost information" + } + ] + ], + "request": { + "ordinary_body": { + "urls": [ + "https://example.com/" + ], + "text": { + "verbosity": "full" + }, + "maxAgeHours": 0, + "livecrawlTimeout": "" + }, + "named_link": { + "urls": [ + "https://example.com/" + ], + "text": { + "verbosity": "full" + }, + "maxAgeHours": 0, + "livecrawlTimeout": "", + "extras": { + "richLinks": 100 + } + } + }, + "source": "harness/web_fetch/providers/exa.py", + "notes": { + "zh": "已知 URL 使用 Contents 的 full 正文与 maxAgeHours=0 实时抓取;Search 先检索,Agent 和 summary 生成回答,不能代替指定 URL 的原文。资源题请求 links/imageLinks,具名列表只接纳 richLinks 的明确 anchor—URL 配对,不把正文标题与裸地址按位置猜配。livecrawlTimeout 和客户端截止取自调用记录。逐网址状态失败或没有正文时,不能仅凭 HTTP 200 判成功;原始响应只保存在私有证据。", + "en": "Known URLs use Contents with full text and maxAgeHours=0 for live crawling. Search discovers pages, while Agent and summary generate answers instead of source text from the specified URL. Resource tasks request links/imageLinks; named lists accept only explicit richLinks anchor–URL pairs, never positionally paired headings and bare URLs. livecrawlTimeout and the client deadline come from call records. Per-URL status failures or missing text are not successes merely because HTTP returned 200; raw responses remain private." + } + }, + "capabilities": { + "scope": "POST /contents", + "sources": [ + "exa" + ], + "web": { + "status": { + "zh": "支持", + "en": "Supported" + }, + "detail": "text → Markdown" + }, + "document": { + "status": { + "zh": "支持", + "en": "Supported" + }, + "detail": { + "zh": "PDF;Office 未声明", + "en": "PDF; Office not declared" + } + }, + "image": { + "status": { + "zh": "仅链接", + "en": "Link only" + }, + "detail": "extras.imageLinks" + }, + "av": { + "status": { + "zh": "未声明", + "en": "Not declared" + }, + "detail": { + "zh": "普通 links 不保证媒体流", + "en": "Ordinary links do not guarantee media streams" + } + }, + "file": { + "status": { + "zh": "未声明", + "en": "Not declared" + }, + "detail": { + "zh": "无原文件返回字段", + "en": "No field for returning the original file" + } + }, + "js": { + "status": { + "zh": "支持", + "en": "Supported" + }, + "detail": { + "zh": "Contents 动态页抓取", + "en": "Contents retrieval for dynamic pages" + } + }, + "input": { + "zh": "一组 URL;可配置 subpages,不是任意本地文件上传接口。", + "en": "A set of URLs; subpages is configurable, and this is not an arbitrary local file-upload interface." + }, + "structure": { + "zh": "text、highlights、summary;摘要可用 schema,含模型生成环节。", + "en": "text, highlights, and summary; summary supports a schema and includes a model-generation step." + }, + "access": { + "zh": "maxAgeHours 控制缓存/实时抓取,不提供登录成功保证。", + "en": "maxAgeHours controls cached/live crawling; it does not guarantee successful login." + }, + "notes": { + "zh": "extras.links 和 imageLinks 是不带名称归属的地址;richLinks 返回明确的 anchor—URL 对。PDF text 是解析后的文字,不是原 PDF。summary/highlights 与正文原文提取的语义不同,生成摘要不能作为独立 ground truth。", + "en": "extras.links and imageLinks are addresses without name ownership; richLinks returns explicit anchor–URL pairs. PDF text is parsed text, not the original PDF. summary/highlights have different semantics from verbatim body extraction, and generated summaries cannot serve as independent ground truth." + }, + "tested": { + "zh": "text.verbosity=full、maxAgeHours=0;资源题取 links/imageLinks,具名列表题取 richLinks 并只接收非空 anchor—URL 对。登记示例中没有调用摘要或高亮生成。", + "en": "Registered example: uses text.verbosity=full and maxAgeHours=0. Resource tasks collect links/imageLinks; named-list tasks collect richLinks and accept only non-empty anchor–URL pairs. Summary and highlight generation are not called." + } + }, + "access": { + "type": "hosted", + "env_vars": [ + "EXA_API_KEY" + ], + "links": [ + [ + { + "zh": "注册 / 登录", + "en": "Register / sign in" + }, + "https://dashboard.exa.ai/" + ], + [ + { + "zh": "获取凭据", + "en": "Get credentials" + }, + "https://dashboard.exa.ai/api-keys" + ] + ], + "account": { + "zh": "需要控制台账号;用量由 credits/预算控制。", + "en": "A console account is required; usage is governed by credits and budget." + }, + "configuration": { + "zh": "注册或登录 Exa 控制台,打开 API Keys,创建 key;如系统要求则设置预算。", + "en": "Register for or sign in to the Exa console, open API Keys, and create a key; set a budget if required by the system." + }, + "required": true, + "priority": { + "zh": "B · 已知 URL 抽取", + "en": "B · Known-URL extraction" + } + }, + "status": "implemented" + }, + "firecrawl": { + "label": { + "zh": "firecrawl", + "en": "firecrawl" + }, + "kind": "adapter", + "source_url": "https://docs.firecrawl.dev/features/scrape", + "profile": { + "kind": { + "zh": "托管 API", + "en": "Managed API" + }, + "description": { + "zh": "云端 Scrape 为正文与资源题返回 Markdown/地址,原文件题用 rawBase64 交付目标字节。", + "en": "Cloud-hosted Scrape returns Markdown/addresses for body and resource tasks, while original-file tasks use rawBase64 to deliver the target bytes." + } + }, + "adapter_types": [ + "firecrawl" + ], + "label_template": "{label} {version}", + "interface": { + "entry": "POST https://api.firecrawl.dev/v2/scrape", + "auth": "Authorization: Bearer ", + "params": [ + [ + "url", + { + "zh": "string · 必填", + "en": "string · required" + }, + { + "zh": "目标网页或文档", + "en": "Target web page or document" + } + ], + [ + "formats", + "array", + { + "zh": "正文/资源为 markdown、links、images;视频资源题另请求 video;原文件只使用 rawBase64", + "en": "Body and resource tasks request markdown, links, and images; video-resource tasks also request video; original-file tasks use rawBase64 alone" + } + ], + [ + "actions", + "array", + { + "zh": "动态顶层选择器用 wait action,随后执行预声明稳定延迟", + "en": "Use a wait action for a dynamic top-level selector, followed by the predeclared settle delay" + } + ], + [ + "timeout", + "number", + { + "zh": "不超过共享内部 HTTP 截止预算", + "en": "Does not exceed the shared inner HTTP deadline budget" + } + ], + [ + "maxAge", + "number", + { + "zh": "登记示例中 0,不复用旧缓存", + "en": "0 in this registered example; no old cache is reused" + } + ] + ], + "fields": [ + [ + "success", + "boolean", + { + "zh": "接口成功标记", + "en": "Interface success flag" + } + ], + [ + "data.markdown", + "string", + { + "zh": "正文 → content", + "en": "Body text → content" + } + ], + [ + "data.rawBase64", + "string", + { + "zh": "原文件题解码为目标 URL 字节并统一验收", + "en": "Decoded into the target URL bytes for uniform original-file validation" + } + ], + [ + "data.links / images / video", + "array / string", + { + "zh": "资源地址;video 是有时效的视频下载入口", + "en": "Resource URLs; video provides a time-limited video download URL" + } + ], + [ + "data.metadata", + "object", + { + "zh": "标题、源 URL、源站状态、积分等", + "en": "Title, source URL, origin status, credits, and related fields" + } + ], + [ + "error / reason", + "string", + { + "zh": "错误仅作私有响应证据;公开记录使用稳定分类", + "en": "Errors remain in private response evidence; public records use stable classifications" + } + ] + ], + "request": { + "ordinary_body": { + "url": "https://example.com/", + "formats": [ + "markdown" + ], + "maxAge": 0, + "timeout": 40500 + }, + "dynamic": { + "url": "https://example.com/", + "formats": [ + "markdown", + "links", + "images" + ], + "actions": [ + { + "type": "wait", + "selector": ".ready" + } + ], + "maxAge": 0 + }, + "video_resource": { + "url": "https://example.com/", + "formats": [ + "markdown", + "links", + "images", + "video" + ], + "maxAge": 0 + }, + "original_file": { + "url": "https://example.com/", + "formats": [ + "rawBase64" + ], + "parsers": [], + "maxAge": 0 + } + }, + "source": "harness/web_fetch/providers/firecrawl.py", + "notes": { + "zh": "已知单页使用 Scrape;Search、Crawl、Agent 会先找页面或跟随其他链接,不符合登记示例中给定 URL 的单次任务。动态题由 Actions 等待可表达的顶层选择器;匹配数量和 iframe 子文档条件仍可能不完整。video 返回的下载地址不等于原文件或画面理解。调用间隔由运行配置决定,不能当作单次请求耗时;完整 API 响应只保存在私有哈希绑定证据。", + "en": "Known single URLs use Scrape. Search, Crawl, and Agent discover pages or follow additional links instead of fulfilling this one-call URL task. Actions wait for top-level selectors where expressible; match counts and child-frame conditions may remain unenforced. A video download URL is not the original file or visual understanding. The interval between calls is controlled by run configuration and is separate from per-call latency; full API responses remain in private hash-bound evidence." + } + }, + "capabilities": { + "scope": "POST /v2/scrape", + "sources": [ + "fire", + "fire-api", + "fire-doc" + ], + "web": { + "status": { + "zh": "支持", + "en": "Supported" + }, + "detail": "Markdown / HTML" + }, + "document": { + "status": { + "zh": "支持", + "en": "Supported" + }, + "detail": { + "zh": "PDF(含 OCR)/ Office", + "en": "PDF (including OCR) / Office" + } + }, + "image": { + "status": { + "zh": "仅链接", + "en": "Link only" + }, + "detail": { + "zh": "images 返回图片 URL", + "en": "images returns image URLs" + } + }, + "av": { + "status": { + "zh": "需配置", + "en": "Configuration required" + }, + "detail": { + "zh": "audio / video 下载地址", + "en": "audio / video download URLs" + } + }, + "file": { + "status": { + "zh": "需配置", + "en": "Configuration required" + }, + "detail": { + "zh": "rawBase64 原始响应", + "en": "rawBase64 raw response" + } + }, + "js": { + "status": { + "zh": "支持", + "en": "Supported" + }, + "detail": { + "zh": "渲染与 Actions", + "en": "Rendering and Actions" + } + }, + "input": { + "zh": "Scrape 接收 URL;本地文件上传走另一个 /v2/parse 接口。", + "en": "Scrape accepts a URL; local file uploads use the separate /v2/parse endpoint." + }, + "structure": { + "zh": "可选 JSON schema、产品字段、截图等格式;不能都视为登记示例中已测。", + "en": "Optional formats include JSON schema, product fields, and screenshots; they cannot all be treated as tested in this registered example." + }, + "access": { + "zh": "可配置页面动作;站点授权、反爬与套餐限制仍会影响访问。", + "en": "Page actions are configurable; site authorization, anti-bot measures, and plan limits can still affect access." + }, + "notes": { + "zh": "rawBase64 必须单独请求,解码后是目标 URL 的原始 HTTP 响应体,不包含页面引用的所有资源。audio/video 针对支持的视频站点,返回有时效的下载 URL,不保证任意站点或原编码字节,也不是字幕转写/画面理解。文档解析覆盖 PDF、Word、Excel、PowerPoint 等。", + "en": "rawBase64 must be requested separately; after decoding, it is the raw HTTP response body for the target URL, not every resource referenced by the page. audio/video targets supported video sites and returns time-limited download URLs; it does not guarantee arbitrary sites or original encoded bytes, and it is neither subtitle transcription nor visual understanding. Document parsing covers PDF, Word, Excel, PowerPoint, and other formats." + }, + "tested": { + "zh": "正文与资源题请求 markdown、links、images;页面视频资源题另请求 video;原文件题只请求 rawBase64 并验收目标 URL 的解码字节。动态顶层选择器使用 Actions wait,随后执行预声明稳定延迟;选择器数量与 iframe 子文档条件仍标为未完整执行。未测试 audio、JSON、截图或 Parse。", + "en": "Body and resource tasks request markdown, links, and images; page-video resources additionally request video. Original-file tasks use rawBase64 alone and validate the target URL's decoded bytes. Top-level dynamic selectors use Actions wait followed by the declared settle delay; selector counts and child-frame conditions remain incompletely enforced. Audio, JSON, screenshots, and Parse were not tested." + } + }, + "access": { + "type": "hosted", + "env_vars": [ + "FIRECRAWL_API_KEY" + ], + "links": [ + [ + { + "zh": "注册 / 登录", + "en": "Register / sign in" + }, + "https://www.firecrawl.dev/signin" + ], + [ + { + "zh": "获取凭据", + "en": "Get credentials" + }, + "https://www.firecrawl.dev/app/api-keys" + ] + ], + "account": { + "zh": "存在无 key 的限流层;可复现评测应使用免费账号。", + "en": "A rate-limited keyless tier exists; reproducible evaluation should use a free account." + }, + "configuration": { + "zh": "注册账号,打开 Firecrawl 控制台 API Keys 页面,创建以 fc- 开头的 key。", + "en": "Register an account, open the API Keys page in the Firecrawl console, and create a key beginning with fc-." + }, + "required": true, + "priority": { + "zh": "A · 核心评测", + "en": "A · Core evaluation" + } + }, + "status": "implemented" + }, + "fixture": { + "label": { + "zh": "fixture", + "en": "fixture" + }, + "kind": "adapter", + "source_url": "https://github.com/lexmount/browser-eval", + "adapter_types": [ + "fixture" + ], + "label_template": "{label} {version}", + "access": { + "type": "local", + "env_vars": [], + "links": [ + [ + { + "zh": "注册 / 登录", + "en": "Register / sign in" + }, + "https://github.com/lexmount/browser-eval" + ] + ], + "account": { + "zh": "不需要账号或环境变量。", + "en": "No account or environment variables required." + }, + "configuration": { + "zh": "仓库内测试适配器;只用于确定性回归测试。", + "en": "Repository test adapter; used only for deterministic regression tests." + } + }, + "status": "implemented" + }, + "kitesurf": { + "label": { + "zh": "Kitesurf", + "en": "Kitesurf" + }, + "kind": "browser", + "source_url": "https://developers.cloudflare.com/browser-run/kitesurf/", + "profile": { + "kind": { + "zh": "远程账号 API", + "en": "Remote account API" + }, + "description": { + "zh": "Cloudflare Browser Run 账户级 Markdown 端点,指定 browser=kitesurf,直接返回 Markdown。", + "en": "Cloudflare Browser Run account-level Markdown endpoint with browser=kitesurf, returning Markdown directly." + } + }, + "adapter_types": [], + "label_template": "{label} {version}", + "interface": { + "entry": "POST …/browser-rendering/markdown?browser=kitesurf", + "auth": "Authorization: Bearer ", + "params": [ + [ + "url", + "string", + { + "zh": "唯一任务输入", + "en": "Only task input" + } + ], + [ + "gotoOptions.waitUntil", + "string", + "networkidle2" + ], + [ + "browser", + "query", + "kitesurf" + ], + [ + "timeout", + "seconds", + { + "zh": "客户端端到端截止取自运行配置", + "en": "Client-side end-to-end deadline from run configuration" + } + ] + ], + "fields": [ + [ + "result", + "string", + { + "zh": "Cloudflare 原生 Markdown → content", + "en": "Cloudflare native Markdown → content" + } + ], + [ + "success / errors", + "boolean / array", + { + "zh": "接口状态与错误", + "en": "Interface status and errors" + } + ], + [ + "browser_ms_used", + "number-like", + { + "zh": "服务端浏览器计费时间;不等于客户端耗时", + "en": "Billable server-side browser time; not client-observed latency" + } + ] + ], + "request": { + "url": "https://example.com/", + "gotoOptions": { + "waitUntil": "networkidle2" + } + }, + "source": "harness/web_fetch/providers/cloudflare.py", + "notes": { + "zh": "网页正文、嵌入文档以及网页中的图片或视频信息都使用指定 Kitesurf 的 Browser Run 原生 Markdown。直接文档也使用官方 Markdown 接口,直接文件通过官方 CDP 获取原始响应。Workers AI 文档转换不计入 Kitesurf 成绩。客户端耗时包含公网往返、排队与执行。", + "en": "Webpage body text, embedded documents, and image or video information use native Browser Run Markdown from the specified Kitesurf. Direct documents also use the official Markdown interface; direct files use official CDP to obtain original responses. Workers AI document conversion is excluded from Kitesurf results. Client latency includes public-network round trips, queuing, and execution." + } + }, + "capabilities": { + "scope": { + "zh": "Cloudflare 账户级 /markdown 原生输出", + "en": "Native output from Cloudflare's account-level /markdown endpoint" + }, + "sources": [ + "headless-kitesurf" + ], + "web": { + "status": { + "zh": "支持", + "en": "Supported" + }, + "detail": { + "zh": "原生 Markdown", + "en": "Native Markdown" + } + }, + "document": { + "status": { + "zh": "未声明", + "en": "Not declared" + }, + "detail": { + "zh": "登记示例中适配器没有 PDF / Office 解析器,不据此推断产品能力", + "en": "Registered example: the adapter has no PDF / Office parser; no broader product limitation is inferred" + } + }, + "image": { + "status": { + "zh": "仅链接", + "en": "Link only" + }, + "detail": { + "zh": "取决于 DOM 与转换器保留", + "en": "Depends on preservation by the DOM and converter" + } + }, + "av": { + "status": { + "zh": "未声明", + "en": "Not declared" + }, + "detail": { + "zh": "登记示例中没有媒体解析,不据此推断产品能力", + "en": "Registered example: the adapter performs no media parsing; no broader product limitation is inferred" + } + }, + "file": { + "status": { + "zh": "未声明", + "en": "Not declared" + }, + "detail": { + "zh": "登记示例中异常二进制输出仅保留证据", + "en": "Anomalous binary output from this registered example is retained as evidence only" + } + }, + "js": { + "status": { + "zh": "支持", + "en": "Supported" + }, + "detail": { + "zh": "兼容范围看逐例结果", + "en": "See individual results for compatibility coverage" + } + }, + "input": { + "zh": "给定公开 HTTP(S) URL;不登录、不点击、不滚动。", + "en": "A public HTTP(S) URL; no login, clicks, or scrolling." + }, + "structure": { + "zh": "统一 Markdown;不要求引擎按评分答案生成结构。", + "en": "Normalized Markdown; engines are not required to generate structures tailored to the scoring answer." + }, + "access": { + "zh": "每次新会话;Kitesurf 使用 Cloudflare 账号版远程 Browser Run API,其余为本地进程。", + "en": "A fresh session for every call; Kitesurf uses the account-level remote Cloudflare Browser Run API, while the others use local processes." + }, + "notes": { + "zh": "这里只声明登记示例中调用模式。没有启用文档解析、截图、下载、OCR 等模式,不能从登记示例中失败推断产品没有这些能力。", + "en": "Only the invocation mode used in this registered example is described. Document parsing, screenshots, downloads, OCR, and other modes were not enabled, so failures here do not establish that the product lacks those capabilities." + }, + "tested": { + "zh": "直接使用账户级 /markdown;等待条件和客户端预算以调用记录为准。", + "en": "Use the account-level /markdown endpoint; wait conditions and the client deadline come from call records." + } + }, + "access": { + "account": { + "zh": "需要开通 Browser Rendering / Browser Run 的 Cloudflare 账号及 API Token。", + "en": "Requires a Cloudflare account with Browser Rendering / Browser Run enabled and an API token." + }, + "configuration": { + "zh": "在仓库本地 .env 配置 与 ;报告和示例只保留占位符。", + "en": "Configure and in the repository-local .env; reports and examples retain placeholders only." + }, + "env_vars": [ + "CLOUDFLARE_ACCOUNT_ID", + "CLOUDFLARE_API_TOKEN" + ], + "links": [ + [ + { + "zh": "Cloudflare 控制台", + "en": "Cloudflare dashboard" + }, + "https://dash.cloudflare.com/" + ], + [ + { + "zh": "Kitesurf 接入说明", + "en": "Kitesurf integration guide" + }, + "https://developers.cloudflare.com/browser-run/kitesurf/" + ], + [ + { + "zh": "Markdown 端点", + "en": "Markdown endpoint" + }, + "https://developers.cloudflare.com/browser-run/quick-actions/markdown-endpoint/" + ] + ], + "type": "hosted" + }, + "version_policy": "hosted", + "version_unavailable": { + "zh": "Beta · 托管后端版本未公开", + "en": "Beta · hosted backend version not disclosed" + }, + "routes": { + "capabilities": { + "zh": "官方 Browser Run /markdown?browser=kitesurf 返回 Markdown;这是托管服务的转换能力,不是客户端共同转换。", + "en": "Official Browser Run /markdown?browser=kitesurf returns Markdown through hosted conversion, not shared client-side conversion." + }, + "markdown": { + "zh": "/markdown?browser=kitesurf → 官方托管转换 → Markdown", + "en": "/markdown?browser=kitesurf → official hosted conversion → Markdown" + }, + "original": { + "zh": "指定 Kitesurf 的官方 CDP 原始响应", + "en": "Official CDP raw response with Kitesurf selected" + }, + "resource": { + "zh": "/markdown?browser=kitesurf → 原生 Markdown;评分器只从交付内容中核对资源地址", + "en": "/markdown?browser=kitesurf → native Markdown; resource URLs are checked in the delivered output" + }, + "runtime": { + "zh": "指定 Kitesurf 的官方 CDP", + "en": "Official CDP with Kitesurf selected" + }, + "waits": { + "zh": "Markdown API 等待 networkidle2", + "en": "Markdown API waits for networkidle2" + } + }, + "status": "implemented", + "observed_routes": [ + { + "metadata_equals": { + "browser": "kitesurf", + "extractor": "cloudflare_native_markdown" + }, + "text": { + "zh": "Cloudflare /markdown?browser=kitesurf → 托管原生 Markdown", + "en": "Cloudflare /markdown?browser=kitesurf → hosted native Markdown" + }, + "compact_text": { + "zh": "原生 Markdown", + "en": "Native Markdown" + }, + "mode": "markdown" + }, + { + "metadata_equals": { + "output_contract": "original_file_bytes", + "extractor": "kitesurf_cdp_response" + }, + "text": { + "zh": "Kitesurf 官方 CDP 原始响应 → 原始文件字节", + "en": "Kitesurf official CDP original response → original file bytes" + }, + "compact_text": { + "zh": "Kitesurf 官方 CDP 原始响应 → 文件字节", + "en": "Kitesurf official CDP original response → file bytes" + }, + "mode": "file" + } + ], + "extends": "native_markdown_browser" + }, + "langchain-webbase": { + "label": { + "zh": "langchain-webbase", + "en": "langchain-webbase" + }, + "kind": "adapter", + "source_url": "https://reference.langchain.com/python/langchain-community/document_loaders/web_base/WebBaseLoader", + "profile": { + "kind": { + "zh": "本地开源库", + "en": "Local open-source library" + }, + "description": { + "zh": "WebBaseLoader 使用 HTTP 与 BeautifulSoup 取得网页文字。", + "en": "WebBaseLoader uses HTTP and BeautifulSoup to retrieve webpage text." + } + }, + "adapter_types": [ + "langchain_webbase" + ], + "label_template": "{label} {version}", + "interface": { + "entry": "Python · WebBaseLoader(...).load()", + "auth": { + "zh": "本地开源库;无供应商 API Key", + "en": "Local open-source library; no provider API key" + }, + "params": [ + [ + "web_paths", + "tuple[str]", + { + "zh": "登记示例中仅一个目标 URL", + "en": "One target URL in this registered example" + } + ], + [ + "requests_kwargs.timeout", + "number", + { + "zh": "timeout 参数,单位秒", + "en": "timeout parameter in seconds" + } + ], + [ + "raise_for_status", + "boolean", + { + "zh": "登记示例中 true,HTTP 错误抛异常", + "en": "true in this registered example; raise on HTTP errors" + } + ], + [ + "continue_on_failure", + "boolean", + { + "zh": "登记示例中 false", + "en": "false in this registered example" + } + ] + ], + "fields": [ + [ + "Document.page_content", + "string", + { + "zh": "各 Document 正文合并 → content", + "en": "Merged body text from each Document → content" + } + ], + [ + "Document.metadata", + "dict", + { + "zh": "source、title、language 等", + "en": "source, title, language, and related fields" + } + ], + [ + "HTTP / Python exception", + "exception", + { + "zh": "转成失败记录及错误说明", + "en": "Converted to a failed record with an error explanation" + } + ] + ], + "request": "loader = WebBaseLoader(\n web_paths=(\"https://example.com/\",),\n requests_kwargs={\"timeout\": TIMEOUT_SECONDS},\n raise_for_status=True,\n continue_on_failure=False,\n)\ndocs = loader.load()", + "source": "harness/web_fetch/workers/langchain_webbase_worker.py", + "notes": { + "zh": "不运行网页 JavaScript;登记示例中没有把它替换成其他 LangChain loader。", + "en": "Web-page JavaScript is not run. This benchmark does not replace it with another LangChain loader." + } + }, + "capabilities": { + "scope": "WebBaseLoader.load", + "sources": [ + "lc", + "lc-loaders" + ], + "web": { + "status": { + "zh": "支持", + "en": "Supported" + }, + "detail": { + "zh": "BeautifulSoup → 纯文本", + "en": "BeautifulSoup → plain text" + } + }, + "document": { + "status": { + "zh": "无此能力", + "en": "Not supported" + }, + "detail": { + "zh": "PDF / Office 需其他 loader", + "en": "PDF / Office requires another loader" + } + }, + "image": { + "status": { + "zh": "无此能力", + "en": "Not supported" + }, + "detail": { + "zh": "load 不返回 src", + "en": "load does not return src" + } + }, + "av": { + "status": { + "zh": "无此能力", + "en": "Not supported" + }, + "detail": { + "zh": "load 不返回媒体字段", + "en": "load does not return media fields" + } + }, + "file": { + "status": { + "zh": "无此能力", + "en": "Not supported" + }, + "detail": { + "zh": "Document 不是原始文件", + "en": "Document is not the original file" + } + }, + "js": { + "status": { + "zh": "无此能力", + "en": "Not supported" + }, + "detail": { + "zh": "HTTP,不运行 JS", + "en": "HTTP without JavaScript" + } + }, + "input": { + "zh": "单个或多个网页 URL;load 返回 Document 对象。", + "en": "One or more webpage URLs; load returns Document objects." + }, + "structure": { + "zh": "page_content + 页面元数据;scrape 可另取 BeautifulSoup 自行提取属性。", + "en": "page_content + page metadata; scrape can separately return BeautifulSoup for custom attribute extraction." + }, + "access": { + "zh": "可传入 requests Session / headers;不自动执行登录交互。", + "en": "Accepts a requests Session / headers; does not automate login interactions." + }, + "notes": { + "zh": "get_text 不保留 href/src 属性;页面若本身写出 URL,文字中仍可能出现地址。LangChain 有独立 PDF、Word 和浏览器加载器,但不能合并成 WebBaseLoader.load 的能力。", + "en": "get_text does not preserve href/src attributes; addresses may still appear in the text when the page itself prints URLs. LangChain has separate PDF, Word, and browser loaders, but their capabilities cannot be attributed to WebBaseLoader.load." + }, + "tested": { + "zh": "只调用 load 读取正文;没有额外 soup 解析、PDF loader 或浏览器渲染。", + "en": "Only load was called to read body content; no additional soup parsing, PDF loader, or browser rendering was used." + } + }, + "access": { + "type": "local", + "env_vars": [ + "OSS_PYTHON" + ], + "links": [ + [ + { + "zh": "注册 / 登录", + "en": "Register / sign in" + }, + "https://reference.langchain.com/python/langchain-community/document_loaders/web_base/WebBaseLoader" + ], + [ + { + "zh": "获取凭据", + "en": "Get credentials" + }, + "https://python.langchain.com/docs/how_to/document_loader_web/" + ] + ], + "account": { + "zh": "本地开源库;不需要供应商账号。", + "en": "Local open-source library; no provider account is required." + }, + "configuration": { + "zh": "使用仓库冻结的 Python 环境运行;OSS_PYTHON 只在显式覆盖 worker 解释器时设置。", + "en": "Run with the repository's frozen Python environment; set OSS_PYTHON only when explicitly overriding the worker interpreter." + } + }, + "status": "implemented" + }, + "lexmount-cn": { + "label": { + "zh": "lexmount-cn", + "en": "lexmount-cn" + }, + "kind": "adapter", + "source_url": "https://browser.lexmount.com/en/docs/webfetch", + "profile": { + "kind": { + "zh": "托管 API", + "en": "Managed API" + }, + "description": { + "zh": "Lexmount 中国区 API;与国际 API 分开实测,不重复计算品牌采用。", + "en": "Lexmount China API; tested separately from the international API without double-counting brand adoption." + } + }, + "adapter_types": [ + "lexmount_cn" + ], + "label_template": "{label} {version}", + "interface": { + "entry": "POST https://api.lexmount.cn/v1/extract", + "auth": "X-API-Key: ;X-Project-Id: ", + "params": [ + [ + "extract.url", + { + "zh": "string · 必填", + "en": "string · required" + }, + { + "zh": "目标网页 URL", + "en": "Target web-page URL" + } + ], + [ + "extract.engine", + { + "zh": "string · 可选", + "en": "string · optional" + }, + { + "zh": "指定提取引擎;登记示例中不传,使用服务端选择", + "en": "Selects the extraction engine; omitted in this registered example so the server chooses" + } + ] + ], + "fields": [ + [ + "result.main_text", + "string", + { + "zh": "正文 → content", + "en": "Body text → content" + } + ], + [ + "result.title / final_url", + "string", + { + "zh": "页面标题 / 最终地址", + "en": "Page title / final URL" + } + ], + [ + "result.links / images", + "array", + { + "zh": "页面链接和图片 → provider_metadata", + "en": "Page links and images → provider_metadata" + } + ], + [ + "result.status_code", + "number", + { + "zh": "源站状态;与 HTTP API 状态分开", + "en": "Origin status, separate from HTTP API status" + } + ], + [ + "metadata.server_elapsed_ms", + "number", + { + "zh": "服务端耗时", + "en": "Server-side latency" + } + ] + ], + "request": { + "extract": { + "url": "https://example.com/" + } + }, + "source": "harness/web_fetch/providers/lexmount.py", + "notes": { + "zh": "登记示例中为实时提取。中国区与国际区分开调用、分开计分。", + "en": "Registered example: uses live extraction. The China and international endpoints are called and scored separately." + } + }, + "capabilities": { + "scope": { + "zh": "POST /v1/extract · 中国区", + "en": "POST /v1/extract · China region" + }, + "sources": [ + "lex", + "lex-overview" + ], + "web": { + "status": { + "zh": "支持", + "en": "Supported" + }, + "detail": "main_text" + }, + "document": { + "status": { + "zh": "未声明", + "en": "Not declared" + }, + "detail": { + "zh": "未列 PDF / Office 契约", + "en": "No PDF / Office contract listed" + } + }, + "image": { + "status": { + "zh": "仅链接", + "en": "Link only" + }, + "detail": "images" + }, + "av": { + "status": { + "zh": "未声明", + "en": "Not declared" + }, + "detail": { + "zh": "无专用媒体字段", + "en": "No dedicated media fields" + } + }, + "file": { + "status": { + "zh": "未声明", + "en": "Not declared" + }, + "detail": { + "zh": "无原文件返回契约", + "en": "No contract for returning the original file" + } + }, + "js": { + "status": { + "zh": "支持", + "en": "Supported" + }, + "detail": { + "zh": "按需渲染", + "en": "Rendering on demand" + } + }, + "input": { + "zh": "URL 或已捕获的 dom_id;/v1/dom/dump 为独立 DOM 捕获接口。", + "en": "URL or a captured dom_id; /v1/dom/dump is a separate DOM-capture endpoint." + }, + "structure": { + "zh": "title、author、links、images 等固定字段;可选 trace/raw_dom。", + "en": "Fixed fields such as title, author, links, and images; optional trace/raw_dom." + }, + "access": { + "zh": "API Key + Project ID 是服务鉴权,不是源站登录凭据;公开文档面向公共网页。", + "en": "API Key + Project ID authenticate the service, not the source site; the public documentation targets public webpages." + }, + "notes": { + "zh": "images 是资源引用,不是图片字节或视觉理解;links 可能包含文档或视频入口,但不保证提取视频流。PDF/Office、音视频转写和通用二进制下载均未在所核对的 Extract 契约中明确声明,不能写成确定不支持。", + "en": "images are resource references, not image bytes or visual understanding; links may include document or video entry points but do not guarantee video-stream extraction. PDF/Office, audio/video transcription, and general binary downloads are not explicitly declared in the reviewed Extract contract, so they cannot be described as definitively unsupported." + }, + "tested": { + "zh": "国际区与中国区共用公开契约,但部署与实测分开;这里不推断两区内部引擎完全相同。", + "en": "The international and China regions share a public contract, but their deployments and measurements are separate; this does not assume that their internal engines are identical." + } + }, + "access": { + "type": "hosted", + "env_vars": [ + "LEXMOUNT_CN_API_KEY", + "LEXMOUNT_CN_PROJECT_ID" + ], + "links": [ + [ + { + "zh": "注册 / 登录", + "en": "Register / sign in" + }, + "https://browser.lexmount.cn/" + ] + ], + "account": { + "zh": "API key 与 Project ID 必须来自同一个中国区项目。", + "en": "The API key and Project ID must come from the same China-region project." + }, + "configuration": { + "zh": "登录中国区控制台,复制同一项目下的 API key 与 Project ID;本评测使用 api.lexmount.cn。", + "en": "Sign in to the China-region console and copy the API key and Project ID from the same project; this evaluation uses api.lexmount.cn." + }, + "required": true, + "priority": { + "zh": "A · 核心评测", + "en": "A · Core evaluation" + } + }, + "status": "implemented" + }, + "lexmount-global": { + "label": { + "zh": "lexmount-global", + "en": "lexmount-global" + }, + "kind": "adapter", + "source_url": "https://browser.lexmount.com/en/docs/webfetch", + "profile": { + "kind": { + "zh": "托管 API", + "en": "Managed API" + }, + "description": { + "zh": "Lexmount 国际 API,返回网页主正文与元数据。", + "en": "Lexmount international API, returning the webpage's main body and metadata." + } + }, + "adapter_types": [ + "lexmount" + ], + "label_template": "{label} {version}", + "interface": { + "entry": "POST https://api.lexmount.com/v1/extract", + "auth": "X-API-Key: ;X-Project-Id: ", + "params": [ + [ + "extract.url", + { + "zh": "string · 必填", + "en": "string · required" + }, + { + "zh": "目标网页 URL", + "en": "Target web-page URL" + } + ], + [ + "extract.engine", + { + "zh": "string · 可选", + "en": "string · optional" + }, + { + "zh": "指定提取引擎;登记示例中不传,使用服务端选择", + "en": "Selects the extraction engine; omitted in this registered example so the server chooses" + } + ] + ], + "fields": [ + [ + "result.main_text", + "string", + { + "zh": "正文 → content", + "en": "Body text → content" + } + ], + [ + "result.title / final_url", + "string", + { + "zh": "页面标题 / 最终地址", + "en": "Page title / final URL" + } + ], + [ + "result.links / images", + "array", + { + "zh": "页面链接和图片 → provider_metadata", + "en": "Page links and images → provider_metadata" + } + ], + [ + "result.status_code", + "number", + { + "zh": "源站状态;与 HTTP API 状态分开", + "en": "Origin status, separate from HTTP API status" + } + ], + [ + "metadata.server_elapsed_ms", + "number", + { + "zh": "服务端耗时", + "en": "Server-side latency" + } + ] + ], + "request": { + "extract": { + "url": "https://example.com/" + } + }, + "source": "harness/web_fetch/providers/lexmount.py", + "notes": { + "zh": "登记示例中为实时提取。中国区与国际区分开调用、分开计分。", + "en": "Registered example: uses live extraction. The China and international endpoints are called and scored separately." + } + }, + "capabilities": { + "scope": { + "zh": "POST /v1/extract · 国际区", + "en": "POST /v1/extract · International region" + }, + "sources": [ + "lex", + "lex-overview" + ], + "web": { + "status": { + "zh": "支持", + "en": "Supported" + }, + "detail": "main_text" + }, + "document": { + "status": { + "zh": "未声明", + "en": "Not declared" + }, + "detail": { + "zh": "未列 PDF / Office 契约", + "en": "No PDF / Office contract listed" + } + }, + "image": { + "status": { + "zh": "仅链接", + "en": "Link only" + }, + "detail": "images" + }, + "av": { + "status": { + "zh": "未声明", + "en": "Not declared" + }, + "detail": { + "zh": "无专用媒体字段", + "en": "No dedicated media fields" + } + }, + "file": { + "status": { + "zh": "未声明", + "en": "Not declared" + }, + "detail": { + "zh": "无原文件返回契约", + "en": "No contract for returning the original file" + } + }, + "js": { + "status": { + "zh": "支持", + "en": "Supported" + }, + "detail": { + "zh": "按需渲染", + "en": "Rendering on demand" + } + }, + "input": { + "zh": "URL 或已捕获的 dom_id;/v1/dom/dump 为独立 DOM 捕获接口。", + "en": "URL or a captured dom_id; /v1/dom/dump is a separate DOM-capture endpoint." + }, + "structure": { + "zh": "title、author、links、images 等固定字段;可选 trace/raw_dom。", + "en": "Fixed fields such as title, author, links, and images; optional trace/raw_dom." + }, + "access": { + "zh": "API Key + Project ID 是服务鉴权,不是源站登录凭据;公开文档面向公共网页。", + "en": "API Key + Project ID authenticate the service, not the source site; the public documentation targets public webpages." + }, + "notes": { + "zh": "images 是资源引用,不是图片字节或视觉理解;links 可能包含文档或视频入口,但不保证提取视频流。PDF/Office、音视频转写和通用二进制下载均未在所核对的 Extract 契约中明确声明,不能写成确定不支持。", + "en": "images are resource references, not image bytes or visual understanding; links may include document or video entry points but do not guarantee video-stream extraction. PDF/Office, audio/video transcription, and general binary downloads are not explicitly declared in the reviewed Extract contract, so they cannot be described as definitively unsupported." + }, + "tested": { + "zh": "国际区与中国区共用公开契约,但部署与实测分开;这里不推断两区内部引擎完全相同。", + "en": "The international and China regions share a public contract, but their deployments and measurements are separate; this does not assume that their internal engines are identical." + } + }, + "access": { + "type": "hosted", + "env_vars": [ + "LEXMOUNT_API_KEY", + "LEXMOUNT_PROJECT_ID" + ], + "links": [ + [ + { + "zh": "注册 / 登录", + "en": "Register / sign in" + }, + "https://browser.lexmount.com/settings/api-keys" + ] + ], + "account": { + "zh": "内测准入;API key 与 Project ID 必须来自同一个 Global 项目。", + "en": "Private-beta access; the API key and Project ID must come from the same Global project." + }, + "configuration": { + "zh": "登录后打开 Settings → API Keys,创建或复制 API key,并复制对应 Project ID;本评测使用 api.lexmount.com。", + "en": "After signing in, open Settings → API Keys, create or copy an API key, and copy the corresponding Project ID; this evaluation uses api.lexmount.com." + }, + "required": true, + "priority": { + "zh": "A · 核心评测", + "en": "A · Core evaluation" + } + }, + "status": "implemented" + }, + "lightpanda": { + "label": { + "zh": "Lightpanda", + "en": "Lightpanda" + }, + "kind": "browser", + "source_url": "https://github.com/lightpanda-io/browser", + "profile": { + "kind": { + "zh": "本地浏览器 CLI", + "en": "Local browser CLI" + }, + "description": { + "zh": "Lightpanda fetch --dump markdown,直接输出浏览器原生 Markdown。", + "en": "Lightpanda fetch --dump markdown returns browser-native Markdown directly." + } + }, + "adapter_types": [], + "label_template": "{label} {version}", + "interface": { + "entry": "CLI · lightpanda fetch --dump markdown", + "auth": { + "zh": "本地独立浏览器;不需要供应商 API Key", + "en": "Independent local browser; no provider API key required" + }, + "params": [ + [ + "URL", + "string", + { + "zh": "唯一任务输入", + "en": "Only task input" + } + ], + [ + "--dump", + "string", + "markdown" + ], + [ + "wait_until", + "string", + "done" + ], + [ + "timeout", + "seconds", + { + "zh": "外层截止取自运行配置;CLI 参数按各自单位换算", + "en": "Outer deadline from run configuration; CLI parameters use their native units" + } + ] + ], + "fields": [ + [ + "stdout", + "UTF-8 string", + { + "zh": "浏览器原生 Markdown → content", + "en": "Browser-native Markdown → content" + } + ], + [ + "native_markdown_ms", + "number", + { + "zh": "CLI 调用及原生序列化阶段耗时", + "en": "CLI call and native serialization latency" + } + ], + [ + "browser_stderr", + "string", + { + "zh": "浏览器诊断;不进入正文", + "en": "Browser diagnostics; excluded from body text" + } + ] + ], + "request": { + "url": "https://example.com/", + "engine": "lightpanda", + "timeout": "", + "wait_until": "done", + "dump": "markdown" + }, + "source": "harness/web_fetch/providers/headless_worker.py", + "notes": { + "zh": "网页正文、嵌入文档以及网页中的图片或视频信息都使用浏览器原生 Markdown。直接文件使用浏览器已有的原始响应接口。不在失败后切换输出方式、调用共同 DOM 脚本,或请求外部服务补正文。接口未提供 HTTP 状态或最终网址时保留空值。", + "en": "Webpage body text, embedded documents, and image or video information use the browser's native Markdown. Direct files use its existing original-response interface. Failures do not trigger output switching, a shared DOM script, or an external service to supply missing text. HTTP status and final URL remain empty when the interface does not provide them." + } + }, + "capabilities": { + "scope": { + "zh": "lightpanda fetch --dump markdown 原生输出", + "en": "Native output from lightpanda fetch --dump markdown" + }, + "sources": [ + "headless-lightpanda" + ], + "web": { + "status": { + "zh": "支持", + "en": "Supported" + }, + "detail": { + "zh": "原生 Markdown", + "en": "Native Markdown" + } + }, + "document": { + "status": { + "zh": "未声明", + "en": "Not declared" + }, + "detail": { + "zh": "登记示例中适配器没有 PDF / Office 解析器,不据此推断产品能力", + "en": "Registered example: the adapter has no PDF / Office parser; no broader product limitation is inferred" + } + }, + "image": { + "status": { + "zh": "仅链接", + "en": "Link only" + }, + "detail": { + "zh": "取决于 DOM 与转换器保留", + "en": "Depends on preservation by the DOM and converter" + } + }, + "av": { + "status": { + "zh": "未声明", + "en": "Not declared" + }, + "detail": { + "zh": "登记示例中没有媒体解析,不据此推断产品能力", + "en": "Registered example: the adapter performs no media parsing; no broader product limitation is inferred" + } + }, + "file": { + "status": { + "zh": "未声明", + "en": "Not declared" + }, + "detail": { + "zh": "登记示例中异常二进制输出仅保留证据", + "en": "Anomalous binary output from this registered example is retained as evidence only" + } + }, + "js": { + "status": { + "zh": "支持", + "en": "Supported" + }, + "detail": { + "zh": "兼容范围看逐例结果", + "en": "See individual results for compatibility coverage" + } + }, + "input": { + "zh": "给定公开 HTTP(S) URL;不登录、不点击、不滚动。", + "en": "A public HTTP(S) URL; no login, clicks, or scrolling." + }, + "structure": { + "zh": "统一 Markdown;不要求引擎按评分答案生成结构。", + "en": "Normalized Markdown; engines are not required to generate structures tailored to the scoring answer." + }, + "access": { + "zh": "每次新会话;Kitesurf 使用 Cloudflare 账号版远程 Browser Run API,其余为本地进程。", + "en": "A fresh session for every call; Kitesurf uses the account-level remote Cloudflare Browser Run API, while the others use local processes." + }, + "notes": { + "zh": "这里只声明登记示例中调用模式。没有启用文档解析、截图、下载、OCR 等模式,不能从登记示例中失败推断产品没有这些能力。", + "en": "Only the invocation mode used in this registered example is described. Document parsing, screenshots, downloads, OCR, and other modes were not enabled, so failures here do not establish that the product lacks those capabilities." + }, + "tested": { + "zh": "直接使用原生 Markdown;CLI 等待和外层预算取自调用记录。", + "en": "Use native Markdown directly; CLI waiting and the outer deadline come from call records." + } + }, + "access": { + "account": { + "zh": "不需要供应商账号或 API Key。", + "en": "No provider account or API key is required." + }, + "configuration": { + "zh": "从官方项目获取二进制,配置 LIGHTPANDA_BINARY;不使用托管服务凭据。", + "en": "Obtain the binary from the official project and configure LIGHTPANDA_BINARY; no managed-service credentials are used." + }, + "env_vars": [ + "LIGHTPANDA_BINARY" + ], + "links": [ + [ + { + "zh": "官方项目与安装说明", + "en": "Official project and installation guide" + }, + "https://github.com/lightpanda-io/browser" + ] + ], + "type": "local" + }, + "version_policy": "native", + "routes": { + "capabilities": { + "zh": "内置 Markdown 转换;同一任务大类只使用 CLI --dump markdown --json,不传逐题选择器、数量、延迟或 iframe 参数。", + "en": "Built-in Markdown conversion; each task family uses CLI --dump markdown --json without per-task selectors, counts, delays or iframe parameters." + }, + "markdown": { + "zh": "CLI fetch --dump markdown --json → 内置转换 → 解包 Markdown", + "en": "CLI fetch --dump markdown --json → built-in conversion → unpack Markdown" + }, + "original": { + "zh": "原生 CDP Response.body()", + "en": "Native CDP Response.body()" + }, + "resource": { + "zh": "CLI fetch --dump markdown --json → 原生 Markdown;评分器只从交付内容中核对资源地址", + "en": "CLI fetch --dump markdown --json → native Markdown; resource URLs are checked in the delivered output" + }, + "runtime": { + "zh": "原生 CDP", + "en": "Native CDP" + }, + "waits": { + "zh": "Markdown CLI 等待 done", + "en": "Markdown CLI waits for done" + } + }, + "status": "implemented", + "observed_routes": [ + { + "metadata_equals": { + "extractor": "lightpanda_native_markdown" + }, + "text": { + "zh": "Lightpanda --dump markdown{flags} → 原生 Markdown", + "en": "Lightpanda --dump markdown{flags} → native Markdown" + }, + "fragments": { + "flags": [ + { + "metadata_equals": { + "native_metadata.format": "lightpanda_json" + }, + "text": { + "zh": " --json", + "en": " --json" + } + } + ] + }, + "compact_text": { + "zh": "原生 Markdown", + "en": "Native Markdown" + }, + "mode": "markdown" + }, + { + "metadata_equals": { + "output_contract": "original_file_bytes", + "extractor": "lightpanda_cdp_response" + }, + "text": { + "zh": "Lightpanda CDP 原始响应 → 原始文件字节", + "en": "Lightpanda CDP original response → original file bytes" + }, + "compact_text": { + "zh": "Lightpanda CDP 原始响应 → 文件字节", + "en": "Lightpanda CDP original response → file bytes" + }, + "mode": "file" + } + ], + "interface_variants": [ + { + "metadata_equals": { + "native_metadata.format": "lightpanda_json" + }, + "command": "lightpanda fetch URL --dump markdown --json", + "remove_request_fields": [], + "remove_parameters": [], + "notes": { + "zh": "Lightpanda 解包同一次调用返回的原生 Markdown;Obscura 执行固定哈希的官方 Markdown 脚本。网页正文、嵌入文档以及网页中的图片或视频信息均保留该原生输出。直接文件分别使用 Lightpanda CDP 与 Obscura --dump original。评测端不再加工 DOM 或外部解析文档;获取与输出处理均计入耗时。", + "en": "Lightpanda unwraps native Markdown from the same call; Obscura executes the hash-bound official Markdown script. Page text, embedded documents and embedded image or video information retain that native output. Direct files use Lightpanda CDP or Obscura --dump original. The harness performs no additional DOM processing or external document parsing; retrieval and output processing are included in elapsed time." + } + } + ], + "extends": "native_markdown_browser" + }, + "moli": { + "label": { + "zh": "Moli", + "en": "Moli" + }, + "kind": "browser", + "source_url": "https://github.com/lexmount/moli", + "profile": { + "kind": { + "zh": "本地浏览器 CLI", + "en": "Local browser CLI" + }, + "description": { + "zh": "本地浏览器 CLI;实际接口、输出和等待方式以保存的调用记录为准。", + "en": "Local browser CLI; saved call records identify the actual interface, output and waiting behavior." + } + }, + "adapter_types": [], + "label_template": "{label} {version}", + "interface": { + "entry": "CLI · Moli · recorded native interface", + "auth": { + "zh": "本地独立浏览器;不需要供应商 API Key", + "en": "Independent local browser; no provider API key required" + }, + "params": [ + [ + "URL", + { + "zh": "string · 必填", + "en": "string · required" + }, + { + "zh": "唯一任务输入", + "en": "Only task input" + } + ], + [ + "--dump markdown", + { + "zh": "网页、文档正文、结构与网页内嵌媒体任务", + "en": "Webpage, document-body, structured, and embedded-media tasks" + }, + { + "zh": "直接取得 Markdown;直接 PDF/DOCX 不提供可读正文时失败", + "en": "Obtain Markdown directly; direct PDF/DOCX tasks fail without readable body text" + } + ], + [ + "--layout / --with-frames", + { + "zh": "动态网站正文整类;网页内嵌媒体任务使用 --with-frames", + "en": "Every dynamic-body task; webpage media-information tasks use --with-frames" + }, + { + "zh": "动态正文统一启用 Moli 页面布局能力,并在同一次原生 Markdown 调用中包含已加载子文档;不按题目设置 selector", + "en": "Dynamic-body tasks uniformly enable Moli's page-layout capability and include loaded child documents in the same native Markdown call; no task-specific selector is supplied" + } + ], + [ + "--wait-selector / --wait-script", + { + "zh": "旧版预声明完成条件", + "en": "Previously declared completion conditions" + }, + { + "zh": "只等待,不抽取或拼接正文", + "en": "Wait only; do not extract or assemble body text" + } + ], + [ + { + "zh": "外层截止", + "en": "Outer deadline" + }, + "seconds", + { + "zh": "外层截止和内部参数以冻结的调用记录为准;未记录的内部默认值不作推断", + "en": "Outer and internal deadlines follow the frozen call record; unrecorded internal defaults are not inferred." + } + ] + ], + "fields": [ + [ + "stdout", + "UTF-8 string / bytes", + { + "zh": "网页与文档任务为原生 Markdown;直接文件为原始字节", + "en": "Native Markdown for webpage and document tasks; original bytes for direct files" + } + ], + [ + "raw_stdout_path / sha256", + "string", + { + "zh": "保存的原始文档或媒体证据", + "en": "Saved original-document or media evidence" + } + ], + [ + "file_evaluation", + "object", + { + "zh": "文件格式、长度和目标 SHA-256 验收", + "en": "File format, length, and target SHA-256 validation" + } + ] + ], + "request": { + "page_or_document_body": "moli fetch --dump markdown --wait-until done URL", + "dynamic_body": "moli fetch --dump markdown --layout --with-frames --wait-until domstable URL", + "webpage_media": "moli fetch --dump markdown --with-frames --wait-until done URL", + "direct_file": { + "zh": "原始响应或附件接口;具体路线见逐次调用记录", + "en": "Raw response or attachment interface; consult individual call records for the route" + } + }, + "source": "harness/web_fetch/providers/headless_worker.py", + "notes": { + "zh": "实际版本、配置和调用路线见“环境与复现”及逐次调用记录。", + "en": "See Environment & reproduction and the individual call records for measured versions, settings and execution routes." + } + }, + "capabilities": { + "scope": { + "zh": "网页和文档正文使用原生 Markdown;直接媒体文件使用原始响应", + "en": "Native Markdown for webpage and document body text; raw responses for direct media files" + }, + "sources": [ + "headless-moli-1.1.5" + ], + "web": { + "status": { + "zh": "支持", + "en": "Supported" + }, + "detail": { + "zh": "CLI 原生 Markdown", + "en": "Native Markdown from CLI" + } + }, + "document": { + "status": { + "zh": "有限支持", + "en": "Limited support" + }, + "detail": { + "zh": "HTML/TXT 可以直接返回正文;PDF/DOCX 必须由同一原生 Markdown 接口交付可读文字", + "en": "HTML and TXT can return body text directly; PDF and DOCX must deliver readable text through the same native Markdown interface" + } + }, + "image": { + "status": { + "zh": "仅链接", + "en": "Link only" + }, + "detail": { + "zh": "网页资源取决于 Markdown 保留;直接 URL 可验收原始字节", + "en": "Page resources depend on Markdown preservation; direct URLs can be validated as original bytes" + } + }, + "av": { + "status": { + "zh": "仅链接", + "en": "Link only" + }, + "detail": { + "zh": "网页资源取决于 Markdown 保留;直接 URL 可验收原始字节", + "en": "Page resources depend on Markdown preservation; direct URLs can be validated as original bytes" + } + }, + "file": { + "status": { + "zh": "支持", + "en": "Supported" + }, + "detail": { + "zh": "裸 fetch 返回直接 PDF、PNG、WebM、MP4 等原始响应", + "en": "A bare fetch returns original responses for direct PDF, PNG, WebM, MP4, and similar inputs" + } + }, + "js": { + "status": { + "zh": "支持", + "en": "Supported" + }, + "detail": { + "zh": "兼容范围看逐例结果", + "en": "See individual results for compatibility coverage" + } + }, + "input": { + "zh": "给定公开 HTTP(S) URL;不登录、不点击、不滚动。", + "en": "A public HTTP(S) URL; no login, clicks, or scrolling." + }, + "structure": { + "zh": "网页和文档正文输出 Markdown;直接媒体文件输出原始字节。", + "en": "Webpage and document body tasks output Markdown; direct media files output original bytes." + }, + "access": { + "zh": "每次新进程;本地二进制,不使用托管服务凭据。", + "en": "A fresh process for every call; local binaries use no managed-service credentials." + }, + "notes": { + "zh": "正文与文件是不同交付契约。文档题要求 Moli 的原生 Markdown 接口直接交付可读正文;只返回 PDF/DOCX 字节或地址不通过,评测端不再外部解析。直接媒体文件只校验格式、长度和冻结 SHA-256,不进行内容理解。", + "en": "Body text and files have different delivery contracts. Document tasks require Moli's native Markdown interface to return readable body text directly. PDF or DOCX bytes or a URL alone do not pass, and the benchmark does not parse them externally. Direct media files are checked only for format, length, and frozen SHA-256, without content understanding." + }, + "tested": { + "zh": "网页与文档正文使用 --dump markdown;直接媒体文件返回原始字节;重试设置和端到端预算取自运行记录。", + "en": "Webpage and document body tasks use --dump markdown; direct media files return original bytes. Retry settings and the end-to-end deadline come from run records." + } + }, + "access": { + "account": { + "zh": "不需要供应商账号或 API Key。", + "en": "No provider account or API key is required." + }, + "configuration": { + "zh": "安装与本报告版本和 SHA-256 一致的二进制,配置 MOLI_BINARY;详见“环境与复现”。", + "en": "Install the binary matching this report's version and SHA-256, and configure MOLI_BINARY; see Environment & reproduction." + }, + "env_vars": [ + "MOLI_BINARY" + ], + "links": [ + [ + { + "zh": "官方项目与安装说明", + "en": "Official project and installation guide" + }, + "https://github.com/lexmount/moli" + ] + ], + "type": "local" + }, + "version_policy": "native", + "routes": { + "capabilities": { + "zh": "网页正文、文档正文、结构和网页内嵌媒体统一使用原生 Markdown;动态网站正文整类统一启用 --layout、--with-frames 和 domstable;网页媒体只核对输出中的目标地址。最终网址不参与正文判分。", + "en": "Webpage body text, document body text, structured tasks, and webpage media information use native Markdown. Every dynamic-body task uses --layout, --with-frames, and domstable. Media-information tasks only check target URLs in the output. The final URL does not affect body-task acceptance." + }, + "markdown": { + "zh": "CLI fetch --dump markdown → 内置转换 → Markdown", + "en": "CLI fetch --dump markdown → built-in conversion → Markdown" + }, + "original": { + "zh": "原生 CDP 响应;附件事件使用同一 Moli 原始附件接口", + "en": "Native CDP response; attachment events use the same Moli binary's raw attachment interface" + }, + "resource": { + "zh": "CLI fetch --dump markdown --with-frames → 原生 Markdown;共同评分器只识别题目指定的资源 URL", + "en": "CLI fetch --dump markdown --with-frames → native Markdown; the shared scorer checks the task's declared resource URLs" + }, + "runtime": { + "zh": "原生 CDP", + "en": "Native CDP" + }, + "waits": { + "zh": "CLI 等待 done", + "en": "CLI waits for done" + } + }, + "release_url_template": "https://github.com/lexmount/moli/releases/tag/v{version}", + "status": "implemented", + "version_prefix": "moli", + "observed_routes": [ + { + "metadata_equals": { + "extractor": "moli_native_markdown", + "dump": "markdown" + }, + "text": { + "zh": "moli fetch --dump markdown{flags} → Moli 原生 Markdown", + "en": "moli fetch --dump markdown{flags} → Moli native Markdown" + }, + "fragments": { + "flags": [ + { + "metadata_equals": { + "layout": true, + "with_frames": true + }, + "text": { + "zh": " --layout --with-frames", + "en": " --layout --with-frames" + } + } + ] + }, + "compact_text": { + "zh": "原生 Markdown", + "en": "Native Markdown" + }, + "mode": "markdown" + }, + { + "metadata_equals": { + "output_contract": "original_file_bytes", + "extractor": "moli_cdp_response" + }, + "text": { + "zh": "Moli CDP 原始响应 → 原始文件字节", + "en": "Moli CDP original response → original file bytes" + }, + "compact_text": { + "zh": "Moli CDP 原始响应 → 文件字节", + "en": "Moli CDP original response → file bytes" + }, + "mode": "file" + } + ], + "task_interfaces": [ + { + "case_equals": { + "task_kind": "file" + }, + "description": { + "zh": "{original} → 原始文件字节(不转 Markdown)", + "en": "{original} → original file bytes (no Markdown conversion)" + }, + "wait": { + "zh": "读取原始响应或下载文件,不等待查看器绘制", + "en": "Read the original response or download; do not wait for viewer rendering" + } + }, + { + "case_equals": { + "track": "document" + }, + "description": { + "zh": "{markdown};HTML/TXT 可直接返回正文,直接 PDF/DOCX 未提供可读正文;评测端不使用外部解析器", + "en": "{markdown}; HTML/TXT can return text directly; direct PDF/DOCX did not provide readable text; the harness uses no external parser" + }, + "wait": "{waits}" + }, + { + "case_equals": { + "track": "dynamic" + }, + "description": { + "zh": "{markdown};动态正文整类统一启用 --layout 和 --with-frames,不使用逐题 selector", + "en": "{markdown}; all dynamic-text tasks enable --layout and --with-frames, without per-task selectors" + }, + "wait": { + "zh": "动态网站正文整类使用 CLI 原生 domstable", + "en": "All dynamic-text tasks use native CLI domstable" + } + }, + { + "case_equals": { + "track": "image", + "task_kind": "resource" + }, + "description": { + "zh": "{resource} → 图片或视频资源地址(直接从原生 Markdown 核对)", + "en": "{resource} → image or video resource URLs (checked directly in native Markdown)" + }, + "wait": "{waits}" + }, + { + "case_equals": { + "track": "video", + "task_kind": "resource" + }, + "description": { + "zh": "{resource} → 图片或视频资源地址(直接从原生 Markdown 核对)", + "en": "{resource} → image or video resource URLs (checked directly in native Markdown)" + }, + "wait": "{waits}" + }, + { + "case_equals": {}, + "description": { + "zh": "{markdown}", + "en": "{markdown}" + }, + "wait": "{waits}" + } + ], + "task_capabilities": { + "document": { + "status": { + "zh": "有限支持", + "en": "Limited support" + }, + "detail": { + "zh": "HTML/TXT 可由原生 Markdown 返回;直接 PDF/DOCX 未提供可读正文,评测端不使用外部解析器", + "en": "HTML/TXT can be returned as native Markdown; direct PDF/DOCX did not provide readable text, and the harness uses no external parser" + } + } + }, + "extends": "native_markdown_browser" + }, + "moli-webfetch": { + "label": { + "zh": "moli-webfetch", + "en": "moli-webfetch" + }, + "kind": "adapter", + "source_url": "https://github.com/lexmount/moli/tree/main/skills/moli-webfetch", + "profile": { + "kind": { + "zh": "本地 CLI", + "en": "Local CLI" + }, + "description": { + "zh": "Moli:网页正文、文档正文、结构和网页内嵌媒体统一使用原生 Markdown;直接媒体文件使用原始响应。完成条件只控制官方等待参数;不调用模型。", + "en": "Moli uses native Markdown for page/document text, structure and embedded media, and raw responses for direct media files. Completion conditions control official wait parameters; no model is called." + } + }, + "adapter_types": [ + "moli_webfetch" + ], + "label_template": "{label} {version}", + "interface": { + "entry": { + "zh": "CLI · 原生 Markdown 或原始响应路线", + "en": "CLI · native Markdown or raw-response route" + }, + "auth": { + "zh": "本地二进制;无 API Key", + "en": "Local binary; no API key" + }, + "params": [ + [ + "URL", + { + "zh": "string · 必填", + "en": "string · required" + }, + { + "zh": "公开 HTTP(S) 页面或直接文件", + "en": "Public HTTP(S) page or direct file" + } + ], + [ + "--dump markdown", + { + "zh": "所有网页、文档正文与网页内嵌媒体任务", + "en": "All webpage, document-body, and embedded-media tasks" + }, + { + "zh": "直接取得 Moli 原生 Markdown;直接 PDF/DOCX 未提供可读正文时失败", + "en": "Returns Moli's native Markdown directly; a direct PDF or DOCX fails when the interface does not deliver readable body text" + } + ], + [ + "--layout / --with-frames", + { + "zh": "动态网站正文整类;网页内嵌媒体任务使用 --with-frames", + "en": "Every dynamic-body task; webpage media-information tasks use --with-frames" + }, + { + "zh": "动态正文统一启用 Moli 页面布局能力,并在同一次原生 Markdown 调用中包含已加载子文档;不按题目设置 selector", + "en": "Dynamic-body tasks uniformly enable Moli's page-layout capability and include loaded child documents in the same native Markdown call; no task-specific selector is supplied" + } + ], + [ + "--wait-until / --wait-selector / --wait-script", + "string", + { + "zh": "使用 Moli 官方等待能力;只等待,不抽取或拼接正文", + "en": "Uses Moli's official wait capability for readiness only; it neither extracts nor assembles body text" + } + ], + [ + { + "zh": "外层截止", + "en": "Outer deadline" + }, + "number", + { + "zh": "外层截止和内部参数以冻结的调用记录为准;未记录的内部默认值不作推断", + "en": "Outer and internal deadlines follow the frozen call record; unrecorded internal defaults are not inferred." + } + ], + [ + "--block-private-networks", + "flag", + { + "zh": "默认启用", + "en": "Enabled by default" + } + ] + ], + "fields": [ + [ + "stdout", + "UTF-8 string / bytes", + { + "zh": "Markdown、eval 结果或直接文件原始字节", + "en": "Markdown, eval results, or original bytes from a direct file" + } + ], + [ + "stderr", + "string", + { + "zh": "独立诊断;不混入正文", + "en": "Separate diagnostics; excluded from body text" + } + ], + [ + "exit_code", + "integer", + { + "zh": "0 表示工具请求完成;正文或文件为空仍由任务规则单独判失败", + "en": "Exit code 0 means the tool request completed; empty text or file output still fails the task under its separate acceptance rules" + } + ], + [ + "status_code / final_url", + { + "zh": "可能未提供", + "en": "May be unavailable" + }, + { + "zh": "CLI 未提供时保持未知,不伪造 HTTP 200 或最终 URL", + "en": "Remains unknown when the CLI does not provide it; no HTTP 200 status or final URL is invented" + } + ] + ], + "request": { + "page_body": "moli fetch --dump markdown --wait-until done URL", + "dynamic_body": "moli fetch --dump markdown --layout --with-frames --wait-until domstable URL", + "exact_page_identity": { + "zh": "正文仍用原生 Markdown;最终网址不是评分条件,无法确认时保持空值", + "en": "Text still uses native Markdown; the final URL is not a scoring condition and remains absent if it cannot be confirmed" + }, + "webpage_media": "moli fetch --dump markdown --with-frames --wait-until done URL", + "direct_document_body": "moli fetch --dump markdown --wait-until done URL", + "direct_file": { + "zh": "Moli 原生 CDP 响应;附件事件使用同一 Moli 原始附件接口", + "en": "Moli native CDP response; attachment events use the same Moli binary's raw attachment interface" + } + }, + "source": "harness/web_fetch/providers/moli.py", + "notes": { + "zh": "网页正文、文档正文、结构题和网页内嵌媒体题统一使用 Moli 原生 Markdown。动态网站正文整类统一启用 --layout、--with-frames 和 domstable,不按题目设置 selector。网页内嵌媒体只在原生输出中核对题目指定的资源地址,不下载媒体,也不切换 HTML、JSON 或网络跟踪接口。直接 PDF/DOCX 没有交付可读正文时按失败记录,评测端不下载后解析。直接媒体文件使用原生 CDP 响应或同一二进制的原始附件接口。最终网址只作诊断,不决定正文是否通过;失败后不切换格式或借用其他 provider。", + "en": "Moli uses native Markdown for webpage body text, document body text, structured tasks, and media information from webpages. Every dynamic-body task uses --layout, --with-frames, and domstable; no task-specific selector is supplied. Media-information tasks check only the requested resource URLs in native output; they do not download the media or switch to HTML, JSON, or network tracing. A direct PDF or DOCX fails when the interface does not deliver readable body text; the benchmark does not download and parse it. Direct media files use native CDP responses or the same binary's raw attachment interface. The final URL is diagnostic and does not decide body-task acceptance. A failure never triggers a format switch or another provider." + } + }, + "capabilities": { + "scope": { + "zh": "moli fetch;网页、文档正文、结构和网页内嵌媒体使用原生 Markdown,预声明完成条件只调用官方等待参数,直接媒体文件使用原始响应", + "en": "moli fetch; webpage body text, document body text, structured tasks, and webpage media information use native Markdown; preregistered completion conditions only call official wait parameters; direct media files use raw responses" + }, + "sources": [ + "moli", + "moli-recipes", + "moli-release" + ], + "web": { + "status": { + "zh": "支持", + "en": "Supported" + }, + "detail": { + "zh": "JS 渲染网页 → Markdown / HTML", + "en": "JS-rendered webpage → Markdown / HTML" + } + }, + "document": { + "status": { + "zh": "需配置", + "en": "Configuration required" + }, + "detail": { + "zh": "裸 fetch 取得原始 PDF 后在同一预算内本地解析;Office 取决于实际响应", + "en": "A bare fetch retrieves the original PDF for local parsing within the same budget; Office support depends on the actual response" + } + }, + "image": { + "status": { + "zh": "有限支持", + "en": "Limited support" + }, + "detail": { + "zh": "原生 Markdown 中保留的图片地址;直接文件可验收原始字节", + "en": "Image URLs retained in native Markdown; direct files can validate raw bytes" + } + }, + "av": { + "status": { + "zh": "有限支持", + "en": "Limited support" + }, + "detail": { + "zh": "原生 Markdown 中保留的媒体地址;直接文件可验收原始字节", + "en": "Media URLs retained in native Markdown; direct files can validate raw bytes" + } + }, + "file": { + "status": { + "zh": "有限支持", + "en": "Limited support" + }, + "detail": { + "zh": "裸 fetch 可自动返回直接非 HTML 响应的原始字节", + "en": "A bare fetch can automatically return the original bytes of a direct non-HTML response" + } + }, + "js": { + "status": { + "zh": "支持", + "en": "Supported" + }, + "detail": { + "zh": "默认运行 JS,支持多种就绪等待", + "en": "Runs JavaScript by default with multiple readiness waits" + } + }, + "input": { + "zh": "单个 URL;每个页面单独调用 fetch。", + "en": "A single URL; fetch is called separately for each page." + }, + "structure": { + "zh": "网页与文档任务由 --dump markdown 返回原生 Markdown;直接媒体文件返回原始字节;等待条件不参与内容抽取。", + "en": "Webpage and document tasks use --dump markdown to return native Markdown. Direct media files return original bytes; wait conditions do not extract content." + }, + "access": { + "zh": "支持授权 cookie-file / profile-dir 和请求头;仍可能被站点拒绝。默认适配器不加载登录状态。", + "en": "Supports authorized cookie-file / profile-dir and request headers; sites may still reject access. The default adapter does not load login state." + }, + "notes": { + "zh": "截图和 PDF 是启用 --layout 后生成的页面产物,不是读取已有文档或下载原媒体。官方未承诺文档 OCR、媒体理解或音频转写;直接非 HTML 输入不强制使用 Markdown。", + "en": "Screenshots and PDFs generated with --layout are page artifacts, not reads of existing documents or downloads of original media. The official contract does not promise document OCR, media understanding, or audio transcription; direct non-HTML inputs are not forced through Markdown." + }, + "tested": { + "zh": "网页正文、文档正文、结构和网页内嵌媒体任务统一使用 Moli 原生 Markdown;动态网站正文整类统一启用 --layout、--with-frames 和 domstable,不使用逐题 selector。网页媒体只核对同一 Markdown 输出里的目标地址,不下载媒体。直接 PDF/DOCX 未提供可读正文时按失败记录;直接媒体文件验收原始字节。失败后不切换格式,也不借用其他 provider 补抓;只有实际执行记录参与榜单。", + "en": "Webpage body text, document body text, structured tasks, and webpage media information all use Moli's native Markdown. Every dynamic-body task uses --layout, --with-frames, and domstable; no task-specific selector is supplied. Webpage media tasks check target URLs in the same Markdown output and do not download the media. A direct PDF or DOCX fails when it does not provide readable body text; direct media files validate raw bytes. A failure does not switch formats or fetch through another provider. Only measured records enter the ranking." + } + }, + "access": { + "type": "local", + "env_vars": [ + "MOLI_BINARY" + ], + "links": [ + [ + { + "zh": "注册 / 登录", + "en": "Register / sign in" + }, + "https://github.com/lexmount/moli/tree/main/skills/moli-webfetch" + ], + [ + { + "zh": "获取凭据", + "en": "Get credentials" + }, + "https://github.com/lexmount/moli/releases" + ] + ], + "account": { + "zh": "本地 CLI;二进制路径和版本属于运行证据。", + "en": "Local CLI; the binary path and version are part of the run evidence." + }, + "configuration": { + "zh": "安装并固定登记示例中 Moli 可执行文件;不需要供应商账号或 API Key。", + "en": "Install and pin the Moli executable used in this registered example; no provider account or API key is required." + } + }, + "status": "implemented", + "version_prefix": "moli", + "runtime_parameters": [ + { + "parameter": { + "zh": "外层截止", + "en": "Outer deadline" + }, + "rules": [ + { + "metadata_equals": { + "internal_timeout_policy": "native_default" + }, + "description": { + "text": { + "zh": "{budget};Moli 总等待、HTTP 和连接超时保留该二进制默认值", + "en": "{budget}; Moli total, HTTP and connection timers retain this binary’s defaults" + }, + "fields": { + "budget": "report_outer_budget" + } + }, + "note": { + "zh": "这批调用未传入 Moli 内部超时参数。", + "en": "These calls did not pass Moli internal timeout parameters." + } + }, + { + "metadata_equals": { + "internal_timeout_policy": "explicit" + }, + "description": { + "text": { + "zh": "{budget};实测命令同时显式传入 Moli 总等待、HTTP 和连接超时{value}", + "en": "{budget}; recorded commands explicitly set Moli total, HTTP and connection timeouts{value}" + }, + "fields": { + "budget": "report_outer_budget" + }, + "fragments": { + "value": [ + { + "text": { + "zh": " {timeout_ms:,} 毫秒", + "en": " {timeout_ms:,} ms" + }, + "fields": { + "timeout_ms": "http_connect_timeout_ms" + }, + "positive_numeric_fields": [ + "timeout_ms" + ] + } + ] + } + }, + "note": { + "zh": "这批调用使用保存记录中的显式 Moli 超时,未写成产品默认值。", + "en": "These calls use recorded explicit Moli timeouts, not assumed product defaults." + } + }, + { + "metadata_equals": {}, + "description": { + "text": { + "zh": "{budget};Moli 内部超时以逐次调用记录为准", + "en": "{budget}; Moli internal timeouts follow individual call records" + }, + "fields": { + "budget": "report_outer_budget" + } + }, + "note": { + "zh": "保存证据未提供可核对的 Moli 内部超时策略。", + "en": "Saved evidence does not identify a verifiable Moli internal timeout policy." + } + } + ] + } + ], + "unregistered_participant_entry": { + "zh": "CLI · Moli · 路线以逐次调用记录为准", + "en": "CLI · Moli · route from individual call records" + } + }, + "obscura": { + "label": { + "zh": "Obscura", + "en": "Obscura" + }, + "kind": "browser", + "source_url": "https://github.com/h4ckf0r0day/obscura", + "profile": { + "kind": { + "zh": "本地浏览器 CLI", + "en": "Local browser CLI" + }, + "description": { + "zh": "Obscura fetch --dump markdown;标准模式,不启用 stealth 或备用引擎。", + "en": "Obscura fetch --dump markdown in standard mode, without stealth or a fallback engine." + } + }, + "adapter_types": [], + "label_template": "{label} {version}", + "interface": { + "entry": "CLI · obscura fetch --dump markdown", + "auth": { + "zh": "本地独立浏览器;不需要供应商 API Key", + "en": "Independent local browser; no provider API key required" + }, + "params": [ + [ + "URL", + "string", + { + "zh": "唯一任务输入", + "en": "Only task input" + } + ], + [ + "--dump", + "string", + "markdown" + ], + [ + "wait_until", + "string", + "load + adaptive settle" + ], + [ + "timeout", + "seconds", + { + "zh": "外层截止取自运行配置;CLI 参数按各自单位换算", + "en": "Outer deadline from run configuration; CLI parameters use their native units" + } + ] + ], + "fields": [ + [ + "stdout", + "UTF-8 string", + { + "zh": "浏览器原生 Markdown → content", + "en": "Browser-native Markdown → content" + } + ], + [ + "native_markdown_ms", + "number", + { + "zh": "CLI 调用及原生序列化阶段耗时", + "en": "CLI call and native serialization latency" + } + ], + [ + "browser_stderr", + "string", + { + "zh": "浏览器诊断;不进入正文", + "en": "Browser diagnostics; excluded from body text" + } + ] + ], + "request": { + "url": "https://example.com/", + "engine": "obscura", + "timeout": "", + "wait_until": "load + adaptive settle", + "dump": "markdown" + }, + "source": "harness/web_fetch/providers/headless_worker.py", + "notes": { + "zh": "网页正文、嵌入文档以及网页中的图片或视频信息都使用浏览器原生 Markdown。直接文件使用浏览器已有的原始响应接口。不在失败后切换输出方式、调用共同 DOM 脚本,或请求外部服务补正文。接口未提供 HTTP 状态或最终网址时保留空值。", + "en": "Webpage body text, embedded documents, and image or video information use the browser's native Markdown. Direct files use its existing original-response interface. Failures do not trigger output switching, a shared DOM script, or an external service to supply missing text. HTTP status and final URL remain empty when the interface does not provide them." + } + }, + "capabilities": { + "scope": { + "zh": "obscura fetch --dump markdown 原生输出", + "en": "Native output from obscura fetch --dump markdown" + }, + "sources": [ + "headless-obscura" + ], + "web": { + "status": { + "zh": "支持", + "en": "Supported" + }, + "detail": { + "zh": "原生 Markdown", + "en": "Native Markdown" + } + }, + "document": { + "status": { + "zh": "未声明", + "en": "Not declared" + }, + "detail": { + "zh": "登记示例中适配器没有 PDF / Office 解析器,不据此推断产品能力", + "en": "Registered example: the adapter has no PDF / Office parser; no broader product limitation is inferred" + } + }, + "image": { + "status": { + "zh": "仅链接", + "en": "Link only" + }, + "detail": { + "zh": "取决于 DOM 与转换器保留", + "en": "Depends on preservation by the DOM and converter" + } + }, + "av": { + "status": { + "zh": "未声明", + "en": "Not declared" + }, + "detail": { + "zh": "登记示例中没有媒体解析,不据此推断产品能力", + "en": "Registered example: the adapter performs no media parsing; no broader product limitation is inferred" + } + }, + "file": { + "status": { + "zh": "未声明", + "en": "Not declared" + }, + "detail": { + "zh": "登记示例中异常二进制输出仅保留证据", + "en": "Anomalous binary output from this registered example is retained as evidence only" + } + }, + "js": { + "status": { + "zh": "支持", + "en": "Supported" + }, + "detail": { + "zh": "兼容范围看逐例结果", + "en": "See individual results for compatibility coverage" + } + }, + "input": { + "zh": "给定公开 HTTP(S) URL;不登录、不点击、不滚动。", + "en": "A public HTTP(S) URL; no login, clicks, or scrolling." + }, + "structure": { + "zh": "统一 Markdown;不要求引擎按评分答案生成结构。", + "en": "Normalized Markdown; engines are not required to generate structures tailored to the scoring answer." + }, + "access": { + "zh": "每次新会话;Kitesurf 使用 Cloudflare 账号版远程 Browser Run API,其余为本地进程。", + "en": "A fresh session for every call; Kitesurf uses the account-level remote Cloudflare Browser Run API, while the others use local processes." + }, + "notes": { + "zh": "这里只声明登记示例中调用模式。没有启用文档解析、截图、下载、OCR 等模式,不能从登记示例中失败推断产品没有这些能力。", + "en": "Only the invocation mode used in this registered example is described. Document parsing, screenshots, downloads, OCR, and other modes were not enabled, so failures here do not establish that the product lacks those capabilities." + }, + "tested": { + "zh": "直接使用原生 Markdown;CLI 等待和外层预算取自调用记录。", + "en": "Use native Markdown directly; CLI waiting and the outer deadline come from call records." + } + }, + "access": { + "account": { + "zh": "不需要供应商账号或 API Key。", + "en": "No provider account or API key is required." + }, + "configuration": { + "zh": "从官方项目获取程序及配套 worker,配置 OBSCURA_BINARY。", + "en": "Obtain the program and companion worker from the official project and configure OBSCURA_BINARY." + }, + "env_vars": [ + "OBSCURA_BINARY" + ], + "links": [ + [ + { + "zh": "官方项目与安装说明", + "en": "Official project and installation guide" + }, + "https://github.com/h4ckf0r0day/obscura" + ] + ], + "type": "local" + }, + "version_policy": "native", + "routes": { + "capabilities": { + "zh": "内置 Markdown 与 LP.getMarkdown 共用官方 JS 实现;本次执行固定版本的同一脚本,不按题目切换接口。", + "en": "Built-in Markdown and LP.getMarkdown share the official JavaScript implementation; one pinned script is used without switching interfaces per task." + }, + "markdown": { + "zh": "CLI --eval 官方固定版本脚本 → 官方转换 → Markdown", + "en": "CLI --eval pinned official script → official conversion → Markdown" + }, + "original": { + "zh": "CLI fetch --dump original(原始 HTTP 响应,不运行页面 JS)", + "en": "CLI fetch --dump original (raw HTTP response without page JavaScript)" + }, + "resource": { + "zh": "CLI --eval 官方固定版本脚本 → 原生 Markdown;评分器只从交付内容中核对资源地址", + "en": "CLI --eval pinned official script → native Markdown; resource URLs are checked in the delivered output" + }, + "runtime": { + "zh": "原生 CDP", + "en": "Native CDP" + }, + "waits": { + "zh": "CLI 使用官方默认 load 加自适应静默等待(最多 5 秒)", + "en": "CLI uses the official default load event plus adaptive settling (up to 5 seconds)" + } + }, + "status": "implemented", + "observed_routes": [ + { + "metadata_equals": { + "extractor": "obscura_native_markdown" + }, + "text": { + "zh": "Obscura --eval 官方 Markdown 脚本{script} → 原生 Markdown", + "en": "Obscura --eval official Markdown script{script} → native Markdown" + }, + "fragments": { + "script": [ + { + "metadata_equals": {}, + "text": { + "zh": "(脚本 SHA-256 {sha})", + "en": " (script SHA-256 {sha})" + }, + "fields": { + "sha": "native_metadata.script_sha256" + } + } + ] + }, + "compact_text": { + "zh": "官方脚本 → Markdown", + "en": "Official script → Markdown" + }, + "mode": "markdown" + }, + { + "metadata_equals": { + "output_contract": "original_file_bytes", + "extractor": "obscura_original_response" + }, + "text": { + "zh": "Obscura --dump original 原始响应 → 原始文件字节", + "en": "Obscura --dump original response → original file bytes" + }, + "compact_text": { + "zh": "Obscura --dump original 原始响应 → 文件字节", + "en": "Obscura --dump original response → file bytes" + }, + "mode": "file" + } + ], + "interface_variants": [ + { + "metadata_equals": { + "native_metadata.format": "obscura_eval" + }, + "command": "obscura fetch URL --eval ", + "remove_request_fields": [ + "dump" + ], + "remove_parameters": [ + "--dump" + ], + "notes": { + "zh": "Lightpanda 解包同一次调用返回的原生 Markdown;Obscura 执行固定哈希的官方 Markdown 脚本。网页正文、嵌入文档以及网页中的图片或视频信息均保留该原生输出。直接文件分别使用 Lightpanda CDP 与 Obscura --dump original。评测端不再加工 DOM 或外部解析文档;获取与输出处理均计入耗时。", + "en": "Lightpanda unwraps native Markdown from the same call; Obscura executes the hash-bound official Markdown script. Page text, embedded documents and embedded image or video information retain that native output. Direct files use Lightpanda CDP or Obscura --dump original. The harness performs no additional DOM processing or external document parsing; retrieval and output processing are included in elapsed time." + } + } + ], + "extends": "native_markdown_browser" + }, + "openclaw-built-in": { + "label": { + "zh": "openclaw-built-in", + "en": "openclaw-built-in" + }, + "kind": "adapter", + "source_url": "https://github.com/openclaw/openclaw/blob/main/docs/tools/web-fetch.md", + "profile": { + "kind": { + "zh": "项目内置", + "en": "Project built-in" + }, + "description": { + "zh": "登记示例中使用 HTTP + Readability;关闭 Firecrawl 回退,避免混成两个方法。", + "en": "Registered example: uses HTTP + Readability with the Firecrawl fallback disabled to avoid mixing two methods." + } + }, + "adapter_types": [ + "openclaw_builtin" + ], + "label_template": "{label} {version}", + "interface": { + "entry": "TypeScript · createWebFetchTool(...).execute(callId, args, signal)", + "auth": { + "zh": "本地内置工具;登记示例中禁用 Firecrawl 回退", + "en": "Local built-in tool; Firecrawl fallback disabled in this registered example" + }, + "params": [ + [ + "url", + { + "zh": "string · 必填", + "en": "string · required" + }, + { + "zh": "目标页面", + "en": "Target page" + } + ], + [ + "extractMode", + "string", + { + "zh": "登记示例中 markdown", + "en": "markdown in this registered example" + } + ], + [ + "maxChars", + "number", + { + "zh": "登记示例中 200000;输出可能截断", + "en": "200000 in this registered example; output may be truncated" + } + ], + [ + "signal", + "AbortSignal", + { + "zh": "timeout 参数,单位秒预算,通过函数参数传入", + "en": "Deadline passed as a function argument, as recorded for the run" + } + ] + ], + "fields": [ + [ + "details.text", + "string", + { + "zh": "正文 → content;缺失时取 content[].text", + "en": "Body text → content; falls back to content[].text when absent" + } + ], + [ + "details.status / finalUrl", + "number / string", + { + "zh": "源站状态和最终 URL", + "en": "Origin status and final URL" + } + ], + [ + "details.extractor", + "string", + { + "zh": "实际正文提取器", + "en": "Actual body-text extractor" + } + ], + [ + "details.truncated", + "boolean", + { + "zh": "输出是否被截断", + "en": "Whether output was truncated" + } + ] + ], + "request": { + "url": "https://example.com/", + "extractMode": "markdown", + "maxChars": 200000 + }, + "source": "harness/web_fetch/workers/openclaw_webfetch_worker.mts", + "notes": { + "zh": "Readability 开启,cacheTtlMinutes=0。函数异常转成失败记录,不把其他 provider 的回退结果算给 OpenClaw。", + "en": "Readability is enabled and cacheTtlMinutes=0. Function exceptions become failed records; fallback output from another provider is not credited to OpenClaw." + } + }, + "capabilities": { + "scope": { + "zh": "web_fetch · 内置 HTTP 路径", + "en": "web_fetch · Built-in HTTP path" + }, + "sources": [ + "claw", + "claw-code", + "claw-html", + "claw-reader" + ], + "web": { + "status": { + "zh": "支持", + "en": "Supported" + }, + "detail": "Readability → Markdown" + }, + "document": { + "status": { + "zh": "无此能力", + "en": "Not supported" + }, + "detail": { + "zh": "无 PDF / Office 解析器", + "en": "No PDF / Office parser" + } + }, + "image": { + "status": { + "zh": "有限支持", + "en": "Limited support" + }, + "detail": { + "zh": "原生 Markdown / 超链接", + "en": "Native Markdown / hyperlinks" + } + }, + "av": { + "status": { + "zh": "有限支持", + "en": "Limited support" + }, + "detail": { + "zh": "正文中的普通超链接", + "en": "Ordinary hyperlinks in the body" + } + }, + "file": { + "status": { + "zh": "无此能力", + "en": "Not supported" + }, + "detail": { + "zh": "文本输出不是原文件", + "en": "Text output is not the original file" + } + }, + "js": { + "status": { + "zh": "无此能力", + "en": "Not supported" + }, + "detail": { + "zh": "需另用 Browser", + "en": "Requires the separate Browser tool" + } + }, + "input": { + "zh": "HTTP(S) URL;可选 Markdown 或纯文本。", + "en": "HTTP(S) URL; optional Markdown or plain text." + }, + "structure": { + "zh": "正文与抓取元数据;不提供任意 schema 抽取。", + "en": "Body content and crawl metadata; no arbitrary schema extraction." + }, + "access": { + "zh": "可配置请求头;登录/JS 重页面由独立 Browser 工具处理。", + "en": "Request headers are configurable; login-heavy or JS-heavy pages are handled by the separate Browser tool." + }, + "notes": { + "zh": "Readability 的 HTML 输出经自带转换器处理,不直接提取 img.src;只有服务端原生 Markdown 或正文普通超链接等路径可能留下图片地址。不能保证收集图片或播放入口。PDF/媒体二进制误解码成字符串不是原文件支持;Firecrawl 等可选回退是另一路径。", + "en": "Readability's HTML output is processed by its bundled converter and does not directly extract img.src; image addresses may remain only through paths such as server-native Markdown or ordinary body hyperlinks. Image collection or playback entry points are not guaranteed. Misdecoding PDF/media binaries as strings is not original-file support; optional fallbacks such as Firecrawl are separate paths." + }, + "tested": { + "zh": "登记示例中开启 Readability、关闭 Firecrawl 回退及缓存;不能把整个 OpenClaw 的浏览器或 PDF 工具能力算到此行。", + "en": "Registered example: enables Readability and disables the Firecrawl fallback and cache; the capabilities of OpenClaw's broader browser or PDF tools cannot be attributed to this row." + } + }, + "access": { + "type": "local", + "env_vars": [ + "OPENCLAW_REPO", + "OPENCLAW_NODE", + "OPENCLAW_PNPM" + ], + "links": [ + [ + { + "zh": "注册 / 登录", + "en": "Register / sign in" + }, + "https://github.com/openclaw/openclaw" + ], + [ + { + "zh": "获取凭据", + "en": "Get credentials" + }, + "https://github.com/openclaw/openclaw/blob/main/docs/tools/web-fetch.md" + ] + ], + "account": { + "zh": "本地项目内置实现;不需要云端抓取 Key。", + "en": "Built into the local project; no cloud-retrieval key is required." + }, + "configuration": { + "zh": "检出并固定 OpenClaw 源码、Node 与 pnpm;登记示例中关闭 Firecrawl 回退。", + "en": "Check out and pin the OpenClaw source, Node, and pnpm; the Firecrawl fallback is disabled for this registered example." + } + }, + "status": "implemented" + }, + "tavily": { + "label": { + "zh": "tavily", + "en": "tavily" + }, + "kind": "adapter", + "source_url": "https://docs.tavily.com/documentation/api-reference/endpoint/extract", + "profile": { + "kind": { + "zh": "托管 API", + "en": "Managed API" + }, + "description": { + "zh": "登记示例中调用 Extract,从给定 URL 提取可读内容。", + "en": "Registered example: calls Extract to retrieve readable content from the given URL." + } + }, + "adapter_types": [ + "tavily_extract" + ], + "label_template": "{label} {version}", + "interface": { + "entry": "POST https://api.tavily.com/extract", + "auth": "Authorization: Bearer ", + "params": [ + [ + "urls", + { + "zh": "array · 必填", + "en": "array · required" + }, + { + "zh": "接口接受网址数组;登记示例中每次只放一个", + "en": "The interface accepts an array of URLs; each call in this registered example contains one" + } + ], + [ + "extract_depth", + "string", + { + "zh": "登记示例中 advanced", + "en": "advanced in this registered example" + } + ], + [ + "timeout", + "number", + { + "zh": "服务端提取截止取自保存的请求,单位秒", + "en": "Server-side extraction deadline in seconds, from the saved request" + } + ], + [ + "format", + "string", + { + "zh": "登记示例中 markdown", + "en": "markdown in this registered example" + } + ], + [ + "include_images", + "boolean", + { + "zh": "正文配置 false;资源配置 true,额外返回图片地址", + "en": "false for body tasks; true for resource tasks to return image URLs" + } + ] + ], + "fields": [ + [ + "results[].raw_content", + "string", + { + "zh": "提取正文 → content", + "en": "Extracted body text → content" + } + ], + [ + "results[].url", + "string", + { + "zh": "结果 URL", + "en": "Result URL" + } + ], + [ + "results[].images / images", + "array", + { + "zh": "结果条目或响应顶层的图片地址", + "en": "Image URLs from result items or the response top level" + } + ], + [ + "failed_results", + "array", + { + "zh": "未成功提取的网址及原因", + "en": "URLs not extracted successfully and their reasons" + } + ], + [ + "response_time", + "number", + { + "zh": "服务端耗时", + "en": "Server-side latency" + } + ] + ], + "request": { + "urls": [ + "https://example.com/" + ], + "extract_depth": "advanced", + "timeout": "", + "format": "markdown", + "include_images": false + }, + "source": "harness/web_fetch/providers/tavily.py", + "notes": { + "zh": "已知 URL 使用 advanced Extract;Search 根据查询找页面,Crawl 扩展到链接页面,Research 生成答案,均不替代目标 URL 的正文。资源题开启 include_images;没有原文件或视频下载字段。外层、客户端和服务端提取截止分别以运行配置和请求记录为准。HTTP 200 但没有 raw_content 仍按适配器请求失败;原始响应只保存在私有哈希绑定证据。", + "en": "Known URLs use advanced Extract. Search finds pages from a query, Crawl follows linked pages, and Research generates answers; none replaces text from the target URL. Resource tasks enable include_images, but the API has no original-file or video-download output. Outer, client and server extraction deadlines are recorded separately in run configuration and requests. HTTP 200 without raw_content remains an adapter-level request failure; raw responses remain private hash-bound evidence." + } + }, + "capabilities": { + "scope": "POST /extract", + "sources": [ + "tav", + "tav-depth", + "tav-research" + ], + "web": { + "status": { + "zh": "支持", + "en": "Supported" + }, + "detail": { + "zh": "Markdown / 纯文本", + "en": "Markdown / plain text" + } + }, + "document": { + "status": { + "zh": "未声明", + "en": "Not declared" + }, + "detail": { + "zh": "Extract 未列格式保证", + "en": "Extract lists no format guarantee" + } + }, + "image": { + "status": { + "zh": "仅链接", + "en": "Link only" + }, + "detail": "include_images" + }, + "av": { + "status": { + "zh": "未声明", + "en": "Not declared" + }, + "detail": { + "zh": "无转写/媒体下载契约", + "en": "No transcription/media download contract" + } + }, + "file": { + "status": { + "zh": "未声明", + "en": "Not declared" + }, + "detail": { + "zh": "无原始字节字段", + "en": "No raw-byte field" + } + }, + "js": { + "status": { + "zh": "支持", + "en": "Supported" + }, + "detail": { + "zh": "复杂页建议 advanced", + "en": "advanced is recommended for complex pages" + } + }, + "input": { + "zh": "单个或批量 URL;Extract 与 Search、Research 是不同接口。", + "en": "Single or batch URLs; Extract, Search, and Research are separate APIs." + }, + "structure": { + "zh": "raw_content 和图片列表;query 可选择相关片段,并非必然返回全文。", + "en": "raw_content and an image list; query can select relevant snippets and does not necessarily return the full text." + }, + "access": { + "zh": "公开 Extract 参数没有源站登录会话接口;advanced 不等于保证绕过登录。", + "en": "The public Extract parameters provide no interface for authenticated source-site sessions; advanced does not guarantee bypassing login." + }, + "notes": { + "zh": "advanced 面向动态页、表格和嵌入内容;不能把 embedded media 理解为下载视频或理解画面。官方 Research 的 PDF/Markdown/JSON/TXT 文件上传能力不属于本次 Extract;Extract 的 PDF/Office 支持在本次核对资料中未明确承诺。", + "en": "advanced targets dynamic pages, tables, and embedded content; embedded media must not be interpreted as video downloading or visual understanding. The official Research capability to upload PDF/Markdown/JSON/TXT files is outside this Extract test; PDF/Office support for Extract is not explicitly promised in the reviewed materials." + }, + "tested": { + "zh": "登记示例中 advanced + markdown + include_images=true,不传 query;未测试 Research 文件上传。", + "en": "Registered example: uses advanced + markdown + include_images=true without query; Research file uploads were not tested." + } + }, + "access": { + "type": "hosted", + "env_vars": [ + "TAVILY_API_KEY" + ], + "links": [ + [ + { + "zh": "注册 / 登录", + "en": "Register / sign in" + }, + "https://app.tavily.com" + ], + [ + { + "zh": "获取凭据", + "en": "Get credentials" + }, + "https://app.tavily.com/home" + ] + ], + "account": { + "zh": "官方快速开始说明每月有 1,000 免费 API credits,无需绑卡。", + "en": "The official quickstart states that 1,000 free API credits are available each month with no card required." + }, + "configuration": { + "zh": "注册或登录 Tavily Platform,从控制台复制 API key。", + "en": "Register for or sign in to Tavily Platform and copy an API key from the console." + }, + "required": true, + "priority": { + "zh": "B · 已知 URL 抽取", + "en": "B · Known-URL extraction" + } + }, + "status": "implemented" + }, + "browserbase": { + "label": { + "zh": "Browserbase Fetch", + "en": "Browserbase Fetch" + }, + "status": "planned", + "source_url": "https://www.browserbase.com/sign-up", + "access": { + "type": "hosted", + "env_vars": [ + "BROWSERBASE_API_KEY", + "BROWSERBASE_PROJECT_ID" + ], + "links": [ + [ + { + "zh": "注册 / 登录", + "en": "Register / sign in" + }, + "https://www.browserbase.com/sign-up" + ], + [ + { + "zh": "获取凭据", + "en": "Get credentials" + }, + "https://www.browserbase.com/overview" + ] + ], + "account": { + "zh": "官方页面存在契约冲突;打分前必须先以账号内实际接口核验。", + "en": "Official pages describe conflicting contracts; verify the account-specific interface before scoring." + }, + "configuration": { + "zh": "注册后创建或选择项目,从 Overview 控制台复制 API key 与 Project ID。", + "en": "After registering, create or select a project and copy its API key and Project ID from Overview." + }, + "required": true, + "priority": { + "zh": "B · 已知 URL 抓取候选", + "en": "B · Known-URL fetch candidate" + } + } + }, + "bocha-web-search": { + "label": { + "zh": "Bocha Web Search", + "en": "Bocha Web Search" + }, + "status": "planned", + "source_url": "https://open.bochaai.com/", + "access": { + "type": "hosted", + "env_vars": [ + "BOCHA_API_KEY" + ], + "links": [ + [ + { + "zh": "注册 / 登录", + "en": "Register / sign in" + }, + "https://open.bochaai.com/" + ] + ], + "account": { + "zh": "搜索/摘要产品,不是任意已知 URL 抽取;应使用独立查询数据集。", + "en": "Search and summaries do not extract arbitrary known URLs; use a separate query dataset." + }, + "configuration": { + "zh": "在博查开放平台注册,在控制台创建 API key,并填入 BOCHA_API_KEY。", + "en": "Register on the Bocha platform, create an API key in the console and set BOCHA_API_KEY." + }, + "required": true, + "priority": { + "zh": "D · 查询到搜索结果", + "en": "D · Query-based search results" + } + } + }, + "baidu-ai-search": { + "label": { + "zh": "Baidu Qianfan AI Search", + "en": "Baidu Qianfan AI Search" + }, + "status": "planned", + "source_url": "https://console.bce.baidu.com/qianfan/ais/console/apiKey", + "access": { + "type": "hosted", + "env_vars": [ + "QIANFAN_API_KEY" + ], + "links": [ + [ + { + "zh": "注册 / 登录", + "en": "Register / sign in" + }, + "https://console.bce.baidu.com/qianfan/ais/console/apiKey" + ] + ], + "account": { + "zh": "Web Search 按查询返回摘要、URL 与片段,不是任意 URL 抓取 API。", + "en": "Web Search returns summaries, URLs and snippets for a query; it is not an arbitrary-URL fetch API." + }, + "configuration": { + "zh": "注册并完成百度智能云认证,打开千帆 API Key 管理并创建或复制 key。", + "en": "Register and verify a Baidu AI Cloud account, then create or copy a key in Qianfan API Key Management." + }, + "required": true, + "priority": { + "zh": "D · 查询到搜索结果", + "en": "D · Query-based search results" + } + } + }, + "tencent-wsa": { + "label": { + "zh": "Tencent Cloud Web Search API", + "en": "Tencent Cloud Web Search API" + }, + "status": "planned", + "source_url": "https://console.cloud.tencent.com/wsa", + "access": { + "type": "hosted", + "env_vars": [ + "TENCENT_WSA_API_KEY" + ], + "links": [ + [ + { + "zh": "注册 / 登录", + "en": "Register / sign in" + }, + "https://console.cloud.tencent.com/wsa" + ] + ], + "account": { + "zh": "返回标题、片段与来源 URL 的搜索 API,应与已知 URL 抽取分开评测。", + "en": "This search API returns titles, snippets and source URLs; evaluate it separately from known-URL extraction." + }, + "configuration": { + "zh": "注册并完成认证,开通 Web Search API,再在控制台创建服务 API key。", + "en": "Register and verify an account, enable Web Search API and create a service API key in the console." + }, + "required": true, + "priority": { + "zh": "D · 查询到搜索结果", + "en": "D · Query-based search results" + } + } + }, + "aliyun-model-studio": { + "label": { + "zh": "Alibaba Cloud Model Studio web_search + web_extractor", + "en": "Alibaba Cloud Model Studio web_search + web_extractor" + }, + "status": "planned", + "source_url": "https://bailian.console.aliyun.com/", + "access": { + "type": "hosted", + "env_vars": [ + "DASHSCOPE_API_KEY" + ], + "links": [ + [ + { + "zh": "注册 / 登录", + "en": "Register / sign in" + }, + "https://bailian.console.aliyun.com/" + ], + [ + { + "zh": "获取凭据", + "en": "Get credentials" + }, + "https://bailian.console.aliyun.com/?tab=model#/api-key" + ] + ], + "account": { + "zh": "web_extractor 当前随 web_search 出现在模型响应中;模型 token 与工具调用都计入成本。", + "en": "web_extractor currently appears alongside web_search in model responses; both model tokens and tool calls contribute to cost." + }, + "configuration": { + "zh": "注册并完成阿里云认证,开通百炼 Model Studio,在 API Key 管理创建 key。", + "en": "Register and verify an Alibaba Cloud account, enable Model Studio and create an API key." + }, + "required": true, + "priority": { + "zh": "E · 模型绑定检索", + "en": "E · Model-bound retrieval" + } + } + }, + "gemini-url-context": { + "label": { + "zh": "Google Gemini URL Context", + "en": "Google Gemini URL Context" + }, + "status": "planned", + "source_url": "https://aistudio.google.com/", + "access": { + "type": "hosted", + "env_vars": [ + "GEMINI_API_KEY" + ], + "links": [ + [ + { + "zh": "注册 / 登录", + "en": "Register / sign in" + }, + "https://aistudio.google.com/" + ], + [ + { + "zh": "获取凭据", + "en": "Get credentials" + }, + "https://aistudio.google.com/apikey" + ] + ], + "account": { + "zh": "模型把检索到的 URL 内容作为输入 token,返回答案/引用,而不是独立原始 Markdown。", + "en": "The model consumes retrieved URL content as input tokens and returns answers or citations, not standalone raw Markdown." + }, + "configuration": { + "zh": "登录 Google AI Studio,创建 API key 并填入 GEMINI_API_KEY。", + "en": "Sign in to Google AI Studio, create an API key and set GEMINI_API_KEY." + }, + "required": true, + "priority": { + "zh": "E · 模型绑定检索", + "en": "E · Model-bound retrieval" + } + } + }, + "brightdata": { + "label": { + "zh": "Bright Data Web Unlocker", + "en": "Bright Data Web Unlocker" + }, + "status": "planned", + "source_url": "https://brightdata.com/cp/start", + "access": { + "type": "hosted", + "env_vars": [ + "BRIGHTDATA_API_TOKEN", + "BRIGHTDATA_UNLOCKER_ZONE" + ], + "links": [ + [ + { + "zh": "注册 / 登录", + "en": "Register / sign in" + }, + "https://brightdata.com/cp/start" + ], + [ + { + "zh": "获取凭据", + "en": "Get credentials" + }, + "https://brightdata.com/cp/zones" + ] + ], + "account": { + "zh": "需要账号、计费或试用资格,以及 Web Unlocker zone。", + "en": "Requires an account, billing or trial eligibility, and a Web Unlocker zone." + }, + "configuration": { + "zh": "注册账号,创建 Web Unlocker zone,再复制 API token 与 zone 名称。", + "en": "Register, create a Web Unlocker zone and copy the API token and zone name." + }, + "required": true, + "priority": { + "zh": "C · 解锁场景数据集", + "en": "C · Access-unblocking dataset" + } + } + }, + "scrapingbee": { + "label": { + "zh": "ScrapingBee", + "en": "ScrapingBee" + }, + "status": "planned", + "source_url": "https://app.scrapingbee.com/account/register", + "access": { + "type": "hosted", + "env_vars": [ + "SCRAPINGBEE_API_KEY" + ], + "links": [ + [ + { + "zh": "注册 / 登录", + "en": "Register / sign in" + }, + "https://app.scrapingbee.com/account/register" + ], + [ + { + "zh": "获取凭据", + "en": "Get credentials" + }, + "https://app.scrapingbee.com/account/manage/api_key" + ] + ], + "account": { + "zh": "受试用/账户限额约束;生产套餐有月度最低消费。", + "en": "Trial and account limits apply; production plans have a monthly minimum charge." + }, + "configuration": { + "zh": "注册后打开 Account → API Key,复制 key。", + "en": "Register and copy the key from Account → API Key." + }, + "required": true, + "priority": { + "zh": "C · 抓取 API 对比", + "en": "C · Fetch API comparison" + } + } + }, + "zyte": { + "label": { + "zh": "Zyte API", + "en": "Zyte API" + }, + "status": "planned", + "source_url": "https://app.zyte.com/", + "access": { + "type": "hosted", + "env_vars": [ + "ZYTE_API_KEY" + ], + "links": [ + [ + { + "zh": "注册 / 登录", + "en": "Register / sign in" + }, + "https://app.zyte.com/" + ] + ], + "account": { + "zh": "官方文档说明首月有 5 美元初始 credit。", + "en": "The recorded official documentation describes an initial $5 credit for the first month." + }, + "configuration": { + "zh": "注册后在控制台打开 Zyte API,创建或复制 API key。", + "en": "Register, open Zyte API in the console and create or copy an API key." + }, + "required": true, + "priority": { + "zh": "C · 抓取 API 对比", + "en": "C · Fetch API comparison" + } + } + }, + "zenrows": { + "label": { + "zh": "ZenRows", + "en": "ZenRows" + }, + "status": "planned", + "source_url": "https://app.zenrows.com/register", + "access": { + "type": "hosted", + "env_vars": [ + "ZENROWS_API_KEY" + ], + "links": [ + [ + { + "zh": "注册 / 登录", + "en": "Register / sign in" + }, + "https://app.zenrows.com/register" + ], + [ + { + "zh": "获取凭据", + "en": "Get credentials" + }, + "https://app.zenrows.com/builder" + ] + ], + "account": { + "zh": "受账号/试用限额约束。", + "en": "Account and trial limits apply." + }, + "configuration": { + "zh": "注册并复制控制台或 request builder 中显示的 API key。", + "en": "Register and copy the API key shown in the console or request builder." + }, + "required": true, + "priority": { + "zh": "C · 抓取 API 对比", + "en": "C · Fetch API comparison" + } + } + }, + "scrapfly": { + "label": { + "zh": "Scrapfly", + "en": "Scrapfly" + }, + "status": "planned", + "source_url": "https://scrapfly.io/register", + "access": { + "type": "hosted", + "env_vars": [ + "SCRAPFLY_API_KEY" + ], + "links": [ + [ + { + "zh": "注册 / 登录", + "en": "Register / sign in" + }, + "https://scrapfly.io/register" + ], + [ + { + "zh": "获取凭据", + "en": "Get credentials" + }, + "https://scrapfly.io/dashboard" + ] + ], + "account": { + "zh": "受账号/试用限额约束。", + "en": "Account and trial limits apply." + }, + "configuration": { + "zh": "注册后打开控制台,复制项目 API key。", + "en": "Register, open the console and copy the project API key." + }, + "required": true, + "priority": { + "zh": "C · 抓取 API 对比", + "en": "C · Fetch API comparison" + } + } + }, + "oxylabs": { + "label": { + "zh": "Oxylabs Web Scraper API", + "en": "Oxylabs Web Scraper API" + }, + "status": "planned", + "source_url": "https://dashboard.oxylabs.io/en/registration", + "access": { + "type": "hosted", + "env_vars": [ + "OXYLABS_USERNAME", + "OXYLABS_PASSWORD" + ], + "links": [ + [ + { + "zh": "注册 / 登录", + "en": "Register / sign in" + }, + "https://dashboard.oxylabs.io/en/registration" + ], + [ + { + "zh": "获取凭据", + "en": "Get credentials" + }, + "https://dashboard.oxylabs.io/" + ] + ], + "account": { + "zh": "所选产品可能需要试用或销售审批。", + "en": "The selected product may require trial or sales approval." + }, + "configuration": { + "zh": "注册或申请试用,开通 Web Scraper API 并复制用户凭据。", + "en": "Register or request a trial, enable Web Scraper API and copy the user credentials." + }, + "required": true, + "priority": { + "zh": "C · 抓取 API 对比", + "en": "C · Fetch API comparison" + } + } + } + }, + "sources": { + "cow": { + "label": { + "zh": "CowAgent · WebFetch 源码", + "en": "CowAgent · WebFetch source" + }, + "url": "https://github.com/zhayujie/CowAgent/blob/ed5bb344cfe42cee443c8266ba47ee666d8d7471/agent/tools/web_fetch/web_fetch.py", + "checked_at": "2026-09-11", + "checked_version": null + }, + "lex": { + "label": { + "zh": "Lexmount · Extract 契约", + "en": "Lexmount · Extract contract" + }, + "url": "https://browser.lexmount.com/docs/webfetch/reference/all-in-one-extract", + "checked_at": "2026-09-11", + "checked_version": null + }, + "lex-overview": { + "label": { + "zh": "Lexmount · WebFetch 概览", + "en": "Lexmount · WebFetch overview" + }, + "url": "https://browser.lexmount.com/en/docs/webfetch", + "checked_at": "2026-09-11", + "checked_version": null + }, + "fire": { + "label": { + "zh": "Firecrawl · Scrape 格式", + "en": "Firecrawl · Scrape formats" + }, + "url": "https://docs.firecrawl.dev/features/scrape", + "checked_at": "2026-09-11", + "checked_version": null + }, + "fire-api": { + "label": { + "zh": "Firecrawl · Scrape API 与 video 格式", + "en": "Firecrawl · Scrape API and video format" + }, + "url": "https://docs.firecrawl.dev/api-reference/endpoint/scrape", + "checked_at": "2026-09-15", + "checked_version": null + }, + "fire-doc": { + "label": { + "zh": "Firecrawl · 文档解析", + "en": "Firecrawl · document parsing" + }, + "url": "https://docs.firecrawl.dev/features/document-parsing", + "checked_at": "2026-09-11", + "checked_version": null + }, + "tav": { + "label": { + "zh": "Tavily · Extract 契约", + "en": "Tavily · Extract contract" + }, + "url": "https://docs.tavily.com/documentation/api-reference/endpoint/extract", + "checked_at": "2026-09-11", + "checked_version": null + }, + "tav-depth": { + "label": { + "zh": "Tavily · 提取深度", + "en": "Tavily · extraction depth" + }, + "url": "https://docs.tavily.com/documentation/best-practices/best-practices-extract", + "checked_at": "2026-09-11", + "checked_version": null + }, + "tav-research": { + "label": { + "zh": "Tavily · Research 文件上传", + "en": "Tavily · Research file uploads" + }, + "url": "https://www.tavily.com/blog/what-tavily-shipped-in-may-26", + "checked_at": "2026-09-11", + "checked_version": null + }, + "exa": { + "label": { + "zh": "Exa · Contents 参数", + "en": "Exa · Contents parameters" + }, + "url": "https://exa.ai/docs/reference/contents-api-guide-for-coding-agents", + "checked_at": "2026-09-11", + "checked_version": null + }, + "cf": { + "label": { + "zh": "Cloudflare · Markdown 端点", + "en": "Cloudflare · Markdown endpoint" + }, + "url": "https://developers.cloudflare.com/browser-run/quick-actions/markdown-endpoint/", + "checked_at": "2026-09-11", + "checked_version": null + }, + "cf-scrape": { + "label": { + "zh": "Cloudflare · Scrape 端点", + "en": "Cloudflare · Scrape endpoint" + }, + "url": "https://developers.cloudflare.com/browser-run/quick-actions/scrape-endpoint/", + "checked_at": "2026-09-15", + "checked_version": null + }, + "cf-files": { + "label": { + "zh": "Cloudflare · 文档转 Markdown", + "en": "Cloudflare · document-to-Markdown conversion" + }, + "url": "https://developers.cloudflare.com/workers-ai/features/markdown-conversion/usage/rest-api/", + "checked_at": "2026-09-15", + "checked_version": null + }, + "cf-formats": { + "label": { + "zh": "Cloudflare · 支持的文档格式", + "en": "Cloudflare · supported document formats" + }, + "url": "https://developers.cloudflare.com/workers-ai/features/markdown-conversion/supported-formats/", + "checked_at": "2026-09-15", + "checked_version": null + }, + "claw": { + "label": { + "zh": "OpenClaw · Web Fetch 文档", + "en": "OpenClaw · Web Fetch documentation" + }, + "url": "https://github.com/openclaw/openclaw/blob/12c4bdf7482f24c85b011e9d858fdec3c245101c/docs/tools/web-fetch.md", + "checked_at": "2026-09-11", + "checked_version": null + }, + "claw-code": { + "label": { + "zh": "OpenClaw · 抽取实现", + "en": "OpenClaw · extraction implementation" + }, + "url": "https://github.com/openclaw/openclaw/blob/12c4bdf7482f24c85b011e9d858fdec3c245101c/src/agents/tools/web-fetch.ts", + "checked_at": "2026-09-11", + "checked_version": null + }, + "claw-html": { + "label": { + "zh": "OpenClaw · HTML 转换器", + "en": "OpenClaw · HTML converter" + }, + "url": "https://github.com/openclaw/openclaw/blob/12c4bdf7482f24c85b011e9d858fdec3c245101c/src/agents/tools/web-fetch-utils.ts", + "checked_at": "2026-09-11", + "checked_version": null + }, + "claw-reader": { + "label": { + "zh": "OpenClaw · Readability 插件", + "en": "OpenClaw · Readability extension" + }, + "url": "https://github.com/openclaw/openclaw/blob/12c4bdf7482f24c85b011e9d858fdec3c245101c/extensions/web-readability/web-content-extractor.ts", + "checked_at": "2026-09-11", + "checked_version": null + }, + "dsh": { + "label": { + "zh": "DeepSeek Harness · MIME 策略", + "en": "DeepSeek Harness · MIME policy" + }, + "url": "https://github.com/deepseek-ai/deepseek-harness/blob/dd6322d604e00eec1ba5e0c8541159906a21094a/packages/web/web-fetch-http/src/policy.ts", + "checked_at": "2026-09-11", + "checked_version": null + }, + "dsh-text": { + "label": { + "zh": "DeepSeek Harness · Markdown 转换", + "en": "DeepSeek Harness · Markdown conversion" + }, + "url": "https://github.com/deepseek-ai/deepseek-harness/blob/dd6322d604e00eec1ba5e0c8541159906a21094a/packages/web/tool-web/src/fetch.ts", + "checked_at": "2026-09-11", + "checked_version": null + }, + "lc": { + "label": { + "zh": "LangChain · WebBaseLoader 源码", + "en": "LangChain · WebBaseLoader source" + }, + "url": "https://github.com/langchain-ai/langchain-community/blob/libs/community/v0.3.30/libs/community/langchain_community/document_loaders/web_base.py", + "checked_at": "2026-09-11", + "checked_version": null + }, + "lc-loaders": { + "label": { + "zh": "LangChain · 其他文档加载器", + "en": "LangChain · other document loaders" + }, + "url": "https://docs.langchain.com/oss/python/integrations/document_loaders/index", + "checked_at": "2026-09-11", + "checked_version": null + }, + "c4-media": { + "label": { + "zh": "Crawl4AI · 链接与媒体", + "en": "Crawl4AI · links and media" + }, + "url": "https://docs.crawl4ai.com/core/link-media/", + "checked_at": "2026-09-11", + "checked_version": null + }, + "c4-pdf": { + "label": { + "zh": "Crawl4AI · PDF 策略", + "en": "Crawl4AI · PDF strategy" + }, + "url": "https://docs.crawl4ai.com/advanced/pdf-parsing/", + "checked_at": "2026-09-11", + "checked_version": null + }, + "c4-download": { + "label": { + "zh": "Crawl4AI · 文件下载", + "en": "Crawl4AI · file downloads" + }, + "url": "https://docs.crawl4ai.com/advanced/file-downloading/", + "checked_at": "2026-09-11", + "checked_version": null + }, + "c4-config": { + "label": { + "zh": "Crawl4AI · 配置", + "en": "Crawl4AI · configuration" + }, + "url": "https://docs.crawl4ai.com/api/parameters/", + "checked_at": "2026-09-11", + "checked_version": null + }, + "moli": { + "label": "Moli 1.1.4 · WebFetch skill", + "url": "https://github.com/lexmount/moli/blob/v1.1.4/skills/moli-webfetch/SKILL.md", + "checked_at": "2026-09-11", + "checked_version": "1.1.4" + }, + "moli-recipes": { + "label": { + "zh": "Moli 1.1.4 · 输出与接口边界", + "en": "Moli 1.1.4 · output and interface boundaries" + }, + "url": "https://github.com/lexmount/moli/blob/v1.1.4/skills/moli-webfetch/references/fetch-recipes.md", + "checked_at": "2026-09-11", + "checked_version": "1.1.4" + }, + "moli-release": { + "label": { + "zh": "Moli 1.1.4 · 官方发布", + "en": "Moli 1.1.4 · official release" + }, + "url": "https://github.com/lexmount/moli/releases/tag/v1.1.4", + "checked_at": "2026-09-11", + "checked_version": "1.1.4" + }, + "headless-kitesurf": { + "label": { + "zh": "kitesurf · 官方文档(2026-09-03 核对)", + "en": "kitesurf · official documentation (verified 2026-09-03)" + }, + "url": "https://developers.cloudflare.com/browser-run/kitesurf/", + "checked_at": "2026-09-03", + "checked_version": null + }, + "headless-lightpanda": { + "label": { + "zh": "lightpanda · 官方文档(2026-09-03 核对)", + "en": "lightpanda · official documentation (verified 2026-09-03)" + }, + "url": "https://github.com/lightpanda-io/browser", + "checked_at": "2026-09-03", + "checked_version": null + }, + "headless-obscura": { + "label": { + "zh": "obscura · 官方文档(2026-09-03 核对)", + "en": "obscura · official documentation (verified 2026-09-03)" + }, + "url": "https://github.com/h4ckf0r0day/obscura", + "checked_at": "2026-09-03", + "checked_version": null + }, + "headless-chromium": { + "label": { + "zh": "chromium · 官方文档(2026-09-03 核对)", + "en": "chromium · official documentation (verified 2026-09-03)" + }, + "url": "https://developer.chrome.com/docs/automation-and-testing/headless", + "checked_at": "2026-09-03", + "checked_version": null + }, + "headless-moli-1.1.5": { + "label": { + "zh": "Moli 1.1.5 · 官方发布", + "en": "Moli 1.1.5 · official release" + }, + "url": "https://github.com/lexmount/moli/releases/tag/v1.1.5", + "checked_at": "2026-09-11", + "checked_version": "1.1.5" + } + }, + "output_contract_labels": { + "readable_document_text": { + "zh": "处理路线:{method} 原生接口获取文档 → 共同 PDF / DOCX 正文解析;实际完成步骤见运行证据", + "en": "Retrieval path: {method} native document retrieval → shared PDF / DOCX text parsing; consult execution evidence for completed steps" + }, + "native_markdown_resource_urls": { + "zh": "处理路线:{method} 原生 Markdown → 共同评分器只核对其中的目标图片或视频地址", + "en": "Retrieval path: {method} native Markdown → shared scorer checks target image or video URLs" + }, + "native_html_resource_urls": { + "zh": "处理路线:{method} 原生 HTML → 共同评分器只识别其中的图片或视频资源地址", + "en": "Retrieval path: {method} native HTML → shared scorer identifies image or video resource URLs" + }, + "native_json_resource_urls": { + "zh": "处理路线:{method} 原生 JSON 页面与网络记录 → 共同评分器只识别其中的视频资源地址", + "en": "Retrieval path: {method} native JSON page and network records → shared scorer identifies video resource URLs" + }, + "structured_resource_urls": { + "zh": "处理路线:{method} 原生 DOM 接口 → 共同资源读取脚本 → 图片或视频资源地址", + "en": "Retrieval path: {method} native DOM interface → shared resource reader → image or video URLs" + }, + "readable_media_metadata": { + "zh": "处理路线:{method} 原生页面接口 → 共同脚本读取正文、作者和媒体元数据", + "en": "Retrieval path: {method} native page interface → shared script reads body text, author and media metadata" + } + }, + "templates": { + "browser_tasks": { + "task_interfaces": [ + { + "case_equals": { + "task_kind": "file" + }, + "description": { + "zh": "{original} → 原始文件字节(不转 Markdown)", + "en": "{original} → original file bytes (no Markdown conversion)" + }, + "wait": { + "zh": "读取原始响应或下载文件,不等待查看器绘制", + "en": "Read the original response or download; do not wait for viewer rendering" + } + }, + { + "case_equals": { + "track": "document" + }, + "description": { + "zh": "{markdown};文档必须由此接口直接返回文字,不使用外部 PDF/DOCX 解析器", + "en": "{markdown}; this interface must return document text directly; no external PDF/DOCX parser" + }, + "wait": "{waits}" + }, + { + "case_equals": {}, + "description": { + "zh": "{markdown}", + "en": "{markdown}" + }, + "wait": "{waits}" + } + ] + }, + "native_markdown_browser": { + "extends": "browser_tasks", + "task_capabilities": { + "web": { + "status": { + "zh": "支持", + "en": "Supported" + }, + "detail": { + "zh": "原生网页正文输出", + "en": "Native page-text output" + } + }, + "document": { + "status": { + "zh": "原生输出", + "en": "Native output" + }, + "detail": { + "zh": "直接使用浏览器正文接口;不使用外部 PDF/DOCX 解析器", + "en": "Use the browser text interface directly, without external PDF/DOCX parsers" + } + }, + "image": { + "status": { + "zh": "资源地址 / 字节", + "en": "Resource URLs / bytes" + }, + "detail": { + "zh": "原生 Markdown 由共同评分器识别目标资源 URL;直接文件通过原生响应取得", + "en": "The shared scorer identifies target resource URLs in native Markdown; direct files use the native response" + } + }, + "av": { + "status": { + "zh": "资源与元数据", + "en": "Resources and metadata" + }, + "detail": { + "zh": "原生接口读取地址、页面元数据或原始字节;不做媒体理解", + "en": "Native interfaces retrieve URLs, page metadata or original bytes, without media understanding" + } + }, + "file": { + "status": { + "zh": "原生获取", + "en": "Native retrieval" + }, + "detail": { + "zh": "使用该浏览器原始响应或附件接口,按固定规则验收字节", + "en": "Use the browser original-response or attachment interface; check bytes against fixed acceptance rules" + } + }, + "structure": { + "zh": "按任务交付 Markdown、文档正文、资源地址、媒体元数据或原始文件。", + "en": "Deliver Markdown, document text, resource URLs, media metadata or original files according to the task." + }, + "notes": { + "zh": "浏览器负责访问与原生产物获取;网页内嵌媒体与正文统一走原生 Markdown,共同评分器只核对目标资源 URL。完整分类及边界见报告解读的接口表。", + "en": "The browser handles access and native output retrieval. Embedded media and text use native Markdown; the shared scorer only checks target resource URLs. See the interpretation interface table for the full scope and boundaries." + } + } + } + } +} diff --git a/harness/web_fetch/local_revision.py b/harness/web_fetch/local_revision.py index 0e75a1b54..07ba0ee24 100644 --- a/harness/web_fetch/local_revision.py +++ b/harness/web_fetch/local_revision.py @@ -776,7 +776,7 @@ def compact_local_snapshot(source, binding, **options): temporary.cleanup() -def _project_native_correction(source, original, request): +def _project_native_correction(source, original, request, *, historical_scoring_source=None): """One projection path for preparing and verifying an offline correction.""" from .native_projection import project_native_answer, project_native_result_fields from .publication_privacy import original_evidence_size @@ -788,7 +788,7 @@ def _project_native_correction(source, original, request): declaration = request['projection'] payload = (native.read_bytes() if declaration.get('source_format') == 'bytes' else json.loads(native.read_bytes())) - text, fields = project_native_answer(payload, declaration) + text, fields = project_native_answer(payload, declaration, historical_scoring_source=historical_scoring_source) diagnostic = None if any(spec.get('format') == 'fetch_log_final_url' for spec in declaration.get('result_fields', {}).values()): @@ -805,11 +805,11 @@ def _project_native_correction(source, original, request): return native, text, fields, result_fields -def _require_native_answer_correction(source, original, corrected, proof): +def _require_native_answer_correction(source, original, corrected, proof, *, historical_scoring_source=None): """Verify a derived answer against the same immutable native response.""" if proof.get('attempt_id') != original['attempt_id']: raise ValueError('Native answer correction lost its attempt identity') - native, text, fields, result_fields = _project_native_correction(source, original, proof) + native, text, fields, result_fields = _project_native_correction(source, original, proof, historical_scoring_source=historical_scoring_source) metadata = original.get('provider_metadata', {}) before = proof['original_artifact'] after = proof['corrected_artifact'] @@ -975,7 +975,8 @@ def _require_revision_step(source, receipt, dataset, records, outputs): for identifier, old in old_rows.items(): new = new_rows[identifier] if identifier in corrected_answers: - _require_native_answer_correction(source, old, new, corrected_answers[identifier]) + _require_native_answer_correction(source, old, new, corrected_answers[identifier], + historical_scoring_source=receipt['scoring_source_sha256']) elif identifier in corrected_contexts: _require_native_context_correction(source, old, new, corrected_contexts[identifier]) elif ({k: v for k, v in old.items() if k not in {'metrics', 'scoring', 'diagnosis', 'failure_category'}} diff --git a/harness/web_fetch/methods.py b/harness/web_fetch/methods.py index e4fd3acdb..9f4a7d5ca 100644 --- a/harness/web_fetch/methods.py +++ b/harness/web_fetch/methods.py @@ -1,6 +1,9 @@ from __future__ import annotations import json +import hashlib +import inspect +from copy import deepcopy import importlib.metadata import re from collections import Counter @@ -10,6 +13,188 @@ from typing import Any +def _resolve_catalog_templates(data): + """Expand named defaults once; methods override dictionaries and replace lists.""" + def merge(base, override): + result = deepcopy(base) + for key, value in override.items(): + if key == 'extends': + continue + result[key] = (merge(result[key], value) if isinstance(result.get(key), dict) + and isinstance(value, dict) else deepcopy(value)) + return result + + templates = data.get('templates', {}) + def resolve(row, chain=()): + parent = row.get('extends') + if parent is None: + return deepcopy(row) + if parent not in templates: + raise ValueError('Unknown method template: ' + str(parent)) + if parent in chain: + raise ValueError('Cyclic method template: ' + parent) + return merge(resolve(templates[parent], (*chain, parent)), row) + + return {**data, 'methods': {name: resolve(row) for name, row in data['methods'].items()}} + + +@lru_cache(maxsize=1) +def _raw_report_method_catalog(): + path = Path(__file__).resolve().parent / "data/report-methods.json" + data = json.loads(path.read_text(encoding="utf-8")) + if data.get("schema_version") != 1: + raise ValueError("Unsupported report method registry schema") + return _resolve_catalog_templates(data) + + +def _catalog_value(value, path=(), *, language="zh"): + if isinstance(value, dict): + if set(value) == {"zh", "en"}: + if len(path) == 3 and path[0] == "methods" and path[-1] == "label": + return value + return value[language] + return {key: _catalog_value(item, (*path,key), language=language) for key,item in value.items()} + if isinstance(value, list): + return [_catalog_value(item, (*path,str(index)), language=language) for index,item in enumerate(value)] + return value + + +@lru_cache(maxsize=2) +def report_method_catalog(language="zh"): + """Select colocated language fields by identity, never by matching prose.""" + if language not in {"zh", "en"}: + raise ValueError("Unsupported method catalog language: " + language) + return _catalog_value(_raw_report_method_catalog(), language=language) + + +_REPORT_RECORDS = ContextVar("webfetch_report_records", default={}) +_REPORT_EXECUTION_CONFIGS = ContextVar("webfetch_report_execution_configs", default={}) + + +def frozen_execution_configs(manifests): + """Read installation bindings only from bytes named by execution manifests. + + Missing or conflicting configurations remain unavailable. Never publish raw + configuration values, which may include credentials and machine paths. + """ + bindings = {} + for manifest in manifests: + config = {} + binary_key = ("resource_binary" if manifest.get("parameters", {}).get("resource_sample_interval_ms") is not None else "binary") + source = manifest.get("provider_config_source") + digest = manifest.get("provider_config_sha256") + if source and digest: + path = Path(source) + if path.is_file() and not path.is_symlink(): + raw = path.read_bytes() + if hashlib.sha256(raw).hexdigest() == digest: + payload = json.loads(raw) + config = payload.get("providers", {}) + # A manifest does not identify which cohort override was + # selected. Do not infer an executable when overrides differ. + config = deepcopy(config) + for cohort in payload.get("cohorts", {}).values(): + for name, override in cohort.get("provider_overrides", {}).items(): + if any(key in override and override[key] != config.get(name, {}).get(key) + for key in (binary_key, "engine")): + config.pop(name, None) + if binary_key != "binary": + config = {name: {**value, "binary": value.get(binary_key)} + for name, value in config.items()} + for provider in manifest.get("providers", []): + name = provider["name"] + value = config.get(name) + if name in bindings and bindings[name] != value: + bindings[name] = None + else: + bindings[name] = value + return bindings + + +def _browser_installation_access(name, selected, registered, access, language): + if registered.get("kind") != "browser" or access.get("type") != "local": + return access + measured = selected or _REPORT_RECORDS.get().get(name) + config = (_REPORT_EXECUTION_CONFIGS.get().get(name) if measured + else browser_method_registry().get(name)) + engine = (measured or {}).get("provider_metadata", {}).get("engine") + if config and engine and config.get("engine") != engine: + config = None + binary = config.get("binary") if config else None + variables = [] + if binary is None: + explanation = ("未找到可核验的执行配置,安装变量未记录。" if language == "zh" else + "A verifiable execution configuration is unavailable; installation variables were not recorded.") + else: + match = re.fullmatch(r"\$\{([A-Za-z_][A-Za-z0-9_]*)(?::-([^}]*))?}", str(binary)) + if match: + variables = [match.group(1)] + explanation = (("安装与执行记录版本一致的程序,通过 " + match.group(1) + " 指定程序位置。") + if language == "zh" else + ("Install the recorded program version and set " + match.group(1) + " to its location.")) + else: + explanation = ("执行配置直接指定程序位置,未使用安装环境变量。" if language == "zh" else + "The execution configuration specifies the executable directly, without an installation environment variable.") + return {**access, "env_vars": variables, "configuration": explanation} + + +def report_method(name, record=None, *, language="zh"): + """Resolve run IDs through recorded identity and the execution registry.""" + methods = (report_method_catalog() if language == "zh" else report_method_catalog(language))["methods"] + record = record or _REPORT_RECORDS.get().get(name, {}) + metadata = record.get("provider_metadata") or {} + if metadata.get("engine"): + return methods.get(metadata["engine"], {}) + execution = browser_method_registry().get(name, {}) + for key in (execution.get("engine"), name): + if key in methods: + return methods[key] + provider_type = record.get("provider_type", execution.get("type")) + matches = [item for item in methods.values() + if provider_type in item.get("adapter_types", [])] + if len(matches) > 1: + raise ValueError(f"Ambiguous report method for adapter {provider_type!r}; record an engine") + return matches[0] if matches else {} + + +def normalized_method_version(registered, version): + """Normalize presentation only; preserve the full recorded build suffix.""" + value = str(version).strip() + prefix = registered.get("version_prefix") + if prefix and value.casefold().startswith(prefix.casefold() + " "): + value = value[len(prefix):].strip() + return value + + +def versioned_method_label(registered, base, version): + value = normalized_method_version(registered, version) + return registered.get("label_template", "{label} {version}").format( + label=base, version=value) + + +def method_access(name, records, *, language="zh"): + selected = next((row for row in records if row.get("provider") == name), None) + registered = report_method(name, selected, language=language) + access = registered.get("access") + if access: + access = _browser_installation_access(name, selected, registered, access, language) + template = registered.get("release_url_template") + versions = {normalized_method_version(registered, value) if value else None + for row in records if row.get("provider") == name + for value in [method_version(row)]} + if template and len(versions) == 1 and None not in versions: + version = next(iter(versions)) + # A build suffix is not proof that a corresponding release exists. + if re.fullmatch(r"\d+\.\d+\.\d+", version): + return {**access, "links": [(versioned_method_label( + registered, registered["label"][language], version), + template.format(version=version))]} + return access + return {"type": "unknown", "account": "未登记 / Not registered", + "configuration": "Account and installation requirements have not been registered.", + "env_vars": [], "links": []} + + @lru_cache(maxsize=1) def browser_method_registry(): """Use the execution registry for release and source-build participants.""" @@ -27,111 +212,146 @@ def browser_method_registry(): return json.loads(path.read_text())["providers"] -def native_browser_engine(name): - return browser_method_registry().get(name, {}).get("engine") +def native_browser_engine(name, record=None): + record = record or _REPORT_RECORDS.get().get(name, {}) + recorded = (record.get("provider_metadata") or {}).get("engine") + if recorded: + return recorded + configured = browser_method_registry().get(name, {}).get("engine") + if configured: + return configured + if report_method_catalog()["methods"].get(name, {}).get("kind") == "browser": + return name + return None -def browser_method_label(name): - return _MEASURED_LABELS.get().get( - name, browser_method_registry().get(name, {}).get("label", name) - ) +def browser_method_label(name, *, language="zh"): + measured = _MEASURED_LABELS.get().get(name) + if measured: + return measured[language] + record = _REPORT_RECORDS.get().get(name, {}) + engine = (record.get("provider_metadata") or {}).get("engine") + if engine: + return report_method(name, record).get("label", {}).get(language, engine) + return (browser_method_registry().get(name, {}).get("label") + or report_method(name).get("label", {}).get(language, name)) _MEASURED_LABELS = ContextVar("webfetch_measured_labels", default={}) def measured_method_label(name, fallback): - return _MEASURED_LABELS.get().get(name, fallback) + return _MEASURED_LABELS.get().get(name, {}).get("zh", fallback) + + +def method_version(record, provenance=None, *, language="zh"): + """Prefer the recorded executable version; CDP compatibility is not an identity.""" + metadata = record.get("provider_metadata") or {} + registered = report_method(record.get("provider", ""), record, language=language) + policy = registered.get("version_policy") + if metadata.get("reported_version"): + return str(metadata["reported_version"]) + bound = (provenance or {}).get(metadata.get("binary_sha256"), {}) + if bound.get("version"): + return str(bound["version"]) + for key in ("binary_version", "library_version", "version"): + if metadata.get(key): + return str(metadata[key]) + if policy == "hosted": + return registered.get("version_unavailable") + browser_version = metadata.get("browser_version") + if policy == "native" and re.fullmatch(r"\d+\.\d+\.\d+\.\d+", str(browser_version)): + return None + return str(browser_version) if browser_version else None def with_measured_method_labels(render): - """Scope Moli version labels to the actual report's hash-bound binaries.""" + """Scope all participant labels to recorded versions and binary identities.""" + signature = inspect.signature(render) @wraps(render) def wrapped(out, dataset, records, *args, **kwargs): path = Path(out) / "implementation-provenance.json" provenance = json.loads(path.read_text()) if path.is_file() else {} - identities = { - item["binary_sha256"]: item["version"] - for item in provenance.get("implementations", []) - if item.get("version") and item.get("binary_sha256") - } + identities = {item["binary_sha256"]: item for item in provenance.get("implementations", []) + if item.get("binary_sha256")} + records_token = _REPORT_RECORDS.set({row["provider"]: row for row in records}) + arguments = signature.bind(out, dataset, records, *args, **kwargs).arguments + configs_token = _REPORT_EXECUTION_CONFIGS.set( + frozen_execution_configs(arguments["manifests"]) if "manifests" in arguments + else _REPORT_EXECUTION_CONFIGS.get()) labels = {} - for name in {r["provider"] for r in records}: - if native_browser_engine(name) != "moli": - continue - subset = [r for r in records if r["provider"] == name] - versions = [ - identities.get(r.get("provider_metadata", {}).get("binary_sha256")) - for r in subset - ] - if versions and all(versions): - counts = Counter(versions) - if len(counts) == 1: - labels[name] = "Moli " + next(iter(counts)) - else: - releases = [ - match.group(1) if (match := re.match(r"^(\d+\.\d+\.\d+)", version)) else None - for version in counts - ] - if None not in releases and len(set(releases)) == 1: - release = releases[0] - configured = browser_method_registry().get(name, {}).get("label", "") - qualifier = "(候选版)" if "candidate" in configured.lower() else "" - labels[name] = f"Moli {release}{qualifier}" - else: - labels[name] = "Moli 多构建结果(" + ";".join( - f"{count} 例 {version}" for version, count in sorted(counts.items()) - ) + ")" - token = _MEASURED_LABELS.set(labels) try: - return render(out, dataset, records, *args, **kwargs) + for name in {row["provider"] for row in records}: + registered = report_method(name) + subset = [row for row in records if row["provider"] == name] + versions = [normalized_method_version(registered, value) if value else None + for row in subset for value in [method_version(row, identities)]] + if not versions or not all(versions): + continue + metadata = subset[0].get("provider_metadata") or {} + execution = browser_method_registry().get(name, {}) + preserve_name = (name not in report_method_catalog()["methods"] + and not metadata.get("engine") and not execution.get("engine")) + channels = {row.get("provider_metadata", {}).get( + "release_channel", execution.get("release_channel")) for row in subset} + labels[name] = {} + for language in ("zh", "en"): + counts = Counter(normalized_method_version(registered, method_version( + row, identities, language=language)) for row in subset) + releases = [match.group(1) if (match := re.match( + r"^(\d+\.\d+\.\d+)", version)) else None for version in counts] + base = name if preserve_name else registered.get("label", {}).get( + language, metadata.get("engine") or name) + def label(version): + return versioned_method_label(registered, base, version) + if len(counts) == 1: + value = label(next(iter(counts))) + elif None not in releases and len(set(releases)) == 1: + qualifier = ("(候选版)" if language == "zh" else " (candidate build)") if channels == {"candidate"} else "" + value = label(releases[0]) + qualifier + elif language == "zh": + value = base + " 多构建结果(" + ";".join( + f"{count} 例 {version}" for version, count in sorted(counts.items())) + ")" + else: + value = base + " mixed builds (" + "; ".join( + f"{count} {'case' if count == 1 else 'cases'}: {version}" for version, count in sorted(counts.items())) + ")" + labels[name][language] = value + token = _MEASURED_LABELS.set(labels) + try: + return render(out, dataset, records, *args, **kwargs) + finally: + _MEASURED_LABELS.reset(token) finally: - _MEASURED_LABELS.reset(token) + _REPORT_EXECUTION_CONFIGS.reset(configs_token) + _REPORT_RECORDS.reset(records_token) return wrapped -METHOD_REGISTRY: dict[str, dict[str, Any]] = { - "http": {"endpoint":"HTTP GET","contract":"已知 URL → HTML 文本或文件任务的原始字节","output":"HTML 提取文本;文件任务保留响应字节","freshness":"每次独立请求,不执行页面 JavaScript","leaderboard":"独立 HTTP 对照"}, - "headless_html": {"endpoint":"browser HTML → node-html-markdown 2.0.0","contract":"已知 URL → DOM HTML → Markdown","output":"Markdown;独立保存 HTML 和诊断","freshness":"每次新进程/会话;domcontentloaded;45 秒外层预算","leaderboard":"六类任务分别验收","report":{"observed_routes":True}}, - "headless_markdown": {"endpoint":"各方法最快的正式任务原生产物路径","contract":"已知 URL → 正文 Markdown/文本,或文件任务的原始字节","output":"正文 Markdown/文本;文件任务为原始字节;保留接口与阶段诊断","freshness":"每次新进程/会话;45 秒端到端预算","leaderboard":"六类任务分别验收","report":{"observed_routes":True}}, - "moli_webfetch": {"source_url":"https://github.com/lexmount/moli","endpoint":"moli fetch(网页、文档正文、结构与网页内嵌媒体用原生 Markdown,直接媒体文件用原始响应)","contract":"已知 URL → 原生 Markdown 或原文件字节","output":"stdout Markdown/字节;stderr 独立诊断","freshness":"每次启动独立进程;结构完成条件只控制官方等待参数","leaderboard":"七类任务分别验收","report":{"reported_version":{"pattern":r"^moli\s+","replacement":"Moli "},"profiles":[{"metadata_equals":{"dump":"markdown","route":"dynamic_native_markdown","wait_until":"domstable","wait.strategy":"native_task_family_completion","layout":True,"with_frames":True},"endpoint":"moli fetch --dump markdown --layout --with-frames --wait-until domstable URL","freshness":"每例启动一个独立进程;动态网站正文统一等待 Moli 原生 DOM 稳定信号,并启用原生页面布局和已加载子文档。","configuration":"本轮 {record_count} 个动态网站正文任务统一使用 Moli 原生 Markdown、--layout、--with-frames 和 domstable。除目标 URL 外,没有逐题等待元素、延迟、提取脚本或特殊参数,也没有启用理解模型、格式回退或付费后备方法。"},{"metadata_equals":{"dump":"markdown","route":"dynamic_native_markdown","wait_until":"done","wait.strategy":"native_default_completion"},"endpoint":"moli fetch --dump markdown --wait-until done URL","freshness":"每例启动一个独立进程;由 Moli 的默认 done 完成方式决定何时交付内容。","configuration":"本轮 {record_count} 例统一使用 Moli 原生 Markdown 和默认 done 完成方式。除目标 URL 外,没有逐例等待元素、额外等待时间、提取脚本或特殊参数,也没有启用理解模型、格式回退或付费后备方法。"}]}}, - "cowagent_builtin": {"endpoint":"仓库内 WebFetch.execute","contract":"已知 URL → 可读文本/文档文字","output":"纯文本","freshness":"实时请求","leaderboard":"可读正文"}, - "lexmount": {"endpoint":"POST https://api.lexmount.com/v1/extract","contract":"已知 URL → 主正文 + 元数据","output":"main_text","freshness":"托管实时抓取","leaderboard":"可读正文"}, - "lexmount_cn": {"endpoint":"POST https://api.lexmount.cn/v1/extract","contract":"已知 URL → 主正文 + 元数据","output":"main_text","freshness":"中国区托管实时抓取","leaderboard":"可读正文"}, - "firecrawl": {"endpoint":"POST https://api.firecrawl.dev/v2/scrape","contract":"已知 URL → Markdown/资源地址,或原文件字节","output":"Markdown/资源地址;视频资源题另请求 video;文件题为 rawBase64 解码字节","freshness":"maxAge=0;动态题使用预声明 Actions 等待","leaderboard":"六类任务分别验收","report":{"pricing_anchor":"pricing-firecrawl","configuration":"正文与资源题请求 Markdown、links、images;原文件题改用唯一 rawBase64 格式并验收解码字节。动态顶层选择器使用 wait action,数量与 iframe 条件明确标为未完整执行。未启用额外的理解模型或付费后备方法。"}}, - "tavily_extract": {"endpoint":"POST https://api.tavily.com/extract","contract":"已知 URL → raw_content","output":"Markdown(advanced)","freshness":"供应商实时抽取","leaderboard":"可读正文","report":{"pricing_anchor":"pricing-tavily","configuration":"本轮请求 Markdown;图片资源仅来自同一 Extract 响应的 images 字段。未启用额外的理解模型或付费后备方法。"}}, - "exa_contents": {"endpoint":"POST https://api.exa.ai/contents","contract":"已知 URL → 正文文本","output":"纯文本","freshness":"maxAgeHours=0","leaderboard":"可读正文","report":{"pricing_anchor":"pricing-exa","configuration":"本轮请求正文文本;资源链接仅来自同一 Contents 响应的 extras 字段。未启用额外的理解模型或付费后备方法。"}}, - "cloudflare_markdown": {"endpoint":"POST /accounts/{id}/browser-rendering/markdown 或 /scrape;直接文档 POST /accounts/{id}/ai/tomarkdown","contract":"已知 URL → 网页正文、页面资源地址或直接文档文字","output":"Browser Run Markdown / DOM 资源地址;Workers AI 文档 Markdown","freshness":"Quick Actions 默认最多缓存 5 秒;waitUntil=networkidle2;文档下载与转换同一预算","leaderboard":"六类任务分别验收","report":{"pricing_anchor":"pricing-cloudflare-markdown"}}, - "cloudflare_content": {"endpoint":"POST /accounts/{id}/browser-rendering/content","contract":"已知 URL → 含 head 的完整渲染 HTML","output":"渲染 HTML","freshness":"Quick Actions 默认最多缓存 5 秒;waitUntil=networkidle2","leaderboard":"HTML 诊断"}, - "openclaw_builtin": {"endpoint":"OpenClaw createWebFetchTool","contract":"HTTP GET + Readability 正文抽取(关闭 Firecrawl 回退)","output":"Markdown","freshness":"cacheTtlMinutes=0","leaderboard":"可读正文"}, - "deepseek_harness_builtin": {"endpoint":"DeepSeek Harness ctx.tools.execute(web_fetch)","contract":"匿名 HTTP provider + Turndown","output":"Markdown","freshness":"实时请求","leaderboard":"可读正文","report":{"profiles":[{"metadata_equals":{"output_projection":"declared_body"},"endpoint":"DeepSeek Harness ctx.tools.execute(web_fetch) → value.body.content","contract":"已知 URL → 原生声明 HTML/文本正文 → 共有解析","output":"声明正文;完整工具回包和展示文本独立保存"}]}}, - "langchain_webbase": {"endpoint":"LangChain WebBaseLoader.load()","contract":"HTTP + BeautifulSoup 网页文本加载器","output":"纯文本","freshness":"实时请求","leaderboard":"可读正文"}, - "crawl4ai": {"endpoint":"Crawl4AI AsyncWebCrawler.arun()","contract":"本地浏览器渲染 + Markdown 抽取","output":"Markdown","freshness":"cache_mode=BYPASS","leaderboard":"可读正文"}, - "fixture": {"endpoint":"本地 fixture","contract":"确定性安装测试","output":"纯文本","freshness":"固定","leaderboard":"离线诊断"}, -} +METHOD_REGISTRY = report_method_catalog()["adapters"] def is_browser_comparison(records: list[dict[str, Any]]) -> bool: """Recognize browser cohorts independently of their task-specific adapters.""" - types = {row.get("provider_type") for row in records} - return bool(types & {"headless_html", "headless_markdown"}) and types <= { - "headless_html", "headless_markdown", "moli_webfetch"} + roles = [ + "primary" if report_method(row.get("provider", ""), row).get("kind") == "browser" + else method_info(row.get("provider_type", "")).get("browser_cohort_role") + for row in records + ] + return "primary" in roles and all(role in {"primary", "companion"} for role in roles) -def method_info(provider_type: str) -> dict[str, Any]: - return METHOD_REGISTRY.get(provider_type, {"endpoint":"未记录","contract":"未知","output":"未知","freshness":"未知","leaderboard":"未分类"}) +def method_info(provider_type: str, *, language="zh") -> dict[str, Any]: + registry = METHOD_REGISTRY if language == "zh" else report_method_catalog(language)["adapters"] + missing = {"endpoint":"未记录","contract":"未知","output":"未知","freshness":"未知","leaderboard":"未分类"} if language == "zh" else { + "endpoint":"Not recorded", "contract":"Unknown", "output":"Unknown", + "freshness":"Unknown", "leaderboard":"Unclassified"} + return registry.get(provider_type, missing) -ROUTE_LABELS = { - "cloudflare_native_markdown": "Cloudflare Browser Run /markdown 直接返回 Markdown", - "lightpanda_native_markdown": "Lightpanda CLI 直接返回 Markdown", - "obscura_native_markdown": "Obscura CLI 直接返回 Markdown", - "moli_native_markdown": "Moli CLI 直接返回 Markdown", - "node-html-markdown": "浏览器渲染 DOM → node-html-markdown 2.0.0", -} +ROUTE_LABELS = report_method_catalog()["route_labels"] -def _metadata_matches(metadata: dict[str, Any], expected: dict[str, Any]) -> bool: +def metadata_matches(metadata: dict[str, Any], expected: dict[str, Any]) -> bool: for path, value in expected.items(): current: Any = metadata for key in path.split("."): @@ -143,26 +363,19 @@ def _metadata_matches(metadata: dict[str, Any], expected: dict[str, Any]) -> boo return True -def method_page_model(name: str, label: str, records: list[dict[str, Any]]) -> dict[str, str]: +def method_page_model(name: str, label: str, records: list[dict[str, Any]], *, language="zh") -> dict[str, str]: """Resolve method-specific evidence into provider-neutral page fields.""" if not records: raise ValueError("method page requires at least one record") - info = method_info(records[0].get("provider_type", "")) + info = method_info(records[0].get("provider_type", ""), language=language) report = info.get("report", {}) - versions = sorted({ - str(record.get("provider_metadata", {}).get("reported_version", "")).strip() - for record in records - if str(record.get("provider_metadata", {}).get("reported_version", "")).strip() - }) + registered = report_method(name, records[0], language=language) + versions = {method_version(row, language=language) for row in records} title = label - version_rule = report.get("reported_version") - if version_rule and len(versions) == 1: - version = re.sub( - version_rule["pattern"], version_rule.get("replacement", ""), versions[0], - flags=re.I, - ) + if len(versions) == 1 and None not in versions: + version = normalized_method_version(registered, next(iter(versions))) if version not in title: - title += f" · {version}" + title = versioned_method_label(registered, label, version) model = { "title": title, "contract": info["contract"], @@ -170,28 +383,33 @@ def method_page_model(name: str, label: str, records: list[dict[str, Any]]) -> d "freshness": info["freshness"], "configuration": report.get( "configuration", - "调用参数与交付路线以本次配置和逐例记录为准。", + "调用参数与交付路线以本次配置和逐例记录为准。" if language == "zh" else "Call parameters and delivery routes follow this run’s configuration and per-case records.", ), "pricing_anchor": report.get("pricing_anchor", "method-guide-pricing"), } metadata = [record.get("provider_metadata", {}) for record in records] if report.get("observed_routes"): + route_labels = ROUTE_LABELS if language == "zh" else report_method_catalog(language)["route_labels"] extractors = sorted({ - ROUTE_LABELS.get(item.get("extractor"), item.get("extractor")) + route_labels.get(item.get("extractor"), item.get("extractor")) for item in metadata if item.get("extractor") }) contracts = sorted({str(item.get("output_contract")) for item in metadata if item.get("output_contract")}) route_count = sum(bool(item.get("extractor")) for item in metadata) output_count = sum(bool(item.get("output_contract")) for item in metadata) - route = ";".join(extractors) or "未记录" - output = "、".join(contracts) or "未记录" + missing = "未记录" if language == "zh" else "Not recorded" + route = (";" if language == "zh" else "; ").join(extractors) or missing + output = ("、" if language == "zh" else ", ").join(contracts) or missing model["configuration"] = ( f"交付路线({route_count}/{len(records)} 条记录):{route};" f"输出格式({output_count}/{len(records)} 条记录):{output}。" + ) if language == "zh" else ( + f"Delivery route ({route_count}/{len(records)} records): {route}; " + f"output format ({output_count}/{len(records)} records): {output}." ) for profile in report.get("profiles", []): - if metadata and all(_metadata_matches(item, profile["metadata_equals"]) + if metadata and all(metadata_matches(item, profile["metadata_equals"]) for item in metadata): for field in ("endpoint", "freshness", "configuration"): if field in profile: diff --git a/harness/web_fetch/native_projection.py b/harness/web_fetch/native_projection.py index 742f5787d..e24745f04 100644 --- a/harness/web_fetch/native_projection.py +++ b/harness/web_fetch/native_projection.py @@ -9,6 +9,7 @@ import hashlib import json import re +from functools import lru_cache from urllib.parse import parse_qsl, quote, urljoin, urlsplit from lexbench_webfetch.paths import validate_http_url @@ -119,7 +120,7 @@ def native_answer_projection(provider_type, case, payload, status_code=None, *, 'content_format': 'json' if success else 'empty', 'result_fields': {'final_url': {'pointer': ['meta', 'finalUrl'], 'fallback': 'request_url'}}, 'answer_fields': {key: {'pointer': ['result'], 'format': 'html_attribute_urls', - 'base_url': meta.get('finalUrl') or get('url')}} if success else {}} + 'srcset_parser': SRCSET_PARSER, 'base_url': meta.get('finalUrl') or get('url')}} if success else {}} text = success and isinstance(payload.get('result', ''), str) if not text and not meta: return None @@ -168,7 +169,37 @@ def native_field_value(payload, pointer): return value -def _attribute_record_urls(groups, base_url): +SRCSET_PARSER = 'html-candidates-v1' + + +@lru_cache(maxsize=1) +def _historical_srcset_parsers(): + registry = json.loads((Path(__file__).parent / 'data/native-projection-history.json').read_text()) + if registry.get('schema_version') != 2: + raise ValueError('Unsupported native projection history schema') + parsers = {source: entry['parser'] for source, entry in registry['scoring_sources'].items()} + if any(parser not in {SRCSET_PARSER, 'legacy-comma-split'} for parser in parsers.values()): + raise ValueError('Unsupported historical srcset parser') + return parsers + + +def _srcset_urls(value, parser, historical_scoring_source): + # Source identities bind the historical projection, never the resulting answer. + historical = _historical_srcset_parsers().get(historical_scoring_source) + if parser is not None: + if parser != SRCSET_PARSER or historical not in {None, parser}: + raise ValueError('Native srcset parser differs from its bound semantics') + else: + parser = historical + if parser == SRCSET_PARSER: + from lexbench_webfetch.resource_urls import srcset_urls + return srcset_urls(value) + if parser == 'legacy-comma-split': + return [part.strip().split()[0] for part in value.split(',') if part.strip()] + raise ValueError('Native srcset projection lacks a recognized parser binding') + + +def _attribute_record_urls(groups, base_url, *, parser=None, historical_scoring_source=None): """Project declared HTML URL attributes without classifying their targets.""" urls = [] for group in groups if isinstance(groups, list) else []: @@ -188,8 +219,8 @@ def _attribute_record_urls(groups, base_url): name = name.casefold() if name not in {'src', 'srcset', 'href', 'data-src', 'data-original'}: continue - from lexbench_webfetch.resource_urls import srcset_urls - values = srcset_urls(value) if name == 'srcset' else [value] + values = (_srcset_urls(value, parser, historical_scoring_source) + if name == 'srcset' else [value]) for raw in values: try: urls.append(urljoin(base_url, raw.strip())) @@ -216,13 +247,14 @@ def _selected(payload, spec): return spec['default'] -def _project_field(payload, spec): +def _project_field(payload, spec, *, historical_scoring_source=None): value = _selected(payload, spec) kind = spec.get('format', 'identity') if kind == 'identity': return value if kind == 'html_attribute_urls': - return _attribute_record_urls(value, spec['base_url']) + return _attribute_record_urls(value, spec['base_url'], parser=spec.get('srcset_parser'), + historical_scoring_source=historical_scoring_source) if kind == 'named_links': return [{'text': item['anchor'], 'href': item['url']} for item in value if isinstance(item, dict) @@ -261,6 +293,9 @@ def _require_field_spec(spec, field): """Reject malformed declarations with a field location, before reading data.""" if not isinstance(spec, dict): raise ValueError(field + ' must be an object') + if 'srcset_parser' in spec and (spec.get('format') != 'html_attribute_urls' + or spec['srcset_parser'] != SRCSET_PARSER): + raise ValueError(field + '.srcset_parser is unsupported') if 'format' in spec and not isinstance(spec['format'], str): raise ValueError(field + '.format must be text') if 'pointer' not in spec and spec.get('format', 'identity') not in { @@ -276,7 +311,7 @@ def _require_field_spec(spec, field): _require_field_spec(spec['legacy'], field + '.legacy') -def project_native_answer(payload, declaration): +def project_native_answer(payload, declaration, *, historical_scoring_source=None): """Select declared native fields; never inspect references or URL meaning.""" require_native_projection_schema(declaration) kind = declaration.get('content_format') @@ -305,7 +340,7 @@ def project_native_answer(payload, declaration): for name, spec in declaration.get('answer_fields', {}).items(): if name not in SCORE_ANSWER_FIELDS: raise ValueError('Unknown native answer metadata field') - fields[name] = _project_field(payload, spec) + fields[name] = _project_field(payload, spec, historical_scoring_source=historical_scoring_source) return content, fields @@ -482,7 +517,8 @@ def project_native_result_fields(payload, declaration, *, request_url=None, def require_native_answer_projection(content, metadata, raw_response, *, files=(), title=None, final_url=None, - request_url=None, diagnostic=None, transport_receipt=None): + request_url=None, diagnostic=None, transport_receipt=None, + historical_scoring_source=None): """Reject every undeclared score-bearing input, then check exact provenance.""" declaration = metadata.get('native_answer_projection') present = {key for key in SCORE_ANSWER_FIELDS if metadata.get(key)} @@ -532,7 +568,7 @@ def check_source(spec): for spec in declaration.get('answer_fields', {}).values(): if spec.get('format') == 'html_attribute_urls' and spec.get('base_url') != (final_url or request_url): raise ValueError('Native resource base URL differs from its verified result URL') - expected, fields = project_native_answer(payload, declaration) + expected, fields = project_native_answer(payload, declaration, historical_scoring_source=historical_scoring_source) if content != expected or any(metadata.get(key) != value for key, value in fields.items()): raise ValueError('Scored answer differs from its declared native field projection') if [item['data'] for item in files] != project_native_files(payload, declaration): diff --git a/harness/web_fetch/native_routes.py b/harness/web_fetch/native_routes.py index dfc814cc6..abccf1281 100644 --- a/harness/web_fetch/native_routes.py +++ b/harness/web_fetch/native_routes.py @@ -567,7 +567,8 @@ def verify_saved_native_answer(root, record, *, cases_by_id=None): title=record.get('title') if declaration.get('schema_version') == 2 else None, final_url=record.get('final_url') if declaration.get('schema_version') == 2 else None, request_url=record.get('url'), - diagnostic=diagnostic, transport_receipt=metadata.get('source_transport_receipt')) + diagnostic=diagnostic, transport_receipt=metadata.get('source_transport_receipt'), + historical_scoring_source=record.get('metrics', {}).get('scoring_source_sha256')) if files or 'file_evaluation' in metadata: from lexbench_webfetch.file_acceptance import evaluate_files source_verification = {} diff --git a/harness/web_fetch/providers/cloudflare.py b/harness/web_fetch/providers/cloudflare.py index 804ba27ea..d516a2310 100644 --- a/harness/web_fetch/providers/cloudflare.py +++ b/harness/web_fetch/providers/cloudflare.py @@ -499,7 +499,7 @@ def _fetch_media_resources( if not isinstance(payload, dict): payload = {} envelope_success = payload.get("success") is True - from ..native_projection import project_native_answer + from ..native_projection import project_native_answer, SRCSET_PARSER answer_key = 'images' if case.track == 'image' else 'videos' meta = payload.get('meta') if isinstance(payload.get('meta'), dict) else {} effective_url = meta.get('finalUrl') or case.url @@ -508,7 +508,7 @@ def _fetch_media_resources( 'pointer': ['meta', 'finalUrl'], 'fallback': 'request_url'}}, 'answer_fields': {answer_key: { 'pointer': ['result'], 'format': 'html_attribute_urls', - 'base_url': effective_url}}} + 'srcset_parser': SRCSET_PARSER, 'base_url': effective_url}}} answer, fields = ('', {}) if envelope_success: answer, fields = project_native_answer(payload, projection) diff --git a/harness/web_fetch/report.py b/harness/web_fetch/report.py index 6ab24d128..1acdf8fa8 100644 --- a/harness/web_fetch/report.py +++ b/harness/web_fetch/report.py @@ -16,8 +16,8 @@ from harness.record_io import load_jsonl -from .accounts import report_access_registry from .failure_diagnosis import diagnose +from harness.numbers import nonnegative_number from .methods import ROUTE_LABELS, method_page_model, is_browser_comparison, browser_method_label from lexbench_webfetch.metrics import percentile, task_measured, task_passed from harness.web_fetch.models import REPORT_FAMILIES, execution_id, report_family @@ -36,15 +36,17 @@ tabs, task_instruction, ) -from .report_interfaces import HEADLESS_ACCESS, method_access, method_version, method_provenance +from .report_interfaces import method_provenance +from .methods import method_access from .report_methods import PROFILES, adoption_counts +from .methods import report_method, method_version, report_method_catalog from .report_resources import render_resource_comparison from .report_style import STYLE from lexbench_webfetch.paths import contained_path from harness.web_fetch.safety import safe_href, validate_report_ids from .webmainbench_diagnostics import ERROR_PAGE -PROVIDER_LABELS={'moli-1.0.6':'Moli 1.0.6','moli':'Moli','moli-1.1.3':'Moli 1.1.3','moli-1.1.7':'Moli 1.1.7','moli-pr643':'Moli 1.1.7 · PR #643','moli-1.1.11':'Moli 1.1.11'} + def format_wcxb_conditional_latency(value: float | None) -> str: @@ -80,6 +82,9 @@ def generate_wcxb(out: Path, rows: list[dict], summary: dict, ground_truth: dict if len(by_case)!=case_count or any(set(group)!=set(providers) for group in by_case.values()): raise ValueError('WCXB same-case matrix is incomplete') from .wcxb_latency import load_full_paired_latency, summarize_full_paired_latency + from .wcxb_scoring import paired_latency_methods + paired_methods=paired_latency_methods(profile,{"providers":[{"provider":p,"identity":identities[p]} for p in providers]}) + pair={role:provider for provider,role in paired_methods.items()} _, selected_latency = load_full_paired_latency(out/'research/wcxb-full-paired-latency', profile, set(by_case)) paired_latency = summarize_full_paired_latency(selected_latency, set(by_case)) adoption_data=json.loads((out/'research/provider-adoption.json').read_text()) @@ -91,16 +96,14 @@ def generate_wcxb(out: Path, rows: list[dict], summary: dict, ground_truth: dict script='''function theme(){document.body.classList.toggle('dark');try{localStorage.setItem('wcxb-theme',document.body.classList.contains('dark')?'dark':'light')}catch(e){}}let savedTheme='light';try{savedTheme=localStorage.getItem('wcxb-theme')}catch(e){}if(savedTheme==='dark')document.addEventListener('DOMContentLoaded',()=>document.body.classList.add('dark'));function filter(){let q=(document.getElementById('q')?.value||'').toLowerCase(),p=document.getElementById('provider')?.value||'',t=document.getElementById('type')?.value||'',scope=document.getElementById('scope')?.value||'issues',n=0;document.querySelectorAll('tr[data-provider]').forEach(r=>{let show=(!q||r.dataset.search.includes(q))&&(!p||r.dataset.provider===p)&&(!t||r.dataset.type===t)&&(scope==='all'||(scope==='issues'?r.dataset.diagnosis!=='none':r.dataset.diagnosis===scope));r.hidden=!show;if(show)n++});let c=document.getElementById('count');if(c)c.textContent=n}const evidenceCache=new Map();function evidence(path){if(!evidenceCache.has(path))evidenceCache.set(path,fetch(path).then(r=>{if(!r.ok)throw Error(r.status);return r.json()}));return evidenceCache.get(path)}function loadOutput(d){let pre=d.querySelector('pre[data-src]');if(!pre||pre.dataset.loaded)return;pre.dataset.loaded='1';evidence(pre.dataset.src).then(data=>pre.dataset.provider?data.outputs[pre.dataset.provider]:data.source_html).then(value=>pre.textContent=value).catch(e=>pre.textContent='Unable to load evidence: '+e)}function showCase(){let s=document.getElementById('case-select'),body=document.getElementById('case-matrix');if(!s||!body)return;let d=window.WCXB_CASES?.[s.value];if(!d)return;let prefix=document.documentElement.lang==='en'?'en/':'';body.innerHTML=d.rows.map(r=>''+r.provider+''+r.f1+''+r.precision+''+r.recall+''+r.diagnosis+'').join('')}document.addEventListener('DOMContentLoaded',()=>{document.querySelectorAll('.filters input,.filters select').forEach(x=>x.addEventListener('input',filter));document.querySelectorAll('details[data-output]').forEach(d=>d.addEventListener('toggle',()=>{if(d.open)loadOutput(d)}));document.getElementById('case-select')?.addEventListener('change',showCase);showCase();filter()})''' script=script[:script.index('function filter(){')]+script[script.index('const evidenceCache=new Map();'):] L={ - 'zh':{'home':'首页','results':'结果分析','method':'评测方法','guide':'报告解读','repro':'环境与复现','theme':'切换主题','headline':'WCXB 主内容提取对比','core':'核心结果','provider':'浏览器','delivered':'生成非空内容','precision':'精确率','recall':'召回率','f1':'F1','latency':'平均端到端耗时','type':'页面类型','case':'网页','diagnosis':'定位','detail':'查看同页证据','all':'全部','issues':'待查问题','query':'搜索网页或错误','count':'条结果','reference':'人工主内容参考','output':'完整浏览器输出','error':'原始程序错误','missing':'参考中未匹配的高频词','extra':'输出中多出的高频词','summary':'同一网页的五种结果','note':'低精确率通常表示输出夹带较多参考外文字;低召回率表示参考词语大量缺失。这是定位线索,不能仅凭词袋分数确定程序故障。','scope':'统计口径','limit':'边界与已知标注问题','source':'冻结来源','version':'实测版本或服务身份','no_review':f'典型异常审查覆盖 {reviewed_cases} 页、{reviewed_results} 条输出;其余 {unreviewed_results:,} 条尚未逐项人工审查。定位分类由确定性规则产生,不等同于根因。'}, - 'en':{'home':'Conclusions','results':'Results analysis','method':'Method','guide':'Interpretation','repro':'Reproduce','theme':'Theme','headline':'WCXB main-content extraction comparison','core':'Core results','provider':'Browser','delivered':'Nonempty outputs','precision':'Precision','recall':'Recall','f1':'F1','latency':'Mean end-to-end latency','type':'Page type','case':'Page','diagnosis':'Locator','detail':'Same-page evidence','all':'All','issues':'Issues to inspect','query':'Search page or error','count':'results','reference':'Human main-content reference','output':'Complete browser output','error':'Exact program error','missing':'Frequent unmatched reference words','extra':'Frequent extra output words','summary':'Five results for this page','note':'Low precision often means output contains much text outside the reference; low recall means much reference text is missing. These are locators, not proof of a program defect.','scope':'Population and calculation','limit':'Limits and annotation issues','source':'Frozen source','version':'Measured version or service identity','no_review':f'Targeted anomaly review covers {reviewed_cases} pages and {reviewed_results} outputs; the other {unreviewed_results:,} results have not been individually reviewed. Deterministic locators are not root causes.'}} + 'zh':{'home':'首页','results':'结果分析','method':'评测方法','guide':'报告解读','repro':'环境与复现','theme':'切换主题','headline':'WCXB 主内容提取对比','core':'核心结果','provider':'浏览器','delivered':'生成非空内容','precision':'精确率','recall':'召回率','f1':'F1','latency':'平均端到端耗时','type':'页面类型','case':'网页','diagnosis':'定位','detail':'查看同页证据','all':'全部','issues':'待查问题','query':'搜索网页或错误','count':'条结果','reference':'人工主内容参考','output':'完整浏览器输出','error':'原始程序错误','missing':'参考中未匹配的高频词','extra':'输出中多出的高频词','summary':'同一网页的全部方法结果','note':'低精确率通常表示输出夹带较多参考外文字;低召回率表示参考词语大量缺失。这是定位线索,不能仅凭词袋分数确定程序故障。','scope':'统计口径','limit':'边界与已知标注问题','source':'冻结来源','version':'实测版本或服务身份','no_review':f'典型异常审查覆盖 {reviewed_cases} 页、{reviewed_results} 条输出;其余 {unreviewed_results:,} 条尚未逐项人工审查。定位分类由确定性规则产生,不等同于根因。'}, + 'en':{'home':'Conclusions','results':'Results analysis','method':'Method','guide':'Interpretation','repro':'Reproduce','theme':'Theme','headline':'WCXB main-content extraction comparison','core':'Core results','provider':'Browser','delivered':'Nonempty outputs','precision':'Precision','recall':'Recall','f1':'F1','latency':'Mean end-to-end latency','type':'Page type','case':'Page','diagnosis':'Locator','detail':'Same-page evidence','all':'All','issues':'Issues to inspect','query':'Search page or error','count':'results','reference':'Human main-content reference','output':'Complete browser output','error':'Exact program error','missing':'Frequent unmatched reference words','extra':'Frequent extra output words','summary':'All method results for this page','note':'Low precision often means output contains much text outside the reference; low recall means much reference text is missing. These are locators, not proof of a program defect.','scope':'Population and calculation','limit':'Limits and annotation issues','source':'Frozen source','version':'Measured version or service identity','no_review':f'Targeted anomaly review covers {reviewed_cases} pages and {reviewed_results} outputs; the other {unreviewed_results:,} results have not been individually reviewed. Deterministic locators are not root causes.'}} L['zh'].update({'dataset':'评测集','review':'核验记录','adoption':'方法介绍','changes':'变更记录','accounts':'账号与安装'}) L['en'].update({'dataset':'Dataset','review':'Verification','adoption':'Methods compared','changes':'Changes','accounts':'Accounts and setup'}) - L['zh']['headline']='WCXB 主内容提取:五款浏览器、六种配置' - L['en']['headline']='WCXB main-content extraction: five browsers, six configurations' - L['zh']['summary']='同一网页的全部方法结果' - L['en']['summary']='All method results for this page' - L['zh']['intro']=f'五款浏览器的 {len(providers)} 种配置处理同一批 {case_count} 页冻结网页;Moli 正式版与 PR 修复版分别计分。F1 比较提取词语与人工主内容的重合,不衡量 Markdown 结构或链接可用性。' - L['en']['intro']=f'{len(providers)} configurations across five browsers processed the same {case_count} frozen pages; released and repaired Moli builds have separate results. F1 measures word overlap with human main content, not Markdown structure or link usability.' + L['zh']['headline']=f'WCXB 主内容提取:{len(providers)} 种配置' + L['en']['headline']=f'WCXB main-content extraction: {len(providers)} configurations' + L['zh']['intro']=f'{len(providers)} 种配置处理同一批 {case_count} 页冻结网页;每种配置分别计分。F1 比较提取词语与人工主内容的重合,不衡量 Markdown 结构或链接可用性。' + L['en']['intro']=f'{len(providers)} configurations processed the same {case_count} frozen pages; each configuration has separate results. F1 measures word overlap with human main content, not Markdown structure or link usability.' style+='''body{background:#f5f8fc}body.dark{background:#101923}header{background:#fff;color:#1b2c41;border-bottom:1px solid #dce5ee}body.dark header{background:#172534;color:#eaf3ff;border-color:#31475d}header a{color:#1156ae}body.dark header a{color:#92c3ff}header .wrap{max-width:1220px;padding-top:13px;padding-bottom:13px}header strong{font-family:ui-monospace,monospace;color:#1554a0}.wrap{max-width:1220px}.hero{padding:58px 0 46px}.hero h1{font-size:clamp(34px,4.5vw,56px);letter-spacing:-.035em}.hero p{font-size:17px}.eyebrow{font:700 12px ui-monospace,monospace;letter-spacing:.1em;color:#1457ac}.callout{padding:16px 20px;border-left:4px solid #21845b;background:#eaf8f2;border-radius:0 12px 12px 0;font-weight:650}.panel{box-shadow:0 8px 24px rgba(25,56,88,.04)}.lead-grid{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:14px}.lead-grid .panel{margin:0}.lead-grid b{display:block;color:#1457ac;margin-bottom:7px}.section-lead{max-width:840px;color:#50657c}.winner{color:#117647;font-weight:800}body.dark .winner{color:#8fe3b5}.tabs{display:flex;gap:8px;flex-wrap:wrap}.tabs a{padding:7px 10px;border-radius:8px;background:#e9f0f8;text-decoration:none}.evidence-links{display:flex;gap:12px;flex-wrap:wrap}.reader-grid{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:18px}@media(max-width:700px){.lead-grid,.reader-grid{grid-template-columns:1fr}header nav{gap:8px 14px}header .wrap{display:block}header strong{display:block;margin-bottom:8px}.hero{padding:34px 0 24px}}''' style+='''.hero{position:relative;isolation:isolate;padding:70px 0 54px}.hero:before{content:"";position:absolute;left:50%;top:0;transform:translateX(-50%);height:100%;width:100vw;background:#f1f6ff;border-bottom:1px solid #dbe5f1;z-index:-1}body.dark .hero:before{background:#152638;border-color:#31465c}body.dark .callout{background:#163b31;color:#e5fff0}body.dark .lead-grid b{color:#9bc6ff}.panel p{overflow-wrap:anywhere}header button{border-color:#cbd9e8;background:#fff;color:#243a52}.scroll{scrollbar-gutter:stable}h2{letter-spacing:-.02em}.hero+.callout{margin-top:26px;margin-bottom:42px}@media(max-width:700px){.hero{padding:42px 0 30px}.hero+.callout{margin-top:18px}}''' style+='''.flow{overflow:auto;padding:14px;border:1px solid #dbe3eb;border-radius:12px;background:#fff}.flow .mermaid{max-height:none;min-width:920px;background:transparent;color:inherit}.flow-fallback{display:flex;align-items:center;gap:8px;min-width:820px}.flow-fallback .tag{border-radius:9px;padding:8px 12px;white-space:nowrap}@media(max-width:700px){.flow-fallback{min-width:0;display:grid;grid-template-columns:1fr}.flow-fallback .arrow{text-align:center;transform:rotate(90deg)}}''' @@ -139,13 +142,14 @@ def fmt(v,digits=1):return f'{v*100:.{digits}f}%' def diag(lang,value):return diag_labels[lang].get(value,value.replace('_',' ')) type_labels={'article':'文章','service':'服务','product':'商品','collection':'合集','forum':'论坛','listing':'列表','documentation':'文档'} def type_label(lang,value):return type_labels.get(value,value) if lang=='zh' else value + access_records=[{'provider':p, 'provider_metadata':identities[p]} for p in providers] def browser_label(provider): - if provider=='moli-pr643':return 'Moli PR #643 · 384d1d2a' - if provider=='moli':return 'Moli 1.1.8' - version=identities[provider].get('reported_version') or '' - match=re.search(r'\d+(?:\.\d+)+',version) - number=(match.group(0).split('.')[0] if provider=='chromium' else match.group(0)) if match else '' - return provider.capitalize()+(' '+number if number else '') + from .methods import versioned_method_label + record={'provider':provider, 'provider_metadata':identities[provider]} + registered=report_method(provider,record) + base=registered.get('label',{}).get('en',identities[provider].get('engine') or provider) + version=identities[provider].get('reported_version') + return versioned_method_label(registered,base,version) if version else base def comparison_cell(value, metric, direction, winning, display): marker=f'{display}' if winning else display return f'{marker}' @@ -161,12 +165,14 @@ def score_digits(values, direction='max'): return 4 for lang in (('zh','en') if include_english else ('zh',)): x=L[lang] - best=max(providers,key=lambda p:summary['providers'][p]['quality_f1']);fast=min(providers,key=lambda p:summary['providers'][p]['mean_latency_ms']) + best_value=max(summary['providers'][p]['quality_f1'] for p in providers) + fast_value=min(summary['providers'][p]['mean_latency_ms'] for p in providers) + best=[p for p in providers if summary['providers'][p]['quality_f1']==best_value] + fast=[p for p in providers if summary['providers'][p]['mean_latency_ms']==fast_value] metrics=(('quality_precision','max','precision'),('quality_recall','max','recall'),('quality_f1','max','f1'),('mean_latency_ms','min','mean'),('p50_latency_ms','min','p50'),('p95_latency_ms','min','p95')) winners={key:unique_winner({p:summary['providers'][p][key] for p in providers},direction) for key,direction,_ in metrics} metric_digits={key:score_digits({p:summary['providers'][p][key] for p in providers},direction) for key,direction,_ in metrics if direction=='max'} - if 'moli' in providers and 'moli-pr643' in providers: - metric_digits['quality_f1']=4 + metric_digits['quality_f1']=max(metric_digits['quality_f1'],4) core_rows=[] for p in sorted(providers,key=lambda name:summary['providers'][name]['quality_f1'],reverse=True): s=summary['providers'][p] @@ -179,23 +185,17 @@ def score_digits(values, direction='max'): next_direction=' data-next-direction="asc"' if index==4 else '' core_header_html.append(f'' if index else f'{esc(label)}') core='
'+''.join(core_header_html)+''+''.join(core_rows)+'
' - moli_before=summary['providers'].get('moli') - moli_after=summary['providers'].get('moli-pr643') - if moli_before and moli_after: - page_deltas=[by_case[c]['moli-pr643']['f1']-by_case[c]['moli']['f1'] for c in by_case if c not in exclusion_ids] - up=sum(delta>1e-10 for delta in page_deltas);down=sum(delta< -1e-10 for delta in page_deltas);same=quality_count-up-down - precision_delta=(moli_after['quality_precision']-moli_before['quality_precision'])*100 - recall_delta=(moli_after['quality_recall']-moli_before['quality_recall'])*100 - f1_delta=(moli_after['quality_f1']-moli_before['quality_f1'])*100 - paired=(f'

Moli 修复版改善了什么

PR #643 修复代码内容粘连,并防止可选清理删掉商品详情。{quality_count} 页中,5412、5413 的逐页 F1 上升,5411、5458 下降,其余 {same} 页不变。召回均值提高 {recall_delta:+.4f} 个百分点,精确率均值变化 {precision_delta:+.4f} 个百分点,F1 变化 {f1_delta:+.4f} 个百分点。F1 是逐页 F1 的平均,并非用首页平均精确率和召回率重新计算;两项降幅略大,故召回提高而平均 F1 略降。5458 的内联 HTML 标签名还会进入精确率分母。

查看代码内容对照 · 查看逐页原因

' if lang=='zh' else f'

What the Moli fix improves

PR #643 separates joined code values and keeps product details when optional UI stripping is used. Per-page F1 rises on 5412 and 5413, falls on 5411 and 5458, and is unchanged on the other {same} pages. Mean recall changes {recall_delta:+.4f} percentage points, mean precision {precision_delta:+.4f}, and mean F1 {f1_delta:+.4f}. F1 is the mean of the {quality_count} page-level F1 values, not a recomputation from the headline mean precision and recall. The two decreases are slightly larger, so mean recall rises while average F1 falls slightly; on 5458, raw scoring also counts the inline HTML tag names in the precision denominator.

Compare the code output · Read the page-level analysis

') - else:paired='' - latency_context=(f'

Moli 1.1.8 与实测 PR 在同一批 {case_count} 页做交叉顺序复测;主表取预定最终轮:稳定版 {moli_before["mean_latency_ms"]:,.0f} ms,PR 版 {moli_after["mean_latency_ms"]:,.0f} ms,逐页平均差 {paired_latency["paired_delta_mean_ms"]:+.1f} ms。两版源码基线不同,耗时差不能全归因于本 PR。查看测速依据

' if lang=='zh' else f'

Moli 1.1.8 and the measured PR were measured in a balanced same-window crossover on all {case_count} pages. The preselected final round gives {moli_before["mean_latency_ms"]:,.0f} ms for the release and {moli_after["mean_latency_ms"]:,.0f} ms for the PR, a {paired_latency["paired_delta_mean_ms"]:+.1f} ms page-paired difference. The codebases diverge, so this difference cannot all be attributed to this PR. Read the timing basis

') - lead=(f'{browser_label(best)} 在 {quality_count} 页有效参考上的平均 F1 最高({fmt(summary["providers"][best]["quality_f1"])});{browser_label(fast)} 的 {case_count} 次调用平均端到端耗时最短({summary["providers"][fast]["mean_latency_ms"]:,.0f} ms)。' if lang=='zh' else f'{browser_label(best)} leads mean F1 across {quality_count} qualified pages ({fmt(summary["providers"][best]["quality_f1"])}); {browser_label(fast)} has the shortest mean end-to-end latency across {case_count} calls ({summary["providers"][fast]["mean_latency_ms"]:,.0f} ms).') + before=summary['providers'][pair['release']] + after=summary['providers'][pair['pr']] + latency_context=(f'

{esc(browser_label(pair["release"]))} 与 {esc(browser_label(pair["pr"]))} 在同一批 {case_count} 页做交叉顺序复测;主表取预定最终轮:稳定版 {before["mean_latency_ms"]:,.0f} ms,PR 版 {after["mean_latency_ms"]:,.0f} ms,逐页平均差 {paired_latency["paired_delta_mean_ms"]:+.1f} ms。耗时差描述本轮实测,不能单凭耗时确定变化原因。查看测速依据

' if lang=='zh' else f'

{esc(browser_label(pair["release"]))} and {esc(browser_label(pair["pr"]))} were measured in a balanced same-window crossover on all {case_count} pages. The preselected final round gives {before["mean_latency_ms"]:,.0f} ms for the release and {after["mean_latency_ms"]:,.0f} ms for the PR, a {paired_latency["paired_delta_mean_ms"]:+.1f} ms page-paired difference. Timing differences describe this measured comparison; timing alone does not establish their cause. Read the timing basis

') + best_labels=('、' if lang=='zh' else ', ').join(browser_label(p) for p in best) + fast_labels=('、' if lang=='zh' else ', ').join(browser_label(p) for p in fast) + lead=(f'{best_labels} 在 {quality_count} 页有效参考上的平均 F1 {"并列最高" if len(best)>1 else "最高"}({fmt(best_value)});{fast_labels} 的 {case_count} 次调用平均端到端耗时{"并列最短" if len(fast)>1 else "最短"}({fast_value:,.0f} ms)。' if lang=='zh' else f'{best_labels} {"tie for the highest" if len(best)>1 else "has the highest"} mean F1 across {quality_count} qualified pages ({fmt(best_value)}); {fast_labels} {"tie for the shortest" if len(fast)>1 else "has the shortest"} mean end-to-end latency across {case_count} calls ({fast_value:,.0f} ms).') type_counts=WordCounter(gt['page_type'] for c,gt in ground_truth.items() if c not in exclusion_ids) slice_values={t:{p:sum(by_case[c][p]['f1'] for c,g in ground_truth.items() if g['page_type']==t and c not in exclusion_ids)/type_counts[t] for p in providers} for t in type_counts} slices=''.join(f'{esc(type_label(lang,t))}{type_counts[t]}'+''.join(comparison_cell(slice_values[t][p],f'type-{t}','max',p==unique_winner(slice_values[t],'max'),fmt(slice_values[t][p],score_digits(slice_values[t]))) for p in providers)+'' for t in sorted(type_counts)) slice_table='
'+''.join(f'' for p in providers)+''+slices+'
'+esc(x['type'])+''+('网页数' if lang=='zh' else 'Pages')+'{esc(browser_label(p))} F1
' - home=f'
Web Fetch · WCXB v1.0

{esc(x["headline"])}

{esc(x["intro"])}

{esc(x["core"])}

{esc(lead)}

'+((f'绿色星标是该列唯一最优值。质量和三项耗时均以同一批 {case_count} 页为分母,耗时包含失败重试。P50 是中位数,P95 是 95% 调用不超过的耗时。不同方法的批次跨日期;Moli 两版耗时已用同窗交叉实测纠正。') if lang=='zh' else f'Green stars mark a unique column best. Quality and all three latency measures use the same {case_count} pages; latency includes failed retries. P50 is the median and P95 is the time within which 95% of calls complete. Other methods ran on different dates; the two Moli timings use the corrected same-window crossover.')+f'

{core}{latency_context}
'+paired+f'

{"不同网页类型" if lang=="zh" else "By page type"}

{"筛选页面类型;每格是该类型的逐页平均 F1,绿色星标是该行唯一最高值。" if lang=="zh" else "Filter by page type. Each cell is mean page F1; a green star marks the unique row leader."}

{slice_table}

{esc(x["dataset"])}

{"查看冻结网页、来源和人工参考。" if lang=="zh" else "Inspect frozen pages, source URLs and references."}

{esc(x["dataset"])} →

{esc(x["results"])}

{"筛选逐项结果,查看响应和定位证据。" if lang=="zh" else "Filter individual results and inspect responses and evidence."}

{esc(x["results"])} →

{esc(x["guide"])}

{"读懂结果差异与评分边界。" if lang=="zh" else "Understand differences and scoring limits."}

{esc(x["guide"])} →
' + home=f'
Web Fetch · WCXB v1.0

{esc(x["headline"])}

{esc(x["intro"])}

{esc(x["core"])}

{esc(lead)}

'+((f'绿色星标是该列唯一最优值。质量和三项耗时均以同一批 {case_count} 页为分母,耗时包含失败重试。P50 是中位数,P95 是 95% 调用不超过的耗时。各方法耗时描述各自实测批次;指定两组配置的同窗比较以成对测速记录为准。') if lang=='zh' else f'Green stars mark a unique column best. Quality and all three latency measures use the same {case_count} pages; latency includes failed retries. P50 is the median and P95 is the time within which 95% of calls complete. Each method describes its measured batch; the designated pair’s timings use the certified same-window crossover.')+f'

{core}{latency_context}
'+f'

{"不同网页类型" if lang=="zh" else "By page type"}

{"筛选页面类型;每格是该类型的逐页平均 F1,绿色星标是该行唯一最高值。" if lang=="zh" else "Filter by page type. Each cell is mean page F1; a green star marks the unique row leader."}

{slice_table}

{esc(x["dataset"])}

{"查看冻结网页、来源和人工参考。" if lang=="zh" else "Inspect frozen pages, source URLs and references."}

{esc(x["dataset"])} →

{esc(x["results"])}

{"筛选逐项结果,查看响应和定位证据。" if lang=="zh" else "Filter individual results and inspect responses and evidence."}

{esc(x["results"])} →

{esc(x["guide"])}

{"读懂结果差异与评分边界。" if lang=="zh" else "Understand differences and scoring limits."}

{esc(x["guide"])} →
' success_rows=''.join(f'{esc(p)}{summary["providers"][p]["success_latency_count"]}/{case_count}{format_wcxb_conditional_latency(summary["providers"][p]["success_mean_latency_ms"])}{format_wcxb_conditional_latency(summary["providers"][p]["success_p50_latency_ms"])}{format_wcxb_conditional_latency(summary["providers"][p]["success_p95_latency_ms"])}' for p in providers) if any(summary['providers'][p]['success_latency_count'] != case_count for p in providers): home+='

'+('仅成功交付的条件耗时' if lang=='zh' else 'Conditional latency for delivered content')+'

'+('这张表只计算最终交付非空正文的页面,分母随方法变化;每次失败重试的耗时仍计入该页。完整共同分母的同题耗时见首页核心表。' if lang=='zh' else 'Only pages with a final nonempty response enter this table, so denominators differ by method; failed retry time remains part of that page. The core table above retains the full common-denominator latency.')+'

'+success_rows+'
'+esc(x['provider'])+''+('成功页数' if lang=='zh' else 'Delivered pages')+'MeanP50P95
' @@ -230,10 +230,16 @@ def score_digits(values, direction='max'): flow_labels=(['冻结 HTML',f'{len(providers)} 种配置转换','最终响应与重试','官方逐页评分','有效参考汇总'] if lang=='zh' else ['Frozen HTML',f'{len(providers)} conversions','Final response and retries','Official page score','Qualified aggregate']) flow_source='flowchart LR\n A["'+flow_labels[0]+'"] --> B["'+flow_labels[1]+'"]\n B --> C["'+flow_labels[2]+'"]\n C --> D["'+flow_labels[3]+'"]\n D --> E["'+flow_labels[4]+'"]\n C -- '+('失败重试' if lang=='zh' else 'failure retry')+' --> B' flow='

'+('一页怎样变成一行结果' if lang=='zh' else 'From one page to one result')+'

'+' → '.join(f'{esc(label)}' for label in flow_labels)+'
' + from .report_interfaces import method_spec, _observed_route + from .methods import native_browser_engine + measured_records={p:{'provider':p,'provider_metadata':identities[p]} for p in providers} + route_descriptions={p:_observed_route(native_browser_engine(p,measured_records[p]),identities[p],language=lang) for p in providers} + interface_specs={p:method_spec(p,measured_records[p],language=lang) for p in providers} + measured_routes='
    '+''.join(f'
  • {esc(browser_label(p))}: {esc(route_descriptions[p])}
  • ' for p in providers)+'
' if lang=='zh': - method=f'

评测方法

{case_count} 份冻结 HTML、六种执行配置、一套官方词语重合评分。

'+flow+f'

输入与运行边界

WCXB v1.0 test 分区中,按冻结输入和人工参考核定的 {case_count} 页进入本报告。六种配置读取完全相同的冻结 HTML;本地五种配置通过隔离的回环 HTTPS 服务读取,Kitesurf 通过 Cloudflare HTML 输入接口读取。外部子资源被阻断。每页一次逻辑调用;失败最多再试一次,全部物理调用保留并累计端到端耗时,最后一次响应是唯一正式结果。

Kitesurf、Lightpanda、Obscura 和 Moli 返回原生 Markdown;Chromium 渲染整页 DOM,再用 node-html-markdown 2.0.0 转换。没有共同的主内容选择器。

分母与评分

官方评估器将文本转为小写词语,按词频计算精确率、召回率和 F1。质量比较只使用事前验证的 {quality_count} 页有效参考,逐页等权。F1 不计段落顺序、Markdown 结构或表格行列。应包含和应排除片段另作诊断,不进入 F1。

质量与耗时都使用同一批 {case_count} 页;正式版与 PR 构建版分别保留原始调用证据。

定位与耗时

请求失败、空输出和错误页单独定位;非空参考下精确率或召回率低于 25% 时显示对应低值。该阈值只是筛选条件,不是 WCXB 官方及格线。耗时包括调用及失败重试,不是浏览器内核速度;六种配置的工作路线和远近端部署不同。

' + method=f'

评测方法

{case_count} 份冻结 HTML、全部执行配置、一套官方词语重合评分。

'+flow+f'

输入与运行边界

WCXB v1.0 test 分区中,按冻结输入和人工参考核定的 {case_count} 页进入本报告。全部配置读取完全相同的冻结 HTML;本地配置通过隔离的回环 HTTPS 服务读取,远程配置使用 HTML 输入接口。外部子资源被阻断。每页一次逻辑调用;失败最多再试一次,全部物理调用保留并累计端到端耗时,最后一次响应是唯一正式结果。

没有共同的主内容选择器。

{measured_routes}

分母与评分

官方评估器将文本转为小写词语,按词频计算精确率、召回率和 F1。质量比较只使用事前验证的 {quality_count} 页有效参考,逐页等权。F1 不计段落顺序、Markdown 结构或表格行列。应包含和应排除片段另作诊断,不进入 F1。

质量与耗时都使用同一批 {case_count} 页;正式版与 PR 构建版分别保留原始调用证据。

定位与耗时

请求失败、空输出和错误页单独定位;非空参考下精确率或召回率低于 25% 时显示对应低值。该阈值只是筛选条件,不是 WCXB 官方及格线。耗时包括调用及失败重试,不是浏览器内核速度;全部配置的工作路线和远近端部署不同。

' else: - method=f'

Method

All {case_count} frozen HTML pages, six configurations, one official word-overlap evaluator.

'+flow+f'

Inputs and execution

The {case_count} WCXB v1.0 test pages qualified by frozen input and human reference enter this report. The six configurations read the same frozen HTML. Five local configurations use an isolated loopback HTTPS fixture; Kitesurf receives HTML through Cloudflare’s input field. External subresources are blocked. Each page has one logical result; a failed call may be retried once. Every physical call and its latency are retained, with the final response authoritative.

Kitesurf, Lightpanda, Obscura and Moli return native Markdown. Chromium renders the full DOM and converts it with node-html-markdown 2.0.0. No shared main-content selector is applied.

Denominators and score

The official evaluator lowercases and tokenizes text, then computes word-frequency precision, recall and F1. Quality compares only the {quality_count} references qualified before calls, with equal page weights. F1 does not score paragraph order, Markdown structure or table layout. Required and forbidden snippets are separate diagnostics outside F1.

Quality and latency use the same {case_count} pages. Released and PR-build Moli calls each retain their own immutable evidence.

Locators and latency

Request errors, empty outputs and error pages are separate locators. Precision or recall below 25% on nonempty references triggers a low-value locator; this is a filter, not an official WCXB pass threshold. Latency includes calls and failed retries, not pure browser-core time; local and remote routes differ.

' + method=f'

Method

All {case_count} frozen HTML pages, participating configurations, one official word-overlap evaluator.

'+flow+f'

Inputs and execution

The {case_count} WCXB v1.0 test pages qualified by frozen input and human reference enter this report. The participating configurations read the same frozen HTML. Local configurations use an isolated loopback HTTPS fixture; remote configurations use an HTML input interface. External subresources are blocked. Each page has one logical result; a failed call may be retried once. Every physical call and its latency are retained, with the final response authoritative.

No shared main-content selector is applied.

{measured_routes}

Denominators and score

The official evaluator lowercases and tokenizes text, then computes word-frequency precision, recall and F1. Quality compares only the {quality_count} references qualified before calls, with equal page weights. F1 does not score paragraph order, Markdown structure or table layout. Required and forbidden snippets are separate diagnostics outside F1.

Quality and latency use the same {case_count} pages. The paired configurations’ calls each retain their own immutable evidence.

Locators and latency

Request errors, empty outputs and error pages are separate locators. Precision or recall below 25% on nonempty references triggers a low-value locator; this is a filter, not an official WCXB pass threshold. Latency includes calls and failed retries, not pure browser-core time; local and remote routes differ.

' guide=report_content['guide'][lang] method+='

'+('片段标注边界' if lang=='zh' else 'Snippet annotation boundary')+'

'+((f'{audit["without_present_in_main_content"]} 个应排除片段同时出现在人工主内容参考中。官方片段分照原程序保留,但这类冲突不用于判断浏览器错误;逐页详情会明确标注冲突。') if lang=='zh' else f'{audit["without_present_in_main_content"]} forbidden snippets also appear in their human main-content references. Official snippet values are retained, but these conflicts do not diagnose browser errors; result pages identify them explicitly.')+'

' page(lang,'methodology/index.html',x['method'],method) @@ -250,30 +256,34 @@ def score_digits(values, direction='max'): f'{"阅读冻结 HTML" if lang=="zh" else "Read frozen HTML"}' f'{"人工主内容" if lang=="zh" else "Human main content"}' f'{"查看结果" if lang=="zh" else "View results"}') - dataset_intro=(f'本轮任务是从 {case_count} 页冻结 HTML 提取主内容。这里列出输入和人工参考;六种配置的实测分数在结果分析中。' if lang=='zh' else f'The task is to extract main content from {case_count} frozen HTML pages. This inventory owns inputs and human references; the measured browser scores are in Results analysis.') + dataset_intro=(f'本轮任务是从 {case_count} 页冻结 HTML 提取主内容。这里列出输入和人工参考;全部配置的实测分数在结果分析中。' if lang=='zh' else f'The task is to extract main content from {case_count} frozen HTML pages. This inventory owns inputs and human references; the measured browser scores are in Results analysis.') dataset=f'

{esc(x["dataset"])}

{esc(dataset_intro)}

{"冻结网页与参考" if lang=="zh" else "Frozen pages and references"}

{"人工参考是源评测集标注的主内容纯文本;并非完整 Markdown 标准答案。字符数只描述标注范围,不代表网页正文完整性。" if lang=="zh" else "The source benchmark provides human-selected main-content plain text, not a complete Markdown answer. Character count describes annotation scope, not page completeness."}

{case_count} / {case_count} {"页" if lang=="zh" else "pages"}

{"".join(dataset_rows)}
ID{esc(x["type"])}{"原网页地址" if lang=="zh" else "Original URL"}{"参考字符数" if lang=="zh" else "Reference characters"}{"冻结输入" if lang=="zh" else "Frozen input"}{"人工参考" if lang=="zh" else "Human reference"}{"评测结果" if lang=="zh" else "Results"}
' page(lang,'datasets/index.html',x['dataset'],dataset) review_intro=(f'认证覆盖 {result_count:,}/{result_count:,} 条逻辑结果;源文件在调用前后哈希相同。典型异常审查核对 {reviewed_cases} 页、{reviewed_results} 条输出,其余 {unreviewed_results:,} 条未逐项人工审查。' if lang=='zh' else f'Certification covers {result_count:,}/{result_count:,} logical results; source bytes matched before and after all calls. Targeted anomaly review inspected {reviewed_cases} pages and {reviewed_results} outputs; the other {unreviewed_results:,} results were not individually reviewed.') - review_zh=f"当前评测范围按冻结 HTML 和人工参考的有效性确定,不依据浏览器输出或得分。六种配置在同一批 {case_count} 页上比较。" - review_en=f"The current evaluation population was qualified from frozen HTML and human references before reading browser outputs or scores. All six configurations are compared on the same {case_count} pages." + review_zh=f"当前评测范围按冻结 HTML 和人工参考的有效性确定,不依据浏览器输出或得分。全部配置在同一批 {case_count} 页上比较。" + review_en=f"The current evaluation population was qualified from frozen HTML and human references before reading browser outputs or scores. All participating configurations are compared on the same {case_count} pages." review=f'

{esc(x["review"])}

{esc(review_intro)}

{"源数据资格检查" if lang=="zh" else "Source qualification"}

{review_zh if lang=="zh" else review_en}

{"执行证据" if lang=="zh" else "Execution evidence"}

SHA-256: {summary["source_sha256"]}

{"完整认证记录" if lang=="zh" else "Certification receipt"} · {"评分汇总 JSON" if lang=="zh" else "Score summary JSON"} · {"逐页证据哈希" if lang=="zh" else "Per-page evidence hashes"} · {"典型审查记录" if lang=="zh" else "Representative review record"}

' page(lang,'review/index.html',x['review'],review) - route_descriptions={'kitesurf':('Cloudflare Browser Run /markdown · remote HTML input','Cloudflare Browser Run /markdown · remote HTML input'),'lightpanda':('Lightpanda CLI · 原生 Markdown','Lightpanda CLI · native Markdown'),'obscura':('Obscura CLI · 原生 Markdown','Obscura CLI · native Markdown'),'moli':('Moli CLI 1.1.8 正式版 · 原生 Markdown','Moli CLI 1.1.8 release · native Markdown'),'moli-pr643':('Moli CLI PR #643 构建版 · 原生 Markdown','Moli CLI PR #643 build · native Markdown'),'chromium':('Chrome for Testing · 完整 DOM → node-html-markdown 2.0.0','Chrome for Testing · full DOM → node-html-markdown 2.0.0')} - versions=''.join(f'{esc(browser_label(p))}{esc(profile["versions"][p] if p=="moli-pr643" else identities[p].get("reported_version") or identities[p].get("endpoint") or "service version undisclosed")}{esc(route_descriptions[p][0 if lang=="zh" else 1])}{esc(identities[p].get("binary_sha256") or "hosted")}' for p in providers) - interface_example='Kitesurf: POST Browser Run /markdown {html: }\nLightpanda / Obscura / Moli: CLI fetch → Markdown\nChromium: Playwright page content → node-html-markdown 2.0.0' - adoption=f'

{esc(x["adoption"])}

{"六种实际执行配置、版本、输入路线和输出契约。" if lang=="zh" else "Six measured configurations, versions, input routes and output contracts."}

{"本轮实现" if lang=="zh" else "Measured implementations"}

{versions}
{esc(x["provider"])}{esc(x["version"])}{"路线" if lang=="zh" else "Route"}SHA-256

{"接口示例" if lang=="zh" else "Interface examples"}

{esc(interface_example)}

{"此表列的是实测接口,不把公开仓库搜索命中数当作产品采用量。" if lang=="zh" else "These are measured interfaces; repository search hits are not treated as product adoption counts."}

' - adoption+='

'+('公开代码采用线索' if lang=='zh' else 'Public-code adoption signals')+'

'+((f'{adoption_data["generated_at"][:10]} 的签名核验样本中,Cloudflare /markdown 接口在 {cloudflare_repo_count} 个仓库、{cloudflare_file_count} 个文件出现。该签名没有区分 Kitesurf 后端,也不能推算实际用户数。本轮其余五种 CLI / 浏览器配置没有同口径签名统计,不作采用量排名。') if lang=='zh' else f'In the {adoption_data["generated_at"][:10]} verified signature sample, the Cloudflare /markdown interface appears in {cloudflare_repo_count} repositories and {cloudflare_file_count} files. This signature does not identify the Kitesurf backend or user count. The other five CLI/browser configurations have no matching signature inventory, so no adoption ranking is made.')+'

'+('核验样本与来源' if lang=='zh' else 'Verified sample and sources')+'

' + + versions=''.join(f'{esc(browser_label(p))}{esc(identities[p].get("reported_version") or identities[p].get("endpoint") or "service version undisclosed")}{esc(route_descriptions[p])}{esc(identities[p].get("binary_sha256") or "hosted")}' for p in providers) + interface_example='\n\n'.join(browser_label(p)+': '+interface_specs[p]['entry']+'\n'+json.dumps(interface_specs[p]['request'],ensure_ascii=False,indent=2) for p in providers) + adoption=f'

{esc(x["adoption"])}

{"各项实际执行配置、版本、输入路线和输出契约。" if lang=="zh" else "Measured configurations, versions, input routes and output contracts."}

{"本轮实现" if lang=="zh" else "Measured implementations"}

{versions}
{esc(x["provider"])}{esc(x["version"])}{"路线" if lang=="zh" else "Route"}SHA-256

{"接口示例" if lang=="zh" else "Interface examples"}

{esc(interface_example)}

{"这些是登记资料中的接口示例;本轮实际路线以保存的调用记录为准。" if lang=="zh" else "These are registered interface examples; measured routes follow the saved call records."}

' + adoption+='

'+('公开代码采用线索' if lang=='zh' else 'Public-code adoption signals')+'

'+((f'{adoption_data["generated_at"][:10]} 的签名核验样本中,Cloudflare /markdown 接口在 {cloudflare_repo_count} 个仓库、{cloudflare_file_count} 个文件出现。该签名没有区分 Kitesurf 后端,也不能推算实际用户数。本轮其余 CLI / 浏览器配置没有同口径签名统计,不作采用量排名。') if lang=='zh' else f'In the {adoption_data["generated_at"][:10]} verified signature sample, the Cloudflare /markdown interface appears in {cloudflare_repo_count} repositories and {cloudflare_file_count} files. This signature does not identify the Kitesurf backend or user count. The other CLI/browser configurations have no matching signature inventory, so no adoption ranking is made.')+'

'+('核验样本与来源' if lang=='zh' else 'Verified sample and sources')+'

' page(lang,'adoption/index.html',x['adoption'],adoption) for p in providers: s=summary['providers'][p] - profile_body=f'

{esc(p)}

{esc(route_descriptions[p][0 if lang=="zh" else 1])}

{"本轮结果" if lang=="zh" else "Measured result"}

{"有效参考 F1" if lang=="zh" else "Qualified F1"}: {fmt(s["quality_f1"])} ({quality_count}) · {"非空输出" if lang=="zh" else "Nonempty outputs"}: {s["delivered"]}/{case_count}

{"完整调用端到端耗时均值 / P50 / P95" if lang=="zh" else "All-call end-to-end mean / P50 / P95"}: {s["mean_latency_ms"]:,.0f} / {s["p50_latency_ms"]:,.0f} / {s["p95_latency_ms"]:,.0f} ms ({case_count})

{esc(x["version"])}: {esc(identities[p].get("reported_version") or identities[p].get("endpoint") or "service version undisclosed")}

{esc(identities[p].get("binary_sha256") or "hosted")}

{esc(x["results"])} →

' + profile_body=f'

{esc(p)}

{esc(route_descriptions[p])}

{"本轮结果" if lang=="zh" else "Measured result"}

{"有效参考 F1" if lang=="zh" else "Qualified F1"}: {fmt(s["quality_f1"])} ({quality_count}) · {"非空输出" if lang=="zh" else "Nonempty outputs"}: {s["delivered"]}/{case_count}

{"完整调用端到端耗时均值 / P50 / P95" if lang=="zh" else "All-call end-to-end mean / P50 / P95"}: {s["mean_latency_ms"]:,.0f} / {s["p50_latency_ms"]:,.0f} / {s["p95_latency_ms"]:,.0f} ms ({case_count})

{esc(x["version"])}: {esc(identities[p].get("reported_version") or identities[p].get("endpoint") or "service version undisclosed")}

{esc(identities[p].get("binary_sha256") or "hosted")}

{esc(x["results"])} →

' page(lang,'providers/'+p+'.html',p,profile_body) - changes_zh=f"六种配置完成全部 {case_count} 页调用;质量展示只使用事前核定的 {quality_count} 页有效参考,共 {visible_count:,} 条结果。端到端耗时保留每款 {case_count} 次调用,以免事后剔除慢或失败的调用。" - changes_en=f"All six configurations completed {case_count} calls. Quality displays use only {quality_count} references qualified before calls, or {visible_count:,} results. End-to-end latency retains all {case_count} calls per method so slow or failed calls are not removed after measurement." + changes_zh=f"全部配置完成全部 {case_count} 页调用;质量展示只使用事前核定的 {quality_count} 页有效参考,共 {visible_count:,} 条结果。端到端耗时保留每款 {case_count} 次调用,以免事后剔除慢或失败的调用。" + changes_en=f"All participating configurations completed {case_count} calls. Quality displays use only {quality_count} references qualified before calls, or {visible_count:,} results. End-to-end latency retains all {case_count} calls per method so slow or failed calls are not removed after measurement." changes=f'

{esc(x["changes"])}

{"当前报告的结果、分母和证据范围。" if lang=="zh" else "Current result, denominator and evidence scope."}

{"有效参考与完整调用" if lang=="zh" else "Qualified references and all calls"}

{changes_zh if lang=="zh" else changes_en}

{"结构化变更记录" if lang=="zh" else "Structured change record"}

' page(lang,'changes/index.html',x['changes'],changes) - account_rows=''.join(f'{esc(p)}{esc("、".join(HEADLESS_ACCESS[p]["env_vars"]))}{esc(HEADLESS_ACCESS[p]["account"] if lang=="zh" else ("Cloudflare Browser Run account and API token required." if p=="kitesurf" else "No provider account or API key required."))}'+''.join(f'{esc(("Moli 1.1.8 正式发布" if p=="moli" else label) if lang=="zh" else ("Moli 1.1.8 official release" if p=="moli" else "Official source"))} ' for label,url in HEADLESS_ACCESS[p]['links'])+'' for p in providers) - accounts=f'

{esc(x["accounts"])}

{"只有 Kitesurf 需要 Cloudflare 账号凭据;本地配置值不进入报告。" if lang=="zh" else "Only Kitesurf needs Cloudflare account credentials; local values are not published."}

{account_rows}
{esc(x["provider"])}{"环境变量" if lang=="zh" else "Environment variables"}{"账号要求" if lang=="zh" else "Account requirement"}{"官方入口" if lang=="zh" else "Official source"}
' + account_rows=[] + for p in providers: + access=method_access(p,access_records,language=lang) + links=' '.join(f'{esc(label)}' for label,url in access['links']) + account_rows.append(f'{esc(browser_label(p))}{esc("、".join(access["env_vars"]))}{esc(access["account"])}{links}{esc(access["configuration"])}') + accounts=f'

{esc(x["accounts"])}

{"账号要求来自方法登记资料;安装配置缺失时明确标注,本机路径和凭据值不进入报告。" if lang=="zh" else "Account requirements come from the method registry. Missing installation bindings are disclosed; local paths and credential values are not published."}

{"".join(account_rows)}
{esc(x["provider"])}{"环境变量" if lang=="zh" else "Environment variables"}{"账号要求" if lang=="zh" else "Account requirement"}{"官方入口" if lang=="zh" else "Official source"}{"安装配置" if lang=="zh" else "Installation configuration"}
' page(lang,'accounts/index.html',x['accounts'],accounts) repro=f'

{esc(x["repro"])}

{"版本和来源由本轮认证记录固定。" if lang=="zh" else "Versions and source are bound by this cohort certificate."}

WCXB v1.0 test · {case_count} × {len(providers)} · SHA-256 {summary["source_sha256"]}

{esc(x["source"])}: GitHub {source_commit[:7]} · {"完整 test 清单" if lang=="zh" else "Full test manifest"} · certification.json · score summary JSON

{versions}
{esc(x["provider"])}{esc(x["version"])}{"路线" if lang=="zh" else "Route"}SHA-256
uv run --frozen python scripts/run_certified_benchmark.py wcxb --dataset <WCXB source> --sample-manifest evaluation/public-wcxb-test-qualified.json --output-root <new evidence dir> --env-file .env\nuv run --frozen python scripts/score_wcxb_results.py --source <WCXB source> --sample-manifest evaluation/public-wcxb-test-qualified.json --evidence-root <certified evidence dir>\nuv run --frozen python scripts/build_report.py --evidence <certified evidence dir> --wcxb-source <WCXB source> --output reports/webfetch

{esc(x["no_review"])}

' page(lang,'reproduce/index.html',x['repro'],repro) @@ -295,7 +305,7 @@ def score_digits(values, direction='max'): retry_html=('
'+('前次失败调用' if lang=='zh' else 'Earlier failed calls')+'
') if retries else '' timing_note=('

'+('耗时取同页交叉实验最终轮;正文与评分来自原始调用。' if lang=='zh' else 'Latency is from the selected final crossover round; content and scores are from the original call.')+' '+('逐次测速' if lang=='zh' else 'Timing calls')+'

') if r.get('latency_source')=='paired_final_round' else '' panels.append(f'

{esc(p)} · F1 {fmt(r["f1"])}

{esc(diag(lang,"excluded" if case in exclusion_ids else r["diagnosis"]))} · {r["total_elapsed_ms"]:,.0f} ms · {r["physical_attempt_count"]} calls · {esc(r["attempt_id"])}

'+timing_note+f'

{esc(x["missing"])}: {esc(missing)}

{esc(x["extra"])}: {esc(extra)}

'+snippet_detail+(f'

{esc(x["error"])}: {esc(r["error_type"])}{(" · HTTP "+str(r["status_code"])) if r.get("status_code") else ""}{(" · "+esc(r["error_message"])) if r["error_message"] else ""}

' if r['error_type'] else '')+retry_html+f'
{esc(x["output"])} · {r["output_chars"]:,} characters

{"完整证据 JSON" if lang=="zh" else "Complete evidence JSON"} ↓

') - qualification=(f'

{"此页参考不合格,保留官方原始分与输出,不计入 504 页主质量均值。" if lang=="zh" else "This reference was excluded before calls; official raw scores and outputs remain visible, but it does not enter the 504-page main quality mean."} {esc(exclusion_ids[case])}

' if case in exclusion_ids else '') + qualification=(f'

{"此页参考不合格,保留官方原始分与输出,不计入主质量均值。" if lang=="zh" else "This reference was excluded before calls; official raw scores and outputs remain visible, but it does not enter the main quality mean."} {esc(exclusion_ids[case])}

' if case in exclusion_ids else '') preview=_wmb_html_preview(source_html,lang=='en').replace('class="wmb-html-preview"','class="frozen-preview"') raw_input=evidence_path preview_note=('这里只按冻结 HTML 的文档顺序展示文字和结构,不是原网页截图。评测集未保存站点样式和图片文件,因此无法从这份输入还原原站排版;脚本、外部资源与跳转均已禁用。' if lang=='zh' else 'This reading view follows the frozen HTML document order; it is not a screenshot of the original website. The dataset does not preserve site stylesheets or image files, so the original layout cannot be reconstructed from this input. Scripts, external resources and navigation are disabled.') @@ -305,8 +315,7 @@ def score_digits(values, direction='max'): from harness.report_languages import write_language_contract write_language_contract(out, include_english=include_english) def provider_label(name): - from .methods import measured_method_label - return measured_method_label(name,PROVIDER_LABELS.get(name,browser_method_label(name))) + return browser_method_label(name) def case_track_label(case): family=report_family(case) if family in {'web_media','media_file'}:return REPORT_FAMILIES[family] @@ -338,13 +347,18 @@ def provider_configuration(name,records,info): def provider_page_body(name,records,cases,headers,tracks,*,native_default=False): """Render one method page from generic result records and method metadata.""" model=method_page_model(name,provider_label(name),records) + english_model=method_page_model(name,browser_method_label(name,language='en'),records,language='en') + def field(key):return model_text(model[key],english_model[key]) controls=filter_controls([('track','类型',tracks),('result','结果',[('pass','任务通过'),('fail','任务失败')])]+failure_filters(records,cases)) - configuration=f'

{esc(model["endpoint"])}

' + configuration=f'

{field("endpoint")}

' if native_default: - configuration+='

本轮统一使用固定原生配置:每题调用一次,外层截止时间 45 秒,产品内部超时保留默认值。具体选项及请求字段见接口示例、冻结配置和该次调用记录。

' + configuration+='

'+model_text( + '调用次数、重试和预算以运行记录为准;内部参数以保存的请求为准。', + 'Call counts, retries and deadlines come from run records; internal parameters come from saved requests.')+'

' + configuration+='

'+link('../methodology/index.html','查看运行参数','View execution parameters')+' · '+link('../reproduce/index.html','查看冻结配置与证据','View frozen configuration and evidence')+'

' else: - configuration+=f'

{esc(model["freshness"])}

{esc(model["configuration"])}

' - body=hero(esc(model['title']),esc(model['contract']))+section('本轮配置',configuration) + configuration+=f'

{field("freshness")}

{field("configuration")}

' + body=hero(field('title'),field('contract'))+section('本轮配置',configuration) body+=section('方法资料','

'+link('../adoption/index.html#'+model['pricing_anchor'],'价格与计费 →')+link('../adoption/index.html#interface-'+name,'接口与示例 →')+'

') return body+section(f'{len(records)} 条执行记录',controls+table(headers,[attempt_row(r,cases[r['case_id']]) for r in records],'filtered-table')) JS=r'''function readTheme(){try{return localStorage.getItem('webfetch-theme')}catch{return 'light'}} @@ -1714,7 +1728,7 @@ def avg(key): if r['metrics'].get(key) is not None:per_case[r['case_id']].append(r['metrics'][key] if r['ok'] else 0) return statistics.mean(statistics.mean(values) for values in per_case.values()) if per_case else None times=[r['elapsed_ms'] for r in rs if succeeded(r,cases[r['case_id']])] - values=cell(link(base+f'providers/{name}.html',provider_label(name)))+cell(rate(ok,len(rs)),ok/len(rs)) + values=cell(link(base+f'providers/{name}.html',provider_label(name),browser_method_label(name,language='en')))+cell(rate(ok,len(rs)),ok/len(rs)) if text_track: values+=cell(rate(passed,n),passed/n if n else -1) values+=''.join(cell(pct(avg(k)),avg(k) if full else -1) for k in ['word_recall','word_precision','word_f1']) @@ -1802,7 +1816,7 @@ def overview_all_call_latency(records,cases): rows=[] for name in names: times=[r['elapsed_ms'] for r in records if r['provider']==name] - rows.append(('',cell(link('providers/'+name+'.html',provider_label(name)))+cell(str(len(times)))+ + rows.append(('',cell(link('providers/'+name+'.html',provider_label(name),browser_method_label(name,language='en')))+cell(str(len(times)))+ cell(ms(statistics.mean(times)))+cell(ms(percentile(times,.5)))+cell(ms(percentile(times,.95))))) return ('

'+model_text(f'每种方法均使用同一组 {len(cases)} 个任务,包含通过、未通过、错误和超时。耗时是单次调用从输入到返回的时间,不包含调用之间的统一冷却间隔。每种方法内部按案例依次执行;方法之间的并发安排和限制见调用链检查。耗时仍受本地与远程环境、网络和产品内部限制影响。',f'Each method uses the same {len(cases)} tasks, including passes, failures, errors and timeouts. Latency covers one call from input to output, excluding cooldown intervals between calls. Each method executes cases sequentially; method concurrency and limits are documented in the call-chain audit. Local/remote execution, network conditions and native product limits still affect latency.')+'

'+ table(['方法','调用数','全部调用均值','全部调用 P50','全部调用 P95'],rows,'all-call-latency-table',metrics=[])) @@ -1830,7 +1844,7 @@ def overview_matched_latency(records,cases): metrics=['mean_ms','p50_ms','p95_ms'];rows=[] minima={key:min(row[key] for row in population['rows']) for key in metrics} for row in population['rows']: - values=cell(link('providers/'+row['provider']+'.html',provider_label(row['provider']))) + values=cell(link('providers/'+row['provider']+'.html',provider_label(row['provider']),browser_method_label(row['provider'],language='en'))) for key in metrics: value=ms(row[key]);values+=cell(''+value+'' if row[key]==minima[key] else value,row[key]) rows.append(('',values)) @@ -1841,7 +1855,7 @@ def overview_matched_latency(records,cases): for row in population['rows']: observed=row['recorded_start_dates'] if observed: - dates.append(esc(provider_label(row['provider']))+':'+esc('、'.join(f'{date}({number} 题)' for date,number in observed.items()))) + dates.append(model_text(provider_label(row['provider']),browser_method_label(row['provider'],language="en"))+model_text(':'+'、'.join(f'{date}({number} 题)' for date,number in observed.items()),': '+', '.join(f'{date} ({number} tasks)' for date,number in observed.items()))) links='、'.join(link('datasets/'+cid+'.html',cases[cid].get('name',cid)) for cid in population['common_pass_cases']) content=(note+table(['方法','同题均值','同题 P50','同题 P95'],rows,'overall-matched-latency-table',metrics=[]) +'
共同任务与调用日期

'+links+'

'+'
'.join(dates)+'

') @@ -1892,7 +1906,7 @@ def overall_ranking(records,cases,audit=None,*,diagnostic=False,call_budget_seco for name,rs in groups.items(): n=sum(measured(r,cases[r['case_id']]) for r in rs) passed=sum(succeeded(r,cases[r['case_id']]) for r in rs) - values=cell(link(f'providers/{name}.html',provider_label(name)))+cell(rate(sum(bool(r['ok']) for r in rs),len(rs)),sum(bool(r['ok']) for r in rs)/len(rs))+cell(rate(passed,n)+(f'
{passed}/{n}' if has_unknown else ''),passed/n if n else -1) + values=cell(link(f'providers/{name}.html',provider_label(name),browser_method_label(name,language='en')))+cell(rate(sum(bool(r['ok']) for r in rs),len(rs)),sum(bool(r['ok']) for r in rs)/len(rs))+cell(rate(passed,n)+(f'
{passed}/{n}' if has_unknown else ''),passed/n if n else -1) if has_unknown:values+=cell(str(len(rs)-n)) for track,_ in active_tracks(cases): subset=[r for r in rs if report_family(cases[r['case_id']])==track and measured(r,cases[r['case_id']])] @@ -1938,7 +1952,7 @@ def filter_controls(options,all_first=()): controls='' for key,label,choices in options: ordered=([('','全部')]+choices) if key in {'provider','track','category','failureStage','failureCategory',*all_first} else (choices+[('','全部')]) - controls+=f'' + controls+=f'' return '
'+controls+'
' def failure_filters(records,cases): @@ -1951,7 +1965,7 @@ def comparison_rows(cases,bycase,names): attempts={r['provider']:r for r in bycase.get(case_id,[])} passed=[name for name in names if name in attempts and measured(attempts[name],c) and succeeded(attempts[name],c)] failed=[name for name in names if name in attempts and measured(attempts[name],c) and not succeeded(attempts[name],c)] - method_links=lambda selected,state:(''+''.join(f'{esc(provider_label(name))}' for name in selected)+'') if selected else '—' + method_links=lambda selected,state:(''+''.join(f'{model_text(provider_label(name),browser_method_label(name,language="en"))}' for name in selected)+'') if selected else '—' attrs=(dataset_attrs(c)+f' data-pass="{esc(" ".join(passed))}" data-fail="{esc(" ".join(failed))}"') task=(f'
{link("../datasets/"+case_id+".html",c["name"])}
{esc(case_id)}' f'{esc(case_track_label(c))}
') @@ -1967,10 +1981,10 @@ def method_comparisons(cases,bycase,names): ordered=sorted(names,key=lambda name:(-sum(succeeded(attempt,cases[attempt['case_id']]) for attempt in all_attempts if attempt['provider']==name),provider_label(name))) if len(ordered)<2:return '

至少需要两种方法才能比较任务交集。

' a=ordered[0];b=ordered[1] - options=lambda selected:''.join(f'' for name in ordered) + options=lambda selected:''.join(f'' for name in ordered) tracks=''+''.join(f'' for key,label in REPORT_TRACKS.items()) rows=comparison_rows(cases,bycase,names) - checks=''.join(f'' for name in ordered) + checks=''.join(f'' for name in ordered) return (f'

同题结果组合

以方法 A 为基准,可选择一个或多个方法共同比较;方法名可直达该次完整证据。

' '
' f'' @@ -2002,7 +2016,7 @@ def local_call_chain_section(dataset, prefix="../"): rows=[] for item in audit.get('methods',[]): note=item['native_log_note'] - rows.append(('',cell(link(prefix+'providers/'+item['provider']+'.html',provider_label(item['provider'])))+cell(esc(note)))) + rows.append(('',cell(link(prefix+'providers/'+item['provider']+'.html',provider_label(item['provider']),browser_method_label(item['provider'],language='en')))+cell(esc(note)))) intro='

每次调用都保存了测试程序的调用记录:何时开始、传入什么参数、何时返回,以及是否发生错误。各案例详情中的“调用方事件”展示这些记录。

“产品与依赖库日志”展示产品或底层库实际输出的运行信息。云端服务没有开放的内部日志,不会用测试程序的记录代替。下表说明各方法留下了哪些日志。

' return section('本轮保存了哪些日志',intro+table(['方法','产品与依赖库日志范围'],rows,metrics=[])+'

完整日志在各案例详情中查看。日志可以帮助定位问题,但没有记录的内部过程不能据此推断;日志文件存在,也不等于产品的全部内部过程都有记录。

','call-chain-audit') @@ -2019,7 +2033,7 @@ def local_audit_banner(dataset): return '

'+model_text('按本次所列任务的通过率排序;未完成仓库认证流程。测试日期和核验范围见详情。','Ordered by pass rate on the listed tasks; repository certification is incomplete. See details for test dates and verification scope.')+'

' note='已有复核记录;覆盖和裁定依据见核验页面。' if dataset['comparison'].get('independent_review') else '结果为机器判定,未进行独立人工审核。' return '' - return '
本地审核版

本页合并两个不同日期的 Moli 调用批次,保留每例的真实调用时间。本次仅更换报告展示代码,没有新增 Moli 调用,也不代表正式发布。

' + return '
'+model_text('本地审核版','Local audit report')+'

'+model_text('本报告未正式发布。工具版本、调用时间与核验范围以保存记录为准。','This report is not a formal release. Consult the saved records for tool versions, call times and verification scope.')+'

' from .methods import with_measured_method_labels @@ -2208,7 +2222,7 @@ def generate(out:Path,dataset:dict,records:list,manifests:list,*,include_english for r in detail_records: c=cases[r['case_id']];m=r['metrics'];ref=reference_text(c) output=contained_path(out,r['artifact_path']).read_text(encoding='utf-8',errors='replace') - rows=[('',cell(k)+cell(v)) for k,v in [('任务 ID',f'{esc(c["id"])}'),('方法',link('../providers/'+r['provider']+'.html',provider_label(r['provider']))),('请求','成功' if r['ok'] else '失败'),('任务结果',result_label(r,c)),('HTTP 状态',esc(r.get('status_code'))),('耗时',ms(r['elapsed_ms'])),('全文完整度 / 纯净度 / F1',f'{pct(m.get("word_recall"))} / {pct(m.get("word_precision"))} / {pct(m.get("word_f1"))}'),('输出字符',f'{len(output):,}'),('输出校验值',f'{esc(m.get("content_sha256"))}')]] + rows=[('',cell(k)+cell(v)) for k,v in [('任务 ID',f'{esc(c["id"])}'),('方法',link('../providers/'+r['provider']+'.html',provider_label(r['provider']),browser_method_label(r['provider'],language='en'))),('请求','成功' if r['ok'] else '失败'),('任务结果',result_label(r,c)),('HTTP 状态',esc(r.get('status_code'))),('耗时',ms(r['elapsed_ms'])),('全文完整度 / 纯净度 / F1',f'{pct(m.get("word_recall"))} / {pct(m.get("word_precision"))} / {pct(m.get("word_f1"))}'),('输出字符',f'{len(output):,}'),('输出校验值',f'{esc(m.get("content_sha256"))}')]] if not c.get('ground_truth_main_content'): rows[6]=('',cell('全文完整度 / 纯净度 / F1')+cell('— / — / —(全文指标无完整参考,未评测)')) else: @@ -2283,36 +2297,30 @@ def generate(out:Path,dataset:dict,records:list,manifests:list,*,include_english detail_metrics=['请求','结果','耗时','HTTP 状态','输出字符','输出校验值']+(['具名详情入口'] if c.get('task_kind')=='structure' else ['完整度','纯净度','F1'] if c.get('ground_truth_main_content') else []) state=result_label(r,c);passed=succeeded(r,c) provider_meta=r.get('provider_metadata',{}) + route_english=None if provider_meta.get('document_extractor'): route_label=f'处理路线:方法直接取得原始文档 → {provider_meta["document_extractor"]} → 可读正文' elif c.get('task_kind')=='file': route_label='处理路线:方法直接取得原始文件 → 格式、长度与 SHA-256 验收' - elif provider_meta.get('output_contract')=='readable_document_text': - route_label='处理路线:浏览器原生接口获取文档 → 共同 PDF / DOCX 正文解析;实际完成步骤见运行证据' - elif provider_meta.get('output_contract')=='native_markdown_resource_urls': - route_label='处理路线:Moli 原生 Markdown → 共同评分器只核对其中的目标图片或视频地址' - elif provider_meta.get('output_contract')=='native_html_resource_urls': - route_label='处理路线:Moli 原生 HTML → 共同评分器只识别其中的图片或视频资源地址' - elif provider_meta.get('output_contract')=='native_json_resource_urls': - route_label='处理路线:Moli 原生 JSON 页面与网络记录 → 共同评分器只识别其中的视频资源地址' - elif provider_meta.get('output_contract')=='structured_resource_urls': - route_label='处理路线:浏览器原生 DOM 接口 → 共同资源读取脚本 → 图片或视频资源地址' - elif provider_meta.get('output_contract')=='readable_media_metadata': - route_label='处理路线:浏览器原生页面接口 → 共同脚本读取正文、作者和媒体元数据' + elif provider_meta.get('output_contract') in report_method_catalog()['output_contract_labels']: + contract=provider_meta['output_contract'] + route_label=report_method_catalog()['output_contract_labels'][contract].format(method=provider_label(r['provider'])) + route_english=report_method_catalog('en')['output_contract_labels'][contract].format(method=browser_method_label(r['provider'],language='en')) else: extractor=str(provider_meta.get('extractor') or '') route_label='处理路线:'+ROUTE_LABELS.get(extractor,extractor or '参评方法直接返回正文') + route_english='Retrieval path: '+report_method_catalog('en')['route_labels'].get(extractor,extractor or 'Text returned directly by the method') delivered_summary=provider_meta.get('file_evaluation',{}).get('files',[]) actual_summary=(f'实际交付:{len(delivered_summary)} 个文件、{sum(x.get("bytes",0) for x in delivered_summary):,} 字节' if c.get('task_kind')=='file' else f'实际输出:{len(output):,} 字符') hero_meta=(f'任务 ID:{esc(c["id"])}
{esc(task_instruction(c))}' - f'方法:{esc(provider_label(r["provider"]))}' + f'方法:{model_text(provider_label(r["provider"]),browser_method_label(r["provider"],language="en"))}' f'运行 ID:{esc(r.get("run_id") or execution_id(r))}') summary=(f'
' f'{esc(state)}

{esc(reason(r,c) if passed or not measured(r,c) else (r.get("diagnosis") or {}).get("category") or "任务规则未满足")}

' - f'

方法:{link("../providers/"+r["provider"]+".html",provider_label(r["provider"]))} · ' + f'

方法:{link("../providers/"+r["provider"]+".html",provider_label(r["provider"]),browser_method_label(r["provider"],language="en"))} · ' f'请求:{"成功" if r["ok"] else "失败"} · 耗时:{ms(r["elapsed_ms"])} · {actual_summary}

') - summary+=f'

{esc(route_label)}

' + summary+=f'

{model_text(route_label,route_english)}

' summary+=('

查看判定使用的标准答案' '查看本次完整实际返回' '查看逐项判定' @@ -2477,7 +2485,7 @@ def generate(out:Path,dataset:dict,records:list,manifests:list,*,include_english write(out,'attempts/'+r['attempt_id']+'.html',c['name']+' · '+r['provider'],body) controls=filter_controls([('provider','方法',[(n,provider_label(n)) for n in names]),('track','类型',tracks),('result','结果',[('pass','任务通过'),('fail','任务失败')])]+failure_filters(records,cases)) comparison=section('方法组合对比','

按同一任务逐项比较,不用各方法不同的成功样本拼接结论。成功与失败均沿用主榜的程序判定;点击方法可查看标准答案、实际返回和原因。

'+method_comparisons(cases,bycase,names),'method-comparison') - write(out,'analysis/index.html','结果分析',hero('结果分析','先比较方法在同一任务上的成败组合,再筛选全部执行记录。')+comparison+section('全部执行记录',controls+table(['方法']+headers,[(case_attrs(r,cases[r['case_id']]),cell(link('../providers/'+r['provider']+'.html',provider_label(r['provider'])))+attempt_row(r,cases[r['case_id']])[1]) for r in records],'filtered-table',cls='all-method-results'))) + write(out,'analysis/index.html','结果分析',hero('结果分析','先比较方法在同一任务上的成败组合,再筛选全部执行记录。')+comparison+section('全部执行记录',controls+table(['方法']+headers,[(case_attrs(r,cases[r['case_id']]),cell(link('../providers/'+r['provider']+'.html',provider_label(r['provider']),browser_method_label(r['provider'],language='en')))+attempt_row(r,cases[r['case_id']])[1]) for r in records],'filtered-table',cls='all-method-results'))) write_support(out,records,manifests,authored_interpretation=authored_interpretation) from .report_changes import render_changes if records and all(row.get('provider_type') == 'fixture' for row in records): @@ -2663,6 +2671,66 @@ def load_frozen_method_options(out,records): return document +def render_account_requirements(records): + names=sorted({row['provider'] for row in records},key=lambda name:(provider_label(name).lower(),name)) + accesses={name:method_access(name,records) for name in names} + english_accesses={name:method_access(name,records,language='en') for name in names} + hosted=[name for name in names if accesses[name].get('type')=='hosted'] + intro=model_text('账号与安装要求来自本轮方法登记资料;未登记的要求明确标注。', + 'Account and installation requirements come from the participating method registrations; missing requirements are marked.') + if hosted: + intro+=' '+model_text('需要供应商账号的方法:','Methods requiring a provider account: ')+model_text('、'.join(provider_label(name) for name in hosted),', '.join(browser_method_label(name,language='en') for name in hosted)) + rows=[] + for name,access in accesses.items(): + english=english_accesses[name] + official='
'.join(link(url,label,english['links'][index][0]) + for index,(label,url) in enumerate(access['links'])) + variables='
'.join(''+esc(value)+'' for value in access['env_vars']) + rows.append(('',cell(model_text(provider_label(name),browser_method_label(name,language='en')))+cell(model_text(access['account'],english['account']))+cell(official)+cell(variables)+cell(model_text(access['configuration'],english['configuration'])))) + body=hero('账号与安装',intro) + body+=section('本轮方法清单','

这里列的是完成本报告所需的全部外部准备:账号、官方获取入口和变量名称。变量值、本机路径、登录会话、Cookie 和 Token 均不进入报告。

'+table(['方法','账号要求','官方获取入口','变量名称','本轮配置'],rows,cls='accounts-table',metrics=[])) + body+=section('公开内容边界','

报告只公开复现所需的名称和入口,不公开凭据值。 发布门禁会扫描 HTML、JSON、下载文件和逐次输出中的常见 Token、私钥、Authorization 值与用户目录路径;第三方原始 HTML 中命中密钥形态的片段单独脱敏,计分输出保持逐字节不变。扫描结果和脱敏文件哈希随发布包保存。

') + return body + + +def render_execution_contract(dataset,records,manifests): + """Render recorded budgets and scheduling without inferring missing values.""" + def seconds(value): + if not nonnegative_number(value): + return model_text('未记录','Not recorded') + return model_text(f'{value:g} 秒',f'{value:g} seconds') + def count(value): + return str(value) if isinstance(value,int) and not isinstance(value,bool) and value>=0 else model_text('未记录','Not recorded') + declared=dataset.get('comparison',{}).get('timeout_seconds') + intro='' + if declared is not None: + intro='

'+model_text('事前声明的外层预算:','Predeclared outer budget: ')+seconds(declared)+'

' + names={row['provider'] for row in records} + rows=[] + represented=set() + for index,manifest in enumerate(manifests,1): + parameters=manifest.get('parameters',{}) + for provider in manifest.get('providers',[]): + name=provider.get('name') + if name not in names:continue + represented.add(name) + values=[model_text(provider_label(name),browser_method_label(name,language="en")),str(index),seconds(parameters.get('timeout')), + seconds(provider.get('min_interval_seconds')),seconds(parameters.get('delay')), + count(parameters.get('runs')),count(parameters.get('max_failure_retries'))] + rows.append(('', ''.join(cell(value) for value in values))) + for name in sorted(names-represented): + rows.append(('',cell(model_text(provider_label(name),browser_method_label(name,language="en")))+cell('—')+ + ''.join(cell(model_text('未记录','Not recorded')) for _ in range(5)))) + body=intro+table([model_text('方法','Method'),model_text('批次','Batch'), + model_text('外层超时','Outer timeout'),model_text('方法最小调用间隔','Method minimum interval'), + model_text('运行器调用间延迟','Runner delay between calls'),model_text('计划轮数','Planned runs'), + model_text('失败重试上限','Failure retry limit')],rows,metrics=[]) + body+='

'+model_text( + '上表读取保存的执行批次,缺失值不补为当前默认值。方法最小间隔与运行器延迟是各自的配置,不是实测等待时间;方法之间的并发和实际调度以批次记录与调用时间为准。产品内部加载、连接和处理限制见冻结配置及真实请求,不从外层预算推断。', + 'The table reads saved execution batches; missing values are not replaced with current defaults. Method intervals and runner delays are separate settings, not measured waiting time. Inter-method concurrency and actual scheduling follow batch records and call timestamps. Native loading, connection and processing limits come from frozen configuration and actual requests, not from the outer budget.')+' '+link('../reproduce/index.html','环境与复现','Environment and reproduction')+'

' + return body + + def write_support(out,records,manifests,*,authored_interpretation=True): dataset=json.loads((out/'dataset.json').read_text()) if (out/'dataset.json').is_file() else {} local_matrix=measurement_presentation(dataset).local_matrix @@ -2675,7 +2743,24 @@ def write_support(out,records,manifests,*,authored_interpretation=True): methodology+=section('每类怎么测',examples_html)+section('指标说明',metric_guide()) methodology+=section('共同输入、计时边界与调用预算','

所有方法进入同一个评分入口,但按任务交付物验收,而不是强制使用同一种中间格式。网页与文档题验收可读正文;网页媒体题验收指定资源地址或登记文字;直接媒体文件题验收图片或视频字节;结构任务验收字段和稳定文字。HTML/Markdown 链接与明确配对的结构化名称—链接字段按同一规则核验;不把请求回显或诊断日志当成答案。原始输出与原始适配器状态保留,报告层不按方法名称改写任务结果。

计入耗时

从准备发起该次 provider 调用开始,到形成可送入统一评分器的最终答案为止:线程调度、SDK/CLI/HTTP 调用、远端处理与网络下载、浏览器渲染与正文提取、本地解析、统一输入转换,以及文件字节读取、格式识别和 SHA-256 计算均在计时内。

不计入耗时

数据集与参考加载、provider 配置校验、调用间限速等待、最终答案形成后的证据落盘、统一评分计算、报告汇总与页面生成不计入。供应商上报的服务端耗时只作附加证据,排名使用客户端观测的端到端耗时。

这个边界比较的是“拿到本任务最终交付物”所需时间:服务直接返回成品时计完整往返;本地方法若还需下载、渲染或解析,也必须完成后才停表。文档正文必须由参评方法交付为可读文字;直接媒体文件任务到字节校验完成即停止,不做无关的 Markdown 转换。所有方法使用同一外层计时器和截止预算。超时答案不接收;无法取消的调用会被隔离,整批结果因此不完整并触发不可靠状态,构建产物不得发布。

完整正文任务除原有核对点外,按参考正文最多五个连续位置检查字符六元组覆盖:每个位置至少达到 60%,通常要求至少四处达到 80%。所有位置一次性做全局容量分配;同一返回片段实例只能计入一个位置,位置排列顺序不影响结论。若页面存在经独立证据确认的合法重排,案例可显式登记不同位置门槛。比较忽略空白和排版符号但保留文字顺序;全文 F1 独立保留实际值。直接媒体文件任务必须有字节验收,资源 URL 不等于文件。

') if (out/'resource-cohort/resource-comparison.json').is_file(): - methodology+=section('资源测量边界','

CPU、内存和网络使用独立的 20 题同窗队列,不回填历史正确率调用。每次调用在受限的 Linux 容器中运行,使用 cgroup v2 累计 CPU、独立网络命名空间累计收发字节,并以 50 ms 采样内存与瞬时速率。容器固定为 2 个逻辑 CPU、2 GiB 内存和 512 个 PID;宿主控制器与容器启停不计入。

资源主表纳入预先确定的全部任务,包括失败调用,统计容器内调用进程、浏览器与转换过程的消耗;同时列出各方法通过数。各方法成功调用另作条件统计,共同通过的任务仅作诊断。Kitesurf 的远端 CPU、内存和上游流量不可见,因此不进入资源表。缺少任一资源字段、非 Linux/cgroup v2、镜像或二进制身份变化、并发不为 1,都会阻断资源包构建。

CPU 与网络总量来自累计计数;内存峰值和速率分位数依赖 50 ms 采样,可能遗漏更短的尖峰。报告分别呈现 CPU、内存、网络和任务通过率,不合成总分。

') + resource_summary=json.loads((out/'resource-cohort/resource-comparison.json').read_text()) + resource_environment=resource_summary.get('environment',{}) + resource_parameters=[ + (('任务数','Task count'),len(resource_summary.get('planned_cases',[]))), + (('方法数','Method count'),len(resource_summary.get('providers',[]))), + (('CPU 核数','CPU cores'),resource_environment.get('cpus','—')), + (('内存限制','Memory limit'),resource_environment.get('memory','—')), + (('进程数限制','Process limit'),resource_environment.get('pids_limit','—')), + (('采样间隔(毫秒)','Sampling interval (ms)'),resource_environment.get('sample_interval_ms','—')), + ] + methodology+=section('资源测量边界','

'+model_text( + 'CPU、内存和网络来自独立的资源测量队列。主表统计预先确定的全部调用,包含失败;各方法成功调用另作条件统计,共同通过子集只作诊断。', + 'CPU, memory and network measurements come from a separate resource cohort. The primary table includes all predeclared calls, including failures; successful-call distributions are conditional and the common-pass subset is diagnostic only.')+'

' + +table([model_text('参数','Parameter'),model_text('值','Value')],[('',cell(model_text(*key))+cell(esc(value))) for key,value in resource_parameters],metrics=[]) + +'

'+model_text( + '累计计数用于 CPU 与网络总量;内存峰值和速率分位数受实际采样间隔限制,可能遗漏更短的尖峰。环境身份、统计范围与排除项见资源证据。', + 'Cumulative counters provide CPU and network totals. Memory peaks and rate percentiles depend on the recorded sampling interval and may miss shorter peaks. The resource evidence identifies the environment, scope and exclusions.')+' ' + +link('../resource-cohort/resource-comparison.json','资源测量证据')+'

') if (out/'scoring-revisions.json').exists(): revision_links=[link('../scoring-revisions.json','评分规则修订收据')] if (out/'reference-revisions.json').exists():revision_links.append(link('../reference-revisions.json','参考答案修订收据')) @@ -2703,16 +2788,13 @@ def write_support(out,records,manifests,*,authored_interpretation=True): methodology+=section('参考与匹配','

原始快照保留不变;评分参考清除零宽空格、BOM 和软连字符,并检查空正文、乱码、重复核对点及核对点是否存在于参考中。通过检查不等于语义完整性已获证明,正文范围仍以来源记录为准。

关键事实按规范化后的可见文本机械匹配:统一 Unicode、大小写及排版空白,菜单路径如 Settings > AI 与 Settings>AI 等价;忽略 Markdown 链接地址、行内代码与加粗标记。一个已被完整记录边界约束的字段允许丢失内联元素间空白,例如标签列表 change deep-thoughts 与 changedeep-thoughts 内容等价;相邻独立链接仍按独立词段核对,不猜测跨记录的粘连分词。保留数字、正负号、否定词、标点和英文词边界。不使用大模型或模糊相似度判定通过。

') methodology+=section('调用时间','

各方法使用同一套冻结任务与参考。不同批次的耗时不直接排名;同窗速度子集单独标识。实时网页内容、登录状态和站点策略可能变化,成绩只代表所记录的调用。环境、版本与命令见“环境与复现”。

') methodology+=section('变更后重跑范围','

不把“代码有更新”直接等同于全部重跑。只改展示时重建报告;只改裁判且原始输出足够时离线重算;某个参考变化时重跑该题的全部方法;新增或升级单个实现时重跑该方法的全部题;计时边界变化时废弃并重跑同窗测速。只有全局任务输入、调用边界或所有方法共同依赖发生变化,才需要全量重跑。

') - methodology+=section('计算与边界','''
全文评分公式

完整度=取回的参考词数 ÷ 参考词数;纯净度=匹配的正文词数 ÷ 返回词数;F1=2 × 完整度 × 纯净度 ÷ 两者之和。英文按词、中文按字,统一大小写与 Unicode 字符形式;Markdown 链接只计文字。对有完整参考的全部案例取平均;请求失败计 0,请求成功但任务未通过仍按实际正文评分,没有参考不计算。

参考范围与公平性

网页、文档使用独立保存的参考正文;仓库以 README 为准,列表只覆盖指定的已加载范围,不要求无限滚动或跨页翻页。登录页和验证码不能代替目标正文。参考正文自身包含的“404 Not Found”等术语不作为错误页判据。

不同方法并行,同一方法串行;正文结束后再测其他任务。Firecrawl 每次完成后冷却 6.1 秒。本报告不是 WCXB 官方榜单。

媒体网址只证明找到入口,不代表取得文件。文件任务没有另接下载器代替参评方法完成。

''') - if headless: - methods_count=len({row['provider'] for row in records}) - methodology=methodology.replace('Firecrawl 每次完成后冷却 6.1 秒。',f'本轮 {methods_count} 种配置分别使用独立进程或新上下文。具体接口、等待条件与输出处理见报告解读的任务接口表,实际版本和配置见执行批次。每次调用使用 45 秒端到端预算,调用次数和重试规则以事前声明的协议为准。命令、版本、参考、评分或计时错误按完整声明范围纠正,保留旧证据;不按分数选择重试结果。') - if dataset.get('comparison',{}).get('retrieval_mode') in { - 'native_default_completion', 'native_task_family_completion'}: - methodology=methodology.replace('正文任务中,方法只返回原始 PDF 或 DOCX 时,本地正文解析也计时;原始文件任务则到字节校验完成即停止,不做无关的 Markdown 转换。','本轮文档正文只验收浏览器原生接口交付的文字;取得文件字节后不另接外部解析器补正文。原始文件任务到字节校验完成即停止。') - if local_matrix: - methodology+=section('本轮固定原生配置','

每种方法内部串行执行,每题只调用一次,45秒为外层交卷期限。内部加载、连接和处理时间保留产品默认值。允许测试前固定的原生选项,不传入逐题等待、选择器、数量提示或脚本;不补抓、不额外重试。产品返回的正文和原始响应分别保存。

每种方法内部串行执行;方法之间的调度以执行批次和保存的调用时间为准。分段执行保留每个原始分段,完整调用时间见执行批次。

') - methodology=methodology.replace('方法之间并行、同一方法串行。','每种方法内部串行;方法之间的调度见执行批次。') + methodology+=section('计算与边界','''
全文评分公式

完整度=取回的参考词数 ÷ 参考词数;纯净度=匹配的正文词数 ÷ 返回词数;F1=2 × 完整度 × 纯净度 ÷ 两者之和。英文按词、中文按字,统一大小写与 Unicode 字符形式;Markdown 链接只计文字。对有完整参考的全部案例取平均;请求失败计 0,请求成功但任务未通过仍按实际正文评分,没有参考不计算。

参考范围与公平性

网页、文档使用独立保存的参考正文;仓库以 README 为准,列表只覆盖指定的已加载范围,不要求无限滚动或跨页翻页。登录页和验证码不能代替目标正文。参考正文自身包含的“404 Not Found”等术语不作为错误页判据。

媒体网址只证明找到入口,不代表取得文件。文件任务没有另接下载器代替参评方法完成。

''') + methodology+=section(model_text('运行参数与调用边界','Run parameters and call boundaries'), + render_execution_contract(dataset,records,manifests)) + if measurement_presentation(dataset).native_outputs: + methodology+='

'+model_text( + '正文验收使用原生接口交付的内容,不以额外解析器补齐回答。原始文件任务按其字节验收规则核对;请求、输出与错误保留在逐次调用证据中。', + 'Text acceptance uses content delivered by the native interface, without an additional parser completing the answer. Original-file tasks use their byte-acceptance rules; requests, outputs and errors remain in per-call evidence.')+'

' write(out,'methodology/index.html','评测方法',methodology) downloads=' · '.join(link('../'+path,label) for path,label in [('dataset.json','评测集与参考'),('results.jsonl','全部调用记录'),('summary.json','分类汇总'),('execution.json','执行批次')]+([('implementation-provenance.json','浏览器版本与二进制来源')] if (out/'implementation-provenance.json').is_file() else [])+([('reference-revisions.json','参考修订收据')] if (out/'reference-revisions.json').is_file() else [])+([('scoring-revisions.json','评分修订收据')] if (out/'scoring-revisions.json').is_file() else [])+([('latency-corrections.json','时延纠正收据')] if (out/'latency-corrections.json').is_file() else [])+([('request-status-corrections.json','请求状态纠正收据')] if (out/'request-status-corrections.json').is_file() else [])) reproduce='

在评测仓库中执行;读取 evaluation/ 的当前实测数据,不调用供应商接口。

make setup\nmake report EVIDENCE=/absolute/path/to/evidence\nmake report-check

输出目录:reports/webfetch/。固定的 Python 3.11 与 uv.lock 依赖不满足时会立即失败。凭据仅从本地环境加载,不包含在报告中。

' @@ -2758,9 +2840,9 @@ def write_support(out,records,manifests,*,authored_interpretation=True): version=';'.join(versions) or ('云端服务未公开后端版本' if not hashes and not any(identities) else '未取得可确认的版本信息') if versions and all(re.fullmatch(r'[0-9a-f]{40}', value) for value in versions): version='源码提交:'+version - provider_rows.append(('',cell(link('../providers/'+name+'.html',provider_label(name)))+cell(''+esc(version)+''))) + provider_rows.append(('',cell(link('../providers/'+name+'.html',provider_label(name),browser_method_label(name,language='en')))+cell(''+esc(version)+''))) else: - provider_rows.append(('',cell(link('../providers/'+name+'.html',provider_label(name)))+cell(esc(';'.join(versions) or '版本未记录'))+cell(esc(basis))+cell(esc(';'.join(deployments)))+cell('
'.join(''+esc(x)+'' for x in hashes) or '—')+cell(str(len(subset))))) + provider_rows.append(('',cell(link('../providers/'+name+'.html',provider_label(name),browser_method_label(name,language='en')))+cell(esc(';'.join(versions) or '版本未记录'))+cell(esc(basis))+cell(esc(';'.join(deployments)))+cell('
'.join(''+esc(x)+'' for x in hashes) or '—')+cell(str(len(subset))))) write_review_page(out,records) repo='https://github.com/lexmount/browser-eval' support=hero('环境与复现','代码、评测集和复现说明以 browser-eval 仓库为准;本页只列本轮必要配置。') @@ -2783,7 +2865,7 @@ def write_support(out,records,manifests,*,authored_interpretation=True): labels=NATIVE_OPTION_LABELS def option_text(value): return '开启' if value is True else '关闭' if value is False else '、'.join(map(str,value)) if isinstance(value,list) else str(value) - config_rows=[('',cell(link('../providers/'+name+'.html',provider_label(name)))+cell(''.join('
'+esc(labels.get(k,k))+':'+esc(option_text(v))+'
' for k,v in value.items()))) for name,value in options.items()] + config_rows=[('',cell(link('../providers/'+name+'.html',provider_label(name),browser_method_label(name,language='en')))+cell(''.join('
'+esc(labels.get(k,k))+':'+esc(option_text(v))+'
' for k,v in value.items()))) for name,value in options.items()] support+=section(model_text(options_document['scope_title'],options_document['scope_title_en']), '

'+model_text(options_document['scope_description'],options_document['scope_description_en'])+'

' +table(['方法','固定选项'],config_rows,metrics=[])) @@ -2809,40 +2891,7 @@ def option_text(value): reproduce+='

'+model_text('评分时的原件私下保留;公开回答和完整原生响应隐藏了目录或敏感值。清单保留评分原件的标识,并单独记录公开副本。公开文件可重建本报告,不能还原隐藏内容;正文质量等指标保留原件的计算值,不保证用脱敏文字逐字重算一致。复核片段的坐标记录原件位置;公开副本仅能核对仍可见的片段。','Scoring originals remain private. Public answers and complete native responses hide paths or sensitive values. The inventory preserves original scoring identities and separately binds public views. Public files rebuild this report but cannot reconstruct omitted content. Text-quality diagnostics retain the original calculations and may not reproduce exactly from redacted text. Review coordinates refer to originals; public views verify only the visible excerpts.')+' '+link('../'+public_profile['publication_evidence_views']['path'],'PUBLIC-EVIDENCE-VIEWS.json')+'

' support+=section('核心复现指令',reproduce) write(out,'reproduce/index.html','环境与复现',support) - if not authored_interpretation and not local_matrix: - rows=[] - for name in sorted({r['provider'] for r in records}): - selected=[r for r in records if r['provider']==name] - types=sorted({r.get('provider_type','未记录') for r in selected}) - engines=sorted({r.get('provider_metadata',{}).get('engine','未记录') for r in selected}) - rows.append(('',cell(esc(name))+cell(esc(';'.join(types)))+cell(esc(';'.join(engines))))) - body=hero('账号与安装','本报告的方法由调用方的显式配置提供。') - body+=section('本轮方法',table(['方法名称','适配器类型','记录的引擎'],rows,metrics=[])) - body+='

运行前准备配置对应的程序与账户。报告保留方法名称、版本和执行证据,不包含凭据值。实际参数与版本见 '+link('../reproduce/index.html','环境与复现')+'。

' - write(out,'accounts/index.html','账号与安装',body) - elif headless: - access_rows=[] - for name in sorted({r['provider'] for r in records},key=lambda value:(provider_label(value).lower(),value)): - access=method_access(name,records) - official='
'.join(link(url,label) for label,url in access['links']) - variables='
'.join(''+esc(value)+'' for value in access['env_vars']) - access_rows.append(('',cell(provider_label(name))+cell(esc(access['account']))+cell(official)+cell(variables)+cell(esc(access['configuration'])))) - account_intro='只有 Kitesurf 的账户级 Cloudflare Browser Run 路线需要供应商账号与凭据;其余方法是本地二进制,不需要账号,但仍需安装并明确指定可执行文件。' - account_body=hero('账号与安装',account_intro) - account_body+=section('本轮方法清单','

这里列的是完成本报告所需的全部外部准备:账号、官方获取入口和变量名称。变量值、本机路径、登录会话、Cookie 和 Token 均不进入报告。

'+table(['方法','账号要求','官方获取入口','变量名称','本轮配置'],access_rows,cls='accounts-table',metrics=[])) - account_body+=section('公开内容边界','

报告只公开复现所需的名称和入口,不公开凭据值。 发布门禁会扫描 HTML、JSON、下载文件和逐次输出中的常见 Token、私钥、Authorization 值与用户目录路径;第三方原始 HTML 中命中密钥形态的片段单独脱敏,计分输出保持逐字节不变。扫描结果和脱敏文件哈希随发布包保存。

') - write(out,'accounts/index.html','账号与安装',account_body) - else: - accounts=report_access_registry( - {r['provider'] for r in records}, - fixture_providers={r['provider'] for r in records if r.get('provider_type')=='fixture'}, - ) - accountrows=[] - for a in accounts: - official=link(a['registration_url'],'官方获取入口' if local_matrix else '注册 / 登录') - if a['key_url']!=a['registration_url']:official+='
'+link(a['key_url'],'获取凭据') - accountrows.append(('',cell(esc(a['provider']))+cell(official)+cell('
'.join(''+esc(x)+'' for x in a['env_vars']))+cell(esc(a['steps']))+cell(esc(a['free_note'])))) - write(out,'accounts/index.html','账号与安装',hero('账号与安装','只列官方入口和环境变量名称,不保存实际凭据。')+section('安装与账号准备',table(['提供方','官方入口','环境变量','取得方式','免费 / 准入说明'],accountrows,cls='accounts-table'))) + write(out,'accounts/index.html','账号与安装',render_account_requirements(records)) from .report_methods import render_methods write(out,'adoption/index.html','方法介绍',render_methods(out,records,hero=hero,section=section,table=table,cell=cell,link=link,esc=esc,authored=authored_interpretation)) @@ -3564,7 +3613,7 @@ def evidence_links(urls): for name in names: example=next((r for r in records if r['provider']==name and r['ok']),next(r for r in records if r['provider']==name)) entry,input_route,output_route=routes[name] - official=PROFILES[name][2] + official=report_method(name).get('source_url','') interface_rows.append(('',cell(link('../providers/'+name+'.html',labels[name]))+cell(esc(entry))+cell(esc(input_route))+cell(esc(output_route))+cell(link('../attempts/'+example['attempt_id']+'.html',h('查看实际输入、返回和逐项评分','Actual input, output and component scores')))+cell(link(official,h('官方获取入口','Official source'))))) adoption+=section(h('本轮输入、输出与实例','Run inputs, outputs and examples'),'

'+h('这里列的是公开集冻结 HTML 的实际路线,不是原 URL 的实时抓取接口。实例链接对应存档调用;账号字段只展示变量名和占位符。','These are the frozen-HTML routes used for this public benchmark, not live fetching of original URLs. Example links open archived calls; account fields show variable names or placeholders only.')+'

'+table([h('方法','Method'),h('接口 / 程序','Interface / program'),h('本轮输入','Run input'),h('本轮输出','Run output'),h('真实调用实例','Archived call example'),h('官方来源','Official source')],interface_rows,cls='wide-prose-table',metrics=[])) if research: @@ -3613,7 +3662,7 @@ def evidence_links(urls): account_rows=[] for name in names: - access=HEADLESS_ACCESS[name] + access=method_access(name,[]) account_rows.append(('',cell(labels[name])+cell(esc(access['account']))+cell('
'.join(''+esc(v)+'' for v in access['env_vars']))+cell('
'.join(link(url,label) for label,url in access['links'])))) accounts=hero(h('账号与安装','Accounts and setup'),h('只有 Kitesurf 的 Cloudflare 路线需要账号凭据;报告不公开值。','Only the Kitesurf Cloudflare route needs account credentials; values are never published.')) accounts+=section(h('配置','Configuration'),table([h('方法','Method'),h('账号','Account'),h('变量','Variables'),h('官方入口','Official source')],account_rows,cls='accounts-table',metrics=[])) diff --git a/harness/web_fetch/report_article_cases.py b/harness/web_fetch/report_article_cases.py deleted file mode 100644 index 671422be7..000000000 --- a/harness/web_fetch/report_article_cases.py +++ /dev/null @@ -1,319 +0,0 @@ -"""One article example per benchmark track, with hash-verified actual output.""" -import base64 -import hashlib -import re -from html import escape -from pathlib import Path -from urllib.parse import quote, urlsplit - -from lexbench_webfetch.metrics import task_passed -from .publication_target import public_report_url -from lexbench_webfetch.paths import contained_path, safe_identifier - - -CASE_TABS = ( - ('static-simple', '普通网页'), - ('dynamic-delayed', '动态网页'), - ('challenge-01', '热门站点'), - ('document-01', '文档'), - ('image-10', '图片'), - ('video-02', '视频'), -) -CASE_TRACKS = ('web', 'dynamic', 'challenge', 'document', 'image', 'video') -CASE_TITLES = { - 'static-simple': 'Example Domain 网页', - 'dynamic-delayed': 'Quotes 延迟加载的名言', - 'document-01': 'Attention Is All You Need 论文', - 'challenge-01': 'Anthropologie 蜡烛列表', - 'image-10': 'httpbin PNG 文件', - 'video-02': 'MDN 视频链接', -} - - -def _link(url, label): - if urlsplit(url).scheme not in {'http', 'https'}: - raise ValueError('Article case link requires HTTP(S)') - return f'{escape(label)}' - - -def _attempt_link(record): - route = 'attempts/' + safe_identifier(record['attempt_id']) + '.html' - return _link(public_report_url() + '#page=' + quote(route, safe=''), '完整输出与逐项判定') - - -def _read_output(out, record): - raw = contained_path(out, record['artifact_path']).read_bytes() - if hashlib.sha256(raw).hexdigest() != record['metrics']['content_sha256']: - raise ValueError('Article case output hash mismatch: ' + record['attempt_id']) - return raw.decode('utf-8', errors='replace') - - -def _pre(text, marks=()): - rendered = escape(text) - terms = sorted({escape(term) for term in marks if term}, key=len, reverse=True) - if terms: - pattern = '|'.join(re.escape(term) for term in terms) - rendered = re.sub(pattern, lambda match: '' + match.group() + '', rendered) - return '
' + rendered + '
' - - -def _row(name, expected, actual, passed): - return (f'{escape(str(name))}{escape(str(expected))}' - f'{escape(str(actual))}{"通过" if passed else "未通过"}') - - -def _text_example(case, record, output, *, pdf=False): - metrics = record['metrics'] - reference = case['ground_truth_main_content'] - marks = [term for item in metrics['fact_results'] for term in item['matched']] - if pdf: - # Corresponding abstract fragments, without inventing or translating text. - marker = 'The dominant sequence transduction models' - if marker not in reference or marker not in output: - raise ValueError('Reviewed PDF abstract excerpt is missing') - def abstract_excerpt(text): - start = text.index(marker) - endings = list(re.finditer(r'\.(?=\s|$)', text[start:])) - if len(endings) < 3: - raise ValueError('Reviewed PDF abstract requires three complete sentences') - return text[start:start + endings[2].end()] - reference_excerpt = abstract_excerpt(reference) - output_excerpt = abstract_excerpt(output) - left = '

独立参考正文 · 摘要节选

' + _pre(reference_excerpt, marks) - right = '

Moli 1.1.1 · 同一摘要位置节选

' + _pre(output_excerpt, marks) - # The abstract demonstrates Transformer; expose the other two checks - # with corresponding source lines, rather than only asserting a match. - def remaining_fact_lines(text, excerpt): - lines = [] - for term in marks: - if term in excerpt: - continue - line = next((line for line in text.splitlines() if term in line), None) - if line is None: - raise ValueError('Reviewed PDF fact excerpt is missing: ' + term) - if line not in lines: - lines.append(line) - return '\n'.join(lines) - left += '

标题与注意力机制 · 原文行节选

' + _pre(remaining_fact_lines(reference, reference_excerpt), marks) - right += '

标题与注意力机制 · 原文行节选

' + _pre(remaining_fact_lines(output, output_excerpt), marks) - meta = record['provider_metadata'] - right += ('

' - f'Moli 取得 PDF {meta["raw_fetch_ms"]/1000:.2f} 秒' - f' → pypdf 提取正文 {meta["document_extract_ms"]/1000:.2f} 秒' - f' → {metrics["content_chars"]:,} 字符

' - f'

从输入到交付共 {record["elapsed_ms"]/1000:.2f} 秒,包含流程开销。本题预先声明为 PDF 文档,未测试未知链接的自动类型识别。

') - note = '两侧均为节选;评分使用完整正文。参考正文按词数从头到尾近似等分为五段,再检查每段连续六字符片段在输出中找回的比例。Moli 取得 PDF 后,由本地 pypdf 在同一预算内提取文字。' - else: - left = '

独立参考正文 · 全文

' + _pre(reference, marks) - right = '

Moli 1.1.1 · 原始 Markdown 全文

' + _pre(output, marks) - note = '参考以正文保存,实际输出保留 Markdown 标记;按文字内容验收。' - checks = [] - for item in metrics['fact_results']: - checks.append(_row('必要文字', ' / '.join(item['alternatives']), - ' / '.join(item['matched']) or '未命中', item['pass'])) - checks.append(_row('输出长度', f'至少 {case["min_content_chars"]:,} 字符', - f'{metrics["content_chars"]:,} 字符', metrics['length_pass'])) - regions = metrics['body_region_results'] - required = metrics['body_required_target_regions'] - threshold = case['body_requirements']['minimum_coverage'] - floor = case['body_requirements']['minimum_floor'] - checks.append(_row('正文分区覆盖', - f'至少 {required}/{len(regions)} 区达到 {threshold:.0%};每区不低于 {floor:.0%}', - ' / '.join(f'{r["coverage"]:.0%}' for r in regions), - metrics['body_coverage_pass'])) - if 'text_encoding_corruption' in metrics: - checks.append(_row('文字解码', '替换字符数 ≥20 且占比 ≥2% 时拒绝', - f'{metrics.get("replacement_character_count", 0)} 个替换字符', - not metrics['text_encoding_corruption'])) - return left, right, checks, note - - -def _list_example(case, record, output): - contract, metrics = case['acceptance_contract'], record['metrics'] - anchors = [' / '.join(a['alternatives']) for a in contract['stable_text_anchors']] - left = '

独立观察的稳定文字

' + _pre('\n'.join(anchors), anchors) - left += ('

记录要求

' - f'

至少 {contract["minimum_records"]} 条不同记录

' - '

每条:非空名称 + 本站详情链接

' - '
链接识别细节' - '

名称至少 2 个字符且含字母;以 http(s) 开头的名称,以及“详情”“Read more”等登记的入口标签会被剔除。

' - '

按域名与路径去重;查询参数变化不增加记录数。

' - '

域名:' + escape(' / '.join(contract['hosts'])) + '

' - '

详情路径规则:' + escape(contract['entry_path']) + '

') - excerpt = [line for line in output.splitlines() - if line.startswith('[## ') and '](/shop/' in line][:3] - if len(excerpt) != 3: - raise ValueError('Reviewed product-list excerpts are missing') - anchor_lines = [] - for anchor in contract['stable_text_anchors']: - candidates = [line for line in output.splitlines() - if any(word in line for word in anchor['alternatives'])] - matched = next((line for line in candidates - if line.strip().lstrip('# ').strip() in anchor['alternatives']), - candidates[0] if candidates else None) - if matched is None: - raise ValueError('Reviewed stable-text excerpt is missing: ' + anchor['id']) - anchor_lines.append(matched) - right = '

Moli 1.1.1 · 四组稳定文字对应的原文行

' + _pre('\n'.join(anchor_lines), anchors) - right += '

原始 Markdown 中三条商品记录节选

' + _pre('\n\n'.join(excerpt)) - checks = [_row('稳定文字', ' / '.join(anchors), - f'{metrics["stable_anchor_hits"]}/{metrics["stable_anchor_total"]} 组命中', - metrics['stable_content_pass']), - _row('完整记录', f'至少 {contract["minimum_records"]} 条,字段与链接规则合格', - f'{metrics["stable_complete_record_count"]} 条', metrics['stable_structure_pass']), - _row('唯一记录', f'去重后至少 {contract["minimum_records"]} 条', - f'{metrics["stable_unique_record_count"]} 条', metrics['stable_structure_pass'])] - checks.append(_row('文字解码', '替换字符数 ≥20 且占比 ≥2% 时拒绝', - f'{metrics["replacement_character_count"]} 个替换字符', - not metrics['text_encoding_corruption'])) - note = '商品名称、价格和排序不作为固定答案;本题不核验名称的事实正确性或信息时效。右侧节选不是完整列表。' - note += '后续三次固定诊断调用中,Moli 一次取得 72 条记录,两次返回空内容;单次通过尚不能说明长期稳定性。这三次用于观察波动,不替换主榜的那次结果。' - return left, right, checks, note - - -def _dynamic_example(case, record, output): - contract, metrics = case['acceptance_contract'], record['metrics'] - anchors = contract['stable_text_anchors'] - first = anchors[0]['alternatives'][0] - if first not in case['ground_truth_main_content']: - raise ValueError('Reviewed dynamic reference entry is missing') - left = '

独立渲染后可见的第一条完整记录

' + _pre(first) - left += ('
查看全部 10 条独立参考记录' - + _pre(case['ground_truth_main_content']) + '
') - left += '

页面自动加载完成后应有 10 条名言,每条保留原句、作者与标签;无需滚动或点击。

' - right = '

Moli 1.1.1 · 原始 Markdown 全文

' + _pre(output) - right += f'

返回 {metrics["content_chars"]} 字符,用时 {record["elapsed_ms"]/1000:.2f} 秒;输出里有标题、登录和翻页入口,没有名言记录。

' - checks = [] - for field, item in zip(('第一条原句', '第一条作者', '第一条标签'), metrics['fact_results'][:3]): - checks.append(_row(field, ' / '.join(item['alternatives']), - ' / '.join(item['matched']) or '未命中', item['pass'])) - checks += [ - _row('完整且不同的记录', f'至少 {contract["minimum_records"]} 条,每条包含原句、作者和标签', - f'完整 {metrics["stable_complete_record_count"]} 条;去重后 {metrics["stable_unique_record_count"]} 条', - metrics['stable_structure_pass']), - _row('全部固定名言', '10 组原句、作者与标签均须匹配独立参考', - f'{metrics["stable_anchor_hits"]}/{metrics["stable_anchor_total"]} 组命中', metrics['stable_content_pass']), - _row('文字解码', '替换字符数 ≥20 且占比 ≥2% 时拒绝', - f'{metrics["replacement_character_count"]} 个替换字符', not metrics['text_encoding_corruption']), - ] - note = ('这个练习页将名言延迟呈现。调用正常返回,不等于目标内容已经到齐;' - 'Moli 与 Chromium 本轮都只取得了页面框架。等待与内容完成判断,是这类任务需要改进的环节。') - return left, right, checks, note - - -def _resource_example(case, record, output): - metrics = record['metrics'] - expected = case['expected_resources'][0]['urls'][0] - left = '

独立源文档确认的视频文件地址

' + _pre(expected) - left += '

应交付目标视频文件的地址或有效播放器入口;封面图片与普通首页链接不算。

' - right = '

Moli 1.1.1 · 原始 Markdown 全文

' + _pre(output, ('rabbit320.webm',)) - right += '

以输入页面地址补全相对路径后

' + _pre('\n'.join(metrics['resource_candidate_urls'])) - checks = [_row('视频资源地址', expected, - ' / '.join(item['matched_urls']) or '未命中', item['pass']) - for item in metrics['resource_results']] - note = ('输出虽含“不支持 HTML5 视频”的后备提示,仍提供了正确的播放文件链接。' - '资源题检查目标地址,而不是把这句话当作抓取失败。本题 Moli 与 Chromium 均通过,不涉及视频听写或内容理解。') - return left, right, checks, note - - -def _file_example(out, case, record): - expected_hash = case['source_verification']['file_sha256'] - files = record['provider_metadata']['file_evaluation']['files'] - if len(files) != 1: - raise ValueError('Reviewed PNG example requires one delivered file') - delivered = files[0] - raw = contained_path(out, delivered['artifact_path']).read_bytes() - digest = hashlib.sha256(raw).hexdigest() - if digest != delivered['sha256'] or len(raw) != delivered['bytes']: - raise ValueError('Article case delivered-file integrity mismatch') - if not raw.startswith(b'\x89PNG\r\n\x1a\n'): - raise ValueError('Article case PNG signature mismatch') - left = '

独立目标文件

' + _pre(case['reference_answer']) - left += '

目标 SHA-256

' + _pre(expected_hash) - uri = 'data:image/png;base64,' + base64.b64encode(raw).decode('ascii') - right = '

Moli 1.1.1 · 实际交付的 PNG

' - right += f'PNG file returned by Moli in the recorded benchmark' - right += '

实际 SHA-256

' + _pre(digest) - checks = [_row('文件签名', 'PNG 文件头:89504e470d0a1a0a', raw[:8].hex(), - raw[:8] == b'\x89PNG\r\n\x1a\n'), - _row('文件长度', '至少 32 字节;独立目标为 8,090 字节', - f'{len(raw):,} 字节', len(raw) >= 32), - _row('文件一致性', '匹配独立目标 SHA-256', - '一致' if digest == expected_hash else '不一致', digest == expected_hash)] - note = '文件长度的通用门槛是 32 字节。SHA-256 是按文件字节计算的指纹,用来核对是否同一份文件;不评价图片识别或媒体语义。' - return left, right, checks, note - - -def render_article_cases(out, cases, records): - """Return six track panels; no calls, rescoring, or artifact mutation.""" - out = Path(out) - if tuple(cases[case_id].get('track') for case_id, _ in CASE_TABS) != CASE_TRACKS: - raise ValueError('Article examples must cover each benchmark track exactly once') - indexed = {} - for record in records: - key = (record['provider'], record['case_id']) - if key in indexed: - raise ValueError('Article examples require one authoritative record per provider and case') - indexed[key] = record - panels = [] - for index, (case_id, tab) in enumerate(CASE_TABS, 1): - case = cases[case_id] - record = indexed[('moli', case_id)] - output = _read_output(out, record) - if case_id == 'challenge-01': - left, right, checks, note = _list_example(case, record, output) - elif case_id == 'dynamic-delayed': - left, right, checks, note = _dynamic_example(case, record, output) - elif case_id == 'video-02': - left, right, checks, note = _resource_example(case, record, output) - elif case_id == 'image-10': - left, right, checks, note = _file_example(out, case, record) - else: - left, right, checks, note = _text_example(case, record, output, pdf=case_id == 'document-01') - chromium = indexed.get(('chromium', case_id)) - comparison = '' - if chromium and not task_passed(chromium['ok'], chromium['metrics'], case.get('task_kind')): - other = _read_output(out, chromium) - comparison = '

Chromium · 本轮对照输出

' - comparison += _pre(other[:400]) if other else '

空输出 · 0 字符

' - if len(other) > 400: - comparison += '

以上为前 400 字符节选。

' - other_metrics = chromium['metrics'] - if case_id == 'document-01': - reason = (f'正文 {other_metrics["content_chars"]} 字符;必要文字命中 ' - f'{other_metrics["fact_hits"]}/{other_metrics["fact_total"]}。') - elif case_id == 'challenge-01': - reason = (f'完整记录 {other_metrics["stable_complete_record_count"]} 条;稳定文字命中 ' - f'{other_metrics["stable_anchor_hits"]}/{other_metrics["stable_anchor_total"]}。') - elif case_id == 'dynamic-delayed': - reason = (f'完整名言记录 {other_metrics["stable_complete_record_count"]} 条;固定名言命中 ' - f'{other_metrics["stable_anchor_hits"]}/{other_metrics["stable_anchor_total"]} 组。') - elif case_id == 'video-02': - reason = f'视频资源地址命中 {other_metrics["resource_hits"]}/{other_metrics["resource_total"]}。' - elif case_id == 'image-10': - count = len(chromium['provider_metadata']['file_evaluation']['files']) - reason = f'实际交付 {count} 个原始文件;这里只返回了 Markdown 图片链接。' - else: - reason = '未满足本题全部验收要求。' - comparison += '

' + escape(reason) + '

' + _attempt_link(chromium) + '

' - comparison += '
' - passed = task_passed(record['ok'], record['metrics'], case.get('task_kind')) - status = '通过' if passed else '未通过' - panels.append( - f'
' - f'

{escape(CASE_TITLES[case_id])}

输入:{_link(case["url"], case["url"])}

' - '

标准答案与要求

' + left + '
' - '

本次真实输出

' + right + comparison + '
' - '

左右滑动查看完整判定。

' - '
' - '' + ''.join(row.replace('', f'', 1) for i, row in enumerate(checks, 1)) + '
序号检查项验收要求Moli 实际结果判定
{i}
' - f'

本题{status}。 {escape(note)}

' - f'

{_attempt_link(record)}

') - radios = ''.join( - f'' for i in range(1, len(CASE_TABS) + 1)) - labels = ''.join(f'' - for i, (_, tab) in enumerate(CASE_TABS, 1)) - return ('
' + radios + '
' + labels + - '
' + ''.join(panels) + '
') diff --git a/harness/web_fetch/report_article_figures.py b/harness/web_fetch/report_article_figures.py deleted file mode 100644 index 8e4078772..000000000 --- a/harness/web_fetch/report_article_figures.py +++ /dev/null @@ -1,48 +0,0 @@ -"""Reviewed editorial illustrations, with identical online and offline bytes.""" -import base64 -import hashlib -import json -from html import escape -from pathlib import Path - -from lexbench_webfetch.paths import contained_path -from .storage import data_home - -MANIFEST = Path(__file__).resolve().parents[2] / 'research/editorial-figures.json' - - -def render_article_figures(out): - manifest = json.loads(MANIFEST.read_text()) - source = data_home() / 'editorial/moli-webfetch' - online, embedded, provenance = {}, {}, [] - for figure in manifest['figures']: - urls, uris = {}, {} - for locale, asset in figure['assets'].items(): - raw = contained_path(source, asset['file']).read_bytes() - if hashlib.sha256(raw).hexdigest() != asset['sha256']: - raise ValueError('Editorial illustration changed; review image and update its binding: ' + asset['file']) - if raw[:8] != b'\x89PNG\r\n\x1a\n' or raw[12:16] != b'IHDR': - raise ValueError('Editorial illustration must be a PNG: ' + asset['file']) - relative = 'assets/editorial/' + asset['file'] - destination = contained_path(out, relative) - destination.parent.mkdir(parents=True, exist_ok=True) - destination.write_bytes(raw) - urls[locale] = '../' + relative - uris[locale] = 'data:image/png;base64,' + base64.b64encode(raw).decode('ascii') - provenance.append({'figure': figure['slot'], 'locale': locale, 'path': relative, - 'sha256': asset['sha256'], 'bytes': len(raw), - 'kind': figure['kind'], 'generated': True}) - - def markup(links): - main, english = links['zh'], links.get('en') - en_src = f' data-en-src="{escape(english, quote=True)}"' if english else '' - en_href = f' data-en-href="{escape(english, quote=True)}"' if english else '' - return (f'
' - f'' - f'{escape(figure[' - f'
{escape(figure["caption"])} ' - f'下载图片' - '
') - online[figure['slot']] = markup(urls) - embedded[figure['slot']] = markup(uris) - return online, embedded, provenance diff --git a/harness/web_fetch/report_build.py b/harness/web_fetch/report_build.py index b448376b4..04319e446 100644 --- a/harness/web_fetch/report_build.py +++ b/harness/web_fetch/report_build.py @@ -1226,8 +1226,11 @@ def build_wcxb_report(source: Path, corpus: Path, sample_path: Path, profile: di from harness.report_languages import includes_english, select_report_language include_english = includes_english(language, include_english) from harness.web_fetch.wcxb_dataset import validate_manifest - from harness.web_fetch.webmainbench_runner import WCXB_PROVIDERS, load_wcxb_source + from harness.web_fetch.webmainbench_runner import load_wcxb_source source, output = source.resolve(), output.resolve() + providers = profile['providers'] + if not providers or len(set(providers)) != len(providers): + raise ValueError('WCXB profile must declare unique participants') from harness.reporting.assets import load_bound_content report_content = load_bound_content( source / "report-assets", content_binding, @@ -1255,14 +1258,10 @@ def build_wcxb_report(source: Path, corpus: Path, sample_path: Path, profile: di evaluator_hash = file_sha256(contained_path(corpus, 'evaluate.py')) if evaluator_hash != profile['official_evaluator_sha256'] or summary.get('official_evaluator_sha256') != evaluator_hash: raise ValueError('WCXB official evaluator differs from the certified profile') - if [x['provider'] for x in certificate['providers']] != list(WCXB_PROVIDERS) or profile['providers'] != list(WCXB_PROVIDERS): + if [x['provider'] for x in certificate['providers']] != providers: raise ValueError('WCXB participant registry differs from the certificate') - candidate = next(x for x in certificate['providers'] if x['provider'] == 'moli-pr643') - if candidate['identity'].get('binary_sha256') != profile['candidate_binary_sha256']: - raise ValueError('WCXB candidate binary differs from the current report profile') - released = next(x for x in certificate['providers'] if x['provider'] == 'moli') - if released['identity'].get('binary_sha256') != profile['release_binary_sha256']: - raise ValueError('WCXB released Moli binary differs from the current report profile') + from harness.web_fetch.wcxb_scoring import paired_latency_methods + paired_methods = paired_latency_methods(profile, certificate) rows = load_jsonl(source / 'wcxb-scores.jsonl', skip_blank=True) case_ids = {r['track_id'] for r in records} correction_root = source / 'latency-correction' @@ -1280,22 +1279,27 @@ def build_wcxb_report(source: Path, corpus: Path, sample_path: Path, profile: di raise ValueError('WCXB corrected timing is not certified') if any( abs(row['total_elapsed_ms'] - corrected_calls[row['case_id'], - 'release' if row['provider'] == 'moli' else 'pr']['elapsed_ms']) > .01 - for row in rows if row['provider'] in ('moli', 'moli-pr643') + paired_methods[row['provider']]]['elapsed_ms']) > .01 + for row in rows if row['provider'] in paired_methods ): - raise ValueError('WCXB reported Moli latency differs from the selected paired round') - optional_root = source / 'optional-ui-4017' - optional_receipt = optional_root / 'receipt.json' + raise ValueError('WCXB reported paired latency differs from the selected paired round') + optional_paths = [name for name, sha in content_binding['evidence'].items() + if sha == profile.get('optional_ui_receipt_sha256')] + if len(optional_paths) != 1: + raise ValueError('Optional feature receipt must have one evidence binding') + optional_receipt = contained_path(source, optional_paths[0], reject_symlinks=True) + optional_root = optional_receipt.parent + optional_public = Path('research') / ('wcxb-' + optional_root.name) if (not optional_receipt.is_file() or optional_receipt.is_symlink() or file_sha256(optional_receipt) != profile.get('optional_ui_receipt_sha256')): raise ValueError('WCXB optional UI-stripping evidence is missing or differs from profile') optional = json.loads(optional_receipt.read_text()) - if (optional.get('case_id') != '4017' or optional.get('source_sha256') != corpus_hash + if (optional.get('case_id') not in case_ids or optional.get('source_sha256') != corpus_hash or optional.get('builds', {}).get('release', {}).get('binary_sha256') != profile['release_binary_sha256'] or optional.get('builds', {}).get('candidate', {}).get('binary_sha256') != profile['candidate_binary_sha256'] - or optional['builds']['release']['description_present'] - or not optional['builds']['candidate']['description_present']): + or any(not isinstance(build.get('description_present'), bool) + for build in optional['builds'].values())): raise ValueError('WCXB optional UI-stripping comparison does not bind both builds') for name in ('release', 'candidate'): for ext, key in (('md', 'output_sha256'), ('stderr', 'stderr_sha256')): @@ -1303,7 +1307,7 @@ def build_wcxb_report(source: Path, corpus: Path, sample_path: Path, profile: di if (not artifact.is_file() or artifact.is_symlink() or file_sha256(artifact) != optional['builds'][name][key]): raise ValueError('WCXB optional UI-stripping artifact changed: ' + artifact.name) - if len(rows) != len(records) * len(WCXB_PROVIDERS) or {(r['provider'], r['case_id']) for r in rows} != {(p,c) for p in WCXB_PROVIDERS for c in case_ids}: + if len(rows) != len(records) * len(providers) or {(r['provider'], r['case_id']) for r in rows} != {(p,c) for p in providers for c in case_ids}: raise ValueError('WCXB score matrix is incomplete') if output.exists() and not (output / '.generated-report').is_file(): raise ValueError('Refusing to replace an unmarked report') @@ -1317,9 +1321,9 @@ def build_wcxb_report(source: Path, corpus: Path, sample_path: Path, profile: di (staging / 'research/wcxb-full-paired-latency').mkdir() for name in ('receipt.json', 'certification.json', 'calls.jsonl'): shutil.copy2(correction_root / name, staging / 'research/wcxb-full-paired-latency' / name) - (staging / 'research/wcxb-optional-ui-4017').mkdir() + (staging / optional_public).mkdir() for name in ('receipt.json', 'release.md', 'candidate.md', 'release.stderr', 'candidate.stderr'): - shutil.copy2(optional_root / name, staging / 'research/wcxb-optional-ui-4017' / name) + shutil.copy2(optional_root / name, staging / optional_public / name) shutil.copy2(Path(__file__).resolve().parents[2] / 'research/report-changes.json', staging / 'research/report-changes.json') shutil.copy2(Path(__file__).resolve().parents[2] / 'research/provider-adoption.json', @@ -1385,7 +1389,7 @@ def build_wcxb_report(source: Path, corpus: Path, sample_path: Path, profile: di case_id = record['track_id'] outputs = {} retries = {} - for provider in WCXB_PROVIDERS: + for provider in providers: outputs[provider] = (staging / 'artifacts' / provider / f'{case_id}.txt').read_text(encoding='utf-8') retries[provider] = [ {**retry, 'output': contained_path(staging, retry['artifact_path']).read_text(encoding='utf-8')} @@ -1409,7 +1413,7 @@ def build_wcxb_report(source: Path, corpus: Path, sample_path: Path, profile: di raise ValueError('WCXB representative review scope differs from measured matrix') for case_review in review['cases']: case_id = case_review['case_id'] - if case_id not in rows_by_case or not set(case_review['outputs']) <= set(WCXB_PROVIDERS): + if case_id not in rows_by_case or not set(case_review['outputs']) <= set(providers): raise ValueError('WCXB review case or method inventory differs') evidence = json.loads((evidence_root / f'{case_id}.json').read_text()) if (hashlib.sha256(evidence['source_html'].encode()).hexdigest() != case_review['source_html_sha256'] diff --git a/harness/web_fetch/report_capabilities.py b/harness/web_fetch/report_capabilities.py index 28004f574..e28ea83ab 100644 --- a/harness/web_fetch/report_capabilities.py +++ b/harness/web_fetch/report_capabilities.py @@ -1,315 +1,137 @@ """Official capability evidence, deliberately independent of evaluation scores.""" import json +from harness.numbers import positive_number from html import escape -from .report_components import tabs +from .report_components import tabs, bilingual_text + +from .methods import report_method, report_method_catalog, browser_method_label, method_version, normalized_method_version -CHECKED_AT = '2026-09-11' -ROUTE_CHECKED_AT = '2026-09-15' -SOURCES = { - 'cow': ('CowAgent · WebFetch 源码', 'https://github.com/zhayujie/CowAgent/blob/ed5bb344cfe42cee443c8266ba47ee666d8d7471/agent/tools/web_fetch/web_fetch.py'), - 'lex': ('Lexmount · Extract 契约', 'https://browser.lexmount.com/docs/webfetch/reference/all-in-one-extract'), - 'lex-overview': ('Lexmount · WebFetch 概览', 'https://browser.lexmount.com/en/docs/webfetch'), - 'fire': ('Firecrawl · Scrape 格式', 'https://docs.firecrawl.dev/features/scrape'), - 'fire-api': ('Firecrawl · Scrape API 与 video 格式', 'https://docs.firecrawl.dev/api-reference/endpoint/scrape'), - 'fire-doc': ('Firecrawl · 文档解析', 'https://docs.firecrawl.dev/features/document-parsing'), - 'tav': ('Tavily · Extract 契约', 'https://docs.tavily.com/documentation/api-reference/endpoint/extract'), - 'tav-depth': ('Tavily · 提取深度', 'https://docs.tavily.com/documentation/best-practices/best-practices-extract'), - 'tav-research': ('Tavily · Research 文件上传', 'https://www.tavily.com/blog/what-tavily-shipped-in-may-26'), - 'exa': ('Exa · Contents 参数', 'https://exa.ai/docs/reference/contents-api-guide-for-coding-agents'), - 'cf': ('Cloudflare · Markdown 端点', 'https://developers.cloudflare.com/browser-run/quick-actions/markdown-endpoint/'), - 'cf-scrape': ('Cloudflare · Scrape 端点', 'https://developers.cloudflare.com/browser-run/quick-actions/scrape-endpoint/'), - 'cf-files': ('Cloudflare · 文档转 Markdown', 'https://developers.cloudflare.com/workers-ai/features/markdown-conversion/usage/rest-api/'), - 'cf-formats': ('Cloudflare · 支持的文档格式', 'https://developers.cloudflare.com/workers-ai/features/markdown-conversion/supported-formats/'), - 'claw': ('OpenClaw · Web Fetch 文档', 'https://github.com/openclaw/openclaw/blob/12c4bdf7482f24c85b011e9d858fdec3c245101c/docs/tools/web-fetch.md'), - 'claw-code': ('OpenClaw · 抽取实现', 'https://github.com/openclaw/openclaw/blob/12c4bdf7482f24c85b011e9d858fdec3c245101c/src/agents/tools/web-fetch.ts'), - 'claw-html': ('OpenClaw · HTML 转换器', 'https://github.com/openclaw/openclaw/blob/12c4bdf7482f24c85b011e9d858fdec3c245101c/src/agents/tools/web-fetch-utils.ts'), - 'claw-reader': ('OpenClaw · Readability 插件', 'https://github.com/openclaw/openclaw/blob/12c4bdf7482f24c85b011e9d858fdec3c245101c/extensions/web-readability/web-content-extractor.ts'), - 'dsh': ('DeepSeek Harness · MIME 策略', 'https://github.com/deepseek-ai/deepseek-harness/blob/dd6322d604e00eec1ba5e0c8541159906a21094a/packages/web/web-fetch-http/src/policy.ts'), - 'dsh-text': ('DeepSeek Harness · Markdown 转换', 'https://github.com/deepseek-ai/deepseek-harness/blob/dd6322d604e00eec1ba5e0c8541159906a21094a/packages/web/tool-web/src/fetch.ts'), - 'lc': ('LangChain · WebBaseLoader 源码', 'https://github.com/langchain-ai/langchain-community/blob/libs/community/v0.3.30/libs/community/langchain_community/document_loaders/web_base.py'), - 'lc-loaders': ('LangChain · 其他文档加载器', 'https://docs.langchain.com/oss/python/integrations/document_loaders/index'), - 'c4-media': ('Crawl4AI · 链接与媒体', 'https://docs.crawl4ai.com/core/link-media/'), - 'c4-pdf': ('Crawl4AI · PDF 策略', 'https://docs.crawl4ai.com/advanced/pdf-parsing/'), - 'c4-download': ('Crawl4AI · 文件下载', 'https://docs.crawl4ai.com/advanced/file-downloading/'), - 'c4-config': ('Crawl4AI · 配置', 'https://docs.crawl4ai.com/api/parameters/'), -} -# Each cell identifies the contract, not a score or an extrapolated success rate. def c(status, detail): - return {'status': status, 'detail': detail} + return {'status':status, 'detail':detail} -CAPABILITIES = { - 'cowagent-built-in': { - 'scope': 'WebFetch.execute', 'sources': ['cow'], - 'web': c('支持', 'HTML → 纯文本'), 'document': c('支持', 'PDF / DOCX / TXT 等'), - 'image': c('无此能力', '不提取图片属性'), 'av': c('无此能力', '无媒体提取器'), - 'file': c('有限支持', '受支持文档落盘'), 'js': c('无此能力', 'HTTP,不运行 JS'), - 'input': 'HTTP(S) URL;不是本地文件上传接口。', - 'structure': '文本、文档标题和保存路径;没有通用 JSON schema 抽取。', - 'access': '当前工具未提供站点 Cookie 参数;不是登录浏览器。', - 'notes': 'PDF 使用 pypdf 读取文字层,不含 OCR;DOCX 读取段落。另有 XLSX、PPTX 解析分支并依赖相应库。虽然扩展名列表包含 XLS/PPT,实际采用 openpyxl/python-pptx,不能据此保证旧二进制格式可解析。文档下载成功可返回本地路径;不是任意图片或视频下载器。', - 'tested': '本轮调用同一 WebFetch;本机是否安装解析依赖、文件是否能解析仍看实测,支持声明不等于必然成功。', - }, - 'lexmount-global': { - 'scope': 'POST /v1/extract · 国际区', 'sources': ['lex', 'lex-overview'], - 'web': c('支持', 'main_text'), 'document': c('未声明', '未列 PDF / Office 契约'), - 'image': c('仅链接', 'images'), 'av': c('未声明', '无专用媒体字段'), - 'file': c('未声明', '无原文件返回契约'), 'js': c('支持', '按需渲染'), - 'input': 'URL 或已捕获的 dom_id;/v1/dom/dump 为独立 DOM 捕获接口。', - 'structure': 'title、author、links、images 等固定字段;可选 trace/raw_dom。', - 'access': 'API Key + Project ID 是服务鉴权,不是源站登录凭据;公开文档面向公共网页。', - 'notes': 'images 是资源引用,不是图片字节或视觉理解;links 可能包含文档或视频入口,但不保证提取视频流。PDF/Office、音视频转写和通用二进制下载均未在所核对的 Extract 契约中明确声明,不能写成确定不支持。', - 'tested': '国际区与中国区共用公开契约,但部署与实测分开;这里不推断两区内部引擎完全相同。', - }, - 'firecrawl': { - 'scope': 'POST /v2/scrape', 'sources': ['fire', 'fire-api', 'fire-doc'], - 'web': c('支持', 'Markdown / HTML'), 'document': c('支持', 'PDF(含 OCR)/ Office'), - 'image': c('仅链接', 'images 返回图片 URL'), 'av': c('需配置', 'audio / video 下载地址'), - 'file': c('需配置', 'rawBase64 原始响应'), 'js': c('支持', '渲染与 Actions'), - 'input': 'Scrape 接收 URL;本地文件上传走另一个 /v2/parse 接口。', - 'structure': '可选 JSON schema、产品字段、截图等格式;不能都视为本轮已测。', - 'access': '可配置页面动作;站点授权、反爬与套餐限制仍会影响访问。', - 'notes': 'rawBase64 必须单独请求,解码后是目标 URL 的原始 HTTP 响应体,不包含页面引用的所有资源。audio/video 针对支持的视频站点,返回有时效的下载 URL,不保证任意站点或原编码字节,也不是字幕转写/画面理解。文档解析覆盖 PDF、Word、Excel、PowerPoint 等。', - 'tested': '正文与资源题请求 markdown、links、images;页面视频资源题另请求 video;原文件题只请求 rawBase64 并验收目标 URL 的解码字节。动态顶层选择器使用 Actions wait,随后执行预声明稳定延迟;选择器数量与 iframe 子文档条件仍标为未完整执行。未测试 audio、JSON、截图或 Parse。', - }, - 'tavily': { - 'scope': 'POST /extract', 'sources': ['tav', 'tav-depth', 'tav-research'], - 'web': c('支持', 'Markdown / 纯文本'), 'document': c('未声明', 'Extract 未列格式保证'), - 'image': c('仅链接', 'include_images'), 'av': c('未声明', '无转写/媒体下载契约'), - 'file': c('未声明', '无原始字节字段'), 'js': c('支持', '复杂页建议 advanced'), - 'input': '单个或批量 URL;Extract 与 Search、Research 是不同接口。', - 'structure': 'raw_content 和图片列表;query 可选择相关片段,并非必然返回全文。', - 'access': '公开 Extract 参数没有源站登录会话接口;advanced 不等于保证绕过登录。', - 'notes': 'advanced 面向动态页、表格和嵌入内容;不能把 embedded media 理解为下载视频或理解画面。官方 Research 的 PDF/Markdown/JSON/TXT 文件上传能力不属于本次 Extract;Extract 的 PDF/Office 支持在本次核对资料中未明确承诺。', - 'tested': '本轮 advanced + markdown + include_images=true,不传 query;未测试 Research 文件上传。', - }, - 'exa': { - 'scope': 'POST /contents', 'sources': ['exa'], - 'web': c('支持', 'text → Markdown'), 'document': c('支持', 'PDF;Office 未声明'), - 'image': c('仅链接', 'extras.imageLinks'), 'av': c('未声明', '普通 links 不保证媒体流'), - 'file': c('未声明', '无原文件返回字段'), 'js': c('支持', 'Contents 动态页抓取'), - 'input': '一组 URL;可配置 subpages,不是任意本地文件上传接口。', - 'structure': 'text、highlights、summary;摘要可用 schema,含模型生成环节。', - 'access': 'maxAgeHours 控制缓存/实时抓取,不提供登录成功保证。', - 'notes': 'extras.links 和 imageLinks 是不带名称归属的地址;richLinks 返回明确的 anchor—URL 对。PDF text 是解析后的文字,不是原 PDF。summary/highlights 与正文原文提取的语义不同,生成摘要不能作为独立 ground truth。', - 'tested': 'text.verbosity=full、maxAgeHours=0;资源题取 links/imageLinks,具名列表题取 richLinks 并只接收非空 anchor—URL 对。本轮没有调用摘要或高亮生成。', - }, - 'cloudflare-markdown': { - 'scope': 'Browser Run /markdown、/scrape;直接文档 Workers AI /ai/tomarkdown', 'sources': ['cf', 'cf-scrape', 'cf-files', 'cf-formats'], - 'web': c('支持', '网页 → Markdown'), 'document': c('需配置', '上传直接 PDF / DOCX 后转换为 Markdown'), - 'image': c('仅链接', '/scrape 读取 DOM 图片属性'), 'av': c('仅链接', '/scrape 读取 DOM 媒体属性'), - 'file': c('无此能力', '文档上传后返回文字,不交付原文件'), 'js': c('支持', 'Browser Run 浏览器 + 等待参数'), - 'input': '网页输入为 URL;直接文档由客户端下载原始字节后上传 Workers AI。', - 'structure': '/markdown 返回网页文本,/scrape 返回元素属性,/ai/tomarkdown 返回文档文字。', - 'access': 'Browser Rendering Edit 与 Workers AI 权限分别控制两类端点;浏览器访问仍可能被识别和拦截。', - 'notes': '/pdf 打印网页,不是读取已有 PDF;/links 只列页面链接,不能代替图片和媒体 DOM 属性。直接文档下载与转换共享同一次耗时;客户端下载不是 Cloudflare 独立成绩。/scrape 交付资源地址,不保证取得媒体字节或理解画面。', - 'tested': '网页正文与动态页使用 /markdown;直接 PDF / DOCX 使用 /ai/tomarkdown;页面图片和视频资源使用 /scrape。未提供源站登录 Cookie,不用 Search/Crawl、截图、浏览器控制或失败后接口回退。', - }, - 'openclaw-built-in': { - 'scope': 'web_fetch · 内置 HTTP 路径', 'sources': ['claw', 'claw-code', 'claw-html', 'claw-reader'], - 'web': c('支持', 'Readability → Markdown'), 'document': c('无此能力', '无 PDF / Office 解析器'), - 'image': c('有限支持', '原生 Markdown / 超链接'), 'av': c('有限支持', '正文中的普通超链接'), - 'file': c('无此能力', '文本输出不是原文件'), 'js': c('无此能力', '需另用 Browser'), - 'input': 'HTTP(S) URL;可选 Markdown 或纯文本。', - 'structure': '正文与抓取元数据;不提供任意 schema 抽取。', - 'access': '可配置请求头;登录/JS 重页面由独立 Browser 工具处理。', - 'notes': 'Readability 的 HTML 输出经自带转换器处理,不直接提取 img.src;只有服务端原生 Markdown 或正文普通超链接等路径可能留下图片地址。不能保证收集图片或播放入口。PDF/媒体二进制误解码成字符串不是原文件支持;Firecrawl 等可选回退是另一路径。', - 'tested': '本轮开启 Readability、关闭 Firecrawl 回退及缓存;不能把整个 OpenClaw 的浏览器或 PDF 工具能力算到此行。', - }, - 'deepseek-harness-built-in': { - 'scope': 'web_fetch · HTTP provider', 'sources': ['dsh', 'dsh-text'], - 'web': c('支持', 'HTML → Markdown'), 'document': c('无此能力', '拒绝 PDF / Office MIME'), - 'image': c('有限支持', 'HTML 中的 Markdown 引用'), 'av': c('有限支持', '页面中的普通超链接'), - 'file': c('无此能力', '拒绝二进制 MIME'), 'js': c('无此能力', '匿名 HTTP 获取'), - 'input': 'HTTP(S) URL;只接受 HTML、text/*、JSON/XML 等文字 MIME。', - 'structure': 'Turndown 转换正文和表格,返回工具文本与状态。', - 'access': '拒绝 URL 内嵌凭据;跨源重定向需独立调用,不是登录会话浏览器。', - 'notes': '依据本轮固定源码的内容类型白名单;PDF、image/*、video/* 不在可解码类型中。HTML 内出现媒体链接不意味着可直接抓取对应二进制 URL。', - 'tested': '仅匿名 HTTP provider;不是 DeepSeek 模型 API,也没有使用模型作评分。', - }, - 'langchain-webbase': { - 'scope': 'WebBaseLoader.load', 'sources': ['lc', 'lc-loaders'], - 'web': c('支持', 'BeautifulSoup → 纯文本'), 'document': c('无此能力', 'PDF / Office 需其他 loader'), - 'image': c('无此能力', 'load 不返回 src'), 'av': c('无此能力', 'load 不返回媒体字段'), - 'file': c('无此能力', 'Document 不是原始文件'), 'js': c('无此能力', 'HTTP,不运行 JS'), - 'input': '单个或多个网页 URL;load 返回 Document 对象。', - 'structure': 'page_content + 页面元数据;scrape 可另取 BeautifulSoup 自行提取属性。', - 'access': '可传入 requests Session / headers;不自动执行登录交互。', - 'notes': 'get_text 不保留 href/src 属性;页面若本身写出 URL,文字中仍可能出现地址。LangChain 有独立 PDF、Word 和浏览器加载器,但不能合并成 WebBaseLoader.load 的能力。', - 'tested': '只调用 load 读取正文;没有额外 soup 解析、PDF loader 或浏览器渲染。', - }, - 'crawl4ai': { - 'scope': '正文 AsyncWebCrawler;文件 AsyncHTTPCrawlerStrategy', 'sources': ['c4-media', 'c4-pdf', 'c4-download', 'c4-config'], - 'web': c('支持', '渲染 HTML → Markdown'), 'document': c('需配置', 'PDF 专用策略'), - 'image': c('仅链接', 'media.images + 元数据'), 'av': c('仅链接', 'media.videos / audios'), - 'file': c('需配置', '原生 HTTP 文件策略或浏览器下载'), 'js': c('支持', '浏览器 + JS 动作'), - 'input': '网页 URL;远程或本地 PDF 需专用策略。', - 'structure': '可选 CSS/JSON 或 LLM 提取策略;media/links 是结构化资源信息。', - 'access': '可配置持久会话、Cookie 和页面动作;并非保证突破每个站点限制。', - 'notes': 'PDF 需 PDFCrawlerStrategy + PDFContentScrapingStrategy,可选提取并保存内嵌图片,默认不含扫描件 OCR。浏览器下载需 accept_downloads 并触发下载;原生 HTTP 文件策略直接将非 HTML 响应保存到 downloaded_files。网页截图和打印 PDF 是新生成的文件,不是目标原文件。', - 'tested': '非动态网页使用通用浏览器策略、domcontentloaded 与无缓存;动态任务使用预声明的完成条件。直接 PDF 正文在调用前选择 PDFCrawlerStrategy 与 PDFContentScrapingStrategy;文件任务选择原生 HTTP 文件策略并统一校验。未启用截图、OCR 或 LLM 提取;媒体字段不代表内容理解。', - }, -} -SOURCES['moli'] = ('Moli 1.1.4 · WebFetch skill', 'https://github.com/lexmount/moli/blob/v1.1.4/skills/moli-webfetch/SKILL.md') -SOURCES['moli-recipes'] = ('Moli 1.1.4 · 输出与接口边界', 'https://github.com/lexmount/moli/blob/v1.1.4/skills/moli-webfetch/references/fetch-recipes.md') -SOURCES['moli-release'] = ('Moli 1.1.4 · 官方发布', 'https://github.com/lexmount/moli/releases/tag/v1.1.4') -CAPABILITIES['moli-webfetch'] = { - 'scope':'moli fetch;网页、文档正文、结构和网页内嵌媒体使用原生 Markdown,预声明完成条件只调用官方等待参数,直接媒体文件使用原始响应', 'sources':['moli','moli-recipes','moli-release'], - 'web':c('支持','JS 渲染网页 → Markdown / HTML'), - 'document':c('需配置','裸 fetch 取得原始 PDF 后在同一预算内本地解析;Office 取决于实际响应'), - 'image':c('有限支持','原生 Markdown 中保留的图片地址;直接文件可验收原始字节'), - 'av':c('有限支持','原生 Markdown 中保留的媒体地址;直接文件可验收原始字节'), - 'file':c('有限支持','裸 fetch 可自动返回直接非 HTML 响应的原始字节'), - 'js':c('支持','默认运行 JS,支持多种就绪等待'), - 'input':'单个 URL;每个页面单独调用 fetch。', - 'structure':'网页与文档任务由 --dump markdown 返回原生 Markdown;直接媒体文件返回原始字节;等待条件不参与内容抽取。', - 'access':'支持授权 cookie-file / profile-dir 和请求头;仍可能被站点拒绝。默认适配器不加载登录状态。', - 'notes':'截图和 PDF 是启用 --layout 后生成的页面产物,不是读取已有文档或下载原媒体。官方未承诺文档 OCR、媒体理解或音频转写;直接非 HTML 输入不强制使用 Markdown。', - 'tested':'网页正文、文档正文、结构和网页内嵌媒体任务统一使用 Moli 原生 Markdown;动态网站正文整类统一启用 --layout、--with-frames 和 domstable,不使用逐题 selector。网页媒体只核对同一 Markdown 输出里的目标地址,不下载媒体。直接 PDF/DOCX 未提供可读正文时按失败记录;直接媒体文件验收原始字节。失败后不切换格式,也不借用其他 provider 补抓;只有实际执行记录参与榜单。', -} -CAPABILITIES['lexmount-cn'] = {**CAPABILITIES['lexmount-global'], 'scope': 'POST /v1/extract · 中国区'} -for engine,url in { - 'kitesurf':'https://developers.cloudflare.com/browser-run/kitesurf/', - 'lightpanda':'https://github.com/lightpanda-io/browser', - 'obscura':'https://github.com/h4ckf0r0day/obscura', - 'chromium':'https://developer.chrome.com/docs/automation-and-testing/headless', -}.items(): - SOURCES['headless-'+engine]=(engine+' · 官方文档(2026-09-03 核对)',url) - native = engine != 'chromium' - route = ('Cloudflare 账户级 /markdown 原生输出' if engine == 'kitesurf' else - engine+' fetch --dump markdown 原生输出' if native else - 'Chromium DOM HTML + node-html-markdown 2.0.0') - CAPABILITIES[engine]={ - 'scope':route, 'sources':['headless-'+engine], - 'web':c('支持','原生 Markdown' if native else '渲染 DOM 转 Markdown'), 'document':c('未声明','本轮适配器没有 PDF / Office 解析器,不据此推断产品能力'), - 'image':c('仅链接','取决于 DOM 与转换器保留'), 'av':c('未声明','本轮没有媒体解析,不据此推断产品能力'), - 'file':c('未声明','本轮异常二进制输出仅保留证据'), 'js':c('支持','兼容范围看逐例结果'), - 'input':'给定公开 HTTP(S) URL;不登录、不点击、不滚动。', - 'structure':'统一 Markdown;不要求引擎按评分答案生成结构。', - 'access':'每次新会话;Kitesurf 使用 Cloudflare 账号版远程 Browser Run API,其余为本地进程。', - 'notes':'这里只声明本轮调用模式。没有启用文档解析、截图、下载、OCR 等模式,不能从本轮失败推断产品没有这些能力。', - 'tested':('networkidle2;45 秒客户端预算;直接使用账户级 /markdown。' if engine == 'kitesurf' else - 'CLI 自带等待;45 秒外层预算;直接使用原生 Markdown。' if native else - 'DOMContentLoaded;45 秒端到端预算;完整 DOM 经 node-html-markdown 2.0.0 转换。'), - } -for provider,version in (('moli-1.0.6','1.0.6'),('moli','1.1.5'),('moli-1.1.3','1.1.3'),('moli-1.1.11','1.1.11')): - source='headless-moli-'+version - SOURCES[source]=(f'Moli {version} · 官方发布', - f'https://github.com/lexmount/moli/releases/tag/v{version}') - CAPABILITIES[provider]={ - 'scope':'网页和文档正文使用原生 Markdown;直接媒体文件使用原始响应', - 'sources':[source], - 'web':c('支持','CLI 原生 Markdown'), - 'document':c('有限支持','HTML/TXT 可以直接返回正文;PDF/DOCX 必须由同一原生 Markdown 接口交付可读文字'), - 'image':c('仅链接','网页资源取决于 Markdown 保留;直接 URL 可验收原始字节'), - 'av':c('仅链接','网页资源取决于 Markdown 保留;直接 URL 可验收原始字节'), - 'file':c('支持','裸 fetch 返回直接 PDF、PNG、WebM、MP4 等原始响应'), - 'js':c('支持','兼容范围看逐例结果'), - 'input':'给定公开 HTTP(S) URL;不登录、不点击、不滚动。', - 'structure':'网页和文档正文输出 Markdown;直接媒体文件输出原始字节。', - 'access':'每次新进程;本地二进制,不使用托管服务凭据。', - 'notes':'正文与文件是不同交付契约。文档题要求 Moli 的原生 Markdown 接口直接交付可读正文;只返回 PDF/DOCX 字节或地址不通过,评测端不再外部解析。直接媒体文件只校验格式、长度和冻结 SHA-256,不进行内容理解。', - 'tested':'网页与文档正文使用 --dump markdown;直接媒体文件返回原始字节;本轮没有启用失败重试,端到端预算 45 秒。', - } -SOURCES['headless-moli-1.1.7']=('Moli 1.1.7 · 官方发布','https://github.com/lexmount/moli/releases/tag/v1.1.7') -CAPABILITIES['moli-1.1.7']={ - **CAPABILITIES['moli'], - 'scope':'冻结 HTML 经本地测试服务器 URL 输入,CLI 原生输出 Markdown;不重新抓取原始网站。', - 'sources':['headless-moli-1.1.7'], - 'document':c('未声明','本轮只测试 HTML 转 Markdown,未测试直接文档'), - 'file':c('未声明','本轮未测试原始文件下载'), - 'image':c('仅链接','仅保留 Markdown 中的图片链接,未测试图片下载'), - 'av':c('未声明','本轮未测试音视频下载或理解'), - 'input':'预选 200 页冻结 HTML;本地 HTTPS 地址,外部资源隔离。', - 'structure':'原生 Markdown,内部使用 moli-html2md。', - 'notes':'本条声明限于 1.1.7 的冻结 HTML 转换,不继承其他版本的文档或媒体实测结论。', - 'tested':'fetch --dump markdown --wait-until done;最终结果为准,失败重试链单独保留。', -} +CAPABILITIES = {name:item['capabilities'] for name,item in report_method_catalog()['methods'].items() + if item.get('capabilities')} +SOURCES = {key:(item['label'],item['url']) for key,item in report_method_catalog()['sources'].items()} -def task_capabilities(out, names, records): +def task_capabilities(out, names, records, *, sources=None, language="zh"): """Describe the configured native task routes, without upgrading product claims.""" + catalog = report_method_catalog() if language == "zh" else report_method_catalog(language) + sources = {key:(item["label"],item["url"]) for key,item in catalog["sources"].items()} if sources is None else sources from .report_interfaces import browser_task_interface_matrix - from .methods import native_browser_engine, browser_method_registry measured=[r for r in records if r.get('provider_metadata',{}).get('output_contract')] - if (not measured or not all(native_browser_engine(name) in {'moli','lightpanda','obscura','chromium','kitesurf'} for name in names) + if (not measured or not all(report_method(name).get('kind') == 'browser' for name in names) or not (out/'dataset.json').is_file()):return None dataset=json.loads((out/'dataset.json').read_text()) cases={case['id']:case for case in dataset['cases']} - matrix=browser_task_interface_matrix(cases,names) + matrix=browser_task_interface_matrix(cases,names,language=language) configured={} budget=dataset.get('comparison',{}).get('timeout_seconds') - budget_zh=(f'外层预算 {budget:g} 秒。' if isinstance(budget,(int,float)) and not isinstance(budget,bool) and budget>0 else '外层预算未记录。') - budget_en=(f'Outer budget: {budget:g} seconds.' if isinstance(budget,(int,float)) and not isinstance(budget,bool) and budget>0 else 'Outer budget not recorded.') + budget_zh=(f'外层预算 {budget:g} 秒。' if positive_number(budget) else '外层预算未记录。') + budget_en=(f'Outer budget: {budget:g} seconds.' if positive_number(budget) else 'Outer budget not recorded.') for name in names: - engine=native_browser_engine(name) - row={**CAPABILITIES[engine]} - row.update( - scope=matrix['capabilities'][name], - web=c('支持','原生网页正文输出' if engine!='chromium' else '原生 DOM 加共同 HTML 转换'), - document=(c('有限支持','HTML/TXT 可由原生 Markdown 返回;直接 PDF/DOCX 未提供可读正文,评测端不使用外部解析器') - if engine=='moli' else - c('共同转换','使用浏览器渲染后的完整 HTML,再由固定 HTML-to-Markdown 包转换;不读取查看器私有对象,不使用外部 PDF/DOCX 解析器') - if engine=='chromium' else - c('原生输出','直接使用浏览器正文接口;不使用外部 PDF/DOCX 解析器')), - image=c('资源地址 / 字节', - '原生 Markdown 由共同评分器识别目标资源 URL;直接文件通过原生响应取得' - if engine!='chromium' else - '完整渲染 HTML 经过共同转换,评分器从 Markdown 核对资源地址;直接文件通过原生响应取得'), - av=c('资源与元数据', - '完整渲染 HTML 经过共同转换,评分器从 Markdown 核对地址或页面信息;直接文件读取原始字节' - if engine=='chromium' else - '原生接口读取地址、页面元数据或原始字节;不做媒体理解'), - file=c('原生获取','使用该浏览器原始响应或附件接口,按固定规则验收字节'), - structure='按任务交付 Markdown、文档正文、资源地址、媒体元数据或原始文件。', - notes=('浏览器负责访问与原生产物获取;网页内嵌媒体与正文统一走原生 Markdown,共同评分器只核对目标资源 URL;只有 Chromium 使用标准 HTML 转 Markdown 包。完整分类及边界见报告解读的接口表。' - if engine!='chromium' else - 'Chromium 负责访问和渲染;所有页面类任务都取得完整 HTML,再由固定标准包转成 Markdown。评测端不运行资源提取脚本,也不读取文档查看器私有对象。完整分类及边界见报告解读的接口表。'), - tested=budget_zh+'接口按任务大类预选;具体请求参数见保存的调用记录。', - tested_en=budget_en+' Interfaces are selected by task family; exact request parameters are in the saved call records.') + registered=report_method(name,language=language) + row={**registered.get('capabilities',{})} + if not row:continue + row.update(registered.get('task_capabilities',{})) + row.update(scope=matrix['capabilities'][name], + tested=budget_zh+'接口按任务大类预选;具体请求参数见保存的调用记录。', + tested_en=budget_en+' Interfaces are selected by task family; exact request parameters are in the saved call records.') source='task-native-'+name - source_url={'moli':'https://github.com/lexmount/moli','lightpanda':'https://github.com/lightpanda-io/browser','obscura':'https://github.com/h4ckf0r0day/obscura','chromium':'https://developer.chrome.com/docs/automation-and-testing/headless','kitesurf':'https://developers.cloudflare.com/browser-run/kitesurf/'}[engine] - source_url=browser_method_registry()[name].get('source_url',source_url) - SOURCES[source]=(name+' · 官方接口',source_url) - row['sources']=[source] + source_url=report_method(name).get('source_url','') + sources[source]=(name+(' · 官方接口' if language=='zh' else ' · Official interface'),source_url) + row['sources']=[*row.get('sources',[]),source] configured[name]=row return configured +def source_version_checks(names, records): + """A dated source check applies to its bound version, not every future binary.""" + checks = report_method_catalog()['sources'] + result = {} + for name in names: + subset = [row for row in records if row.get('provider') == name] + registered = report_method(name, subset[0] if subset else None) + versions = {normalized_method_version(registered, version) if version else None + for row in subset for version in [method_version(row)]} + rows = [] + for key in registered.get('capabilities', {}).get('sources', []): + source = checks[key] + checked = source.get('checked_version') + expected = normalized_method_version(registered, checked) if checked else None + if expected and versions and None not in versions: + status = 'matched' if versions == {expected} else 'mismatch' + else: + status = 'unverified' + rows.append({'source': key, 'checked_version': checked, + 'measured_versions': sorted(v for v in versions if v is not None), + 'missing_measured_version': not versions or None in versions, + 'status': status}) + result[name] = rows + return result + + +def render_source_version_notice(rows): + checks = report_method_catalog()['sources'] + english_checks = report_method_catalog('en')['sources'] + notices = [] + for row in rows: + if row['status'] == 'matched': + continue + measured = ', '.join(row['measured_versions']) or 'not recorded' + checked = row['checked_version'] or 'not recorded' + source = checks[row['source']] + zh_status = '资料版本与实测版本不一致' if row['status'] == 'mismatch' else '资料适用版本未确认' + en_status = 'Source and measured versions differ' if row['status'] == 'mismatch' else 'Source applicability is unverified' + notices.append('
  • '+bilingual_text( + f"{zh_status}:核对版本 {checked};实测版本 {measured}。不能据此认定当前版本具备相同能力。", + f"{en_status}: checked version {checked}; measured version {measured}. This does not establish the same capability for the measured version.")+ + ' '+bilingual_text(source['label'],english_checks[row['source']]['label'])+'
  • ') + return '
      '+''.join(notices)+'
    ' if notices else '' + + def render_capabilities(out, names, *, table, cell, link, records=()): - labels={'moli-1.0.6':'Moli 1.0.6','moli':'Moli','moli-1.1.3':'Moli 1.1.3','moli-1.1.7':'Moli 1.1.7','moli-1.1.11':'Moli 1.1.11'} - from .methods import measured_method_label - labels.update({name:measured_method_label(name,labels.get(name,name)) for name in names}) - configured=task_capabilities(out,names,records) - capabilities=configured or CAPABILITIES + labels={name:browser_method_label(name) for name in names} + sources = dict(SOURCES) + configured=task_capabilities(out,names,records,sources=sources) + capabilities=configured or {name:report_method(name).get('capabilities') for name in names if report_method(name).get('capabilities')} + english_catalog=report_method_catalog('en') + english_sources={key:(item['label'],item['url']) for key,item in english_catalog['sources'].items()} + english_configured=task_capabilities(out,names,records,sources=english_sources,language='en') + english_capabilities=english_configured or {name:report_method(name,language='en').get('capabilities') for name in capabilities} unknown = sorted(set(names) - set(capabilities)) - evidence = {'checked_at': CHECKED_AT, 'scope': '官方契约/固定源码能力,不是实测成绩', - 'sources': SOURCES, 'methods': {n: capabilities[n] for n in names if n in capabilities}, 'unreviewed': unknown} + version_checks = source_version_checks(names, records) + evidence = {'source_version_checks': version_checks, 'source_checks': report_method_catalog()['sources'], 'scope': '官方契约/固定源码能力,不是实测成绩', + 'sources': sources, 'methods': {n: capabilities[n] for n in names if n in capabilities}, 'unreviewed': unknown} (out / 'adoption' / 'capabilities.json').write_text(json.dumps(evidence, ensure_ascii=False, indent=2)+'\n') content_rows, runtime_rows, details = [], [], [] for name in names: if name not in capabilities: continue row = capabilities[name] - from .report_components import bilingual_text - tested = (bilingual_text(row['tested'], row['tested_en']) if row.get('tested_en') else bilingual_text('仓库登记配置示例;实际调用以保存记录为准。', 'Repository configuration example; consult saved records for actual calls.') + escape(row['tested'])) - method = cell(link('#capability-'+name, labels.get(name,name))) + english = english_capabilities[name] + def localized(key): + return bilingual_text(row[key],english[key]) + tested = (bilingual_text(row['tested'], row['tested_en']) if row.get('tested_en') else bilingual_text('仓库登记配置示例;实际调用以保存记录为准。', 'Repository configuration example; consult saved records for actual calls.') + localized('tested')) + method = cell(link('#capability-'+name, labels.get(name,name),browser_method_label(name,language='en'))) values = [] for key in ['web', 'document', 'image', 'av', 'file']: item = row[key] - values.append(cell(''+escape(item['status'])+''+escape(item['detail'])+'')) + values.append(cell(''+bilingual_text(item['status'],english[key]['status'])+''+bilingual_text(item['detail'],english[key]['detail'])+'')) content_rows.append(('', method+''.join(values))) - runtime_rows.append(('', method+cell(escape(row['js']['status']+' · '+row['js']['detail']))+ - ''.join(cell(escape(row[k])) for k in ['input', 'structure', 'access']))) - sources = ' · '.join(link(SOURCES[s][1], SOURCES[s][0]+' ↗') for s in row['sources']) + runtime_rows.append(('', method+cell(bilingual_text(row['js']['status']+' · '+row['js']['detail'],english['js']['status']+' · '+english['js']['detail']))+ + ''.join(cell(localized(k)) for k in ['input', 'structure', 'access']))) + source_links = ' · '.join(link(sources[key][1], sources[key][0]+' ↗',english_sources[key][0]+' ↗') for key in row['sources']) details.append(f'
    {escape(labels.get(name,name))} · 范围与官方依据'+ - f'

    对应接口:{escape(row["scope"])}

    {escape(row["notes"])}

    '+ - f'

    登记配置:{tested}

    {sources}

    ') - legend = ('

    原有资料核对日期:'+CHECKED_AT+';本版新增 Cloudflare 与 Firecrawl 接口核对日期:'+ROUTE_CHECKED_AT+'。托管服务按官方文档;内置工具按固定源码。此处说明可选能力,不是实测成绩;没有启用的选项不补算进榜单。

    ' - '
    支持 / 需配置
    有对应实现或契约;需配置表示要启用额外模式。
    ' - '
    仅链接 / 有限支持
    仅返回地址,或只覆盖指定输入、正文区域。
    ' - '
    无此能力 / 未声明
    前者有明确实现边界;后者是官方资料不足,不能当作不支持。
    ') + f'

    对应接口:{localized("scope")}

    {localized("notes")}

    '+ + f'

    登记配置:{tested}

    {source_links}

    '+render_source_version_notice(version_checks[name])+'') + checks = report_method_catalog()['sources'] + used = sorted({source for row in capabilities.values() for source in row.get('sources',[])}) + dates = ' · '.join(link(checks[key]['url'], checks[key]['label']+' · '+checks[key]['checked_at'],english_catalog['sources'][key]['label']+' · '+checks[key]['checked_at']) + for key in used if key in checks) + legend = '

    '+bilingual_text('官方能力资料不是实测成绩。官方来源及资料核对日期:', 'Official capability documentation is not a measured result. Sources and verification dates: ')+dates+'

    ' if configured: - legend='

    下表说明本报告配置的接口与处理边界,浏览器版本及固定来源见“环境与复现”。共同解析、资源读取脚本和 HTML 转换由评测提供,不计作浏览器内置能力。接口可用不代表每题都能完成;实测结果见结果分析。

    ' + legend='

    '+bilingual_text('下表说明本报告配置的接口与处理边界,浏览器版本及固定来源见“环境与复现”。共同解析、资源读取脚本和 HTML 转换由评测提供,不计作浏览器内置能力。接口可用不代表每题都能完成;实测结果见结果分析。', 'The table describes the configured interfaces and processing boundaries. See Methodology and Reproduction for browser versions and pinned sources. Shared parsing, resource-reading scripts and HTML conversion are supplied by the benchmark, not counted as built-in browser capabilities. An available interface does not guarantee task completion; see result analysis for measured outcomes.')+'

    ' content = ('

    能力声明不是实测任务成绩;任务通过按登记的稳定内容锚点与字段结构等规则判定,不证明频繁变化值的事实准确性或实时性。文档列指 PDF / Office 中的文字;普通 TXT 不等于文档解析。图片、音视频列说明网页资源取得方式,不代表视觉理解或转写;直接媒体 URL 能否取得字节看原文件列。原文件不是页面正文、截图或下载链接本身。

    '+ table(['方法 / 依据', '静态网站正文', '文档文字', '图片资源', '音视频资源', '原文件'], content_rows, cls='capability-matrix', metrics=[])) runtime = table(['方法 / 依据', 'JS 渲染', '输入范围', '结构化输出', '访问与会话'], runtime_rows, cls='capability-runtime', metrics=[]) diff --git a/harness/web_fetch/report_check.py b/harness/web_fetch/report_check.py index 7effbde07..92a7b36d1 100644 --- a/harness/web_fetch/report_check.py +++ b/harness/web_fetch/report_check.py @@ -11,7 +11,6 @@ from harness.record_io import load_jsonl from harness.report_languages import check_language_contract, generated_languages, visible_numbers -from harness.reporting.article_assets import ArticleAssetParity from harness.reporting.presentation import check_presentation from harness.web_fetch.reference_lifecycle import validate_resource_source_evidence from harness.web_fetch.reliability import require_reliable @@ -157,7 +156,7 @@ def handle_data(self, text): class PageShape(HTMLParser): - def __init__(self, page=None, assets=None): + def __init__(self, page=None): super().__init__() self.tags = Counter() self.ids = set() @@ -166,16 +165,10 @@ def __init__(self, page=None, assets=None): self.skip = 0 self.numbers = [] self.page = page - self.assets = assets - self.figures = [] self.images = [] self.english_images = [] def handle_starttag(self, tag, attrs): - if tag == "figure": - self.figures.append(dict(attrs).get("id")) - if self.assets and self.figures: - attrs = self.assets.rewrite(self.page, self.figures[-1], tag, attrs) values = dict(attrs) # This script selects the primary language at the shared entry point; # it is not report content and intentionally exists only on that page. @@ -203,8 +196,6 @@ def handle_starttag(self, tag, attrs): self.skip += 1 def handle_endtag(self, tag): - if tag == "figure" and self.figures: - self.figures.pop() if tag in {"pre", "code", "script", "style"}: self.skip = max(0, self.skip - 1) @@ -568,19 +559,13 @@ def canonical_link(page, value): ) bilingual_issues = [] - asset_manifest = repository / "research/editorial-figures.json" - article_assets = ( - ArticleAssetParity(root, asset_manifest) - if has_english and asset_manifest.exists() and (root / "assets/editorial").exists() - else None - ) if has_english: for relative in sorted(zh_pages): zh_path = root / relative en_path = root / "en" / relative - zh = PageShape(zh_path, article_assets) + zh = PageShape(zh_path) zh.feed(zh_path.read_text()) - en = PageShape(en_path, article_assets) + en = PageShape(en_path) en.feed(en_path.read_text()) if zh.tags != en.tags: bilingual_issues.append((relative, "semantic tag counts differ")) diff --git a/harness/web_fetch/report_explainer.py b/harness/web_fetch/report_explainer.py index ad5043c2a..368e0690f 100644 --- a/harness/web_fetch/report_explainer.py +++ b/harness/web_fetch/report_explainer.py @@ -12,6 +12,7 @@ from harness.record_io import canonical_json_sha256 from .report_resources import render_resource_comparison +from .report_components import bilingual_text from lexbench_webfetch.metrics import task_passed, percentile from .methods import with_measured_method_labels from .report_style import STYLE @@ -29,7 +30,7 @@ def _mean(values): def _table(headers, rows, caption): headers = ['序号'] + list(headers) rows = [(str(index), *row) for index, row in enumerate(rows, 1)] - head = ''.join(f'{escape(x)}' for x in headers) + head = ''.join(''+(bilingual_text(*x) if isinstance(x,tuple) else escape(x))+'' for x in headers) body = ''.join('' + ''.join(f'{x}' for x in row) + '' for row in rows) return ('

    表格可横向滚动查看全部列。

    ' '
    ' @@ -340,7 +341,8 @@ def distribution(rows): examples.append({'row': row, 'explanation': item['explanation'], 'evidence': browser_example_evidence(out,cases[row['case_id']],row,focus=item.get('focus'),reference_focus=item.get('reference_focus'))}) stats = {} - from .report_interfaces import method_provenance, method_version + from .report_interfaces import method_provenance + from .methods import method_version provenance = method_provenance(out) for name in providers: rows = [indexed[name, cid] for cid in cases] @@ -524,11 +526,13 @@ def _render_current_browser_article(out, cases, records, binding): from .methods import browser_method_label, native_browser_engine engine_count = len({native_browser_engine(name) or name for name in names}) def label(name): - return escape(browser_method_label(name) if browser_method_label(name) != name else name.title()) + return bilingual_text(browser_method_label(name) if browser_method_label(name) != name else name.title(), + browser_method_label(name,language='en') if browser_method_label(name,language='en') != name else name.title()) def paragraph(value): return '

    ' + escape(value).replace('\n\n', '

    ') + '

    ' - def section(title, body, anchor): - return '

    '+escape(title)+'

    '+body+'
    ' + def section(title, body, anchor, english_title=None): + heading = escape(title) if english_title is None else bilingual_text(title,english_title) + return '

    '+heading+'

    '+body+'
    ' def metric(name, key, value, values, higher=False, suffix=''): valid = [v for v in values if v is not None] extreme = (max(valid) if higher else min(valid)) if valid else None @@ -557,16 +561,21 @@ def family_cell(name, track): return f"{group['passed']}/{group['n']}
    " + metric(name,'family_'+track,rate,values,True) from .report_interfaces import browser_task_interface_matrix interfaces = browser_task_interface_matrix(cases, names, records, compact=True) + english_interfaces = browser_task_interface_matrix(cases, names, records, compact=True, language='en') + english_routes = {(name,row['track']):row['cells'][name] for row in english_interfaces['rows'] for name in names} route_rows = [] for row in interfaces['rows']: cells = [] for name in names: parts = row['cells'][name] + english_parts = english_routes[name,row['track']] + if [(p['mode'],p['count']) for p in parts] != [(p['mode'],p['count']) for p in english_parts]: + raise ValueError('Interface route populations differ between languages') cells.append('
    ' - + '

    '.join('' - +str(part['count'])+' 题:'+escape(part['description'])+'' for part in parts)+'
    ') + + '

    '.join('' + +str(part['count'])+' 题:'+escape(part['description'])+'' for part,english_part in zip(parts,english_parts))+'
    ') route_rows.append((escape(BROWSER_FAMILY_NAMES[row['track']])+f" · {row['count']} 题", *cells)) - matrix = _table(['任务类型',*(label(name) for name in names)], route_rows, '每格为实际请求的接口路线与目标交付物;调用失败或能力不足仍保留原路线,不代表已经成功交付。') + matrix = _table(['任务类型',*((browser_method_label(name),browser_method_label(name,language='en')) for name in names)], route_rows, '每格为实际请求的接口路线与目标交付物;调用失败或能力不足仍保留原路线,不代表已经成功交付。') network_rows = _browser_network_rows(records, names) network_note = '
    网络执行位置与解析方式'+''.join( '

    '+matrix+'

    ' +'

    Moli 使用 fetch --dump markdown;动态正文整类启用 --layout 与 --with-frames。Lightpanda 使用 --dump markdown,JSON 外壳只用于读取同一次返回的 Markdown;Obscura 执行固定版本的官方 Markdown 脚本;Kitesurf 使用 Cloudflare Browser Run 的 /markdown 接口。完整命令、转换器版本和各题原始调用见评测方法及方法介绍。接口缺失的记录在表中单列,不推定它使用了什么路线。

    ' +_table(['方法','原生等待条件及题数'],wait_rows,'等待条件来自保存的调用记录;文件字节路线不等待查看器绘制。') - +'

    单次物理调用预算为 45 秒,包含启动、访问、等待和转换。传输或 HTTP 临时错误允许一次重试,整题总耗时会累加两次调用;评分在计时结束后执行。最终网址只用于诊断,不参与正文判分。评测端没有追加 PDF 或 DOCX 解析器。

    '+network_note,'interfaces') + +'

    '+bilingual_text('调用预算和重试上限见评测方法中的执行配置。耗时包含启动、访问、等待、转换及实际发生的重试;评分在计时结束后执行。最终网址只用于诊断,不参与正文判分。评测端没有追加 PDF 或 DOCX 解析器。','Call budgets and retry limits are listed in the execution configuration on the methodology page. Timing includes startup, access, waiting, conversion and any recorded retries; scoring runs after timing ends. The final URL is diagnostic and does not affect body-text scoring. The harness adds no PDF or DOCX parser.')+'

    '+network_note,'interfaces') flow = 'flowchart LR\n A[同一目标网址与验收规则] --> B[各浏览器原生访问与提取]\n B --> C[保留完整输出与调用日志]\n C --> D[统一评分]\n D --> E[整体与专项结果]' fallback = ' → '.join(''+x+'' for x in ('目标网址与规则','浏览器原生访问','完整输出与日志','统一评分','整体与专项')) mechanism = '
    '+fallback+'
    ' @@ -595,7 +604,7 @@ def family_cell(name, track): examples += _table(['检查项目','题目要求','实际结果','判定'],[(escape(check['label']),escape(check['expected']),escape(check['actual']),''+escape(check['verdict'])+'') for check in evidence['checks']], '检查值来自本次保存的统一评分结果;完整输出和逐项证据见结果详情。') examples += '

    任务与验收规则 · 实际返回与判定证据

    ' body += section('同一任务怎样验收', examples, 'examples') - body += section('全部调用的耗时', '

    P50 是中位数,P95 表示 95% 任务的累计耗时不超过该值。每行均为全部任务,包含成功、失败与自动重试的累计耗时。45 秒是单次调用预算,允许一次重试的任务可能接近 90 秒。评分时间不计入。调用分布在不同日期,不能把耗时差异直接归因于浏览器引擎或新版改进;耗时较短也不等于任务通过率更高。

    '+latency_table('all'), 'all-latency') + body += section('全部调用的耗时', '

    P50 是中位数,P95 表示 95% 任务的累计耗时不超过该值。每行均为全部任务,包含成功、失败与自动重试的累计耗时。'+bilingual_text('发生重试时,累计耗时包含此前调用及重试等待;单次调用预算不等于整题总耗时。','When retries occur, total latency includes prior calls and retry waits; a per-call budget is not the total task duration.')+'评分时间不计入。调用分布在不同日期,不能把耗时差异直接归因于浏览器引擎或新版改进;耗时较短也不等于任务通过率更高。

    '+latency_table('all'), 'all-latency') timing_rows=[(label(name),str(stats[name]['physical_calls']), *(metric(name,key,stats[name][key],[stats[n][key] for n in names]) for key in ('final_call_mean_ms','prior_call_mean_ms'))) for name in names] body += section('重试与原生等待怎样影响耗时', @@ -661,7 +670,8 @@ def reviewed_distribution(items, provider): example += '

    原始页面 · 任务与验收规则 · 完整结果、评分与日志

    ' example += '

    参考范围或验收要求(节选)

    '+escape(reference)+'

    实际返回(节选)

    '+escape(raw)+'

    程序返回或验收结果

    '+escape(error)+'
    ' body += section((browser_method_label(name) if browser_method_label(name) != name else name.title())+':'+str(stat['passed'])+'/'+str(model['cases'])+' 题通过', - '
    '+paragraph(chapter['explanation'])+distribution+example+'
    ', 'browser-'+name) + '
    '+paragraph(chapter['explanation'])+distribution+example+'
    ', 'browser-'+name, + browser_method_label(name,language='en')+': '+str(stat['passed'])+'/'+str(model['cases'])+' tasks passed') review_path = contained_path(out,'result-review.json') review = json.loads(review_path.read_text()) if review_path.is_file() else None body += section('结论与审查范围', paragraph(content['conclusions'])+ @@ -738,7 +748,7 @@ def append_html(parent, markup): latency=parts['all-latency'];latency_table_markup=''.join(str(node) for node in latency.contents if getattr(node,'name',None) not in {'h2','p'}) latency.clear();append_html(latency,'

    耗时的差距:等待策略与长尾

    以下每行使用全部 '+str(model['cases'])+' 题,包含成功、失败和重试。P50 是中位数,P95 是 95% 任务耗时不超过的值。调用来自不同日期,这些数值描述本轮交付路径,不能分离出纯浏览器内核的速度。

    全部任务的总耗时 · 秒

    '+''.join(columns)+'
    从进程启动到交付结果,包含访问、原生等待、转换及该题重试;评分时间不计入。
    '+paragraph(content.get('timing_explanation','不同方法的原生完成条件不同;等待成本需要和内容交付一同判断。'))+'
    精确耗时数值与样本范围'+latency_table_markup+'
    ') retry=parts['timing-boundary'];retry_tables=''.join(str(node) for node in retry.contents if getattr(node,'name',None) not in {'h2','p'}) - retry.clear();retry.name='details';retry['class']='m-details';append_html(retry,'重试增加了多少耗时

    45 秒是单次物理调用预算。允许一次重试时,整题累计耗时可能接近 90 秒;下表两项平均耗时相加等于平均总耗时。

    '+retry_tables) + retry.clear();retry.name='details';retry['class']='m-details';append_html(retry,'重试增加了多少耗时

    '+bilingual_text('重试上限以执行配置为准,实际调用次数见下表。末次调用与此前调用及重试等待的平均耗时相加,等于平均总耗时。','The retry limit follows the execution configuration; the table shows actual call counts. Mean final-call latency plus mean prior-call and retry-wait time equals mean total latency.')+'

    '+retry_tables) successful=parts['successful-latency'];heading=successful.find('h2');heading.name='summary';successful.name='details';successful['class']='m-details' # Reuse the case workbench: frozen reference, exact output and checks stay together. example_section=parts.get('examples') @@ -773,7 +783,7 @@ def append_html(parent, markup): panels.append(str(node.extract()));tab=BROWSER_FAMILY_NAMES[report_family(case)] tabs.append('') radios.append('') - example_section.clear();append_html(example_section,'

    各类任务的真实输入与输出

    每组一道实测,左右对照题目要求和原始返回,下面保留实际判定。示例用于解释验收边界,不代表各类失败的比例。

    每题只记通过或失败相同网址与要求单次预算 45 秒失败计入总题数
    '+''.join(radios)+'
    '+''.join(tabs)+'
    '+''.join(panels)+'
    ') + example_section.clear();append_html(example_section,'

    各类任务的真实输入与输出

    每组一道实测,左右对照题目要求和原始返回,下面保留实际判定。示例用于解释验收边界,不代表各类失败的比例。

    每题只记通过或失败相同网址与要求'+bilingual_text('预算见执行配置','See execution configuration for budgets')+'失败计入总题数
    '+''.join(radios)+'
    '+''.join(tabs)+'
    '+''.join(panels)+'
    ') # Charts explain frequency; the exact tables and canonical result links remain inspectable. for provider,node in [('all',parts.get('failure-summary'))]+[(name,parts['browser-'+name]) for name in names]: if node is None:continue diff --git a/harness/web_fetch/report_i18n.py b/harness/web_fetch/report_i18n.py index 81bb0c753..cc940fea0 100644 --- a/harness/web_fetch/report_i18n.py +++ b/harness/web_fetch/report_i18n.py @@ -37,36 +37,19 @@ # Exact editorial translations take priority. Dynamic evidence (task names, # references and provider output) is deliberately absent from this catalog. TEXT = { - "仓库内测试适配器;只用于确定性回归测试。": "Repository test adapter; used only for deterministic regression tests.", - "不需要账号或环境变量。": "No account or environment variables required.", "先检查请求与返回内容,再逐项检查本例声明的稳定文字、正文覆盖与组合规则;诊断文字只显示命中情况。": "Check the request and returned content, then apply the declared stable text, body coverage and composite requirements. Diagnostic text reports matches without changing the verdict.", "本轮所有本地方法的每个主机名和跳转都使用固定的 Cloudflare HTTPS DNS,且不回退到系统 DNS。Moli、Lightpanda 和 Obscura 通过进程内解析器取得同一组公网地址;Chromium 使用自身的严格安全 DNS 配置。各浏览器原有的私网访问保护保持不变。": "Every local method uses the fixed Cloudflare HTTPS DNS service for each hostname and redirect, with no fallback to system DNS. Moli, Lightpanda, and Obscura receive the same public addresses through a process-scoped resolver; Chromium uses its own strict secure-DNS profile. Each browser keeps its existing private-network protection.", "Kitesurf 在 Cloudflare Browser Run 中执行,使用云端网络。它的任务结果可以按同一验收规则比较,但耗时包含不同的部署位置和公网路径,不能据此判断本地浏览器引擎谁更快。": "Kitesurf runs in Cloudflare Browser Run and uses its cloud network. Its task outcomes use the same acceptance rules, but its latency includes a different deployment location and public-network path and therefore cannot rank local browser-engine speed.", - "CLI · 原生 Markdown 或原始响应路线": "CLI · native Markdown or raw-response route", - "直接取得 Moli 原生 Markdown;直接 PDF/DOCX 未提供可读正文时失败": "Returns Moli's native Markdown directly; a direct PDF or DOCX fails when the interface does not deliver readable body text", "让同一次原生 Markdown 调用包含已加载子文档;不切换输出格式": "Includes loaded child documents in the same native Markdown call without switching output format", - "网页正文、文档正文、结构题和网页内嵌媒体题统一使用 Moli 原生 Markdown。动态网站正文整类统一启用 --layout、--with-frames 和 domstable,不按题目设置 selector。网页内嵌媒体只在原生输出中核对题目指定的资源地址,不下载媒体,也不切换 HTML、JSON 或网络跟踪接口。直接 PDF/DOCX 没有交付可读正文时按失败记录,评测端不下载后解析。直接媒体文件使用原生 CDP 响应或同一二进制的原始附件接口。最终网址只作诊断,不决定正文是否通过;失败后不切换格式或借用其他 provider。": "Moli uses native Markdown for webpage body text, document body text, structured tasks, and media information from webpages. Every dynamic-body task uses --layout, --with-frames, and domstable; no task-specific selector is supplied. Media-information tasks check only the requested resource URLs in native output; they do not download the media or switch to HTML, JSON, or network tracing. A direct PDF or DOCX fails when the interface does not deliver readable body text; the benchmark does not download and parse it. Direct media files use native CDP responses or the same binary's raw attachment interface. The final URL is diagnostic and does not decide body-task acceptance. A failure never triggers a format switch or another provider.", - "moli fetch;网页、文档正文、结构和网页内嵌媒体使用原生 Markdown,预声明完成条件只调用官方等待参数,直接媒体文件使用原始响应": "moli fetch; webpage body text, document body text, structured tasks, and webpage media information use native Markdown; preregistered completion conditions only call official wait parameters; direct media files use raw responses", - "网页正文、文档正文、结构和网页内嵌媒体任务统一使用 Moli 原生 Markdown;动态网站正文整类统一启用 --layout、--with-frames 和 domstable,不使用逐题 selector。网页媒体只核对同一 Markdown 输出里的目标地址,不下载媒体。直接 PDF/DOCX 未提供可读正文时按失败记录;直接媒体文件验收原始字节。失败后不切换格式,也不借用其他 provider 补抓;只有实际执行记录参与榜单。": "Webpage body text, document body text, structured tasks, and webpage media information all use Moli's native Markdown. Every dynamic-body task uses --layout, --with-frames, and domstable; no task-specific selector is supplied. Webpage media tasks check target URLs in the same Markdown output and do not download the media. A direct PDF or DOCX fails when it does not provide readable body text; direct media files validate raw bytes. A failure does not switch formats or fetch through another provider. Only measured records enter the ranking.", - "动态网站正文整类;网页内嵌媒体任务使用 --with-frames": "Every dynamic-body task; webpage media-information tasks use --with-frames", - "动态正文统一启用 Moli 页面布局能力,并在同一次原生 Markdown 调用中包含已加载子文档;不按题目设置 selector": "Dynamic-body tasks uniformly enable Moli's page-layout capability and include loaded child documents in the same native Markdown call; no task-specific selector is supplied", - "网页正文、文档正文、结构和网页内嵌媒体统一使用原生 Markdown;动态网站正文整类统一启用 --layout、--with-frames 和 domstable;网页媒体只核对输出中的目标地址。最终网址不参与正文判分。": "Webpage body text, document body text, structured tasks, and webpage media information use native Markdown. Every dynamic-body task uses --layout, --with-frames, and domstable. Media-information tasks only check target URLs in the output. The final URL does not affect body-task acceptance.", "CLI fetch --dump markdown → 内置转换 → Markdown;动态正文整类统一启用 --layout 和 --with-frames,不使用逐题 selector": "CLI fetch --dump markdown → built-in conversion → Markdown; every dynamic-body task uses --layout and --with-frames, with no task-specific selector", - "原生 Markdown 中保留的图片地址;直接文件可验收原始字节": "Image URLs retained in native Markdown; direct files can validate raw bytes", - "原生 Markdown 中保留的媒体地址;直接文件可验收原始字节": "Media URLs retained in native Markdown; direct files can validate raw bytes", "按题目要求预先选定参评浏览器的官方原生接口。网页正文和文档正文都必须以可读 Markdown 交付;除 Chromium 使用固定的 HTML-to-Markdown 包外,不会改取 HTML、JSON DOM 或 PDF/DOCX 字节后替选手提取正文。网页内嵌媒体题只检查原生输出中的指定图片、视频或条目地址,媒体文件不下载。只有直接媒体文件题返回原始字节,并校验类型、文件头、长度和 SHA-256;这是二进制文件交付,不冒充 Markdown。下表按固定任务输入列出处理链;失败后不切换接口或引擎。": "Each task is assigned an official native interface before execution. Webpage and document body tasks must deliver readable Markdown. Chromium is the sole exception: it uses the fixed HTML-to-Markdown package because it has no native Markdown output. The benchmark does not take HTML, a JSON DOM, or PDF or DOCX bytes and extract body text on a participant's behalf. Webpage media-information tasks check only the required image, video, or entry URL in native output and do not download the media. Direct-media-file tasks alone return original bytes, which are checked for type, signature, length, and SHA-256. That binary deliverable is not presented as Markdown. The table lists the route fixed for each task input; a failure never switches interface or engine.", - "外层统一为 45 秒。内部参数以冻结的调用记录为准:显式模式留出收尾和清理时间;uniform 模式沿用产品默认值": "The outer deadline is always 45 seconds. Internal parameters follow the frozen call record: explicit mode reserves time for finalization and cleanup; uniform mode keeps the product defaults.", "这批调用未传入 Moli 内部超时参数。": "These calls did not pass Moli internal-timeout arguments.", "这批调用使用保存记录中的显式 Moli 超时,未写成产品默认值。": "These calls used the explicit Moli timeouts preserved in their records; the report does not describe them as product defaults.", "保存证据未提供可核对的 Moli 内部超时策略。": "The saved evidence does not expose a verifiable Moli internal-timeout policy.", - "网页和文档正文使用原生 Markdown;直接媒体文件使用原始响应": "Native Markdown for webpage and document body text; raw responses for direct media files", - "HTML/TXT 可以直接返回正文;PDF/DOCX 必须由同一原生 Markdown 接口交付可读文字": "HTML and TXT can return body text directly; PDF and DOCX must deliver readable text through the same native Markdown interface", - "网页和文档正文输出 Markdown;直接媒体文件输出原始字节。": "Webpage and document body tasks output Markdown; direct media files output original bytes.", "CLI · 按任务预选原生 Markdown、HTML 或原始响应路线": "CLI · preselects native Markdown, HTML, or raw-response routes by task", "网页内嵌媒体": "Embedded webpage media", "直接媒体文件": "Direct media files", "直接取得 Moli 原生 HTML;共同评分器只识别其中的资源 URL": "Returns Moli's native HTML directly; the shared scorer only identifies resource URLs in it", - "使用 Moli 官方等待能力;只等待,不抽取或拼接正文": "Uses Moli's official wait capability for readiness only; it neither extracts nor assembles body text", "所有正文与结构任务使用 Moli 原生 Markdown。旧版 selector 合同只映射到官方等待参数,不参与正文抽取。图片、视频资源页使用原生 HTML,共同评分器只识别其中的资源 URL。直接 PDF/DOCX 仍调用同一原生接口;接口没有交付可读正文时按失败记录,评测端不下载后解析。文件使用原生 CDP 响应或同一二进制的原始附件接口。最终网址仅作诊断,不决定正文是否通过;失败后不切换格式或借用其他 provider。": "All body and structured tasks use Moli's native Markdown. Legacy selector contracts map only to official wait parameters and do not extract body text. Image and video resource pages use native HTML, from which the shared scorer only identifies resource URLs. Direct PDF and DOCX inputs still use the same native interface; if it does not deliver readable text, the task fails and the benchmark does not download and parse the file. File tasks use native CDP responses or the same binary's raw attachment interface. The final URL is diagnostic only and does not determine body-task acceptance. A failure never triggers a format switch or another provider.", "moli fetch;正文使用原生 Markdown,资源页使用原生 HTML,预声明完成条件只调用官方等待参数,直接文件使用原始响应": "moli fetch; body tasks use native Markdown, resource pages use native HTML, preregistered completion conditions only call official wait parameters, and direct files use raw responses", "资源页原生 HTML 中的图片地址": "Image URLs in native HTML from the resource page", @@ -188,10 +171,8 @@ "题目与计时规则": "Tasks and timing rules", "配置、版本和复现说明": "Configuration, versions, and reproduction", "本轮固定原生配置": "Fixed native configuration", - "每种方法内部串行执行,每题只调用一次,45秒为外层交卷期限。内部加载、连接和处理时间保留产品默认值。允许测试前固定的原生选项,不传入逐题等待、选择器、数量提示或脚本;不补抓、不额外重试。产品返回的正文和原始响应分别保存。": "Each method runs its cases serially, with one call per case and a 45-second caller deadline. Internal loading, connection, and processing timers retain product defaults. Native options may be fixed before testing; no per-case waits, selectors, count hints, or scripts are supplied. No supplemental fetches or caller retries are made. Returned text and raw responses are saved separately.", "每种方法内部串行执行;方法之间的调度以执行批次和保存的调用时间为准。分段执行保留每个原始分段,完整调用时间见执行批次。": "Each method runs serially internally. Execution batches and saved timestamps document scheduling between methods. Segmented executions retain every original segment; see the execution batches for full call times.", "每种方法内部串行;方法之间的调度见执行批次。": "Each method runs serially internally; see execution batches for scheduling between methods.", - "本轮统一使用固定原生配置:每题调用一次,外层截止时间 45 秒,产品内部超时保留默认值。具体选项及请求字段见接口示例、冻结配置和该次调用记录。": "This run uses fixed native configurations: one call per case, a 45-second caller deadline, and default product timers. See interface examples, frozen configurations, and individual call records for options and request fields.", "本轮保存的调用字段": "Saved request fields from this run", "此处展示该次公开调用记录保存的请求字段;完整配置和日志见环境与调用详情。产品内部超时以冻结配置和实际请求为准。": "These request fields come from this public call record. See environment and call details for full configurations and logs. Frozen configurations and actual requests determine product timer settings.", "已测方法的输入展示保存的公开调用字段,输出展示同一次调用的实际节选。完整配置、原始响应和日志见环境与调用详情。未参评方法保留仓库登记示例。": "For measured methods, inputs show saved public request fields and outputs show excerpts from the same call. See environment and call details for full configurations, raw responses, and logs. Unmeasured methods retain registered repository examples.", @@ -208,7 +189,6 @@ '记录的版本': 'Recorded version', '记录的提取方式': 'Recorded extractor', '记录的输出契约': 'Recorded output contract', - '费用信息': 'Cost information', '本轮结果未绑定价格资料;费用取决于调用方配置与服务账单。': 'This run has no bound pricing information; costs depend on the caller’s configuration and service billing.', '本报告的方法由调用方的显式配置提供。': 'The caller’s explicit configuration supplies the methods in this report.', '方法名称': 'Method name', @@ -244,11 +224,8 @@ '本报告由本地测试适配器生成。没有正式评测发布记录。': 'This report was generated by a local test adapter and has no formal benchmark release history.', "Lightpanda、Obscura 与 Moli 的普通网页使用 CLI 正文输出。Moli 的直接文档和媒体 URL 使用裸 fetch:正文任务在预算内解析,文件任务校验字节。": "Lightpanda, Obscura and Moli use CLI body output for ordinary web pages. Moli uses bare fetch for direct document and media URLs: body parsing stays within the time budget and file tasks validate bytes.", - "实际版本、配置和调用路线见“环境与复现”及逐次调用记录。": "See Environment & reproduction and the individual call records for measured versions, settings and execution routes.", "保留同一次调用的原生 Markdown 和最终页面地址。Lightpanda 解包原生 JSON;Obscura 执行固定哈希的官方 Markdown 脚本,并读取当前地址;页面完成条件不变。": "Retain native Markdown and the final page URL from the same call. Lightpanda unwraps its native JSON; Obscura evaluates the hash-pinned upstream Markdown script and reads the current URL, preserving the page completion conditions.", - "安装与本报告版本和 SHA-256 一致的浏览器,配置 CHROMIUM_BINARY;详见“环境与复现”。": "Install the browser matching this report's version and SHA-256, and configure CHROMIUM_BINARY; see Environment & reproduction.", "登记配置:": "Registered configuration:", - "安装与本报告版本和 SHA-256 一致的二进制,配置 MOLI_BINARY;详见“环境与复现”。": "Install the binary matching this report's version and SHA-256, and configure MOLI_BINARY; see Environment & reproduction.", "只有 Kitesurf 的账户级 Cloudflare Browser Run 路线需要供应商账号与凭据;其余方法是本地二进制,不需要账号,但仍需安装并明确指定可执行文件。": "Only the account-level Kitesurf Cloudflare Browser Run route requires a provider account and credentials; the other methods use local binaries that must be installed and explicitly selected.", "查看各方法执行结果 →": "View execution results for all methods →", "浏览器版本与二进制来源": "Browser versions and binary sources", @@ -256,12 +233,7 @@ "本报告的任务范围与验收规则": "Task scope and acceptance rules", "查看全部任务及验收规则": "View all tasks and acceptance rules", "查看结果、实际返回及失败证据": "View results, actual outputs, and failure evidence", - "安装并固定官方 v1.1.11 二进制;不需要供应商账号或 API Key。": "Install and pin the official v1.1.11 binary; no provider account or API key is required.", - "本地 CLI。": "Local CLI.", "普通网页使用 fetch --dump markdown;本轮风险网站评测固定 debug 日志并逐次完整留存。": "Ordinary web pages use fetch --dump markdown; this risk-control benchmark pins debug logging and retains the complete log for every call.", - "从官方 v1.1.11 release 获取二进制,配置 MOLI_1_1_11_BINARY。": "Obtain the binary from the official v1.1.11 release and configure MOLI_1_1_11_BINARY.", - "Moli 1.1.11 正式发布": "Moli 1.1.11 official release", - "本方法只使用官方 1.1.11 发行二进制,与其他 Moli 版本分开计分。普通网页使用原生 Markdown;直接文档与媒体的裸 fetch 路线在正式二进制上实测确认。正文任务若取得受支持的原始文档,会在同一计时预算内解析文字;文件任务直接验收字节。不在失败后切换引擎或输出模式。": "This method uses only the official 1.1.11 release binary and is scored separately from other Moli versions. Ordinary web pages use native Markdown; bare-fetch routes for direct documents and media were verified on the release binary. If a body-text task retrieves a supported original document, text is parsed within the same timing budget; file tasks validate bytes directly. The method does not switch engines or output modes after a failure.", "从官方 v1.1.5 release 获取二进制,配置 MOLI_BINARY;不是 Lexmount 云服务接口。": "Obtain the binary from the official v1.1.5 release and configure MOLI_BINARY; this is not the Lexmount cloud-service interface.", "Moli 1.1.5 正式发布": "Moli 1.1.5 official release", "本方法只使用官方 1.1.5 发行二进制,与其他 Moli 版本分开计分。普通网页使用原生 Markdown;直接文档与媒体的裸 fetch 路线在正式二进制上实测确认。正文任务若取得受支持的原始文档,会在同一计时预算内解析文字;文件任务直接验收字节。不在失败后切换引擎或输出模式。": "This method uses only the official 1.1.5 release binary and is scored separately from other Moli versions. Ordinary web pages use native Markdown; bare-fetch routes for direct documents and media were verified on the release binary. If a body-text task retrieves a supported original document, text is parsed within the same timing budget; file tasks validate bytes directly. The method does not switch engines or output modes after a failure.", @@ -287,15 +259,10 @@ "按独立保存的目标页面证据登记稳定文字、完整正文分区,以及名称—链接、路径、字段、去重和数量要求。稳定文字会明确标为必需或诊断。": "Use independently saved target-page evidence to register stable text, complete body regions, and name-link, path, field, deduplication, and quantity requirements. Stable text is explicitly marked as required or diagnostic.", "先排除请求失败、空返回、验证或拦截页面和页面外壳,再逐项检查本例声明的稳定文字、正文覆盖与组合规则;诊断文字只显示命中情况。": "First exclude request failures, empty responses, verification or blocked pages, and page shells. Then check the case's declared stable text, body coverage, and composite rules; diagnostic text only reports whether it matched.", "正文与列表规则及标为必需的稳定文字均通过;诊断文字不影响结论。文章只返回标题或开头、列表数量不足、链接不符合真实路径,均不通过。": "Body and list rules and stable text marked as required must pass; diagnostic text does not affect the verdict. An article with only a title or opening, a list below its quantity threshold, or links that do not match real paths fails.", - "每例启动一个独立进程;由 Moli 的默认 done 完成方式决定何时交付内容。": "Each case starts a separate process; Moli's default done completion mode decides when to deliver the content.", - "每例启动一个独立进程;动态网站正文统一等待 Moli 原生 DOM 稳定信号,并启用原生页面布局和已加载子文档。": "Each case starts a separate process. Every dynamic-body task waits for Moli's native DOM-stability signal and enables native page layout and loaded child documents.", "动态网站正文整类使用 CLI 原生 domstable": "The complete dynamic-page body family uses the CLI's native domstable mode", - "本轮 {record_count} 个动态网站正文任务统一使用 Moli 原生 Markdown、--layout、--with-frames 和 domstable。除目标 URL 外,没有逐题等待元素、延迟、提取脚本或特殊参数,也没有启用理解模型、格式回退或付费后备方法。": "All {record_count} dynamic-body tasks use Moli's native Markdown, --layout, --with-frames, and domstable mode. Apart from the target URL, there are no per-task element waits, delays, extraction scripts, or special parameters. No interpretation model, format fallback, or paid backup method is enabled.", "本轮100例统一使用 Moli 原生 Markdown Fetch 与默认 done 完成方式;除目标 URL 外没有逐例等待元素、额外等待时间、提取脚本或特殊参数。未启用额外的理解模型或付费后备方法。": "All 100 cases use Moli's native Markdown Fetch and default done completion mode. Apart from the target URL, no case-specific wait element, extra delay, extraction script, or special parameter is used. No additional interpretation model or paid fallback is enabled.", "核验初次加载范围:稳定内容锚点(稳定标题、栏目、说明)与条目的必需字段、数量、唯一性和类型共同决定任务通过。频繁变化的名单、条目标题和价格按声明的结构与类型核验,不固定旧快照取值。HTTP 页面壳不通过;渲染、内嵌数据和公开 API 均可作为实现路径。全文质量仅在有完整、可比的参考时另算。 已登记正文覆盖规则的案例,还须在最多 5 个参考区域中逐区达到 80% 词语计数覆盖,不能只返回标题或开头。 Ajax | Web Scraper Test Sites 的字段覆盖要求:price至少 6 条、description至少 6 条、reviews至少 6 条,各归属于完整且唯一的记录。 React · 商品与分期信息 的字段覆盖要求:分期至少 15 条、配送至少 13 条,各归属于完整且唯一的记录。": "Initial-load acceptance combines stable content anchors (stable headings, sections, and explanations) with each record's required fields, count, uniqueness, and types. Frequently changing lists, item titles, and prices are checked against declared structure and types rather than frozen snapshot values. An HTTP shell alone fails; rendering, embedded data, and public APIs are all valid implementation routes. Full-text quality is calculated separately only when a complete comparable reference exists. Cases with registered body-coverage rules must also reach 80% token-count coverage in each of up to 5 reference regions; returning only a title or opening section is insufficient. For Ajax | Web Scraper Test Sites, field coverage requires at least 6 price, 6 description, and 6 review values, each belonging to a complete unique record. For React · 商品与分期信息, field coverage requires at least 15 installment entries and 13 delivery entries, each belonging to a complete unique record.", "新增25例结果审核 25/25;未决 0;75例沿用既有审核记录": "Added 25-case result review: 25/25; 0 unresolved. Existing audit records are retained for the prior 75 cases", - "本地审核版": "Local audit build", - "本页合并两个不同日期的 Moli 调用批次,保留每例的真实调用时间。本次仅更换报告展示代码,没有新增 Moli 调用,也不代表正式发布。": "This page combines two Moli call cohorts from different dates and retains each case's actual call time. This build only changes report presentation code, makes no new Moli calls, and is not a formal release.", "Moli Web Fetch 实测:五款浏览器,150 道任务": "Moli Web Fetch benchmark: five browsers, 150 tasks", "解读突出耗时减少比例并增加三张信息图": "Interpretation highlights latency reductions and adds three infographics", "首屏以 Chromium 为基准展示均值、P50、P95 的耗时减少比例;以结果总览和两张流程图替代重复总结与部分流程文字,标题改为直接的内容名称。": "The opening now shows reductions in mean, P50, and P95 latency relative to Chromium. A results overview and two flow diagrams replace repeated summaries and some procedural prose, and the title now names the subject directly.", @@ -309,14 +276,9 @@ "流程图为说明性示意,六组案例仍展示原始证据。图片逐项核对并绑定哈希,不参与评分。": "The diagrams are explanatory. The six case groups still show original evidence; images are checked item by item and hash-bound, but do not affect scoring.", "解读开篇增加综合通过率与六类结果": "Interpretation opens with overall pass rates and six task groups", "五浏览器解读先展示综合通过率与六类任务成绩,正文解释差异,结尾归纳结论。浏览器配置改为对照表,章节统一编号。": "The five-browser article opens with overall pass rates and results for six task groups, explains the differences in the body, and closes with findings. Browser configurations use a comparison table, with consistently numbered sections.", - "Quick Actions 默认最多缓存 5 秒;waitUntil=networkidle2;文档下载与转换同一预算": "Quick Actions caches for up to 5 seconds by default; waitUntil=networkidle2; document download and conversion share one time budget", "十方法全矩阵复核与评分根因修订": "Full-matrix review and root-cause scoring revision for ten methods", - "当前十种方法主矩阵的 1,500 条结果;本轮发布要求机器通过与失败均逐条独立核验,独立同窗测速不在结果审查范围。": "This release requires individual independent review of machine passes and failures across all 1,500 results in the ten-method primary matrix. The separate same-window latency cohort is outside result review.", "全量审查除既有问题外,又确认了四类共因:正文分段按顺序贪心分配会制造假阴性;Exa 裸链接缺少名称归属,而 richLinks 才提供明确配对;序列化脚本状态会被误当可读正文;DOCX 任务只登记章节标题,无法发现表格、日历和尾注缺失。审查在修正前提出 56 条分歧,其中 2 条直接确认了后两类评分错误。": "Beyond previously identified issues, the full review confirmed four shared causes: greedy region-by-region body matching created false negatives; Exa bare links lacked name ownership while richLinks provided explicit pairs; serialized script state could be mistaken for readable body text; and the DOCX task registered only section headings, so missing tables, calendar content, and endnotes went undetected. Before correction, the review raised 56 disagreements, 2 of which directly confirmed the latter scoring errors.", "正文覆盖改为顺序无关的全局容量分配;Exa 具名链接题使用官方 richLinks 并只认证重跑受影响的 10 个挑战任务;可读正文拒绝脚本状态壳;DOCX 登记表格、日历和尾注内容地标,并支持等价 Markdown 表格排版。每类根因均有防假阳性与防假阴性的成对可执行回归。": "Body coverage now uses order-independent global capacity allocation. Exa named-link tasks use official richLinks, with certified reruns limited to the 10 affected challenge tasks. Readable-body acceptance rejects script-state shells. The DOCX contract registers table, calendar, and endnote landmarks while accepting equivalent Markdown table layout. Every root cause has paired executable controls against both false positives and false negatives.", - "无未决判定。当前主矩阵 1,500/1,500 条均有独立核验;修正后保留的审查分歧均已回到完整实际产物、标准答案和任务合同裁决。": "There are no unresolved verdicts. Independent review covers all 1,500/1,500 primary results; every disagreement retained after correction was adjudicated against the complete actual artifact, reference answer, and task contract.", - "当前十种方法的 1,500 条主结果已全部独立核验,未决为 0。审查确认并根治正文分段分配、Exa 具名链接、脚本状态假正文和 DOCX 内容地标四类问题;只有请求接口变化影响的 Exa 10 个挑战任务重新调用,其余修正均复用原始产物离线重算。": "All 1,500 primary results across the current ten methods were independently reviewed, with 0 unresolved. The review confirmed and fixed four root causes: body-region allocation, Exa named links, script state mistaken for body text, and incomplete DOCX content landmarks. Only the 10 Exa challenge tasks affected by a request-interface change were called again; all other corrections rescored the original artifacts offline.", - "当前十方法矩阵审查覆盖 1,500/1,500。修正前审查提出 56 条代理与机器分歧,其中 2 条确认评分错误并先修根因;其余均依据实际产物、标准答案和任务合同裁决,未决为 0。同窗测速只作时延测量,不在结果审查范围。": "Review covers 1,500/1,500 results in the current ten-method matrix. Before correction, the reviewer raised 56 disagreements with machine judgments; 2 confirmed scoring errors whose root causes were fixed first, and the remainder were adjudicated against actual artifacts, reference answers, and task contracts. There are 0 unresolved items. The same-window cohort remains latency-only and is outside result review.", "旧评分实现可能在代码更新后继续被报告读取;分段正文按参考顺序贪心匹配会让较长实际片段先占用错误区域;序列化脚本状态含目标字符串时又可能被当成可读正文。": "A report could keep reading stale scorer output after code changes; greedy body matching in reference order could let longer returned fragments consume the wrong region first; and serialized script state containing target strings could be mistaken for readable body text.", "报告构建拒绝旧评分实现;正文片段改为全局容量分配且不受参考区域顺序影响;脚本或应用状态壳不能满足正文任务。": "Report builds reject stale scorer output; body fragments now use global capacity allocation independent of reference-region order; and script or application-state shells cannot satisfy body tasks.", "两类修正均通过公共 score_attempt 与任务判定入口执行正反向回归;保存输出统一离线重算,不按方法或单条结果增加特例。": "Both corrections have positive and negative regressions executed through the shared score_attempt and task-decision paths. Saved output is consistently rescored offline without provider-specific or row-specific exceptions.", @@ -330,19 +292,16 @@ "保存产物统一离线复评分:CowAgent 的不完整结果改为失败,Firecrawl 与 Tavily 的完整 Markdown 表格保持通过;不需要重新调用供应商。": "Saved artifacts were consistently rescored offline: CowAgent's incomplete result now fails, while Firecrawl and Tavily retain passes for complete Markdown tables. No provider calls were needed.", "逐项核验又发现两类跨方法根因:易变网页的整页快照会制造假失败;稳定锚点合同覆盖了最小正文长度,使只含标题和摘要的短输出误判通过。审查代理另有 21 条分歧,来源是忽略显式截断、忽略大小写与空白规范化,或把互不绑定的标题和链接数组按位置配对。": "Item-by-item review found two further cross-method causes: full-page snapshots of mutable pages created false failures, while stable-anchor contracts overrode the minimum body length and let title-only or summary-only output pass. The reviewer also disagreed on 21 results after overlooking explicit truncation, case and whitespace normalization, or the lack of ownership between separate title and URL arrays.", "82 个易变正文案例改为以独立来源确认的稳定锚点判定,整页快照仅作诊断;正文合同重新执行最小长度门槛。两类修订各有正反向可执行回归,并对保存输出统一离线重算;需新调用的 Crawl4AI 声明范围经认证重跑。Firecrawl 动态任务从当前地区不支持的 actions 改为官方 waitFor 路线,仅认证重跑并复核受影响的 10 条。Lexmount CN 与 Global 的旧方法成绩不再进入当前报告,适配器与接口回归仍保留。": "The 82 mutable body cases now use independently evidenced stable anchors, with full-page snapshots retained only as diagnostics; body contracts again enforce the minimum-length gate. Both corrections have executable positive and negative controls and consistently rescore saved output. The declared Crawl4AI scope was rerun under certification. Firecrawl dynamic tasks moved from actions, which is unavailable in the current account region, to the official waitFor route; only the 10 affected results were certified and reviewed again. Lexmount CN and Global scores are excluded from the current report, while their adapters and interface regressions remain.", - "无未决判定。当前主矩阵 1,500/1,500 条均有独立核验;21 条分歧均已回到实际产物与任务合同裁决。": "There are no unresolved verdicts. Independent review covers 1,500/1,500 primary results; all 21 disagreements were adjudicated against the actual artifacts and task contracts.", "十二方法全矩阵复核与评分根因修订": "Full-matrix review and root-cause scoring revision for twelve methods", "十二种方法主矩阵的 1,800 条结果;本轮发布要求机器通过与失败均逐条独立核验,240 条独立同窗测速不在结果审查范围。": "The release requires individual independent review of machine passes and failures across all 1,800 results in the twelve-method primary matrix. The separate 240-call same-window latency cohort is outside result review.", "同类扫描确认多类系统性判定风险:旧评分实现可能被沿用、明确截断正文可能通过、动态任务排除项未进入可执行合同、复合事实和 Markdown 相对自链接可能误判,挑战页识别与图片资源口径也不一致。": "Family-wide scans identified several systematic verdict risks: stale scorer identities could be reused; explicitly truncated bodies could pass; dynamic-task exclusions were missing from executable contracts; compound facts and Markdown relative autolinks could be misread; and challenge-page detection and image-resource rules were inconsistent.", "共同根因已在评分身份、正文与任务合同、链接规范化、挑战页判定和资源验收边界中修复;每类修订均绑定正反向可执行回归,并对受影响的保存输出统一重算。图片地址题要求命中声明目标,额外页面图片只作纯净度诊断,不再误作失败依据。": "The shared causes were fixed in scorer identity, body and task contracts, link normalization, challenge-page decisions, and resource-acceptance boundaries. Each revision binds executable positive and negative controls and consistently rescores affected saved outputs. Image-URL tasks require the declared target; additional page images remain a precision diagnostic and no longer cause failure.", "最终假阳性、假阴性、核验覆盖和未决数量以本轮独立审查收据为准;收据完整绑定前不得发布。接口、版本或请求合同变化的声明范围使用新调用重跑,不写成纯离线重评分。": "Final false-positive, false-negative, review-coverage, and unresolved counts come from this release's independent review receipt; publication is blocked until that receipt is fully bound. Declared scopes affected by an interface, version, or request-contract change are rerun with new calls rather than described as offline-only rescoring.", "修复评测全链路根因并设置全矩阵核验门禁": "Fix root causes across the evaluation chain and require full-matrix review", - "当前十种方法的 1,500 条主结果已全部独立核验,21 条审查分歧全部裁决、未决为 0。易变正文改用稳定评分点,最小长度继续作为正文硬门槛;Firecrawl 仅对接口变化影响的 10 个动态任务认证重跑并复核,6/10 通过。Lexmount CN 与 Global 的评测实现保留,但旧方法成绩不再进入当前报告。": "All 1,500 primary results across the current ten methods have independent review; all 21 disagreements were adjudicated, with 0 unresolved. Mutable body pages now use stable scoring points and retain minimum length as a hard gate. Firecrawl reran and reviewed only the 10 dynamic tasks affected by its interface change, passing 6/10. Lexmount CN and Global evaluation implementations remain, but their legacy-method scores are excluded from this report.", "本轮从任务合同、标准答案、公共评分入口、链接与挑战页判定、资源验收和证据绑定修复系统性误判风险;接口、版本或请求合同变化的范围使用新调用重跑,评分修订则基于原始保存输出离线重算。最终假阳性与假阴性数量由完整独立审查收据生成,不在发布前预写。": "This release addresses systematic verdict risks in task contracts, references, the shared scorer, link and challenge-page decisions, resource acceptance, and evidence binding. Scopes affected by interface, version, or request-contract changes use new calls; scoring revisions rescore the originally saved outputs offline. Final false-positive and false-negative counts are generated from the complete independent review receipt rather than written in advance.", "全量结果核验": "Full result review", "上一版只独立审查机器失败,机器通过尚未逐条核验,无法发现被宽松规则放行的系统性假阳性。": "The previous release independently reviewed machine failures only. Machine passes had not been checked individually, leaving systematic false positives from permissive rules undetectable.", "机器通过与失败统一使用隐藏机器结论的审查包;任务和审查可见证据哈希均未变化的审查继续有效,只重审发生变化的结果。": "Machine passes and failures use the same packets with machine judgments hidden. Prior reviews remain valid only when both the task and reviewer-visible evidence hash are unchanged; only changed results are reviewed again.", - "当前十方法矩阵审查覆盖 1,500/1,500,出现 21 条代理与机器分歧,均依据实际产物和任务合同裁决,未决为 0。同窗测速只作时延测量,不在结果审查范围。": "Review covers 1,500/1,500 results in the current ten-method matrix. All 21 reviewer-machine disagreements were adjudicated from actual artifacts and task contracts, with 0 unresolved. The same-window cohort remains latency-only and is outside result review.", "上一版只独立审查 705 条机器失败,1,095 条机器通过尚未逐条核验,无法发现被宽松规则放行的系统性假阳性。": "The previous release independently reviewed only 705 machine failures; 1,095 machine passes had not been checked individually, so systematic false positives admitted by permissive rules could remain hidden.", "机器通过与失败统一使用隐藏机器结论的审查包;本轮以主矩阵 1,800 条逐项核验、修订影响项复核和未决清零作为发布门禁。": "Machine passes and failures use the same packets with machine judgments hidden. Publication requires item-by-item review of all 1,800 primary results, renewed review of revision-affected cases, and zero unresolved items.", "实际核验覆盖、假阳性、假阴性、分歧和未决数量从最终审查收据读取;发布说明不预写尚未完成的数字。同窗测速仍只作时延测量。": "Actual coverage, false-positive, false-negative, disagreement, and unresolved counts come from the final review receipt; release notes do not predeclare unfinished numbers. The same-window cohort remains latency-only.", @@ -369,7 +328,6 @@ "全部方法共同成功任务的跨批次耗时表默认展开,占据总榜下方较大篇幅;综合总榜没有同时给出完整 150 次调用的时延分布。": "The cross-batch latency table for tasks passed by every method was expanded by default and occupied substantial space below the ranking; the overall ranking did not show latency over all 150 calls.", "共同成功任务耗时改为按需展开;综合总榜新增每种方法全部 150 次调用的均值、P50、P95,和通过率使用相同固定分母。": "Shared-success latency is now collapsed on demand. The overall ranking adds mean, P50, and P95 over all 150 calls per method, using the same fixed denominator as coverage.", "时延列不参与通过率排序,也不选择成功子集;失败调用耗时保留在全调用分布中。": "Latency columns do not affect coverage ordering or select a successful subset; failed-call elapsed time remains in the all-call distribution.", - "当前十种方法的 1,500 条主结果已全部独立核验,未决为 0;其中 309 条异常触发结果进一步完成输入、参考、请求、原始响应、适配器、评分器和最终判定七层审查。已修复正文分段、具名链接、脚本状态、文档地标、请求状态、动态卡片边界、公开路径泄漏和参考元数据等共因;仅认证重跑接口或产物身份受影响的声明范围,其余修正复用原始产物离线完成。Moli 保持 1.1.4。": "All 1,500 primary results across ten methods were independently reviewed with 0 unresolved decisions. The 309 anomaly-triggered results also completed seven-layer review covering input, reference, request, raw response, adapter, scorer, and final verdict. Shared fixes cover body segmentation, named links, script-state text, document landmarks, request status, dynamic-card boundaries, public path leakage, and reference metadata. Only declared scopes whose interface or artifact identity changed were rerun; all other corrections reused saved outputs offline. Moli remains on 1.1.4.", "请求状态与空输出": "Request status and empty output", "旧适配器把供应商调用成功但没有可验收正文,与供应商或目标请求失败混成同一种失败,导致请求成功率与任务通过率语义串线。": "The old adapters conflated a successful provider call with no acceptable body and a provider or target request failure, mixing request-success and task-pass semantics.", "Cloudflare 依据保存的 success、result 与 origin status 信封归一化;Moli 依据 1.1.4 的退出状态和完整响应错误信封归一化。空的成功响应仍是请求成功、任务失败;明确的目标非 2xx 仍是请求失败。": "Cloudflare now normalizes saved success, result, and origin-status envelopes; Moli uses the 1.1.4 exit status and whole-response error envelope. An empty successful response remains request success but task failure, while an explicit target non-2xx remains request failure.", @@ -385,16 +343,11 @@ "异常根因门禁": "Anomaly root-cause gate", "仅比较标准答案与最终判定无法主动发现空输出、跨站相同错误、集中零分、接口误用、参考冲突和公开产物泄漏;按批次序号缓存又会在小修后错配审查结果。": "Comparing only a reference and final verdict cannot proactively find empty output, repeated cross-site errors, concentrated zero scores, interface misuse, reference conflicts, or public-artifact leakage. Caching by batch number also misaligns reviews after small revisions.", "逐条盲审之后,对确定性异常信号执行七层调查;异常缓存按精确 attempt 范围而不是批次位置复用,范围变化时只重审变化组。": "After row-level blind review, deterministic anomaly signals receive a seven-layer investigation. Anomaly caches are reused by exact attempt scope rather than batch position, so only changed groups are reviewed again.", - "最终矩阵 1,500/1,500 已核验,309 条异常触发结果全部完成调查,未决 0;缓存范围、参考修订和最终审查均有哈希绑定。": "The final matrix is reviewed 1,500/1,500; all 309 anomaly-triggered results completed investigation with 0 unresolved items. Cache scope, reference revisions, and final review are hash-bound.", "请求语义、公开产物与异常门禁复核": "Request semantics, public artifacts, and anomaly-gate review", - "十种方法的 1,500 条最终结果;逐条盲审完成后,对 309 条由空输出、集中失败、状态冲突、接口错误或产物泄漏等信号触发的结果执行七层异常核验。": "The final 1,500 results across ten methods. After row-level blind review, 309 results triggered by empty output, concentrated failures, status conflicts, interface errors, or artifact leakage received seven-layer anomaly review.", "发现请求成功与正文合格被混为一谈、Crawl4AI 动态卡片边界丢失、OpenClaw/CowAgent 公开产物含本机路径,以及 NOAA 已有完整独立参考却保留“全文不可用”旧备注。": "The review found conflated request success and acceptable content, lost Crawl4AI dynamic-card boundaries, host-local paths in OpenClaw and CowAgent public artifacts, and a stale NOAA note declaring full text unavailable despite a complete independent reference.", "请求状态按供应商保存信封与退出状态统一归一化;Crawl4AI 使用已声明的限定提取并保留卡片边界;公开产物在适配器边界删除本机路径;NOAA 参考元数据重新绑定独立 200 响应、main 区域和正文哈希。只对接口或产物身份受影响的范围认证重跑,其余修正离线完成。异常审查缓存改按精确结果范围复用,不再依赖易漂移的批次序号。": "Request status is normalized from saved provider envelopes and exit status. Crawl4AI uses the declared bounded extraction and preserves card boundaries. Host-local paths are removed at the adapter boundary. NOAA reference metadata is rebound to the independent 200 response, main region, and body hash. Only scopes with changed interface or artifact identity were rerun; other corrections were completed offline. Anomaly-review caches now reuse exact result scopes rather than unstable batch numbers.", - "未决 0。最终 1,500/1,500 条结果均有独立核验;309 条异常触发结果全部完成根因审查。Moli 保持 1.1.4,本轮不测试或引入 1.1.5。": "There are 0 unresolved decisions. The final 1,500/1,500 results were independently reviewed, and all 309 anomaly-triggered results completed root-cause review. Moli remains on 1.1.4; this release does not test or introduce 1.1.5.", "Moli:普通正文用 Markdown,挑战题用 JSON 返回最终 DOM、状态和跳转诊断;动态等待与正文范围分离。DOCX 在同一次 CLI 运行中二次 fetch 原始字节;不调用模型。": "Moli: ordinary body tasks use Markdown, while challenge tasks use JSON to return the final DOM, status, and redirect diagnostics. Dynamic waiting is separate from body scope. DOCX performs a second fetch for original bytes within the same CLI run; no model is called.", "动态 / 资源页 / DOCX": "Dynamic / resource pages / DOCX", - "服务端提取截止;由客户端内层截止推导,本轮 40.5 秒,官方范围 1–60 秒": "Server-side extraction deadline; derived from the inner client deadline, 40.5 seconds in this run, within the official 1–60 second range", - "本轮 15,000 毫秒;采用官方建议上界,且小于 40.5 秒客户端内层截止": "15,000 milliseconds in this run; the upper end of the official recommendation and below the 40.5-second inner client deadline", "资源输出契约:只允许在答案图片字段中返回登记的目标、别名或匹配规则;额外图片会使任务未通过。": "Resource output contract: answer-bearing image fields may contain only registered targets, aliases, or matching patterns; additional images fail the task.", "资源召回率 / 纯净度": "Resource recall / precision", "非目标资源": "Unexpected resources", @@ -422,9 +375,6 @@ "实际交付 0 个原始文件;这里只返回了 Markdown 图片链接。": "Original files delivered: 0; only a Markdown image link was returned here.", "与上方登记的完整 URL 匹配": "Match the complete URL registered above", "规范化后相同": "Identical after normalization", - "Moli 1.1.1 读取网页与文件,": "How well does Moli 1.1.1", - "Moli 1.1.1 的 Web Fetch 表现:五款浏览器、150 个任务": "Moli 1.1.1's Web Fetch performance: five browsers, 150 tasks", - "完整报告测试了三个 Moli 版本;本文在比较总通过数后,统一采用其中表现最好的 1.1.1,没有按题选择不同版本的结果。": "The full report tests three Moli versions. After comparing total passes, this article consistently uses the best-performing version, 1.1.1, without selecting different versions task by task.", "PDF 是一个具体原因:Moli 能先交回文件字节,再由本地 pypdf 提取正文。有六道 PDF 题只被 Moli+pypdf 路线完成,其他四款的本轮交付路线未通过。这个成绩比较的是浏览器及其内容处理路径,包含 PDF 解析所用时间。": "PDFs explain part of the difference: Moli returns file bytes, then local pypdf extracts the text. Six PDF tasks are completed only by the Moli + pypdf path; the other four browsers' tested paths fail them. The comparison includes the browser and its content-processing path, including PDF parsing time.", "每条:非空名称 + 本站详情链接": "Each record: nonempty name + detail link on this site", "每条:非空名称+本站详情链接": "Each record: nonempty name + detail link on this site", @@ -443,13 +393,11 @@ "替换字符数 ≥20 且占比 ≥2% 时拒绝": "Reject when replacement characters number ≥20 and make up ≥2%", "PNG 文件头:89504e470d0a1a0a": "PNG header: 89504e470d0a1a0a", "文件长度的通用门槛是 32 字节。SHA-256 是按文件字节计算的指纹,用来核对是否同一份文件;不评价图片识别或媒体语义。": "The general file-length threshold is 32 bytes. SHA-256 is a fingerprint calculated from the file's bytes to check file identity; it does not assess image recognition or media semantics.", - "文章对比 Moli 1.1.1 与另外四款浏览器,明确 150 题中通过 135 题,完整展示均值、P50、P95,并用五个可切换案例并列标准答案与真实输出。原始调用、评分及榜单分母不变。": "The article compares Moli 1.1.1 with four other browsers, states that across 150 tasks it passed 135, presents mean latency, P50, and P95, and provides five selectable examples pairing references with actual outputs. Original calls, scores, and ranking denominators are unchanged.", "网页、PDF、列表、资源地址和原文件采用可切换面板,逐项核对独立参考与真实返回;保留原始 PNG,关键数字直接排版。": "Selectable panels for web text, PDFs, lists, resource URLs, and original files compare independent references with actual outputs check by check. The original PNG is retained, with key figures presented directly as text.", "明确结构检查不核验商品名称事实、文件长度门槛为 32 字节,以及正文规则经过统一离线修订。": "Clarify that structural checks do not verify product-name facts, the file-length threshold is 32 bytes, and body rules received shared offline revisions.", "五款浏览器 · 150 道任务": "Five browsers · 150 tasks", "Moli 读取网页与文件,": "How well does Moli", "实际表现如何?": "read pages and files?", - "我们把同一组 150 道 Web Fetch 任务交给五款浏览器。Moli 1.1.1 完成 135 道,任务通过率最高;在同一批成功任务上,平均耗时和 P50 也最低。优势主要来自 PDF 与媒体文件,网页正文仍有需要追赶的地方。": "We gave five browsers the same 150 Web Fetch tasks. Moli 1.1.1 completed 135, the highest task pass rate; it also had the lowest mean latency and P50 on the same successful tasks. PDFs and media files account for most of its advantage, while page text still has room to improve.", "任务通过率 · 五款中最高": "Task pass rate · highest of the five", "共 150 道,通过 135 道,未通过 15 道": "150 tasks: 135 passed, 15 failed", "相比 Chromium": "Compared with Chromium", @@ -493,19 +441,16 @@ "绿色表示该项最低值。均值由原始耗时求算术平均;P50、P95 沿用完整报告口径。包含内容提取与后处理,远端服务还包含网络往返。": "Green marks the lowest value in each column. The mean is the arithmetic average of recorded latencies; P50 and P95 follow the full report's definitions. Content extraction and post-processing are included, as are network round trips for the remote service.", "Moli 在平均耗时和 P50 上领先这四款浏览器,但并没有包揽三项第一:Lightpanda 的 P95 为 6.03 秒,低于 Moli 的 6.55 秒。P50 就是中位数;把三项一起看,既能看到通常要等多久,也能看到慢任务的影响。": "Moli leads these four browsers on mean latency and P50, but not all three measures: Lightpanda's P95 is 6.03 seconds, below Moli's 6.55 seconds. P50 is the median. Together, the three measures show typical waits and the effect of slower tasks.", "为什么是 8 道?这组速度数据能说明什么?": "Why 8 tasks, and what do these latency figures establish?", - "完整报告另外测试了 Moli 1.0.6 和 1.1.3,因此共有七个受测配置。20 道预选任务中,七个配置全部通过的交集是 8 道。本文保留原报告的这组共同样本,只展示 Moli 1.1.1 和其他四款浏览器,没有重新挑选快任务。": "The full report also tests Moli 1.0.6 and 1.1.3, giving seven configurations. Of 20 preselected tasks, all seven pass an intersection of 8. This article retains that shared sample and shows only Moli 1.1.1 and the other four browsers, without selecting a new set of fast tasks.", "这组数据适合观察同一批任务中的速度差异,不能估计长期服务延迟或吞吐。样本只有 8 道,按当前 P95 算法,P95 实际等于其中最慢的一道。": "These data show latency differences within the same task set, but cannot estimate long-term service latency or throughput. With only 8 tasks, the current P95 calculation makes P95 equal to the latency of the slowest task.", "任务怎样判分": "How tasks are scored", "标准答案与真实输出,放在一起看": "Reference answers beside actual outputs", "下面五个案例分别展示网页正文、PDF、列表、资源地址和原文件怎样验收。左边是独立参考或预先登记的要求,右边是本轮真实返回,下面逐项标出通过与失败。长内容只截取对应片段,并标明范围。": "Five examples show acceptance for page text, PDFs, lists, resource URLs, and original files. The left side contains an independent reference or registered requirements; the right side shows actual output from this run, with pass and fail checks below. Long content uses corresponding excerpts with their scope stated.", "每题只记通过或失败": "Each task is pass or fail", - "共同预算 45 秒": "Shared 45-second budget", "必需检查全部满足": "All required checks must pass", "未通过项保留在分母": "Failed tasks stay in the denominator", "评测集、阈值与独立复核": "Dataset, thresholds, and independent review", "五个案例是五种阅读入口,不是五个统计分组。完整评测集分为普通网页、动态网页、热门站点、文档、图片、视频六组。图片和视频可能要求资源地址或原文件,PDF 正文则使用正文覆盖规则。": "The five examples are ways to explore the results, not five reporting groups. The full dataset has six groups: general pages, dynamic pages, popular sites, documents, images, and videos. Image and video tasks can require URLs or original files; PDF text uses body-coverage rules.", "参考内容来自独立取得的页面或文件;列表使用独立确认的结构要求,不固定每天变化的商品名单。正文阈值是工程验收下限,未通过敏感性实验确定为最优值。评分曾统一修订并离线重算,当前成绩采用最终复核口径。": "References come from independently retrieved pages or files. Lists use independently confirmed structural requirements without freezing a changing daily catalog. Body thresholds are engineering acceptance floors, not values shown optimal by sensitivity experiments. Scoring rules were revised consistently and results rescored offline; current scores use the final reviewed rules.", - "每题保留一次主结果,不在失败后择优重试。Codex 独立复核了完整报告的 1,050 条主结果,未决判定为 0,其中本文五款浏览器占 750 条。Moli 1.1.1 是三个受测版本中总通过数最高的版本;本文统一使用这个版本,没有按题拼接不同版本。": "Each task retains one primary result, without retrying failures to choose a better output. Codex independently reviewed the full report's 1,050 primary results, with 0 unresolved verdicts; this article's five browsers account for 750. Moli 1.1.1 has the highest total passes among the three tested versions. This article uses that version throughout, without mixing versions task by task.", "相同任务,五条内容获取路径": "The same tasks, five content-retrieval paths", "定位与运行位置": "Purpose and execution location", "本轮交付路径": "Output path in this run", @@ -532,7 +477,6 @@ "目录在 iframe 里": "Catalog inside an iframe", "下一轮": "Next round", "扩大样本,再验证优势能否保持": "Expand the sample and test whether the advantages hold", - "这轮测试给出的结论已经具体:Moli 1.1.1 在 150 道混合内容任务中完成得最多,在共同成功的速度样本上平均耗时和 P50 最低;PDF 与原文件是主要增益,长网页与动态内容仍有缺口。": "This run gives a concrete result: Moli 1.1.1 completes the most tasks in the 150-task mixed-content set and has the lowest mean latency and P50 on the shared-success sample. PDFs and original files account for most of its gains; long pages and dynamic content still have gaps.", "下一轮将扩大站点与文档样本,为热门站点安排固定次数的重复运行,并重点回归延迟列表、iframe、DOCX 和嵌入式查看器。速度也需要更多共同成功样本,才能更稳妥地观察 P95。": "The next round will expand site and document samples, schedule fixed repetition counts for popular sites, and prioritize regression tests for delayed lists, iframes, DOCX, and embedded viewers. Latency also needs more shared successes for a more reliable view of P95.", "随后还会用独立的浏览器自动化评测,比较搜索、点击、翻页和填写等连续操作,检验 Agent 能否完成整段网页任务。": "A separate browser-automation evaluation will then compare sequences of searches, clicks, pagination, and form inputs to test whether an agent completes the whole web task.", "查看完整报告与全部真实输出 →": "View the full report and every actual output →", @@ -545,10 +489,8 @@ "图片地址": "Image URL", "完整输出与逐项判定": "Complete output and item-level verdicts", "独立参考正文 · 摘要节选": "Independent reference text · abstract excerpt", - "Moli 1.1.1 · 同一摘要位置节选": "Moli 1.1.1 · excerpt from the same abstract position", "两侧均为节选;评分使用完整正文。Moli 取得 PDF 后,由本地 pypdf 在同一预算内提取文字。": "Both sides show excerpts; scoring uses the complete text. After Moli retrieves the PDF, local pypdf extracts text within the same budget.", "独立参考正文 · 全文": "Independent reference text · complete body", - "Moli 1.1.1 · 原始 Markdown 全文": "Moli 1.1.1 · complete original Markdown", "参考以正文保存,实际输出保留 Markdown 标记;按文字内容验收。": "The reference is saved as body text; actual output retains Markdown markup. Acceptance checks the textual content.", "必要文字": "Required text", "正文分区覆盖": "Body-region coverage", @@ -559,19 +501,16 @@ "未触发已登记的解码损坏判据": "Registered decoding-damage criteria not triggered", "独立观察的稳定文字": "Independently observed stable text", "记录要求": "Record requirements", - "Moli 1.1.1 · 四组稳定文字对应的原文行": "Moli 1.1.1 · original lines for the four stable-text groups", "原始 Markdown 中三条商品记录节选": "Three product-record excerpts from the original Markdown", "稳定文字": "Stable text", "完整记录": "Complete records", "唯一记录": "Unique records", "商品名称、价格和排序不作为固定答案;本题不核验名称的事实正确性或信息时效。右侧节选不是完整列表。": "Product names, prices, and ordering are not fixed answers. This task does not verify the factual correctness of names or information freshness. The right-hand excerpt is not the complete list.", "独立确认的目标资源": "Independently confirmed target resource", - "Moli 1.1.1 · 含目标下载链接的原始输出节选": "Moli 1.1.1 · original-output excerpt containing the target download link", "目标图片地址": "Target image URL", "本题的资源匹配规则忽略 URL 查询参数,因此实际下载链接与登记目标匹配;这不是对任意下载地址的通用等价判断。本题不要求下载图片,也不检查推荐图片的数量。": "This task's resource-matching rules ignore URL query parameters, so the actual download link matches the registered target. This is not a general equivalence rule for arbitrary download URLs. The task neither requires downloading the image nor checks the number of recommended images.", "独立目标文件": "Independent target file", "目标 SHA-256": "Target SHA-256", - "Moli 1.1.1 · 实际交付的 PNG": "Moli 1.1.1 · actual delivered PNG", "实际 SHA-256": "Actual SHA-256", "文件签名": "File signature", "至少 32 字节;独立目标为 8,090 字节": "At least 32 bytes; independent target is 8,090 bytes", @@ -596,24 +535,20 @@ "直接 PNG:用文件指纹核对实际字节": "Direct PNG: verify the actual bytes with a file fingerprint", "本轮输入统一是 URL。有的地址指向网页,其中可能嵌有图片、视频或文档;有的直接指向 PDF、PNG 或视频文件,点击地址就能取得文件响应。评测会按题目要求读取正文、定位资源地址或取得原文件。它关心从 URL 到可用内容这一段:该有的正文是否齐全,列表是否包含要求的字段,返回的文件是否正确,以及这一过程花了多久。模型拿到内容后能否推理正确、浏览器能否完成连续网页操作,都需要另外评测。": "Every input in this run is a URL. Some point to pages that may embed images, videos, or documents; others point directly to PDFs, PNGs, or videos and return a file response. Depending on the task, the benchmark reads body text, locates a resource URL, or retrieves the original file. It checks whether the required text is complete, list fields are present, files are correct, and how long retrieval takes. Correct reasoning from that content and completing sequences of web interactions need separate evaluations.", "在相同成功任务上,Moli 的中位耗时在本文五款浏览器中最低": "On the same successful tasks, Moli has the lowest median latency among the five browsers here", - "Moli 1.1.1 的 P50 为 2.70 秒,Chromium 为 3.63 秒,前者低约 25.8%,也是本文五款浏览器中最低的。P50 是耗时分布的中点:这 8 题中,一半任务的耗时不超过 2.70 秒,不意味着每个网页都更快。P95 描述较慢一端的等待时间,在这组样本中 Lightpanda 的数值更低,8 题也不足以说明生产环境的长期延迟和吞吐。": "Moli 1.1.1's P50 was 2.70 seconds, compared with Chromium's 3.63 seconds: about 25.8% lower, and the lowest among the five browsers here. P50 is the midpoint of the latency distribution: among these 8 tasks, half took no more than 2.70 seconds. This does not mean every page is faster. P95 describes the slower end, where Lightpanda had the lower value. These 8 tasks cannot establish long-term production latency or throughput.", "网页或 PDF 地址": "Web or PDF URL", "列表页 URL": "List-page URL", "资源详情页 URL": "Resource-detail URL", "文件直链 URL": "Direct-file URL", "Moli+pypdf 在六道其他四款浏览器都未完成的 PDF 任务中取得了正文,构成文档组的六题差距。直接 DOCX 和两个嵌入式查看器仍未通过;普通 PDF 的这条处理路线尚未覆盖这些输入。": "Moli + pypdf retrieves the text on six PDF tasks that all four other browsers fail, accounting for the document group's six-task gap. The direct DOCX task and two embedded viewers still fail; this ordinary-PDF path does not yet cover those inputs.", "Moli 在这道图片题胜出,但在 Blender 官方视频下载包题中只返回了目标文件名,没有形成可解析的下载地址;Chromium、Lightpanda 和 Obscura 则交付了指定的 1080p、30fps、normal 版本入口。资源发现仍要按具体目标逐项测试。": "Moli wins this image task, but returns only the target filename in the official Blender video-package task, without a parseable download URL. Chromium, Lightpanda, and Obscura deliver the specified 1080p, 30fps, normal-version link. Resource discovery still needs target-by-target testing.", - "每题保留一次主结果,没有失败后择优重试。自动评分之后,本轮还用 Codex 独立复核了 1,050 条主结果,未决判定为 0。其中 750 条属于本文的五款浏览器,其余对应另外两个 Moli 版本。本文采用三个受测版本中总通过数最高的 1.1.1;这是看到结果后的版本选择,没有按题拼接不同版本的输出。上面的人工删改对照只用于检查评分方法,不增加分母或改写主榜。": "Each task retains one primary result, without retrying failures to choose a better response. After automated scoring, Codex independently reviewed 1,050 primary results, with 0 unresolved verdicts. Of these, 750 belong to this article's five browsers; the rest cover two other Moli versions. This article uses 1.1.1, the highest-scoring of the three tested versions. It was selected after seeing results, without mixing versions task by task. The edited controls above test the scoring method only; they neither add to the denominator nor change the main ranking.", "Lightpanda 不是 Chromium 分支。本轮普通网页完成 92/100,三篇 Moli 未完成的 W3C 标准都取到了正文,IMDb 任务也只有它通过。共同成功速度子集中,P50 为 3.49 秒,P95 在本文五款浏览器中最低;文档 1/10、热门站点 4/10 则显示这条路线在混合输入中的缺口。": "Lightpanda is not a Chromium fork. It completes 92/100 general pages, retrieves all three W3C standards that Moli missed, and is the only browser here to pass IMDb. In the shared-success latency subset, its P50 is 3.49 seconds and its P95 is lowest among the five browsers. Scores of 1/10 for documents and 4/10 for popular sites show gaps in this mixed-input path.", "Obscura 的标准模式完成 90/100 个普通网页和 5/10 个热门站点任务,也找到了 Moli 未完成的 Blender 下载包入口。相同成功任务上的 P50 为 11.69 秒,是本文五款浏览器中最高的。这个结果对应当前模式与端到端调用,不能外推到未启用的其他模式。": "Obscura's standard mode completes 90/100 general pages and 5/10 popular-site tasks, and finds the Blender package link Moli missed. Its P50 on the same successful tasks is 11.69 seconds, the highest of the five browsers here. This describes the current mode and end-to-end call, not other modes that were not enabled.", "动态网页:五款浏览器都停在 8/10": "Dynamic pages: all five browsers score 8/10", - "本轮最明确的结果是:Moli 1.1.1 以 90.0% 的任务通过率覆盖了更多输入,尤其是 PDF 与原始媒体文件;在共同成功的 8 个速度任务中,中位耗时也低于其他四款浏览器。普通网页通过数接近 Chromium,但参考正文找回比例和 F1 仍有差距。": "Moli 1.1.1 passes 90.0% of tasks, covering more inputs, especially PDFs and original media files. Across the 8 shared-success latency tasks, its median is lower than those of the other four browsers. General-page pass counts are close to Chromium's, but body recall and F1 still trail.", "窄屏可左右滑动查看完整表格。": "On narrow screens, scroll horizontally to view the full table.", "0 秒:页面框架": "0 seconds: page shell", "3 秒:目标列表出现": "3 seconds: target list appears", "外围页面": "Outer page", "iframe 子页面:目标目录": "iframe content: target catalog", - "文章聚焦 Moli 1.1.1 与另外四款浏览器,用五份真实验收样例解释标准答案、通过门槛与实际返回,增加双语总览插图和离线删改对照。原始调用、评分及榜单分母不变。": "The article focuses on Moli 1.1.1 and four other browsers. Five real acceptance examples explain reference answers, passing thresholds, and actual outputs, with bilingual overview illustrations and edited offline controls. Original calls, scores, and ranking denominators are unchanged.", "验收解释": "Acceptance explained", "五类任务原先只有概念流程,难以核对具体通过原因。": "The five task types previously had only conceptual flows, making the reasons for passing difficult to check.", "补充网页、PDF、列表、资源地址和原文件的真实样例、逐项门槛及人为删改对照;明确四类交付物、五个样例与六个统计分组。": "Add real web, PDF, list, resource-URL, and original-file examples, with itemized thresholds and edited controls. Distinguish four output types, five examples, and six reporting groups.", @@ -626,7 +561,6 @@ "用五个真实样本,把验收过程展开": "Five real examples, with the acceptance checks explained", "同样是交给浏览器一个 URL,任务可能要求一篇正文、几条商品记录、一条下载地址,也可能直接要文件。判断是否完成,必须先明确要交回什么。下面用五个样本解释这四类交付物,网页正文和 PDF 正文因处理路线不同分开举例。实际数据按六个分组统计:普通网页 100 题,动态网页、热门站点、文档、图片、视频各 10 题。图片和视频组都可能要求资源地址或原文件,PDF 则沿用正文验收。": "A URL can require body text, product records, a download link, or the file itself. Acceptance starts by defining that output. Five examples below explain these four output types; web and PDF text have separate examples because their processing paths differ. Results use six reporting groups: general pages with 100 tasks, and dynamic pages, popular sites, documents, images, and videos with 10 each. Image and video tasks can require URLs or files, while PDFs use body-text acceptance.", "从调用开始,到可验收结果形成": "From the start of the call to an assessable output", - "共同预算:45 秒 · 输入的任务类型预先已知": "Shared budget: 45 seconds · input task type known in advance", "网页 / PDF": "Web / PDF", "页面转文本,或取文件后解析": "Convert page to text, or retrieve and parse a file", "正文内容": "Body text", @@ -747,7 +681,6 @@ "评分规则也经历过修订:独立复核曾发现排版差异导致的误判,以及正文不完整却被放行的情况,随后对各方案统一规范化、补充正文覆盖检查并离线重算。因此当前成绩对应最终复核口径,不能把所有规则都描述成第一次调用前已经冻结。": "Scoring rules have also been revised. Independent review found false verdicts caused by formatting differences and incomplete text that had passed. Shared normalization and body-coverage checks were then applied across methods, followed by offline rescoring. Current scores use the final reviewed rules; not every rule was frozen before the first call.", "五个样例分别拒绝了正文遗漏、文档截断、重复记录、错误资源地址和文件字节变化。它们说明规则能识别这些具体失败方式,而不是证明任意网站都能被正确评分。列表中的虚构名称能通过结构检查,也暴露了当前指标没有覆盖的事实核验。": "The five examples reject missing body text, a truncated document, duplicate records, an incorrect resource URL, and changed file bytes. They show that the rules detect these specific failures, not that they score every website correctly. Invented product names passing the structural check also expose factual verification that the current metrics do not cover.", "任务通过率按完成题数计算,Moli 的结果是 135 ÷ 150 = 90.0%。只有存在完整参考正文的任务,才另算 Precision、Recall 和 F1:分别衡量返回文字与参考的重合比例、参考正文找回比例,以及两者的综合结果。它们不检查事实真伪或时效;没有完整参考的任务不参与全文平均。": "Task pass rate counts completed tasks; Moli's result is 135 ÷ 150 = 90.0%. Tasks with complete body references also receive precision, recall, and F1: output overlap with the reference, reference text recovered, and their combined score. These do not check factual correctness or freshness. Tasks without complete references are excluded from full-text averages.", - "每题保留一次主结果,没有失败后择优重试。自动评分之后,本轮还用 Codex 独立复核了 1,050 条主结果,未决判定为 0。其中 750 条属于本文的五款方案,其余对应另外两个 Moli 版本。本文采用三个受测版本中总通过数最高的 1.1.1;这是看到结果后的版本选择,没有按题拼接不同版本的输出。上面的人工删改对照只用于检查评分方法,不增加分母或改写主榜。": "Each task retains one primary result, without retrying failures to choose a better response. After automated scoring, Codex independently reviewed 1,050 primary results, with 0 unresolved verdicts. Of these, 750 belong to this article's five methods; the rest cover two other Moli versions. This article uses 1.1.1, the highest-scoring of the three tested versions. It was selected after seeing results, without mixing versions task by task. The edited controls above test the scoring method only; they neither add to the denominator nor change the main ranking.", "03 / 五款受测方案": "03 / Five tested methods", "浏览器名称之外,还要看怎样取得结果": "Beyond the browser name, examine how it produces the result", "同一款浏览器可以接不同的提取器和文件处理工具。本轮比较的是下面五条配置好的路线,既包含本地运行,也包含远端服务。产品定位参考各自官方说明,分数来自相同的 150 个任务。": "A browser can be paired with different extractors and file-processing tools. This run compares the five configured paths below, including local execution and a remote service. Product descriptions follow official documentation; scores use the same 150 tasks.", @@ -781,18 +714,15 @@ "04 / 尚未解决的问题": "04 / Remaining gaps", "05 / 结论与下一轮": "05 / Conclusions and next round", "在相同成功任务上,Moli 的中位耗时在本文五款方案中最低": "On the same successful tasks, Moli has the lowest median latency among the five methods here", - "自动评分之后,本轮还用 Codex 独立复核了 1,050 条主结果,未决判定为 0。其中 750 条属于本文展示的五款方案,另外的记录对应两个其他 Moli 版本。本文固定采用三个受测版本中总通过数最高的 1.1.1;这是看到结果后的版本选择,但没有按题拼接不同版本的最好输出。": "After automated scoring, this run used Codex to independently review 1,050 primary results, with 0 unresolved verdicts. Of these, 750 belong to the five methods shown here; the remaining records cover two other Moli versions. This article uses 1.1.1, the highest-scoring of the three tested versions. That choice was made after seeing the results, but outputs were not selected across versions task by task.", "它的突出结果是文档 7/10、图片 10/10、视频 7/10;普通网页完成 97/100,与 Chromium 的 99/100 接近。在共同成功的速度子集中,它的中位耗时在本文五款方案中最低。三个 W3C 页面停在安全验证页,直接 DOCX 和两个嵌入式查看器也未完成。": "Its strongest results are documents at 7/10, images at 10/10, and videos at 7/10. General pages score 97/100, close to Chromium's 99/100. In the shared-success latency subset, its median is lowest among the five methods here. Three W3C pages stop at security verification; the direct DOCX task and two embedded viewers also fail.", "它完成了 92/100 个普通网页和 8/10 个动态任务;三篇 Moli 未完成的 W3C 标准都取得了正文,IMDb 任务也只有它通过。在独立速度子集中,它的 P50 为 3.49 秒,样本 P95 在本文五款方案中最低。文档 1/10、热门站点 4/10,则说明它在本次混合输入中的覆盖仍有缺口。": "It completes 92/100 general pages and 8/10 dynamic tasks. It retrieves the body text of all three W3C standards Moli missed and is the only method here to pass the IMDb task. In the separate latency subset, its P50 is 3.49 seconds and its sample P95 is lowest among the five methods here. Scores of 1/10 for documents and 4/10 for popular sites show gaps in this mixed-input sample.", "Moli 的 Web Fetch 表现:五款浏览器、150 个任务": "Moli's Web Fetch performance: five browsers, 150 tasks", "20 题中 · 七条路线共同成功的 8 题": "From 20 tasks · 8 passed by all seven paths", "把内容类型分开看,Moli 与 Chromium 在 120 个网页类任务中都完成了 111 个;文档、图片和视频合计 30 题,分别完成 24 个和 14 个。因此,Moli 相对 Chromium 的 10 题净差,全部来自文档与媒体。这里比较的是完整处理路线:Moli 的 PDF 成绩包含获取文件后由本地 pypdf 提取文字的步骤,其他方案采用本轮配置的页面转文本路径。": "Across 120 web tasks, Moli and Chromium each completed 111. Across 30 document, image, and video tasks, they completed 24 and 14 respectively. Moli's net 10-task lead over Chromium therefore comes entirely from documents and media. This compares complete processing paths: Moli's PDF scores include local pypdf text extraction after file retrieval; the other methods use the page-to-text paths configured for this run.", - "Moli 1.1.1 的 P50 为 2.70 秒,Chromium 为 3.63 秒,前者低约 25.8%,也是本文五款方案中最低的。P50 是耗时分布的中点:这 8 题中,一半任务的耗时不超过 2.70 秒,不意味着每个网页都更快。P95 描述较慢一端的等待时间,在这组样本中 Lightpanda 的数值更低,8 题也不足以说明生产环境的长期延迟和吞吐。": "Moli 1.1.1's P50 was 2.70 seconds, compared with Chromium's 3.63 seconds: about 25.8% lower, and the lowest among the five methods here. P50 is the midpoint of the latency distribution: among these 8 tasks, half took no more than 2.70 seconds. This does not mean every page is faster. P95 describes the slower end, where Lightpanda had the lower value. These 8 tasks cannot establish long-term production latency or throughput.", "Web Fetch 不只需要处理 HTML。一篇 PDF 要交回正文,一张图片详情页可能只要求找到原图链接,直接图片地址则要交回文件。相同的“抓取成功”,放到这些任务里有不同含义。以下五种交付要求各用一个例子说明;箭头表示处理过程,具体浏览器可以采用不同实现。": "Web Fetch handles more than HTML. A PDF task needs text; an image detail page may need only the original-image link; a direct image URL needs the file itself. Successful retrieval means something different in each case. Each of the five output requirements below has an example; arrows show the processing sequence, while individual browsers may implement it differently.", "交付要求与统计分组是两回事:图片和视频组都可能要求资源地址或原文件,热门站点也可能验收正文或列表。统计时保留六个分组:100 个普通网页,以及动态网页、热门站点、文档、图片、视频各 10 个。普通网页包含文档站、百科、项目说明和标准文本;动态网页专门检查延迟出现、后续取数和 iframe 内的内容;热门站点考察真实站点上较复杂的页面与列表。": "Output requirements differ from reporting groups: image and video tasks may require resource URLs or original files; popular-site tasks may check body text or lists. Results retain six groups: 100 general pages, plus 10 each for dynamic pages, popular sites, documents, images, and videos. General pages include documentation sites, encyclopedias, project descriptions, and standards. Dynamic tasks examine delayed content, subsequent data retrieval, and iframe content; popular-site tasks cover more complex real-world pages and lists.", "它的突出结果是文档 7/10、图片 10/10、视频 7/10;普通网页完成 97/100,与 Chromium 的 99/100 接近。在共同成功的速度子集中,它的中位耗时最低。三个 W3C 页面停在安全验证页,直接 DOCX 和两个嵌入式查看器也未完成。": "Its strongest results are documents at 7/10, images at 10/10, and videos at 7/10. General pages score 97/100, close to Chromium's 99/100. It has the lowest median latency in the shared-success subset. Three W3C pages stop at security verification; the direct DOCX task and two embedded viewers also fail.", "六个只有 Moli+pypdf 路线完成的 PDF 任务支持了这组结果,构成文档类别的六题差距。结果支持将 Moli 原始文件获取与解析器组合用于这组 PDF 输入;直接 DOCX 和两个嵌入式查看器未通过,仍需单独完善。": "Six PDF tasks completed only by the Moli + pypdf path account for the six-task document gap. These results support combining Moli's raw-file retrieval with a parser for this set of PDFs. The direct DOCX task and two embedded viewers failed and still need separate work.", - "Moli 仍在迭代:1.1.1 之后已发布至 1.1.4,其中包含流式解析暂停时的重定向处理修正。本文没有测试这些后续版本;动态任务是否改善,将用相同任务重新检验。": "Moli continues to develop: releases after 1.1.1 extend through 1.1.4, which includes a redirect-handling fix during streaming-parser suspension. This article does not test those later versions. Improvements on dynamic tasks will be checked by rerunning the same tasks.", "Web Fetch 实测": "Web Fetch benchmark", "150 个任务 · 五款浏览器": "150 tasks · five browsers", "Moli 的 Web Fetch 表现": "Moli's Web Fetch performance", @@ -827,9 +757,7 @@ "浏览器的选择也因此有了具体标准。即使都能打开一个页面,不同方案交回的东西仍可能不同:完整正文、只有标题的页面框架、安全验证提示,或者下游无法解析的文件内容。我们把这些差异放进同一组任务中比较。": "That gives browser selection concrete criteria. Browsers that can all open a page may return very different things: complete text, a title-only shell, a security prompt, or file content the next tool cannot parse. We compare those differences through a shared task set.", "01 / 主要结果": "01 / Main results", "Moli 完成 135 个任务,优势集中在文档与媒体": "Moli completed 135 tasks, with its lead concentrated in documents and media", - "Moli 1.1.1 在 150 个任务中完成 135 个,通过率为 90.0%。Chromium 完成 125 个,Lightpanda 完成 119 个,Obscura 完成 117 个,Kitesurf 完成 112 个。每题按相同要求验收,速度和正文重合指标不计入这个通过率。": "Moli 1.1.1 faced 150 tasks and completed 135, a 90.0% pass rate. Chromium completed 125, Lightpanda 119, Obscura 117, and Kitesurf 112. Each task uses the same acceptance requirements across methods; latency and text-overlap metrics do not contribute to this pass rate.", "五款浏览器的任务通过数": "Tasks passed by the five browsers", - "分母均为 150 题,每题等权。失败、超时和内容不达标的任务均保留在分母中。": "Every denominator is 150 equally weighted tasks. Errors, timeouts, and outputs that fail content requirements remain in the denominator.", "把内容类型分开看,Moli 与 Chromium 在 120 个网页类任务中都完成了 111 个;文档、图片和视频合计 30 题,分别完成 24 个和 14 个。因此,Moli 相对 Chromium 的 10 题净差,全部来自文档与媒体。": "By content type, Moli and Chromium each completed 111 of the 120 web tasks. Across 30 document, image, and video tasks, they completed 24 and 14 respectively. Moli's net 10-task lead over Chromium therefore comes entirely from documents and media.", "网页类持平,文档与媒体相差 10 题": "Web results tie; documents and media differ by 10 tasks", "网页类包含普通网页、动态网页和热门站点;文档、图片、视频各有 10 题。": "Web tasks cover general pages, dynamic pages, and popular sites. Documents, images, and videos each have 10 tasks.", @@ -837,7 +765,6 @@ "在相同成功任务上,Moli 的中位耗时最低": "Moli had the lowest median latency on the same successful tasks", "速度另设一组预先选定的 20 个普通网页。包括三个 Moli 版本在内的七条受测路线共同完成了其中 8 个,速度图只比较这 8 个相同任务,避免把各自成功的不同网页混在一起。": "Latency uses a separate, preselected set of 20 general pages. All seven tested paths, including three Moli versions, completed 8 of them. The chart compares only those same 8 tasks, rather than mixing different successful subsets.", "共同成功任务的中位耗时 · 越短越快": "Median latency on shared successes · shorter is faster", - "Moli 1.1.1 的 P50 为 2.70 秒,Chromium 为 3.63 秒,前者低约 25.8%,也是本文五款方案中最低的。P50 是耗时分布的中点;一半任务的耗时不超过这个值。它表示典型等待时间,不意味着每个网页都更快。P95 描述较慢一端的等待时间,在这组样本中 Lightpanda 的数值更低,8 题也不足以说明生产环境的长期延迟和吞吐。": "Moli 1.1.1's P50 was 2.70 seconds, compared with Chromium's 3.63 seconds: about 25.8% lower, and the lowest among the five methods in this article. P50 is the midpoint of the latency distribution; half the tasks take no longer than this value. It describes a typical wait, not a promise that every page is faster. P95 describes the slower end, where Lightpanda had the lower value in this sample. These 8 tasks cannot establish long-term production latency or throughput.", "02 / 任务与验收": "02 / Tasks and acceptance", "从一个地址,到一个可以检查的结果": "From an address to a result we can check", "Web Fetch 不只需要处理 HTML。一篇 PDF 要交回正文,一张图片详情页可能只要求找到原图链接,直接图片地址则要交回文件。相同的“抓取成功”,放到这些任务里有不同含义。下面五个例子展示了常见的处理过程;箭头表示典型路径,具体浏览器可以采用不同实现。": "Web Fetch handles more than HTML. A PDF task needs text; an image detail page may need only the original-image link; a direct image URL needs the file itself. Successful retrieval means something different in each case. The five examples below show typical paths; individual browsers may implement them differently.", @@ -875,7 +802,6 @@ "不能通过的结果": "An unacceptable result", "只剩 Example Domain 标题;或整页只有 Access denied。请求即使正常返回,也没有交付题目要求的内容。": "Only the Example Domain title remains, or the entire page says Access denied. A normal response still fails to deliver the required content.", "参考内容独立于被测浏览器准备。对于动态网页,保留初始 HTTP 内容和独立渲染后可见的目标内容,核对页面是否真的需要后续加载。对经常变化的榜单和商品页,则检查稳定文字、必要字段、数量和唯一性,不把某一刻的价格和排名永久固定成答案。": "References are prepared independently of the tested browsers. Dynamic-page references retain initial HTTP content and independently rendered visible target content, so subsequent loading requirements can be checked. Frequently changing rankings and product pages are assessed through stable text, required fields, count, and uniqueness, without freezing a moment's prices or ranks as permanent answers.", - "自动评分之后,报告中的 1,050 条主结果还由独立的 Codex 评审 agent 逐条复核,未决判定为 0。其中 750 条属于本文展示的五款方案,另外的记录对应两个其他 Moli 版本。本文固定采用三个受测版本中总通过数最高的 1.1.1;这是看到结果后的版本选择,但没有按题拼接不同版本的最好输出。": "After automated scoring, independent Codex reviewer agents checked all 1,050 primary results individually, with 0 unresolved verdicts. Of these, 750 belong to the five methods shown here; the remaining records cover two other Moli versions. This article uses 1.1.1, the highest-scoring of the three tested versions. That choice was made after seeing the results, but outputs were not selected across versions task by task.", "任务通过率与正文完整度,分别回答什么": "What task pass rate and body coverage each tell us", "任务通过率按完成题数计算,例如 Moli 的 135 ÷ 150 = 90.0%。正文质量另外检查:Recall 看参考正文找回了多少,Precision 看返回文字有多少落在参考范围内,F1 综合两者。导航、页脚和格式差异会影响这些数值;它们衡量文字重合,不验证内容的事实真伪或时效。": "Task pass rate counts completed tasks: for Moli, 135 ÷ 150 = 90.0%. Text quality is checked separately. Recall measures recovered reference text; precision measures returned text within the reference scope; F1 combines both. Navigation, footers, and formatting affect these numbers. They measure text overlap, not factual correctness or freshness.", "一个结果可以满足任务的必要要求,同时带有多余导航;也可能找回部分正文,却漏掉一段必需内容而未通过。因此任务成败与全文指标分别保留。缺少完整参考的任务不参与全文平均,也不会把未测值当作零或满分。": "An output can satisfy the task while containing extra navigation. It can also recover some body text but fail because a required passage is missing. Task verdicts and full-text metrics therefore remain separate. Tasks without complete references are excluded from full-text averages; unmeasured values are neither zero nor perfect scores.", @@ -908,7 +834,6 @@ "04 / 实际输出": "04 / Actual outputs", "看清差距,要把同一道题的输出放在一起": "Compare outputs from the same task to understand the difference", "一篇 PDF:文件是否进入了解析步骤": "A PDF: did the file reach the parser?", - "Moli 1.1.1 的本题实际处理流程": "Moli 1.1.1's actual path on this task", "取回 PDF 原始字节": "Retrieve raw PDF bytes", "本地提取正文": "Extract body text locally", "评测预先知道输入属于文档类型。图中流程没有测试未知 URL 的自动识别,也不能把本地解析器的工作归给 Moli 本身。": "The benchmark knows in advance that the input is a document. This path does not test automatic detection for unknown URLs; the local parser's work must not be attributed to Moli itself.", @@ -949,18 +874,13 @@ "Obscura 在这两题均未完成取页,说明相同失败结果不一定由相同原因造成。当前证据能定位缺失的交付物,但尚未通过受控实验分离网络、等待策略和 iframe 处理各自的影响。": "Obscura fails to complete retrieval on both tasks, so identical verdicts need not have identical causes. The evidence identifies missing outputs, but controlled experiments have not yet separated network, wait-strategy, and iframe effects.", "06 / 结论与下一轮": "06 / Conclusions and next round", "Moli 已有可用优势,下一步要验证稳定覆盖": "Moli has practical advantages; consistent coverage is the next test", - "本轮最明确的结果是:Moli 1.1.1 以 90.0% 的任务通过率覆盖了更多输入,尤其是 PDF 与原始媒体文件;在共同成功的 8 个速度任务中,中位耗时也低于其他四款方案。普通网页通过数接近 Chromium,但参考正文找回比例和 F1 仍有差距。": "Moli 1.1.1 passes 90.0% of tasks, covering more inputs, especially PDFs and original media files. Across the 8 shared-success latency tasks, its median is lower than those of the other four methods. General-page pass counts are close to Chromium's, but body recall and F1 still trail.", "对于同时读取网页、PDF 和媒体的 Agent,这些结果支持把 Moli 纳入实际接入测试。如果主要处理长 HTML 正文,Chromium 仍是重要的质量参照。输入中 PDF 的比例、是否需要原文件、是否经常遇到验证页和 iframe,会直接改变这组成绩对业务的意义。": "For agents reading pages, PDFs, and media, these results support including Moli in integration tests. For workloads dominated by long HTML text, Chromium remains an important quality reference. PDF frequency, original-file requirements, verification pages, and iframes determine how relevant these scores are to a workload.", - "Moli 的公开版本也在持续更新:按 UTC 日期,9 月 1 日发布 1.1.1,9 月 4 日、5 日和 8 日又发布了后续版本,变更涉及 DOM、HTML、抓取和渲染。例如 1.1.4 包含流式解析暂停时的重定向处理修正。这些是版本进展;本文成绩仍对应 1.1.1,动态任务是否改善需要用同题复测确认。": "Moli's public releases continue to evolve. In UTC, month 9 day 1 saw version 1.1.1, followed by releases in month 9 on days 4, 5, and 8, covering DOM, HTML, retrieval, and rendering changes. For example, 1.1.4 includes a redirect-handling fix during streaming-parser suspension. These are release developments; this article's scores still describe 1.1.1. Improvements on dynamic tasks require rerunning the same tasks.", "查看公开版本记录": "View public release history", "下一轮会扩大站点和文档样本,并对热门站点安排固定次数的重复运行。延迟列表、iframe、DOCX 和嵌入式查看器将作为重点回归对象,同时报告通过率、正文质量与相同任务上的速度,观察覆盖扩大后能否保持当前优势。": "The next round will expand site and document samples and run a fixed number of repetitions on popular sites. Delayed lists, iframes, DOCX, and embedded viewers will be priority regression cases. Pass rate, body quality, and same-task latency will show whether the current advantages hold as coverage expands.", "Web Fetch 之外,我们还会用独立的 Browser Use 评测比较搜索、点击、翻页和填写等连续操作。届时关注的是 Agent 能否完成整段网页任务,以及过程中需要多少操作、等待和恢复。": "A separate Browser Use evaluation will compare sequences of searches, clicks, pagination, and form inputs. It will examine whether an agent completes the whole web task, and how many actions, waits, and recovery steps it needs.", "完整报告、评测规则与核验记录": "Full report, evaluation rules, and review records", - "150 个 Web Fetch 任务实测:Moli 1.1.1 多完成了什么?": "150 Web Fetch tasks tested: what more did Moli 1.1.1 complete?", "150 个 Web Fetch 任务实测:": "150 Web Fetch tasks tested:", - "Moli 1.1.1 多完成了什么?": "What more did Moli 1.1.1 complete?", "五款浏览器,六类内容。把总成绩拆开,看看网页、文档和媒体文件是怎样拉开差距的。": "Five browsers, six content families. A closer look at how web pages, documents, and media files contribute to the results.", - "Moli 1.1.1 任务通过率": "Moli 1.1.1 task pass rate", "本轮比 Chromium 多完成": "More tasks completed than Chromium in this run:", "同一任务要求": "Same task requirements", "45 秒预算": "45-second budget", @@ -976,7 +896,6 @@ "一篇文章需要正文,商品列表需要名称和链接,图片地址可能需要返回原文件。打开页面、收到响应,还只是中间步骤。": "An article needs body text; a product list needs names and links; an image URL may need to return the original file. Opening the page and receiving a response are intermediate steps.", "01 / 总成绩": "01 / Overall results", "10 题净差,全部来自文档与媒体": "The 10-task net difference comes entirely from documents and media", - "任务通过数 / 150 题": "Tasks passed / 150 tasks", "每道任务权重相同。条形长度表示通过任务数,未通过的任务也保留在分母中。": "Every task has equal weight. Bar length represents tasks passed; failed tasks remain in the denominator.", "总榜中,Moli 完成 135 题。它与 Chromium 在普通网页、动态网页和热门站点组成的 120 题中,都完成了 111 题。最终差距来自另外 30 个文档、图片和视频任务。": "Moli completed 135 tasks overall. Across the 120 general-web, dynamic-web, and popular-site tasks, it and Chromium each completed 111. The overall difference comes from the remaining 30 document, image, and video tasks.", "同样的总样本,拆成两种输入": "The same sample, split by input type", @@ -996,10 +915,8 @@ "动态网页": "Dynamic web", "热门站点": "Popular sites", "文档": "Documents", - "比较对象是配置好的完整路线:Chromium 包含 DOM 转 Markdown;Moli 的文档结果包含原始文件获取和本地解析;Kitesurf 使用远端服务。输入 URL 和验收要求相同,内部处理方式可以不同。本轮每题保留一次主调用结果,失败后未重试。Moli 固定展示三个受测版本中总分最高的 1.1.1;这是看到结果后的版本选择,全部 150 题来自同一版本,没有逐题拼接最佳结果。": "The comparison covers configured complete paths: Chromium includes DOM-to-Markdown conversion; Moli document results include file retrieval and local parsing; Kitesurf uses a remote service. Input URLs and acceptance requirements match, while internal processing can differ. This run retains one primary call per task, with no retry after failure. Moli is represented by 1.1.1, the highest-scoring of its three tested versions. This version was selected after seeing the results; all 150 tasks use that single version, without picking the best version task by task.", "02 / 文档": "02 / Documents", "PDF 要交付正文,先要取得完整文件": "Delivering PDF text starts with fetching the complete file", - "文档类别中,Moli 1.1.1 完成 7/10,另外四款浏览器的受测路线均为 1/10。六个只有 Moli 完成的任务都是 PDF,包括《Attention Is All You Need》《BERT》以及固定页数的示例文档。": "Moli 1.1.1 completed 7/10 document tasks; each of the other four tested paths completed 1/10. The six tasks completed only by Moli were PDFs, including Attention Is All You Need, BERT, and fixed-page sample documents.", "案例:Attention Is All You Need · PDF": "Example: Attention Is All You Need · PDF", "Moli 获取 PDF": "Moli fetches the PDF", "本地 pypdf 解析": "Local pypdf parsing", @@ -1012,7 +929,6 @@ "03 / 图片与视频": "03 / Images and videos", "有些任务需要的,就是原文件": "Some tasks need the original file", "图片和视频任务分两类:从详情页找出目标资源地址,或者直接取得文件本身。后一类要检查文件格式、长度和哈希,确认交回的字节与目标文件一致。": "Image and video tasks either locate a resource URL on a detail page or retrieve the file itself. The latter checks file format, length, and hash to confirm that the returned bytes match the target file.", - "Moli 1.1.1 交付的三个直接文件": "Three direct files delivered by Moli 1.1.1", "图片原文件": "Original image file", "视频原文件": "Original video file", "✓ 格式、长度与哈希通过": "✓ Format, length, and hash match", @@ -1052,16 +968,12 @@ "速度比较另用一组预先选定的普通网页,并将范围收窄到全部受测路线共同完成的任务,避免拿“各自成功的不同网页”直接比较。": "Latency uses a separately preselected set of general pages, narrowed to tasks completed by every tested path. This avoids directly comparing different successful subsets.", "相同成功任务的 P50 · 越短越快": "P50 on identical successful tasks · shorter is faster", "预先选定 20 个普通网页,全部 7 条受测路线共同通过 8 个。图中只展示本文五款浏览器在这组相同任务上的 P50;P50 表示一半任务的耗时不超过该值。": "Of 20 preselected general pages, all 7 tested paths passed 8. The chart shows P50 for this article's five browsers on that identical subset; P50 means half the tasks took no longer than that value.", - "Moli 1.1.1 的 P50 比 Chromium 的 3.63 秒低约 25.8%。较慢端的 P95 则由 Lightpanda 更低。这是小样本中的端到端耗时观察,包含客户端与远端路线各自的处理开销,不能换算为生产吞吐或长期服务承诺。": "Moli 1.1.1's P50 was below Chromium's 3.63 seconds by about 25.8%. Lightpanda had the lower slow-end P95. This small-sample end-to-end observation includes each local or remote path's processing overhead; it cannot establish production throughput or a long-term service guarantee.", - "主结果调用日期:2026-09-04 至 2026-09-08(UTC)。数据范围:Moli 1.1.1 与四款浏览器的本轮受测配置,150 个任务。每项任务按预先声明的内容、结构或文件要求验收。": "Primary-call dates: 2026-09-04 to 2026-09-08 (UTC). Scope: the tested configurations of Moli 1.1.1 and four other browsers, across 150 tasks. Each task is assessed against predeclared content, structure, or file requirements.", "06 / 如何理解这份成绩": "06 / Interpreting the results", "输入内容的构成,决定这些优势有多大": "Your input mix determines how much these advantages matter", "如果工作主要是提取普通 HTML 正文,而且看重完整覆盖,Chromium 在本轮中仍是重要参照。它多完成了 2 个普通网页任务,正文找回比例和 F1 也更高。": "For work focused on general HTML body extraction and complete coverage, Chromium remains an important reference in this run. It completed 2 more general-web tasks and achieved higher body recall and F1.", - "如果输入包含已识别的 PDF、图片和视频,并配置对应处理路线,Moli 1.1.1 在这组样本中覆盖得更宽:30 个文档与媒体任务完成 24 个,Chromium 完成 14 个。": "For known PDF, image, and video inputs with corresponding processing paths configured, Moli 1.1.1 covered more of this sample: of 30 document and media tasks, it completed 24 and Chromium completed 14.", "Moli 在本轮的领先,来自更多内容类型最终形成了可用交付物。": "Moli's lead in this run comes from turning more content types into usable outputs.", "接入时,可以按自己的 URL 构成重新抽样:PDF 占比、是否需要原文件、是否常遇到验证页和 iframe,都会改变这 10 题差距对业务的意义。正文、列表和文件也各自需要对应的结果校验。": "For adoption, resample according to your own URL mix. PDF frequency, original-file requirements, verification pages, and iframes all change what the 10-task difference means for your workload. Body text, lists, and files also need their own output validation.", "后续评测将重点扩充样本,并补充热门站点的重复运行、DOCX、嵌入式查看器、延迟列表和 iframe 测试。搜索、点击、翻页和表单填写则需要独立的 Browser Use 评测,检验浏览器真正执行网页操作的表现。": "Future evaluation will expand the sample and add repeated popular-site runs, DOCX, embedded viewers, delayed lists, and iframe tests. Search, clicks, pagination, and form filling require a separate Browser Use evaluation of actual web interactions.", - "主结果调用日期:2026-09-04 至 2026-09-08(UTC)。数据范围:Moli 1.1.1 与四款浏览器的本轮受测配置,150 个任务。本文是对已保存结果的解读,未新增调用或改变评分。": "Primary-call dates: 2026-09-04 to 2026-09-08 (UTC). Scope: the tested configurations of Moli 1.1.1 and four other browsers, across 150 tasks. This article interprets saved results; it adds no calls and changes no scores.", "关键原始案例:": "Key original cases:", "PDF 正文": "PDF body text", "PNG 原文件": "Original PNG", @@ -1069,11 +981,7 @@ "完整评测、任务规则与核验记录 ↗": "Full benchmark, task contracts, and review records ↗", "本文计算明细": "Article calculation details", "增加五浏览器 Web Fetch 图文解读": "Add an illustrated five-browser Web Fetch interpretation", - "文章聚焦 Moli 1.1.1 与另外四款浏览器,按任务差异解释网页、PDF 和媒体交付结果,并提供独立 HTML 阅读文件。原始调用、评分及榜单分母不变。": "The article focuses on Moli 1.1.1 and four other browsers, explains web, PDF, and media outputs through task-level differences, and provides a standalone HTML reading file. Original calls, scores, and ranking denominators are unchanged.", "文章范围": "Article scope", - "多版本结果与案例细节分散,难以独立理解 Moli 1.1.1 的表现。": "Results across versions and scattered case details made it difficult to understand Moli 1.1.1's performance independently.", - "从同一证据生成五浏览器结果图、120/30 题分解、PDF 路径与媒体案例;明确 Moli 1.1.1 为看到版本结果后的选择。": "Generate five-browser charts, a 120/30-task breakdown, a PDF path, and media examples from the same evidence; disclose that Moli 1.1.1 was selected after seeing version results.", - "正文展示五种方法的 750 条结果;完整七路线 1,050 条证据保留,不逐题选择版本。": "The article presents 750 results from five methods; all 1,050 records across seven paths remain available, without task-by-task version selection.", "结论依据": "Basis for conclusions", "总通过数不能说明差距来自何种输入。": "The total pass count does not explain which input types contribute to the difference.", "展示 Moli 与 Chromium 在 120 个网页任务中同为 111 题,30 个文档与媒体任务分别完成 24、14 题。": "Show that, across 120 web tasks, Moli and Chromium each complete 111; across 30 document and media tasks, they complete 24 and 14 respectively.", @@ -1399,20 +1307,12 @@ "调用数": "Calls", "本轮配置:": "Configuration:", "对应接口:": "Interface:", - "未声明": "Not declared", "版本未记录": "Version not recorded", "托管后端未公开版本": "Managed backend version not disclosed", "无本地二进制": "No local binary", "运行时版本与 SHA-256": "Runtime version and SHA-256", "运行时 SHA-256;nightly 源修订不可得": "Runtime SHA-256; the nightly source revision is unavailable", "事后与官方归档同哈希;原调用未记版本字符串": "Post-run hash matches the official archive; the original call did not record a version string", - "本地浏览器 CLI": "Local browser CLI", - "本地浏览器 CDP": "Local browser over CDP", - "远程账号 API": "Remote account API", - "本地 CLI": "Local CLI", - "托管 API": "Managed API", - "项目内置": "Project built-in", - "本地开源库": "Local open-source library", "测试适配器": "Test adapter", "已纳入上述样本。": "Included in the sample above.", @@ -1455,8 +1355,6 @@ "配对证据校验": "Paired-evidence validation", "任务结果与耗时进入主榜;全文指标无完整参考时显示 —。": "Task verdict and latency enter the primary ranking; full-text metrics are shown as — without a complete reference.", "资源": "Resource", - "支持": "Supported", - "仅链接": "Link only", # Shared metric definitions and task-family explanations. "静态网站正文、动态网站正文、热门网站正文、风控网页正文和文档正文使用同一评分流程。页面来源与加载方式不同,改变的是案例要求,不改变指标定义。": "Static-site, dynamic-site, popular-site, risk-control, and document body-text tasks share one scoring pipeline. Their sources and loading behavior affect case requirements, not metric definitions.", @@ -1603,22 +1501,9 @@ "速度汇总与样本分母": "Latency summary and sample denominators", "速度子集逐次记录": "Per-call latency-cohort records", "唯一输出目录为 reports/webfetch/。固定的 Python 3.11 与 uv.lock 依赖不满足时会立即失败。重新测量使用 uv run --frozen python scripts/run_headless_benchmark.py 和 configs/providers.headless.json;先独立采集并冻结参考,再调用,最后做测后核验。完整命令与版本见仓库 harness/web_fetch/HEADLESS.md。": "The only output directory is reports/webfetch/. The build fails immediately if the pinned Python 3.11 and uv.lock environment is unavailable. Fresh measurement uses uv run --frozen python scripts/run_headless_benchmark.py and configs/providers.headless.json: capture and freeze references independently, run provider calls, then perform post-run validation. See harness/web_fetch/HEADLESS.md for complete commands and versions.", - "官方浏览器安装说明": "Official browser installation guide", - "Cloudflare 控制台": "Cloudflare dashboard", - "Kitesurf 接入说明": "Kitesurf integration guide", - "Markdown 端点": "Markdown endpoint", - "官方项目与安装说明": "Official project and installation guide", "Moli 1.0.6 正式发布": "Moli 1.0.6 release", - "Moli 1.1.1 正式发布": "Moli 1.1.1 release", "Moli 1.1.3 正式发布": "Moli 1.1.3 release", - "不需要 Google 账号或 API Key;不加载个人登录会话。": "No Google account or API key is required; personal login sessions are not loaded.", - "需要开通 Browser Rendering / Browser Run 的 Cloudflare 账号及 API Token。": "Requires a Cloudflare account with Browser Rendering / Browser Run enabled and an API token.", - "在仓库本地 .env 配置 与 ;报告和示例只保留占位符。": "Configure and in the repository-local .env; reports and examples retain placeholders only.", - "不需要供应商账号或 API Key。": "No provider account or API key is required.", - "从官方项目获取二进制,配置 LIGHTPANDA_BINARY;不使用托管服务凭据。": "Obtain the binary from the official project and configure LIGHTPANDA_BINARY; no managed-service credentials are used.", - "从官方项目获取程序及配套 worker,配置 OBSCURA_BINARY。": "Obtain the program and companion worker from the official project and configure OBSCURA_BINARY.", "从官方 v1.0.6 release 获取二进制,配置 MOLI_1_0_6_BINARY;发布归档与可执行文件均校验 SHA-256。": "Obtain the binary from the official v1.0.6 release and configure MOLI_1_0_6_BINARY; both the release archive and executable are verified by SHA-256.", - "从官方 v1.1.1 release 获取二进制,配置 MOLI_BINARY;不是 Lexmount 云服务接口。": "Obtain the binary from the official v1.1.1 release and configure MOLI_BINARY; this is not the Lexmount cloud-service interface.", "从官方 v1.1.3 release 获取二进制,配置 MOLI_1_1_3_BINARY;发布包与可执行文件均校验 SHA-256。": "Obtain the binary from the official v1.1.3 release and configure MOLI_1_1_3_BINARY; both the release package and executable are verified by SHA-256.", "发布门禁会扫描 HTML、JSON、下载文件和逐次输出中的常见 Token、私钥、Authorization 值与用户目录路径;第三方原始 HTML 中命中密钥形态的片段单独脱敏,计分输出保持逐字节不变。扫描结果和脱敏文件哈希随发布包保存。": "The publication gate scans HTML, JSON, downloads, and per-call outputs for common token forms, private keys, Authorization values, and user-directory paths. Secret-shaped fragments in third-party raw HTML are redacted separately while scored output remains byte-for-byte unchanged. Scan results and redacted-file hashes are retained with the release bundle.", @@ -1716,25 +1601,8 @@ "处理路线:参评方法直接返回正文": "Retrieval path: evaluated method returns body text directly", "下载方法直接取得的原始文档": "Download the original document obtained by the method", "本地可重复测试实现。": "Locally reproducible test implementation.", - "已知 URL → 正文 Markdown/文本,或文件任务的原始字节": "Known URL → body Markdown/text, or original bytes for file tasks", - "已知 URL → Markdown/资源地址,或原文件字节": "Known URL → Markdown/resource addresses, or original-file bytes", "Markdown/资源地址;文件题为 rawBase64 解码字节": "Markdown/resource addresses; decoded rawBase64 bytes for original-file tasks", - "maxAge=0;动态题使用预声明 Actions 等待": "maxAge=0; dynamic tasks use predeclared Actions waits", - "每次新进程/会话;45 秒端到端预算": "Fresh process/session for each call; 45-second end-to-end budget", - "正文 Markdown/文本;文件任务为原始字节;保留接口与阶段诊断": "Body Markdown/text; original bytes for file tasks; interface and stage diagnostics retained", - "给定公开 HTTP(S) URL;不登录、不点击、不滚动。": "A public HTTP(S) URL; no login, clicks, or scrolling.", - "唯一任务输入": "Only task input", - "服务端浏览器计费时间;不等于客户端耗时": "Billable server-side browser time; not client-observed latency", - "本地独立浏览器;不需要供应商 API Key": "Independent local browser; no provider API key required", - "浏览器原生 Markdown → content": "Browser-native Markdown → content", - "CLI 调用及原生序列化阶段耗时": "CLI call and native serialization latency", - "浏览器诊断;不进入正文": "Browser diagnostics; excluded from body text", - "45 秒外层截止;CLI 参数按各自单位换算": "45-second outer deadline; CLI parameters use their native units", "不点击、不滚动、不补抓资源,不在失败后切换引擎或输出模式。CLI 未提供可靠 HTTP 状态和最终 URL 时保持空值。": "No clicks, scrolling, or supplemental resource fetches, and no engine or output-mode switch after failure. HTTP status and final URL remain blank when the CLI does not provide reliable values.", - "本地独立浏览器;不需要 Google 账号,不加载个人会话": "Independent local browser; no Google account and no personal session", - "转换后的 Markdown": "Converted Markdown", - "转换前 DOM HTML 证据": "Pre-conversion DOM HTML evidence", - "分阶段耗时": "Stage-level latency", "Chromium/CDP 没有原生 Markdown 输出,因此取完整渲染 DOM 后转换;转换计入端到端耗时,HTML 与转换器版本、哈希均保留。": "Chromium/CDP has no native Markdown output, so the complete rendered DOM is converted. Conversion is included in end-to-end latency, and the HTML plus converter version and hashes are retained.", # Repeated task-contract and scoring prose. These are benchmark rules, @@ -1887,7 +1755,6 @@ "页面在 CowAgent 输出里留下页面壳,名言和作者核对点缺失;飞书帮助文档只有标题,没有要求的正文。零售退换货页则是请求被拒绝。前两例要靠内容判断才能触发后备方法,最后一例可以由请求错误触发。因此,只在网络异常时切换方法,无法复现这里看到的全部互补覆盖。": "page leaves only a page shell in CowAgent output, with quote and author checks missing; the Feishu help document contains only its title, not the required body. The retail returns page is rejected at request time. The first two need content validation to trigger a fallback, while the last can use a request error. Switching methods only on network errors therefore cannot reproduce all of the complementary coverage observed here.", "页面壳也不能直接证明正文必须经异步请求取得。对初始源文档的复核发现,": "A page shell also does not prove that the body must be fetched asynchronously. Review of the initial source documents found that", "的名言载荷和飞书文章内容已经嵌入 HTML;取回源字节与正确提取嵌入内容是两件事。它们揭示的是当前提取路径的缺口,不能作为浏览器执行能力的独立证据。": "already embeds both the quote payload and the Feishu article content in HTML. Retrieving source bytes and correctly extracting embedded content are distinct tasks. These cases expose a gap in the current extraction path; they are not independent evidence that browser execution is required.", - "不同方法并行,同一方法串行;正文结束后再测其他任务。Firecrawl 每次完成后冷却 6.1 秒。本报告不是 WCXB 官方榜单。": "Different methods run in parallel while calls within each method are serial. Non-body tasks run after body-text tasks. Firecrawl cools down for 6.1 seconds after each call. This report is not an official WCXB leaderboard.", "十二方法机器失败项盲审与分歧裁决": "Blind review and adjudication of machine failures across twelve methods", "失败项盲审发现 2 条机器判定与独立审查分歧,均为 Exa 正文名称与另一组裸 URL 被审查者按顺序推断成具名链接。": "Failure-only blind review found 2 disagreements with machine verdicts. In both, the reviewer inferred named Exa links by positionally joining body names to a separate array of bare URLs.", "两条分歧分别由独立代理回到任务合同、完整返回和原生字段裁决;由于输出没有明确的名称—URL 配对,均维持机器失败。审查记录保存模型、协议、证据绑定与裁决理由。": "Independent agents adjudicated both disagreements against the task contract, complete response, and native fields. Because neither output explicitly paired names with URLs, both machine failures were retained. The review record preserves the model, protocol, evidence bindings, and adjudication reasons.", @@ -2052,7 +1919,6 @@ "浏览器执行上下文中断": "Browser execution context interrupted", # Review-round records. These are public release evidence, not internal logs. - "主结果 1,050/1,050;同窗测速 140/140;代理分歧 0;已裁定 0;未决 0。": "Primary results 1,050/1,050; same-window latency 140/140; agent disagreements 0; adjudicated 0; unresolved 0.", "五方法初次逐项复核": "Initial item-level review of five methods", "五种方法的 750 条主结果。": "750 primary results across five methods.", "排版空白、Markdown 链接换行等格式差异造成假失败,也有不完整正文被放行。": "Formatting differences such as whitespace and Markdown link wrapping caused false failures, while some incomplete body text had been accepted.", @@ -2064,7 +1930,6 @@ "直接文件改用裸 fetch;统一按任务交付物验收,并对全部输出盲审。": "Changed direct files to bare fetch, aligned acceptance on the required task deliverable, and blind-reviewed every output.", "无未决判定;当时尚未纳入 Moli 1.0.6。": "No unresolved verdicts; Moli 1.0.6 was not yet in scope.", "七方法矩阵复核": "Seven-method matrix review", - "七种方法的 1,050 条主结果和 140 条同窗测速。": "1,050 primary results and 140 same-window latency results across seven methods.", "加入 Moli 1.0.6 后需要同口径比较;部分高变化网页参考不适合长期复现。": "Adding Moli 1.0.6 required an equivalent comparison, and several highly volatile web references were unsuitable for long-term reproduction.", "三个 Moli 版本统一路线;八题换为不可变来源,live-025 重新观测并重测。": "Aligned the retrieval path across all three Moli versions, moved eight tasks to immutable sources, and re-observed and reran live-025.", "无未决判定。": "No unresolved verdicts.", @@ -2078,20 +1943,16 @@ "默认证据切换为七方法最终证据;增加当前报告配置门禁,并把路线说明与实测实现绑定。": "Changed the default to the final seven-method evidence, added a current-report configuration gate, and bound path descriptions to measured implementations.", "无。": "None.", "版本差异与样本可访问性复核": "Version-difference and sample-accessibility review", - "Moli 1.1.1/1.1.3 在 document-03、challenge-01 上的三轮配对诊断;京东搜索页的当前浏览器复查。": "Three paired diagnostic rounds for Moli 1.1.1/1.1.3 on document-03 and challenge-01, plus a current browser check of the JD search page.", - "document-03 两版重复调用均成功;challenge-01 两版均出现空响应,1.1.3 的三轮都失败而 1.1.1 仅一轮成功。京东两条搜索地址在已有浏览器会话等待后可打开,但正式新会话调用仍可能 DNS 失败或只取得不合格页面。": "Repeated document-03 calls succeeded for both versions. Both produced empty responses on challenge-01: all three 1.1.3 rounds failed, while 1.1.1 succeeded once. Both JD search URLs could open after waiting in an existing browser session, but fresh formal calls may still fail DNS or return an unacceptable page.", "保留正式单次调用和原分数;报告不把两题差距归因为稳定版本回归,也不因当前会话可访问而改写历史结果。京东继续作为会话与网络敏感的热门站点挑战样本,不冒充确定性能力样本。": "Retained the formal single calls and original scores. The report does not attribute the two-task difference to a stable version regression or rewrite historical results because the current session can access the page. JD remains a session- and network-sensitive popular-site challenge rather than a deterministic capability sample.", "版本稳定性需要预先设计的多轮重复实验;本报告的主榜仍是每题一次调用。": "Version stability requires a preregistered repeated-run experiment; this report's primary matrix still uses one call per task.", "Moli 时延回退线索复核": "Moli latency-regression signal review", "issue #66 指向的版本时延差异;四个正式差异较大的代表任务各三轮顺序轮换复测,另以本地固定 HTML 各五轮分离启动与转换开销。": "Version latency differences reported in issue #66: three order-rotated rounds on four representative tasks with large formal differences, plus five local fixed-HTML rounds per version to separate startup and conversion overhead.", - "36 次公网调用均成功。1.1.3 相对 1.1.1 的任务中位数分别为 WebM 慢 12%、RFC 文本慢 18%、HTML 表格慢 13%,动态页快 6%;单版本 HTML 表格仍可从约 5.9 秒波动到 32.7 秒。本地固定页中位数为 1.0.6 65 ms、1.1.1 56 ms、1.1.3 57 ms。": "All 36 public-network calls succeeded. Relative to 1.1.1, 1.1.3 median latency was 12% slower for WebM, 18% slower for RFC text, 13% slower for the HTML table, and 6% faster for the rendered page. A single version's HTML-table calls still ranged from about 5.9 to 32.7 seconds. Local fixed-page medians were 65 ms for 1.0.6, 56 ms for 1.1.1, and 57 ms for 1.1.3.", "把 issue 定位为需要继续追踪的公网抓取长尾线索,而不是已确认的版本回退;报告补充诊断范围、轮换顺序和本地对照,不改变正式一次调用结果。": "Classified the issue as a public-network tail-latency signal requiring further study, not a confirmed version regression. Added diagnostic scope, rotation order, and local controls without changing formal single-call results.", "现有适配器的原生抓取计时仍同时包含网络、页面等待和浏览器内部处理,尚不能把公网差异拆成单一根因;若要形成产品回退结论,应另建预注册多轮性能实验。": "The current adapter's native-fetch timing still combines network transfer, page waiting, and browser-internal processing, so public-network differences cannot be reduced to one cause. A product-regression conclusion requires a separate preregistered repeated-run performance experiment.", "Moli 时延异常纠正": "Moli latency-anomaly correction", "四个差异最大的代表任务、三个 Moli 版本;三轮轮换共 36 次物理调用,纠正 12 条权威逻辑结果的耗时。": "Four representative tasks with the largest differences across three Moli versions; 36 physical calls in three rotated rounds corrected latency for 12 authoritative logical results.", "旧主矩阵的少量极端耗时会显著扭曲各版本整体均值和 P95;这些差异不能由内容得分或版本固定开销解释。": "A few extreme latencies in the old primary matrix materially distorted each version's overall mean and P95; content scores and fixed version overhead could not explain them.", "固定同一四题和三个版本,统一采用第三轮而非各自最快轮;只替换耗时与阶段计时,正文、哈希、评分和判定保持不变。旧值、36 次调用和选择规则全部保留在纠正回执中。": "Fixed the same four tasks and three versions and selected round three uniformly rather than each version's fastest round. Only elapsed and stage timing changed; body text, hashes, scores, and verdicts remain unchanged. Old values, all 36 calls, and the selection rule are retained in the correction receipt.", - "这仍是针对已识别异常的纠正,不是全 150 题重复性能实验;主榜延迟可用于本轮观察,但不能外推为稳定尾延迟或 SLA。": "This remains a correction of identified anomalies, not a repeated performance experiment across all 150 tasks. Primary-ranking latency supports observations for this run but cannot be extrapolated to stable tail latency or an SLA.", # Reproduction download labels. "评测集与参考": "Dataset and references", @@ -2104,12 +1965,9 @@ "时延纠正收据": "Latency-correction receipt", "请求状态纠正收据": "Request-status correction receipt", "二进制 SHA-256": "Binary SHA-256", - "最终报告纳入 1,050 次调用。任务与参考冻结后执行;每个方法内部串行,不复用个人登录状态。": "The final report includes 1,050 calls. Execution began after tasks and references were frozen; calls were sequential within each method and did not reuse personal login state.", "与": "and", # Interpretation findings. - "静态网页通过数:chromium 99/100;kitesurf 88/100;lightpanda 92/100;Moli 1.1.1 97/100;Moli 1.0.6 97/100;Moli 1.1.3 97/100;obscura 90/100。综合榜按 150 个任务等权计数;六类分榜仍分别解释,耗时和文字质量不混入综合通过率。": "General-web passes: chromium 99/100; kitesurf 88/100; lightpanda 92/100; Moli 1.1.1 97/100; Moli 1.0.6 97/100; Moli 1.1.3 97/100; obscura 90/100. The overall ranking counts all 150 tasks equally. Each of the six family rankings is interpreted separately, and neither latency nor text quality is blended into overall task pass rate.", - "三版使用相同路线;当前总通过数为 Moli 1.0.6 134/150、Moli 1.1.1 135/150、Moli 1.1.3 133/150,文档正文分别为 7/10、7/10、6/10。本轮没有显示版本越新通过越多。三轮独立配对诊断中,document-03 两版均为 3/3;challenge-01 的 1.1.1 为 1/3、1.1.3 为 0/3,且失败均为空响应。另对正式时延差异最大的四题做三轮顺序轮换复测:1.1.3 相对 1.1.1 的各题中位数从快 6% 到慢 18%,区间大量重叠;本地固定页五轮中位数则为 57 ms 对 56 ms。这说明公网长尾主要发生在原生抓取与页面等待阶段,可能仍含版本策略差异,但尚不能归因为核心性能普遍回退。纠正后的主榜对三版相同四题统一采用第三轮计时,不按最快结果挑选;此前计时及全部三轮仍由纠正收据绑定。": "All three versions use the same retrieval path. Current overall passes are Moli 1.0.6 134/150, Moli 1.1.1 135/150, and Moli 1.1.3 133/150; document-text passes are 7/10, 7/10, and 6/10. This run does not show a monotonic improvement with version number. In three independent paired diagnostic rounds, both compared versions scored 3/3 on document-03; on challenge-01, 1.1.1 scored 1/3 and 1.1.3 scored 0/3, with every failure producing an empty response. Three order-rotated reruns on the four largest formal latency differences put 1.1.3 task medians between 6% faster and 18% slower than 1.1.1, with substantial overlap; five local fixed-page rounds measured 57 ms versus 56 ms. Public-network tails therefore occur mainly during native retrieval and page waiting and may still include policy differences, but do not establish a general core-performance regression. The corrected primary ranking uses round three uniformly for the same four tasks across all three versions, never each version's fastest response; prior timing and every diagnostic round remain bound by the correction receipt.", "Wikipedia 单页实测。原生路径不会虚构 HTML/转换拆分;Cloudflare 的 browser_ms_used 是服务端计费诊断,不等于客户端端到端耗时;超时缺少阶段数据时记为 —。": "Single-page Wikipedia measurement. Native-output paths do not invent an HTML/conversion split. Cloudflare browser_ms_used is a server-side billing diagnostic, not client-observed end-to-end latency; missing stage data on timeout is shown as —.", "远端 Kitesurf 包含公网往返、排队与远端执行;本地方法包含进程启动和本机资源竞争。不同批次的耗时不直接排名;速度结论只使用同一时间窗口、双方共同通过的样本。它衡量客户端拿到合格 Markdown 的时间,不是纯内核 CPU、长期吞吐或 SLA。": "Remote Kitesurf includes public-network round trips, queueing, and remote execution; local methods include process startup and local resource contention. Latency from different batches is not ranked directly. Speed conclusions use only tasks that both methods passed in the same time window. The metric is time for the client to receive acceptable Markdown—not isolated engine CPU time, sustained throughput, or an SLA.", "每次新进程或新上下文,不复用个人会话;本轮每题一次且未重试。后续若预先启用一次失败重试,保留两次物理证据并固定采用最后一次,禁止择优": "Use a fresh process or context for every call without reusing personal sessions. This run made one call per task with no retries. A future run may predeclare one failure retry, but must retain both physical calls and use the final response rather than selecting the better one.", @@ -2118,7 +1976,6 @@ "同窗速度子集:预先声明 20 题,7 种方法共同通过 8 题;所有方法使用完全相同的成功样本。样本少于 20 时,P95 只作本次样本诊断,不视为稳定尾延迟。": "Same-window latency cohort: 20 tasks were declared in advance and all 7 methods passed 8; every method is compared on exactly the same successful sample. With fewer than 20 observations, P95 is only a diagnostic for this sample and not a stable tail-latency estimate.", "逐次词级指标的算术平均,包含已测失败调用;未通过任务不会把已有重合归零。完整度与纯净度均受已声明参考范围约束。": "Arithmetic means of per-call text metrics include measured calls that failed task acceptance; a failed task does not erase observed overlap. Both recall and precision are bounded by the declared reference scope.", "完整度、纯净度与 F1 是参考范围内的词级诊断,不证明逐句语义或实时事实。任务未通过时,已取回的有效文字也不会被强制记为零;没有完整参考的题显示未测。": "Recall, precision, and F1 are text-overlap diagnostics within the reference scope; they do not establish sentence-level meaning or current facts. Useful text recovered by a failed task is not forced to zero, and tasks without complete references are shown as unmeasured.", - "动态任务通过数:chromium 8/10;kitesurf 8/10;lightpanda 8/10;Moli 1.1.1 8/10;Moli 1.0.6 8/10;Moli 1.1.3 8/10;obscura 8/10。允许浏览器渲染、读取页面内嵌状态或使用页面公开数据接口;不允许为单题追加特例、失败后切换实现,或把另一个抓取器的结果算给当前方法。": "Rendered-web passes: chromium 8/10; kitesurf 8/10; lightpanda 8/10; Moli 1.1.1 8/10; Moli 1.0.6 8/10; Moli 1.1.3 8/10; obscura 8/10. Browser rendering, embedded page state, and public page data endpoints are valid. Per-task exceptions, switching implementations after failure, and assigning another retriever's result to the current method are not.", "动态网页逐例验收。延迟、异步取数与 iframe 分别检查;固定 DOMContentLoaded 不是内容完成判据。": "Rendered-web tasks are accepted individually. Delayed presentation, asynchronous data, and iframes are checked separately; a fixed DOMContentLoaded event is not evidence that content is complete.", "主结果全部输出 1050/1050;同窗测速全部输出 140/140;未决 0。成功项与失败项均已逐条独立核验,轮次与修正见“核验记录”。": "Primary outputs are complete at 1050/1050; same-window latency outputs are complete at 140/140; unresolved items: 0. Both passing and failing results received independent item-level review. Review rounds and corrections appear on the Review page.", "报告保存任务 ID、原始输出、哈希、运行 ID、方法版本或版本缺口、参数、计时与参考来源。正文分区页直接展示每段标准答案、最低要求、实际命中数和缺失词。环境、批次、代码与命令集中在“环境与复现”,不在多页重复。": "The report retains task ID, raw output, hashes, run ID, method version or version gap, parameters, timing, and reference source. Body-region pages show each reference region, minimum requirement, observed matches, and missing text directly. Environment, batches, code, and commands are centralized on Reproduction rather than repeated across pages.", @@ -2128,14 +1985,9 @@ "Kitesurf 使用 Cloudflare 账户级 Browser Run /markdown API;耗时包含公网往返、远端排队与执行。": "Kitesurf uses Cloudflare's account-level Browser Run /markdown API. Latency includes public-network round trips, remote queueing, and execution.", "Lightpanda、Obscura 与三个 Moli 版本的普通网页使用 CLI 正文输出。三个 Moli 版本的直接文档和媒体 URL 均使用裸 fetch:正文任务在预算内解析,文件任务校验字节。": "Lightpanda, Obscura, and all three Moli versions use CLI body output for ordinary web pages. Direct document and media URLs use bare fetch for every Moli version: body tasks parse within the deadline, while file tasks validate bytes.", "Chromium 152 由 Playwright 取完整渲染 DOM,再用 node-html-markdown 2.0.0 转换。七种方法都不调用模型。": "Playwright retrieves the complete rendered DOM from Chromium 152, then node-html-markdown 2.0.0 converts it. None of the seven methods calls a model.", - "Lightpanda、Obscura 与 Moli 1.1.1 的普通网页使用 CLI 正文输出。Moli 的直接文档和媒体 URL 使用裸 fetch:正文任务在预算内解析,文件任务校验字节。": "Lightpanda, Obscura, and Moli 1.1.1 use CLI body output for general pages. Moli uses bare fetch for direct document and media URLs: body tasks parse within the budget, while file tasks validate bytes.", "截至 2026-09-07,Cloudflare 尚未发布 Kitesurf 源码或自行部署包;本轮只能通过 Cloudflare 账号版 Browser Run 远程测试,不能把它当作本地开源浏览器。": "As of 2026-09-07, Cloudflare has not released Kitesurf source code or a self-hosting package. This benchmark can test it only remotely through account-level Browser Run, so it is not treated as a local open-source browser.", "Cloudflare 官方博客表示计划在准备好后开源,并希望允许客户部署到自己的账户。这是未来计划,不是当前已有能力。": "Cloudflare's official blog states an intention to open-source Kitesurf when ready and to let customers deploy it in their own accounts. That is a future plan, not a current capability.", - "Playwright 启动 Chromium,取完整渲染 DOM,再由 node-html-markdown 2.0.0 生成 Markdown。": "Playwright starts Chromium and captures the complete rendered DOM, which node-html-markdown 2.0.0 converts to Markdown.", - "Cloudflare Browser Run 账户级 Markdown 端点,指定 browser=kitesurf,直接返回 Markdown。": "Cloudflare Browser Run account-level Markdown endpoint with browser=kitesurf, returning Markdown directly.", - "Lightpanda fetch --dump markdown,直接输出浏览器原生 Markdown。": "Lightpanda fetch --dump markdown returns browser-native Markdown directly.", "普通网页使用 fetch --dump markdown;直接文档和媒体使用裸 fetch。原始文档的本地正文解析计入耗时,文件任务直接校验字节。": "Ordinary web pages use fetch --dump markdown; direct documents and media use bare fetch. Local body extraction from an original document is included in latency, while file tasks validate bytes directly.", - "Obscura fetch --dump markdown;标准模式,不启用 stealth 或备用引擎。": "Obscura fetch --dump markdown in standard mode, without stealth or a fallback engine.", "下表介绍本轮实际调用的实现;方法名称可打开其全部已测案例。各组覆盖数量以执行记录为准,功能范围以本轮配置为准。": "The table describes the implementations actually called in this benchmark. A method name opens all of its measured tasks. Execution records determine coverage, and the measured configuration determines capability scope.", # Methodology examples (illustrative report prose, not saved references). @@ -2228,30 +2080,19 @@ "方法 / 依据": "Method / basis", "图片资源": "Image resources", "音视频资源": "Audio and video resources", - "渲染 DOM 转 Markdown": "Rendered DOM to Markdown", - "本轮适配器没有 PDF / Office 解析器,不据此推断产品能力": "This run's adapter has no PDF / Office parser; no broader product limitation is inferred", - "取决于 DOM 与转换器保留": "Depends on preservation by the DOM and converter", - "本轮没有媒体解析,不据此推断产品能力": "This run performs no media parsing; no broader product limitation is inferred", - "本轮异常二进制输出仅保留证据": "Anomalous binary output from this run is retained as evidence only", - "原生 Markdown": "Native Markdown", "直接取得 Moli 原生 Markdown": "Retrieve Moli native Markdown directly", "直接取得 Moli 原生 Markdown;直接 PDF/DOCX 不提供可读正文": "Retrieve Moli native Markdown directly; direct PDF/DOCX inputs do not yield readable body text", "通过 Moli 原生执行接口读取已加载内容;表达式只由公开任务类型决定": "Read loaded content through Moli's native execution interface; expressions are selected only by public task type", "Markdown 正文统一使用 Moli 默认 done 完成方式": "Markdown body uses Moli's default done completion mode", - "默认启用": "Enabled by default", "普通、动态、热门、风控和直接文档正文统一使用 Moli 原生 Markdown。文件使用原生 CDP 响应或同一二进制的原始附件接口。图片、视频资源和嵌入文档只通过 Moli 原生执行接口读取已加载内容,不另抓模块或下载文档后外部解析。最终网址仅作诊断,不决定正文是否通过;失败后不切换格式或借用其他 provider。": "Regular, dynamic, popular-site, risk-control, and direct-document body tasks use Moli's native Markdown. Files use native CDP responses or the same binary's raw attachment interface. Image and video resources and embedded documents use only Moli's native execution interface to inspect loaded content; the runner does not fetch modules separately or parse downloaded documents externally. The final URL is diagnostic only and does not determine whether body extraction passes. A failure does not switch format or borrow another provider.", "普通、动态、热门、风控及 HTML/TXT 文档正文使用 Moli 原生 Markdown。直接 PDF/DOCX 仍调用同一原生接口;接口没有交付可读正文时按失败记录,评测端不下载后解析。文件使用原生 CDP 响应或同一二进制的原始附件接口。图片、视频资源和嵌入文档只通过 Moli 原生执行接口读取已加载内容。最终网址仅作诊断,不决定正文是否通过;失败后不切换格式或借用其他 provider。": "Regular, dynamic, popular-site, risk-control, and HTML/TXT document-body tasks use Moli's native Markdown. Direct PDF/DOCX inputs still use that native interface; if it does not return readable body text, the task fails and the benchmark does not download and parse the file. Files use native CDP responses or the same binary's raw attachment interface. Image and video resources and embedded documents use only Moli's native execution interface to inspect loaded content. The final URL is diagnostic only and does not determine whether body extraction passes. A failure does not switch format or borrow another provider.", - "CLI 原生 Markdown": "Native Markdown from CLI", "CLI · 按任务预选 Markdown、DOM eval 或裸响应路线": "CLI · preselect Markdown, DOM eval, or bare-response routes by task", "CLI · 按任务预选 Markdown、DOM eval 或原始响应路线": "CLI · preselect Markdown, DOM eval, or raw-response routes by task", "CLI · 按任务预选 Markdown、JSON 诊断、DOM eval 或裸响应路线": "CLI · preselect Markdown, JSON diagnostics, DOM eval, or bare-response routes by task", "Moli 一次性 fetch:普通正文用 Markdown,挑战题用 JSON 返回最终 DOM、状态和跳转诊断后在同一截止内本地转 Markdown;不调用模型。": "Moli performs one fetch per task: ordinary body tasks use Markdown, while challenge tasks return final-DOM JSON, status, and redirect diagnostics before local Markdown conversion within the same deadline; no model is called.", "stdout 文本/JSON/字节;stderr 独立诊断": "Text, JSON, or bytes on stdout; separate diagnostics on stderr", "每次启动独立进程;使用预声明的有界完成条件": "Starts a separate process for each call and uses predeclared bounded completion conditions", - "检出并固定 DeepSeek Harness 源码、Node、pnpm 与构建依赖;本轮不调用聊天模型。": "Check out and pin the DeepSeek Harness source, Node, pnpm, and build dependencies; this run does not call a chat model.", - "检出并固定 OpenClaw 源码、Node 与 pnpm;本轮关闭 Firecrawl 回退。": "Check out and pin the OpenClaw source, Node, and pnpm; the Firecrawl fallback is disabled for this run.", "moli fetch;普通正文使用 Markdown,挑战题使用 JSON 最终 DOM 与网络诊断,动态/资源/嵌入文档使用有界 DOM eval,直接文档与媒体使用裸 fetch 自动响应": "moli fetch; ordinary body tasks use Markdown, challenge tasks use final-DOM JSON with network diagnostics, dynamic/resource/embedded-document tasks use bounded DOM eval, and direct documents and media use automatic bare responses", - "公开 HTTP(S) 页面或直接文件": "Public HTTP(S) page or direct file", "普通正文": "Ordinary body text", "动态 / 资源页 / 嵌入文档": "Dynamic / resource-page / embedded-document tasks", "一次返回有界记录文本、结构化 URL 或页面运行时文档": "Returns bounded record text, structured URLs, or a page-runtime document in one call", @@ -2260,60 +2101,28 @@ "普通网页使用预声明的生命周期或动态完成条件": "Ordinary pages use a predeclared lifecycle or dynamic completion condition", "毫秒;导航、HTTP、进程与本地文档解析共享同一端到端预算": "Milliseconds; navigation, HTTP, process execution, and local document parsing share one end-to-end budget", "默认启用;仅对冻结公开测试域名的精确白名单省略": "Enabled by default; omitted only for an exact allowlist of frozen public benchmark hosts", - "Markdown、eval 结果或直接文件原始字节": "Markdown, eval results, or original bytes from a direct file", - "0 表示工具请求完成;正文或文件为空仍由任务规则单独判失败": "Exit code 0 means the tool request completed; empty text or file output still fails the task under its separate acceptance rules", - "可能未提供": "May be unavailable", - "CLI 未提供时保持未知,不伪造 HTTP 200 或最终 URL": "Remains unknown when the CLI does not provide it; no HTTP 200 status or final URL is invented", "普通正文使用 Markdown;动态、资源页和嵌入文档使用一次有界 DOM eval;直接 PDF、DOCX 与媒体文件在调用前选择不带 --dump 和页面等待参数的原始响应路线。直接文档正文在同一截止预算内解析,文件任务按统一字节规则验收;不在失败后切换格式或借用其他 provider。": "Ordinary body tasks use Markdown. Dynamic, resource-page, and embedded-document tasks use one bounded DOM eval. Direct PDF, DOCX, and media inputs preselect a raw-response route without --dump or page-wait flags. Direct-document text is parsed within the same deadline, and file tasks use shared byte-level validation; no post-failure format switch or cross-provider fallback is used.", "普通正文使用 Markdown;动态完成条件与正文提取范围分离。直接 PDF 与媒体使用不带 --dump 的原始响应路线;DOCX 因当前 Moli CLI 的 OOXML 误分类,在同一次 CLI 运行中由页面运行时二次 fetch 原始字节并明确披露。文档解析计入同一截止预算;私网保护默认开启,只对冻结公开测试域名使用精确白名单;不在失败后切换格式或借用其他 provider。": "Ordinary body tasks use Markdown, and dynamic completion is separate from extraction scope. Direct PDF and media inputs use raw responses without --dump. Because the current Moli CLI misclassifies OOXML, DOCX tasks make a disclosed second fetch for the original bytes from the page runtime within the same CLI invocation. Document parsing remains inside the shared deadline. Private-network protection stays enabled by default, with exact exceptions only for frozen public benchmark hosts; no post-failure format switch or cross-provider fallback is used.", "普通正文使用 Markdown;挑战题使用一次 JSON 请求返回最终 DOM、HTTP 状态、最终 URL 与有界网络诊断,并在同一截止内本地转 Markdown;动态、资源页和嵌入文档使用一次有界 DOM eval;直接 PDF、DOCX 与媒体文件在调用前选择不带 --dump 和页面等待参数的原始响应路线。直接文档正文在同一截止预算内解析,文件任务按统一字节规则验收;不在失败后切换格式或借用其他 provider。": "Ordinary body tasks use Markdown. Challenge tasks use one JSON request to return the final DOM, HTTP status, final URL, and bounded network diagnostics, then convert that DOM to Markdown locally within the same deadline. Dynamic, resource-page, and embedded-document tasks use one bounded DOM eval. Direct PDF, DOCX, and media inputs preselect a raw-response route without --dump or page-wait flags. Direct-document text is parsed within the same deadline, and file tasks use shared byte-level validation; no post-failure format switch or cross-provider fallback is used.", - "本轮 advanced": "advanced in this run", "使用官方高级提取路线;HTTP 200 但没有 raw_content 仍按适配器请求失败记录。原始响应在私有证据边界独立哈希,公开报告只保留规范化结果与绑定摘要。": "The run uses the documented advanced extraction route. An HTTP 200 response without raw_content is still recorded as an adapter request failure. The raw response is independently hash-bound as private evidence; the public report retains only the normalized result and binding summary.", - "本轮 full,要求完整正文而非 compact 默认值": "full in this run, requesting complete body text instead of the compact default", "没有正文或逐网址状态失败,不能仅凭 HTTP 200 判成功;原始 API 响应只进入私有哈希绑定证据,不随公开报告发布。": "Missing body text or a per-URL failure status cannot pass solely because the HTTP response is 200. Raw API responses remain private hash-bound evidence and are not published with the report.", - "非动态网页使用本地无头 Chrome,并在 DOMContentLoaded 后提取;动态任务仍按预声明完成条件等待。直接 PDF 正文在调用前选择 PDFCrawlerStrategy 与 PDFContentScrapingStrategy,文件交付任务选择 AsyncHTTPCrawlerStrategy 并统一验收 downloaded_files。媒体链接不等于下载;文件字节校验不等于理解媒体。": "Non-dynamic pages use local headless Chrome and extract after DOMContentLoaded; dynamic tasks still wait for their predeclared completion condition. Direct PDF body tasks preselect PDFCrawlerStrategy with PDFContentScrapingStrategy, while file-delivery tasks use AsyncHTTPCrawlerStrategy and validate downloaded_files with the shared checks. A media link is not a download, and byte validation is not media understanding.", - "非动态网页为 domcontentloaded;动态任务使用预声明值": "domcontentloaded for non-dynamic pages; the predeclared value for dynamic tasks", - "需配置": "Configuration required", - "裸 fetch 取得原始 PDF 后在同一预算内本地解析;Office 取决于实际响应": "A bare fetch retrieves the original PDF for local parsing within the same budget; Office support depends on the actual response", - "裸 fetch 可自动返回直接非 HTML 响应的原始字节": "A bare fetch can automatically return the original bytes of a direct non-HTML response", - "默认运行 JS,支持多种就绪等待": "Runs JavaScript by default with multiple readiness waits", - "复杂页建议 advanced": "advanced is recommended for complex pages", - "Contents 动态页抓取": "Contents retrieval for dynamic pages", "浏览器 + 等待参数": "Browser plus wait parameters", - "浏览器 + JS 动作": "Browser plus JavaScript actions", "裸 fetch 取得原始 PDF 后在预算内解析;Office 格式取决于实际响应": "A bare fetch retrieves the original PDF and parses it within the budget; Office support depends on the actual response", - "网页资源取决于 Markdown 保留;直接 URL 可验收原始字节": "Page resources depend on Markdown preservation; direct URLs can be validated as original bytes", - "裸 fetch 返回直接 PDF、PNG、WebM、MP4 等原始响应": "A bare fetch returns original responses for direct PDF, PNG, WebM, MP4, and similar inputs", "JS 渲染": "JavaScript rendering", "输入范围": "Input coverage", "结构化输出": "Structured output", "访问与会话": "Access and sessions", - "统一 Markdown;不要求引擎按评分答案生成结构。": "Normalized Markdown; engines are not required to generate structures tailored to the scoring answer.", - "每次新会话;Kitesurf 使用 Cloudflare 账号版远程 Browser Run API,其余为本地进程。": "A fresh session for every call; Kitesurf uses the account-level remote Cloudflare Browser Run API, while the others use local processes.", "普通网页输出 Markdown;直接文档或媒体输出序列化 HTML 或原始字节。": "Ordinary pages produce Markdown; direct documents or media produce serialized HTML or original bytes.", - "每次新进程;本地二进制,不使用托管服务凭据。": "A fresh process for every call; local binaries use no managed-service credentials.", "模型生成的摘要、OCR 文字、网页截图、资源 URL 与原始字节是不同产物;均不能自动当作独立标准答案。原文件实测仍需核验 MIME、字节数和 SHA-256。访问被拒绝只说明该次访问受限,不等于网站无效或所有 provider 均不支持。": "Model-generated summaries, OCR text, page screenshots, resource URLs, and original bytes are different deliverables; none automatically becomes an independent reference answer. Original-file measurements still validate MIME type, byte count, and SHA-256. A denied request shows only that this access attempt was restricted; it does not mean the site is invalid or every provider is unsupported.", "接口边界与依据": "Interface boundaries and evidence", "点击方法名定位;展开查看本轮启用了什么,以及哪些属于其他接口或配置。": "Select a method name to jump to its profile; expand it to see what was enabled in this run and what belongs to other interfaces or configurations.", - "这里只声明本轮调用模式。没有启用文档解析、截图、下载、OCR 等模式,不能从本轮失败推断产品没有这些能力。": "Only the invocation mode used in this run is described. Document parsing, screenshots, downloads, OCR, and other modes were not enabled, so failures here do not establish that the product lacks those capabilities.", - "DOMContentLoaded;45 秒端到端预算;完整 DOM 经 node-html-markdown 2.0.0 转换。": "DOMContentLoaded; 45-second end-to-end budget; the complete DOM is converted with node-html-markdown 2.0.0.", - "Cloudflare 账户级 /markdown 原生输出": "Native output from Cloudflare's account-level /markdown endpoint", - "networkidle2;45 秒客户端预算;直接使用账户级 /markdown。": "networkidle2; 45-second client budget; direct use of the account-level /markdown endpoint.", - "lightpanda fetch --dump markdown 原生输出": "Native output from lightpanda fetch --dump markdown", - "CLI 自带等待;45 秒外层预算;直接使用原生 Markdown。": "CLI-managed waiting; 45-second outer budget; native Markdown used directly.", "普通网页原生 Markdown;直接文档与媒体使用裸 fetch 自动响应;原始文档的正文解析计入任务耗时": "Native Markdown for ordinary pages; automatic bare-fetch responses for direct documents and media; body parsing from original documents is included in task latency", "moli fetch;普通正文使用 Markdown,动态/资源/嵌入文档使用有界 DOM eval,直接文档与媒体使用裸 fetch 自动响应": "moli fetch; ordinary body tasks use Markdown, dynamic/resource/embedded-document tasks use bounded DOM eval, and direct documents and media use automatic bare-fetch responses", "Moli 1.1.4 · 输出与接口边界 ↗": "Moli 1.1.4 · output and interface boundaries ↗", "Moli 1.1.4 · 官方发布 ↗": "Moli 1.1.4 · official release ↗", - "截图和 PDF 是启用 --layout 后生成的页面产物,不是读取已有文档或下载原媒体。官方未承诺文档 OCR、媒体理解或音频转写;直接非 HTML 输入不强制使用 Markdown。": "Screenshots and PDFs generated with --layout are page artifacts, not reads of existing documents or downloads of original media. The official contract does not promise document OCR, media understanding, or audio transcription; direct non-HTML inputs are not forced through Markdown.", "本轮 advanced + markdown + include_images=true,不传 query;未测试 Research 文件上传。": "This run uses advanced + markdown + include_images=true without query; Research file upload is not tested.", "text.verbosity=full、maxAgeHours=0,并取 links/imageLinks;本轮没有调用摘要或高亮生成。": "This run uses text.verbosity=full and maxAgeHours=0 and collects links/imageLinks; summary and highlight generation are not called.", - "text.verbosity=full、maxAgeHours=0;资源题取 links/imageLinks,具名列表题取 richLinks 并只接收非空 anchor—URL 对。本轮没有调用摘要或高亮生成。": "This run uses text.verbosity=full and maxAgeHours=0. Resource tasks collect links/imageLinks; named-list tasks collect richLinks and accept only non-empty anchor–URL pairs. Summary and highlight generation are not called.", "只调用 /markdown;普通网页使用 networkidle2,动态任务使用预声明 selector 和有界稳定等待;未提供源站登录 Cookie,未调用其他端点。": "Only /markdown is called. Ordinary pages use networkidle2, while dynamic tasks use a predeclared selector and bounded settling wait. No origin-login cookie is supplied and no other endpoint is called.", - "非动态网页使用通用浏览器策略、domcontentloaded 与无缓存;动态任务使用预声明的完成条件。直接 PDF 正文在调用前选择 PDFCrawlerStrategy 与 PDFContentScrapingStrategy;文件任务选择原生 HTTP 文件策略并统一校验。未启用截图、OCR 或 LLM 提取;媒体字段不代表内容理解。": "Non-dynamic pages use the general browser strategy with domcontentloaded and no cache; dynamic tasks use their predeclared completion condition. Direct PDF body tasks preselect PDFCrawlerStrategy with PDFContentScrapingStrategy; file tasks select the native HTTP file strategy and shared validation. Screenshots, OCR, and LLM extraction are not enabled, and media fields do not imply content understanding.", - "正文与文件是不同交付契约。文档题要求 Moli 的原生 Markdown 接口直接交付可读正文;只返回 PDF/DOCX 字节或地址不通过,评测端不再外部解析。直接媒体文件只校验格式、长度和冻结 SHA-256,不进行内容理解。": "Body text and files have different delivery contracts. Document tasks require Moli's native Markdown interface to return readable body text directly. PDF or DOCX bytes or a URL alone do not pass, and the benchmark does not parse them externally. Direct media files are checked only for format, length, and frozen SHA-256, without content understanding.", - "网页与文档正文使用 --dump markdown;直接媒体文件返回原始字节;本轮没有启用失败重试,端到端预算 45 秒。": "Webpage and document body tasks use --dump markdown; direct media files return original bytes. Failure retries were disabled in this run, with a 45-second end-to-end budget.", - "obscura fetch --dump markdown 原生输出": "Native output from obscura fetch --dump markdown", "下载能力与来源数据": "Acquisition capability and source data", "Beta 期免费;账户级限制": "Free during beta; account-level limits apply", "2026-09-07 核验的 Kitesurf 专项文档明确写明 Beta 期免费且受每账户限制。通用 Browser Run 另有浏览器时长计价;在专项文档改口前,不把通用超额价格当成 Kitesurf Beta 的已证实单价。": "Kitesurf-specific documentation checked on 2026-09-07 explicitly states that the beta is free and subject to per-account limits. General Browser Run separately charges for browser duration; until the Kitesurf documentation changes, general overage pricing is not presented as a verified Kitesurf beta unit price.", @@ -2333,11 +2142,6 @@ # provider output, so the English top-level pages localize it. TEXT.update({ "支持 · Browser Run 浏览器 + 等待参数": "Supported · Browser Run browser + wait parameters", - "Browser Run Markdown / DOM 资源地址;Workers AI 文档 Markdown": "Browser Run Markdown / DOM resource URLs; Workers AI document Markdown", - "Markdown/资源地址;视频资源题另请求 video;文件题为 rawBase64 解码字节": "Markdown/resource URLs; video-resource tasks also request video; file tasks decode rawBase64 bytes", - "array · 资源题": "array · resource tasks", - "multipart · 直接文档": "multipart · direct documents", - "string · 直接文档": "string · direct documents", }) @@ -2380,128 +2184,42 @@ TEXT.update({ "langchain-webbase 官方实现:": "Official langchain-webbase implementation:", "按本轮适配器介绍,不混用完整产品能力。输入示例展示正文配置,资源配置差异单列;它们不是每条历史请求的逐字回放。各调用所属批次及实际配置见“环境与复现”。输出示例取存档调用,经适配器整理后的真实节选,不冒充原始 HTTP 响应。Key 和账号均用占位符。": "Descriptions follow the adapters used in this run and do not imply each product's full capabilities. Request examples show body-text settings; resource-setting differences are listed separately. They are not verbatim replays of every historical request. See Reproduction for each call's batch and effective configuration. Response examples are genuine excerpts from archived calls after adapter normalization, not raw HTTP responses. Keys and account values use placeholders.", - "45 秒端到端截止": "45-second end-to-end deadline", - "45 秒客户端端到端截止": "45-second client-side end-to-end deadline", - "Cloudflare 原生 Markdown → content": "Cloudflare native Markdown → content", - "接口状态与错误": "Interface status and errors", "账号版 Browser Run 原生 Markdown 路径。客户端耗时包含公网往返、远端排队与执行;browser_ms_used 仅作附加诊断。": "Account-level Browser Run native-Markdown path. Client latency includes public-network round trips, remote queueing, and execution; browser_ms_used is an additional diagnostic only.", - "string · 必填": "string · required", "普通网页正文": "Ordinary web-page body", "直接取得 Markdown": "Markdown returned directly", "直接文档或媒体": "Direct document or media input", "由 CLI 自动选择原始响应或序列化页面;不附加页面等待参数": "The CLI automatically selects a raw response or serialized page; no page-wait parameters are added", - "45 秒外层端到端截止;文档解析包含在内": "45-second outer end-to-end deadline, including document parsing", "普通网页为 Markdown;直接文件为原始字节": "Markdown for ordinary pages; raw bytes for direct files", "直接文档正文使用的本地解析器": "Local parser used for direct-document body text", - "保存的原始文档或媒体证据": "Saved original-document or media evidence", - "文件格式、长度和目标 SHA-256 验收": "File format, length, and target SHA-256 validation", - "本方法只使用官方 1.1.1 发行二进制,与其他 Moli 版本分开计分。普通网页使用原生 Markdown;直接文档与媒体的裸 fetch 路线在正式二进制上实测确认。正文任务若取得受支持的原始文档,会在同一计时预算内解析文字;文件任务直接验收字节。不在失败后切换引擎或输出模式。 旧批次只保存了二进制 SHA-256,随后与官方 v1.1.1 归档逐字节匹配;受错误输出模式影响的直接文档与媒体已用正确路线重测。": "This method uses only the official 1.1.1 release binary and is scored separately from other Moli versions. Ordinary pages use native Markdown. Bare fetch for direct documents and media was verified on the release binary. When a body task obtains a supported original document, text is parsed within the same timing budget; file tasks validate bytes directly. The engine or output mode is not changed after a failure. Older batches retained only the binary SHA-256, which was later matched byte for byte to the official v1.1.1 archive. Direct-document and media calls affected by the incorrect output mode were rerun through the correct path.", "本方法只使用官方 1.0.6 发行二进制,与其他 Moli 版本分开计分。普通网页使用原生 Markdown;直接文档与媒体的裸 fetch 路线在正式二进制上实测确认。正文任务若取得受支持的原始文档,会在同一计时预算内解析文字;文件任务直接验收字节。不在失败后切换引擎或输出模式。": "This method uses only the official 1.0.6 release binary and is scored separately from other Moli versions. Ordinary pages use native Markdown. Bare fetch for direct documents and media was verified on the release binary. When a body task obtains a supported original document, text is parsed within the same timing budget; file tasks validate bytes directly. The engine or output mode is not changed after a failure.", "本方法只使用官方 1.1.3 发行二进制,与其他 Moli 版本分开计分。普通网页使用原生 Markdown;直接文档与媒体的裸 fetch 路线在正式二进制上实测确认。正文任务若取得受支持的原始文档,会在同一计时预算内解析文字;文件任务直接验收字节。不在失败后切换引擎或输出模式。": "This method uses only the official 1.1.3 release binary and is scored separately from other Moli versions. Ordinary pages use native Markdown. Bare fetch for direct documents and media was verified on the release binary. When a body task obtains a supported original document, text is parsed within the same timing budget; file tasks validate bytes directly. The engine or output mode is not changed after a failure.", - "目标页面": "Target page", "本轮 networkidle2,等待页面网络活动趋于空闲": "networkidle2 in this run; wait until page network activity settles", - "接口是否成功": "Whether the interface succeeded", "Markdown 正文 → content": "Markdown body → content", - "接口错误代码与信息": "Interface error code and message", - "object · 若提供": "object · when provided", "最终 URL、标题、源站状态等": "Final URL, title, origin status, and related fields", "本轮是 Markdown 端点,不是截图或浏览器控制接口;账户配额不足会单列失败。": "This run uses the Markdown endpoint, not screenshot or browser-control APIs. Insufficient account quota is reported as a distinct failure.", "· 未纳入本轮;参数说明为仓库登记模式,不计入本轮结果 ·": "· Not included in this benchmark; parameters describe the repository-registered mode and do not contribute to results ·", - "本地内置工具;无供应商 API Key": "Local built-in tool; no provider API key", - "公开 HTTP/HTTPS 网页或文档 URL": "Public HTTP/HTTPS web-page or document URL", - "构造配置": "Construction configuration", - "解析文档时使用的本地工作目录,不是工具输入参数": "Local working directory used for document parsing, not a tool input", - "success / error 等工具状态": "Tool status such as success / error", - "提取文字或错误说明 → content": "Extracted text or error explanation → content", - "底层默认请求超时 30 秒;评测预算 45 秒不会改写工具自己的默认值。网页与 PDF/DOCX 走不同解析路径。": "The underlying request timeout defaults to 30 seconds. The 45-second benchmark budget does not override the tool's own default. Web pages and PDF/DOCX inputs use different parsing paths.", - "本地 Chromium;无云端抓取 Key": "Local Chromium; no cloud-retrieval key", - "目标网址": "Target URL", - "本轮 BYPASS": "BYPASS in this run", - "45000 毫秒": "45000 milliseconds", - "本轮均为 true": "true for every call in this run", - "爬取是否成功": "Whether crawling succeeded", - "源站状态或失败信息": "Origin status or failure information", "正文使用本地无头 Chrome;文件任务在调用前选择原生 AsyncHTTPCrawlerStrategy.crawl(url, config),读取其 downloaded_files 并统一验收字节。媒体链接不等于下载;文件字节校验不等于理解媒体。": "Body text uses local headless Chrome. File tasks select native AsyncHTTPCrawlerStrategy.crawl(url, config) before the call, read downloaded_files, and apply the shared byte-validation rules. A media link is not a download, and validating file bytes is not media understanding.", - "本地 HTTP fetch provider;无云端抓取 Key": "Local HTTP fetch provider; no cloud-retrieval key", - "运行时参数": "Runtime parameters", - "调用标识和取消信号;示例省略初始化": "Call identifier and cancellation signal; initialization omitted from the example", - "工具执行是否报错": "Whether tool execution raised an error", "文本块合并 → content": "Merged text blocks → content", "成功态的规范源站状态与最终 URL": "Canonical origin status and final URL for a successful execution", "展示投影;仅在规范值缺失时补充状态或 URL": "Presentation projection; used for status or URL only when the canonical value is absent", - "string · 若提供": "string · when provided", - "工具错误类型": "Tool error type", "本轮 response 上限 10 MB、正文上限 200000 字符;不是 DeepSeek 聊天 API,也没有调用模型生成答案。": "This run caps each response at 10 MB and body text at 200000 characters. This is not the DeepSeek chat API, and no model generates the answer.", - "array · 必填": "array · required", - "本轮每次一个目标 URL,不调用搜索": "One target URL per call in this run; search is not invoked", "本轮 true,要求正文": "true in this run; body text required", - "本轮 0": "0 in this run", - "正文配置不传;资源配置 links:20、imageLinks:20;具名列表配置 richLinks:100": "Omitted for body tasks; resource tasks use links:20 and imageLinks:20; named-list tasks use richLinks:100", - "明确的 anchor—URL 对 → 具名详情入口": "Explicit anchor–URL pairs → named detail links", - "不带名称归属的链接和图片地址": "Links and image addresses without name ownership", - "正文 → content": "Body text → content", - "标题与结果网址": "Title and result URL", "额外链接和图片": "Additional links and images", - "逐网址的成功状态或错误": "Per-URL success status or error", - "费用信息": "Cost information", "没有正文或逐网址状态失败,不能仅凭 HTTP 200 判成功。": "An HTTP 200 alone does not establish success when body text is absent or a per-URL status failed.", - "目标网页或文档": "Target web page or document", "正文/资源为 markdown、links、images;原文件唯一使用 rawBase64": "markdown, links, and images for body/resource tasks; rawBase64 alone for original-file tasks", - "动态顶层选择器用 wait action,随后执行预声明稳定延迟": "Use a wait action for a dynamic top-level selector, followed by the predeclared settle delay", - "不超过共享内部 HTTP 截止预算": "Does not exceed the shared inner HTTP deadline budget", - "原文件题解码为目标 URL 字节并统一验收": "Decoded into the target URL bytes for uniform original-file validation", - "错误仅作私有响应证据;公开记录使用稳定分类": "Errors remain in private response evidence; public records use stable classifications", "正文配置为 markdown;资源配置为 markdown、links、images": "markdown for body tasks; markdown, links, and images for resource tasks", - "本轮 0,不复用旧缓存": "0 in this run; no old cache is reused", - "接口成功标记": "Interface success flag", "资源 → provider_metadata": "Resources → provider_metadata", - "标题、源 URL、源站状态、积分等": "Title, source URL, origin status, credits, and related fields", "错误或额度原因": "Error or quota reason", - "本轮每次响应后冷却 6.1 秒;冷却不计入单次请求耗时,不代表无速率限制的吞吐量。完整 API 响应进入私有哈希绑定证据。选择器需要多个匹配或位于 iframe 时,Scrape Actions 不能完整表达该条件,报告明确披露而不伪称已等待。": "This run pauses for 6.1 seconds after each response. The pause is excluded from per-call latency and does not represent throughput without rate limits. The complete API response enters private, hash-bound evidence. When a selector requires more than one match or is inside an iframe, Scrape Actions cannot fully express the condition; the report discloses that limitation instead of claiming it was awaited.", - "本地开源库;无供应商 API Key": "Local open-source library; no provider API key", - "本轮仅一个目标 URL": "One target URL in this run", - "本轮 45 秒": "45 seconds in this run", - "本轮 true,HTTP 错误抛异常": "true in this run; raise on HTTP errors", - "本轮 false": "false in this run", - "各 Document 正文合并 → content": "Merged body text from each Document → content", - "source、title、language 等": "source, title, language, and related fields", - "转成失败记录及错误说明": "Converted to a failed record with an error explanation", - "不运行网页 JavaScript;本轮没有把它替换成其他 LangChain loader。": "Web-page JavaScript is not run. This benchmark does not replace it with another LangChain loader.", - "目标网页 URL": "Target web-page URL", - "string · 可选": "string · optional", - "指定提取引擎;本轮不传,使用服务端选择": "Selects the extraction engine; omitted in this run so the server chooses", - "页面标题 / 最终地址": "Page title / final URL", - "页面链接和图片 → provider_metadata": "Page links and images → provider_metadata", - "源站状态;与 HTTP API 状态分开": "Origin status, separate from HTTP API status", - "服务端耗时": "Server-side latency", - "本轮为实时提取。中国区与国际区分开调用、分开计分。": "This run uses live extraction. The China and international endpoints are called and scored separately.", - "本地二进制;无 API Key": "Local binary; no API key", "公开 HTTP(S) 页面": "Public HTTP(S) page", "done;可配置 networkidle / domstable 等": "done; networkidle / domstable and other states are configurable", "毫秒;与外层进程截止时间使用同一预算": "Milliseconds; shares the outer process deadline budget", "默认启用,阻止私网地址": "Enabled by default to block private-network addresses", "原样保存 Markdown → content": "Markdown retained verbatim → content", - "独立诊断;不混入正文": "Separate diagnostics; excluded from body text", "0 且正文非空表示工具执行成功,任务另按规则判定": "0 with nonempty body text means tool execution succeeded; task acceptance is judged separately", "未提供": "Not provided", "Markdown 模式没有这些字段,不伪造 HTTP 200 或最终 URL": "Markdown mode does not expose these fields; HTTP 200 and a final URL are not fabricated", "正文用 Markdown;直接文件任务预先选择不带 --dump 和页面等待参数的自动响应,保存原始字节后按统一文件规则验收。不是失败后的切换或额外下载。七方法报告中的直接 PDF/DOCX 正文另由 headless 适配器在同一截止预算内解析。": "Body output uses Markdown. Direct-file tasks preselect automatic responses without --dump or page-wait parameters, retain raw bytes, and apply the shared file rules. This is neither a post-failure switch nor an extra download. For direct PDF/DOCX body tasks in the seven-method report, the headless adapter parses within the same deadline budget.", - "本地内置工具;本轮禁用 Firecrawl 回退": "Local built-in tool; Firecrawl fallback disabled in this run", - "本轮 markdown": "markdown in this run", - "本轮 200000;输出可能截断": "200000 in this run; output may be truncated", - "本轮 45 秒预算,通过函数参数传入": "45-second budget in this run, passed as a function argument", - "正文 → content;缺失时取 content[].text": "Body text → content; falls back to content[].text when absent", - "源站状态和最终 URL": "Origin status and final URL", - "实际正文提取器": "Actual body-text extractor", - "输出是否被截断": "Whether output was truncated", - "Readability 开启,cacheTtlMinutes=0。函数异常转成失败记录,不把其他 provider 的回退结果算给 OpenClaw。": "Readability is enabled and cacheTtlMinutes=0. Function exceptions become failed records; fallback output from another provider is not credited to OpenClaw.", - "接口接受网址数组;本轮每次只放一个": "The interface accepts an array of URLs; each call in this run contains one", "本轮 basic": "basic in this run", - "正文配置 false;资源配置 true,额外返回图片地址": "false for body tasks; true for resource tasks to return image URLs", - "提取正文 → content": "Extracted body text → content", - "结果 URL": "Result URL", - "结果条目或响应顶层的图片地址": "Image URLs from result items or the response top level", - "未成功提取的网址及原因": "URLs not extracted successfully and their reasons", "HTTP 200 但没有 raw_content 仍按适配器请求失败记录;原始状态与规范化依据分开保留。": "An HTTP 200 without raw_content is still recorded as an adapter request failure. Raw status and normalization evidence are retained separately.", "2026-09-02 核验:15 个签名,纳入 87 个仓库。候选集包括每签名前 100 条搜索结果和已登记的专用签名文件;按默认分支逐文件核验。要求 stars ≥ 1,000、近 7 天活跃且未归档。": "Checked on 2026-09-02: 15 signatures across 87 included repositories. The candidate set contains the first 100 search results per signature plus registered signature-specific files, each checked on the default branch. Repositories require stars ≥ 1,000, activity within 7 days, and non-archived status.", "同一方法在同一仓库命中多处,仓库数只加 1;同一文件命中多个签名,文件数只加 1。保留样本中的官方仓库、示例、文档和 fork,不冒充独立生产用户。": "Multiple matches for one method in one repository count as 1 repository; multiple signatures in one file count as 1 file. Official repositories, examples, documentation, and forks remain in the sample and are not presented as independent production users.", @@ -2509,7 +2227,6 @@ "逐一读取命中的 Git 文件并校验内容。通用 web_fetch 和同名函数不能证明归属;Exa 要求 Contents 端点或 SDK 导入及 get_contents。内置方法也包括源码复制与官方仓库,不能冒充第三方集成。": "Every matched Git file is read and its content verified. Generic web_fetch names or same-named functions do not establish attribution. Exa requires the Contents endpoint or an SDK import plus get_contents. Built-in-method counts also include copied source and official repositories, which are not presented as third-party integrations.", "固定十二方法升级合同与重跑范围": "Freeze the twelve-method refresh contract and rerun scope", "十二方法报告已重新绑定当前评测集、实现身份、选择性重跑计划和独立发布目标。主矩阵计划新增 432 次调用,同窗测速计划新增 240 次;这些调用及独立核验尚未在本条说明中宣告完成,因此不公布新分数、排名或版本结论。": "The twelve-method report is now bound to the current dataset, implementation identities, selective-rerun plan, and a separate publication target. The primary matrix plans 432 new calls and the same-window latency cohort plans 240. This entry does not claim that those calls or their independent review are complete, so it reports no new scores, rankings, or version conclusions.", - "LangChain 与 Crawl4AI 因旧权威证据未记录库版本,必须各自完整重跑 150 题;源码型方法只有精确且干净的绑定实现可进入本轮。当前不据此推断分数变化。": "Because the old authoritative evidence did not record their library versions, LangChain and Crawl4AI must each rerun all 150 tasks. Only the exact, clean, bound checkouts of source-backed methods may enter this run. No score change is inferred from these requirements.", "受路线影响的文档结果进入声明的重跑范围;保存的新输出完成评分和盲审前,不把实现修正写成能力提升。": "Document results affected by this path are included in the declared rerun scope. The implementation fix is not presented as a capability gain before the new saved outputs have been scored and blindly reviewed.", "主矩阵预先声明 13 题 × 12 方法、LangChain 与 Crawl4AI 的其余完整方法 cohort,以及两个超时边界修正,共 432 次新调用;另以 provider-blind、域名去重的 20 题执行 240 次十二方法同窗测速。": "The primary matrix predeclares 13 tasks × 12 methods, the remaining full method cohorts for LangChain and Crawl4AI, and two deadline-boundary corrections, for 432 new calls in total. A separate provider-blind, hostname-deduplicated set of 20 tasks produces 240 same-window latency calls across the twelve methods.", "主矩阵仍固定每个方法—任务一个权威逻辑结果,不按最好或最快挑选;测速只在共同成功样本上比较。合计 672 次是计划调用数,不是已完成数。": "The primary matrix still fixes one authoritative logical result per method–task pair and never selects the best or fastest response. Latency is compared only on jointly successful tasks. The combined 672 calls are planned, not completed.", @@ -2523,7 +2240,6 @@ # reference answers, or provider output. PHRASES = { "处理路线:": "Retrieval path: ", - "Moli 1.1.15 · 独立资源批次": "Moli 1.1.15 · independent resource cohort", "的 Moli 已保存输出仍包含": "'s saved Moli output still contains ", "Quotes to Scrape 的": "Quotes to Scrape's ", "CowAgent 已通过网页中 F1 最低的是": "The lowest-F1 general-web task passed by CowAgent is ", @@ -2536,10 +2252,6 @@ "未通过。书名和价格留在文本里,不能保证封面地址也交付了。下游需要分别验收文字和资源地址,才能发现这种交付差异。": " fails. Titles and prices remaining in text do not guarantee delivery of cover URLs. Downstream must validate text and resource URLs separately to detect this difference.", "没有任何方法满足任务规则的案例包括:": "Tasks passed by no method include: ", "。这些共同缺口使单纯增加后备方法的收益存疑;应先查实际输出缺少哪些字段、是否只有加载占位,或是否收到拦截页面。登录、地区和访问限制本身不使任务无效,它们可能就是目标业务需要处理的条件。": ". These shared gaps make the value of simply adding a fallback uncertain. First inspect missing fields, loading placeholders, or block pages. Login, region, and access constraints do not invalidate the task; they may be conditions the target workflow must handle.", - "Cloudflare Browser Run /markdown 直接返回 Markdown": "Markdown returned directly by Cloudflare Browser Run /markdown", - "Lightpanda CLI 直接返回 Markdown": "Markdown returned directly by the Lightpanda CLI", - "Obscura CLI 直接返回 Markdown": "Markdown returned directly by the Obscura CLI", - "Moli CLI 直接返回 Markdown": "Markdown returned directly by the Moli CLI", "Chromium 渲染 DOM → node-html-markdown 2.0.0": "Chromium rendered DOM → node-html-markdown 2.0.0", "未启用额外的理解模型或付费后备方法。": "No additional understanding model or paid fallback was enabled.", "调用参数与交付路线以本次配置和逐例记录为准。": "See the run configuration and individual records for call parameters and delivery paths.", @@ -2594,7 +2306,6 @@ "输出目录:reports/webfetch/。固定的 Python 3.11 与 uv.lock 依赖不满足时会立即失败。凭据仅从本地环境加载,不包含在报告中。": "Output directory: reports/webfetch/. The command fails immediately unless the pinned Python 3.11 and uv.lock dependencies are satisfied. Credentials are loaded only from the local environment and are not included in the report.", "完成十二方法升级、影响范围重跑与失败项核验": "Complete the twelve-method upgrade, impact-scoped reruns, and failure review", "十二方法报告已重新绑定当前评测集、实现身份和独立发布目标。主矩阵按预声明影响范围新增 432 次调用,同窗测速新增 240 次;主矩阵 705 条机器失败结果完成独立核验,1,095 条机器通过结果未逐条审查,分歧逐条裁决,未决为 0。": "The twelve-method report is now bound to the current evaluation set, implementation identities, and an independent publication target. The primary matrix adds 432 calls within the predeclared impact scope and the same-window latency cohort adds 240 calls. All 705 machine failures in the primary matrix received independent review; the 1,095 machine passes were not reviewed individually. Every disagreement was adjudicated, with 0 unresolved.", - "LangChain 与 Crawl4AI 因旧权威证据未记录库版本,各自完整重跑 150 题;源码型方法只有精确且干净的绑定实现进入本轮。": "LangChain and Crawl4AI each reran all 150 tasks because the previous authoritative evidence did not record their library versions. For source-based methods, only exact, clean, bound implementations entered this run.", "受路线影响的文档结果已进入声明的重跑范围;评分与盲审均使用新保存输出。": "Document results affected by the route change were included in the declared rerun scope; scoring and blind review both use the newly saved outputs.", "主矩阵预先声明 13 题 × 12 方法、LangChain 与 Crawl4AI 的其余完整方法 cohort,以及两个超时边界修正,共完成 432 次新调用;另以 provider-blind、域名去重的 20 题完成 240 次十二方法同窗测速。": "The primary matrix predeclared 13 tasks × 12 methods, the remaining full-method cohorts for LangChain and Crawl4AI, and two deadline-boundary corrections, for 432 new calls in total. A separate provider-blind, domain-deduplicated set of 20 tasks produced 240 same-window latency calls across all twelve methods.", "主矩阵固定每个方法—任务一个权威逻辑结果,不按最好或最快挑选。测速保留完整预声明矩阵:全调用延迟按同一 20 题分母统计,成功调用延迟另作条件统计;十二方法共同通过为 1/20,只作为诊断,不据此筛样本或排名。": "The primary matrix fixes one authoritative logical result for every method–task pair and never selects the best or fastest result. The latency cohort retains its complete predeclared matrix: all-call latency uses the same 20-task denominator, while successful-call latency is reported separately as a conditional statistic. All twelve methods jointly passed 1/20 tasks; this is diagnostic only and is not used to select samples or rank methods.", @@ -2618,28 +2329,16 @@ "免费 / 准入说明": "Free tier / access notes", "注册 Cloudflare,复制 32 位 Account ID,再创建自定义 API token:Account → Browser Rendering → Edit;运行前用 GET /client/v4/user/tokens/verify 验证。": "Register with Cloudflare, copy the 32-character Account ID, then create a custom API token with Account → Browser Rendering → Edit; verify it with GET /client/v4/user/tokens/verify before running.", "Workers Free 可用 Browser Rendering;token 需要 Browser Rendering Edit 权限,并受账户限额约束。": "Browser Rendering is available on Workers Free; the token needs Browser Rendering Edit permission and remains subject to account limits.", - "CowAgent 内置 Web Fetch": "CowAgent built-in Web Fetch", - "检出并固定 CowAgent 源码,配置仓库路径和隔离工作目录;不需要供应商 API Key。": "Check out and pin the CowAgent source, then configure its repository path and isolated working directory; no provider API key is required.", - "本地项目内置实现。": "Built into the local project.", - "使用仓库冻结的 Python 环境及匹配的浏览器运行;OSS_PYTHON 只在显式覆盖 worker 解释器时设置。": "Run with the repository's frozen Python environment and matching browser; set OSS_PYTHON only when explicitly overriding the worker interpreter.", - "本地开源库;不需要供应商账号。": "Local open-source library; no provider account is required.", - "DeepSeek Harness 内置 Web Fetch": "DeepSeek Harness built-in Web Fetch", "检出并固定 DeepSeek Harness 源码与依赖;本轮不调用聊天模型。": "Check out and pin the DeepSeek Harness source and dependencies; this run does not call a chat model.", - "本地项目内置实现;不需要模型 API Key。": "Built into the local project; no model API key is required.", "注册或登录 Exa 控制台,打开 API Keys,创建 key;如系统要求则设置预算。": "Register for or sign in to the Exa console, open API Keys, and create a key; set a budget if required by the system.", "需要控制台账号;用量由 credits/预算控制。": "A console account is required; usage is governed by credits and budget.", "注册账号,打开 Firecrawl 控制台 API Keys 页面,创建以 fc- 开头的 key。": "Register an account, open the API Keys page in the Firecrawl console, and create a key beginning with fc-.", "存在无 key 的限流层;可复现评测应使用免费账号。": "A rate-limited keyless tier exists; reproducible evaluation should use a free account.", - "使用仓库冻结的 Python 环境运行;OSS_PYTHON 只在显式覆盖 worker 解释器时设置。": "Run with the repository's frozen Python environment; set OSS_PYTHON only when explicitly overriding the worker interpreter.", "登录中国区控制台,复制同一项目下的 API key 与 Project ID;本评测使用 api.lexmount.cn。": "Sign in to the China-region console and copy the API key and Project ID from the same project; this evaluation uses api.lexmount.cn.", "API key 与 Project ID 必须来自同一个中国区项目。": "The API key and Project ID must come from the same China-region project.", "登录后打开 Settings → API Keys,创建或复制 API key,并复制对应 Project ID;本评测使用 api.lexmount.com。": "After signing in, open Settings → API Keys, create or copy an API key, and copy the corresponding Project ID; this evaluation uses api.lexmount.com.", "内测准入;API key 与 Project ID 必须来自同一个 Global 项目。": "Private-beta access; the API key and Project ID must come from the same Global project.", - "安装并固定本轮 Moli 可执行文件;不需要供应商账号或 API Key。": "Install and pin the Moli executable used in this run; no provider account or API key is required.", - "本地 CLI;二进制路径和版本属于运行证据。": "Local CLI; the binary path and version are part of the run evidence.", - "OpenClaw 内置 Web Fetch": "OpenClaw built-in Web Fetch", "检出并固定 OpenClaw 源码及 Node 运行时;本轮关闭 Firecrawl 回退。": "Check out and pin the OpenClaw source and Node runtime; the Firecrawl fallback is disabled in this run.", - "本地项目内置实现;不需要云端抓取 Key。": "Built into the local project; no cloud-retrieval key is required.", "注册或登录 Tavily Platform,从控制台复制 API key。": "Register for or sign in to Tavily Platform and copy an API key from the console.", "官方快速开始说明每月有 1,000 免费 API credits,无需绑卡。": "The official quickstart states that 1,000 free API credits are available each month with no card required.", } @@ -2669,6 +2368,7 @@ def _body_region_summary(match): REGEX = [ + (re.compile(r"^新批次启用自身 DEBUG 并捕获网络库日志;旧 (\d+) 条不计入本报告。$"), r"The new batch enabled native DEBUG output and captured network-library logs; \1 earlier records are excluded from this report."), (re.compile(r"^每次物理调用预算为 ([\d.]+) 秒,重试后的累计耗时可能超过该预算。$"), r"Each physical call has a \1-second budget; accumulated latency after retries can exceed it."), (re.compile(r"^本报告包含 (\d+) 个任务、(\d+) 个逻辑结果,任务通过 (\d+)/(\d+)。结论仅适用于已登记的任务和规则。$"), r"This report contains \1 tasks and \2 logical results, with \3/\4 task passes. Conclusions apply only to the registered tasks and rules."), (re.compile(r"^的 F1 为 ([\d.]+%),但没有满足该例任务规则。零通过表示没有一次满足全部任务门槛,不表示输出与参考毫无词语重合。这里的 F1 对照已保存的样本文本,只诊断文字重合,不承担实时事实核验。词频相似度允许部分文字得分,任务规则检查指定的内容或字段结构;二者应保留各自的含义。$"), @@ -2734,7 +2434,6 @@ def _body_region_summary(match): (re.compile(r"^供应商明确声明返回内容已截断:(.*)$"), r"The provider explicitly declares that the returned body is truncated: \1"), (re.compile(r"^当前报告共 (\d+) 个案例、(\d+) 种方法和 ([\d,]+) 条权威逻辑结果;其中其他 ([\d,]+) 条未被本次文件补测改写。$"), r"The current report contains \1 tasks, \2 methods, and \3 authoritative logical results; the other \4 results were not changed by this file-task rerun."), (re.compile(r"^至少 (\d+) 条不同记录$"), r"At least \1 distinct records"), - (re.compile(r"^主结果时间:(.+)(UTC)。Moli 统一采用 1.1.1。产品定位依据各自公开说明;结果仅对应本轮输入、配置与运行条件。$"), lambda m: "Primary-result dates: " + m.group(1).replace(" 至 ", " to ") + " (UTC). Moli uses 1.1.1 throughout. Product descriptions follow each project's public documentation; results apply only to this run's inputs, configurations, and conditions."), (re.compile(r"^排除:(.+)$"), r"Exclude: \1"), (re.compile(r"^至少 ([\d,]+) 字符$"), r"At least \1 characters"), (re.compile(r"^([\d,]+) 字符$"), r"\1 characters"), @@ -2751,7 +2450,6 @@ def _body_region_summary(match): (re.compile(r"^(Moli|pypdf) · ([\d.]+) 秒$"), r"\1 · \2 seconds"), (re.compile(r"^端到端 ([\d.]+) 秒$"), r"End to end: \1 seconds"), (re.compile(r"^输入《Attention Is All You Need》的 PDF 地址,目标是取得论文正文。Moli 获取文件约用 ([\d.]+) 秒,本地 pypdf 提取文字约用 ([\d.]+) 秒,整条路线共 ([\d.]+) 秒,返回 ([\d,]+) 个字符。五段参考正文全部覆盖,本题词级 Precision、Recall、F1 均为 100%。$"), r"The Attention Is All You Need PDF task requires the paper's body text. Moli retrieves the file in about \1 seconds, local pypdf extracts text in about \2 seconds, and the complete path takes \3 seconds, returning \4 characters. All five reference body regions are covered, with word-level precision, recall, and F1 each at 100%."), - (re.compile(r"^实测范围:(.+)(UTC)的主结果;Moli 固定采用 1.1.1。本文任务、参考和实际返回均可追溯到独立复核记录。产品说明与版本动态核对日期:2026-09-10。$"), lambda m: "Scope: primary results from " + m.group(1).replace(" 至 ", " to ") + " (UTC); Moli is fixed at 1.1.1. Tasks, references, and actual outputs are traceable to independent review records. Product descriptions and release updates checked on 2026-09-10."), (re.compile(r"^代表案例:(.*)$"), r"Representative cases: \1"), (re.compile(r"^([\d,]+) 个字符$"), r"\1 characters"), (re.compile(r"^([\d,]+) 字节$"), r"\1 bytes"), @@ -2846,7 +2544,6 @@ def _body_region_summary(match): TEXT.update({ "Moli 的 Web Fetch 实测:": "Moli Web Fetch benchmark:", "五款浏览器,150 道任务": "five browsers, 150 tasks", - "同一个链接,浏览器返回的可能是整篇论文,也可能只有标题或一个下载地址。我们用 150 道网页与文件读取任务,对比 Moli 1.1.1、Chromium、Lightpanda、Obscura 和 Kitesurf。Moli 通过 135 道,比 Chromium 多 10 道;在 8 道共同成功的网页任务中,平均耗时为 3.41 秒,比 Chromium 少 35.5%。": "The same link can return a complete paper, just a title, or a download URL, depending on the browser. We compared Moli 1.1.1, Chromium, Lightpanda, Obscura, and Kitesurf on 150 page and file retrieval tasks. Moli passed 135, 10 more than Chromium. On 8 web tasks completed by every tested configuration, its mean latency was 3.41 seconds, 35.5% below Chromium.", "Chromium:125 / 150 · 83.3%": "Chromium: 125 / 150 · 83.3%", "多通过 10 道,提高 6.7 个百分点": "10 more tasks passed; 6.7 percentage points higher", "平均耗时 · 同样 8 道网页": "Mean latency · the same 8 pages", @@ -2863,7 +2560,6 @@ def _body_region_summary(match): "正文或文件": "Text or file", "Browser Use:操作网页": "Browser Use: interact with pages", "让 AI 搜索商品、筛选规格,再加入购物车。它需要看当前页面,决定点击或填写什么,操作后再继续观察。": "Ask AI to search for a product, filter specifications, and add it to the cart. It must inspect the current page, decide what to click or fill in, act, and inspect the result.", - "完整报告包含三个 Moli 版本。本文选取总通过数最高的 1.1.1,所有任务统一使用该版本结果。": "The full report includes three Moli versions. This article uses 1.1.1, the version with the most total passes, consistently across all tasks.", "文档与媒体:Moli 通过 24/30,Chromium 通过 14/30": "Documents and media: Moli passes 24/30, Chromium 14/30", "把 150 道题按内容拆开,净增的 10 道全部来自文档、图片和视频。120 道网页任务中,两者各通过 111 道。": "Breaking the 150 tasks down by content, all 10 net additional passes come from documents, images, and videos. Of the 120 web tasks, both browsers pass 111.", "其中六道 PDF 题只有 Moli 的本轮配置通过。Moli 先取得 PDF 文件,再交给本地 pypdf 解析器提取正文;其余四款使用本轮配置的 Markdown 输出路线,未能交回合格正文。对需要读论文的 AI 来说,这一步决定了后续有没有内容可读。成绩属于这条完整处理路径,PDF 解析时间也计入耗时。": "Six PDF tasks pass only with Moli’s tested configuration. Moli first retrieves the PDF, then uses local pypdf to extract text. The other four browsers’ tested Markdown output paths do not return acceptable text. For AI reading a paper, this step determines whether there is content to work with. The score belongs to this complete processing path, and latency includes PDF parsing.", @@ -2877,7 +2573,6 @@ def _body_region_summary(match): "一篇 PDF 怎样算读完,一张图片怎样算取到": "What counts as retrieving a PDF or an image", "《Attention Is All You Need》这道题要求取得论文正文。只返回标题,或者只返回 PDF 地址,都不合格:输出需要包含指定文字,并覆盖论文各段的内容。换成图片任务,要求又可能是下载地址,也可能是图片文件本身。这些不同的交付要求,决定了每道题怎样判分。": "The Attention Is All You Need task requires the paper’s text. Returning only a title or PDF URL does not pass: the output must contain specified text and cover regions of the paper. An image task may instead require a download URL or the image file itself. These delivery requirements determine how each task is scored.", "正文与文件参考来自独立获取的来源;商品列表检查经独立确认的稳定文字与结构,不把每天变化的商品名单固定成答案。正文覆盖阈值是本评测采用的工程验收下限,尚未通过阈值敏感性实验校准。当前成绩采用统一修订、离线重算后的规则。": "Text and file references come from independently retrieved sources. Product lists are checked against independently confirmed stable text and structure, without freezing a changing daily catalog. Body-coverage thresholds are engineering acceptance floors for this benchmark and have not been calibrated through threshold-sensitivity experiments. Current scores use consistently revised rules applied through offline rescoring.", - "每款配置每题保留一次主结果。完整报告的 1,050 条主结果经 Codex 独立复核,无未决判定;其中本文五款浏览器占 750 条。": "Each configuration retains one primary result per task. Codex independently reviewed the full report’s 1,050 primary results, with no unresolved verdicts. The five browsers in this article account for 750 results.", "选型时,任务构成会改变这张榜单的意义。文档与媒体占本轮题量的 20%,Moli 在这 30 道里比 Chromium 多通过 10 道;如果应用只读普通网页,对应成绩就是 97/100 与 99/100。需要云端部署的团队,还要结合并发和成本测试;本轮未测量这两项。": "The task mix matters when choosing a browser. Documents and media make up 20% of this dataset; Moli passes 10 more of those 30 tasks than Chromium. For an application reading only general web pages, the relevant scores are 97/100 and 99/100. Teams needing cloud deployment also need concurrency and cost tests, neither of which was measured here.", "未通过的任务": "Failed tasks", "长网页的验证页,动态页面的等待": "Verification pages on long documents, waiting for dynamic content", @@ -2885,7 +2580,6 @@ def _body_region_summary(match): "98 道有完整可比参考的普通网页中,Moli 平均找回 95.3% 的参考正文词语,Chromium 为 99.8%。这个比例称为 Recall。另一个指标 Precision 检查输出词语中有多少与参考重合,F1 综合两者;它们衡量文字重合,不判断文字中的事实是否正确。": "Across 98 general pages with complete, comparable references, Moli recovers an average of 95.3% of reference words, versus Chromium’s 99.8%. This proportion is recall. Precision measures how much of the output overlaps the reference, and F1 combines the two. These metrics measure text overlap, not factual correctness.", "后续评测": "Future evaluations", "从 150 道读取任务,走向连续网页操作": "From 150 retrieval tasks to sequences of web interactions", - "这次测试中,Moli 1.1.1 的 90.0% 任务通过率排在五款浏览器首位;同样 8 道成功网页的平均耗时为 3.41 秒,P50 为 2.70 秒,两项也排在首位。PDF 和原文件交付带来了额外的成功任务,长网页正文的找回比例则仍低于 Chromium。": "In this test, Moli 1.1.1 ranks first among the five browsers with a 90.0% task pass rate. On the same 8 successfully retrieved pages, it also ranks first for mean latency at 3.41 seconds and P50 at 2.70 seconds. PDF and original-file delivery add successful tasks, while recovery of long-page text remains below Chromium.", "后续会扩大站点与文档样本。延迟列表、iframe,以及本轮未通过的 DOCX 和嵌入式文档查看器,需要在迭代后重新验证;热门站点需要重复运行,速度比较也需要更多共同成功样本。": "Future tests will include more sites and documents. Delayed lists, iframes, DOCX, and embedded document viewers that failed this run need retesting after updates. Popular sites need repeated runs, and latency comparisons need more shared successful tasks." }) @@ -2908,8 +2602,6 @@ def _body_region_summary(match): }) -TEXT.update({ -}) TEXT.update({ @@ -2930,7 +2622,6 @@ def _body_region_summary(match): TEXT.update({ - "让 AI 读一篇论文、整理一页商品,浏览器先要把内容交回来。我们用 150 道 Web Fetch 任务比较 Moli 1.1.1、Chromium、Lightpanda、Obscura 和 Kitesurf。Moli 通过 135 道,比 Chromium 多 10 道,净增来自文档与媒体。速度则同时看全部共同成功任务的耗时,以及另行安排的同窗测速。": "Before AI can read a paper or organize a product page, the browser must return the content. We compared Moli 1.1.1, Chromium, Lightpanda, Obscura, and Kitesurf on 150 Web Fetch tasks. Moli passed 135, 10 more than Chromium, with the net gain coming from documents and media. For speed, we examine all shared successful tasks and a separate latency test run within the same time window.", "98 道共同成功任务 · 主评测均值": "98 shared successes · main evaluation mean", "Chromium:5.27 秒": "Chromium: 5.27 seconds", "历史主结果,调用跨批次;详见测速分析": "Recorded primary results span batches; see the latency analysis", @@ -3036,47 +2727,15 @@ def _body_region_summary(match): TEXT.update({ - "已知单页使用 Scrape;Search、Crawl、Agent 会先找页面或跟随其他链接,不符合本轮给定 URL 的单次任务。动态题由 Actions 等待可表达的顶层选择器;匹配数量和 iframe 子文档条件仍可能不完整。video 返回的下载地址不等于原文件或画面理解。每次响应后冷却 6.1 秒,冷却不计入单次耗时;完整 API 响应只保存在私有哈希绑定证据。": "Known single URLs use Scrape. Search, Crawl, and Agent discover pages or follow additional links instead of fulfilling this one-call URL task. Actions wait for top-level selectors where expressible; match counts and child-frame conditions may remain unenforced. A video download URL is not the original file or visual understanding. The 6.1-second cooldown after a response is excluded from per-call latency; full API responses remain in private hash-bound evidence.", - "正文/资源为 markdown、links、images;视频资源题另请求 video;原文件只使用 rawBase64": "Body and resource tasks request markdown, links, and images; video-resource tasks also request video; original-file tasks use rawBase64 alone", - "资源地址;video 是有时效的视频下载入口": "Resource URLs; video provides a time-limited video download URL", - "已知 URL 使用 advanced Extract;Search 根据查询找页面,Crawl 扩展到链接页面,Research 生成答案,均不替代目标 URL 的正文。资源题开启 include_images;没有原文件或视频下载字段。45 秒外层预算预留持久化后,客户端与服务端提取截止均为 40.5 秒。HTTP 200 但没有 raw_content 仍按适配器请求失败;原始响应只保存在私有哈希绑定证据。": "Known URLs use advanced Extract. Search finds pages from a query, Crawl follows linked pages, and Research generates answers; none replaces text from the target URL. Resource tasks enable include_images, but the API has no original-file or video-download output. The 45-second outer limit reserves time for persistence, leaving 40.5 seconds for client and server extraction. HTTP 200 without raw_content remains an adapter-level request failure; raw responses remain private hash-bound evidence.", - "已知 URL 使用 Contents 的 full 正文与 maxAgeHours=0 实时抓取;Search 先检索,Agent 和 summary 生成回答,不能代替指定 URL 的原文。资源题请求 links/imageLinks,具名列表只接纳 richLinks 的明确 anchor—URL 配对,不把正文标题与裸地址按位置猜配。livecrawlTimeout 为 15 秒,低于 40.5 秒客户端截止。逐网址状态失败或没有正文时,不能仅凭 HTTP 200 判成功;原始响应只保存在私有证据。": "Known URLs use Contents with full text and maxAgeHours=0 for live crawling. Search discovers pages, while Agent and summary generate answers instead of source text from the specified URL. Resource tasks request links/imageLinks; named lists accept only explicit richLinks anchor–URL pairs, never positionally paired headings and bare URLs. livecrawlTimeout is 15 seconds within the 40.5-second client limit. Per-URL status failures or missing text are not successes merely because HTTP returned 200; raw responses remain private.", - "目标页面;直接文件先下载公开 URL 的原始字节再上传": "Target page; for a direct file, first download bytes from its public URL before uploading", - "网页和资源页使用 networkidle2;动态题使用预声明就绪信号": "Web and resource pages use networkidle2; dynamic tasks use their predeclared readiness signal", - "/scrape 按预声明 CSS 选择器取图片或媒体属性": "/scrape reads image or media attributes through predeclared CSS selectors", - "/ai/tomarkdown 接受 PDF / DOCX 等受支持文件;输入字节由客户端取得": "/ai/tomarkdown accepts supported files such as PDF and DOCX; the client obtains input bytes", - "网页和资源页绑定 40,500 毫秒客户端内层截止": "Web and resource pages share the 40,500-ms inner client deadline", - "/markdown 网页正文;/scrape 元素属性;/ai/tomarkdown 文档 Markdown": "/markdown page text; /scrape element attributes; /ai/tomarkdown document Markdown", - "Browser Run 最终 URL、标题、源站状态等": "Browser Run final URL, title, origin status, and related values", - "上传文件的输入字节摘要;下载与转换均计入同一次耗时": "Hash of uploaded input bytes; both download and conversion count toward the same call latency", - "按任务预选官方接口:网页正文用 Browser Run /markdown;页面图片与视频地址用 /scrape 的 DOM 属性;直接 PDF / DOCX 使用 Workers AI 文件转换。/pdf 是生成网页 PDF,/links 只列普通链接,/crawl 会跟随其他页面,均不满足这里的文档文字或目标资源合同。直接文档先在客户端下载,下载与转换共享预算;该下载不是 Cloudflare 的独立成绩。浏览器等待计时器彼此独立,账户配额或转换权限错误单列。": "The official route is chosen before each task: Browser Run /markdown for page text, /scrape DOM attributes for page image and video URLs, and Workers AI file conversion for direct PDF/DOCX text. /pdf prints a webpage, /links lists ordinary links, and /crawl follows other pages; none fulfills the document-text or target-resource contract here. A direct file is downloaded by the client, then converted within the same deadline; that download is not an independent Cloudflare score. Browser wait timers are independent; quota and conversion-permission errors are separate failure categories.", "Firecrawl · Scrape API 与 video 格式 ↗": "Firecrawl · Scrape API and video format ↗", "Cloudflare · Scrape 端点 ↗": "Cloudflare · Scrape endpoint ↗", "Cloudflare · 文档转 Markdown ↗": "Cloudflare · Document-to-Markdown conversion ↗", "Cloudflare · 支持的文档格式 ↗": "Cloudflare · Supported document formats ↗", - "正文与资源题请求 markdown、links、images;页面视频资源题另请求 video;原文件题只请求 rawBase64 并验收目标 URL 的解码字节。动态顶层选择器使用 Actions wait,随后执行预声明稳定延迟;选择器数量与 iframe 子文档条件仍标为未完整执行。未测试 audio、JSON、截图或 Parse。": "Body and resource tasks request markdown, links, and images; page-video resources additionally request video. Original-file tasks use rawBase64 alone and validate the target URL's decoded bytes. Top-level dynamic selectors use Actions wait followed by the declared settle delay; selector counts and child-frame conditions remain incompletely enforced. Audio, JSON, screenshots, and Parse were not tested.", - "Browser Run /markdown、/scrape;直接文档 Workers AI /ai/tomarkdown": "Browser Run /markdown and /scrape; Workers AI /ai/tomarkdown for direct documents", - "上传直接 PDF / DOCX 后转换为 Markdown": "Upload direct PDF/DOCX files and convert to Markdown", - "/scrape 读取 DOM 图片属性": "/scrape reads DOM image attributes", - "/scrape 读取 DOM 媒体属性": "/scrape reads DOM media attributes", - "文档上传后返回文字,不交付原文件": "Document upload returns text, not the original file", - "Browser Run 浏览器 + 等待参数": "Browser Run browser with wait settings", - "网页输入为 URL;直接文档由客户端下载原始字节后上传 Workers AI。": "Pages are submitted as URLs; the client downloads direct-document bytes before uploading them to Workers AI.", - "/markdown 返回网页文本,/scrape 返回元素属性,/ai/tomarkdown 返回文档文字。": "/markdown returns page text, /scrape element attributes, and /ai/tomarkdown document text.", - "Browser Rendering Edit 与 Workers AI 权限分别控制两类端点;浏览器访问仍可能被识别和拦截。": "Browser Rendering Edit and Workers AI permissions govern different endpoints; browser visits can still be identified and blocked.", - "/pdf 打印网页,不是读取已有 PDF;/links 只列页面链接,不能代替图片和媒体 DOM 属性。直接文档下载与转换共享同一次耗时;客户端下载不是 Cloudflare 独立成绩。/scrape 交付资源地址,不保证取得媒体字节或理解画面。": "/pdf prints a webpage rather than reading an existing PDF. /links lists page links but does not replace image and media DOM attributes. A direct-document download and its conversion share one measured call; the client download is not an independent Cloudflare score. /scrape provides URLs, not a guarantee of media bytes or visual understanding.", - "网页正文与动态页使用 /markdown;直接 PDF / DOCX 使用 /ai/tomarkdown;页面图片和视频资源使用 /scrape。未提供源站登录 Cookie,不用 Search/Crawl、截图、浏览器控制或失败后接口回退。": "Page text and dynamic tasks use /markdown; direct PDF/DOCX tasks use /ai/tomarkdown; page image and video resources use /scrape. No source-site login Cookie, Search/Crawl, screenshot, browser-control route, or post-failure interface fallback was used.", "Cloudflare Browser Run /markdown、/scrape 与 Workers AI /ai/tomarkdown": "Cloudflare Browser Run /markdown and /scrape with Workers AI /ai/tomarkdown", "Browser Run 可用 Workers Free 额度。Browser Run 需要 Browser Rendering Edit;文件转换 API 接受 Workers AI Read 或 Edit。两类产品的配额和计费分开。": "Browser Run has a Workers Free allowance. It requires Browser Rendering Edit; file conversion accepts Workers AI Read or Edit. The products have separate quotas and billing.", "注册 Cloudflare,复制 Account ID;创建覆盖 Account → Browser Rendering → Edit 与 Account → Workers AI → Read 的自定义 token。官方 Workers AI REST 入门建议 Read 和 Edit 都授予;运行前用 GET /client/v4/user/tokens/verify 验证。": "Register with Cloudflare and copy the Account ID. Create a custom token covering Account → Browser Rendering → Edit and Account → Workers AI → Read. The Workers AI REST getting-started guide recommends both Read and Edit; verify the token at GET /client/v4/user/tokens/verify before running.", }) -TEXT.update({ - "十种方法仍执行同一组 150 题,保留 1,500 条权威结果。接口修正只认证重跑 Cloudflare 23 条和 Firecrawl 17 条,共 40 次新调用;任务通过数由 1,024 增至 1,029。document-05 的公共格式归一化不调用供应商,又增加 1 条通过,最终为 1,030/1,500。Moli 仍为 1.1.4。": "The same 150 tasks and ten methods retain 1,500 authoritative results. Certified interface reruns were limited to 23 Cloudflare and 17 Firecrawl results, for 40 new calls; task passes rose from 1,024 to 1,029. Shared formatting normalization for document-05 added one pass without another provider call, yielding 1,030/1,500. Moli remains at 1.1.4.", - "40 条新物理调用取代同一方法—题目的旧权威结果;其余 1,460 条沿用旧调用。document-05 的 Markdown 表格分隔行在所有方法共用的格式归一化中按排版处理,保存输出离线复评分。": "Forty new physical calls supersede the old authoritative results for the same method–task pairs; the other 1,460 calls are inherited. The Markdown table divider in document-05 is treated as layout by shared normalization across all methods, with saved outputs rescored offline.", - "40 条新结果先隐藏机器判定逐条独立核验;未变化的 1,460 条沿用哈希绑定的旧逐条核验。image-04 按同资产缩略图域名迁移裁决,并在公共资源规则中同时拒绝缺少缩略图标识的近似地址。": "The 40 new results were independently checked one by one with machine judgments hidden; 1,460 unchanged results inherit hash-bound earlier individual reviews. image-04 was adjudicated as a thumbnail-host migration of the same asset, while shared resource rules also reject similar URLs lacking a thumbnail marker.", - "当前 1,500/1,500 条结果均有逐条判定核验,未决判定 0。image-04 修正未改变当前成绩;异常根因调查与逐条判定核验分别记录,不把后者冒充前者。": "All 1,500/1,500 results have individual verdict review, with zero unresolved verdicts. The image-04 correction did not affect the current score. Root-cause anomaly investigation and individual verdict review are recorded separately; the latter is not presented as the former.", -}) PHRASES.update({ "原有资料核对日期:": "Existing sources checked: ", @@ -3121,8 +2780,6 @@ def add(source, target): TEXT.update({ - "150 道 Web Fetch 任务,Moli 1.1.1 通过 135 道,通过率为 90.0%,比 Chromium 多 10 道。净增来自文档与媒体:取得 PDF 正文、图片和视频资源,也是 AI 读取网络内容的一部分。": "Across 150 Web Fetch tasks, Moli 1.1.1 passes 135, a 90.0% pass rate and 10 more tasks than Chromium. The net gain comes from documents and media: retrieving PDF text, images, and video resources is also part of giving AI access to online content.", - "每题等权,失败与超时计入总数;通过表示交付结果满足题目要求。完整报告包含三个 Moli 版本,本文统一采用总通过数最高的 1.1.1。": "Tasks are equally weighted, with failures and timeouts included in the total. A pass means the delivered result meets the task requirements. The full report includes three Moli versions; this article consistently uses 1.1.1, which has the most total passes.", "98 道共同成功任务 · 平均耗时": "98 shared successful tasks · mean latency", "同组 98 道任务 · P50": "The same 98 tasks · P50", "Chromium:4.58 秒": "Chromium: 4.58 seconds", @@ -3156,7 +2813,6 @@ def add(source, target): "普通网页 88/100、动态网页 8/10;YouTube 题仅它通过": "General pages 88/100; dynamic pages 8/10; the only browser to pass YouTube", "计时包含网络往返;未测试托管成本、扩容和生产并发": "Timing includes network round trips; hosting cost, scaling, and production concurrency were not tested", "05 尚未完成的网页与文档": "05 Pages and documents still incomplete", - "Moli 1.1.1 通过 135/150 道任务,完成率为 90.0%;Chromium 为 125/150、83.3%。这是按题目要求验收的完成率。": "Moli 1.1.1 passes 135/150 tasks, a 90.0% completion rate, versus Chromium’s 125/150 and 83.3%. Completion is assessed against each task’s requirements.", "文档与媒体通过数为 24/30 对 14/30,构成 Moli 多通过的 10 道;120 道网页任务中,两者均为 111/120。": "Document and media passes are 24/30 versus 14/30, accounting for Moli’s 10 additional passes. Across 120 web tasks, both score 111/120.", "98 道共同成功任务中,Moli 的均值、P50、P95 为 3.01、2.44、6.03 秒,Chromium 为 5.27、4.58、10.83 秒。Moli 三项均较低,但这组跨批次记录尚不能隔离网络和运行条件的影响。": "Across 98 shared successful tasks, Moli’s mean, P50, and P95 are 3.01, 2.44, and 6.03 seconds, versus Chromium’s 5.27, 4.58, and 10.83 seconds. Moli is lower on all three, but these records collected across batches cannot isolate network and execution conditions.", "待补的缺口也具体:三份 W3C 标准的验证页、延迟列表、iframe 子页面,以及 DOCX 和嵌入式文档查看器。普通网页的正文词语找回比例,Moli 为 95.3%,Chromium 为 99.8%,还不能称为完全对齐。": "The remaining gaps are specific: verification pages on three W3C standards, delayed lists, iframe content, DOCX, and embedded document viewers. Reference-word recovery on general pages is 95.3% for Moli and 99.8% for Chromium, so they are not yet fully matched.", @@ -3165,13 +2821,10 @@ def add(source, target): }) -TEXT.update({ -}) TEXT.update({ "综合通过率 90.0%,五款中最高。": "Overall pass rate: 90.0%, the highest of the five.", - "Moli 1.1.1 通过 135/150 道任务,Chromium 为 125/150、83.3%。这是按题目要求验收的完成率。": "Moli 1.1.1 passes 135/150 tasks, versus Chromium’s 125/150 and 83.3%. Completion is assessed against each task’s requirements.", "多通过的 10 道,来自文档与媒体。": "Documents and media account for 10 additional passes.", "这 30 道中,Moli 通过 24 道,Chromium 通过 14 道;120 道网页任务中,两者均为 111/120。": "Of these 30 tasks, Moli passes 24 and Chromium passes 14. Across 120 web tasks, both score 111/120.", "98 道共同成功任务,三项耗时均较低。": "Lower on all three latency measures across 98 shared successes.", @@ -3397,7 +3050,6 @@ def add(source, target): "容器环境与镜像身份": "Container environment and image identity", "资源测量边界": "Resource measurement boundary", "四种本地方法资源消耗复核": "Resource-use review for four local methods", - "20 个预声明普通网页 × Lightpanda、Obscura、Moli 1.1.1、Chromium,共 80 条隔离容器结果。": "20 predeclared general pages × Lightpanda, Obscura, Moli 1.1.1, and Chromium: 80 isolated-container results.", "远程 Kitesurf 无法观测服务端 CPU、内存和上游网络;把失败结果的低消耗或宿主机进程采样混入比较,也会产生误导。": "Remote Kitesurf does not expose server-side CPU, memory, or upstream network use. Mixing in the low cost of failed results or host-process samples would also be misleading.", "每次调用使用独立 Docker 容器、cgroup v2 和专用网络命名空间;资源汇总只比较四种本地方法共同通过的 15 个页面。80/80 条结果在隐藏机器判定后逐条复核,分歧 0、未决 0。": "Every call uses a dedicated Docker container, cgroup v2, and a dedicated network namespace. Resource aggregates compare only the 15 pages passed by all four local methods. All 80/80 results were reviewed with machine verdicts hidden: 0 disagreements and 0 unresolved.", "50 ms 采样可能遗漏更短的内存和瞬时速率峰值;结果表示本次 arm64 Linux 容器中的客户端成本,不代表远程服务端成本、生产吞吐或 SLA。": "Sampling every 50 ms may miss shorter memory or instantaneous-rate peaks. Results describe client cost in this arm64 Linux container run, not remote server cost, production throughput, or an SLA.", @@ -3472,7 +3124,6 @@ def add(source, target): "内存资源对比": "Memory resource comparison", "网络资源对比": "Network resource comparison", "每次调用使用一次性 Linux 客户端容器、cgroup v2 与独立网络命名空间;宿主控制器和容器启停不计入。Kitesurf 的远端浏览器 CPU、内存和远端上游流量不可见,因此不进入资源表或本地引擎资源排名。": "Each call uses a disposable Linux client container, cgroup v2, and a dedicated network namespace; the host controller and container lifecycle are excluded. Kitesurf's remote-browser CPU, memory, and upstream traffic are invisible, so it is excluded from the resource table and local-engine resource ranking.", - "CPU 与网络总量来自累计计数;内存与瞬时速率按 50 ms 采样,可能遗漏更短尖峰。CPU 占用、传输速率不按“越低越好”评选。完整方法与逐次证据见环境与复现页。": "CPU and network totals come from cumulative counters. Memory and instantaneous rates are sampled every 50 ms and may miss shorter spikes. CPU utilization and transfer rates are not ranked as lower-is-better. See Methodology and Reproduction for full methods and per-call evidence.", "左右滑动查看完整对比": "Scroll horizontally for the full comparison", }) @@ -3480,9 +3131,7 @@ def add(source, target): re.compile(r"^资源表覆盖预先确定的全部 (\d+) 个普通网页,包含失败调用。这组任务的通过数:(.*)。各项平均值、P50、P95 使用同一完整范围。$"), lambda match: ( f"The resource table covers all {match.group(1)} predeclared general pages, including failed calls. " - "Pass counts: " + match.group(2).replace( - "Moli 1.1.15 · 独立资源批次", "Moli 1.1.15 · independent resource cohort" - ).replace(",", ", ") + "Pass counts: " + match.group(2).replace(",", ", ") + ". Mean, P50, and P95 share this complete population." ), )) @@ -3502,9 +3151,7 @@ def add(source, target): )) REGEX.insert(0, ( re.compile(r"^(?:Chromium|Lightpanda|Moli(?: \d+(?:\.\d+)*)?(?: · 独立资源批次)?|Obscura)(?:、(?:Chromium|Lightpanda|Moli(?: \d+(?:\.\d+)*)?(?: · 独立资源批次)?|Obscura))+$"), - lambda match: match.group(0).replace( - "Moli 1.1.15 · 独立资源批次", "Moli 1.1.15 · independent resource cohort" - ).replace("、", ", "), + lambda match: match.group(0).replace("、", ", "), )) REGEX.insert(0, ( re.compile(r"^(.+) 是本轮资源效率领先者,在 (\d+) 个可按低消耗比较的指标中取得 (\d+) 项最低。这里的资源领先不等于综合任务通过率领先。$"), @@ -3673,7 +3320,7 @@ def handle_decl(self, decl: str) -> None: self.parts.append(f"") def handle_starttag(self, tag: str, attrs) -> None: - localized = dict(attrs).get('data-report-en') if tag == 'span' else None + localized = dict(attrs).get('data-report-en') if tag in {'span', 'option'} else None parent_model = any(self.model_text_stack) self.model_text_stack.append(localized is not None or parent_model) self.translate_stack.append(self._allows_translation(attrs)) @@ -3837,17 +3484,8 @@ def preserve(match): TEXT.update({'内容选择': 'Content selection', '典型处理流程': 'Typical processing flow', '变更记录 · Web Fetch': 'Changelog · Web Fetch', - '已知 URL → 网页正文、页面资源地址或直接文档文字': 'Known URL → page body, resource URLs, or direct document text', - '已知 URL → 可读文本/文档文字': 'Known URL → readable text or document text', - '本地浏览器渲染 + Markdown 抽取': 'Local browser rendering + Markdown extraction', - '匿名 HTTP provider + Turndown': 'Anonymous HTTP provider + Turndown', - '已知 URL → 正文文本': 'Known URL → body text', - 'HTTP + BeautifulSoup 网页文本加载器': 'HTTP + BeautifulSoup page-text loader', '已知 URL → 渲染正文、结构化资源 URL 或原文件字节': 'Known URL → rendered body, structured resource URLs, or original file ' 'bytes', - 'HTTP GET + Readability 正文抽取(关闭 Firecrawl 回退)': 'HTTP GET + Readability body extraction (Firecrawl fallback ' - 'disabled)', - '已知 URL → raw_content': 'Known URL → raw_content', '逐任务结果与证据': 'Per-task results and evidence', '运行参数与版本': 'Run parameters and versions', '本报告纳入 4,300 个逻辑结果。运行参数见下表;保留的物理调用与输出见各结果详情。': 'This report includes 4,300 logical results. Run parameters ' diff --git a/harness/web_fetch/report_i18n_adoption_a.py b/harness/web_fetch/report_i18n_adoption_a.py index cad71d809..8bdbfd7ac 100644 --- a/harness/web_fetch/report_i18n_adoption_a.py +++ b/harness/web_fetch/report_i18n_adoption_a.py @@ -9,55 +9,15 @@ "LangChain WebBaseLoader、Crawl4AI、Moli WebFetch;使用本机网络与运行环境。": "LangChain WebBaseLoader, Crawl4AI, and Moli WebFetch; using the local network and runtime environment.", "内容选择": "Content selection", - "网页 → Markdown": "Webpage → Markdown", "现有 PDF / Office 解析": "Existing PDF / Office parsing", - "有限支持": "Limited support", "Markdown 引用;无独立列表": "Markdown references; no separate list", "页面超链接;非媒体接口": "Page hyperlinks; not a media interface", - "无此能力": "Not supported", "本端点只返回 Markdown": "This endpoint returns Markdown only", - "HTML → 纯文本": "HTML → plain text", - "PDF / DOCX / TXT 等": "PDF / DOCX / TXT, etc.", - "不提取图片属性": "Does not extract image attributes", - "无媒体提取器": "No media extractor", - "受支持文档落盘": "Supported documents saved to disk", - "渲染 HTML → Markdown": "Rendered HTML → Markdown", - "PDF 专用策略": "PDF-specific strategy", - "media.images + 元数据": "media.images + metadata", - "原生 HTTP 文件策略或浏览器下载": "Native HTTP file strategy or browser download", - "拒绝 PDF / Office MIME": "Rejects PDF / Office MIME types", - "HTML 中的 Markdown 引用": "Markdown references in HTML", - "页面中的普通超链接": "Ordinary hyperlinks on the page", - "拒绝二进制 MIME": "Rejects binary MIME types", - "PDF;Office 未声明": "PDF; Office not declared", - "普通 links 不保证媒体流": "Ordinary links do not guarantee media streams", - "无原文件返回字段": "No field for returning the original file", - "PDF(含 OCR)/ Office": "PDF (including OCR) / Office", - "images 返回图片 URL": "images returns image URLs", - "audio / video 下载地址": "audio / video download URLs", - "rawBase64 原始响应": "rawBase64 raw response", - "BeautifulSoup → 纯文本": "BeautifulSoup → plain text", - "PDF / Office 需其他 loader": "PDF / Office requires another loader", - "load 不返回 src": "load does not return src", - "load 不返回媒体字段": "load does not return media fields", - "Document 不是原始文件": "Document is not the original file", - "未列 PDF / Office 契约": "No PDF / Office contract listed", - "无专用媒体字段": "No dedicated media fields", - "无原文件返回契约": "No contract for returning the original file", - "JS 渲染网页 → Markdown / HTML": "JS-rendered webpage → Markdown / HTML", "未声明 PDF / Office 正文解析": "PDF / Office body parsing not declared", "页面引用;JSON HTML / eval 可取地址": "Page references; JSON HTML / eval can retrieve URLs", "页面入口;HTML / eval 可取地址": "Page entries; HTML / eval can retrieve URLs", "原始非 HTML 响应仅支持 html / json 模式": "Raw non-HTML responses are supported only in html / json modes", - "无 PDF / Office 解析器": "No PDF / Office parser", - "原生 Markdown / 超链接": "Native Markdown / hyperlinks", - "正文中的普通超链接": "Ordinary hyperlinks in the body", - "文本输出不是原文件": "Text output is not the original file", - "Markdown / 纯文本": "Markdown / plain text", - "Extract 未列格式保证": "Extract lists no format guarantee", - "无转写/媒体下载契约": "No transcription/media download contract", - "无原始字节字段": "No raw-byte field", "支持 · 浏览器 + 等待参数": "Supported · Browser + wait parameters", "URL 或 HTML 字符串;/content、/links、/screenshot 为其他端点。": "URL or HTML string; /content, /links, and /screenshot are separate endpoints.", @@ -66,64 +26,15 @@ "支持 cookies、HTTP authenticate 等参数;浏览器流量仍可能被识别和拦截。": "Supports cookies, HTTP authenticate, and other parameters; browser traffic may still be detected and blocked.", "无此能力 · HTTP,不运行 JS": "Not supported · HTTP, no JS execution", - "HTTP(S) URL;不是本地文件上传接口。": "HTTP(S) URL; not a local file-upload interface.", - "文本、文档标题和保存路径;没有通用 JSON schema 抽取。": - "Text, document title, and saved path; no generic JSON schema extraction.", - "当前工具未提供站点 Cookie 参数;不是登录浏览器。": - "The current tool provides no site Cookie parameter; it is not a logged-in browser.", "支持 · 浏览器 + JS 动作": "Supported · Browser + JS actions", - "网页 URL;远程或本地 PDF 需专用策略。": - "Webpage URL; remote or local PDF requires a dedicated strategy.", - "可选 CSS/JSON 或 LLM 提取策略;media/links 是结构化资源信息。": - "Optional CSS/JSON or LLM extraction strategies; media/links provides structured resource information.", - "可配置持久会话、Cookie 和页面动作;并非保证突破每个站点限制。": - "Configurable persistent sessions, cookies, and page actions; does not guarantee bypassing every site's restrictions.", "无此能力 · 匿名 HTTP 获取": "Not supported · anonymous HTTP fetching", - "HTTP(S) URL;只接受 HTML、text/*、JSON/XML 等文字 MIME。": - "HTTP(S) URL; accepts only textual MIME types such as HTML, text/*, and JSON/XML.", - "Turndown 转换正文和表格,返回工具文本与状态。": - "Turndown converts body content and tables, returning tool text and status.", - "拒绝 URL 内嵌凭据;跨源重定向需独立调用,不是登录会话浏览器。": - "Rejects credentials embedded in URLs; cross-origin redirects require a separate call, and this is not a logged-in session browser.", "支持 · Contents 动态页抓取": "Supported · Contents dynamic-page crawling", - "一组 URL;可配置 subpages,不是任意本地文件上传接口。": - "A set of URLs; subpages is configurable, and this is not an arbitrary local file-upload interface.", - "text、highlights、summary;摘要可用 schema,含模型生成环节。": - "text, highlights, and summary; summary supports a schema and includes a model-generation step.", - "maxAgeHours 控制缓存/实时抓取,不提供登录成功保证。": - "maxAgeHours controls cached/live crawling; it does not guarantee successful login.", "支持 · 渲染与 Actions": "Supported · rendering and Actions", - "Scrape 接收 URL;本地文件上传走另一个 /v2/parse 接口。": - "Scrape accepts a URL; local file uploads use the separate /v2/parse endpoint.", - "可选 JSON schema、产品字段、截图等格式;不能都视为本轮已测。": - "Optional formats include JSON schema, product fields, and screenshots; they cannot all be treated as tested in this run.", - "可配置页面动作;站点授权、反爬与套餐限制仍会影响访问。": - "Page actions are configurable; site authorization, anti-bot measures, and plan limits can still affect access.", - "单个或多个网页 URL;load 返回 Document 对象。": - "One or more webpage URLs; load returns Document objects.", - "page_content + 页面元数据;scrape 可另取 BeautifulSoup 自行提取属性。": - "page_content + page metadata; scrape can separately return BeautifulSoup for custom attribute extraction.", - "可传入 requests Session / headers;不自动执行登录交互。": - "Accepts a requests Session / headers; does not automate login interactions.", "支持 · 按需渲染": "Supported · on-demand rendering", - "URL 或已捕获的 dom_id;/v1/dom/dump 为独立 DOM 捕获接口。": - "URL or a captured dom_id; /v1/dom/dump is a separate DOM-capture endpoint.", - "title、author、links、images 等固定字段;可选 trace/raw_dom。": - "Fixed fields such as title, author, links, and images; optional trace/raw_dom.", - "API Key + Project ID 是服务鉴权,不是源站登录凭据;公开文档面向公共网页。": - "API Key + Project ID authenticate the service, not the source site; the public documentation targets public webpages.", "支持 · 默认运行 JS,支持多种就绪等待": "Supported · runs JS by default and supports multiple readiness waits", - "单个 URL;每个页面单独调用 fetch。": "A single URL; fetch is called separately for each page.", "JSON 返回最终 URL、状态、标题、响应头、重定向链和 HTML;--eval 可返回 DOM 字段。": "JSON returns the final URL, status, title, response headers, redirect chain, and HTML; --eval can return DOM fields.", - "支持授权 cookie-file / profile-dir 和请求头;仍可能被站点拒绝。默认适配器不加载登录状态。": - "Supports authorized cookie-file / profile-dir and request headers; sites may still reject access. The default adapter does not load login state.", "无此能力 · 需另用 Browser": "Not supported · requires a separate Browser", - "HTTP(S) URL;可选 Markdown 或纯文本。": "HTTP(S) URL; optional Markdown or plain text.", - "正文与抓取元数据;不提供任意 schema 抽取。": - "Body content and crawl metadata; no arbitrary schema extraction.", - "可配置请求头;登录/JS 重页面由独立 Browser 工具处理。": - "Request headers are configurable; login-heavy or JS-heavy pages are handled by the separate Browser tool.", "支持 · 复杂页建议 advanced": "Supported · advanced is recommended for complex pages", } diff --git a/harness/web_fetch/report_i18n_adoption_b.py b/harness/web_fetch/report_i18n_adoption_b.py index bdb5ae71d..1b977c0df 100644 --- a/harness/web_fetch/report_i18n_adoption_b.py +++ b/harness/web_fetch/report_i18n_adoption_b.py @@ -1,66 +1,36 @@ """English translations for the assigned adoption-report narrative segment.""" TEXT = { - "接口声明的 HTML/文本正文;声明为空时保持为空": "Declared HTML/text body; an empty declared body stays empty", - "模型展示文本;与声明正文分别保存": "Model-facing display text; saved separately from the declared body", - "成功态的规范源站状态与最终 URL": "Canonical origin status and final URL for a successful tool result", - "展示投影;仅在规范值缺失时补充状态或 URL": "Display projection; supplies status or URL only when the canonical value is absent", - "通过内置 HTTP provider 返回声明正文及展示文本;调用预算和读取字段以保留证据为准。不是 DeepSeek 聊天 API,也没有调用模型生成答案。": "The built-in HTTP provider returns the declared body and display text; retained evidence identifies the call budget and selected fields. This is not the DeepSeek chat API, and no model generated the answer.", - "已知 URL → 原生声明 HTML/文本正文 → 共有解析": "Known URL \u2192 native declared HTML/text body \u2192 shared parsing", - "声明正文;完整工具回包和展示文本独立保存": "Declared body; the complete tool response and display text are saved separately", - "渲染与 Actions": "Rendering and Actions", - "单个或批量 URL;Extract 与 Search、Research 是不同接口。": "Single or batch URLs; Extract, Search, and Research are separate APIs.", - "raw_content 和图片列表;query 可选择相关片段,并非必然返回全文。": "raw_content and an image list; query can select relevant snippets and does not necessarily return the full text.", - "公开 Extract 参数没有源站登录会话接口;advanced 不等于保证绕过登录。": "The public Extract parameters provide no interface for authenticated source-site sessions; advanced does not guarantee bypassing login.", "页面中的链接可能被保留,不是媒体专用抽取保证。/pdf 是把网页打印成 PDF,不是读取已有 PDF;Workers AI 的 AI.toMarkdown 文档转换也不是本轮 /markdown。": "Links on the page may be retained, but this is not a dedicated media-extraction guarantee. /pdf prints a webpage to PDF rather than reading an existing PDF; Workers AI's AI.toMarkdown document conversion is also outside this run's /markdown endpoint.", "只调用 /markdown,waitUntil=networkidle2;未提供源站登录 Cookie,未调用其他端点。": "Only /markdown was called with waitUntil=networkidle2; no source-site login Cookie was provided, and no other endpoint was called.", "Cloudflare · Markdown 端点 ↗": "Cloudflare · Markdown endpoint ↗", - "PDF 使用 pypdf 读取文字层,不含 OCR;DOCX 读取段落。另有 XLSX、PPTX 解析分支并依赖相应库。虽然扩展名列表包含 XLS/PPT,实际采用 openpyxl/python-pptx,不能据此保证旧二进制格式可解析。文档下载成功可返回本地路径;不是任意图片或视频下载器。": "PDF uses pypdf to read the text layer, without OCR; DOCX reads paragraphs. Separate XLSX and PPTX parsing branches depend on their respective libraries. Although the extension list includes XLS/PPT, the implementation uses openpyxl/python-pptx, so this does not guarantee support for legacy binary formats. A successfully downloaded document can return a local path; this is not a general-purpose image or video downloader.", - "本轮调用同一 WebFetch;本机是否安装解析依赖、文件是否能解析仍看实测,支持声明不等于必然成功。": "This run called the same WebFetch; whether parsing dependencies are installed locally and whether a file can be parsed still depends on measured behavior. Declared support does not guarantee success.", "CowAgent · WebFetch 源码 ↗": "CowAgent · WebFetch source ↗", - "正文 AsyncWebCrawler;文件 AsyncHTTPCrawlerStrategy": "Body content: AsyncWebCrawler; files: AsyncHTTPCrawlerStrategy", - "PDF 需 PDFCrawlerStrategy + PDFContentScrapingStrategy,可选提取并保存内嵌图片,默认不含扫描件 OCR。浏览器下载需 accept_downloads 并触发下载;原生 HTTP 文件策略直接将非 HTML 响应保存到 downloaded_files。网页截图和打印 PDF 是新生成的文件,不是目标原文件。": "PDF requires PDFCrawlerStrategy + PDFContentScrapingStrategy and can optionally extract and save embedded images; scanned-document OCR is not included by default. Browser downloads require accept_downloads and a triggered download; the native HTTP file strategy saves non-HTML responses directly to downloaded_files. Webpage screenshots and printed PDFs are newly generated files, not the original target files.", "正文使用原通用浏览器策略、无缓存;文件补测在调用前选择原生 HTTP 文件策略,读取 downloaded_files 并统一校验。未启用专用 PDF 策略、截图或 LLM 提取;媒体字段不代表内容理解。": "Body content used the original general browser strategy without caching; the supplemental file tests selected the native HTTP file strategy before each call, read downloaded_files, and applied uniform validation. The dedicated PDF strategy, screenshots, and LLM extraction were not enabled; media fields do not imply content understanding.", "Crawl4AI · 链接与媒体 ↗": "Crawl4AI · Links and media ↗", "Crawl4AI · PDF 策略 ↗": "Crawl4AI · PDF strategy ↗", "Crawl4AI · 文件下载 ↗": "Crawl4AI · File downloads ↗", "Crawl4AI · 配置 ↗": "Crawl4AI · Configuration ↗", - "依据本轮固定源码的内容类型白名单;PDF、image/*、video/* 不在可解码类型中。HTML 内出现媒体链接不意味着可直接抓取对应二进制 URL。": "Based on the content-type allowlist in the source fixed for this run; PDF, image/*, and video/* are not decodable types. Media links appearing in HTML do not imply that the corresponding binary URLs can be fetched directly.", - "仅匿名 HTTP provider;不是 DeepSeek 模型 API,也没有使用模型作评分。": "Anonymous HTTP provider only; this is not the DeepSeek model API, and no model was used for scoring.", "DeepSeek Harness · MIME 策略 ↗": "DeepSeek Harness · MIME policy ↗", "DeepSeek Harness · Markdown 转换 ↗": "DeepSeek Harness · Markdown conversion ↗", "extras.links 和 imageLinks 是地址;PDF text 是解析后的文字,不是原 PDF。summary/highlights 与正文原文提取的语义不同,生成摘要不能作为独立 ground truth。": "extras.links and imageLinks are addresses; PDF text is parsed text, not the original PDF. summary/highlights have different semantics from verbatim body extraction, and generated summaries cannot serve as independent ground truth.", - "extras.links 和 imageLinks 是不带名称归属的地址;richLinks 返回明确的 anchor—URL 对。PDF text 是解析后的文字,不是原 PDF。summary/highlights 与正文原文提取的语义不同,生成摘要不能作为独立 ground truth。": "extras.links and imageLinks are addresses without name ownership; richLinks returns explicit anchor–URL pairs. PDF text is parsed text, not the original PDF. summary/highlights have different semantics from verbatim body extraction, and generated summaries cannot serve as independent ground truth.", "text=true、maxAgeHours=0,并取 links/imageLinks;本轮没有调用摘要或高亮生成。": "text=true and maxAgeHours=0, with links/imageLinks requested; this run did not invoke summary or highlight generation.", "Exa · Contents 参数 ↗": "Exa · Contents parameters ↗", - "rawBase64 必须单独请求,解码后是目标 URL 的原始 HTTP 响应体,不包含页面引用的所有资源。audio/video 针对支持的视频站点,返回有时效的下载 URL,不保证任意站点或原编码字节,也不是字幕转写/画面理解。文档解析覆盖 PDF、Word、Excel、PowerPoint 等。": "rawBase64 must be requested separately; after decoding, it is the raw HTTP response body for the target URL, not every resource referenced by the page. audio/video targets supported video sites and returns time-limited download URLs; it does not guarantee arbitrary sites or original encoded bytes, and it is neither subtitle transcription nor visual understanding. Document parsing covers PDF, Word, Excel, PowerPoint, and other formats.", "正文与资源题请求 markdown、links、images;原文件题只请求 rawBase64 并验收目标 URL 的解码字节。动态顶层选择器使用 Actions wait,随后执行预声明稳定延迟;选择器数量与 iframe 子文档条件仍标为未完整执行。未测试 audio、video、JSON、截图或 Parse。": "Body and resource tasks request markdown, links, and images; original-file tasks request rawBase64 alone and validate the decoded bytes for the target URL. Dynamic top-level selectors use an Actions wait followed by the predeclared settle delay; selector-count and iframe child-document conditions remain marked as not fully enforced. Audio, video, JSON, screenshots, and Parse were not tested.", "Firecrawl · Scrape 格式 ↗": "Firecrawl · Scrape formats ↗", "Firecrawl · 文档解析 ↗": "Firecrawl · Document parsing ↗", - "get_text 不保留 href/src 属性;页面若本身写出 URL,文字中仍可能出现地址。LangChain 有独立 PDF、Word 和浏览器加载器,但不能合并成 WebBaseLoader.load 的能力。": "get_text does not preserve href/src attributes; addresses may still appear in the text when the page itself prints URLs. LangChain has separate PDF, Word, and browser loaders, but their capabilities cannot be attributed to WebBaseLoader.load.", - "只调用 load 读取正文;没有额外 soup 解析、PDF loader 或浏览器渲染。": "Only load was called to read body content; no additional soup parsing, PDF loader, or browser rendering was used.", "LangChain · WebBaseLoader 源码 ↗": "LangChain · WebBaseLoader source ↗", "LangChain · 其他文档加载器 ↗": "LangChain · Other document loaders ↗", - "POST /v1/extract · 中国区": "POST /v1/extract · China region", - "images 是资源引用,不是图片字节或视觉理解;links 可能包含文档或视频入口,但不保证提取视频流。PDF/Office、音视频转写和通用二进制下载均未在所核对的 Extract 契约中明确声明,不能写成确定不支持。": "images are resource references, not image bytes or visual understanding; links may include document or video entry points but do not guarantee video-stream extraction. PDF/Office, audio/video transcription, and general binary downloads are not explicitly declared in the reviewed Extract contract, so they cannot be described as definitively unsupported.", - "国际区与中国区共用公开契约,但部署与实测分开;这里不推断两区内部引擎完全相同。": "The international and China regions share a public contract, but their deployments and measurements are separate; this does not assume that their internal engines are identical.", "Lexmount · Extract 契约 ↗": "Lexmount · Extract contract ↗", "Lexmount · WebFetch 概览 ↗": "Lexmount · WebFetch overview ↗", - "POST /v1/extract · 国际区": "POST /v1/extract · International region", "moli fetch;正文 markdown,原文件 html 字节输出": "moli fetch; markdown for body content and html byte output for original files", "截图和 PDF 是启用 --layout 后生成的页面产物,不是读取已有文档或下载原媒体。官方未承诺文档 OCR、媒体理解或音频转写;非 HTML 响应不支持 Markdown。": "Screenshots and PDFs are page artifacts generated after enabling --layout, not reads of existing documents or downloads of original media. The official documentation does not promise document OCR, media understanding, or audio transcription; non-HTML responses do not support Markdown.", "正文使用 Markdown,文件任务预先选择 html 原始字节模式;done + 私网屏蔽,无 layout、额外资源加载或 eval,无失败后格式回退。只有实际执行记录参与榜单。": "Body content used Markdown, while file tasks selected html raw-byte mode in advance; done + private-network blocking was used, without layout, additional resource loading, or eval, and without a post-failure format fallback. Only actual execution records enter the ranking.", "Moli · 输出与接口边界 ↗": "Moli · Output and API boundaries ↗", - "web_fetch · 内置 HTTP 路径": "web_fetch · Built-in HTTP path", - "Readability 的 HTML 输出经自带转换器处理,不直接提取 img.src;只有服务端原生 Markdown 或正文普通超链接等路径可能留下图片地址。不能保证收集图片或播放入口。PDF/媒体二进制误解码成字符串不是原文件支持;Firecrawl 等可选回退是另一路径。": "Readability's HTML output is processed by its bundled converter and does not directly extract img.src; image addresses may remain only through paths such as server-native Markdown or ordinary body hyperlinks. Image collection or playback entry points are not guaranteed. Misdecoding PDF/media binaries as strings is not original-file support; optional fallbacks such as Firecrawl are separate paths.", - "本轮开启 Readability、关闭 Firecrawl 回退及缓存;不能把整个 OpenClaw 的浏览器或 PDF 工具能力算到此行。": "This run enabled Readability and disabled the Firecrawl fallback and cache; the capabilities of OpenClaw's broader browser or PDF tools cannot be attributed to this row.", "OpenClaw · Web Fetch 文档 ↗": "OpenClaw · Web Fetch documentation ↗", "OpenClaw · 抽取实现 ↗": "OpenClaw · Extraction implementation ↗", "OpenClaw · HTML 转换器 ↗": "OpenClaw · HTML converter ↗", "OpenClaw · Readability 插件 ↗": "OpenClaw · Readability plugin ↗", - "advanced 面向动态页、表格和嵌入内容;不能把 embedded media 理解为下载视频或理解画面。官方 Research 的 PDF/Markdown/JSON/TXT 文件上传能力不属于本次 Extract;Extract 的 PDF/Office 支持在本次核对资料中未明确承诺。": "advanced targets dynamic pages, tables, and embedded content; embedded media must not be interpreted as video downloading or visual understanding. The official Research capability to upload PDF/Markdown/JSON/TXT files is outside this Extract test; PDF/Office support for Extract is not explicitly promised in the reviewed materials.", - "本轮 advanced + markdown + include_images=true,不传 query;未测试 Research 文件上传。": "This run used advanced + markdown + include_images=true without query; Research file uploads were not tested.", "Tavily · Extract 契约 ↗": "Tavily · Extract contract ↗", "Tavily · 提取深度 ↗": "Tavily · Extraction depth ↗", "Tavily · Research 文件上传 ↗": "Tavily · Research file uploads ↗", diff --git a/harness/web_fetch/report_i18n_adoption_d.py b/harness/web_fetch/report_i18n_adoption_d.py index 85570d5ea..6b53342b3 100644 --- a/harness/web_fetch/report_i18n_adoption_d.py +++ b/harness/web_fetch/report_i18n_adoption_d.py @@ -1,24 +1,13 @@ """English translations for the adoption report, shard D.""" TEXT = { - "所有网页、文档正文与网页内嵌媒体任务": "All webpage, document-body, and embedded-media tasks", "本地浏览器;无托管调用单价": "Local browser; no managed per-call price", - "网页与文档任务由 --dump markdown 返回原生 Markdown;直接媒体文件返回原始字节;等待条件不参与内容抽取。": "Webpage and document tasks use --dump markdown to return native Markdown. Direct media files return original bytes; wait conditions do not extract content.", "本地 CLI;无托管 API 单价": "Local CLI; no managed API unit price", "本地开源库;非云套餐": "Local open-source library; no cloud plan", "计算、浏览器、网络及代理成本自担;商业云服务价格不能当成本地调用的费用。": "The caller pays compute, browser, network, and proxy costs. Commercial cloud prices do not represent local-call costs.", "内置或本地实现": "Built-in or local implementation", "接口与实现 ↗": "Interface and implementation ↗", - "已知 URL → 原生 Markdown 或原文件字节": "Known URL → native Markdown or original file bytes", "实际版本和接口见运行记录;下方链接指向官方说明。": "See the run records for the actual version and interface. The links below point to official documentation.", - "所有页面类任务都取得完整渲染 DOM,再使用 node-html-markdown 2.0.0 转为 Markdown;不运行资源提取脚本,不读取 PDF.js 等查看器私有对象,也不使用外部文档解析器。页面获取、DOM 转换与原始文件读取均计入耗时;原始字节、HTML 与转换器哈希均保留。": "Every webpage task obtains the complete rendered DOM and converts it to Markdown with node-html-markdown 2.0.0. No resource-extraction script, private viewer object such as PDF.js, or external document parser is used. Page retrieval, DOM conversion, and original-file reads all count toward latency. Original bytes, HTML, and converter hashes are retained.", - "网页正文、嵌入文档以及网页中的图片或视频信息都使用指定 Kitesurf 的 Browser Run 原生 Markdown。直接文档也使用官方 Markdown 接口,直接文件通过官方 CDP 获取原始响应。Workers AI 文档转换不计入 Kitesurf 成绩。客户端耗时包含公网往返、排队与执行。": "Webpage body text, embedded documents, and image or video information use native Browser Run Markdown from the specified Kitesurf. Direct documents also use the official Markdown interface; direct files use official CDP to obtain original responses. Workers AI document conversion is excluded from Kitesurf results. Client latency includes public-network round trips, queuing, and execution.", - "网页正文、嵌入文档以及网页中的图片或视频信息都使用浏览器原生 Markdown。直接文件使用浏览器已有的原始响应接口。不在失败后切换输出方式、调用共同 DOM 脚本,或请求外部服务补正文。接口未提供 HTTP 状态或最终网址时保留空值。": "Webpage body text, embedded documents, and image or video information use the browser's native Markdown. Direct files use its existing original-response interface. Failures do not trigger output switching, a shared DOM script, or an external service to supply missing text. HTTP status and final URL remain empty when the interface does not provide them.", - "网页、文档正文、结构与网页内嵌媒体任务": "Webpage, document-body, structured, and embedded-media tasks", - "直接取得 Markdown;直接 PDF/DOCX 不提供可读正文时失败": "Obtain Markdown directly; direct PDF/DOCX tasks fail without readable body text", - "旧版预声明完成条件": "Previously declared completion conditions", - "只等待,不抽取或拼接正文": "Wait only; do not extract or assemble body text", - "网页与文档任务为原生 Markdown;直接文件为原始字节": "Native Markdown for webpage and document tasks; original bytes for direct files", "使用本轮绑定提交构建的二进制;提交、构建环境与 SHA-256 见环境与复现。": "Use the binary built from this run's bound commit. See Reproduction for the commit, build environment, and SHA-256.", "假设仅使用 /markdown,全部用量可计费;按响应头 X-Browser-Ms-Used 计时。每千页边际价=每页秒数 × $0.025,不含最低月费、赠送时长和月度取整。": "Assumes exclusive use of /markdown and that all usage is billable, timed by the X-Browser-Ms-Used response header. Marginal price per thousand pages = seconds per page × $0.025, excluding the monthly minimum, included duration, and monthly rounding.", "5 秒": "5 seconds", @@ -48,24 +37,7 @@ "单位成本=该档全部请求的资源与运营成本 ÷ 可计费成功页数;价格底线=单位成本 ÷(1−目标毛利率)。即使失败不向客户收费,失败重试成本仍须纳入分子。套餐定价还需考虑未用额度、峰值并发及承诺服务。": "Unit cost = resource and operating costs for all requests in the tier ÷ billable successful pages; price floor = unit cost ÷ (1 − target gross margin). Even when customers are not charged for failures, failed-retry costs must remain in the numerator. Plan pricing must also account for unused credits, peak concurrency, and committed service levels.", "比较实际采购成本时,用实际总账单 ÷ 满足相同业务验收条件的页面数。不要直接把当前公开价格除以本轮任务通过率:执行日期、付费档位、供应商成功口径、缓存与功能加价都未必一致。以上为定价设计建议,不是提供方现行条款。": "To compare actual procurement costs, use the actual total bill ÷ pages meeting the same business acceptance criteria. Do not simply divide current public pricing by this run's task pass rate: execution dates, paid tiers, provider definitions of success, caching, and feature surcharges may differ. These are pricing-design recommendations, not current provider terms.", "本表指标说明": "Metric definitions for this table", - "云端浏览器加载网页后转换为 Markdown。": "A cloud browser loads the page and converts it to Markdown.", - "Quick Actions 默认最多缓存 5 秒;waitUntil=networkidle2": "Quick Actions uses its default cache for up to 5 seconds; waitUntil=networkidle2", - "CowAgent 内的 HTTP 抓取和文本、文档解析工具。": "HTTP retrieval and text and document parsing tools within CowAgent.", - "纯文本": "Plain text", - "实时请求": "Real-time request", - "本地浏览器渲染,再抽取 Markdown、媒体及链接信息。": "Renders in a local browser, then extracts Markdown, media, and link information.", - "本轮使用匿名 HTTP provider 与 Turndown 转换。": "This run used the anonymous HTTP provider with Turndown conversion.", - "本轮调用 Contents 取得指定 URL 的正文,不测试搜索。": "This run called Contents to retrieve body text from the specified URL; search was not tested.", - "云端 Scrape 为正文与资源题返回 Markdown/地址,原文件题用 rawBase64 交付目标字节。": "Cloud-hosted Scrape returns Markdown/addresses for body and resource tasks, while original-file tasks use rawBase64 to deliver the target bytes.", - "WebBaseLoader 使用 HTTP 与 BeautifulSoup 取得网页文字。": "WebBaseLoader uses HTTP and BeautifulSoup to retrieve webpage text.", - "Lexmount 中国区 API;与国际 API 分开实测,不重复计算品牌采用。": "Lexmount China API; tested separately from the international API without double-counting brand adoption.", - "中国区托管实时抓取": "Managed real-time retrieval in the China region", - "Lexmount 国际 API,返回网页主正文与元数据。": "Lexmount international API, returning the webpage's main body and metadata.", - "托管实时抓取": "Managed real-time retrieval", "Moli 一次性 fetch,默认运行网页 JavaScript 并输出 Markdown;不调用模型。": "Moli performs a one-shot fetch, runs webpage JavaScript by default, and outputs Markdown; it does not call a model.", "stdout Markdown;stderr 独立诊断": "Markdown on stdout; separate diagnostics on stderr", "每次启动独立进程;wait-until=done": "Starts a separate process for each call; wait-until=done", - "本轮使用 HTTP + Readability;关闭 Firecrawl 回退,避免混成两个方法。": "This run used HTTP + Readability with the Firecrawl fallback disabled to avoid mixing two methods.", - "本轮调用 Extract,从给定 URL 提取可读内容。": "This run called Extract to retrieve readable content from the given URL.", - "供应商实时抽取": "Provider-hosted real-time extraction", } diff --git a/harness/web_fetch/report_interfaces.py b/harness/web_fetch/report_interfaces.py index 162c79ade..1673eebf6 100644 --- a/harness/web_fetch/report_interfaces.py +++ b/harness/web_fetch/report_interfaces.py @@ -1,214 +1,76 @@ """Interfaces actually used by the benchmark, not a claim to cover entire SDKs.""" import json import re +from types import SimpleNamespace from html import escape +from harness.numbers import positive_number from lexbench_webfetch.paths import contained_path -from .methods import native_browser_engine, browser_method_registry, browser_method_label -from .report_components import measurement_presentation +from .methods import native_browser_engine, browser_method_label +from .report_components import measurement_presentation, bilingual_text +from .providers.headless import output_mode_for_case -URL='https://example.com/' -HEADLESS_ACCESS={ - 'kitesurf':{ - 'account':'需要开通 Browser Rendering / Browser Run 的 Cloudflare 账号及 API Token。', - 'configuration':'在仓库本地 .env 配置 与 ;报告和示例只保留占位符。', - 'env_vars':['CLOUDFLARE_ACCOUNT_ID','CLOUDFLARE_API_TOKEN'], - 'links':[('Cloudflare 控制台','https://dash.cloudflare.com/'),('Kitesurf 接入说明','https://developers.cloudflare.com/browser-run/kitesurf/'),('Markdown 端点','https://developers.cloudflare.com/browser-run/quick-actions/markdown-endpoint/')], - }, - 'lightpanda':{'account':'不需要供应商账号或 API Key。','configuration':'从官方项目获取二进制,配置 LIGHTPANDA_BINARY;不使用托管服务凭据。','env_vars':['LIGHTPANDA_BINARY'],'links':[('官方项目与安装说明','https://github.com/lightpanda-io/browser')]}, - 'obscura':{'account':'不需要供应商账号或 API Key。','configuration':'从官方项目获取程序及配套 worker,配置 OBSCURA_BINARY。','env_vars':['OBSCURA_BINARY'],'links':[('官方项目与安装说明','https://github.com/h4ckf0r0day/obscura')]}, - 'moli-1.0.6':{'account':'不需要供应商账号或 API Key。','configuration':'从官方 v1.0.6 release 获取二进制,配置 MOLI_1_0_6_BINARY;发布归档与可执行文件均校验 SHA-256。','env_vars':['MOLI_1_0_6_BINARY'],'links':[('Moli 1.0.6 正式发布','https://github.com/lexmount/moli/releases/tag/v1.0.6')]}, - 'moli':{'account':'不需要供应商账号或 API Key。','configuration':'安装与本报告版本和 SHA-256 一致的二进制,配置 MOLI_BINARY;详见“环境与复现”。','env_vars':['MOLI_BINARY'],'links':[('官方项目与安装说明','https://github.com/lexmount/moli')]}, - 'moli-1.1.7':{'account':'不需要供应商账号或 API Key。','configuration':'从官方 v1.1.7 release 获取二进制,配置 MOLI_1_1_7_BINARY。','env_vars':['MOLI_1_1_7_BINARY'],'links':[('Moli 1.1.7 正式发布','https://github.com/lexmount/moli/releases/tag/v1.1.7')]}, - 'moli-1.1.11':{'account':'不需要供应商账号或 API Key。','configuration':'从官方 v1.1.11 release 获取二进制,配置 MOLI_1_1_11_BINARY。','env_vars':['MOLI_1_1_11_BINARY'],'links':[('Moli 1.1.11 正式发布','https://github.com/lexmount/moli/releases/tag/v1.1.11')]}, - 'moli-pr643':{'account':'不需要供应商账号或 API Key。','configuration':'从 Moli PR #643 的固定源码提交构建,配置 MOLI_PR643_BINARY;此方法是候选构建版。','env_vars':['MOLI_PR643_BINARY'],'links':[('Moli 源码 PR','https://github.com/lexmount/moli/pull/643')]}, - 'moli-1.1.3':{'account':'不需要供应商账号或 API Key。','configuration':'从官方 v1.1.3 release 获取二进制,配置 MOLI_1_1_3_BINARY;发布包与可执行文件均校验 SHA-256。','env_vars':['MOLI_1_1_3_BINARY'],'links':[('Moli 1.1.3 正式发布','https://github.com/lexmount/moli/releases/tag/v1.1.3')]}, - 'chromium':{'account':'不需要 Google 账号或 API Key;不加载个人登录会话。','configuration':'安装与本报告版本和 SHA-256 一致的浏览器,配置 CHROMIUM_BINARY;详见“环境与复现”。','env_vars':['CHROMIUM_BINARY'],'links':[('Chrome for Testing','https://googlechromelabs.github.io/chrome-for-testing/'),('官方浏览器安装说明','https://playwright.dev/python/docs/browsers')]}, -} -def spec(entry,auth,params,fields,request,source,notes): - return dict(entry=entry,auth=auth,params=params,fields=fields,request=request,source=source,notes=notes) -SPECS={ - 'moli-webfetch':spec('CLI · 原生 Markdown 或原始响应路线','本地二进制;无 API Key', - [('URL','string · 必填','公开 HTTP(S) 页面或直接文件'),('--dump markdown','所有网页、文档正文与网页内嵌媒体任务','直接取得 Moli 原生 Markdown;直接 PDF/DOCX 未提供可读正文时失败'),('--layout / --with-frames','动态网站正文整类;网页内嵌媒体任务使用 --with-frames','动态正文统一启用 Moli 页面布局能力,并在同一次原生 Markdown 调用中包含已加载子文档;不按题目设置 selector'),('--wait-until / --wait-selector / --wait-script','string','使用 Moli 官方等待能力;只等待,不抽取或拼接正文'),('外层截止','number','外层统一为 45 秒。内部参数以冻结的调用记录为准:显式模式留出收尾和清理时间;uniform 模式沿用产品默认值'),('--block-private-networks','flag','默认启用')], - [('stdout','UTF-8 string / bytes','Markdown、eval 结果或直接文件原始字节'),('stderr','string','独立诊断;不混入正文'),('exit_code','integer','0 表示工具请求完成;正文或文件为空仍由任务规则单独判失败'),('status_code / final_url','可能未提供','CLI 未提供时保持未知,不伪造 HTTP 200 或最终 URL')], - {'page_body':'moli fetch --dump markdown --wait-until done URL','dynamic_body':'moli fetch --dump markdown --layout --with-frames --wait-until domstable URL','exact_page_identity':'正文仍用原生 Markdown;最终网址不是评分条件,无法确认时保持空值','webpage_media':'moli fetch --dump markdown --with-frames --wait-until done URL','direct_document_body':'moli fetch --dump markdown --wait-until done URL','direct_file':'Moli 原生 CDP 响应;附件事件使用同一 Moli 原始附件接口'}, - 'harness/web_fetch/providers/moli.py','网页正文、文档正文、结构题和网页内嵌媒体题统一使用 Moli 原生 Markdown。动态网站正文整类统一启用 --layout、--with-frames 和 domstable,不按题目设置 selector。网页内嵌媒体只在原生输出中核对题目指定的资源地址,不下载媒体,也不切换 HTML、JSON 或网络跟踪接口。直接 PDF/DOCX 没有交付可读正文时按失败记录,评测端不下载后解析。直接媒体文件使用原生 CDP 响应或同一二进制的原始附件接口。最终网址只作诊断,不决定正文是否通过;失败后不切换格式或借用其他 provider。'), - 'lexmount-global':spec('POST https://api.lexmount.com/v1/extract','X-API-Key: ;X-Project-Id: ', - [('extract.url','string · 必填','目标网页 URL'),('extract.engine','string · 可选','指定提取引擎;本轮不传,使用服务端选择')], - [('result.main_text','string','正文 → content'),('result.title / final_url','string','页面标题 / 最终地址'),('result.links / images','array','页面链接和图片 → provider_metadata'),('result.status_code','number','源站状态;与 HTTP API 状态分开'),('metadata.server_elapsed_ms','number','服务端耗时')], - {'extract':{'url':URL}},'harness/web_fetch/providers/lexmount.py','本轮为实时提取。中国区与国际区分开调用、分开计分。'), - 'firecrawl':spec('POST https://api.firecrawl.dev/v2/scrape','Authorization: Bearer ', - [('url','string · 必填','目标网页或文档'),('formats','array','正文/资源为 markdown、links、images;视频资源题另请求 video;原文件只使用 rawBase64'),('actions','array','动态顶层选择器用 wait action,随后执行预声明稳定延迟'),('timeout','number','不超过共享内部 HTTP 截止预算'),('maxAge','number','本轮 0,不复用旧缓存')], - [('success','boolean','接口成功标记'),('data.markdown','string','正文 → content'),('data.rawBase64','string','原文件题解码为目标 URL 字节并统一验收'),('data.links / images / video','array / string','资源地址;video 是有时效的视频下载入口'),('data.metadata','object','标题、源 URL、源站状态、积分等'),('error / reason','string','错误仅作私有响应证据;公开记录使用稳定分类')], - {'ordinary_body':{'url':URL,'formats':['markdown'],'maxAge':0,'timeout':40500},'dynamic':{'url':URL,'formats':['markdown','links','images'],'actions':[{'type':'wait','selector':'.ready'}],'maxAge':0},'video_resource':{'url':URL,'formats':['markdown','links','images','video'],'maxAge':0},'original_file':{'url':URL,'formats':['rawBase64'],'parsers':[],'maxAge':0}},'harness/web_fetch/providers/firecrawl.py','已知单页使用 Scrape;Search、Crawl、Agent 会先找页面或跟随其他链接,不符合本轮给定 URL 的单次任务。动态题由 Actions 等待可表达的顶层选择器;匹配数量和 iframe 子文档条件仍可能不完整。video 返回的下载地址不等于原文件或画面理解。每次响应后冷却 6.1 秒,冷却不计入单次耗时;完整 API 响应只保存在私有哈希绑定证据。'), - 'tavily':spec('POST https://api.tavily.com/extract','Authorization: Bearer ', - [('urls','array · 必填','接口接受网址数组;本轮每次只放一个'),('extract_depth','string','本轮 advanced'),('timeout','number','服务端提取截止;由客户端内层截止推导,本轮 40.5 秒,官方范围 1–60 秒'),('format','string','本轮 markdown'),('include_images','boolean','正文配置 false;资源配置 true,额外返回图片地址')], - [('results[].raw_content','string','提取正文 → content'),('results[].url','string','结果 URL'),('results[].images / images','array','结果条目或响应顶层的图片地址'),('failed_results','array','未成功提取的网址及原因'),('response_time','number','服务端耗时')], - {'urls':[URL],'extract_depth':'advanced','timeout':40.5,'format':'markdown','include_images':False},'harness/web_fetch/providers/tavily.py','已知 URL 使用 advanced Extract;Search 根据查询找页面,Crawl 扩展到链接页面,Research 生成答案,均不替代目标 URL 的正文。资源题开启 include_images;没有原文件或视频下载字段。45 秒外层预算预留持久化后,客户端与服务端提取截止均为 40.5 秒。HTTP 200 但没有 raw_content 仍按适配器请求失败;原始响应只保存在私有哈希绑定证据。'), - 'exa':spec('POST https://api.exa.ai/contents','x-api-key: ', - [('urls','array · 必填','本轮每次一个目标 URL,不调用搜索'),('text.verbosity','string','本轮 full,要求完整正文而非 compact 默认值'),('maxAgeHours','number','本轮 0'),('livecrawlTimeout','integer','本轮 15,000 毫秒;采用官方建议上界,且小于 40.5 秒客户端内层截止'),('extras','object','正文配置不传;资源配置 links:20、imageLinks:20;具名列表配置 richLinks:100')], - [('results[].text','string','正文 → content'),('results[].title / url','string','标题与结果网址'),('results[].extras.richLinks','array','明确的 anchor—URL 对 → 具名详情入口'),('results[].extras.links / imageLinks','array','不带名称归属的链接和图片地址'),('statuses[]','array','逐网址的成功状态或错误'),('costDollars','object','费用信息')], - {'ordinary_body':{'urls':[URL],'text':{'verbosity':'full'},'maxAgeHours':0,'livecrawlTimeout':15000},'named_link':{'urls':[URL],'text':{'verbosity':'full'},'maxAgeHours':0,'livecrawlTimeout':15000,'extras':{'richLinks':100}}},'harness/web_fetch/providers/exa.py','已知 URL 使用 Contents 的 full 正文与 maxAgeHours=0 实时抓取;Search 先检索,Agent 和 summary 生成回答,不能代替指定 URL 的原文。资源题请求 links/imageLinks,具名列表只接纳 richLinks 的明确 anchor—URL 配对,不把正文标题与裸地址按位置猜配。livecrawlTimeout 为 15 秒,低于 40.5 秒客户端截止。逐网址状态失败或没有正文时,不能仅凭 HTTP 200 判成功;原始响应只保存在私有证据。'), - 'cloudflare-markdown':spec('POST https://api.cloudflare.com/client/v4/accounts//browser-rendering/{markdown|scrape}; direct files POST …/ai/tomarkdown','Authorization: Bearer ', - [('url','string · 必填','目标页面;直接文件先下载公开 URL 的原始字节再上传'),('gotoOptions.waitUntil','string','网页和资源页使用 networkidle2;动态题使用预声明就绪信号'),('elements','array · 资源题','/scrape 按预声明 CSS 选择器取图片或媒体属性'),('files','multipart · 直接文档','/ai/tomarkdown 接受 PDF / DOCX 等受支持文件;输入字节由客户端取得'),('gotoOptions.timeout / actionTimeout','number','网页和资源页绑定 40,500 毫秒客户端内层截止')], - [('success','boolean','接口是否成功'),('result','string / array','/markdown 网页正文;/scrape 元素属性;/ai/tomarkdown 文档 Markdown'),('errors[]','array','接口错误代码与信息'),('meta','object · 若提供','Browser Run 最终 URL、标题、源站状态等'),('source_sha256','string · 直接文档','上传文件的输入字节摘要;下载与转换均计入同一次耗时')], - {'web':{'endpoint':'/browser-rendering/markdown','url':URL,'gotoOptions':{'waitUntil':'networkidle2','timeout':40500},'actionTimeout':40500},'image_or_video_resource':{'endpoint':'/browser-rendering/scrape','url':URL,'elements':[{'selector':'img'},{'selector':'video'}]},'video_detail_link':{'endpoint':'/browser-rendering/scrape','url':URL,'elements':[{'selector':'a[href]'}]},'direct_pdf_or_docx':{'endpoint':'/ai/tomarkdown','files':''}},'harness/web_fetch/providers/cloudflare.py','按任务预选官方接口:网页正文用 Browser Run /markdown;页面图片与视频地址用 /scrape 的 DOM 属性;直接 PDF / DOCX 使用 Workers AI 文件转换。/pdf 是生成网页 PDF,/links 只列普通链接,/crawl 会跟随其他页面,均不满足这里的文档文字或目标资源合同。直接文档先在客户端下载,下载与转换共享预算;该下载不是 Cloudflare 的独立成绩。浏览器等待计时器彼此独立,账户配额或转换权限错误单列。'), - 'cowagent-built-in':spec('Python · WebFetch({"cwd": workspace}).execute(args)','本地内置工具;无供应商 API Key', - [('url','string · 必填','公开 HTTP/HTTPS 网页或文档 URL'),('cwd','构造配置','解析文档时使用的本地工作目录,不是工具输入参数')], - [('ToolResult.status','string','success / error 等工具状态'),('ToolResult.result','string','提取文字或错误说明 → content')], - {'url':URL},'harness/web_fetch/workers/cowagent_worker.py','底层默认请求超时 30 秒;评测预算 45 秒不会改写工具自己的默认值。网页与 PDF/DOCX 走不同解析路径。'), - 'openclaw-built-in':spec('TypeScript · createWebFetchTool(...).execute(callId, args, signal)','本地内置工具;本轮禁用 Firecrawl 回退', - [('url','string · 必填','目标页面'),('extractMode','string','本轮 markdown'),('maxChars','number','本轮 200000;输出可能截断'),('signal','AbortSignal','本轮 45 秒预算,通过函数参数传入')], - [('details.text','string','正文 → content;缺失时取 content[].text'),('details.status / finalUrl','number / string','源站状态和最终 URL'),('details.extractor','string','实际正文提取器'),('details.truncated','boolean','输出是否被截断')], - {'url':URL,'extractMode':'markdown','maxChars':200000},'harness/web_fetch/workers/openclaw_webfetch_worker.mts','Readability 开启,cacheTtlMinutes=0。函数异常转成失败记录,不把其他 provider 的回退结果算给 OpenClaw。'), - 'deepseek-harness-built-in':spec('TypeScript · ctx.tools.execute({name: "web_fetch", arguments, ...})','本地 HTTP fetch provider;无云端抓取 Key', - [('name','string','web_fetch'),('arguments.url','string · 必填','目标网址'),('callId / signal','运行时参数','调用标识和取消信号;示例省略初始化')], - [('isError','boolean','工具执行是否报错'),('value.body.content','string','接口声明的 HTML/文本正文;声明为空时保持为空'),('content[].text','string','模型展示文本;与声明正文分别保存'),('value.statusCode / value.url','number / string','成功态的规范源站状态与最终 URL'),('meta','object · 若提供','展示投影;仅在规范值缺失时补充状态或 URL'),('error.info.code','string · 若提供','工具错误类型')], - {'name':'web_fetch','arguments':{'url':URL}},'harness/web_fetch/workers/dsh_webfetch_worker.mts','通过内置 HTTP provider 返回声明正文及展示文本;调用预算和读取字段以保留证据为准。不是 DeepSeek 聊天 API,也没有调用模型生成答案。'), - 'langchain-webbase':spec('Python · WebBaseLoader(...).load()','本地开源库;无供应商 API Key', - [('web_paths','tuple[str]','本轮仅一个目标 URL'),('requests_kwargs.timeout','number','本轮 45 秒'),('raise_for_status','boolean','本轮 true,HTTP 错误抛异常'),('continue_on_failure','boolean','本轮 false')], - [('Document.page_content','string','各 Document 正文合并 → content'),('Document.metadata','dict','source、title、language 等'),('HTTP / Python exception','exception','转成失败记录及错误说明')], - 'loader = WebBaseLoader(\n web_paths=("https://example.com/",),\n requests_kwargs={"timeout": 45},\n raise_for_status=True,\n continue_on_failure=False,\n)\ndocs = loader.load()', - 'harness/web_fetch/workers/langchain_webbase_worker.py','不运行网页 JavaScript;本轮没有把它替换成其他 LangChain loader。'), - 'crawl4ai':spec('Python · await AsyncWebCrawler(...).arun(url, config)','本地 Chromium;无云端抓取 Key', - [('url','string · 必填','目标网址'),('cache_mode','CacheMode','本轮 BYPASS'),('page_timeout','number','45000 毫秒'),('wait_until','string','非动态网页为 domcontentloaded;动态任务使用预声明值'),('process_iframes / remove_overlay_elements','boolean','本轮均为 true')], - [('result.success','boolean','爬取是否成功'),('result.markdown.raw_markdown','string','Markdown → content'),('result.media','dict','images、videos、audios'),('result.links','dict','internal / external'),('result.status_code / error_message','number / string','源站状态或失败信息')], - 'config = CrawlerRunConfig(\n cache_mode=CacheMode.BYPASS,\n page_timeout=45000,\n wait_until="domcontentloaded",\n process_iframes=True,\n remove_overlay_elements=True,\n)\nresult = await crawler.arun(\n url="https://example.com/", config=config\n)', - 'harness/web_fetch/workers/crawl4ai_worker.py','非动态网页使用本地无头 Chrome,并在 DOMContentLoaded 后提取;动态任务仍按预声明完成条件等待。直接 PDF 正文在调用前选择 PDFCrawlerStrategy 与 PDFContentScrapingStrategy,文件交付任务选择 AsyncHTTPCrawlerStrategy 并统一验收 downloaded_files。媒体链接不等于下载;文件字节校验不等于理解媒体。'), -} -SPECS['lexmount-cn']={**SPECS['lexmount-global'],'entry':'POST https://api.lexmount.cn/v1/extract','auth':'X-API-Key: ;X-Project-Id: '} -SPECS['kitesurf']=spec( - 'POST …/browser-rendering/markdown?browser=kitesurf','Authorization: Bearer ', - [('url','string','唯一任务输入'),('gotoOptions.waitUntil','string','networkidle2'),('browser','query','kitesurf'),('timeout','seconds','45 秒客户端端到端截止')], - [('result','string','Cloudflare 原生 Markdown → content'),('success / errors','boolean / array','接口状态与错误'),('browser_ms_used','number-like','服务端浏览器计费时间;不等于客户端耗时')], - {'url':URL,'gotoOptions':{'waitUntil':'networkidle2'}},'harness/web_fetch/providers/cloudflare.py', - '网页正文、嵌入文档以及网页中的图片或视频信息都使用指定 Kitesurf 的 Browser Run 原生 Markdown。直接文档也使用官方 Markdown 接口,直接文件通过官方 CDP 获取原始响应。Workers AI 文档转换不计入 Kitesurf 成绩。客户端耗时包含公网往返、排队与执行。') -for engine,wait in (('lightpanda','done'),('obscura','load + adaptive settle'),('moli','done')): - SPECS[engine]=spec( - 'CLI · '+engine+' fetch --dump markdown','本地独立浏览器;不需要供应商 API Key', - [('URL','string','唯一任务输入'),('--dump','string','markdown'),('wait_until','string',wait),('timeout','seconds','45 秒外层截止;CLI 参数按各自单位换算')], - [('stdout','UTF-8 string','浏览器原生 Markdown → content'),('native_markdown_ms','number','CLI 调用及原生序列化阶段耗时'),('browser_stderr','string','浏览器诊断;不进入正文')], - {'url':URL,'engine':engine,'timeout':45,'wait_until':wait,'dump':'markdown'},'harness/web_fetch/providers/headless_worker.py', - '网页正文、嵌入文档以及网页中的图片或视频信息都使用浏览器原生 Markdown。直接文件使用浏览器已有的原始响应接口。不在失败后切换输出方式、调用共同 DOM 脚本,或请求外部服务补正文。接口未提供 HTTP 状态或最终网址时保留空值。') -def moli_release_spec(version): - base=SPECS['moli'] - return {**base, - 'entry':f'CLI · Moli {version} 原生 Markdown / 原始文件接口', - 'params':[('URL','string · 必填','唯一任务输入'),('--dump markdown','网页、文档正文、结构与网页内嵌媒体任务','直接取得 Markdown;直接 PDF/DOCX 不提供可读正文时失败'),('--layout / --with-frames','动态网站正文整类;网页内嵌媒体任务使用 --with-frames','动态正文统一启用 Moli 页面布局能力,并在同一次原生 Markdown 调用中包含已加载子文档;不按题目设置 selector'),('--wait-selector / --wait-script','旧版预声明完成条件','只等待,不抽取或拼接正文'),('外层截止','seconds','外层统一为 45 秒。内部参数以冻结的调用记录为准:显式模式留出收尾和清理时间;uniform 模式沿用产品默认值')], - 'fields':[('stdout','UTF-8 string / bytes','网页与文档任务为原生 Markdown;直接文件为原始字节'),('raw_stdout_path / sha256','string','保存的原始文档或媒体证据'),('file_evaluation','object','文件格式、长度和目标 SHA-256 验收')], - 'request':{'page_or_document_body':'moli fetch --dump markdown --wait-until done URL','dynamic_body':'moli fetch --dump markdown --layout --with-frames --wait-until domstable URL','webpage_media':'moli fetch --dump markdown --with-frames --wait-until done URL','direct_file':'原始响应或附件接口;具体路线见逐次调用记录','version':version}, - 'notes':f'本方法只使用官方 {version} 发行二进制,与其他 Moli 版本分开计分。网页正文、文档正文、结构题和网页内嵌媒体题统一使用原生 Markdown;动态网站正文整类统一启用 --layout、--with-frames 和 domstable,不按题目设置 selector。网页媒体只核对这份输出中的目标地址,不下载,也不切换 HTML、JSON 或网络跟踪接口。直接 PDF/DOCX 没有交付可读正文时按失败记录;直接媒体文件验收原始字节。评测端不使用外部 PDF/DOCX 解析器补正文,也不在失败后切换引擎或输出模式。'} -SPECS['moli-1.0.6']=moli_release_spec('1.0.6') -SPECS['moli']=moli_release_spec('1.1.5') -SPECS['moli-1.1.7']=moli_release_spec('1.1.7') -SPECS['moli-1.1.3']=moli_release_spec('1.1.3') -SPECS['moli-1.1.11']=moli_release_spec('1.1.11') -SPECS['chromium']=spec( - 'Playwright · Chromium → DOM HTML → node-html-markdown','本地独立浏览器;不需要 Google 账号,不加载个人会话', - [('url','string','唯一任务输入'),('wait_until','string','load'),('timeout','seconds','45 秒端到端截止'),('converter','string','node-html-markdown 2.0.0')], - [('content','string','转换后的 Markdown'),('rendered_html_path / sha256','string','转换前 DOM HTML 证据'),('fetch_html_ms / conversion_ms','number','分阶段耗时'),('browser_stderr','string','浏览器诊断;不进入正文')], - {'url':URL,'engine':'chromium','timeout':45,'wait_until':'load','dump':'html','converter':'node-html-markdown 2.0.0'},'harness/web_fetch/providers/headless_worker.py', - '所有页面类任务都取得完整渲染 DOM,再使用 node-html-markdown 2.0.0 转为 Markdown;不运行资源提取脚本,不读取 PDF.js 等查看器私有对象,也不使用外部文档解析器。页面获取、DOM 转换与原始文件读取均计入耗时;原始字节、HTML 与转换器哈希均保留。') +from .methods import report_method, report_method_catalog, method_access, metadata_matches, method_page_model, browser_method_registry + +# Views of the shared catalog; execution aliases inherit their engine's metadata. +SPECS = {name: item['interface'] for name, item in report_method_catalog()['methods'].items() + if item.get('interface')} def method_provenance(out): path=out/'implementation-provenance.json' return {item['binary_sha256']:item for item in json.loads(path.read_text()).get('implementations',[])} if path.is_file() else {} -def method_version(record, provenance=None): - """Use executable identity, never a different engine's CDP compatibility number.""" - meta=record.get('provider_metadata') or {} - name=record.get('provider') - if name=='kitesurf':return 'Beta · 托管后端版本未公开' - bound=(provenance or {}).get(meta.get('binary_sha256'),{}) - if bound.get('version'):return bound['version'] - if meta.get('reported_version'):return meta['reported_version'] - value=meta.get('browser_version') - if name in {'moli','lightpanda','obscura'} and re.fullmatch(r'\d+\.\d+\.\d+\.\d+',str(value)): - return None - return value or meta.get('binary_version') or meta.get('moli_version') or meta.get('library_version') or meta.get('version') - - -def method_access(name, records): - engine=native_browser_engine(name) or name - access=HEADLESS_ACCESS.get(name) or HEADLESS_ACCESS.get(engine) - if access and engine!=name: - source=browser_method_registry()[name].get('source_url') - return {**access,'configuration':'使用本轮绑定提交构建的二进制;提交、构建环境与 SHA-256 见环境与复现。','links':[(name+' · 源码分支',source)] if source else access['links']} - if access is None or name!='moli':return access - versions=set() - for row in records: - if row['provider']!=name:continue - match=re.fullmatch(r'moli (\d+\.\d+\.\d+)',row.get('provider_metadata',{}).get('reported_version','')) - if match:versions.add(match.group(1)) - if len(versions)!=1:return access - version=next(iter(versions)) - return {**access,'links':[(f'Moli {version}',f'https://github.com/lexmount/moli/releases/tag/v{version}')]} - - -def method_spec(name, record, *, timeout_seconds=None): - engine=native_browser_engine(name) or name - if record and record.get('provider_type')=='moli_webfetch': - base=SPECS['moli-webfetch'] - meta=record.get('provider_metadata') or {} - policy=meta.get('internal_timeout_policy') - budget=(f'{timeout_seconds:g} 秒外层截止' if isinstance(timeout_seconds,(int,float)) and not isinstance(timeout_seconds,bool) and timeout_seconds>0 else '外层截止时间未记录') - if policy=='native_default': - timeout_description=budget+';Moli 总等待、HTTP 和连接超时保留该二进制默认值' - timeout_note='这批调用未传入 Moli 内部超时参数。' - elif policy=='explicit': - timeout_ms=meta.get('http_connect_timeout_ms') - timeout_description=(budget+';实测命令同时显式传入 Moli 总等待、HTTP 和连接超时' - +((' '+f'{timeout_ms:,}'+' 毫秒') if isinstance(timeout_ms,int) else '')) - timeout_note='这批调用使用保存记录中的显式 Moli 超时,未写成产品默认值。' - else: - timeout_description=budget+';Moli 内部超时以逐次调用记录为准' - timeout_note='保存证据未提供可核对的 Moli 内部超时策略。' - params=[(key,kind,timeout_description if key=='外层截止' else description) - for key,kind,description in base['params']] - entry=base['entry'] if name in SPECS else 'CLI · Moli · 路线以逐次调用记录为准' - return {**base,'entry':entry,'params':params,'notes':base['notes']+' '+timeout_note} - base=SPECS.get(name) or SPECS[engine] +def method_spec(name, record, *, timeout_seconds=None, language="zh"): + def t(zh,en):return zh if language=="zh" else en + catalog=report_method_catalog() if language=='zh' else report_method_catalog(language) + owner=catalog['adapters'].get((record or {}).get('provider_type'),{}).get('interface_method') + registered=catalog['methods'][owner] if owner else report_method(name,record,language=language) + base=registered.get('interface') or { + 'entry':t('未登记','Not registered'),'auth':t('未登记','Not registered'),'params':[], + 'fields':[],'request':{},'source':t('未登记','Not registered'), + 'notes':t('接口资料尚未登记;请查阅保存的调用证据。','Interface metadata has not been registered; consult saved call evidence.')} meta=(record or {}).get('provider_metadata',{}) - if name=='moli': - match=re.fullmatch(r'moli (\d+\.\d+\.\d+)',meta.get('reported_version','')) - if match:base=moli_release_spec(match.group(1)) - else: - base={**base,'entry':'CLI · Moli','request':{k:v for k,v in base['request'].items() if k!='version'}} - return {**base,'notes':'实际版本、配置和调用路线见“环境与复现”及逐次调用记录。'} - if name=='chromium': - request={**base['request']} - params=list(base['params']) - wait=meta.get('navigation_wait_until') - if wait in {'load','domcontentloaded','networkidle','commit'}: - request['wait_until']=wait - params=[(key,kind,wait if key=='wait_until' else description) - for key,kind,description in params] - converter_sha=meta.get('converter_sha256') - notes=base['notes'] - if isinstance(converter_sha,str) and re.fullmatch(r'[0-9a-f]{64}',converter_sha): - request['converter_sha256']=converter_sha - params=[(key,kind, - description+';包装脚本 SHA-256 '+converter_sha - if key=='converter' else description) - for key,kind,description in params] - notes+=' 转换包装还负责代码块围栏与语言标签;本轮以完整脚本 SHA-256 绑定,不把它写成未修改的包默认行为。' - return {**base,'request':request,'params':params,'notes':notes} - native={k:v for k,v in meta.get('native_metadata',{}).items() if k in {'format','script_sha256'}} - format_name=native.get('format') - if (name,format_name) not in {('lightpanda','lightpanda_json'),('obscura','obscura_eval')}:return base - command=('lightpanda fetch URL --dump markdown --json' if name=='lightpanda' - else 'obscura fetch URL --eval ') - request={**base['request'],'command':command,'native_metadata':native} - if name=='obscura':request.pop('dump',None) - return {**base,'entry':'CLI · '+command,'request':request, - 'params':[row for row in base['params'] if name!='obscura' or row[0]!='--dump'], - 'notes':'Lightpanda 解包同一次调用返回的原生 Markdown;Obscura 执行固定哈希的官方 Markdown 脚本。网页正文、嵌入文档以及网页中的图片或视频信息均保留该原生输出。直接文件分别使用 Lightpanda CDP 与 Obscura --dump original。评测端不再加工 DOM 或外部解析文档;获取与输出处理均计入耗时。'} - - - -def _observed_wait(meta): + if registered.get('runtime_parameters'): + budget=(t(f'{timeout_seconds:g} 秒外层截止',f'{timeout_seconds:g}-second outer deadline') + if positive_number(timeout_seconds) + else t('外层截止时间未记录','Outer deadline not recorded')) + context={**meta,'report_outer_budget':budget} + params=list(base['params']);notes=base['notes'];request=dict(base['request']) + for parameter in registered['runtime_parameters']: + rule=next((rule for rule in parameter['rules'] if metadata_matches(context,rule.get('metadata_equals',{}))),None) + if rule is None:continue + description=_route_template(rule['description'],context) + if description is None:continue + params=[(key,kind,(detail+description if parameter.get('append') else description) + if key==parameter['parameter'] else detail) for key,kind,detail in params] + for key,template in rule.get('request_fields',{}).items(): + value=_route_template(template,context) + if value is not None:request[key]=value + if rule.get('note'):notes+=' '+rule['note'] + entry=base['entry'] if name in catalog['methods'] else registered.get('unregistered_participant_entry',base['entry']) + base={**base,'entry':entry,'params':params,'notes':notes,'request':request} + for variant in registered.get('interface_variants',[]): + if not metadata_matches(meta,variant['metadata_equals']):continue + native={k:v for k,v in meta.get('native_metadata',{}).items() if k in {'format','script_sha256'}} + command=variant['command'] + request={**base['request'],'command':command,'native_metadata':native} + for key in variant.get('remove_request_fields',[]):request.pop(key,None) + return {**base,'entry':'CLI · '+command,'request':request, + 'params':[row for row in base['params'] if row[0] not in variant.get('remove_parameters',[])], + 'notes':variant['notes']} + return base + + + +def _observed_wait(meta, *, language="zh"): navigation=meta.get('navigation_wait_until') completion=meta.get('wait_until') + if language=='en': + if navigation and completion and navigation!=completion: + return f'Navigation wait {navigation}; output completion {completion}' + if navigation:return f'Navigation wait {navigation}' + if completion:return f'Output completion {completion}' + return 'Call records do not provide a verifiable wait condition' if navigation and completion and navigation!=completion: return f'导航等待 {navigation};输出完成条件 {completion}' if navigation:return f'导航等待 {navigation}' @@ -216,92 +78,60 @@ def _observed_wait(meta): return '调用记录未提供可核对的等待条件' -def _observed_route(engine, meta): - """Describe only the interface persisted by one measured call.""" +def _route_template(rule, metadata): + """Render a registered route only when all its evidence fields are present.""" + if not metadata_matches(metadata,rule.get('metadata_equals',{})):return None + values={} + for name,path in rule.get('fields',{}).items(): + value=metadata + for key in path.split('.'): + value=value.get(key) if isinstance(value,dict) else None + if value is None or value=='':return None + if name in rule.get('field_values',{}) and value not in rule['field_values'][name]:return None + if name in rule.get('field_patterns',{}) and (not isinstance(value,str) or not re.fullmatch(rule['field_patterns'][name],value)):return None + if name in rule.get('positive_numeric_fields',[]): + if not positive_number(value):return None + values[name]=value + for name,fragments in rule.get('fragments',{}).items(): + values[name]=next((text for fragment in fragments + if (text:=_route_template(fragment,metadata)) is not None),'') + return rule['text'].format(**values) + + +def _observed_route(engine, meta, *, language="zh", compact=False): + """Describe only the registered interface persisted by one measured call.""" + def t(zh,en):return zh if language=='zh' else en route=meta.get('route') extractor=meta.get('extractor') contract=meta.get('output_contract') - dump=meta.get('dump') - browser=meta.get('browser') - native=meta.get('native_metadata') or {} - route_note=f'(route={route})' if route else '' - if engine=='moli' and extractor=='moli_native_markdown' and dump=='markdown': - flags=' --layout --with-frames' if (meta.get('layout') and meta.get('with_frames')) else '' - return 'moli fetch --dump markdown'+flags+' → Moli 原生 Markdown'+route_note - if engine=='lightpanda' and extractor=='lightpanda_native_markdown': - json_flag=' --json' if native.get('format')=='lightpanda_json' else '' - return 'Lightpanda --dump markdown'+json_flag+' → 原生 Markdown'+route_note - if engine=='obscura' and extractor=='obscura_native_markdown': - script='(脚本 SHA-256 '+native['script_sha256']+')' if native.get('script_sha256') else '' - return 'Obscura --eval 官方 Markdown 脚本'+script+' → 原生 Markdown'+route_note - if engine=='chromium' and extractor=='node-html-markdown': - version=(' '+str(meta['extractor_version'])) if meta.get('extractor_version') else '' - return 'Chromium 渲染 HTML → node-html-markdown'+version+' → Markdown'+route_note - if (engine=='kitesurf' and browser=='kitesurf' - and extractor=='cloudflare_native_markdown'): - return 'Cloudflare /markdown?browser=kitesurf → 托管原生 Markdown'+route_note + route_note=t(f'(route={route})',f' (route={route})') if route and not compact else '' + for rule in report_method(engine,language=language).get('observed_routes',[]): + description=_route_template(rule,meta) + if description is not None: + return rule.get('compact_text',description) if compact else description+route_note if contract=='original_file_bytes': - origins={ - 'moli_cdp_response':'Moli CDP 原始响应', - 'lightpanda_cdp_response':'Lightpanda CDP 原始响应', - 'obscura_original_response':'Obscura --dump original 原始响应', - 'kitesurf_cdp_response':'Kitesurf 官方 CDP 原始响应', - 'browser_native_response_or_download':'Chromium 原始响应或原生下载', - } - source=origins.get(extractor,('调用记录 extractor='+str(extractor) - if extractor else '调用记录未注明原始文件接口')) - return source+' → 原始文件字节'+route_note + source=(t('调用记录 extractor=','Recorded extractor=')+str(extractor) if extractor + else t('调用记录未注明原始文件接口','Original-file interface not recorded')) + return source+(t(' → 文件字节',' → file bytes') if compact else t(' → 原始文件字节',' → original file bytes'))+route_note fields=[] - for key,value in (('route',route),('extractor',extractor), - ('output_contract',contract),('dump',dump)): - if value:fields.append(f'{key}={value}') + for key in ('route','extractor','output_contract','dump'): + if meta.get(key):fields.append(f'{key}={meta[key]}') if fields: - return '调用记录:'+';'.join(fields)+';未据此补写未记录的转换步骤' - return '调用记录未提供可核对的接口路线;不推断原生 Markdown、HTML 或额外转换' + return t('调用记录:','Call record: ')+t(';','; ').join(fields)+t(';未据此补写未记录的转换步骤','; unrecorded conversion steps are not inferred') + if compact:return t('接口未记录','Interface not recorded') + return t('调用记录未提供可核对的接口路线;不推断原生 Markdown、HTML 或额外转换','Call records do not provide a verifiable route; native Markdown, HTML or additional conversion is not inferred') -def browser_task_interface_matrix(cases, providers, records=None, *, compact=False): +def browser_task_interface_matrix(cases, providers, records=None, *, compact=False, language="zh"): """Describe task routes, preferring persisted call metadata when available.""" from collections import Counter - from types import SimpleNamespace - from .providers.headless import output_mode_for_case - capabilities={ - 'moli':'网页正文、文档正文、结构和网页内嵌媒体统一使用原生 Markdown;动态网站正文整类统一启用 --layout、--with-frames 和 domstable;网页媒体只核对输出中的目标地址。最终网址不参与正文判分。', - 'lightpanda':'内置 Markdown 转换;同一任务大类只使用 CLI --dump markdown --json,不传逐题选择器、数量、延迟或 iframe 参数。', - 'obscura':'内置 Markdown 与 LP.getMarkdown 共用官方 JS 实现;本次执行固定版本的同一脚本,不按题目切换接口。', - 'chromium':'没有直接输出 Markdown 的原生接口;取得渲染后的 DOM,再使用共同的 node-html-markdown 2.0.0。', - 'kitesurf':'官方 Browser Run /markdown?browser=kitesurf 返回 Markdown;这是托管服务的转换能力,不是客户端共同转换。', - } - markdown={ - 'moli':'CLI fetch --dump markdown → 内置转换 → Markdown', - 'lightpanda':'CLI fetch --dump markdown --json → 内置转换 → 解包 Markdown', - 'obscura':'CLI --eval 官方固定版本脚本 → 官方转换 → Markdown', - 'chromium':'Playwright/CDP 导航 → 最终 DOM → 共同转换 → Markdown', - 'kitesurf':'/markdown?browser=kitesurf → 官方托管转换 → Markdown', - } - original={ - 'moli':'原生 CDP 响应;附件事件使用同一 Moli 原始附件接口', - 'lightpanda':'原生 CDP Response.body()', - 'obscura':'CLI fetch --dump original(原始 HTTP 响应,不运行页面 JS)', - 'chromium':'Playwright/CDP 原始响应或原生下载', - 'kitesurf':'指定 Kitesurf 的官方 CDP 原始响应', - } - resource={ - 'moli':'CLI fetch --dump markdown --with-frames → 原生 Markdown;共同评分器只识别题目指定的资源 URL', - 'lightpanda':'CLI fetch --dump markdown --json → 原生 Markdown;评分器只从交付内容中核对资源地址', - 'obscura':'CLI --eval 官方固定版本脚本 → 原生 Markdown;评分器只从交付内容中核对资源地址', - 'chromium':'Playwright/CDP 取得完整渲染 HTML → node-html-markdown 2.0.0 → 评分器核对资源地址', - 'kitesurf':'/markdown?browser=kitesurf → 原生 Markdown;评分器只从交付内容中核对资源地址', - } - runtime={'moli':'原生 CDP','obscura':'原生 CDP','lightpanda':'原生 CDP', - 'chromium':'Playwright/CDP','kitesurf':'指定 Kitesurf 的官方 CDP'} - waits={ - 'moli':'CLI 等待 done', - 'lightpanda':'Markdown CLI 等待 done', - 'obscura':'CLI 使用官方默认 load 加自适应静默等待(最多 5 秒)', - 'chromium':'页面提取等待 load', - 'kitesurf':'Markdown API 等待 networkidle2', - } + def t(zh,en):return zh if language=='zh' else en + methods=(report_method_catalog() if language=='zh' else report_method_catalog(language))['methods'] + selected={name:next((row for row in (records or []) if row['provider']==name),None) + for name in providers} + engines={name:native_browser_engine(name,selected[name]) for name in providers} + routes={name:item.get('routes',{}) for name,item in methods.items()} + capabilities={name:item.get('capabilities','Not registered') for name,item in routes.items()} tracks=[] from .models import REPORT_FAMILIES, report_family for case in cases.values(): @@ -316,79 +146,60 @@ def browser_task_interface_matrix(cases, providers, records=None, *, compact=Fal group=[case for case in cases.values() if report_family(case)==track] cells={} for provider in providers: - engine=native_browser_engine(provider) - if engine not in capabilities:raise ValueError('Unregistered native browser participant: '+provider) + engine=engines[provider] + if engine not in methods: + cells[provider]=[{'mode':'unregistered','count':len(group),'description':t('未登记','Not registered'),'wait':'Not recorded'}] + continue parts=[];counts=Counter() for case in group: case_id=case.get('id',case_ids[id(case)]) - mode=output_mode_for_case(engine,'headless_markdown',SimpleNamespace( - url=case['url'],track=case.get('track','web'),task_kind=case.get('task_kind','body'))) + case_metadata={**case,'track':case.get('track','web'),'task_kind':case.get('task_kind','body')} + plan=next((rule for rule in methods[engine].get('task_interfaces',[]) + if metadata_matches(case_metadata,rule['case_equals'])),None) measured=indexed.get((provider,case_id)) + provider_type=(measured or selected[provider] or {}).get('provider_type') or browser_method_registry().get(provider,{}).get('type','headless_markdown') + selector_type=report_method_catalog()['adapters'].get(provider_type,{}).get('task_mode_provider_type',provider_type) + mode=output_mode_for_case(engine, selector_type, SimpleNamespace( + **{**case_metadata, 'url': case.get('url', '')})) if records is not None: meta=(measured or {}).get('provider_metadata') or {} - counts[(mode,_observed_route(engine,meta),_observed_wait(meta))]+=1 + if compact: + observed=next((rule for rule in methods[engine].get('observed_routes',[]) + if _route_template(rule,meta) is not None),None) + mode=observed['mode'] if observed else 'unregistered' + description=_observed_route(engine,meta,language=language,compact=compact) + wait=_observed_wait(meta,language=language) + elif plan: + description=plan['description'].format(**routes.get(engine,{})) + wait=plan['wait'].format(**routes.get(engine,{})) else: - counts[(mode,None,None)]+=1 - for (mode,observed_description,observed_wait),count in counts.items(): - if mode=='markdown': - if track=='document' and engine=='moli': - description=markdown[engine]+';HTML/TXT 可直接返回正文,直接 PDF/DOCX 未提供可读正文;评测端不使用外部解析器' - else: - description=markdown[engine]+(';文档必须由此接口直接返回文字,不使用外部 PDF/DOCX 解析器' if track=='document' else '') - elif mode=='document':description=original[engine]+' → 原始 PDF/DOCX;只取得字节不能通过文档正文题,评测端不外部解析' - elif mode=='file':description=original[engine]+' → 原始文件字节(不转 Markdown)' - elif mode=='resource':description=resource[engine]+( - ' → 图片或视频资源地址(直接从原生 Markdown 核对)' if engine=='moli' - else ' → 图片或视频资源地址(不转 Markdown)') - elif mode=='document_page':description=runtime[engine]+' 读取嵌入文档 → 已加载 PDF 查看器原生文字,或页面可见文字 → 文档正文;不下载后另行解析' - elif mode=='media_metadata':description=runtime[engine]+' 执行共用脚本,读取正文、作者及 JSON-LD → 页面正文与媒体元数据(不转 Markdown)' - else:raise ValueError('Unsupported browser task interface mode: '+mode) - wait=('读取原始响应或下载文件,不等待查看器绘制' if mode in {'file','document'} else waits[engine]) - if engine=='moli' and track=='dynamic' and mode=='markdown': - description+=';动态正文整类统一启用 --layout 和 --with-frames,不使用逐题 selector' - wait='动态网站正文整类使用 CLI 原生 domstable' - if mode not in {'file','document','markdown'}: - if engine in {'lightpanda','kitesurf'} or (engine=='obscura' and mode in {'document_page','media_metadata'}): - wait='CDP 页面提取等待 load' - if mode=='document_page':wait+=';随后等待查看器文档就绪或页面内容' - if records is not None: - description=observed_description - wait=observed_wait + description=t('未登记','Not registered') + wait='Not recorded' + counts[(mode,description,wait)]+=1 + for (mode,description,wait),count in counts.items(): parts.append({'count':count,'mode':mode,'description':description,'wait':wait}) if compact and records is not None: grouped={} for part in parts: description=part['description'] - if description.startswith('调用记录未提供'): - description='接口未记录' - elif 'node-html-markdown' in description: - description='渲染 HTML → node-html-markdown → Markdown' - elif '原生 Markdown' in description or '官方 Markdown 脚本' in description: - description='官方脚本 → Markdown' if engine=='obscura' else '原生 Markdown' - elif '原始文件字节' in description: - description=description.split(' → ')[0]+' → 文件字节' - elif description.startswith('调用记录未提供'): - description='接口未记录' - else: - description=description.split('(route=')[0] - mode='markdown' if 'Markdown' in description else part['mode'] + mode=part['mode'] key=(mode,description) if key not in grouped:grouped[key]={'mode':mode,'count':0,'description':description,'wait':''} grouped[key]['count']+=part['count'] parts=list(grouped.values()) cells[provider]=parts rows.append({'track':track,'count':len(group),'cells':cells}) - selected_capabilities={name:capabilities[native_browser_engine(name)] for name in providers} + selected_capabilities={name:capabilities.get(engines[name],t('未登记','Not registered')) for name in providers} if records is not None: for provider in providers: - engine=native_browser_engine(provider) + engine=engines[provider] observed=[] for row in records: if row['provider']!=provider:continue - description=_observed_route(engine,row.get('provider_metadata') or {}) + description=_observed_route(engine,row.get('provider_metadata') or {},language=language) if description not in observed:observed.append(description) - selected_capabilities[provider]=('本轮调用记录:'+';'.join(observed) - if observed else '本轮没有可核对的调用接口记录。') + selected_capabilities[provider]=(t('本轮调用记录:','Recorded calls: ')+t(';','; ').join(observed) + if observed else t('本轮没有可核对的调用接口记录。','No verifiable call interfaces were recorded.')) return {'providers':providers,'capabilities':selected_capabilities,'rows':rows} @@ -399,38 +210,46 @@ def render_interfaces(out,records): comparison=dataset.get("comparison",{}) native_default=measurement_presentation(dataset).native_outputs def pre(value):return '
    '+escape(value if isinstance(value,str) else json.dumps(value,ensure_ascii=False,indent=2))+'
    ' - def fields(rows):return '
    '+''.join('
    '+escape(k)+' '+escape(t)+'
    '+escape(d)+'
    ' for k,t,d in rows)+'
    ' + def fields(rows,english): + if len(rows)!=len(english):raise ValueError('Interface field language mismatch') + return '
    '+''.join('
    '+bilingual_text(k,ek)+' '+bilingual_text(t,et)+'
    '+bilingual_text(d,ed)+'
    ' for (k,t,d),(ek,et,ed) in zip(rows,english))+'
    ' + measured={r['provider'] for r in records} - names=sorted(set(SPECS)|{name for name in measured if native_browser_engine(name)},key=lambda name:(name not in measured,name)) + names=sorted(set(SPECS)|measured,key=lambda name:(name not in measured,name)) body='

    按本轮适配器介绍,不混用完整产品能力。输入示例展示正文配置,资源配置差异单列;它们不是每条历史请求的逐字回放。各调用所属批次及实际配置见“环境与复现”。输出示例取存档调用,经适配器整理后的真实节选,不冒充原始 HTTP 响应。Key 和账号均用占位符。

    ' if native_default: body='

    已测方法的输入展示保存的公开调用字段,输出展示同一次调用的实际节选。完整配置、原始响应和日志见环境与调用详情。未参评方法保留仓库登记示例。

    ' + case_order={case['id']: index for index, case in enumerate(dataset.get('cases', []))} for i,name in enumerate(names): - r=next((r for r in records if r['provider']==name and r['case_id']=='static-simple'),next((r for r in records if r['provider']==name),None)) + r=min((r for r in records if r['provider']==name), key=lambda row: case_order.get(row['case_id'], len(case_order)), default=None) s=method_spec(name,r,timeout_seconds=comparison.get("timeout_seconds")) + english=method_spec(name,r,timeout_seconds=comparison.get("timeout_seconds"),language="en") bindings=record_public_call_log_bindings(r) if r else [] if r and native_default and not bindings: raise ValueError("Measured native interface requires a saved call log: "+name) if bindings: request=json.loads(verify_public_call_log(out,bindings[-1]).read_text())["request"] - from .methods import method_page_model model=method_page_model(name,name,[x for x in records if x["provider"]==name]) s={**s,"entry":model["endpoint"],"params":[],"request":request,"notes":"此处展示该次公开调用记录保存的请求字段;完整配置和日志见环境与调用详情。产品内部超时以冻结配置和实际请求为准。"} + english_model=method_page_model(name,name,[x for x in records if x["provider"]==name],language="en") + english={**english,"entry":english_model["endpoint"],"params":[],"request":request,"notes":"These are the request fields saved in this public call record. Full configuration and logs are in the environment and call details; internal product timeouts follow the frozen configuration and actual request."} access=method_access(name,records) + english_access=method_access(name,records,language="en") if r: content=contained_path(out,r['artifact_path']).read_text();sample={k:r.get(k) for k in ['ok','status_code','title','final_url']};sample['content_excerpt']=content[:450];sample['content_characters']=len(content) else:sample={'measurement':None,'note':'仓库登记接口,未纳入本轮;此处不提供虚构的运行输出。'} # Explicit whitelist: no local paths, request IDs, credentials or session metadata. meta=r.get('provider_metadata',{}) if r else {};sample['metadata']={k:meta[k] for k in ['engine','upstream_status','links_count','images_count','extractor','truncated','document_count'] if k in meta} dest=out/'adoption'/'interfaces';dest.mkdir(exist_ok=True);(dest/(name+'.json')).write_text(json.dumps({'interface':s,'access':access,'sample':sample,'attempt_id':r['attempt_id'] if r else None},ensure_ascii=False,indent=2)+'\n') - body+=f'
    '+escape(browser_method_label(name))+'' - body+='

    '+escape(s['entry'])+'

    '+escape(s['auth'])+'

    ' + body+=f'
    '+bilingual_text(browser_method_label(name),browser_method_label(name,language='en'))+'' + body+='

    '+bilingual_text(s['entry'],english['entry'])+'

    '+bilingual_text(s['auth'],english['auth'])+'

    ' if access: - body+='

    账号与获取入口

    '+escape(access['account'])+'

    '+escape(access['configuration'])+'

    '+ ' · '.join(''+escape(label)+'' for label,url in access['links'])+'

    实际版本和接口见运行记录;下方链接指向官方说明。

    ' + body+='

    账号与获取入口

    '+bilingual_text(access['account'],english_access['account'])+'

    '+bilingual_text(access['configuration'],english_access['configuration'])+'

    '+ ' · '.join(''+bilingual_text(label,english_access['links'][index][0])+'' for index,(label,url) in enumerate(access['links']))+'

    实际版本和接口见运行记录;下方链接指向官方说明。

    ' + if access and access.get('env_vars'): + body+='

    '+', '.join(''+escape(value)+'' for value in access['env_vars'])+'

    ' sample_label='本轮输出节选 · 统一记录' if r else '本轮未执行' - body+='

    输入

    '+fields(s['params'])+('

    本轮保存的调用字段

    ' if bindings else '

    调用输入示例

    ')+pre(s['request'])+'

    输出

    '+fields(s['fields'])+'

    '+sample_label+'

    '+pre(sample)+'
    ' + body+='

    输入

    '+fields(s['params'],english['params'])+('

    本轮保存的调用字段

    ' if bindings else '

    调用输入示例

    ')+pre(s['request'])+'

    输出

    '+fields(s['fields'],english['fields'])+'

    '+sample_label+'

    '+pre(sample)+'
    ' measured_link='该次完整输出与判定 →' if r else '未纳入本轮;参数说明为仓库登记模式,不计入本轮结果' - from .report_components import bilingual_text example_note='' if bindings else bilingual_text('以下接口说明与数值属于仓库登记示例,不是这次调用的请求记录。', 'The following interface descriptions and values are registered examples, not this call’s request record.') - body+='

    '+example_note+escape(s['notes'])+'

    核对实现:'+escape(s['source'])+' · '+measured_link+' · 示例 JSON

    ' + body+='

    '+example_note+bilingual_text(s['notes'],english['notes'])+'

    核对实现:'+escape(s['source'])+' · '+measured_link+' · 示例 JSON

    ' return body diff --git a/harness/web_fetch/report_methods.py b/harness/web_fetch/report_methods.py index 34c6311e3..077675d89 100644 --- a/harness/web_fetch/report_methods.py +++ b/harness/web_fetch/report_methods.py @@ -1,37 +1,20 @@ """Provider-centred introduction and reproducible, sample-scoped adoption counts.""" import json from pathlib import Path -from .methods import method_info, method_page_model, is_browser_comparison, browser_method_label, measured_method_label +from .methods import method_info, method_page_model, browser_method_label, method_version from .report_components import tabs,cards,measurement_presentation from .report_interfaces import render_interfaces from .report_capabilities import render_capabilities from .report_pricing import render_pricing -PROFILES={ - 'kitesurf':('远程账号 API','Cloudflare Browser Run 账户级 Markdown 端点,指定 browser=kitesurf,直接返回 Markdown。','https://developers.cloudflare.com/browser-run/kitesurf/'), - 'lightpanda':('本地浏览器 CLI','Lightpanda fetch --dump markdown,直接输出浏览器原生 Markdown。','https://github.com/lightpanda-io/browser'), - 'obscura':('本地浏览器 CLI','Obscura fetch --dump markdown;标准模式,不启用 stealth 或备用引擎。','https://github.com/h4ckf0r0day/obscura'), - 'moli-1.0.6':('本地浏览器 CLI','普通网页使用 fetch --dump markdown;直接文档和媒体使用裸 fetch。原始文档的本地正文解析计入耗时,文件任务直接校验字节。','https://github.com/lexmount/moli/releases/tag/v1.0.6'), - 'moli':('本地浏览器 CLI','普通网页使用 fetch --dump markdown;直接文档和媒体使用裸 fetch。原始文档的本地正文解析计入耗时,文件任务直接校验字节。','https://github.com/lexmount/moli'), - 'moli-1.1.7':('本地浏览器 CLI','普通网页使用 fetch --dump markdown;直接文档和媒体使用裸 fetch。原始文档的本地正文解析计入耗时,文件任务直接校验字节。','https://github.com/lexmount/moli/releases/tag/v1.1.7'), - 'moli-1.1.11':('本地浏览器 CLI','普通网页使用 fetch --dump markdown;本轮风险网站评测固定 debug 日志并逐次完整留存。','https://github.com/lexmount/moli/releases/tag/v1.1.11'), - 'moli-1.1.3':('本地浏览器 CLI','普通网页使用 fetch --dump markdown;直接文档和媒体使用裸 fetch。原始文档的本地正文解析计入耗时,文件任务直接校验字节。','https://github.com/lexmount/moli/releases/tag/v1.1.3'), - 'chromium':('本地浏览器 CDP','Playwright 启动 Chromium,取完整渲染 DOM,再由 node-html-markdown 2.0.0 生成 Markdown。','https://playwright.dev/python/docs/browsers'), - 'moli-webfetch':('本地 CLI','Moli:网页正文、文档正文、结构和网页内嵌媒体统一使用原生 Markdown;直接媒体文件使用原始响应。完成条件只控制官方等待参数;不调用模型。','https://github.com/lexmount/moli/tree/main/skills/moli-webfetch'), - 'cowagent-built-in':('项目内置','CowAgent 内的 HTTP 抓取和文本、文档解析工具。','https://github.com/zhayujie/CowAgent/blob/main/agent/tools/web_fetch/web_fetch.py'), - 'lexmount-global':('托管 API','Lexmount 国际 API,返回网页主正文与元数据。','https://browser.lexmount.com/en/docs/webfetch'), - 'lexmount-cn':('托管 API','Lexmount 中国区 API;与国际 API 分开实测,不重复计算品牌采用。','https://browser.lexmount.com/en/docs/webfetch'), - 'firecrawl':('托管 API','云端 Scrape 为正文与资源题返回 Markdown/地址,原文件题用 rawBase64 交付目标字节。','https://docs.firecrawl.dev/features/scrape'), - 'tavily':('托管 API','本轮调用 Extract,从给定 URL 提取可读内容。','https://docs.tavily.com/documentation/api-reference/endpoint/extract'), - 'exa':('托管 API','本轮调用 Contents 取得指定 URL 的正文,不测试搜索。','https://exa.ai/docs/reference/contents-retrieval'), - 'cloudflare-markdown':('托管 API','云端浏览器加载网页后转换为 Markdown。','https://developers.cloudflare.com/browser-run/quick-actions/markdown-endpoint/'), - 'openclaw-built-in':('项目内置','本轮使用 HTTP + Readability;关闭 Firecrawl 回退,避免混成两个方法。','https://github.com/openclaw/openclaw/blob/main/docs/tools/web-fetch.md'), - 'deepseek-harness-built-in':('项目内置','本轮使用匿名 HTTP provider 与 Turndown 转换。','https://github.com/deepseek-ai/deepseek-harness/tree/master/packages/web'), - 'langchain-webbase':('本地开源库','WebBaseLoader 使用 HTTP 与 BeautifulSoup 取得网页文字。','https://reference.langchain.com/python/langchain-community/document_loaders/web_base/WebBaseLoader'), - 'crawl4ai':('本地开源库','本地浏览器渲染,再抽取 Markdown、媒体及链接信息。','https://docs.crawl4ai.com/'), -} -DISPLAY_NAMES={'moli-1.0.6':'Moli 1.0.6','moli':'Moli','moli-1.1.3':'Moli 1.1.3','moli-1.1.7':'Moli 1.1.7','moli-1.1.11':'Moli 1.1.11'} -def display_name(name):return measured_method_label(name,DISPLAY_NAMES.get(name,browser_method_label(name))) +from .methods import report_method, report_method_catalog + +PROFILES = {name: (item['profile']['kind'], item['profile']['description'], item['source_url']) + for name,item in report_method_catalog()['methods'].items() if item.get('profile')} + +def display_name(name): + return browser_method_label(name) + def adoption_counts(data,names): result=[] @@ -76,7 +59,7 @@ def render_methods(out,records,*,hero,section,table,cell,link,esc,authored=True) for row in selected: metadata=row.get('provider_metadata',{}) values=(row.get('provider_type','未记录'),metadata.get('engine','未记录'), - metadata.get('browser_version') or metadata.get('reported_version') or '未记录', + method_version(row) or '未记录', metadata.get('extractor','未记录'),metadata.get('output_contract','未记录')) if values not in rows:rows.append(values) body+=section(esc(name),'

    '+link('../providers/'+name+'.html','查看该方法的结果')+'

    ' @@ -95,19 +78,28 @@ def render_methods(out,records,*,hero,section,table,cell,link,esc,authored=True) (dest/'counts.json').write_text(json.dumps({'snapshot':data['generated_at'],'scope':'签名命中线索,不是运行调用次数或全网采用量','methods':counts},ensure_ascii=False,indent=2)+'\n') intro=[];rows=[];details='' for name in names: - from .methods import native_browser_engine, browser_method_registry - engine=native_browser_engine(name) - kind,desc,url=PROFILES.get(name,PROFILES.get(engine,('测试适配器','本地可重复测试实现。','https://example.com'))) + registered=report_method(name,next(row for row in records if row['provider']==name)) + english_registered=report_method(name,next(row for row in records if row['provider']==name),language='en') + profile=registered.get('profile',{}) + english_profile=english_registered.get('profile',{}) + kind=profile.get('kind','未登记 / Not registered') + desc=profile.get('description','Method description has not been registered.') + english_kind=english_profile.get('kind','Not registered') + english_desc=english_profile.get('description','Method description has not been registered.') + url=registered.get('source_url','') r=next(r for r in records if r['provider']==name);info=method_info(r['provider_type']) - url=browser_method_registry().get(name,{}).get('source_url',info.get('source_url',url)) + english_info=method_info(r['provider_type'],language='en') if native: model=method_page_model(name,name,[row for row in records if row['provider']==name]) desc=model['contract'] + english_desc=method_page_model(name,name,[row for row in records if row['provider']==name],language='en')['contract'] + english_info={**english_info,'output':'Native product output; actual responses and logs retained','freshness':'Cache and wait settings are in the frozen configuration'} info={**info,'output':'产品原生返回;保留实际输出与日志','freshness':'缓存与等待选项见本轮固定配置'} - intro.append(('',cell(link('../providers/'+name+'.html',display_name(name)))+cell(kind)+cell(desc)+cell(esc(info['output']))+cell(esc(info['freshness']))+cell(link(url,'文档 / 源码 ↗')))) + from .report_components import bilingual_text + intro.append(('',cell(link('../providers/'+name+'.html',display_name(name),browser_method_label(name,language='en')))+cell(bilingual_text(kind,english_kind))+cell(bilingual_text(desc,english_desc))+cell(bilingual_text(info['output'],english_info['output']))+cell(bilingual_text(info['freshness'],english_info['freshness']))+cell(link(url,'文档 / 源码 ↗')))) for item in counts: name=item['provider'];n=item['repository_count'];known=n is not None - provider_label=link('../providers/'+name+'.html',display_name(name)) if name in names else link('#interface-'+name,display_name(name)+'(未纳入本轮)') + provider_label=link('../providers/'+name+'.html',display_name(name),browser_method_label(name,language='en')) if name in names else link('#interface-'+name,display_name(name)+'(未纳入本轮)',browser_method_label(name,language='en')+' (not included in this run)') rows.append(('',cell(provider_label)+cell(link('#evidence-'+name,str(n)) if known else '—',n if known else -1)+cell(str(item['file_count']) if known else '—',item['file_count'] if known else -1))) if known: evidence_rows=[('',cell(link(r['url'],r['repo']))+cell('
    '.join(link(m['url'],m['path']) for m in r['matches']))) for r in item['repositories']] @@ -124,11 +116,8 @@ def render_methods(out,records,*,hero,section,table,cell,link,esc,authored=True) ('方法归属规则','逐一读取命中的 Git 文件并校验内容。通用 web_fetch 和同名函数不能证明归属;Exa 要求 Contents 端点或 SDK 导入及 get_contents。内置方法也包括源码复制与官方仓库,不能冒充第三方集成。'), ])+'

    '+link('evidence.json','完整检索证据')+' · '+link('counts.json','逐方法统计数据')+'

    ' body=hero('方法介绍','参评方法的官方能力、价格、接口与开源采用;实测表现见各方法执行记录。') - if is_browser_comparison(records): - body+=section('参评方法',cards([('远程接口','Kitesurf 使用指定 browser=kitesurf 的 Cloudflare Browser Run API;耗时包含公网往返、远端排队与执行。'),('本地原生接口','Lightpanda、Obscura 与 Moli 按任务使用各自官方接口。正文、文档和媒体任务的具体调用及输出格式见报告解读中的接口表。'),('Chromium 的正文转换','Chromium 通过原生接口读取渲染后的 DOM,再用 node-html-markdown 2.0.0 转成 Markdown。其他浏览器的正文不经过共同转换;文档任务不使用外部 PDF / DOCX 解析器。所有方法均不调用模型。')])) - body+=section('Kitesurf 能否本地部署',cards([('当前结论','截至 2026-09-07,Cloudflare 尚未发布 Kitesurf 源码或自行部署包;本轮只能通过 Cloudflare 账号版 Browser Run 远程测试,不能把它当作本地开源浏览器。'),('后续意向','Cloudflare 官方博客表示计划在准备好后开源,并希望允许客户部署到自己的账户。这是未来计划,不是当前已有能力。')])+'

    当前官方接入说明 · 官方开源计划说明

    ') - else: - body+=section('参评方法',cards([('托管 API','Firecrawl、Tavily、Exa、Cloudflare;通过各自 API 调用。'),('项目内置','CowAgent、OpenClaw、DeepSeek Harness;按本轮保留的实现与配置执行。'),('本地运行','LangChain WebBaseLoader、Crawl4AI、Moli WebFetch;使用本机网络与运行环境。')])) + body+=section('参评方法','

    '+' · '.join( + link('../providers/'+name+'.html',display_name(name),browser_method_label(name,language='en')) for name in names)+'

    ') body+=section('能力、价格与接口',tabs('method-guide',[ ('capabilities','官方能力',render_capabilities(out,names,table=table,cell=cell,link=link,records=records)), ('pricing','价格与计费',render_pricing(out,names,table=table,cell=cell,link=link,esc=esc)), diff --git a/harness/web_fetch/report_quality.py b/harness/web_fetch/report_quality.py index a28f0cf82..2d1bc7123 100644 --- a/harness/web_fetch/report_quality.py +++ b/harness/web_fetch/report_quality.py @@ -2,6 +2,7 @@ import re from lexbench_webfetch.metrics import task_passed +from .report_components import bilingual_text def quality_observations(record, case, output=''): @@ -21,7 +22,7 @@ def quality_observations(record, case, output=''): def render_quality_observations(record, case, output, *, esc): info = quality_observations(record, case, output) verdict = '排除比较' if record['metrics'].get('evaluation_excluded') else '通过' if info['task_pass'] else '未通过' - precision = '未评测' if info['word_precision'] is None else f'{info["word_precision"]:.2%}' + precision = bilingual_text('未评测','Not measured') if info['word_precision'] is None else f'{info["word_precision"]:.2%}' component = '包含验证码组件文字' if info['challenge_component_text'] else '未检测到该组件文字特征' hints = '、'.join(info['hint_words']) or '无命中' hint_html=''+esc(hints)+'' if info['hint_words'] else '无命中' diff --git a/harness/web_fetch/report_resources.py b/harness/web_fetch/report_resources.py index a1cacb44c..eec1cc171 100644 --- a/harness/web_fetch/report_resources.py +++ b/harness/web_fetch/report_resources.py @@ -3,12 +3,9 @@ from html import escape -PROVIDER_NAMES = { - 'chromium': 'Chromium', - 'lightpanda': 'Lightpanda', - 'moli': 'Moli 1.1.15 · 独立资源批次', - 'obscura': 'Obscura', -} +from .methods import report_method +from .report_components import bilingual_text +from harness.numbers import positive_number GROUPS = ( @@ -63,6 +60,10 @@ def render_resource_comparison(summary, providers): return ('

    当前证据没有独立的客户端资源测量;不从耗时反推 CPU 或内存。

    ', {'winner': None, 'winner_count': 0, 'ranked_metric_count': 0}) indexed = {row['provider']: row for row in summary['rows']} + def provider_name(provider): + # This independent cohort must not borrow versions from the main matrix. + label = report_method(provider).get('label', {}) + return label.get('zh', provider) declared = summary.get('providers', list(indexed)) local = [provider for provider in providers if provider in declared] missing = sorted(set(local) - set(indexed)) @@ -83,11 +84,11 @@ def render_resource_comparison(summary, providers): leader = max(local, key=lambda provider: wins[provider]) leaders = [provider for provider in local if wins[provider] == wins[leader]] leader_tied = len(leaders) > 1 - leader_label = '、'.join(PROVIDER_NAMES.get(provider, provider) for provider in leaders) + leader_label = '、'.join(provider_name(provider) for provider in leaders) def lowest(key, statistic): best = min(_metric(indexed, provider, key, statistic) for provider in local) - labels = '、'.join(PROVIDER_NAMES.get(provider, provider) for provider in local + labels = '、'.join(provider_name(provider) for provider in local if _metric(indexed, provider, key, statistic) == best) return best, labels @@ -132,7 +133,7 @@ def lowest(key, statistic): f'data-sort="{values[provider]}">' + rendered + '') rows.append(f'{row_index}' + escape(metric_name) + '' + ''.join(cells) + '') - header = ''.join('' + escape(PROVIDER_NAMES.get(provider, provider)) + '' + header = ''.join('' + escape(provider_name(provider)) + '' for provider in local) sections.append( '

    ' + escape(group_name) + '

    ' @@ -143,7 +144,7 @@ def lowest(key, statistic): + ''.join(rows) + '
    ') count = len(summary['planned_cases']) - completions = ','.join(PROVIDER_NAMES.get(provider, provider) + ' ' + + completions = ','.join(provider_name(provider) + ' ' + str(indexed[provider].get('successful_attempts', '—')) + '/' + str(count) for provider in local) sample = (f'

    所列配置共同通过 ' f'{len(summary["common_pass_cases"])}/{count};' '这项交集只作附加诊断,不用于选择资源表的样本。各方法成功调用的条件统计另保留在资源证据中。

    ') - caveat = ('

    可比边界:每次调用使用一次性 Linux 客户端容器、cgroup v2 与独立网络命名空间;' - '宿主控制器和容器启停不计入。Kitesurf 的远端浏览器 CPU、内存和远端上游流量不可见,' - '因此不进入资源表或本地引擎资源排名。

    ' - '

    CPU 与网络总量来自累计计数;内存与瞬时速率按 50 ms 采样,可能遗漏更短尖峰。' - 'CPU 占用、传输速率不按“越低越好”评选。完整方法与逐次证据见环境与复现页。

    ') + interval = summary.get('environment', {}).get('sample_interval_ms') + sampling_note = bilingual_text( + f'CPU 与网络总量来自累计计数;内存与瞬时速率按 {interval:g} ms 采样,可能遗漏更短尖峰。' + if positive_number(interval) else 'CPU 与网络总量来自累计计数;采样间隔未记录,不能据此判断短时尖峰覆盖。', + f'CPU and network totals come from cumulative counters. Memory and instantaneous rates are sampled every {interval:g} ms and may miss shorter spikes.' + if positive_number(interval) else 'CPU and network totals come from cumulative counters. The sample interval was not recorded, so short-lived peak coverage is unknown.') + unmeasured = [provider for provider in providers if provider not in declared] + scope_note = bilingual_text( + '每次调用使用一次性 Linux 客户端容器、cgroup v2 与独立网络命名空间;宿主控制器和容器启停不计入。不可见的远端 CPU、内存和上游流量不能计为本地资源测量。', + 'Each call uses a disposable Linux client container, cgroup v2 and a dedicated network namespace; the host controller and container lifecycle are excluded. Unobservable remote CPU, memory and upstream traffic cannot count as local resource measurements.') + if unmeasured: + scope_note += bilingual_text( + '未列入本资源测量范围的方法:'+ '、'.join(provider_name(provider) for provider in unmeasured)+'。', + 'Methods outside this resource cohort: '+ ', '.join( + report_method(provider).get('label', {}).get('en', provider) for provider in unmeasured)+'.') + caveat = ('

    '+bilingual_text('可比边界:', 'Comparison scope: ')+ + ''+scope_note+'

    '+sampling_note+ + bilingual_text('CPU 占用、传输速率不按“越低越好”评选。完整方法与逐次证据见环境与复现页。', + 'CPU utilization and transfer rates are not ranked as lower-is-better. See Methodology and Reproduction for full methods and per-call evidence.')+'

    ') return headline + cards + sample + '
    ' + ''.join(sections) + '
    ' + caveat, { 'winner': leader, 'winner_label': leader_label, diff --git a/harness/web_fetch/resource_cohort.py b/harness/web_fetch/resource_cohort.py index b1d30c4e5..5569ee14f 100644 --- a/harness/web_fetch/resource_cohort.py +++ b/harness/web_fetch/resource_cohort.py @@ -4,6 +4,7 @@ import hashlib import json import math +from harness.numbers import positive_number import statistics from pathlib import Path @@ -80,6 +81,10 @@ def build_summary(dataset: dict, records: list[dict], batches: list[dict]) -> di if any(any(environment.get(key) != expected for key, expected in required_environment.items()) for environment in environments): raise ValueError("Resource cohort is not uniformly Docker/cgroup-v2 isolated") + for environment in environments: + interval = environment.get("sample_interval_ms") + if not positive_number(interval): + raise ValueError("Resource cohort requires a recorded positive sample_interval_ms") invariant_keys = ( "docker_architecture", "image_id", "cpus", "memory", "pids_limit", "sample_interval_ms", "network_isolation", "controller_excluded", @@ -134,7 +139,7 @@ def build_summary(dataset: dict, records: list[dict], batches: list[dict]) -> di ), "sampling_limit": ( "CPU and network totals come from cumulative container counters. Memory and sampled " - "CPU/rate percentiles use 50 ms Docker/cgroup samples, so shorter-lived peaks can be " + f"CPU/rate percentiles use {environments[0]['sample_interval_ms']} ms Docker/cgroup samples, so shorter-lived peaks can be " "missed." ), "environment": {key: environments[0].get(key) for key in ( diff --git a/harness/web_fetch/wcxb_report_check.py b/harness/web_fetch/wcxb_report_check.py index a4e3e19fd..0b02d347c 100644 --- a/harness/web_fetch/wcxb_report_check.py +++ b/harness/web_fetch/wcxb_report_check.py @@ -22,6 +22,34 @@ ROOT = Path(__file__).resolve().parents[2] +def require_comparison_highlights(html): + """Validate actual highlights and exercise applicable corruptions on copies.""" + source = str(html) + home = check_presentation(source) + cells = home.select('#wcxb-core td[data-metric-direction][data-sort]') + require(bool(cells), 'Core comparison lacks numeric cells') + winner = home.select_one('#wcxb-core .comparison-winner') + if winner is not None: + winner.unwrap() + try: + check_presentation(home) + except ValueError: + pass + else: + raise AssertionError('A missing comparison highlight was accepted') + home = BeautifulSoup(source, 'html.parser') + unmarked = next((cell for cell in home.select('#wcxb-core td[data-metric-direction][data-sort]') + if cell.select_one('.comparison-winner') is None), None) + if unmarked is not None: + unmarked.append(BeautifulSoup('wrong', 'html.parser')) + try: + check_presentation(home) + except ValueError: + pass + else: + raise AssertionError('An incorrect comparison highlight was accepted') + + class Page(HTMLParser): def __init__(self): super().__init__() @@ -111,64 +139,43 @@ def require_analysis_controls(html: str | BeautifulSoup, result_count: int) -> B def require_home_interpretation(html: str | BeautifulSoup, summary: dict, - paired_latency: dict, lang: str) -> BeautifulSoup: + paired_latency: dict, lang: str, paired_methods: dict) -> BeautifulSoup: """Keep the version comparison readable and its timing caveat beside the table.""" soup = BeautifulSoup(html, 'html.parser') if isinstance(html, str) else html core = soup.select_one('#wcxb-core') - contexts = soup.select('#moli-latency-context') - changes = soup.select('#moli-change') - require(core is not None and len(contexts) == len(changes) == 1, - 'Home Moli comparison or timing context is missing or duplicated') + contexts = soup.select('#paired-latency-context') + require(not soup.select('#moli-change'), 'Report-specific findings belong in bound interpretation content') + require(core is not None and len(contexts) == 1, + 'Home paired comparison or timing context is missing or duplicated') context = contexts[0] require(core.find_parent('section') == context.find_parent('section'), - 'Moli timing context must be beside the core comparison') - require(core.find_next(id='moli-latency-context') == context, - 'Moli timing context must follow the core comparison') - release = summary['providers']['moli']['mean_latency_ms'] - candidate = summary['providers']['moli-pr643']['mean_latency_ms'] + 'Paired timing context must be beside the core comparison') + require(core.find_next(id='paired-latency-context') == context, + 'Paired timing context must follow the core comparison') + pair = {role: provider for provider, role in paired_methods.items()} + release = summary['providers'][pair['release']]['mean_latency_ms'] + candidate = summary['providers'][pair['pr']]['mean_latency_ms'] required = (f'{release:,.0f} ms', f'{candidate:,.0f} ms', f'{paired_latency["paired_delta_mean_ms"]:+.1f} ms') context_text = context.get_text(' ', strip=True) require(all(value in context_text for value in required), 'Home timing context disagrees with measured evidence') require(('交叉顺序' if lang == 'zh' else 'balanced same-window') in context_text - and ('504 页' if lang == 'zh' else '504 pages') in context_text, + and re.search(r'(? BeautifulSoup: + providers: list[str], lang: str, *, expected_html: str) -> BeautifulSoup: """A guide must work as a standalone explanation, with traceable examples.""" soup = BeautifulSoup(html, 'html.parser') if isinstance(html, str) else html - anchors = ('guide-results', 'guide-score', 'guide-cases', 'guide-repair', 'guide-methods', + expected = BeautifulSoup(expected_html, 'html.parser') + require(str(soup.select_one('main') or soup) == str(expected.select_one('main') or expected), + 'Guide differs from verified bound interpretation') + anchors = ('guide-results', 'guide-score', 'guide-cases', 'guide-methods', 'guide-timing', 'guide-conclusion') toc = soup.select_one('nav.essay-toc') require(toc is not None and all(soup.select_one(f'#{anchor}') is not None @@ -190,42 +197,9 @@ def require_guide_interpretation(html: str | BeautifulSoup, summary: dict, f'Guide overview disagrees with {provider} evidence') specimen = soup.select_one('#guide-cases') require(specimen is not None, 'Guide lacks a direct source-example entry') - repair = soup.select_one('#guide-repair') - images = ('code-block.png', 'description.png', 'adjacent-code.png') - require(repair is not None and len(repair.select('.repair-case')) == len(images), - 'Guide must explain each Moli repair in its own figure') - for case, image in zip(repair.select('.repair-case'), images): - graphic = case.select_one('figure img') - require(graphic is not None and graphic.get('src') == f'assets/wcxb-repair/{image}' - and len(graphic.get('alt', '')) >= 30 - and len(case.select('figcaption span')) == 3 - and len(case.select('.repair-samples > div')) == 3 - and case.select_one('.repair-mechanism a[href*="github.com/lexmount/moli/blob/384d1d2a/"]'), - f'Guide repair figure {image} lacks its image, code samples or current source') - captions = [node.get_text(' ', strip=True) for node in case.select('figcaption span')] - scope = case.select_one('.repair-scope') - require('1.1.8' in captions[1] and '384d1d2a' in captions[2] and scope is not None, - 'Guide figure does not compare the tagged release with current PR') - if image == 'description.png': - mode = '可选 UI 清理' if lang == 'zh' else 'optional UI stripping' - require(all(mode in caption for caption in captions[1:]), - 'Optional stripping captions omit the measured mode') - require('tsfunction identity(arg: number)' in repair.select('.repair-case')[0].get_text(' ', strip=True) - and ' None: inventory(root, profile) binding_path = contained_path(repository, 'evaluation/wcxb/report-content-binding.json', reject_symlinks=True) - check_bound_interpretation(root, json.loads(binding_path.read_bytes())) + content_binding = json.loads(binding_path.read_bytes()) + check_bound_interpretation(root, content_binding) + guide_sources = {lang: (root / prefix / 'guide/index.html').read_text() + for lang, prefix in [('zh', ''), ('en', 'en')] + if (root / prefix / 'guide/index.html').is_file()} sample_file = root / 'sample-manifest.json' sample = json.loads(sample_file.read_text()) require(sample.get('scope') == 'qualified_test', 'WCXB current selection is not source-qualified') @@ -286,25 +264,26 @@ def check(root: Path, *, authority_root: Path | None = None) -> None: case_ids = {row['file_id'] for row in sample['cases']} excluded = {row['file_id'] for row in sample.get('exclusions', [])} qualified = case_ids - excluded - require(len(case_ids) == profile['case_count'] == 504, 'WCXB case count differs') + require(len(case_ids) == profile['case_count'] == summary['case_count'], 'WCXB case count differs') require(len(scores) == len(case_ids) * len(providers), 'WCXB score count differs') - require(summary['quality_case_count'] == profile['primary_quality_case_count'] == sample['primary_quality_case_count'] == 504, 'WCXB quality denominator differs') + require(summary['quality_case_count'] == profile['primary_quality_case_count'] == sample['primary_quality_case_count'] == len(qualified), 'WCXB quality denominator differs') require(summary['exclusions'] == sample.get('exclusions', []) == [], 'WCXB current cohort contains excluded cases') require(summary['benchmark'] == sample['dataset_id'] == certificate['benchmark'], 'WCXB certified dataset identity differs') require({(r['provider'], r['case_id']) for r in scores} == {(p, c) for p in providers for c in case_ids}, 'WCXB method-case matrix differs') require([r['provider'] for r in certificate['providers']] == providers, 'WCXB certificate participants differ') - candidate = next(r for r in certificate['providers'] if r['provider'] == 'moli-pr643') - require(candidate['identity'].get('binary_sha256') == profile['candidate_binary_sha256'], - 'WCXB candidate binary differs from the current report profile') - released = next(r for r in certificate['providers'] if r['provider'] == 'moli') - require(released['identity'].get('binary_sha256') == profile['release_binary_sha256'], - 'WCXB released Moli binary differs from the current report profile') - optional_root = root / 'research/wcxb-optional-ui-4017' - optional_receipt = optional_root / 'receipt.json' - require(optional_receipt.is_file() and not optional_receipt.is_symlink() - and digest(optional_receipt) == profile['optional_ui_receipt_sha256'], - 'WCXB optional UI-stripping evidence differs from current profile') + from harness.web_fetch.wcxb_scoring import paired_latency_methods + paired_methods = paired_latency_methods(profile, certificate) + optional_paths = [name for name, sha in content_binding['evidence'].items() + if sha == profile['optional_ui_receipt_sha256']] + require(len(optional_paths) == 1, 'Optional feature receipt must have one profile binding') + optional_source = Path(optional_paths[0]) + optional_public = Path('research') / ('wcxb-' + optional_source.parent.name) / optional_source.name + optional_receipt = contained_path(root, optional_public, reject_symlinks=True) + require(optional_receipt.is_file() and digest(optional_receipt) == profile['optional_ui_receipt_sha256'], + 'WCXB optional feature evidence differs from current profile') + optional_root = optional_receipt.parent optional = json.loads(optional_receipt.read_text()) + require(optional.get('case_id') in case_ids, 'Optional feature case is outside the current population') for name, binary_hash in (('release', profile['release_binary_sha256']), ('candidate', profile['candidate_binary_sha256'])): build = optional['builds'][name] @@ -313,16 +292,16 @@ def check(root: Path, *, authority_root: Path | None = None) -> None: and artifact.is_file() and not artifact.is_symlink() and digest(artifact) == build['output_sha256'], 'WCXB optional UI-stripping output is not bound to its binary') - require(not optional['builds']['release']['description_present'] - and optional['builds']['candidate']['description_present'], - 'WCXB optional UI-stripping finding is not supported') + require(all(isinstance(build.get('description_present'), bool) + for build in optional['builds'].values()), + 'WCXB optional feature evidence lacks observed outcomes') from harness.web_fetch.wcxb_latency import ( load_full_paired_latency, summarize_full_paired_latency, ) _, selected_latency = load_full_paired_latency(root / 'research/wcxb-full-paired-latency', profile, case_ids) paired_latency = summarize_full_paired_latency(selected_latency, case_ids) - for provider, method in (('moli', 'release'), ('moli-pr643', 'pr')): + for provider, method in paired_methods.items(): values = sorted(selected_latency[case_id, method]['elapsed_ms'] for case_id in case_ids) position = (len(values) - 1) * .95 low = int(position) @@ -331,7 +310,7 @@ def check(root: Path, *, authority_root: Path | None = None) -> None: require(abs(measured['mean_latency_ms'] - paired_latency[method]['mean_ms']) < .01 and abs(measured['p50_latency_ms'] - paired_latency[method]['p50_ms']) < .01 and abs(measured['p95_latency_ms'] - p95) < .01, - 'WCXB Moli timing summary differs from certified full paired evidence') + 'WCXB Paired timing summary differs from certified full paired evidence') evidence_hashes = json.loads((root / 'evidence-manifest.json').read_text()) require(set(evidence_hashes) == case_ids, 'WCXB evidence inventory differs') by_case = {} @@ -384,24 +363,11 @@ def check(root: Path, *, authority_root: Path | None = None) -> None: require(all(f'data-case="{case}"' not in html for case in excluded), 'Invalid references entered result inventory') if route == 'guide/index.html': guide = BeautifulSoup(html, 'html.parser') - require_guide_interpretation(guide, summary, providers, lang) - for image in ('code-block.png', 'description.png', 'adjacent-code.png'): - asset = root / 'assets/wcxb-repair' / image - require(asset.is_file() and asset.read_bytes()[:8] == b'\x89PNG\r\n\x1a\n', - f'Repair graphic missing or invalid: {image}') - headings = {node.get_text(' ', strip=True) for node in guide.select('h2')} - require(('Obscura 为什么分数更高' if lang == 'zh' else 'Why Obscura scores higher') in headings, - 'Obscura content analysis is missing') - require(('Moli 两版耗时' if lang == 'zh' else 'Moli latency') in headings, - 'Paired Moli latency analysis is missing') - require(guide.select_one('a[href="research/wcxb-full-paired-latency/calls.jsonl"]') is not None, - 'Paired latency evidence link is missing') - require(guide.select_one('a[href$="cases/4461.html#obscura"]') is not None, - 'Obscura boundary counterexample is missing') + require_guide_interpretation(guide, summary, providers, lang, expected_html=guide_sources[lang]) if route in ('index.html', 'datasets/index.html'): check_presentation(html) if route == 'index.html': - require_home_interpretation(html, summary, paired_latency, lang) + require_home_interpretation(html, summary, paired_latency, lang, paired_methods) require('id="home-type"' in html, 'Page-type filter missing') require('官方全量 F1' not in html and 'Official full F1' not in html, 'Invalid full-reference score remains visible') first_card = BeautifulSoup(html, 'html.parser').select_one('.lead-grid .panel') @@ -454,29 +420,7 @@ def check(root: Path, *, authority_root: Path | None = None) -> None: pass else: raise AssertionError('A broken bilingual navigation link was accepted') - # Negative controls ensure the semantic contract rejects missing or wrong highlights. - home = BeautifulSoup((root / 'index.html').read_text(), 'html.parser') - winner = home.select_one('#wcxb-core .comparison-winner') - require(winner is not None, 'No numeric winner for negative control') - winner.unwrap() - try: - check_presentation(home) - except ValueError: - pass - else: - raise AssertionError('A missing comparison highlight was accepted') - home = BeautifulSoup((root / 'index.html').read_text(), 'html.parser') - first = home.select_one('#wcxb-core .comparison-winner').find_parent('td') - metric = first['data-metric'] - other = next(cell for cell in home.select('#wcxb-core td[data-metric]') - if cell['data-metric'] == metric and cell is not first) - other.append(BeautifulSoup('wrong', 'html.parser')) - try: - check_presentation(home) - except ValueError: - pass - else: - raise AssertionError('An incorrect comparison highlight was accepted') + require_comparison_highlights((root / 'index.html').read_text()) analysis = require_analysis_controls((root / 'analysis/index.html').read_text(), len(qualified) * len(providers)) analysis.select_one('#diagnosis-dialog [data-diagnosis-link]').decompose() try: @@ -496,79 +440,27 @@ def check(root: Path, *, authority_root: Path | None = None) -> None: pass else: raise AssertionError('A duplicate result summary was accepted') + # Mutations use the verified content's actual nodes, not historical case IDs, + # filenames, versions or claims. Any edited authored evidence must fail. for lang, prefix in language_pairs: - guide = BeautifulSoup((root / prefix / 'guide/index.html').read_text(), 'html.parser') - captions = guide.select('#guide-repair .repair-case')[1].select('figcaption span') - captions[1].string = 'Moli 1.1.8' - try: - require_guide_interpretation(guide, summary, providers, lang) - except ValueError: - pass - else: - raise AssertionError(f'A {lang} optional-feature caption without its mode was accepted') - guide = BeautifulSoup((root / 'guide/index.html').read_text(), 'html.parser') - guide.select_one('#guide-results tbody tr').decompose() - try: - require_guide_interpretation(guide, summary, providers, 'zh') - except ValueError: - pass - else: - raise AssertionError('A guide with a missing measured method was accepted') - guide = BeautifulSoup((root / 'guide/index.html').read_text(), 'html.parser') - guide.select_one('#guide-repair a[href$="cases/5458.html#moli-pr643"]').decompose() - try: - require_guide_interpretation(guide, summary, providers, 'zh') - except ValueError: - pass - else: - raise AssertionError('A guide with broken worked evidence was accepted') - for lang, prefix in language_pairs: - guide = BeautifulSoup((root / prefix / 'guide/index.html').read_text(), 'html.parser') - guide.select_one('#guide-repair .repair-case').decompose() - try: - require_guide_interpretation(guide, summary, providers, lang) - except ValueError: - pass - else: - raise AssertionError(f'A {lang} guide missing a repair diagram was accepted') - guide = BeautifulSoup((root / prefix / 'guide/index.html').read_text(), 'html.parser') - guide.select_one('#guide-repair figure img').decompose() - try: - require_guide_interpretation(guide, summary, providers, lang) - except ValueError: - pass - else: - raise AssertionError(f'A {lang} guide missing a repair image was accepted') - guide = BeautifulSoup((root / prefix / 'guide/index.html').read_text(), 'html.parser') - guide.select('#guide-repair .repair-case')[2].select('figcaption span')[1].string = ( - '正式版 1.1.7' if lang == 'zh' else 'Release 1.1.7') - try: - require_guide_interpretation(guide, summary, providers, lang) - except ValueError: - pass - else: - raise AssertionError(f'A {lang} guide mislabeled the tagged release') - guide = BeautifulSoup((root / prefix / 'guide/index.html').read_text(), 'html.parser') - for node in guide.find_all(string=lambda value: value and 'gap-2' in value): - node.replace_with(node.replace('gap-2', '')) - try: - require_guide_interpretation(guide, summary, providers, lang) - except ValueError: - pass - else: - raise AssertionError(f'A {lang} guide missing the CSS-spacing limit was accepted') - guide = BeautifulSoup((root / 'guide/index.html').read_text(), 'html.parser') - guide.select_one('#guide-results .comparison-winner').unwrap() - try: - require_guide_interpretation(guide, summary, providers, 'zh') - except ValueError: - pass - else: - raise AssertionError('A guide without a winning-column highlight was accepted') + source = guide_sources[lang] + for selector in ('#guide-results tbody tr', '#guide-cases a', + '#guide-conclusion'): + guide = BeautifulSoup(source, 'html.parser') + target = guide.select_one(selector) + require(target is not None, 'Guide lacks required interpretation evidence: ' + selector) + target.decompose() + try: + require_guide_interpretation(guide, summary, providers, lang, + expected_html=source) + except ValueError: + pass + else: + raise AssertionError('An altered bound interpretation was accepted: ' + selector) home = BeautifulSoup((root / 'index.html').read_text(), 'html.parser') - home.select_one('#moli-latency-context').decompose() + home.select_one('#paired-latency-context').decompose() try: - require_home_interpretation(home, summary, paired_latency, 'zh') + require_home_interpretation(home, summary, paired_latency, 'zh', paired_methods) except ValueError: pass else: @@ -666,18 +558,16 @@ def check_visual(root: Path, browser_binary: str) -> None: if errors: raise ValueError(f'Browser error: {lang} {route}: {errors}') if route == 'guide/index.html': - cards = page.locator('#guide-repair .repair-case') - if cards.count() != 3: - raise ValueError('Guide lost a repair example') - for card in cards.all(): - image = card.locator('figure img') + expected = BeautifulSoup((root / prefix / route).read_text(), 'html.parser') + images = page.locator('main img') + if images.count() != len(expected.select('main img')): + raise ValueError('Guide lost a bound illustration') + for image in images.all(): image.scroll_into_view_if_needed() try: image.evaluate('(node) => node.decode()', timeout=15000) except Exception as error: - raise ValueError(f'Guide repair image did not render: {image.get_attribute("src")}') from error - if card.locator('.repair-samples pre').count() != 3: - raise ValueError('Guide repair image lacks visible source examples') + raise ValueError(f'Guide image did not render: {image.get_attribute("src")}') from error if route == 'index.html' and not dark: check_presentation(page.content()) quality = [float(cell.get_attribute('data-sort')) for cell in page.locator('#wcxb-core tbody td:nth-child(5)').all()] @@ -687,7 +577,7 @@ def check_visual(root: Path, browser_binary: str) -> None: latency = [float(cell.get_attribute('data-sort')) for cell in page.locator('#wcxb-core tbody td:nth-child(6)').all()] if float(page.locator('#wcxb-core tbody tr').first.locator('td').nth(5).get_attribute('data-sort')) != min(latency): raise ValueError('Core latency sorting did not work') - page.locator('#home-type').select_option('article') + page.locator('#home-type').select_option(passing_row['page_type']) if page.locator('#wcxb-types tbody tr:visible').count() != 1: raise ValueError('Page-type filter does not narrow the comparison') if route == 'datasets/index.html' and not dark: diff --git a/pyproject.toml b/pyproject.toml index fa63e419c..924a83d3f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -6,7 +6,7 @@ readme = "README.md" license = {file = "LICENSE"} requires-python = ">=3.11" dependencies = [ - "lexbench-webfetch @ git+https://github.com/lexmount/lexbench-webfetch.git@84c177d97da3b82567d9881e377aa728929cf129", + "lexbench-webfetch @ git+https://github.com/lexmount/lexbench-webfetch.git@5ac291a592fec849d86bbb791a80579cc7a6c2c6", "beautifulsoup4>=4.12,<5", "fastcdc==1.7.0", ] diff --git a/research/editorial-figures.json b/research/editorial-figures.json deleted file mode 100644 index c1e89eb1e..000000000 --- a/research/editorial-figures.json +++ /dev/null @@ -1,46 +0,0 @@ -{ - "schema_version": 1, - "generator": "built-in image_gen", - "figures": [ - { - "slot": "overview_figure", - "kind": "measured-summary", - "alt": "五款浏览器的通过率、Moli 相对 Chromium 的耗时减少比例,以及文档与媒体的通过数。", - "caption": "本轮结果汇总。耗时减少比例以 Chromium 为基准,取 98 道共同成功任务。", - "assets": { - "zh": { - "file": "overview-zh.png", - "sha256": "bdeae9f5af822e5b62376cb9ef78de47fdec2040e1b1271c1c543d1951684676" - }, - "en": { - "file": "overview-en.png", - "sha256": "0b0ab93ffad68c3cca567ad75f1b8865301530e4f464d14ae45673cf09aa3230" - } - } - }, - { - "slot": "browser_modes_figure", - "kind": "conceptual-illustration", - "alt": "上方读取论文内容,下方搜索商品、筛选并加入购物车,对比 Web Fetch 与 Browser Use。", - "caption": "以读论文和购物为例,说明读取内容与连续操作的区别。图中页面为流程示意。", - "assets": { - "zh": { - "file": "browser-modes.png", - "sha256": "df41509f9453b7f7204b030318cf7a1128563add17bd53270cf54834cc714602" - } - } - }, - { - "slot": "delivery_types_figure", - "kind": "conceptual-illustration", - "alt": "五行分别展示网页正文、文档文字、列表记录、媒体地址和原始文件的获取与交付。", - "caption": "按最终交付物区分任务;图中页面为流程示意,下面的案例展示实测输出。", - "assets": { - "zh": { - "file": "delivery-types.png", - "sha256": "71d592a562e43dcd634d2dbe9fc3258152da0ee48b89e41dad503957e8dafd00" - } - } - } - ] -} diff --git a/scripts/bump_lexbench.py b/scripts/bump_lexbench.py new file mode 100644 index 000000000..410358410 --- /dev/null +++ b/scripts/bump_lexbench.py @@ -0,0 +1,9 @@ +"""Update the current evaluator and its frozen definition bindings.""" +import sys +from pathlib import Path + +sys.path.insert(0, str(Path(__file__).resolve().parents[1])) +from harness.lexbench_pin import main + +if __name__ == "__main__": + main() diff --git a/tests/fixtures/native-route-baseline.json b/tests/fixtures/native-route-baseline.json index 2ee28fb91..af4a2959c 100644 --- a/tests/fixtures/native-route-baseline.json +++ b/tests/fixtures/native-route-baseline.json @@ -1,74 +1,74 @@ { "moli_webfetch:defaults": { "cells": 168, - "sha256": "5793dbba5f66d0a986adbce2b3e4a53193a4a15860aa9758c1f650bf6c57ded9" + "sha256": "4e66d03b097e2e0b8d8a3025079eb0a30bf565120199c97a410abc044e1bb027" }, "moli_webfetch:configured": { "cells": 168, - "sha256": "34cde528073e28439123c56bc0257690ae826fc632ac28dfc33641e9fabac4a5" + "sha256": "f9586a678d144f518486e9e436dd5c64880f048b6e0e4ed93ba26beac38001c0" }, "firecrawl:defaults": { "cells": 168, - "sha256": "793e0acea3d7fdb2b975401624aeccaa25c72d618e2600d8daf8826ab8a008b4" + "sha256": "85fe1b1eb0d29ddf7f293c91266e896c3616a7b2e839feb8a56b48beafb304c8" }, "firecrawl:configured": { "cells": 168, - "sha256": "eae62898cf41b07ead4bc9d691587b15b6542077acf0412bf039bd2c3ec5a33c" + "sha256": "f2a97410b7f9e06ae06637d0e7656e138a3521a60dd8027c426614f30b639dea" }, "tavily_extract:defaults": { "cells": 168, - "sha256": "cb0583ec0d7eed473052643e276773b58ee2ff297049b91960ec990ebc0649c7" + "sha256": "1da2806f37b9b2b224265e220a96fb9849ea82f0c40f3d7777ed1cdc3d0c15f6" }, "tavily_extract:configured": { "cells": 168, - "sha256": "3da16b509518eadd7c0bc06e513476015a2967aaf0516adcc00a8d6bc60736e3" + "sha256": "a40b318ccca1978af7462a62baa0d4d47fbab326162f2c83446432e0b3c0b057" }, "exa_contents:defaults": { "cells": 168, - "sha256": "85204b6297b5f13b424ba84ac714fa44b45d24e0fd796a46691468c098bb6f72" + "sha256": "1312dfa3eb33b9b29de55543fc3ffd663b0e59f1becb2cd10a7e12777d31bca0" }, "exa_contents:configured": { "cells": 168, - "sha256": "7b85538d8a41e5367c94e0a619c4edbf467a64d338f35640819aa4f27e252002" + "sha256": "9c7ffd79e20fcfb322a317dfae3271203c1c2c42aa09f52daaaabadfdf946d99" }, "cloudflare_markdown:defaults": { "cells": 168, - "sha256": "e0218dd4a0ed9f3e35a318ed97c712da86281246085d2c64726a85ed7dcd0542" + "sha256": "bc8d5ea1939f7810a0d1b38d0ee98f86bbc8d53e9d25940dea9efcbadb0a7328" }, "cloudflare_markdown:configured": { "cells": 168, - "sha256": "a982efa5d1ebe54bff4683d0b3b80d1e91246a88d233142f9caec1e02b3c1b08" + "sha256": "8fe63eb401e2964c6af30ec49dbc66625ccaf8e4f75af573e8f7f6c7563fed25" }, "cloudflare_content:defaults": { "cells": 168, - "sha256": "72cd510bc4e3e32f43acfa02a6824b594c074bae37d430cea932fd6801416345" + "sha256": "a9c73f32a531965a9e55293c18b72766266010a231fcf9150af41922d4eeb8bf" }, "cloudflare_content:configured": { "cells": 168, - "sha256": "16a136fd92e2abe6cef504c34f329ab16462c0a873645d98f494beae9b5f1cde" + "sha256": "6e3983ae12da6d3f28edb35b102f48e13b51e31cf3faeeeb5297e41840158b75" }, "subprocess_jsonl:defaults": { "cells": 168, - "sha256": "6a7c4a9754afe5c0e17737a1c832dbd44401c7d3b41d0827f9a1cb168ba8d488" + "sha256": "4edb323b311ee86004aa160120562ab42ca95bcb65da7b330a9e819e4148d28e" }, "subprocess_jsonl:configured": { "cells": 168, - "sha256": "498c5d9ea4928d053b64d1654a6de0ef309d3ab2bf6c8954e046001706178642" + "sha256": "6612065c03bd9061fd165345c96d239ec973493e190dea7bf829644412dda6a1" }, "cowagent_builtin:defaults": { "cells": 168, - "sha256": "18e394a644f6edf4149b1f67143acb23824a3f69ecfb306ddeaf146247e45806" + "sha256": "bbbfed5ef4238db2652f8aaac170d5144ab68dbc962e53fc59300d51ff78c672" }, "cowagent_builtin:configured": { "cells": 168, - "sha256": "d7c7e7cc95c71d9272be6685d96825ab3461afc22ae3149b1e3e99134d1f17f0" + "sha256": "f5a5763367ad7861c3f8e7d8180c27ecf5104829a96036e0610650fa24b851f2" }, "fixture:defaults": { "cells": 168, - "sha256": "9639ff1aa324d9d422f8c08d8b6f9b156fc173dbff169b242118bdca8eab86c3" + "sha256": "46897fa8480097bed7680f1e0057abaddd45cf8e6acc3a9857b80580ba526eae" }, "fixture:configured": { "cells": 168, - "sha256": "cdf031ef8e403970fbb583e9f27bf999958de6194d663981e9ca9411a590a9f0" + "sha256": "78d72000b12c48bcafc5c1fbe6a704194ee09202f9307615f8e2336ad5486bfe" } } diff --git a/tests/test_accounts.py b/tests/test_accounts.py index ba2470fce..03ada2114 100644 --- a/tests/test_accounts.py +++ b/tests/test_accounts.py @@ -1,67 +1,223 @@ -import unittest -from unittest.mock import patch - -from harness.web_fetch.accounts import ( - ACCOUNT_REGISTRY, REPORT_LOCAL_ACCESS, REPORT_PROVIDER_ACCOUNT_IDS, env_template, - report_access_registry, -) - - -class AccountRegistryTest(unittest.TestCase): - def test_registry_has_official_links_and_unique_ids(self) -> None: - ids = [item["id"] for item in ACCOUNT_REGISTRY] - self.assertEqual(len(ids), len(set(ids))) - for item in ACCOUNT_REGISTRY: - self.assertTrue(item["registration_url"].startswith("https://")) - self.assertTrue(item["key_url"].startswith("https://")) - self.assertTrue(item["env_vars"]) - - def test_env_template_contains_core_keys_without_values(self) -> None: - template = env_template() - self.assertIn("LEXMOUNT_API_KEY=\n", template) - self.assertIn("FIRECRAWL_API_KEY=\n", template) - self.assertNotIn("secret", template.casefold()) - - def test_report_registry_is_exactly_the_measured_methods(self) -> None: - providers = { - "cloudflare-markdown", "cowagent-built-in", "crawl4ai", - "deepseek-harness-built-in", "exa", "firecrawl", - "langchain-webbase", "lexmount-cn", "lexmount-global", - "moli-webfetch", "openclaw-built-in", "tavily", - } - rows = report_access_registry(providers) - self.assertEqual(len(rows), len(providers)) - names = {row["provider"] for row in rows} - self.assertNotIn("Browserbase Fetch", names) - self.assertIn("Moli WebFetch", names) - - def test_report_registry_fails_closed_for_unknown_method(self) -> None: - with self.assertRaisesRegex(ValueError, "unknown-method"): - report_access_registry({"unknown-method"}) - - def test_local_source_methods_document_their_runtime_commands(self) -> None: - self.assertEqual( - REPORT_LOCAL_ACCESS["openclaw-built-in"]["env_vars"], - ["OPENCLAW_REPO", "OPENCLAW_NODE", "OPENCLAW_PNPM"], - ) - self.assertEqual( - REPORT_LOCAL_ACCESS["deepseek-harness-built-in"]["env_vars"], - ["DEEPSEEK_HARNESS_REPO", "OPENCLAW_NODE", "DEEPSEEK_PNPM"], - ) - - def test_report_registry_names_stale_account_mapping(self) -> None: - with patch.dict(REPORT_PROVIDER_ACCOUNT_IDS, {"stale-method": "missing-account"}): - with self.assertRaisesRegex(ValueError, "stale-method"): - report_access_registry({"stale-method"}) - - def test_cloudflare_account_exposes_both_official_product_permissions(self) -> None: - account = next(item for item in ACCOUNT_REGISTRY - if item['id'] == 'cloudflare-browser-rendering') - self.assertEqual(account['env_vars'], ['CLOUDFLARE_API_TOKEN', 'CLOUDFLARE_ACCOUNT_ID']) - self.assertIn('Browser Rendering → Edit', account['steps']) - self.assertIn('Workers AI → Read', account['steps']) - self.assertIn('Workers AI Read 或 Edit', account['free_note']) - - -if __name__ == "__main__": - unittest.main() +"""Account setup and report access share the method catalog.""" +import copy +from pathlib import PurePosixPath + +from harness.web_fetch.accounts import account_checklist, env_template +from harness.web_fetch.methods import method_access, report_method_catalog +from harness.web_fetch.report import render_account_requirements +from harness.web_fetch.report_i18n import localize_html + + +def test_checklist_has_unique_ids_and_official_links(): + entries = account_checklist() + assert len({item['id'] for item in entries}) == len(entries) + for item in entries: + assert item['type'] == 'hosted' + assert item['env_vars'] + assert all(url.startswith('https://') for _, url in item['links']) + + +def test_env_template_contains_core_keys_without_values(): + template = env_template() + assert 'LEXMOUNT_API_KEY=\n' in template + assert 'FIRECRAWL_API_KEY=\n' in template + assert 'secret' not in template.casefold() + + +def test_every_method_uses_one_access_schema(): + for name, method in report_method_catalog()['methods'].items(): + assert 'account_id' not in method + assert 'account' not in method + access = method_access(name, []) + assert access['type'] in {'hosted', 'local'} + assert access['account'] and access['configuration'] and access['links'] + assert isinstance(access['env_vars'], list) + + +def test_report_uses_only_measured_methods_and_includes_all_hosted_accounts(): + names = {'firecrawl', 'tavily', 'exa', 'cloudflare-markdown', 'moli', 'crawl4ai'} + page = render_account_requirements([{'provider': name} for name in names]) + intro = page.split('需要供应商账号的方法:', 1)[1].split('

    ', 1)[0] + for name in ('firecrawl', 'tavily', 'exa', 'cloudflare'): + assert name in intro.casefold() + assert 'Moli' not in intro + assert 'Crawl4AI' not in intro + assert 'Browserbase' not in page + for variable in ('FIRECRAWL_API_KEY', 'TAVILY_API_KEY', 'EXA_API_KEY'): + assert variable in page + + +def test_unknown_method_has_explicit_missing_access(): + access = method_access('unknown-method', []) + assert access['type'] == 'unknown' + assert 'Not registered' in access['account'] + assert access['env_vars'] == [] + assert 'unknown-method' in render_account_requirements([{'provider': 'unknown-method'}]) + + +def test_local_tools_retain_runtime_commands(): + assert method_access('openclaw-built-in', [])['env_vars'] == [ + 'OPENCLAW_REPO', 'OPENCLAW_NODE', 'OPENCLAW_PNPM'] + assert method_access('deepseek-harness-built-in', [])['env_vars'] == [ + 'DEEPSEEK_HARNESS_REPO', 'OPENCLAW_NODE', 'DEEPSEEK_PNPM'] + + +def test_cloudflare_retains_both_product_permissions(): + access = method_access('cloudflare-markdown', []) + assert access['env_vars'] == ['CLOUDFLARE_API_TOKEN', 'CLOUDFLARE_ACCOUNT_ID'] + assert 'Browser Rendering → Edit' in access['configuration'] + assert 'Workers AI → Read' in access['configuration'] + assert 'Workers AI Read 或 Edit' in access['account'] + + +def test_unimplemented_candidates_remain_marked_planned(): + entries = account_checklist() + planned = {item['id'] for item in entries if not item['implemented']} + assert 'browserbase' in planned + assert len(planned) == 12 + catalog = report_method_catalog()['methods'] + assert all(catalog[name]['status'] == 'planned' for name in planned) + + +def test_json_registered_hosted_method_appears_in_both_languages(monkeypatch): + catalog = copy.deepcopy(report_method_catalog()) + catalog['methods']['acme-api'] = { + 'label': {'zh': 'Acme API', 'en': 'Acme API'}, + 'access': {'type': 'hosted', 'env_vars': ['ACME_API_KEY'], + 'links': [['Dashboard', 'https://example.com/acme']], + 'account': 'Requires an account.', 'configuration': 'Create an API key.'}} + monkeypatch.setattr('harness.web_fetch.methods.report_method_catalog', lambda language='zh': catalog) + page = render_account_requirements([{'provider': 'acme-api', + 'provider_metadata': {'api_key': 'secret-canary'}}]) + assert '需要供应商账号的方法:' in page + assert 'Acme API' in page.split('需要供应商账号的方法:', 1)[1].split('

    ', 1)[0] + english = localize_html(page, PurePosixPath('accounts/index.html')) + assert 'Methods requiring a provider account: ' in english + assert 'ACME_API_KEY' in english + assert 'secret-canary' not in page + english + + + +def test_access_localization_uses_field_identity_when_chinese_text_collides(monkeypatch): + from harness.web_fetch.methods import _raw_report_method_catalog, _catalog_value + from harness.web_fetch.report import render_account_requirements + from harness.web_fetch.report_i18n import localize_html + from pathlib import PurePosixPath + catalog = copy.deepcopy(_raw_report_method_catalog()) + for name, meaning in [('alpha-api', 'Create an Alpha account'), + ('beta-api', 'Use your existing Beta account')]: + catalog['methods'][name] = { + 'label': {'zh': name, 'en': name}, + 'access': {'type': 'hosted', 'env_vars': [], 'links': [], + 'account': {'zh': '账号要求', 'en': meaning}, + 'configuration': {'zh': '配置要求', 'en': name+' configuration'}}} + monkeypatch.setattr('harness.web_fetch.methods.report_method_catalog', + lambda language='zh': _catalog_value(catalog, language=language)) + html = render_account_requirements([{'provider': name} for name in ['alpha-api', 'beta-api']]) + english = localize_html(html, PurePosixPath('accounts/index.html')) + assert 'Create an Alpha account' in english + assert 'Use your existing Beta account' in english + assert 'alpha-api configuration' in english + assert 'beta-api configuration' in english + + + +def test_official_links_render_text_instead_of_escaped_bilingual_markup(): + from bs4 import BeautifulSoup + from pathlib import PurePosixPath + from harness.web_fetch.report_i18n import localize_html + body = render_account_requirements([{'provider': 'moli'}]) + for page in (body, localize_html(body, PurePosixPath('accounts/index.html'))): + soup = BeautifulSoup(page, 'html.parser') + links = soup.select('a[href^="https://"]') + assert links + assert all('账号与获取入口'), len(set(SPECS) & set(HEADLESS_ACCESS))) - for name, access in HEADLESS_ACCESS.items(): + self.assertEqual(body.count('

    账号与获取入口

    '), len(SPECS)) + for name, access in accesses.items(): if name in SPECS: saved = json.loads((root / f'adoption/interfaces/{name}.json').read_text()) self.assertEqual(saved['access']['account'], access['account']) @@ -48,7 +48,7 @@ def test_exported_chromium_interface_uses_observed_completion(self): self.assertEqual(SPECS['chromium']['request']['wait_until'], 'load') def test_executable_version_is_not_replaced_by_cdp_compatibility_version(self): - from harness.web_fetch.report_interfaces import method_version + from harness.web_fetch.methods import method_version for name,version,compat in [('moli','1.1.15','145.0.0.0'),('lightpanda','1.0.0','124.0.6367.29'),('obscura','0.2.4','145.0.0.0')]: row={'provider':name,'provider_metadata':{'binary_sha256':'hash','browser_version':compat}} self.assertEqual(method_version(row,{'hash':{'version':version}}),version) @@ -61,3 +61,19 @@ def test_executable_version_is_not_replaced_by_cdp_compatibility_version(self): if __name__ == '__main__': unittest.main() + + + +def test_interface_authored_fields_use_direct_english(tmp_path, monkeypatch): + from html import unescape + from pathlib import PurePosixPath + from harness.web_fetch.methods import report_method_catalog + from harness.web_fetch.report_i18n import localize_html + (tmp_path/'adoption').mkdir() + body = render_interfaces(tmp_path, []) + english = unescape(localize_html(body, PurePosixPath('adoption/index.html'))) + expected = report_method_catalog('en')['methods']['chromium']['interface'] + assert expected['notes'] in english + assert expected['params'][0][2] in english + assert expected['auth'] in english + assert '未记录的内部默认值不作推断' not in english diff --git a/tests/test_headless_report.py b/tests/test_headless_report.py index db2108690..497b3e6ef 100644 --- a/tests/test_headless_report.py +++ b/tests/test_headless_report.py @@ -96,8 +96,15 @@ def test_resource_co_leaders_are_named_in_both_languages(self): self.assertTrue(result['winner_tied']) english = localize_html(page, PurePosixPath('guide/index.html')) self.assertIn('Resource-efficiency co-leaders in this run', english) - self.assertIn('Moli 1.1.15 · independent resource cohort, Lightpanda, Obscura, Chromium', english) + self.assertIn('Moli, Lightpanda, Obscura, Chromium', english) self.assertNotIn('资源效率并列领先', english) + self.assertNotIn('1.1.15', page) + self.assertNotIn('1.1.15', english) + self.assertIn('sample interval was not recorded', english) + summary['environment'] = {'sample_interval_ms': 125} + recorded, _ = render_resource_comparison(summary, providers) + self.assertIn('125 ms', recorded) + self.assertNotIn('50 ms', recorded) def test_resource_table_excludes_unobservable_remote_provider(self): resource_providers = ['moli', 'chromium', 'lightpanda', 'obscura'] @@ -836,6 +843,16 @@ def test_new_cohort_has_dynamic_families_populations_and_evidence(self): self.assertIn('统一解析策略不保证', body) self.assertNotIn('150 道',body) self.assertNotIn('1.1.5',body) + from bs4 import BeautifulSoup + self.assertNotIn(' 2" in line for line in binding_changes(binding, result)) + + +def test_rejected_update_leaves_all_bindings_unchanged(tmp_path, monkeypatch): + (tmp_path / "evaluation").mkdir() + names = ("pyproject.toml", "uv.lock", "evaluation/lexbench-webfetch.json") + for name in names: + shutil.copyfile(ROOT / name, tmp_path / name) + before = {name: (tmp_path / name).read_bytes() for name in names} + def reject(*args): + raise ValueError("not merged") + monkeypatch.setattr("harness.lexbench_pin.require_merged_revision", reject) + with pytest.raises(ValueError, match="not merged"): + bump(tmp_path, "a" * 40) + assert before == {name: (tmp_path / name).read_bytes() for name in names} + + +def test_upstream_check_accepts_ancestor_and_rejects_unmerged_commit(tmp_path): + repo = tmp_path / "repo" + repo.mkdir() + def git(*args): + return subprocess.check_output(["git", *args], cwd=repo, text=True).strip() + git("init", "-b", "main") + git("-c", "user.name=Test", "-c", "user.email=test@example.com", "commit", "--allow-empty", "-m", "main") + merged = git("rev-parse", "HEAD") + git("checkout", "-b", "unmerged") + git("-c", "user.name=Test", "-c", "user.email=test@example.com", "commit", "--allow-empty", "-m", "unmerged") + unmerged = git("rev-parse", "HEAD") + require_merged_revision(str(repo), merged, tmp_path / "accepted") + with pytest.raises(ValueError, match="not in upstream main"): + require_merged_revision(str(repo), unmerged, tmp_path / "rejected") diff --git a/tests/test_local_audit.py b/tests/test_local_audit.py index f505e8b3e..0752a6913 100644 --- a/tests/test_local_audit.py +++ b/tests/test_local_audit.py @@ -325,7 +325,10 @@ def test_new_local_matrix_is_complete_and_keeps_logs_without_claiming_review(tmp method_records = [{**records[0], "provider_type": "firecrawl"}] method_cases = {actual["id"]: actual} page = provider_page_body("firecrawl", method_records, method_cases, [], [], native_default=True) - assert "产品内部超时保留默认值" in page + assert "产品内部超时保留默认值" not in page + assert "内部参数以保存的请求为准" in page + assert 'href="../methodology/index.html"' in page + assert 'href="../reproduce/index.html"' in page assert "Actions" not in page assert "Actions" in provider_page_body("firecrawl", method_records, method_cases, [], []) diff --git a/tests/test_native_answer_projection.py b/tests/test_native_answer_projection.py index c66816b8a..18fb943b4 100644 --- a/tests/test_native_answer_projection.py +++ b/tests/test_native_answer_projection.py @@ -117,7 +117,7 @@ def test_optional_result_fields_preserve_empty_but_reject_invented_defaults(): ]) def test_attribute_projection_preserves_commas_inside_urls(attribute, expected): declaration = {'schema_version': 2, 'content_format': 'empty', 'answer_fields': { - 'images': {'pointer': ['elements'], 'format': 'html_attribute_urls', + 'images': {'pointer': ['elements'], 'format': 'html_attribute_urls', 'srcset_parser': 'html-candidates-v1', 'base_url': 'https://example.test/page'}}} payload = {'elements': [{'results': [{'attributes': [{'name': 'srcset', 'value': attribute}]}]}]} assert project_native_answer(payload, declaration)[1]['images'] == sorted(expected) @@ -165,3 +165,70 @@ def test_projection_controls_cover_public_persistence_and_saved_verification(tmp 'artifact_path': str(artifact), 'provider_metadata': result.metadata, 'metrics': {'content_sha256': hashlib.sha256(row['artifact'].encode()).hexdigest()}} verify_saved_native_answer(tmp_path, record) + + +@pytest.mark.parametrize('source, current_parser', [ + ('b17fc68566409232d3840f69aa4511c6dcdfd154c774eb169ea43787659496f7', False), + ('bd08ad5ba4a6ef6962391ef0ea6f3ed7cccaac3294708de87cfd1b9e95d6969c', False), + ('7e1b468e981cfe1da89d443047aff7facd841cd03f00bfc4b016ed67ba292da3', True), + ('d042394ea0e8c3aa7aed5851663eaf73772fc4357781999e20a87f48e857e00a', False), +]) +def test_historical_srcset_replay_is_source_bound_and_exact(tmp_path, source, current_parser): + from harness.web_fetch.native_routes import verify_saved_native_answer + payload = {'elements': [{'results': [{'attributes': [ + {'name': 'srcset', 'value': '/image?crop=10,20 1x, /large.png 2x'}]}]}]} + declaration = {'schema_version': 2, 'content_format': 'empty', 'answer_fields': { + 'images': {'pointer': ['elements'], 'format': 'html_attribute_urls', + 'base_url': 'https://example.test/page'}}} + expected = ['https://example.test/20', 'https://example.test/image?crop=10', + 'https://example.test/large.png'] + legacy = expected + current = ['https://example.test/image?crop=10,20', 'https://example.test/large.png'] + expected = current if current_parser else legacy + raw = json.dumps(payload).encode() + (tmp_path / 'answer.txt').write_bytes(b'') + (tmp_path / 'raw.json').write_bytes(raw) + metadata = {'native_answer_projection': declaration, 'images': expected, + 'raw_response_path': 'raw.json', 'raw_response_sha256': hashlib.sha256(raw).hexdigest()} + record = {'ok': True, 'url': 'https://example.test/page', 'artifact_path': 'answer.txt', + 'provider_metadata': metadata, 'metrics': { + 'content_sha256': hashlib.sha256(b'').hexdigest(), 'scoring_source_sha256': source}} + frozen = deepcopy(record) + verify_saved_native_answer(tmp_path, record) + assert record == frozen + assert (tmp_path / 'raw.json').read_bytes() == raw + # Neither parser's answer may rescue a receipt bound to the other parser. + metadata['images'] = legacy if current_parser else current + with pytest.raises(ValueError, match='differs from its declared native field projection'): + verify_saved_native_answer(tmp_path, record) + metadata['images'] = expected + record['metrics']['scoring_source_sha256'] = '0' * 64 + with pytest.raises(ValueError, match='lacks a recognized parser binding'): + verify_saved_native_answer(tmp_path, record) + record['metrics']['scoring_source_sha256'] = source + declaration['answer_fields']['images']['srcset_parser'] = 'html-candidates-v1' + if current_parser: + verify_saved_native_answer(tmp_path, record) + else: + with pytest.raises(ValueError, match='differs from its bound semantics'): + verify_saved_native_answer(tmp_path, record) + + +@pytest.mark.parametrize('parser', [None, '', 1, True, 'future-v9', 'comma-split-v1']) +def test_current_srcset_rejects_absent_or_unknown_parser(parser): + spec = {'pointer': ['elements'], 'format': 'html_attribute_urls', + 'base_url': 'https://example.test/'} + if parser is not None: + spec['srcset_parser'] = parser + declaration = {'schema_version': 2, 'content_format': 'empty', 'answer_fields': {'images': spec}} + payload = {'elements': [{'results': [{'attributes': [{'name': 'srcset', 'value': '/one.png 1x'}]}]}]} + with pytest.raises(ValueError, match='srcset'): + project_native_answer(payload, declaration) + + +def test_acquisition_declares_current_srcset_semantics(): + from harness.web_fetch.native_projection import native_answer_projection + declaration = native_answer_projection('cloudflare_markdown', + {'track': 'image', 'task_kind': 'resource', 'url': 'https://example.test/'}, + {'success': True, 'result': []}, 200) + assert declaration['answer_fields']['images']['srcset_parser'] == 'html-candidates-v1' diff --git a/tests/test_native_browser_variants.py b/tests/test_native_browser_variants.py index cedcbe674..104ed26de 100644 --- a/tests/test_native_browser_variants.py +++ b/tests/test_native_browser_variants.py @@ -36,7 +36,9 @@ def test_candidate_slot_is_distinct_from_release_without_report_lineage(self): from harness.web_fetch.methods import browser_method_label, browser_method_registry, native_browser_engine self.assertEqual(native_browser_engine("moli-candidate"), "moli") self.assertEqual(browser_method_label("moli-release"), "Moli release build") - self.assertEqual(browser_method_registry()["moli-candidate"]["source_url"], + self.assertNotIn("source_url", browser_method_registry()["moli-candidate"]) + from harness.web_fetch.methods import report_method + self.assertEqual(report_method("moli-candidate")["source_url"], "https://github.com/lexmount/moli") self.assertNotEqual(browser_method_label("moli-candidate"), browser_method_label("moli-release")) @@ -190,9 +192,8 @@ def test_moli_base_timeout_description_covers_both_evidence_policies(self): for method in ("moli-webfetch", "moli"): description = str(SPECS[method]["params"]) self.assertIn("内部参数以冻结的调用记录为准", description) - self.assertIn("显式模式留出收尾和清理时间", description) - self.assertIn("uniform", description) - self.assertIn("沿用产品默认值", description) + self.assertIn("未记录的内部默认值不作推断", description) + self.assertNotIn("45 秒", description) def test_moli_interface_discloses_observed_explicit_timeout_policy(self): from harness.web_fetch.report_i18n import translate_text diff --git a/tests/test_report_article_cases.py b/tests/test_report_article_cases.py deleted file mode 100644 index 46ee030ec..000000000 --- a/tests/test_report_article_cases.py +++ /dev/null @@ -1,130 +0,0 @@ -"""Safety boundaries for article evidence excerpts.""" -import hashlib -from pathlib import Path -import tempfile -import unittest - -from harness.web_fetch.report_article_cases import ( - CASE_TABS, _dynamic_example, _link, _list_example, _read_output, - _resource_example, _text_example, render_article_cases, -) - - -class ArticleCaseTests(unittest.TestCase): - def test_excerpt_rejects_changed_artifact_bytes(self): - with tempfile.TemporaryDirectory() as temp: - out = Path(temp) - (out / 'actual.txt').write_bytes(b'original') - record = {'attempt_id': 'moli__case', 'artifact_path': 'actual.txt', - 'metrics': {'content_sha256': hashlib.sha256(b'original').hexdigest()}} - self.assertEqual(_read_output(out, record), 'original') - (out / 'actual.txt').write_bytes(b'changed') - with self.assertRaisesRegex(ValueError, 'hash mismatch'): - _read_output(out, record) - - def test_reference_urls_cannot_execute_code(self): - with self.assertRaisesRegex(ValueError, 'HTTP'): - _link('javascript:alert(1)', 'unsafe') - link = _link('https://example.com/?x="', '' - case = {'ground_truth_main_content': text, 'min_content_chars': 1, - 'body_requirements': {'minimum_coverage': .8, 'minimum_floor': .6}} - record = {'metrics': {'fact_results': [], 'content_chars': len(text), 'length_pass': True, - 'body_region_results': [{'coverage': 1.0}], - 'body_required_target_regions': 1, 'body_coverage_pass': True}} - left, right, _, _ = _text_example(case, record, text) - self.assertIn('<script>', left) - self.assertIn('<script>', right) - self.assertNotIn('