@pippit-dev/cli 1.0.0 → 1.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -196,15 +196,17 @@ npx @pippit-dev/cli@latest install
196
196
  export XYQ_ACCESS_KEY="<access-key>"
197
197
  pippit-tool-cli --version
198
198
  pippit-tool-cli short-drama +submit-run --message "写一个赛博朋克短剧开头"
199
- pippit-tool-cli short-drama +upload-file --path ./reference.doc
200
- pippit-tool-cli short-drama +get-thread --thread-id thread_123 --run-id run_456
201
- pippit-tool-cli short-drama +download-result --output-path ./thread_123/results/result.mp4 --url URL --updated-at 1779716734
199
+ pippit-tool-cli upload-file --path ./reference.doc
200
+ pippit-tool-cli get-thread --thread-id thread_123 --run-id run_456
201
+ pippit-tool-cli list-thread-file --thread-id thread_123 --page-num 1 --page-size 200
202
+ pippit-tool-cli download-result --output-path ./thread_123/results/result.mp4 --url URL --updated-at 1779716734
202
203
  ```
203
204
 
204
205
  `+submit-run`: 输出 `thread_id`、`run_id` 和 `web_thread_link`;其中 `--message` 为必填参数。
205
- `+get-thread`: 请求中带 `version=v2`,并输出 `readable_text`。
206
- `+upload-file`: 输出返回的 `asset_id`。 当前仅支持 `.doc`、`.docx` 和 `.txt` 文件。
207
- `+download-result`: 会把结果 URL 下载到 `--output-path` 指定的文件路径;传入 `--updated-at` 后,如果本地文件早于该时间戳会覆盖更新,否则跳过。
206
+ `get-thread`: 请求中带 `version=v2`,并输出 `readable_text`。
207
+ `list-thread-file`: 输出会话文件列表、分页提示和可直接传给下载命令的 `file_path`。
208
+ `upload-file`: 输出返回的 `asset_id`。 当前仅支持 `.doc`、`.docx` `.txt` 文件。
209
+ `download-result`: 会把结果 URL 下载到 `--output-path` 指定的文件路径;传入 `--updated-at` 后,如果本地文件早于该时间戳会覆盖更新,否则跳过。
208
210
 
209
211
  短剧命令的错误日志会追加写入本地每日日志文件:`~/.pippit_tool_cli/logs/yyyy-mm-dd.log`。日志路径会基于当前用户主目录和系统路径分隔符生成,因此可在 macOS、Linux 和 Windows 上使用。
210
212
 
@@ -214,4 +216,4 @@ pippit-tool-cli short-drama +download-result --output-path ./thread_123/results/
214
216
 
215
217
  ## 鉴权
216
218
 
217
- `short-drama +submit-run`、`short-drama +get-thread`、`short-drama +upload-file` 以及 `xyq-skill` Python 脚本都使用 `Authorization: Bearer <XYQ_ACCESS_KEY>` 鉴权。OAuth 命令代码仍保留在仓库中,但短剧运行时请求不使用 OAuth。
219
+ `short-drama +submit-run`、`get-thread`、`list-thread-file`、`upload-file` 以及 `xyq-skill` Python 脚本都使用 `Authorization: Bearer <XYQ_ACCESS_KEY>` 鉴权。OAuth 命令代码仍保留在仓库中,但短剧运行时请求不使用 OAuth。
package/checksums.txt CHANGED
@@ -1,6 +1,6 @@
1
- 91020e43a8e12d7e491b43200a4c1df4dd88a41683e5fe531b3fe038f6573e1a pippit-tool-cli-1.0.0-darwin-amd64.tar.gz
2
- a0eea79da53e4f6877aa27c731503c7434d5cb72ab0aa62cd602ac5da0e779f0 pippit-tool-cli-1.0.0-darwin-arm64.tar.gz
3
- babc1965565d1f2a458990b7bfa8f4b6b93c27ce74afc85d0099e82a14a332e0 pippit-tool-cli-1.0.0-linux-amd64.tar.gz
4
- 0c202f681c1c85c1b4ddd0e5980eb6ef35fa36bc3d6bd4bc840406e5c2661fa8 pippit-tool-cli-1.0.0-linux-arm64.tar.gz
5
- 3d6dae6812bd6fc95a2b6e54b8ef8bc24630d20cfc37417f3f84e787d81de11f pippit-tool-cli-1.0.0-windows-amd64.zip
6
- 1efdeaacda1dc09008ff0ed0582740ba989fdb766c26edec1ee667202bd565e0 pippit-tool-cli-1.0.0-windows-arm64.zip
1
+ 95c7bf8bb745dc6e390a8e71ebf9c464769b29f819e3dd9d77d3539453d320b4 pippit-tool-cli-1.0.1-darwin-amd64.tar.gz
2
+ 2163023d193b8886518d9b0481ae6306ddfde01de1f6fb61bbbd40a140b75b67 pippit-tool-cli-1.0.1-darwin-arm64.tar.gz
3
+ 15962cc45394ee0b204efb88753929b0842c8ad8d74333fe84ba91fe3f3e1596 pippit-tool-cli-1.0.1-linux-amd64.tar.gz
4
+ dbad696c1c212c6d14ef3e9788597d1751386ab7685111832b3ec8b82964bd57 pippit-tool-cli-1.0.1-linux-arm64.tar.gz
5
+ 8c298e5200d83d72ddb6c044d63183aa0104ef2b62fb1b0492d3652bfd103f8a pippit-tool-cli-1.0.1-windows-amd64.zip
6
+ eebeac5fccccbf63e4949efbf92ae2e3518e34b0758a1379cd0f4cc7328e5997 pippit-tool-cli-1.0.1-windows-arm64.zip
@@ -0,0 +1,57 @@
1
+ package cmd
2
+
3
+ import (
4
+ "fmt"
5
+ "io"
6
+ "strconv"
7
+ "strings"
8
+
9
+ "github.com/Pippit-dev/pippit-cli/internal/common"
10
+ "github.com/spf13/cobra"
11
+ )
12
+
13
+ func newDownloadResultCommand(stdout, stderr io.Writer, runner *common.Runner) *cobra.Command {
14
+ var opts common.DownloadResultOptions
15
+
16
+ cmd := &cobra.Command{
17
+ Use: "download-result",
18
+ Short: "Download a generated result URL",
19
+ Args: cobra.NoArgs,
20
+ RunE: withErrorLog("download-result", func() map[string]string {
21
+ fields := map[string]string{
22
+ "output_path": opts.OutputPath,
23
+ "has_url": strconv.FormatBool(strings.TrimSpace(opts.URL) != ""),
24
+ "workers": strconv.Itoa(opts.Workers),
25
+ }
26
+ if opts.UpdatedAt > 0 {
27
+ fields["updated_at"] = strconv.FormatInt(opts.UpdatedAt, 10)
28
+ }
29
+ return fields
30
+ }, func(cmd *cobra.Command, _ []string) error {
31
+ opts.OutputPath = strings.TrimSpace(opts.OutputPath)
32
+ if opts.OutputPath == "" {
33
+ return fmt.Errorf("--output-path is required")
34
+ }
35
+ opts.URL = strings.TrimSpace(opts.URL)
36
+ if opts.URL == "" {
37
+ return fmt.Errorf("--url is required")
38
+ }
39
+ if opts.Workers <= 0 {
40
+ return fmt.Errorf("--workers must be greater than 0")
41
+ }
42
+
43
+ result, err := common.DownloadResult(cmd.Context(), opts, runner)
44
+ if err != nil {
45
+ return err
46
+ }
47
+ return writeJSON(stdout, result)
48
+ }),
49
+ }
50
+ cmd.SetOut(stdout)
51
+ cmd.SetErr(stderr)
52
+ cmd.Flags().StringVar(&opts.URL, "url", "", "URL to download")
53
+ cmd.Flags().StringVar(&opts.OutputPath, "output-path", "", "local output file path")
54
+ cmd.Flags().Int64Var(&opts.UpdatedAt, "updated-at", 0, "remote file update time as a Unix timestamp")
55
+ cmd.Flags().IntVar(&opts.Workers, "workers", 5, "parallel download workers")
56
+ return cmd
57
+ }
@@ -0,0 +1,58 @@
1
+ package cmd
2
+
3
+ import (
4
+ "fmt"
5
+ "io"
6
+ "strings"
7
+
8
+ "github.com/Pippit-dev/pippit-cli/internal/common"
9
+ "github.com/spf13/cobra"
10
+ )
11
+
12
+ func newGetThreadCommand(stdout, stderr io.Writer, runner *common.Runner) *cobra.Command {
13
+ var opts common.GetThreadOptions
14
+
15
+ cmd := &cobra.Command{
16
+ Use: "get-thread",
17
+ Short: "Get a thread detail",
18
+ Args: cobra.NoArgs,
19
+ RunE: withErrorLog("get-thread", func() map[string]string {
20
+ return map[string]string{
21
+ "thread_id": opts.ThreadID,
22
+ "run_id": opts.RunID,
23
+ }
24
+ }, func(cmd *cobra.Command, _ []string) error {
25
+ opts.ThreadID = strings.TrimSpace(opts.ThreadID)
26
+ if opts.ThreadID == "" {
27
+ return fmt.Errorf("--thread-id is required")
28
+ }
29
+ opts.RunID = strings.TrimSpace(opts.RunID)
30
+
31
+ result, err := common.GetThread(cmd.Context(), &opts, runner)
32
+ if err != nil {
33
+ return err
34
+ }
35
+ _, err = fmt.Fprintln(stdout, result.ReadableText)
36
+ return err
37
+ }),
38
+ }
39
+ cmd.SetOut(stdout)
40
+ cmd.SetErr(stderr)
41
+ cmd.Flags().StringVar(&opts.ThreadID, "thread-id", "", "thread ID to fetch")
42
+ cmd.Flags().StringVar(&opts.RunID, "run-id", "", "run ID to fetch")
43
+ return cmd
44
+ }
45
+
46
+ func withErrorLog(command string, fields func() map[string]string, run func(*cobra.Command, []string) error) func(*cobra.Command, []string) error {
47
+ return func(cmd *cobra.Command, args []string) error {
48
+ err := run(cmd, args)
49
+ if err != nil {
50
+ logFields := map[string]string(nil)
51
+ if fields != nil {
52
+ logFields = fields()
53
+ }
54
+ _ = common.AppendDailyErrorLog(command, err, logFields)
55
+ }
56
+ return err
57
+ }
58
+ }
package/cmd/json.go ADDED
@@ -0,0 +1,17 @@
1
+ package cmd
2
+
3
+ import (
4
+ "fmt"
5
+ "io"
6
+
7
+ "github.com/bytedance/sonic"
8
+ )
9
+
10
+ func writeJSON(w io.Writer, v any) error {
11
+ data, err := sonic.Marshal(v)
12
+ if err != nil {
13
+ return err
14
+ }
15
+ _, err = fmt.Fprintln(w, string(data))
16
+ return err
17
+ }
@@ -0,0 +1,51 @@
1
+ package cmd
2
+
3
+ import (
4
+ "fmt"
5
+ "io"
6
+ "strconv"
7
+ "strings"
8
+
9
+ "github.com/Pippit-dev/pippit-cli/internal/common"
10
+ "github.com/spf13/cobra"
11
+ )
12
+
13
+ func newListThreadFileCommand(stdout, stderr io.Writer, runner *common.Runner) *cobra.Command {
14
+ var opts common.ListThreadFileOptions
15
+
16
+ cmd := &cobra.Command{
17
+ Use: "list-thread-file",
18
+ Short: "List files in a thread",
19
+ Args: cobra.NoArgs,
20
+ RunE: withErrorLog("list-thread-file", func() map[string]string {
21
+ return map[string]string{
22
+ "thread_id": opts.ThreadID,
23
+ "page_num": strconv.Itoa(opts.PageNum),
24
+ "page_size": strconv.Itoa(opts.PageSize),
25
+ }
26
+ }, func(cmd *cobra.Command, _ []string) error {
27
+ opts.ThreadID = strings.TrimSpace(opts.ThreadID)
28
+ if opts.ThreadID == "" {
29
+ return fmt.Errorf("--thread-id is required")
30
+ }
31
+ if opts.PageSize <= 0 || opts.PageSize > common.MaxListThreadFilePageSize {
32
+ return fmt.Errorf("--page-size must be between 1 and %d", common.MaxListThreadFilePageSize)
33
+ }
34
+ if opts.PageNum <= 0 {
35
+ return fmt.Errorf("--page-num must be greater than 0")
36
+ }
37
+
38
+ result, err := common.ListThreadFile(cmd.Context(), &opts, runner)
39
+ if err != nil {
40
+ return err
41
+ }
42
+ return writeJSON(stdout, result)
43
+ }),
44
+ }
45
+ cmd.SetOut(stdout)
46
+ cmd.SetErr(stderr)
47
+ cmd.Flags().StringVar(&opts.ThreadID, "thread-id", "", "thread ID to list files for")
48
+ cmd.Flags().IntVar(&opts.PageNum, "page-num", 1, "page number (1-based)")
49
+ cmd.Flags().IntVar(&opts.PageSize, "page-size", common.MaxListThreadFilePageSize, fmt.Sprintf("number of files per page (between 1 and %d)", common.MaxListThreadFilePageSize))
50
+ return cmd
51
+ }
package/cmd/root.go CHANGED
@@ -39,6 +39,10 @@ func newRootCommand(stdout, stderr io.Writer, runner *common.Runner) *cobra.Comm
39
39
  root.SetOut(stdout)
40
40
  root.SetErr(stderr)
41
41
  // root.AddCommand(authcmd.NewCommand(stdout, stderr, runner)) // temporarily disabled; auth is via access key injection
42
+ root.AddCommand(newDownloadResultCommand(stdout, stderr, runner))
43
+ root.AddCommand(newGetThreadCommand(stdout, stderr, runner))
44
+ root.AddCommand(newListThreadFileCommand(stdout, stderr, runner))
45
+ root.AddCommand(newUploadFileCommand(stdout, stderr, runner))
42
46
  root.AddCommand(short_drama.NewCommand(stdout, stderr, runner))
43
47
  root.AddCommand(updatecmd.NewCommand(stdout, stderr))
44
48
  return root
@@ -3,7 +3,6 @@ package short_drama
3
3
  import (
4
4
  "fmt"
5
5
  "io"
6
- "path/filepath"
7
6
  "strconv"
8
7
  "strings"
9
8
 
@@ -23,10 +22,6 @@ func NewCommand(stdout, stderr io.Writer, runner *common.Runner) *cobra.Command
23
22
  cmd.SetOut(stdout)
24
23
  cmd.SetErr(stderr)
25
24
  cmd.AddCommand(newShortDramaSubmitRunCommand(stdout, stderr, runner))
26
- cmd.AddCommand(newShortDramaUploadFileCommand(stdout, stderr, runner))
27
- cmd.AddCommand(newShortDramaDownloadResultCommand(stdout, stderr, runner))
28
- cmd.AddCommand(newShortDramaGetThreadCommand(stdout, stderr, runner))
29
- cmd.AddCommand(newShortDramaListThreadFileCommand(stdout, stderr, runner))
30
25
  return cmd
31
26
  }
32
27
 
@@ -65,157 +60,6 @@ func newShortDramaSubmitRunCommand(stdout, stderr io.Writer, runner *common.Runn
65
60
  return cmd
66
61
  }
67
62
 
68
- func newShortDramaUploadFileCommand(stdout, stderr io.Writer, runner *common.Runner) *cobra.Command {
69
- var opts common.UploadFileOptions
70
-
71
- cmd := &cobra.Command{
72
- Use: "+upload-file",
73
- Short: "Upload a file for the short drama scene",
74
- Args: cobra.NoArgs,
75
- RunE: withErrorLog("short-drama +upload-file", func() map[string]string {
76
- return map[string]string{
77
- "file_name": fileNameForLog(opts.Path),
78
- }
79
- }, func(cmd *cobra.Command, _ []string) error {
80
- opts.Path = strings.TrimSpace(opts.Path)
81
-
82
- if opts.Path == "" {
83
- return fmt.Errorf("--path is required")
84
- }
85
-
86
- result, err := common.UploadFile(cmd.Context(), opts, runner)
87
- if err != nil {
88
- return err
89
- }
90
- return writeJSON(stdout, result)
91
- }),
92
- }
93
- cmd.SetOut(stdout)
94
- cmd.SetErr(stderr)
95
- cmd.Flags().StringVar(&opts.Path, "path", "", "local file path to upload")
96
- return cmd
97
- }
98
-
99
- func newShortDramaDownloadResultCommand(stdout, stderr io.Writer, runner *common.Runner) *cobra.Command {
100
- var opts common.DownloadResultOptions
101
-
102
- cmd := &cobra.Command{
103
- Use: "+download-result",
104
- Short: "Download a generated result URL",
105
- Args: cobra.NoArgs,
106
- RunE: withErrorLog("short-drama +download-result", func() map[string]string {
107
- fields := map[string]string{
108
- "output_path": opts.OutputPath,
109
- "has_url": strconv.FormatBool(strings.TrimSpace(opts.URL) != ""),
110
- "workers": strconv.Itoa(opts.Workers),
111
- }
112
- if opts.UpdatedAt > 0 {
113
- fields["updated_at"] = strconv.FormatInt(opts.UpdatedAt, 10)
114
- }
115
- return fields
116
- }, func(cmd *cobra.Command, _ []string) error {
117
- opts.OutputPath = strings.TrimSpace(opts.OutputPath)
118
- if opts.OutputPath == "" {
119
- return fmt.Errorf("--output-path is required")
120
- }
121
- opts.URL = strings.TrimSpace(opts.URL)
122
- if opts.URL == "" {
123
- return fmt.Errorf("--url is required")
124
- }
125
- if opts.Workers <= 0 {
126
- return fmt.Errorf("--workers must be greater than 0")
127
- }
128
-
129
- result, err := common.DownloadResult(cmd.Context(), opts, runner)
130
- if err != nil {
131
- return err
132
- }
133
- return writeJSON(stdout, result)
134
- }),
135
- }
136
- cmd.SetOut(stdout)
137
- cmd.SetErr(stderr)
138
- cmd.Flags().StringVar(&opts.URL, "url", "", "URL to download")
139
- cmd.Flags().StringVar(&opts.OutputPath, "output-path", "", "local output file path")
140
- cmd.Flags().Int64Var(&opts.UpdatedAt, "updated-at", 0, "remote file update time as a Unix timestamp")
141
- cmd.Flags().IntVar(&opts.Workers, "workers", 5, "parallel download workers")
142
- return cmd
143
- }
144
-
145
- func newShortDramaGetThreadCommand(stdout, stderr io.Writer, runner *common.Runner) *cobra.Command {
146
- var opts common.GetThreadOptions
147
-
148
- cmd := &cobra.Command{
149
- Use: "+get-thread",
150
- Short: "Get a short drama thread detail",
151
- Args: cobra.NoArgs,
152
- RunE: withErrorLog("short-drama +get-thread", func() map[string]string {
153
- return map[string]string{
154
- "thread_id": opts.ThreadID,
155
- "run_id": opts.RunID,
156
- }
157
- }, func(cmd *cobra.Command, _ []string) error {
158
- opts.ThreadID = strings.TrimSpace(opts.ThreadID)
159
- if opts.ThreadID == "" {
160
- return fmt.Errorf("--thread-id is required")
161
- }
162
- opts.RunID = strings.TrimSpace(opts.RunID)
163
-
164
- result, err := common.GetThread(cmd.Context(), &opts, runner)
165
- if err != nil {
166
- return err
167
- }
168
- _, err = fmt.Fprintln(stdout, result.ReadableText)
169
- return err
170
- }),
171
- }
172
- cmd.SetOut(stdout)
173
- cmd.SetErr(stderr)
174
- cmd.Flags().StringVar(&opts.ThreadID, "thread-id", "", "thread ID to fetch")
175
- cmd.Flags().StringVar(&opts.RunID, "run-id", "", "run ID to fetch")
176
- return cmd
177
- }
178
-
179
- func newShortDramaListThreadFileCommand(stdout, stderr io.Writer, runner *common.Runner) *cobra.Command {
180
- var opts common.ListThreadFileOptions
181
-
182
- cmd := &cobra.Command{
183
- Use: "+list-thread-file",
184
- Short: "List files in a short drama thread",
185
- Args: cobra.NoArgs,
186
- RunE: withErrorLog("short-drama +list-thread-file", func() map[string]string {
187
- return map[string]string{
188
- "thread_id": opts.ThreadID,
189
- "page_num": strconv.Itoa(opts.PageNum),
190
- "page_size": strconv.Itoa(opts.PageSize),
191
- }
192
- }, func(cmd *cobra.Command, _ []string) error {
193
- opts.ThreadID = strings.TrimSpace(opts.ThreadID)
194
- if opts.ThreadID == "" {
195
- return fmt.Errorf("--thread-id is required")
196
- }
197
- if opts.PageSize <= 0 || opts.PageSize > common.MaxListThreadFilePageSize {
198
- return fmt.Errorf("--page-size must be between 1 and %d", common.MaxListThreadFilePageSize)
199
- }
200
- if opts.PageNum <= 0 {
201
- return fmt.Errorf("--page-num must be greater than 0")
202
- }
203
-
204
- result, err := common.ListThreadFile(cmd.Context(), &opts, runner)
205
- if err != nil {
206
- return err
207
- }
208
- return writeJSON(stdout, result)
209
- }),
210
- }
211
- cmd.SetOut(stdout)
212
- cmd.SetErr(stderr)
213
- cmd.Flags().StringVar(&opts.ThreadID, "thread-id", "", "thread ID to list files for")
214
- cmd.Flags().IntVar(&opts.PageNum, "page-num", 1, "page number (1-based)")
215
- cmd.Flags().IntVar(&opts.PageSize, "page-size", common.MaxListThreadFilePageSize, fmt.Sprintf("number of files per page (between 1 and %d)", common.MaxListThreadFilePageSize))
216
- return cmd
217
- }
218
-
219
63
  func withErrorLog(command string, fields func() map[string]string, run func(*cobra.Command, []string) error) func(*cobra.Command, []string) error {
220
64
  return func(cmd *cobra.Command, args []string) error {
221
65
  err := run(cmd, args)
@@ -230,14 +74,6 @@ func withErrorLog(command string, fields func() map[string]string, run func(*cob
230
74
  }
231
75
  }
232
76
 
233
- func fileNameForLog(path string) string {
234
- path = strings.TrimSpace(path)
235
- if path == "" {
236
- return ""
237
- }
238
- return filepath.Base(path)
239
- }
240
-
241
77
  func writeJSON(w io.Writer, v any) error {
242
78
  data, err := sonic.Marshal(v)
243
79
  if err != nil {
@@ -125,7 +125,11 @@ func TestRootHelpListsSupportedCommands(t *testing.T) {
125
125
  got := stdout.String()
126
126
  for _, want := range []string{
127
127
  "Pippit CLI submits short-drama workflows",
128
+ "download-result",
129
+ "get-thread",
130
+ "list-thread-file",
128
131
  "short-drama",
132
+ "upload-file",
129
133
  "update",
130
134
  "--version",
131
135
  } {
@@ -140,6 +144,29 @@ func TestRootHelpListsSupportedCommands(t *testing.T) {
140
144
  }
141
145
  }
142
146
 
147
+ func TestShortDramaDoesNotIncludeCommonThreadCommands(t *testing.T) {
148
+ var stdout, stderr bytes.Buffer
149
+ root := NewRootCommand(&stdout, &stderr)
150
+ cmd, _, err := root.Find([]string{"short-drama"})
151
+ if err != nil {
152
+ t.Fatalf("Find(short-drama) error = %v", err)
153
+ }
154
+ if cmd.CommandPath() != "pippit-tool-cli short-drama" {
155
+ t.Fatalf("CommandPath() = %q, want short-drama command", cmd.CommandPath())
156
+ }
157
+ removedCommands := map[string]bool{
158
+ "+download-result": true,
159
+ "+get-thread": true,
160
+ "+list-thread-file": true,
161
+ "+upload-file": true,
162
+ }
163
+ for _, child := range cmd.Commands() {
164
+ if removedCommands[child.Name()] {
165
+ t.Fatalf("short-drama child %s = %#v, want nil", child.Name(), child)
166
+ }
167
+ }
168
+ }
169
+
143
170
  func TestVersionFlagPrintsVersion(t *testing.T) {
144
171
  var stdout, stderr bytes.Buffer
145
172
  root := NewRootCommand(&stdout, &stderr)
@@ -187,7 +214,7 @@ func TestShortDramaSubmitRunRequiresAccessKey(t *testing.T) {
187
214
  assertAccessKeyGuidance(t, err)
188
215
  }
189
216
 
190
- func TestShortDramaUploadFile(t *testing.T) {
217
+ func TestUploadFile(t *testing.T) {
191
218
  server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
192
219
  if r.Method != http.MethodPost {
193
220
  t.Fatalf("method = %s, want POST", r.Method)
@@ -238,7 +265,7 @@ func TestShortDramaUploadFile(t *testing.T) {
238
265
 
239
266
  var stdout, stderr bytes.Buffer
240
267
  root := newTestRootCommand(t, &stdout, &stderr, server.URL)
241
- root.SetArgs([]string{"short-drama", "+upload-file", "--path", path})
268
+ root.SetArgs([]string{"upload-file", "--path", path})
242
269
 
243
270
  if err := root.Execute(); err != nil {
244
271
  t.Fatalf("Execute() error = %v, stderr = %s", err, stderr.String())
@@ -250,10 +277,10 @@ func TestShortDramaUploadFile(t *testing.T) {
250
277
  }
251
278
  }
252
279
 
253
- func TestShortDramaUploadFileRequiresPath(t *testing.T) {
280
+ func TestUploadFileRequiresPath(t *testing.T) {
254
281
  var stdout, stderr bytes.Buffer
255
282
  root := NewRootCommand(&stdout, &stderr)
256
- root.SetArgs([]string{"short-drama", "+upload-file"})
283
+ root.SetArgs([]string{"upload-file"})
257
284
 
258
285
  err := root.Execute()
259
286
  if err == nil {
@@ -264,7 +291,7 @@ func TestShortDramaUploadFileRequiresPath(t *testing.T) {
264
291
  }
265
292
  }
266
293
 
267
- func TestShortDramaUploadFileRequiresAccessKey(t *testing.T) {
294
+ func TestUploadFileRequiresAccessKey(t *testing.T) {
268
295
  server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
269
296
  t.Fatal("server should not receive request without access key")
270
297
  }))
@@ -278,7 +305,7 @@ func TestShortDramaUploadFileRequiresAccessKey(t *testing.T) {
278
305
 
279
306
  var stdout, stderr bytes.Buffer
280
307
  root := newTestRootCommandWithAccessKey(t, &stdout, &stderr, server.URL, "")
281
- root.SetArgs([]string{"short-drama", "+upload-file", "--path", path})
308
+ root.SetArgs([]string{"upload-file", "--path", path})
282
309
 
283
310
  err := root.Execute()
284
311
  if err == nil {
@@ -287,7 +314,7 @@ func TestShortDramaUploadFileRequiresAccessKey(t *testing.T) {
287
314
  assertAccessKeyGuidance(t, err)
288
315
  }
289
316
 
290
- func TestShortDramaUploadFileRejectsUnsupportedFileType(t *testing.T) {
317
+ func TestUploadFileRejectsUnsupportedFileType(t *testing.T) {
291
318
  cwd := chdirTemp(t)
292
319
  path := filepath.Join(cwd, "story.png")
293
320
  if err := os.WriteFile(path, []byte("png-data"), 0o644); err != nil {
@@ -296,7 +323,7 @@ func TestShortDramaUploadFileRejectsUnsupportedFileType(t *testing.T) {
296
323
 
297
324
  var stdout, stderr bytes.Buffer
298
325
  root := NewRootCommand(&stdout, &stderr)
299
- root.SetArgs([]string{"short-drama", "+upload-file", "--path", path})
326
+ root.SetArgs([]string{"upload-file", "--path", path})
300
327
 
301
328
  err := root.Execute()
302
329
  if err == nil {
@@ -307,7 +334,7 @@ func TestShortDramaUploadFileRejectsUnsupportedFileType(t *testing.T) {
307
334
  }
308
335
  }
309
336
 
310
- func TestShortDramaDownloadResult(t *testing.T) {
337
+ func TestDownloadResult(t *testing.T) {
311
338
  server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
312
339
  if r.Header.Get("User-Agent") != "Pippit-CLI/1.0" {
313
340
  t.Fatalf("User-Agent = %q, want Pippit-CLI/1.0", r.Header.Get("User-Agent"))
@@ -326,7 +353,7 @@ func TestShortDramaDownloadResult(t *testing.T) {
326
353
  root := NewRootCommand(&stdout, &stderr)
327
354
  outputPath := filepath.Join("results", "cover.jpeg")
328
355
  root.SetArgs([]string{
329
- "short-drama", "+download-result",
356
+ "download-result",
330
357
  "--output-path", outputPath,
331
358
  "--workers", "2",
332
359
  "--url", server.URL + "/image?filename=ignored.jpeg",
@@ -358,7 +385,7 @@ func TestShortDramaDownloadResult(t *testing.T) {
358
385
  assertFileContent(t, wantFiles[0], "image-data")
359
386
  }
360
387
 
361
- func TestShortDramaDownloadResultSkipsExistingFile(t *testing.T) {
388
+ func TestDownloadResultSkipsExistingFile(t *testing.T) {
362
389
  serverCalled := false
363
390
  server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
364
391
  serverCalled = true
@@ -378,7 +405,7 @@ func TestShortDramaDownloadResultSkipsExistingFile(t *testing.T) {
378
405
  var stdout, stderr bytes.Buffer
379
406
  root := NewRootCommand(&stdout, &stderr)
380
407
  root.SetArgs([]string{
381
- "short-drama", "+download-result",
408
+ "download-result",
382
409
  "--output-path", outputPath,
383
410
  "--url", server.URL + "/image",
384
411
  })
@@ -404,7 +431,7 @@ func TestShortDramaDownloadResultSkipsExistingFile(t *testing.T) {
404
431
  assertFileContent(t, outputPath, "existing-data")
405
432
  }
406
433
 
407
- func TestShortDramaDownloadResultSkipsExistingFileWhenLocalIsFresh(t *testing.T) {
434
+ func TestDownloadResultSkipsExistingFileWhenLocalIsFresh(t *testing.T) {
408
435
  serverCalled := false
409
436
  server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
410
437
  serverCalled = true
@@ -429,7 +456,7 @@ func TestShortDramaDownloadResultSkipsExistingFileWhenLocalIsFresh(t *testing.T)
429
456
  var stdout, stderr bytes.Buffer
430
457
  root := NewRootCommand(&stdout, &stderr)
431
458
  root.SetArgs([]string{
432
- "short-drama", "+download-result",
459
+ "download-result",
433
460
  "--output-path", outputPath,
434
461
  "--updated-at", "1779716734",
435
462
  "--url", server.URL + "/image",
@@ -450,7 +477,7 @@ func TestShortDramaDownloadResultSkipsExistingFileWhenLocalIsFresh(t *testing.T)
450
477
  assertFileContent(t, outputPath, "existing-data")
451
478
  }
452
479
 
453
- func TestShortDramaDownloadResultOverwritesStaleExistingFile(t *testing.T) {
480
+ func TestDownloadResultOverwritesStaleExistingFile(t *testing.T) {
454
481
  serverCalled := false
455
482
  server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
456
483
  serverCalled = true
@@ -478,7 +505,7 @@ func TestShortDramaDownloadResultOverwritesStaleExistingFile(t *testing.T) {
478
505
  var stdout, stderr bytes.Buffer
479
506
  root := NewRootCommand(&stdout, &stderr)
480
507
  root.SetArgs([]string{
481
- "short-drama", "+download-result",
508
+ "download-result",
482
509
  "--output-path", outputPath,
483
510
  "--updated-at", "1779716734",
484
511
  "--url", server.URL + "/image",
@@ -509,7 +536,7 @@ func TestShortDramaDownloadResultOverwritesStaleExistingFile(t *testing.T) {
509
536
  }
510
537
  }
511
538
 
512
- func TestShortDramaDownloadResultDownloadsMetaJSON(t *testing.T) {
539
+ func TestDownloadResultDownloadsMetaJSON(t *testing.T) {
513
540
  server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
514
541
  if r.URL.Path != "/meta.json" {
515
542
  t.Fatalf("path = %s, want meta.json", r.URL.Path)
@@ -524,7 +551,7 @@ func TestShortDramaDownloadResultDownloadsMetaJSON(t *testing.T) {
524
551
  var stdout, stderr bytes.Buffer
525
552
  root := NewRootCommand(&stdout, &stderr)
526
553
  root.SetArgs([]string{
527
- "short-drama", "+download-result",
554
+ "download-result",
528
555
  "--output-path", outputPath,
529
556
  "--url", server.URL + "/meta.json",
530
557
  })
@@ -547,10 +574,10 @@ func TestShortDramaDownloadResultDownloadsMetaJSON(t *testing.T) {
547
574
  assertFileContent(t, outputPath, `{"ok":true}`)
548
575
  }
549
576
 
550
- func TestShortDramaDownloadResultRequiresOutputPath(t *testing.T) {
577
+ func TestDownloadResultRequiresOutputPath(t *testing.T) {
551
578
  var stdout, stderr bytes.Buffer
552
579
  root := NewRootCommand(&stdout, &stderr)
553
- root.SetArgs([]string{"short-drama", "+download-result", "--url", "https://example.com/image.png"})
580
+ root.SetArgs([]string{"download-result", "--url", "https://example.com/image.png"})
554
581
 
555
582
  err := root.Execute()
556
583
  if err == nil {
@@ -561,11 +588,11 @@ func TestShortDramaDownloadResultRequiresOutputPath(t *testing.T) {
561
588
  }
562
589
  }
563
590
 
564
- func TestShortDramaDownloadResultRejectsOutputDirFlag(t *testing.T) {
591
+ func TestDownloadResultRejectsOutputDirFlag(t *testing.T) {
565
592
  var stdout, stderr bytes.Buffer
566
593
  root := NewRootCommand(&stdout, &stderr)
567
594
  root.SetArgs([]string{
568
- "short-drama", "+download-result",
595
+ "download-result",
569
596
  "--output-dir", filepath.Join("results", "image.png"),
570
597
  "--url", "https://example.com/image.png",
571
598
  })
@@ -579,10 +606,10 @@ func TestShortDramaDownloadResultRejectsOutputDirFlag(t *testing.T) {
579
606
  }
580
607
  }
581
608
 
582
- func TestShortDramaDownloadResultRequiresURL(t *testing.T) {
609
+ func TestDownloadResultRequiresURL(t *testing.T) {
583
610
  var stdout, stderr bytes.Buffer
584
611
  root := NewRootCommand(&stdout, &stderr)
585
- root.SetArgs([]string{"short-drama", "+download-result", "--output-path", filepath.Join("results", "image.png")})
612
+ root.SetArgs([]string{"download-result", "--output-path", filepath.Join("results", "image.png")})
586
613
 
587
614
  err := root.Execute()
588
615
  if err == nil {
@@ -593,10 +620,10 @@ func TestShortDramaDownloadResultRequiresURL(t *testing.T) {
593
620
  }
594
621
  }
595
622
 
596
- func TestShortDramaDownloadResultRejectsInvalidScheme(t *testing.T) {
623
+ func TestDownloadResultRejectsInvalidScheme(t *testing.T) {
597
624
  var stdout, stderr bytes.Buffer
598
625
  root := NewRootCommand(&stdout, &stderr)
599
- root.SetArgs([]string{"short-drama", "+download-result", "--output-path", filepath.Join("results", "image.png"), "--url", "file:///etc/passwd"})
626
+ root.SetArgs([]string{"download-result", "--output-path", filepath.Join("results", "image.png"), "--url", "file:///etc/passwd"})
600
627
 
601
628
  err := root.Execute()
602
629
  if err == nil {
@@ -607,7 +634,7 @@ func TestShortDramaDownloadResultRejectsInvalidScheme(t *testing.T) {
607
634
  }
608
635
  }
609
636
 
610
- func TestShortDramaDownloadResultAllFailed(t *testing.T) {
637
+ func TestDownloadResultAllFailed(t *testing.T) {
611
638
  server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
612
639
  w.WriteHeader(http.StatusNotFound)
613
640
  }))
@@ -618,7 +645,7 @@ func TestShortDramaDownloadResultAllFailed(t *testing.T) {
618
645
  var stdout, stderr bytes.Buffer
619
646
  root := NewRootCommand(&stdout, &stderr)
620
647
  root.SetArgs([]string{
621
- "short-drama", "+download-result",
648
+ "download-result",
622
649
  "--output-path", filepath.Join("results", "missing.png"),
623
650
  "--url", server.URL + "/notfound",
624
651
  })
@@ -643,7 +670,7 @@ func TestShortDramaDownloadResultAllFailed(t *testing.T) {
643
670
  }
644
671
  }
645
672
 
646
- func TestShortDramaDownloadResultOutputPath(t *testing.T) {
673
+ func TestDownloadResultOutputPath(t *testing.T) {
647
674
  server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
648
675
  _, _ = w.Write([]byte("image-data"))
649
676
  }))
@@ -655,7 +682,7 @@ func TestShortDramaDownloadResultOutputPath(t *testing.T) {
655
682
  root := NewRootCommand(&stdout, &stderr)
656
683
  outputPath := filepath.Join("custom", "nested", "cover.png")
657
684
  root.SetArgs([]string{
658
- "short-drama", "+download-result",
685
+ "download-result",
659
686
  "--output-path", outputPath,
660
687
  "--url", server.URL + "/image.png",
661
688
  })
@@ -671,7 +698,7 @@ func TestShortDramaDownloadResultOutputPath(t *testing.T) {
671
698
  assertFileContent(t, outputPath, "image-data")
672
699
  }
673
700
 
674
- func TestShortDramaGetThread(t *testing.T) {
701
+ func TestGetThread(t *testing.T) {
675
702
  server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
676
703
  if r.Method != http.MethodPost {
677
704
  t.Fatalf("method = %s, want POST", r.Method)
@@ -709,7 +736,7 @@ func TestShortDramaGetThread(t *testing.T) {
709
736
  var stdout, stderr bytes.Buffer
710
737
  root := newTestRootCommand(t, &stdout, &stderr, server.URL)
711
738
  root.SetArgs([]string{
712
- "short-drama", "+get-thread",
739
+ "get-thread",
713
740
  "--thread-id", "thread_123",
714
741
  "--run-id", "run_456",
715
742
  })
@@ -723,12 +750,12 @@ func TestShortDramaGetThread(t *testing.T) {
723
750
  }
724
751
  }
725
752
 
726
- func TestShortDramaGetThreadRequiresThreadID(t *testing.T) {
753
+ func TestGetThreadRequiresThreadID(t *testing.T) {
727
754
  clearDailyErrorLog(t)
728
755
 
729
756
  var stdout, stderr bytes.Buffer
730
757
  root := NewRootCommand(&stdout, &stderr)
731
- root.SetArgs([]string{"short-drama", "+get-thread"})
758
+ root.SetArgs([]string{"get-thread"})
732
759
 
733
760
  err := root.Execute()
734
761
  if err == nil {
@@ -742,7 +769,7 @@ func TestShortDramaGetThreadRequiresThreadID(t *testing.T) {
742
769
  if len(entries) != 1 {
743
770
  t.Fatalf("log entries = %d, want 1: %#v", len(entries), entries)
744
771
  }
745
- if entries[0]["command"] != "short-drama +get-thread" {
772
+ if entries[0]["command"] != "get-thread" {
746
773
  t.Fatalf("command = %v, want get-thread", entries[0]["command"])
747
774
  }
748
775
  if entries[0]["error"] != "--thread-id is required" {
@@ -750,7 +777,7 @@ func TestShortDramaGetThreadRequiresThreadID(t *testing.T) {
750
777
  }
751
778
  }
752
779
 
753
- func TestShortDramaGetThreadRequiresAccessKey(t *testing.T) {
780
+ func TestGetThreadRequiresAccessKey(t *testing.T) {
754
781
  server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
755
782
  t.Fatal("server should not receive request without access key")
756
783
  }))
@@ -758,7 +785,7 @@ func TestShortDramaGetThreadRequiresAccessKey(t *testing.T) {
758
785
 
759
786
  var stdout, stderr bytes.Buffer
760
787
  root := newTestRootCommandWithAccessKey(t, &stdout, &stderr, server.URL, "")
761
- root.SetArgs([]string{"short-drama", "+get-thread", "--thread-id", "thread_123"})
788
+ root.SetArgs([]string{"get-thread", "--thread-id", "thread_123"})
762
789
 
763
790
  err := root.Execute()
764
791
  if err == nil {
@@ -767,7 +794,7 @@ func TestShortDramaGetThreadRequiresAccessKey(t *testing.T) {
767
794
  assertAccessKeyGuidance(t, err)
768
795
  }
769
796
 
770
- func TestShortDramaListThreadFile(t *testing.T) {
797
+ func TestListThreadFile(t *testing.T) {
771
798
  server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
772
799
  if r.Method != http.MethodPost {
773
800
  t.Fatalf("method = %s, want POST", r.Method)
@@ -802,7 +829,7 @@ func TestShortDramaListThreadFile(t *testing.T) {
802
829
  var stdout, stderr bytes.Buffer
803
830
  root := newTestRootCommand(t, &stdout, &stderr, server.URL)
804
831
  root.SetArgs([]string{
805
- "short-drama", "+list-thread-file",
832
+ "list-thread-file",
806
833
  "--thread-id", "thread_123",
807
834
  "--page-num", "2",
808
835
  "--page-size", "10",
@@ -843,7 +870,7 @@ func TestShortDramaListThreadFile(t *testing.T) {
843
870
  }
844
871
  }
845
872
 
846
- func TestShortDramaListThreadFileMessageWhenPageFull(t *testing.T) {
873
+ func TestListThreadFileMessageWhenPageFull(t *testing.T) {
847
874
  server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
848
875
  if !strings.Contains(r.URL.Path, "list_thread_file") {
849
876
  t.Fatalf("path = %s, want list_thread_file path", r.URL.Path)
@@ -855,7 +882,7 @@ func TestShortDramaListThreadFileMessageWhenPageFull(t *testing.T) {
855
882
  var stdout, stderr bytes.Buffer
856
883
  root := newTestRootCommand(t, &stdout, &stderr, server.URL)
857
884
  root.SetArgs([]string{
858
- "short-drama", "+list-thread-file",
885
+ "list-thread-file",
859
886
  "--thread-id", "thread_123",
860
887
  "--page-num", "2",
861
888
  "--page-size", "200",
@@ -872,10 +899,10 @@ func TestShortDramaListThreadFileMessageWhenPageFull(t *testing.T) {
872
899
  }
873
900
  }
874
901
 
875
- func TestShortDramaListThreadFileRequiresThreadID(t *testing.T) {
902
+ func TestListThreadFileRequiresThreadID(t *testing.T) {
876
903
  var stdout, stderr bytes.Buffer
877
904
  root := NewRootCommand(&stdout, &stderr)
878
- root.SetArgs([]string{"short-drama", "+list-thread-file"})
905
+ root.SetArgs([]string{"list-thread-file"})
879
906
 
880
907
  err := root.Execute()
881
908
  if err == nil {
@@ -886,7 +913,7 @@ func TestShortDramaListThreadFileRequiresThreadID(t *testing.T) {
886
913
  }
887
914
  }
888
915
 
889
- func TestShortDramaListThreadFileRequiresAccessKey(t *testing.T) {
916
+ func TestListThreadFileRequiresAccessKey(t *testing.T) {
890
917
  server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
891
918
  t.Fatal("server should not receive request without access key")
892
919
  }))
@@ -894,7 +921,7 @@ func TestShortDramaListThreadFileRequiresAccessKey(t *testing.T) {
894
921
 
895
922
  var stdout, stderr bytes.Buffer
896
923
  root := newTestRootCommandWithAccessKey(t, &stdout, &stderr, server.URL, "")
897
- root.SetArgs([]string{"short-drama", "+list-thread-file", "--thread-id", "thread_123"})
924
+ root.SetArgs([]string{"list-thread-file", "--thread-id", "thread_123"})
898
925
 
899
926
  err := root.Execute()
900
927
  if err == nil {
@@ -903,11 +930,11 @@ func TestShortDramaListThreadFileRequiresAccessKey(t *testing.T) {
903
930
  assertAccessKeyGuidance(t, err)
904
931
  }
905
932
 
906
- func TestShortDramaListThreadFileRejectsPageSizeAboveMax(t *testing.T) {
933
+ func TestListThreadFileRejectsPageSizeAboveMax(t *testing.T) {
907
934
  var stdout, stderr bytes.Buffer
908
935
  root := NewRootCommand(&stdout, &stderr)
909
936
  root.SetArgs([]string{
910
- "short-drama", "+list-thread-file",
937
+ "list-thread-file",
911
938
  "--thread-id", "thread_123",
912
939
  "--page-size", "201",
913
940
  })
@@ -942,13 +969,13 @@ func assertAccessKeyGuidance(t *testing.T, err error) {
942
969
  t.Fatal("error = nil, want access key guidance")
943
970
  }
944
971
  msg := err.Error()
945
- if !strings.Contains(msg, "XYQ_ACCESS_KEY is required") {
972
+ if !strings.Contains(msg, "XYQ_ACCESS_KEY") {
946
973
  t.Fatalf("error = %q, want access key guidance", err)
947
974
  }
948
975
  if !strings.Contains(msg, "https://xyq.jianying.com/home?tab_name=home") {
949
976
  t.Fatalf("error = %q, want access key settings URL", err)
950
977
  }
951
- if !strings.Contains(msg, `export XYQ_ACCESS_KEY="<access-key>"`) {
978
+ if !strings.Contains(msg, `export XYQ_ACCESS_KEY="<your-access-key>"`) {
952
979
  t.Fatalf("error = %q, want setup command guidance", err)
953
980
  }
954
981
  }
@@ -0,0 +1,50 @@
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
+ }
@@ -32,7 +32,7 @@ type DownloadResultError struct {
32
32
  Error string `json:"error"`
33
33
  }
34
34
 
35
- // DownloadResultResponse is the JSON envelope printed by `pippit-tool-cli short-drama +download-result`.
35
+ // DownloadResultResponse is the JSON envelope printed by `pippit-tool-cli download-result`.
36
36
  type DownloadResultResponse struct {
37
37
  OutputPath string `json:"output_path"`
38
38
  Downloaded []string `json:"downloaded"`
@@ -16,7 +16,7 @@ func TestAppendDailyErrorLog(t *testing.T) {
16
16
  t.Setenv("HOME", home)
17
17
  t.Setenv("USERPROFILE", home)
18
18
 
19
- err := AppendDailyErrorLog("short-drama +get-thread", fmt.Errorf("request failed"), map[string]string{
19
+ err := AppendDailyErrorLog("get-thread", fmt.Errorf("request failed"), map[string]string{
20
20
  "thread_id": "thread_123",
21
21
  "access_key": "secret",
22
22
  "empty": "",
@@ -24,7 +24,7 @@ func TestAppendDailyErrorLog(t *testing.T) {
24
24
  if err != nil {
25
25
  t.Fatalf("AppendDailyErrorLog(): %v", err)
26
26
  }
27
- if err := AppendDailyErrorLog("short-drama +get-thread", fmt.Errorf("second failure"), nil); err != nil {
27
+ if err := AppendDailyErrorLog("get-thread", fmt.Errorf("second failure"), nil); err != nil {
28
28
  t.Fatalf("AppendDailyErrorLog() second call: %v", err)
29
29
  }
30
30
 
@@ -42,7 +42,7 @@ func TestAppendDailyErrorLog(t *testing.T) {
42
42
  if err := sonic.Unmarshal([]byte(lines[0]), &first); err != nil {
43
43
  t.Fatalf("decode first log line: %v\n%s", err, lines[0])
44
44
  }
45
- if first["command"] != "short-drama +get-thread" {
45
+ if first["command"] != "get-thread" {
46
46
  t.Fatalf("command = %v, want get-thread command", first["command"])
47
47
  }
48
48
  if first["error"] != "request failed" {
@@ -15,7 +15,7 @@ type GetThreadOptions struct {
15
15
  RunID string `json:"run_id,omitempty"`
16
16
  }
17
17
 
18
- // GetThreadResult is the parsed get_thread response used by `pippit-tool-cli short-drama +get-thread`.
18
+ // GetThreadResult is the parsed get_thread response used by `pippit-tool-cli get-thread`.
19
19
  type GetThreadResult struct {
20
20
  ReadableText string `json:"readable_text"`
21
21
  }
@@ -25,7 +25,7 @@ type ThreadFile struct {
25
25
  UpdatedAt *int64 `json:"updated_at,omitempty"`
26
26
  }
27
27
 
28
- // ListThreadFileResult is the JSON envelope printed by `pippit-tool-cli short-drama +list-thread-file`.
28
+ // ListThreadFileResult is the JSON envelope printed by `pippit-tool-cli list-thread-file`.
29
29
  type ListThreadFileResult struct {
30
30
  Files []*ThreadFile `json:"files"`
31
31
  Total int64 `json:"total"`
@@ -17,7 +17,7 @@ type UploadFileOptions struct {
17
17
  FileName string `json:"file_name"`
18
18
  }
19
19
 
20
- // UploadFileResult is the JSON envelope printed by `pippit-tool-cli short-drama +upload-file`.
20
+ // UploadFileResult is the JSON envelope printed by `pippit-tool-cli upload-file`.
21
21
  type UploadFileResult struct {
22
22
  AssetID string `json:"asset_id"`
23
23
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pippit-dev/cli",
3
- "version": "1.0.0",
3
+ "version": "1.0.1",
4
4
  "description": "Pippit CLI",
5
5
  "bin": {
6
6
  "pippit-tool-cli": "scripts/run.js"
@@ -29,7 +29,7 @@ metadata:
29
29
  4. **获取会话文件** - 根据 `thread_id` 拉取会话文件列表,得到 `file_path`、`download_url`。这和查询会话进展同等重要。
30
30
  5. **下载重要资产** - 使用文件列表中的 `download_url` 下载资源,并按 `file_path` 写入用户本地目标文件路径。
31
31
 
32
- 重要资产包括但不限于:剧本设计、场景设计、场景图、人物角色设计、人物图、分集草稿、故事板、最终视频产物。只要 `+list-thread-file` 返回了这些资产的 `download_url`,就要及时调用下载工具落盘,不要只展示文件元信息。
32
+ 重要资产包括但不限于:剧本设计、场景设计、场景图、人物角色设计、人物图、分集草稿、故事板、最终视频产物。只要 `list-thread-file` 返回了这些资产的 `download_url`,就要及时调用下载工具落盘,不要只展示文件元信息。
33
33
 
34
34
  ## 短剧主流程顺序
35
35
 
@@ -94,17 +94,17 @@ pippit-tool-cli short-drama +submit-run --message "参考这个大纲写第一
94
94
 
95
95
  ```bash
