@pippit-dev/cli 0.0.18 → 0.0.20

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 CHANGED
@@ -1,11 +1,195 @@
1
1
  # pippit-tool-cli
2
2
 
3
- Minimal demo CLI for Pippit workflows.
3
+ 面向 Pippit / 小云雀工作流的命令行工具与智能体技能集合。
4
4
 
5
- ## Short Drama Run Demo
5
+ ## 技能列表
6
6
 
7
- Install from npm after the package is published. The installer downloads the
8
- matching prebuilt binary for macOS, Linux, or Windows:
7
+ 本仓库在 `skills/` 目录下包含两个智能体技能:
8
+
9
+ | 技能 | 说明 | 路径 |
10
+ |-------|-------------|------|
11
+ | `pippit-short-drama-skill` | 短剧工作流技能,支持提交创作任务、上传参考文件、查询进度、列出会话文件和下载产物。 | `skills/short-drama/` |
12
+ | `xyq-nest-skill` | 通用 NestAgent 技能,支持图片/视频生成、编辑、文件上传、进度查询和结果下载。 | `skills/xyq-nest-skill/` |
13
+
14
+ ## 通用 NestAgent 技能
15
+
16
+ `xyq-nest-skill` 通过接入小云雀 NestAgent 的综合创作能力,实现 AI 图片/视频生成、编辑、风格转换、文件上传、进度查询和结果下载。
17
+
18
+ ### 功能特性
19
+
20
+ | 功能 | 说明 |
21
+ |------|------|
22
+ | 创建会话 / 发送消息 | 向小云雀发送自然语言指令,生成图片或视频。 |
23
+ | 查询会话进展 | 增量拉取会话消息,轮询创作进度和产物结果。 |
24
+ | 上传文件 | 上传图片/视频到小云雀资产库,获取 `asset_id` 用于编辑和参考。 |
25
+ | 下载结果 | 批量下载生成的图片/视频到本地,支持并行下载。 |
26
+
27
+ 小云雀平台能力覆盖:
28
+
29
+ - 生成:文生图、文生视频、图生视频、视频续写。
30
+ - 编辑:局部修改、元素替换、镜头调整、风格迁移。
31
+ - 复杂创作:一句话生成短剧、复刻视频/TVC/宣传片、音乐 MV 生成、产品展示片制作。
32
+
33
+ ### 配置
34
+
35
+ 所有 `xyq-nest-skill` 脚本都使用 Bearer 令牌鉴权:
36
+
37
+ ```bash
38
+ export XYQ_ACCESS_KEY="<access-key>"
39
+ ```
40
+
41
+ 可选 API 地址:
42
+
43
+ ```bash
44
+ export XYQ_OPENAPI_BASE="https://xyq.jianying.com"
45
+ # 或
46
+ export XYQ_BASE_URL="https://xyq.jianying.com"
47
+ ```
48
+
49
+ ### 创建会话 / 发送消息
50
+
51
+ ```bash
52
+ # 创建新会话
53
+ python3 skills/xyq-nest-skill/scripts/submit_run.py --message "生一个动漫视频"
54
+
55
+ # 向已有会话发送消息
56
+ python3 skills/xyq-nest-skill/scripts/submit_run.py \
57
+ --message "再生成一个故事视频" \
58
+ --thread-id THREAD_ID
59
+
60
+ # 携带参考文件发送
61
+ python3 skills/xyq-nest-skill/scripts/submit_run.py \
62
+ --message "参考这个视频做修改" \
63
+ --asset-ids asset_id1 asset_id2
64
+ ```
65
+
66
+ | 参数 | 必填 | 说明 |
67
+ |------|------|------|
68
+ | `--message` | 是 | 创作指令内容。 |
69
+ | `--thread-id` | 否 | 已有会话 ID,不传则创建新会话。 |
70
+ | `--asset-ids` | 否 | 资产 ID 列表,支持多个。 |
71
+
72
+ 返回示例:
73
+
74
+ ```json
75
+ {
76
+ "thread_id": "90f05e0c-...",
77
+ "run_id": "abc123-..."
78
+ }
79
+ ```
80
+
81
+ ### 查询会话进展
82
+
83
+ ```bash
84
+ python3 skills/xyq-nest-skill/scripts/get_thread.py \
85
+ --thread-id THREAD_ID \
86
+ --run-id RUN_ID \
87
+ --after-seq 0
88
+ ```
89
+
90
+ | 参数 | 必填 | 说明 |
91
+ |------|------|------|
92
+ | `--thread-id` | 是 | 会话 ID。 |
93
+ | `--run-id` | 否 | 运行 ID。 |
94
+ | `--after-seq` | 否 | 增量拉取起始序号,默认 `0`。 |
95
+
96
+ 脚本会返回会话消息和产物条目。后续轮询时,根据已获取消息更新 `after_seq`。
97
+
98
+ ### 上传文件
99
+
100
+ ```bash
101
+ # 上传图片
102
+ python3 skills/xyq-nest-skill/scripts/upload_file.py /path/to/image.png
103
+
104
+ # 上传视频
105
+ python3 skills/xyq-nest-skill/scripts/upload_file.py /path/to/video.mp4
106
+ ```
107
+
108
+ 仅支持 `image/*` 和 `video/*` 类型,单文件大小限制 200 MB。
109
+
110
+ 返回示例:
111
+
112
+ ```json
113
+ {
114
+ "asset_id": "asset_xxx"
115
+ }
116
+ ```
117
+
118
+ ### 下载结果
119
+
120
+ ```bash
121
+ python3 skills/xyq-nest-skill/scripts/download_results.py \
122
+ --urls URL1 URL2 URL3 \
123
+ --output-dir ./xyq_output \
124
+ --prefix "storyboard" \
125
+ --workers 5
126
+ ```
127
+
128
+ | 参数 | 必填 | 说明 |
129
+ |------|------|------|
130
+ | `--urls` | 是 | 要下载的 URL 列表。 |
131
+ | `--output-dir` | 否 | 输出目录,默认 `./xyq_output`。 |
132
+ | `--prefix` | 否 | 文件名前缀,例如 `storyboard_01.png`。 |
133
+ | `--workers` | 否 | 并行下载线程数,默认 `5`。 |
134
+
135
+ 返回示例:
136
+
137
+ ```json
138
+ {
139
+ "output_dir": "./xyq_output",
140
+ "downloaded": ["./xyq_output/storyboard_01.png"],
141
+ "total": 1
142
+ }
143
+ ```
144
+
145
+ ### 典型示例
146
+
147
+ 文生视频:
148
+
149
+ ```text
150
+ 1. submit_run.py --message "生成一个赛博朋克风格的城市夜景视频"
151
+ 2. 每 10 秒轮询:
152
+ get_thread.py --thread-id THREAD_ID --run-id RUN_ID --after-seq SEQUENCE
153
+ 3. 拿到产物 URL 后下载:
154
+ download_results.py --urls URL1 URL2 --output-dir ./output --prefix "cyberpunk"
155
+ ```
156
+
157
+ 编辑已有视频:
158
+
159
+ ```text
160
+ 1. upload_file.py /path/to/video.mp4
161
+ 2. submit_run.py --message "把背景换成星空" --asset-ids asset_id
162
+ 3. 按文生视频流程轮询和下载。
163
+ ```
164
+
165
+ 多参考图/视频生成:
166
+
167
+ ```text
168
+ 1. upload_file.py /path/to/ref1.png
169
+ 2. upload_file.py /path/to/ref2.png
170
+ 3. upload_file.py /path/to/ref3.mp4
171
+ 4. submit_run.py --message "根据参考图和视频生成科普故事视频" --asset-ids asset_id1 asset_id2 asset_id3
172
+ 5. 按文生视频流程轮询和下载。
173
+ ```
174
+
175
+ 在已有会话中追加需求:
176
+
177
+ ```text
178
+ 1. submit_run.py --message "把刚才的视频加个片头" --thread-id EXISTING_THREAD_ID
179
+ 2. 使用新的 run_id 轮询和下载。
180
+ ```
181
+
182
+ 轮询策略:
183
+
184
+ - 间隔:每 10 秒查询一次。
185
+ - 增量拉取:首次 `--after-seq 0`,后续根据已获取消息数更新 seq。
186
+ - 意图确认:如果智能体追问用户,先展示问题,再用同一个 `thread_id` 提交用户回复。
187
+ - 超时:连续轮询 48 小时无结果则停止。
188
+ - 错误重试:单次失败可重试 1 次,连续 3 次失败则停止。
189
+
190
+ ## 短剧工作流技能
191
+
192
+ 包发布后可以通过 npm 安装。安装器会按当前系统下载匹配的预构建二进制文件,支持 macOS、Linux 和 Windows:
9
193
 
