@pippit-dev/cli 1.0.1 → 1.0.3

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.
Files changed (33) hide show
  1. package/README.md +33 -3
  2. package/checksums.txt +6 -6
  3. package/cmd/download_result.go +1 -1
  4. package/cmd/generate_video/generate_video.go +48 -0
  5. package/cmd/generate_video/query_result.go +42 -0
  6. package/cmd/generate_video_test.go +542 -0
  7. package/cmd/get_thread.go +1 -0
  8. package/cmd/list_thread_file.go +1 -1
  9. package/cmd/root.go +27 -2
  10. package/cmd/short_drama/short_drama.go +52 -9
  11. package/cmd/short_drama_test.go +19 -19
  12. package/cmd/update/update.go +8 -8
  13. package/internal/common/client.go +29 -21
  14. package/internal/common/download_results.go +20 -14
  15. package/internal/common/error_log.go +62 -1
  16. package/internal/common/error_log_test.go +30 -0
  17. package/internal/common/get_thread.go +28 -14
  18. package/internal/common/get_thread_test.go +74 -0
  19. package/{cmd → internal/common}/json.go +3 -2
  20. package/internal/common/list_thread_file.go +6 -6
  21. package/internal/common/submit_run.go +29 -0
  22. package/internal/common/upload_file.go +23 -19
  23. package/internal/config/config.go +1 -1
  24. package/internal/generate_video/generate_video.go +199 -0
  25. package/internal/generate_video/query_result.go +344 -0
  26. package/internal/generate_video/query_result_test.go +31 -0
  27. package/internal/short_drama/submit_run.go +8 -28
  28. package/package.json +1 -1
  29. package/skills/short-drama/SKILL.md +8 -8
  30. package/skills/xyq-nest-skill/scripts/download_results.py +3 -1
  31. package/skills/xyq-nest-skill/scripts/upload_file.py +2 -2
  32. package/skills/xyq-nest-skill/scripts/xyq_common.py +4 -3
  33. package/cmd/upload_file.go +0 -50
