@pippit-dev/cli 1.0.17 → 1.0.20
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 +41 -2
- package/checksums.txt +6 -6
- package/cmd/auth/auth.go +136 -142
- package/cmd/auth/auth_test.go +134 -0
- package/cmd/canvas/canvas.go +241 -0
- package/cmd/canvas/canvas_test.go +182 -0
- package/cmd/get_credit_balance.go +37 -0
- package/cmd/get_credit_balance_test.go +73 -0
- package/cmd/root.go +31 -5
- package/cmd/root_test.go +69 -0
- package/cmd/short_drama_test.go +5 -7
- package/cmd/update/update.go +34 -5
- package/cmd/update/update_test.go +84 -0
- package/dist/checksums.txt +6 -0
- package/dist/xyq-canvas-command-runtime.cjs +16 -0
- package/dist/xyq-canvas-command-runtime.cjs.LEGAL.txt +599 -0
- package/dist/xyq-canvas-command-runtime.cjs.sha256 +2 -0
- package/internal/auth/auth_test.go +813 -0
- package/internal/auth/browser_darwin.go +14 -0
- package/internal/auth/browser_env.go +35 -0
- package/internal/auth/browser_linux.go +14 -0
- package/internal/auth/browser_windows.go +14 -0
- package/internal/auth/identity.go +89 -0
- package/internal/auth/loopback.go +324 -0
- package/internal/auth/manager.go +338 -144
- package/internal/auth/store.go +303 -0
- package/internal/auth/store_file_unix.go +176 -0
- package/internal/auth/store_file_windows.go +11 -0
- package/internal/auth/types.go +72 -0
- package/internal/canvas/allocate.go +70 -0
- package/internal/canvas/apply.go +279 -0
- package/internal/canvas/canvas_test.go +559 -0
- package/internal/canvas/create.go +380 -0
- package/internal/canvas/get.go +156 -0
- package/internal/canvas/types.go +68 -0
- package/internal/canvas/upload.go +250 -0
- package/internal/common/access_key.go +42 -6
- package/internal/common/access_key_test.go +113 -0
- package/internal/common/client.go +119 -21
- package/internal/common/client_test.go +213 -0
- package/internal/common/get_credit_balance.go +61 -0
- package/internal/common/get_credit_balance_test.go +75 -0
- package/internal/common/runner.go +15 -0
- package/internal/config/config.go +11 -28
- package/internal/config/config_test.go +3 -18
- package/package.json +9 -2
- package/scripts/canvas-command.js +881 -0
- package/scripts/run.js +21 -4
- package/skills/short-drama/SKILL.md +4 -4
- 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
|
+
}
|