Skip to content
Merged
9 changes: 9 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
# 模型发现输出约定

- 模型查询详情应提供可直接用于生成命令的参数值;数字枚举按对应 IDL 显式转换,不按数组位置或展示文案推断。
- 覆盖 IDL 已定义且生成入口可消费的枚举;未知枚举直接跳过,不猜测或回退到其他值。已定义但没有 CLI 请求表达方式的枚举须明确识别为不可用。
- 内部配置指纹不向用户输出;原始配置缓存和用户展示结构分离,保留可选字段缺失与零值的区别。
- 模型发现验收必须分别核对用户确认的预期模型集合与真实接口返回值转换;CLI 与接口同时少返回模型不能判为完整通过。请求携带 PPE 头也不能替代实际路由和生效配置证据。
- `Seedance_2.0_mini` 和 `Seedance_2.0_mini_lite` 允许省略生成分辨率,由服务端默认 `720p`;CLI 不主动补值,不因模型查询缺少分辨率维度而将该参数标为必填或伪造配置。

- 安装引导的最小兼容修复只补必需命令检查;可选参数已有 `--help` 判断约定时复用该约定,不另增能力返回字段或升级条件。
24 changes: 23 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
| 登录授权 | `status` / `login` / `logout` | [授权](skills/xyq-nest-skill/commands/auth.md) |
| 个人 Canvas 画布与节点编辑 | `canvas` | [画布](skills/xyq-nest-skill/commands/canvas.md) |
| 生图、参考图编辑 | `generate-image` | [图片](skills/xyq-nest-skill/commands/generate-image.md) |
| 查看可用视频模型、参数配置 | `model list` / `model describe` | [模型发现](skills/xyq-nest-skill/commands/model.md) |
| 生视频、首尾帧 | `generate-video` | [视频](skills/xyq-nest-skill/commands/generate-video.md) |
| 视频超分 | `video-super-resolution` | [超分](skills/xyq-nest-skill/commands/video-super-resolution.md) |
| 擦字幕 | `erase-video-subtitle` | [擦字幕](skills/xyq-nest-skill/commands/erase-video-subtitle.md) |
Expand Down Expand Up @@ -209,6 +210,17 @@ pippit-tool-cli generate-image \

图片支持 `.jpg`、`.jpeg`、`.png`、`.gif`、`.bmp`、`.webp`、`.svg`。CLI 会在提交前校验 prompt、model 必填、ratio 整数格式、generate-image-count 非负和文件后缀。

## 视频模型发现

```bash
pippit-tool-cli model list
pippit-tool-cli model search MiniMax
pippit-tool-cli model describe MiniMax-H3
pippit-tool-cli model list --refresh
```

使用当前登录凭证查询服务端;成功结果按账号和环境隔离缓存 5 分钟,`--refresh` 强制刷新。失败时提示重试,不回退静态列表或过期缓存。`describe` 将比例枚举转换为可直接传给 `--ratio` 的字符串,并整理分辨率、时长和素材限制;不展示内部 `config_key`,未知比例枚举跳过。生成仍由服务端校验。详见 [模型发现](skills/xyq-nest-skill/commands/model.md)。

## 生视频 CLI

