@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.
- package/README.md +19 -1
- package/checksums.txt +6 -6
- package/cmd/generate_video/query_result.go +5 -1
- package/cmd/generate_video_test.go +138 -93
- package/cmd/query_result_branches_test.go +265 -0
- package/cmd/root.go +1 -0
- package/cmd/space.go +1 -1
- package/cmd/testdata/query-result/recorded-failure.json +6 -0
- package/cmd/watermark.go +38 -0
- package/dist/checksums.txt +6 -6
- package/internal/commandnames/names.go +1 -0
- package/internal/common/get_thread.go +9 -5
- package/internal/common/get_thread_test.go +36 -0
- package/internal/common/watermark.go +54 -0
- package/internal/config/config.go +2 -0
- package/internal/generate_video/query_result.go +115 -59
- package/internal/generate_video/query_result_branches_test.go +63 -0
- package/package.json +1 -1
- package/skills/short-drama/SKILL.md +1 -0
- package/skills/short-drama/commands/watermark.md +15 -0
- package/skills/xyq-nest-skill/SKILL.md +1 -0
- package/skills/xyq-nest-skill/commands/query-result.md +12 -2
- package/skills/xyq-nest-skill/commands/watermark.md +15 -0
- package/skills/xyq-nest-skill/workflows/async-delivery.md +3 -3
package/cmd/watermark.go
ADDED
|
@@ -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
|
+
}
|
package/dist/checksums.txt
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
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
|
|
@@ -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
|
|
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
|
-
|
|
72
|
-
|
|
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
|
|
18
|
-
failedRunState
|
|
19
|
-
canceledRunState
|
|
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
|
|
26
|
-
statusSucceeded
|
|
27
|
-
statusFailed
|
|
28
|
-
statusCanceled
|
|
29
|
-
statusUnknown
|
|
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:
|
|
42
|
-
7: statusRunning,
|
|
43
|
-
8: statusRunning,
|
|
44
|
-
9:
|
|
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
|
|
104
|
-
State int
|
|
105
|
-
ErrorMessage string
|
|
106
|
-
ErrorMsg string
|
|
107
|
-
Errmsg string
|
|
108
|
-
|
|
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,
|
|
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
|
-
|
|
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
|
|
207
|
+
return result, fmt.Errorf("下载失败:未找到可下载的产物")
|
|
191
208
|
}
|
|
192
209
|
|
|
193
210
|
downloadDir, err := common.ExpandPath(opts.DownloadDir)
|
|
194
211
|
if err != nil {
|
|
195
|
-
return
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
263
|
+
result.Images = append(result.Images, QueryResultImage{
|
|
249
264
|
DownloadURL: image.DownloadURL,
|
|
250
265
|
OutputPath: actualOutputPath,
|
|
251
266
|
})
|
|
252
267
|
}
|
|
253
268
|
|
|
254
|
-
return
|
|
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
|
-
//
|
|
265
|
-
//
|
|
266
|
-
//
|
|
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
|
|
283
|
-
//
|
|
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
|
@@ -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;普通生图、生视频不自动创建画布。“修改节点提示词”只修改配置,不隐含生成;指定节点生成或导出须先确认当前命令目录有对应能力。
|