Files
slogan/styleagent/imagegen/wanx_client.go
T
adminandClaude Opus 4.7 ef22f7672a feat: slogan-agent MVP 服务端完整实现
- 用户域:注册/登录(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>
2026-07-31 12:15:11 +08:00

137 lines
3.6 KiB
Go

package imagegen
import (
"bytes"
"context"
"encoding/json"
"fmt"
"io"
"net/http"
"time"
)
// wanxClient 通义万相人像写真(image-synthesis 异步接口 + 轮询)
type wanxClient struct {
apiKey string
model string
base string
}
type wanxSubmitReq struct {
Model string `json:"model"`
Input wanxInput `json:"input"`
Parameters map[string]any `json:"parameters,omitempty"`
}
type wanxInput struct {
Prompt string `json:"prompt"`
BaseImageURL string `json:"base_image_url,omitempty"`
BaseImagePath string `json:"base_image_path,omitempty"`
}
type wanxResp struct {
Output struct {
TaskID string `json:"task_id"`
TaskStatus string `json:"task_status"`
Results []struct {
URL string `json:"url"`
} `json:"results"`
} `json:"output"`
Code string `json:"code"`
Message string `json:"message"`
}
type wanxTaskResp struct {
Output struct {
TaskStatus string `json:"task_status"`
Results []struct {
URL string `json:"url"`
} `json:"results"`
} `json:"output"`
Code string `json:"code"`
Message string `json:"message"`
}
// Generate 提交任务并轮询直到完成,失败返回错误(由上层降级 mock)
func (c *wanxClient) Generate(ctx context.Context, req *GenerateReq) (string, error) {
body, err := json.Marshal(wanxSubmitReq{
Model: c.model,
Input: wanxInput{Prompt: buildPrompt(req.Prompt, "", "", req.Angle), BaseImageURL: req.BaseImageURL},
Parameters: map[string]any{"n": 1, "size": "768*1024", "seed": req.Seed},
})
if err != nil {
return "", err
}
taskID, err := c.submit(ctx, body)
if err != nil {
return "", err
}
url, err := c.poll(ctx, taskID)
if err != nil {
return "", err
}
return url, nil
}
func (c *wanxClient) submit(ctx context.Context, body []byte) (string, error) {
req, err := http.NewRequestWithContext(ctx, "POST", c.base, bytes.NewReader(body))
if err != nil {
return "", err
}
req.Header.Set("Authorization", "Bearer "+c.apiKey)
req.Header.Set("Content-Type", "application/json")
req.Header.Set("X-DashScope-Async", "enable")
client := &http.Client{Timeout: 30 * time.Second}
resp, err := client.Do(req)
if err != nil {
return "", err
}
defer resp.Body.Close()
data, _ := io.ReadAll(resp.Body)
var r wanxResp
if err := json.Unmarshal(data, &r); err != nil {
return "", fmt.Errorf("万相响应解析失败: %s", string(data))
}
if r.Output.TaskID == "" {
return "", fmt.Errorf("万相提交失败 code=%s msg=%s", r.Code, r.Message)
}
return r.Output.TaskID, nil
}
func (c *wanxClient) poll(ctx context.Context, taskID string) (string, error) {
taskURL := c.base + "?task_id=" + taskID
client := &http.Client{Timeout: 30 * time.Second}
for i := 0; i < 60; i++ {
select {
case <-ctx.Done():
return "", ctx.Err()
case <-time.After(5 * time.Second):
}
req, err := http.NewRequestWithContext(ctx, "GET", taskURL, nil)
if err != nil {
return "", err
}
req.Header.Set("Authorization", "Bearer "+c.apiKey)
resp, err := client.Do(req)
if err != nil {
return "", err
}
data, _ := io.ReadAll(resp.Body)
resp.Body.Close()
var r wanxTaskResp
if err := json.Unmarshal(data, &r); err != nil {
return "", fmt.Errorf("万相任务查询解析失败: %s", string(data))
}
switch r.Output.TaskStatus {
case "SUCCEEDED":
if len(r.Output.Results) > 0 && r.Output.Results[0].URL != "" {
return r.Output.Results[0].URL, nil
}
return "", fmt.Errorf("万相任务成功但无结果")
case "FAILED":
return "", fmt.Errorf("万相任务失败: %s", r.Message)
}
}
return "", fmt.Errorf("万相任务超时")
}