96
96
  # 查询会话可读进展
97
- pippit-tool-cli short-drama +get-thread --thread-id THREAD_ID --run-id RUN_ID
97
+ pippit-tool-cli get-thread --thread-id THREAD_ID --run-id RUN_ID
98
98
  ```
99
99
 
100
100
  > `thread_id` 和 `run_id` 由 `+submit-run` 返回。`run_id` 可省略,省略时返回当前 `thread_id` 下的所有 Run;传入时只看指定 Run。
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 short-drama +upload-file --path /path/to/outline.txt
107
+ pippit-tool-cli upload-file --path /path/to/outline.txt
108
108
  ```
109
109
 
110
110
  上传成功后命令只返回 `asset_id`:
@@ -121,10 +121,10 @@ pippit-tool-cli short-drama +upload-file --path /path/to/outline.txt
121
121
 
122
122
  ```bash
123
123
  # 获取会话文件列表
124
- pippit-tool-cli short-drama +list-thread-file --thread-id THREAD_ID --page-num 1 --page-size 200
124
+ pippit-tool-cli list-thread-file --thread-id THREAD_ID --page-num 1 --page-size 200
125
125
  ```
126
126
 
127
- `+list-thread-file` 返回的每个文件对象包含:
127
+ `list-thread-file` 返回的每个文件对象包含:
128
128
 
