@remixmate/cli 0.9.13 → 0.9.14
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/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 +17 -6
- package/dist/http.js +39 -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 +27 -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.json +1 -0
- package/skills/gen-image/skill.json +1 -0
- package/skills/gen-script/skill.json +1 -0
- package/skills/gen-video/skill.json +1 -0
- package/skills/gen-voice/skill.json +1 -0
- package/skills/prepare-video-assets/skill.json +1 -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/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
|
@@ -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
|
|
|
@@ -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": {
|