@pippit-dev/cli 1.0.31 → 1.0.33

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.
@@ -21,12 +21,12 @@ func description(t *testing.T, raw string) map[string]json.RawMessage {
21
21
  }
22
22
 
23
23
  func TestDescriptionCLIParametersAndRawPreservation(t *testing.T) {
24
- raw := `{"key":"MiniMax-H3","is_default":true,"supported_ratio_list":[0,2,13,3,4,5,6],"default_ratio":3,"config_key":"internal",
24
+ raw := `{"key":"MiniMax-H3","kind":"video","is_default":true,"supported_ratio_list":[0,2,13,3,4,5,6],"default_ratio":3,"config_key":"internal",
25
25
  "future_field":9007199254740993,"audio_total_limit":0,"max_image_size":31457280,"min_video_duration":2000,
26
26
  "supported_duration_list":[{"value":999}],"default_duration_value":999,
27
27
  "parameter_config":{"dimensions":[
28
- {"key":"duration","default_value":"10","range_config":{"min_value":4,"max_value":15,"step":1}},
29
- {"key":"resolution","default_value":" 768P ","option_list":[{"value":"768p"},{"value":"2k"},{"value":"4k","disabled":true}]},
28
+ {"key":"duration","label":"视频时长","description":"生成的视频时长","required_field":true,"default_value":"10","range_config":{"min_value":4,"max_value":15,"step":1}},
29
+ {"key":"resolution","label":"视频分辨率","description":"输出清晰度","required_field":false,"default_value":" 768P ","option_list":[{"value":"768p"},{"value":"2k"},{"value":"4k","disabled":true}]},
30
30
  {"key":"seed","default_value":"random"}],"need_available_combinations":true}}
31
31
  `
32
32
  out := description(t, raw)
@@ -46,8 +46,10 @@ func TestDescriptionCLIParametersAndRawPreservation(t *testing.T) {
46
46
  t.Fatalf("ratio=%+v", ratio)
47
47
  }
48
48
  var resolution struct {
49
- Options []string
50
- Default string
49
+ Options []string
50
+ Default string
51
+ Label, Description string
52
+ Required *bool `json:"required_field"`
51
53
  }
52
54
  if err := json.Unmarshal(out["resolution"], &resolution); err != nil {
53
55
  t.Fatal(err)
@@ -55,9 +57,14 @@ func TestDescriptionCLIParametersAndRawPreservation(t *testing.T) {
55
57
  if !reflect.DeepEqual(resolution.Options, []string{"768p", "2k"}) || resolution.Default != "768p" {
56
58
  t.Fatalf("resolution=%+v", resolution)
57
59
  }
60
+ if resolution.Label != "视频分辨率" || resolution.Description != "输出清晰度" || resolution.Required == nil || *resolution.Required {
61
+ t.Fatalf("video resolution metadata lost: %+v", resolution)
62
+ }
58
63
  var duration struct {
59
64
  Min, Max, Step, Default int
60
65
  Unit string
66
+ Label, Description string
67
+ Required *bool `json:"required_field"`
61
68
  }
62
69
  if err := json.Unmarshal(out["duration"], &duration); err != nil {
63
70
  t.Fatal(err)
@@ -65,6 +72,9 @@ func TestDescriptionCLIParametersAndRawPreservation(t *testing.T) {
65
72
  if duration.Min != 4 || duration.Max != 15 || duration.Step != 1 || duration.Default != 10 || duration.Unit != "seconds" {
66
73
  t.Fatalf("duration=%+v", duration)
67
74
  }
75
+ if duration.Label != "视频时长" || duration.Description != "生成的视频时长" || duration.Required == nil || !*duration.Required {
76
+ t.Fatalf("video duration metadata lost: %+v", duration)
77
+ }
68
78
  if string(out["future_field"]) != "9007199254740993" {
69
79
  t.Fatal("integer precision lost")
70
80
  }
@@ -197,3 +207,119 @@ func TestDescriptionCreationModesAndCacheNotMutated(t *testing.T) {
197
207
  t.Fatal("description mutated cached raw config")
198
208
  }
199
209
  }
210
+
211
+ func TestImageDescriptionParametersAndConstraints(t *testing.T) {
212
+ raw := `{"key":"future-image","name":"未来图片模型","report_name":"internal-report-model","kind":"image","is_default":true,
213
+ "supported_ratio_list":[0,2,6,13,1,999],"default_ratio":6,
214
+ "parameter_config":{"dimensions":[
215
+ {"key":"resolution","label":"图片分辨率","required_field":false,"default_value":"2k","option_list":[{"value":"2k","label":"高清"},{"value":"4K","disabled":true}]},
216
+ {"key":"effort","label":"推理强度","description":"计算档位","required_field":true,"default_value":"HIGH","active_when_any":[{"resolution":["2k"]}],"option_list":[{"value":"low"},{"value":"HIGH","label":"高","description":"更多计算"},{"value":"max","disabled":true}]},
217
+ {"key":"future-dimension","option_list":[{"value":"future-choice"}]}],
218
+ "need_available_combinations":true,"combination_dimension_keys":["resolution","effort"],
219
+ "default_combination":{"resolution":"2k","effort":"HIGH"},
220
+ "available_combinations":[{"option_values":{"resolution":"2k","effort":"HIGH"}}]},
221
+ "creation_mode_config":{"modes":[{"key":"reference_generation","enabled":true}]}}
222
+ `
223
+ out := description(t, raw)
224
+ if _, exists := out["key"]; exists || string(out["name"]) != `"未来图片模型"` {
225
+ t.Fatalf("image identity must contain only the display name: %s", out)
226
+ }
227
+ if _, exists := out["report_name"]; exists {
228
+ t.Fatal("internal reporting model name exposed")
229
+ }
230
+ var ratio struct {
231
+ Options []int64
232
+ Default int64
233
+ Labels map[string]string `json:"option_labels"`
234
+ }
235
+ var resolution, effort struct {
236
+ Options []string
237
+ Default string
238
+ Required *bool `json:"required_field"`
239
+ Active json.RawMessage `json:"active_when_any"`
240
+ Label string
241
+ Description string
242
+ }
243
+ for key, target := range map[string]any{"ratio": &ratio, "resolution": &resolution, "effort": &effort} {
244
+ if err := json.Unmarshal(out[key], target); err != nil {
245
+ t.Fatal(err)
246
+ }
247
+ }
248
+ if !reflect.DeepEqual(ratio.Options, []int64{0, 2, 6, 13}) || ratio.Default != 6 || !reflect.DeepEqual(ratio.Labels, map[string]string{"0": "adaptive", "2": "16:9", "6": "1:1", "13": "21:9"}) {
249
+ t.Fatalf("ratio=%+v", ratio)
250
+ }
251
+ if !reflect.DeepEqual(resolution.Options, []string{"2K"}) || resolution.Default != "2K" || resolution.Required == nil || *resolution.Required {
252
+ t.Fatalf("resolution=%+v", resolution)
253
+ }
254
+ if !reflect.DeepEqual(effort.Options, []string{"low", "high"}) || effort.Default != "high" || effort.Required == nil || !*effort.Required || effort.Label != "推理强度" || effort.Description != "计算档位" || string(effort.Active) != `[{"resolution":["2k"]}]` {
255
+ t.Fatalf("effort=%+v", effort)
256
+ }
257
+ var source map[string]json.RawMessage
258
+ if err := json.Unmarshal([]byte(raw), &source); err != nil {
259
+ t.Fatal(err)
260
+ }
261
+ var compact bytes.Buffer
262
+ if err := json.Compact(&compact, source["parameter_config"]); err != nil {
263
+ t.Fatal(err)
264
+ }
265
+ if !bytes.Equal(out["parameter_config"], compact.Bytes()) {
266
+ t.Fatalf("raw dimension metadata or combination rules changed: %s", out["parameter_config"])
267
+ }
268
+ if bytes.Contains(out["creation_mode_config"], []byte("generate_type")) || len(out["creation_modes"]) != 0 {
269
+ t.Fatal("image modes must not acquire video generation selectors")
270
+ }
271
+ if _, exists := out["is_default"]; exists {
272
+ t.Fatal("model default marker exposed")
273
+ }
274
+ }
275
+
276
+ func TestImageDescriptionOptionalEffortAndRatioDimension(t *testing.T) {
277
+ out := description(t, `{"kind":"image","supported_ratio_list":[2],"parameter_config":{"dimensions":[{"key":"ratio","default_value":"13","option_list":[{"value":"13"},{"value":"6"},{"value":"2","disabled":true},{"value":"999"},{"value":"1"}]}]}}`)
278
+ if len(out["effort"]) != 0 || len(out["resolution"]) != 0 {
279
+ t.Fatal("absent capabilities must not be invented")
280
+ }
281
+ var ratio struct {
282
+ Options []int64
283
+ Default int64
284
+ Labels map[string]string `json:"option_labels"`
285
+ }
286
+ if err := json.Unmarshal(out["ratio"], &ratio); err != nil {
287
+ t.Fatal(err)
288
+ }
289
+ if !reflect.DeepEqual(ratio.Options, []int64{13, 6}) || ratio.Default != 13 || !reflect.DeepEqual(ratio.Labels, map[string]string{"13": "21:9", "6": "1:1"}) {
290
+ t.Fatalf("ratio dimension did not override legacy list: %+v", ratio)
291
+ }
292
+ for _, raw := range []string{
293
+ `{"kind":"image","parameter_config":{"dimensions":[{"key":"effort","default_value":"high","option_list":[{"value":"low"},{"value":"high","disabled":true}]}]}}`,
294
+ `{"kind":"image","parameter_config":{"dimensions":[{"key":"effort","option_list":[]}]}}`,
295
+ } {
296
+ out := description(t, raw)
297
+ if len(out["warnings"]) == 0 || bytes.Contains(out["effort"], []byte(`"default"`)) {
298
+ t.Fatalf("invalid effort configuration accepted: %s", out["effort"])
299
+ }
300
+ }
301
+ _, err := describeModel(json.RawMessage(`{"kind":"image","parameter_config":{"dimensions":[{"key":"effort"},{"key":"effort"}]}}`))
302
+ if err == nil {
303
+ t.Fatal("duplicate effort dimension accepted")
304
+ }
305
+ }
306
+
307
+ func TestImageRatioZeroDefaultAndUnavailableValues(t *testing.T) {
308
+ for _, raw := range []string{
309
+ `{"kind":"image","supported_ratio_list":[0,0,3,1,999],"default_ratio":0}`,
310
+ `{"kind":"image","parameter_config":{"dimensions":[{"key":"ratio","default_value":"0","option_list":[{"value":"0"},{"value":"0"},{"value":"3"},{"value":"6","disabled":true},{"value":"1"},{"value":"999"}]}]}}`,
311
+ } {
312
+ out := description(t, raw)
313
+ var ratio struct {
314
+ Options []int64
315
+ Default *int64
316
+ Labels map[string]string `json:"option_labels"`
317
+ }
318
+ if err := json.Unmarshal(out["ratio"], &ratio); err != nil {
319
+ t.Fatal(err)
320
+ }
321
+ if !reflect.DeepEqual(ratio.Options, []int64{0, 3}) || ratio.Default == nil || *ratio.Default != 0 || !reflect.DeepEqual(ratio.Labels, map[string]string{"0": "adaptive", "3": "9:16"}) {
322
+ t.Fatalf("numeric zero default or available ratio values changed: %s", out["ratio"])
323
+ }
324
+ }
325
+ }
@@ -7,6 +7,7 @@ import (
7
7
  "fmt"
8
8
  "os"
9
9
  "path/filepath"
10
+ "sort"
10
11
  "strings"
11
12
  "time"
12
13
 
@@ -16,12 +17,14 @@ import (
16
17
  )
17
18
 
18
19
  const (
19
- Scene = "web_turbo_video_generator"
20
- CacheTTL = 5 * time.Minute
20
+ VideoScene = "web_turbo_video_generator"
21
+ ImageScene = "web_image_agent"
22
+ CacheTTL = 5 * time.Minute
21
23
  )
22
24
 
23
25
  // Catalog preserves the server's model configuration, including unknown fields
24
- // and integer enums. It is never used as a local generation allowlist.
26
+ // and integer enums. Image names are resolved here before submission; admission
27
+ // and parameter validation remain the server's responsibility.
25
28
  type Catalog struct {
26
29
  Scene string `json:"scene"`
27
30
  ConfigKey string `json:"config_key"`
@@ -31,7 +34,7 @@ type Catalog struct {
31
34
  }
32
35
 
33
36
  type Summary struct {
34
- Key string `json:"key"`
37
+ Key string `json:"key,omitempty"`
35
38
  Name string `json:"name"`
36
39
  Kind string `json:"kind"`
37
40
  }
@@ -62,7 +65,22 @@ func NewService(runner *common.Runner) *Service {
62
65
  return &Service{runner: runner, cacheDir: dir, now: time.Now}
63
66
  }
64
67
 
65
- func (s *Service) Get(ctx context.Context, refresh bool) (*Result, error) {
68
+ func sceneForType(modelType string) (string, error) {
69
+ switch modelType {
70
+ case "video":
71
+ return VideoScene, nil
72
+ case "image":
73
+ return ImageScene, nil
74
+ default:
75
+ return "", fmt.Errorf("不支持的模型类型 %q;--type 可选 video 或 image", modelType)
76
+ }
77
+ }
78
+
79
+ func (s *Service) Get(ctx context.Context, modelType string, refresh bool) (*Result, error) {
80
+ scene, err := sceneForType(modelType)
81
+ if err != nil {
82
+ return nil, err
83
+ }
66
84
  if err := ctx.Err(); err != nil {
67
85
  return nil, err
68
86
  }
@@ -79,9 +97,9 @@ func (s *Service) Get(ctx context.Context, refresh bool) (*Result, error) {
79
97
  if s.runner.Config.Paths != nil && s.runner.Config.Paths.GetAvailableModelList != "" {
80
98
  path = s.runner.Config.Paths.GetAvailableModelList
81
99
  }
82
- cachePath := s.cachePath(accessKey, path)
100
+ cachePath := s.cachePath(accessKey, path, scene)
83
101
  if !refresh && cachePath != "" {
84
- if entry := s.readCache(cachePath); entry != nil {
102
+ if entry := s.readCache(cachePath, scene); entry != nil {
85
103
  return &Result{Catalog: entry.Catalog, Cached: true, FetchedAt: entry.FetchedAt}, nil
86
104
  }
87
105
  }
@@ -100,14 +118,14 @@ func (s *Service) Get(ctx context.Context, refresh bool) (*Result, error) {
100
118
  err = s.runner.Client.SendRequest(queryCtx, path, struct {
101
119
  Scene string `json:"scene"`
102
120
  SourceModelKey string `json:"source_model_key"`
103
- }{Scene: Scene}, &resp)
121
+ }{Scene: scene}, &resp)
104
122
  if err != nil {
105
123
  return nil, fmt.Errorf("模型查询失败,请稍后重试(可加 --refresh): %w", err)
106
124
  }
107
125
  if resp.Ret != "0" {
108
126
  return nil, common.NewLogIDError(fmt.Sprintf("模型查询失败,请稍后重试(可加 --refresh): ret=%s errmsg=%s", resp.Ret, resp.Errmsg), resp.LogID)
109
127
  }
110
- if err := validateCatalog(resp.Data); err != nil {
128
+ if err := validateCatalog(resp.Data, scene); err != nil {
111
129
  return nil, common.NewLogIDError("模型配置无效,请稍后重试(可加 --refresh): "+err.Error(), resp.LogID)
112
130
  }
113
131
  currentKey, err := s.runner.Auth.ResolveAccessKey(ctx)
@@ -121,24 +139,24 @@ func (s *Service) Get(ctx context.Context, refresh bool) (*Result, error) {
121
139
  return result, nil
122
140
  }
123
141
 
124
- func (s *Service) cachePath(accessKey, path string) string {
142
+ func (s *Service) cachePath(accessKey, path, scene string) string {
125
143
  if s.cacheDir == "" {
126
144
  return ""
127
145
  }
128
146
  scope, _ := json.Marshal([]string{
129
- "v1", strings.TrimRight(s.runner.Config.BaseURL, "/"), path, Scene,
147
+ "v1", strings.TrimRight(s.runner.Config.BaseURL, "/"), path, scene,
130
148
  version.Current(), accessKey,
131
149
  })
132
150
  return filepath.Join(s.cacheDir, fmt.Sprintf("%x.json", sha256.Sum256(scope)))
133
151
  }
134
152
 
135
- func (s *Service) readCache(path string) *cacheEntry {
153
+ func (s *Service) readCache(path, scene string) *cacheEntry {
136
154
  raw, err := os.ReadFile(path)
137
155
  if err != nil {
138
156
  return nil
139
157
  }
140
158
  var entry cacheEntry
141
- if json.Unmarshal(raw, &entry) != nil || validateCatalog(entry.Catalog) != nil {
159
+ if json.Unmarshal(raw, &entry) != nil || validateCatalog(entry.Catalog, scene) != nil {
142
160
  return nil
143
161
  }
144
162
  age := s.now().Sub(entry.FetchedAt)
@@ -172,20 +190,37 @@ func (s *Service) writeCache(path string, result *Result) error {
172
190
  return os.Rename(f.Name(), path)
173
191
  }
174
192
 
175
- func validateCatalog(catalog *Catalog) error {
176
- if catalog == nil || catalog.Config == nil || catalog.Scene != Scene || catalog.ConfigKey == "" {
193
+ func validateCatalog(catalog *Catalog, scene string) error {
194
+ kind := "video"
195
+ if scene == ImageScene {
196
+ kind = "image"
197
+ } else if scene != VideoScene {
198
+ return fmt.Errorf("不支持的模型场景 %q", scene)
199
+ }
200
+ if catalog == nil || catalog.Config == nil || catalog.Scene != scene || catalog.ConfigKey == "" {
177
201
  return fmt.Errorf("缺少配置或场景不匹配")
178
202
  }
179
203
  seen := make(map[string]bool)
204
+ seenNames := make(map[string]bool)
180
205
  for _, raw := range catalog.Config.Models {
181
206
  var model Summary
182
- if json.Unmarshal(raw, &model) != nil || strings.TrimSpace(model.Key) == "" || model.Kind != "video" {
207
+ if json.Unmarshal(raw, &model) != nil || strings.TrimSpace(model.Key) == "" || model.Kind != kind {
183
208
  return fmt.Errorf("模型条目缺少有效 key 或 kind")
184
209
  }
185
210
  if seen[model.Key] {
211
+ if kind == "image" {
212
+ return fmt.Errorf("图片模型配置包含重复条目")
213
+ }
186
214
  return fmt.Errorf("模型 key 重复: %s", model.Key)
187
215
  }
188
216
  seen[model.Key] = true
217
+ if kind == "image" {
218
+ name := strings.TrimSpace(model.Name)
219
+ if name == "" || strings.EqualFold(name, strings.TrimSpace(model.Key)) || seenNames[name] {
220
+ return fmt.Errorf("图片模型展示名称缺失、与模型标识相同或重复,请刷新模型列表")
221
+ }
222
+ seenNames[name] = true
223
+ }
189
224
  }
190
225
  return nil
191
226
  }
@@ -196,7 +231,10 @@ func (c *Catalog) Search(query string) []Summary {
196
231
  for _, raw := range c.Config.Models {
197
232
  var model Summary
198
233
  _ = json.Unmarshal(raw, &model) // validated at the network/cache boundary
199
- if model.Key == query {
234
+ if c.Scene == ImageScene {
235
+ model.Key = "" // Wire identifiers stay in the raw catalog only.
236
+ model.Name = strings.TrimSpace(model.Name)
237
+ } else if model.Key == query {
200
238
  return []Summary{model}
201
239
  }
202
240
  if query == "" || strings.Contains(strings.ToLower(model.Name), strings.ToLower(query)) || strings.Contains(strings.ToLower(model.Key), strings.ToLower(query)) {
@@ -206,14 +244,78 @@ func (c *Catalog) Search(query string) []Summary {
206
244
  return matches
207
245
  }
208
246
 
209
- func (c *Catalog) Describe(key string) (json.RawMessage, error) {
210
- key = strings.TrimSpace(key)
247
+ func (c *Catalog) findModel(selector string) (json.RawMessage, error) {
248
+ selector = strings.TrimSpace(selector)
249
+ var found json.RawMessage
211
250
  for _, raw := range c.Config.Models {
212
251
  var model Summary
213
252
  _ = json.Unmarshal(raw, &model)
214
- if model.Key == key {
215
- return describeModel(raw)
253
+ value := model.Key
254
+ if c.Scene == ImageScene {
255
+ value = strings.TrimSpace(model.Name)
216
256
  }
257
+ if value != "" && value == selector {
258
+ if found != nil {
259
+ return nil, fmt.Errorf("模型名称重复,请执行 model list --type image --refresh 刷新后重试")
260
+ }
261
+ found = raw
262
+ }
263
+ }
264
+ if found != nil {
265
+ return found, nil
266
+ }
267
+ if c.Scene == ImageScene {
268
+ return nil, fmt.Errorf("未找到该图片模型名称;请执行 model list --type image --refresh,并使用列表中的完整名称")
269
+ }
270
+ return nil, fmt.Errorf("未找到可用模型 %q;请执行 model list --type video --refresh 查看当前模型", selector)
271
+ }
272
+
273
+ func (c *Catalog) Describe(selector string) (json.RawMessage, error) {
274
+ raw, err := c.findModel(selector)
275
+ if err != nil {
276
+ return nil, err
277
+ }
278
+ return describeModel(raw)
279
+ }
280
+
281
+ // ImageModelKey translates the user's exact display name into a wire value.
282
+ // Never guess keys from names or send an unresolved name to the server.
283
+ func (c *Catalog) ImageModelKey(name string) (string, error) {
284
+ if c.Scene != ImageScene {
285
+ return "", fmt.Errorf("图片生成需要图片模型配置")
286
+ }
287
+ raw, err := c.findModel(name)
288
+ if err != nil {
289
+ return "", err
290
+ }
291
+ var model Summary
292
+ _ = json.Unmarshal(raw, &model)
293
+ return model.Key, nil
294
+ }
295
+
296
+ // ImageDisplayMessage keeps known wire identifiers out of server error messages.
297
+ func (c *Catalog) ImageDisplayMessage(message string) string {
298
+ var models []Summary
299
+ for _, raw := range c.Config.Models {
300
+ var model struct {
301
+ Summary
302
+ ReportName string `json:"report_name"`
303
+ }
304
+ _ = json.Unmarshal(raw, &model)
305
+ name := strings.TrimSpace(model.Name)
306
+ if model.Kind == "image" && name != "" {
307
+ for _, value := range []string{model.Key, model.ReportName} {
308
+ if value != "" && value != name {
309
+ models = append(models, Summary{Key: value, Name: name})
310
+ }
311
+ }
312
+ }
313
+ }
314
+ // A key can be a prefix of another key. Replace the longer one first.
315
+ sort.Slice(models, func(i, j int) bool { return len(models[i].Key) > len(models[j].Key) })
316
+ pairs := make([]string, 0, len(models)*2)
317
+ for _, model := range models {
318
+ pairs = append(pairs, model.Key, strings.TrimSpace(model.Name))
217
319
  }
218
- return nil, fmt.Errorf("未找到可用模型 %q;请执行 model list --refresh 查看当前模型", key)
320
+ return strings.NewReplacer(pairs...).Replace(message)
219
321
  }