10
194
  ```bash
11
195
  npx @pippit-dev/cli@latest install
@@ -17,43 +201,17 @@ pippit-tool-cli short-drama +get-thread --thread-id thread_123 --run-id run_456
17
201
  pippit-tool-cli short-drama +download-result --output-path ./thread_123/results/result.mp4 --url URL
18
202
  ```
19
203
 
20
- NPM package names must be lowercase, so the publishable package name is
21
- `@pippit-dev/cli` rather than `@Pippit-dev/cli`.
204
+ `+submit-run`: 输出 `thread_id`、`run_id` `web_thread_link`;其中 `--message` 为必填参数。
205
+ `+get-thread`: 请求中带 `version=v2`,并输出 `readable_text`。
206
+ `+upload-file`: 输出返回的 `asset_id`。 当前仅支持 `.doc`、`.docx` 和 `.txt` 文件。
207
+ `+download-result`: 会把结果 URL 下载到 `--output-path` 指定的文件路径。
22
208
 
23
- Submit a Run task for the short drama scene:
209
+ 短剧命令的错误日志会追加写入本地每日日志文件:`~/.pippit_tool_cli/logs/yyyy-mm-dd.log`。日志路径会基于当前用户主目录和系统路径分隔符生成,因此可在 macOS、Linux Windows 上使用。
24
210
 
25
- ```bash
26
- export XYQ_ACCESS_KEY="<access-key>"
27
- go run . --version
28
- go run . short-drama +submit-run --message "写一个赛博朋克短剧开头"
29
- go run . short-drama +upload-file --path ./reference.doc
30
- go run . short-drama +get-thread --thread-id thread_123 --run-id run_456
31
- go run . short-drama +download-result --output-path ./thread_123/results/result.mp4 --url URL
32
- ```
33
-
34
- `+submit-run` calls `/api/biz/v1/skill/submit_run` and prints `thread_id`,
35
- `run_id`, and `web_thread_link`; `--message` is required.
36
- `+get-thread` calls
37
- `/api/biz/v1/skill/get_thread` with `version=v2` and prints `readable_text`.
38
- `+upload-file` calls `/api/biz/v1/skill/upload_file` with
39
- `multipart/form-data` and prints the returned `asset_id`.
40
- Only `.doc`, `.docx`, and `.txt` file extensions are supported.
41
- `+download-result` downloads the result URL to
42
- the `--output-path` file path.
43
-
44
- Short drama command errors are appended to a daily local log file under
45
- `~/.pippit_tool_cli/logs/yyyy-mm-dd.log`. The path is built with the current
46
- user home directory and the platform path separator, so it works on macOS,
47
- Linux, and Windows.
48
-
49
- ## HTTP Client
50
-
51
- Command modules should receive `common.Runner` for service calls. Runtime
52
- settings such as base URL, HTTP timeout, and API paths are loaded by
53
- `internal/config` and paired with `common.Client` in the runner.
54
-
55
- ## Auth
56
-
57
- `short-drama +submit-run`, `short-drama +get-thread`, and `short-drama +upload-file` authenticate with
58
- `Authorization: Bearer <XYQ_ACCESS_KEY>`. OAuth command code remains available,
59
- but runtime short drama requests do not use it.
211
+ ## HTTP 客户端
212
+
213
+ 命令模块通过 `common.Runner` 发起服务调用。运行时配置,例如基础地址、HTTP 超时时间和接口路径,由 `internal/config` 加载,并在运行器中与 `common.Client` 组合使用。
214
+
215
+ ## 鉴权
216
+
217
+ `short-drama +submit-run`、`short-drama +get-thread`、`short-drama +upload-file` 以及 `xyq-nest-skill` Python 脚本都使用 `Authorization: Bearer <XYQ_ACCESS_KEY>` 鉴权。OAuth 命令代码仍保留在仓库中,但短剧运行时请求不使用 OAuth。
package/checksums.txt CHANGED
@@ -1,6 +1,6 @@
1
- 2cd3bb5e214d85004196806643c3b7d0a8ceb141ef3df58d1bbfc7aa9642f6ab pippit-tool-cli-0.0.18-darwin-amd64.tar.gz
2
- 8fb57bef9d7e81b8bcc3421f2179879b656134c23387104e01a1090022f6e7c9 pippit-tool-cli-0.0.18-darwin-arm64.tar.gz
3
- bdde278cbee36f2b90602b1b58f7be05790a186b3adbc90b8dd8fd970b48b20c pippit-tool-cli-0.0.18-linux-amd64.tar.gz
4
- 878c6b0cd41a2cafdd62f99be98968eee05f0d16de0c6b173f84bb0328c1bef6 pippit-tool-cli-0.0.18-linux-arm64.tar.gz
5
- e4423154ba1a52625f255e11060459844a4c5bcf3aab21af682f3e1adcce82cd pippit-tool-cli-0.0.18-windows-amd64.zip
6
- cca47eab19bdc7200aabcea2d59a535c63267fe293f993b1013aa049a4be16b4 pippit-tool-cli-0.0.18-windows-arm64.zip
1
+ 200e40ca3a4ef54a76860bcbe807233fc548d5686eea8b4bb5069f4d393e6fd2 pippit-tool-cli-0.0.20-darwin-amd64.tar.gz
2
+ 2371e7b72c39b8b590604afe1abdba34e583b69ad787a78913b306de0145401b pippit-tool-cli-0.0.20-darwin-arm64.tar.gz
3
+ 1df765184d0f7061c15e2829c7792f7982eeb711da534f3883488efc34b02b5d pippit-tool-cli-0.0.20-linux-amd64.tar.gz
4
+ d5e0d1fcd774cc7d05ab3c749fff79e7f3507e8f92c0c5abaed4ef884ff46f20 pippit-tool-cli-0.0.20-linux-arm64.tar.gz
5
+ 0ccfa68f9fd289b680756ba664a82d68af6636c1337368946c53fc58d74f1f2a pippit-tool-cli-0.0.20-windows-amd64.zip
6
+ 2fac1102a7e0fbd75fae736636a3c2e9a2559b7a1a297530f18fa66adc5e11c7 pippit-tool-cli-0.0.20-windows-arm64.zip
@@ -74,7 +74,7 @@ func installSkills(root string, stderr io.Writer) error {
74
74
  } else if !info.IsDir() {
75
75
  return fmt.Errorf("%s is not a directory", filepath.Join(root, "skills"))
76
76
  }
77
- return runInherit(stderr, "npx", "-y", "skills", "add", root, "-g", "-y")
77
+ return runInherit(stderr, "npx", "-y", "skills", "add", root, "-g", "-y", "--skill", "*")
78
78
  }
79
79
 