129
129
  ```json
130
130
  {
@@ -134,16 +134,16 @@ pippit-tool-cli short-drama +list-thread-file --thread-id THREAD_ID --page-num 1
134
134
  }
135
135
  ```
136
136
 
137
- `+list-thread-file` 只负责获取会话文件列表,不负责下载文件,也不需要判断本地文件是否已存在。
137
+ `list-thread-file` 只负责获取会话文件列表,不负责下载文件,也不需要判断本地文件是否已存在。
138
138
 
139
139
  ### 5. 下载文件资源
140
140
 
141
141
  ```bash
142
142
  # 下载文件资源到指定文件路径
143
- pippit-tool-cli short-drama +download-result --url DOWNLOAD_URL --output-path FILE_PATH --updated-at UPDATED_AT
143
+ pippit-tool-cli download-result --url DOWNLOAD_URL --output-path FILE_PATH --updated-at UPDATED_AT
144
144
  ```
145
145
 
146
- `FILE_PATH` 必须直接使用 `+list-thread-file` 返回的完整 `file_path`,包含文件名,不要取父目录。`UPDATED_AT` 使用同一文件对象返回的 `updated_at`;如果没有 `updated_at`,可省略 `--updated-at`。`+download-result` 负责把会话产生的文件通过 URL 下载到该目标文件路径;如果目标文件已存在且本地修改时间不早于 `updated_at`,跳过下载;如果本地文件早于 `updated_at`,覆盖更新。
146
+ `FILE_PATH` 必须直接使用 `list-thread-file` 返回的完整 `file_path`,包含文件名,不要取父目录。`UPDATED_AT` 使用同一文件对象返回的 `updated_at`;如果没有 `updated_at`,可省略 `--updated-at`。`download-result` 负责把会话产生的文件通过 URL 下载到该目标文件路径;如果目标文件已存在且本地修改时间不早于 `updated_at`,跳过下载;如果本地文件早于 `updated_at`,覆盖更新。
147
147
 
