- 用户域:注册/登录(JWT)/修改密码/个人资料 - 照片/衣橱/身形/化身:上传存储 + 3D 化身模板匹配 - 穿搭生成:天气(高德+和风+缓存) → 规则预筛 → LLM 规划(1次调用) → 规则评分(5维100分制) → 全低分触发 LLM 兜底创作 → 异步任务状态机 - 效果图:选主方案后异步生成 3 视角(mock/wanx 供应商 + 内容 hash 缓存 + 每日限额) - 商业化:合作门店列表(seed 4 家) - 冒烟:全链路端到端验证通过(mock LLM/天气),23 个 API 端点 Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
250 lines
6.1 KiB
Go
250 lines
6.1 KiB
Go
package common
|
|
|
|
import (
|
|
"encoding/base64"
|
|
"encoding/json"
|
|
"fmt"
|
|
"os"
|
|
"strings"
|
|
)
|
|
|
|
// ImageFileToBase64 reads an image file and returns a data:image/...;base64 string.
|
|
func ImageFileToBase64(path string) (string, error) {
|
|
data, err := os.ReadFile(path)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
ext := strings.ToLower(pathExt(path))
|
|
mime := "image/png"
|
|
switch ext {
|
|
case ".jpg", ".jpeg":
|
|
mime = "image/jpeg"
|
|
case ".gif":
|
|
mime = "image/gif"
|
|
case ".webp":
|
|
mime = "image/webp"
|
|
}
|
|
return "data:" + mime + ";base64," + base64.StdEncoding.EncodeToString(data), nil
|
|
}
|
|
|
|
// pathExt extracts the extension from a path.
|
|
func pathExt(path string) string {
|
|
for i := len(path) - 1; i >= 0 && path[i] != '/'; i-- {
|
|
if path[i] == '.' {
|
|
return path[i:]
|
|
}
|
|
}
|
|
return ""
|
|
}
|
|
|
|
// BuildSchemaRequest validates input values against a JSON schema definition,
|
|
// fills in default values for missing optional fields,
|
|
// and returns the result matching the schema's nested structure.
|
|
//
|
|
// The schema format follows test.json convention:
|
|
//
|
|
// {
|
|
// "section": {
|
|
// "field_name": {
|
|
// "type": "string|integer|number|boolean|array|object",
|
|
// "required": true|false,
|
|
// "default": value,
|
|
// "enum": [...],
|
|
// "min": number,
|
|
// "max": number,
|
|
// "max_chars": number,
|
|
// "min_items": number,
|
|
// "max_items": number
|
|
// }
|
|
// }
|
|
// }
|
|
//
|
|
// input is a flat map like {"prompt": "hello", "duration": 5}.
|
|
// Fields not present in input but with a "default" in the schema are filled automatically.
|
|
// Nodes without "type" are treated as grouping sections and recursed into.
|
|
// When validate is false, required/type/range/enum checks are skipped (only structure + defaults).
|
|
func BuildSchemaRequest(schema map[string]any, input map[string]any, validate bool) (map[string]any, error) {
|
|
result := make(map[string]any)
|
|
for key, val := range schema {
|
|
fieldDef, ok := val.(map[string]any)
|
|
if !ok {
|
|
result[key] = val
|
|
continue
|
|
}
|
|
if _, hasType := fieldDef["type"]; hasType {
|
|
processed, err := processField(key, fieldDef, input, validate)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if processed != nil {
|
|
result[key] = processed
|
|
}
|
|
continue
|
|
}
|
|
nested, err := BuildSchemaRequest(fieldDef, input, validate)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if len(nested) > 0 {
|
|
result[key] = nested
|
|
}
|
|
}
|
|
return result, nil
|
|
}
|
|
|
|
func processField(name string, def map[string]any, input map[string]any, validate bool) (any, error) {
|
|
fieldType, _ := def["type"].(string)
|
|
required, _ := def["required"].(bool)
|
|
|
|
rawVal, exists := input[name]
|
|
if !exists {
|
|
if validate && required {
|
|
return nil, fmt.Errorf("%s", def["description"])
|
|
}
|
|
if dflt, ok := def["default"]; ok {
|
|
return convertDefault(dflt, fieldType), nil
|
|
}
|
|
return nil, nil
|
|
}
|
|
|
|
if !validate {
|
|
return rawVal, nil
|
|
}
|
|
|
|
switch fieldType {
|
|
case "string":
|
|
s, ok := rawVal.(string)
|
|
if !ok {
|
|
return nil, fmt.Errorf("'%s' must be a string", name)
|
|
}
|
|
if maxChars, ok := def["max_chars"].(float64); ok && len([]rune(s)) > int(maxChars) {
|
|
return nil, fmt.Errorf("'%s' exceeds max length of %d", name, int(maxChars))
|
|
}
|
|
if enum, ok := def["enum"].([]any); ok && len(enum) > 0 {
|
|
if !containsValue(enum, s) {
|
|
return nil, fmt.Errorf("'%s' must be one of %v", name, enum)
|
|
}
|
|
}
|
|
return s, nil
|
|
|
|
case "integer":
|
|
v, err := toInt(rawVal)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("'%s' must be an integer", name)
|
|
}
|
|
if minVal, ok := def["min"].(float64); ok && v < int(minVal) {
|
|
return nil, fmt.Errorf("'%s' must be >= %d", name, int(minVal))
|
|
}
|
|
if maxVal, ok := def["max"].(float64); ok && v > int(maxVal) {
|
|
return nil, fmt.Errorf("'%s' must be <= %d", name, int(maxVal))
|
|
}
|
|
return v, nil
|
|
|
|
case "number":
|
|
v, ok := rawVal.(float64)
|
|
if !ok {
|
|
if iv, err := toInt(rawVal); err == nil {
|
|
v = float64(iv)
|
|
} else {
|
|
return nil, fmt.Errorf("'%s' must be a number", name)
|
|
}
|
|
}
|
|
if minVal, ok := def["min"].(float64); ok && v < minVal {
|
|
return nil, fmt.Errorf("'%s' must be >= %v", name, minVal)
|
|
}
|
|
if maxVal, ok := def["max"].(float64); ok && v > maxVal {
|
|
return nil, fmt.Errorf("'%s' must be <= %v", name, maxVal)
|
|
}
|
|
return v, nil
|
|
|
|
case "boolean":
|
|
_, ok := rawVal.(bool)
|
|
if !ok {
|
|
return nil, fmt.Errorf("'%s' must be a boolean", name)
|
|
}
|
|
return rawVal, nil
|
|
|
|
case "array":
|
|
arr, ok := rawVal.([]any)
|
|
if !ok {
|
|
return nil, fmt.Errorf("'%s' must be an array", name)
|
|
}
|
|
if minItems, ok := def["min_items"].(float64); ok && len(arr) < int(minItems) {
|
|
return nil, fmt.Errorf("'%s' must have at least %d items", name, int(minItems))
|
|
}
|
|
if maxItems, ok := def["max_items"].(float64); ok && len(arr) > int(maxItems) {
|
|
return nil, fmt.Errorf("'%s' must have at most %d items", name, int(maxItems))
|
|
}
|
|
if itemsDef, ok := def["items"].(map[string]any); ok {
|
|
items, err := processArrayItems(arr, itemsDef, validate)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("'%s': %w", name, err)
|
|
}
|
|
return items, nil
|
|
}
|
|
return arr, nil
|
|
}
|
|
|
|
return rawVal, nil
|
|
}
|
|
|
|
func processArrayItems(arr []any, itemsDef map[string]any, validate bool) ([]any, error) {
|
|
itemType, _ := itemsDef["type"].(string)
|
|
if itemType != "object" {
|
|
return arr, nil
|
|
}
|
|
props, _ := itemsDef["properties"].(map[string]any)
|
|
if props == nil {
|
|
return arr, nil
|
|
}
|
|
|
|
result := make([]any, len(arr))
|
|
for i, item := range arr {
|
|
itemMap, ok := item.(map[string]any)
|
|
if !ok {
|
|
result[i] = item
|
|
continue
|
|
}
|
|
processed, err := BuildSchemaRequest(props, itemMap, validate)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("item[%d]: %w", i, err)
|
|
}
|
|
result[i] = processed
|
|
}
|
|
return result, nil
|
|
}
|
|
|
|
func toInt(v any) (int, error) {
|
|
switch val := v.(type) {
|
|
case float64:
|
|
return int(val), nil
|
|
case int:
|
|
return val, nil
|
|
case int64:
|
|
return int(val), nil
|
|
case json.Number:
|
|
n, err := val.Int64()
|
|
return int(n), err
|
|
default:
|
|
return 0, fmt.Errorf("cannot convert %T to int", v)
|
|
}
|
|
}
|
|
|
|
func convertDefault(dflt any, fieldType string) any {
|
|
if fieldType == "integer" {
|
|
if f, ok := dflt.(float64); ok {
|
|
return int(f)
|
|
}
|
|
}
|
|
return dflt
|
|
}
|
|
|
|
func containsValue(arr []any, val any) bool {
|
|
for _, v := range arr {
|
|
if v == val {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|