@pippit-dev/cli 1.0.22 → 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.
@@ -0,0 +1,19 @@
1
+ #!/usr/bin/env node
2
+
3
+ const { install } = require("./install");
4
+
5
+ if (require.main === module) {
6
+ if (process.argv.length === 3 && process.argv[2] === "--help") {
7
+ console.log("Usage: node scripts/install-cli.js\nInstall this package's CLI binary without changing global Skills.");
8
+ } else if (process.argv.length !== 2) {
9
+ console.error("Unsupported arguments. Usage: node scripts/install-cli.js");
10
+ process.exitCode = 1;
11
+ } else {
12
+ try {
13
+ install({ cliOnly: true });
14
+ } catch (err) {
15
+ console.error(`Failed to install pippit-tool-cli: ${err.message || err}`);
16
+ process.exitCode = 1;
17
+ }
18
+ }
19
+ }
@@ -115,7 +115,7 @@ function extractArchive(archivePath, destDir) {
115
115
  run("tar", ["-xzf", archivePath, "-C", destDir]);
116
116
  }
117
117
 
118
- function install() {
118
+ function install({ cliOnly = false } = {}) {
119
119
  if (!platform || !arch) {
120
120
  throw new Error(`Unsupported platform: ${process.platform}-${process.arch}`);
121
121
  }
@@ -133,11 +133,13 @@ function install() {
133
133
  fs.copyFileSync(extracted, dest);
134
134
  fs.chmodSync(dest, 0o755);
135
135
 
136
- if (process.env.PIPPIT_CLI_SKIP_SKILLS !== "1") {
137
- installSkillsFromRoot(ROOT);
138
- reportBundledSkillTelemetry("install", "npm_install");
139
- } else {
140
- cleanupLegacyGlobalSkills();
136
+ if (!cliOnly) {
137
+ if (process.env.PIPPIT_CLI_SKIP_SKILLS !== "1") {
138
+ installSkillsFromRoot(ROOT);
139
+ reportBundledSkillTelemetry("install", "npm_install");
140
+ } else {
141
+ cleanupLegacyGlobalSkills();
142
+ }
141
143
  }
142
144
  console.log(`${NAME} v${VERSION} installed successfully`);
143
145
  } finally {
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: xyq-skill
3
- description: 通过小云雀的 AI 能力进行综合创作,支持生成和编辑图片/视频,并在用户明确要求图片或视频模型直出、指定图片或视频模型或直接调用 CLI 时使用 pippit-tool-cli generate-image / generate-video;用户要求视频超分、提升视频清晰度、擦字幕或去字幕时,使用 video-super-resolution / erase-video-subtitle。覆盖文生图、文生视频、图生视频、首尾帧生视频、视频编辑、风格转换、视频续写、视频复刻、TVC、宣传片、音乐 MV、产品广告、分镜和教育短视频等场景。当用户提到小云雀、xyq、上传参考图/视频/mp3或wav音频、查看生成进度,或查询小云雀积分余额、剩余积分、credits 时也应触发;积分查询使用 pippit-tool-cli get-credit-balance。短剧生成、续写、改写、人物设定和分集创作应使用 xyq-short-drama-skill,不在本技能中执行。
3
+ description: 通过小云雀的 AI 能力进行综合创作,支持生成和编辑图片/视频,并在用户明确要求图片或视频模型直出、指定图片或视频模型或直接调用 CLI 时使用 pippit-tool-cli generate-image / generate-video;用户要求视频超分、提升视频清晰度、擦字幕或去字幕时,使用 video-super-resolution / erase-video-subtitle。覆盖文生图、文生视频、图生视频、首尾帧生视频、视频编辑、风格转换、视频续写、视频复刻、TVC、宣传片、音乐 MV、产品广告、分镜和教育短视频等场景。当用户提到小云雀、xyq、上传参考图/视频/mp3或wav音频、查看生成进度,或查询小云雀积分余额、剩余积分、credits 时也应触发;积分查询使用 pippit-tool-cli get-credit-balance
4
4
  user-invocable: true
5
5
  metadata:
6
6
  {
@@ -9,7 +9,7 @@ metadata:
9
9
  "emoji": "💬",
10
10
  "requires":
11
11
  {
12
- "bins": ["python3"],
12
+ "bins": ["python3", "node"],
13
13
  "env": ["XYQ_ACCESS_KEY"]
14
14
  },
15
15
  "primaryEnv": "XYQ_ACCESS_KEY"
@@ -23,6 +23,8 @@ metadata:
23
23
 
24
24
  小云雀是一个 AI 综合创作平台,同时为人类创作者和 Agent 设计。Agent 通过 Skill 入口理解任务、调用模型并自动编排工作流。
25
25
 
26
+ 每次开始执行本技能任务时,先按“前置要求”运行 `scripts/ensure-cli.js`,检查已有 CLI,不存在或缺少必需命令时获取最新版本。本文命令中的 `pippit-tool-cli` 均代表该脚本返回的 `cli_path`;实际执行时替换为带引号的绝对路径。
27
+
26
28
  **平台核心能力:**
27
29
  - **生成**:文生图、文生视频、图生视频、视频续写
28
30
  - **编辑**:局部修改、元素替换、镜头调整、风格迁移
@@ -33,11 +35,11 @@ metadata:
33
35
 
34
36
  ## 执行路由(必须先判断)
35
37
 
36
- 积分余额查询优先走路由 F,不进入创作、视频处理或短剧工作流。
38
+ 积分余额查询优先走路由 E,不进入创作或视频处理工作流。
37
39
 
38
40
  ### 路由 A:图片模型直出
39
41
 
40
- 满足任一条件时,必须直接使用 `pippit-tool-cli generate-image`,不要改走 `submit_run.py`:
42
+ 满足任一条件时,必须直接使用 `pippit-tool-cli generate-image`,不要改走 `pippit-tool-cli submit-run`:
41
43
 
42
44
  - 用户明确说“图片模型直出”、“直接调图片模型”或明确要求用 CLI 生图。
43
45
  - 用户指定了具体图片模型(如 `seedream_5.0_pro`),并希望单次直接生成图片。
@@ -45,7 +47,7 @@ metadata:
45
47
 
46
48
  执行原则:
47
49
 
48
- 1. 执行前用 `command -v pippit-tool-cli` 确认 CLI 可用;不可用时报告阻塞,不要悄悄降级到会话 API。
50
+ 1. 执行前完成“前置要求”的CLI 安装检查,使用返回的 `cli_path`;失败时报告阻塞,不要悄悄降级到会话 API。
49
51
  2. 真实提交会消耗 credits;如果用户本轮尚未明确确认生成,按“用户确认与反问”规则征得明确确认后再运行。
50
52
  3. 保留用户原始 prompt,不要自行扩写、润色、翻译或增加风格词。
51
53
  4. `--model` 必填;用户未提供图片模型时,先询问使用哪个模型。只添加用户已经给出的 `--ratio`、`--resolution`、`--generate-image-count`、`--image` 参数,不补默认值。
@@ -70,7 +72,7 @@ pippit-tool-cli query-result \
70
72
 
71
73
  ### 路由 B:视频模型直出
72
74
 
73
- 满足任一条件时,必须直接使用 `pippit-tool-cli generate-video`,不要改走 `submit_run.py`:
75
+ 满足任一条件时,必须直接使用 `pippit-tool-cli generate-video`,不要改走 `pippit-tool-cli submit-run`:
74
76
 
75
77
  - 用户明确说“视频模型直出”、“直接调模型”或“直接调用 CLI”。
76
78
  - 用户指定了具体视频模型(如 `Seedance_2.5`),并希望单次直接生成视频。
@@ -79,7 +81,7 @@ pippit-tool-cli query-result \
79
81
 
80
82
  执行原则:
81
83
 
82
- 1. 执行前用 `command -v pippit-tool-cli` 确认 CLI 可用;不可用时报告阻塞,不要悄悄降级到会话 API。
84
+ 1. 执行前完成“前置要求”的CLI 安装检查,使用返回的 `cli_path`;失败时报告阻塞,不要悄悄降级到会话 API。
83
85
  2. 真实提交会消耗 credits;如果用户本轮尚未明确确认生成,按“用户确认与反问”规则征得明确确认后再运行。
84
86
  3. 保留用户原始 prompt,不要自行扩写、润色、翻译或增加风格词。
85
87
  4. 只添加用户已经给出的 `--model`、`--duration`、`--ratio`、`--resolution`、`--image`、`--video`、`--audio`、`--generate-type` 参数;未给参数交给 CLI 默认值。
@@ -107,14 +109,14 @@ pippit-tool-cli query-result \
107
109
 
108
110
  ### 路由 C:视频超分和擦字幕
109
111
 
110
- 用户明确要求视频超分、提升视频清晰度、擦字幕或去字幕时,直接调用对应的 `pippit-tool-cli` 视频处理命令,不要改走 `submit_run.py`:
112
+ 用户明确要求视频超分、提升视频清晰度、擦字幕或去字幕时,直接调用对应的 `pippit-tool-cli` 视频处理命令,不要改走 `pippit-tool-cli submit-run`:
111
113
 
112
114
  - 视频超分、提升视频清晰度:`video-super-resolution`
113
115
  - 擦字幕、去字幕:`erase-video-subtitle`
114
116
 
115
117
  执行原则:
116
118
 
117
- 1. 执行前用 `command -v pippit-tool-cli` 确认 CLI 可用;不可用时报告阻塞,不要悄悄降级到会话 API。
119
+ 1. 执行前完成“前置要求”的CLI 安装检查,使用返回的 `cli_path`;失败时报告阻塞,不要悄悄降级到会话 API。
118
120
  2. 真实提交会消耗 credits;如果用户本轮尚未明确确认处理,按“用户确认与反问”规则征得明确确认后再运行。
119
121
  3. 把用户提供的本地视频路径和处理参数直接交给对应 CLI;缺少必填输入时先询问用户。
120
122
  4. 命令返回后,保存 `thread_id`、`run_id`,并立即向用户展示 `web_thread_link`。
@@ -136,19 +138,17 @@ pippit-tool-cli query-result \
136
138
 
137
139
  ### 路由 D:小云雀后端 Agent 编排
138
140
 
139
- 需要意图确认、脚本/分镜拆解、MV、TVC、局部编辑、复杂参考素材编排,或者用户未明确要求模型直出的创作需求,继续使用本技能内置的 `submit_run.py` / `get_thread.py` 会话工作流;明确的首尾帧请求走路由 B,明确的视频超分和擦字幕请求走路由 C;积分余额查询走路由 F
140
-
141
- ### 路由 E:短剧工作流
141
+ 需要意图确认、脚本/分镜拆解、MV、TVC、局部编辑、复杂参考素材编排,或者用户未明确要求模型直出的创作需求,使用 `pippit-tool-cli submit-run` 提交消息,再用 `get_thread.py` 查询会话进展;明确的首尾帧请求走路由 B,明确的视频超分和擦字幕请求走路由 C;积分余额查询走路由 E
142
142
 
143
- 用户要求短剧生成、续写、改写、剧情扩展、人物设定、分集草稿或短剧会话文件处理时,停止本技能流程并转交 `xyq-short-drama-skill`,不要用 `submit_run.py`、`generate-image` `generate-video` 假装执行完整短剧流程。
143
+ 提交前完成“前置要求”的CLI 安装检查,使用返回的 `cli_path` 调用 `submit-run`;失败时报告版本或安装阻塞。
144
144
 
145
- ### 路由 F:积分余额查询
145
+ ### 路由 E:积分余额查询
146
146
 
147
147
  用户询问小云雀“积分余额”、“还剩多少积分”、“剩余 credits”或要求查询个人有效积分时,直接使用 `pippit-tool-cli get-credit-balance`。
148
148
 
149
149
  执行原则:
150
150
 
151
- 1. 执行前用 `command -v pippit-tool-cli` 确认 CLI 可用;不可用或版本不支持该命令时报告阻塞,不要改走 `submit_run.py`。
151
+ 1. 执行前完成“前置要求”的CLI 安装检查,使用返回的 `cli_path`;不可用或版本不支持该命令时报告阻塞,不要改走 `pippit-tool-cli submit-run`。
152
152
  2. 使用当前 CLI 登录凭证或显式配置的 `XYQ_ACCESS_KEY` 查询凭证所属用户的个人有效积分余额;无需传入用户 ID、`thread_id` 或 `run_id`。鉴权要求见“前置要求”。
153
153
  3. 这是只读查询,不需要积分消耗确认;不创建会话,不提交生成任务,也不调用 `get_thread.py` 或 `query-result` 轮询。
154
154
  4. 成功时读取 JSON 中字符串类型的 `total_remain_amount`,向用户展示当前有效积分余额;`"0"` 是有效的零余额。查询失败或缺少余额字段时报告错误,不得当作零余额。
@@ -175,7 +175,7 @@ pippit-tool-cli get-credit-balance --with-log-id
175
175
  - **Codex**:准确工具名是 `request_user_input`。仅在工具已暴露且当前模式允许时调用;不可用时退回普通聊天提问。不要在 Codex 中调用 `ask_user_question`。
176
176
  - **WorkBuddy**:准确工具名是 `ask_user_question`(Ask User Question)。需要用户补充、选择或确认时优先调用;工具未暴露时才退回普通聊天提问。
177
177
  - **Trae 及其他宿主**:先查看当前宿主实际暴露的工具,再使用同类结构化提问、确认或表单工具;不要臆造具体工具名。没有同类工具时退回普通聊天提问。
178
- 2. 涉及 credits 消耗、真实生成、外部提交或不可逆操作时,必须等待用户明确答复;不要默认同意或超时后继续。路由 F 的只读积分余额查询不需要额外确认。
178
+ 2. 涉及 credits 消耗、真实生成、外部提交或不可逆操作时,必须等待用户明确答复;不要默认同意或超时后继续。路由 E 的只读积分余额查询不需要额外确认。
179
179
  3. 后端已经给出问题或选项时,保持原意传给用户,不要代替用户回答。
180
180
  4. 当前宿主没有结构化提问工具,或当前模式不允许调用时,使用一条简洁的普通聊天问题并暂停。
181
181
  5. 收到回复后,把用户答案原样发回同一 `thread_id`,获取新的 `run_id`,再继续轮询;不要新开会话。
@@ -194,7 +194,29 @@ pippit-tool-cli get-credit-balance --with-log-id
194
194
 
195
195
  ## 前置要求
196
196
 
197
- 图片/视频模型直出、视频处理和积分余额查询(路由 A/B/C/F)使用原生 CLI。首次使用时运行网页登录,CLI 会自动申请或复用本机专属凭证,并保存到系统安全凭证库:
197
+ ### 检查并按需安装 CLI
198
+
199
+ 运行环境需要 Python 3、Node.js 16+,支持执行本地程序。首次安装或自动升级 CLI 时需要 npm、可写的用户缓存目录、访问 npm 源及 GitHub Release 的网络、`curl` 和解压工具(macOS/Linux 的 `tar`,Windows 的 PowerShell)。复用已有 CLI 不需要 npm 或下载网络;安装或升级缺少这些能力时,报告具体安装阻塞。
200
+
201
+ 开始执行本技能任务时运行以下脚本。它先检查 PATH 中的 CLI,再检查自身的安装缓存;找到命令齐全的 CLI 就复用,不访问 npm 或下载二进制。两处均不存在 CLI,或已有 CLI 的必需命令帮助检查返回非零退出码时,获取 npm `latest` 并安装或升级。PATH 中的旧版本缺少命令但缓存可用时,直接复用缓存,不重复升级。同一任务内的提交、轮询、上传和下载复用返回路径。
202
+
203
+ ```bash
204
+ node "{baseDir}/scripts/ensure-cli.js"
205
+ ```
206
+
207
+ 需要安装或升级时,脚本跳过 npm 生命周期脚本获取 `@pippit-dev/cli@latest`,再调用包内的 `scripts/install-cli.js` 只安装 CLI。新版本通过全部检查后保存在 `~/.cache/pippit-tool-cli/xyq-skill/<平台>-<架构>/current`,供后续任务复用。它不会安装、清理全局 Skill,也不要求全局 npm 写入权限。安装与检查日志写入 stderr,成功时 stdout 返回 JSON:
208
+
209
+ ```json
210
+ {"cli_path":"/absolute/cache/path/current/node_modules/@pippit-dev/cli/bin/pippit-tool-cli","version":"实际安装版本"}
211
+ ```
212
+
213
+ 保存 `cli_path`,后续用它替换所有示例中的 `pippit-tool-cli`。不要依赖上一次 shell 调用中的临时环境变量;Windows PowerShell 用 `& "绝对路径" 参数` 调用。保留安装缓存以便后续任务复用;如果路径已被清理,重新运行安装脚本。
214
+
215
+ 脚本验证 CLI 版本命令,以及 `login`、`submit-run`、`upload-file`、`download-result`、`query-result`、`generate-image`、`generate-video`、`video-super-resolution`、`erase-video-subtitle`、`get-credit-balance` 的 `--help`。检查不发送创作请求,也不需要凭据。已有 CLI 缺少必需命令时自动升级;版本命令无法运行或检查超时时报告运行错误。单次调用最多下载安装一次,最新版本仍不支持必需命令时停止并报告,不反复升级。升级成功前保留原安装;下载失败或最新包缺少只安装 CLI 的入口时,报告安装阻塞。
216
+
217
+ ### 配置凭据
218
+
219
+ 创建会话/发送消息、媒体上传、图片/视频模型直出、视频处理和积分余额查询(路由 A/B/C/D/E)使用原生 CLI。首次使用时运行网页登录,CLI 会自动申请或复用本机专属凭证,并保存到系统安全凭证库:
198
220
 
199
221
  ```bash
200
222
  pippit-tool-cli login
@@ -202,15 +224,15 @@ pippit-tool-cli login
202
224
 
203
225
  `XYQ_ACCESS_KEY` 仅作为原生 CLI 在 CI、Agent 等非交互环境中的显式覆盖。如果该环境变量已经设置但无效,CLI 不会静默改用网页登录凭证,应先修正或取消该环境变量。
204
226
 
205
- 默认的后端 Agent 编排(路由 D)仍由独立 Python 脚本 `submit_run.py`、`get_thread.py` 和 `upload_file.py` 执行。这些脚本尚未接入 CLI 的系统安全凭证库,使用前必须配置:
227
+ 路由 D 使用 CLI 提交消息和上传素材;查询进展仍使用独立 Python 脚本 `get_thread.py`。该脚本尚未接入 CLI 的系统安全凭证库,使用前必须配置同一用户的凭证:
206
228
 
207
229
  ```bash
208
230
  export XYQ_ACCESS_KEY="your-access-key"
209
231
  ```
210
232
 
211
- 可选:`XYQ_OPENAPI_BASE` `XYQ_BASE_URL`,默认 `https://xyq.jianying.com`。
233
+ 原生 CLI 和保留的 Python 脚本携带用户密钥的 API 请求固定发往 `https://xyq.jianying.com`,不接受 `XYQ_OPENAPI_BASE` 或 `XYQ_BASE_URL` 覆盖。CLI 拒绝 API 跨域重定向,Python API 脚本禁止自动重定向;上传只通过 Authorization 请求头携带密钥。
212
234
 
213
- 会话 API 路由无需安装额外依赖,仅使用 Python 标准库。图片/视频模型直出、视频处理和积分余额查询路由要求 `pippit-tool-cli` 在 `PATH` 中可用。
235
+ 所有 CLI 路由使用本次安装检查返回的 `cli_path`。保留的 Python 脚本仅使用标准库;安装检查脚本仅使用 Node.js 内置模块。
214
236
 
215
237
  ## 使用方法
216
238
 
@@ -218,12 +240,17 @@ export XYQ_ACCESS_KEY="your-access-key"
218
240
 
219
241
  ```bash
220
242
  # 创建新会话并发送「生一个动漫视频」
221
- python3 {baseDir}/scripts/submit_run.py --message "生一个动漫视频"
243
+ pippit-tool-cli submit-run --message "生一个动漫视频"
222
244
 
223
245
  # 向已有会话发送消息
224
- python3 {baseDir}/scripts/submit_run.py --message "再生成一个故事视频" --thread-id THREAD_ID
246
+ pippit-tool-cli submit-run --message "再生成一个故事视频" --thread-id THREAD_ID
247
+
248
+ # 携带多个已上传的素材,每个 ID 重复一次参数
249
+ pippit-tool-cli submit-run --message "根据参考素材生成视频" --asset-ids ASSET_ID1 --asset-ids ASSET_ID2
225
250
  ```
226
251
 
252
+ `--message` 必填且不能全为空白,内容原样发送。`--thread-id` 可选;`--asset-ids` 每次接收一个 ID,多个素材必须重复该参数,不能在一次参数后以空格罗列多个 ID。
253
+
227
254
  ### 2. 查询会话进展
228
255
 
229
256
  ```bash
@@ -235,37 +262,48 @@ python3 {baseDir}/scripts/get_thread.py --thread-id THREAD_ID --run-id RUN_ID --
235
262
 
236
263
  ### 3. 上传文件
237
264
 
265
+ 先完成“前置要求”的CLI 安装检查,再使用返回的 `cli_path` 调用 `upload-file`;缺少命令时报告版本或安装阻塞。该命令使用 CLI 登录凭证或显式设置的 `XYQ_ACCESS_KEY`,成功输出 `{"asset_id":"..."}`。
266
+
238
267
  - 当用户提供了参考的文件地址时,先进行文件上传,仅支持图片、视频、`.mp3/.wav` 音频。
239
- - 单次指令执行仅支持单个文件,多个文件可并行调用,单个文件大小必须在200MB以下。
268
+ - 单次指令执行仅支持单个文件,多个文件可并行调用,单个文件必须小于 500 MB(500000000 字节,达到上限会拒绝上传)。
240
269
 
241
270
  ```bash
242
271
  # 上传图片
243
- python3 {baseDir}/scripts/upload_file.py /path/to/image.png
272
+ pippit-tool-cli upload-file --path /path/to/image.png
244
273
 
245
274
  # 上传视频
246
- python3 {baseDir}/scripts/upload_file.py /path/to/video.mp4
275
+ pippit-tool-cli upload-file --path /path/to/video.mp4
247
276
 
248
277
  # 上传音频
249
- python3 {baseDir}/scripts/upload_file.py /path/to/audio.mp3
278
+ pippit-tool-cli upload-file --path /path/to/audio.mp3
250
279
  ```
251
280
 
252
281
  ### 4. 下载结果
253
282
 
254
- 任务完成后,可以将会话中的所有产物批量下载到本地。
283
+ 会话 API 路由从 `get_thread.py` 返回的 `messages` 中提取产物 URL,逐文件调用 `pippit-tool-cli download-result` 下载到本地。
284
+
285
+ - 输出目录沿用用户指定的目录,未指定时使用 `./xyq_output`。
286
+ - 保留原有命名规则:按产物 URL 列表顺序从 `01` 开始编号,有前缀时为 `前缀_01.ext`,无前缀时为 `01.ext`。扩展名优先取 URL 查询参数 `filename` 中的扩展名,其次取 URL 路径的扩展名,无法取得时使用 `.bin`。
287
+ - 将目录和文件名拼成完整的 `--output-path`;每个 URL 调用一次,可最多并行执行 5 个下载命令。重试时保持 URL 与目标路径的对应关系。
255
288
 
256
289
  ```bash
257
- # 指定 URL 列表,指定输出目录,指定文件名前缀(如 artifact_01.png, artifact_02.png ...)进行下载
258
- python3 {baseDir}/scripts/download_results.py --urls URL1 URL2 URL3 --output-dir ./xyq_output --prefix "artifact"
290
+ # 示例:输出目录 ./xyq_output,前缀 artifact,两个 URL 的扩展名分别为 .png .mp4
291
+ pippit-tool-cli download-result --url "URL1" --output-path "./xyq_output/artifact_01.png"
292
+ pippit-tool-cli download-result --url "URL2" --output-path "./xyq_output/artifact_02.mp4"
259
293
  ```
260
294
 
295
+ CLI 默认跳过已存在的目标文件,返回 `already_exist`。仅在来源提供真实的文件更新时间时传入 `--updated-at`(Unix 秒),让 CLI 根据本地文件修改时间决定是否重新下载;不要用当前时间代替远端更新时间。跳过不代表已校验本地内容与远端一致,不能将已知属于其他产物的同名文件当作本次结果。
296
+
297
+ 逐项收集下载结果;单项失败不阻断其他文件,只对失败项重试一次,仍失败则记录该产物、目标路径及 CLI 返回的错误。
298
+
261
299
  ## 典型工作流
262
300
 
263
- 理解这些工作流,才能正确组合上面的脚本完成用户需求。
301
+ 理解这些工作流,才能正确组合上面的 CLI 和脚本完成用户需求。
264
302
 
265
303
  ### 场景 1:用户要求生成图片或视频(非模型直出)
266
304
 
267
305
  ```
268
- 1. submit_run.py --message "用户的描述" → 拿到 thread_id、run_id 和 web_thread_link
306
+ 1. pippit-tool-cli submit-run --message "用户的描述" → 拿到 thread_id、run_id 和 web_thread_link
269
307
  2. **立即**将 `web_thread_link` 展示给用户(如"任务已提交,可在此查看:{web_thread_link}")
270
308
  3. 每隔 `10` 秒钟调用 get_thread.py --thread-id THREAD_ID --run-id RUN_ID --after-seq SEQUENCE 进行轮询
271
309
  4. 检查 messages:
@@ -278,14 +316,14 @@ python3 {baseDir}/scripts/download_results.py --urls URL1 URL2 URL3 --output-dir
278
316
  → 回到步骤 2 继续轮询(可能多轮,直到不再意图确认)
279
317
  - 如果 content 中包含产物 URL:
280
318
  → 信息展示 → 下载产物 → 结果展示
281
- 5. 自动下载:download_results.py --urls URL1 URL2 URL3 --output-dir 输出目录 --prefix 有意义的前缀
282
- 6. 向用户展示:过程中的创作信息,以及下载后的本地文件列表
319
+ 5. 自动下载:按“下载结果”的目录、前缀和编号规则,为每个产物 URL 调用 pippit-tool-cli download-result --url URL --output-path 完整文件路径
320
+ 6. 汇总每次调用的下载成功、已存在跳过和失败结果,向用户展示产物链接及对应的本地文件
283
321
  ```
284
322
 
285
323
  ### 场景 2:用户明确要求图片模型直出
286
324
 
287
325
  ```
288
- 1. command -v pippit-tool-cli → 确认 CLI 可用
326
+ 1. 按“前置要求”检查并按需安装 CLI,后续使用返回的 cli_path
289
327
  2. 检查图片模型:用户未提供时先询问,不要自行选择
290
328
  3. pippit-tool-cli generate-image --prompt "用户原始描述" --model IMAGE_MODEL [仅添加用户已给出的其他参数]
291
329
  4. 拿到 thread_id、run_id 和 web_thread_link,立即展示 web_thread_link
@@ -296,7 +334,7 @@ python3 {baseDir}/scripts/download_results.py --urls URL1 URL2 URL3 --output-dir
296
334
  ### 场景 3:用户明确要求视频模型直出(含首尾帧)
297
335
 
298
336
  ```
299
- 1. command -v pippit-tool-cli → 确认 CLI 可用
337
+ 1. 按“前置要求”检查并按需安装 CLI,后续使用返回的 cli_path
300
338
  2. 普通视频模型直出:pippit-tool-cli generate-video --prompt "用户原始描述" [仅添加用户已给出的其他参数]
301
339
  3. 首尾帧直出:确认两张图片的首帧/尾帧角色,按顺序执行 generate-video --image FIRST_FRAME_PATH --image LAST_FRAME_PATH --generate-type 1
302
340
  4. 拿到 thread_id、run_id 和 web_thread_link,立即展示 web_thread_link
@@ -307,9 +345,9 @@ python3 {baseDir}/scripts/download_results.py --urls URL1 URL2 URL3 --output-dir
307
345
  ### 场景 4:用户提供图片/视频/音频要求编辑修改或作为参考(如"参考这个视频做一个新的"、"用这首歌做MV")
308
346
 
309
347
  ```
310
- 1. upload_file.py /path/to/video.mp4 → 拿到 asset_id1
311
- 2. upload_file.py /path/to/audio.mp3 → 拿到 asset_id2
312
- 3. submit_run.py --message "参考这个视频并用这首歌做一个新的" --asset-ids asset_id1 asset_id2 → 拿到 thread_id、run_id、web_thread_link
348
+ 1. pippit-tool-cli upload-file --path /path/to/video.mp4 → 拿到 asset_id1
349
+ 2. pippit-tool-cli upload-file --path /path/to/audio.mp3 → 拿到 asset_id2
350
+ 3. pippit-tool-cli submit-run --message "参考这个视频并用这首歌做一个新的" --asset-ids asset_id1 --asset-ids asset_id2 → 拿到 thread_id、run_id、web_thread_link
313
351
  4. 后续同场景 1 的步骤 2-6
314
352
  ```
315
353
 
@@ -318,25 +356,25 @@ python3 {baseDir}/scripts/download_results.py --urls URL1 URL2 URL3 --output-dir
318
356
  ### 场景 5:用户提供参考图/视频/音频要求生成新内容
319
357
 
320
358
  ```
321
- 1. upload_file.py /path/to/ref1.png → 拿到 asset_id1
322
- 2. upload_file.py /path/to/ref2.mp4 → 拿到 asset_id2
323
- 3. upload_file.py /path/to/ref3.mp3 → 拿到 asset_id3
359
+ 1. pippit-tool-cli upload-file --path /path/to/ref1.png → 拿到 asset_id1
360
+ 2. pippit-tool-cli upload-file --path /path/to/ref2.mp4 → 拿到 asset_id2
361
+ 3. pippit-tool-cli upload-file --path /path/to/ref3.mp3 → 拿到 asset_id3
324
362
  4. 直到所有文件上传完成,拿到所有 asset_id
325
- 5. submit_run.py --message "根据参考图、视频、音频生成xxx" --asset-ids asset_id1 asset_id2 asset_id3, ... → 拿到 thread_id、run_id、web_thread_link
363
+ 5. pippit-tool-cli submit-run --message "根据参考图、视频、音频生成xxx" --asset-ids asset_id1 --asset-ids asset_id2 --asset-ids asset_id3 → 拿到 thread_id、run_id、web_thread_link
326
364
  6. 后续同场景 1 的步骤 2-6
327
365
  ```
328
366
 
329
367
  ### 场景 6:在已有会话中追加新需求
330
368
 
331
369
  ```
332
- 1. submit_run.py --message "新的描述" --thread-id THREAD_ID → 拿到 thread_id、run_id、web_thread_link
370
+ 1. pippit-tool-cli submit-run --message "新的描述" --thread-id THREAD_ID → 拿到 thread_id、run_id、web_thread_link
333
371
  2. 后续同场景 1 的步骤 2-6
334
372
  ```
335
373
 
336
374
  ### 场景 7:用户要求视频超分或擦字幕
337
375
 
338
376
  ```
339
- 1. command -v pippit-tool-cli → 确认 CLI 可用
377
+ 1. 按“前置要求”检查并按需安装 CLI,后续使用返回的 cli_path
340
378
  2. 根据用户意图调用 video-super-resolution 或 erase-video-subtitle,并传入用户提供的本地视频路径和处理参数
341
379
  3. 拿到 thread_id、run_id 和 web_thread_link,立即展示 web_thread_link
342
380
  4. 每隔 10 秒调用 query-result --thread-id THREAD_ID --run-id RUN_ID --download-dir OUTPUT_DIR
@@ -353,7 +391,7 @@ python3 {baseDir}/scripts/download_results.py --urls URL1 URL2 URL3 --output-dir
353
391
 
354
392
  ## 输出格式
355
393
 
356
- **submit_run** 返回:
394
+ **pippit-tool-cli submit-run** 返回:
357
395
  ```json
358
396
  {
359
397
  "thread_id": "90f05e0c-...",
@@ -385,22 +423,38 @@ python3 {baseDir}/scripts/download_results.py --urls URL1 URL2 URL3 --output-dir
385
423
  }
386
424
  ```
387
425
 
388
- **upload_file** 返回:
426
+ **pippit-tool-cli upload-file** 返回:
389
427
  ```json
390
428
  {
391
429
  "asset_id": "{asset_id}"
392
430
  }
393
431
  ```
394
432
 
395
- **download_results** 返回:
433
+ **pippit-tool-cli download-result** 每次下载一个文件,成功返回:
434
+ ```json
435
+ {
436
+ "output_path": "./xyq_output/artifact_01.png",
437
+ "downloaded": ["./xyq_output/artifact_01.png"]
438
+ }
439
+ ```
440
+
441
+ 目标文件已存在而跳过时返回:
396
442
  ```json
397
443
  {
398
- "output_dir": "./xyq_output",
399
- "downloaded": ["./xyq_output/01.png", "..."],
400
- "total": 10
444
+ "output_path": "./xyq_output/artifact_01.png",
445
+ "downloaded": null,
446
+ "already_exist": ["./xyq_output/artifact_01.png"]
401
447
  }
402
448
  ```
403
449
 
450
+ 单文件下载失败时,命令以非零退出码返回错误,不保证输出 JSON;不能只检查 JSON 中是否有 `errors` 来判断成功。由用户侧 Agent 汇总各次调用的 `downloaded`、`already_exist` 和失败项,不再依赖批量返回的 `output_dir`、`total`。
451
+
452
+ ## 会话 API 路由的下载完成标准
453
+
454
+ - run 结束后,先处理意图确认或流程中断;收到产物 URL 后才进入下载交付。
455
+ - 每个待交付产物都要有对应结果:本次下载成功、已存在而跳过,或下载失败。只有所有产物均已下载或明确复用已有文件时,才能报告本地交付完成。
456
+ - 已存在跳过的文件须单独说明,不能计为本次新下载;仍有失败项时报告“生成已完成,部分产物下载失败”,列出失败项和原始产物链接,不宣称全部下载完成。
457
+
404
458
  ## 向用户展示内容
405
459
 
406
460
  - 任务提交后:立即将 `web_thread_link` 展示给用户,方便用户直接打开浏览器查看任务页面
@@ -408,15 +462,14 @@ python3 {baseDir}/scripts/download_results.py --urls URL1 URL2 URL3 --output-dir
408
462
  - 展示过程中的创作信息等,继续轮询
409
463
  - 任务完成(run 结束):
410
464
  - 若涉及意图确认/流程中断(如"请回答以下问题")→ 按“用户确认与反问”规则优先调用结构化提问工具 → 等待用户回复 → 使用同一 `thread_id` 重新提交任务 → 继续轮询(可能多轮)
411
- - 若 content 中包含产物 URL
412
- - 结果地址:来自 `get_thread` 返回的 `messages` 中,任务创作完成会包含产物 URL,将产物链接、下载的本地文件等信息告知用户。
465
+ - 若 content 中包含产物 URL:展示来自 `get_thread` 返回的 `messages` 的产物链接,以及对应本地文件的可点击绝对路径;区分本次下载、已存在跳过和下载失败,并按上述完成标准说明交付状态。
413
466
 
414
467
  ## 核心原则:用户侧不做创作,只做传话
415
468
 
416
- 你(用户侧 Agent)的职责是**搬运工**,不是创作者。会话 API 路由由后端 Agent 负责理解需求、拆解分镜、编排工作流、选模型、写 prompt;图片/视频模型直出和视频处理路由把用户原始参数传给 CLI。积分余额查询按路由 F 直接查询并展示余额;以下步骤适用于创作和视频处理任务:
469
+ 你(用户侧 Agent)的职责是**搬运工**,不是创作者。会话 API 路由由后端 Agent 负责理解需求、拆解分镜、编排工作流、选模型、写 prompt;图片/视频模型直出和视频处理路由把用户原始参数传给 CLI。积分余额查询按路由 E 直接查询并展示余额;以下步骤适用于创作和视频处理任务:
417
470
 
418
- 1. **准备素材**:会话 API 路由用 `upload_file.py` 把本地文件转为 asset_id;图片/视频模型直出和视频处理路由把本地路径直接交给对应 CLI;首尾帧任务固定传 `--generate-type 1` 并保持首帧、尾帧顺序
419
- 2. **提交任务**:先按“执行路由”判断;图片模型直出调用 `pippit-tool-cli generate-image`,视频模型直出调用 `pippit-tool-cli generate-video`,视频超分和擦字幕调用对应的视频处理命令,其余通用创作任务把用户的原始描述 + asset_id 原封不动发给 `submit_run.py`
471
+ 1. **准备素材**:会话 API 路由用 `pippit-tool-cli upload-file` 把本地文件转为 asset_id;图片/视频模型直出和视频处理路由把本地路径直接交给对应 CLI;首尾帧任务固定传 `--generate-type 1` 并保持首帧、尾帧顺序
472
+ 2. **提交任务**:先按“执行路由”判断;图片模型直出调用 `pippit-tool-cli generate-image`,视频模型直出调用 `pippit-tool-cli generate-video`,视频超分和擦字幕调用对应的视频处理命令,其余通用创作任务把用户的原始描述 + asset_id 原封不动发给 `pippit-tool-cli submit-run`
420
473
  3. **传话**:根据 `get_thread.py` 返回的消息列表,展示过程中的意图询问、创作信息等
421
474
  4. **取件**:会话 API 路由用 `get_thread.py` 轮询,图片/视频模型直出和视频处理路由用 `query-result` 轮询 → 检查结果 → 下载产物 → 结果展示给用户
422
475
 
@@ -432,10 +485,10 @@ python3 {baseDir}/scripts/download_results.py --urls URL1 URL2 URL3 --output-dir
432
485
  用户说:「根据多张参考图,做个科普故事视频」
433
486
  用户给了参考图:/path/to/ref1.png, /path/to/ref2.png, /path/to/ref3.png
434
487
 
435
- upload_file.py /path/to/ref1.png → 拿到 asset_id1
436
- upload_file.py /path/to/ref2.png → 拿到 asset_id2
437
- upload_file.py /path/to/ref3.png → 拿到 asset_id3
438
- submit_run.py --message "根据参考图、视频生成xxx" --asset-ids asset_id1 asset_id2, asset_id3 → 拿到 web_thread_link,立即展示给用户
488
+ pippit-tool-cli upload-file --path /path/to/ref1.png → 拿到 asset_id1
489
+ pippit-tool-cli upload-file --path /path/to/ref2.png → 拿到 asset_id2
490
+ pippit-tool-cli upload-file --path /path/to/ref3.png → 拿到 asset_id3
491
+ pippit-tool-cli submit-run --message "根据参考图、视频生成xxx" --asset-ids asset_id1 --asset-ids asset_id2 --asset-ids asset_id3 → 拿到 web_thread_link,立即展示给用户
439
492
  → 轮询 ─┬─ 意图确认 → 用户确认 → 使用 thread_id 重新提交 → 继续轮询
440
493
  └─ 无意图确认 → 信息展示 → 下载产物 → 结果展示
441
494
  ```
@@ -452,6 +505,6 @@ python3 {baseDir}/scripts/download_results.py --urls URL1 URL2 URL3 --output-dir
452
505
  - 独立 Python 会话 API 脚本的鉴权方式为请求头 `Authorization: Bearer <XYQ_ACCESS_KEY>`
453
506
  - 创建会话时 `message` 是用户的指令要求,不能为空
454
507
  - 查询会话时可用 --after-seq 做增量拉取,便于轮询新消息(含 assistant 回复与生图/生视频结果)
455
- - 上传文件仅支持图片(image/*)、视频(video/*)和 `.mp3/.wav` 音频文件,其他类型会被拒绝,文件大小须在 200MB 以下
508
+ - 上传文件仅支持图片(image/*)、视频(video/*)和 `.mp3/.wav` 音频文件,其他类型会被拒绝,文件必须小于 500 MB(500000000 字节)
456
509
  - 生成过程中将过程中的创作信息展示给用户;任务完成后给出**产物结果(图片/视频)URL链接**和下载的**本地文件列表**。
457
510
  - 图片/视频模型直出和视频处理任务必须保留 CLI 返回的 `thread_id` / `run_id`,并用 `query-result` 取回最终图片或视频。
@@ -0,0 +1,149 @@
1
+ #!/usr/bin/env node
2
+
3
+ // Self-contained entrypoint shipped inside the Skill ZIP. No npm dependencies.
4
+ const { execFileSync } = require("child_process");
5
+ const fs = require("fs");
6
+ const os = require("os");
7
+ const path = require("path");
8
+
9
+ const REQUIRED_COMMANDS = [
10
+ "login", "submit-run", "upload-file", "download-result", "query-result",
11
+ "generate-image", "generate-video", "video-super-resolution",
12
+ "erase-video-subtitle", "get-credit-balance",
13
+ ];
14
+
15
+ function npmCommand() {
16
+ if (process.platform !== "win32") return ["npm", []];
17
+ // Run npm's JS entrypoint directly, avoiding cmd.exe quoting for paths with spaces.
18
+ const dirs = [path.dirname(process.execPath), ...(process.env.PATH || "").split(path.delimiter)];
19
+ for (const dir of dirs.filter(Boolean)) {
20
+ const script = path.join(dir, "node_modules", "npm", "bin", "npm-cli.js");
21
+ if (fs.existsSync(script)) return [process.execPath, [script]];
22
+ }
23
+ throw new Error("未找到 npm,请先安装 Node.js 16+ 和 npm。");
24
+ }
25
+
26
+ function findCLIOnPath() {
27
+ for (const dir of (process.env.PATH || "").split(path.delimiter).filter(Boolean)) {
28
+ const candidate = path.resolve(dir, `pippit-tool-cli${process.platform === "win32" ? ".exe" : ""}`);
29
+ if (fs.existsSync(candidate) && fs.statSync(candidate).isFile()) {
30
+ const resolved = fs.realpathSync(candidate);
31
+ // Bypass the npm JS launcher, which may download a binary or check for updates.
32
+ if (resolved.endsWith(path.join("scripts", "run.js"))) {
33
+ const binary = path.resolve(path.dirname(resolved), "../bin/pippit-tool-cli");
34
+ if (fs.existsSync(binary)) return binary;
35
+ } else {
36
+ return candidate;
37
+ }
38
+ }
39
+ if (process.platform === "win32" && fs.existsSync(path.join(dir, "pippit-tool-cli.cmd"))) {
40
+ const binary = path.resolve(dir, "node_modules/@pippit-dev/cli/bin/pippit-tool-cli.exe");
41
+ if (fs.existsSync(binary)) return binary;
42
+ }
43
+ }
44
+ return null;
45
+ }
46
+
47
+ function ensureCLI() {
48
+ if (Number(process.versions.node.split(".")[0]) < 16) {
49
+ throw new Error("需要 Node.js 16+ 和 npm。");
50
+ }
51
+ if (!["darwin", "linux", "win32"].includes(process.platform)
52
+ || !["x64", "arm64"].includes(process.arch)) {
53
+ throw new Error(`不支持的平台:${process.platform}-${process.arch}`);
54
+ }
55
+ const cacheDir = path.join(os.homedir(), ".cache", "pippit-tool-cli", "xyq-skill", `${process.platform}-${process.arch}`);
56
+ const installedDir = path.join(cacheDir, "current");
57
+ const binaryRelative = path.join("node_modules", "@pippit-dev", "cli", "bin", `pippit-tool-cli${process.platform === "win32" ? ".exe" : ""}`);
58
+ const cachedCLI = path.join(installedDir, binaryRelative);
59
+ const env = { ...process.env };
60
+ delete env.XYQ_ACCESS_KEY;
61
+ env.PIPPIT_CLI_DISABLE_UPDATE_CHECK = "1";
62
+ let installDir;
63
+
64
+ function run(command, args, label, quiet = false) {
65
+ try {
66
+ return execFileSync(command, args, {
67
+ cwd: installDir, env, timeout: quiet ? 10000 : 180000,
68
+ stdio: quiet ? ["ignore", "pipe", "pipe"] : ["ignore", 2, 2],
69
+ });
70
+ } catch (err) {
71
+ const failure = new Error(`${label}失败(${err.code || err.status || "unknown"}),请检查安装、命令支持情况和网络。`);
72
+ failure.exitStatus = err.status;
73
+ throw failure;
74
+ }
75
+ }
76
+
77
+ function checkCLI(cliPath, expectedVersion) {
78
+ const version = run(cliPath, ["--version"], "检查 CLI 版本", true).toString().trim();
79
+ if (expectedVersion && version !== expectedVersion) {
80
+ throw new Error(`CLI 版本 ${version} 与 npm 包版本 ${expectedVersion} 不一致。`);
81
+ }
82
+ for (const command of REQUIRED_COMMANDS) {
83
+ try {
84
+ run(cliPath, [command, "--help"], `检查 ${command} 命令`, true);
85
+ } catch (err) {
86
+ if (Number.isInteger(err.exitStatus) && err.exitStatus !== 0) {
87
+ err.missingCommand = command;
88
+ }
89
+ throw err;
90
+ }
91
+ }
92
+ return { cli_path: cliPath, version };
93
+ }
94
+
95
+ const candidates = new Set([findCLIOnPath(), fs.existsSync(cachedCLI) ? cachedCLI : null]);
96
+ for (const candidate of candidates) {
97
+ if (!candidate) continue;
98
+ try {
99
+ return checkCLI(candidate);
100
+ } catch (err) {
101
+ if (!err.missingCommand) throw err;
102
+ console.error(`已有 CLI 的 ${err.missingCommand} 命令检查未通过;继续检查缓存,无可用缓存时自动安装最新版本。`);
103
+ }
104
+ }
105
+
106
+ const [npm, npmArgs] = npmCommand();
107
+ fs.mkdirSync(cacheDir, { recursive: true });
108
+ installDir = fs.mkdtempSync(path.join(cacheDir, "install-"));
109
+ try {
110
+ run(npm, [...npmArgs, "install", "--prefix", installDir,
111
+ "--cache", path.join(installDir, ".npm-cache"), "--global=false", "--no-save",
112
+ "--package-lock=false", "--ignore-scripts", "--prefer-online",
113
+ "--no-audit", "--no-fund", "@pippit-dev/cli@latest"], "下载最新 npm 包");
114
+ const packageDir = path.join(installDir, "node_modules", "@pippit-dev", "cli");
115
+ const installer = path.join(packageDir, "scripts", "install-cli.js");
116
+ if (!fs.existsSync(installer)) {
117
+ throw new Error("npm latest 尚未提供 scripts/install-cli.js,请先发布包含只安装 CLI 入口的版本。");
118
+ }
119
+ run(process.execPath, [installer], "下载并安装最新 CLI 二进制");
120
+ const cliPath = path.join(packageDir, "bin", `pippit-tool-cli${process.platform === "win32" ? ".exe" : ""}`);
121
+ const pkg = JSON.parse(fs.readFileSync(path.join(packageDir, "package.json"), "utf8"));
122
+ const result = checkCLI(cliPath, pkg.version);
123
+ // Preserve the previous installation until the replacement passes all checks.
124
+ fs.rmSync(installedDir, { recursive: true, force: true });
125
+ fs.renameSync(installDir, installedDir);
126
+ return { ...result, cli_path: cachedCLI };
127
+ } catch (err) {
128
+ fs.rmSync(installDir, { recursive: true, force: true });
129
+ throw err;
130
+ }
131
+ }
132
+
133
+ if (require.main === module) {
134
+ if (process.argv.length === 3 && process.argv[2] === "--help") {
135
+ console.log("Usage: node ensure-cli.js\n优先复用 PATH 或缓存中命令齐全的 CLI,不存在或缺少必需命令时安装 npm latest,成功输出 {cli_path, version} JSON。");
136
+ } else if (process.argv.length !== 2) {
137
+ console.error("不支持的参数。用法:node ensure-cli.js");
138
+ process.exitCode = 1;
139
+ } else {
140
+ try {
141
+ console.log(JSON.stringify(ensureCLI()));
142
+ } catch (err) {
143
+ console.error(err.message);
144
+ process.exitCode = 1;
145
+ }
146
+ }
147
+ }
148
+
149
+ module.exports = { ensureCLI };
@@ -17,7 +17,7 @@ def main():
17
17
  epilog="""
18
18
  环境变量:
19
19
  XYQ_ACCESS_KEY 必填,Bearer 鉴权
20
- XYQ_OPENAPI_BASE XYQ_BASE_URL 可选,默认 https://xyq.jianying.com
20
+ API 地址固定为 https://xyq.jianying.com,不支持环境变量覆盖
21
21
 
22
22
  示例:
23
23
  python3 get_thread.py --thread-id abc123 --run-id def456 --after-seq 0