@pippit-dev/cli 1.0.51 → 1.0.53

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.
@@ -0,0 +1,38 @@
1
+ package cmd
2
+
3
+ import (
4
+ "fmt"
5
+ "io"
6
+
7
+ "github.com/Pippit-dev/pippit-cli/internal/commandnames"
8
+ "github.com/Pippit-dev/pippit-cli/internal/common"
9
+ "github.com/spf13/cobra"
10
+ )
11
+
12
+ func newWatermarkCommand(stdout, stderr io.Writer, runner *common.Runner) *cobra.Command {
13
+ cmd := &cobra.Command{
14
+ Use: commandnames.Watermark + " [on|off]",
15
+ Short: "Query or set the current account's AIGC watermark",
16
+ Long: "Without arguments, query the current account's AIGC watermark setting. Use on to retain it or off to remove it from saved content. This changes a persistent account setting; team selection does not change its scope. Final downloads remain subject to server policies.",
17
+ Args: cobra.MaximumNArgs(1),
18
+ ValidArgs: []string{"on", "off"},
19
+ RunE: withErrorLog(commandnames.Watermark, nil, func(cmd *cobra.Command, args []string) error {
20
+ var enabled *bool
21
+ if len(args) > 0 {
22
+ if args[0] != "on" && args[0] != "off" {
23
+ return fmt.Errorf("水印状态只支持 on 或 off")
24
+ }
25
+ value := args[0] == "on"
26
+ enabled = &value
27
+ }
28
+ result, err := common.Watermark(cmd.Context(), runner, enabled)
29
+ if err != nil {
30
+ return err
31
+ }
32
+ return common.WriteJSON(stdout, result)
33
+ }),
34
+ }
35
+ cmd.SetOut(stdout)
36
+ cmd.SetErr(stderr)
37
+ return cmd
38
+ }
@@ -1,6 +1,6 @@
1
- 95c1a5b68a1df337ef3869de5cd00172b766c9324975f2a7358631d92b843137 pippit-tool-cli-1.0.51-darwin-amd64.tar.gz
2
- 05a617b10aa6768f1a0d51c82d5f1bbf7a3ed6196db17247dfaad7cd1f46f957 pippit-tool-cli-1.0.51-darwin-arm64.tar.gz
3
- fb1edb92514aa2941b8c83ca1b806f76cdd1a0e19610b6ac9af38e415b9e060e pippit-tool-cli-1.0.51-linux-amd64.tar.gz
4
- 45b57561a0f190d32e4f05cc2aef6e9d2421263f7cec4cacd1e0151d5b1898aa pippit-tool-cli-1.0.51-linux-arm64.tar.gz
5
- 0f727048f4e3915a3f58e7c3b0b4df6d3fcd4214a17881cdb37f2958c417d86a pippit-tool-cli-1.0.51-windows-amd64.zip
6
- 1b05ef17e005e405997cf78622afbdc6d161f20c855347fabc5a0a46cf00399d pippit-tool-cli-1.0.51-windows-arm64.zip
1
+ 08e0f157a05bdcbbb4c7a53eeeee3374d5be4bf86ec31139c5d42328c8cc781e pippit-tool-cli-1.0.53-darwin-amd64.tar.gz
2
+ fa1b79ed862a997d7ec8117895158f1914d2fcaa30792b303fd924d2856a834f pippit-tool-cli-1.0.53-darwin-arm64.tar.gz
3
+ e0d30c4edf201108587da2bd1ce97f9e7a30721e41700c50e7ddb57cc4c2474a pippit-tool-cli-1.0.53-linux-amd64.tar.gz
4
+ b60c817d9b58659838baf9954e5cc110ddc92ed2736e5512047dc2b20a9dfde8 pippit-tool-cli-1.0.53-linux-arm64.tar.gz
5
+ b890470552424b37fbd98d8a0e202d66526f7f575233cfb860599dadbc802be2 pippit-tool-cli-1.0.53-windows-amd64.zip
6
+ cf549f5b6e7d929d7b8b48b0a1a3df467ad921c23b4a93361799d7723b023bde pippit-tool-cli-1.0.53-windows-arm64.zip
@@ -10,5 +10,6 @@ const (
10
10
  Completion = "completion"
11
11
  Help = "help"
12
12
  Model = "model"
13
+ Watermark = "watermark"
13
14
  DownloadResult = "download-result"
14
15
  )