`generate-video` 会上传本地参考图片、视频和音频,然后向视频片段 Agent 提交生视频请求:
Expand All @@ -227,7 +239,7 @@ pippit-tool-cli generate-video \
--resolution "720p"
```

命令输出 `thread_id`、`run_id` 和 `web_thread_link`。提交生视频 HTTP 请求时,参考图、参考视频和参考音频会使用上传接口返回的 `pippit_asset_id`,并分别写入 `video_part_tool_param.images`、`video_part_tool_param.videos` 和 `video_part_tool_param.audios`。图片支持 `.jpg`、`.jpeg`、`.png`、`.gif`、`.bmp`、`.webp`、`.svg`;视频支持 `.mp4`、`.avi`、`.mov`、`.wmv`、`.flv`、`.webm`、`.mkv`、`.m4v`;音频仅支持 `.mp3`、`.wav`。普通用户支持模型 `Seedance_2.0_mini_lite`;`seedance2.0_vision`、`seedance2.0_fast_vision`、`Seedance_2.0_mini` 和 `Seedance_2.5` 为 VIP 专属模型。CLI 会在提交前校验 prompt 和文件后缀;模型、比例、分辨率等语义校验由服务端处理。
命令输出 `thread_id`、`run_id` 和 `web_thread_link`。提交生视频 HTTP 请求时,参考图、参考视频和参考音频会使用上传接口返回的 `pippit_asset_id`,并分别写入 `video_part_tool_param.images`、`video_part_tool_param.videos` 和 `video_part_tool_param.audios`。图片支持 `.jpg`、`.jpeg`、`.png`、`.gif`、`.bmp`、`.webp`、`.svg`;视频支持 `.mp4`、`.avi`、`.mov`、`.wmv`、`.flv`、`.webm`、`.mkv`、`.m4v`;音频仅支持 `.mp3`、`.wav`。当前可用模型通过 `model list` 查询,参数详情通过 `model describe MODEL_KEY` 查询。CLI 会在提交前校验 prompt 和文件后缀;模型、比例、分辨率等语义校验由服务端处理。

首尾帧生视频时,按首帧、尾帧的顺序传入两次 `--image`,并设置 `--generate-type 1`:

Expand Down Expand Up @@ -292,3 +304,13 @@ pippit-tool-cli query-result \
原生 CLI 命令通过 `pippit-tool-cli login` 打开小云雀网页授权,并把本机设备专属凭证保存到系统安全凭证库;Access Key 不会显示在终端。可用 `pippit-tool-cli status` 查看状态、`pippit-tool-cli logout` 清除本机登录。

CI 或 Agent 可继续显式设置 `XYQ_ACCESS_KEY`,它会覆盖本机网页登录凭证;配置错误时不会静默回退到个人登录。会话提交和查询共享上述凭据。

### 宿主来源统计

调用 Skill 提交接口的命令均支持可选 `--source`:`submit-run`、`generate-image`、`generate-video`、`video-super-resolution`、`erase-video-subtitle`、`short-drama +submit-run`。

由宿主 Agent 根据实际环境静默填写稳定标识,例如豆包办公 `doubao_office`、WorkBuddy `workbuddy`、Codex `codex`。其它来源可使用其真实产品标识;来源未知时省略,不询问用户,也不从 prompt 猜测。该值去掉首尾空白后写入请求顶层 `platform`,仅供统计,不参与创作、模型选择或鉴权;未提供/空值时不发送该字段。不会自动读取环境变量、持久化来源或影响查询、上传、下载、Canvas 命令。

```bash
pippit-tool-cli generate-video --prompt "小猫在花园散步" --model Seedance_2.0_mini --source workbuddy
```
1 change: 1 addition & 0 deletions cmd/generate_image/generate_image.go
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ func NewCommand(stdout, stderr io.Writer, runner *common.Runner) *cobra.Command
cmd.SetOut(stdout)
cmd.SetErr(stderr)
flags := cmd.Flags()
flags.StringVar(&opts.Source, "source", "", "optional host agent/platform identifier for statistics only; filled silently by the host agent (e.g. doubao_office, workbuddy, codex)")
flags.StringVar(&opts.Prompt, "prompt", "", "image generation prompt")
flags.StringArrayVar(&opts.ImagePaths, "image", nil, "local reference image path; repeat for multiple images")
flags.StringVar(&opts.Model, "model", "", "image model; supported: seedream_5.0_pro, seedream_5.0, seedream_4.3, nova2, seedream_4.5, seedream_4.1, seedream_4")
Expand Down
7 changes: 4 additions & 3 deletions cmd/generate_video/generate_video.go
Original file line number Diff line number Diff line change
Expand Up @@ -42,14 +42,15 @@ func NewCommand(stdout, stderr io.Writer, runner *common.Runner) *cobra.Command
cmd.SetOut(stdout)
cmd.SetErr(stderr)
flags := cmd.Flags()
flags.StringVar(&opts.Source, "source", "", "optional host agent/platform identifier for statistics only; filled silently by the host agent (e.g. doubao_office, workbuddy, codex)")
flags.StringVar(&opts.Prompt, "prompt", "", "video generation prompt")
flags.StringArrayVar(&opts.ImagePaths, "image", nil, "local reference image path; repeat for multiple images")
flags.StringArrayVar(&opts.VideoPaths, "video", nil, "local reference video path; repeat for multiple videos")
flags.StringArrayVar(&opts.AudioPaths, "audio", nil, "local reference audio path; repeat for multiple audios")
flags.IntVar(&durationSec, "duration", 0, "video duration in seconds")
flags.StringVar(&opts.Ratio, "ratio", "", "video ratio, such as 9:16, 16:9, 3:4, 4:3")
flags.StringVar(&opts.Model, "model", "", "video model; normal users: Seedance_2.0_mini_lite; VIP-only: seedance2.0_vision, seedance2.0_fast_vision, Seedance_2.0_mini, Seedance_2.5")
flags.StringVar(&opts.Resolution, "resolution", "", "video resolution, such as 720p, 1080p")
flags.Int64Var(&generateType, "generate-type", 0, "generation type passed to the service; set 1 for first-and-last-frame generation and provide two --image values in first-frame, last-frame order")
flags.StringVar(&opts.Model, "model", "", "video model key; use 'model list' to discover available models")
flags.StringVar(&opts.Resolution, "resolution", "", "video resolution; optional for Seedance_2.0_mini and Seedance_2.0_mini_lite (server defaults to 720p); use 'model describe <key>' for current configuration")
flags.Int64Var(&generateType, "generate-type", 0, "generation type passed to the service; set 1 for first-and-last-frame generation and provide --image values in first-frame, last-frame order; MiniMax and Wan also accept a single first-frame image in this mode")
return cmd
}
74 changes: 74 additions & 0 deletions cmd/model.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
package cmd

import (
"fmt"
"io"
"strings"
"time"

"github.com/Pippit-dev/pippit-cli/internal/common"
"github.com/Pippit-dev/pippit-cli/internal/models"
"github.com/spf13/cobra"
)

func newModelCommand(stdout, stderr io.Writer, runner *common.Runner) *cobra.Command {
var refresh bool
var modelType string
service := models.NewService(runner)
query := func(cmd *cobra.Command, args []string, describe bool) error {
if modelType != "video" {
return fmt.Errorf("当前仅支持 --type video")
}
result, err := service.Get(cmd.Context(), refresh)
if err != nil {
return err
}
if result.Warning != "" {
fmt.Fprintln(stderr, result.Warning)
}
output := map[string]any{
"scene": result.Catalog.Scene,
"cached": result.Cached, "fetched_at": result.FetchedAt,
"expires_at": result.FetchedAt.Add(models.CacheTTL).Format(time.RFC3339Nano),
}
if describe {
model, err := result.Catalog.Describe(strings.Join(args, " "))
if err != nil {
return err
}
output["model"] = model
} else {
output["models"] = result.Catalog.Search(strings.Join(args, " "))
}
return common.WriteJSON(stdout, output)
}
cmd := &cobra.Command{
Use: "model [key]",
Short: "Discover available video models and their server configuration",
Long: "Query available video models using your current credentials. Successful queries are cached locally for 5 minutes. Use --refresh to bypass the cache; retry if a query fails.",
Args: cobra.MaximumNArgs(1),
RunE: withErrorLog("model", nil, func(cmd *cobra.Command, args []string) error {
return query(cmd, args, len(args) > 0)
}),
}
cmd.SetOut(stdout)
cmd.SetErr(stderr)
cmd.PersistentFlags().BoolVar(&refresh, "refresh", false, "refresh the 5-minute local model cache")
cmd.PersistentFlags().StringVarP(&modelType, "type", "t", "video", "model type (currently video only)")
cmd.AddCommand(&cobra.Command{
Use: "list [query]", Aliases: []string{"search"},
Short: "List video models, optionally filtered by key or name",
Args: cobra.MaximumNArgs(1),
RunE: withErrorLog("model list", nil, func(cmd *cobra.Command, args []string) error {
return query(cmd, args, false)
}),
})
cmd.AddCommand(&cobra.Command{
Use: "describe <key>", Short: "Show model parameters with CLI-ready ratios, defaults, and limits",
Args: cobra.ExactArgs(1),
RunE: withErrorLog("model describe", nil, func(cmd *cobra.Command, args []string) error {
return query(cmd, args, true)
}),
})
return cmd
}
81 changes: 81 additions & 0 deletions cmd/model_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
package cmd

import (
"bytes"
"encoding/json"
"net/http"
"net/http/httptest"
"strings"
"testing"

"github.com/Pippit-dev/pippit-cli/internal/config"
)

func TestModelCommands(t *testing.T) {
t.Setenv("HOME", t.TempDir())
t.Setenv("XDG_CACHE_HOME", t.TempDir())
t.Setenv("LocalAppData", t.TempDir())
requests := 0
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
requests++
if r.URL.Path != config.GetAvailableModelListPath {
t.Errorf("path=%s", r.URL.Path)
}
_, _ = w.Write([]byte(`{"ret":"0","data":{"scene":"web_turbo_video_generator","config_key":"key","config":{"models":[{"key":"MiniMax-H3","name":"MiniMax","kind":"video","supported_ratio_list":[0,2,3],"default_ratio":3,"audio_total_limit":0}]}}}`))
}))
defer server.Close()
for i, args := range [][]string{
{"model", "list"},
{"model", "search", "MiniMax", "--type", "video"},
{"model", "describe", "MiniMax-H3"},
{"model", "MiniMax-H3"},
{"model", "list", "--refresh"},
} {
var stdout, stderr bytes.Buffer
cfg := config.Load()
cfg.BaseURL, cfg.AccessKey = server.URL, "model-command-test-key"
root := newRootCommand(&stdout, &stderr, newRootRunner(cfg))
root.SetArgs(args)
if err := root.Execute(); err != nil {
t.Fatalf("%v: %v", args, err)
}
var output map[string]json.RawMessage
if err := json.Unmarshal(stdout.Bytes(), &output); err != nil {
t.Fatal(err)
}
if _, exists := output["config_key"]; exists {
t.Fatal("internal config key must not be exposed")
}
if i == 2 || i == 3 {
if bytes.Contains(output["model"], []byte("supported_ratio_list")) || !bytes.Contains(output["model"], []byte(`"default":"9:16"`)) {
t.Fatalf("ratio must use CLI strings: %s", stdout.String())
}
if !bytes.Contains(output["model"], []byte(`"audio_total_limit":0`)) {
t.Fatalf("missing config: %s", stdout.String())
}
} else if !bytes.Contains(output["models"], []byte("MiniMax-H3")) {
t.Fatalf("missing model: %s", stdout.String())
}
want := 1
if i == 4 {
want = 2
}
if requests != want {
t.Fatalf("requests=%d want=%d", requests, want)
}
}
}

func TestModelHelpDoesNotRequireLoginOrRequest(t *testing.T) {
for _, args := range [][]string{{"model", "--help"}, {"generate-video", "--help"}} {
var stdout, stderr bytes.Buffer
root := NewRootCommand(&stdout, &stderr)
root.SetArgs(args)
if err := root.Execute(); err != nil {
t.Fatal(err)
}
if !strings.Contains(stdout.String(), "list") {
t.Fatalf("help must show discovery: %s", stdout.String())
}
}
}
1 change: 1 addition & 0 deletions cmd/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,7 @@ func newRootCommand(stdout, stderr io.Writer, runner *common.Runner) *cobra.Comm
root.AddCommand(canvascmd.NewCommand(stdout, stderr, runner))
root.AddCommand(newDownloadResultCommand(stdout, stderr, runner))
root.AddCommand(newGetCreditBalanceCommand(stdout, stderr, runner))
root.AddCommand(newModelCommand(stdout, stderr, runner))
root.AddCommand(newGetThreadCommand(stdout, stderr, runner))
root.AddCommand(newSubmitRunCommand(stdout, stderr, runner))
root.AddCommand(newUploadFileCommand(stdout, stderr, runner))
Expand Down
1 change: 1 addition & 0 deletions cmd/short_drama/short_drama.go
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ func newShortDramaSubmitRunCommand(stdout, stderr io.Writer, runner *common.Runn
}
cmd.SetOut(stdout)
cmd.SetErr(stderr)
cmd.Flags().StringVar(&opts.Source, "source", "", "optional host agent/platform identifier for statistics only; filled silently by the host agent (e.g. doubao_office, workbuddy, codex)")
cmd.Flags().StringVar(&opts.Message, "message", "", "message to send to the short drama agent")
cmd.Flags().StringVar(&opts.ThreadID, "thread-id", "", "existing thread ID; omit to create a new thread")
cmd.Flags().StringArrayVar(&opts.AssetIDs, "asset-ids", nil, "asset ID to attach; repeat for multiple assets")
Expand Down
Loading
Loading