148
148
  ## 典型工作流
149
149
 
@@ -154,8 +154,8 @@ pippit-tool-cli short-drama +download-result --url DOWNLOAD_URL --output-path FI
154
154
  → 拿到 thread_id、run_id 和 web_thread_link
155
155
  2. 立即将 web_thread_link 展示给用户
156
156
  3. 并行发起,二者同等重要:
157
- a. pippit-tool-cli short-drama +get-thread --thread-id THREAD_ID --run-id RUN_ID
158
- b. pippit-tool-cli short-drama +list-thread-file --thread-id THREAD_ID --page-num PAGE_NUM --page-size 200
157
+ a. pippit-tool-cli get-thread --thread-id THREAD_ID --run-id RUN_ID
158
+ b. pippit-tool-cli list-thread-file --thread-id THREAD_ID --page-num PAGE_NUM --page-size 200
159
159
  4. 检查 `get-thread` 返回的 readable_text:
160
160
  - 如果任务仍在进行中:展示可读进展,继续查询
161
161
  - 如果后端 Agent 提出问题:从 readable_text 中提取问题并展示,等待用户回复
@@ -163,9 +163,9 @@ pippit-tool-cli short-drama +download-result --url DOWNLOAD_URL --output-path FI
163
163
  - 对每个文件取 file_path、download_url、updated_at