@@ -18,8 +18,13 @@ type GetThreadOptions struct {
18
18
  }
19
19
 
20
20
  // GetThreadResult is the parsed get_thread response used by `pippit-tool-cli get-thread`.
21
+ // A business error may return both a result and an error so structured callers
22
+ // can still inspect the returned Run. Callers must continue to handle the error.
21
23
  type GetThreadResult struct {
22
24
  ReadableText string `json:"readable_text"`
25
+ Ret string `json:"-"`
26
+ Errmsg string `json:"-"`
27
+ LogID string `json:"-"`
23
28
  RawData []byte `json:"-"`
24
29
  }
25
30
 
@@ -49,11 +54,12 @@ func GetThread(ctx context.Context, opts *GetThreadOptions, runner *Runner) (*Ge
49
54
  if err := runner.Client.SendRequest(ctx, getThreadPath(runner), body, &resp); err != nil {
50
55
  return nil, fmt.Errorf("获取线程请求失败: %w", err)
51
56
  }
57
+ result := &GetThreadResult{Ret: resp.Ret, Errmsg: resp.Errmsg, LogID: resp.LogID, RawData: resp.Data}
52
58
  if resp.Ret != "0" {
53
59
  if resp.Errmsg == "" {
54
60
  resp.Errmsg = "未知错误"
55
61
  }
56
- return nil, NewLogIDError(fmt.Sprintf("获取线程请求返回失败: ret=%s errmsg=%s", resp.Ret, resp.Errmsg), resp.LogID)
62
+ return result, NewLogIDError(fmt.Sprintf("获取线程请求返回失败: ret=%s errmsg=%s", resp.Ret, resp.Errmsg), resp.LogID)
57
63
  }
58
64
  if len(resp.Data) == 0 {
59
65
  return nil, fmt.Errorf("get_thread 响应缺少 data")
@@ -68,10 +74,8 @@ func GetThread(ctx context.Context, opts *GetThreadOptions, runner *Runner) (*Ge
68
74
  }
69
75
  }
70
76
 
71
- return &GetThreadResult{
72
- ReadableText: readable.ReadableText,
73
- RawData: resp.Data,
74
- }, nil
77
+ result.ReadableText = readable.ReadableText
78
+ return result, nil
75
79
  }
76
80
 
77
81
  func getThreadPath(runner *Runner) string {
@@ -57,6 +57,21 @@ func TestGetThreadRequiresRetZero(t *testing.T) {
57
57
  }
58
58
  }
59
59
 
