@remixmate/cli 0.9.26 → 0.9.28
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +7 -4
- package/README.zh-CN.md +8 -4
- package/dist/handlers/gen-voice.d.ts +11 -1
- package/dist/handlers/gen-voice.js +17 -1
- package/dist/manifest.json +161 -10
- package/package.json +3 -1
- package/skills/gen-script/scripts/gen_script.py +1 -1
- package/skills/gen-voice/SKILL.md +5 -3
- package/skills/gen-voice/skill.json +3 -8
- package/skills/render-video/scripts/remote_renderer_client.py +16 -2
- package/skills/render-video/scripts/render_video.py +118 -28
- package/skills/web-read/SKILL.md +146 -0
- package/skills/web-read/skill.json +131 -0
- package/skills/web-screenshot/scripts/_media_screenshot/__init__.py +2 -0
- package/skills/web-screenshot/scripts/_media_screenshot/js/extract_article.js +373 -0
- package/skills/web-screenshot/scripts/_media_screenshot/reader.py +245 -0
- package/skills/web-screenshot/scripts/_media_screenshot/urlguard.py +90 -0
- package/skills/web-screenshot/scripts/read_page.py +136 -0
|
@@ -1647,6 +1647,23 @@ def render_with_local_cli(render_plan: dict, output_path: str) -> bool:
|
|
|
1647
1647
|
return False
|
|
1648
1648
|
|
|
1649
1649
|
|
|
1650
|
+
def _persist_plan_checkpoint(render_plan: dict, job_id: Optional[int], private_token: str) -> None:
|
|
1651
|
+
"""把当前 RenderPlan 存个档(尽力而为,失败只记日志)。
|
|
1652
|
+
|
|
1653
|
+
存在的唯一理由:远端 taskId 必须在**渲染结束之前**就落库。脚本可能在任何时刻
|
|
1654
|
+
被外层超时 SIGTERM,那一刻没有任何机会写盘;只在流程末尾 save_plan 的话,
|
|
1655
|
+
taskId 永远存不下来,"续跑"就无从谈起。
|
|
1656
|
+
"""
|
|
1657
|
+
if not job_id:
|
|
1658
|
+
return
|
|
1659
|
+
try:
|
|
1660
|
+
import render_job_client # type: ignore
|
|
1661
|
+
render_job_client.save_plan(job_id, json.dumps(render_plan, ensure_ascii=False), private_token)
|
|
1662
|
+
LogPrint(f" 💾 RenderPlan checkpoint saved (jobId={job_id})", file=sys.stderr)
|
|
1663
|
+
except Exception as exc: # noqa: BLE001 — 存档失败不该让渲染本身失败
|
|
1664
|
+
LogPrint(f" ⚠️ RenderPlan checkpoint failed (jobId={job_id}): {exc}", file=sys.stderr)
|
|
1665
|
+
|
|
1666
|
+
|
|
1650
1667
|
def render_with_remote_api(
|
|
1651
1668
|
render_plan: dict,
|
|
1652
1669
|
output_path: str,
|
|
@@ -1656,10 +1673,14 @@ def render_with_remote_api(
|
|
|
1656
1673
|
poll_interval: float = 5.0,
|
|
1657
1674
|
upload_title: Optional[str] = None,
|
|
1658
1675
|
conversation_id: Optional[str] = None,
|
|
1676
|
+
job_id: Optional[int] = None,
|
|
1659
1677
|
) -> bool:
|
|
1660
1678
|
"""Submit render to ab-api /tool/renderVideo and poll until completion.
|
|
1661
1679
|
|
|
1662
1680
|
On success, writes upload.fileUrl into render_plan and skips local MP4 download.
|
|
1681
|
+
|
|
1682
|
+
job_id 用于中途存档(见 _persist_plan_checkpoint):有它才能在被强杀后续跑,
|
|
1683
|
+
没有则退化成旧行为(一次性,杀了就得重渲)。
|
|
1663
1684
|
"""
|
|
1664
1685
|
import remote_renderer_client
|
|
1665
1686
|
|
|
@@ -1706,6 +1727,13 @@ def render_with_remote_api(
|
|
|
1706
1727
|
# 内置模板即使带 sourceOssKey(builtin-template-registry 回填,仅服务
|
|
1707
1728
|
# derive_template 派生),渲染仍走 ab-render 预构建的 /render,绝不走 /renderDraft,
|
|
1708
1729
|
# 否则会用 OSS 上的旧快照 + DraftMainVideo 复刻渲染,与生产 MainVideo 漂移。
|
|
1730
|
+
# 下面这套 payload 构造在"续跑既有任务"时其实用不上(见后面的 resuming 分支),
|
|
1731
|
+
# 所以它的失败不该让续跑也跟着失败 —— 任务还在后端好好渲着。
|
|
1732
|
+
pending_resume = bool(str(render_plan.get("remoteTaskId") or "").strip())
|
|
1733
|
+
# 先给默认值:presign 失败时下面的分支不会走到,续跑分支仍要读 status_path。
|
|
1734
|
+
render_path = "/render"
|
|
1735
|
+
status_path = "/renderStatus"
|
|
1736
|
+
|
|
1709
1737
|
source_oss_key = None
|
|
1710
1738
|
template_id = render_plan.get("templateId", "")
|
|
1711
1739
|
if template_id:
|
|
@@ -1719,20 +1747,27 @@ def render_with_remote_api(
|
|
|
1719
1747
|
|
|
1720
1748
|
if source_oss_key:
|
|
1721
1749
|
import render_job_client # type: ignore
|
|
1750
|
+
tarball_url = None
|
|
1722
1751
|
try:
|
|
1723
1752
|
src = render_job_client.presign_template_source(template_id, private_token)
|
|
1724
1753
|
tarball_url = src.get("tarballUrl")
|
|
1725
1754
|
if not tarball_url:
|
|
1726
1755
|
raise RuntimeError(f"presignSource 未返回 tarballUrl: {src}")
|
|
1727
1756
|
except Exception as exc:
|
|
1728
|
-
|
|
1729
|
-
|
|
1730
|
-
|
|
1731
|
-
|
|
1732
|
-
|
|
1733
|
-
|
|
1734
|
-
|
|
1735
|
-
|
|
1757
|
+
if pending_resume:
|
|
1758
|
+
LogPrint(
|
|
1759
|
+
f"⚠️ presignSource failed ({exc}) — 但有待续跑的任务,忽略继续",
|
|
1760
|
+
file=sys.stderr,
|
|
1761
|
+
)
|
|
1762
|
+
else:
|
|
1763
|
+
LogPrint(f"❌ presignSource failed for private template {template_id}: {exc}", file=sys.stderr)
|
|
1764
|
+
render_plan["status"] = "failed"
|
|
1765
|
+
render_plan["errors"].append({
|
|
1766
|
+
"phase": "render",
|
|
1767
|
+
"message": f"presignSource failed: {exc}",
|
|
1768
|
+
"timestamp": now_iso(),
|
|
1769
|
+
})
|
|
1770
|
+
return False
|
|
1736
1771
|
payload = {
|
|
1737
1772
|
"tarballUrl": tarball_url,
|
|
1738
1773
|
"inputProps": input_props,
|
|
@@ -1759,8 +1794,10 @@ def render_with_remote_api(
|
|
|
1759
1794
|
status_path = "/renderStatus"
|
|
1760
1795
|
|
|
1761
1796
|
total_frames = config.get("totalFrames", 0)
|
|
1762
|
-
|
|
1763
|
-
|
|
1797
|
+
if pending_resume:
|
|
1798
|
+
# 续跑:下面那堆"正在提交"的日志会误导人,跳过
|
|
1799
|
+
LogPrint(f"🎬 Remote render task already exists, will resume polling", file=sys.stderr)
|
|
1800
|
+
elif source_oss_key:
|
|
1764
1801
|
LogPrint(f" Private template (OSS dynamic bundle): {template_id}", file=sys.stderr)
|
|
1765
1802
|
else:
|
|
1766
1803
|
LogPrint(f" Composition: {composition_id}", file=sys.stderr)
|
|
@@ -1770,28 +1807,54 @@ def render_with_remote_api(
|
|
|
1770
1807
|
LogPrint(f" total frames: {total_frames}", file=sys.stderr)
|
|
1771
1808
|
sys.stderr.flush()
|
|
1772
1809
|
|
|
1773
|
-
|
|
1774
|
-
|
|
1775
|
-
|
|
1776
|
-
|
|
1777
|
-
|
|
1778
|
-
|
|
1810
|
+
# ─── 续跑既有任务 ────────────────────────────────────────────────────
|
|
1811
|
+
# 上一次调用可能是被外层超时 SIGTERM 掉的(skill 执行有上限),而远端任务还在
|
|
1812
|
+
# ab-render 上跑着。这时重新提交 = 白烧一遍渲染、并且大概率再次撞上同一个上限。
|
|
1813
|
+
# 所以先看 plan 里有没有上次留下的 taskId:有就直接续着轮询。
|
|
1814
|
+
#
|
|
1815
|
+
# 只在"任务不可用"(已失败 / 查不到)时才退回重新提交 —— 轮询超时不算不可用,
|
|
1816
|
+
# 那条路走 RemoteRenderTimeout,保留 taskId 交给下一次调用。
|
|
1817
|
+
task_id = str(render_plan.get("remoteTaskId") or "").strip()
|
|
1818
|
+
if task_id:
|
|
1819
|
+
# 提交时用的状态端点存在 plan 里(内置模板 /renderStatus,私有模板
|
|
1820
|
+
# /renderDraftStatus)。用存的那个,别依赖这次重新推导的结果。
|
|
1821
|
+
status_path = render_plan.get("remoteStatusPath") or status_path
|
|
1822
|
+
LogPrint(f"🔄 resuming existing remote task: taskId={task_id}", file=sys.stderr)
|
|
1823
|
+
render_plan["logs"].append({
|
|
1779
1824
|
"phase": "render",
|
|
1780
|
-
"message": f"remote
|
|
1825
|
+
"message": f"Resuming remote task: {task_id}",
|
|
1781
1826
|
"timestamp": now_iso(),
|
|
1782
1827
|
})
|
|
1783
|
-
|
|
1828
|
+
else:
|
|
1829
|
+
try:
|
|
1830
|
+
task_id = remote_renderer_client.start_render(payload, private_token=private_token, conversation_id=conversation_id, path=render_path)
|
|
1831
|
+
except Exception as exc:
|
|
1832
|
+
LogPrint(f"❌ remote render submission failed: {exc}", file=sys.stderr)
|
|
1833
|
+
render_plan["status"] = "failed"
|
|
1834
|
+
render_plan["errors"].append({
|
|
1835
|
+
"phase": "render",
|
|
1836
|
+
"message": f"remote start_render failed: {exc}",
|
|
1837
|
+
"timestamp": now_iso(),
|
|
1838
|
+
})
|
|
1839
|
+
return False
|
|
1784
1840
|
|
|
1785
|
-
|
|
1786
|
-
|
|
1787
|
-
|
|
1788
|
-
"
|
|
1789
|
-
"
|
|
1790
|
-
|
|
1791
|
-
|
|
1841
|
+
LogPrint(f" ✅ task submitted, taskId={task_id}", file=sys.stderr)
|
|
1842
|
+
render_plan["remoteTaskId"] = task_id
|
|
1843
|
+
render_plan["remoteStatusPath"] = status_path
|
|
1844
|
+
render_plan["remoteStartedAt"] = time.time()
|
|
1845
|
+
render_plan["logs"].append({
|
|
1846
|
+
"phase": "render",
|
|
1847
|
+
"message": f"Remote task submitted: {task_id}",
|
|
1848
|
+
"timestamp": now_iso(),
|
|
1849
|
+
})
|
|
1850
|
+
# **立刻落库**,不等渲染结束。被 SIGTERM 的那一刻脚本没有任何机会写盘,
|
|
1851
|
+
# 所以 taskId 必须在这里就进 DB,否则下一次调用无从续起(这正是旧代码
|
|
1852
|
+
# 只写内存、`remoteTaskId` 全程没人读的后果)。
|
|
1853
|
+
_persist_plan_checkpoint(render_plan, job_id, private_token)
|
|
1792
1854
|
|
|
1793
1855
|
last_progress = -1.0
|
|
1794
|
-
|
|
1856
|
+
# 续跑时用提交时刻算 elapsed,否则 ETA 会按"刚开始"估,一上来就偏小。
|
|
1857
|
+
render_start_time = float(render_plan.get("remoteStartedAt") or time.time())
|
|
1795
1858
|
|
|
1796
1859
|
def _on_progress(status_data: dict):
|
|
1797
1860
|
nonlocal last_progress
|
|
@@ -1834,9 +1897,32 @@ def render_with_remote_api(
|
|
|
1834
1897
|
adaptive_interval=True,
|
|
1835
1898
|
status_path=status_path,
|
|
1836
1899
|
)
|
|
1900
|
+
except remote_renderer_client.RemoteRenderTimeout as exc:
|
|
1901
|
+
# 等不动了,但任务**没失败**。保留 taskId,让下一次调用接着轮询;
|
|
1902
|
+
# 状态不写 failed(写了模型会当成"渲染失败"而重新走一遍完整流程)。
|
|
1903
|
+
LogPrint(f"⏳ {exc}", file=sys.stderr)
|
|
1904
|
+
render_plan["status"] = "rendering"
|
|
1905
|
+
_persist_plan_checkpoint(render_plan, job_id, private_token)
|
|
1906
|
+
render_plan["logs"].append({
|
|
1907
|
+
"phase": "render",
|
|
1908
|
+
"message": f"poll timeout, task still running: {task_id}",
|
|
1909
|
+
"timestamp": now_iso(),
|
|
1910
|
+
})
|
|
1911
|
+
print(
|
|
1912
|
+
f"\n⏳ 远端渲染仍在进行(taskId={task_id})。"
|
|
1913
|
+
f"用同一个 job_id 再调一次 render_video 即可续上,不会重新渲染。",
|
|
1914
|
+
flush=True,
|
|
1915
|
+
)
|
|
1916
|
+
return False
|
|
1837
1917
|
except Exception as exc:
|
|
1838
1918
|
LogPrint(f"❌ remote render polling failed: {exc}", file=sys.stderr)
|
|
1839
1919
|
render_plan["status"] = "failed"
|
|
1920
|
+
# 任务确实不可用了(失败 / 查不到)。清掉 taskId,否则下一次调用会一直
|
|
1921
|
+
# 去续一个永远好不了的任务,再也提交不了新的。
|
|
1922
|
+
render_plan.pop("remoteTaskId", None)
|
|
1923
|
+
render_plan.pop("remoteStatusPath", None)
|
|
1924
|
+
render_plan.pop("remoteStartedAt", None)
|
|
1925
|
+
_persist_plan_checkpoint(render_plan, job_id, private_token)
|
|
1840
1926
|
render_plan["errors"].append({
|
|
1841
1927
|
"phase": "render",
|
|
1842
1928
|
"message": f"remote poll_render failed: {exc}",
|
|
@@ -2030,8 +2116,11 @@ Examples:
|
|
|
2030
2116
|
parser.add_argument(
|
|
2031
2117
|
"--remote-poll-timeout",
|
|
2032
2118
|
type=float,
|
|
2033
|
-
default=float(os.environ.get("REMOTION_REMOTE_POLL_TIMEOUT", "
|
|
2034
|
-
|
|
2119
|
+
default=float(os.environ.get("REMOTION_REMOTE_POLL_TIMEOUT", "2700")),
|
|
2120
|
+
# 实测最长的一支约 30 分钟,加上 ab-render 排队要留余量,所以是 45 分钟。
|
|
2121
|
+
# 这个值必须小于 ab-agent 侧 render_video 的 skill 超时(默认 50 分钟),
|
|
2122
|
+
# 否则外层先 SIGTERM,这里这套"超时但保留 taskId"的续跑逻辑根本轮不到执行。
|
|
2123
|
+
help="Remote-render polling timeout (seconds, default 2700)",
|
|
2035
2124
|
)
|
|
2036
2125
|
parser.add_argument(
|
|
2037
2126
|
"--remote-poll-interval",
|
|
@@ -2405,6 +2494,7 @@ Examples:
|
|
|
2405
2494
|
poll_interval=args.remote_poll_interval,
|
|
2406
2495
|
upload_title=upload_title_hint,
|
|
2407
2496
|
conversation_id=conversation_id,
|
|
2497
|
+
job_id=args.job_id if args.save_job else None,
|
|
2408
2498
|
)
|
|
2409
2499
|
else:
|
|
2410
2500
|
LogPrint(f"💻 Using local render mode", file=sys.stderr)
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: web-read
|
|
3
|
+
description: |
|
|
4
|
+
Web-page reading skill — open a URL in a headless browser (Playwright Python) and get back the page's **main text**: title, headings, paragraphs, lists, code blocks and tables, as Markdown / plain text / structured JSON.
|
|
5
|
+
Boilerplate (nav, sidebar, comments, ads, footer) is stripped by a Readability-style pass, and JS-rendered pages work because a real browser runs the page first.
|
|
6
|
+
|
|
7
|
+
Text only. For a still screenshot (`.png` / `.jpg`) use web-screenshot / `web_screenshot`; for a recording (`.mp4` / `.webm`) use web-record / `web_record`.
|
|
8
|
+
|
|
9
|
+
Use this skill immediately whenever the user asks for any of:
|
|
10
|
+
- Read this link / what does this page say / summarize this article
|
|
11
|
+
- Fetch page content, extract the article text, get the text of a URL
|
|
12
|
+
- Use a web page as source material for a script, outline, or video
|
|
13
|
+
- Read a README / docs page / changelog / blog post
|
|
14
|
+
- Pull the code samples or the table out of a page
|
|
15
|
+
- Check what is behind a link before acting on it
|
|
16
|
+
|
|
17
|
+
Even when the user does not say "read", any request that needs the *content* of a URL (rather than a picture of it) should route here.
|
|
18
|
+
triggers:
|
|
19
|
+
- Read this link / what does this page say / summarize this article
|
|
20
|
+
- Fetch page content, extract article text, get the text of a URL
|
|
21
|
+
- Use a web page as source material for a script or video
|
|
22
|
+
- Read a README / docs page / changelog / blog post
|
|
23
|
+
- Pull code samples or tables out of a page
|
|
24
|
+
---
|
|
25
|
+
|
|
26
|
+
# Web Read Skill (`web_read`)
|
|
27
|
+
|
|
28
|
+
Turns a URL into text. Entry script **`read_page.py`**, pure Python, prints to **stdout**.
|
|
29
|
+
|
|
30
|
+
> **Text only.** A screenshot is `web_screenshot`, a recording is `web_record`. Those two produce *files*; this one produces *content you can reason about*.
|
|
31
|
+
|
|
32
|
+
**Script location**: this skill has no `scripts/` of its own — it reuses `read_page.py` and the `_media_screenshot/` package from the web-screenshot directory (`skill.json`'s `entry.scriptPath` points relatively at `../web-screenshot/scripts/read_page.py`). Everywhere the commands below say **`<ReadScript>`**, substitute:
|
|
33
|
+
|
|
34
|
+
```
|
|
35
|
+
<SkillDir>/../web-screenshot/scripts/read_page.py
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
> Path convention: read the system-injected `Base directory for this skill: <path>` as `<SkillDir>`. Never hardcode an absolute path.
|
|
39
|
+
|
|
40
|
+
## Prerequisites
|
|
41
|
+
|
|
42
|
+
- **Python 3.9+**
|
|
43
|
+
- **The `playwright` pip package + the chromium engine**: the first run **bootstraps automatically** (`pip install playwright` + `playwright install chromium`).
|
|
44
|
+
|
|
45
|
+
## Basic usage
|
|
46
|
+
|
|
47
|
+
```bash
|
|
48
|
+
python3 <ReadScript> --url "https://example.com/article/123"
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
Prints a Markdown document: an `# title` line, a `site · author · date · url` line, then the body. A one-line extraction diagnostic (`blocks / chars / container`) goes to **stderr**, so piping stdout gives you clean text.
|
|
52
|
+
|
|
53
|
+
## Output formats
|
|
54
|
+
|
|
55
|
+
```bash
|
|
56
|
+
# Markdown (default) — headings, lists, ``` code fences, | tables |
|
|
57
|
+
python3 <ReadScript> --url "https://docs.python.org/3/tutorial/introduction.html"
|
|
58
|
+
|
|
59
|
+
# Plain text — no markup, for TTS or keyword work
|
|
60
|
+
python3 <ReadScript> --url "https://example.com/post" --format text
|
|
61
|
+
|
|
62
|
+
# JSON — typed blocks + metadata, for programmatic consumption
|
|
63
|
+
python3 <ReadScript> --url "https://example.com/post" --format json
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
The JSON shape is `{url, status, metadata{title,byline,siteName,publishedTime,description,lang}, container, charCount, blocks[]}`, where each block is one of `heading` / `paragraph` / `quote` / `code` / `list` / `table` / `image` / `rule`. JSON is **never truncated** (half a JSON document is not a JSON document) — cap it with `--selector` instead.
|
|
67
|
+
|
|
68
|
+
## Length control (read this before pointing it at a long page)
|
|
69
|
+
|
|
70
|
+
`--max-chars` defaults to **20000** and cuts on a block boundary, appending an explicit `[truncated] 已显示 N / M 字符` notice. Nothing is silently dropped.
|
|
71
|
+
|
|
72
|
+
```bash
|
|
73
|
+
# Keep the whole document on disk, read a bounded slice now
|
|
74
|
+
python3 <ReadScript> \
|
|
75
|
+
--url "https://example.com/very-long-guide" \
|
|
76
|
+
--max-chars 8000 \
|
|
77
|
+
--output "./guide.md"
|
|
78
|
+
|
|
79
|
+
# No cap at all
|
|
80
|
+
python3 <ReadScript> --url "https://example.com/post" --max-chars 0
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
## When the automatic extraction misses
|
|
84
|
+
|
|
85
|
+
The container is picked by paragraph-density scoring, which is right on ordinary article/docs/blog pages and can miss on unusual layouts. In order of what to try:
|
|
86
|
+
|
|
87
|
+
```bash
|
|
88
|
+
# 1. Content renders late (SPA): wait for the real element
|
|
89
|
+
python3 <ReadScript> --url "https://app.example.com/doc/1" --wait-for-selector "article.body"
|
|
90
|
+
|
|
91
|
+
# 2. Still short: give it a fixed settle window
|
|
92
|
+
python3 <ReadScript> --url "https://example.com/x" --settle-ms 3000
|
|
93
|
+
|
|
94
|
+
# 3. Wrong part of the page: name the container yourself
|
|
95
|
+
python3 <ReadScript> --url "https://example.com/x" --selector "#main-content"
|
|
96
|
+
|
|
97
|
+
# 4. Behind a login
|
|
98
|
+
python3 <ReadScript> --url "https://example.com/x" --storage-state "./auth.json"
|
|
99
|
+
python3 <ReadScript> --url "https://example.com/x" --cookies '[{"name":"sid","value":"…","domain":"example.com","path":"/"}]'
|
|
100
|
+
|
|
101
|
+
# 5. Site serves headless browsers a stub page
|
|
102
|
+
python3 <ReadScript> --url "https://example.com/x" --user-agent "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0 Safari/537.36"
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
The stderr diagnostics tell you which case you are in: `⚠️ 整页有 N 字符,却只抽到 M` means the wrong container was chosen (→ `--selector`), while `⚠️ 这个页面几乎没有文本` means the page itself never rendered text (→ waiting, cookies, or user-agent).
|
|
106
|
+
|
|
107
|
+
## Links and images
|
|
108
|
+
|
|
109
|
+
Both are dropped by default, because they are noise for a summarize/rewrite task and they inflate the character budget.
|
|
110
|
+
|
|
111
|
+
```bash
|
|
112
|
+
# Keep links as [text](url) — when you need to follow them
|
|
113
|
+
python3 <ReadScript> --url "https://example.com/index" --include-links
|
|
114
|
+
|
|
115
|
+
# Keep images as  — when harvesting illustration URLs
|
|
116
|
+
python3 <ReadScript> --url "https://example.com/post" --include-images
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
## Private addresses are refused
|
|
120
|
+
|
|
121
|
+
`web_read` hands page text to a model, so by default it refuses URLs that resolve to private / loopback / link-local addresses (`localhost`, `10.*`, `169.254.169.254`, …). Reading an intranet page or a local dev server on purpose:
|
|
122
|
+
|
|
123
|
+
```bash
|
|
124
|
+
WEB_CAPTURE_ALLOW_PRIVATE_HOSTS=1 python3 <ReadScript> --url "http://localhost:5173/"
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
## Full flag list
|
|
128
|
+
|
|
129
|
+
| Flag | Meaning |
|
|
130
|
+
|---|---|
|
|
131
|
+
| `-u, --url` | Target URL (required, http/https) |
|
|
132
|
+
| `--format` | `markdown` (default) / `text` / `json` |
|
|
133
|
+
| `--max-chars` | stdout cap, block-aligned (default 20000, `0` = unlimited) |
|
|
134
|
+
| `--selector` | Extract only inside this CSS selector |
|
|
135
|
+
| `--include-links` / `--include-images` | Keep `[text](url)` / `` |
|
|
136
|
+
| `-o, --output` | Write the **full** text to a file (stdout stays capped) |
|
|
137
|
+
| `--settle-ms` | Extra wait before extracting |
|
|
138
|
+
| `--quiet` | Suppress the stderr diagnostic line |
|
|
139
|
+
| `-b, --browser` | `chromium` (default) / `firefox` / `webkit` |
|
|
140
|
+
| `--device`, `--viewport`, `--color-scheme`, `--user-agent` | Emulation |
|
|
141
|
+
| `--wait-for-selector`, `--wait-for-timeout`, `--timeout` | Waiting |
|
|
142
|
+
| `--storage-state`, `--cookies`, `--ignore-https-errors` | Session |
|
|
143
|
+
|
|
144
|
+
## Exit codes
|
|
145
|
+
|
|
146
|
+
`0` on success — including a page that legitimately has little text. Non-zero only for a refused URL, an unresolvable selector, or a navigation failure; each prints a single actionable line rather than a Python traceback.
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "web-read",
|
|
3
|
+
"toolName": "web_read",
|
|
4
|
+
"tier": "tool",
|
|
5
|
+
"category": "consuming",
|
|
6
|
+
"title": "Web Page Reader",
|
|
7
|
+
"description": "Open any URL in a headless browser (Playwright Python) and return the page's MAIN TEXT — title, headings, paragraphs, lists, code blocks and tables — as Markdown, plain text, or structured JSON. Boilerplate (nav / sidebar / comments / ads / footer) is stripped by a Readability-style pass, and JS-rendered pages work because a real browser runs the page. This is the tool to use whenever you need to KNOW WHAT A PAGE SAYS: summarizing an article, pulling source material for a script, reading a README or docs page, checking what a link contains. It returns text, not pictures — for a screenshot (png/jpg) use web_screenshot, for a recording (mp4/webm) use web_record. Output is capped by max_chars (default 20000) and truncated on a block boundary; pass an `output` path to keep the full text on disk.",
|
|
8
|
+
"auth": "none",
|
|
9
|
+
"envVars": [
|
|
10
|
+
"WEB_CAPTURE_BROWSER",
|
|
11
|
+
"WEB_CAPTURE_ALLOW_PRIVATE_HOSTS",
|
|
12
|
+
"PLAYWRIGHT_BROWSERS_PATH"
|
|
13
|
+
],
|
|
14
|
+
"entry": {
|
|
15
|
+
"type": "python",
|
|
16
|
+
"scriptPath": "../web-screenshot/scripts/read_page.py"
|
|
17
|
+
},
|
|
18
|
+
"parameters": {
|
|
19
|
+
"type": "object",
|
|
20
|
+
"properties": {
|
|
21
|
+
"url": {
|
|
22
|
+
"type": "string",
|
|
23
|
+
"description": "Target page URL (http/https). Private / loopback / link-local addresses are refused unless WEB_CAPTURE_ALLOW_PRIVATE_HOSTS=1."
|
|
24
|
+
},
|
|
25
|
+
"format": {
|
|
26
|
+
"type": "string",
|
|
27
|
+
"enum": ["markdown", "text", "json"],
|
|
28
|
+
"description": "markdown (default: keeps headings, lists, code fences, tables) | text (plain) | json (structured blocks + metadata, not truncated)"
|
|
29
|
+
},
|
|
30
|
+
"max_chars": {
|
|
31
|
+
"type": "number",
|
|
32
|
+
"description": "Cap on the printed text, cut at a block boundary with an explicit [truncated] notice (default 20000, 0 = unlimited). Raise it when you need the whole document; a very long page will otherwise fill your context."
|
|
33
|
+
},
|
|
34
|
+
"selector": {
|
|
35
|
+
"type": "string",
|
|
36
|
+
"description": "Read only inside this CSS selector. Leave empty to auto-detect the article container — only reach for this when the auto-detected container was wrong."
|
|
37
|
+
},
|
|
38
|
+
"include_links": {
|
|
39
|
+
"type": "boolean",
|
|
40
|
+
"description": "Keep hyperlinks as [text](url) instead of plain text. Useful when you need to follow links from the page."
|
|
41
|
+
},
|
|
42
|
+
"include_images": {
|
|
43
|
+
"type": "boolean",
|
|
44
|
+
"description": "Keep images as . Useful for harvesting illustration URLs out of an article."
|
|
45
|
+
},
|
|
46
|
+
"output": {
|
|
47
|
+
"type": "string",
|
|
48
|
+
"description": "Also write the FULL (untruncated) text to this local path. stdout still respects max_chars — use this when a long page must be kept for later steps."
|
|
49
|
+
},
|
|
50
|
+
"settle_ms": {
|
|
51
|
+
"type": "number",
|
|
52
|
+
"description": "Extra wait before extracting, in ms. Raise for pages that render content late."
|
|
53
|
+
},
|
|
54
|
+
"wait_for_selector": {
|
|
55
|
+
"type": "string",
|
|
56
|
+
"description": "Wait for this CSS selector before extracting (the reliable fix for JS-rendered content)"
|
|
57
|
+
},
|
|
58
|
+
"wait_for_timeout": {
|
|
59
|
+
"type": "number",
|
|
60
|
+
"description": "Fixed wait before extracting, in ms"
|
|
61
|
+
},
|
|
62
|
+
"device": {
|
|
63
|
+
"type": "string",
|
|
64
|
+
"description": "Device emulation name, e.g. 'iPhone 15 Pro' — some sites serve a leaner page to mobile"
|
|
65
|
+
},
|
|
66
|
+
"viewport": {
|
|
67
|
+
"type": "string",
|
|
68
|
+
"description": "Viewport as 'width,height', e.g. '1280,800'"
|
|
69
|
+
},
|
|
70
|
+
"color_scheme": {
|
|
71
|
+
"type": "string",
|
|
72
|
+
"enum": ["light", "dark", "no-preference"],
|
|
73
|
+
"description": "Emulate prefers-color-scheme"
|
|
74
|
+
},
|
|
75
|
+
"user_agent": {
|
|
76
|
+
"type": "string",
|
|
77
|
+
"description": "Override the User-Agent (try this when a site blocks headless browsers)"
|
|
78
|
+
},
|
|
79
|
+
"timeout": {
|
|
80
|
+
"type": "number",
|
|
81
|
+
"description": "Global Playwright action timeout in ms"
|
|
82
|
+
},
|
|
83
|
+
"ignore_https_errors": {
|
|
84
|
+
"type": "boolean",
|
|
85
|
+
"description": "Ignore HTTPS certificate errors"
|
|
86
|
+
},
|
|
87
|
+
"storage_state": {
|
|
88
|
+
"type": "string",
|
|
89
|
+
"description": "Path to a Playwright storageState JSON file (logged-in session)"
|
|
90
|
+
},
|
|
91
|
+
"cookies": {
|
|
92
|
+
"type": "string",
|
|
93
|
+
"description": "Playwright cookies as a JSON string or a path to a JSON file (top level is an array)"
|
|
94
|
+
},
|
|
95
|
+
"browser": {
|
|
96
|
+
"type": "string",
|
|
97
|
+
"enum": ["chromium", "firefox", "webkit"],
|
|
98
|
+
"description": "Browser engine (default chromium)"
|
|
99
|
+
},
|
|
100
|
+
"quiet": {
|
|
101
|
+
"type": "boolean",
|
|
102
|
+
"description": "Suppress the extraction diagnostics on stderr"
|
|
103
|
+
}
|
|
104
|
+
},
|
|
105
|
+
"required": ["url"]
|
|
106
|
+
},
|
|
107
|
+
"ui": {
|
|
108
|
+
"primary": ["url", "format", "max_chars"],
|
|
109
|
+
"advanced": [
|
|
110
|
+
"selector",
|
|
111
|
+
"include_links",
|
|
112
|
+
"include_images",
|
|
113
|
+
"wait_for_selector",
|
|
114
|
+
"settle_ms",
|
|
115
|
+
"device",
|
|
116
|
+
"viewport",
|
|
117
|
+
"color_scheme",
|
|
118
|
+
"timeout"
|
|
119
|
+
],
|
|
120
|
+
"hidden": [
|
|
121
|
+
"output",
|
|
122
|
+
"quiet",
|
|
123
|
+
"user_agent",
|
|
124
|
+
"ignore_https_errors",
|
|
125
|
+
"storage_state",
|
|
126
|
+
"cookies",
|
|
127
|
+
"browser",
|
|
128
|
+
"wait_for_timeout"
|
|
129
|
+
]
|
|
130
|
+
}
|
|
131
|
+
}
|
|
@@ -6,12 +6,14 @@ package is implementation detail.
|
|
|
6
6
|
from __future__ import annotations
|
|
7
7
|
|
|
8
8
|
from . import cli_args, scenes, template, trim
|
|
9
|
+
from .reader import do_read
|
|
9
10
|
from .recording import do_record
|
|
10
11
|
from .screenshot import do_screenshot
|
|
11
12
|
from .storyboard import do_storyboard
|
|
12
13
|
|
|
13
14
|
__all__ = [
|
|
14
15
|
"cli_args",
|
|
16
|
+
"do_read",
|
|
15
17
|
"do_record",
|
|
16
18
|
"do_screenshot",
|
|
17
19
|
"do_storyboard",
|