@remixmate/cli 0.9.13 → 0.9.15
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 +54 -1
- package/README.zh-CN.md +37 -1
- package/dist/auth/auto-login.js +10 -1
- package/dist/auth/commands.js +181 -21
- package/dist/auth/credential-store.d.ts +16 -0
- package/dist/auth/credential-store.js +17 -0
- package/dist/auth/device-flow-runner.d.ts +15 -0
- package/dist/auth/device-flow-runner.js +60 -12
- package/dist/auth/device-flow.js +24 -7
- package/dist/auth/ensure.d.ts +55 -0
- package/dist/auth/ensure.js +62 -0
- package/dist/auth/environment.d.ts +13 -0
- package/dist/auth/environment.js +21 -0
- package/dist/auth/pending-store.d.ts +33 -0
- package/dist/auth/pending-store.js +45 -0
- package/dist/billing.d.ts +44 -0
- package/dist/billing.js +76 -0
- package/dist/cli.js +75 -8
- package/dist/doctor.d.ts +22 -0
- package/dist/doctor.js +142 -0
- package/dist/errors.d.ts +29 -0
- package/dist/errors.js +33 -0
- package/dist/exec.d.ts +18 -0
- package/dist/exec.js +47 -0
- package/dist/handlers/gen-digital-human.js +1 -0
- package/dist/handlers/gen-image.js +1 -0
- package/dist/handlers/gen-video.js +1 -0
- package/dist/handlers/gen-voice.js +2 -0
- package/dist/handlers/index.d.ts +7 -0
- package/dist/http.d.ts +20 -6
- package/dist/http.js +43 -15
- package/dist/manifest.json +2 -2
- package/dist/registry.d.ts +4 -2
- package/dist/registry.js +2 -1
- package/dist/runner.d.ts +4 -0
- package/dist/runner.js +36 -12
- package/dist/skill-schema.d.ts +19 -0
- package/dist/skill-schema.js +18 -0
- package/dist/text.d.ts +8 -0
- package/dist/text.js +15 -0
- package/package.json +2 -2
- package/skills/export-jianying/skill.json +1 -0
- package/skills/gen-digital-human/SKILL.md +12 -0
- package/skills/gen-digital-human/skill.json +1 -0
- package/skills/gen-image/SKILL.md +12 -0
- package/skills/gen-image/skill.json +1 -0
- package/skills/gen-script/skill.json +1 -0
- package/skills/gen-video/SKILL.md +12 -0
- package/skills/gen-video/skill.json +1 -0
- package/skills/gen-voice/SKILL.md +12 -0
- package/skills/gen-voice/skill.json +1 -0
- package/skills/prepare-video-assets/SKILL.md +12 -0
- package/skills/prepare-video-assets/skill.json +1 -0
- package/skills/render-video/SKILL.md +12 -0
- package/skills/render-video/scripts/render_video.py +63 -0
- package/skills/render-video/skill.json +1 -0
- package/skills/template-registry/scripts/list_templates.py +16 -1
- package/skills/template-registry/scripts/registry_loader.py +63 -63
- package/skills/template-registry/scripts/render_job_client.py +27 -0
- package/skills/template-registry/skill.json +1 -0
- package/skills/video-parser/skill.json +1 -0
- package/skills/web-record/skill.json +1 -0
- package/skills/web-screenshot/skill.json +1 -0
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
"category": "asset",
|
|
6
6
|
"title": "Digital-Human Talking-Head",
|
|
7
7
|
"description": "Digital-human video: list available avatars; produce a talking-head video from text via TTS, or drive an avatar from an existing audio URL.",
|
|
8
|
+
"auth": "required",
|
|
8
9
|
"envVars": ["PRIV_TOKEN", "MM_API_BASE_URL", "AGENT_NAME"],
|
|
9
10
|
"entry": { "type": "http", "handler": "gen-digital-human" },
|
|
10
11
|
"parameters": {
|
|
@@ -122,6 +122,18 @@ remixmate gen-image \
|
|
|
122
122
|
| `--api-base-url` | Override API root | see above |
|
|
123
123
|
| `--priv-token` | Override token | see above |
|
|
124
124
|
|
|
125
|
+
## Credits
|
|
126
|
+
|
|
127
|
+
Every run charges credits. The CLI prints a footer on stdout when it does:
|
|
128
|
+
|
|
129
|
+
```
|
|
130
|
+
💳 Charged 31 credits · balance 1,240
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
Relay it to the user whenever it appears — it is the only signal they get about what a
|
|
134
|
+
generation cost, and the balance is the only warning before a run fails with
|
|
135
|
+
`insufficient_credits`. Do not drop it from your summary.
|
|
136
|
+
|
|
125
137
|
## Error handling
|
|
126
138
|
|
|
127
139
|
- **401** / **token missing** (non-OpenClaw): set `PRIV_TOKEN`.
|
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
"category": "asset",
|
|
6
6
|
"title": "AI Image Generation",
|
|
7
7
|
"description": "AI image generation: produce an image from a text prompt. Supports Seedream and Gemini models, plus image-to-image with reference images.",
|
|
8
|
+
"auth": "required",
|
|
8
9
|
"envVars": ["PRIV_TOKEN", "MM_API_BASE_URL", "AGENT_NAME", "MM_IMAGE_MODEL"],
|
|
9
10
|
"entry": { "type": "http", "handler": "gen-image" },
|
|
10
11
|
"parameters": {
|
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
"category": "authoring",
|
|
6
6
|
"title": "Video Script Generation",
|
|
7
7
|
"description": "Video script generation: turn a topic into a structured Video DSL (JSON) that describes the full video — scene structure, asset requirements, and narrative flow.",
|
|
8
|
+
"auth": "none",
|
|
8
9
|
"envVars": ["DEFAULT_IMAGE_MODEL", "DEFAULT_VIDEO_MODEL", "STUB_IMAGE_URL", "STUB_VIDEO_URL"],
|
|
9
10
|
"scriptPath": "scripts/gen_script.py",
|
|
10
11
|
"parameters": {
|
|
@@ -150,6 +150,18 @@ remixmate gen-video \
|
|
|
150
150
|
- Describe motion explicitly.
|
|
151
151
|
- Example: `"At sunrise, an aerial drone shot of a futuristic city, golden light on glass facades, mist swirling, 4K ultra-clear"`.
|
|
152
152
|
|
|
153
|
+
## Credits
|
|
154
|
+
|
|
155
|
+
Every run charges credits. The CLI prints a footer on stdout when it does:
|
|
156
|
+
|
|
157
|
+
```
|
|
158
|
+
💳 Charged 31 credits · balance 1,240
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
Relay it to the user whenever it appears — it is the only signal they get about what a
|
|
162
|
+
generation cost, and the balance is the only warning before a run fails with
|
|
163
|
+
`insufficient_credits`. Do not drop it from your summary.
|
|
164
|
+
|
|
153
165
|
## Error handling
|
|
154
166
|
|
|
155
167
|
- **401** / **token missing** (non-OpenClaw): set `PRIV_TOKEN`.
|
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
"category": "asset",
|
|
6
6
|
"title": "AI Video Generation",
|
|
7
7
|
"description": "AI video generation: produce a short video clip from a text prompt. Supports Seedance and Veo models, plus first/last frame and reference images.",
|
|
8
|
+
"auth": "required",
|
|
8
9
|
"envVars": ["PRIV_TOKEN", "MM_API_BASE_URL", "AGENT_NAME", "MM_VIDEO_MODEL"],
|
|
9
10
|
"entry": { "type": "http", "handler": "gen-video" },
|
|
10
11
|
"parameters": {
|
|
@@ -95,6 +95,18 @@ remixmate gen-voice --text "<text-to-synthesize>" --json-output
|
|
|
95
95
|
| Voice-over / narration | 1.0–1.2 |
|
|
96
96
|
| Fast announcement | 1.2–1.5 |
|
|
97
97
|
|
|
98
|
+
## Credits
|
|
99
|
+
|
|
100
|
+
Every run charges credits. The CLI prints a footer on stdout when it does:
|
|
101
|
+
|
|
102
|
+
```
|
|
103
|
+
💳 Charged 31 credits · balance 1,240
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
Relay it to the user whenever it appears — it is the only signal they get about what a
|
|
107
|
+
generation cost, and the balance is the only warning before a run fails with
|
|
108
|
+
`insufficient_credits`. Do not drop it from your summary.
|
|
109
|
+
|
|
98
110
|
## Error handling
|
|
99
111
|
|
|
100
112
|
- **401** / **token missing** (non-OpenClaw): set `PRIV_TOKEN`.
|
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
"category": "asset",
|
|
6
6
|
"title": "Text-to-Speech (Minimax)",
|
|
7
7
|
"description": "Text-to-speech (TTS): synthesize narration audio from text via the Minimax TTS model. Returns the persisted audio URL — no download needed.",
|
|
8
|
+
"auth": "required",
|
|
8
9
|
"envVars": ["PRIV_TOKEN", "MM_API_BASE_URL", "AGENT_NAME"],
|
|
9
10
|
"entry": { "type": "http", "handler": "gen-voice" },
|
|
10
11
|
"parameters": {
|
|
@@ -195,6 +195,18 @@ The Asset Resolver handles each `AssetRef` in this order:
|
|
|
195
195
|
4. **Parallel generation**: assets of the same type are generated in parallel; different types are sequenced by dependency.
|
|
196
196
|
5. **Cache reuse**: assets with the same payload are checked against `asset-cache-dir` to avoid duplicate generation.
|
|
197
197
|
|
|
198
|
+
## Credits
|
|
199
|
+
|
|
200
|
+
Every run charges credits. The CLI prints a footer on stdout when it does:
|
|
201
|
+
|
|
202
|
+
```
|
|
203
|
+
💳 Charged 31 credits · balance 1,240
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
Relay it to the user whenever it appears — it is the only signal they get about what a
|
|
207
|
+
generation cost, and the balance is the only warning before a run fails with
|
|
208
|
+
`insufficient_credits`. Do not drop it from your summary.
|
|
209
|
+
|
|
198
210
|
## Error handling
|
|
199
211
|
|
|
200
212
|
- **DSL validation failed**: pre-validate with `gen-script --validate`.
|
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
"category": "authoring",
|
|
6
6
|
"title": "Video Asset Preparation",
|
|
7
7
|
"description": "Resolves and generates every asset (image / audio / video) referenced by a Video DSL, persists a RenderPlan to the database, and returns a job_id for the subsequent render_video call. This is Phase 1 of the two-phase video pipeline; Phase 3 (Remotion render) lives in render_video.",
|
|
8
|
+
"auth": "required",
|
|
8
9
|
"envVars": ["PRIV_TOKEN", "MM_API_BASE_URL", "MM_BACKEND_API_URL", "AGENT_NAME", "REMOTION_RENDER_API_URL", "REMOTION_RENDER_MODE", "REMOTION_OUTPUT_DIR", "ASSET_CACHE_DIR"],
|
|
9
10
|
"scriptPath": "scripts/prepare_video_assets.py",
|
|
10
11
|
"parameters": {
|
|
@@ -210,6 +210,18 @@ The Remotion render project lives at the monorepo root under `<monorepo-root>/re
|
|
|
210
210
|
- Production (default): the client uses `https://api-render.remixmate.com` when `REMOTION_RENDER_API_URL` is unset — no config needed.
|
|
211
211
|
- Local debugging: in the renderer directory run `npm install && npm run server:dev`, then point the client at it with `REMOTION_RENDER_API_URL=http://localhost:3000`.
|
|
212
212
|
|
|
213
|
+
## Credits
|
|
214
|
+
|
|
215
|
+
Every run charges credits. The CLI prints a footer on stdout when it does:
|
|
216
|
+
|
|
217
|
+
```
|
|
218
|
+
💳 Charged 31 credits · balance 1,240
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
Relay it to the user whenever it appears — it is the only signal they get about what a
|
|
222
|
+
generation cost, and the balance is the only warning before a run fails with
|
|
223
|
+
`insufficient_credits`. Do not drop it from your summary.
|
|
224
|
+
|
|
213
225
|
## Error handling
|
|
214
226
|
|
|
215
227
|
- **`job_id` missing or not assets-ready**: the script exits non-zero with a message like "render job N is not in assets-ready state". The agent should call `prepare_video_assets` again with the updated DSL to regenerate, then re-call this skill with the new `job_id`.
|
|
@@ -24,6 +24,7 @@
|
|
|
24
24
|
"""
|
|
25
25
|
|
|
26
26
|
import argparse
|
|
27
|
+
import atexit
|
|
27
28
|
import builtins
|
|
28
29
|
import json
|
|
29
30
|
import os
|
|
@@ -356,6 +357,62 @@ def LogPrint(*args, sep=" ", end="\n", file=None, flush=False):
|
|
|
356
357
|
builtins.print(f"[{stamp}] {message}", end=end, file=file, flush=flush)
|
|
357
358
|
|
|
358
359
|
|
|
360
|
+
# ─── 扣费汇总 ────────────────────────────────────────────────────────────────
|
|
361
|
+
# 一条视频管线的花费分散在多处:每个 gen-image / gen-voice / gen-video /
|
|
362
|
+
# gen-digital-human 子进程各扣一次,渲染再通过 saveManifest 扣一次。子进程是
|
|
363
|
+
# capture_output 起的,它们自己打的 💳 页脚会被吞掉,所以这里把子进程 stdout 里的
|
|
364
|
+
# billing 事件抓出来累加,最后统一报一次总账——否则用户跑完一整条管线,只知道视频
|
|
365
|
+
# 好了,不知道花了多少积分。
|
|
366
|
+
_BILLING: dict = {"credits": 0, "balance": None}
|
|
367
|
+
|
|
368
|
+
|
|
369
|
+
def absorb_child_billing(stdout: str) -> None:
|
|
370
|
+
"""从子 skill 的 stdout 里收集 `__progress__` billing 事件(见 CLI src/billing.ts)。"""
|
|
371
|
+
for line in (stdout or "").splitlines():
|
|
372
|
+
line = line.strip()
|
|
373
|
+
if not line.startswith("{") or "__progress__" not in line:
|
|
374
|
+
continue
|
|
375
|
+
try:
|
|
376
|
+
event = json.loads(line)
|
|
377
|
+
except json.JSONDecodeError:
|
|
378
|
+
continue
|
|
379
|
+
if not isinstance(event, dict) or event.get("phase") != "billing":
|
|
380
|
+
continue
|
|
381
|
+
credits = event.get("credits")
|
|
382
|
+
if not isinstance(credits, (int, float)) or credits <= 0:
|
|
383
|
+
continue
|
|
384
|
+
_BILLING["credits"] += int(credits)
|
|
385
|
+
balance = event.get("balance")
|
|
386
|
+
if isinstance(balance, (int, float)):
|
|
387
|
+
_BILLING["balance"] = int(balance)
|
|
388
|
+
|
|
389
|
+
|
|
390
|
+
def print_billing_footer() -> None:
|
|
391
|
+
"""收尾时报一次总账。通过 atexit 注册,所以 sys.exit / 中途失败也会打印
|
|
392
|
+
——已经发生的扣费不该因为后面某一步失败就不告知用户。
|
|
393
|
+
|
|
394
|
+
走 stdout 而不是 stderr:ab-agent 交给 LLM 的是 `result.stdout || result.stderr`
|
|
395
|
+
(mcp-tools.ts),只写 stderr 在托管 agent 里等于不可见。
|
|
396
|
+
"""
|
|
397
|
+
credits = _BILLING["credits"]
|
|
398
|
+
balance = _BILLING["balance"]
|
|
399
|
+
try:
|
|
400
|
+
import render_job_client # type: ignore
|
|
401
|
+
|
|
402
|
+
job_billing = render_job_client.billing_summary()
|
|
403
|
+
if job_billing.get("credits"):
|
|
404
|
+
credits += int(job_billing["credits"])
|
|
405
|
+
# saveManifest(渲染扣费)在管线里排最后,它带回的余额最新。
|
|
406
|
+
if job_billing.get("balance") is not None:
|
|
407
|
+
balance = int(job_billing["balance"])
|
|
408
|
+
except Exception: # noqa: BLE001 — 报账失败绝不能影响已完成的渲染
|
|
409
|
+
pass
|
|
410
|
+
if credits <= 0:
|
|
411
|
+
return
|
|
412
|
+
suffix = f" · balance {balance:,}" if balance is not None else ""
|
|
413
|
+
builtins.print(f"\n💳 Charged {credits:,} credits{suffix}", flush=True)
|
|
414
|
+
|
|
415
|
+
|
|
359
416
|
def sync_chrome_headless_vendor(renderer_dir: str, render_plan: dict) -> None:
|
|
360
417
|
"""Chrome Headless vendor 同步——实现已抽到独立模块 ``_chrome_vendor``。
|
|
361
418
|
|
|
@@ -758,6 +815,7 @@ def resolve_asset_image(asset: dict, private_token: str, timeout: int) -> dict:
|
|
|
758
815
|
|
|
759
816
|
try:
|
|
760
817
|
result = subprocess.run(cmd, capture_output=True, text=True, timeout=timeout, stdin=subprocess.DEVNULL)
|
|
818
|
+
absorb_child_billing(result.stdout)
|
|
761
819
|
if result.returncode == 0:
|
|
762
820
|
for line in reversed(result.stdout.strip().split("\n")):
|
|
763
821
|
line = line.strip()
|
|
@@ -809,6 +867,7 @@ def resolve_asset_audio(asset: dict, private_token: str, timeout: int) -> dict:
|
|
|
809
867
|
|
|
810
868
|
try:
|
|
811
869
|
result = subprocess.run(cmd, capture_output=True, text=True, timeout=timeout, stdin=subprocess.DEVNULL)
|
|
870
|
+
absorb_child_billing(result.stdout)
|
|
812
871
|
if result.returncode == 0:
|
|
813
872
|
for line in reversed(result.stdout.strip().split("\n")):
|
|
814
873
|
line = line.strip()
|
|
@@ -854,6 +913,7 @@ def resolve_asset_video(asset: dict, private_token: str, timeout: int) -> dict:
|
|
|
854
913
|
|
|
855
914
|
try:
|
|
856
915
|
result = subprocess.run(cmd, capture_output=True, text=True, timeout=timeout, stdin=subprocess.DEVNULL)
|
|
916
|
+
absorb_child_billing(result.stdout)
|
|
857
917
|
if result.returncode == 0:
|
|
858
918
|
for line in reversed(result.stdout.strip().split("\n")):
|
|
859
919
|
line = line.strip()
|
|
@@ -893,6 +953,7 @@ def resolve_asset_digital_human(asset: dict, private_token: str, timeout: int) -
|
|
|
893
953
|
dh_timeout = max(timeout, 660)
|
|
894
954
|
try:
|
|
895
955
|
result = subprocess.run(cmd, capture_output=True, text=True, timeout=dh_timeout, stdin=subprocess.DEVNULL)
|
|
956
|
+
absorb_child_billing(result.stdout)
|
|
896
957
|
if result.returncode == 0:
|
|
897
958
|
for line in reversed(result.stdout.strip().split("\n")):
|
|
898
959
|
line = line.strip()
|
|
@@ -1892,6 +1953,8 @@ def _log_render_plan_summary(render_plan: dict) -> None:
|
|
|
1892
1953
|
|
|
1893
1954
|
|
|
1894
1955
|
def main():
|
|
1956
|
+
# 无论正常结束、sys.exit 还是中途失败,都在最后报一次积分账(无扣费时静默)。
|
|
1957
|
+
atexit.register(print_billing_footer)
|
|
1895
1958
|
parser = argparse.ArgumentParser(
|
|
1896
1959
|
description="Video render tool — DSL + TemplateBinding → Remotion video.",
|
|
1897
1960
|
formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
"category": "authoring",
|
|
6
6
|
"title": "Remotion Video Renderer",
|
|
7
7
|
"description": "Loads a persisted RenderPlan by job_id and drives the Remotion engine to produce the final video. Assets must already be generated via prepare_video_assets — this skill never resolves or regenerates assets.",
|
|
8
|
+
"auth": "required",
|
|
8
9
|
"envVars": ["PRIV_TOKEN", "MM_API_BASE_URL", "MM_BACKEND_API_URL", "AGENT_NAME", "REMOTION_RENDER_API_URL", "REMOTION_RENDER_MODE", "REMOTION_OUTPUT_DIR", "ASSET_CACHE_DIR"],
|
|
9
10
|
"scriptPath": "scripts/render_video.py",
|
|
10
11
|
"parameters": {
|
|
@@ -39,7 +39,13 @@ from pathlib import Path
|
|
|
39
39
|
|
|
40
40
|
# When run as `python3 <skillDir>/scripts/list_templates.py`, this script's own
|
|
41
41
|
# directory is sys.path[0], so the sibling shared modules import directly.
|
|
42
|
-
from registry_loader import
|
|
42
|
+
from registry_loader import ( # noqa: E402
|
|
43
|
+
EXIT_BACKEND_UNREACHABLE,
|
|
44
|
+
EXIT_NOT_AUTHENTICATED,
|
|
45
|
+
RegistryAuthError,
|
|
46
|
+
RegistryUnreachableError,
|
|
47
|
+
list_templates as load_visible_templates,
|
|
48
|
+
)
|
|
43
49
|
|
|
44
50
|
|
|
45
51
|
def _matches(tpl: dict, tag: str | None, aspect: str | None, language: str | None) -> bool:
|
|
@@ -166,6 +172,15 @@ def main() -> None:
|
|
|
166
172
|
|
|
167
173
|
try:
|
|
168
174
|
templates = load_visible_templates()
|
|
175
|
+
except RegistryAuthError as exc:
|
|
176
|
+
# 退出码 4 = 需要授权(见 registry_loader.EXIT_NOT_AUTHENTICATED),宿主
|
|
177
|
+
# 据此引导用户重新登录,而不是把它当成一次普通失败。
|
|
178
|
+
print(f"❌ {exc}", file=sys.stderr)
|
|
179
|
+
sys.exit(EXIT_NOT_AUTHENTICATED)
|
|
180
|
+
except RegistryUnreachableError as exc:
|
|
181
|
+
# 退出码 5 = 后端不可达;重试或修配置即可,不需要重新登录。
|
|
182
|
+
print(f"❌ {exc}", file=sys.stderr)
|
|
183
|
+
sys.exit(EXIT_BACKEND_UNREACHABLE)
|
|
169
184
|
except RuntimeError as exc:
|
|
170
185
|
print(f"❌ {exc}", file=sys.stderr)
|
|
171
186
|
sys.exit(1)
|
|
@@ -19,7 +19,8 @@
|
|
|
19
19
|
兼容 ab-agent 注入的 MM_BACKEND_API_URL。默认值内置在 CLI 自身,独立安装
|
|
20
20
|
(codex / npm i -g)开箱即用,无需宿主在 spawn 时注入。
|
|
21
21
|
|
|
22
|
-
|
|
22
|
+
鉴权:只读 PRIV_TOKEN 环境变量。凭证库 / 钥匙串 / 设备授权流由 CLI 的 Node 侧
|
|
23
|
+
唯一鉴权入口负责(src/auth/ensure.ts),并在 spawn 时注入本进程环境。
|
|
23
24
|
|
|
24
25
|
monorepo / 显式本地文件等其它来源已**移除**。需要离线 / pinned 数据的维护脚本
|
|
25
26
|
(check_contracts.py / sync_registry.py)直接走 template_paths 读 monorepo,
|
|
@@ -47,7 +48,6 @@ import urllib.error
|
|
|
47
48
|
import urllib.request
|
|
48
49
|
from functools import lru_cache
|
|
49
50
|
from typing import Optional
|
|
50
|
-
from urllib.parse import urlparse
|
|
51
51
|
|
|
52
52
|
__all__ = [
|
|
53
53
|
"load_registry_data",
|
|
@@ -55,8 +55,29 @@ __all__ = [
|
|
|
55
55
|
"get_template",
|
|
56
56
|
"list_templates",
|
|
57
57
|
"invalidate_cache",
|
|
58
|
+
"RegistryAuthError",
|
|
59
|
+
"RegistryUnreachableError",
|
|
60
|
+
"EXIT_NOT_AUTHENTICATED",
|
|
61
|
+
"EXIT_BACKEND_UNREACHABLE",
|
|
58
62
|
]
|
|
59
63
|
|
|
64
|
+
# 与 Node 侧 src/errors.ts 的 EXIT 常量保持一致:宿主据此判断「该引导用户授权」
|
|
65
|
+
# 还是「后端故障,等一会儿再试」,而不是把两者都当成一次普通失败。
|
|
66
|
+
EXIT_NOT_AUTHENTICATED = 4
|
|
67
|
+
EXIT_BACKEND_UNREACHABLE = 5
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
class RegistryAuthError(RuntimeError):
|
|
71
|
+
"""凭证缺失 / 失效 / 被吊销 —— 与「后端不可达」区分开的独立失败模式。
|
|
72
|
+
|
|
73
|
+
这两种情况以前被压成同一段「请确认 ab-api 可达…并已设置有效 PRIV_TOKEN」的
|
|
74
|
+
文案,用户无从判断该去登录还是该等后端恢复。
|
|
75
|
+
"""
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
class RegistryUnreachableError(RuntimeError):
|
|
79
|
+
"""网络不通 / 超时 / 5xx —— 用户无需重新登录,重试或修配置即可。"""
|
|
80
|
+
|
|
60
81
|
# 默认缓存 TTL(秒)。HTTP 拉取后该时间内不再重新请求。
|
|
61
82
|
_DEFAULT_TTL_SECONDS = 300
|
|
62
83
|
|
|
@@ -127,58 +148,20 @@ def _write_cached(cache_file: str, payload: dict) -> None:
|
|
|
127
148
|
raise
|
|
128
149
|
|
|
129
150
|
|
|
130
|
-
# ── PrivToken
|
|
151
|
+
# ── PrivToken 解析 ────────────────────────────────────────────────────────
|
|
131
152
|
#
|
|
132
|
-
#
|
|
133
|
-
#
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
def _origin(url: str) -> tuple:
|
|
141
|
-
p = urlparse(url)
|
|
142
|
-
return (p.scheme, p.hostname, p.port)
|
|
143
|
-
|
|
153
|
+
# 只读 PRIV_TOKEN 环境变量 —— 凭证库 / 系统钥匙串 / 设备授权流全部由 Node 侧的
|
|
154
|
+
# 唯一鉴权入口(src/auth/ensure.ts)负责,并在 spawn 时把解析结果注入本进程的
|
|
155
|
+
# 环境变量(src/runner.ts)。
|
|
156
|
+
#
|
|
157
|
+
# 这里曾经自己读 ~/.config/remixmate/credentials.json,带来两个问题:一是与 Node
|
|
158
|
+
# 侧的解析逻辑各写一份、只有本 skill 享受得到;二是「只有一条凭证就直接用」的兜底
|
|
159
|
+
# 会把某个后端(如本地开发环境)的 token 发往另一个后端(如生产),属于跨源泄露。
|
|
144
160
|
|
|
145
|
-
def _priv_token_from_credential_store(url: str) -> str:
|
|
146
|
-
"""从 ~/.config/remixmate/credentials.json 取与 url 同源的 PrivToken。
|
|
147
161
|
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
"""
|
|
151
|
-
try:
|
|
152
|
-
with open(_credentials_file_path(), encoding="utf-8") as f:
|
|
153
|
-
doc = json.load(f)
|
|
154
|
-
except (OSError, json.JSONDecodeError):
|
|
155
|
-
return ""
|
|
156
|
-
creds = doc.get("credentials") if isinstance(doc, dict) else None
|
|
157
|
-
if not isinstance(creds, dict) or not creds:
|
|
158
|
-
return ""
|
|
159
|
-
|
|
160
|
-
target = _origin(url)
|
|
161
|
-
for base_url, entry in creds.items():
|
|
162
|
-
if not isinstance(entry, dict) or entry.get("secret") == "keychain":
|
|
163
|
-
continue
|
|
164
|
-
if _origin(base_url) == target:
|
|
165
|
-
tok = str(entry.get("privToken") or "").strip()
|
|
166
|
-
if tok:
|
|
167
|
-
return tok
|
|
168
|
-
# 单条目兜底:CLI 通常只登录一个后端。
|
|
169
|
-
if len(creds) == 1:
|
|
170
|
-
entry = next(iter(creds.values()))
|
|
171
|
-
if isinstance(entry, dict) and entry.get("secret") != "keychain":
|
|
172
|
-
return str(entry.get("privToken") or "").strip()
|
|
173
|
-
return ""
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
def _resolve_priv_token(url: str) -> str:
|
|
177
|
-
"""PrivToken 解析:环境变量优先,空则回退凭证库(与 CLI 一致的优先级)。"""
|
|
178
|
-
env = os.environ.get("PRIV_TOKEN", "").strip()
|
|
179
|
-
if env:
|
|
180
|
-
return env
|
|
181
|
-
return _priv_token_from_credential_store(url)
|
|
162
|
+
def _resolve_priv_token(_url: str) -> str:
|
|
163
|
+
"""PrivToken 解析:只认由 CLI 注入 / 宿主设置的 PRIV_TOKEN 环境变量。"""
|
|
164
|
+
return os.environ.get("PRIV_TOKEN", "").strip()
|
|
182
165
|
|
|
183
166
|
|
|
184
167
|
def _fetch_http(url: str, timeout: int) -> dict:
|
|
@@ -214,9 +197,13 @@ def _fetch_http(url: str, timeout: int) -> dict:
|
|
|
214
197
|
|
|
215
198
|
# ab-api 风格:{code, msg, data} → 取 data;其他场景视作直接 registry
|
|
216
199
|
if isinstance(parsed, dict) and "data" in parsed and "code" in parsed:
|
|
217
|
-
|
|
200
|
+
code = parsed.get("code")
|
|
201
|
+
if str(code) in ("401", "403"):
|
|
202
|
+
# ab-api 用 HTTP 200 + code=401 表达鉴权失败,所以这里也要判。
|
|
203
|
+
raise RegistryAuthError(str(parsed.get("msg") or f"code={code}"))
|
|
204
|
+
if code not in (0, "0"):
|
|
218
205
|
raise RuntimeError(
|
|
219
|
-
f"registry HTTP 业务失败: code={
|
|
206
|
+
f"registry HTTP 业务失败: code={code} msg={parsed.get('msg')}"
|
|
220
207
|
)
|
|
221
208
|
data = parsed["data"]
|
|
222
209
|
else:
|
|
@@ -233,6 +220,8 @@ def _fetch_http(url: str, timeout: int) -> dict:
|
|
|
233
220
|
cached["fetchedAt"] = time.time()
|
|
234
221
|
_write_cached(cache_file, cached)
|
|
235
222
|
return cached["data"]
|
|
223
|
+
if e.code in (401, 403):
|
|
224
|
+
raise RegistryAuthError(f"HTTP {e.code}") from e
|
|
236
225
|
raise
|
|
237
226
|
|
|
238
227
|
|
|
@@ -249,6 +238,13 @@ def _load_from_url(url: str, ttl: int, timeout: int) -> dict:
|
|
|
249
238
|
|
|
250
239
|
try:
|
|
251
240
|
return _fetch_http(url, timeout)
|
|
241
|
+
except RegistryAuthError as exc:
|
|
242
|
+
# 鉴权失败不降级到缓存:缓存可能属于另一个账号,静默返回它会把「凭证已失效」
|
|
243
|
+
# 掩盖成一份看似正常、实则不属于当前用户的模板列表。
|
|
244
|
+
raise RegistryAuthError(
|
|
245
|
+
f"凭证无效或已过期({exc})。运行 remixmate login 重新授权,"
|
|
246
|
+
f"或设置有效的 PRIV_TOKEN。当前后端:{url}"
|
|
247
|
+
) from exc
|
|
252
248
|
except Exception as exc:
|
|
253
249
|
# HTTP 故障时降级:有磁盘缓存就继续用(同一数据源的容错,不是第二个源)
|
|
254
250
|
if cached:
|
|
@@ -257,10 +253,9 @@ def _load_from_url(url: str, ttl: int, timeout: int) -> dict:
|
|
|
257
253
|
file=sys.stderr,
|
|
258
254
|
)
|
|
259
255
|
return cached["data"]
|
|
260
|
-
raise
|
|
261
|
-
f"registry
|
|
262
|
-
f" 请确认 ab-api 可达、VIDEO_TEMPLATE_REGISTRY_URL / MM_API_BASE_URL"
|
|
263
|
-
f" 配置正确,并已设置有效 PRIV_TOKEN。"
|
|
256
|
+
raise RegistryUnreachableError(
|
|
257
|
+
f"registry 后端不可达且无可用缓存: {url} ({exc})。"
|
|
258
|
+
f" 请确认 ab-api 可达、VIDEO_TEMPLATE_REGISTRY_URL / MM_API_BASE_URL 配置正确。"
|
|
264
259
|
) from exc
|
|
265
260
|
|
|
266
261
|
|
|
@@ -275,16 +270,21 @@ def load_registry_data(
|
|
|
275
270
|
未设置则按 ``MM_API_BASE_URL`` 推导(见 ``_default_registry_url``)。
|
|
276
271
|
带磁盘缓存(TTL + ETag/304),返回 ``data.get("templates", [])`` 形状的 dict。
|
|
277
272
|
"""
|
|
273
|
+
# 这两行是实现细节(endpoint 从哪来),对终端用户没有意义,却出现在每一次
|
|
274
|
+
# 成功调用的 stderr 上。默认收起,排障时用 REMIXMATE_DEBUG=1 打开。
|
|
275
|
+
debug = os.environ.get("REMIXMATE_DEBUG", "").strip() == "1"
|
|
278
276
|
url = os.environ.get("VIDEO_TEMPLATE_REGISTRY_URL", "").strip()
|
|
279
277
|
if url:
|
|
280
|
-
|
|
278
|
+
if debug:
|
|
279
|
+
print(f"[registry_loader] using HTTP registry: {url}", file=sys.stderr)
|
|
281
280
|
else:
|
|
282
281
|
url = _default_registry_url()
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
282
|
+
if debug:
|
|
283
|
+
print(
|
|
284
|
+
f"[registry_loader] VIDEO_TEMPLATE_REGISTRY_URL unset; using derived"
|
|
285
|
+
f" default URL: {url}",
|
|
286
|
+
file=sys.stderr,
|
|
287
|
+
)
|
|
288
288
|
return _load_from_url(url, ttl=ttl_seconds, timeout=http_timeout)
|
|
289
289
|
|
|
290
290
|
|
|
@@ -37,6 +37,31 @@ def _make_headers(priv_token: str) -> dict:
|
|
|
37
37
|
return headers
|
|
38
38
|
|
|
39
39
|
|
|
40
|
+
# ─── 扣费回传 ────────────────────────────────────────────────────────────────
|
|
41
|
+
# ab-api 会在扣了积分的成功响应上挂 billing 字段(见 core.Success)。渲染积分是在
|
|
42
|
+
# /renderJob/saveManifest 里扣的,所以这里是渲染花费唯一的可见入口——累加起来交给
|
|
43
|
+
# render_video.py 在收尾时统一告知用户。
|
|
44
|
+
_BILLING: dict = {"credits": 0, "balance": None}
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def _record_billing(result: dict) -> None:
|
|
48
|
+
billing = result.get("billing")
|
|
49
|
+
if not isinstance(billing, dict):
|
|
50
|
+
return
|
|
51
|
+
credits = billing.get("credits")
|
|
52
|
+
if not isinstance(credits, (int, float)) or credits <= 0:
|
|
53
|
+
return
|
|
54
|
+
_BILLING["credits"] += int(credits)
|
|
55
|
+
balance = billing.get("balance")
|
|
56
|
+
if isinstance(balance, (int, float)):
|
|
57
|
+
_BILLING["balance"] = int(balance)
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def billing_summary() -> dict:
|
|
61
|
+
"""本进程通过本模块产生的累计扣费:{"credits": int, "balance": int|None}。"""
|
|
62
|
+
return dict(_BILLING)
|
|
63
|
+
|
|
64
|
+
|
|
40
65
|
def _post(path: str, payload: dict, priv_token: str, timeout: int = 30) -> dict:
|
|
41
66
|
"""发送 POST 请求并返回响应 data 字段;失败时抛出 RuntimeError。"""
|
|
42
67
|
url = f"{_api_base()}{path}"
|
|
@@ -50,6 +75,8 @@ def _post(path: str, payload: dict, priv_token: str, timeout: int = 30) -> dict:
|
|
|
50
75
|
except urllib.error.URLError as e:
|
|
51
76
|
raise RuntimeError(f"network request failed ({path}): {e.reason}") from e
|
|
52
77
|
|
|
78
|
+
_record_billing(result)
|
|
79
|
+
|
|
53
80
|
if result.get("code") != 0:
|
|
54
81
|
msg = result.get("msg") or result.get("message") or "unknown error"
|
|
55
82
|
raise RuntimeError(f"API returned an error [{path}]: {msg}")
|
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
"category": "authoring",
|
|
6
6
|
"title": "Video Template Registry",
|
|
7
7
|
"description": "List all available video templates (templateId / name / aspect ratio / style tags). Template-to-DSL binding is no longer exposed as a separate step — once prepare_video_assets receives a template_id it runs the full DSL→RenderPlan pipeline internally.",
|
|
8
|
+
"auth": "required",
|
|
8
9
|
"envVars": ["PRIV_TOKEN", "VIDEO_TEMPLATE_REGISTRY_URL", "MM_API_BASE_URL", "VIDEO_TEMPLATE_REGISTRY_HTTP_METHOD"],
|
|
9
10
|
"entry": { "type": "python", "scriptPath": "scripts/list_templates.py" },
|
|
10
11
|
"parameters": {
|
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
"category": "consuming",
|
|
6
6
|
"title": "Video Deconstruction",
|
|
7
7
|
"description": "Video deconstruction: split a video into reusable assets (audio extraction, ASR transcript, keyframe extraction, scene segmentation).",
|
|
8
|
+
"auth": "required",
|
|
8
9
|
"envVars": ["RENDER_API_URL", "PRIV_TOKEN", "CONVERSATION_ID"],
|
|
9
10
|
"scriptPath": "scripts/parse_via_render.py",
|
|
10
11
|
"parameters": {
|
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
"category": "consuming",
|
|
6
6
|
"title": "Web Page Recording",
|
|
7
7
|
"description": "Drive a headless browser (Playwright Python) to RECORD any URL to a video, then (by default) transcode to mp4, grab a cover frame, upload to VOD and return a playable CDN URL. Modes: fixed-duration recording, condition-triggered stop (element appears / disappears), auto-scroll from top to bottom, custom storyboards, and parameterized templates. Storyboard scenes: highlight / focus / zoom / scroll / virtual-cursor click / type / hover / caption / title-card / arrow / numbered sequence / redact / code-line highlight. For still images (png/jpg) use the web_screenshot tool instead. Use this tool whenever the user wants a video / recording / screencast of a web page: record a page, scroll-through video, page-from-top-to-bottom clip, demo of clicks/typing/hover, storyboard / multi-scene intro video, or a template-based clip.",
|
|
8
|
+
"auth": "optional",
|
|
8
9
|
"envVars": ["WEB_CAPTURE_BROWSER", "WEB_CAPTURE_OUTPUT_BASE", "WEB_CAPTURE_NO_SANDBOX", "WEB_CAPTURE_ALLOW_PRIVATE_HOSTS", "PLAYWRIGHT_BROWSERS_PATH", "REMOTION_RENDER_API_URL", "PRIV_TOKEN"],
|
|
9
10
|
"entry": { "type": "python", "scriptPath": "../web-screenshot/scripts/record.py" },
|
|
10
11
|
"parameters": {
|
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
"category": "consuming",
|
|
6
6
|
"title": "Web Page Screenshot",
|
|
7
7
|
"description": "Drive a headless browser (Playwright Python) to capture any URL to a local STILL IMAGE (png/jpg): full-page / viewport / element / region screenshots, with device emulation, waiting, hide/mask/redact, and static annotations (highlight / arrow / caption / numbered sequence / redact). Images only — for video / recording / scroll-through / storyboard clips (webm) use the web_record tool instead.",
|
|
8
|
+
"auth": "none",
|
|
8
9
|
"envVars": ["WEB_CAPTURE_BROWSER", "WEB_CAPTURE_OUTPUT_BASE", "WEB_CAPTURE_NO_SANDBOX", "WEB_CAPTURE_ALLOW_PRIVATE_HOSTS", "PLAYWRIGHT_BROWSERS_PATH"],
|
|
9
10
|
"entry": { "type": "python", "scriptPath": "scripts/screenshot.py" },
|
|
10
11
|
"parameters": {
|