@remixmate/cli 0.9.69 → 0.9.70

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.
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "schemaVersion": 1,
3
- "version": "0.9.69",
4
- "generatedAt": "2026-10-05T15:20:28.516Z",
3
+ "version": "0.9.70",
4
+ "generatedAt": "2026-10-06T15:32:49.285Z",
5
5
  "skills": [
6
6
  {
7
7
  "id": "doc-parse",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@remixmate/cli",
3
- "version": "0.9.69",
3
+ "version": "0.9.70",
4
4
  "description": "AI media generation skills for Claude Code / Codex — 12 skills covering image, video, voice, digital human, web screenshot, web recording, script, template registry, rendering, Jianying export, and video deconstruction.",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -110,6 +110,8 @@ python3 <ReadScript> --url "https://example.com/x" --user-agent "Mozilla/5.0 (Ma
110
110
 
111
111
  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).
112
112
 
113
+ Slow or unreachable sub-resources do not fail the read: images / fonts / media / stylesheets are never fetched, every other sub-resource is cut off after 8s, and extraction runs on whatever DOM has loaded (stderr says `[web_read] N 个子资源…已跳过`). A non-zero `打开页面失败` therefore means the server never returned the page itself — retrying the same URL with a bigger timeout, another user-agent, device or browser will not help; ask for the text or a different URL.
114
+
113
115
  ## Links and images
114
116
 
115
117
  Both are dropped by default, because they are noise for a summarize/rewrite task and they inflate the character budget.
@@ -21,11 +21,15 @@ def _ensure_playwright_pkg() -> None:
21
21
  _pip_install("playwright")
22
22
 
23
23
 
24
- def ensure_chromium() -> None:
25
- """Install the chromium browser via `playwright install`."""
26
- print("[bootstrap] playwright install chromium ...", file=sys.stderr)
24
+ def ensure_browser(name: str = "chromium") -> None:
25
+ """Install the given browser engine via `playwright install <name>`.
26
+
27
+ Must install the engine that is about to be launched: installing chromium
28
+ and then relaunching firefox fails the same way a second time.
29
+ """
30
+ print(f"[bootstrap] playwright install {name} ...", file=sys.stderr)
27
31
  subprocess.check_call(
28
- [sys.executable, "-m", "playwright", "install", "chromium"]
32
+ [sys.executable, "-m", "playwright", "install", name]
29
33
  )
30
34
 
31
35
 
@@ -4,7 +4,7 @@ from __future__ import annotations
4
4
  from typing import Any
5
5
 
6
6
  from . import js_loader
7
- from .bootstrap import ensure_chromium
7
+ from .bootstrap import ensure_browser
8
8
 
9
9
 
10
10
  def pick_browser_type(p, name: str | None):
@@ -42,15 +42,35 @@ def build_context_options(p, cfg: dict, record_video_dir: str | None) -> dict:
42
42
  return opts
43
43
 
44
44
 
45
+ def _is_missing_executable(e: Exception) -> bool:
46
+ msg = str(e)
47
+ return "Executable doesn't exist" in msg or "playwright install" in msg.lower()
48
+
49
+
45
50
  def launch_with_browser_install(p, browser_name: str | None, launch_kwargs: dict | None = None):
46
51
  btype = pick_browser_type(p, browser_name)
52
+ engine = btype.name # "chromium" | "firefox" | "webkit"
53
+ try:
54
+ return btype.launch(**(launch_kwargs or {}))
55
+ except Exception as e:
56
+ if not _is_missing_executable(e):
57
+ raise
58
+ # 引擎没装:装「这次要启动的那个」引擎再试一次。装不上 / 装了仍起不来时给一句
59
+ # 能据以行动的话,而不是 30 行 Playwright traceback。
60
+ hint = (
61
+ f"浏览器引擎 {engine} 在当前环境不可用。"
62
+ + ("去掉 browser 参数改用默认的 chromium 即可。" if engine != "chromium"
63
+ else "请运行 `python3 -m playwright install chromium`。")
64
+ )
65
+ try:
66
+ ensure_browser(engine)
67
+ except Exception as e:
68
+ raise SystemExit(f"{hint}\n(自动安装失败:{type(e).__name__}: {str(e).splitlines()[0] if str(e) else ''})")
47
69
  try:
48
70
  return btype.launch(**(launch_kwargs or {}))
49
71
  except Exception as e:
50
- msg = str(e)
51
- if "Executable doesn't exist" in msg or "playwright install" in msg.lower():
52
- ensure_chromium()
53
- return btype.launch(**(launch_kwargs or {}))
72
+ if _is_missing_executable(e):
73
+ raise SystemExit(hint)
54
74
  raise
55
75
 
56
76
 
@@ -12,12 +12,74 @@ disk so nothing is actually lost.
12
12
  from __future__ import annotations
13
13
 
14
14
  import json
15
+ import sys
15
16
  from pathlib import Path
17
+ from urllib.parse import urlsplit
16
18
 
17
19
  from . import js_loader
18
20
  from .bootstrap import ensure_runtime
19
21
  from .browser import apply_pre_action_waits, build_context_options, launch_with_browser_install
20
22
 
23
+ # 读正文只需要 HTML 和(SPA 场景下)脚本 / 接口;图片、字体、媒体、样式表一律不拉。
24
+ # 抽取逻辑只读 DOM 文本与属性(img 的 src 照样在),不依赖布局。
25
+ _SKIPPED_RESOURCE_TYPES = {"image", "media", "font", "stylesheet"}
26
+ # 单个子资源的拉取上限。浏览器要等 defer 脚本 / 阻塞样式表下载完才触发
27
+ # DOMContentLoaded,一个从本机出口挂死的 CDN 就能让整页永远「打不开」
28
+ # (2026-10-06:新加坡出口读腾讯云社区文章,_next chunk 与广州 COS 资源全部挂起)。
29
+ # 超过上限的请求被 abort,浏览器跳过它继续解析。
30
+ _SUBRESOURCE_TIMEOUT_MS = 8000
31
+ # 拿到主文档响应头之后,最多再等这么久的 DOMContentLoaded;到点不报错,按当前 DOM 抽取。
32
+ _DOM_READY_TIMEOUT_MS = 15000
33
+
34
+
35
+ class _SubresourceGate:
36
+ """page.route 处理器:丢弃不需要的资源,给其余子资源加单请求超时。"""
37
+
38
+ def __init__(self) -> None:
39
+ self.stalled: list[str] = []
40
+
41
+ def __call__(self, route) -> None:
42
+ req = route.request
43
+ if req.is_navigation_request():
44
+ route.continue_()
45
+ return
46
+ if req.resource_type in _SKIPPED_RESOURCE_TYPES:
47
+ route.abort()
48
+ return
49
+ try:
50
+ resp = route.fetch(timeout=_SUBRESOURCE_TIMEOUT_MS)
51
+ except Exception:
52
+ self.stalled.append(urlsplit(req.url).hostname or "?")
53
+ try:
54
+ route.abort()
55
+ except Exception:
56
+ pass # 页面已关闭 / 已导航走
57
+ return
58
+ try:
59
+ route.fulfill(response=resp)
60
+ except Exception:
61
+ pass
62
+
63
+ def summary(self) -> str:
64
+ hosts = sorted(set(self.stalled))
65
+ shown = ", ".join(hosts[:5]) + (" …" if len(hosts) > 5 else "")
66
+ return f"{len(self.stalled)} 个子资源超过 {_SUBRESOURCE_TIMEOUT_MS // 1000}s 未返回已跳过({shown})"
67
+
68
+
69
+ def _navigation_error(url: str, e: Exception) -> SystemExit:
70
+ # 一条模型能据以行动的错误,胜过 30 行 Playwright traceback。
71
+ # 走到这里说明连主文档的响应头都没拿到(子资源挂死已由 _SubresourceGate 兜住),
72
+ # 问题在网络层或对方服务器;换参数重试几乎不可能有不同结果,要明确告诉调用方别再试。
73
+ first = str(e).splitlines()[0] if str(e) else ""
74
+ kind = "超时未响应" if type(e).__name__ == "TimeoutError" else "连接失败"
75
+ return SystemExit(
76
+ f"打开页面失败({kind}):{url}\n"
77
+ f"{type(e).__name__}: {first}\n"
78
+ "服务器没有返回页面:多半是从当前网络出口无法访问该站点,或站点拒绝了本机 IP。"
79
+ "加大 timeout、换 user_agent / device / browser 通常不会有不同结果,不要对同一 URL 反复重试;"
80
+ "请让用户直接粘贴正文,或换一个可访问的链接。"
81
+ )
82
+
21
83
 
22
84
  def extract(cfg: dict) -> dict:
23
85
  """Load cfg['url'] and return the raw extraction dict from the page."""
@@ -38,19 +100,24 @@ def extract(cfg: dict) -> dict:
38
100
  if cfg.get("timeout"):
39
101
  context.set_default_timeout(cfg["timeout"])
40
102
  page = context.new_page()
103
+ gate = _SubresourceGate()
104
+ page.route("**/*", gate)
41
105
  try:
42
106
  try:
43
- response = page.goto(cfg["url"], wait_until="domcontentloaded")
107
+ # commit = 拿到主文档响应头即返回,不让任何子资源决定「页面打不打得开」。
108
+ response = page.goto(cfg["url"], wait_until="commit")
44
109
  except Exception as e:
45
- # 一条模型能据以行动的错误,胜过 30 行 Playwright traceback。
46
- # 导航失败是这个 skill 最常见的失败形态(打不开 / 超时 / DNS),
47
- # 让它读起来像"这个网址打不开",而不是像程序崩了。
48
- raise SystemExit(
49
- f"打开页面失败:{cfg['url']}\n"
50
- f"{type(e).__name__}: {str(e).splitlines()[0]}\n"
51
- "可能是网络不可达、站点屏蔽了无头浏览器,或加载超过了超时时间。"
52
- "可尝试加大 --timeout、换 --user-agent,或确认该 URL 在本机能打开。"
53
- )
110
+ raise _navigation_error(cfg["url"], e)
111
+ dom_ready = True
112
+ try:
113
+ page.wait_for_load_state("domcontentloaded", timeout=_DOM_READY_TIMEOUT_MS)
114
+ except Exception:
115
+ dom_ready = False
116
+ if gate.stalled or not dom_ready:
117
+ note = gate.summary() if gate.stalled else "部分子资源未返回"
118
+ if not dom_ready:
119
+ note += f";页面 {_DOM_READY_TIMEOUT_MS // 1000}s 内未完成解析,按当前已加载的 DOM 抽取"
120
+ print(f"[web_read] {note}", file=sys.stderr)
54
121
  status = response.status if response else None
55
122
  if status is not None and status >= 400:
56
123
  # Keep going: many sites serve real content under a 403/404
@@ -2,6 +2,6 @@
2
2
  "skillName": "web-screenshot",
3
3
  "repoName": "agent-skill-media-maker",
4
4
  "skillId": "555",
5
- "version": "V7",
5
+ "version": "V8",
6
6
  "skillDescription": "Web-page screenshot skill (still images only, png/jpg), built on Playwright Python — captures any URL to a local image file.\nSupports: full-page / viewport / element / region capture, device emulation, waiting for dynamic content, hiding or masking distracting elements, and static annotations (highlight box / arrow / caption / numbered sequence / redact / code-line highlight).\n\nImages only (`.png` / `.jpg`). For video (`.webm` / screen recording / storyboard clips) use web-record / `web_record`.\n\nUse this skill immediately whenever the user asks for any of:\n- Web page screenshot, screen capture, full-page screenshot, long screenshot\n- Capture a specific element / region, partial screenshot, focus on an area\n- Hide or mask elements before capturing (cookie banner, sensitive information)\n- Annotate a still screenshot (highlight box / arrow / label / caption bar / numbering)\n- Highlight lines L5-L20 of a file and screenshot it\n- Wait for an element to appear, then capture\n- Mobile / device-emulated screenshot (iPhone / Pixel, etc.)\n- Capture with cookies / a logged-in session / storageState\n\nEven when the user does not say \"playwright\", any request to screenshot a URL and get a local image file should route here."
7
7
  }