@pippit-dev/cli 1.0.17 → 1.0.18

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (40) hide show
  1. package/README.md +23 -2
  2. package/checksums.txt +6 -6
  3. package/cmd/auth/auth.go +136 -142
  4. package/cmd/auth/auth_test.go +134 -0
  5. package/cmd/canvas/canvas.go +235 -0
  6. package/cmd/canvas/canvas_test.go +126 -0
  7. package/cmd/root.go +29 -4
  8. package/cmd/root_test.go +60 -0
  9. package/cmd/short_drama_test.go +4 -7
  10. package/cmd/update/update.go +34 -5
  11. package/cmd/update/update_test.go +84 -0
  12. package/internal/auth/auth_test.go +813 -0
  13. package/internal/auth/browser_darwin.go +14 -0
  14. package/internal/auth/browser_env.go +35 -0
  15. package/internal/auth/browser_linux.go +14 -0
  16. package/internal/auth/browser_windows.go +14 -0
  17. package/internal/auth/identity.go +89 -0
  18. package/internal/auth/loopback.go +324 -0
  19. package/internal/auth/manager.go +338 -144
  20. package/internal/auth/store.go +303 -0
  21. package/internal/auth/store_file_unix.go +176 -0
  22. package/internal/auth/store_file_windows.go +11 -0
  23. package/internal/auth/types.go +72 -0
  24. package/internal/canvas/allocate.go +70 -0
  25. package/internal/canvas/apply.go +203 -0
  26. package/internal/canvas/canvas_test.go +446 -0
  27. package/internal/canvas/create.go +380 -0
  28. package/internal/canvas/get.go +156 -0
  29. package/internal/canvas/types.go +68 -0
  30. package/internal/canvas/upload.go +250 -0
  31. package/internal/common/access_key.go +42 -6
  32. package/internal/common/access_key_test.go +113 -0
  33. package/internal/common/client.go +119 -21
  34. package/internal/common/client_test.go +213 -0
  35. package/internal/common/runner.go +15 -0
  36. package/internal/config/config.go +0 -20
  37. package/internal/config/config_test.go +0 -18
  38. package/package.json +1 -1
  39. package/skills/short-drama/SKILL.md +4 -4
  40. package/skills/xyq-nest-skill/SKILL.md +11 -1