80
80
  func runInherit(stderr io.Writer, name string, args ...string) error {
@@ -0,0 +1,47 @@
1
+ package updatecmd
2
+
3
+ import (
4
+ "bytes"
5
+ "os"
6
+ "path/filepath"
7
+ "runtime"
8
+ "strings"
9
+ "testing"
10
+ )
11
+
12
+ func TestInstallSkillsInstallsAllBundledSkills(t *testing.T) {
13
+ if runtime.GOOS == "windows" {
14
+ t.Skip("test uses a POSIX shell script to stub npx")
15
+ }
16
+
17
+ root := t.TempDir()
18
+ if err := os.Mkdir(filepath.Join(root, "skills"), 0o755); err != nil {
19
+ t.Fatal(err)
20
+ }
21
+
22
+ binDir := t.TempDir()
23
+ capturePath := filepath.Join(t.TempDir(), "args.txt")
24
+ npxPath := filepath.Join(binDir, "npx")
25
+ script := "#!/bin/sh\nfor arg in \"$@\"; do printf '%s\\n' \"$arg\"; done > \"$CAPTURE_ARGS\"\n"
26
+ if err := os.WriteFile(npxPath, []byte(script), 0o755); err != nil {
27
+ t.Fatal(err)
28
+ }
29
+
30
+ t.Setenv("PATH", binDir+string(os.PathListSeparator)+os.Getenv("PATH"))
31
+ t.Setenv("CAPTURE_ARGS", capturePath)
32
+
33
+ var stderr bytes.Buffer
34
+ if err := installSkills(root, &stderr); err != nil {
35
+ t.Fatalf("installSkills() error = %v, stderr = %s", err, stderr.String())
36
+ }
37
+
38
+ gotBytes, err := os.ReadFile(capturePath)
39
+ if err != nil {
40
+ t.Fatal(err)
41
+ }
42
+ got := strings.Split(strings.TrimSpace(string(gotBytes)), "\n")
43
+ want := []string{"-y", "skills", "add", root, "-g", "-y", "--skill", "*"}
44
+ if strings.Join(got, "\n") != strings.Join(want, "\n") {
45
+ t.Fatalf("npx args mismatch\ngot: %#v\nwant: %#v", got, want)
46
+ }
47
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pippit-dev/cli",
3
- "version": "0.0.18",
3
+ "version": "0.0.20",
4
4
  "description": "Pippit CLI",
5
5
  "bin": {
6
6
  "pippit-tool-cli": "scripts/run.js"
package/scripts/skills.js CHANGED
@@ -10,7 +10,7 @@ function installSkillsFromRoot(root, opts = {}) {
10
10
  if (!fs.existsSync(skillsDir)) {
11
11
  throw new Error(`skills directory not found: ${skillsDir}`);
12
12
  }
13
- run("npx", ["-y", "skills", "add", source, "-g", "-y"], {
13
+ run("npx", ["-y", "skills", "add", source, "-g", "-y", "--skill", "*"], {
14
14
  timeout: opts.timeout || 120000,
15
15
  });
16
16
  }
@@ -31,6 +31,34 @@ metadata:
31
31
 
32
32
  重要资产包括但不限于:剧本设计、场景设计、场景图、人物角色设计、人物图、分集草稿、故事板、最终视频产物。只要 `+list-thread-file` 返回了这些资产的 `download_url`,就要及时调用下载工具落盘,不要只展示文件元信息。
33
33
 
34
+ ## 短剧主流程顺序
35
+
36
+ 短剧创作按以下主流程推进。用户侧 Agent 在展示后端 Agent 的表单、问卷、选项或确认问题时,必须先参考这个顺序判断当前阶段和合理下一步。
37
+
38
+ 1. 剧本上传 / AI 剧本生成 / AI 剧本编辑
39
+ 2. 剧本合并与完整剧本确认
40
+ 3. 剧本分析
41
+ 4. 短剧风格推荐确认
42
+ 5. 剧本标准化(可选)
43
+ 6. 场景分析
44
+ 7. 所有必要场景图生成
45
+ 8. 角色分析
46
+ 9. 所有必要角色图生成
47
+ 10. 分镜设计
48
+ 11. 分镜视频生成
49
+ 12. 完整视频合成
50
+
51
+ ## 表单与问卷选项处理原则
52
+
53
+ 后端 Agent 通过 `readable_text` 发出表单、问卷、选项、按钮或询问用户时,用户侧 Agent 不要机械原样转述所有选项。先结合短剧主流程顺序清洗选项,再把合理、必要、当前可执行的流程项呈现给用户。
54
+
55
+ - 保留当前阶段的确认项,以及不会跳过必要阶段的下一步流程项。
56
+ - 剔除跳过必要阶段的选项。例如未完成“剧本合并与完整剧本确认”前,不应让用户直接进入“剧本分析”;未完成“所有必要场景图生成”前,不应让用户直接进入“角色分析”。
57
+ - 剔除倒退到无关阶段的选项。只有用户明确要求返工、修改或重新生成时,才展示回退选项。
58
+ - `剧本标准化` 是可选阶段,只能出现在“短剧风格推荐确认”之后、“场景分析”之前。不要把它包装成任意阶段都可以跳过或补做的通用选项。
59
+ - 不替用户决定创意内容,例如风格、剧情方向、角色设定、镜头方案。只能清洗流程选项,不能代替用户选择创作偏好。
60
+ - 如果服务端问题混入跨度过大的多个流程选项,重新组织成当前阶段可回答的问题,并说明已按主流程剔除不合理或跳跃选项。
61
+
34
62
  ## 前置要求
35
63
 
36
64
  需要已安装 `pippit-tool-cli`:
@@ -167,7 +195,7 @@ pippit-tool-cli short-drama +download-result --url DOWNLOAD_URL --output-path FI
167
195
  - **重要资产识别**:每轮都检查 `+list-thread-file` 返回的文件。剧本设计、场景设计、场景图、人物角色设计、人物图、分集草稿、故事板、最终视频产物都是重要资产。
168
196
  - **文件下载**:解析 `+list-thread-file` 的结果后,对带 `download_url` 的重要资产立即调用 `+download-result` 下载资源;不要在 `list-thread-file` 阶段检查文件是否已存在,存在性检查由下载工具内部处理。
169
197
  - **下载完成标准**:不要把文件元信息展示当成下载完成;必须拿到本地 `file_path`,或明确记录该文件在重试后仍下载失败。
170
- - **用户确认**:如果消息中出现需要用户确认、补充设定或回答问题的内容,先展示给用户,等待用户回复。
198
+ - **用户确认**:如果消息中出现需要用户确认、补充设定或回答问题的内容,先判断是否包含表单、问卷、选项或按钮;包含时按“短剧主流程顺序”和“表单与问卷选项处理原则”清洗选项,再展示给用户并等待回复。
171
199
  - **超时**:如果长时间无结果,告知用户任务仍在生成中,可稍后通过 `web_thread_link` 查看。
172
200
  - **错误处理**:`+get-thread`、`+list-thread-file` 或 `+download-result` 任一调用失败时,记录失败原因和参数,在后续轮询中主动重试;重试期间继续处理其他成功返回的消息和文件。连续多轮失败后再向用户说明仍未完成的查询或下载项。
173
201
 
@@ -178,7 +206,8 @@ pippit-tool-cli short-drama +download-result --url DOWNLOAD_URL --output-path FI
178
206
  1. 已处理 `+get-thread` 返回的最新 `readable_text`、用户确认问题和最终消息。
179
207
  2. 已用 `--page-size 200` 调用 `+list-thread-file` 获取会话文件列表;如果本轮 `total` 达到 200,已在后续轮询中递增 `page-num` 查询新一页。
180
208
  3. 对所有带 `download_url` 的重要资产,已调用 `+download-result` 下载到本地 `file_path`。
181
- 4. 对查询失败或下载失败的资产,已在后续轮询中主动重试,并在最终回复中列出仍失败的文件或命令。
209
+ 4. 已按短剧主流程顺序检查服务端表单、问卷和选项,没有把跳过必要阶段的选项直接呈现给用户;如果跳过 `剧本标准化`,已明确这是可选阶段。
210
+ 5. 对查询失败或下载失败的资产,已在后续轮询中主动重试,并在最终回复中列出仍失败的文件或命令。
182
211
 
183
212
  ## 输出格式
184
213
 
@@ -271,7 +300,7 @@ Thread: thread_...
271
300
 
272
301
  - 任务提交后:立即展示 `web_thread_link`。
273
302
  - 任务进行中:展示后端 Agent 返回的过程消息。
274
- - 需要用户补充信息时:原样展示后端 Agent 的问题,等待用户回复。
303
+ - 需要用户补充信息时:如果是普通问题,展示后端 Agent 的问题并等待用户回复;如果包含表单、问卷、选项或按钮,先按短剧主流程清洗不合理或跳跃的流程选项,再展示给用户。
275
304
  - 任务完成后:展示短剧内容、分集草稿、设定说明或其他结果信息,同时检查是否有未下载的重要资产。
276
305
  - 获取会话文件后:展示或记录文件元信息,不把它当成已下载结果。
277
306
  - 文件资源下载后:展示已落盘的本地文件路径;已存在而跳过下载的文件也要标明。
@@ -285,7 +314,7 @@ Thread: thread_...
285
314
 
286
315
  1. **上传**:如果用户给了本地 `.doc` / `.docx` / `.txt` 参考文件,先调用 `+upload-file`。
287
316
  2. **提交任务**:首次创作时把用户原始短剧需求和唯一剧本 `asset_id` 通过 `+submit-run --asset-ids` 发给后端;同一 `thread_id` 后续续写或修改不再追加新的剧本文件。
288
- 3. **传话、取文件、下载资源**:根据 `+get-thread` 返回的 `readable_text` 展示进展、问题和结果;根据 `+list-thread-file` 获取文件列表;再根据 `download_url` 调用 `+download-result` 把缺失资源下载到用户本地。
317
+ 3. **传话、取文件、下载资源**:根据 `+get-thread` 返回的 `readable_text` 展示进展、问题和结果;遇到表单、问卷、选项或按钮时,只做流程合理性清洗,不替用户决定创作内容;根据 `+list-thread-file` 获取文件列表;再根据 `download_url` 调用 `+download-result` 把缺失资源下载到用户本地。
289
318
 
290
319
  **不要做的事:**
291
320
 
@@ -0,0 +1,255 @@
1
+ ---
2
+ name: xyq-nest-skill
3
+ description: 通过小云雀的 AI 能力进行综合创作,支持生成和编辑图片/视频。覆盖场景包括:生成(文生图、文生视频、图生视频、做动画、画一个xxx、来段xxx)、编辑修改(把xxx换成yyy、去掉xxx、加上xxx、改成xxx、调整xxx、局部修改、改镜头)、风格转换(风格迁移、转绘、换风格)、视频续写延长、复刻视频/TVC/宣传片、短剧/短漫剧生成、音乐MV生成、产品广告/展示片制作、分镜/故事板设计、教育视频/短视频制作。当用户提到小云雀、xyq、上传参考图/视频、查看生成进度时也应触发。关键判断:只要用户的请求涉及 AI 视频的创作、生成、编辑、修改,无论措辞如何(如"画只猫"、"做个海报"、"做个视频"、"这个视频帮我改一下"、"帮我复刻这段视频"、"用这首歌做个MV"、"一句话生成短剧"),都必须触发此技能。
4
+ user-invocable: true
5
+ metadata:
6
+ {
7
+ "openclaw":
8
+ {
9
+ "emoji": "💬",
10
+ "requires":
11
+ {
12
+ "bins": ["python3"],
13
+ "env": ["XYQ_ACCESS_KEY"]
14
+ },
15
+ "primaryEnv": "XYQ_ACCESS_KEY"
16
+ }
17
+ }
18
+ ---
19
+
20
+ # 小云雀会话(生视频)
21
+
22
+ 通过 小云雀的API 创建会话、发送消息(生图、生视频、编辑视频等)、上传图片/视频文件,并查询会话消息进展。
23
+
24
+ 小云雀是一个 AI 综合创作平台,同时为人类创作者和 Agent 设计。Agent 通过 Skill 入口理解任务、调用模型并自动编排工作流。
25
+
26
+ **平台核心能力:**
27
+ - **生成**:文生图、文生视频、图生视频、视频续写
28
+ - **编辑**:局部修改、元素替换、镜头调整、风格迁移
29
+ - **复杂创作**:一句话生成完整短剧(剧本→分镜→成片)、复刻已有视频风格做 TVC/宣传片、用音乐生成 MV、产品展示片制作
30
+
31
+ 用户的所有创作和编辑需求都通过发送自然语言消息来完成,Agent 会自主编排工作流。复杂任务(短剧、MV)耗时较长,需耐心轮询。
32
+
33
+ ## 功能
34
+
35
+ 1. **创建会话 / 发消息** - 创建新会话或向已有会话发送一条消息(如「创作一个视频」)
36
+ 2. **查询会话进展** - 根据 `thread_id` 、 `run_id`、`after_seq` 增量拉取该会话的消息列表,用于轮询创作过程的消息和最终产物结果
37
+ 3. **上传文件** - 支持上传`单张图片`或`单个视频文件`到小云雀资产库,得到文件对应的 `asset_id`(编辑已有视频/图片时需要先上传)
38
+ 4. **下载结果** - 将会话中生成的图片/视频批量下载到本地,支持指定输出目录和文件名前缀。
39
+
40
+
41
+ ## 前置要求
42
+
43
+ ```bash
44
+ export XYQ_ACCESS_KEY="your-access-key"
45
+ ```
46
+
47
+ 可选:`XYQ_OPENAPI_BASE` 或 `XYQ_BASE_URL`,默认 `https://xyq.jianying.com`。
48
+
49
+ 无需安装额外依赖,仅使用 Python 标准库。
50
+
51
+ ## 使用方法
52
+
53
+ ### 1. 创建会话 / 发送消息
54
+
55
+ ```bash
56
+ # 创建新会话并发送「生一个动漫视频」
57
+ python3 {baseDir}/scripts/submit_run.py --message "生一个动漫视频"
58
+
59
+ # 向已有会话发送消息
60
+ python3 {baseDir}/scripts/submit_run.py --message "再生成一个故事视频" --thread-id THREAD_ID
61
+ ```
62
+
63
+ ### 2. 查询会话进展
64
+
65
+ ```bash
66
+ # 查询会话消息列表
67
+ python3 {baseDir}/scripts/get_thread.py --thread-id THREAD_ID --run-id RUN_ID --after-seq SEQUENCE
68
+ ```
69
+
70
+ > `run_id` 由 `submit_run` 返回,用于指定查询某次具体运行的结果。
71
+
72
+ ### 3. 上传文件
73
+
74
+ - 当用户提供了参考的文件地址时,先进行文件上传,仅支持图片、视频。
75
+ - 单次指令执行仅支持单个文件,多个文件可并行调用,单个文件大小必须在200MB以下。
76
+
77
+ ```bash
78
+ # 上传图片
79
+ python3 {baseDir}/scripts/upload_file.py /path/to/image.png
80
+
81
+ # 上传视频
82
+ python3 {baseDir}/scripts/upload_file.py /path/to/video.mp4
83
+ ```
84
+
85
+ ### 4. 下载结果
86
+
87
+ 任务完成后,可以将会话中的所有产物批量下载到本地。
88
+
89
+ ```bash
90
+ # 指定 URL 列表,指定输出目录,指定文件名前缀(如 artifact_01.png, artifact_02.png ...)进行下载
91
+ python3 {baseDir}/scripts/download_results.py --urls URL1 URL2 URL3 --output-dir ./xyq_output --prefix "artifact"
92
+ ```
93
+
94
+ ## 典型工作流
95
+
96
+ 理解这些工作流,才能正确组合上面的脚本完成用户需求。
97
+
98
+ ### 场景 1:用户要求生成图片或视频(最常见)
99
+
100
+ ```
101
+ 1. submit_run.py --message "用户的描述" → 拿到 thread_id、run_id 和 web_thread_link
102
+ 2. **立即**将 `web_thread_link` 展示给用户(如"任务已提交,可在此查看:{web_thread_link}")
103
+ 3. 每隔 `10` 秒钟调用 get_thread.py --thread-id THREAD_ID --run-id RUN_ID --after-seq SEQUENCE 进行轮询
104
+ 4. 检查 messages:
105
+ - 当任务还在创作中:
106
+ - 将过程创作信息展示给用户,继续轮询
107
+ - 当任务完成(run 结束):
108
+ - 如果涉及意图确认/流程中断(如"请回答以下问题"):
109
+ → 向用户展示问题,等待用户回复
110
+ → 使用 `thread_id` 重新提交任务(保持同一会话,产生新的 run_id)
111
+ → 回到步骤 2 继续轮询(可能多轮,直到不再意图确认)
112
+ - 如果 content 中包含产物 URL:
113
+ → 信息展示 → 下载产物 → 结果展示
114
+ 5. 自动下载:download_results.py --urls URL1 URL2 URL3 --output-dir 输出目录 --prefix 有意义的前缀
115
+ 6. 向用户展示:过程中的创作信息,以及下载后的本地文件列表
116
+ ```
117
+
118
+ ### 场景 2:用户提供图片/视频要求编辑修改(如"参考这个视频做一个新的")
119
+
120
+ ```
121
+ 1. upload_file.py /path/to/video.mp4 → 拿到 asset_id
122
+ 2. submit_run.py --message "参考这个视频做一个新的" --asset-ids asset_id → 拿到 thread_id、run_id、web_thread_link
123
+ 3. 后续同场景 1 的步骤 2-6
124
+ ```
125
+
126
+ 用户给了文件路径 + 编辑指令 = 先上传文件,再把编辑指令和 所有asset_id 一起发送。
127
+
128
+ ### 场景 3:用户提供参考图/视频要求生成新内容
129
+
130
+ ```
131
+ 1. upload_file.py /path/to/ref1.png → 拿到 asset_id1
132
+ 2. upload_file.py /path/to/ref2.mp4 → 拿到 asset_id2
133
+ 3. 直到所有文件上传完成,拿到所有 asset_id
134
+ 4. submit_run.py --message "根据参考图、视频生成xxx" --asset-ids asset_id1 asset_id2, ... → 拿到 thread_id、run_id、web_thread_link
135
+ 5. 后续同场景 1 的步骤 2-6
136
+ ```
137
+
138
+ ### 场景 4:在已有会话中追加新需求
139
+
140
+ ```
141
+ 1. submit_run.py --message "新的描述" --thread-id THREAD_ID → 拿到 thread_id、run_id、web_thread_link
142
+ 2. 后续同场景 1 的步骤 2-6
143
+ ```
144
+
145
+ ### 轮询策略
146
+
147
+ - **间隔**:每 10 秒查询一次
148
+ - **增量拉取**:首次用 --after-seq 0,后续根据messages消息列表长度,计算新的 seq 值
149
+ - **完成判断**:当创作任务完成且messages的content中包含产物结果 URL(图片/视频地址)
150
+ - **超时**:连续轮询 `48 小时`仍无结果,告知用户"生成时间较长,可稍后查看",不再继续轮询
151
+ - **错误重试**:单次查询失败可重试 1 次,连续 3 次失败则停止并告知用户
152
+
153
+ ## 输出格式
154
+
155
+ **submit_run** 返回:
156
+ ```json
157
+ {
158
+ "thread_id": "90f05e0c-...",
159
+ "run_id": "abc123-...",
160
+ "web_thread_link": "https://xyq.jianying.com/..."
161
+ }
162
+ ```
163
+
164
+ **get_thread** 返回:
165
+ ```json
166
+ {
167
+ "messages": [
168
+ {"id": "1", "role": "user", "content": "生一个动漫视频"},
169
+ {"id": "2", "role": "assistant", "content": [
170
+ {
171
+ "type": "{type}",
172
+ "subtype": "{sub_type}",
173
+ "data": {...}
174
+ }
175
+ ]},
176
+ {"id": "3", "role": "assistant", "content": [
177
+ {
178
+ "type": "{type}",
179
+ "subtype": "{sub_type}",
180
+ "data": {..., "url": "{url}"....}
181
+ }
182
+ ]}
183
+ ]
184
+ }
185
+ ```
186
+
187
+ **upload_file** 返回:
188
+ ```json
189
+ {
190
+ "asset_id": "{asset_id}"
191
+ }
192
+ ```
193
+
194
+ **download_results** 返回:
195
+ ```json
196
+ {
197
+ "output_dir": "./xyq_output",
198
+ "downloaded": ["./xyq_output/01.png", "..."],
199
+ "total": 10
200
+ }
201
+ ```
202
+
203
+ ## 向用户展示内容
204
+
205
+ - 任务提交后:立即将 `web_thread_link` 展示给用户,方便用户直接打开浏览器查看任务页面
206
+ - 任务在创作中:
207
+ - 展示过程中的创作信息等,继续轮询
208
+ - 任务完成(run 结束):
209
+ - 若涉及意图确认/流程中断(如"请回答以下问题")→ 展示问题 → 等待用户回复 → 使用同一 `thread_id` 重新提交任务 → 继续轮询(可能多轮)
210
+ - 若 content 中包含产物 URL:
211
+ - 结果地址:来自 `get_thread` 返回的 `messages` 中,任务创作完成会包含产物 URL,将产物链接、下载的本地文件等信息告知用户。
212
+
213
+ ## 核心原则:用户侧不做创作,只做传话
214
+
215
+ 你(用户侧 Agent)的职责是**搬运工**,不是创作者。后端有专门的 Agent 负责理解需求、拆解分镜、编排工作流、选模型、写 prompt。你要做的只有三件事:
216
+
217
+ 1. **上传**:如果用户给了本地文件 → `upload_file.py` 拿到 asset_id
218
+ 2. **提交任务**:把用户的原始描述 + asset_id 原封不动发给 `submit_run.py`
219
+ 3. **传话**:根据 `get_thread.py` 返回的消息列表,展示过程中的意图询问、创作信息等
220
+ 4. **取件**:`get_thread.py` 轮询结果 → 检查结果 → 下载产物 → 结果展示给用户
221
+
222
+ **绝对不要做的事:**
223
+ - 不要替用户扩写、润色、翻译 prompt(用户说"帮我推演分镜",就直接传"帮我推演分镜",不要自己先写个分镜表再逐条发)
224
+ - 不要自行编排镜头描述、剧情推演、风格分析
225
+ - 不要在消息中添加自己编的 prompt(如"超写实风格,电影级光影,8K分辨率"之类的描述词)
226
+
227
+ 后端 Agent 对模型能力、参数配置、prompt 工程远比用户侧更专业。用户侧越俎代庖只会降低生成质量,换个弱模型更是灾难。
228
+
229
+ **正确示例:**
230
+ ```
231
+ 用户说:「根据多张参考图,做个科普故事视频」
232
+ 用户给了参考图:/path/to/ref1.png, /path/to/ref2.png, /path/to/ref3.png
233
+
234
+ → upload_file.py /path/to/ref1.png → 拿到 asset_id1
235
+ → upload_file.py /path/to/ref2.png → 拿到 asset_id2
236
+ → upload_file.py /path/to/ref3.png → 拿到 asset_id3
237
+ → submit_run.py --message "根据参考图、视频生成xxx" --asset-ids asset_id1 asset_id2, asset_id3 → 拿到 web_thread_link,立即展示给用户
238
+ → 轮询 ─┬─ 意图确认 → 用户确认 → 使用 thread_id 重新提交 → 继续轮询
239
+ └─ 无意图确认 → 信息展示 → 下载产物 → 结果展示
240
+ ```
241
+
242
+ **错误示例:**
243
+ ```
244
+ ❌ 用户侧自己先写了个九宫格分镜表(对峙、交锋、危机...)
245
+ ❌ 然后把自己编的描述发给后端
246
+ ❌ 或者拆成9次 submit_run 分别发送
247
+ ```
248
+
249
+ ## 注意事项
250
+
251
+ - 鉴权方式为请求头 `Authorization: Bearer <XYQ_ACCESS_KEY>`
252
+ - 创建会话时 `message` 是用户的指令要求,不能为空
253
+ - 查询会话时可用 --after-seq 做增量拉取,便于轮询新消息(含 assistant 回复与生图/生视频结果)
254
+ - 上传文件仅支持图片(image/*)和视频(video/*)类型,其他类型会被拒绝,文件大小须在 200MB 以下
255
+ - 生成过程中将过程中的创作信息展示给用户;任务完成后给出**产物结果(图片/视频)URL链接**和下载的**本地文件列表**。
@@ -0,0 +1,158 @@
1
+ """小云雀 agent-im OpenAPI 公共模块:创建会话、查询会话(鉴权为 Authorization: Bearer <access_key>)"""
2
+
3
+ import json
4
+ import os
5
+ import sys
6
+ import urllib.request
7
+ import urllib.error
8
+
9
+ XYQ_BASE = os.environ.get("XYQ_OPENAPI_BASE", os.environ.get("XYQ_BASE_URL", "https://xyq.jianying.com"))
10
+ ACCESS_KEY = os.environ.get("XYQ_ACCESS_KEY", "")
11
+
12
+ # API 路径常量
13
+ SUBMIT_RUN_PATH = "/api/biz/v1/skill/submit_run"
14
+ GET_THREAD_PATH = "/api/biz/v1/skill/get_thread"
15
+ UPLOAD_FILE_PATH = "/api/biz/v1/skill/upload_file"
16
+
17
+ if not ACCESS_KEY:
18
+ print("错误:请设置 XYQ_ACCESS_KEY 环境变量", file=sys.stderr)
19
+ sys.exit(1)
20
+
21
+
22
+ def _headers():
23
+ return {
24
+ "Authorization": f"Bearer {ACCESS_KEY}",
25
+ "Content-Type": "application/json"
26
+ }
27
+
28
+
29
+ def api_post(path: str, body: dict) -> dict:
30
+ """POST 请求 agent-im OpenAPI"""
31
+ url = f"{XYQ_BASE.rstrip('/')}{path}"
32
+ data = json.dumps(body).encode("utf-8")
33
+ req = urllib.request.Request(
34
+ url,
35
+ data=data,
36
+ method="POST",
37
+ headers=_headers(),
38
+ )
39
+ try:
40
+ with urllib.request.urlopen(req, timeout=30) as resp:
41
+ return json.loads(resp.read().decode("utf-8"))
42
+ except urllib.error.HTTPError as e:
43
+ err_body = e.read().decode("utf-8") if e.fp else ""
44
+ print(f"API 错误 {e.code}: {err_body}", file=sys.stderr)
45
+ sys.exit(1)
46
+ except urllib.error.URLError as e:
47
+ print(f"网络错误: {e.reason}", file=sys.stderr)
48
+ sys.exit(1)
49
+
50
+
51
+ def api_get(path: str) -> dict:
52
+ """GET 请求 agent-im OpenAPI"""
53
+ url = f"{XYQ_BASE.rstrip('/')}{path}"
54
+ req = urllib.request.Request(url, method="GET", headers=_headers())
55
+ try:
56
+ with urllib.request.urlopen(req, timeout=30) as resp:
57
+ return json.loads(resp.read().decode("utf-8"))
58
+ except urllib.error.HTTPError as e:
59
+ err_body = e.read().decode("utf-8") if e.fp else ""
60
+ print(f"API 错误 {e.code}: {err_body}", file=sys.stderr)
61
+ sys.exit(1)
62
+ except urllib.error.URLError as e:
63
+ print(f"网络错误: {e.reason}", file=sys.stderr)
64
+ sys.exit(1)
65
+
66
+
67
+ def parse_response(resp: dict) -> dict:
68
+ """
69
+ 分析 API 响应。
70
+ 响应结构:{"ret":"0","errmsg":"","data":{}}
71
+ 如果 ret 不是 "0",打印错误信息并退出;否则返回 data。
72
+ """
73
+ ret = resp.get("ret", "")
74
+ if ret != "0":
75
+ errmsg = resp.get("errmsg", "未知错误")
76
+ print(f"错误码: {ret}, 错误信息: {errmsg}", file=sys.stderr)
77
+ sys.exit(1)
78
+ return resp.get("data", {})
79
+
80
+
81
+ def submit_run(thread_id: str = "", message: str = "", asset_ids: list = None) -> dict:
82
+ """
83
+ 创建会话或向已有会话发消息。
84
+ 返回 data: { projectUuid, sessionId }。
85
+ """
86
+ body = {}
87
+ if thread_id:
88
+ body["thread_id"] = thread_id
89
+ if message:
90
+ body["message"] = message
91
+ if asset_ids:
92
+ body["asset_ids"] = asset_ids
93
+ resp = api_post(SUBMIT_RUN_PATH, body)
94
+ return parse_response(resp)
95
+
96
+
97
+ def get_thread(thread_id: str, run_id: str = "", after_seq: int = 0) -> dict:
98
+ """
99
+ 查询会话消息列表。
100
+ 返回 data: { messages: [...] }。
101
+ """
102
+ body = {}
103
+ if thread_id:
104
+ body["thread_id"] = thread_id
105
+ if run_id:
106
+ body["run_id"] = run_id
107
+ body["after_seq"] = after_seq
108
+ resp = api_post(GET_THREAD_PATH, body)
109
+ resp = parse_response(resp)
110
+ thread = resp.get("thread", {})
111
+ run_list = thread.get("run_list", [])
112
+ if len(run_list) == 0:
113
+ print("错误:未返回 run_list", file=sys.stderr)
114
+ sys.exit(1)
115
+ run = run_list[0]
116
+ run_state = run.get("state", "")
117
+
118
+ # 判断 run_state
119
+ if run_state == 3:
120
+ # 成功
121
+ print("成功:本次创作已完成", file=sys.stderr)
122
+ return run
123
+ elif run_state == 4:
124
+ # 失败
125
+ fail_reason = run.get("fail_reason", "未知失败原因")
126
+ print(f"错误:{fail_reason}", file=sys.stderr)
127
+ sys.exit(1)
128
+ elif run_state == 5:
129
+ # 取消
130
+ print("错误:本次创作已被终止", file=sys.stderr)
131
+ sys.exit(1)
132
+ else:
133
+ print("本次创作进行中", file=sys.stdout)
134
+ return run
135
+
136
+
137
+ def extract_entries_from_run(run: dict) -> list:
138
+ """
139
+ 从 Run 的 EntryList 中提取符合条件的 entry。
140
+ """
141
+ matched = []
142
+ for entry in run.get("entry_list") or []:
143
+ e = {}
144
+ message = entry.get("message")
145
+ artifact = entry.get("artifact")
146
+ if message:
147
+ e["id"] = message.get("message_id", "")
148
+ e["role"] = message.get("role", "")
149
+ e["content"] = message.get("content", [])
150
+ client_tool_calls = message.get("client_tool_calls", [])
151
+ if len(client_tool_calls) > 0:
152
+ e["content"].extend(client_tool_calls)
153
+ if artifact:
154
+ e["id"] = artifact.get("artifact_id", "")
155
+ e["role"] = artifact.get("role", "")
156
+ e["content"] = artifact.get("content", [])
157
+ matched.append(e)
158
+ return matched
@@ -0,0 +1,103 @@
1
+ #!/usr/bin/env python3
2
+ """下载生成结果:从会话中提取所有图片/视频 URL 并批量下载到本地"""
3
+
4
+ import argparse
5
+ import json
6
+ import os
7
+ import sys
8
+ import urllib.request
9
+ import urllib.error
10
+ from concurrent.futures import ThreadPoolExecutor, as_completed
11
+
12
+ sys.path.insert(0, os.path.dirname(__file__))
13
+
14
+
15
+ def download_file(url, filepath):
16
+ """下载单个文件"""
17
+ import shutil
18
+ req = urllib.request.Request(url, headers={"User-Agent": "XYQ-Nest-Skill/1.0"})
19
+ tmp_path = filepath + ".tmp"
20
+ try:
21
+ with urllib.request.urlopen(req, timeout=600) as resp:
22
+ with open(tmp_path, "wb") as f:
23
+ shutil.copyfileobj(resp, f, length=1024 * 1024)
24
+ os.replace(tmp_path, filepath)
25
+ return filepath, None
26
+ except Exception as e:
27
+ if os.path.exists(tmp_path):
28
+ os.remove(tmp_path)
29
+ return filepath, str(e)
30
+
31
+
32
+ def main():
33
+ parser = argparse.ArgumentParser(
34
+ description="根据产物URL,下载生成的产物到本地,支持指定输出目录和文件名前缀",
35
+ epilog="""
36
+ 使用方式:
37
+ # 直接下载指定 URL 列表
38
+ python3 download_results.py --urls URL1 URL2 URL3 --output-dir ./output --prefix "storyboard"
39
+ """,
40
+ formatter_class=argparse.RawDescriptionHelpFormatter,
41
+ )
42
+ parser.add_argument("--urls", nargs="+", required=True, help="直接指定要下载的 URL 列表")
43
+ parser.add_argument("--output-dir", default="", help="输出目录(默认 ./xyq_output")
44
+ parser.add_argument("--prefix", default="", help="文件名前缀(如 'storyboard' → storyboard_01.png)")
45
+ parser.add_argument("--workers", type=int, default=5, help="并行下载线程数(默认 5)")
46
+ args = parser.parse_args()
47
+
48
+ # 准备输出目录
49
+ output_dir = args.output_dir or "./xyq_output"
50
+ os.makedirs(output_dir, exist_ok=True)
51
+
52
+ def _get_ext(url):
53
+ """从 URL 中提取文件扩展名,优先从 query 的 filename 参数取,其次从路径取"""
54
+ from urllib.parse import urlparse, parse_qs
55
+ parsed = urlparse(url)
56
+ qs = parse_qs(parsed.query)
57
+ filenames = qs.get("filename", [])
58
+ if filenames:
59
+ _, ext = os.path.splitext(filenames[0])
60
+ if ext:
61
+ return ext
62
+ _, ext = os.path.splitext(parsed.path)
63
+ return ext or ".bin"
64
+
65
+ # 构建下载任务
66
+ tasks = []
67
+ for i, url in enumerate(args.urls, 1):
68
+ ext = _get_ext(url)
69
+ if args.prefix:
70
+ filename = f"{args.prefix}_{i:02d}{ext}"
71
+ else:
72
+ filename = f"{i:02d}{ext}"
73
+ filepath = os.path.join(output_dir, filename)
74
+ tasks.append((url, filepath))
75
+
76
+ # 并行下载
77
+ results = []
78
+ errors = []
79
+ with ThreadPoolExecutor(max_workers=args.workers) as pool:
80
+ futures = {pool.submit(download_file, url, fp): (url, fp) for url, fp in tasks}
81
+ for future in as_completed(futures):
82
+ fp, err = future.result()
83
+ if err:
84
+ errors.append({"file": fp, "error": err})
85
+ else:
86
+ results.append(fp)
87
+
88
+ # 按文件名排序输出
89
+ results.sort()
90
+
91
+ output = {
92
+ "output_dir": output_dir,
93
+ "downloaded": results,
94
+ "total": len(results),
95
+ }
96
+ if errors:
97
+ output["errors"] = errors
98
+
99
+ print(json.dumps(output, ensure_ascii=False, indent=2))
100
+
101
+
102
+ if __name__ == "__main__":
103
+ main()
@@ -0,0 +1,53 @@
1
+ #!/usr/bin/env python3
2
+ """查询会话进展:POST /api/biz/v1/skill/get_thread,返回消息列表"""
3
+
4
+ from _common import extract_entries_from_run
5
+ import argparse
6
+ import json
7
+ import sys
8
+ import os
9
+
10
+ sys.path.insert(0, os.path.dirname(__file__))
11
+ from _common import get_thread
12
+
13
+
14
+ def main():
15
+ parser = argparse.ArgumentParser(
16
+ description="查询会话消息列表(会话进展)",
17
+ epilog="""
18
+ 环境变量:
19
+ XYQ_ACCESS_KEY 必填,Bearer 鉴权
20
+ XYQ_OPENAPI_BASE 或 XYQ_BASE_URL 可选,默认 https://xyq.jianying.com
21
+
22
+ 示例:
23
+ python3 get_thread.py --thread-id abc123 --run-id def456 --after-seq 0
24
+ """,
25
+ formatter_class=argparse.RawDescriptionHelpFormatter,
26
+ )
27
+ parser.add_argument(
28
+ "--thread-id",
29
+ required=True,
30
+ help="会话 ID(由 submit_run 返回)",
31
+ )
32
+ parser.add_argument(
33
+ "--run-id",
34
+ default="",
35
+ help="运行 ID(由 submit_run 返回)",
36
+ )
37
+ parser.add_argument(
38
+ "--after-seq",
39
+ type=int,
40
+ default=0,
41
+ help="只返回 seq 大于等于该值的消息,用于增量拉取(默认 0)",
42
+ )
43
+ args = parser.parse_args()
44
+
45
+ run = get_thread(args.thread_id, run_id=args.run_id, after_seq=args.after_seq)
46
+ # 从run中提取Message和Artifact
47
+ entries = extract_entries_from_run(run)
48
+ out = {"messages": entries}
49
+ print(json.dumps(out, ensure_ascii=False, indent=2))
50
+
51
+
52
+ if __name__ == "__main__":
53
+ main()
@@ -0,0 +1,76 @@
1
+ #!/usr/bin/env python3
2
+ """创建会话 / 向会话发送消息(生图、生视频等):POST /api/biz/v1/skill/submit_run"""
3
+
4
+ import argparse
5
+ import json
6
+ import sys
7
+ import os
8
+
9
+ sys.path.insert(0, os.path.dirname(__file__))
10
+ from _common import submit_run
11
+
12
+
13
+ def main():
14
+ parser = argparse.ArgumentParser(
15
+ description="创建会话或向已有会话发送消息(仅用于生视频)",
16
+ epilog="""
17
+ 环境变量:
18
+ XYQ_ACCESS_KEY 必填,Bearer 鉴权
19
+ XYQ_OPENAPI_BASE 或 XYQ_BASE_URL 可选,默认 https://xyq.jianying.com
20
+
21
+ 示例:
22
+ # 创建新会话并发送「生一个动漫视频」
23
+ python3 submit_run.py --message 生一个动漫视频
24
+
25
+ # 向已有会话发送消息
26
+ python3 submit_run.py --message 再生成一个动漫视频 --thread-id 90f05e0c-5d08-4148-be40-e30fc7c7bedf
27
+
28
+ # 传入文件资产 ID
29
+ python3 submit_run.py --message 生成视频 --asset-ids asset123
30
+
31
+ # 传入多个文件资产 ID
32
+ python3 submit_run.py --message 生成视频 --asset-ids asset123 asset456 asset789
33
+ """,
34
+ formatter_class=argparse.RawDescriptionHelpFormatter,
35
+ )
36
+ parser.add_argument(
37
+ "--message",
38
+ required=True,
39
+ help="要发送的消息内容(生图/生视频描述等),必填",
40
+ )
41
+ parser.add_argument(
42
+ "--thread-id",
43
+ default="",
44
+ help="已有会话 ID,不传则创建新会话或返回已有默认会话",
45
+ )
46
+ parser.add_argument(
47
+ "--asset-ids",
48
+ nargs="+",
49
+ default=[],
50
+ help="资产 ID 列表,可传入多个,例如:--asset-ids id1 id2 id3",
51
+ )
52
+ args = parser.parse_args()
53
+
54
+ data = submit_run(
55
+ thread_id=args.thread_id or "",
56
+ message=args.message or "",
57
+ asset_ids=args.asset_ids if args.asset_ids else None
58
+ )
59
+ run_data = data.get("run", {})
60
+ web_thread_link = data.get("web_thread_link", "")
61
+ thread_id = run_data.get("thread_id", "")
62
+ run_id = run_data.get("run_id", "")
63
+
64
+ if not thread_id:
65
+ print("错误:未返回 thread_id", file=sys.stderr)
66
+ sys.exit(1)
67
+ if not run_id:
68
+ print("错误:未返回 run_id", file=sys.stderr)
69
+ sys.exit(1)
70
+
71
+ out = {"thread_id": thread_id, "run_id": run_id, "web_thread_link": web_thread_link}
72
+ print(json.dumps(out, ensure_ascii=False, indent=2))
73
+
74
+
75
+ if __name__ == "__main__":
76
+ main()
@@ -0,0 +1,120 @@
1
+ #!/usr/bin/env python3
2
+ """上传图片/视频到小云雀资产库:POST /api/biz/v1/skill/upload_file(multipart/form-data)"""
3
+
4
+ import argparse
5
+ import json
6
+ import mimetypes
7
+ import os
8
+ import sys
9
+ import uuid
10
+ import urllib.request
11
+ import urllib.error
12
+
13
+ sys.path.insert(0, os.path.dirname(__file__))
14
+ from _common import XYQ_BASE, ACCESS_KEY, UPLOAD_FILE_PATH, parse_response
15
+
16
+ # 允许的 MIME 类型前缀
17
+ ALLOWED_PREFIXES = ("image/", "video/")
18
+
19
+
20
+ def upload_file(file_path: str) -> dict:
21
+ """
22
+ 上传本地文件到小云雀资产库。
23
+ 返回 data: { asset_id: str }。
24
+ """
25
+ if not os.path.isfile(file_path):
26
+ print(f"错误:文件不存在: {file_path}", file=sys.stderr)
27
+ sys.exit(1)
28
+
29
+ # 检查 MIME 类型
30
+ mime_type, _ = mimetypes.guess_type(file_path)
31
+ if mime_type and not any(mime_type.startswith(p) for p in ALLOWED_PREFIXES):
32
+ print(f"错误:不支持的文件类型: {mime_type},仅支持图片和视频", file=sys.stderr)
33
+ sys.exit(1)
34
+
35
+ # 构建 multipart/form-data 请求体
36
+ boundary = f"----PythonUpload{uuid.uuid4().hex}"
37
+ filename = os.path.basename(file_path)
38
+
39
+ body_parts = []
40
+
41
+ # accessKey 字段
42
+ body_parts.append(f"--{boundary}\r\n".encode())
43
+ body_parts.append(b'Content-Disposition: form-data; name="accessKey"\r\n\r\n')
44
+ body_parts.append(f"{ACCESS_KEY}\r\n".encode())
45
+
46
+ # file 字段
47
+ content_type = mime_type or "application/octet-stream"
48
+ body_parts.append(f"--{boundary}\r\n".encode())
49
+ body_parts.append(
50
+ f'Content-Disposition: form-data; name="file"; filename="{filename}"\r\n'.encode()
51
+ )
52
+ body_parts.append(f"Content-Type: {content_type}\r\n\r\n".encode())
53
+ with open(file_path, "rb") as f:
54
+ body_parts.append(f.read())
55
+ body_parts.append(b"\r\n")
56
+
57
+ # 结束边界
58
+ body_parts.append(f"--{boundary}--\r\n".encode())
59
+
60
+ data = b"".join(body_parts)
61
+
62
+ url = f"{XYQ_BASE.rstrip('/')}{UPLOAD_FILE_PATH}"
63
+ req = urllib.request.Request(
64
+ url,
65
+ data=data,
66
+ method="POST",
67
+ headers={
68
+ "Authorization": f"Bearer {ACCESS_KEY}",
69
+ "Content-Type": f"multipart/form-data; boundary={boundary}"
70
+ },
71
+ )
72
+ try:
73
+ with urllib.request.urlopen(req, timeout=120) as resp:
74
+ result = json.loads(resp.read().decode("utf-8"))
75
+ return parse_response(result)
76
+ except urllib.error.HTTPError as e:
77
+ err_body = e.read().decode("utf-8") if e.fp else ""
78
+ print(f"API 错误 {e.code}: {err_body}", file=sys.stderr)
79
+ sys.exit(1)
80
+ except urllib.error.URLError as e:
81
+ print(f"网络错误: {e.reason}", file=sys.stderr)
82
+ sys.exit(1)
83
+
84
+
85
+ def main():
86
+ parser = argparse.ArgumentParser(
87
+ description="上传图片或视频文件到小云雀资产库",
88
+ epilog="""
89
+ 环境变量:
90
+ XYQ_ACCESS_KEY 必填,Bearer 鉴权
91
+ XYQ_OPENAPI_BASE 或 XYQ_BASE_URL 可选,默认 https://xyq.jianying.com
92
+
93
+ 示例:
94
+ # 上传图片
95
+ python3 upload_file.py /path/to/image.png
96
+
97
+ # 上传视频
98
+ python3 upload_file.py /path/to/video.mp4
99
+ """,
100
+ formatter_class=argparse.RawDescriptionHelpFormatter,
101
+ )
102
+ parser.add_argument(
103
+ "file",
104
+ help="要上传的图片或视频文件路径",
105
+ )
106
+ args = parser.parse_args()
107
+
108
+ data = upload_file(args.file)
109
+ asset_id = data.get("pippit_asset_id", "")
110
+
111
+ if not asset_id:
112
+ print("错误:未返回 asset_id", file=sys.stderr)
113
+ sys.exit(1)
114
+
115
+ out = {"asset_id": asset_id}
116
+ print(json.dumps(out, ensure_ascii=False, indent=2))
117
+
118
+
119
+ if __name__ == "__main__":
120
+ main()