@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.
- package/dist/manifest.json +2 -2
- package/package.json +1 -1
- package/skills/render-video/SKILL.md +22 -8
- package/skills/render-video/scripts/render_video.py +7 -0
- package/skills/render-video/version.json +1 -1
- package/skills/web-read/SKILL.md +2 -0
- package/skills/web-screenshot/scripts/_media_screenshot/bootstrap.py +8 -4
- package/skills/web-screenshot/scripts/_media_screenshot/browser.py +25 -5
- package/skills/web-screenshot/scripts/_media_screenshot/reader.py +77 -10
- package/skills/web-screenshot/version.json +1 -1
package/dist/manifest.json
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@remixmate/cli",
|
|
3
|
-
"version": "0.9.
|
|
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
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
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": "
|
|
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
|
}
|
package/skills/web-read/SKILL.md
CHANGED
|
@@ -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
|
|
25
|
-
"""Install the
|
|
26
|
-
|
|
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",
|
|
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
|
|
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
|
-
|
|
51
|
-
|
|
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
|
-
|
|
107
|
+
# commit = 拿到主文档响应头即返回,不让任何子资源决定「页面打不打得开」。
|
|
108
|
+
response = page.goto(cfg["url"], wait_until="commit")
|
|
44
109
|
except Exception as e:
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
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": "
|
|
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
|
}
|