@remixmate/cli 0.9.71 → 0.9.73

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.
Files changed (181) hide show
  1. package/LICENSE +45 -21
  2. package/README.md +1 -1
  3. package/README.zh-CN.md +1 -1
  4. package/dist/argv.js +1 -51
  5. package/dist/auth/auth-lock.js +1 -100
  6. package/dist/auth/auto-login.js +7 -75
  7. package/dist/auth/commands.js +40 -290
  8. package/dist/auth/credential-store.js +1 -143
  9. package/dist/auth/device-flow-runner.js +13 -110
  10. package/dist/auth/device-flow.js +1 -132
  11. package/dist/auth/ensure.js +1 -62
  12. package/dist/auth/environment.js +1 -69
  13. package/dist/auth/pending-store.js +1 -45
  14. package/dist/auth/resolve.js +1 -44
  15. package/dist/billing.js +3 -76
  16. package/dist/capabilities.js +1 -75
  17. package/dist/cli.js +39 -211
  18. package/dist/diagnostics.js +2 -70
  19. package/dist/doctor.js +9 -188
  20. package/dist/errors.js +1 -33
  21. package/dist/exec.js +4 -47
  22. package/dist/handlers/gen-digital-human.js +33 -264
  23. package/dist/handlers/gen-image.js +9 -113
  24. package/dist/handlers/gen-post.js +15 -188
  25. package/dist/handlers/gen-video.js +7 -185
  26. package/dist/handlers/gen-voice.js +19 -133
  27. package/dist/handlers/index.js +1 -20
  28. package/dist/handlers/shared.js +1 -66
  29. package/dist/handlers/template-init.js +1 -21
  30. package/dist/http.js +3 -155
  31. package/dist/index.js +1 -8
  32. package/dist/install.js +40 -448
  33. package/dist/manifest.json +53 -13
  34. package/dist/progress.js +2 -15
  35. package/dist/project/commands.js +34 -156
  36. package/dist/project/host.js +1 -151
  37. package/dist/project/resolve.js +1 -65
  38. package/dist/project/store.js +2 -97
  39. package/dist/project/take.js +1 -106
  40. package/dist/project/trace.js +5 -278
  41. package/dist/registry.js +3 -70
  42. package/dist/runner.js +7 -225
  43. package/dist/skill-schema.js +1 -194
  44. package/dist/template/args.js +1 -69
  45. package/dist/template/check.js +2 -30
  46. package/dist/template/commands.js +34 -163
  47. package/dist/template/constants.js +1 -46
  48. package/dist/template/init.js +5 -133
  49. package/dist/template/list.js +1 -46
  50. package/dist/template/preview.js +7 -318
  51. package/dist/template/proc.js +1 -43
  52. package/dist/template/project.js +6 -161
  53. package/dist/template/publish.js +5 -185
  54. package/dist/template/render-client.js +2 -79
  55. package/dist/template/upgrade.js +4 -123
  56. package/dist/text.js +1 -15
  57. package/package.json +2 -35
  58. package/skills/doc-parse/scripts/extract_via_render.py +32 -32
  59. package/skills/export-jianying/scripts/gen_jianying_draft.py +188 -188
  60. package/skills/gen-post/SKILL.md +23 -13
  61. package/skills/gen-post/skill.json +9 -10
  62. package/skills/gen-script/SKILL.md +23 -1
  63. package/skills/gen-script/scripts/gen_script.py +644 -551
  64. package/skills/gen-script/skill.json +12 -0
  65. package/skills/gen-script/version.json +1 -1
  66. package/skills/prepare-video-assets/scripts/prepare_video_assets.py +21 -21
  67. package/skills/render-video/scripts/_chrome_vendor.py +17 -17
  68. package/skills/render-video/scripts/_digital_human_segments.py +34 -34
  69. package/skills/render-video/scripts/_video_probe.py +33 -33
  70. package/skills/render-video/scripts/_vod_polling.py +13 -13
  71. package/skills/render-video/scripts/remote_renderer_client.py +42 -42
  72. package/skills/render-video/scripts/render_video.py +639 -639
  73. package/skills/render-video/scripts/required_props.py +37 -37
  74. package/skills/render-video/scripts/upload_video.py +35 -35
  75. package/skills/render-video/version.json +1 -1
  76. package/skills/template-registry/SKILL.md +16 -0
  77. package/skills/template-registry/scripts/countdown_spec.py +92 -92
  78. package/skills/template-registry/scripts/doc_extract.py +40 -40
  79. package/skills/template-registry/scripts/list_templates.py +303 -191
  80. package/skills/template-registry/scripts/log_diagnostics.py +1 -1
  81. package/skills/template-registry/scripts/match_template.py +87 -87
  82. package/skills/template-registry/scripts/payload_hydrator.py +89 -89
  83. package/skills/template-registry/scripts/registry_loader.py +159 -159
  84. package/skills/template-registry/scripts/render_job_client.py +75 -75
  85. package/skills/template-registry/scripts/showcase_loader.py +44 -44
  86. package/skills/template-registry/skill.json +4 -1
  87. package/skills/template-registry/version.json +1 -1
  88. package/skills/template-registry/video_dsl/runtime/__init__.py +8 -8
  89. package/skills/template-registry/video_dsl/runtime/dsl_validator.py +329 -329
  90. package/skills/template-registry/video_dsl/runtime/narration_budget.py +36 -36
  91. package/skills/template-registry/video_dsl/runtime/payload_contract.py +7 -7
  92. package/skills/template-registry/video_dsl/runtime/prompt_enhancer.py +98 -98
  93. package/skills/template-registry/video_dsl/runtime/timeline_compiler.py +127 -127
  94. package/skills/template-registry/video_dsl/schema/video-dsl-v1alpha1.json +14 -1
  95. package/skills/video-parser/scripts/analyze_video.py +173 -173
  96. package/skills/video-parser/scripts/deconstruct_video.py +91 -91
  97. package/skills/video-parser/scripts/parse_via_render.py +21 -21
  98. package/skills/web-screenshot/scripts/_media_screenshot/__init__.py +4 -4
  99. package/skills/web-screenshot/scripts/_media_screenshot/bootstrap.py +7 -7
  100. package/skills/web-screenshot/scripts/_media_screenshot/browser.py +4 -4
  101. package/skills/web-screenshot/scripts/_media_screenshot/cli_args.py +4 -4
  102. package/skills/web-screenshot/scripts/_media_screenshot/js_loader.py +2 -2
  103. package/skills/web-screenshot/scripts/_media_screenshot/overlay.py +1 -1
  104. package/skills/web-screenshot/scripts/_media_screenshot/reader.py +40 -40
  105. package/skills/web-screenshot/scripts/_media_screenshot/recording.py +11 -11
  106. package/skills/web-screenshot/scripts/_media_screenshot/scenes/__init__.py +8 -8
  107. package/skills/web-screenshot/scripts/_media_screenshot/scenes/arrow.py +2 -2
  108. package/skills/web-screenshot/scripts/_media_screenshot/scenes/caption.py +1 -1
  109. package/skills/web-screenshot/scripts/_media_screenshot/scenes/click.py +1 -1
  110. package/skills/web-screenshot/scripts/_media_screenshot/scenes/code_lines.py +5 -5
  111. package/skills/web-screenshot/scripts/_media_screenshot/scenes/focus.py +1 -1
  112. package/skills/web-screenshot/scripts/_media_screenshot/scenes/highlight.py +3 -3
  113. package/skills/web-screenshot/scripts/_media_screenshot/scenes/hover.py +1 -1
  114. package/skills/web-screenshot/scripts/_media_screenshot/scenes/redact.py +1 -1
  115. package/skills/web-screenshot/scripts/_media_screenshot/scenes/scroll.py +1 -1
  116. package/skills/web-screenshot/scripts/_media_screenshot/scenes/sequence.py +1 -1
  117. package/skills/web-screenshot/scripts/_media_screenshot/scenes/title_card.py +1 -1
  118. package/skills/web-screenshot/scripts/_media_screenshot/scenes/type.py +1 -1
  119. package/skills/web-screenshot/scripts/_media_screenshot/scenes/wait.py +1 -1
  120. package/skills/web-screenshot/scripts/_media_screenshot/scenes/zoom.py +2 -2
  121. package/skills/web-screenshot/scripts/_media_screenshot/screenshot.py +15 -15
  122. package/skills/web-screenshot/scripts/_media_screenshot/storyboard.py +2 -2
  123. package/skills/web-screenshot/scripts/_media_screenshot/template.py +11 -11
  124. package/skills/web-screenshot/scripts/_media_screenshot/trim.py +10 -10
  125. package/skills/web-screenshot/scripts/_media_screenshot/urlguard.py +22 -22
  126. package/skills/web-screenshot/scripts/_media_upload.py +33 -33
  127. package/skills/web-screenshot/scripts/read_page.py +15 -15
  128. package/skills/web-screenshot/scripts/record.py +26 -26
  129. package/skills/web-screenshot/scripts/screenshot.py +5 -5
  130. package/dist/argv.d.ts +0 -15
  131. package/dist/auth/auth-lock.d.ts +0 -26
  132. package/dist/auth/auto-login.d.ts +0 -20
  133. package/dist/auth/commands.d.ts +0 -8
  134. package/dist/auth/credential-store.d.ts +0 -60
  135. package/dist/auth/device-flow-runner.d.ts +0 -58
  136. package/dist/auth/device-flow.d.ts +0 -52
  137. package/dist/auth/ensure.d.ts +0 -55
  138. package/dist/auth/environment.d.ts +0 -38
  139. package/dist/auth/pending-store.d.ts +0 -33
  140. package/dist/auth/resolve.d.ts +0 -30
  141. package/dist/billing.d.ts +0 -44
  142. package/dist/capabilities.d.ts +0 -63
  143. package/dist/cli.d.ts +0 -13
  144. package/dist/diagnostics.d.ts +0 -24
  145. package/dist/doctor.d.ts +0 -22
  146. package/dist/errors.d.ts +0 -29
  147. package/dist/exec.d.ts +0 -18
  148. package/dist/handlers/gen-digital-human.d.ts +0 -17
  149. package/dist/handlers/gen-image.d.ts +0 -20
  150. package/dist/handlers/gen-post.d.ts +0 -21
  151. package/dist/handlers/gen-video.d.ts +0 -19
  152. package/dist/handlers/gen-voice.d.ts +0 -26
  153. package/dist/handlers/index.d.ts +0 -20
  154. package/dist/handlers/shared.d.ts +0 -26
  155. package/dist/handlers/template-init.d.ts +0 -10
  156. package/dist/http.d.ts +0 -72
  157. package/dist/index.d.ts +0 -9
  158. package/dist/install.d.ts +0 -71
  159. package/dist/progress.d.ts +0 -23
  160. package/dist/project/commands.d.ts +0 -19
  161. package/dist/project/host.d.ts +0 -91
  162. package/dist/project/resolve.d.ts +0 -43
  163. package/dist/project/store.d.ts +0 -31
  164. package/dist/project/take.d.ts +0 -50
  165. package/dist/project/trace.d.ts +0 -98
  166. package/dist/registry.d.ts +0 -30
  167. package/dist/runner.d.ts +0 -39
  168. package/dist/skill-schema.d.ts +0 -138
  169. package/dist/template/args.d.ts +0 -25
  170. package/dist/template/check.d.ts +0 -23
  171. package/dist/template/commands.d.ts +0 -25
  172. package/dist/template/constants.d.ts +0 -39
  173. package/dist/template/init.d.ts +0 -28
  174. package/dist/template/list.d.ts +0 -23
  175. package/dist/template/preview.d.ts +0 -60
  176. package/dist/template/proc.d.ts +0 -26
  177. package/dist/template/project.d.ts +0 -47
  178. package/dist/template/publish.d.ts +0 -43
  179. package/dist/template/render-client.d.ts +0 -39
  180. package/dist/template/upgrade.d.ts +0 -44
  181. package/dist/text.d.ts +0 -8
