@pippit-dev/cli 1.0.26 → 1.0.27
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 +23 -1
- package/checksums.txt +6 -6
- package/cmd/generate_image/generate_image.go +1 -0
- package/cmd/generate_video/generate_video.go +4 -3
- package/cmd/model.go +74 -0
- package/cmd/model_test.go +81 -0
- package/cmd/root.go +1 -0
- package/cmd/short_drama/short_drama.go +1 -0
- package/cmd/source_test.go +179 -0
- package/cmd/submit_run.go +3 -0
- package/cmd/update/update.go +1 -1
- package/cmd/video_tool/video_tool.go +2 -0
- package/dist/checksums.txt +6 -6
- package/internal/common/submit_run.go +38 -1
- package/internal/config/config.go +13 -10
- package/internal/config/config_test.go +3 -0
- package/internal/generate_image/generate_image.go +2 -1
- package/internal/generate_video/generate_video.go +12 -3
- package/internal/generate_video/generate_video_test.go +126 -0
- package/internal/models/describe.go +281 -0
- package/internal/models/describe_test.go +199 -0
- package/internal/models/models.go +220 -0
- package/internal/models/models_test.go +200 -0
- package/internal/short_drama/submit_run.go +2 -0
- package/internal/video_tool/video_tool.go +4 -2
- package/package.json +1 -1
- package/skills/short-drama/SKILL.md +4 -0
- package/skills/xyq-nest-skill/SKILL.md +10 -0
- package/skills/xyq-nest-skill/commands/erase-video-subtitle.md +2 -0
- package/skills/xyq-nest-skill/commands/generate-image.md +2 -0
- package/skills/xyq-nest-skill/commands/generate-video.md +31 -7
- package/skills/xyq-nest-skill/commands/model.md +59 -0
- package/skills/xyq-nest-skill/commands/video-super-resolution.md +2 -0
- package/skills/xyq-nest-skill/scripts/ensure-cli.js +2 -2
- package/skills/xyq-nest-skill/scripts/install.md +1 -1
package/README.md
CHANGED
|
@@ -27,6 +27,7 @@
|
|
|
27
27
|
| 登录授权 | `status` / `login` / `logout` | [授权](skills/xyq-nest-skill/commands/auth.md) |
|
|
28
28
|
| 个人 Canvas 画布与节点编辑 | `canvas` | [画布](skills/xyq-nest-skill/commands/canvas.md) |
|
|
29
29
|
| 生图、参考图编辑 | `generate-image` | [图片](skills/xyq-nest-skill/commands/generate-image.md) |
|
|
30
|
+
| 查看可用视频模型、参数配置 | `model list` / `model describe` | [模型发现](skills/xyq-nest-skill/commands/model.md) |
|
|
30
31
|
| 生视频、首尾帧 | `generate-video` | [视频](skills/xyq-nest-skill/commands/generate-video.md) |
|
|
31
32
|
| 视频超分 | `video-super-resolution` | [超分](skills/xyq-nest-skill/commands/video-super-resolution.md) |
|
|
32
33
|
| 擦字幕 | `erase-video-subtitle` | [擦字幕](skills/xyq-nest-skill/commands/erase-video-subtitle.md) |
|
|
@@ -209,6 +210,17 @@ pippit-tool-cli generate-image \
|
|
|
209
210
|
|
|
210
211
|
图片支持 `.jpg`、`.jpeg`、`.png`、`.gif`、`.bmp`、`.webp`、`.svg`。CLI 会在提交前校验 prompt、model 必填、ratio 整数格式、generate-image-count 非负和文件后缀。
|
|
211
212
|
|
|
213
|
+
## 视频模型发现
|
|
214
|
+
|
|
215
|
+
```bash
|
|
216
|
+
pippit-tool-cli model list
|
|
217
|
+
pippit-tool-cli model search MiniMax
|
|
218
|
+
pippit-tool-cli model describe MiniMax-H3
|
|
219
|
+
pippit-tool-cli model list --refresh
|
|
220
|
+
```
|
|
221
|
+
|
|
222
|
+
使用当前登录凭证查询服务端;成功结果按账号和环境隔离缓存 5 分钟,`--refresh` 强制刷新。失败时提示重试,不回退静态列表或过期缓存。`describe` 将比例枚举转换为可直接传给 `--ratio` 的字符串,并整理分辨率、时长和素材限制;不展示内部 `config_key`,未知比例枚举跳过。生成仍由服务端校验。详见 [模型发现](skills/xyq-nest-skill/commands/model.md)。
|
|
223
|
+
|
|
212
224
|
## 生视频 CLI
|
|
213
225
|
|
|
214
226
|
`generate-video` 会上传本地参考图片、视频和音频,然后向视频片段 Agent 提交生视频请求:
|
|
@@ -227,7 +239,7 @@ pippit-tool-cli generate-video \
|
|
|
227
239
|
--resolution "720p"
|
|
228
240
|
```
|
|
229
241
|
|
|
230
|
-
命令输出 `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
|
|
242
|
+
命令输出 `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 和文件后缀;模型、比例、分辨率等语义校验由服务端处理。
|
|
231
243
|
|
|
232
244
|
首尾帧生视频时,按首帧、尾帧的顺序传入两次 `--image`,并设置 `--generate-type 1`:
|
|
233
245
|
|
|
@@ -292,3 +304,13 @@ pippit-tool-cli query-result \
|
|
|
292
304
|
原生 CLI 命令通过 `pippit-tool-cli login` 打开小云雀网页授权,并把本机设备专属凭证保存到系统安全凭证库;Access Key 不会显示在终端。可用 `pippit-tool-cli status` 查看状态、`pippit-tool-cli logout` 清除本机登录。
|
|
293
305
|
|
|
294
306
|
CI 或 Agent 可继续显式设置 `XYQ_ACCESS_KEY`,它会覆盖本机网页登录凭证;配置错误时不会静默回退到个人登录。会话提交和查询共享上述凭据。
|
|
307
|
+
|
|
308
|
+
### 宿主来源统计
|
|
309
|
+
|
|
310
|
+
调用 Skill 提交接口的命令均支持可选 `--source`:`submit-run`、`generate-image`、`generate-video`、`video-super-resolution`、`erase-video-subtitle`、`short-drama +submit-run`。
|
|
311
|
+
|
|
312
|
+
由宿主 Agent 根据实际环境静默填写稳定标识,例如豆包办公 `doubao_office`、WorkBuddy `workbuddy`、Codex `codex`。其它来源可使用其真实产品标识;来源未知时省略,不询问用户,也不从 prompt 猜测。该值去掉首尾空白后写入请求顶层 `platform`,仅供统计,不参与创作、模型选择或鉴权;未提供/空值时不发送该字段。不会自动读取环境变量、持久化来源或影响查询、上传、下载、Canvas 命令。
|
|
313
|
+
|
|
314
|
+
```bash
|
|
315
|
+
pippit-tool-cli generate-video --prompt "小猫在花园散步" --model Seedance_2.0_mini --source workbuddy
|
|
316
|
+
```
|
package/checksums.txt
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
1
|
+
730f01e4c20d9848690579384fb3c635f74d5b0c4938c53a470159eb0798ad56 pippit-tool-cli-1.0.27-darwin-amd64.tar.gz
|
|
2
|
+
03520884774e9082f5ef070044fa87ff70f18c52fe8d1a4bf4d665509c3af023 pippit-tool-cli-1.0.27-darwin-arm64.tar.gz
|
|
3
|
+
70348ce883d4223cf6ee0789bd0a72635bfef857d832c54586290eb956a1eb1e pippit-tool-cli-1.0.27-linux-amd64.tar.gz
|
|
4
|
+
d548d7ff144ee723c6420e1d3977c8defbbb92cc19ee86cdb7f021a3812d881c pippit-tool-cli-1.0.27-linux-arm64.tar.gz
|
|
5
|
+
ced13bd604d19987d3d71cb3a8d5518afee64420496dea1b646c19e34efdd23c pippit-tool-cli-1.0.27-windows-amd64.zip
|
|
6
|
+
aeee3a5159843c0b64e453c19a3a7c3dd805f4f0eeafdb08a0e36c604829602f pippit-tool-cli-1.0.27-windows-arm64.zip
|
|
@@ -44,6 +44,7 @@ func NewCommand(stdout, stderr io.Writer, runner *common.Runner) *cobra.Command
|
|
|
44
44
|
cmd.SetOut(stdout)
|
|
45
45
|
cmd.SetErr(stderr)
|
|
46
46
|
flags := cmd.Flags()
|
|
47
|
+
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)")
|
|
47
48
|
flags.StringVar(&opts.Prompt, "prompt", "", "image generation prompt")
|
|
48
49
|
flags.StringArrayVar(&opts.ImagePaths, "image", nil, "local reference image path; repeat for multiple images")
|
|
49
50
|
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")
|
|
@@ -42,14 +42,15 @@ func NewCommand(stdout, stderr io.Writer, runner *common.Runner) *cobra.Command
|
|
|
42
42
|
cmd.SetOut(stdout)
|
|
43
43
|
cmd.SetErr(stderr)
|
|
44
44
|
flags := cmd.Flags()
|
|
45
|
+
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)")
|
|
45
46
|
flags.StringVar(&opts.Prompt, "prompt", "", "video generation prompt")
|
|
46
47
|
flags.StringArrayVar(&opts.ImagePaths, "image", nil, "local reference image path; repeat for multiple images")
|
|
47
48
|
flags.StringArrayVar(&opts.VideoPaths, "video", nil, "local reference video path; repeat for multiple videos")
|
|
48
49
|
flags.StringArrayVar(&opts.AudioPaths, "audio", nil, "local reference audio path; repeat for multiple audios")
|
|
49
50
|
flags.IntVar(&durationSec, "duration", 0, "video duration in seconds")
|
|
50
51
|
flags.StringVar(&opts.Ratio, "ratio", "", "video ratio, such as 9:16, 16:9, 3:4, 4:3")
|
|
51
|
-
flags.StringVar(&opts.Model, "model", "", "video model;
|
|
52
|
-
flags.StringVar(&opts.Resolution, "resolution", "", "video resolution
|
|
53
|
-
flags.Int64Var(&generateType, "generate-type", 0, "generation type passed to the service; set 1 for first-and-last-frame generation and provide
|
|
52
|
+
flags.StringVar(&opts.Model, "model", "", "video model key; use 'model list' to discover available models")
|
|
53
|
+
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")
|
|
54
|
+
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")
|
|
54
55
|
return cmd
|
|
55
56
|
}
|
package/cmd/model.go
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
package cmd
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
"fmt"
|
|
5
|
+
"io"
|
|
6
|
+
"strings"
|
|
7
|
+
"time"
|
|
8
|
+
|
|
9
|
+
"github.com/Pippit-dev/pippit-cli/internal/common"
|
|
10
|
+
"github.com/Pippit-dev/pippit-cli/internal/models"
|
|
11
|
+
"github.com/spf13/cobra"
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
func newModelCommand(stdout, stderr io.Writer, runner *common.Runner) *cobra.Command {
|
|
15
|
+
var refresh bool
|
|
16
|
+
var modelType string
|
|
17
|
+
service := models.NewService(runner)
|
|
18
|
+
query := func(cmd *cobra.Command, args []string, describe bool) error {
|
|
19
|
+
if modelType != "video" {
|
|
20
|
+
return fmt.Errorf("当前仅支持 --type video")
|
|
21
|
+
}
|
|
22
|
+
result, err := service.Get(cmd.Context(), refresh)
|
|
23
|
+
if err != nil {
|
|
24
|
+
return err
|
|
25
|
+
}
|
|
26
|
+
if result.Warning != "" {
|
|
27
|
+
fmt.Fprintln(stderr, result.Warning)
|
|
28
|
+
}
|
|
29
|
+
output := map[string]any{
|
|
30
|
+
"scene": result.Catalog.Scene,
|
|
31
|
+
"cached": result.Cached, "fetched_at": result.FetchedAt,
|
|
32
|
+
"expires_at": result.FetchedAt.Add(models.CacheTTL).Format(time.RFC3339Nano),
|
|
33
|
+
}
|
|
34
|
+
if describe {
|
|
35
|
+
model, err := result.Catalog.Describe(strings.Join(args, " "))
|
|
36
|
+
if err != nil {
|
|
37
|
+
return err
|
|
38
|
+
}
|
|
39
|
+
output["model"] = model
|
|
40
|
+
} else {
|
|
41
|
+
output["models"] = result.Catalog.Search(strings.Join(args, " "))
|
|
42
|
+
}
|
|
43
|
+
return common.WriteJSON(stdout, output)
|
|
44
|
+
}
|
|
45
|
+
cmd := &cobra.Command{
|
|
46
|
+
Use: "model [key]",
|
|
47
|
+
Short: "Discover available video models and their server configuration",
|
|
48
|
+
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.",
|
|
49
|
+
Args: cobra.MaximumNArgs(1),
|
|
50
|
+
RunE: withErrorLog("model", nil, func(cmd *cobra.Command, args []string) error {
|
|
51
|
+
return query(cmd, args, len(args) > 0)
|
|
52
|
+
}),
|
|
53
|
+
}
|
|
54
|
+
cmd.SetOut(stdout)
|
|
55
|
+
cmd.SetErr(stderr)
|
|
56
|
+
cmd.PersistentFlags().BoolVar(&refresh, "refresh", false, "refresh the 5-minute local model cache")
|
|
57
|
+
cmd.PersistentFlags().StringVarP(&modelType, "type", "t", "video", "model type (currently video only)")
|
|
58
|
+
cmd.AddCommand(&cobra.Command{
|
|
59
|
+
Use: "list [query]", Aliases: []string{"search"},
|
|
60
|
+
Short: "List video models, optionally filtered by key or name",
|
|
61
|
+
Args: cobra.MaximumNArgs(1),
|
|
62
|
+
RunE: withErrorLog("model list", nil, func(cmd *cobra.Command, args []string) error {
|
|
63
|
+
return query(cmd, args, false)
|
|
64
|
+
}),
|
|
65
|
+
})
|
|
66
|
+
cmd.AddCommand(&cobra.Command{
|
|
67
|
+
Use: "describe <key>", Short: "Show model parameters with CLI-ready ratios, defaults, and limits",
|
|
68
|
+
Args: cobra.ExactArgs(1),
|
|
69
|
+
RunE: withErrorLog("model describe", nil, func(cmd *cobra.Command, args []string) error {
|
|
70
|
+
return query(cmd, args, true)
|
|
71
|
+
}),
|
|
72
|
+
})
|
|
73
|
+
return cmd
|
|
74
|
+
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
package cmd
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
"bytes"
|
|
5
|
+
"encoding/json"
|
|
6
|
+
"net/http"
|
|
7
|
+
"net/http/httptest"
|
|
8
|
+
"strings"
|
|
9
|
+
"testing"
|
|
10
|
+
|
|
11
|
+
"github.com/Pippit-dev/pippit-cli/internal/config"
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
func TestModelCommands(t *testing.T) {
|
|
15
|
+
t.Setenv("HOME", t.TempDir())
|
|
16
|
+
t.Setenv("XDG_CACHE_HOME", t.TempDir())
|
|
17
|
+
t.Setenv("LocalAppData", t.TempDir())
|
|
18
|
+
requests := 0
|
|
19
|
+
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
20
|
+
requests++
|
|
21
|
+
if r.URL.Path != config.GetAvailableModelListPath {
|
|
22
|
+
t.Errorf("path=%s", r.URL.Path)
|
|
23
|
+
}
|
|
24
|
+
_, _ = 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}]}}}`))
|
|
25
|
+
}))
|
|
26
|
+
defer server.Close()
|
|
27
|
+
for i, args := range [][]string{
|
|
28
|
+
{"model", "list"},
|
|
29
|
+
{"model", "search", "MiniMax", "--type", "video"},
|
|
30
|
+
{"model", "describe", "MiniMax-H3"},
|
|
31
|
+
{"model", "MiniMax-H3"},
|
|
32
|
+
{"model", "list", "--refresh"},
|
|
33
|
+
} {
|
|
34
|
+
var stdout, stderr bytes.Buffer
|
|
35
|
+
cfg := config.Load()
|
|
36
|
+
cfg.BaseURL, cfg.AccessKey = server.URL, "model-command-test-key"
|
|
37
|
+
root := newRootCommand(&stdout, &stderr, newRootRunner(cfg))
|
|
38
|
+
root.SetArgs(args)
|
|
39
|
+
if err := root.Execute(); err != nil {
|
|
40
|
+
t.Fatalf("%v: %v", args, err)
|
|
41
|
+
}
|
|
42
|
+
var output map[string]json.RawMessage
|
|
43
|
+
if err := json.Unmarshal(stdout.Bytes(), &output); err != nil {
|
|
44
|
+
t.Fatal(err)
|
|
45
|
+
}
|
|
46
|
+
if _, exists := output["config_key"]; exists {
|
|
47
|
+
t.Fatal("internal config key must not be exposed")
|
|
48
|
+
}
|
|
49
|
+
if i == 2 || i == 3 {
|
|
50
|
+
if bytes.Contains(output["model"], []byte("supported_ratio_list")) || !bytes.Contains(output["model"], []byte(`"default":"9:16"`)) {
|
|
51
|
+
t.Fatalf("ratio must use CLI strings: %s", stdout.String())
|
|
52
|
+
}
|
|
53
|
+
if !bytes.Contains(output["model"], []byte(`"audio_total_limit":0`)) {
|
|
54
|
+
t.Fatalf("missing config: %s", stdout.String())
|
|
55
|
+
}
|
|
56
|
+
} else if !bytes.Contains(output["models"], []byte("MiniMax-H3")) {
|
|
57
|
+
t.Fatalf("missing model: %s", stdout.String())
|
|
58
|
+
}
|
|
59
|
+
want := 1
|
|
60
|
+
if i == 4 {
|
|
61
|
+
want = 2
|
|
62
|
+
}
|
|
63
|
+
if requests != want {
|
|
64
|
+
t.Fatalf("requests=%d want=%d", requests, want)
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
func TestModelHelpDoesNotRequireLoginOrRequest(t *testing.T) {
|
|
70
|
+
for _, args := range [][]string{{"model", "--help"}, {"generate-video", "--help"}} {
|
|
71
|
+
var stdout, stderr bytes.Buffer
|
|
72
|
+
root := NewRootCommand(&stdout, &stderr)
|
|
73
|
+
root.SetArgs(args)
|
|
74
|
+
if err := root.Execute(); err != nil {
|
|
75
|
+
t.Fatal(err)
|
|
76
|
+
}
|
|
77
|
+
if !strings.Contains(stdout.String(), "list") {
|
|
78
|
+
t.Fatalf("help must show discovery: %s", stdout.String())
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
}
|
package/cmd/root.go
CHANGED
|
@@ -72,6 +72,7 @@ func newRootCommand(stdout, stderr io.Writer, runner *common.Runner) *cobra.Comm
|
|
|
72
72
|
root.AddCommand(canvascmd.NewCommand(stdout, stderr, runner))
|
|
73
73
|
root.AddCommand(newDownloadResultCommand(stdout, stderr, runner))
|
|
74
74
|
root.AddCommand(newGetCreditBalanceCommand(stdout, stderr, runner))
|
|
75
|
+
root.AddCommand(newModelCommand(stdout, stderr, runner))
|
|
75
76
|
root.AddCommand(newGetThreadCommand(stdout, stderr, runner))
|
|
76
77
|
root.AddCommand(newSubmitRunCommand(stdout, stderr, runner))
|
|
77
78
|
root.AddCommand(newUploadFileCommand(stdout, stderr, runner))
|
|
@@ -55,6 +55,7 @@ func newShortDramaSubmitRunCommand(stdout, stderr io.Writer, runner *common.Runn
|
|
|
55
55
|
}
|
|
56
56
|
cmd.SetOut(stdout)
|
|
57
57
|
cmd.SetErr(stderr)
|
|
58
|
+
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)")
|
|
58
59
|
cmd.Flags().StringVar(&opts.Message, "message", "", "message to send to the short drama agent")
|
|
59
60
|
cmd.Flags().StringVar(&opts.ThreadID, "thread-id", "", "existing thread ID; omit to create a new thread")
|
|
60
61
|
cmd.Flags().StringArrayVar(&opts.AssetIDs, "asset-ids", nil, "asset ID to attach; repeat for multiple assets")
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
package cmd
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
"bytes"
|
|
5
|
+
"encoding/json"
|
|
6
|
+
"fmt"
|
|
7
|
+
"net/http"
|
|
8
|
+
"net/http/httptest"
|
|
9
|
+
"os"
|
|
10
|
+
"path/filepath"
|
|
11
|
+
"reflect"
|
|
12
|
+
"strings"
|
|
13
|
+
"testing"
|
|
14
|
+
|
|
15
|
+
"github.com/Pippit-dev/pippit-cli/internal/config"
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
func TestSkillSubmitSourcePreservesCreativeRequest(t *testing.T) {
|
|
19
|
+
for _, name := range []string{"submit-run", "generate-image", "generate-video", "video-super-resolution", "erase-video-subtitle", "short-drama"} {
|
|
20
|
+
t.Run(name, func(t *testing.T) {
|
|
21
|
+
video := filepath.Join(t.TempDir(), "input.mp4")
|
|
22
|
+
if err := os.WriteFile(video, []byte("test-video"), 0600); err != nil {
|
|
23
|
+
t.Fatal(err)
|
|
24
|
+
}
|
|
25
|
+
args := map[string][]string{
|
|
26
|
+
"submit-run": {"submit-run", "--message", " 保留原文\nworkbuddy 只是内容 ", "--thread-id", "skill_existing", "--asset-ids", "asset_1"},
|
|
27
|
+
"generate-image": {"generate-image", "--prompt", "cat", "--model", "seedream_5.0_pro", "--ratio", "6", "--resolution", "4K"},
|
|
28
|
+
"generate-video": {"generate-video", "--prompt", "cat", "--model", "Seedance_2.0_mini", "--duration", "4", "--ratio", "16:9"},
|
|
29
|
+
"video-super-resolution": {"video-super-resolution", "--video", video, "--output-resolution", "1080p"},
|
|
30
|
+
"erase-video-subtitle": {"erase-video-subtitle", "--video", video},
|
|
31
|
+
"short-drama": {"short-drama", "+submit-run", "--message", "剧本", "--thread-id", "skill_existing", "--asset-ids", "asset_1"},
|
|
32
|
+
}[name]
|
|
33
|
+
var received map[string]any
|
|
34
|
+
var query string
|
|
35
|
+
response := `{"ret":"0","data":{"run":{"thread_id":"thread_1","run_id":"run_1"},"web_thread_link":"https://xyq.example/thread_1"}}`
|
|
36
|
+
submitCount := 0
|
|
37
|
+
uploadFailure := false
|
|
38
|
+
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
39
|
+
switch r.URL.Path {
|
|
40
|
+
case config.SubmitRunPath:
|
|
41
|
+
submitCount++
|
|
42
|
+
if r.Method != http.MethodPost || r.Header.Get("Authorization") != "Bearer test-token" {
|
|
43
|
+
t.Error("method or authorization changed")
|
|
44
|
+
}
|
|
45
|
+
if err := json.NewDecoder(r.Body).Decode(&received); err != nil {
|
|
46
|
+
t.Error(err)
|
|
47
|
+
}
|
|
48
|
+
query = r.URL.RawQuery
|
|
49
|
+
_, _ = w.Write([]byte(response))
|
|
50
|
+
case "/api/biz/v1/skill/upload_file":
|
|
51
|
+
if err := r.ParseMultipartForm(1 << 20); err != nil {
|
|
52
|
+
t.Error(err)
|
|
53
|
+
}
|
|
54
|
+
if r.FormValue("platform") != "" || r.FormValue("source") != "" || r.URL.Query().Has("source") || r.URL.Query().Has("platform") {
|
|
55
|
+
t.Error("host attribution leaked into upload")
|
|
56
|
+
}
|
|
57
|
+
if uploadFailure {
|
|
58
|
+
_, _ = w.Write([]byte(`{"ret":"1","errmsg":"upload rejected"}`))
|
|
59
|
+
return
|
|
60
|
+
}
|
|
61
|
+
_, _ = w.Write([]byte(`{"ret":"0","data":{"pippit_asset_id":"video_1"}}`))
|
|
62
|
+
default:
|
|
63
|
+
t.Errorf("unexpected request path: %s", r.URL.Path)
|
|
64
|
+
}
|
|
65
|
+
}))
|
|
66
|
+
defer server.Close()
|
|
67
|
+
execute := func(sourceFlags ...string) string {
|
|
68
|
+
t.Helper()
|
|
69
|
+
received = nil
|
|
70
|
+
var stdout, stderr bytes.Buffer
|
|
71
|
+
root := newTestRootCommand(t, &stdout, &stderr, server.URL)
|
|
72
|
+
commandArgs := append([]string(nil), args...)
|
|
73
|
+
commandArgs = append(commandArgs, sourceFlags...)
|
|
74
|
+
root.SetArgs(commandArgs)
|
|
75
|
+
if err := root.Execute(); err != nil {
|
|
76
|
+
t.Fatal(err)
|
|
77
|
+
}
|
|
78
|
+
if received == nil {
|
|
79
|
+
t.Fatal("no skill submission received")
|
|
80
|
+
}
|
|
81
|
+
return stdout.String()
|
|
82
|
+
}
|
|
83
|
+
wantOutput := execute()
|
|
84
|
+
if _, exists := received["platform"]; exists {
|
|
85
|
+
t.Fatal("missing source must omit platform")
|
|
86
|
+
}
|
|
87
|
+
wantBody, wantQuery := received, query
|
|
88
|
+
for i, tc := range []struct {
|
|
89
|
+
flags []string
|
|
90
|
+
want string
|
|
91
|
+
}{
|
|
92
|
+
{[]string{"--source", "workbuddy"}, "workbuddy"},
|
|
93
|
+
{[]string{"--source", " doubao_office "}, "doubao_office"},
|
|
94
|
+
{[]string{"--source", "codex"}, "codex"},
|
|
95
|
+
{[]string{"--source", "新的宿主"}, "新的宿主"},
|
|
96
|
+
{[]string{"--source", ""}, ""},
|
|
97
|
+
{[]string{"--source", " \t "}, ""},
|
|
98
|
+
{[]string{"--source=workbuddy"}, "workbuddy"},
|
|
99
|
+
{[]string{"--source="}, ""},
|
|
100
|
+
{[]string{"--source=codex", "--source=workbuddy"}, "workbuddy"},
|
|
101
|
+
{[]string{"--source=codex", "--source="}, ""},
|
|
102
|
+
{[]string{"--source", "\u3000codex\u00a0"}, "codex"},
|
|
103
|
+
{[]string{"--source", "host\"&=?/\\测试\nnext"}, "host\"&=?/\\测试\nnext"},
|
|
104
|
+
} {
|
|
105
|
+
t.Run(fmt.Sprintf("source_%02d", i), func(t *testing.T) {
|
|
106
|
+
if got := execute(tc.flags...); got != wantOutput {
|
|
107
|
+
t.Errorf("source changed output: %s", got)
|
|
108
|
+
}
|
|
109
|
+
value, exists := received["platform"]
|
|
110
|
+
if (tc.want == "" && exists) || (tc.want != "" && value != tc.want) {
|
|
111
|
+
t.Errorf("platform = %#v (present=%v), want=%q", value, exists, tc.want)
|
|
112
|
+
}
|
|
113
|
+
delete(received, "platform")
|
|
114
|
+
if !reflect.DeepEqual(received, wantBody) || query != wantQuery {
|
|
115
|
+
t.Errorf("source changed creative request or ROI query: %#v / %s", received, query)
|
|
116
|
+
}
|
|
117
|
+
})
|
|
118
|
+
}
|
|
119
|
+
// A fresh invocation without source must not reuse previous attribution.
|
|
120
|
+
execute()
|
|
121
|
+
if _, exists := received["platform"]; exists {
|
|
122
|
+
t.Error("source leaked into next invocation")
|
|
123
|
+
}
|
|
124
|
+
for _, failure := range []struct{ name, body string }{
|
|
125
|
+
{"business_error", `{"ret":"1","errmsg":"rejected"}`},
|
|
126
|
+
{"invalid_json", `invalid-json`},
|
|
127
|
+
} {
|
|
128
|
+
t.Run(failure.name, func(t *testing.T) {
|
|
129
|
+
response = failure.body
|
|
130
|
+
submitCount = 0
|
|
131
|
+
var stdout, stderr bytes.Buffer
|
|
132
|
+
root := newTestRootCommand(t, &stdout, &stderr, server.URL)
|
|
133
|
+
root.SetArgs(append(append([]string(nil), args...), "--source=codex"))
|
|
134
|
+
if err := root.Execute(); err == nil {
|
|
135
|
+
t.Fatal("source swallowed submit error")
|
|
136
|
+
}
|
|
137
|
+
if submitCount != 1 {
|
|
138
|
+
t.Errorf("submission count = %d, want 1", submitCount)
|
|
139
|
+
}
|
|
140
|
+
if received["platform"] != "codex" {
|
|
141
|
+
t.Error("failed submission lost source")
|
|
142
|
+
}
|
|
143
|
+
})
|
|
144
|
+
}
|
|
145
|
+
if name == "video-super-resolution" || name == "erase-video-subtitle" {
|
|
146
|
+
t.Run("upload_failure", func(t *testing.T) {
|
|
147
|
+
uploadFailure = true
|
|
148
|
+
submitCount = 0
|
|
149
|
+
var stdout, stderr bytes.Buffer
|
|
150
|
+
root := newTestRootCommand(t, &stdout, &stderr, server.URL)
|
|
151
|
+
root.SetArgs(append(append([]string(nil), args...), "--source=workbuddy"))
|
|
152
|
+
if err := root.Execute(); err == nil {
|
|
153
|
+
t.Fatal("source swallowed upload error")
|
|
154
|
+
}
|
|
155
|
+
if submitCount != 0 {
|
|
156
|
+
t.Errorf("submitted %d times after failed upload", submitCount)
|
|
157
|
+
}
|
|
158
|
+
})
|
|
159
|
+
}
|
|
160
|
+
})
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
func TestSourceHelpWithoutCredentials(t *testing.T) {
|
|
165
|
+
for _, args := range [][]string{
|
|
166
|
+
{"submit-run"}, {"generate-image"}, {"generate-video"},
|
|
167
|
+
{"video-super-resolution"}, {"erase-video-subtitle"}, {"short-drama", "+submit-run"},
|
|
168
|
+
} {
|
|
169
|
+
var stdout, stderr bytes.Buffer
|
|
170
|
+
root := newRootCommand(&stdout, &stderr, nil)
|
|
171
|
+
root.SetArgs(append(args, "--help"))
|
|
172
|
+
if err := root.Execute(); err != nil {
|
|
173
|
+
t.Fatal(err)
|
|
174
|
+
}
|
|
175
|
+
if !strings.Contains(stdout.String(), "--source") {
|
|
176
|
+
t.Errorf("help for %v missing --source", args)
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
}
|
package/cmd/submit_run.go
CHANGED
|
@@ -11,6 +11,7 @@ import (
|
|
|
11
11
|
|
|
12
12
|
func newSubmitRunCommand(stdout, stderr io.Writer, runner *common.Runner) *cobra.Command {
|
|
13
13
|
var body struct {
|
|
14
|
+
Platform string `json:"platform,omitempty"`
|
|
14
15
|
Message string `json:"message"`
|
|
15
16
|
ThreadID string `json:"thread_id,omitempty"`
|
|
16
17
|
AssetIDs []string `json:"asset_ids,omitempty"`
|
|
@@ -23,6 +24,7 @@ func newSubmitRunCommand(stdout, stderr io.Writer, runner *common.Runner) *cobra
|
|
|
23
24
|
if strings.TrimSpace(body.Message) == "" {
|
|
24
25
|
return fmt.Errorf("缺少必填参数 --message")
|
|
25
26
|
}
|
|
27
|
+
body.Platform = strings.TrimSpace(body.Platform)
|
|
26
28
|
// Preserve the original message and identifiers; the backend owns routing.
|
|
27
29
|
result, err := common.SubmitRun(cmd.Context(), "submit-run", body, runner)
|
|
28
30
|
if err != nil {
|
|
@@ -33,6 +35,7 @@ func newSubmitRunCommand(stdout, stderr io.Writer, runner *common.Runner) *cobra
|
|
|
33
35
|
}
|
|
34
36
|
cmd.SetOut(stdout)
|
|
35
37
|
cmd.SetErr(stderr)
|
|
38
|
+
cmd.Flags().StringVar(&body.Platform, "source", "", "optional host agent/platform identifier for statistics only; filled silently by the host agent (e.g. doubao_office, workbuddy, codex)")
|
|
36
39
|
cmd.Flags().StringVar(&body.Message, "message", "", "original message to send (required)")
|
|
37
40
|
cmd.Flags().StringVar(&body.ThreadID, "thread-id", "", "existing thread ID; omit to create a new thread")
|
|
38
41
|
cmd.Flags().StringArrayVar(&body.AssetIDs, "asset-ids", nil, "asset ID to attach; repeat for multiple assets")
|
package/cmd/update/update.go
CHANGED
|
@@ -63,7 +63,7 @@ func runUpdate(stdout, stderr io.Writer) error {
|
|
|
63
63
|
if err != nil {
|
|
64
64
|
return fmt.Errorf("准备替换当前可执行文件失败: %w", err)
|
|
65
65
|
}
|
|
66
|
-
if err := runInheritEnv(stderr, []string{"PIPPIT_CLI_SKIP_SKILLS=1"}, "npm", "install", "-g", pkg); err != nil {
|
|
66
|
+
if err := runInheritEnv(stderr, []string{"PIPPIT_CLI_SKIP_SKILLS=1"}, "npm", "install", "-g", "--prefer-online", pkg); err != nil {
|
|
67
67
|
restore()
|
|
68
68
|
return fmt.Errorf("更新 pippit-tool-cli 失败: %w", err)
|
|
69
69
|
}
|
|
@@ -31,6 +31,7 @@ func NewSuperResolutionCommand(stdout, stderr io.Writer, runner *common.Runner)
|
|
|
31
31
|
cmd.SetOut(stdout)
|
|
32
32
|
cmd.SetErr(stderr)
|
|
33
33
|
flags := cmd.Flags()
|
|
34
|
+
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)")
|
|
34
35
|
flags.StringVar(&opts.VideoPath, "video", "", "local input video path")
|
|
35
36
|
flags.StringVar(&opts.OutputResolution, "output-resolution", "", "output resolution passed to the service: 720p, 1080p, 2k, or 4k")
|
|
36
37
|
flags.StringVar(&opts.ToolVersion, "tool-version", "", "optional tool version passed to the service: standard, professional_v1, or professional_v2")
|
|
@@ -58,6 +59,7 @@ func NewEraseSubtitleCommand(stdout, stderr io.Writer, runner *common.Runner) *c
|
|
|
58
59
|
cmd.SetOut(stdout)
|
|
59
60
|
cmd.SetErr(stderr)
|
|
60
61
|
flags := cmd.Flags()
|
|
62
|
+
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)")
|
|
61
63
|
flags.StringVar(&opts.VideoPath, "video", "", "local input video path")
|
|
62
64
|
return cmd
|
|
63
65
|
}
|
package/dist/checksums.txt
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
1
|
+
730f01e4c20d9848690579384fb3c635f74d5b0c4938c53a470159eb0798ad56 pippit-tool-cli-1.0.27-darwin-amd64.tar.gz
|
|
2
|
+
03520884774e9082f5ef070044fa87ff70f18c52fe8d1a4bf4d665509c3af023 pippit-tool-cli-1.0.27-darwin-arm64.tar.gz
|
|
3
|
+
70348ce883d4223cf6ee0789bd0a72635bfef857d832c54586290eb956a1eb1e pippit-tool-cli-1.0.27-linux-amd64.tar.gz
|
|
4
|
+
d548d7ff144ee723c6420e1d3977c8defbbb92cc19ee86cdb7f021a3812d881c pippit-tool-cli-1.0.27-linux-arm64.tar.gz
|
|
5
|
+
ced13bd604d19987d3d71cb3a8d5518afee64420496dea1b646c19e34efdd23c pippit-tool-cli-1.0.27-windows-amd64.zip
|
|
6
|
+
aeee3a5159843c0b64e453c19a3a7c3dd805f4f0eeafdb08a0e36c604829602f pippit-tool-cli-1.0.27-windows-arm64.zip
|
|
@@ -2,7 +2,10 @@ package common
|
|
|
2
2
|
|
|
3
3
|
import (
|
|
4
4
|
"context"
|
|
5
|
+
"encoding/json"
|
|
5
6
|
"fmt"
|
|
7
|
+
"net/url"
|
|
8
|
+
"strings"
|
|
6
9
|
|
|
7
10
|
"github.com/Pippit-dev/pippit-cli/internal/config"
|
|
8
11
|
)
|
|
@@ -37,11 +40,36 @@ type SubmitRunResponseRun struct {
|
|
|
37
40
|
|
|
38
41
|
// SubmitRun sends a submit_run request and validates its shared response envelope.
|
|
39
42
|
func SubmitRun(ctx context.Context, command string, body any, runner *Runner) (*SubmitRunResult, error) {
|
|
43
|
+
return submitRun(ctx, command, SubmitRunPath(runner), body, runner)
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// SubmitRunWithBabiParam carries client attribution in the same query field as Web.
|
|
47
|
+
func SubmitRunWithBabiParam(ctx context.Context, command string, body any, runner *Runner, babiParam map[string]string) (*SubmitRunResult, error) {
|
|
48
|
+
path, err := url.Parse(SubmitRunPath(runner))
|
|
49
|
+
if err != nil {
|
|
50
|
+
return nil, fmt.Errorf("解析 submit_run 路径失败: %w", err)
|
|
51
|
+
}
|
|
52
|
+
query, err := url.ParseQuery(path.RawQuery)
|
|
53
|
+
if err != nil {
|
|
54
|
+
return nil, fmt.Errorf("解析 submit_run 查询参数失败: %w", err)
|
|
55
|
+
}
|
|
56
|
+
raw, err := json.Marshal(babiParam)
|
|
57
|
+
if err != nil {
|
|
58
|
+
return nil, fmt.Errorf("序列化 babi_param 失败: %w", err)
|
|
59
|
+
}
|
|
60
|
+
if !query.Has("babi_param") {
|
|
61
|
+
query.Set("babi_param", string(raw))
|
|
62
|
+
}
|
|
63
|
+
path.RawQuery = query.Encode()
|
|
64
|
+
return submitRun(ctx, command, path.String(), body, runner)
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
func submitRun(ctx context.Context, command, path string, body any, runner *Runner) (*SubmitRunResult, error) {
|
|
40
68
|
if runner == nil || runner.Client == nil {
|
|
41
69
|
return nil, fmt.Errorf("%s 运行器客户端缺失", command)
|
|
42
70
|
}
|
|
43
71
|
var resp SubmitRunResponse
|
|
44
|
-
if err := runner.Client.SendRequest(ctx,
|
|
72
|
+
if err := runner.Client.SendRequest(ctx, path, body, &resp); err != nil {
|
|
45
73
|
return nil, fmt.Errorf("提交 %s 请求失败: %w", command, err)
|
|
46
74
|
}
|
|
47
75
|
if resp.Ret != "0" {
|
|
@@ -71,3 +99,12 @@ func SubmitRunPath(runner *Runner) string {
|
|
|
71
99
|
}
|
|
72
100
|
return config.SubmitRunPath
|
|
73
101
|
}
|
|
102
|
+
|
|
103
|
+
// WithSubmitRunSource adds optional host attribution to a newly constructed skill request.
|
|
104
|
+
// The API consumes this metadata as platform; it is not part of the creative input.
|
|
105
|
+
func WithSubmitRunSource(body map[string]any, source string) map[string]any {
|
|
106
|
+
if source = strings.TrimSpace(source); source != "" {
|
|
107
|
+
body["platform"] = source
|
|
108
|
+
}
|
|
109
|
+
return body
|
|
110
|
+
}
|
|
@@ -13,6 +13,7 @@ const (
|
|
|
13
13
|
DefaultAuthStoreServiceName = "pippit-cli"
|
|
14
14
|
SubmitRunPath = "/api/biz/v1/skill/submit_run"
|
|
15
15
|
GetCreditBalancePath = "/api/biz/v1/skill/get_credit_balance"
|
|
16
|
+
GetAvailableModelListPath = "/api/biz/v1/skill/get_available_model_list"
|
|
16
17
|
GetThreadPath = "/api/biz/v1/skill/get_thread"
|
|
17
18
|
UploadFilePath = "/api/biz/v1/skill/upload_file"
|
|
18
19
|
ListThreadFilePath = "/api/biz/v1/skill/list_thread_file"
|
|
@@ -30,11 +31,12 @@ type Config struct {
|
|
|
30
31
|
}
|
|
31
32
|
|
|
32
33
|
type Paths struct {
|
|
33
|
-
SubmitRun
|
|
34
|
-
GetCreditBalance
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
34
|
+
SubmitRun string
|
|
35
|
+
GetCreditBalance string
|
|
36
|
+
GetAvailableModelList string
|
|
37
|
+
GetThread string
|
|
38
|
+
UploadFile string
|
|
39
|
+
ListThreadFile string
|
|
38
40
|
}
|
|
39
41
|
|
|
40
42
|
// Load resolves the built-in runtime config.
|
|
@@ -45,11 +47,12 @@ func Load() *Config {
|
|
|
45
47
|
AuthTTL: DefaultAuthTTL,
|
|
46
48
|
AccessKey: strings.TrimSpace(os.Getenv(EnvXYQAccessKey)),
|
|
47
49
|
Paths: &Paths{
|
|
48
|
-
SubmitRun:
|
|
49
|
-
GetCreditBalance:
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
50
|
+
SubmitRun: SubmitRunPath,
|
|
51
|
+
GetCreditBalance: GetCreditBalancePath,
|
|
52
|
+
GetAvailableModelList: GetAvailableModelListPath,
|
|
53
|
+
GetThread: GetThreadPath,
|
|
54
|
+
UploadFile: UploadFilePath,
|
|
55
|
+
ListThreadFile: ListThreadFilePath,
|
|
53
56
|
},
|
|
54
57
|
}
|
|
55
58
|
}
|
|
@@ -23,6 +23,9 @@ func TestLoadUsesDefaultConfig(t *testing.T) {
|
|
|
23
23
|
if cfg.Paths.GetCreditBalance != GetCreditBalancePath {
|
|
24
24
|
t.Fatalf("GetCreditBalance path = %q, want %q", cfg.Paths.GetCreditBalance, GetCreditBalancePath)
|
|
25
25
|
}
|
|
26
|
+
if cfg.Paths.GetAvailableModelList != GetAvailableModelListPath {
|
|
27
|
+
t.Fatalf("GetAvailableModelList path = %q, want %q", cfg.Paths.GetAvailableModelList, GetAvailableModelListPath)
|
|
28
|
+
}
|
|
26
29
|
}
|
|
27
30
|
|
|
28
31
|
func TestLoadReadsAccessKey(t *testing.T) {
|
|
@@ -23,6 +23,7 @@ const ratioUsage = "enum values: 0=原始比例/自动, 2=16:9(横屏), 13=21:9(
|
|
|
23
23
|
|
|
24
24
|
// Options is the stable command-facing request shape for generate-image.
|
|
25
25
|
type Options struct {
|
|
26
|
+
Source string
|
|
26
27
|
Prompt string
|
|
27
28
|
ImagePaths []string
|
|
28
29
|
Model string
|
|
@@ -151,7 +152,7 @@ func buildSubmitRunBody(opts *Options, imageAssetIDs []string) map[string]any {
|
|
|
151
152
|
if len(imageAssetIDs) > 0 {
|
|
152
153
|
body["asset_ids"] = imageAssetIDs
|
|
153
154
|
}
|
|
154
|
-
return body
|
|
155
|
+
return common.WithSubmitRunSource(body, opts.Source)
|
|
155
156
|
}
|
|
156
157
|
|
|
157
158
|
func parseRatio(raw string) (*int, error) {
|