164
164
  - 将 file_path 作为本地目标文件路径,包含文件名
165
165
  - 有 download_url 的重要资产:加入本轮下载队列
166
- - 不判断 file_path 在本地是否已存在,是否跳过由 +download-result 内部处理
166
+ - 不判断 file_path 在本地是否已存在,是否跳过由 download-result 内部处理
167
167
  - 如果本轮 total 达到 200:下一轮将 PAGE_NUM 加 1,继续查询新一页文件
168
- 6. 对重要资产,立即调用 +download-result 并行下载资源:
168
+ 6. 对重要资产,立即调用 download-result 并行下载资源:
169
169
  - 使用第 5 步获取的 download_url 作为 --url
170
170
  - 使用第 5 步获取的完整 file_path 作为 --output-path
171
171
  - 如果第 5 步返回 updated_at,作为 --updated-at 传入
@@ -179,7 +179,7 @@ pippit-tool-cli short-drama +download-result --url DOWNLOAD_URL --output-path FI
179
179
 
180
180
  ```
181
181
  1. 检查用户提供的是一个本地 `.doc`、`.docx` 或 `.txt` 剧本文件路径;如果不是,告知当前上传命令只支持这三类文件,不要擅自转换或改写文件。
182
- 2. pippit-tool-cli short-drama +upload-file --path /path/to/file.txt
182
+ 2. pippit-tool-cli 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
@@ -199,23 +199,23 @@ pippit-tool-cli short-drama +download-result --url DOWNLOAD_URL --output-path FI
199
199
  ## 轮询策略
200
200
 
201
201
  - **间隔**:每 10 秒查询一次。
202
- - **进展查询**:每轮调用 `+get-thread` 查看 `readable_text`。优先带上本轮 `run_id` 聚焦当前任务;需要查看整个会话时可省略 `--run-id`。
203
- - **并行查询**:每次 `+submit-run` 返回 `thread_id` 后,同时发起 `+get-thread` 和 `+list-thread-file`;二者同等重要,不能只查询会话进展而忽略会话文件。
204
- - **文件分页**:`+list-thread-file` 使用 `--page-size 200`。如果本轮返回的 `total` 达到 200,下一轮使用 `--page-num` 加 1 查询新一页结果;如果未达到 200,保持当前页继续轮询新增产物。
205
- - **重要资产识别**:每轮都检查 `+list-thread-file` 返回的文件。剧本设计、场景设计、场景图、人物角色设计、人物图、分集草稿、故事板、最终视频产物都是重要资产。
206
- - **文件下载**:解析 `+list-thread-file` 的结果后,对带 `download_url` 的重要资产立即调用 `+download-result` 下载资源;不要在 `list-thread-file` 阶段检查文件是否已存在,存在性检查由下载工具内部处理。
202
+ - **进展查询**:每轮调用 `get-thread` 查看 `readable_text`。优先带上本轮 `run_id` 聚焦当前任务;需要查看整个会话时可省略 `--run-id`。
203
+ - **并行查询**:每次 `+submit-run` 返回 `thread_id` 后,同时发起 `get-thread` 和 `list-thread-file`;二者同等重要,不能只查询会话进展而忽略会话文件。
204
+ - **文件分页**:`list-thread-file` 使用 `--page-size 200`。如果本轮返回的 `total` 达到 200,下一轮使用 `--page-num` 加 1 查询新一页结果;如果未达到 200,保持当前页继续轮询新增产物。
205
+ - **重要资产识别**:每轮都检查 `list-thread-file` 返回的文件。剧本设计、场景设计、场景图、人物角色设计、人物图、分集草稿、故事板、最终视频产物都是重要资产。
206
+ - **文件下载**:解析 `list-thread-file` 的结果后,对带 `download_url` 的重要资产立即调用 `download-result` 下载资源;不要在 `list-thread-file` 阶段检查文件是否已存在,存在性检查由下载工具内部处理。
207
207
  - **下载完成标准**:不要把文件元信息展示当成下载完成;必须拿到本地 `file_path`,或明确记录该文件在重试后仍下载失败。
208
208
  - **用户确认**:如果消息中出现需要用户确认、补充设定或回答问题的内容,先判断是否包含表单、问卷、选项或按钮;包含时按“短剧主流程顺序”和“表单与问卷选项处理原则”清洗选项,再展示给用户并等待回复。
209
209
  - **超时**:如果长时间无结果,告知用户任务仍在生成中,可稍后通过 `web_thread_link` 查看。
210
- - **错误处理**:`+get-thread`、`+list-thread-file` 或 `+download-result` 任一调用失败时,记录失败原因和参数,在后续轮询中主动重试;重试期间继续处理其他成功返回的消息和文件。连续多轮失败后再向用户说明仍未完成的查询或下载项。
210
+ - **错误处理**:`get-thread`、`list-thread-file` 或 `download-result` 任一调用失败时,记录失败原因和参数,在后续轮询中主动重试;重试期间继续处理其他成功返回的消息和文件。连续多轮失败后再向用户说明仍未完成的查询或下载项。
211
211
 
212
212
  ## 完成标准
213
213
 
214
- 一次短剧任务不能只以 `+get-thread` 返回的 `readable_text` 作为结束条件。完成前必须同时检查:
214
+ 一次短剧任务不能只以 `get-thread` 返回的 `readable_text` 作为结束条件。完成前必须同时检查:
215
215
 
216
- 1. 已处理 `+get-thread` 返回的最新 `readable_text`、用户确认问题和最终消息。
217
- 2. 已用 `--page-size 200` 调用 `+list-thread-file` 获取会话文件列表;如果本轮 `total` 达到 200,已在后续轮询中递增 `page-num` 查询新一页。
218
- 3. 对所有带 `download_url` 的重要资产,已调用 `+download-result` 下载到本地 `file_path`。
216
+ 1. 已处理 `get-thread` 返回的最新 `readable_text`、用户确认问题和最终消息。
217
+ 2. 已用 `--page-size 200` 调用 `list-thread-file` 获取会话文件列表;如果本轮 `total` 达到 200,已在后续轮询中递增 `page-num` 查询新一页。
218
+ 3. 对所有带 `download_url` 的重要资产,已调用 `download-result` 下载到本地 `file_path`。
219
219
  4. 已按短剧主流程顺序检查服务端表单、问卷和选项,没有把跳过必要阶段的选项直接呈现给用户;如果跳过 `剧本标准化`,已明确这是可选阶段。
220
220
  5. 对查询失败或下载失败的资产,已在后续轮询中主动重试,并在最终回复中列出仍失败的文件或命令。
221
221
 
@@ -231,7 +231,7 @@ pippit-tool-cli short-drama +download-result --url DOWNLOAD_URL --output-path FI
231
231
  }
232
232
  ```