@@ -0,0 +1,344 @@
1
+ package generate_video
2
+
3
+ import (
4
+ "context"
5
+ "encoding/json"
6
+ "fmt"
7
+ "path/filepath"
8
+ "strconv"
9
+ "strings"
10
+ "unicode"
11
+
12
+ "github.com/Pippit-dev/pippit-cli/internal/common"
13
+ )
14
+
15
+ const (
16
+ successRunState = 3
17
+ failedRunState = 4
18
+ )
19
+
20
+ // QueryResultOptions is the command-facing request shape for query-result.
21
+ type QueryResultOptions struct {
22
+ ThreadID string
23
+ RunID string
24
+ DownloadDir string
25
+ }
26
+
27
+ // QueryResultResult describes the user-facing query-result outcome.
28
+ type QueryResultResult struct {
29
+ Completed bool `json:"completed"`
30
+ ThreadID string `json:"thread_id"`
31
+ RunID string `json:"run_id"`
32
+ ErrorMessage string `json:"error_message"`
33
+ Videos []QueryResultVideo `json:"videos"`
34
+ }
35
+
36
+ // QueryResultVideo describes a downloaded video from query-result.
37
+ type QueryResultVideo struct {
38
+ DownloadURL string `json:"download_url"`
39
+ OutputPath string `json:"output_path"`
40
+ }
41
+
42
+ type queryThread struct {
43
+ ThreadID string `json:"thread_id"`
44
+ RunList []queryRun `json:"run_list"`
45
+ }
46
+
47
+ type queryRun struct {
48
+ RunID string `json:"run_id"`
49
+ State int `json:"state"`
50
+ ErrorMessage string `json:"error_message"`
51
+ ErrorMsg string `json:"error_msg"`
52
+ Errmsg string `json:"errmsg"`
53
+ EntryList []queryEntry `json:"entry_list"`
54
+ }
55
+
56
+ type queryEntry struct {
57
+ Artifact queryArtifact `json:"artifact"`
58
+ }
59
+
60
+ type queryArtifact struct {
61
+ Content []queryContent `json:"content"`
62
+ }
63
+
64
+ type queryContent struct {
65
+ SubType string `json:"sub_type"`
66
+ Data queryContentData `json:"data"`
67
+ }
68
+
69
+ type queryContentData struct {
70
+ Video *queryVideo `json:"video"`
71
+ ErrorMessage string `json:"error_message"`
72
+ ErrorCode json.RawMessage `json:"error_code"`
73
+ }
74
+
75
+ type queryVideo struct {
76
+ DownloadURL string `json:"download_url"`
77
+ Title string `json:"title"`
78
+ VID string `json:"vid"`
79
+ AssetID string `json:"asset_id"`
80
+ }
81
+
82
+ func QueryResult(ctx context.Context, opts *QueryResultOptions, runner *common.Runner) (*QueryResultResult, error) {
83
+ if err := validateQueryResultOptions(opts); err != nil {
84
+ return nil, err
85
+ }
86
+
87
+ threadResult, err := common.GetThread(ctx, &common.GetThreadOptions{
88
+ ThreadID: opts.ThreadID,
89
+ RunID: opts.RunID,
90
+ }, runner)
91
+ if err != nil {
92
+ return nil, fmt.Errorf("查询失败:%w", err)
93
+ }
94
+
95
+ thread, err := parseQueryThread(threadResult)
96
+ if err != nil {
97
+ return nil, fmt.Errorf("查询失败:%w", err)
98
+ }
99
+
100
+ run, ok := findQueryRun(thread, opts.RunID)
101
+ if !ok {
102
+ return nil, fmt.Errorf("查询失败:未找到 run_id=%s 对应的 Run", opts.RunID)
103
+ }
104
+ if run.State != successRunState {
105
+ result := &QueryResultResult{
106
+ Completed: run.State == failedRunState,
107
+ ThreadID: firstNonEmpty(thread.ThreadID, opts.ThreadID),
108
+ RunID: opts.RunID,
109
+ Videos: []QueryResultVideo{},
110
+ }
111
+ if run.State == failedRunState {
112
+ result.ErrorMessage = firstNonEmpty(extractQueryErrorMessage(run), "Run 失败")
113
+ }
114
+ return result, nil
115
+ }
116
+
117
+ videos := extractQueryVideos(run)
118
+ if len(videos) == 0 {
119
+ return nil, fmt.Errorf("下载失败:未找到可下载的视频产物")
120
+ }
121
+
122
+ downloadDir, err := expandPath(opts.DownloadDir)
123
+ if err != nil {
124
+ return nil, fmt.Errorf("下载失败:解析下载目录失败:%w", err)
125
+ }
126
+
127
+ resultVideos := make([]QueryResultVideo, 0, len(videos))
128
+ usedNames := make(map[string]int, len(videos))
129
+ for i, video := range videos {
130
+ if strings.TrimSpace(video.DownloadURL) == "" {
131
+ return nil, fmt.Errorf("下载失败:第 %d 个视频产物 download_url 为空", i+1)
132
+ }
133
+ outputPath := filepath.Join(downloadDir, uniqueQueryResultFileName(videoFileName(video, i+1), usedNames))
134
+ download, err := common.DownloadResult(ctx, common.DownloadResultOptions{
135
+ URL: video.DownloadURL,
136
+ OutputPath: outputPath,
137
+ Workers: 5,
138
+ }, runner)
139
+ if err != nil {
140
+ return nil, fmt.Errorf("下载失败:%w", err)
141
+ }
142
+ actualOutputPath := outputPath
143
+ if len(download.Downloaded) > 0 {
144
+ actualOutputPath = download.Downloaded[0]
145
+ } else if len(download.AlreadyExist) > 0 {
146
+ actualOutputPath = download.AlreadyExist[0]
147
+ }
148
+ resultVideos = append(resultVideos, QueryResultVideo{
149
+ DownloadURL: video.DownloadURL,
150
+ OutputPath: actualOutputPath,
151
+ })
152
+ }
153
+
154
+ return &QueryResultResult{
155
+ Completed: true,
156
+ ThreadID: firstNonEmpty(thread.ThreadID, opts.ThreadID),
157
+ RunID: opts.RunID,
158
+ Videos: resultVideos,
159
+ }, nil
160
+ }
161
+
162
+ func validateQueryResultOptions(opts *QueryResultOptions) error {
163
+ if opts == nil {
164
+ return fmt.Errorf("查询失败:缺少必填参数 --thread-id")
165
+ }
166
+ opts.ThreadID = strings.TrimSpace(opts.ThreadID)
167
+ opts.RunID = strings.TrimSpace(opts.RunID)
168
+ opts.DownloadDir = strings.TrimSpace(opts.DownloadDir)
169
+ if opts.ThreadID == "" {
170
+ return fmt.Errorf("查询失败:缺少必填参数 --thread-id")
171
+ }
172
+ if opts.RunID == "" {
173
+ return fmt.Errorf("查询失败:缺少必填参数 --run-id")
174
+ }
175
+ if opts.DownloadDir == "" {
176
+ return fmt.Errorf("查询失败:缺少必填参数 --download-dir")
177
+ }
178
+ return nil
179
+ }
180
+
181
+ func parseQueryThread(result *common.GetThreadResult) (*queryThread, error) {
182
+ if result == nil {
183
+ return nil, fmt.Errorf("get_thread 响应为空")
184
+ }
185
+ if len(result.RawData) > 0 {
186
+ var data map[string]json.RawMessage
187
+ if err := json.Unmarshal(result.RawData, &data); err == nil {
188
+ if raw := data["thread"]; len(raw) > 0 {
189
+ if thread, ok := decodeQueryThread(raw); ok {
190
+ return thread, nil
191
+ }
192
+ }
193
+ }
194
+ }
195
+ return nil, fmt.Errorf("get_thread 响应中未找到 data.thread")
196
+ }
197
+
198
+ func decodeQueryThread(raw []byte) (*queryThread, bool) {
199
+ var thread queryThread
200
+ if err := json.Unmarshal(raw, &thread); err != nil {
201
+ return nil, false
202
+ }
203
+ if thread.ThreadID == "" && len(thread.RunList) == 0 {
204
+ return nil, false
205
+ }
206
+ return &thread, true
207
+ }
208
+
209
+ func findQueryRun(thread *queryThread, runID string) (queryRun, bool) {
210
+ for _, run := range thread.RunList {
211
+ if run.RunID == runID {
212
+ return run, true
213
+ }
214
+ }
215
+ return queryRun{}, false
216
+ }
217
+
218
+ func (data *queryContentData) UnmarshalJSON(raw []byte) error {
219
+ raw = []byte(strings.TrimSpace(string(raw)))
220
+ if len(raw) == 0 || string(raw) == "null" {
221
+ return nil
222
+ }
223
+ if raw[0] == '"' {
224
+ var encoded string
225
+ if err := json.Unmarshal(raw, &encoded); err != nil {
226
+ return err
227
+ }
228
+ encoded = strings.TrimSpace(encoded)
229
+ if encoded == "" {
230
+ return nil
231
+ }
232
+ raw = []byte(encoded)
233
+ }
234
+ if len(raw) == 0 || raw[0] != '{' {
235
+ return nil
236
+ }
237
+ type alias queryContentData
238
+ return json.Unmarshal(raw, (*alias)(data))
239
+ }
240
+
241
+ func extractQueryVideos(run queryRun) []queryVideo {
242
+ videos := make([]queryVideo, 0)
243
+ for _, entry := range run.EntryList {
244
+ artifact := entry.Artifact
245
+ for _, content := range artifact.Content {
246
+ if content.SubType != "biz/x_data_video" {
247
+ continue
248
+ }
249
+ data := content.Data
250
+ if data.Video != nil {
251
+ videos = append(videos, *data.Video)
252
+ }
253
+ }
254
+ }
255
+ return videos
256
+ }
257
+
258
+ func extractQueryErrorMessage(run queryRun) string {
259
+ if message := firstNonEmpty(run.ErrorMessage, run.ErrorMsg, run.Errmsg); message != "" {
260
+ return message
261
+ }
262
+ for _, entry := range run.EntryList {
263
+ for _, content := range entry.Artifact.Content {
264
+ data := content.Data
265
+ if message := firstNonEmpty(data.ErrorMessage); message != "" {
266
+ if code := rawMessageString(data.ErrorCode); code != "" {
267
+ return fmt.Sprintf("%s (error_code=%s)", message, code)
268
+ }
269
+ return message
270
+ }
271
+ if code := rawMessageString(data.ErrorCode); code != "" {
272
+ return "error_code=" + code
273
+ }
274
+ }
275
+ }
276
+ return ""
277
+ }
278
+
279
+ func rawMessageString(raw json.RawMessage) string {
280
+ raw = json.RawMessage(strings.TrimSpace(string(raw)))
281
+ if len(raw) == 0 || string(raw) == "null" {
282
+ return ""
283
+ }
284
+ var value string
285
+ if err := json.Unmarshal(raw, &value); err == nil {
286
+ return strings.TrimSpace(value)
287
+ }
288
+ return strings.TrimSpace(string(raw))
289
+ }
290
+
291
+ func videoFileName(video queryVideo, index int) string {
292
+ name := firstNonEmpty(video.VID, video.Title, video.AssetID)
293
+ if name == "" {
294
+ name = "result_" + strconv.Itoa(index)
295
+ }
296
+ name = sanitizeFileName(name)
297
+ if !hasVideoExtension(name) {
298
+ name += ".mp4"
299
+ }
300
+ return name
301
+ }
302
+
303
+ func hasVideoExtension(name string) bool {
304
+ switch strings.ToLower(strings.TrimSpace(filepath.Ext(name))) {
305
+ case ".mp4", ".mov", ".m4v", ".webm":
306
+ return true
307
+ default:
308
+ return false
309
+ }
310
+ }
311
+
312
+ func firstNonEmpty(values ...string) string {
313
+ for _, value := range values {
314
+ value = strings.TrimSpace(value)
315
+ if value != "" {
316
+ return value
317
+ }
318
+ }
319
+ return ""
320
+ }
321
+
322
+ func sanitizeFileName(name string) string {
323
+ name = strings.TrimSpace(name)
324
+ if name == "" {
325
+ return "result.mp4"
326
+ }
327
+ return strings.Map(func(r rune) rune {
328
+ if unicode.IsControl(r) || r == '/' || r == '\\' || strings.ContainsRune(`<>:"|?*`, r) {
329
+ return '_'
330
+ }
331
+ return r
332
+ }, name)
333
+ }
334
+
335
+ func uniqueQueryResultFileName(name string, used map[string]int) string {
336
+ count := used[name] + 1
337
+ used[name] = count
338
+ if count == 1 {
339
+ return name
340
+ }
341
+ ext := filepath.Ext(name)
342
+ base := strings.TrimSuffix(name, ext)
343
+ return fmt.Sprintf("%s-%d%s", base, count, ext)
344
+ }
@@ -0,0 +1,31 @@
1
+ package generate_video
2
+
3
+ import "testing"
4
+
5
+ func TestVideoFileNameUsesVIDBeforeTimestampTitle(t *testing.T) {
6
+ got := videoFileName(queryVideo{
7
+ Title: "v03c76g10004d8jp38iljhtepa11k25g_2026-06-09T120814.516",
8
+ VID: "v03c76g10004d8jp38iljhtepa11k25g",
9
+ }, 1)
10
+ want := "v03c76g10004d8jp38iljhtepa11k25g.mp4"
11
+ if got != want {
12
+ t.Fatalf("videoFileName() = %q, want %q", got, want)
13
+ }
14
+ }
15
+
16
+ func TestVideoFileNameAddsMP4ForTimestampTitleWithoutVID(t *testing.T) {
17
+ got := videoFileName(queryVideo{
18
+ Title: "v03c76g10004d8jp38iljhtepa11k25g_2026-06-09T120814.516",
19
+ }, 1)
20
+ want := "v03c76g10004d8jp38iljhtepa11k25g_2026-06-09T120814.516.mp4"
21
+ if got != want {
22
+ t.Fatalf("videoFileName() = %q, want %q", got, want)
23
+ }
24
+ }
25
+
26
+ func TestVideoFileNameKeepsVideoExtension(t *testing.T) {
27
+ got := videoFileName(queryVideo{Title: "cat_video.mp4"}, 1)
28
+ if got != "cat_video.mp4" {
29
+ t.Fatalf("videoFileName() = %q, want cat_video.mp4", got)
30
+ }
31
+ }
@@ -5,7 +5,6 @@ import (
5
5
  "fmt"
6
6
 
7
7
  "github.com/Pippit-dev/pippit-cli/internal/common"
8
- "github.com/Pippit-dev/pippit-cli/internal/config"
9
8
  )