@@ -1,14 +1,14 @@
1
- """Read operation: load a page and extract its main content as text.
2
1
 
3
- The DOM work lives in ``js/extract_article.js`` (one injected pass returning
4
- typed blocks); this module drives the browser, renders those blocks into the
5
- requested format, and enforces the character budget.
6
2
 
7
- Truncation is the part that matters for the caller: a 200k-character page
8
- pasted into a model's context is worse than useless. ``max_chars`` cuts on a
9
- block boundary and says so in-band, and ``--output`` keeps the full text on
10
- disk so nothing is actually lost.
11
- """
3
+
4
+
5
+
6
+
7
+
8
+
9
+
10
+
11
+
12
12
  from __future__ import annotations
13
13
 
14
14
  import json
@@ -20,20 +20,20 @@ from . import js_loader
20
20
  from .bootstrap import ensure_runtime
21
21
  from .browser import apply_pre_action_waits, build_context_options, launch_with_browser_install
22
22
 
23
- # 读正文只需要 HTML 和(SPA 场景下)脚本 / 接口;图片、字体、媒体、样式表一律不拉。
24
- # 抽取逻辑只读 DOM 文本与属性(img 的 src 照样在),不依赖布局。
23
+
24
+
25
25
  _SKIPPED_RESOURCE_TYPES = {"image", "media", "font", "stylesheet"}