@@ -0,0 +1,380 @@
1
+ package canvas
2
+
3
+ import (
4
+ "context"
5
+ "encoding/json"
6
+ "errors"
7
+ "fmt"
8
+ "net/url"
9
+ "strings"
10
+ "time"
11
+
12
+ "github.com/Pippit-dev/pippit-cli/internal/common"
13
+ )
14
+
15
+ const defaultCreationPollInterval = time.Second
16
+
17
+ var terminalCreationStates = map[string]struct{}{
18
+ "4": {}, "5": {}, "9": {}, "failed": {}, "canceled": {}, "hitl_interrupt": {},
19
+ }
20
+
21
+ type CreateOptions struct {
22
+ Title string
23
+ RequestID string
24
+ Wait bool
25
+ PollInterval time.Duration
26
+ WaitTimeout time.Duration
27
+ }
28
+
29
+ type CreateResult struct {
30
+ RequestID string `json:"request_id"`
31
+ State string `json:"state"`
32
+ ProjectID string `json:"project_id"`
33
+ ThreadID string `json:"thread_id"`
34
+ RunID string `json:"run_id"`
35
+ CanvasAssetID string `json:"canvas_asset_id"`
36
+ OverviewPippitAssetID string `json:"overview_pippit_asset_id,omitempty"`
37
+ WebURL string `json:"web_url"`
38
+ LogID string `json:"log_id,omitempty"`
39
+ PollAttempts int `json:"poll_attempts,omitempty"`
40
+ Warning string `json:"warning,omitempty"`
41
+ }
42
+
43
+ type CreationTerminalError struct {
44
+ State string
45
+ }
46
+
47
+ func (e *CreationTerminalError) Error() string {
48
+ return fmt.Sprintf("canvas creation reached terminal state %q", strings.TrimSpace(e.State))
49
+ }
50
+
51
+ type createRequest struct {
52
+ Surface string `json:"surface"`
53
+ Title string `json:"title,omitempty"`
54
+ RequestID string `json:"request_id"`
55
+ Base map[string]any `json:"Base"`
56
+ }
57
+
58
+ type createData struct {
59
+ State string `json:"state"`
60
+ ProjectID string `json:"project_id"`
61
+ ThreadID string `json:"thread_id"`
62
+ RunID string `json:"run_id"`
63
+ CanvasAssetID string `json:"canvas_asset_id"`
64
+ OverviewPippitAssetID string `json:"overview_pippit_asset_id"`
65
+ WebURL string `json:"web_url"`
66
+ }
67
+
68
+ func Create(ctx context.Context, opts CreateOptions, runner *common.Runner) (*CreateResult, error) {
69
+ client, err := runnerClient(runner, "canvas create")
70
+ if err != nil {
71
+ return nil, err
72
+ }
73
+ if opts.PollInterval < 0 || opts.WaitTimeout < 0 {
74
+ return nil, fmt.Errorf("canvas create polling durations must not be negative")
75
+ }
76
+ requestID := strings.TrimSpace(opts.RequestID)
77
+ if requestID == "" {
78
+ return nil, fmt.Errorf("canvas create request_id is required")
79
+ }
80
+ if len([]rune(requestID)) > 128 {
81
+ return nil, fmt.Errorf("canvas create request_id must not exceed 128 characters")
82
+ }
83
+ title := strings.TrimSpace(opts.Title)
84
+ if len([]rune(title)) > 50 {
85
+ return nil, fmt.Errorf("canvas create title must not exceed 50 characters")
86
+ }
87
+
88
+ var envelope responseEnvelope
89
+ err = client.SendRequest(ctx, CreatePath, createRequest{
90
+ Surface: SurfaceNovel,
91
+ Title: title,
92
+ RequestID: requestID,
93
+ Base: base(),
94
+ }, &envelope)
95
+ if err != nil {
96
+ return nil, fmt.Errorf("canvas create request failed; outcome may be ambiguous, do not retry blindly: %w", err)
97
+ }
98
+ if err := envelope.validate("canvas create"); err != nil {
99
+ return nil, err
100
+ }
101
+ var data createData
102
+ if err := json.Unmarshal(envelope.Data, &data); err != nil {
103
+ return nil, common.NewLogIDError(fmt.Sprintf("canvas create returned invalid data: %v", err), envelope.LogID)
104
+ }
105
+ result := &CreateResult{
106
+ RequestID: requestID,
107
+ State: strings.TrimSpace(data.State),
108
+ ProjectID: strings.TrimSpace(data.ProjectID),
109
+ ThreadID: strings.TrimSpace(data.ThreadID),
110
+ RunID: strings.TrimSpace(data.RunID),
111
+ CanvasAssetID: strings.TrimSpace(data.CanvasAssetID),
112
+ OverviewPippitAssetID: strings.TrimSpace(data.OverviewPippitAssetID),
113
+ WebURL: strings.TrimSpace(data.WebURL),
114
+ LogID: strings.TrimSpace(envelope.LogID),
115
+ }
116
+ if err := validateCreateResult(result); err != nil {
117
+ return nil, common.NewLogIDError(err.Error(), envelope.LogID)
118
+ }
119
+ if !opts.Wait {
120
+ return result, nil
121
+ }
122
+ if result.OverviewPippitAssetID != "" {
123
+ return finalizeReadyCanvas(ctx, result, runner)
124
+ }
125
+
126
+ artifact, attempts, waitErr := waitForCreationArtifact(ctx, runner, result.ThreadID, result.RunID, opts.PollInterval, opts.WaitTimeout)
127
+ result.PollAttempts = attempts
128
+ if waitErr != nil {
129
+ result.Warning = waitErr.Error()
130
+ var terminal *CreationTerminalError
131
+ if errors.As(waitErr, &terminal) {
132
+ result.State = "failed"
133
+ return result, acceptedCreationError(result, waitErr)
134
+ }
135
+ // The create response was already accepted. A timeout or transient
136
+ // get_thread failure must preserve the operation IDs and must not make
137
+ // callers assume it is safe to create another project.
138
+ return result, nil
139
+ }
140
+ if artifact.CanvasAssetID != "" && artifact.CanvasAssetID != result.CanvasAssetID {
141
+ result.State = "failed"
142
+ result.Warning = fmt.Sprintf("canvas create artifact canvas_asset_id mismatch: got %q, want %q", artifact.CanvasAssetID, result.CanvasAssetID)
143
+ return result, acceptedCreationError(result, errors.New(result.Warning))
144
+ }
145
+ result.OverviewPippitAssetID = artifact.OverviewPippitAssetID
146
+ return finalizeReadyCanvas(ctx, result, runner)
147
+ }
148
+
149
+ func finalizeReadyCanvas(ctx context.Context, result *CreateResult, runner *common.Runner) (*CreateResult, error) {
150
+ // Preserve the confirmed overview locator even if the final root visibility
151
+ // check is temporarily unavailable. The state remains creating until that
152
+ // check succeeds, but callers can resume with the canonical project URL.
153
+ result.WebURL = readyCanvasURL(result.WebURL, result.ProjectID, result.OverviewPippitAssetID)
154
+ if _, err := queryAssets(ctx, []string{result.CanvasAssetID}, true, runner); err != nil {
155
+ result.State = StateCreating
156
+ result.Warning = fmt.Sprintf("canvas overview is complete but the root asset is not queryable yet: %v", err)
157
+ return result, nil
158
+ }
159
+ result.State = StateReady
160
+ return result, nil
161
+ }
162
+
163
+ func acceptedCreationError(result *CreateResult, cause error) error {
164
+ return fmt.Errorf(
165
+ "%w; accepted canvas IDs: project_id=%s thread_id=%s run_id=%s canvas_asset_id=%s; do not create again blindly",
166
+ cause, result.ProjectID, result.ThreadID, result.RunID, result.CanvasAssetID,
167
+ )
168
+ }
169
+
170
+ func validateCreateResult(result *CreateResult) error {
171
+ if result == nil {
172
+ return fmt.Errorf("canvas create response is empty")
173
+ }
174
+ missing := make([]string, 0, 4)
175
+ for name, value := range map[string]string{
176
+ "project_id": result.ProjectID, "thread_id": result.ThreadID,
177
+ "run_id": result.RunID, "canvas_asset_id": result.CanvasAssetID,
178
+ } {
179
+ if value == "" {
180
+ missing = append(missing, name)
181
+ }
182
+ }
183
+ if len(missing) != 0 {
184
+ return fmt.Errorf("canvas create response is missing %s", strings.Join(missing, ", "))
185
+ }
186
+ if result.State == "" {
187
+ return fmt.Errorf("canvas create response is missing state")
188
+ }
189
+ if result.WebURL == "" {
190
+ return fmt.Errorf("canvas create response is missing web_url")
191
+ }
192
+ return nil
193
+ }
194
+
195
+ type creationArtifact struct {
196
+ OverviewPippitAssetID string
197
+ CanvasAssetID string
198
+ }
199
+
200
+ func waitForCreationArtifact(
201
+ ctx context.Context,
202
+ runner *common.Runner,
203
+ threadID, runID string,
204
+ pollInterval, waitTimeout time.Duration,
205
+ ) (creationArtifact, int, error) {
206
+ if pollInterval <= 0 {
207
+ pollInterval = defaultCreationPollInterval
208
+ }
209
+ if waitTimeout <= 0 {
210
+ waitTimeout = 2 * time.Minute
211
+ }
212
+ waitCtx, cancel := context.WithTimeout(ctx, waitTimeout)
213
+ defer cancel()
214
+ attempts := 0
215
+ for {
216
+ attempts++
217
+ result, err := common.GetThread(waitCtx, &common.GetThreadOptions{ThreadID: threadID, RunID: runID}, runner)
218
+ if err != nil {
219
+ if waitCtx.Err() != nil {
220
+ return creationArtifact{}, attempts, creationWaitError(waitCtx.Err(), waitTimeout)
221
+ }
222
+ return creationArtifact{}, attempts, fmt.Errorf("poll canvas creation: %w", err)
223
+ }
224
+ run := findRunByID(result.RawData, runID)
225
+ if run != nil {
226
+ state := normalizeState(run["state"])
227
+ artifact := findCreationArtifact(run)
228
+ if (state == "3" || state == "completed") && artifact.OverviewPippitAssetID != "" {
229
+ return artifact, attempts, nil
230
+ }
231
+ if _, terminal := terminalCreationStates[state]; terminal {
232
+ return creationArtifact{}, attempts, &CreationTerminalError{State: state}
233
+ }
234
+ }
235
+
236
+ timer := time.NewTimer(pollInterval)
237
+ select {
238
+ case <-waitCtx.Done():
239
+ timer.Stop()
240
+ return creationArtifact{}, attempts, creationWaitError(waitCtx.Err(), waitTimeout)
241
+ case <-timer.C:
242
+ }
243
+ }
244
+ }
245
+
246
+ func creationWaitError(err error, timeout time.Duration) error {
247
+ if errors.Is(err, context.DeadlineExceeded) {
248
+ return fmt.Errorf("canvas creation artifact was not ready within %s", timeout)
249
+ }
250
+ return fmt.Errorf("canvas creation wait canceled: %w", err)
251
+ }
252
+
253
+ func findRunByID(raw json.RawMessage, runID string) map[string]any {
254
+ var value any
255
+ if json.Unmarshal(raw, &value) != nil {
256
+ return nil
257
+ }
258
+ return walkForRun(value, runID, 0)
259
+ }
260
+
261
+ func walkForRun(value any, runID string, depth int) map[string]any {
262
+ if depth > 14 {
263
+ return nil
264
+ }
265
+ switch typed := parseJSONValue(value).(type) {
266
+ case map[string]any:
267
+ if stringValue(firstValue(typed, "run_id", "runId")) == runID {
268
+ return typed
269
+ }
270
+ for _, child := range typed {
271
+ if found := walkForRun(child, runID, depth+1); found != nil {
272
+ return found
273
+ }
274
+ }
275
+ case []any:
276
+ for _, child := range typed {
277
+ if found := walkForRun(child, runID, depth+1); found != nil {
278
+ return found
279
+ }
280
+ }
281
+ }
282
+ return nil
283
+ }
284
+
285
+ func findCreationArtifact(value any) creationArtifact {
286
+ artifact := creationArtifact{}
287
+ walkArtifact(parseJSONValue(value), 0, &artifact)
288
+ return artifact
289
+ }
290
+
291
+ func walkArtifact(value any, depth int, artifact *creationArtifact) {
292
+ if depth > 14 || (artifact.OverviewPippitAssetID != "" && artifact.CanvasAssetID != "") {
293
+ return
294
+ }
295
+ switch typed := parseJSONValue(value).(type) {
296
+ case map[string]any:
297
+ if stringValue(firstValue(typed, "sub_type", "subType")) == overviewPartSubtype {
298
+ data, _ := parseJSONValue(typed["data"]).(map[string]any)
299
+ if data == nil {
300
+ data = map[string]any{}
301
+ }
302
+ artifact.OverviewPippitAssetID = firstNonEmpty(
303
+ stringValue(firstValue(data, "pippit_asset_id", "pippitAssetId")),
304
+ stringValue(firstValue(typed, "pippit_asset_id", "pippitAssetId")),
305
+ )
306
+ artifact.CanvasAssetID = stringValue(firstValue(data, "canvas_asset_id", "canvasAssetId"))
307
+ }
308
+ for _, child := range typed {
309
+ walkArtifact(child, depth+1, artifact)
310
+ }
311
+ case []any:
312
+ for _, child := range typed {
313
+ walkArtifact(child, depth+1, artifact)
314
+ }
315
+ }
316
+ }
317
+
318
+ func parseJSONValue(value any) any {
319
+ text, ok := value.(string)
320
+ if !ok {
321
+ return value
322
+ }
323
+ text = strings.TrimSpace(text)
324
+ if text == "" || (text[0] != '{' && text[0] != '[') {
325
+ return value
326
+ }
327
+ var parsed any
328
+ if json.Unmarshal([]byte(text), &parsed) == nil {
329
+ return parsed
330
+ }
331
+ return value
332
+ }
333
+
334
+ func firstValue(values map[string]any, keys ...string) any {
335
+ for _, key := range keys {
336
+ if value, ok := values[key]; ok {
337
+ return value
338
+ }
339
+ }
340
+ return nil
341
+ }
342
+
343
+ func firstNonEmpty(values ...string) string {
344
+ for _, value := range values {
345
+ if value = strings.TrimSpace(value); value != "" {
346
+ return value
347
+ }
348
+ }
349
+ return ""
350
+ }
351
+
352
+ func stringValue(value any) string {
353
+ switch typed := value.(type) {
354
+ case string:
355
+ return strings.TrimSpace(typed)
356
+ case json.Number:
357
+ return typed.String()
358
+ case float64:
359
+ return fmt.Sprintf("%.0f", typed)
360
+ default:
361
+ return ""
362
+ }
363
+ }
364
+
365
+ func normalizeState(value any) string {
366
+ return strings.ToLower(stringValue(value))
367
+ }
368
+
369
+ func readyCanvasURL(raw, projectID, overviewID string) string {
370
+ parsed, err := url.Parse(strings.TrimSpace(raw))
371
+ if err != nil || parsed.Scheme == "" || parsed.Host == "" {
372
+ parsed, _ = url.Parse("https://xyq.jianying.com/novel/detail/canvas")
373
+ }
374
+ query := parsed.Query()
375
+ query.Set("projectId", projectID)
376
+ query.Set("overviewPippitAssetId", overviewID)
377
+ query.Del("canvasId")
378
+ parsed.RawQuery = query.Encode()
379
+ return parsed.String()
380
+ }
@@ -0,0 +1,156 @@
1
+ package canvas
2
+
3
+ import (
4
+ "context"
5
+ "encoding/json"
6
+ "fmt"
7
+ "strings"
8
+
9
+ "github.com/Pippit-dev/pippit-cli/internal/common"
10
+ )
11
+
12
+ type GetOptions struct {
13
+ AssetIDs []string
14
+ }
15
+
16
+ type GetResult struct {
17
+ RequestedAssetIDs []string `json:"requested_asset_ids"`
18
+ Assets []json.RawMessage `json:"assets"`
19
+ LogID string `json:"log_id,omitempty"`
20
+ }
21
+
22
+ type queryRequest struct {
23
+ PippitAssetIDs []string `json:"pippit_asset_ids"`
24
+ Base map[string]any `json:"Base"`
25
+ }
26
+
27
+ func Get(ctx context.Context, opts GetOptions, runner *common.Runner) (*GetResult, error) {
28
+ assetIDs, err := normalizeAssetIDs(opts.AssetIDs)
29
+ if err != nil {
30
+ return nil, err
31
+ }
32
+ return queryAssets(ctx, assetIDs, true, runner)
33
+ }
34
+
35
+ func queryAssets(ctx context.Context, assetIDs []string, requireAll bool, runner *common.Runner) (*GetResult, error) {
36
+ client, err := runnerClient(runner, "canvas get")
37
+ if err != nil {
38
+ return nil, err
39
+ }
40
+ var envelope responseEnvelope
41
+ if err := client.SendRequest(ctx, QueryPath, queryRequest{PippitAssetIDs: assetIDs, Base: base()}, &envelope); err != nil {
42
+ return nil, fmt.Errorf("canvas get request failed: %w", err)
43
+ }
44
+ if err := envelope.validate("canvas get"); err != nil {
45
+ return nil, err
46
+ }
47
+ assets, err := assetsFromData(envelope.Data)
48
+ if err != nil {
49
+ return nil, common.NewLogIDError(fmt.Sprintf("canvas get returned invalid data: %v", err), envelope.LogID)
50
+ }
51
+
52
+ byID := make(map[string]json.RawMessage, len(assets))
53
+ for _, asset := range assets {
54
+ assetID, err := assetIDFromRaw(asset)
55
+ if err != nil {
56
+ return nil, common.NewLogIDError(fmt.Sprintf("canvas get returned invalid asset: %v", err), envelope.LogID)
57
+ }
58
+ if _, duplicate := byID[assetID]; duplicate {
59
+ return nil, common.NewLogIDError(fmt.Sprintf("canvas get returned duplicate asset %q", assetID), envelope.LogID)
60
+ }
61
+ byID[assetID] = asset
62
+ }
63
+
64
+ ordered := make([]json.RawMessage, 0, len(assetIDs))
65
+ missing := make([]string, 0)
66
+ for _, assetID := range assetIDs {
67
+ asset, ok := byID[assetID]
68
+ if !ok {
69
+ missing = append(missing, assetID)
70
+ continue
71
+ }
72
+ ordered = append(ordered, asset)
73
+ }
74
+ if requireAll && len(missing) != 0 {
75
+ return nil, common.NewLogIDError(
76
+ fmt.Sprintf("canvas get did not return requested assets: %s", strings.Join(missing, ", ")),
77
+ envelope.LogID,
78
+ )
79
+ }
80
+ return &GetResult{
81
+ RequestedAssetIDs: append([]string(nil), assetIDs...),
82
+ Assets: ordered,
83
+ LogID: strings.TrimSpace(envelope.LogID),
84
+ }, nil
85
+ }
86
+
87
+ func normalizeAssetIDs(values []string) ([]string, error) {
88
+ if len(values) == 0 {
89
+ return nil, fmt.Errorf("at least one canvas asset_id is required")
90
+ }
91
+ result := make([]string, 0, len(values))
92
+ seen := make(map[string]struct{}, len(values))
93
+ for index, value := range values {
94
+ value = strings.TrimSpace(value)
95
+ if value == "" {
96
+ return nil, fmt.Errorf("canvas asset_id at index %d is empty", index)
97
+ }
98
+ if _, duplicate := seen[value]; duplicate {
99
+ return nil, fmt.Errorf("canvas asset_id %q is duplicated", value)
100
+ }
101
+ seen[value] = struct{}{}
102
+ result = append(result, value)
103
+ }
104
+ return result, nil
105
+ }
106
+
107
+ func assetsFromData(raw json.RawMessage) ([]json.RawMessage, error) {
108
+ var data map[string]json.RawMessage
109
+ if len(raw) == 0 || string(raw) == "null" {
110
+ return nil, fmt.Errorf("data is missing")
111
+ }
112
+ if err := json.Unmarshal(raw, &data); err != nil {
113
+ return nil, err
114
+ }
115
+ assetsRaw, ok := rawField(data, "Assets", "assets")
116
+ if !ok {
117
+ return nil, fmt.Errorf("data.assets is missing")
118
+ }
119
+ var assets []json.RawMessage
120
+ if err := json.Unmarshal(assetsRaw, &assets); err != nil {
121
+ return nil, fmt.Errorf("decode data.assets: %w", err)
122
+ }
123
+ if assets == nil {
124
+ assets = []json.RawMessage{}
125
+ }
126
+ return assets, nil
127
+ }
128
+
129
+ func assetIDFromRaw(raw json.RawMessage) (string, error) {
130
+ var asset map[string]json.RawMessage
131
+ if err := json.Unmarshal(raw, &asset); err != nil {
132
+ return "", err
133
+ }
134
+ value, ok := rawField(asset, "PippitAssetID", "pippit_asset_id", "pippitAssetId")
135
+ if !ok {
136
+ return "", fmt.Errorf("asset is missing pippit_asset_id")
137
+ }
138
+ var assetID string
139
+ if err := json.Unmarshal(value, &assetID); err != nil {
140
+ return "", fmt.Errorf("pippit_asset_id must be a JSON string")
141
+ }
142
+ assetID = strings.TrimSpace(assetID)
143
+ if assetID == "" {
144
+ return "", fmt.Errorf("pippit_asset_id is empty")
145
+ }
146
+ return assetID, nil
147
+ }
148
+
149
+ func rawField(values map[string]json.RawMessage, keys ...string) (json.RawMessage, bool) {
150
+ for _, key := range keys {
151
+ if value, ok := values[key]; ok {
152
+ return value, true
153
+ }
154
+ }
155
+ return nil, false
156
+ }
@@ -0,0 +1,68 @@
1
+ package canvas
2
+
3
+ import (
4
+ "encoding/json"
5
+ "fmt"
6
+ "net/url"
7
+ "strings"
8
+
9
+ "github.com/Pippit-dev/pippit-cli/internal/common"
10
+ )
11
+
12
+ const (
13
+ CreatePath = "/api/biz/v1/skill/canvas/create"
14
+ QueryPath = "/api/biz/v1/skill/canvas/query"
15
+ AllocatePath = "/api/biz/v1/skill/canvas/batch_generate_asset_id"
16
+ ApplyPath = "/api/biz/v1/skill/canvas/batch_patch_asset"
17
+ UploadPath = "/api/biz/v1/skill/upload_file"
18
+ )
19
+
20
+ const (
21
+ SurfaceNovel = "novel"
22
+ StateCreating = "creating"
23
+ StateProcessing = "processing"
24
+ StateReady = "ready"
25
+ overviewPartSubtype = "biz/x_data_novel_script_overview"
26
+ )
27
+
28
+ type responseEnvelope struct {
29
+ Ret string `json:"ret"`
30
+ Errmsg string `json:"errmsg"`
31
+ LogID string `json:"log_id"`
32
+ Data json.RawMessage `json:"data"`
33
+ }
34
+
35
+ func (r responseEnvelope) validate(operation string) error {
36
+ if r.Ret == "0" {
37
+ return nil
38
+ }
39
+ message := strings.TrimSpace(r.Errmsg)
40
+ if message == "" {
41
+ message = "unknown error"
42
+ }
43
+ return common.NewLogIDError(
44
+ fmt.Sprintf("%s failed: ret=%q errmsg=%s", operation, r.Ret, message),
45
+ r.LogID,
46
+ )
47
+ }
48
+
49
+ func base() map[string]any {
50
+ return map[string]any{}
51
+ }
52
+
53
+ func runnerClient(runner *common.Runner, operation string) (common.Client, error) {
54
+ if runner == nil || runner.Client == nil {
55
+ return nil, fmt.Errorf("%s client is missing", operation)
56
+ }
57
+ return runner.Client, nil
58
+ }
59
+
60
+ func pathWithProjectID(path, projectID string) string {
61
+ projectID = strings.TrimSpace(projectID)
62
+ if projectID == "" {
63
+ return path
64
+ }
65
+ query := url.Values{}
66
+ query.Set("project_id", projectID)
67
+ return path + "?" + query.Encode()
68
+ }