@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
@@ -3,12 +3,12 @@ package short_drama
3
3
  import (
4
4
  "fmt"
5
5
  "io"
6
+ "path/filepath"
6
7
  "strconv"
7
8
  "strings"
8
9
 
9
10
  "github.com/Pippit-dev/pippit-cli/internal/common"
10
11
  "github.com/Pippit-dev/pippit-cli/internal/short_drama"
11
- "github.com/bytedance/sonic"
12
12
  "github.com/spf13/cobra"
13
13
  )
14
14
 
@@ -22,6 +22,7 @@ func NewCommand(stdout, stderr io.Writer, runner *common.Runner) *cobra.Command
22
22
  cmd.SetOut(stdout)
23
23
  cmd.SetErr(stderr)
24
24
  cmd.AddCommand(newShortDramaSubmitRunCommand(stdout, stderr, runner))
25
+ cmd.AddCommand(newShortDramaUploadFileCommand(stdout, stderr, runner))
25
26
  return cmd
26
27
  }
27
28
 
@@ -42,14 +43,14 @@ func newShortDramaSubmitRunCommand(stdout, stderr io.Writer, runner *common.Runn
42
43
  opts.ThreadID = strings.TrimSpace(opts.ThreadID)
43
44
 
44
45
  if opts.Message == "" {
45
- return fmt.Errorf("--message is required")
46
+ return fmt.Errorf("缺少必填参数 --message")
46
47
  }
47
48
 
48
49
  result, err := short_drama.SubmitRun(cmd.Context(), &opts, runner)
49
50
  if err != nil {
50
51
  return err
51
52
  }
52
- return writeJSON(stdout, result)
53
+ return common.WriteJSON(stdout, result)
53
54
  }),
54
55
  }
55
56
  cmd.SetOut(stdout)
@@ -60,6 +61,40 @@ func newShortDramaSubmitRunCommand(stdout, stderr io.Writer, runner *common.Runn
60
61
  return cmd
61
62
  }
62
63
 