26
- # 单个子资源的拉取上限。浏览器要等 defer 脚本 / 阻塞样式表下载完才触发
27
- # DOMContentLoaded,一个从本机出口挂死的 CDN 就能让整页永远「打不开」
28
- # (2026-10-06:新加坡出口读腾讯云社区文章,_next chunk 与广州 COS 资源全部挂起)。
29
- # 超过上限的请求被 abort,浏览器跳过它继续解析。
26
+
27
+
28
+
29
+
30
30
  _SUBRESOURCE_TIMEOUT_MS = 8000
31
- # 拿到主文档响应头之后,最多再等这么久的 DOMContentLoaded;到点不报错,按当前 DOM 抽取。
31
+
32
32
  _DOM_READY_TIMEOUT_MS = 15000
33
33
 
34
34
 
35
35
  class _SubresourceGate:
36
- """page.route 处理器:丢弃不需要的资源,给其余子资源加单请求超时。"""
36
+
37
37
 
38
38
  def __init__(self) -> None:
39
39
  self.stalled: list[str] = []
@@ -53,7 +53,7 @@ class _SubresourceGate:
53
53
  try:
54
54
  route.abort()
55
55
  except Exception:
56
- pass # 页面已关闭 / 已导航走
56
+ pass
57
57
  return
58
58
  try:
59
59
  route.fulfill(response=resp)
@@ -67,9 +67,9 @@ class _SubresourceGate:
67
67
 
68
68
 
69
69
  def _navigation_error(url: str, e: Exception) -> SystemExit:
70
- # 一条模型能据以行动的错误,胜过 30 行 Playwright traceback。
71
- # 走到这里说明连主文档的响应头都没拿到(子资源挂死已由 _SubresourceGate 兜住),
72
- # 问题在网络层或对方服务器;换参数重试几乎不可能有不同结果,要明确告诉调用方别再试。
70
+
71
+
72
+
73
73
  first = str(e).splitlines()[0] if str(e) else ""
74
74
  kind = "超时未响应" if type(e).__name__ == "TimeoutError" else "连接失败"
75
75
  return SystemExit(
@@ -82,7 +82,7 @@ def _navigation_error(url: str, e: Exception) -> SystemExit:
82
82
 
83
83
 
84
84
  def extract(cfg: dict) -> dict:
85
- """Load cfg['url'] and return the raw extraction dict from the page."""
85
+
86
86
  ensure_runtime()
87
87
  from playwright.sync_api import sync_playwright
88
88
 
@@ -104,7 +104,7 @@ def extract(cfg: dict) -> dict:
104
104
  page.route("**/*", gate)
105
105
  try:
106
106
  try:
107
- # commit = 拿到主文档响应头即返回,不让任何子资源决定「页面打不打得开」。
107
+
108
108
  response = page.goto(cfg["url"], wait_until="commit")
109
109
  except Exception as e:
110
110
  raise _navigation_error(cfg["url"], e)
@@ -120,13 +120,13 @@ def extract(cfg: dict) -> dict:
120
120
  print(f"[web_read] {note}", file=sys.stderr)
121
121
  status = response.status if response else None
122
122
  if status is not None and status >= 400:
123
- # Keep going: many sites serve real content under a 403/404
124
- # (paywalls, soft 404s). The status rides along in the output
125
- # so the caller can tell "empty page" from "blocked".
123
+
124
+
125
+
126
126
  pass
127
127
  apply_pre_action_waits(page, cfg)
128
- # SPA 正文往往在 domcontentloaded 之后才注水。networkidle 等不到就
129
- # 算了——静态页本来就不会再有请求,硬等只是白白花掉 20 秒。
128
+
129
+
130
130
  try:
131
131
  page.wait_for_load_state("networkidle", timeout=cfg.get("networkIdleMs", 8000))
132
132
  except Exception:
@@ -145,10 +145,10 @@ def extract(cfg: dict) -> dict:
145
145
  browser.close()
146
146
 
147
147
 
148
- # ── rendering ────────────────────────────────────────────────────────────────
148
+
149
149
 
150
150
  def _render_blocks(blocks: list, fmt: str) -> list[str]:
151
- """One string per block, ready to be joined with a blank line."""
151
+
152
152
  md = fmt == "markdown"
153
153
  out: list[str] = []
154
154
  for block in blocks:
@@ -227,14 +227,14 @@ def _header(data: dict, fmt: str) -> str:
227
227
 
228
228
 
229
229
  def render(data: dict, cfg: dict) -> tuple[str, str, bool]:
230
- """Return (stdout_text, full_text, truncated).
231
230
 
232
- ``full_text`` is always the complete document; ``stdout_text`` is what the
233
- caller should print, i.e. the same thing cut to ``maxChars``.
234
- """
231
+
232
+
233
+
234
+
235
235
  fmt = cfg.get("format", "markdown")
236
236
  blocks = list(data.get("blocks") or [])
237
- # 标题已经在 header 里印过一次;正文第一个块又是同一句时,去掉重复的那个。
237
+
238
238
  title = ((data.get("metadata") or {}).get("title") or "").strip()
239
239
  if blocks and title and blocks[0].get("type") == "heading":
240
240
  first = (blocks[0].get("text") or "").strip().rstrip("¶").strip()
@@ -254,8 +254,8 @@ def render(data: dict, cfg: dict) -> tuple[str, str, bool]:
254
254
  "blocks": data.get("blocks"),
255
255
  }
256
256
  full = json.dumps(payload, ensure_ascii=False, indent=2)
257
- # JSON 不做块级截断:切一半的 JSON 不是 JSON。超预算时只报告,
258
- # 让调用方自己决定是改格式还是配 --output。
257
+
258
+
259
259
  return full, full, False