10
9
 
11
10
  // SubmitRunOptions is the stable command-facing request shape for short drama run submission.
@@ -22,21 +21,9 @@ type SubmitRunResult struct {
22
21
  WebThreadLink string `json:"web_thread_link"`
23
22
  }
24
23
 
25
- type submitRunResponse struct {
26
- Ret string `json:"ret"`
27
- Errmsg string `json:"errmsg"`
28
- Data struct {
29
- WebThreadLink string `json:"web_thread_link"`
30
- Run struct {
31
- ThreadID string `json:"thread_id"`
32
- RunID string `json:"run_id"`
33
- } `json:"run"`
34
- } `json:"data"`
35
- }
36
-
37
24
  func SubmitRun(ctx context.Context, opts *SubmitRunOptions, runner *common.Runner) (*SubmitRunResult, error) {
38
25
  if runner == nil || runner.Client == nil {
39
- return nil, fmt.Errorf("submit_run runner client is required")
26
+ return nil, fmt.Errorf("submit_run 运行器客户端缺失")
40
27
  }
41
28
 
42
29
  body := map[string]any{
@@ -50,21 +37,21 @@ func SubmitRun(ctx context.Context, opts *SubmitRunOptions, runner *common.Runne
50
37
  }
51
38
  body["agent_name"] = "pippit_nest_novel_agent"
52
39
 
53
- var resp submitRunResponse
54
- if err := runner.Client.SendRequest(ctx, submitRunPath(runner), body, &resp); err != nil {
55
- return nil, fmt.Errorf("submit_run request failed: %w", err)
40
+ var resp common.SubmitRunResponse
41
+ if err := runner.Client.SendRequest(ctx, common.SubmitRunPath(runner), body, &resp); err != nil {
42
+ return nil, fmt.Errorf("提交 short_drama 请求失败: %w", err)
56
43
  }
57
44
  if resp.Ret != "0" {
58
45
  if resp.Errmsg == "" {
59
- resp.Errmsg = "unknown error"
46
+ resp.Errmsg = "未知错误"
60
47
  }
61
- return nil, fmt.Errorf("submit_run failed: ret=%s errmsg=%s", resp.Ret, resp.Errmsg)
48
+ return nil, fmt.Errorf("short_drama 请求返回失败: ret=%s errmsg=%s", resp.Ret, resp.Errmsg)
62
49
  }
63
50
  if resp.Data.Run.ThreadID == "" {
64
- return nil, fmt.Errorf("submit_run response missing data.run.thread_id")
51
+ return nil, fmt.Errorf("short_drama 响应缺少 data.run.thread_id")
65
52
  }
66
53
  if resp.Data.Run.RunID == "" {
67
- return nil, fmt.Errorf("submit_run response missing data.run.run_id")
54
+ return nil, fmt.Errorf("short_drama 响应缺少 data.run.run_id")
68
55
  }
69
56
  return &SubmitRunResult{
70
57
  ThreadID: resp.Data.Run.ThreadID,
@@ -72,10 +59,3 @@ func SubmitRun(ctx context.Context, opts *SubmitRunOptions, runner *common.Runne
72
59
  WebThreadLink: resp.Data.WebThreadLink,
73
60
  }, nil
74
61
  }
75
-
76
- func submitRunPath(runner *common.Runner) string {
77
- if runner != nil && runner.Config != nil && runner.Config.Paths != nil && runner.Config.Paths.SubmitRun != "" {
78
- return runner.Config.Paths.SubmitRun
79
- }
80
- return config.SubmitRunPath
81
- }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pippit-dev/cli",
3
- "version": "1.0.1",
3
+ "version": "1.0.3",
4
4
  "description": "Pippit CLI",
5
5
  "bin": {
6
6
  "pippit-tool-cli": "scripts/run.js"
@@ -101,10 +101,10 @@ pippit-tool-cli get-thread --thread-id THREAD_ID --run-id RUN_ID
101
101
 
102
102
  ### 3. 上传文件
103
103
 
104
- 当用户提供短剧大纲、人物设定、世界观设定、已有分集或剧本等本地参考文件时,可先上传文件。`upload-file` 当前只接收本地文件路径,并且只支持 `.doc`、`.docx` 和 `.txt` 后缀;不要把 `.md`、`.pdf`、图片、视频或 URL 传给该命令。
104
+ 当用户提供短剧大纲、人物设定、世界观设定、已有分集或剧本等本地参考文件时,可先上传文件。`+upload-file` 当前只接收本地文件路径,并且只支持 `.doc`、`.docx` 和 `.txt` 后缀;不要把 `.md`、`.pdf`、图片、视频或 URL 传给该命令。
105
105
 
106
106
  ```bash
107
- pippit-tool-cli upload-file --path /path/to/outline.txt
107
+ pippit-tool-cli short-drama +upload-file --path /path/to/outline.txt
108
108
  ```
109
109
 
110
110
  上传成功后命令只返回 `asset_id`:
@@ -179,7 +179,7 @@ pippit-tool-cli download-result --url DOWNLOAD_URL --output-path FILE_PATH --upd
179
179
 
180
180
  ```
181
181
  1. 检查用户提供的是一个本地 `.doc`、`.docx` 或 `.txt` 剧本文件路径;如果不是,告知当前上传命令只支持这三类文件,不要擅自转换或改写文件。
182
- 2. pippit-tool-cli upload-file --path /path/to/file.txt
182
+ 2. pippit-tool-cli short-drama +upload-file --path /path/to/file.txt
183
183
  → 拿到 asset_id
184
184
  3. pippit-tool-cli short-drama +submit-run --message "用户的原始短剧需求" --asset-ids asset_id
185
185
  → 拿到 thread_id、run_id 和 web_thread_link
@@ -242,7 +242,7 @@ Thread: thread_...
242
242
  [assistant] ...
243
243
  ```
244
244
 
245
- **upload-file** 返回:
245
+ **short-drama +upload-file** 返回:
246
246
 
247
247
  ```json
248
248
  {
@@ -250,7 +250,7 @@ Thread: thread_...
250
250
  }
251
251
  ```
252
252
 
253
- `upload-file` 通过 `multipart/form-data` 上传文件,表单文件字段名为 `file`。本地文件必须存在、不能是目录,后缀必须是 `.doc`、`.docx` 或 `.txt`;不支持的后缀会直接报错。返回的 `asset_id` 来自服务端 `pippit_asset_id`,如果没有该字段才回退到 `asset_id`。
253
+ `+upload-file` 通过 `multipart/form-data` 上传文件,表单文件字段名为 `file`。本地文件必须存在、不能是目录,后缀必须是 `.doc`、`.docx` 或 `.txt`;不支持的后缀会直接报错。返回的 `asset_id` 来自服务端 `pippit_asset_id`,如果没有该字段才回退到 `asset_id`。
254
254
 
255
255
  **list-thread-file** 返回:
256
256
 
@@ -323,7 +323,7 @@ Thread: thread_...
323
323
 
324
324
  你要做的只有三件事:
325
325
 
326
- 1. **上传**:如果用户给了本地 `.doc` / `.docx` / `.txt` 参考文件,先调用 `upload-file`。
326
+ 1. **上传**:如果用户给了本地 `.doc` / `.docx` / `.txt` 参考文件,先调用 `+upload-file`。
327
327
  2. **提交任务**:首次创作时把用户原始短剧需求和唯一剧本 `asset_id` 通过 `+submit-run --asset-ids` 发给后端;同一 `thread_id` 后续续写或修改不再追加新的剧本文件。
328
328
  3. **传话、取文件、下载资源**:根据 `get-thread` 返回的 `readable_text` 展示进展、问题和结果;遇到表单、问卷、选项或按钮时,只做流程合理性清洗,不替用户决定创作内容;根据 `list-thread-file` 获取文件列表;再根据 `download_url` 调用 `download-result` 把缺失资源下载到用户本地。
329
329
 
@@ -341,8 +341,8 @@ Thread: thread_...
341
341
  - `--message` 是用户的原始短剧需求,不能为空。
342
342
  - 查询进展时优先使用 `+submit-run` 返回的 `thread_id` 和 `run_id`;如果需要查看整个会话,可以省略 `--run-id`。
343
343
  - `get-thread` 当前固定走服务端 v2 响应,输出字段是 `readable_text`;不要解析旧版 `messages` 数组。
344
- - `upload-file` 当前用于短剧场景文件上传链路,只支持本地 `.doc` / `.docx` / `.txt` 文件;`--path` 不能为空,路径必须指向真实文件,不能是目录。
345
- - `upload-file` 上传成功后只返回 `asset_id`;把该值原样作为 `+submit-run --asset-ids` 的参数。
344
+ - `+upload-file` 当前用于短剧场景文件上传链路,只支持本地 `.doc` / `.docx` / `.txt` 文件;`--path` 不能为空,路径必须指向真实文件,不能是目录。
345
+ - `+upload-file` 上传成功后只返回 `asset_id`;把该值原样作为 `+submit-run --asset-ids` 的参数。
346
346
  - 单次创作会话中(相同 `thread_id`),`+submit-run` 只支持绑定一个剧本文件。不要在同一 `thread_id` 下重复上传并追加第二个剧本 `asset_id`;用户给多个剧本时,先让用户选择一个,或分别开启新的创作会话。
347
347
  - `list-thread-file` 只需要 `thread_id`;分页参数使用 `--page-num 1 --page-size 200` 起步,`total` 达到 200 时下一轮递增 `page-num`。
348
348
  - `list-thread-file` 和 `download-result` 是两个不同的 CLI 指令:前者获取会话文件元信息,后者下载 URL 资源并写入到本地目标文件路径。
@@ -11,6 +11,8 @@ from concurrent.futures import ThreadPoolExecutor, as_completed
11
11
 
12
12
  sys.path.insert(0, os.path.dirname(__file__))
13
13
 
14
+ HTTP_TIMEOUT_SECONDS = 30 * 60
15
+
14
16
 
15
17
  def download_file(url, filepath):
16
18
  """下载单个文件"""
@@ -18,7 +20,7 @@ def download_file(url, filepath):
18
20
  req = urllib.request.Request(url, headers={"User-Agent": "XYQ-Nest-Skill/1.0"})
19
21
  tmp_path = filepath + ".tmp"
20
22
  try:
21
- with urllib.request.urlopen(req, timeout=600) as resp:
23
+ with urllib.request.urlopen(req, timeout=HTTP_TIMEOUT_SECONDS) as resp:
22
24
  with open(tmp_path, "wb") as f:
23
25
  shutil.copyfileobj(resp, f, length=1024 * 1024)
24
26
  os.replace(tmp_path, filepath)
@@ -11,7 +11,7 @@ import urllib.request
11
11
  import urllib.error
12
12
 
13
13
  sys.path.insert(0, os.path.dirname(__file__))
14
- from xyq_common import XYQ_BASE, ACCESS_KEY, UPLOAD_FILE_PATH, parse_response
14
+ from xyq_common import XYQ_BASE, ACCESS_KEY, UPLOAD_FILE_PATH, HTTP_TIMEOUT_SECONDS, parse_response
15
15
 
16
16
  # 允许的 MIME 类型前缀
17
17
  ALLOWED_PREFIXES = ("image/", "video/")
@@ -70,7 +70,7 @@ def upload_file(file_path: str) -> dict:
70
70
  },
71
71
  )
72
72
  try:
73
- with urllib.request.urlopen(req, timeout=120) as resp:
73
+ with urllib.request.urlopen(req, timeout=HTTP_TIMEOUT_SECONDS) as resp:
74
74
  result = json.loads(resp.read().decode("utf-8"))
75
75
  return parse_response(result)
76
76
  except urllib.error.HTTPError as e:
@@ -13,6 +13,7 @@ ACCESS_KEY = os.environ.get("XYQ_ACCESS_KEY", "")
13
13
  SUBMIT_RUN_PATH = "/api/biz/v1/skill/submit_run"
14
14
  GET_THREAD_PATH = "/api/biz/v1/skill/get_thread"
15
15
  UPLOAD_FILE_PATH = "/api/biz/v1/skill/upload_file"
16
+ HTTP_TIMEOUT_SECONDS = 30 * 60
16
17
 
17
18
  if not ACCESS_KEY:
18
19
  print("错误:请设置 XYQ_ACCESS_KEY 环境变量", file=sys.stderr)
@@ -22,7 +23,7 @@ if not ACCESS_KEY:
22
23
  def _headers():
23
24
  return {
24
25
  "Authorization": f"Bearer {ACCESS_KEY}",
25
- "Content-Type": "application/json"
26
+ "Content-Type": "application/json",
26
27
  }
27
28
 
28
29
 
@@ -37,7 +38,7 @@ def api_post(path: str, body: dict) -> dict:
37
38
  headers=_headers(),
38
39
  )
39
40
  try:
40
- with urllib.request.urlopen(req, timeout=30) as resp:
41
+ with urllib.request.urlopen(req, timeout=HTTP_TIMEOUT_SECONDS) as resp:
41
42
  return json.loads(resp.read().decode("utf-8"))
42
43
  except urllib.error.HTTPError as e:
43
44
  err_body = e.read().decode("utf-8") if e.fp else ""
@@ -53,7 +54,7 @@ def api_get(path: str) -> dict:
53
54
  url = f"{XYQ_BASE.rstrip('/')}{path}"
54
55
  req = urllib.request.Request(url, method="GET", headers=_headers())
55
56
  try:
56
- with urllib.request.urlopen(req, timeout=30) as resp:
57
+ with urllib.request.urlopen(req, timeout=HTTP_TIMEOUT_SECONDS) as resp:
57
58
  return json.loads(resp.read().decode("utf-8"))
58
59
  except urllib.error.HTTPError as e:
59
60
  err_body = e.read().decode("utf-8") if e.fp else ""
@@ -1,50 +0,0 @@
1
- package cmd
2
-
3
- import (
4
- "fmt"
5
- "io"
6
- "path/filepath"
7
- "strings"
8
-
9
- "github.com/Pippit-dev/pippit-cli/internal/common"
10
- "github.com/spf13/cobra"
11
- )
12
-
13
- func newUploadFileCommand(stdout, stderr io.Writer, runner *common.Runner) *cobra.Command {
14
- var opts common.UploadFileOptions
15
-
16
- cmd := &cobra.Command{
17
- Use: "upload-file",
18
- Short: "Upload a file",
19
- Args: cobra.NoArgs,
20
- RunE: withErrorLog("upload-file", func() map[string]string {
21
- return map[string]string{
22
- "file_name": fileNameForLog(opts.Path),
23
- }
24
- }, func(cmd *cobra.Command, _ []string) error {
25
- opts.Path = strings.TrimSpace(opts.Path)
26
-
27
- if opts.Path == "" {
28
- return fmt.Errorf("--path is required")
29
- }
30
-
31
- result, err := common.UploadFile(cmd.Context(), opts, runner)
32
- if err != nil {
33
- return err
34
- }
35
- return writeJSON(stdout, result)
36
- }),
37
- }
38
- cmd.SetOut(stdout)
39
- cmd.SetErr(stderr)
40
- cmd.Flags().StringVar(&opts.Path, "path", "", "local file path to upload")
41
- return cmd
42
- }
43
-
44
- func fileNameForLog(path string) string {
45
- path = strings.TrimSpace(path)
46
- if path == "" {
47
- return ""
48
- }
49
- return filepath.Base(path)
50
- }