@pippit-dev/cli 1.0.34 → 1.0.37
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 +44 -3
- package/checksums.txt +6 -6
- package/cmd/generate_video/generate_video.go +13 -1
- package/cmd/generate_video_test.go +126 -1
- package/cmd/marketing.go +5 -1
- package/cmd/marketing_test.go +11 -1
- package/cmd/root.go +11 -0
- package/cmd/source_test.go +24 -0
- package/cmd/submit_run_test.go +1 -1
- package/cmd/update/update.go +23 -16
- package/cmd/update/update_test.go +23 -9
- package/dist/checksums.txt +6 -6
- package/internal/common/host_source.go +36 -0
- package/internal/common/host_source_test.go +27 -0
- package/internal/common/video_part.go +4 -0
- package/internal/generate_video/generate_video.go +9 -1
- package/internal/generate_video/query_result.go +6 -0
- package/internal/models/describe.go +8 -0
- package/package.json +1 -1
- package/scripts/install-wizard.js +16 -6
- package/scripts/install.js +1 -1
- package/scripts/telemetry.js +2 -1
- package/skills/short-drama/SKILL.md +2 -0
- package/skills/short-drama/scripts/install.md +2 -0
- package/skills/xyq-marketing-skill/SKILL.md +9 -1
- package/skills/xyq-marketing-skill/agents/openai.yaml +1 -1
- package/skills/xyq-marketing-skill/references/api.md +2 -0
- package/skills/xyq-marketing-skill/scripts/marketing.js +36 -7
- package/skills/xyq-nest-skill/SKILL.md +3 -1
- package/skills/xyq-nest-skill/agents/openai.yaml +1 -1
- package/skills/xyq-nest-skill/commands/generate-video.md +58 -7
- package/skills/xyq-nest-skill/commands/model.md +1 -1
- package/skills/xyq-nest-skill/commands/query-result.md +10 -0
- package/skills/xyq-nest-skill/scripts/install.md +2 -0
package/README.md
CHANGED
|
@@ -274,7 +274,7 @@ pippit-tool-cli generate-video \
|
|
|
274
274
|
--resolution "720p"
|
|
275
275
|
```
|
|
276
276
|
|
|
277
|
-
命令输出 `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
|
|
277
|
+
命令输出 `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 和文件后缀,仅传入非空 `--draft-task-id` 时允许省略 prompt;模型、比例、分辨率等语义校验由服务端处理。
|
|
278
278
|
|
|
279
279
|
首尾帧生视频时,按首帧、尾帧的顺序传入两次 `--image`,并设置 `--generate-type 1`:
|
|
280
280
|
|
|
@@ -292,6 +292,25 @@ pippit-tool-cli generate-video \
|
|
|
292
292
|
|
|
293
293
|
`--generate-type` 可选,填写后原样写入 `video_part_tool_param.generate_type`;值 `1` 表示首尾帧生成。CLI 保持图片上传和请求中的输入顺序,不在本地校验该参数的枚举值,具体能力与约束由服务端决定。
|
|
294
294
|
|
|
295
|
+
### Seedance 2.5 Draft
|
|
296
|
+
|
|
297
|
+
复用 `generate-video` 分两次提交。需要目标服务端支持 Draft 协议和无 prompt 的成片请求。
|
|
298
|
+
|
|
299
|
+
```bash
|
|
300
|
+
# 样片
|
|
301
|
+
pippit-tool-cli generate-video --model Seedance_2.5_draft --draft \
|
|
302
|
+
--prompt "小猫钓鱼视频" --task-type reference --duration 10 --ratio 16:9
|
|
303
|
+
pippit-tool-cli query-result --thread-id DRAFT_THREAD_ID --run-id DRAFT_RUN_ID --download-dir ./draft
|
|
304
|
+
|
|
305
|
+
# 用户预览后要求生成成片:使用 videos[].draft_task_id 原值
|
|
306
|
+
pippit-tool-cli generate-video --model Seedance_2.5_draft --draft-task-id DRAFT_TASK_ID
|
|
307
|
+
pippit-tool-cli query-result --thread-id FINAL_THREAD_ID --run-id FINAL_RUN_ID --download-dir ./final
|
|
308
|
+
```
|
|
309
|
+
|
|
310
|
+
`query-result` 在视频结果中保留可选的 `draft`、`draft_task_id`,继续返回 `download_url` 和 `output_path`。两阶段分别计费,下游固定生成 480p 样片和 1080p 成片;样片创建后 7 天内可转成片。CLI 不自动续跑,不要求重复提示词和素材。
|
|
311
|
+
|
|
312
|
+
新增 `--task-type`、`--seed` 透传生成模式与 seed。完整参数与两阶段示例见 [生视频命令](skills/xyq-nest-skill/commands/generate-video.md)。
|
|
313
|
+
|
|
295
314
|
## 视频处理工具 CLI
|
|
296
315
|
|
|
297
316
|
`video-super-resolution` 会上传一个本地视频并提交视频超分任务:
|
|
@@ -340,11 +359,33 @@ pippit-tool-cli query-result \
|
|
|
340
359
|
|
|
341
360
|
CI 或 Agent 可继续显式设置 `XYQ_ACCESS_KEY`,它会覆盖本机网页登录凭证;配置错误时不会静默回退到个人登录。会话提交和查询共享上述凭据。
|
|
342
361
|
|
|
362
|
+
### 安装与更新来源统计
|
|
363
|
+
|
|
364
|
+
宿主执行安装/更新时,按可信运行环境静默传入真实稳定标识,未知时省略;不固定为某个平台,不询问用户。以下 `HOST` 是占位符:
|
|
365
|
+
|
|
366
|
+
```bash
|
|
367
|
+
pippit-tool-cli install --source HOST
|
|
368
|
+
npx @pippit-dev/cli install --source HOST
|
|
369
|
+
pippit-tool-cli update --source HOST
|
|
370
|
+
```
|
|
371
|
+
|
|
372
|
+
直接使用 `npm install -g @pippit-dev/cli` 时,可由宿主在该次命令的环境中设置 `PIPPIT_CLI_SOURCE`。install/update 的显式 `--source` 优先于此环境变量;首尾空白去除,显式空值清空来源。该值不持久化;生成命令运行环境中若也存在此变量,同样会读取它作为宿主来源,具体兜底规则见下文。
|
|
373
|
+
|
|
374
|
+
`report_telemetry` 新增可选请求字段 `host_platform` 上报宿主标识;原有 `source` 保留 `npm_install`、`npx_install`、`cli_update`,`event` 区分 install/update,`platform` 仍表示操作系统。当前仅完成 CLI 字段上报,服务端协议和指标接收仍需适配。帮助不安装也不上报,`PIPPIT_CLI_DISABLE_TELEMETRY=1` 仍可关闭上报。内部 `install-cli.js` 仅安装二进制,沿用不安装 Skill、不上报的原有行为。
|
|
375
|
+
|
|
343
376
|
### 宿主来源统计
|
|
344
377
|
|
|
345
|
-
|
|
378
|
+
生成提交命令均支持可选 `--source`:`submit-run`、`generate-image`、`generate-video`、`video-super-resolution`、`erase-video-subtitle`、`short-drama +submit-run`、`marketing generate`。
|
|
379
|
+
|
|
380
|
+
由宿主 Agent 根据实际环境静默填写稳定标识,例如豆包办公 `doubao_office`、WorkBuddy `workbuddy`、Codex `codex`。其它来源可使用其真实产品标识;来源未知时省略,不询问用户,也不从 prompt 猜测。该值去掉首尾空白后写入请求顶层 `platform`,仅供统计,不参与创作、模型选择或鉴权;无法解析来源或显式空值时不发送该字段。省略 `--source` 时,CLI 按以下顺序尽力补齐来源:
|
|
381
|
+
|
|
382
|
+
1. 宿主显式设置的 `PIPPIT_CLI_SOURCE`。
|
|
383
|
+
2. 运行标记:`CODEX_THREAD_ID` / `CODEX_SESSION_ID` → `codex`,`CLAUDECODE` → `claude_code`,`CURSOR_AGENT` → `cursor`,`GEMINI_CLI` → `gemini_cli`。
|
|
384
|
+
3. 标记缺失、为 0/false 或不同宿主标记冲突时不猜测,省略来源。
|
|
385
|
+
|
|
386
|
+
显式 `--source` 始终优先,显式空值可关闭本次归因。仅消费环境标记是否存在,不上传会话标识,不根据 API Key、已安装软件、普通终端名或用户 prompt 猜来源,也不持久化。查询、上传、下载、Canvas 不附加来源。营销脚本的预览与提交采用相同规则;原生 update 也复用该解析,仍写入 `host_platform`,保留旧 source。豆包办公、WorkBuddy 等尚无已核实运行标记的宿主,应优先主动传参或设置上述环境变量。
|
|
346
387
|
|
|
347
|
-
|
|
388
|
+
Cursor 标记依据:[官方终端文档](https://docs.cursor.com/en/agent/terminal);Gemini 标记依据:[官方命令文档](https://geminicli.com/docs/reference/commands/)。Codex 标记已在本机运行环境核实;Claude Code 标记已在本机安装产物核实。
|
|
348
389
|
|
|
349
390
|
```bash
|
|
350
391
|
pippit-tool-cli generate-video --prompt "小猫在花园散步" --model Seedance_2.0_mini --source workbuddy
|
package/checksums.txt
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
1
|
+
ef54466711739e6d6060dbdc5dd186d3953f76a65e613d028c5f4ee2e33bd9f4 pippit-tool-cli-1.0.37-darwin-amd64.tar.gz
|
|
2
|
+
56c413f48de1e411f0cfefbe20f7589a0c4baaa66c49e640ef034dc683eec4a6 pippit-tool-cli-1.0.37-darwin-arm64.tar.gz
|
|
3
|
+
48647c2756b5a6007385bfcaa4566907e7f80098fdd912dbb22314f840cdef19 pippit-tool-cli-1.0.37-linux-amd64.tar.gz
|
|
4
|
+
c7f6c85f512b4ccb4a524515178e5e0cf56009ff8ec0628bc2db780c0f248cfb pippit-tool-cli-1.0.37-linux-arm64.tar.gz
|
|
5
|
+
6d5ab207b53ded67b1be1ee8cd7f7eb7725f3f87a6c83d2115feb0e3acdf5bef pippit-tool-cli-1.0.37-windows-amd64.zip
|
|
6
|
+
d180077635826e968629278da2c17d5e8f733238db94e74ece273d2be36e6b9f pippit-tool-cli-1.0.37-windows-arm64.zip
|
|
@@ -14,6 +14,8 @@ func NewCommand(stdout, stderr io.Writer, runner *common.Runner) *cobra.Command
|
|
|
14
14
|
opts := &internalgen.Options{}
|
|
15
15
|
var durationSec int
|
|
16
16
|
var generateType int64
|
|
17
|
+
var seed int64
|
|
18
|
+
var draft bool
|
|
17
19
|
|
|
18
20
|
cmd := &cobra.Command{
|
|
19
21
|
Use: "generate-video",
|
|
@@ -26,6 +28,12 @@ func NewCommand(stdout, stderr io.Writer, runner *common.Runner) *cobra.Command
|
|
|
26
28
|
if cmd.Flags().Changed("generate-type") {
|
|
27
29
|
opts.GenerateType = &generateType
|
|
28
30
|
}
|
|
31
|
+
if cmd.Flags().Changed("seed") {
|
|
32
|
+
opts.Seed = &seed
|
|
33
|
+
}
|
|
34
|
+
if cmd.Flags().Changed("draft") {
|
|
35
|
+
opts.Draft = &draft
|
|
36
|
+
}
|
|
29
37
|
|
|
30
38
|
result, err := internalgen.Run(cmd.Context(), opts, runner)
|
|
31
39
|
if err != nil {
|
|
@@ -43,7 +51,7 @@ func NewCommand(stdout, stderr io.Writer, runner *common.Runner) *cobra.Command
|
|
|
43
51
|
cmd.SetErr(stderr)
|
|
44
52
|
flags := cmd.Flags()
|
|
45
53
|
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)")
|
|
46
|
-
flags.StringVar(&opts.Prompt, "prompt", "", "video generation prompt")
|
|
54
|
+
flags.StringVar(&opts.Prompt, "prompt", "", "video generation prompt; may be omitted when rendering a draft")
|
|
47
55
|
flags.StringArrayVar(&opts.ImagePaths, "image", nil, "local reference image path; repeat for multiple images")
|
|
48
56
|
flags.StringArrayVar(&opts.VideoPaths, "video", nil, "local reference video path; repeat for multiple videos")
|
|
49
57
|
flags.StringArrayVar(&opts.AudioPaths, "audio", nil, "local reference audio path; repeat for multiple audios")
|
|
@@ -52,5 +60,9 @@ func NewCommand(stdout, stderr io.Writer, runner *common.Runner) *cobra.Command
|
|
|
52
60
|
flags.StringVar(&opts.Model, "model", "", "video model key; use 'model list' to discover available models")
|
|
53
61
|
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
62
|
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")
|
|
63
|
+
flags.StringVar(&opts.TaskType, "task-type", "", "video task type passed to the service: auto, reference, edit, extend")
|
|
64
|
+
flags.Int64Var(&seed, "seed", 0, "custom generation seed; omitted when not supplied")
|
|
65
|
+
flags.BoolVar(&draft, "draft", false, "generate a Seedance_2.5_draft preview; --draft=false explicitly requests a final video")
|
|
66
|
+
flags.StringVar(&opts.DraftTaskID, "draft-task-id", "", "original draft_task_id returned by query-result, used to render a final video")
|
|
55
67
|
return cmd
|
|
56
68
|
}
|
|
@@ -189,6 +189,82 @@ func TestGenerateVideoRequiresPrompt(t *testing.T) {
|
|
|
189
189
|
}
|
|
190
190
|
}
|
|
191
191
|
|
|
192
|
+
func TestGenerateVideoDraftParameters(t *testing.T) {
|
|
193
|
+
for _, tt := range []struct {
|
|
194
|
+
name string
|
|
195
|
+
args []string
|
|
196
|
+
want map[string]any
|
|
197
|
+
absent []string
|
|
198
|
+
}{
|
|
199
|
+
{
|
|
200
|
+
name: "preview with explicit zero values",
|
|
201
|
+
args: []string{"--draft", "--prompt", "cat", "--seed", "0", "--generate-type", "0", "--task-type", "reference"},
|
|
202
|
+
want: map[string]any{"draft": true, "prompt": "cat", "seed": float64(0), "generate_type": float64(0), "task_type": "reference"},
|
|
203
|
+
absent: []string{"draft_task_id", "resolution", "ratio", "duration_sec"},
|
|
204
|
+
},
|
|
205
|
+
{
|
|
206
|
+
name: "final with ID only",
|
|
207
|
+
args: []string{"--draft-task-id", "cgt-example-draft"},
|
|
208
|
+
want: map[string]any{"draft_task_id": "cgt-example-draft", "prompt": ""},
|
|
209
|
+
absent: []string{"draft", "resolution", "ratio", "duration_sec", "seed", "task_type", "generate_type", "images", "videos", "audios"},
|
|
210
|
+
},
|
|
211
|
+
{
|
|
212
|
+
name: "explicit false",
|
|
213
|
+
args: []string{"--draft=false", "--draft-task-id", "cgt-example-draft", "--seed", "-1"},
|
|
214
|
+
want: map[string]any{"draft": false, "draft_task_id": "cgt-example-draft", "seed": float64(-1)},
|
|
215
|
+
absent: []string{"resolution", "ratio", "duration_sec"},
|
|
216
|
+
},
|
|
217
|
+
{
|
|
218
|
+
name: "conflicting parameters are left to the service",
|
|
219
|
+
args: []string{"--draft", "--draft-task-id", "cgt-example-draft", "--task-type", "future-mode", "--duration", "-1", "--ratio", "adaptive"},
|
|
220
|
+
want: map[string]any{"draft": true, "draft_task_id": "cgt-example-draft", "task_type": "future-mode", "duration_sec": float64(-1), "ratio": "adaptive"},
|
|
221
|
+
},
|
|
222
|
+
} {
|
|
223
|
+
t.Run(tt.name, func(t *testing.T) {
|
|
224
|
+
submitted := false
|
|
225
|
+
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
226
|
+
if r.URL.Path != "/api/biz/v1/skill/submit_run" {
|
|
227
|
+
t.Fatalf("unexpected path %s", r.URL.Path)
|
|
228
|
+
}
|
|
229
|
+
data, err := io.ReadAll(r.Body)
|
|
230
|
+
if err != nil {
|
|
231
|
+
t.Fatal(err)
|
|
232
|
+
}
|
|
233
|
+
body := decodeJSON(t, data)
|
|
234
|
+
param, ok := body["video_part_tool_param"].(map[string]any)
|
|
235
|
+
if !ok {
|
|
236
|
+
t.Fatalf("missing video_part_tool_param: %#v", body)
|
|
237
|
+
}
|
|
238
|
+
if param["model"] != "Seedance_2.5_draft" || body["agent_name"] != "pippit_video_part_agent" {
|
|
239
|
+
t.Fatalf("unexpected route/model: %#v", body)
|
|
240
|
+
}
|
|
241
|
+
for key, want := range tt.want {
|
|
242
|
+
if param[key] != want {
|
|
243
|
+
t.Fatalf("%s = %#v, want %#v", key, param[key], want)
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
for _, key := range tt.absent {
|
|
247
|
+
if _, exists := param[key]; exists {
|
|
248
|
+
t.Fatalf("%s must stay omitted: %#v", key, param)
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
submitted = true
|
|
252
|
+
_, _ = w.Write([]byte(`{"ret":"0","data":{"run":{"thread_id":"thread_123","run_id":"run_456"}}}`))
|
|
253
|
+
}))
|
|
254
|
+
defer server.Close()
|
|
255
|
+
var stdout, stderr bytes.Buffer
|
|
256
|
+
root := newTestRootCommand(t, &stdout, &stderr, server.URL)
|
|
257
|
+
root.SetArgs(append([]string{"generate-video", "--model", "Seedance_2.5_draft"}, tt.args...))
|
|
258
|
+
if err := root.Execute(); err != nil {
|
|
259
|
+
t.Fatalf("Execute(): %v, stderr=%s", err, stderr.String())
|
|
260
|
+
}
|
|
261
|
+
if !submitted {
|
|
262
|
+
t.Fatal("request not submitted")
|
|
263
|
+
}
|
|
264
|
+
})
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
|
|
192
268
|
func TestGenerateVideoAcceptsReferencesBeyondFormerLimits(t *testing.T) {
|
|
193
269
|
cwd := chdirTemp(t)
|
|
194
270
|
args := []string{"generate-video", "--prompt", "x"}
|
|
@@ -430,7 +506,7 @@ func TestQueryResultDownloadsCompletedVideo(t *testing.T) {
|
|
|
430
506
|
if video["download_url"] != downloadURL || video["output_path"] != outputPath {
|
|
431
507
|
t.Fatalf("video = %#v, want download_url/output_path", video)
|
|
432
508
|
}
|
|
433
|
-
for _, unwanted := range []string{"vid", "asset_id", "title"} {
|
|
509
|
+
for _, unwanted := range []string{"vid", "asset_id", "title", "draft", "draft_task_id"} {
|
|
434
510
|
if _, ok := video[unwanted]; ok {
|
|
435
511
|
t.Fatalf("video = %#v, should not contain %s", video, unwanted)
|
|
436
512
|
}
|
|
@@ -438,6 +514,55 @@ func TestQueryResultDownloadsCompletedVideo(t *testing.T) {
|
|
|
438
514
|
assertFileContent(t, outputPath, "video-data")
|
|
439
515
|
}
|
|
440
516
|
|
|
517
|
+
func TestQueryResultPreservesDraftMetadata(t *testing.T) {
|
|
518
|
+
for _, draft := range []bool{true, false} {
|
|
519
|
+
t.Run(map[bool]string{true: "preview", false: "final"}[draft], func(t *testing.T) {
|
|
520
|
+
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
521
|
+
switch r.URL.Path {
|
|
522
|
+
case "/api/biz/v1/skill/get_thread":
|
|
523
|
+
videoJSON, err := sonic.Marshal(map[string]any{"video": map[string]any{
|
|
524
|
+
"download_url": serverURL(r) + "/video.mp4",
|
|
525
|
+
"draft": draft, "draft_task_id": "cgt-example-draft",
|
|
526
|
+
}})
|
|
527
|
+
if err != nil {
|
|
528
|
+
t.Fatal(err)
|
|
529
|
+
}
|
|
530
|
+
// Agent parts carry their data as a JSON string.
|
|
531
|
+
encoded, err := sonic.Marshal(string(videoJSON))
|
|
532
|
+
if err != nil {
|
|
533
|
+
t.Fatal(err)
|
|
534
|
+
}
|
|
535
|
+
_, _ = w.Write([]byte(`{"ret":"0","data":{"thread":{"thread_id":"draft_thread","run_list":[{"run_id":"draft_run","state":3,"entry_list":[{"artifact":{"content":[{"sub_type":"biz/x_data_video","data":` + string(encoded) + `}]}}]}]}}}`))
|
|
536
|
+
case "/video.mp4":
|
|
537
|
+
_, _ = w.Write([]byte("video-data"))
|
|
538
|
+
default:
|
|
539
|
+
t.Fatalf("unexpected path %s", r.URL.Path)
|
|
540
|
+
}
|
|
541
|
+
}))
|
|
542
|
+
defer server.Close()
|
|
543
|
+
var stdout, stderr bytes.Buffer
|
|
544
|
+
root := newTestRootCommand(t, &stdout, &stderr, server.URL)
|
|
545
|
+
root.SetArgs([]string{"query-result", "--thread-id", "draft_thread", "--run-id", "draft_run", "--download-dir", t.TempDir()})
|
|
546
|
+
if err := root.Execute(); err != nil {
|
|
547
|
+
t.Fatalf("Execute(): %v", err)
|
|
548
|
+
}
|
|
549
|
+
got := decodeJSON(t, stdout.Bytes())
|
|
550
|
+
if got["completed"] != true || got["error_message"] != "" {
|
|
551
|
+
t.Fatalf("unexpected result: %#v", got)
|
|
552
|
+
}
|
|
553
|
+
videos, ok := got["videos"].([]any)
|
|
554
|
+
if !ok || len(videos) != 1 {
|
|
555
|
+
t.Fatalf("unexpected videos: %#v", got)
|
|
556
|
+
}
|
|
557
|
+
video := videos[0].(map[string]any)
|
|
558
|
+
if video["draft"] != draft || video["draft_task_id"] != "cgt-example-draft" {
|
|
559
|
+
t.Fatalf("draft metadata was lost: %#v", video)
|
|
560
|
+
}
|
|
561
|
+
assertFileContent(t, video["output_path"].(string), "video-data")
|
|
562
|
+
})
|
|
563
|
+
}
|
|
564
|
+
}
|
|
565
|
+
|
|
441
566
|
func TestQueryResultIgnoresVideoDataWithoutVideoSubType(t *testing.T) {
|
|
442
567
|
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
443
568
|
switch r.URL.Path {
|
package/cmd/marketing.go
CHANGED
|
@@ -36,7 +36,7 @@ func newMarketingCommand(stdout, stderr io.Writer, runner *common.Runner) *cobra
|
|
|
36
36
|
}
|
|
37
37
|
|
|
38
38
|
func newMarketingAction(action string, stdout, stderr io.Writer, runner *common.Runner) *cobra.Command {
|
|
39
|
-
var requestFile, file, threadID, runID string
|
|
39
|
+
var requestFile, file, threadID, runID, source string
|
|
40
40
|
var execute bool
|
|
41
41
|
var timeout time.Duration
|
|
42
42
|
command := &cobra.Command{Use: action, Args: cobra.NoArgs, Short: "Call marketing " + action}
|
|
@@ -45,6 +45,7 @@ func newMarketingAction(action string, stdout, stderr io.Writer, runner *common.
|
|
|
45
45
|
command.Flags().DurationVar(&timeout, "timeout", 60*time.Second, "request deadline (e.g. 60s)")
|
|
46
46
|
switch action {
|
|
47
47
|
case "generate":
|
|
48
|
+
command.Flags().StringVar(&source, "source", "", "optional host agent/platform identifier for statistics only; filled silently by the host agent (e.g. doubao_office, workbuddy, codex)")
|
|
48
49
|
command.Flags().StringVar(&requestFile, "request", "", "request JSON file, or - for stdin")
|
|
49
50
|
command.Flags().BoolVar(&execute, "execute", false, "submit generation; otherwise preview only")
|
|
50
51
|
case "query":
|
|
@@ -96,6 +97,9 @@ func newMarketingAction(action string, stdout, stderr io.Writer, runner *common.
|
|
|
96
97
|
if json.Unmarshal(value["general_agent_settings"], &settings) != nil || strings.TrimSpace(settings.VideoModel) == "" {
|
|
97
98
|
return fmt.Errorf("general_agent_settings.video_model 必填")
|
|
98
99
|
}
|
|
100
|
+
if platform := strings.TrimSpace(source); platform != "" {
|
|
101
|
+
value["platform"], _ = json.Marshal(platform)
|
|
102
|
+
}
|
|
99
103
|
body = value
|
|
100
104
|
if !execute {
|
|
101
105
|
return common.WriteJSON(stdout, map[string]any{"dry_run": true, "url": config.DefaultBaseURL + marketingPaths[action], "body": body})
|
package/cmd/marketing_test.go
CHANGED
|
@@ -3,6 +3,7 @@ package cmd
|
|
|
3
3
|
import (
|
|
4
4
|
"bytes"
|
|
5
5
|
"context"
|
|
6
|
+
"encoding/json"
|
|
6
7
|
"fmt"
|
|
7
8
|
"io"
|
|
8
9
|
"net/http"
|
|
@@ -49,6 +50,15 @@ func TestMarketingUsesSharedBrowserAuth(t *testing.T) {
|
|
|
49
50
|
if bytes.Contains(data, []byte("browser-secret")) {
|
|
50
51
|
t.Error("credential leaked into payload")
|
|
51
52
|
}
|
|
53
|
+
if action == "generate" {
|
|
54
|
+
var body map[string]any
|
|
55
|
+
if err := json.Unmarshal(data, &body); err != nil || body["platform"] != "codex" || body["message"] != "make an ad" {
|
|
56
|
+
t.Errorf("unexpected marketing payload: %s", data)
|
|
57
|
+
}
|
|
58
|
+
if _, ok := body["source"]; ok {
|
|
59
|
+
t.Error("source must be sent as platform")
|
|
60
|
+
}
|
|
61
|
+
}
|
|
52
62
|
if action == "upload" && !bytes.Contains(data, []byte(`name="file"`)) {
|
|
53
63
|
t.Error("missing multipart file")
|
|
54
64
|
}
|
|
@@ -66,7 +76,7 @@ func TestMarketingUsesSharedBrowserAuth(t *testing.T) {
|
|
|
66
76
|
args := []string{"marketing", action}
|
|
67
77
|
switch action {
|
|
68
78
|
case "generate":
|
|
69
|
-
args = append(args, "--request", "-", "--execute")
|
|
79
|
+
args = append(args, "--request", "-", "--execute", "--source", " codex ")
|
|
70
80
|
root.SetIn(strings.NewReader(marketingRequest))
|
|
71
81
|
case "query":
|
|
72
82
|
args = append(args, "--thread-id", "thread", "--run-id", "run")
|
package/cmd/root.go
CHANGED
|
@@ -62,6 +62,17 @@ func newRootCommand(stdout, stderr io.Writer, runner *common.Runner) *cobra.Comm
|
|
|
62
62
|
SilenceUsage: true,
|
|
63
63
|
SilenceErrors: true,
|
|
64
64
|
}
|
|
65
|
+
// Only commands already exposing --source receive attribution. Explicit empty
|
|
66
|
+
// flags suppress fallback, and no source is persisted between CLI processes.
|
|
67
|
+
root.PersistentPreRunE = func(cmd *cobra.Command, _ []string) error {
|
|
68
|
+
flag := cmd.Flags().Lookup("source")
|
|
69
|
+
if flag != nil && !flag.Changed {
|
|
70
|
+
if source := common.DetectHostSource(); source != "" {
|
|
71
|
+
return cmd.Flags().Set("source", source)
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
return nil
|
|
75
|
+
}
|
|
65
76
|
root.CompletionOptions.DisableDefaultCmd = true
|
|
66
77
|
root.SetVersionTemplate("{{.Version}}\n")
|
|
67
78
|
root.SetOut(stdout)
|
package/cmd/source_test.go
CHANGED
|
@@ -16,6 +16,10 @@ import (
|
|
|
16
16
|
)
|
|
17
17
|
|
|
18
18
|
func TestSkillSubmitSourcePreservesCreativeRequest(t *testing.T) {
|
|
19
|
+
for _, key := range []string{"PIPPIT_CLI_SOURCE", "CODEX_THREAD_ID", "CODEX_SESSION_ID", "CLAUDECODE", "CURSOR_AGENT", "GEMINI_CLI"} {
|
|
20
|
+
t.Setenv(key, "")
|
|
21
|
+
}
|
|
22
|
+
|
|
19
23
|
t.Setenv("HOME", t.TempDir())
|
|
20
24
|
t.Setenv("XDG_CACHE_HOME", t.TempDir())
|
|
21
25
|
t.Setenv("LocalAppData", t.TempDir())
|
|
@@ -121,6 +125,26 @@ func TestSkillSubmitSourcePreservesCreativeRequest(t *testing.T) {
|
|
|
121
125
|
}
|
|
122
126
|
})
|
|
123
127
|
}
|
|
128
|
+
t.Run("host_environment", func(t *testing.T) {
|
|
129
|
+
t.Setenv("CODEX_THREAD_ID", "session-must-not-be-reported")
|
|
130
|
+
execute()
|
|
131
|
+
if received["platform"] != "codex" {
|
|
132
|
+
t.Errorf("runtime host not detected: %#v", received["platform"])
|
|
133
|
+
}
|
|
134
|
+
t.Setenv("PIPPIT_CLI_SOURCE", " workbuddy ")
|
|
135
|
+
execute()
|
|
136
|
+
if received["platform"] != "workbuddy" {
|
|
137
|
+
t.Error("explicit environment must win")
|
|
138
|
+
}
|
|
139
|
+
execute("--source", "doubao_office")
|
|
140
|
+
if received["platform"] != "doubao_office" {
|
|
141
|
+
t.Error("explicit flag must win")
|
|
142
|
+
}
|
|
143
|
+
execute("--source", "")
|
|
144
|
+
if _, exists := received["platform"]; exists {
|
|
145
|
+
t.Error("explicit empty must suppress attribution")
|
|
146
|
+
}
|
|
147
|
+
})
|
|
124
148
|
// A fresh invocation without source must not reuse previous attribution.
|
|
125
149
|
execute()
|
|
126
150
|
if _, exists := received["platform"]; exists {
|
package/cmd/submit_run_test.go
CHANGED
|
@@ -17,7 +17,7 @@ func TestSubmitRunCommandPreservesScriptContract(t *testing.T) {
|
|
|
17
17
|
t.Run(map[bool]string{false: "new", true: "existing_with_assets"}[existing], func(t *testing.T) {
|
|
18
18
|
message := " 根据参考素材生成视频\n保留原文 "
|
|
19
19
|
want := map[string]any{"message": message}
|
|
20
|
-
args := []string{"submit-run", "--message", message}
|
|
20
|
+
args := []string{"submit-run", "--message", message, "--source", ""}
|
|
21
21
|
if existing {
|
|
22
22
|
want["thread_id"] = "skill_original_thread"
|
|
23
23
|
want["asset_ids"] = []any{"asset_original_1", "asset_original_2"}
|
package/cmd/update/update.go
CHANGED
|
@@ -42,17 +42,22 @@ var telemetryHTTPClient = &http.Client{Timeout: telemetryWaitTimeout}
|
|
|
42
42
|
|
|
43
43
|
// NewCommand builds the update command.
|
|
44
44
|
func NewCommand(stdout, stderr io.Writer) *cobra.Command {
|
|
45
|
+
var source string
|
|
45
46
|
cmd := &cobra.Command{
|
|
46
47
|
Use: "update",
|
|
47
48
|
Short: "Update pippit-tool-cli and bundled skills",
|
|
48
49
|
RunE: func(cmd *cobra.Command, _ []string) error {
|
|
49
|
-
|
|
50
|
+
if !cmd.Flags().Changed("source") {
|
|
51
|
+
source = os.Getenv("PIPPIT_CLI_SOURCE")
|
|
52
|
+
}
|
|
53
|
+
return runUpdate(stdout, stderr, source)
|
|
50
54
|
},
|
|
51
55
|
}
|
|
56
|
+
cmd.Flags().StringVar(&source, "source", "", "optional real host identifier reported as host_platform; defaults to PIPPIT_CLI_SOURCE when unset")
|
|
52
57
|
return cmd
|
|
53
58
|
}
|
|
54
59
|
|
|
55
|
-
func runUpdate(stdout, stderr io.Writer) error {
|
|
60
|
+
func runUpdate(stdout, stderr io.Writer, source string) error {
|
|
56
61
|
pkg := os.Getenv("PIPPIT_CLI_INSTALL_PACKAGE")
|
|
57
62
|
if pkg == "" {
|
|
58
63
|
pkg = defaultPackage + "@latest"
|
|
@@ -78,7 +83,7 @@ func runUpdate(stdout, stderr io.Writer) error {
|
|
|
78
83
|
return fmt.Errorf("更新 pippit-tool-cli skills 失败: %w", err)
|
|
79
84
|
}
|
|
80
85
|
|
|
81
|
-
reportBundledSkillTelemetry("update", "cli_update", stderr)
|
|
86
|
+
reportBundledSkillTelemetry("update", "cli_update", source, stderr)
|
|
82
87
|
fmt.Fprintln(stdout, "pippit-tool-cli and skills updated")
|
|
83
88
|
return nil
|
|
84
89
|
}
|
|
@@ -129,27 +134,29 @@ func cleanupLegacyGlobalSkills(globalSkillsDir string) error {
|
|
|
129
134
|
}
|
|
130
135
|
|
|
131
136
|
type telemetryPayload struct {
|
|
132
|
-
Event
|
|
133
|
-
SkillName
|
|
134
|
-
Source
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
137
|
+
Event string `json:"event"`
|
|
138
|
+
SkillName string `json:"skill_name"`
|
|
139
|
+
Source string `json:"source"`
|
|
140
|
+
HostPlatform string `json:"host_platform,omitempty"`
|
|
141
|
+
CliVersion string `json:"cli_version"`
|
|
142
|
+
Platform string `json:"platform"`
|
|
143
|
+
Arch string `json:"arch"`
|
|
138
144
|
}
|
|
139
145
|
|
|
140
|
-
func reportBundledSkillTelemetry(event
|
|
146
|
+
func reportBundledSkillTelemetry(event, source, hostPlatform string, stderr io.Writer) {
|
|
141
147
|
if os.Getenv("PIPPIT_CLI_DISABLE_TELEMETRY") == "1" {
|
|
142
148
|
return
|
|
143
149
|
}
|
|
144
150
|
var wg sync.WaitGroup
|
|
145
151
|
for _, skillName := range telemetrySkillNames {
|
|
146
152
|
payload := telemetryPayload{
|
|
147
|
-
Event:
|
|
148
|
-
SkillName:
|
|
149
|
-
Source:
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
+
Event: event,
|
|
154
|
+
SkillName: skillName,
|
|
155
|
+
Source: source,
|
|
156
|
+
HostPlatform: strings.TrimSpace(hostPlatform),
|
|
157
|
+
CliVersion: telemetryCliVersion(),
|
|
158
|
+
Platform: runtime.GOOS,
|
|
159
|
+
Arch: runtime.GOARCH,
|
|
153
160
|
}
|
|
154
161
|
wg.Add(1)
|
|
155
162
|
go func(payload telemetryPayload) {
|
|
@@ -93,12 +93,13 @@ func TestReportSkillTelemetry(t *testing.T) {
|
|
|
93
93
|
|
|
94
94
|
t.Setenv("PIPPIT_CLI_TELEMETRY_BASE_URL", server.URL+"/")
|
|
95
95
|
err := reportSkillTelemetry(telemetryPayload{
|
|
96
|
-
Event:
|
|
97
|
-
SkillName:
|
|
98
|
-
Source:
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
96
|
+
Event: "update",
|
|
97
|
+
SkillName: "xyq-skill",
|
|
98
|
+
Source: "cli_update",
|
|
99
|
+
HostPlatform: "workbuddy",
|
|
100
|
+
CliVersion: "0.0.26",
|
|
101
|
+
Platform: "darwin",
|
|
102
|
+
Arch: "arm64",
|
|
102
103
|
})
|
|
103
104
|
if err != nil {
|
|
104
105
|
t.Fatalf("reportSkillTelemetry() error = %v", err)
|
|
@@ -106,7 +107,7 @@ func TestReportSkillTelemetry(t *testing.T) {
|
|
|
106
107
|
if gotAuth != telemetryAuthHeader {
|
|
107
108
|
t.Fatalf("Authorization = %q, want %q", gotAuth, telemetryAuthHeader)
|
|
108
109
|
}
|
|
109
|
-
if gotPayload.Event != "update" || gotPayload.SkillName != "xyq-skill" || gotPayload.Source != "cli_update" {
|
|
110
|
+
if gotPayload.Event != "update" || gotPayload.SkillName != "xyq-skill" || gotPayload.Source != "cli_update" || gotPayload.HostPlatform != "workbuddy" {
|
|
110
111
|
t.Fatalf("payload = %#v", gotPayload)
|
|
111
112
|
}
|
|
112
113
|
}
|
|
@@ -120,7 +121,7 @@ func TestReportBundledSkillTelemetryWaitsBriefly(t *testing.T) {
|
|
|
120
121
|
|
|
121
122
|
t.Setenv("PIPPIT_CLI_TELEMETRY_BASE_URL", server.URL)
|
|
122
123
|
start := time.Now()
|
|
123
|
-
reportBundledSkillTelemetry("update", "cli_update", &bytes.Buffer{})
|
|
124
|
+
reportBundledSkillTelemetry("update", "cli_update", " workbuddy ", &bytes.Buffer{})
|
|
124
125
|
if elapsed := time.Since(start); elapsed > 1500*time.Millisecond {
|
|
125
126
|
t.Fatalf("reportBundledSkillTelemetry() blocked for %v, want <= 1.5s", elapsed)
|
|
126
127
|
}
|
|
@@ -133,13 +134,16 @@ func TestReportBundledSkillTelemetryReportsBothSkills(t *testing.T) {
|
|
|
133
134
|
if err := json.NewDecoder(r.Body).Decode(&payload); err != nil {
|
|
134
135
|
t.Fatalf("decode request body: %v", err)
|
|
135
136
|
}
|
|
137
|
+
if payload.Source != "cli_update" || payload.HostPlatform != "workbuddy" || payload.Platform != runtime.GOOS {
|
|
138
|
+
t.Errorf("unexpected attribution: %#v", payload)
|
|
139
|
+
}
|
|
136
140
|
skillNames <- payload.SkillName
|
|
137
141
|
_, _ = w.Write([]byte(`{"ret":"0","errmsg":""}`))
|
|
138
142
|
}))
|
|
139
143
|
defer server.Close()
|
|
140
144
|
|
|
141
145
|
t.Setenv("PIPPIT_CLI_TELEMETRY_BASE_URL", server.URL)
|
|
142
|
-
reportBundledSkillTelemetry("update", "cli_update", &bytes.Buffer{})
|
|
146
|
+
reportBundledSkillTelemetry("update", "cli_update", " workbuddy ", &bytes.Buffer{})
|
|
143
147
|
|
|
144
148
|
got := map[string]bool{}
|
|
145
149
|
for i := 0; i < len(telemetrySkillNames); i++ {
|
|
@@ -256,3 +260,13 @@ func parseEnvironment(environment string) map[string]string {
|
|
|
256
260
|
}
|
|
257
261
|
return result
|
|
258
262
|
}
|
|
263
|
+
|
|
264
|
+
func TestTelemetryOmitsUnknownHostPlatform(t *testing.T) {
|
|
265
|
+
body, err := json.Marshal(telemetryPayload{Event: "update", Source: "cli_update"})
|
|
266
|
+
if err != nil {
|
|
267
|
+
t.Fatal(err)
|
|
268
|
+
}
|
|
269
|
+
if bytes.Contains(body, []byte(`"host_platform"`)) {
|
|
270
|
+
t.Fatalf("unknown host_platform must be omitted: %s", body)
|
|
271
|
+
}
|
|
272
|
+
}
|
package/dist/checksums.txt
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
1
|
+
ef54466711739e6d6060dbdc5dd186d3953f76a65e613d028c5f4ee2e33bd9f4 pippit-tool-cli-1.0.37-darwin-amd64.tar.gz
|
|
2
|
+
56c413f48de1e411f0cfefbe20f7589a0c4baaa66c49e640ef034dc683eec4a6 pippit-tool-cli-1.0.37-darwin-arm64.tar.gz
|
|
3
|
+
48647c2756b5a6007385bfcaa4566907e7f80098fdd912dbb22314f840cdef19 pippit-tool-cli-1.0.37-linux-amd64.tar.gz
|
|
4
|
+
c7f6c85f512b4ccb4a524515178e5e0cf56009ff8ec0628bc2db780c0f248cfb pippit-tool-cli-1.0.37-linux-arm64.tar.gz
|
|
5
|
+
6d5ab207b53ded67b1be1ee8cd7f7eb7725f3f87a6c83d2115feb0e3acdf5bef pippit-tool-cli-1.0.37-windows-amd64.zip
|
|
6
|
+
d180077635826e968629278da2c17d5e8f733238db94e74ece273d2be36e6b9f pippit-tool-cli-1.0.37-windows-arm64.zip
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
package common
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
"os"
|
|
5
|
+
"strings"
|
|
6
|
+
)
|
|
7
|
+
|
|
8
|
+
// DetectHostSource uses explicit environment attribution before runtime markers.
|
|
9
|
+
// It never reads credentials, session contents, process arguments or installed apps.
|
|
10
|
+
func DetectHostSource() string {
|
|
11
|
+
return detectHostSource(os.Getenv)
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
func detectHostSource(getenv func(string) string) string {
|
|
15
|
+
if source := strings.TrimSpace(getenv("PIPPIT_CLI_SOURCE")); source != "" {
|
|
16
|
+
return source
|
|
17
|
+
}
|
|
18
|
+
source := ""
|
|
19
|
+
for _, marker := range []struct{ key, host string }{
|
|
20
|
+
{"CODEX_THREAD_ID", "codex"},
|
|
21
|
+
{"CODEX_SESSION_ID", "codex"},
|
|
22
|
+
{"CLAUDECODE", "claude_code"},
|
|
23
|
+
{"CURSOR_AGENT", "cursor"},
|
|
24
|
+
{"GEMINI_CLI", "gemini_cli"},
|
|
25
|
+
} {
|
|
26
|
+
value := strings.ToLower(strings.TrimSpace(getenv(marker.key)))
|
|
27
|
+
if value == "" || value == "0" || value == "false" {
|
|
28
|
+
continue
|
|
29
|
+
}
|
|
30
|
+
if source != "" && source != marker.host {
|
|
31
|
+
return ""
|
|
32
|
+
}
|
|
33
|
+
source = marker.host
|
|
34
|
+
}
|
|
35
|
+
return source
|
|
36
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
package common
|
|
2
|
+
|
|
3
|
+
import "testing"
|
|
4
|
+
|
|
5
|
+
func TestDetectHostSource(t *testing.T) {
|
|
6
|
+
for _, tc := range []struct {
|
|
7
|
+
name string
|
|
8
|
+
env map[string]string
|
|
9
|
+
want string
|
|
10
|
+
}{
|
|
11
|
+
{"unknown", nil, ""},
|
|
12
|
+
{"explicit host", map[string]string{"PIPPIT_CLI_SOURCE": " workbuddy ", "CODEX_THREAD_ID": "secret"}, "workbuddy"},
|
|
13
|
+
{"codex", map[string]string{"CODEX_THREAD_ID": "secret", "CODEX_SESSION_ID": "other-secret"}, "codex"},
|
|
14
|
+
{"claude", map[string]string{"CLAUDECODE": "1"}, "claude_code"},
|
|
15
|
+
{"cursor", map[string]string{"CURSOR_AGENT": "1"}, "cursor"},
|
|
16
|
+
{"gemini", map[string]string{"GEMINI_CLI": "1"}, "gemini_cli"},
|
|
17
|
+
{"ambiguous", map[string]string{"CODEX_THREAD_ID": "secret", "CURSOR_AGENT": "1"}, ""},
|
|
18
|
+
{"disabled markers", map[string]string{"CLAUDECODE": "0", "GEMINI_CLI": " false "}, ""},
|
|
19
|
+
{"configuration is not runtime evidence", map[string]string{"CODEX_HOME": "/home/example", "ANTHROPIC_API_KEY": "secret", "TERM_PROGRAM": "vscode"}, ""},
|
|
20
|
+
} {
|
|
21
|
+
t.Run(tc.name, func(t *testing.T) {
|
|
22
|
+
if got := detectHostSource(func(key string) string { return tc.env[key] }); got != tc.want {
|
|
23
|
+
t.Errorf("source = %q, want %q", got, tc.want)
|
|
24
|
+
}
|
|
25
|
+
})
|
|
26
|
+
}
|
|
27
|
+
}
|
|
@@ -16,6 +16,10 @@ type VideoPartToolParam struct {
|
|
|
16
16
|
Model string `json:"model,omitempty"`
|
|
17
17
|
Resolution string `json:"resolution,omitempty"`
|
|
18
18
|
GenerateType *int64 `json:"generate_type,omitempty"`
|
|
19
|
+
TaskType string `json:"task_type,omitempty"`
|
|
20
|
+
Seed *int64 `json:"seed,omitempty"`
|
|
21
|
+
Draft *bool `json:"draft,omitempty"`
|
|
22
|
+
DraftTaskID string `json:"draft_task_id,omitempty"`
|
|
19
23
|
MiniToolParam *VideoMiniToolParam `json:"mini_tool_param,omitempty"`
|
|
20
24
|
}
|
|
21
25
|
|
|
@@ -28,6 +28,10 @@ type Options struct {
|
|
|
28
28
|
Model string
|
|
29
29
|
Resolution string
|
|
30
30
|
GenerateType *int64
|
|
31
|
+
TaskType string
|
|
32
|
+
Seed *int64
|
|
33
|
+
Draft *bool
|
|
34
|
+
DraftTaskID string
|
|
31
35
|
}
|
|
32
36
|
|
|
33
37
|
// Result is the JSON envelope printed by `pippit-tool-cli generate-video`.
|
|
@@ -70,7 +74,7 @@ func ValidateOptions(opts *Options) error {
|
|
|
70
74
|
if opts == nil {
|
|
71
75
|
return fmt.Errorf("缺少必填参数 --prompt")
|
|
72
76
|
}
|
|
73
|
-
if strings.TrimSpace(opts.Prompt) == "" {
|
|
77
|
+
if strings.TrimSpace(opts.DraftTaskID) == "" && strings.TrimSpace(opts.Prompt) == "" {
|
|
74
78
|
return fmt.Errorf("缺少必填参数 --prompt")
|
|
75
79
|
}
|
|
76
80
|
if err := validateMediaExtensions("图片", opts.ImagePaths, allowedImageExtensions, allowedImageExtensionList); err != nil {
|
|
@@ -123,6 +127,10 @@ func buildSubmitRunBody(opts *Options, imageAssetIDs []string, videoAssetIDs []s
|
|
|
123
127
|
Model: strings.TrimSpace(opts.Model),
|
|
124
128
|
Resolution: strings.TrimSpace(opts.Resolution),
|
|
125
129
|
GenerateType: opts.GenerateType,
|
|
130
|
+
TaskType: strings.TrimSpace(opts.TaskType),
|
|
131
|
+
Seed: opts.Seed,
|
|
132
|
+
Draft: opts.Draft,
|
|
133
|
+
DraftTaskID: strings.TrimSpace(opts.DraftTaskID),
|
|
126
134
|
}
|
|
127
135
|
|
|
128
136
|
return common.WithSubmitRunSource(map[string]any{
|
|
@@ -39,6 +39,8 @@ type QueryResultResult struct {
|
|
|
39
39
|
type QueryResultVideo struct {
|
|
40
40
|
DownloadURL string `json:"download_url"`
|
|
41
41
|
OutputPath string `json:"output_path"`
|
|
42
|
+
Draft *bool `json:"draft,omitempty"`
|
|
43
|
+
DraftTaskID string `json:"draft_task_id,omitempty"`
|
|
42
44
|
}
|
|
43
45
|
|
|
44
46
|
// QueryResultImage describes a downloaded image from query-result.
|
|
@@ -83,6 +85,8 @@ type queryContentData struct {
|
|
|
83
85
|
|
|
84
86
|
type queryVideo struct {
|
|
85
87
|
DownloadURL string `json:"download_url"`
|
|
88
|
+
Draft *bool `json:"draft"`
|
|
89
|
+
DraftTaskID string `json:"draft_task_id"`
|
|
86
90
|
Title string `json:"title"`
|
|
87
91
|
VID string `json:"vid"`
|
|
88
92
|
AssetID string `json:"asset_id"`
|
|
@@ -173,6 +177,8 @@ func QueryResult(ctx context.Context, opts *QueryResultOptions, runner *common.R
|
|
|
173
177
|
resultVideos = append(resultVideos, QueryResultVideo{
|
|
174
178
|
DownloadURL: video.DownloadURL,
|
|
175
179
|
OutputPath: actualOutputPath,
|
|
180
|
+
Draft: video.Draft,
|
|
181
|
+
DraftTaskID: video.DraftTaskID,
|
|
176
182
|
})
|
|
177
183
|
}
|
|
178
184
|
|
|
@@ -204,6 +204,14 @@ func describeModel(raw json.RawMessage) (json.RawMessage, error) {
|
|
|
204
204
|
case "first_last_frame":
|
|
205
205
|
entry["generate_type"] = 1
|
|
206
206
|
}
|
|
207
|
+
switch mode.Key {
|
|
208
|
+
case "reference_generation":
|
|
209
|
+
entry["task_type"] = "reference"
|
|
210
|
+
case "video_edit":
|
|
211
|
+
entry["task_type"] = "edit"
|
|
212
|
+
case "video_extend":
|
|
213
|
+
entry["task_type"] = "extend"
|
|
214
|
+
}
|
|
207
215
|
switch mode.RatioPolicy.ValueMode {
|
|
208
216
|
case "smart":
|
|
209
217
|
entry["ratio"] = map[string]any{"options": []string{"adaptive"}, "default": "adaptive"}
|
package/package.json
CHANGED
|
@@ -20,6 +20,8 @@ and skill directories. Installs the version of this package by default.
|
|
|
20
20
|
PIPPIT_CLI_INSTALL_PACKAGE can override the package/version to install.
|
|
21
21
|
|
|
22
22
|
Options:
|
|
23
|
+
--source HOST Real host identifier reported as host_platform; unknown hosts omit it
|
|
24
|
+
Defaults to PIPPIT_CLI_SOURCE when unset; an explicit empty value omits host_platform
|
|
23
25
|
-h, --help Show this help without installing, updating, or sending telemetry
|
|
24
26
|
`;
|
|
25
27
|
|
|
@@ -65,11 +67,19 @@ function main(args = process.argv.slice(2)) {
|
|
|
65
67
|
console.log(INSTALL_HELP);
|
|
66
68
|
return;
|
|
67
69
|
}
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
70
|
+
let source = process.env.PIPPIT_CLI_SOURCE || '';
|
|
71
|
+
for (let i = 0; i < args.length; i++) {
|
|
72
|
+
if (args[i] === '--source' && i + 1 < args.length && !args[i + 1].startsWith('--')) {
|
|
73
|
+
source = args[++i];
|
|
74
|
+
} else if (args[i].startsWith('--source=')) {
|
|
75
|
+
source = args[i].slice('--source='.length);
|
|
76
|
+
} else {
|
|
77
|
+
console.error(`Invalid install argument or missing value: ${args[i]}. Run pippit-tool-cli install --help.`);
|
|
78
|
+
process.exitCode = 1;
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
72
81
|
}
|
|
82
|
+
source = source.trim();
|
|
73
83
|
const pkg = installPackage();
|
|
74
84
|
const installed = getGloballyInstalledVersion();
|
|
75
85
|
if (installed) {
|
|
@@ -90,7 +100,7 @@ function main(args = process.argv.slice(2)) {
|
|
|
90
100
|
throw err;
|
|
91
101
|
}
|
|
92
102
|
console.log('Existing global package does not contain skills; reinstalling...');
|
|
93
|
-
run('npm', ['install', '-g', pkg], { timeout: 120000 });
|
|
103
|
+
run('npm', ['install', '-g', pkg], { timeout: 120000, env: { ...process.env, PIPPIT_CLI_SKIP_SKILLS: '1' } });
|
|
94
104
|
installGlobalPackageSkills(DEFAULT_PKG);
|
|
95
105
|
}
|
|
96
106
|
|
|
@@ -102,7 +112,7 @@ function main(args = process.argv.slice(2)) {
|
|
|
102
112
|
}
|
|
103
113
|
|
|
104
114
|
console.log(`pippit-tool-cli is ready: ${bin}`);
|
|
105
|
-
reportBundledSkillTelemetry('install', 'npx_install');
|
|
115
|
+
reportBundledSkillTelemetry('install', 'npx_install', source);
|
|
106
116
|
console.log('Try: pippit-tool-cli short-drama +submit-run --message "写一个短剧开头"');
|
|
107
117
|
}
|
|
108
118
|
|
package/scripts/install.js
CHANGED
|
@@ -137,7 +137,7 @@ function install({ cliOnly = false } = {}) {
|
|
|
137
137
|
if (!cliOnly) {
|
|
138
138
|
if (process.env.PIPPIT_CLI_SKIP_SKILLS !== "1") {
|
|
139
139
|
installSkillsFromRoot(ROOT);
|
|
140
|
-
reportBundledSkillTelemetry("install", "npm_install");
|
|
140
|
+
reportBundledSkillTelemetry("install", "npm_install", process.env.PIPPIT_CLI_SOURCE);
|
|
141
141
|
} else {
|
|
142
142
|
cleanupLegacyGlobalSkills();
|
|
143
143
|
}
|
package/scripts/telemetry.js
CHANGED
|
@@ -19,7 +19,7 @@ function telemetryBaseURL() {
|
|
|
19
19
|
return DEFAULT_BASE_URL;
|
|
20
20
|
}
|
|
21
21
|
|
|
22
|
-
function reportBundledSkillTelemetry(event, source) {
|
|
22
|
+
function reportBundledSkillTelemetry(event, source, hostPlatform) {
|
|
23
23
|
if (process.env.PIPPIT_CLI_DISABLE_TELEMETRY === "1") {
|
|
24
24
|
return;
|
|
25
25
|
}
|
|
@@ -28,6 +28,7 @@ function reportBundledSkillTelemetry(event, source) {
|
|
|
28
28
|
event,
|
|
29
29
|
skill_name: skillName,
|
|
30
30
|
source,
|
|
31
|
+
...((hostPlatform || "").trim() ? { host_platform: hostPlatform.trim() } : {}),
|
|
31
32
|
cli_version: VERSION,
|
|
32
33
|
platform: os.platform(),
|
|
33
34
|
arch: os.arch(),
|
|
@@ -41,6 +41,8 @@ metadata:
|
|
|
41
41
|
|
|
42
42
|
## 宿主来源统计
|
|
43
43
|
|
|
44
|
+
优先从宿主的系统身份、当前工具运行环境和明确的平台元数据确认真实宿主,并在每次提交主动填写 `--source`;不要仅因字段可选就跳过。缺少显式值时,CLI 依次尝试 `PIPPIT_CLI_SOURCE` 和已核实的宿主运行标记。只上报稳定的平台名,不上报标记中的会话 ID;来源冲突或仍无法确认则省略,不询问用户、不扫描登录凭据或安装目录。显式 `--source ""` 禁用该次自动归因。
|
|
45
|
+
|
|
44
46
|
仅 `short-drama +submit-run` 静默附加可选 `--source`,用于统计:豆包办公 `doubao_office`、WorkBuddy `workbuddy`、Codex `codex`;其它宿主使用可信环境中的真实稳定标识,无法确认则省略。不从创作文本猜来源,不附带版本、会话 ID、用户信息或 prompt,不向用户询问,不改写 `--message`。上传、查询、下载不附加此参数;旧 CLI 的 `--help` 不支持时省略,不为统计字段重提任务。
|
|
45
47
|
|
|
46
48
|
## 按需参考的完整场景
|
|
@@ -26,6 +26,8 @@ npm install -g @pippit-dev/cli@latest
|
|
|
26
26
|
|
|
27
27
|
npm 入口声明 Node.js 16+;安装/升级还需 npm、全局目录写权限、访问 npm 和 GitHub Releases 的网络、curl,以及 macOS/Linux 的 tar 或 Windows 的 PowerShell。无需 Go 编译器或 Python。默认安装会同时安装全局 Skills,`pippit-tool-cli update` 也会更新它们;不能当成无副作用的例行检查。升级成功后只恢复原任务,不重复创作提交。
|
|
28
28
|
|
|
29
|
+
宿主调用 `pippit-tool-cli install` / `update` 时,按可信运行环境静默附加可选 `--source HOST`;直接运行 npm 全局安装时,在该次命令环境中设置 `PIPPIT_CLI_SOURCE`。`HOST` 为真实宿主标识的占位符,不固定平台,未知则省略,不询问用户。安装/更新将该值写入 `host_platform`,保留原有 `source` 和操作系统 `platform`;服务端需支持新字段才能计入统计。旧版命令不支持该参数时省略,不为统计重复安装;内部仅安装 CLI 的脚本沿用原有不上报行为。
|
|
30
|
+
|
|
29
31
|
安装或升级一次后仍失败、命令仍缺失时停止,说明失败环节,不循环重装。版本和普通命令可能触发 npm 版本提示检查;宿主需禁用时可设置 `PIPPIT_CLI_DISABLE_UPDATE_CHECK=1`。
|
|
30
32
|
|
|
31
33
|
本技能没有单独的自动安装脚本,也不依赖综合创作 Skill 的脚本。独立 ZIP 应保留整个 commands、workflows、examples、scripts 目录及相对引用;`SKILL.md` 的 name 与 ZIP 文件名保持一致。
|
|
@@ -14,6 +14,8 @@ metadata:
|
|
|
14
14
|
|
|
15
15
|
需要 Node.js 16+ 和支持 `marketing` 命令的 `@pippit-dev/cli`。脚本位置为 `"{baseDir}/scripts/marketing.js"`;下面示例中的相对路径从本 Skill 目录执行。先检查 `pippit-tool-cli marketing --help`;命令不存在时安装或更新 CLI:`npm install -g @pippit-dev/cli@latest`,再检查一次;仍不支持就报告版本阻塞,不改为向用户索要密钥。
|
|
16
16
|
|
|
17
|
+
宿主调用 `pippit-tool-cli install` / `update` 时,按可信运行环境静默附加可选 `--source HOST`;直接运行 npm 全局安装时,在该次命令环境中设置 `PIPPIT_CLI_SOURCE`。`HOST` 为真实宿主标识的占位符,不固定平台,未知则省略,不询问用户。安装/更新将该值写入 `host_platform`,保留原有 `source` 和操作系统 `platform`;服务端需支持新字段才能计入统计。旧版命令不支持该参数时省略,不为统计重复安装;内部仅安装 CLI 的脚本沿用原有不上报行为。
|
|
18
|
+
|
|
17
19
|
营销 API 调用复用其它 CLI 命令的登录态,脚本不读取或导出凭据。执行真实请求前运行 `pippit-tool-cli status`,读取 JSON 的 `logged_in`,不能只看退出码。未登录或凭据过期时运行 `pippit-tool-cli login`,让用户在浏览器完成授权;等待 CLI 成功返回后再次检查 status,再继续原任务。已有有效登录态直接复用,不重复登录;离线预览和帮助不要求登录。
|
|
18
20
|
|
|
19
21
|
不要让用户提供或复制 `access_token`、Access Key,也不要把凭据写入聊天、请求文件、命令参数或日志。CLI 保留原有 `XYQ_ACCESS_KEY` 显式环境覆盖规则,优先于网页登录;这仅用于已配置的自动化环境,不作为普通用户的必填项。覆盖无效时不能静默切换账号。网页登录凭据被拒绝时按 CLI 的 `login --force` 流程处理,不自动重提可能已创建的生成任务。
|
|
@@ -38,6 +40,12 @@ node scripts/marketing.js balance
|
|
|
38
40
|
|
|
39
41
|
生成默认是离线预览;预览不需要登录。上传、查询、余额是实际 API 请求。`--timeout` 设置单请求总时限(秒,默认 60)。`--wait` 由脚本每 10 秒查询,默认最多 900 秒;不再叠加其它轮询器。没有 `--wait` 时只查询一次。脚本输出逐行 JSON,查询下载前先输出服务端响应,再逐个输出已下载文件,最后输出含 `downloaded_files` 的响应。
|
|
40
42
|
|
|
43
|
+
## Source Attribution
|
|
44
|
+
|
|
45
|
+
仅营销生成命令(`marketing generate` 或 `marketing.js generate`)静默附加可选 `--source`,由 CLI 映射为请求体 `platform`,不写入创作文本或请求文件。豆包办公填 `doubao_office`,WorkBuddy 填 `workbuddy`,Codex 填 `codex`;其它宿主使用可信环境中的真实稳定标识,未知则省略,不询问用户。不附带版本、会话 ID、用户信息或 prompt。首尾空白去除,未传或空值省略;首次生成和同一会话继续提交均适用。上传、查询、下载、余额不附加。旧 CLI 不支持时省略,不为来源统计重提任务。
|
|
46
|
+
|
|
47
|
+
优先从宿主的系统身份、当前工具运行环境和明确的平台元数据确认真实宿主,并在每次提交主动填写 `--source`;不要仅因字段可选就跳过。缺少显式值时,CLI 依次尝试 `PIPPIT_CLI_SOURCE` 和已核实的宿主运行标记。只上报稳定的平台名,不上报标记中的会话 ID;来源冲突或仍无法确认则省略,不询问用户、不扫描登录凭据或安装目录。显式 `--source ""` 禁用该次自动归因。
|
|
48
|
+
|
|
41
49
|
## Completion and Recovery
|
|
42
50
|
|
|
43
51
|
- `ret` 为字符串或数字 `0` 才是 API 业务成功。查询 `data.run_state`:1/2/7 为已提交或进行中;8 为程序中断(可能等待工具回调),有时限地继续查询;3 为本轮完成;4/5 为失败/取消;6/9 为等待用户交互。不能把退出码 0 的单次进行中查询当作生成完成。
|
|
@@ -50,4 +58,4 @@ node scripts/marketing.js balance
|
|
|
50
58
|
|
|
51
59
|
## Scope
|
|
52
60
|
|
|
53
|
-
本 Skill 仅使用正式公开营销接口;不宣称支持团队空间切换。鉴权范围沿用当前 CLI 登录身份和服务端授权,公开请求没有 `TeamID` 字段,不自行加入团队字段或跨账号复用资产/任务 ID
|
|
61
|
+
本 Skill 仅使用正式公开营销接口;不宣称支持团队空间切换。鉴权范围沿用当前 CLI 登录身份和服务端授权,公开请求没有 `TeamID` 字段,不自行加入团队字段或跨账号复用资产/任务 ID。沉浸式短片、火山引擎服务不在此脚本范围。
|
|
@@ -22,6 +22,8 @@ Base URL:`https://xyq.jianying.com`。全部使用 POST,`Accept: application
|
|
|
22
22
|
| `general_agent_settings` | object | 必传,且必须包含非空 `video_model` |
|
|
23
23
|
| `thread_id` | string | 可选,只在继续已有营销会话时使用 |
|
|
24
24
|
|
|
25
|
+
来源统计由生成命令的可选 `--source HOST` 提供,CLI 在提交及预览的请求体中映射为 `platform` 字符串;不传或去除首尾空白后为空时省略。请求文件仍只填写上表中的创作字段,不直接添加 `source` 或 `platform`。
|
|
26
|
+
|
|
25
27
|
设置块必须传对象,且 `video_model` 必填。2026-09-22 实际调用及服务端 `validateMarketingGeneralAgentSettings` 均确认:`{}` 或仅有比例/时长会返回 `ret=2`、缺少 `video_model`。不再按文档中的默认策略说明发送空对象;预览和提交都先本地校验。用户未指定模型时先询问,已有明确选择授权时按授权选择,不隐式降级。
|
|
26
28
|
|
|
27
29
|
| 设置字段 | 类型 | 含义 |
|
|
@@ -113,6 +113,7 @@ function invokeCLI(args, input, timeout, invocation = resolveCLI()) {
|
|
|
113
113
|
clearTimeout(timer);
|
|
114
114
|
if (failure) return reject(failure);
|
|
115
115
|
if (code !== 0) return reject(new Error(`CLI 调用失败:${stderr.trim() || `退出码 ${code}`};请求未重试。若不支持 marketing 命令,请更新 CLI。`));
|
|
116
|
+
if (args.includes('--help')) return resolve(stdout);
|
|
116
117
|
try { resolve(JSON.parse(stdout)); }
|
|
117
118
|
catch (_) { reject(new Error('CLI 未返回有效 JSON;提交结果可能不明确,请勿自动重提')); }
|
|
118
119
|
});
|
|
@@ -141,13 +142,19 @@ function createClient({ request = https.request, timeout = 60000, invoke = invok
|
|
|
141
142
|
});
|
|
142
143
|
}
|
|
143
144
|
|
|
144
|
-
async function api(action, body) {
|
|
145
|
+
async function api(action, body, source) {
|
|
145
146
|
requireValue(['generate', 'query', 'balance'].includes(action), '无效营销 API 操作');
|
|
146
147
|
const args = ['marketing', action, '--timeout', `${timeout}ms`];
|
|
147
148
|
let input;
|
|
148
149
|
if (action === 'generate') {
|
|
149
150
|
validate(body);
|
|
150
151
|
args.push('--request', '-', '--execute');
|
|
152
|
+
if (source !== undefined) {
|
|
153
|
+
// Probe before submitting; optional attribution must not break older CLIs.
|
|
154
|
+
let help = '';
|
|
155
|
+
try { help = await invoke(['marketing', 'generate', '--help'], undefined, timeout); } catch (_) {}
|
|
156
|
+
if (typeof help === 'string' && /(?:^|\s)--source(?:[=\s,]|$)/m.test(help)) args.push('--source', source.trim());
|
|
157
|
+
}
|
|
151
158
|
input = JSON.stringify(body);
|
|
152
159
|
}
|
|
153
160
|
if (action === 'query') args.push('--thread-id', body.thread_id, '--run-id', body.run_id);
|
|
@@ -193,28 +200,44 @@ function createClient({ request = https.request, timeout = 60000, invoke = invok
|
|
|
193
200
|
}
|
|
194
201
|
|
|
195
202
|
const HELP = `小云雀营销 Skill(Node.js >= 16)
|
|
196
|
-
node marketing.js generate --request request.json [--dry-run | --execute]
|
|
203
|
+
node marketing.js generate --request request.json [--source HOST] [--dry-run | --execute]
|
|
197
204
|
node marketing.js upload --file product.png
|
|
198
205
|
node marketing.js query --thread-id ID --run-id ID [--wait] [--max-wait 900] [--output-dir DIR]
|
|
199
206
|
node marketing.js balance
|
|
200
207
|
所有 API 调用复用 pippit-tool-cli 登录态;未登录先运行 pippit-tool-cli login,无需提供 access_token。generate 默认仅预览。
|
|
208
|
+
--source HOST:仅生成提交使用,映射为请求体 platform;去除首尾空白,空值省略。
|
|
201
209
|
--timeout 秒数:单请求总时限,默认 60;--max-wait:轮询总时限,默认 900。
|
|
202
210
|
query 输出 API 原始响应;有 --output-dir 时成功结果附带 downloaded_files。
|
|
203
211
|
退出码:0 成功/单次查询进行中;1 输入或接口错误;2 生成失败/取消/无视频;3 等待超时;4 等待用户交互;5 未知或未指定状态。
|
|
204
212
|
`;
|
|
205
213
|
|
|
214
|
+
// Keep runtime marker semantics aligned with internal/common/host_source.go.
|
|
215
|
+
function resolveHostSource(explicit, env = process.env) {
|
|
216
|
+
if (explicit !== undefined) return explicit.trim();
|
|
217
|
+
if ((env.PIPPIT_CLI_SOURCE || '').trim()) return env.PIPPIT_CLI_SOURCE.trim();
|
|
218
|
+
const hosts = new Set();
|
|
219
|
+
for (const [key, host] of [
|
|
220
|
+
['CODEX_THREAD_ID', 'codex'], ['CODEX_SESSION_ID', 'codex'],
|
|
221
|
+
['CLAUDECODE', 'claude_code'], ['CURSOR_AGENT', 'cursor'], ['GEMINI_CLI', 'gemini_cli'],
|
|
222
|
+
]) {
|
|
223
|
+
const value = (env[key] || '').trim().toLowerCase();
|
|
224
|
+
if (value && value !== '0' && value !== 'false') hosts.add(host);
|
|
225
|
+
}
|
|
226
|
+
return hosts.size === 1 ? [...hosts][0] : '';
|
|
227
|
+
}
|
|
228
|
+
|
|
206
229
|
function parseArgs(argv) {
|
|
207
230
|
const [action, ...rest] = argv;
|
|
208
231
|
requireValue(Object.hasOwnProperty.call(PATHS, action), '操作必须是 generate/upload/query/balance');
|
|
209
232
|
const options = {};
|
|
210
233
|
const flags = ['execute', 'dry-run', 'wait'];
|
|
211
|
-
const allowed = { generate: ['request', 'execute', 'dry-run'], upload: ['file'], query: ['thread-id', 'run-id', 'wait', 'max-wait', 'output-dir'], balance: [] }[action].concat('timeout');
|
|
234
|
+
const allowed = { generate: ['request', 'execute', 'dry-run', 'source'], upload: ['file'], query: ['thread-id', 'run-id', 'wait', 'max-wait', 'output-dir'], balance: [] }[action].concat('timeout');
|
|
212
235
|
for (let i = 0; i < rest.length; i++) {
|
|
213
236
|
const name = rest[i].replace(/^--/, '');
|
|
214
237
|
requireValue(rest[i].startsWith('--') && allowed.includes(name) && !(name in options), `无效或重复参数:${rest[i]}`);
|
|
215
238
|
if (flags.includes(name)) options[name] = true;
|
|
216
239
|
else {
|
|
217
|
-
requireValue(nonempty(rest[i + 1]) && !rest[i + 1].startsWith('--'), `${name} 缺少值`);
|
|
240
|
+
requireValue(typeof rest[i + 1] === 'string' && (name === 'source' || nonempty(rest[i + 1])) && !rest[i + 1].startsWith('--'), `${name} 缺少值`);
|
|
218
241
|
options[name] = rest[++i];
|
|
219
242
|
}
|
|
220
243
|
}
|
|
@@ -232,9 +255,15 @@ async function main(argv, { out = console.log, clientFactory = createClient, now
|
|
|
232
255
|
const { action, options } = parseArgs(argv);
|
|
233
256
|
let body = {};
|
|
234
257
|
if (action === 'generate') {
|
|
258
|
+
const source = resolveHostSource(options.source);
|
|
259
|
+
if (options.source !== undefined || source) options.source = source;
|
|
235
260
|
requireValue(options.request, 'generate 需要 --request JSON 文件');
|
|
236
261
|
body = validate(JSON.parse(await fs.promises.readFile(options.request, 'utf8')));
|
|
237
|
-
if (!options.execute) {
|
|
262
|
+
if (!options.execute) {
|
|
263
|
+
const source = (options.source || '').trim();
|
|
264
|
+
out(JSON.stringify({ dry_run: true, url: BASE + PATHS.generate, body: source ? { ...body, platform: source } : body }));
|
|
265
|
+
return 0;
|
|
266
|
+
}
|
|
238
267
|
}
|
|
239
268
|
if (action === 'query') {
|
|
240
269
|
requireValue(nonempty(options['thread-id']) && nonempty(options['run-id']), 'query 需要 thread-id 和 run-id');
|
|
@@ -248,7 +277,7 @@ async function main(argv, { out = console.log, clientFactory = createClient, now
|
|
|
248
277
|
// A poll request cannot overrun the remaining wait budget.
|
|
249
278
|
const pollClient = action === 'query' && options.wait
|
|
250
279
|
? clientFactory({ timeout: Math.min(options.timeout * 1000, Math.max(1, deadline - now())) }) : client;
|
|
251
|
-
result = action === 'upload' ? await client.upload(options.file) : await pollClient.api(action, body);
|
|
280
|
+
result = action === 'upload' ? await client.upload(options.file) : await pollClient.api(action, body, options.source);
|
|
252
281
|
if (action !== 'query' || !options.wait || !WAIT_STATES.includes(String(result.data.run_state))) break;
|
|
253
282
|
if (now() >= deadline) {
|
|
254
283
|
out(JSON.stringify({ ...result, wait_timed_out: true })); return 3;
|
|
@@ -303,4 +332,4 @@ if (require.main === module) {
|
|
|
303
332
|
process.exitCode = 1;
|
|
304
333
|
});
|
|
305
334
|
}
|
|
306
|
-
module.exports = { BASE, PATHS, validate, checkResponse, resolveCLI, invokeCLI, createClient, parseArgs, main };
|
|
335
|
+
module.exports = { BASE, PATHS, validate, checkResponse, resolveCLI, invokeCLI, createClient, resolveHostSource, parseArgs, main };
|
|
@@ -28,7 +28,7 @@ metadata:
|
|
|
28
28
|
| 创建或查询小云雀个人画布,编辑节点、布局、连线、角色/场景、提示词、3D 或多轨草稿 | `canvas` | [Canvas 能力与命令发现](commands/canvas.md) |
|
|
29
29
|
| 生成图片,或基于参考图修改图片 | `generate-image` | [生图与图片编辑](commands/generate-image.md) |
|
|
30
30
|
| 查询当前可用的图片/视频模型、比例、分辨率及推理强度等参数配置 | `model list` / `model describe` | [模型发现](commands/model.md) |
|
|
31
|
-
|
|
|
31
|
+
| 生成视频、参考生成、视频编辑、视频延长、首尾帧生成、Draft 样片及转成片 | `generate-video` | [生视频](commands/generate-video.md) |
|
|
32
32
|
| 提升已有视频分辨率、视频超分 | `video-super-resolution` | [超分](commands/video-super-resolution.md) |
|
|
33
33
|
| 去除已有视频字幕 | `erase-video-subtitle` | [擦字幕](commands/erase-video-subtitle.md) |
|
|
34
34
|
| 查询已有任务进度、下载生成结果 | `query-result` | [查询结果](commands/query-result.md) |
|
|
@@ -59,6 +59,8 @@ metadata:
|
|
|
59
59
|
- 不向用户询问、不增加确认步骤,不为此改写 prompt。无法确认来源时直接省略;不传或空值均不阻塞提交。
|
|
60
60
|
- 来源只随本次提交发送,不加入上传、查询、下载或 Canvas 命令。使用已有旧版 CLI 时先按 `--help` 确认是否支持;不支持则省略,不因统计字段中断任务或在未知提交结果时重提。
|
|
61
61
|
|
|
62
|
+
优先从宿主的系统身份、当前工具运行环境和明确的平台元数据确认真实宿主,并在每次提交主动填写 `--source`;不要仅因字段可选就跳过。缺少显式值时,CLI 依次尝试 `PIPPIT_CLI_SOURCE` 和已核实的宿主运行标记。只上报稳定的平台名,不上报标记中的会话 ID;来源冲突或仍无法确认则省略,不询问用户、不扫描登录凭据或安装目录。显式 `--source ""` 禁用该次自动归因。
|
|
63
|
+
|
|
62
64
|
## 按需参考的完整场景
|
|
63
65
|
|
|
64
66
|
命令文档含最小调用示例;需要了解从需求到交付的组合过程时,再读对应场景:
|
|
@@ -1,24 +1,75 @@
|
|
|
1
1
|
# generate-video:生视频
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
适用于文生视频、参考图/视频/音频生成、视频编辑、视频延长、首尾帧生成,以及 Draft 样片和转成片。仅处理已有视频的清晰度或字幕时,分别使用 [超分](video-super-resolution.md)、[擦字幕](erase-video-subtitle.md)。
|
|
4
4
|
|
|
5
5
|
## 输入与参数
|
|
6
6
|
|
|
7
7
|
| 参数 | 必填 | 规则 |
|
|
8
8
|
| --- | --- | --- |
|
|
9
|
-
| `--prompt` |
|
|
9
|
+
| `--prompt` | 按阶段 | 用户原始描述,不能全为空白;传入非空 `--draft-task-id` 转成片时可省略 |
|
|
10
10
|
| `--model` | 是 | 用户指定的准确模型枚举;未明确模型时先确认,不猜测 |
|
|
11
11
|
| `--image` | 否 | 本地图片路径,重复参数 |
|
|
12
|
-
| `--video` |
|
|
12
|
+
| `--video` | 按模式 | 本地视频路径,重复参数;视频编辑、视频延长需提供原视频 |
|
|
13
13
|
| `--audio` | 否 | 本地 `.mp3/.wav` 音频路径,重复参数 |
|
|
14
|
-
| `--duration` |
|
|
15
|
-
| `--ratio` |
|
|
16
|
-
| `--resolution` | 按模型 |
|
|
14
|
+
| `--duration` | 按模式、阶段 | 整数秒;Seedance 2.5 / Draft 编辑模式不传或传 `-1`,其他三种模式为 `4–30` 秒;Draft 转成片不传。其他模式按模型配置:MiniMax、Wan、HappyHorse 可省略,其他模型需明确提供;用户只给时长范围时先确认具体秒数 |
|
|
15
|
+
| `--ratio` | 按模式、阶段 | 比例字符串,不转换为生图枚举;Seedance 2.5 编辑、延长、首尾帧模式不传或传 `adaptive`;Draft 转成片不传。其他模式按模型配置:MiniMax、Wan、HappyHorse 可省略,其他模型需明确提供 |
|
|
16
|
+
| `--resolution` | 按模型 | Draft 两阶段不传,服务端固定样片 480p、成片 1080p;`Seedance_2.0_mini`、`Seedance_2.0_mini_lite` 可省略,服务端默认 `720p`;MiniMax、Wan、HappyHorse 可省略;其他模型需明确提供 |
|
|
17
17
|
| `--generate-type` | 否 | 首尾帧任务传 `1`;其他显式值交服务端处理 |
|
|
18
|
+
| `--task-type` | 否 | 生成模式:`auto/reference/edit/extend`,未传时保持缺省 |
|
|
19
|
+
| `--seed` | 否 | 自定义 seed,显式 `0/-1` 保留,未传时保持缺省 |
|
|
20
|
+
| `--draft` | 否 | 生成样片;显式 `--draft=false` 保留 false,未传时省略字段 |
|
|
21
|
+
| `--draft-task-id` | 否 | 生成成片时提供查询结果中的原始样片任务 ID |
|
|
18
22
|
|
|
19
23
|
当前支持的模型以 `pippit-tool-cli model list` 为准,完整参数配置用 `pippit-tool-cli model describe MODEL_KEY` 查询;缓存有效期为 5 分钟,需要最新结果时加 `--refresh`。详情见 [模型发现](model.md)。CLI 不自行维护模型白名单或分辨率组合校验。
|
|
20
24
|
|
|
21
|
-
本地图片支持 `.jpg/.jpeg/.png/.gif/.bmp/.webp/.svg`;视频支持 `.mp4/.avi/.mov/.wmv/.flv/.webm/.mkv/.m4v`。CLI
|
|
25
|
+
本地图片支持 `.jpg/.jpeg/.png/.gif/.bmp/.webp/.svg`;视频支持 `.mp4/.avi/.mov/.wmv/.flv/.webm/.mkv/.m4v`;音频支持 `.mp3/.wav`。CLI 内部上传参考素材,保留文件后缀校验和 prompt 非空校验,仅传入非空 `--draft-task-id` 时允许省略 prompt。不新增生成参数范围或组合校验,不补可选参数默认值。
|
|
26
|
+
|
|
27
|
+
## Seedance 2.5 四种模式
|
|
28
|
+
|
|
29
|
+
先确认当前模型配置已开放对应模式,再按下表组装输入。Draft 样片在服务端开放后沿用这组模式输入;转成片只需模型和 `draft_task_id`,见下节。
|
|
30
|
+
|
|
31
|
+
| 模式 | `creation_modes.key` | CLI 参数与输入 | 时长、比例 |
|
|
32
|
+
| --- | --- | --- | --- |
|
|
33
|
+
| 参考生成 | `reference_generation` | `--task-type reference`、prompt,按需提供图片/视频/音频 | duration 为 `4–30` 秒;ratio 按用户要求和模型配置提供 |
|
|
34
|
+
| 视频编辑 | `video_edit` | `--task-type edit`、prompt、`--video` 原视频 | duration 不传或 `-1`;ratio 不传或 `adaptive` |
|
|
35
|
+
| 视频延长 | `video_extend` | `--task-type extend`、prompt、`--video` 原视频 | duration 为 `4–30` 秒;ratio 不传或 `adaptive` |
|
|
36
|
+
| 首尾帧 | `first_last_frame` | `--generate-type 1`、prompt,按首帧、尾帧顺序传图片(支持仅首帧,最多两张),不传 task-type,不传视频/音频 | duration 为 `4–30` 秒;ratio 不传或 `adaptive` |
|
|
37
|
+
|
|
38
|
+
`auto` 是下游自动判断模式的值,不对应上述四个页面选项。不要把 `video_edit`、`video_extend` 等配置 key 直接作为 `--task-type` 的值。
|
|
39
|
+
|
|
40
|
+
## Seedance 2.5 Draft 两阶段
|
|
41
|
+
|
|
42
|
+
两个阶段都使用准确枚举 `Seedance_2.5_draft`。这些参数需要服务端已支持 Draft 协议,并允许无 prompt 的二阶段请求;CLI 支持不等于目标环境已经开放。
|
|
43
|
+
|
|
44
|
+
第一阶段:提交样片并查询下载结果。
|
|
45
|
+
|
|
46
|
+
```bash
|
|
47
|
+
pippit-tool-cli generate-video \
|
|
48
|
+
--model Seedance_2.5_draft --draft \
|
|
49
|
+
--prompt "小猫钓鱼视频" --task-type reference \
|
|
50
|
+
--ratio 16:9 --duration 10
|
|
51
|
+
|
|
52
|
+
pippit-tool-cli query-result \
|
|
53
|
+
--thread-id DRAFT_THREAD_ID --run-id DRAFT_RUN_ID \
|
|
54
|
+
--download-dir ./xyq_output/draft
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
向用户展示样片视频。保存查询结果中该视频的 `draft_task_id`,不要使用 `run_id`、视频资产 ID 或编辑器 `draft_key` 替代。用户尚未要求生成成片时,在预览阶段停止。
|
|
58
|
+
|
|
59
|
+
第二阶段:用户明确要求生成成片后,提交同一模型和样片 ID。
|
|
60
|
+
|
|
61
|
+
```bash
|
|
62
|
+
pippit-tool-cli generate-video \
|
|
63
|
+
--model Seedance_2.5_draft \
|
|
64
|
+
--draft-task-id DRAFT_TASK_ID
|
|
65
|
+
|
|
66
|
+
pippit-tool-cli query-result \
|
|
67
|
+
--thread-id FINAL_THREAD_ID --run-id FINAL_RUN_ID \
|
|
68
|
+
--download-dir ./xyq_output/final
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
成片继承样片的提示词、素材、时长、比例和 seed,无需重复提交,也不切换成 `Seedance_2.5`。样片与成片分别计费;下游固定样片 480p、成片 1080p,样片须在创建后 7 天内转成片。CLI 不推算过期时间、不自动进入下一阶段、不维护本地状态机。需要恢复样片 ID 时查询第一阶段的 thread/run,以实际返回的 `draft_task_id` 为准;缺失时按[样片结果说明](query-result.md#样片结果)处理。
|
|
72
|
+
|
|
22
73
|
|
|
23
74
|
## 模型与模式
|
|
24
75
|
|
|
@@ -93,7 +93,7 @@ pippit-tool-cli model describe MiniMax-H3 --refresh
|
|
|
93
93
|
- `Seedance_2.0_mini`、`Seedance_2.0_mini_lite` 的生成请求可以省略 `--resolution`,服务端默认 `720p`。查询未返回分辨率维度时,不因此要求用户补填,也不在查询结果中伪造选项。
|
|
94
94
|
- `duration` 来自参数维度:范围输出 `min/max/step`,选项输出数字 `options`,均以秒为单位,不把选项枚举号当秒数。若只下发旧 `supported_duration_list`,暂保留该原始字段并明确提示不能直接用于 `--duration`。
|
|
95
95
|
- `material_limits` 保留数量字段;大小字段使用 `max_image_size_bytes`,视频时长字段使用 `min_video_duration_ms/max_video_duration_ms/max_total_video_duration_ms`。字段未返回与值为 `0` 保持区别。
|
|
96
|
-
- `creation_modes` 保留 `enabled`、素材校验等服务端策略,并为文本/参考/首尾帧模式标注对应 `generate_type`。`smart` 比例策略显示 `adaptive`;其他可识别模式继承模型比例。MiniMax 的文本模式排除 `adaptive
|
|
96
|
+
- `creation_modes` 保留 `enabled`、素材校验等服务端策略,并为文本/参考/首尾帧模式标注对应 `generate_type`。按服务端下发的模式 key,为 `reference_generation`、`video_edit`、`video_extend` 分别输出 `task_type: reference/edit/extend`,可直接用于 `--task-type`;首尾帧使用 `generate_type: 1`,不传 `task_type`。`creation_modes.key` 本身不是提交值。`smart` 比例策略显示 `adaptive`;其他可识别模式继承模型比例。MiniMax 的文本模式排除 `adaptive`,纯文生视频使用固定比例。素材和参数要求见[生成说明](generate-video.md#seedance-25-四种模式),实际可用性仍以目标服务端开放情况为准。
|
|
97
97
|
- 条件维度的 `active_when_any`、参数组合约束及其他未转换字段保留;模型级选项不保证任意组合都可用。配置不一致时通过 `warnings` 提示刷新,未知比例枚举直接跳过。
|
|
98
98
|
|
|
99
99
|
内部 `config_key` 不输出。缓存仍保存服务端原始配置,列表/详情展示时转换。图片模型标识仅在提交时从名称解析,视频请求契约保持不变,不新增静态模型准入限制。生成参数格式见 [生视频命令](generate-video.md)。
|
|
@@ -24,6 +24,8 @@ stdout 是 JSON;命令可能将错误编码进 JSON 并以退出码 0 返回
|
|
|
24
24
|
| `error_message` | 非空即错误,不能因为 `completed=false` 而忽略 |
|
|
25
25
|
| `thread_id` / `run_id` | 对应的查询任务 |
|
|
26
26
|
| `images[]` / `videos[]` | 成功后获取的媒体,每项含 `download_url`、`output_path` |
|
|
27
|
+
| `videos[].draft` | 服务端明确返回时才输出;`true` 为样片,`false` 为成片,未返回时省略 |
|
|
28
|
+
| `videos[].draft_task_id` | 原始方舟样片任务 ID,用于样片转成片;有值时才输出 |
|
|
27
29
|
|
|
28
30
|
成功示例(ID、URL 和文件名仅为示意):
|
|
29
31
|
|
|
@@ -40,6 +42,14 @@ stdout 是 JSON;命令可能将错误编码进 JSON 并以退出码 0 返回
|
|
|
40
42
|
|
|
41
43
|
任务尚未完成时通常返回 `completed=false`、空错误、空媒体数组。命令不提供完整会话消息、用户反问或可区分的所有状态;不能仅凭这个响应断言具体进度、取消状态或等待用户输入。轮询停止条件见 [共用流程](../workflows/async-delivery.md)。
|
|
42
44
|
|
|
45
|
+
## 样片结果
|
|
46
|
+
|
|
47
|
+
样片仍按普通视频下载,使用 `output_path` 向用户展示预览。保存所选视频的 `videos[].draft_task_id`,与该视频的预览文件保持一一对应。仅保存下载链接无法继续生成成片,不要用 thread/run、资产 ID 或编辑器 draft_key 替换该字段。
|
|
48
|
+
|
|
49
|
+
样片可预览但未返回 `draft_task_id` 时,不能继续转成片。报告该字段缺失并保留原任务的 thread/run ID,不猜 ID,也不重新生成样片。
|
|
50
|
+
|
|
51
|
+
用户明确要求生成成片后,使用 `generate-video --model Seedance_2.5_draft --draft-task-id DRAFT_TASK_ID`;详见 [两阶段生成](generate-video.md#seedance-25-draft-两阶段)。CLI 不自动发起第二阶段。普通视频没有样片字段时保持原来的输出形态。
|
|
52
|
+
|
|
43
53
|
## 下载行为
|
|
44
54
|
|
|
45
55
|
文件名由 CLI 根据产物信息生成,以 `output_path` 为准,不自行拼接编号或推测扩展名。同目录已有同名文件可能被复用;复用不证明内容相同,也不代表本次新下载。发现同名文件属于其他产物时,选择用户认可范围内的未冲突目录再查询,不删除已有文件。
|
|
@@ -8,6 +8,8 @@ node "{baseDir}/scripts/ensure-cli.js"
|
|
|
8
8
|
|
|
9
9
|
`{baseDir}` 是当前 Skill 的根目录。运行环境需 Node.js 16+,并允许执行本地程序。首次安装或自动升级还需要 npm、可写的用户缓存目录、访问 npm 源和 GitHub Release 的网络、`curl` 与解压工具(macOS/Linux 的 `tar`,Windows 的 PowerShell)。复用已有 CLI 不需要下载网络或 npm。
|
|
10
10
|
|
|
11
|
+
宿主调用 `pippit-tool-cli install` / `update` 时,按可信运行环境静默附加可选 `--source HOST`;直接运行 npm 全局安装时,在该次命令环境中设置 `PIPPIT_CLI_SOURCE`。`HOST` 为真实宿主标识的占位符,不固定平台,未知则省略,不询问用户。安装/更新将该值写入 `host_platform`,保留原有 `source` 和操作系统 `platform`;服务端需支持新字段才能计入统计。旧版命令不支持该参数时省略,不为统计重复安装;内部仅安装 CLI 的脚本沿用原有不上报行为。
|
|
12
|
+
|
|
11
13
|
## 查找与复用
|
|
12
14
|
|
|
13
15
|
脚本依次检查 PATH 中的 CLI 和自身缓存,验证版本及本 Skill 使用命令的 `--help`;命令集合维护在脚本的 `REQUIRED_COMMANDS`,包含 `model list` 和 `model describe`。帮助检查不调用生成服务,也不需要凭据,不证明账号权限或服务端运行状态。
|