diff --git a/README.md b/README.md index 0c31000..699a828 100644 --- a/README.md +++ b/README.md @@ -14,11 +14,16 @@ ## ✨ What it can do -| Capability | Runs on | Status | -| ----------------------------------------------------------------------------------------------------------------------------------- | ------------------ | -------------- | -| **AI** — video enhancement · subtitle removal | cloud | ✅ Available | -| **Editing** (11) — trim · concat · watermark · subtitle · speed · flip · extract / mux audio · image-to-video | cloud **or** local | ✅ Available | -| **More AI workflows** — video understanding · translation · highlight clipping · script extraction · commentary · anime restyling … | cloud | 🚧 Rolling out | +**40+ capabilities across 5 domains** — run `mediakit-cli --help-full` to list them all. + +| Domain | Capabilities | Runs on | Status | +| -------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------ | ------------ | +| 🎬 **Editing** (17) | trim · concat · watermark · subtitle · speed · volume · filter · flip · fade · mix · mux · extract audio · image-to-video | cloud **or** local | ✅ Available | +| 🎚️ **Audio** (2) | voice / background separation · audio metadata probe | cloud | ✅ Available | +| 🖼️ **Image AI** (5) | enhance · object / text erase · quality scoring · OCR · background removal | cloud | ✅ Available | +| 🎥 **Video AI** (14) | enhancement (+ generative restore) · subtitle removal · ASR subtitles · OCR · highlight clipping (short-drama / mini-game) · storyline analysis · scene split · portrait & green-screen matting · metadata probe | cloud | ✅ Available | +| 🔧 **Shared** (2) | async task query · remote-file fetch | local / cloud | ✅ Available | +| 🚧 **Coming** | video translation · commentary generation · anime restyling | cloud | Rolling out | > AI capabilities run in the cloud (elastic compute, async). Editing runs **either** in the cloud **or** locally (sync, zero cost) — pick per command with `--cloud` / `--local`. @@ -44,7 +49,10 @@ mediakit-cli --local editing trim-video --video-url ./in.mp4 --start-time 3 --en ## 📦 Install ```bash -# npm (recommended, cross-platform — pulls the right build for your OS / arch) +# One-click install (CLI + AI agent Skills) +npx @volcengine/mediakit-cli install -y + +# npm only (CLI, recommended, cross-platform — pulls the right build for your OS / arch) npm install -g @volcengine/mediakit-cli # npx (no install) @@ -58,6 +66,18 @@ Pin a version or path: `VERSION= INSTALL_DIR="$HOME/.local/bin" curl -f Verify: `mediakit-cli doctor` (checks cloud readiness + local tool deps + install hints). +### Update + +The CLI checks the npm registry for new releases once a day (TTL 24h). When an update is available you'll see a hint in `stderr` and a `_notice.update` field in the stdout JSON. + +```bash +mediakit-cli version --check # report current vs latest as JSON +mediakit-cli update --check # check only, no install +mediakit-cli update # install the latest via `npm install -g` +``` + +Suppress the check with `MEDIAKIT_DISABLE_UPDATE_CHECK=1` or in CI (`CI` env set). + --- ## 🤖 Use with AI Agents @@ -78,7 +98,7 @@ Every capability is also **MCP-compatible** — `mediakit-cli -- ## 🧩 How it works - **Two modes, one command surface.** `--cloud` runs heavy AI in Volcengine's cloud (elastic compute, async `task_id`); `--local` runs deterministic editing locally (sync, zero cloud cost). Default mode is `cloud-first`; per-command flags override it. -- **Command structure:** `mediakit-cli [--cloud|--local] [flags]` — domains are `editing` · `video` · `shared`. +- **Command structure:** `mediakit-cli [--cloud|--local] [flags]` — domains are `editing` · `audio` · `image` · `video` · `shared`. - **Outputs:** cloud results are returned as URLs; local results write to `~/.mediakit/temp` (override with `--output-path` or `MEDIAKIT_OUTPUT_PATH`). --- @@ -87,6 +107,7 @@ Every capability is also **MCP-compatible** — `mediakit-cli -- - Volcengine AI MediaKit product docs & pricing: https://www.volcengine.com/docs/6448 - Full command reference & FAQ: see the docs site. +- [Error codes & exit code contract](./docs/error-codes.md) — stdout JSON protocol and exit-code rules. --- diff --git a/README.zh.md b/README.zh.md index dd63b50..59e26cd 100644 --- a/README.zh.md +++ b/README.zh.md @@ -14,11 +14,16 @@ ## ✨ 能做什么 -| 能力 | 运行 | 状态 | -| ------------------------------------------------------------------------------------------------- | ---------------- | ----------- | -| **AI 能力** — 画质增强 · 字幕擦除 | 云端 | ✅ 已上线 | -| **剪辑能力**(11 个)— 裁剪 · 拼接 · 加水印 · 加字幕 · 调速 · 翻转 · 提取 / 合成音频 · 图片转视频 | 云端 **或** 本地 | ✅ 已上线 | -| **更多 AI 工作流** — 视频理解 · 视频翻译 · 高光智剪 · 剧本还原 · 解说 · 漫剧转绘 … | 云端 | 🚧 陆续上线 | +**5 大领域、40+ 能力** —— 运行 `mediakit-cli --help-full` 可列全。 + +| 领域 | 能力 | 运行 | 状态 | +| --- | --- | --- | --- | +| 🎬 **剪辑** (17) | 裁剪 · 拼接 · 加水印 · 加字幕 · 调速 · 调音量 · 滤镜 · 翻转 · 淡入淡出 · 混音 · 合成 · 提取音频 · 图片转视频 | 云端 **或** 本地 | ✅ 已上线 | +| 🎚️ **音频** (2) | 人声 / 背景音分离 · 音频元信息探测 | 云端 | ✅ 已上线 | +| 🖼️ **图像 AI** (5) | 画质增强 · 擦除修复 · 画质评分 · OCR · 背景移除 | 云端 | ✅ 已上线 | +| 🎥 **视频 AI** (14) | 画质增强(含生成式修复)· 字幕擦除 · ASR 字幕 · OCR · 高光智剪(短剧 / 小游戏)· 剧情线分析 · 场景切分 · 人像 & 绿幕抠图 · 元信息探测 | 云端 | ✅ 已上线 | +| 🔧 **通用** (2) | 异步任务查询 · 远程文件拉取 | 本地 / 云端 | ✅ 已上线 | +| 🚧 **即将上线** | 视频翻译 · 解说生成 · 漫剧转绘 | 云端 | 陆续上线 | > AI 能力跑在云端(弹性算力、异步);剪辑能力**云端或本地**皆可(本地跑,同步、零成本)—— 每条命令用 `--cloud` / `--local` 选。 @@ -44,7 +49,10 @@ mediakit-cli --local editing trim-video --video-url ./in.mp4 --start-time 3 --en ## 📦 安装 ```bash -# npm(推荐,跨平台——自动拉取对应平台 / 架构的构建产物) +# 一键安装(CLI + AI Agent Skill) +npx @volcengine/mediakit-cli install -y + +# 仅装 CLI(推荐,跨平台——自动拉取对应平台 / 架构的构建产物) npm install -g @volcengine/mediakit-cli # npx(免安装) @@ -58,11 +66,23 @@ curl -fsSL https://raw.githubusercontent.com/volcengine/mediakit-cli/main/script 验证环境:`mediakit-cli doctor`(检查云端就绪 + 本地工具依赖 + 安装指引)。 +### 更新 + +CLI 每天会向 npm registry 检查一次新版本(TTL 24h)。有更新时,`stderr` 会出现提示,stdout JSON 会带上 `_notice.update` 字段。 + +```bash +mediakit-cli version --check # 以 JSON 输出当前版本 vs 最新版本 +mediakit-cli update --check # 只检查,不安装 +mediakit-cli update # 通过 `npm install -g` 安装最新版 +``` + +如需关闭自动检查,设置 `MEDIAKIT_DISABLE_UPDATE_CHECK=1`,或在 CI 中运行(`CI` 环境变量被设置时也会自动抑制)。 + --- ## 🤖 配合 AI Agent 使用 -`mediakit-cli` 自带 **AI Agent Skill**——教 Agent 怎么调它。于是用户只需说一句*“把这个视频增强到 1080p,再剪出最精彩的 5 秒”*,Agent 就能自动编排命令。 +`mediakit-cli` 自带 **AI Agent Skill**——教 Agent 怎么调它。于是用户只需说一句*"把这个视频增强到 1080p,再剪出最精彩的 5 秒"*,Agent 就能自动编排命令。 ```bash # 一个命令把 Skill 装进本机所有支持的 Agent @@ -78,7 +98,7 @@ npx skills add volcengine/mediakit-cli -g -y ## 🧩 工作原理 - **两种模式,同一套命令。** `--cloud` 把重算力 AI 跑在火山引擎云端(弹性算力、异步 `task_id`);`--local` 在本机跑确定性剪辑(同步、零云端成本)。默认 `cloud-first`,单命令 flag 可覆盖。 -- **命令结构:** `mediakit-cli [--cloud|--local] [flags]`——domain 为 `editing` · `video` · `shared`。 +- **命令结构:** `mediakit-cli [--cloud|--local] [flags]`——domain 为 `editing` · `audio` · `image` · `video` · `shared`。 - **输出:** 云端结果以 URL 返回;本地结果写到 `~/.mediakit/temp`(可用 `--output-path` 或 `MEDIAKIT_OUTPUT_PATH` 覆盖)。 --- @@ -87,6 +107,7 @@ npx skills add volcengine/mediakit-cli -g -y - Volcengine AI MediaKit 产品文档 & 定价:https://www.volcengine.com/docs/6448 - 完整命令参考 & FAQ:见文档站。 +- [错误码与退出码契约](./docs/error-codes.md) —— stdout JSON 协议与退出码规则。 --- diff --git a/cmd/mediakit/main.go b/cmd/mediakit/main.go index 7a027cb..f0bd898 100644 --- a/cmd/mediakit/main.go +++ b/cmd/mediakit/main.go @@ -1,15 +1,22 @@ package main import ( - "fmt" - "os" + "errors" + "fmt" + "os" - "mediakit-cli/internal/commands" + "mediakit-cli/internal/cliexit" + "mediakit-cli/internal/commands" ) func main() { - if err := commands.Execute(); err != nil { - fmt.Fprintln(os.Stderr, err) - os.Exit(1) - } + err := commands.Execute() + if err == nil { + return + } + // 业务失败:结构化错误已写入 stdout JSON,stderr 不重复打印 + if !errors.Is(err, cliexit.ErrBusinessFailure) { + fmt.Fprintln(os.Stderr, err) + } + os.Exit(1) } diff --git a/docs/error-codes.md b/docs/error-codes.md new file mode 100644 index 0000000..6fa1b8f --- /dev/null +++ b/docs/error-codes.md @@ -0,0 +1,118 @@ +# mediakit-cli 错误码与退出码契约 + +本文档描述 `mediakit-cli` 在 cloud / local 两种执行路径下的错误协议、字段含义与进程退出码(exit code)规则,作为 SDK / Skill / Plugin 等上层调用方解析的稳定契约。 + +## 一、退出码总览 + +| 退出码 | 含义 | 触发场景 | +| ------ | ------------------------------- | -------------------------------------------------------------------------------------------------------------- | +| `0` | 命令成功 | 所有路径成功完成;同步成功 / 异步提交成功(拿到 task_id)/ query-task 终态为 `completed` | +| `1` | 业务失败或框架/参数错误 | 任意 stdout JSON 出现 `success: false`(cloud)、`error` 字段(local)、`status: failed/canceled/cancelled`(query-task 终态),或参数解析失败 | + +> `mediakit-cli` 不再使用退出码区分"框架错误"和"业务错误";上层调用方应**优先解析 stdout JSON**,把退出码当作辅助信号。 + +## 二、cloud 路径(远程 API) + +### 2.1 输出落点 + +- 业务结果与业务错误:stdout(一个 JSON 对象,pretty 缩进) +- stderr:保留给 update notice、调试日志;**不再写业务错误** + +### 2.2 成功响应(同步 / 异步提交) + +异步主任务提交成功时(HTTP 2xx + `success=true`),CLI 直接透传: + +```json +{ + "success": true, + "task_id": "task_xxx", + "request_id": "req_xxx", + "result": { "...": "..." } +} +``` + +退出码:`0`。 + +### 2.3 业务失败响应 + +任意以下情形都视为业务失败,CLI 写完 JSON 后退出码 `1`: + +| 情形 | 判定字段 | +| ------------------------------------------------- | -------------------------------------------------------------- | +| 主调用 HTTP 2xx 但 `success=false` | `success == false` | +| 主调用 HTTP ≥ 400 网络/服务错误 | `errorResponse(...)` 统一写 `success: false` + `error: {...}` | +| `query-task` 终态为 `failed` / `canceled` / `cancelled` | `status` 字段(注意 queryTaskResponse 没有 `success` 字段) | + +业务失败 JSON 结构: + +```json +{ + "success": false, + "error": { + "code": "InvalidParameter", + "message": "video_url is required" + }, + "task_id": "task_xxx", + "request_id": "req_xxx" +} +``` + +### 2.4 query-task 终态字段 + +`mediakit-cli shared query-task` 返回的 `status` 取值: + +| status | 含义 | 退出码 | +| ----------- | -------------------------- | ------ | +| `running` | 进行中(非终态) | `0` | +| `queued` | 排队中(非终态) | `0` | +| `completed` | 已完成(终态,成功) | `0` | +| `failed` | 已失败(终态,业务失败) | `1` | +| `canceled` | 已取消(终态,业务失败) | `1` | +| `cancelled` | 同 `canceled`(兼容拼写) | `1` | + +只有 `failed/canceled/cancelled` 三个状态被识别为"业务失败终态",退出码为 `1`;其他状态退出码 `0`。 + +## 三、local 路径(本地执行) + +### 3.1 输出落点 + +- 成功结果:stdout(平铺业务字段,与 swagger response schema 对齐,详见 Rule 23) +- 业务失败:stdout 写 `{"error": {...}}` 结构化错误 +- stderr:保留给 update notice、调试日志 + +### 3.2 业务失败响应 + +```json +{ + "error": { + "type": "invalid_parameter | security_violation | environment_error | execution_error", + "code": "missing_required_param | invalid_param_type | param_out_of_range | param_insufficient | unsupported_value | forbidden_operation | not_whitelisted | unsafe_characters | handler_not_implemented | local_unsupported | dependency_missing | execution_failed | download_failed | unknown", + "message": "<原始错误信息(不截断、不摘要)>" + } +} +``` + +退出码:`1`。 + +`type` 与 `code` 取值范围见 `AGENTS.md` Rule 25。 + +## 四、上层调用方解析建议 + +1. 优先解析 stdout JSON: + - cloud:`success === false` 即业务失败;终态 `status` 为 `failed/canceled/cancelled` 也视为失败 + - local:存在非空 `error` 字段即业务失败 +2. 退出码作为辅助信号:`exit code === 0` 表示无业务失败信号,`exit code === 1` 表示存在业务失败或框架/参数错误 +3. stderr 不再承载业务错误内容;只用来读取 update notice 与调试日志 + +## 五、Boolean flag 传参 + +所有 boolean 类型 flag 必须使用以下三种形式之一: + +- 裸 flag:`--enable-foo`(等价于 `--enable-foo=true`) +- 显式赋值:`--enable-foo=true` / `--enable-foo=false` + +禁止使用空格分隔写法(`--enable-foo true`):cobra 会把空格后的 token 视为 positional 参数。`mediakit-cli` 会在该 capability 含 boolean 参数时给出明确提示。 + +## 六、--schema 输出中的 Async 字段 + +每个 capability 命令的 `--schema` 输出在 description 中固定包含一行 `Async: 是/否`;异步 capability 额外打印 `轮询命令: mediakit-cli shared query-task --task-id --poll-complete`,供上层 agent 识别"需要轮询拿终态结果"的语义。 diff --git a/go.mod b/go.mod index 02a313c..f7e0d25 100644 --- a/go.mod +++ b/go.mod @@ -2,9 +2,9 @@ module mediakit-cli go 1.22 -require ( - github.com/spf13/cobra v1.10.2 - github.com/spf13/pflag v1.0.9 -) +require github.com/spf13/cobra v1.10.2 -require github.com/inconshreveable/mousetrap v1.1.0 // indirect +require ( + github.com/inconshreveable/mousetrap v1.1.0 // indirect + github.com/spf13/pflag v1.0.9 // indirect +) diff --git a/internal/cliexit/exit.go b/internal/cliexit/exit.go new file mode 100644 index 0000000..7174d1f --- /dev/null +++ b/internal/cliexit/exit.go @@ -0,0 +1,27 @@ +// Package cliexit 定义 CLI 业务失败的哨兵错误。 +// +// "业务失败" 在 CLI 视角下采用宽口径定义:本次命令未达成功状态。 +// 涵盖三档: +// 1. 业务级失败 — cloud success=false / query-task 失败终态 / local error 字段 +// 2. 框架/参数错误 — capability 参数校验失败、handler 未实现、依赖缺失 +// 3. 透传错误 — 底层 modes/cloud/local 抛出的非 sentinel 错误经 writeCapabilityError 包装 +// +// 三档统一映射到 exit=1,但结构化错误已经写入 stdout JSON; +// main.go 通过 errors.Is 识别此哨兵,跳过 stderr 重复打印。 +// +// 详见 AGENTS.md Rule 32: CLI Exit Code Contract。 +package cliexit + +import "errors" + +// ErrBusinessFailure 表示 CLI 本次命令未达成功状态。 +// +// 由以下三个 writer 统一抛出: +// - internal/cloud/executor.go::writeJSON(cloud 主路径与 query-task 终态) +// - internal/local/executor.go::writeJSON(local 主路径) +// - internal/commands/registry.go::writeCapabilityError(参数/依赖/兜底错误) +// +// cmd/mediakit/main.go 用 errors.Is 识别后: +// - 跳过 stderr 打印(错误结构已在 stdout JSON 中) +// - 进程退出码 os.Exit(1) +var ErrBusinessFailure = errors.New("mediakit-cli: business failure") diff --git a/internal/cloud/executor.go b/internal/cloud/executor.go index 147518d..f393750 100644 --- a/internal/cloud/executor.go +++ b/internal/cloud/executor.go @@ -9,6 +9,9 @@ import ( "time" "github.com/spf13/cobra" + + "mediakit-cli/internal/cliexit" + "mediakit-cli/internal/updatecheck" ) // Executor is the unified cloud execution entry for generated capability handlers. @@ -193,6 +196,11 @@ func syncToolResponse(result map[string]any) map[string]any { } output := map[string]any{} + // 透传后端 success 字段:提交成功返回 true。 + // 失败路径在 formatCommandResponse 处已被 businessFailureResponse 截获,此处不会出现 success=false。 + if success, ok := result["success"].(bool); ok { + output["success"] = success + } if taskID := strings.TrimSpace(fmt.Sprint(result["task_id"])); taskID != "" && taskID != "" { output["task_id"] = taskID } @@ -216,6 +224,10 @@ func asyncTaskResponse(result map[string]any) map[string]any { output := map[string]any{ "task_id": "", } + // 透传后端 success 字段:发起任务成功返回 true,失败返回 false(详细 error 参考 error 字段)。 + if success, ok := result["success"].(bool); ok { + output["success"] = success + } if taskID, ok := result["task_id"]; ok { output["task_id"] = strings.TrimSpace(fmt.Sprint(taskID)) } @@ -239,6 +251,14 @@ func queryTaskResponse(result map[string]any) map[string]any { if status := strings.TrimSpace(fmt.Sprint(result["status"])); status != "" && status != "" { output["status"] = status } + // query-task 失败终态:注入 success: false,统一交给 writeJSON 触发 sentinel。 + // 仅在此处做语义判定,避免污染非 query-task 路径。 + if isTerminalFailure(result) { + output["success"] = false + if errField, ok := result["error"]; ok && errField != nil { + output["error"] = errField + } + } taskResult, ok := result["result"].(map[string]any) if !ok { return output @@ -249,21 +269,64 @@ func queryTaskResponse(result map[string]any) map[string]any { return output } +// isTerminalTaskStatus 判断 query-task 是否处于终态。 +// +// 协议 status 枚举:running / queued / completed / failed / canceled。 +// 终态 = completed / failed / canceled(cancelled 拼写兼容)。 +// 非终态 = running / queued。 func isTerminalTaskStatus(response map[string]any) bool { status := strings.ToLower(strings.TrimSpace(fmt.Sprint(response["status"]))) switch status { - case "completed", "succeeded", "success", "failed", "cancelled", "canceled": + case "completed", "failed", "canceled", "cancelled": return true default: return false } } +// isTerminalFailure 判断异步任务是否处于失败终态。 +// 区别于 isTerminalTaskStatus:后者还包含 completed 这一成功终态; +// 本函数仅匹配 failed / canceled(cancelled 拼写兼容),用于驱动 ErrBusinessFailure sentinel。 +func isTerminalFailure(payload map[string]any) bool { + status := strings.ToLower(strings.TrimSpace(fmt.Sprint(payload["status"]))) + switch status { + case "failed", "canceled", "cancelled": + return true + default: + return false + } +} + +// isSuccessFalse 判断 payload 中 success 字段是否显式为 false。 +func isSuccessFalse(payload map[string]any) bool { + value, ok := payload["success"] + if !ok { + return false + } + if success, ok := value.(bool); ok { + return !success + } + return false +} + func writeJSON(writer io.Writer, value any) error { + if m, ok := value.(map[string]any); ok { + updatecheck.InjectNotice(m) + } encoder := json.NewEncoder(writer) encoder.SetEscapeHTML(false) encoder.SetIndent("", " ") - return encoder.Encode(value) + if err := encoder.Encode(value); err != nil { + return err + } + // 单信号判定:cloud 所有失败路径(HTTP 错误、success=false、query-task 失败终态) + // 都会在最终响应中显式写入 success=false,writeJSON 只需识别该字段。 + if m, ok := value.(map[string]any); ok { + if isSuccessFalse(m) { + return cliexit.ErrBusinessFailure + } + } + return nil } func cloneParams(params map[string]any) map[string]any { diff --git a/internal/cloud/request.go b/internal/cloud/request.go index 3a8ed78..d345af7 100644 --- a/internal/cloud/request.go +++ b/internal/cloud/request.go @@ -87,9 +87,9 @@ func (c *Client) newRequest(method string, path string, query map[string]any, bo req.Header.Set("Content-Type", "application/json") req.Header.Set("x-surface", resolveSurface(c.Surface)) req.Header.Set("x-runtime", resolveRuntime(c.Runtime)) - req.Header.Set("X-Amk-Task-Source", "cli") + req.Header.Set("X-Amk-Task-Source", "cli") if c.APIKey != "" { req.Header.Set("Authorization", "Bearer "+c.APIKey) } return req, nil -} +} \ No newline at end of file diff --git a/internal/cloud/response.go b/internal/cloud/response.go index 662a560..4ba6b1f 100644 --- a/internal/cloud/response.go +++ b/internal/cloud/response.go @@ -68,7 +68,9 @@ func newAPIError(statusCode int, payloadBytes []byte) error { } func errorResponse(err error, taskID string, requestID string) map[string]any { - output := map[string]any{} + output := map[string]any{ + "success": false, + } if apiErr, ok := err.(*APIError); ok { if apiErr.Payload != nil { @@ -115,7 +117,9 @@ func isBusinessFailure(payload map[string]any) bool { // businessFailureResponse 把业务级失败的响应体直接透传输出。 func businessFailureResponse(payload map[string]any) map[string]any { - output := map[string]any{} + output := map[string]any{ + "success": false, + } if errField, ok := payload["error"]; ok && errField != nil { output["error"] = errField } else { diff --git a/internal/commands/registry.go b/internal/commands/registry.go index a11440e..a9fed83 100644 --- a/internal/commands/registry.go +++ b/internal/commands/registry.go @@ -12,9 +12,11 @@ import ( "github.com/spf13/cobra" "github.com/spf13/pflag" + "mediakit-cli/internal/cliexit" cliconfig "mediakit-cli/internal/config" "mediakit-cli/internal/local" "mediakit-cli/internal/modes" + "mediakit-cli/internal/updatecheck" ) type DomainMeta struct { @@ -2647,7 +2649,7 @@ func newCapabilityCommand(meta CapabilityMeta) *cobra.Command { Use: capabilityMeta.Name, Short: capabilityMeta.Description, Long: renderCapabilityHelp(capabilityMeta), - Args: cobra.NoArgs, + Args: capabilityArgsValidator(capabilityMeta), DisableAutoGenTag: true, RunE: func(cmd *cobra.Command, args []string) error { // --schema: 输出工具 schema 后退出 @@ -2660,6 +2662,11 @@ func newCapabilityCommand(meta CapabilityMeta) *cobra.Command { return err } if err := modes.Dispatch(cmd, capabilityMeta.runtimeMeta(), params); err != nil { + // 业务失败 sentinel:JSON 已由底层 writer 写入 stdout, + // 这里直接向上传递,避免再次写一份 capability error JSON。 + if errors.Is(err, cliexit.ErrBusinessFailure) { + return err + } return writeCapabilityError(cmd.OutOrStdout(), err) } return nil @@ -2673,6 +2680,53 @@ func newCapabilityCommand(meta CapabilityMeta) *cobra.Command { return cmd } +// capabilityArgsValidator 为 capability 命令构建自定义 Args 校验器。 +// 在拒绝位置参数(等价 cobra.NoArgs)的同时,识别 bool flag 误用模式 +// (`--flag value` 而非 `--flag=value` 或裸 `--flag`),给出可操作提示。 +func capabilityArgsValidator(meta CapabilityMeta) cobra.PositionalArgs { + return func(cmd *cobra.Command, args []string) error { + if len(args) == 0 { + return nil + } + boolFlags := collectBoolFlagNames(meta) + if len(boolFlags) > 0 { + for _, arg := range args { + if isBoolLiteral(arg) { + return fmt.Errorf( + "boolean flag 必须写成 --flag=true / --flag=false 或裸 --flag,不能用空格分隔;当前命令的 boolean 参数: %s", + strings.Join(boolFlags, ", "), + ) + } + } + } + return fmt.Errorf("unknown command %q for %q", args[0], cmd.CommandPath()) + } +} + +// isBoolLiteral 判断字符串是否是 bool 字面量。 +func isBoolLiteral(s string) bool { + switch strings.ToLower(strings.TrimSpace(s)) { + case "true", "false", "1", "0": + return true + } + return false +} + +// collectBoolFlagNames 返回 capability 所有 boolean 类型参数的 flag 名称。 +func collectBoolFlagNames(meta CapabilityMeta) []string { + var names []string + for _, p := range meta.Params { + if p.Type == "boolean" { + flag := p.FlagName + if flag == "" { + flag = strings.ReplaceAll(p.Name, "_", "-") + } + names = append(names, "--"+flag) + } + } + return names +} + func renderDomainHelp(domain DomainMeta, capabilities []CapabilityMeta) string { lines := []string{fmt.Sprintf("%s — %s", domain.Name, domain.Description), "", "Available commands:"} if len(capabilities) == 0 { @@ -3083,22 +3137,32 @@ func writeCapabilityError(writer io.Writer, err error) error { // 如果是 DependencyError,使用其自带的结构化输出(含 install_guide) var depErr *local.DependencyError if errors.As(err, &depErr) { + payload := depErr.StructuredError() + updatecheck.InjectNotice(payload) encoder := json.NewEncoder(writer) encoder.SetEscapeHTML(false) encoder.SetIndent("", " ") - return encoder.Encode(depErr.StructuredError()) + if encErr := encoder.Encode(payload); encErr != nil { + return encErr + } + return cliexit.ErrBusinessFailure } - encoder := json.NewEncoder(writer) - encoder.SetEscapeHTML(false) - encoder.SetIndent("", " ") - return encoder.Encode(map[string]any{ + payload := map[string]any{ "error": map[string]any{ "type": classifyErrorType(err), "code": classifyErrorCode(err), "message": err.Error(), }, - }) + } + updatecheck.InjectNotice(payload) + encoder := json.NewEncoder(writer) + encoder.SetEscapeHTML(false) + encoder.SetIndent("", " ") + if encErr := encoder.Encode(payload); encErr != nil { + return encErr + } + return cliexit.ErrBusinessFailure } func classifyErrorType(err error) string { @@ -3164,6 +3228,9 @@ func classifyErrorCode(err error) string { } func writeJSON(out io.Writer, value any) error { + if m, ok := value.(map[string]any); ok { + updatecheck.InjectNotice(m) + } encoder := json.NewEncoder(out) encoder.SetEscapeHTML(false) encoder.SetIndent("", " ") @@ -3221,11 +3288,14 @@ func buildCapabilitySchema(meta CapabilityMeta, resolvedMode string) map[string] // 构建描述 description := meta.Description - if meta.Async { - description += "\n\n- Async: 是,用 mediakit_shared_query_task 查询结果" - } modeLabel := modes.ModeLabel(meta.runtimeMeta()) - description += "\n- Mode: " + modeLabel + description += "\n\n- Mode: " + modeLabel + if meta.Async { + description += "\n- Async: 是" + description += "\n- 轮询命令: mediakit-cli shared query-task --task-id --poll-complete" + } else { + description += "\n- Async: 否" + } // 构建工具名(domain_tool 格式) toolName := strings.ReplaceAll(meta.Name, "-", "_") diff --git a/internal/commands/root.go b/internal/commands/root.go index da78b88..7cb51ba 100644 --- a/internal/commands/root.go +++ b/internal/commands/root.go @@ -1,7 +1,10 @@ package commands import ( + "time" + buildinfo "mediakit-cli/internal/build" + "mediakit-cli/internal/updatecheck" "github.com/spf13/cobra" ) @@ -23,6 +26,14 @@ func newRootCmd() *cobra.Command { Short: "MediaKit command line interface", Long: `MediaKit CLI provides system commands, domain navigation, and generated capability commands. +One-click install (CLI + AI agent Skills): + npx @volcengine/mediakit-cli install -y + +Update: + mediakit-cli update # install latest via npm install -g + mediakit-cli update --check # only report status + mediakit-cli version --check # show current vs latest + AI Agent Skills: mediakit-cli pairs with AI agent skills (Claude Code, etc.) that teach the agent MediaKit CLI patterns, best practices, and workflows. @@ -32,11 +43,21 @@ AI Agent Skills: Or pick specific domains: npx skills add volcengine/mediakit-cli -s byted-mediakit-editing -y + npx skills add volcengine/mediakit-cli -s byted-mediakit-audio -y + npx skills add volcengine/mediakit-cli -s byted-mediakit-image -y npx skills add volcengine/mediakit-cli -s byted-mediakit-video -y npx skills add volcengine/mediakit-cli -s byted-mediakit-shared -y`, SilenceUsage: true, + SilenceErrors: true, DisableAutoGenTag: true, Version: buildinfo.Version, + PersistentPreRun: func(cmd *cobra.Command, args []string) { + updatecheck.StartAsync() + }, + PersistentPostRun: func(cmd *cobra.Command, args []string) { + updatecheck.WaitForResult(100 * time.Millisecond) + updatecheck.PrintStderrNag(cmd.ErrOrStderr()) + }, RunE: func(cmd *cobra.Command, args []string) error { switch { case showDomains: @@ -58,6 +79,7 @@ AI Agent Skills: cmd.AddCommand(newDoctorCmd()) cmd.AddCommand(newConfigCmd()) cmd.AddCommand(newVersionCmd()) + cmd.AddCommand(newUpdateCmd()) for _, domainCmd := range newGeneratedDomainCommands() { cmd.AddCommand(domainCmd) diff --git a/internal/commands/update.go b/internal/commands/update.go new file mode 100644 index 0000000..6dc1618 --- /dev/null +++ b/internal/commands/update.go @@ -0,0 +1,81 @@ +package commands + +import ( + "encoding/json" + "fmt" + "os" + "os/exec" + "time" + + buildinfo "mediakit-cli/internal/build" + "mediakit-cli/internal/updatecheck" + + "github.com/spf13/cobra" +) + +func newUpdateCmd() *cobra.Command { + var checkOnly bool + cmd := &cobra.Command{ + Use: "update", + Short: "Check and apply mediakit-cli updates from the npm registry", + Long: "Check the npm registry for the latest @volcengine/mediakit-cli release and optionally install it via npm install -g.", + Args: cobra.NoArgs, + DisableAutoGenTag: true, + RunE: func(cmd *cobra.Command, args []string) error { + updatecheck.StartAsync() + r := updatecheck.WaitForResult(3 * time.Second) + if r == nil { + return writeUpdatePayload(cmd, map[string]any{ + "current": buildinfo.Version, + "action": "skipped", + "reason": "update check disabled or unavailable in this environment", + }) + } + payload := map[string]any{ + "current": r.Current, + "latest": r.Latest, + "has_update": r.HasUpdate, + } + if r.Err != nil { + payload["error"] = r.Err.Error() + payload["action"] = "skipped" + return writeUpdatePayload(cmd, payload) + } + if !r.HasUpdate { + payload["action"] = "noop" + return writeUpdatePayload(cmd, payload) + } + payload["upgrade_command"] = fmt.Sprintf("npm install -g %s@latest", updatecheck.PackageName) + if checkOnly { + payload["action"] = "check" + return writeUpdatePayload(cmd, payload) + } + payload["action"] = "install" + if err := runNpmInstallLatest(cmd); err != nil { + payload["install_status"] = "failed" + payload["error"] = err.Error() + _ = writeUpdatePayload(cmd, payload) + return err + } + payload["install_status"] = "ok" + return writeUpdatePayload(cmd, payload) + }, + } + cmd.Flags().BoolVar(&checkOnly, "check", false, "Only check for updates; do not install") + return cmd +} + +func writeUpdatePayload(cmd *cobra.Command, payload map[string]any) error { + encoder := json.NewEncoder(cmd.OutOrStdout()) + encoder.SetEscapeHTML(false) + encoder.SetIndent("", " ") + return encoder.Encode(payload) +} + +func runNpmInstallLatest(cmd *cobra.Command) error { + target := fmt.Sprintf("%s@latest", updatecheck.PackageName) + c := exec.Command("npm", "install", "-g", target) + c.Stdout = os.Stderr + c.Stderr = os.Stderr + return c.Run() +} diff --git a/internal/commands/version.go b/internal/commands/version.go index 4b3f7e6..4c66943 100644 --- a/internal/commands/version.go +++ b/internal/commands/version.go @@ -1,20 +1,46 @@ package commands import ( + "encoding/json" "fmt" + "time" buildinfo "mediakit-cli/internal/build" + "mediakit-cli/internal/updatecheck" "github.com/spf13/cobra" ) func newVersionCmd() *cobra.Command { - return &cobra.Command{ + var checkUpdate bool + cmd := &cobra.Command{ Use: "version", Short: "Print CLI version information", Args: cobra.NoArgs, DisableAutoGenTag: true, RunE: func(cmd *cobra.Command, args []string) error { + if checkUpdate { + updatecheck.StartAsync() + r := updatecheck.WaitForResult(2 * time.Second) + payload := map[string]any{ + "current": buildinfo.Version, + "date": buildinfo.Date, + } + if r != nil { + payload["latest"] = r.Latest + payload["has_update"] = r.HasUpdate + if r.Err != nil { + payload["error"] = r.Err.Error() + } + if r.HasUpdate { + payload["upgrade_command"] = fmt.Sprintf("npm install -g %s@latest", updatecheck.PackageName) + } + } + encoder := json.NewEncoder(cmd.OutOrStdout()) + encoder.SetEscapeHTML(false) + encoder.SetIndent("", " ") + return encoder.Encode(payload) + } _, err := fmt.Fprintf( cmd.OutOrStdout(), "mediakit-cli %s\nbuild date: %s\n", @@ -24,4 +50,6 @@ func newVersionCmd() *cobra.Command { return err }, } + cmd.Flags().BoolVar(&checkUpdate, "check", false, "Check the npm registry for a newer release and report status") + return cmd } diff --git a/internal/local/executor.go b/internal/local/executor.go index f09e1b2..5adcc5b 100644 --- a/internal/local/executor.go +++ b/internal/local/executor.go @@ -12,9 +12,11 @@ import ( "github.com/spf13/cobra" + "mediakit-cli/internal/cliexit" cliconfig "mediakit-cli/internal/config" "mediakit-cli/internal/local/core" "mediakit-cli/internal/output" + "mediakit-cli/internal/updatecheck" ) // Executor coordinates local capability execution in later stages. @@ -112,10 +114,39 @@ func Execute(cmd *cobra.Command, command string, params map[string]any) error { } func writeJSON(output io.Writer, value any) error { + if m, ok := value.(map[string]any); ok { + updatecheck.InjectNotice(m) + } encoder := json.NewEncoder(output) encoder.SetEscapeHTML(false) encoder.SetIndent("", " ") - return encoder.Encode(value) + if err := encoder.Encode(value); err != nil { + return err + } + if m, ok := value.(map[string]any); ok { + if errField, exists := m["error"]; exists && !isEmptyErrorValue(errField) { + return cliexit.ErrBusinessFailure + } + } + return nil +} + +// isEmptyErrorValue 判断 error 字段是否实质为空: +// nil / 空字符串 / 空 map / 空 slice 都视为"无错误",不应触发 sentinel。 +func isEmptyErrorValue(v any) bool { + if v == nil { + return true + } + switch typed := v.(type) { + case string: + return strings.TrimSpace(typed) == "" + case map[string]any: + return len(typed) == 0 + case []any: + return len(typed) == 0 + default: + return false + } } func cloneParams(params map[string]any) map[string]any { diff --git a/internal/updatecheck/cache.go b/internal/updatecheck/cache.go new file mode 100644 index 0000000..8c5a814 --- /dev/null +++ b/internal/updatecheck/cache.go @@ -0,0 +1,68 @@ +package updatecheck + +import ( + "encoding/json" + "os" + "path/filepath" + "time" + + cliconfig "mediakit-cli/internal/config" +) + +const ( + CacheFileName = "update-check.json" + CacheTTL = 24 * time.Hour +) + +type Cache struct { + Current string `json:"current"` + Latest string `json:"latest"` + CheckedAt time.Time `json:"checked_at"` + HasUpdate bool `json:"has_update"` + NotifiedAt time.Time `json:"notified_at,omitempty"` +} + +func CacheFile(home string) string { + return filepath.Join(cliconfig.ConfigDir(home), CacheFileName) +} + +func LoadCache(home string) (*Cache, error) { + path := CacheFile(home) + data, err := os.ReadFile(path) + if err != nil { + if os.IsNotExist(err) { + return nil, nil + } + return nil, err + } + c := &Cache{} + if err := json.Unmarshal(data, c); err != nil { + return nil, err + } + return c, nil +} + +func SaveCache(home string, c *Cache) error { + dir := cliconfig.ConfigDir(home) + if err := os.MkdirAll(dir, 0o755); err != nil { + return err + } + data, err := json.MarshalIndent(c, "", " ") + if err != nil { + return err + } + return os.WriteFile(CacheFile(home), data, 0o644) +} + +func IsCacheFresh(c *Cache, runningVersion string) bool { + if c == nil { + return false + } + if c.Current != runningVersion { + return false + } + if time.Since(c.CheckedAt) > CacheTTL { + return false + } + return true +} diff --git a/internal/updatecheck/checker.go b/internal/updatecheck/checker.go new file mode 100644 index 0000000..3ae507f --- /dev/null +++ b/internal/updatecheck/checker.go @@ -0,0 +1,226 @@ +package updatecheck + +import ( + "context" + "encoding/json" + "fmt" + "net/http" + "os" + "strings" + "sync/atomic" + "time" + + cliconfig "mediakit-cli/internal/config" + "mediakit-cli/internal/build" +) + +const ( + NpmRegistryURL = "https://registry.npmjs.org/@volcengine/mediakit-cli/latest" + PackageName = "@volcengine/mediakit-cli" + EnvDisable = "MEDIAKIT_DISABLE_UPDATE_CHECK" + EnvCI = "CI" +) + +type Result struct { + HasUpdate bool + Current string + Latest string + Err error +} + +var checkResult atomic.Value // *Result + +func StartAsync() { + if shouldSkip() { + return + } + if r := loadCachedResult(); r != nil { + checkResult.Store(r) + return + } + go func() { + r := runCheck() + if r != nil { + checkResult.Store(r) + } + }() +} + +func loadCachedResult() *Result { + running := strings.TrimSpace(build.Version) + if running == "" { + return nil + } + home, err := cliconfig.ResolveHomeDir() + if err != nil { + return nil + } + cached, _ := LoadCache(home) + if cached == nil || !IsCacheFresh(cached, running) { + return nil + } + return &Result{ + HasUpdate: cached.HasUpdate, + Current: cached.Current, + Latest: cached.Latest, + } +} + +func GetResult() *Result { + v := checkResult.Load() + if v == nil { + return nil + } + r, ok := v.(*Result) + if !ok { + return nil + } + return r +} + +func WaitForResult(timeout time.Duration) *Result { + deadline := time.Now().Add(timeout) + for time.Now().Before(deadline) { + if r := GetResult(); r != nil { + return r + } + time.Sleep(10 * time.Millisecond) + } + return GetResult() +} + +func shouldSkip() bool { + if v := strings.TrimSpace(os.Getenv(EnvDisable)); v != "" && v != "0" && strings.ToLower(v) != "false" { + return true + } + if v := strings.TrimSpace(os.Getenv(EnvCI)); v != "" && v != "0" && strings.ToLower(v) != "false" { + return true + } + if strings.TrimSpace(build.Version) == "dev" { + return true + } + return false +} + +func runCheck() *Result { + running := strings.TrimSpace(build.Version) + if running == "" { + return nil + } + + home, err := cliconfig.ResolveHomeDir() + if err == nil { + if cached, _ := LoadCache(home); cached != nil && IsCacheFresh(cached, running) { + return &Result{ + HasUpdate: cached.HasUpdate, + Current: cached.Current, + Latest: cached.Latest, + } + } + } + + latest, err := fetchLatestVersion() + if err != nil { + return &Result{Current: running, Err: err} + } + latest = strings.TrimSpace(latest) + if latest == "" { + return &Result{Current: running, Err: fmt.Errorf("empty latest version")} + } + + hasUpdate := compareVersions(running, latest) < 0 + cache := &Cache{ + Current: running, + Latest: latest, + CheckedAt: time.Now(), + HasUpdate: hasUpdate, + } + if home, herr := cliconfig.ResolveHomeDir(); herr == nil { + _ = SaveCache(home, cache) + } + + return &Result{ + HasUpdate: hasUpdate, + Current: running, + Latest: latest, + } +} + +func fetchLatestVersion() (string, error) { + ctx, cancel := context.WithTimeout(context.Background(), 1500*time.Millisecond) + defer cancel() + req, err := http.NewRequestWithContext(ctx, http.MethodGet, NpmRegistryURL, nil) + if err != nil { + return "", err + } + resp, err := http.DefaultClient.Do(req) + if err != nil { + return "", err + } + defer resp.Body.Close() + if resp.StatusCode != http.StatusOK { + return "", fmt.Errorf("registry status %d", resp.StatusCode) + } + var payload struct { + Version string `json:"version"` + } + if err := json.NewDecoder(resp.Body).Decode(&payload); err != nil { + return "", err + } + return payload.Version, nil +} + +// compareVersions returns -1 if ab. Best-effort semver. +func compareVersions(a, b string) int { + a = strings.TrimPrefix(strings.TrimSpace(a), "v") + b = strings.TrimPrefix(strings.TrimSpace(b), "v") + if a == b { + return 0 + } + pa := strings.SplitN(a, "-", 2) + pb := strings.SplitN(b, "-", 2) + ca := splitNums(pa[0]) + cb := splitNums(pb[0]) + for i := 0; i < 3; i++ { + var va, vb int + if i < len(ca) { + va = ca[i] + } + if i < len(cb) { + vb = cb[i] + } + if va < vb { + return -1 + } + if va > vb { + return 1 + } + } + // Equal core. Pre-release < release. + if len(pa) == 2 && len(pb) == 1 { + return -1 + } + if len(pa) == 1 && len(pb) == 2 { + return 1 + } + if len(pa) == 2 && len(pb) == 2 { + return strings.Compare(pa[1], pb[1]) + } + return 0 +} + +func splitNums(s string) []int { + parts := strings.Split(s, ".") + out := make([]int, 0, len(parts)) + for _, p := range parts { + n := 0 + for _, r := range p { + if r < '0' || r > '9' { + break + } + n = n*10 + int(r-'0') + } + out = append(out, n) + } + return out +} diff --git a/internal/updatecheck/notice.go b/internal/updatecheck/notice.go new file mode 100644 index 0000000..b9c4ae3 --- /dev/null +++ b/internal/updatecheck/notice.go @@ -0,0 +1,69 @@ +package updatecheck + +import ( + "fmt" + "io" + "os" +) + +// NoticePayload returns a map suitable to be embedded into the JSON stdout +// payload at the top-level `_notice.update` key, or nil if no update is needed. +func NoticePayload() map[string]any { + r := GetResult() + if r == nil || !r.HasUpdate { + return nil + } + return map[string]any{ + "current": r.Current, + "latest": r.Latest, + "command": fmt.Sprintf("npm install -g %s@latest", PackageName), + "message": fmt.Sprintf("New %s release available: %s -> %s", PackageName, r.Current, r.Latest), + } +} + +// InjectNotice adds `_notice.update` to a top-level JSON object map if an +// update is available. It is a no-op when the result is nil or no update. +func InjectNotice(result map[string]any) { + if result == nil { + return + } + payload := NoticePayload() + if payload == nil { + return + } + notice, _ := result["_notice"].(map[string]any) + if notice == nil { + notice = map[string]any{} + } + notice["update"] = payload + result["_notice"] = notice +} + +// PrintStderrNag prints an unobtrusive update hint to stderr when an update is +// available and stderr is a TTY (character device). +func PrintStderrNag(w io.Writer) { + r := GetResult() + if r == nil || !r.HasUpdate { + return + } + f, ok := w.(*os.File) + if !ok { + return + } + if !isCharDevice(f) { + return + } + fmt.Fprintf(w, "\n[mediakit-cli] new version available: %s -> %s\n run: npm install -g %s@latest\n", + r.Current, r.Latest, PackageName) +} + +func isCharDevice(f *os.File) bool { + if f == nil { + return false + } + info, err := f.Stat() + if err != nil { + return false + } + return (info.Mode() & os.ModeCharDevice) != 0 +} diff --git a/mediakit-cli b/mediakit-cli deleted file mode 100755 index 9a75793..0000000 Binary files a/mediakit-cli and /dev/null differ diff --git a/package.json b/package.json index 80a125b..b17a386 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@volcengine/mediakit-cli", - "version": "0.1.5", + "version": "0.1.6", "description": "MediaKit CLI with multi-platform binary distribution via npm", "license": "MIT", "bin": { diff --git a/scripts/install-wizard.js b/scripts/install-wizard.js new file mode 100644 index 0000000..b74823e --- /dev/null +++ b/scripts/install-wizard.js @@ -0,0 +1,153 @@ +#!/usr/bin/env node + +const { spawnSync } = require("node:child_process"); +const path = require("node:path"); + +const pkg = require("../package.json"); + +const PACKAGE_NAME = pkg.name || "@volcengine/mediakit-cli"; +const SKILL_REPO = "volcengine/mediakit-cli"; + +function parseArgs(argv) { + const opts = { + cliOnly: false, + skillsOnly: false, + skills: [], + yes: false, + versionTag: "latest", + }; + for (let i = 0; i < argv.length; i++) { + const a = argv[i]; + switch (a) { + case "--cli-only": + opts.cliOnly = true; + break; + case "--skills-only": + opts.skillsOnly = true; + break; + case "-y": + case "--yes": + opts.yes = true; + break; + case "-s": + case "--skill": { + const next = argv[i + 1]; + if (next && !next.startsWith("-")) { + opts.skills.push(next); + i++; + } + break; + } + case "--version": { + const next = argv[i + 1]; + if (next && !next.startsWith("-")) { + opts.versionTag = next; + i++; + } + break; + } + default: + if (a.startsWith("--version=")) { + opts.versionTag = a.slice("--version=".length); + } else if (a.startsWith("--skills=")) { + opts.skills.push( + ...a.slice("--skills=".length).split(",").map((s) => s.trim()).filter(Boolean) + ); + } + break; + } + } + return opts; +} + +function log(msg) { + console.log(`[mediakit-cli install] ${msg}`); +} + +function whichSync(cmd) { + const probe = process.platform === "win32" ? "where" : "command"; + const probeArgs = process.platform === "win32" ? [cmd] : ["-v", cmd]; + const result = spawnSync(probe, probeArgs, { stdio: "ignore", shell: true }); + return result.status === 0; +} + +function runNpmInstall(target) { + log(`installing ${target} via npm install -g`); + const result = spawnSync("npm", ["install", "-g", target], { + stdio: "inherit", + }); + if (result.error) { + throw result.error; + } + if (result.status !== 0) { + throw new Error(`npm install -g ${target} failed with exit code ${result.status}`); + } +} + +function runSkillsAdd(opts) { + const args = ["-y", "skills", "add", SKILL_REPO]; + if (opts.skills.length === 0) { + args.push("-g"); + } else { + for (const skill of opts.skills) { + args.push("-s", skill); + } + } + if (opts.yes) { + args.push("-y"); + } + log(`installing skills via npx ${args.join(" ")}`); + const result = spawnSync("npx", args, { stdio: "inherit" }); + if (result.error) { + throw result.error; + } + if (result.status !== 0) { + throw new Error(`npx skills add failed with exit code ${result.status}`); + } +} + +function isCliAlreadyInstalled() { + // If MEDIAKIT_CLI_FORCE_REINSTALL=1, always reinstall. + if ( + process.env.MEDIAKIT_CLI_FORCE_REINSTALL === "1" || + process.env.MEDIKIT_CLI_FORCE_REINSTALL === "1" + ) { + return false; + } + return whichSync("mediakit-cli"); +} + +async function runInstallWizard(rawArgs) { + const opts = parseArgs(rawArgs); + + if (!whichSync("npm")) { + throw new Error("npm is required but not found in PATH"); + } + + if (!opts.skillsOnly) { + if (isCliAlreadyInstalled()) { + log("mediakit-cli already installed; skipping CLI install"); + } else { + const target = `${PACKAGE_NAME}@${opts.versionTag}`; + runNpmInstall(target); + } + } + + if (!opts.cliOnly) { + if (!whichSync("npx")) { + throw new Error("npx is required to install skills but not found in PATH"); + } + runSkillsAdd(opts); + } + + log("done."); +} + +module.exports = { runInstallWizard }; + +if (require.main === module) { + runInstallWizard(process.argv.slice(2)).catch((error) => { + console.error(`[mediakit-cli install] ${error.message}`); + process.exit(1); + }); +} diff --git a/scripts/install.js b/scripts/install.js index 41eb460..5715de1 100755 --- a/scripts/install.js +++ b/scripts/install.js @@ -11,8 +11,27 @@ const http = require("node:http"); const pkg = require("../package.json"); const PROJECT_NAME = "mediakit-cli"; + +// Deprecated env names (MEDIKIT_*) are still read for back-compat but emit a +// warning. New env names use the MEDIAKIT_* prefix (Rule 22). The 0.3.0 +// release will drop the deprecated forms. +function readEnv(currentName, deprecatedName) { + const current = process.env[currentName]; + if (current !== undefined && current !== "") { + return current; + } + const deprecated = process.env[deprecatedName]; + if (deprecated !== undefined && deprecated !== "") { + console.warn( + `[mediakit-cli] env ${deprecatedName} is deprecated, please use ${currentName}` + ); + return deprecated; + } + return undefined; +} + const RELEASE_BASE_URL = - process.env.MEDIKIT_CLI_RELEASE_BASE_URL || + readEnv("MEDIAKIT_CLI_RELEASE_BASE_URL", "MEDIKIT_CLI_RELEASE_BASE_URL") || "https://github.com/volcengine/mediakit-cli/releases/download"; const TMP_PREFIX = "mediakit-cli-install-"; const packageRoot = path.resolve(__dirname, ".."); @@ -180,14 +199,15 @@ function findBinary(rootDir, targetName) { } async function install(options = {}) { - if (process.env.MEDIKIT_CLI_SKIP_DOWNLOAD === "1") { - console.log("[mediakit-cli] skip download because MEDIKIT_CLI_SKIP_DOWNLOAD=1"); + if (readEnv("MEDIAKIT_CLI_SKIP_DOWNLOAD", "MEDIKIT_CLI_SKIP_DOWNLOAD") === "1") { + console.log("[mediakit-cli] skip download because MEDIAKIT_CLI_SKIP_DOWNLOAD=1"); return; } const platform = resolvePlatform(); const arch = resolveArch(); - const version = process.env.MEDIKIT_CLI_VERSION || pkg.version; + const version = + readEnv("MEDIAKIT_CLI_VERSION", "MEDIKIT_CLI_VERSION") || pkg.version; const targetBinary = path.join(binDir, binaryName()); if (!options.force && fs.existsSync(targetBinary)) { diff --git a/scripts/install.sh b/scripts/install.sh index 3edd854..08cee7c 100755 --- a/scripts/install.sh +++ b/scripts/install.sh @@ -2,8 +2,28 @@ set -euo pipefail PROJECT_NAME="mediakit-cli" -RELEASE_BASE_URL="${MEDIKIT_CLI_RELEASE_BASE_URL:-https://github.com/volcengine/mediakit-cli/releases/download}" -LATEST_API_URL="${MEDIKIT_CLI_LATEST_API_URL:-https://api.github.com/repos/volcengine/mediakit-cli/releases/latest}" + +# Resolve env vars from MEDIAKIT_* (preferred) or MEDIKIT_* (deprecated). +resolve_env() { + local current="$1" + local legacy="$2" + local fallback="${3:-}" + local cur_val="${!current:-}" + local legacy_val="${!legacy:-}" + if [[ -n "${cur_val}" ]]; then + printf '%s' "${cur_val}" + return + fi + if [[ -n "${legacy_val}" ]]; then + echo "[mediakit-cli] env ${legacy} is deprecated, please use ${current}" >&2 + printf '%s' "${legacy_val}" + return + fi + printf '%s' "${fallback}" +} + +RELEASE_BASE_URL="$(resolve_env MEDIAKIT_CLI_RELEASE_BASE_URL MEDIKIT_CLI_RELEASE_BASE_URL "https://github.com/volcengine/mediakit-cli/releases/download")" +LATEST_API_URL="$(resolve_env MEDIAKIT_CLI_LATEST_API_URL MEDIKIT_CLI_LATEST_API_URL "https://api.github.com/repos/volcengine/mediakit-cli/releases/latest")" INSTALL_DIR="${INSTALL_DIR:-/usr/local/bin}" VERSION="${VERSION:-}" diff --git a/scripts/run.js b/scripts/run.js index d2c26ef..1191496 100755 --- a/scripts/run.js +++ b/scripts/run.js @@ -5,6 +5,7 @@ const path = require("node:path"); const { spawnSync } = require("node:child_process"); const { install } = require("./install"); +const { runInstallWizard } = require("./install-wizard"); const packageRoot = path.resolve(__dirname, ".."); const binDir = path.join(packageRoot, "bin"); @@ -20,10 +21,30 @@ async function ensureBinary() { await install({ force: true }); } +// Detect whether the CLI is being run via `npx @volcengine/mediakit-cli install`, +// where it should bootstrap a global installation rather than execute the +// embedded binary inside the npx cache. MEDIAKIT_CLI_RUN=1 is a re-entry guard. +function shouldInterceptInstall(args) { + if (!args || args.length === 0 || args[0] !== "install") { + return false; + } + if (process.env.MEDIAKIT_CLI_RUN === "1" || process.env.MEDIKIT_CLI_RUN === "1") { + return false; + } + return process.env.npm_command === "exec"; +} + async function main() { + const argv = process.argv.slice(2); + if (shouldInterceptInstall(argv)) { + process.env.MEDIAKIT_CLI_RUN = "1"; + await runInstallWizard(argv.slice(1)); + return; + } await ensureBinary(); - const result = spawnSync(binary, process.argv.slice(2), { + const result = spawnSync(binary, argv, { stdio: "inherit", + env: { ...process.env, MEDIAKIT_CLI_RUN: "1" }, }); if (result.error) { diff --git a/skills/byted-mediakit-editing/SKILL.md b/skills/byted-mediakit-editing/SKILL.md index bc02583..3d8a644 100644 --- a/skills/byted-mediakit-editing/SKILL.md +++ b/skills/byted-mediakit-editing/SKILL.md @@ -2,7 +2,7 @@ name: byted-mediakit-editing version: "1.0.0" license: "MIT" -description: "音视频剪辑,涵盖音视频拼接、裁剪、合成等能力。包含能力:add-image-to-video, add-subtitle-to-video, adjust-video-speed, concat-audio, concat-video, extract-audio, flip-video, image-to-video, mux-audio-video, trim-audio, trim-video。当用户需要使用 editing 域的 MediaKit CLI 能力时触发。" +description: "音视频剪辑,涵盖音视频拼接、裁剪、合成等能力。包含能力:add-image-to-video, add-subtitle-to-video, adjust-audio-speed, adjust-video-speed, adjust-video-volume, apply-video-filter, concat-audio, concat-video, extract-audio, fade-audio, fade-video-audio, flip-video, image-to-video, mix-audio, mux-audio-video, trim-audio, trim-video。当用户需要使用 editing 域的 MediaKit CLI 能力时触发。" permissions: - shell metadata: @@ -11,7 +11,7 @@ metadata: cliHelp: "mediakit-cli editing --help" product: mediakit-cli/skills domain: editing - capability_count: 11 + capability_count: 17 --- # Editing Skills @@ -25,12 +25,18 @@ metadata: |------|------|----------|----------| | add-image-to-video | 视频加图片,可用作加图片水印。 | `video_url:string, sub_image_url:string, sub_image_height?:string, sub_image_width?:string, sub_image_pos_x?:string, sub_image_pos_y?:string, start_time?:number, end_time?:number, callback_args?:string, client_token?:string` | [reference/add-image-to-video.md](reference/add-image-to-video.md) | | add-subtitle-to-video | 将字幕文件或文本内容,以指定样式压制到视频画面中,生成带内嵌字幕的新视频。 | `video_url:string, subtitle_url?:string, subtitles?:array, subtitle_pos_preset?:string, subtitle_font_size?:integer, subtitle_font_color?:string, subtitle_font_type?:string, callback_args?:string, client_token?:string` | [reference/add-subtitle-to-video.md](reference/add-subtitle-to-video.md) | +| adjust-audio-speed | 调整音频的播放倍速,实现快放或慢放效果。 | `audio_url:string, speed?:number, callback_args?:string, client_token?:string` | [reference/adjust-audio-speed.md](reference/adjust-audio-speed.md) | | adjust-video-speed | 调整视频的播放倍速,实现快放或慢放效果。 | `video_url:string, speed?:number, callback_args?:string, client_token?:string` | [reference/adjust-video-speed.md](reference/adjust-video-speed.md) | +| adjust-video-volume | 调整视频音量大小,支持静音;输出 mp4,分辨率与原片一致。 | `video_url:string, volume?:number, callback_args?:string, client_token?:string` | [reference/adjust-video-volume.md](reference/adjust-video-volume.md) | +| apply-video-filter | 为视频添加指定滤镜效果,输出mp4,分辨率与原片一致。 | `video_url:string, filter_style?:string, callback_args?:string, client_token?:string` | [reference/apply-video-filter.md](reference/apply-video-filter.md) | | concat-audio | 拼接多个音频片段。 | `audio_urls:array, callback_args?:string, client_token?:string` | [reference/concat-audio.md](reference/concat-audio.md) | | concat-video | 拼接多个视频片段,支持添加转场效果。 | `video_urls:array, transitions?:array, callback_args?:string, client_token?:string` | [reference/concat-video.md](reference/concat-video.md) | | extract-audio | 将视频文件中的音频流分离并保存为独立的音频文件。 | `video_url:string, format?:string, callback_args?:string, client_token?:string` | [reference/extract-audio.md](reference/extract-audio.md) | +| fade-audio | 对输入音频实现淡入淡出效果,输出 mp3。 | `audio_url:string, fade_in_duration?:number, fade_out_duration?:number, callback_args?:string, client_token?:string` | [reference/fade-audio.md](reference/fade-audio.md) | +| fade-video-audio | 对输入视频的声轨实现淡入淡出效果。 输出 mp4,分辨率与原片一致。 | `video_url:string, fade_in_duration?:number, fade_out_duration?:number, callback_args?:string, client_token?:string` | [reference/fade-video-audio.md](reference/fade-video-audio.md) | | flip-video | 对视频画面进行上下或左右镜像翻转。 | `video_url:string, is_flip_vertical?:boolean, is_flip_horizontal?:boolean, callback_args?:string, client_token?:string` | [reference/flip-video.md](reference/flip-video.md) | | image-to-video | 多张图片生成动画视频。 | `images:array, transitions?:array, callback_args?:string, client_token?:string` | [reference/image-to-video.md](reference/image-to-video.md) | +| mix-audio | 将多个音频文件(如背景音乐、音效、人声)进行混音,生成一个新的音频文件。 处理耗时:处理耗时与视频时长正相关。视频时长越长,处理耗时越长。平均 RTF(处理耗时/原片时长)为 1。 输出音频的时长以最长的音频为准。 输出视频格式:mp3 | `audio_urls:array, callback_args?:string, client_token?:string` | [reference/mix-audio.md](reference/mix-audio.md) | | mux-audio-video | 音视频合成。 | `video_url:string, audio_url:string, is_audio_reserve?:boolean, is_video_audio_sync?:boolean, sync_mode?:string, sync_method?:string, callback_args?:string, client_token?:string` | [reference/mux-audio-video.md](reference/mux-audio-video.md) | | trim-audio | 按起止时间点(秒级)裁剪音频,生成新片段。 | `audio_url:string, start_time?:number, end_time?:number, callback_args?:string, client_token?:string` | [reference/trim-audio.md](reference/trim-audio.md) | | trim-video | 按起止时间点裁剪视频,生成新片段。 | `video_url:string, start_time?:number, end_time?:number, callback_args?:string, client_token?:string` | [reference/trim-video.md](reference/trim-video.md) | diff --git a/skills/byted-mediakit-shared/SKILL.md b/skills/byted-mediakit-shared/SKILL.md index 36e9695..61e5e74 100644 --- a/skills/byted-mediakit-shared/SKILL.md +++ b/skills/byted-mediakit-shared/SKILL.md @@ -2,7 +2,7 @@ name: byted-mediakit-shared version: '1.0.0' license: 'MIT' -description: 'mediakit-cli 共享:环境检查、初始化配置、命令结构、认证配置、异步任务响应与错误处理。' +description: '1. mediakit-cli: supports a variety of operations such as audio/video processing, editing, and images, with some capabilities covering both cloud and local modes; 2. mediakit-cli shared: environment checks, initialization config, command structure, authentication config, async task responses, and error handling.' permissions: - shell metadata: @@ -14,48 +14,48 @@ metadata: capability_count: 14 --- -# MediaKit 共享规则 +# MediaKit Shared Rules -本技能指导你如何通过 mediakit-cli 操作媒体资源,以及调用过程中的通用规则和注意事项。 +This skill explains how to operate on media resources via `mediakit-cli`, and the shared rules and caveats that apply to every call. -## 前置检查 +## Prerequisites -### 依赖安装 +### Install the CLI -首次使用前,确认 CLI 已安装: +Before first use, confirm the CLI is installed: ```bash -# 安装 +# Install npm install -g @volcengine/mediakit-cli -# 验证 +# Verify mediakit-cli --version ``` -### 鉴权信息检查 +### Authentication check -优先级:环境变量 > 配置文件(文件路径 `~/.mediakit/config.json`) +Priority: environment variables > config file (path: `~/.mediakit/config.json`). -#### 字段说明 +#### Field reference -- 环境变量/配置文件:`MEDIAKIT_API_KEY`、`MEDIAKIT_ENDPOINT`、`MEDIAKIT_SURFACE`、`MEDIAKIT_RUNTIME` +- Environment variables / config file: `MEDIAKIT_API_KEY`, `MEDIAKIT_ENDPOINT`, `MEDIAKIT_SURFACE`, `MEDIAKIT_RUNTIME` -| 变量 | 必填 | 说明 | -| ------------------- | ------------ | ----------------------------------------------------------------------------------------------------------------------- | -| `MEDIAKIT_API_KEY` | 云端模式必填 | API 认证 Token | -| `MEDIAKIT_ENDPOINT` | 否 | API 访问点 | -| `MEDIAKIT_SURFACE` | 否 | 请求来源 Header `x-surface`;默认 `cli`,Skill 建议 `skill`,Plugin 建议 `plugin`,最终上报 `cli/skill` 或 `cli/plugin` | -| `MEDIAKIT_RUNTIME` | 否 | 请求来源 Header `x-runtime`;按宿主设置为 `claude`、`arkclaw` 等,未配置时回退环境探测或 `unknown` | +| Variable | Required | Description | +| ------------------- | -------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `MEDIAKIT_API_KEY` | Required (cloud mode) | API authentication token | +| `MEDIAKIT_ENDPOINT` | Optional | API endpoint | +| `MEDIAKIT_SURFACE` | Optional | Request-source Header `x-surface`. Defaults to `cli`. Skill should set `skill`, Plugin should set `plugin`; CLI reports them as `cli/skill` or `cli/plugin`. | +| `MEDIAKIT_RUNTIME` | Optional | Request-source Header `x-runtime`. Set to host name such as `claude` / `arkclaw`; falls back to environment probing or `unknown` when not configured. | -任一必填项缺失时,终止执行并输出所有缺失项的列表及修复建议。 +When any required field is missing, stop execution and output the full list of missing items together with fix suggestions. -云端调用会自动携带 `x-surface` / `x-runtime`。Header 优先级为:环境变量 > `~/.mediakit/config.json` > 默认值/环境探测。当本 Skill/Plugin 通过 `mediakit-cli` 调用云端能力时,运行环境应注入 `MEDIAKIT_SURFACE=skill|plugin` 与 `MEDIAKIT_RUNTIME=<宿主>`;CLI 会保留原始产物前缀并上报 `x-surface=cli/skill|cli/plugin`。若未显式配置,CLI 默认按 `x-surface=cli`,`x-runtime` 依次回退 `IDENTITY_NAME` / `OPENCLAW_SERVICE_MARKER` 环境探测,最后为 `unknown`。 +Cloud calls automatically carry `x-surface` / `x-runtime`. Header priority: environment variables > `~/.mediakit/config.json` > defaults / environment probing. When this Skill/Plugin invokes cloud capabilities through `mediakit-cli`, the runtime should inject `MEDIAKIT_SURFACE=skill|plugin` and `MEDIAKIT_RUNTIME=`. The CLI keeps the original product prefix and reports `x-surface=cli/skill` or `x-surface=cli/plugin`. If no explicit configuration is provided, the CLI defaults to `x-surface=cli`; `x-runtime` falls back to `IDENTITY_NAME` / `OPENCLAW_SERVICE_MARKER` environment probing, and finally to `unknown`. -### 来源上报约束 +### Source-reporting constraints -- Skill 调用 `mediakit-cli` 时,必须显式设置 `MEDIAKIT_SURFACE=skill`,不能依赖用户已有环境变量。 -- Plugin 调用 `mediakit-cli` 时,必须显式设置 `MEDIAKIT_SURFACE=plugin`,不能复用 Skill 的取值。 -- 宿主环境标识建议同时显式设置 `MEDIAKIT_RUNTIME=<宿主>`;若未设置,CLI 会回退为环境探测值或 `unknown`。 +- When a Skill invokes `mediakit-cli`, it must explicitly set `MEDIAKIT_SURFACE=skill` and must not rely on the user's pre-existing environment variables. +- When a Plugin invokes `mediakit-cli`, it must explicitly set `MEDIAKIT_SURFACE=plugin` and must not reuse the Skill value. +- The host runtime identifier should also be explicitly set via `MEDIAKIT_RUNTIME=`; if unset, the CLI falls back to environment probing or `unknown`. ```bash MEDIAKIT_SURFACE=skill MEDIAKIT_RUNTIME= mediakit-cli editing add-image-to-video @@ -63,94 +63,94 @@ MEDIAKIT_SURFACE=skill MEDIAKIT_RUNTIME= mediakit-cli editing add-image MEDIAKIT_SURFACE=plugin MEDIAKIT_RUNTIME= mediakit-cli editing add-image-to-video ``` -## CLI 使用方式 +## CLI usage -### 初始化配置 +### Initialization -首次使用建议先运行初始化向导: +Run the init wizard before first use: ```bash mediakit-cli init ``` -Agent 非交互初始化可显式写入请求来源与运行时配置: +For non-interactive initialization (Agents), pass the request-source and runtime configuration explicitly: ```bash mediakit-cli init --mode cloud-first --api-key --runtime --surface cli --yes mediakit-cli init --mode local-first --api-key --endpoint --output-path ~/mediakit-output --runtime --surface cli --credential-store config --yes ``` -初始化后常用命令如下: +Common commands after initialization: ```bash -# 查看当前配置 +# Show current configuration mediakit-cli config show -# 切换默认模式到本地优先 +# Switch the default mode to local-first mediakit-cli config set mode local-first -# 切换默认模式到云端优先 +# Switch the default mode to cloud-first mediakit-cli config set mode cloud-first -# 刷新环境检查并查看依赖状态 +# Refresh environment checks and view dependency status mediakit-cli doctor ``` -### 命令结构 +### Command structure -MediaKit CLI 统一使用 `domain + tool` 的调用方式: +MediaKit CLI consistently uses the `domain + tool` invocation shape: ```bash mediakit-cli {domain} {tool} [flags] ``` -常见帮助命令: +Common help commands: ```bash -# 查看所有 domain +# List all domains mediakit-cli --domains -# 查看某个分组下的工具列表 +# List tools in a given domain mediakit-cli {domain} --help -# 查看具体工具的参数 +# Show parameters of a specific tool mediakit-cli {domain} {tool} --help -# 动态发现工具能力与返回结构 +# Dynamically discover tool capability and return structure mediakit-cli {domain} {tool} --schema mediakit-cli --local {domain} {tool} --schema ``` -当前产物覆盖的 domain 包括:`editing`, `video`。 +Domains currently covered by this product: `editing`, `video`. -### Schema 发现 +### Schema discovery -每个 capability 命令都支持 `--schema`,用于 Agent 动态读取工具能力,不要求传必填业务参数。 +Every capability command supports `--schema`, used by Agents to dynamically read tool capabilities. Required business parameters are not enforced when `--schema` is used. -返回结构包含: +The returned structure contains: -- `name`:工具名,使用 snake_case,如 `add_image_to_video` -- `description`:工具描述,自动包含 `Mode` 与 `Async` 信息 -- `input_schema`:输入参数 JSON Schema -- `output_schema`:当前执行模式下的返回结构 +- `name`: tool name in snake_case, e.g. `add_image_to_video` +- `description`: tool description, automatically including `Mode` and `Async` information +- `input_schema`: input parameter JSON Schema +- `output_schema`: return structure for the current execution mode -输出区分规则: +Output disambiguation rules: -- 默认按全局 `mode` 配置解析返回面 -- `--local ... --schema` 输出本地模式返回面,本地模式直接返回最终结果字段 -- 云端异步工具输出 `task_id` / `request_id`,并在 `final_result` 中描述 `query-task` 完成态结果 -- `query-task` 是 cloud only,schema 描述任务状态与完成态结果 +- By default the return surface is resolved against the global `mode` configuration +- `--local ... --schema` outputs the local-mode return surface; local mode returns the final result fields directly +- Cloud async tools output `task_id` / `request_id`, and describe the completed-state result of `query-task` under `final_result` +- `query-task` is cloud only; its schema describes task status plus the completed-state result -示例: +Examples: ```bash mediakit-cli editing trim-video --schema mediakit-cli --local editing trim-video --schema ``` -### 单次调用模式覆盖 +### Per-invocation mode override -除 `config set mode` 设置默认模式外,还支持仅对当前命令生效的临时覆盖: +In addition to `config set mode` for setting the default mode, you can override the mode for a single command only: ```bash mediakit-cli --local editing add-image-to-video @@ -158,61 +158,61 @@ mediakit-cli --local editing add-image-to-video mediakit-cli --cloud editing add-image-to-video ``` -补充规则: +Additional rules: -- `--local` / `--cloud` 只影响当前命令,不修改全局 `config.mode` -- `--local` 与 `--cloud` 互斥,不能同时传入 +- `--local` / `--cloud` only affect the current command and do not modify the global `config.mode` +- `--local` and `--cloud` are mutually exclusive and cannot be passed together -## 异步任务 +## Async tasks -提交异步媒体处理任务成功后会返回 `task_id` 字段。通过 `shared query-task` 命令查询结果。 +When an async media-processing task is accepted successfully, the response contains a `task_id` field. Use the `shared query-task` command to poll the result. ```bash mediakit-cli shared query-task --task-id ``` -## local / cloud 约束 +## local / cloud constraints -- `query-task` 是 **cloud only** 工具 -- local 模式下不支持 query-task -- 当前本轮能力以云端执行为主;如需显式声明,请优先使用 `--cloud` +- `query-task` is a **cloud only** tool +- local mode does not support `query-task` +- The current capability set runs primarily in cloud mode; when an explicit declaration is required, prefer `--cloud` -### Cloud 模式媒体输入补充 +### Cloud-mode media input notes -- 当命令以 `--cloud` 或 `cloud-first` 策略执行时,媒体输入参数(如 `video_url`、`audio_url`、`image_url`、`subtitle_url`、`sub_image_url` 及对应数组/对象子字段)可传入 `http://` / `https://` URL、`mediakit://...` file_id 或本地文件路径 -- `http://` / `https://` URL 与 `mediakit://...` file_id 会原样提交;本地文件路径会由 CLI 先上传为 `mediakit://...` file_id,再提交给云端工具 -- 各工具 reference 中的参数说明来自 APIHub/OpenAPI 原始字段描述;若其中写有公网 URL 或 HTTP/HTTPS URL,表示云端 API 最终接收的资源形态,不限制 CLI cloud 模式的本地路径预处理能力 +- When a command runs with `--cloud` or under the `cloud-first` strategy, media input parameters (`video_url`, `audio_url`, `image_url`, `subtitle_url`, `sub_image_url`, and their corresponding array / object sub-fields) accept `http://` / `https://` URLs, `mediakit://...` file_ids, or local file paths +- `http://` / `https://` URLs and `mediakit://...` file_ids are submitted as-is; local file paths are first uploaded by the CLI as `mediakit://...` file_ids and then submitted to the cloud tool +- Parameter descriptions in each tool's reference are sourced from the APIHub/OpenAPI raw field descriptions; even when they show public URLs or HTTP/HTTPS URLs, that only reflects the resource form the cloud API ultimately receives — it does not restrict the CLI's ability to pre-process local paths in cloud mode -### Local 模式补充 +### Local-mode notes -- 本地输出目录优先级:`--output-path` > `MEDIAKIT_OUTPUT_PATH` > config `output_path` > `~/.mediakit/temp` -- 当 `--output-path` 指向具体媒体文件名时,直接作为最终输出文件;否则按输入文件名生成 `{原文件名}_{工具名}.{ext}`,重复时追加 6 位随机数 -- 无法从输入 URL 或路径提取文件名时,退回 `{工具名}-{UnixNano}.{ext}` -- local 模式依赖 `ffmpeg` / `ffprobe`,缺失时错误中会给出 `install_guide` -- local 模式媒体处理输出必须贴合接口 response schema,禁止输出内部执行元数据 +- Local output directory priority: `--output-path` > `MEDIAKIT_OUTPUT_PATH` > config `output_path` > `~/.mediakit/temp` +- When `--output-path` points to a concrete media filename, it is used as the final output file directly; otherwise the filename is built as `{source_name}_{tool_name}.{ext}`, with a 6-digit random suffix appended on collision +- When no filename can be extracted from the input URL or path, fall back to `{tool_name}-{UnixNano}.{ext}` +- Local mode depends on `ffmpeg` / `ffprobe`; when missing, the error response includes an `install_guide` +- Local-mode media processing output must conform to the corresponding interface response schema; internal execution metadata must not be emitted -### 错误响应 +### Error responses -- CLI cloud 模式直接透传 API 返回的原始 error 对象,不提取 `message` -- CLI local 模式返回结构化错误:`{"error":{"type":"...","code":"...","message":"..."}}` -- MCP error_response 直接透传原始 error 内容,dict 原样作为 `error` 字段值 +- CLI cloud mode forwards the original error object returned by the API as-is, without extracting `message` +- CLI local mode returns a structured error: `{"error":{"type":"...","code":"...","message":"..."}}` +- MCP `error_response` forwards the original error content as-is; a dict is used directly as the `error` field value -## 幂等参数维护 +## Idempotency parameter maintenance -| 参数 | 作用 | 维护建议 | -| --------------- | ------------ | ------------------------------------------------------ | -| `client_token` | 主动控制幂等 | 请求重试时复用同一值;强制重新执行时传新的唯一值 | -| `callback_args` | 透传回调参数 | 建议与 `client_token` 一起维护,便于回调对账与重试追踪 | +| Parameter | Purpose | Maintenance guidance | +| ---------------- | ---------------------- | ----------------------------------------------------------------------------------- | +| `client_token` | Explicit idempotency | Reuse the same value on request retries; use a new unique value to force re-execute | +| `callback_args` | Passthrough to callback | Maintain together with `client_token` to ease callback reconciliation and retry tracking | -补充规则: +Additional rules: -- `client_token` 长度不超过 64 个字符 -- `callback_args` 可用于回调透传与对账追踪 +- `client_token` must not exceed 64 characters +- `callback_args` is useful for callback passthrough and reconciliation tracking -## 轮询策略 +## Polling strategy -| 参数 | 描述 | 默认值 | -| ----------------------- | ---------------------- | ------ | -| `poll-interval-seconds` | 轮询间隔 | 10s | -| `max-poll-attempts` | 轮询次数,0 代表不查询 | 0 | -| `poll-complete` | 阻塞至终态 | - | +| Parameter | Description | Default | +| ----------------------- | --------------------------------- | ------- | +| `poll-interval-seconds` | Polling interval | 10s | +| `max-poll-attempts` | Polling attempts; 0 disables it | 0 | +| `poll-complete` | Block until terminal status | - | diff --git a/skills/byted-mediakit-shared/reference/query_task.md b/skills/byted-mediakit-shared/reference/query_task.md index 1b9b2ae..e95853d 100644 --- a/skills/byted-mediakit-shared/reference/query_task.md +++ b/skills/byted-mediakit-shared/reference/query_task.md @@ -1,28 +1,31 @@ -# 查询任务 +# Query Task -## 能力描述 -异步任务结果查询通过task_id查询任务信息 +## Capability -## 执行方式 +Query async task information by `task_id`. -| 项目 | 说明 | -|------|------| -| Domain | `shared` | -| Tool | `query-task` | -| 是否异步 | `否` | -| 是否支持 local | `否` | -| 模式说明 | cloud only;当前命令用于查询云端异步任务状态。 | -| 幂等行为 | 当前命令无额外幂等参数要求。 | +## Execution -## 参数 -| 参数 | CLI flag | 类型 | 必填 | 默认值 | 说明 | -|------|----------|------|------|--------|------| -| task_id | `--task-id` | string | 是 | - | 需要查询的任务 ID。 | -| poll_interval_seconds | `--poll-interval-seconds` | number | 否 | 10 | 轮询间隔秒数。 | -| max_poll_attempts | `--max-poll-attempts` | integer | 否 | 0 | 最大轮询次数,0 表示不自动轮询。 | -| poll_complete | `--poll-complete` | boolean | 否 | - | 是否轮询直到任务完成。 | +| Item | Value | +| --------------- | -------------------------------------------------------------------------- | +| Domain | `shared` | +| Tool | `query-task` | +| Async | `No` | +| Local supported | `No` | +| Mode notes | cloud only; this command is used to query the status of cloud async tasks. | +| Idempotency | No additional idempotency parameters required for this command. | + +## Parameters + +| Parameter | CLI flag | Type | Required | Default | Description | +| --------------------- | ------------------------- | ------- | -------- | ------- | -------------------------------------------------- | +| task_id | `--task-id` | string | Yes | - | Task ID to query. | +| poll_interval_seconds | `--poll-interval-seconds` | number | No | 10 | Polling interval in seconds. | +| max_poll_attempts | `--max-poll-attempts` | integer | No | 0 | Maximum polling attempts; 0 disables auto-polling. | +| poll_complete | `--poll-complete` | boolean | No | - | Whether to poll until the task is complete. | + +## Example -## 调用示例 ```bash mediakit-cli shared query-task \ --task-id task_demo_001 \ @@ -30,8 +33,9 @@ mediakit-cli shared query-task \ --max-poll-attempts 12 ``` -## 受理响应 -异步媒体处理命令提交成功后通常先返回如下受理结果: +## Acceptance response + +Async media-processing commands typically return an acceptance response on successful submission: ```json { @@ -40,7 +44,8 @@ mediakit-cli shared query-task \ } ``` -## 输出格式 +## Output format + ```json { "success": true, @@ -54,7 +59,8 @@ mediakit-cli shared query-task \ } ``` -## 任务结果查询 -当前命令本身就是任务查询入口,无需再次查询。 +## Task result lookup -- 当前命令:`mediakit-cli shared query-task` +This command is itself the task-query entry point; no further query is required. + +- Current command: `mediakit-cli shared query-task` diff --git a/skills/byted-mediakit-video/SKILL.md b/skills/byted-mediakit-video/SKILL.md index 29faa96..48687a8 100644 --- a/skills/byted-mediakit-video/SKILL.md +++ b/skills/byted-mediakit-video/SKILL.md @@ -2,7 +2,7 @@ name: byted-mediakit-video version: "1.0.0" license: "MIT" -description: "视频处理,涵盖视频画质增强、视频理解、字幕擦除等能力。包含能力:enhance-video, erase-video-subtitle-pro。当用户需要使用 video 域的 MediaKit CLI 能力时触发。" +description: "视频处理,涵盖视频画质增强、视频理解、字幕擦除等能力。包含能力:analyze-video-highlights, analyze-video-storyline, asr-subtitles, enhance-video, enhance-video-generative, erase-video-subtitle, erase-video-subtitle-pro, generate-highlights-microdrama, generate-highlights-minigame, matte-greenscreen-video, matte-portrait-video, probe-video-metadata, segment-scenes, video-ocr。当用户需要使用 video 域的 MediaKit CLI 能力时触发。" permissions: - shell metadata: @@ -11,7 +11,7 @@ metadata: cliHelp: "mediakit-cli video --help" product: mediakit-cli/skills domain: video - capability_count: 2 + capability_count: 14 --- # Video Skills @@ -23,5 +23,17 @@ metadata: | 工具 | 说明 | 参数声明 | 参考文档 | |------|------|----------|----------| +| analyze-video-highlights | 智能捕捉视频"情绪波峰"与"关键动作",输出精准时间戳、高光打分、OCR 文本和画面描述等元数据,供下游进行更灵活的二次开发。 支持短剧(Miniseries)和小游戏(Game)两种分析模型。 使用限制:单次最多 100 个视频,累计时长不超过 300 分钟。 | `video_urls:array, model:string, mode:string, minigame_info?:object{name?:string, play_definition?:string, highlight_definition?:string}, callback_args?:string, client_token?:string` | [reference/analyze-video-highlights.md](reference/analyze-video-highlights.md) | +| analyze-video-storyline | 智能解析影视剧内容,生成结构化剧情线,供智能剪辑、内容检索与互动播放等场景使用。 基于大模型视频理解能力,对输入的单个或多个长视频(如电影、电视剧)进行分析,提取并组织成一份完整的故事线。 该故事线由一系列按时间顺序排列的剧情片段(Clips)和基于片段聚合的高光故事线(Highlights)组成。 使用限制:单次最多 30 个视频,单个视频时长不超过 2.5 小时。 | `video_urls:array, enable_snapshot?:boolean, callback_args?:string, client_token?:string` | [reference/analyze-video-storyline.md](reference/analyze-video-storyline.md) | +| asr-subtitles | 对输入视频或音频进行语音识别,输出带时间戳的字幕片段。 支持格式:主流音视频格式(如mp4、mov、mp3、m4a、wav等)。 输入:video_url和audio_url二选一。 | `video_url?:string, audio_url?:string, content_type?:string, language?:string, enable_speaker_info?:boolean, enable_confidence?:boolean, callback_args?:string, client_token?:string` | [reference/asr-subtitles.md](reference/asr-subtitles.md) | | enhance-video | 画质增强:针对 AIGC / UGC / 短剧 / 教育 / 游戏 / 老片修复等场景,提供画质提升 + 超分增强一站式解决方案。依托 AI MediaKit 智能媒体处理引擎,融合视频内容理解、画质指标智能决策、多维度增强原子算法,实现画质的全面优化。 支持格式:主流视频格式如mp4、flv、ts、avi、mov、wmv、mkv。 使用限制:单文件大小不超过100G。 | `video_url:string, scene?:string, tool_version?:string, resolution?:string, resolution_limit?:integer, fps?:number, callback_args?:string, client_token?:string` | [reference/enhance-video.md](reference/enhance-video.md) | +| enhance-video-generative | 生成式视频增强修复(generative_video_restoration)是基于扩散大模型(Diffusion-based Large Model)的生成式视频修复技术。不仅可以还原被破坏的像素,更借助大规模预训练积累的丰富视觉先验,主动补全细节、理解语义,生成真实、自然、高保真的视频内容。 | `video_url:string, resolution?:string, bitrate_level?:string, fps?:number, callback_args?:string, client_token?:string` | [reference/enhance-video-generative.md](reference/enhance-video-generative.md) | +| erase-video-subtitle | 智能检测并擦除视频画面中已有的硬字幕,保留原始背景。 支持格式:主流视频格式如mp4、flv、ts、avi、mov、wmv、mkv。 | `video_url:string, callback_args?:string, client_token?:string` | [reference/erase-video-subtitle.md](reference/erase-video-subtitle.md) | | erase-video-subtitle-pro | 针对视频中的字幕,实现高质量的无痕擦除,最大程度的还原视频画面。 支持格式:主流视频格式如mp4、flv、ts、avi、mov、wmv、mkv。 | `video_url:string, mode?:string, output_encode_mode?:string, erase_ratio_location?:array, callback_args?:string, client_token?:string` | [reference/erase-video-subtitle-pro.md](reference/erase-video-subtitle-pro.md) | +| generate-highlights-microdrama | 深度理解短剧角色、剧情与故事线,自动提取高光片段并混剪成投流视频。 支持故事线混剪模式(StorylineCuts),可选"短剧三要素"视觉模板,输出高光集锦、单集预告等。 支持输出详细分镜信息(storyboard)。 使用限制:单次最多 100 个视频,累计时长不超过 300 分钟。 | `video_urls:array, mode?:string, enable_generate_video?:boolean, enable_return_poster?:boolean, edit_param?:object{mode:string, template_edit?:object{template?:string, title?:string, hint?:string}}, highlight_cuts_param?:object{enable_storyboard?:boolean, min_duration?:number, max_duration?:number, max_number?:integer, cut_mode?:string}, opening_hook_param?:object{enable_opening_hook?:boolean, min_duration?:number, max_duration?:number, min_clip_duration?:number, min_score?:number}, callback_args?:string, client_token?:string` | [reference/generate-highlights-microdrama.md](reference/generate-highlights-microdrama.md) | +| generate-highlights-minigame | 识别小游戏录屏视频中的核心玩法与高光事件(如连击、通关、极限操作等), 快速生成用于买量的视频素材。支持提供游戏名称、玩法描述、高光定义以辅助模型更精准识别。 使用限制:本期仅支持单视频输入。 | `video_urls:array, mode?:string, enable_generate_video?:boolean, minigame_info?:object{name?:string, play_definition?:string, highlight_definition?:string}, callback_args?:string, client_token?:string` | [reference/generate-highlights-minigame.md](reference/generate-highlights-minigame.md) | +| matte-greenscreen-video | 对以绿幕或纯色为背景的视频进行抠图,自动识别主体(人物、物品、动物等),同时移除背景,生成背景透明的视频。 输出视频格式为 WEBM(默认)或 MOV,分辨率与原片对齐。 支持的格式:主流视频格式如 mp4、flv、ts、avi、mov、mkv、wmv。 | `video_url:string, format?:string, callback_args?:string, client_token?:string` | [reference/matte-greenscreen-video.md](reference/matte-greenscreen-video.md) | +| matte-portrait-video | 自动识别人物主体,同时移除背景,生成背景透明的视频,适用于背景替换等场景。 输出格式为 WEBM(默认)或 MOV,分辨率与原片对齐。 支持的格式:主流视频格式如 mp4、flv、ts、avi、mov、mkv、wmv。 | `video_url:string, format?:string, callback_args?:string, client_token?:string` | [reference/matte-portrait-video.md](reference/matte-portrait-video.md) | +| probe-video-metadata | 对输入视频 URL 进行探测,输出标准化媒资元信息,覆盖容器层(format_meta)、视频流层(video_stream_meta)与音频流层(audio_stream_meta)。 字段分类参考 ffprobe,并对 VOD 原始返回做精简与统一,便于上层做分辨率/帧率/码率/编码等策略判断。 使用限制:仅支持公网 HTTP/HTTPS URL;输入视频分辨率最高支持 4K。 | `video_url:string, callback_args?:string, client_token?:string` | [reference/probe-video-metadata.md](reference/probe-video-metadata.md) | +| segment-scenes | 依据视频转场与画面变化自动切分场景,输出切片时间轴和(可选)切片文件。 支持格式:MP4、FLV、ASF、RM、RMVB、MPEG、MOV、AVI、MPEGTS、M4S、WMV、3GP、TS、MPG、WEBM、MKV、WM、MPE、VOB、DAT、MP4V、M4V、F4V、MXF、QT 等主流视频格式。 使用限制:单个视频时长不超过 2 小时。 | `video_url:string, enable_clip_fade?:boolean, segment_threshold?:number, min_duration?:number, max_duration?:number, callback_args?:string, client_token?:string` | [reference/segment-scenes.md](reference/segment-scenes.md) | +| video-ocr | 识别视频画面中的字幕/文字内容,输出带时间戳的字幕片段。 支持格式:主流视频格式如 mp4、flv、ts、avi、mov、wmv、mkv。 | `video_url:string, mode?:string, callback_args?:string, client_token?:string` | [reference/video-ocr.md](reference/video-ocr.md) |