@pippit-dev/cli 1.0.1 → 1.0.2

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 +58 -0
  6. package/cmd/generate_video_test.go +468 -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 +291 -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,291 @@
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 completedRunState = 3
16
+
17
+ // QueryResultOptions is the command-facing request shape for query-result.
18
+ type QueryResultOptions struct {
19
+ ThreadID string
20
+ RunID string
21
+ DownloadDir string
22
+ }
23
+
24
+ // QueryResultResult describes the user-facing query-result outcome.
25
+ type QueryResultResult struct {
26
+ Completed bool
27
+ State int
28
+ OutputPaths []string
29
+ DownloadURLs []string
30
+ }
31
+
32
+ type queryThread struct {
33
+ ThreadID string `json:"thread_id"`
34
+ RunList []queryRun `json:"run_list"`
35
+ }
36
+
37
+ type queryRun struct {
38
+ RunID string `json:"run_id"`
39
+ State int `json:"state"`
40
+ EntryList []queryEntry `json:"entry_list"`
41
+ }
42
+
43
+ type queryEntry struct {
44
+ Artifact queryArtifact `json:"artifact"`
45
+ }
46
+
47
+ type queryArtifact struct {
48
+ Content []queryContent `json:"content"`
49
+ }
50
+
51
+ type queryContent struct {
52
+ SubType string `json:"sub_type"`
53
+ Data queryContentData `json:"data"`
54
+ }
55
+
56
+ type queryContentData struct {
57
+ Video *queryVideo `json:"video"`
58
+ }
59
+
60
+ type queryVideo struct {
61
+ DownloadURL string `json:"download_url"`
62
+ Title string `json:"title"`
63
+ VID string `json:"vid"`
64
+ AssetID string `json:"asset_id"`
65
+ }
66
+
67
+ func QueryResult(ctx context.Context, opts *QueryResultOptions, runner *common.Runner) (*QueryResultResult, error) {
68
+ if err := validateQueryResultOptions(opts); err != nil {
69
+ return nil, err
70
+ }
71
+
72
+ threadResult, err := common.GetThread(ctx, &common.GetThreadOptions{
73
+ ThreadID: opts.ThreadID,
74
+ RunID: opts.RunID,
75
+ }, runner)
76
+ if err != nil {
77
+ return nil, fmt.Errorf("查询失败:%w", err)
78
+ }
79
+
80
+ thread, err := parseQueryThread(threadResult)
81
+ if err != nil {
82
+ return nil, fmt.Errorf("查询失败:%w", err)
83
+ }
84
+
85
+ run, ok := findQueryRun(thread, opts.RunID)
86
+ if !ok {
87
+ return nil, fmt.Errorf("查询失败:未找到 run_id=%s 对应的 Run", opts.RunID)
88
+ }
89
+ if run.State != completedRunState {
90
+ return &QueryResultResult{
91
+ Completed: false,
92
+ State: run.State,
93
+ }, nil
94
+ }
95
+
96
+ videos := extractQueryVideos(run)
97
+ if len(videos) == 0 {
98
+ return nil, fmt.Errorf("下载失败:未找到可下载的视频产物")
99
+ }
100
+
101
+ downloadDir, err := expandPath(opts.DownloadDir)
102
+ if err != nil {
103
+ return nil, fmt.Errorf("下载失败:解析下载目录失败:%w", err)
104
+ }
105
+
106
+ outputPaths := make([]string, 0, len(videos))
107
+ downloadURLs := make([]string, 0, len(videos))
108
+ usedNames := make(map[string]int, len(videos))
109
+ for i, video := range videos {
110
+ if strings.TrimSpace(video.DownloadURL) == "" {
111
+ return nil, fmt.Errorf("下载失败:第 %d 个视频产物 download_url 为空", i+1)
112
+ }
113
+ downloadURLs = append(downloadURLs, video.DownloadURL)
114
+ outputPath := filepath.Join(downloadDir, uniqueQueryResultFileName(videoFileName(video, i+1), usedNames))
115
+ download, err := common.DownloadResult(ctx, common.DownloadResultOptions{
116
+ URL: video.DownloadURL,
117
+ OutputPath: outputPath,
118
+ Workers: 5,
119
+ }, runner)
120
+ if err != nil {
121
+ return nil, fmt.Errorf("下载失败:%w", err)
122
+ }
123
+ if len(download.Downloaded) > 0 {
124
+ outputPaths = append(outputPaths, download.Downloaded...)
125
+ continue
126
+ }
127
+ if len(download.AlreadyExist) > 0 {
128
+ outputPaths = append(outputPaths, download.AlreadyExist...)
129
+ continue
130
+ }
131
+ outputPaths = append(outputPaths, outputPath)
132
+ }
133
+
134
+ return &QueryResultResult{
135
+ Completed: true,
136
+ State: run.State,
137
+ OutputPaths: outputPaths,
138
+ DownloadURLs: downloadURLs,
139
+ }, nil
140
+ }
141
+
142
+ func validateQueryResultOptions(opts *QueryResultOptions) error {
143
+ if opts == nil {
144
+ return fmt.Errorf("查询失败:缺少必填参数 --thread-id")
145
+ }
146
+ opts.ThreadID = strings.TrimSpace(opts.ThreadID)
147
+ opts.RunID = strings.TrimSpace(opts.RunID)
148
+ opts.DownloadDir = strings.TrimSpace(opts.DownloadDir)
149
+ if opts.ThreadID == "" {
150
+ return fmt.Errorf("查询失败:缺少必填参数 --thread-id")
151
+ }
152
+ if opts.RunID == "" {
153
+ return fmt.Errorf("查询失败:缺少必填参数 --run-id")
154
+ }
155
+ if opts.DownloadDir == "" {
156
+ return fmt.Errorf("查询失败:缺少必填参数 --download-dir")
157
+ }
158
+ return nil
159
+ }
160
+
161
+ func parseQueryThread(result *common.GetThreadResult) (*queryThread, error) {
162
+ if result == nil {
163
+ return nil, fmt.Errorf("get_thread 响应为空")
164
+ }
165
+ if len(result.RawData) > 0 {
166
+ var data map[string]json.RawMessage
167
+ if err := json.Unmarshal(result.RawData, &data); err == nil {
168
+ if raw := data["thread"]; len(raw) > 0 {
169
+ if thread, ok := decodeQueryThread(raw); ok {
170
+ return thread, nil
171
+ }
172
+ }
173
+ }
174
+ }
175
+ return nil, fmt.Errorf("get_thread 响应中未找到 data.thread")
176
+ }
177
+
178
+ func decodeQueryThread(raw []byte) (*queryThread, bool) {
179
+ var thread queryThread
180
+ if err := json.Unmarshal(raw, &thread); err != nil {
181
+ return nil, false
182
+ }
183
+ if thread.ThreadID == "" && len(thread.RunList) == 0 {
184
+ return nil, false
185
+ }
186
+ return &thread, true
187
+ }
188
+
189
+ func findQueryRun(thread *queryThread, runID string) (queryRun, bool) {
190
+ for _, run := range thread.RunList {
191
+ if run.RunID == runID {
192
+ return run, true
193
+ }
194
+ }
195
+ return queryRun{}, false
196
+ }
197
+
198
+ func (data *queryContentData) UnmarshalJSON(raw []byte) error {
199
+ raw = []byte(strings.TrimSpace(string(raw)))
200
+ if len(raw) == 0 || string(raw) == "null" {
201
+ return nil
202
+ }
203
+ if raw[0] == '"' {
204
+ var encoded string
205
+ if err := json.Unmarshal(raw, &encoded); err != nil {
206
+ return err
207
+ }
208
+ encoded = strings.TrimSpace(encoded)
209
+ if encoded == "" {
210
+ return nil
211
+ }
212
+ raw = []byte(encoded)
213
+ }
214
+ if len(raw) == 0 || raw[0] != '{' {
215
+ return nil
216
+ }
217
+ type alias queryContentData
218
+ return json.Unmarshal(raw, (*alias)(data))
219
+ }
220
+
221
+ func extractQueryVideos(run queryRun) []queryVideo {
222
+ videos := make([]queryVideo, 0)
223
+ for _, entry := range run.EntryList {
224
+ artifact := entry.Artifact
225
+ for _, content := range artifact.Content {
226
+ if content.SubType != "biz/x_data_video" {
227
+ continue
228
+ }
229
+ data := content.Data
230
+ if data.Video != nil {
231
+ videos = append(videos, *data.Video)
232
+ }
233
+ }
234
+ }
235
+ return videos
236
+ }
237
+
238
+ func videoFileName(video queryVideo, index int) string {
239
+ name := firstNonEmpty(video.VID, video.Title, video.AssetID)
240
+ if name == "" {
241
+ name = "result_" + strconv.Itoa(index)
242
+ }
243
+ name = sanitizeFileName(name)
244
+ if !hasVideoExtension(name) {
245
+ name += ".mp4"
246
+ }
247
+ return name
248
+ }
249
+
250
+ func hasVideoExtension(name string) bool {
251
+ switch strings.ToLower(strings.TrimSpace(filepath.Ext(name))) {
252
+ case ".mp4", ".mov", ".m4v", ".webm":
253
+ return true
254
+ default:
255
+ return false
256
+ }
257
+ }
258
+
259
+ func firstNonEmpty(values ...string) string {
260
+ for _, value := range values {
261
+ value = strings.TrimSpace(value)
262
+ if value != "" {
263
+ return value
264
+ }
265
+ }
266
+ return ""
267
+ }
268
+
269
+ func sanitizeFileName(name string) string {
270
+ name = strings.TrimSpace(name)
271
+ if name == "" {
272
+ return "result.mp4"
273
+ }
274
+ return strings.Map(func(r rune) rune {
275
+ if unicode.IsControl(r) || r == '/' || r == '\\' || strings.ContainsRune(`<>:"|?*`, r) {
276
+ return '_'
277
+ }
278
+ return r
279
+ }, name)
280
+ }
281
+
282
+ func uniqueQueryResultFileName(name string, used map[string]int) string {
283
+ count := used[name] + 1
284
+ used[name] = count
285
+ if count == 1 {
286
+ return name
287
+ }
288
+ ext := filepath.Ext(name)
289
+ base := strings.TrimSuffix(name, ext)
290
+ return fmt.Sprintf("%s-%d%s", base, count, ext)
291
+ }
@@ -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.2",
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
- }