@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.
- package/README.md +33 -3
- package/checksums.txt +6 -6
- package/cmd/download_result.go +1 -1
- package/cmd/generate_video/generate_video.go +48 -0
- package/cmd/generate_video/query_result.go +58 -0
- package/cmd/generate_video_test.go +468 -0
- package/cmd/get_thread.go +1 -0
- package/cmd/list_thread_file.go +1 -1
- package/cmd/root.go +27 -2
- package/cmd/short_drama/short_drama.go +52 -9
- package/cmd/short_drama_test.go +19 -19
- package/cmd/update/update.go +8 -8
- package/internal/common/client.go +29 -21
- package/internal/common/download_results.go +20 -14
- package/internal/common/error_log.go +62 -1
- package/internal/common/error_log_test.go +30 -0
- package/internal/common/get_thread.go +28 -14
- package/internal/common/get_thread_test.go +74 -0
- package/{cmd → internal/common}/json.go +3 -2
- package/internal/common/list_thread_file.go +6 -6
- package/internal/common/submit_run.go +29 -0
- package/internal/common/upload_file.go +23 -19
- package/internal/config/config.go +1 -1
- package/internal/generate_video/generate_video.go +199 -0
- package/internal/generate_video/query_result.go +291 -0
- package/internal/generate_video/query_result_test.go +31 -0
- package/internal/short_drama/submit_run.go +8 -28
- package/package.json +1 -1
- package/skills/short-drama/SKILL.md +8 -8
- package/skills/xyq-nest-skill/scripts/download_results.py +3 -1
- package/skills/xyq-nest-skill/scripts/upload_file.py +2 -2
- package/skills/xyq-nest-skill/scripts/xyq_common.py +4 -3
- package/cmd/upload_file.go +0 -50
package/cmd/root.go
CHANGED
|
@@ -1,10 +1,13 @@
|
|
|
1
1
|
package cmd
|
|
2
2
|
|
|
3
3
|
import (
|
|
4
|
+
"fmt"
|
|
4
5
|
"io"
|
|
5
6
|
"os"
|
|
7
|
+
"strings"
|
|
6
8
|
|
|
7
9
|
// authcmd "github.com/Pippit-dev/pippit-cli/cmd/auth"
|
|
10
|
+
"github.com/Pippit-dev/pippit-cli/cmd/generate_video"
|
|
8
11
|
"github.com/Pippit-dev/pippit-cli/cmd/short_drama"
|
|
9
12
|
updatecmd "github.com/Pippit-dev/pippit-cli/cmd/update"
|
|
10
13
|
"github.com/Pippit-dev/pippit-cli/internal/common"
|
|
@@ -29,7 +32,7 @@ func newRootCommand(stdout, stderr io.Writer, runner *common.Runner) *cobra.Comm
|
|
|
29
32
|
root := &cobra.Command{
|
|
30
33
|
Use: "pippit-tool-cli",
|
|
31
34
|
Short: "Pippit CLI",
|
|
32
|
-
Long: "Pippit CLI submits short-drama workflows, downloads generated assets, and updates the installed CLI package.",
|
|
35
|
+
Long: "Pippit CLI generates videos, submits short-drama workflows, downloads generated assets, and updates the installed CLI package.",
|
|
33
36
|
Version: version.Current(),
|
|
34
37
|
SilenceUsage: true,
|
|
35
38
|
SilenceErrors: true,
|
|
@@ -42,8 +45,30 @@ func newRootCommand(stdout, stderr io.Writer, runner *common.Runner) *cobra.Comm
|
|
|
42
45
|
root.AddCommand(newDownloadResultCommand(stdout, stderr, runner))
|
|
43
46
|
root.AddCommand(newGetThreadCommand(stdout, stderr, runner))
|
|
44
47
|
root.AddCommand(newListThreadFileCommand(stdout, stderr, runner))
|
|
45
|
-
root.AddCommand(
|
|
48
|
+
root.AddCommand(generate_video.NewCommand(stdout, stderr, runner))
|
|
49
|
+
root.AddCommand(generate_video.NewQueryResultCommand(stdout, stderr, runner))
|
|
46
50
|
root.AddCommand(short_drama.NewCommand(stdout, stderr, runner))
|
|
47
51
|
root.AddCommand(updatecmd.NewCommand(stdout, stderr))
|
|
52
|
+
localizeFlagErrors(root)
|
|
48
53
|
return root
|
|
49
54
|
}
|
|
55
|
+
|
|
56
|
+
func localizeFlagErrors(cmd *cobra.Command) {
|
|
57
|
+
cmd.SetFlagErrorFunc(func(_ *cobra.Command, err error) error {
|
|
58
|
+
return localizeFlagError(err)
|
|
59
|
+
})
|
|
60
|
+
for _, child := range cmd.Commands() {
|
|
61
|
+
localizeFlagErrors(child)
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
func localizeFlagError(err error) error {
|
|
66
|
+
msg := err.Error()
|
|
67
|
+
if flag, ok := strings.CutPrefix(msg, "unknown flag: "); ok {
|
|
68
|
+
return fmt.Errorf("未知参数: %s", flag)
|
|
69
|
+
}
|
|
70
|
+
if flag, ok := strings.CutPrefix(msg, "flag needs an argument: "); ok {
|
|
71
|
+
return fmt.Errorf("参数 %s 缺少取值", flag)
|
|
72
|
+
}
|
|
73
|
+
return fmt.Errorf("参数解析失败: %s", msg)
|
|
74
|
+
}
|
|
@@ -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
|
|
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
|
|
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
|
|
78
|
-
|
|
79
|
-
if
|
|
80
|
-
return
|
|
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
|
}
|
package/cmd/short_drama_test.go
CHANGED
|
@@ -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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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(), "
|
|
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(), "
|
|
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(), "
|
|
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(), "
|
|
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-
|
|
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-
|
|
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") {
|
package/cmd/update/update.go
CHANGED
|
@@ -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("
|
|
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("
|
|
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("
|
|
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("
|
|
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
|
|
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
|
|
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]
|
|
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("
|
|
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("
|
|
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("
|
|
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("
|
|
89
|
+
return fmt.Errorf("授权请求缺少认证器")
|
|
85
90
|
}
|
|
86
91
|
if err := c.authorizer.Inject(ctx, req); err != nil {
|
|
87
|
-
return fmt.Errorf("
|
|
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
|
|
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
|
|
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
|
|
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("
|
|
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("
|
|
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("
|
|
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("
|
|
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("
|
|
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
|
|
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("
|
|
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
|
|
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("
|
|
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("
|
|
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("
|
|
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
|
-
|
|
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("
|
|
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("
|
|
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("
|
|
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("
|
|
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("
|
|
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("
|
|
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("
|
|
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("
|
|
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("
|
|
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("
|
|
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("
|
|
244
|
+
return fmt.Errorf("写入临时文件失败: %w", copyErr)
|
|
239
245
|
}
|
|
240
246
|
if closeErr != nil {
|
|
241
|
-
return fmt.Errorf("
|
|
247
|
+
return fmt.Errorf("关闭临时文件失败: %w", closeErr)
|
|
242
248
|
}
|
|
243
249
|
if err := os.Rename(tmpPath, targetPath); err != nil {
|
|
244
|
-
return fmt.Errorf("
|
|
250
|
+
return fmt.Errorf("替换目标文件失败: %w", err)
|
|
245
251
|
}
|
|
246
252
|
tempActive = false
|
|
247
253
|
if updatedAt > 0 {
|