233
233
 
234
- **+get-thread** 返回:
234
+ **get-thread** 返回:
235
235
 
236
236
  ```text
237
237
  Thread: thread_...
@@ -242,7 +242,7 @@ Thread: thread_...
242
242
  [assistant] ...
243
243
  ```
244
244
 
245
- **+upload-file** 返回:
245
+ **upload-file** 返回:
246
246
 
247
247
  ```json
248
248
  {
@@ -250,9 +250,9 @@ 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
- **+list-thread-file** 返回:
255
+ **list-thread-file** 返回:
256
256
 
257
257
  ```json
258
258
  {
@@ -270,7 +270,7 @@ Thread: thread_...
270
270
 
271
271
  当 `total` 达到 200 时,`message` 会用 `<system-remind>` 提示下一轮将 `page-num` 加 1 查询新一页。
272
272
 
273
- **+download-result** 返回:
273
+ **download-result** 返回:
274
274
 
275
275
  ```json
276
276
  {
@@ -281,16 +281,16 @@ Thread: thread_...
281
281
 
282
282
  ## 会话文件与资源下载
283
283
 
284
- 先用 `+list-thread-file` 获取会话文件列表,再用 `+download-result` 并行下载重要资产。获取文件元信息不是最终目标,重要资产落盘才是核心目标。文件是否已存在由下载工具内部检查,`+list-thread-file` 阶段不要做本地存在性判断。
284
+ 先用 `list-thread-file` 获取会话文件列表,再用 `download-result` 并行下载重要资产。获取文件元信息不是最终目标,重要资产落盘才是核心目标。文件是否已存在由下载工具内部检查,`list-thread-file` 阶段不要做本地存在性判断。
285
285
 
286
286
  ### 获取会话文件
287
287
 
288
- `+list-thread-file` 的 `files` 中逐个读取文件元信息:`file_path`、`file_name`、`download_url`、`updated_at`。重点识别剧本设计、场景设计、场景图、人物角色设计、人物图、分集草稿、故事板、最终视频产物等重要资产。
288
+ `list-thread-file` 的 `files` 中逐个读取文件元信息:`file_path`、`file_name`、`download_url`、`updated_at`。重点识别剧本设计、场景设计、场景图、人物角色设计、人物图、分集草稿、故事板、最终视频产物等重要资产。
289
289
 
290
290
  ```
291
291
  1. 有download_url的重要资产
292
292
  → 记录该file_path、URL和updated_at
293
- → 使用 +download-result 将URL资源下载到该file_path;有updated_at时传入--updated-at
293
+ → 使用 download-result 将URL资源下载到该file_path;有updated_at时传入--updated-at
294
294
  2. 本轮total达到200
295
295
  → 下一轮page-num加1,继续查询新一页结果
296
296
  3. 本轮total未达到200
@@ -304,7 +304,7 @@ Thread: thread_...
304
304
 
305
305
  对带 `download_url` 的重要资产调用下载工具,可并行。重要资产必须主动下载,不要等用户再次要求,也不要在调用下载工具前先检查本地文件是否存在。
306
306
 
307
- 1. 调用 `pippit-tool-cli short-drama +download-result --url DOWNLOAD_URL --output-path FILE_PATH --updated-at UPDATED_AT`;如果文件对象没有 `updated_at`,省略 `--updated-at`。
307
+ 1. 调用 `pippit-tool-cli download-result --url DOWNLOAD_URL --output-path FILE_PATH --updated-at UPDATED_AT`;如果文件对象没有 `updated_at`,省略 `--updated-at`。
308
308
  2. 下载完成后,向用户展示本地文件路径;如果某个文件下载失败,记录失败项并在后续轮询中重试,不阻塞已成功落盘的文件展示。
309
309
 
310
310
  ## 向用户展示内容
@@ -323,9 +323,9 @@ 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
- 3. **传话、取文件、下载资源**:根据 `+get-thread` 返回的 `readable_text` 展示进展、问题和结果;遇到表单、问卷、选项或按钮时,只做流程合理性清洗,不替用户决定创作内容;根据 `+list-thread-file` 获取文件列表;再根据 `download_url` 调用 `+download-result` 把缺失资源下载到用户本地。
328
+ 3. **传话、取文件、下载资源**:根据 `get-thread` 返回的 `readable_text` 展示进展、问题和结果;遇到表单、问卷、选项或按钮时,只做流程合理性清洗,不替用户决定创作内容;根据 `list-thread-file` 获取文件列表;再根据 `download_url` 调用 `download-result` 把缺失资源下载到用户本地。
329
329
 
330
330
  **不要做的事:**
331
331
 
@@ -340,10 +340,10 @@ Thread: thread_...
340
340
 
341
341
  - `--message` 是用户的原始短剧需求,不能为空。
342
342
  - 查询进展时优先使用 `+submit-run` 返回的 `thread_id` 和 `run_id`;如果需要查看整个会话,可以省略 `--run-id`。
343
- - `+get-thread` 当前固定走服务端 v2 响应,输出字段是 `readable_text`;不要解析旧版 `messages` 数组。
344
- - `+upload-file` 当前用于短剧场景文件上传链路,只支持本地 `.doc` / `.docx` / `.txt` 文件;`--path` 不能为空,路径必须指向真实文件,不能是目录。
345
- - `+upload-file` 上传成功后只返回 `asset_id`;把该值原样作为 `+submit-run --asset-ids` 的参数。
343
+ - `get-thread` 当前固定走服务端 v2 响应,输出字段是 `readable_text`;不要解析旧版 `messages` 数组。
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
- - `+list-thread-file` 只需要 `thread_id`;分页参数使用 `--page-num 1 --page-size 200` 起步,`total` 达到 200 时下一轮递增 `page-num`。
348
- - `+list-thread-file` 和 `+download-result` 是两个不同的 CLI 指令:前者获取会话文件元信息,后者下载 URL 资源并写入到本地目标文件路径。
349
- - `+download-result` 接收 `--url`、`--output-path`、`--updated-at`、`--workers`;`--output-path` 必须是包含文件名的目标文件路径。
347
+ - `list-thread-file` 只需要 `thread_id`;分页参数使用 `--page-num 1 --page-size 200` 起步,`total` 达到 200 时下一轮递增 `page-num`。
348
+ - `list-thread-file` 和 `download-result` 是两个不同的 CLI 指令:前者获取会话文件元信息,后者下载 URL 资源并写入到本地目标文件路径。
349
+ - `download-result` 接收 `--url`、`--output-path`、`--updated-at`、`--workers`;`--output-path` 必须是包含文件名的目标文件路径。