260
260
 
261
261
  full = f"{_header(data, fmt)}\n\n{body}".strip()
@@ -264,7 +264,7 @@ def render(data: dict, cfg: dict) -> tuple[str, str, bool]:
264
264
  if max_chars <= 0 or len(full) <= max_chars:
265
265
  return full, full, False
266
266
 
267
- # Cut on a block boundary so the tail is a whole paragraph, not half a word.
267
+
268
268
  head = f"{_header(data, fmt)}\n\n"
269
269
  kept: list[str] = []
270
270
  used = len(head)
@@ -274,7 +274,7 @@ def render(data: dict, cfg: dict) -> tuple[str, str, bool]:
274
274
  kept.append(part)
275
275
  used += len(part) + 2
276
276
  if not kept:
277
- # Single oversized block (one giant <pre>, say) — fall back to a hard cut.
277
+
278
278
  kept = [body[: max(0, max_chars - len(head))]]
279
279
  shown = (head + "\n\n".join(kept)).strip()
280
280
  notice = (
@@ -285,7 +285,7 @@ def render(data: dict, cfg: dict) -> tuple[str, str, bool]:
285
285
 
286
286
 
287
287
  def do_read(cfg: dict) -> dict:
288
- """Extract, render, optionally persist. Returns a small result summary."""
288
+
289
289
  data = extract(cfg)
290
290
 
291
291
  if data.get("error") == "selector-not-found":
@@ -1,4 +1,4 @@
1
- """Plain recording: fixed duration / stop-when / scroll-through."""
1
+
2
2
  from __future__ import annotations
3
3
 
4
4
  import sys
@@ -59,11 +59,11 @@ def _wait_for_stop_condition(page, cfg: dict) -> str:
59
59
 
60
60
 
61
61
  def do_record(cfg: dict) -> tuple[Path, int, int]:
62
- """Plain recording — returns (output_path, start_ms, end_ms).
63
62
 
64
- start_ms/end_ms 是相对视频起点(context 创建)的内容窗口:start_ms=页面加载就绪时刻,
65
- end_ms=内容动作结束时刻。调用方据此端锚裁掉开头加载段(skill 侧或委托 ab-render)。
66
- """
63
+
64
+
65
+
66
+
67
67
  ensure_runtime()
68
68
  from playwright.sync_api import sync_playwright
69
69
 
@@ -74,8 +74,8 @@ def do_record(cfg: dict) -> tuple[Path, int, int]:
74
74
  with sync_playwright() as p:
75
75
  browser = launch_with_browser_install(p, cfg.get("browser"))
76
76
  try:
77
- # t0 ≈ 视频开始录制的时刻(context 创建即开录);用于测量加载头/内容窗口,
78
- # 供 trim_video 端锚裁剪。
77
+
78
+
79
79
  t0 = time.time()
80
80
  context = browser.new_context(**build_context_options(p, cfg, out_dir))
81
81
  if cfg.get("timeout"):
@@ -87,8 +87,8 @@ def do_record(cfg: dict) -> tuple[Path, int, int]:
87
87
  try:
88
88
  page.goto(cfg["url"], wait_until="domcontentloaded")
89
89
  apply_pre_action_waits(page, cfg)
90
- # 无条件等页面真正加载完成(networkidle + settled 检测 + settleMs)再开录内容——
91
- # 对所有 URL 生效。加载/白屏段仍会被录进原始视频,随后由 trim_video 端锚裁掉。
90
+
91
+
92
92
  wait_until_settled(page, cfg)
93
93
  start_ms = int((time.time() - t0) * 1000)
94
94
  if cfg.get("scrollThrough"):
@@ -112,6 +112,6 @@ def do_record(cfg: dict) -> tuple[Path, int, int]:
112
112
  src = Path(video_path)
113
113
  if src.resolve() != output:
114
114
  src.replace(output)
115
- # 不在此处裁剪:加载头由调用方在收尾/转码那一步端锚裁掉(见 record.py),
116
- # 避免 skill 侧依赖 ffmpeg 并省去一次重编码。
115
+
116
+
117
117
  return output, start_ms, end_ms
@@ -1,9 +1,9 @@
1
- """Scene registry.
2
1
 
3
- Each scene type registers a handler ``(page, scene_cfg) -> None`` via the
4
- ``@register("type")`` decorator. Import the per-scene modules at the bottom so
5
- their decorators fire at package import time.
6
- """
2
+
3
+
4
+
5
+
6
+
7
7
  from __future__ import annotations
8
8
 
9
9
  from typing import Callable
@@ -28,9 +28,9 @@ def known_types() -> list[str]:
28
28
  return sorted(_REGISTRY.keys())
29
29
 
30
30
 
31
- # Auto-discover every non-underscore module in this package so its
32
- # @register decorators fire at import time. Adding a new scene only
33
- # requires dropping a `scenes/<type>.py` file — no edits here.
31
+
32
+
33
+
34
34
  import importlib as _importlib
35
35
  import pkgutil as _pkgutil
36
36
 
@@ -1,4 +1,4 @@
1
- """Arrow scene: curved arrow from one anchor (selector/coords) to another."""
1
+
2
2
  from __future__ import annotations
3
3
 
4
4
  from .. import js_loader
@@ -7,7 +7,7 @@ from . import register
7
7
 
8
8
 
9
9
  def _resolve_anchor(page, anchor: dict) -> tuple[float, float]:
10
- """Anchor is either {x, y} or {selector, side}. Returns page-fixed (x, y)."""
10
+
11
11
  if anchor is None:
12
12
  raise RuntimeError("arrow: missing anchor (need from/to)")
13
13
  if "x" in anchor and "y" in anchor:
@@ -1,4 +1,4 @@
1
- """Caption scene: subtitle strip sliding in from top or bottom edge."""
1
+
2
2
  from __future__ import annotations
3
3
 
4
4
  from .. import js_loader
@@ -1,4 +1,4 @@
1
- """Click scene: animated cursor flies to target, ripple, then optional real click."""
1
+
2
2
  from __future__ import annotations
3
3
 
4
4
  from .. import js_loader
@@ -1,9 +1,9 @@
1
- """Code-lines scene: highlight a contiguous line range in a code view.
2
1
 
3
- Default pattern targets GitHub's React blob view (``[data-line-number="{n}"]``),
4
- which also covers diff views. Override ``linePattern`` to target the old blob
5
- table (``#LC{n}``) or any other line-keyed element.
6
- """
2
+
3
+
4
+
5
+
6
+
7
7
  from __future__ import annotations
8
8
 
9
9
  from .. import js_loader
@@ -1,4 +1,4 @@
1
- """Focus scene: dim the page with a spotlight cut-out around the target."""
1
+
2
2
  from __future__ import annotations
3
3
 
4
4
  from .. import js_loader
@@ -1,4 +1,4 @@
1
- """Highlight scene: rectangle around a target element (or union of several)."""
1
+
2
2
  from __future__ import annotations
3
3
 
4
4
  from .. import js_loader
@@ -7,8 +7,8 @@ from . import register
7
7
 
8
8
 
9
9
  def _union_box(page, selectors: list[str]) -> dict | None:
10
- """Compute the union bounding box of every matching selector. Skips misses."""
11
- # Make sure the first one is visible / on-screen before measuring.
10
+
11
+
12
12
  resolve_locator(page, selectors[0])
13
13
  boxes: list[dict] = []
14
14
  for sel in selectors:
@@ -1,4 +1,4 @@
1
- """Hover scene: move cursor onto target and hold so tooltips/cards reveal."""
1
+
2
2
  from __future__ import annotations
3
3
 
4
4
  from .. import js_loader
@@ -1,4 +1,4 @@
1
- """Redact scene: blur / pixelate / solid-cover sensitive regions."""
1
+
2
2
  from __future__ import annotations
3
3
 
4
4
  from .. import js_loader
@@ -1,4 +1,4 @@
1
- """Scroll scene: step-scroll for ``duration`` or until ``toSelector`` is hit."""
1
+
2
2
  from __future__ import annotations
3
3
 
4
4
  import time
@@ -1,4 +1,4 @@
1
- """Sequence scene: multiple targets get numbered outlines, revealed in order."""
1
+
2
2
  from __future__ import annotations
3
3
 
4
4
  from .. import js_loader
@@ -1,4 +1,4 @@
1
- """Title card scene: full-screen opener/outro with title + subtitle + accent."""
1
+
2
2
  from __future__ import annotations
3
3
 
4
4
  from .. import js_loader
@@ -1,4 +1,4 @@
1
- """Type scene: move cursor to an input, focus, then typewriter the text."""
1
+
2
2
  from __future__ import annotations
3
3
 
4
4
  from .. import js_loader
@@ -1,4 +1,4 @@
1
- """Wait scene: a simple pause between other scenes."""
1
+
2
2
  from __future__ import annotations
3
3
 
4
4
  from . import register
@@ -1,4 +1,4 @@
1
- """Zoom scene: element-level OR page-level (browser-like) zoom animation."""
1
+
2
2
  from __future__ import annotations
3
3
 
4
4
  from .. import js_loader
@@ -60,7 +60,7 @@ def scene_zoom(page, scene: dict) -> None:
60
60
  target = scene.get("target", "element")
61
61
  locator = resolve_locator(page, scene["selector"])
62
62
  locator.scroll_into_view_if_needed()
63
- # Center in viewport so the element has room to grow.
63
+
64
64
  locator.evaluate(
65
65
  "el => el.scrollIntoView({ block: 'center', inline: 'center', behavior: 'instant' })"
66
66
  )
@@ -1,6 +1,6 @@
1
- """Screenshot operation: full-page / element / region, with optional
2
- hide-before / native mask / drawn annotations.
3
- """
1
+
2
+
3
+
4
4
  from __future__ import annotations
5
5
 
6
6
  import json
@@ -18,7 +18,7 @@ from .overlay import ensure_overlay
18
18
 
19
19
 
20
20
  def _apply_hide(page, selectors: list[str]) -> None:
21
- """display:none every match of every selector (idempotent, scoped to this page)."""
21
+
22
22
  if not selectors:
23
23
  return
24
24
  page.evaluate(
@@ -36,7 +36,7 @@ def _apply_hide(page, selectors: list[str]) -> None:
36
36
 
37
37
 
38
38
  def _apply_annotations(page, annotations: list[dict], settle_ms: int) -> None:
39
- """Reuse scene handlers to draw static overlays before snapshotting."""
39
+
40
40
  if not annotations:
41
41
  return
42
42
  ensure_overlay(page)
@@ -49,8 +49,8 @@ def _apply_annotations(page, annotations: list[dict], settle_ms: int) -> None:
49
49
  file=sys.stderr,
50
50
  )
51
51
  continue
52
- # Scene handlers wait on `duration`; for static annotations we want
53
- # 0-wait so they all stack up before the snapshot fires.
52
+
53
+
54
54
  ann_copy = dict(ann)
55
55
  ann_copy.setdefault("duration", 0)
56
56
  ann_copy.setdefault("holdMs", 0)
@@ -63,7 +63,7 @@ def _apply_annotations(page, annotations: list[dict], settle_ms: int) -> None:
63
63
 
64
64
 
65
65
  def _load_annotations(spec) -> tuple[list[dict], int]:
66
- """Accept either a list, a dict with 'annotations' key, or a path to such."""
66
+
67
67
  if spec is None:
68
68
  return [], 0
69
69
  settle_ms = 900
@@ -93,7 +93,7 @@ def _build_mask_locators(page, selectors: list[str]):
93
93
 
94
94
 
95
95
  def do_screenshot(cfg: dict) -> Path:
96
- """Take a screenshot per cfg. Returns the absolute output path."""
96
+
97
97
  ensure_runtime()
98
98
  from playwright.sync_api import sync_playwright
99
99
  from playwright.sync_api import TimeoutError as PlaywrightTimeoutError
@@ -126,14 +126,14 @@ def do_screenshot(cfg: dict) -> Path:
126
126
 
127
127
  if cfg.get("selector"):
128
128
  locator = page.locator(cfg["selector"]).first
129
- # 先用较短超时确认元素确实存在。否则选择器不匹配时,
130
- # scroll_into_view_if_needed() 会干等全局超时(默认 30s)才抛出
131
- # 一大段难懂的 Playwright traceback、以 exit 1 崩溃。这里快速失败,
132
- # 返回一条模型可读、可据以换选择器的错误信息。
129
+
130
+
131
+
132
+
133
133
  probe_timeout = cfg.get("selectorTimeout")
134
134
  if probe_timeout is None:
135
- # 页面 domcontentloaded 已触发,元素在则几乎立即命中;
136
- # 不在则无需等满 30s。取全局 timeout 与 8s 的较小值。
135
+
136
+
137
137
  probe_timeout = min(cfg.get("timeout") or 8000, 8000)
138
138
  try:
139
139
  locator.wait_for(state="attached", timeout=probe_timeout)
@@ -1,4 +1,4 @@
1
- """Storyboard runner: orchestrate a sequence of scenes with smooth transitions."""
1
+
2
2
  from __future__ import annotations
3
3
 
4
4
  import sys
@@ -45,7 +45,7 @@ def _run_storyboard(page, cfg: dict) -> None:
45
45
 
46
46
 
47
47
  def do_storyboard(cfg: dict) -> tuple[Path, int, int]:
48
- """Run a storyboard recording. Returns (output_path, start_ms, end_ms)."""
48
+
49
49
  ensure_runtime()
50
50
  from playwright.sync_api import sync_playwright
51
51
 
@@ -1,12 +1,12 @@
1
- """Storyboard JSON template engine.
2
-
3
- Templates live in ``skills/media-screenshot/templates/<name>.json``.
4
- Each template is a regular storyboard JSON optionally prefixed with a ``_meta``
5
- block describing the variables it accepts. ``{{var_name}}`` placeholders inside
6
- string values are replaced; when a string consists of a single placeholder its
7
- substituted value is auto-coerced to int / float / bool / null so e.g.
8
- ``"scale": "{{zoom_scale}}"`` becomes a number after rendering.
9
- """
1
+
2
+
3
+
4
+
5
+
6
+
7
+
8
+
9
+
10
10
  from __future__ import annotations
11
11
 
12
12
  import json
@@ -71,7 +71,7 @@ def list_templates() -> list[Path]:
71
71
 
72
72
 
73
73
  def load_template(name: str) -> dict:
74
- """Resolve a template name (or path) to its parsed JSON."""
74
+
75
75
  p = Path(name)
76
76
  if not p.exists():
77
77
  p = _TEMPLATES_DIR / f"{name}.json"
@@ -85,7 +85,7 @@ def load_template(name: str) -> dict:
85
85
 
86
86
 
87
87
  def render_template(name: str, params: dict[str, str]) -> dict:
88
- """Load, validate against ``_meta.params``, render, return the storyboard."""
88
+
89
89
  tpl = load_template(name)
90
90
  meta = tpl.pop("_meta", {})
91
91
  declared = meta.get("params", {}) or {}
@@ -1,11 +1,11 @@
1
- """End-anchored ffmpeg trim for storyboard videos.
2
-
3
- Playwright's recorded video timeline does NOT align 1:1 with Python wall-clock —
4
- recording starts before the first navigation finishes and the file is typically
5
- a few seconds longer than the wall-clock window we measured. The storyboard
6
- ends right before ``context.close()``, which IS the video's tail. So we anchor
7
- on the END.
8
- """
1
+
2
+
3
+
4
+
5
+
6
+
7
+
8
+
9
9
  from __future__ import annotations
10
10
 
11
11
  import os
@@ -48,11 +48,11 @@ def trim_video(out: Path, start_ms: int, end_ms: int) -> None:
48
48
 
49
49
  video_dur = _probe_duration(out)
50
50
  if video_dur is None:
51
- # Fallback: start-anchored (legacy, may be off if recording drifts).
51
+
52
52
  start_s = max(0.0, (start_ms - lead_ms) / 1000.0)
53
53
  duration_s = keep_ms / 1000.0
54
54
  else:
55
- # End-anchored: video tail == storyboard end == context.close.
55
+
56
56
  start_s = max(0.0, video_dur - keep_ms / 1000.0)
57
57
  duration_s = video_dur - start_s
58
58
 
@@ -1,20 +1,20 @@
1
- """Refuse to fetch private / link-local addresses (SSRF guard).
2
-
3
- Only ``read_page.py`` uses this today, and that asymmetry is deliberate:
4
- screenshot and record hand back a *file path*, while web_read hands the page's
5
- **text** straight to a model that is driven by user input. A prompt that says
6
- "read http://169.254.169.254/latest/meta-data/iam/..." is an exfiltration
7
- attempt, not a reading request.
8
-
9
- ``WEB_CAPTURE_ALLOW_PRIVATE_HOSTS=1`` opts out — needed when pointing the skill
10
- at a dev server or an intranet page on purpose.
11
-
12
- Caveat: the check resolves DNS once and then hands the URL to the browser,
13
- which resolves it again. A name that flips between answers (DNS rebinding) can
14
- slip through that gap. Closing it properly needs request-level interception in
15
- Playwright; this guard is aimed at the ordinary case of a hostile or careless
16
- URL, not at an attacker who controls a nameserver.
17
- """
1
+
2
+
3
+
4
+
5
+
6
+
7
+
8
+
9
+
10
+
11
+
12
+
13
+
14
+
15
+
16
+
17
+
18
18
  from __future__ import annotations
19
19
 
20
20
  import ipaddress
@@ -41,7 +41,7 @@ def _is_blocked(ip: ipaddress._BaseAddress) -> bool:
41
41
 
42
42
 
43
43
  def assert_public_url(raw_url: str) -> None:
44
- """Raise SystemExit when *raw_url* is not an ordinary public http(s) page."""
44
+
45
45
  parsed = urlparse(raw_url)
46
46
  if parsed.scheme not in ("http", "https"):
47
47
  raise SystemExit(
@@ -54,7 +54,7 @@ def assert_public_url(raw_url: str) -> None:
54
54
  if allow_private_hosts():
55
55
  return
56
56
 
57
- # A literal IP needs no DNS round-trip.
57
+
58
58
  try:
59
59
  literal = ipaddress.ip_address(host)
60
60
  except ValueError:
@@ -75,9 +75,9 @@ def assert_public_url(raw_url: str) -> None:
75
75
  ip = ipaddress.ip_address(addr)
76
76
  except ValueError:
77
77
  continue
78
- # Any resolved address being internal is enough to refuse — a name that
79
- # answers with one public and one private A record is the exact shape of
80
- # an SSRF attempt.
78
+
79
+
80
+
81
81
  if _is_blocked(ip):
82
82
  raise SystemExit(_refusal(host, addr))
83
83