64
+ func newShortDramaUploadFileCommand(stdout, stderr io.Writer, runner *common.Runner) *cobra.Command {
65
+ var opts common.UploadFileOptions
66
+
67
+ cmd := &cobra.Command{
68
+ Use: "+upload-file",
69
+ Short: "Upload a file for the short drama scene",
70
+ Args: cobra.NoArgs,
71
+ RunE: withErrorLog("short-drama +upload-file", func() map[string]string {
72
+ return map[string]string{
73
+ "file_name": fileNameForLog(opts.Path),
74
+ }
75
+ }, func(cmd *cobra.Command, _ []string) error {
76
+ opts.Path = strings.TrimSpace(opts.Path)
77
+
78
+ if opts.Path == "" {
79
+ return fmt.Errorf("--path is required")
80
+ }
81
+ if !isShortDramaUploadFile(opts.Path) {
82
+ return fmt.Errorf("短剧上传仅支持上传 .doc、.docx 和 .txt 文件")
83
+ }
84
+
85
+ result, err := common.UploadFile(cmd.Context(), opts, runner)
86
+ if err != nil {
87
+ return err
88
+ }
89
+ return common.WriteJSON(stdout, result)
90
+ }),
91
+ }
92
+ cmd.SetOut(stdout)
93
+ cmd.SetErr(stderr)
94
+ cmd.Flags().StringVar(&opts.Path, "path", "", "local file path to upload")
95
+ return cmd
96
+ }
97
+
63
98
  func withErrorLog(command string, fields func() map[string]string, run func(*cobra.Command, []string) error) func(*cobra.Command, []string) error {
64
99
  return func(cmd *cobra.Command, args []string) error {
65
100
  err := run(cmd, args)
@@ -74,11 +109,19 @@ func withErrorLog(command string, fields func() map[string]string, run func(*cob
74
109
  }
75
110
  }
76
111
 
77
- func writeJSON(w io.Writer, v any) error {
78
- data, err := sonic.Marshal(v)
79
- if err != nil {
80
- return err
112
+ func fileNameForLog(path string) string {
113
+ path = strings.TrimSpace(path)
114
+ if path == "" {
115
+ return ""
116
+ }
117
+ return filepath.Base(path)
118
+ }
119
+
120
+ func isShortDramaUploadFile(path string) bool {
121
+ switch strings.ToLower(filepath.Ext(path)) {
122
+ case ".doc", ".docx", ".txt":
123
+ return true
124
+ default:
125
+ return false
81
126
  }
82
- _, err = fmt.Fprintln(w, string(data))
83
- return err
84
127
  }
@@ -124,12 +124,13 @@ func TestRootHelpListsSupportedCommands(t *testing.T) {
124
124
  }
125
125
  got := stdout.String()
126
126
  for _, want := range []string{
127
- "Pippit CLI submits short-drama workflows",
127
+ "Pippit CLI generates videos",
128
+ "generate-video",
128
129
  "download-result",
129
130
  "get-thread",
130
131
  "list-thread-file",
132
+ "query-result",
131
133
  "short-drama",
132
- "upload-file",
133
134
  "update",
134
135
  "--version",
135
136
  } {
@@ -158,7 +159,6 @@ func TestShortDramaDoesNotIncludeCommonThreadCommands(t *testing.T) {
158
159
  "+download-result": true,
159
160
  "+get-thread": true,
160
161
  "+list-thread-file": true,
161
- "+upload-file": true,
162
162
  }
163
163
  for _, child := range cmd.Commands() {
164
164
  if removedCommands[child.Name()] {
@@ -189,7 +189,7 @@ func TestShortDramaSubmitRunRequiresMessage(t *testing.T) {
189
189
  if err == nil {
190
190
  t.Fatal("Execute() error = nil, want validation error")
191
191
  }
192
- if !strings.Contains(err.Error(), "--message is required") {
192
+ if !strings.Contains(err.Error(), "缺少必填参数 --message") {
193
193
  t.Fatalf("error = %q, want message validation", err)
194
194
  }
195
195
  }
@@ -214,7 +214,7 @@ func TestShortDramaSubmitRunRequiresAccessKey(t *testing.T) {
214
214
  assertAccessKeyGuidance(t, err)
215
215
  }
216
216
 
217
- func TestUploadFile(t *testing.T) {
217
+ func TestShortDramaUploadFile(t *testing.T) {
218
218
  server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
219
219
  if r.Method != http.MethodPost {
220
220
  t.Fatalf("method = %s, want POST", r.Method)
@@ -265,7 +265,7 @@ func TestUploadFile(t *testing.T) {
265
265
 
266
266
  var stdout, stderr bytes.Buffer
267
267
  root := newTestRootCommand(t, &stdout, &stderr, server.URL)
268
- root.SetArgs([]string{"upload-file", "--path", path})
268
+ root.SetArgs([]string{"short-drama", "+upload-file", "--path", path})
269
269
 
270
270
  if err := root.Execute(); err != nil {
271
271
  t.Fatalf("Execute() error = %v, stderr = %s", err, stderr.String())
@@ -277,10 +277,10 @@ func TestUploadFile(t *testing.T) {
277
277
  }
278
278
  }
279
279
 
280
- func TestUploadFileRequiresPath(t *testing.T) {
280
+ func TestShortDramaUploadFileRequiresPath(t *testing.T) {
281
281
  var stdout, stderr bytes.Buffer
282
282
  root := NewRootCommand(&stdout, &stderr)
283
- root.SetArgs([]string{"upload-file"})
283
+ root.SetArgs([]string{"short-drama", "+upload-file"})
284
284
 
285
285
  err := root.Execute()
286
286
  if err == nil {
@@ -291,7 +291,7 @@ func TestUploadFileRequiresPath(t *testing.T) {
291
291
  }
292
292
  }
293
293
 
294
- func TestUploadFileRequiresAccessKey(t *testing.T) {
294
+ func TestShortDramaUploadFileRequiresAccessKey(t *testing.T) {
295
295
  server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
296
296
  t.Fatal("server should not receive request without access key")
297
297
  }))
@@ -305,7 +305,7 @@ func TestUploadFileRequiresAccessKey(t *testing.T) {
305
305
 
306
306
  var stdout, stderr bytes.Buffer
307
307
  root := newTestRootCommandWithAccessKey(t, &stdout, &stderr, server.URL, "")
308
- root.SetArgs([]string{"upload-file", "--path", path})
308
+ root.SetArgs([]string{"short-drama", "+upload-file", "--path", path})
309
309
 
310
310
  err := root.Execute()
311
311
  if err == nil {
@@ -314,7 +314,7 @@ func TestUploadFileRequiresAccessKey(t *testing.T) {
314
314
  assertAccessKeyGuidance(t, err)
315
315
  }
316
316
 
317
- func TestUploadFileRejectsUnsupportedFileType(t *testing.T) {
317
+ func TestShortDramaUploadFileRejectsUnsupportedFileType(t *testing.T) {
318
318
  cwd := chdirTemp(t)
319
319
  path := filepath.Join(cwd, "story.png")
320
320
  if err := os.WriteFile(path, []byte("png-data"), 0o644); err != nil {
@@ -323,13 +323,13 @@ func TestUploadFileRejectsUnsupportedFileType(t *testing.T) {
323
323
 
324
324
  var stdout, stderr bytes.Buffer
325
325
  root := NewRootCommand(&stdout, &stderr)
326
- root.SetArgs([]string{"upload-file", "--path", path})
326
+ root.SetArgs([]string{"short-drama", "+upload-file", "--path", path})
327
327
 
328
328
  err := root.Execute()
329
329
  if err == nil {
330
330
  t.Fatal("Execute() error = nil, want file type validation error")
331
331
  }
332
- if !strings.Contains(err.Error(), "only .doc, .docx, and .txt uploads are supported") {
332
+ if !strings.Contains(err.Error(), "仅支持上传 .doc、.docx .txt") {
333
333
  t.Fatalf("error = %q, want unsupported type validation", err)
334
334
  }
335
335
  }
@@ -601,7 +601,7 @@ func TestDownloadResultRejectsOutputDirFlag(t *testing.T) {
601
601
  if err == nil {
602
602
  t.Fatal("Execute() error = nil, want unknown flag error")
603
603
  }
604
- if !strings.Contains(err.Error(), "unknown flag: --output-dir") {
604
+ if !strings.Contains(err.Error(), "未知参数: --output-dir") {
605
605
  t.Fatalf("error = %q, want output-dir rejection", err)
606
606
  }
607
607
  }
@@ -629,7 +629,7 @@ func TestDownloadResultRejectsInvalidScheme(t *testing.T) {
629
629
  if err == nil {
630
630
  t.Fatal("Execute() error = nil, want scheme validation error")
631
631
  }
632
- if !strings.Contains(err.Error(), "only http and https are allowed") {
632
+ if !strings.Contains(err.Error(), "仅支持 http https") {
633
633
  t.Fatalf("error = %q, want scheme validation", err)
634
634
  }
635
635
  }
@@ -654,7 +654,7 @@ func TestDownloadResultAllFailed(t *testing.T) {
654
654
  if err == nil {
655
655
  t.Fatal("Execute() error = nil, want all-failed error")
656
656
  }
657
- if !strings.Contains(err.Error(), "all 1 download(s) failed") {
657
+ if !strings.Contains(err.Error(), "全部 1 个下载任务失败") {
658
658
  t.Fatalf("error = %q, want all-failed message", err)
659
659
  }
660
660
  logs := readDailyErrorLog(t)
@@ -843,7 +843,7 @@ func TestListThreadFile(t *testing.T) {
843
843
  if got["total"] != float64(1) {
844
844
  t.Fatalf("total = %v, want 1", got["total"])
845
845
  }
846
- wantMessage := "<system-remind>\n- total is below 200; continue querying with current --page-num 2\n</system-remind>"
846
+ wantMessage := "<system-remind>\n- 文件总数小于 200;继续使用当前 --page-num 2 查询\n</system-remind>"
847
847
  if got["message"] != wantMessage {
848
848
  t.Fatalf("message = %v, want current page hint", got["message"])
849
849
  }
@@ -893,7 +893,7 @@ func TestListThreadFileMessageWhenPageFull(t *testing.T) {
893
893
  }
894
894
 
895
895
  got := decodeJSON(t, stdout.Bytes())
896
- wantMessage := "<system-remind>\n- total reached 200; query the next page with --page-num 3\n</system-remind>"
896
+ wantMessage := "<system-remind>\n- 文件总数已达到 200;请使用 --page-num 3 查询下一页\n</system-remind>"
897
897
  if got["message"] != wantMessage {
898
898
  t.Fatalf("message = %v, want next page hint", got["message"])
899
899
  }
@@ -969,7 +969,7 @@ func assertAccessKeyGuidance(t *testing.T, err error) {
969
969
  t.Fatal("error = nil, want access key guidance")
970
970
  }
971
971
  msg := err.Error()
972
- if !strings.Contains(msg, "XYQ_ACCESS_KEY") {
972
+ if !strings.Contains(msg, "XYQ_ACCESS_KEY 缺失") {
973
973
  t.Fatalf("error = %q, want access key guidance", err)
974
974
  }
975
975
  if !strings.Contains(msg, "https://xyq.jianying.com/home?tab_name=home") {
@@ -60,21 +60,21 @@ func runUpdate(stdout, stderr io.Writer) error {
60
60
  fmt.Fprintf(stderr, "Updating pippit-tool-cli via npm: %s\n", pkg)
61
61
  restore, err := prepareSelfReplace()
62
62
  if err != nil {
63
- return fmt.Errorf("prepare self replace: %w", err)
63
+ return fmt.Errorf("准备替换当前可执行文件失败: %w", err)
64
64
  }
65
65
  if err := runInheritEnv(stderr, []string{"PIPPIT_CLI_SKIP_SKILLS=1"}, "npm", "install", "-g", pkg); err != nil {
66
66
  restore()
67
- return fmt.Errorf("update pippit-tool-cli: %w", err)
67
+ return fmt.Errorf("更新 pippit-tool-cli 失败: %w", err)
68
68
  }
69
69
 
70
70
  root, err := globalPackageRoot(defaultPackage)
71
71
  if err != nil {
72
- return fmt.Errorf("locate global package: %w", err)
72
+ return fmt.Errorf("定位全局 npm 包失败: %w", err)
73
73
  }
74
74
 
75
75
  fmt.Fprintln(stderr, "Updating pippit-tool-cli skills...")
76
76
  if err := installSkills(root, stderr); err != nil {
77
- return fmt.Errorf("update pippit-tool-cli skills: %w", err)
77
+ return fmt.Errorf("更新 pippit-tool-cli skills 失败: %w", err)
78
78
  }
79
79
 
80
80
  reportBundledSkillTelemetry("update", "cli_update", stderr)
@@ -89,7 +89,7 @@ func globalPackageRoot(pkg string) (string, error) {
89
89
  }
90
90
  npmRoot := strings.TrimSpace(string(out))
91
91
  if npmRoot == "" {
92
- return "", fmt.Errorf("npm root -g returned empty output")
92
+ return "", fmt.Errorf("npm root -g 输出为空")
93
93
  }
94
94
  return filepath.Join(append([]string{npmRoot}, strings.Split(pkg, "/")...)...), nil
95
95
  }
@@ -98,7 +98,7 @@ func installSkills(root string, stderr io.Writer) error {
98
98
  if info, err := os.Stat(filepath.Join(root, "skills")); err != nil {
99
99
  return err
100
100
  } else if !info.IsDir() {
101
- return fmt.Errorf("%s is not a directory", filepath.Join(root, "skills"))
101
+ return fmt.Errorf("%s 不是目录", filepath.Join(root, "skills"))
102
102
  }
103
103
  if err := runInherit(stderr, "npx", "-y", "skills", "add", root, "-g", "-y", "--skill", "*"); err != nil {
104
104
  return err
@@ -153,7 +153,7 @@ func reportBundledSkillTelemetry(event string, source string, stderr io.Writer)
153
153
  go func(payload telemetryPayload) {
154
154
  defer wg.Done()
155
155
  if err := reportSkillTelemetry(payload); err != nil && os.Getenv("PIPPIT_CLI_DEBUG_TELEMETRY") == "1" {
156
- fmt.Fprintf(stderr, "[pippit-tool-cli] telemetry failed: %v\n", err)
156
+ fmt.Fprintf(stderr, "[pippit-tool-cli] 埋点上报失败: %v\n", err)
157
157
  }
158
158
  }(payload)
159
159
  }
@@ -188,7 +188,7 @@ func reportSkillTelemetry(payload telemetryPayload) error {
188
188
  defer resp.Body.Close()
189
189
  _, _ = io.Copy(io.Discard, resp.Body)
190
190
  if resp.StatusCode >= 400 {
191
- return fmt.Errorf("telemetry returned HTTP %d", resp.StatusCode)
191
+ return fmt.Errorf("埋点请求返回 HTTP %d", resp.StatusCode)
192
192
  }
193
193
  return nil
194
194
  }
@@ -20,6 +20,7 @@ import (
20
20
 
21
21
  type Client interface {
22
22
  SendRequest(ctx context.Context, path string, body any, out any) error
23
+ SendRequestWithHeaders(ctx context.Context, path string, body any, headers map[string]string, out any) error
23
24
  SendMultipartRequest(ctx context.Context, path string, fields map[string]string, file MultipartFile, out any) error
24
25
  }
25
26
 
@@ -53,6 +54,10 @@ func NewHTTPClient(baseURL string, timeout time.Duration, authorizer RequestAuth
53
54
  }
54
55
 
55
56
  func (c *httpClient) SendRequest(ctx context.Context, path string, body any, out any) error {
57
+ return c.SendRequestWithHeaders(ctx, path, body, nil, out)
58
+ }
59
+
60
+ func (c *httpClient) SendRequestWithHeaders(ctx context.Context, path string, body any, headers map[string]string, out any) error {
56
61
  method := http.MethodPost
57
62
  if body == nil {
58
63
  method = http.MethodGet
@@ -67,27 +72,27 @@ func (c *httpClient) SendRequest(ctx context.Context, path string, body any, out
67
72
  if body != nil {
68
73
  payload, err := sonic.Marshal(body)
69
74
  if err != nil {
70
- return fmt.Errorf("encode body: %w", err)
75
+ return fmt.Errorf("编码请求体失败: %w", err)
71
76
  }
72
77
  reader = bytes.NewReader(payload)
73
78
  }
74
79
 
75
80
  req, err := http.NewRequestWithContext(ctx, method, reqURL, reader)
76
81
  if err != nil {
77
- return fmt.Errorf("build %s request: %w", method, err)
82
+ return fmt.Errorf("构造 %s 请求失败: %w", method, err)
78
83
  }
79
84
  if body != nil {
80
85
  req.Header.Set("Content-Type", "application/json")
81
86
  }
82
87
 
83
88
  if c.authorizer == nil {
84
- return fmt.Errorf("authorized request requires authorizer")
89
+ return fmt.Errorf("授权请求缺少认证器")
85
90
  }
86
91
  if err := c.authorizer.Inject(ctx, req); err != nil {
87
- return fmt.Errorf("inject auth headers: %w", err)
92
+ return fmt.Errorf("写入认证请求头失败: %w", err)
88
93
  }
89
94
 
90
- c.injectHeaders(req)
95
+ c.injectHeaders(req, headers)
91
96
 
92
97
  // If out is **http.Response, return the raw response for streaming (e.g. file download).
93
98
  if out != nil {
@@ -95,11 +100,11 @@ func (c *httpClient) SendRequest(ctx context.Context, path string, body any, out
95
100
  if rv.Elem().Type().Elem() == reflect.TypeOf(http.Response{}) {
96
101
  resp, err := c.httpClient.Do(req)
97
102
  if err != nil {
98
- return fmt.Errorf("%s %s failed: %w", method, reqURL, err)
103
+ return fmt.Errorf("%s %s 请求失败: %w", method, reqURL, err)
99
104
  }
100
105
  if resp.StatusCode >= 400 {
101
106
  defer resp.Body.Close()
102
- return fmt.Errorf("%s %s returned HTTP %d", method, reqURL, resp.StatusCode)
107
+ return fmt.Errorf("%s %s 返回 HTTP %d", method, reqURL, resp.StatusCode)
103
108
  }
104
109
  rv.Elem().Set(reflect.ValueOf(resp))
105
110
  return nil
@@ -118,7 +123,7 @@ func (c *httpClient) SendMultipartRequest(ctx context.Context, path string, fiel
118
123
  return err
119
124
  }
120
125
  if file.FieldName == "" {
121
- return fmt.Errorf("multipart file field name is required")
126
+ return fmt.Errorf("multipart 文件字段名不能为空")
122
127
  }
123
128
  if file.FileName == "" {
124
129
  file.FileName = filepath.Base(file.Path)
@@ -133,7 +138,7 @@ func (c *httpClient) SendMultipartRequest(ctx context.Context, path string, fiel
133
138
  if err != nil {
134
139
  _ = pr.Close()
135
140
  _ = pw.Close()
136
- return fmt.Errorf("build POST request: %w", err)
141
+ return fmt.Errorf("构造 POST 请求失败: %w", err)
137
142
  }
138
143
  req.Header.Set("Content-Type", writer.FormDataContentType())
139
144
  req.Header.Set("Accept", "application/json")
@@ -141,14 +146,14 @@ func (c *httpClient) SendMultipartRequest(ctx context.Context, path string, fiel
141
146
  if c.authorizer == nil {
142
147
  _ = pr.Close()
143
148
  _ = pw.Close()
144
- return fmt.Errorf("authorized request requires authorizer")
149
+ return fmt.Errorf("授权请求缺少认证器")
145
150
  }
146
151
  if err := c.authorizer.Inject(ctx, req); err != nil {
147
152
  _ = pr.Close()
148
153
  _ = pw.Close()
149
- return fmt.Errorf("inject auth headers: %w", err)
154
+ return fmt.Errorf("写入认证请求头失败: %w", err)
150
155
  }
151
- c.injectHeaders(req)
156
+ c.injectHeaders(req, nil)
152
157
 
153
158
  go func() {
154
159
  err := writeMultipartBody(writer, fields, file)
@@ -172,7 +177,7 @@ func writeMultipartBody(writer *multipart.Writer, fields map[string]string, file
172
177
 
173
178
  f, err := os.Open(file.Path)
174
179
  if err != nil {
175
- return fmt.Errorf("open upload file: %w", err)
180
+ return fmt.Errorf("打开上传文件失败: %w", err)
176
181
  }
177
182
  defer f.Close()
178
183
 
@@ -184,7 +189,7 @@ func writeMultipartBody(writer *multipart.Writer, fields map[string]string, file
184
189
  return err
185
190
  }
186
191
  if _, err := io.Copy(part, f); err != nil {
187
- return fmt.Errorf("write upload file: %w", err)
192
+ return fmt.Errorf("写入上传文件失败: %w", err)
188
193
  }
189
194
  return nil
190
195
  }
@@ -195,38 +200,41 @@ func escapeQuotes(s string) string {
195
200
  return quotesReplacer.Replace(s)
196
201
  }
197
202
 
198
- func (c *httpClient) injectHeaders(req *http.Request) {
203
+ func (c *httpClient) injectHeaders(req *http.Request, headers map[string]string) {
199
204
  for k, values := range c.headers {
200
205
  for _, v := range values {
201
206
  req.Header.Add(k, v)
202
207
  }
203
208
  }
204
209
  req.Header.Set("User-Agent", "Pippit-CLI/1.0")
210
+ for k, v := range headers {
211
+ req.Header.Set(k, v)
212
+ }
205
213
  }
206
214
 
207
215
  func (c *httpClient) do(req *http.Request, out any) error {
208
216
  resp, err := c.httpClient.Do(req)
209
217
  if err != nil {
210
- return fmt.Errorf("%s %s failed: %w", req.Method, req.URL.String(), err)
218
+ return fmt.Errorf("%s %s 请求失败: %w", req.Method, req.URL.String(), err)
211
219
  }
212
220
  defer resp.Body.Close()
213
221
 
214
222
  data, err := io.ReadAll(resp.Body)
215
223
  if err != nil {
216
- return fmt.Errorf("read response body: %w", err)
224
+ return fmt.Errorf("读取响应体失败: %w", err)
217
225
  }
218
226
  if resp.StatusCode >= 400 {
219
227
  msg := strings.TrimSpace(string(data))
220
228
  if msg == "" {
221
229
  msg = http.StatusText(resp.StatusCode)
222
230
  }
223
- return fmt.Errorf("%s %s returned HTTP %d: %s", req.Method, req.URL.String(), resp.StatusCode, msg)
231
+ return fmt.Errorf("%s %s 返回 HTTP %d: %s", req.Method, req.URL.String(), resp.StatusCode, msg)
224
232
  }
225
233
  if out == nil || len(data) == 0 {
226
234
  return nil
227
235
  }
228
236
  if err := sonic.Unmarshal(data, out); err != nil {
229
- return fmt.Errorf("decode response body: %w", err)
237
+ return fmt.Errorf("解析响应体失败: %w", err)
230
238
  }
231
239
  return nil
232
240
  }
@@ -236,7 +244,7 @@ func (c *httpClient) resolveURL(path string, query map[string]string) (string, e
236
244
  return appendQuery(path, query)
237
245
  }
238
246
  if c.baseURL == "" {
239
- return "", fmt.Errorf("base URL is required for relative path %q", path)
247
+ return "", fmt.Errorf("相对路径 %q 需要配置 base URL", path)
240
248
  }
241
249
  if !strings.HasPrefix(path, "/") {
242
250
  path = "/" + path
@@ -247,7 +255,7 @@ func (c *httpClient) resolveURL(path string, query map[string]string) (string, e
247
255
  func appendQuery(raw string, query map[string]string) (string, error) {
248
256
  u, err := url.Parse(raw)
249
257
  if err != nil {
250
- return "", fmt.Errorf("parse URL: %w", err)
258
+ return "", fmt.Errorf("解析 URL 失败: %w", err)
251
259
  }
252
260
  values := u.Query()
253
261
  for k, v := range query {
@@ -58,20 +58,26 @@ type downloadTaskResult struct {
58
58
 
59
59
  func DownloadResult(ctx context.Context, opts DownloadResultOptions, runner *Runner) (*DownloadResultResponse, error) {
60
60
  if err := ctx.Err(); err != nil {
61
- return nil, err
61
+ if errors.Is(err, context.Canceled) {
62
+ return nil, fmt.Errorf("下载已取消")
63
+ }
64
+ if errors.Is(err, context.DeadlineExceeded) {
65
+ return nil, fmt.Errorf("下载超时")
66
+ }
67
+ return nil, fmt.Errorf("下载上下文异常: %w", err)
62
68
  }
63
69
  rawURL := strings.TrimSpace(opts.URL)
64
70
  if rawURL == "" {
65
- return nil, fmt.Errorf("download url is required")
71
+ return nil, fmt.Errorf("下载 URL 不能为空")
66
72
  }
67
73
 
68
74
  outputPath := strings.TrimSpace(opts.OutputPath)
69
75
  if outputPath == "" {
70
- return nil, fmt.Errorf("output_path is required")
76
+ return nil, fmt.Errorf("输出路径不能为空")
71
77
  }
72
78
  if info, err := os.Stat(outputPath); err == nil {
73
79
  if info.IsDir() {
74
- return nil, fmt.Errorf("output path %q is a directory", outputPath)
80
+ return nil, fmt.Errorf("输出路径 %q 是目录,请指定文件", outputPath)
75
81
  }
76
82
  if shouldSkipExistingFile(info, opts.UpdatedAt) {
77
83
  return &DownloadResultResponse{
@@ -80,19 +86,19 @@ func DownloadResult(ctx context.Context, opts DownloadResultOptions, runner *Run
80
86
  }, nil
81
87
  }
82
88
  } else if !os.IsNotExist(err) {
83
- return nil, fmt.Errorf("stat output path: %w", err)
89
+ return nil, fmt.Errorf("获取输出路径信息失败: %w", err)
84
90
  }
85
91
  outputDir := filepath.Dir(outputPath)
86
92
  if err := os.MkdirAll(outputDir, 0o755); err != nil {
87
- return nil, fmt.Errorf("create output dir: %w", err)
93
+ return nil, fmt.Errorf("创建输出目录失败: %w", err)
88
94
  }
89
95
 
90
96
  parsed, err := url.Parse(rawURL)
91
97
  if err != nil {
92
- return nil, fmt.Errorf("invalid url %q: %w", rawURL, err)
98
+ return nil, fmt.Errorf("URL %q 不合法: %w", rawURL, err)
93
99
  }
94
100
  if parsed.Scheme != "http" && parsed.Scheme != "https" {
95
- return nil, fmt.Errorf("invalid url scheme %q in %q, only http and https are allowed", parsed.Scheme, rawURL)
101
+ return nil, fmt.Errorf("URL %q 的协议 %q 不支持,仅支持 http https", rawURL, parsed.Scheme)
96
102
  }
97
103
  tasks := []downloadTask{
98
104
  {
@@ -166,7 +172,7 @@ func DownloadResult(ctx context.Context, opts DownloadResultOptions, runner *Run
166
172
  Errors: errorList,
167
173
  }
168
174
  if len(downloaded) == 0 && len(errorList) > 0 {
169
- return result, fmt.Errorf("all %d download(s) failed", len(errorList))
175
+ return result, fmt.Errorf("全部 %d 个下载任务失败", len(errorList))
170
176
  }
171
177
  return result, nil
172
178
  }
@@ -200,7 +206,7 @@ func downloadFileWithRetry(ctx context.Context, client Client, rawURL string, ta
200
206
  return err
201
207
  }
202
208
  }
203
- return fmt.Errorf("failed after %d retries: %w", maxDownloadRetries, lastErr)
209
+ return fmt.Errorf("重试 %d 次后仍失败: %w", maxDownloadRetries, lastErr)
204
210
  }
205
211
 
206
212
  func isRetryableError(err error) bool {
@@ -230,18 +236,18 @@ func downloadFile(ctx context.Context, client Client, rawURL string, targetPath
230
236
  }()
231
237
  out, err := os.Create(tmpPath)
232
238
  if err != nil {
233
- return fmt.Errorf("create temp file: %w", err)
239
+ return fmt.Errorf("创建临时文件失败: %w", err)
234
240
  }
235
241
  _, copyErr := io.Copy(out, resp.Body)
236
242
  closeErr := out.Close()
237
243
  if copyErr != nil {
238
- return fmt.Errorf("write temp file: %w", copyErr)
244
+ return fmt.Errorf("写入临时文件失败: %w", copyErr)
239
245
  }
240
246
  if closeErr != nil {
241
- return fmt.Errorf("close temp file: %w", closeErr)
247
+ return fmt.Errorf("关闭临时文件失败: %w", closeErr)
242
248
  }
243
249
  if err := os.Rename(tmpPath, targetPath); err != nil {
244
- return fmt.Errorf("replace target file: %w", err)
250
+ return fmt.Errorf("替换目标文件失败: %w", err)
245
251
  }
246
252
  tempActive = false
247
253
  if updatedAt > 0 {