60
+ func TestGetThreadPreservesDataOnBusinessError(t *testing.T) {
61
+ result, err := GetThread(context.Background(), &GetThreadOptions{
62
+ ThreadID: "thread_123",
63
+ RunID: "run_456",
64
+ }, &Runner{
65
+ Client: getThreadFakeClient{response: `{"ret":"2","errmsg":"模拟接口错误","log_id":"fixture-log","data":{"thread":{"thread_id":"thread_123","run_list":[{"run_id":"run_456","state":4}]}}}`},
66
+ })
67
+ if err == nil || !strings.Contains(err.Error(), "ret=2") || !strings.Contains(err.Error(), "fixture-log") {
68
+ t.Fatalf("error = %v, want business error with log ID", err)
69
+ }
70
+ if result == nil || !strings.Contains(string(result.RawData), `"run_456"`) {
71
+ t.Fatalf("result = %#v, want response data alongside business error", result)
72
+ }
73
+ }
74
+
60
75
  func TestGetThreadV2RequiresReadableText(t *testing.T) {
61
76
  _, err := GetThread(context.Background(), &GetThreadOptions{
62
77
  ThreadID: "thread_123",
@@ -72,3 +87,24 @@ func TestGetThreadV2RequiresReadableText(t *testing.T) {
72
87
  t.Fatalf("error = %q, want readable_text validation", err)
73
88
  }
74
89
  }
90
+
91
+ func TestGetThreadPreservesEnvelope(t *testing.T) {
92
+ for _, ret := range []string{"0", "1", "2", "5", "1015", "16019", "987654"} {
93
+ t.Run(ret, func(t *testing.T) {
94
+ result, err := GetThread(context.Background(), &GetThreadOptions{ThreadID: "thread", RunID: "run"}, &Runner{Client: getThreadFakeClient{response: `{"ret":"` + ret + `","errmsg":"server message","log_id":"server-log","data":null}`}})
95
+ if (err != nil) != (ret != "0") {
96
+ t.Fatalf("err=%v ret=%s", err, ret)
97
+ }
98
+ if result == nil || result.Ret != ret || result.Errmsg != "server message" || result.LogID != "server-log" || string(result.RawData) != "null" {
99
+ t.Fatalf("envelope lost: %#v", result)
100
+ }
101
+ raw, marshalErr := sonic.Marshal(result)
102
+ if marshalErr != nil {
103
+ t.Fatal(marshalErr)
104
+ }
105
+ if strings.Contains(string(raw), "server-log") || strings.Contains(string(raw), "server message") {
106
+ t.Fatalf("internal envelope leaked: %s", raw)
107
+ }
108
+ })
109
+ }
110
+ }
@@ -0,0 +1,54 @@
1
+ package common
2
+
3
+ import (
4
+ "context"
5
+ "fmt"
6
+
7
+ "github.com/Pippit-dev/pippit-cli/internal/config"
8
+ )
9
+
10
+ type WatermarkResult struct {
11
+ Enabled bool `json:"enabled"`
12
+ }
13
+
14
+ type watermarkResponse struct {
15
+ Ret string `json:"ret"`
16
+ Errmsg string `json:"errmsg"`
17
+ LogID string `json:"log_id"`
18
+ Data *struct {
19
+ Enabled *bool `json:"enabled"`
20
+ } `json:"data"`
21
+ }
22
+
23
+ // Watermark queries when enabled is nil; updates return the server's readback in the same request.
24
+ func Watermark(ctx context.Context, runner *Runner, enabled *bool) (*WatermarkResult, error) {
25
+ if runner == nil || runner.Client == nil {
26
+ return nil, fmt.Errorf("水印设置运行器客户端缺失")
27
+ }
28
+ path := config.GetWatermarkSettingPath
29
+ var body any = struct{}{}
30
+ if enabled != nil {
31
+ path = config.UpdateWatermarkSettingPath
32
+ body = struct {
33
+ Enabled bool `json:"enabled"`
34
+ }{Enabled: *enabled}
35
+ }
36
+
37
+ var resp watermarkResponse
38
+ if err := runner.Client.SendRequest(ctx, path, body, &resp); err != nil {
39
+ if enabled != nil {
40
+ return nil, fmt.Errorf("设置水印请求失败,结果可能已生效,请运行 watermark 查询确认: %w", err)
41
+ }
42
+ return nil, fmt.Errorf("查询水印设置请求失败: %w", err)
43
+ }
44
+ if resp.Ret != "0" {
45
+ if resp.Errmsg == "" {
46
+ resp.Errmsg = "未知错误"
47
+ }
48
+ return nil, NewLogIDError(fmt.Sprintf("水印设置请求返回失败: ret=%s errmsg=%s", resp.Ret, resp.Errmsg), resp.LogID)
49
+ }
50
+ if resp.Data == nil || resp.Data.Enabled == nil {
51
+ return nil, NewLogIDError("水印设置响应缺少 data.enabled;请查询确认当前状态", resp.LogID)
52
+ }
53
+ return &WatermarkResult{Enabled: *resp.Data.Enabled}, nil
54
+ }
@@ -21,6 +21,8 @@ const (
21
21
  SubmitMarketingRunPath = "/api/biz/v1/agent/submit_marketing_run"
22
22
  QueryGenerateVideoResultPath = "/api/biz/v1/agent/query_generate_video_result"
23
23
  GetCreditBalancePath = "/api/biz/v1/skill/get_credit_balance"
24
+ GetWatermarkSettingPath = "/api/biz/v1/skill/mget_user_setting"
25
+ UpdateWatermarkSettingPath = "/api/biz/v1/skill/update_user_setting"
24
26
  GetLiteTeamListPath = "/api/biz/v1/skill/get_lite_team_list"
25
27
  GetAvailableModelListPath = "/api/biz/v1/skill/get_available_model_list"
26
28
  GetThreadPath = "/api/biz/v1/skill/get_thread"
@@ -14,19 +14,22 @@ import (
14
14
  )
15
15
 
16
16
  const (
17
- successRunState = 3
18
- failedRunState = 4
19
- canceledRunState = 5
17
+ successRunState = 3
18
+ failedRunState = 4
19
+ canceledRunState = 5
20
+ interruptedRunState = 9
20
21
  )
21
22
 
22
23
  // Query-result status values. "completed" stays a terminal flag; "status"
23
24
  // carries the machine-readable result so integrators need not parse text.
24
25
  const (
25
- statusRunning = "running"
26
- statusSucceeded = "succeeded"
27
- statusFailed = "failed"
28
- statusCanceled = "canceled"
29
- statusUnknown = "unknown"
26
+ statusRunning = "running"
27
+ statusSucceeded = "succeeded"
28
+ statusFailed = "failed"
29
+ statusCanceled = "canceled"
30
+ statusUnknown = "unknown"
31
+ statusInputRequired = "input_required"
32
+ statusInterrupted = "interrupted"
30
33
  )
31
34
 
32
35
  // runStateStatus maps the server-side run state (0-9) to the user-facing
@@ -38,16 +41,17 @@ var runStateStatus = map[int]string{
38
41
  3: statusSucceeded,
39
42
  4: statusFailed,
40
43
  5: statusCanceled,
41
- 6: statusUnknown, // InputRequired
42
- 7: statusRunning, // Generating
43
- 8: statusRunning, // Interrupt
44
- 9: statusUnknown, // HITL_Interrupt
44
+ 6: statusInputRequired,
45
+ 7: statusRunning, // Generating
46
+ 8: statusRunning, // Interrupt
47
+ 9: statusInterrupted, // HITL_Interrupt is terminal, unlike Interrupt (8).
45
48
  }
46
49
 
47
50
  var terminalRunStates = map[int]struct{}{
48
- successRunState: {},
49
- failedRunState: {},
50
- canceledRunState: {},
51
+ successRunState: {},
52
+ failedRunState: {},
53
+ canceledRunState: {},
54
+ interruptedRunState: {},
51
55
  }
52
56
 
53
57
  func statusForRunState(state int) string {
@@ -100,12 +104,18 @@ type queryThread struct {
100
104
  }
101
105
 
102
106
  type queryRun struct {
103
- RunID string `json:"run_id"`
104
- State int `json:"state"`
105
- ErrorMessage string `json:"error_message"`
106
- ErrorMsg string `json:"error_msg"`
107
- Errmsg string `json:"errmsg"`
108
- EntryList []queryEntry `json:"entry_list"`
107
+ RunID string `json:"run_id"`
108
+ State int `json:"state"`
109
+ ErrorMessage string `json:"error_message"`
110
+ ErrorMsg string `json:"error_msg"`
111
+ Errmsg string `json:"errmsg"`
112
+ FailReason *queryFailReason `json:"fail_reason"`
113
+ EntryList []queryEntry `json:"entry_list"`
114
+ }
115
+
116
+ type queryFailReason struct {
117
+ Message string `json:"message"`
118
+ Code json.RawMessage `json:"code"`
109
119
  }
110
120
 
111
121
  type queryEntry struct {
@@ -152,34 +162,41 @@ func QueryResult(ctx context.Context, opts *QueryResultOptions, runner *common.R
152
162
  return nil, err
153
163
  }
154
164
 
155
- threadResult, err := common.GetThread(ctx, &common.GetThreadOptions{
165
+ threadResult, queryErr := common.GetThread(ctx, &common.GetThreadOptions{
156
166
  ThreadID: opts.ThreadID,
157
167
  RunID: opts.RunID,
158
168
  }, runner)
159
- if err != nil {
160
- return nil, queryResultQueryError(err)
161
- }
162
-
163
169
  thread, err := parseQueryThread(threadResult)
164
170
  if err != nil {
171
+ if queryErr != nil {
172
+ return queryResultWithoutRun(threadResult, queryErr, opts)
173
+ }
165
174
  return nil, fmt.Errorf("查询失败:%w", err)
166
175
  }
167
176
 
168
177
  run, ok := findQueryRun(thread, opts.RunID)
169
178
  if !ok {
179
+ if queryErr != nil {
180
+ return queryResultWithoutRun(threadResult, queryErr, opts)
181
+ }
170
182
  return nil, fmt.Errorf("查询失败:未找到 run_id=%s 对应的 Run", opts.RunID)
171
183
  }
184
+ logResolvedQueryError(queryErr, opts)
185
+ result := &QueryResultResult{
186
+ Status: statusForRunState(run.State),
187
+ Completed: isTerminalRunState(run.State),
188
+ ThreadID: firstNonEmpty(thread.ThreadID, opts.ThreadID),
189
+ RunID: opts.RunID,
190
+ Videos: []QueryResultVideo{},
191
+ Images: []QueryResultImage{},
192
+ }
172
193
  if run.State != successRunState {
173
- result := &QueryResultResult{
174
- Status: statusForRunState(run.State),
175
- Completed: isTerminalRunState(run.State),
176
- ThreadID: firstNonEmpty(thread.ThreadID, opts.ThreadID),
177
- RunID: opts.RunID,
178
- Videos: []QueryResultVideo{},
179
- Images: []QueryResultImage{},
180
- }
181
194
  if run.State == failedRunState {
182
- result.ErrorMessage = firstNonEmpty(extractQueryErrorMessage(run), "Run 失败")
195
+ message := extractQueryErrorMessage(run)
196
+ if message == "" && queryErr != nil {
197
+ message = queryResultQueryError(queryErr).Error()
198
+ }
199
+ result.ErrorMessage = firstNonEmpty(message, "Run 失败")
183
200
  }
184
201
  return result, nil
185
202
  }
@@ -187,20 +204,19 @@ func QueryResult(ctx context.Context, opts *QueryResultOptions, runner *common.R
187
204
  videos := extractQueryVideos(run)
188
205
  images := extractQueryImages(run)
189
206
  if len(videos) == 0 && len(images) == 0 {
190
- return nil, fmt.Errorf("下载失败:未找到可下载的产物")
207
+ return result, fmt.Errorf("下载失败:未找到可下载的产物")
191
208
  }
192
209
 
193
210
  downloadDir, err := common.ExpandPath(opts.DownloadDir)
194
211
  if err != nil {
195
- return nil, fmt.Errorf("下载失败:解析下载目录失败:%w", err)
212
+ return result, fmt.Errorf("下载失败:解析下载目录失败:%w", err)
196
213
  }
197
214
 
198
215
  usedNames := make(map[string]int, len(videos)+len(images))
199
216
 
200
- resultVideos := make([]QueryResultVideo, 0, len(videos))
201
217
  for i, video := range videos {
202
218
  if strings.TrimSpace(video.DownloadURL) == "" {
203
- return nil, fmt.Errorf("下载失败:第 %d 个视频产物 download_url 为空", i+1)
219
+ return result, fmt.Errorf("下载失败:第 %d 个视频产物 download_url 为空", i+1)
204
220
  }
205
221
  outputPath := filepath.Join(downloadDir, uniqueQueryResultFileName(videoFileName(video, i+1), usedNames))
206
222
  download, err := common.DownloadResult(ctx, common.DownloadResultOptions{
@@ -209,7 +225,7 @@ func QueryResult(ctx context.Context, opts *QueryResultOptions, runner *common.R
209
225
  Workers: 5,
210
226
  }, runner)
211
227
  if err != nil {
212
- return nil, fmt.Errorf("下载失败:%w", err)
228
+ return result, fmt.Errorf("下载失败:%w", err)
213
229
  }
214
230
  actualOutputPath := outputPath
215
231
  if len(download.Downloaded) > 0 {
@@ -217,7 +233,7 @@ func QueryResult(ctx context.Context, opts *QueryResultOptions, runner *common.R
217
233
  } else if len(download.AlreadyExist) > 0 {
218
234
  actualOutputPath = download.AlreadyExist[0]
219
235
  }
220
- resultVideos = append(resultVideos, QueryResultVideo{
236
+ result.Videos = append(result.Videos, QueryResultVideo{
221
237
  DownloadURL: video.DownloadURL,
222
238
  OutputPath: actualOutputPath,
223
239
  Draft: video.Draft,
@@ -225,10 +241,9 @@ func QueryResult(ctx context.Context, opts *QueryResultOptions, runner *common.R
225
241
  })
226
242
  }
227
243
 
228
- resultImages := make([]QueryResultImage, 0, len(images))
229
244
  for i, image := range images {
230
245
  if strings.TrimSpace(image.DownloadURL) == "" {
231
- return nil, fmt.Errorf("下载失败:第 %d 个图片产物 download_url 为空", i+1)
246
+ return result, fmt.Errorf("下载失败:第 %d 个图片产物 download_url 为空", i+1)
232
247
  }
233
248
  outputPath := filepath.Join(downloadDir, uniqueQueryResultFileName(imageFileName(image, i+1), usedNames))
234
249
  download, err := common.DownloadResult(ctx, common.DownloadResultOptions{
@@ -237,7 +252,7 @@ func QueryResult(ctx context.Context, opts *QueryResultOptions, runner *common.R
237
252
  Workers: 5,
238
253
  }, runner)
239
254
  if err != nil {
240
- return nil, fmt.Errorf("下载失败:%w", err)
255
+ return result, fmt.Errorf("下载失败:%w", err)
241
256
  }
242
257
  actualOutputPath := outputPath
243
258
  if len(download.Downloaded) > 0 {
@@ -245,25 +260,52 @@ func QueryResult(ctx context.Context, opts *QueryResultOptions, runner *common.R
245
260
  } else if len(download.AlreadyExist) > 0 {
246
261
  actualOutputPath = download.AlreadyExist[0]
247
262
  }
248
- resultImages = append(resultImages, QueryResultImage{
263
+ result.Images = append(result.Images, QueryResultImage{
249
264
  DownloadURL: image.DownloadURL,
250
265
  OutputPath: actualOutputPath,
251
266
  })
252
267
  }
253
268
 
254
- return &QueryResultResult{
255
- Status: statusSucceeded,
256
- Completed: true,
257
- ThreadID: firstNonEmpty(thread.ThreadID, opts.ThreadID),
258
- RunID: opts.RunID,
259
- Videos: resultVideos,
260
- Images: resultImages,
261
- }, nil
269
+ return result, nil
262
270
  }
263
271
 
264
- // queryResultQueryError converts a get_thread failure into a Go error while
265
- // keeping a clean, customer-facing message. The command layer maps it to
266
- // status=unknown; the technical envelope stays in the local error log.
272
+ // The legacy endpoint omits data for Failed/Canceled. Only these exact
273
+ // endpoint-specific responses prove a terminal Run; other errors (including
274
+ // ret=5 "获取会话信息失败") describe a failed query, not a failed Run.
275
+ func queryResultWithoutRun(threadResult *common.GetThreadResult, queryErr error, opts *QueryResultOptions) (*QueryResultResult, error) {
276
+ status := statusUnknown
277
+ if threadResult != nil && threadResult.Ret == "5" {
278
+ message := strings.TrimSpace(threadResult.Errmsg)
279
+ switch {
280
+ case message == "创作失败" || strings.HasPrefix(message, "创作失败:"):
281
+ status = statusFailed
282
+ case message == "创作已取消":
283
+ status = statusCanceled
284
+ }
285
+ }
286
+ if status == statusUnknown {
287
+ return nil, queryResultQueryError(queryErr)
288
+ }
289
+ logResolvedQueryError(queryErr, opts)
290
+ result := NewQueryResultError(queryResultQueryError(queryErr), opts)
291
+ result.Status = status
292
+ result.Completed = true
293
+ return result, nil
294
+ }
295
+
296
+ // The command logs returned errors. Errors consumed by a resolved Run must
297
+ // also retain their original ret/errmsg/log_id for diagnosis.
298
+ func logResolvedQueryError(err error, opts *QueryResultOptions) {
299
+ if err != nil {
300
+ _ = common.AppendDailyErrorLog("query-result", err, map[string]string{
301
+ "thread_id": opts.ThreadID,
302
+ "run_id": opts.RunID,
303
+ })
304
+ }
305
+ }
306
+
307
+ // queryResultQueryError extracts a customer-facing get_thread error. It is used
308
+ // when no target Run can be resolved, or as the failed Run's fallback reason.
267
309
  func queryResultQueryError(err error) error {
268
310
  var logErr *common.LogIDError
269
311
  if errors.As(err, &logErr) {
@@ -279,9 +321,8 @@ func queryResultQueryError(err error) error {
279
321
  return fmt.Errorf("查询失败:%w", err)
280
322
  }
281
323
 
282
- // NewQueryResultError builds the user-facing result for a failed query. A
283
- // failed call never produces a run status, so it reports unknown rather than
284
- // guessing; the concrete cause stays in error_message.
324
+ // NewQueryResultError builds the user-facing result when no Run could be
325
+ // resolved. The concrete cause stays in error_message.
285
326
  func NewQueryResultError(err error, opts *QueryResultOptions) *QueryResultResult {
286
327
  result := &QueryResultResult{
287
328
  Status: statusUnknown,
@@ -418,6 +459,16 @@ func extractQueryImages(run queryRun) []queryImage {
418
459
  }
419
460
 
420
461
  func extractQueryErrorMessage(run queryRun) string {
462
+ if run.FailReason != nil {
463
+ message := strings.TrimSpace(run.FailReason.Message)
464
+ code := rawMessageString(run.FailReason.Code)
465
+ if message != "" {
466
+ if code != "" && code != "0" {
467
+ return fmt.Sprintf("%s (error_code=%s)", message, code)
468
+ }
469
+ return message
470
+ }
471
+ }
421
472
  if message := firstNonEmpty(run.ErrorMessage, run.ErrorMsg, run.Errmsg); message != "" {
422
473
  return message
423
474
  }
@@ -435,6 +486,11 @@ func extractQueryErrorMessage(run queryRun) string {
435
486
  }
436
487
  }
437
488
  }
489
+ if run.FailReason != nil {
490
+ if code := rawMessageString(run.FailReason.Code); code != "" && code != "0" {
491
+ return "error_code=" + code
492
+ }
493
+ }
438
494
  return ""
439
495
  }
440
496
 
@@ -0,0 +1,63 @@
1
+ package generate_video
2
+
3
+ import (
4
+ "context"
5
+ "encoding/json"
6
+ "errors"
7
+ "testing"
8
+
9
+ "github.com/Pippit-dev/pippit-cli/internal/common"
10
+ )
11
+
12
+ func TestQueryFailureReasons(t *testing.T) {
13
+ for _, tt := range []struct{ name, run, want string }{
14
+ {"real fail reason", `{"fail_reason":{"message":"视频可能不合规,请修改后重试","code":12023}}`, "视频可能不合规,请修改后重试 (error_code=12023)"},
15
+ {"message only", `{"fail_reason":{"message":"reason"}}`, "reason"},
16
+ {"zero code", `{"fail_reason":{"message":"reason","code":0}}`, "reason"},
17
+ {"code only", `{"fail_reason":{"code":12023}}`, "error_code=12023"},
18
+ {"empty reason", `{"fail_reason":{"message":" ","code":0}}`, ""},
19
+ {"null reason", `{"fail_reason":null}`, ""},
20
+ {"structured reason wins", `{"fail_reason":{"message":"real reason","code":12023},"error_message":"legacy"}`, "real reason (error_code=12023)"},
21
+ {"legacy message fallback", `{"fail_reason":{"code":12023},"error_message":"legacy"}`, "legacy"},
22
+ {"error_msg fallback", `{"error_msg":"legacy"}`, "legacy"},
23
+ {"errmsg fallback", `{"errmsg":"legacy"}`, "legacy"},
24
+ {"artifact reason", `{"entry_list":[{"artifact":{"content":[{"data":{"error_message":"artifact reason","error_code":99}}]}}]}`, "artifact reason (error_code=99)"},
25
+ {"artifact code only", `{"entry_list":[{"artifact":{"content":[{"data":{"error_code":"99"}}]}}]}`, "error_code=99"},
26
+ } {
27
+ t.Run(tt.name, func(t *testing.T) {
28
+ var run queryRun
29
+ if err := json.Unmarshal([]byte(tt.run), &run); err != nil {
30
+ t.Fatal(err)
31
+ }
32
+ if got := extractQueryErrorMessage(run); got != tt.want {
33
+ t.Fatalf("got %q want %q", got, tt.want)
34
+ }
35
+ })
36
+ }
37
+ }
38
+
39
+ type queryErrorClient struct{ err error }
40
+
41
+ func (c queryErrorClient) SendRequest(context.Context, string, any, any) error { return c.err }
42
+ func (c queryErrorClient) SendRequestWithHeaders(context.Context, string, any, map[string]string, any) error {
43
+ return c.err
44
+ }
45
+ func (c queryErrorClient) SendMultipartRequest(context.Context, string, map[string]string, common.MultipartFile, any) error {
46
+ return c.err
47
+ }
48
+
49
+ func TestQueryResultTransportFailure(t *testing.T) {
50
+ for _, cause := range []error{context.DeadlineExceeded, context.Canceled, errors.New("connection reset")} {
51
+ t.Run(cause.Error(), func(t *testing.T) {
52
+ opts := &QueryResultOptions{ThreadID: "thread", RunID: "run", DownloadDir: t.TempDir()}
53
+ result, err := QueryResult(context.Background(), opts, &common.Runner{Client: queryErrorClient{cause}})
54
+ if result != nil || !errors.Is(err, cause) {
55
+ t.Fatalf("result=%#v err=%v", result, err)
56
+ }
57
+ result = NewQueryResultError(err, opts)
58
+ if result.Status != "unknown" || result.Completed || result.ErrorMessage == "" {
59
+ t.Fatalf("result=%#v", result)
60
+ }
61
+ })
62
+ }
63
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pippit-dev/cli",
3
- "version": "1.0.51",
3
+ "version": "1.0.53",
4
4
  "description": "Pippit CLI",
5
5
  "bin": {
6
6
  "pippit-tool-cli": "scripts/run.js"
@@ -32,6 +32,7 @@ metadata:
32
32
  | 用户意图 | CLI | 必读文档 |
33
33
  | --- | --- | --- |
34
34
  | 查看登录、登录、退出或切换账号 | `status` / `login` / `logout` | [授权](commands/auth.md) |
35
+ | 查询或修改当前账号的 AIGC 水印开关 | `watermark [on\|off]` | [账号水印设置](commands/watermark.md) |
35
36
  | 新建短剧、续写、改写、回复后端问题 | `short-drama +submit-run` | [提交任务](commands/submit-run.md) |
36
37
  | 上传本地参考剧本 | `short-drama +upload-file` | [上传文件](commands/upload-file.md) |
37
38
  | 查询角色、场景、外观、画布、分镜等业务内容 | 同一会话 `short-drama +submit-run`,再用 `get-thread` 获取回答 | [业务内容查询](workflows/creation.md#业务内容查询) |
@@ -0,0 +1,15 @@
1
+ # 账号 AIGC 水印设置
2
+
3
+ 先用 `pippit-tool-cli watermark --help` 检查命令。缺失时按[安装说明](../scripts/install.md)通过宿主升级,独立 CLI 可执行 `update`;改用升级后的绝对路径重查。
4
+
5
+ ```bash
6
+ pippit-tool-cli watermark # 查询
7
+ pippit-tool-cli watermark off # 关闭
8
+ pippit-tool-cli watermark on # 保留
9
+ ```
10
+
11
+ 返回 `{"enabled":true}` 或 `{"enabled":false}`,`true` 表示保留。设置后返回服务端回读状态。
12
+
13
+ 设置与 Web 同步,按当前账号持续生效,与团队选择无关。用户明确要求修改账号设置时才执行 `on/off`;只要求本次视频无水印时,先确认是否接受修改账号默认值。
14
+
15
+ 此开关控制 AIGC 水印,品牌水印与最终下载权益由服务端决定。修改报错或结果未知时先查询确认。
@@ -45,6 +45,7 @@ metadata:
45
45
  | 去除已有视频字幕 | `erase-video-subtitle` | [擦字幕](commands/erase-video-subtitle.md) |
46
46
  | 查询已有任务进度、下载生成结果 | `query-result` | [查询结果](commands/query-result.md) |
47
47
  | 查询当前空间积分余额、剩余 credits | `get-credit-balance` | [积分](commands/get-credit-balance.md) |
48
+ | 查询或修改当前账号的 AIGC 水印开关 | `watermark [on\|off]` | [账号水印设置](commands/watermark.md) |
48
49
 
49
50
  - 普通生图、生视频也走对应生成命令,无需用户额外声明“模型直出”。
50
51
  - 明确要求修改现有画布或其中节点时优先走 Canvas;普通生图、生视频不自动创建画布。“修改节点提示词”只修改配置,不隐含生成;指定节点生成或导出须先确认当前命令目录有对应能力。