@pippit-dev/cli 1.0.21 → 1.0.23
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 +118 -41
- package/checksums.txt +6 -6
- package/cmd/canvas/canvas.go +2 -0
- package/cmd/canvas/canvas_test.go +2 -2
- package/cmd/canvas/command_help.go +40 -0
- package/cmd/root.go +6 -3
- package/cmd/root_test.go +38 -0
- package/cmd/submit_run.go +40 -0
- package/cmd/submit_run_test.go +103 -0
- package/cmd/upload_file.go +64 -0
- package/cmd/upload_file_test.go +155 -0
- package/dist/checksums.txt +6 -6
- package/dist/xyq-canvas-command-runtime.cjs +14 -14
- package/dist/xyq-canvas-command-runtime.cjs.LEGAL.txt +235 -3
- package/dist/xyq-canvas-command-runtime.cjs.sha256 +2 -2
- package/internal/common/upload_file.go +1 -1
- package/internal/config/config_test.go +10 -0
- package/package.json +8 -3
- package/scripts/canvas-command-discovery.js +202 -0
- package/scripts/canvas-command-guides.js +171 -0
- package/scripts/canvas-command.js +100 -23
- package/scripts/install-cli.js +19 -0
- package/scripts/install-wizard.js +46 -22
- package/scripts/install.js +8 -6
- package/scripts/run.js +58 -42
- package/skills/xyq-nest-skill/SKILL.md +113 -60
- package/skills/xyq-nest-skill/scripts/ensure-cli.js +149 -0
- package/skills/xyq-nest-skill/scripts/get_thread.py +1 -1
- package/skills/xyq-nest-skill/scripts/xyq_common.py +34 -31
- package/skills/xyq-nest-skill/scripts/download_results.py +0 -105
- package/skills/xyq-nest-skill/scripts/submit_run.py +0 -76
- package/skills/xyq-nest-skill/scripts/upload_file.py +0 -133
package/README.md
CHANGED
|
@@ -39,53 +39,58 @@
|
|
|
39
39
|
|
|
40
40
|
- 生成:文生图、文生视频、图生视频、视频续写。
|
|
41
41
|
- 编辑:局部修改、元素替换、镜头调整、风格迁移。
|
|
42
|
-
-
|
|
42
|
+
- 复杂创作:复刻视频/TVC/宣传片、音乐 MV 生成、产品展示片制作。
|
|
43
43
|
|
|
44
44
|
### 配置
|
|
45
45
|
|
|
46
|
-
|
|
46
|
+
单独上传 `xyq-skill` ZIP 时,保留 Skill 内的 `scripts/ensure-cli.js`。每次开始执行 Skill 任务先运行:
|
|
47
47
|
|
|
48
48
|
```bash
|
|
49
|
-
|
|
49
|
+
node /path/to/xyq-skill/scripts/ensure-cli.js
|
|
50
50
|
```
|
|
51
51
|
|
|
52
|
-
|
|
52
|
+
环境需要 Node.js 16+ 和 Python 3;首次安装或自动升级时需要 npm、`curl`、系统解压工具(macOS/Linux 的 `tar`,Windows 的 PowerShell)及访问 npm 源和 GitHub Release 的网络。脚本优先复用 PATH 或自身缓存中命令齐全的 CLI;均不存在或缺少必需命令时获取 `@pippit-dev/cli@latest`,安装到 `~/.cache/pippit-tool-cli/xyq-skill/<平台>-<架构>/current`,返回 `{ "cli_path": "CLI绝对路径", "version": "实际安装版本" }`。后续示例中的 `pippit-tool-cli` 替换为该绝对路径;同一任务内复用,不在轮询时重复安装。已有 CLI 缺少关键命令时自动升级;升级后的缓存可被后续任务复用,避免 PATH 旧版本触发重复下载。每次调用最多下载安装一次,升级失败保留原安装;最新版本仍缺少必需命令时报告阻塞。
|
|
53
|
+
|
|
54
|
+
新入口 `node scripts/install-cli.js` 只安装 npm 包对应版本的 CLI 二进制,不安装或清理全局 Skill。ZIP 安装脚本先以 `--ignore-scripts` 获取 npm 包,再调用这个入口。发布包含新入口的 npm 包及对应 GitHub Release 后,ZIP 的最新版本安装流程才能完整使用。
|
|
55
|
+
|
|
56
|
+
`submit-run` 和 `upload-file` 使用原生 CLI 登录凭证(`pippit-tool-cli login`),也可通过 `XYQ_ACCESS_KEY` 显式覆盖。保留的查询脚本 `get_thread.py` 仍需配置同一用户的 Bearer 凭证:
|
|
53
57
|
|
|
54
58
|
```bash
|
|
55
|
-
export
|
|
56
|
-
# 或
|
|
57
|
-
export XYQ_BASE_URL="https://xyq.jianying.com"
|
|
59
|
+
export XYQ_ACCESS_KEY="<access-key>"
|
|
58
60
|
```
|
|
59
61
|
|
|
62
|
+
CLI 和 Python 脚本携带用户密钥的 API 请求地址固定为 `https://xyq.jianying.com`,不接受 `XYQ_OPENAPI_BASE` / `XYQ_BASE_URL` 覆盖。CLI 拒绝 API 跨域重定向,Python API 脚本禁止自动重定向。上传只在 Authorization 请求头携带密钥。
|
|
63
|
+
|
|
60
64
|
### 创建会话 / 发送消息
|
|
61
65
|
|
|
62
66
|
```bash
|
|
63
67
|
# 创建新会话
|
|
64
|
-
|
|
68
|
+
pippit-tool-cli submit-run --message "生一个动漫视频"
|
|
65
69
|
|
|
66
70
|
# 向已有会话发送消息
|
|
67
|
-
|
|
71
|
+
pippit-tool-cli submit-run \
|
|
68
72
|
--message "再生成一个故事视频" \
|
|
69
73
|
--thread-id THREAD_ID
|
|
70
74
|
|
|
71
75
|
# 携带参考文件发送
|
|
72
|
-
|
|
76
|
+
pippit-tool-cli submit-run \
|
|
73
77
|
--message "参考这个视频做修改" \
|
|
74
|
-
--asset-ids asset_id1 asset_id2
|
|
78
|
+
--asset-ids asset_id1 --asset-ids asset_id2
|
|
75
79
|
```
|
|
76
80
|
|
|
77
81
|
| 参数 | 必填 | 说明 |
|
|
78
82
|
|------|------|------|
|
|
79
|
-
| `--message` | 是 |
|
|
83
|
+
| `--message` | 是 | 非空白的创作指令,原样发送。 |
|
|
80
84
|
| `--thread-id` | 否 | 已有会话 ID,不传则创建新会话。 |
|
|
81
|
-
| `--asset-ids` | 否 |
|
|
85
|
+
| `--asset-ids` | 否 | 每次传一个资产 ID;多个素材重复该参数。 |
|
|
82
86
|
|
|
83
87
|
返回示例:
|
|
84
88
|
|
|
85
89
|
```json
|
|
86
90
|
{
|
|
87
91
|
"thread_id": "90f05e0c-...",
|
|
88
|
-
"run_id": "abc123-..."
|
|
92
|
+
"run_id": "abc123-...",
|
|
93
|
+
"web_thread_link": "https://xyq.jianying.com/..."
|
|
89
94
|
}
|
|
90
95
|
```
|
|
91
96
|
|
|
@@ -108,18 +113,20 @@ python3 skills/xyq-nest-skill/scripts/get_thread.py \
|
|
|
108
113
|
|
|
109
114
|
### 上传文件
|
|
110
115
|
|
|
116
|
+
使用顶层 `upload-file --path`,每次上传一个本地图片、视频或 MP3/WAV 音频文件,文件须小于 500 MB(500000000 字节)。成功返回 `{"asset_id":"..."}`,可直接传给 `submit-run --asset-ids`。
|
|
117
|
+
|
|
111
118
|
```bash
|
|
112
119
|
# 上传图片
|
|
113
|
-
|
|
120
|
+
pippit-tool-cli upload-file --path /path/to/image.png
|
|
114
121
|
|
|
115
122
|
# 上传视频
|
|
116
|
-
|
|
123
|
+
pippit-tool-cli upload-file --path /path/to/video.mp4
|
|
117
124
|
|
|
118
125
|
# 上传音频
|
|
119
|
-
|
|
126
|
+
pippit-tool-cli upload-file --path /path/to/audio.mp3
|
|
120
127
|
```
|
|
121
128
|
|
|
122
|
-
仅支持 `image/*`、`video/*` 和 `.mp3/.wav` 音频文件,单文件大小限制
|
|
129
|
+
仅支持 `image/*`、`video/*` 和 `.mp3/.wav` 音频文件,单文件大小限制 500 MB。
|
|
123
130
|
|
|
124
131
|
返回示例:
|
|
125
132
|
|
|
@@ -131,65 +138,71 @@ python3 skills/xyq-nest-skill/scripts/upload_file.py /path/to/audio.mp3
|
|
|
131
138
|
|
|
132
139
|
### 下载结果
|
|
133
140
|
|
|
141
|
+
每个产物 URL 调用一次 CLI,`--output-path` 必须包含文件名:
|
|
142
|
+
|
|
134
143
|
```bash
|
|
135
|
-
|
|
136
|
-
--
|
|
137
|
-
--output-
|
|
138
|
-
--prefix "storyboard" \
|
|
139
|
-
--workers 5
|
|
144
|
+
pippit-tool-cli download-result \
|
|
145
|
+
--url "URL1" \
|
|
146
|
+
--output-path "./xyq_output/storyboard_01.png"
|
|
140
147
|
```
|
|
141
148
|
|
|
142
149
|
| 参数 | 必填 | 说明 |
|
|
143
150
|
|------|------|------|
|
|
144
|
-
| `--
|
|
145
|
-
| `--output-
|
|
146
|
-
| `--
|
|
147
|
-
| `--workers` | 否 |
|
|
151
|
+
| `--url` | 是 | 单个产物的下载 URL。 |
|
|
152
|
+
| `--output-path` | 是 | 包含文件名的本地目标路径。 |
|
|
153
|
+
| `--updated-at` | 否 | 远端文件真实更新时间(Unix 秒),用于判断是否覆盖已有文件。 |
|
|
154
|
+
| `--workers` | 否 | 下载 worker 数,默认 `5`;当前单 URL 调用实际只使用一个 worker。 |
|
|
148
155
|
|
|
149
|
-
|
|
156
|
+
Skill 沿用用户指定的输出目录,未指定时使用 `./xyq_output`,按 URL 列表顺序从 `01` 编号,组成 `前缀_01.ext`(无前缀时为 `01.ext`)。扩展名优先取 URL 查询参数 `filename`,其次取 URL 路径,无法取得时使用 `.bin`。多文件逐项调用,可最多并行执行 5 个命令;重试保持原目标路径。
|
|
157
|
+
|
|
158
|
+
下载成功返回示例:
|
|
150
159
|
|
|
151
160
|
```json
|
|
152
161
|
{
|
|
153
|
-
"
|
|
154
|
-
"downloaded": ["./xyq_output/storyboard_01.png"]
|
|
155
|
-
"total": 1
|
|
162
|
+
"output_path": "./xyq_output/storyboard_01.png",
|
|
163
|
+
"downloaded": ["./xyq_output/storyboard_01.png"]
|
|
156
164
|
}
|
|
157
165
|
```
|
|
158
166
|
|
|
167
|
+
未传 `--updated-at` 时,CLI 默认跳过已有文件,返回 `already_exist`,此时 `downloaded` 为 `null`;传入真实更新时间后,仅当本地文件修改时间早于该时间时覆盖更新。跳过不代表已校验本地内容与远端一致。
|
|
168
|
+
|
|
169
|
+
下载失败时命令以非零退出码返回错误,不保证输出 JSON。Skill 汇总各次调用的下载成功、已存在跳过和失败项,只对失败项重试一次;仍有失败时明确报告未完整交付。
|
|
170
|
+
|
|
159
171
|
### 典型示例
|
|
160
172
|
|
|
161
173
|
文生视频:
|
|
162
174
|
|
|
163
175
|
```text
|
|
164
|
-
1.
|
|
176
|
+
1. pippit-tool-cli submit-run --message "生成一个赛博朋克风格的城市夜景视频"
|
|
165
177
|
2. 每 10 秒轮询:
|
|
166
178
|
get_thread.py --thread-id THREAD_ID --run-id RUN_ID --after-seq SEQUENCE
|
|
167
179
|
3. 拿到产物 URL 后下载:
|
|
168
|
-
|
|
180
|
+
pippit-tool-cli download-result --url "URL1" --output-path "./output/cyberpunk_01.mp4"
|
|
181
|
+
pippit-tool-cli download-result --url "URL2" --output-path "./output/cyberpunk_02.mp4"
|
|
169
182
|
```
|
|
170
183
|
|
|
171
184
|
编辑已有视频:
|
|
172
185
|
|
|
173
186
|
```text
|
|
174
|
-
1.
|
|
175
|
-
2.
|
|
187
|
+
1. pippit-tool-cli upload-file --path /path/to/video.mp4
|
|
188
|
+
2. pippit-tool-cli submit-run --message "把背景换成星空" --asset-ids asset_id
|
|
176
189
|
3. 按文生视频流程轮询和下载。
|
|
177
190
|
```
|
|
178
191
|
|
|
179
192
|
多参考图/视频生成:
|
|
180
193
|
|
|
181
194
|
```text
|
|
182
|
-
1.
|
|
183
|
-
2.
|
|
184
|
-
3.
|
|
185
|
-
4.
|
|
195
|
+
1. pippit-tool-cli upload-file --path /path/to/ref1.png
|
|
196
|
+
2. pippit-tool-cli upload-file --path /path/to/ref2.png
|
|
197
|
+
3. pippit-tool-cli upload-file --path /path/to/ref3.mp4
|
|
198
|
+
4. pippit-tool-cli submit-run --message "根据参考图和视频生成科普故事视频" --asset-ids asset_id1 --asset-ids asset_id2 --asset-ids asset_id3
|
|
186
199
|
5. 按文生视频流程轮询和下载。
|
|
187
200
|
```
|
|
188
201
|
|
|
189
202
|
在已有会话中追加需求:
|
|
190
203
|
|
|
191
204
|
```text
|
|
192
|
-
1.
|
|
205
|
+
1. pippit-tool-cli submit-run --message "把刚才的视频加个片头" --thread-id EXISTING_THREAD_ID
|
|
193
206
|
2. 使用新的 run_id 轮询和下载。
|
|
194
207
|
```
|
|
195
208
|
|
|
@@ -249,9 +262,21 @@ pippit-tool-cli canvas apply --project-id PROJECT_ID --file ./patch.json
|
|
|
249
262
|
通过 npm 安装的 CLI 还提供基于同一 Canvas SDK 的语义命令目录:
|
|
250
263
|
|
|
251
264
|
```bash
|
|
252
|
-
#
|
|
265
|
+
# 先看精简目录,再按类别或参数定位
|
|
253
266
|
pippit-tool-cli canvas command list
|
|
267
|
+
pippit-tool-cli canvas command list --category timeline
|
|
254
268
|
pippit-tool-cli canvas command describe create_biz_node
|
|
269
|
+
pippit-tool-cli canvas command describe create_biz_node --node-kind role
|
|
270
|
+
pippit-tool-cli canvas command describe xyq.timeline.apply --operation set_output_size
|
|
271
|
+
pippit-tool-cli canvas command describe xyq.generation.update_prompt --path properties.prompt
|
|
272
|
+
|
|
273
|
+
# 完整 schema 按需导出;不指定命令时导出全部
|
|
274
|
+
pippit-tool-cli canvas command schema xyq.timeline.apply
|
|
275
|
+
pippit-tool-cli canvas command schema
|
|
276
|
+
|
|
277
|
+
# 离线指南:先取主题索引,再读正文
|
|
278
|
+
pippit-tool-cli canvas command guide
|
|
279
|
+
pippit-tool-cli canvas command guide storyboard
|
|
255
280
|
|
|
256
281
|
# 由 SDK 业务工厂创建角色节点;修改会通过现有 canvas apply 原子提交
|
|
257
282
|
pippit-tool-cli canvas command run create_biz_node \
|
|
@@ -261,6 +286,58 @@ pippit-tool-cli canvas command run create_biz_node \
|
|
|
261
286
|
|
|
262
287
|
`canvas command` 由 npm 包内固定的 Canvas SDK 运行时提供,复用网页登录、`canvas get`、`canvas allocate` 和 `canvas apply`;不会读取或打印 Access Key,也不直接选择服务端地址。公开目录只包含已登记的 mutation 和业务命令,不开放任意内部 command 调用。
|
|
263
288
|
|
|
289
|
+
`list [--category <category>]` 返回精简命令目录;`describe <command>` 说明入口参数,按 `--operation <name>` 查看一种领域操作、按 `--node-kind <kind>` 查看业务节点初始字段、按 `--path <schema.path>` 查看 schema 子路径。需要完整嵌套结构时使用 `schema [command]` 显式导出。`create_biz_node.nodeKind` 包含 `scene3d` 与 `timeline-composition`;字段枚举、必填项、默认值与动态来源以当前安装版本的 schema 为准。
|
|
290
|
+
|
|
291
|
+
发现输出使用 `schema_version: 2`:`list` 只包含名称、分类和摘要;`describe` 的 `schema_view: "summary"` 表示展示视图,嵌套内容通过 `schema_path` 继续展开,不能直接当作完整校验 schema。原来从 `list` 或 `describe` 读取完整 `input_schema` 的脚本应改用 `schema [command]`。默认索引预算为 16 KiB,单次字段说明预算为 32 KiB;完整导出需要显式调用,执行命令的输入与返回值不受这一发现协议调整影响。
|
|
292
|
+
|
|
293
|
+
`guide [topic]` 提供无需登录的离线帮助。无主题时仅返回索引,可选 `storyboard`、`prompt-references`、`time`、`timeline`、`scene3d`;正文包含单位、ID 来源、前置条件和最小示例。指南不启用新能力,先用 `list` 确认本机运行时支持哪些命令。故事板指南说明原生 `<duration-ms>` 标签累加与引用格式;目前没有公开的故事板脚本编辑、镜头排序或指定镜头生成领域命令,通用视频生成与资产补丁不能替代其业务流程。
|
|
294
|
+
|
|
295
|
+
3D 导演台和多轨道都通过外层画布节点定位,其编辑内容保存在节点引用的独立文档或草稿资产中。先查询取得内部对象、轨道、片段 ID 和版本,再执行编辑:
|
|
296
|
+
|
|
297
|
+
```bash
|
|
298
|
+
pippit-tool-cli canvas command describe xyq.scene3d.apply
|
|
299
|
+
pippit-tool-cli canvas command run xyq.scene3d.query \
|
|
300
|
+
--canvas-id CANVAS_ID --input '{"nodeId":"DIRECTOR_NODE_ID"}'
|
|
301
|
+
pippit-tool-cli canvas command run xyq.scene3d.apply \
|
|
302
|
+
--canvas-id CANVAS_ID \
|
|
303
|
+
--input '{"nodeId":"DIRECTOR_NODE_ID","operations":[{"command":"create_node","args":{"kind":"camera","id":"camera-2","name":"Close-up"}}]}'
|
|
304
|
+
|
|
305
|
+
pippit-tool-cli canvas command describe xyq.timeline.apply
|
|
306
|
+
pippit-tool-cli canvas command run xyq.timeline.query \
|
|
307
|
+
--canvas-id CANVAS_ID --input '{"nodeId":"TIMELINE_NODE_ID"}'
|
|
308
|
+
# expectedRevision 使用上一步返回的 draft.revision
|
|
309
|
+
pippit-tool-cli canvas command run xyq.timeline.apply \
|
|
310
|
+
--canvas-id CANVAS_ID \
|
|
311
|
+
--input '{"nodeId":"TIMELINE_NODE_ID","expectedRevision":0,"commands":[{"type":"set_output_size","payload":{"width":1920,"height":1080}}]}'
|
|
312
|
+
```
|
|
313
|
+
|
|
314
|
+
领域命令的 `dryRun:true` 会完整预演编辑并保留原文档。多轨时间以整数微秒表示;3D 关键帧以帧表示,动作片段 `trimStart/trimEnd` 以源动画秒数表示。3D 对象旋转以度表示,几何体的 `theta/phi/arc` 参数以弧度表示,具体以字段 schema 为准。新增多轨素材须复用已有来源,或关联真实画布素材节点。截图、渲染导出、上传和生成仍需各自的运行环境。
|
|
315
|
+
|
|
316
|
+
运行结构为 `npm 的 JS 入口 → Canvas SDK CJS → Go 二进制的资产命令`。Go 二进制可独立执行其原生命令,无需安装 Go;`canvas command` 需要 npm 包中的 Node.js 入口与 CJS 运行时。
|
|
317
|
+
|
|
318
|
+
图片或视频节点通过 `xyq.generation.update_prompt` 更新提示词,`prompt` 直接使用前端已有的标签文本。CLI 与前端粘贴调用同一份 SDK 标签解析、引用匹配和连边逻辑:
|
|
319
|
+
|
|
320
|
+
```bash
|
|
321
|
+
pippit-tool-cli canvas command describe xyq.generation.update_prompt
|
|
322
|
+
pippit-tool-cli canvas command run xyq.generation.update_prompt \
|
|
323
|
+
--canvas-id CANVAS_ID \
|
|
324
|
+
--input '{"nodeId":"TARGET_IMAGE_NODE_ID","prompt":"参考 <node-asset label=\"人物\">REFERENCE_IMAGE_NODE_ID</node-asset> 的人物,改为雨夜街景"}'
|
|
325
|
+
```
|
|
326
|
+
|
|
327
|
+
示例 ID 应替换为查询到的真实节点或资产 ID。`get_asset` 可查看当前节点与草稿;`describe` 按需查看参数,`schema` 导出完整输入结构。角色连边沿用前端既有默认选择与草稿处理,标签属性原样保留给编辑器和提交解析器。无需另传 `text/reference` 数组、`asset` 包装或 `referenceSource`;引用类型和独立素材前置条件可查看 `guide prompt-references`。
|
|
328
|
+
|
|
329
|
+
直接上传或从素材库选出的素材可以没有节点。对于已在目标 `generation.references` 草稿中的素材,直接使用其 `pippitAssetId`:
|
|
330
|
+
|
|
331
|
+
```bash
|
|
332
|
+
pippit-tool-cli canvas command run xyq.generation.update_prompt \
|
|
333
|
+
--canvas-id CANVAS_ID \
|
|
334
|
+
--input '{"nodeId":"TARGET_IMAGE_NODE_ID","prompt":"参考 <pippit-asset-id label=\"参考图\">PIPPIT_ASSET_ID_IN_DRAFT</pippit-asset-id> 的人物"}'
|
|
335
|
+
```
|
|
336
|
+
|
|
337
|
+
当前版本只解析已有画布候选与目标草稿中的引用;找不到的普通标签返回 `UNRESOLVED_PROMPT_REFERENCE`,不写入文档。仅拿到 `canvas upload` 返回的 ID,还不会自动查询并加入草稿。新独立素材 ID 的自动解析属于后续能力。已有独立素材草稿可由前端上传或素材库流程产生,视频生成可使用其中的图片、视频和音频。同一媒体 ID 若同时匹配到画布源节点,则遵循前端现有的节点优先规则建立关联。`canvas get --asset-id PIPPIT_ASSET_ID` 可查询外部素材,但查询本身不添加引用。
|
|
338
|
+
|
|
339
|
+
该命令先在隔离文档上按前端原有顺序执行 SDK commands,再把实际补丁一次性提交,保留模型参数、已有引用及 caption/title/name 等展示字段。`dryRun:true` 只执行隔离预演,原文档和撤销历史不变;清空 prompt 不移除引用。节点引用由现有生成流程转换成 `node_asset_refs`,独立图片进入 `pippit_asset_ids`,视频生成的独立素材按类型进入 `images`、`videos`、`audios`。本命令不触发生成、上传或远端素材查询。不要用浅合并的 `update_asset.contentPatch.generation` 更新提示词,否则可能覆盖其他生成参数。
|
|
340
|
+
|
|
264
341
|
## 生图 CLI
|
|
265
342
|
|
|
266
343
|
`generate-image` 会上传本地参考图片,然后向综合 Nest Agent 提交生图请求:
|
package/checksums.txt
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
1
|
+
259ad5fb69598b646e035e5dd70fd6c905a25128b1b9c8e9689c58e09ec0a3d9 pippit-tool-cli-1.0.23-darwin-amd64.tar.gz
|
|
2
|
+
610ec13e42a53a472759daf322701e178b062ff2ca2e854382cf5a8b6b894953 pippit-tool-cli-1.0.23-darwin-arm64.tar.gz
|
|
3
|
+
3d371bb915e4b6adaf29cd3b1b06a9ee6a40896f962051ce97adeb1937a1b4a1 pippit-tool-cli-1.0.23-linux-amd64.tar.gz
|
|
4
|
+
541f8a8e1913a591ad98ed3835525eba8459fd12ec289f9b206f9588812311c8 pippit-tool-cli-1.0.23-linux-arm64.tar.gz
|
|
5
|
+
0ded272a827aa39427c53135465c99e6e1f94c81ef1f5e0982c6726d385e03bf pippit-tool-cli-1.0.23-windows-amd64.zip
|
|
6
|
+
ab6fd1baeac84e4e3cdde2392462534bd2a6279ee1cab0395437f1a03b5ace55 pippit-tool-cli-1.0.23-windows-arm64.zip
|
package/cmd/canvas/canvas.go
CHANGED
|
@@ -23,6 +23,7 @@ func NewCommand(stdout, stderr io.Writer, runner *common.Runner) *cobra.Command
|
|
|
23
23
|
cmd := &cobra.Command{
|
|
24
24
|
Use: "canvas",
|
|
25
25
|
Short: "Create and operate personal novel canvases",
|
|
26
|
+
Long: "Create and operate personal novel canvases.\n\n" + CommandDiscoveryHelp,
|
|
26
27
|
Args: cobra.NoArgs,
|
|
27
28
|
}
|
|
28
29
|
cmd.SetOut(stdout)
|
|
@@ -32,6 +33,7 @@ func NewCommand(stdout, stderr io.Writer, runner *common.Runner) *cobra.Command
|
|
|
32
33
|
cmd.AddCommand(newAllocateCommand(stdout, stderr, runner))
|
|
33
34
|
cmd.AddCommand(newApplyCommand(stdout, stderr, runner))
|
|
34
35
|
cmd.AddCommand(newUploadCommand(stdout, stderr, runner))
|
|
36
|
+
cmd.AddCommand(newCommandHelp())
|
|
35
37
|
return cmd
|
|
36
38
|
}
|
|
37
39
|
|
|
@@ -40,8 +40,8 @@ func TestCommandExposesOnlyProviderNeutralPublicVerbs(t *testing.T) {
|
|
|
40
40
|
for _, child := range cmd.Commands() {
|
|
41
41
|
got = append(got, child.Name())
|
|
42
42
|
}
|
|
43
|
-
if strings.Join(got, ",") != "allocate,apply,create,get,upload" {
|
|
44
|
-
t.Fatalf("commands = %v, want allocate/apply/create/get/upload", got)
|
|
43
|
+
if strings.Join(got, ",") != "allocate,apply,command,create,get,upload" {
|
|
44
|
+
t.Fatalf("commands = %v, want allocate/apply/command/create/get/upload", got)
|
|
45
45
|
}
|
|
46
46
|
for _, forbidden := range []string{"import", "bind", "team"} {
|
|
47
47
|
if strings.Contains(strings.ToLower(cmd.CommandPath()+" "+cmd.Short+" "+strings.Join(got, " ")), forbidden) {
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
package canvas
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
"fmt"
|
|
5
|
+
|
|
6
|
+
"github.com/spf13/cobra"
|
|
7
|
+
)
|
|
8
|
+
|
|
9
|
+
// CommandDiscoveryHelp connects native transport help to the npm command catalogue.
|
|
10
|
+
const CommandDiscoveryHelp = `Canvas command catalogue (provided by the npm launcher):
|
|
11
|
+
pippit-tool-cli canvas command list [--category <category>]
|
|
12
|
+
pippit-tool-cli canvas command describe <name> [--operation <operation>] [--node-kind <kind>] [--path <schema.path>]
|
|
13
|
+
pippit-tool-cli canvas command schema [name]
|
|
14
|
+
pippit-tool-cli canvas command guide [topic]
|
|
15
|
+
pippit-tool-cli canvas command run <name> --canvas-id <id> [--input <JSON> | --file <path|->]
|
|
16
|
+
|
|
17
|
+
Start with list, inspect inputs with describe/schema, then read guide for workflows.
|
|
18
|
+
Use the pippit-tool-cli installed by npm, or node <package>/scripts/run.js.
|
|
19
|
+
The standalone native binary provides Canvas transport commands and this help;
|
|
20
|
+
executing the command catalogue requires the npm launcher.`
|
|
21
|
+
|
|
22
|
+
func newCommandHelp() *cobra.Command {
|
|
23
|
+
return &cobra.Command{
|
|
24
|
+
Use: "command",
|
|
25
|
+
Short: "Discover list, describe, schema, guide, and run via the npm launcher",
|
|
26
|
+
Long: CommandDiscoveryHelp,
|
|
27
|
+
DisableFlagParsing: true,
|
|
28
|
+
RunE: func(cmd *cobra.Command, args []string) error {
|
|
29
|
+
if len(args) == 0 || (len(args) == 1 && (args[0] == "--help" || args[0] == "-h")) {
|
|
30
|
+
return cmd.Help()
|
|
31
|
+
}
|
|
32
|
+
switch args[0] {
|
|
33
|
+
case "list", "describe", "schema", "guide", "run":
|
|
34
|
+
return fmt.Errorf("canvas command %s requires the npm launcher; use the npm-installed pippit-tool-cli or node <package>/scripts/run.js canvas command %s; the standalone native binary cannot execute this command", args[0], args[0])
|
|
35
|
+
default:
|
|
36
|
+
return fmt.Errorf("unknown canvas command action %q; expected list, describe, schema, guide, or run via the npm launcher; see canvas command --help", args[0])
|
|
37
|
+
}
|
|
38
|
+
},
|
|
39
|
+
}
|
|
40
|
+
}
|
package/cmd/root.go
CHANGED
|
@@ -54,9 +54,10 @@ func newRunnerAuthorizer(runner *common.Runner) common.RequestAuthorizer {
|
|
|
54
54
|
|
|
55
55
|
func newRootCommand(stdout, stderr io.Writer, runner *common.Runner) *cobra.Command {
|
|
56
56
|
root := &cobra.Command{
|
|
57
|
-
Use:
|
|
58
|
-
Short:
|
|
59
|
-
Long:
|
|
57
|
+
Use: "pippit-tool-cli",
|
|
58
|
+
Short: "Pippit CLI",
|
|
59
|
+
Long: "Pippit CLI generates and processes videos and images, queries credit balances, submits creative conversations and short-drama workflows, downloads generated assets, and updates the installed CLI package.\n\n" +
|
|
60
|
+
canvascmd.CommandDiscoveryHelp + "\n\nInstallation help (npm launcher): pippit-tool-cli install --help",
|
|
60
61
|
Version: version.Current(),
|
|
61
62
|
SilenceUsage: true,
|
|
62
63
|
SilenceErrors: true,
|
|
@@ -72,6 +73,8 @@ func newRootCommand(stdout, stderr io.Writer, runner *common.Runner) *cobra.Comm
|
|
|
72
73
|
root.AddCommand(newDownloadResultCommand(stdout, stderr, runner))
|
|
73
74
|
root.AddCommand(newGetCreditBalanceCommand(stdout, stderr, runner))
|
|
74
75
|
root.AddCommand(newGetThreadCommand(stdout, stderr, runner))
|
|
76
|
+
root.AddCommand(newSubmitRunCommand(stdout, stderr, runner))
|
|
77
|
+
root.AddCommand(newUploadFileCommand(stdout, stderr, runner))
|
|
75
78
|
root.AddCommand(newListThreadFileCommand(stdout, stderr, runner))
|
|
76
79
|
root.AddCommand(generate_image.NewCommand(stdout, stderr, runner))
|
|
77
80
|
root.AddCommand(generate_video.NewCommand(stdout, stderr, runner))
|
package/cmd/root_test.go
CHANGED
|
@@ -67,3 +67,41 @@ func TestRootRegistersTopLevelBrowserAuthCommands(t *testing.T) {
|
|
|
67
67
|
}
|
|
68
68
|
}
|
|
69
69
|
}
|
|
70
|
+
|
|
71
|
+
func TestHelpDiscoversNpmCanvasCommands(t *testing.T) {
|
|
72
|
+
for _, args := range [][]string{{"--help"}, {"canvas", "--help"}, {"canvas", "command", "--help"}, {"canvas", "command"}} {
|
|
73
|
+
t.Run(strings.Join(args, " "), func(t *testing.T) {
|
|
74
|
+
var stdout, stderr bytes.Buffer
|
|
75
|
+
root := NewRootCommand(&stdout, &stderr)
|
|
76
|
+
root.SetArgs(args)
|
|
77
|
+
if err := root.Execute(); err != nil {
|
|
78
|
+
t.Fatalf("Execute() error = %v", err)
|
|
79
|
+
}
|
|
80
|
+
for _, action := range []string{"list", "describe", "schema", "guide", "run"} {
|
|
81
|
+
if !strings.Contains(stdout.String(), "canvas command "+action) {
|
|
82
|
+
t.Fatalf("help does not expose %s: %s", action, stdout.String())
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
if !strings.Contains(stdout.String(), "npm launcher") {
|
|
86
|
+
t.Fatalf("help does not explain launcher boundary: %s", stdout.String())
|
|
87
|
+
}
|
|
88
|
+
})
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
func TestNativeCanvasCommandsRequireNpmLauncher(t *testing.T) {
|
|
93
|
+
for _, action := range []string{"list", "describe", "schema", "guide", "run", "unknown-action"} {
|
|
94
|
+
t.Run(action, func(t *testing.T) {
|
|
95
|
+
var stdout, stderr bytes.Buffer
|
|
96
|
+
root := NewRootCommand(&stdout, &stderr)
|
|
97
|
+
root.SetArgs([]string{"canvas", "command", action, "--category", "example"})
|
|
98
|
+
err := root.Execute()
|
|
99
|
+
if err == nil || !strings.Contains(err.Error(), "npm launcher") {
|
|
100
|
+
t.Fatalf("Execute() error = %v, want launcher guidance", err)
|
|
101
|
+
}
|
|
102
|
+
if stdout.Len() != 0 {
|
|
103
|
+
t.Fatalf("stdout = %q, must not claim execution success", stdout.String())
|
|
104
|
+
}
|
|
105
|
+
})
|
|
106
|
+
}
|
|
107
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
package cmd
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
"fmt"
|
|
5
|
+
"io"
|
|
6
|
+
"strings"
|
|
7
|
+
|
|
8
|
+
"github.com/Pippit-dev/pippit-cli/internal/common"
|
|
9
|
+
"github.com/spf13/cobra"
|
|
10
|
+
)
|
|
11
|
+
|
|
12
|
+
func newSubmitRunCommand(stdout, stderr io.Writer, runner *common.Runner) *cobra.Command {
|
|
13
|
+
var body struct {
|
|
14
|
+
Message string `json:"message"`
|
|
15
|
+
ThreadID string `json:"thread_id,omitempty"`
|
|
16
|
+
AssetIDs []string `json:"asset_ids,omitempty"`
|
|
17
|
+
}
|
|
18
|
+
cmd := &cobra.Command{
|
|
19
|
+
Use: "submit-run",
|
|
20
|
+
Short: "Create a creative conversation or send a message to an existing thread",
|
|
21
|
+
Args: cobra.NoArgs,
|
|
22
|
+
RunE: withErrorLog("submit-run", nil, func(cmd *cobra.Command, _ []string) error {
|
|
23
|
+
if strings.TrimSpace(body.Message) == "" {
|
|
24
|
+
return fmt.Errorf("缺少必填参数 --message")
|
|
25
|
+
}
|
|
26
|
+
// Preserve the original message and identifiers; the backend owns routing.
|
|
27
|
+
result, err := common.SubmitRun(cmd.Context(), "submit-run", body, runner)
|
|
28
|
+
if err != nil {
|
|
29
|
+
return err
|
|
30
|
+
}
|
|
31
|
+
return common.WriteJSON(stdout, result)
|
|
32
|
+
}),
|
|
33
|
+
}
|
|
34
|
+
cmd.SetOut(stdout)
|
|
35
|
+
cmd.SetErr(stderr)
|
|
36
|
+
cmd.Flags().StringVar(&body.Message, "message", "", "original message to send (required)")
|
|
37
|
+
cmd.Flags().StringVar(&body.ThreadID, "thread-id", "", "existing thread ID; omit to create a new thread")
|
|
38
|
+
cmd.Flags().StringArrayVar(&body.AssetIDs, "asset-ids", nil, "asset ID to attach; repeat for multiple assets")
|
|
39
|
+
return cmd
|
|
40
|
+
}
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
package cmd
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
"bytes"
|
|
5
|
+
"encoding/json"
|
|
6
|
+
"net/http"
|
|
7
|
+
"net/http/httptest"
|
|
8
|
+
"reflect"
|
|
9
|
+
"strings"
|
|
10
|
+
"testing"
|
|
11
|
+
|
|
12
|
+
"github.com/Pippit-dev/pippit-cli/internal/config"
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
func TestSubmitRunCommandPreservesScriptContract(t *testing.T) {
|
|
16
|
+
for _, existing := range []bool{false, true} {
|
|
17
|
+
t.Run(map[bool]string{false: "new", true: "existing_with_assets"}[existing], func(t *testing.T) {
|
|
18
|
+
message := " 根据参考素材生成视频\n保留原文 "
|
|
19
|
+
want := map[string]any{"message": message}
|
|
20
|
+
args := []string{"submit-run", "--message", message}
|
|
21
|
+
if existing {
|
|
22
|
+
want["thread_id"] = "skill_original_thread"
|
|
23
|
+
want["asset_ids"] = []any{"asset_original_1", "asset_original_2"}
|
|
24
|
+
args = append(args, "--thread-id", "skill_original_thread", "--asset-ids", "asset_original_1", "--asset-ids", "asset_original_2")
|
|
25
|
+
}
|
|
26
|
+
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
27
|
+
if r.Method != http.MethodPost || r.URL.Path != config.SubmitRunPath {
|
|
28
|
+
t.Errorf("request = %s %s", r.Method, r.URL.Path)
|
|
29
|
+
}
|
|
30
|
+
if r.Header.Get("Authorization") != "Bearer test-token" {
|
|
31
|
+
t.Error("missing bearer authorization")
|
|
32
|
+
}
|
|
33
|
+
var got map[string]any
|
|
34
|
+
if err := json.NewDecoder(r.Body).Decode(&got); err != nil {
|
|
35
|
+
t.Error(err)
|
|
36
|
+
}
|
|
37
|
+
if !reflect.DeepEqual(got, want) {
|
|
38
|
+
t.Errorf("body = %#v, want %#v", got, want)
|
|
39
|
+
}
|
|
40
|
+
_, _ = w.Write([]byte(`{"ret":"0","data":{"run":{"thread_id":"thread_1","run_id":"run_1"},"web_thread_link":"https://xyq.jianying.com/thread_1"}}`))
|
|
41
|
+
}))
|
|
42
|
+
defer server.Close()
|
|
43
|
+
var stdout, stderr bytes.Buffer
|
|
44
|
+
root := newTestRootCommand(t, &stdout, &stderr, server.URL)
|
|
45
|
+
root.SetArgs(args)
|
|
46
|
+
if err := root.Execute(); err != nil {
|
|
47
|
+
t.Fatal(err)
|
|
48
|
+
}
|
|
49
|
+
if got := stdout.String(); got != "{\"thread_id\":\"thread_1\",\"run_id\":\"run_1\",\"web_thread_link\":\"https://xyq.jianying.com/thread_1\"}\n" {
|
|
50
|
+
t.Fatalf("unexpected output: %s", got)
|
|
51
|
+
}
|
|
52
|
+
})
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
func TestSubmitRunCommandValidatesBeforeRequest(t *testing.T) {
|
|
57
|
+
for _, args := range [][]string{
|
|
58
|
+
{"submit-run"},
|
|
59
|
+
{"submit-run", "--message", " \n "},
|
|
60
|
+
{"submit-run", "--message", "hello", "unexpected"},
|
|
61
|
+
} {
|
|
62
|
+
var stdout, stderr bytes.Buffer
|
|
63
|
+
root := newRootCommand(&stdout, &stderr, nil)
|
|
64
|
+
root.SetArgs(args)
|
|
65
|
+
if err := root.Execute(); err == nil {
|
|
66
|
+
t.Fatalf("Execute(%v) should fail before accessing the runner", args)
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
func TestSubmitRunHelpDoesNotRequireCredentials(t *testing.T) {
|
|
72
|
+
var stdout, stderr bytes.Buffer
|
|
73
|
+
root := newRootCommand(&stdout, &stderr, nil)
|
|
74
|
+
root.SetArgs([]string{"submit-run", "--help"})
|
|
75
|
+
if err := root.Execute(); err != nil {
|
|
76
|
+
t.Fatal(err)
|
|
77
|
+
}
|
|
78
|
+
for _, flag := range []string{"--message", "--thread-id", "--asset-ids"} {
|
|
79
|
+
if !strings.Contains(stdout.String(), flag) {
|
|
80
|
+
t.Errorf("help missing %s", flag)
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
func TestSubmitRunCommandRejectsInvalidResponse(t *testing.T) {
|
|
86
|
+
for _, response := range []string{
|
|
87
|
+
`{"ret":"1","errmsg":"rejected"}`,
|
|
88
|
+
`{"ret":"0","data":{"run":{"run_id":"run_1"}}}`,
|
|
89
|
+
`{"ret":"0","data":{"run":{"thread_id":"thread_1"}}}`,
|
|
90
|
+
} {
|
|
91
|
+
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
92
|
+
_, _ = w.Write([]byte(response))
|
|
93
|
+
}))
|
|
94
|
+
var stdout, stderr bytes.Buffer
|
|
95
|
+
root := newTestRootCommand(t, &stdout, &stderr, server.URL)
|
|
96
|
+
root.SetArgs([]string{"submit-run", "--message", "hello"})
|
|
97
|
+
err := root.Execute()
|
|
98
|
+
server.Close()
|
|
99
|
+
if err == nil || stdout.Len() != 0 {
|
|
100
|
+
t.Fatalf("response %s: error = %v, stdout = %s", response, err, stdout.String())
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
package cmd
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
"fmt"
|
|
5
|
+
"io"
|
|
6
|
+
"mime"
|
|
7
|
+
"os"
|
|
8
|
+
"path/filepath"
|
|
9
|
+
"strings"
|
|
10
|
+
|
|
11
|
+
"github.com/Pippit-dev/pippit-cli/internal/common"
|
|
12
|
+
"github.com/spf13/cobra"
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
const maxMediaUploadBytes int64 = 500 * 1000 * 1000
|
|
16
|
+
|
|
17
|
+
func newUploadFileCommand(stdout, stderr io.Writer, runner *common.Runner) *cobra.Command {
|
|
18
|
+
var opts common.UploadFileOptions
|
|
19
|
+
cmd := &cobra.Command{
|
|
20
|
+
Use: "upload-file",
|
|
21
|
+
Short: "Upload an image, video, or MP3/WAV audio file",
|
|
22
|
+
Args: cobra.NoArgs,
|
|
23
|
+
RunE: withErrorLog("upload-file", nil, func(cmd *cobra.Command, _ []string) error {
|
|
24
|
+
opts.Path = strings.TrimSpace(opts.Path)
|
|
25
|
+
if err := validateMediaUpload(opts.Path); err != nil {
|
|
26
|
+
return err
|
|
27
|
+
}
|
|
28
|
+
result, err := common.UploadFile(cmd.Context(), opts, runner)
|
|
29
|
+
if err != nil {
|
|
30
|
+
return err
|
|
31
|
+
}
|
|
32
|
+
return common.WriteJSON(stdout, result)
|
|
33
|
+
}),
|
|
34
|
+
}
|
|
35
|
+
cmd.SetOut(stdout)
|
|
36
|
+
cmd.SetErr(stderr)
|
|
37
|
+
cmd.Flags().StringVar(&opts.Path, "path", "", "local media file to upload (required, less than 500 MB)")
|
|
38
|
+
return cmd
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
func validateMediaUpload(path string) error {
|
|
42
|
+
if path == "" {
|
|
43
|
+
return fmt.Errorf("缺少必填参数 --path")
|
|
44
|
+
}
|
|
45
|
+
info, err := os.Stat(path)
|
|
46
|
+
if err != nil {
|
|
47
|
+
return fmt.Errorf("获取上传文件信息失败: %w", err)
|
|
48
|
+
}
|
|
49
|
+
if !info.Mode().IsRegular() {
|
|
50
|
+
return fmt.Errorf("上传路径必须是普通文件")
|
|
51
|
+
}
|
|
52
|
+
if info.Size() >= maxMediaUploadBytes {
|
|
53
|
+
return fmt.Errorf("上传文件必须小于 500 MB(500000000 字节)")
|
|
54
|
+
}
|
|
55
|
+
ext := strings.ToLower(filepath.Ext(path))
|
|
56
|
+
contentType := mime.TypeByExtension(ext)
|
|
57
|
+
if strings.HasPrefix(contentType, "image/") || strings.HasPrefix(contentType, "video/") {
|
|
58
|
+
return nil
|
|
59
|
+
}
|
|
60
|
+
if ext == ".mp3" || ext == ".wav" {
|
|
61
|
+
return nil
|
|
62
|
+
}
|
|
63
|
+
return fmt.Errorf("不支持的文件类型 %q,仅支持图片、视频和 .mp3/.wav 音频", ext)
|
|
64
|
+
}
|