@remixmate/cli 0.9.68 → 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.68",
4
- "generatedAt": "2026-10-05T12:22:45.024Z",
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.68",
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",
@@ -117,14 +117,28 @@ python3 <SkillDir>/scripts/render_video.py --job-id 42 --upload-title "My video"
117
117
  After rendering, the script auto-uploads by default; `render-manifest.json` then contains an
118
118
  `upload.fileUrl` field, and `upload.playbackUrl` (transcoded) once VOD processing finishes.
119
119
 
120
- **Do not retype the video URL into your reply.** On success the script prints a structured
121
- `__render_video_asset__` line; the host reads the authoritative URL from there and renders a
122
- player for the user. Tell the user the video is ready — the player appears on its own.
123
-
124
- Reproducing a 32-char opaque URL from memory is unreliable: on 2026-08-30 a single character
125
- was dropped (`…c1c20102` → `…c1c2012`), so the user got a 404 while the file sat fine on the
126
- CDN. The URL in your prose is redundant with the structured asset and is the only copy that
127
- can be wrong — the UI now renders it as plain, non-clickable text for exactly this reason.
120
+ On success the script prints these lines on stdout:
121
+
122
+ ```
123
+ 🎬 Video playback URL: <videoUrl>
124
+ 🖼️ Cover image URL: <coverUrl> # only when the template ships a cover
125
+ __render_video_asset__ {"url": "...", "coverUrl": "...", "durationSec": ...}
126
+ ```
127
+
128
+ How to report the result depends on the host:
129
+
130
+ - **Host with a built-in player** (the RemixMate web app; the env has `REMIXMATE_HOST=workbuddy`):
131
+ do **not** retype the URLs. The host reads `__render_video_asset__` and renders the player and
132
+ cover itself. Just tell the user the video is ready.
133
+ - **Any other host** (Codex, Claude Code, other CLIs): nothing renders a player, so the links in
134
+ your reply are the only way the user can get to the result. You **must** output both:
135
+ - the video link (from `🎬 Video playback URL`)
136
+ - the cover link (from `🖼️ Cover image URL`, or `coverUrl` in `__render_video_asset__`).
137
+ If neither is printed, say the template has no cover rather than leaving it out silently.
138
+
139
+ Copy each URL **verbatim from the stdout line**, character for character. Never retype one
140
+ from memory: on 2026-08-30 a single character was dropped (`…c1c20102` → `…c1c2012`), and the
141
+ user got a 404 while the file sat fine on the CDN.
128
142
 
129
143
  ### Render but skip the upload
130
144
 
@@ -3059,6 +3059,10 @@ Examples:
3059
3059
  effective_playback = playback_url or remote_url
3060
3060
  if effective_playback and not effective_playback.startswith("vod://"):
3061
3061
  print(f"\n🎬 Video playback URL: {effective_playback}")
3062
+ # 封面也走 stdout:Codex / Claude Code 等没有内联播放器的宿主只能从
3063
+ # 这里拿到封面地址(之前只打到 stderr,模型回复里因此只有视频没有封面)。
3064
+ if upload_info.get("coverUrl"):
3065
+ print(f"🖼️ Cover image URL: {upload_info['coverUrl']}")
3062
3066
 
3063
3067
  # 结构化资产标记 —— 宿主(ab-agent)据此把权威 URL 作为 attachment
3064
3068
  # 下发前端渲染播放器,绕开模型正文。
@@ -3113,6 +3117,9 @@ Examples:
3113
3117
  # ab-api 保存 manifest 时按 fileId 给成片建语义卡 / 片段 / 血缘
3114
3118
  "fileId": upload_result.get("fileId"),
3115
3119
  }
3120
+ # 本地渲染 + 上传成功时同样把地址放到 stdout,供无播放器的宿主回显给用户
3121
+ if upload_result.get("fileUrl"):
3122
+ print(f"\n🎬 Video playback URL: {upload_result['fileUrl']}")
3116
3123
  if manifest_path:
3117
3124
  save_json(manifest, manifest_path)
3118
3125
  LogPrint(f" Manifest updated: {manifest_path}", file=sys.stderr)
@@ -2,6 +2,6 @@
2
2
  "skillName": "render-video",
3
3
  "repoName": "agent-skill-media-maker",
4
4
  "skillId": "473",
5
- "version": "V35",
5
+ "version": "V36",
6
6
  "skillDescription": "Final-render skill: loads a persisted RenderPlan by `job_id` and drives the Remotion engine to produce the final video.\n\nUse this skill as soon as the user mentions any of these intents (after assets are already prepared):\n- Render the video, composite the video, export the video\n- Turn the prepared assets into the final clip\n- Render with Remotion\n\nPrerequisite: assets must already be generated via `prepare_video_assets`. This skill never resolves or regenerates assets — pass it a `job_id` from a previous `prepare_video_assets` call.\n\n⚠️ Stop-and-confirm gate: never call this skill until the user has explicitly confirmed the assets prepared by `prepare_video_assets`. If those assets were prepared in the current turn and the user has not replied since, stop and ask instead of rendering."
7
7
  }
@@ -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
  }