ai_provider.go 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628
  1. package service
  2. import (
  3. "bytes"
  4. "encoding/base64"
  5. "encoding/json"
  6. "errors"
  7. "fmt"
  8. "io"
  9. "log"
  10. "net/http"
  11. "strings"
  12. "time"
  13. )
  14. // AIProvider AI 服务提供商接口(可扩展设计)
  15. // 不同的 AI 服务提供商需要实现这个接口
  16. type AIProvider interface {
  17. // GenerateResponse 生成 AI 回复
  18. // imageBase64、imageMimeType 非空时表示当前用户消息带一张图(多模态识图),将与本条文本一起作为 user 消息发送
  19. GenerateResponse(conversationHistory []MessageHistory, userMessage string, imageBase64 string, imageMimeType string) (string, error)
  20. // GenerateResponseWithTools 带工具调用的生成;messages 与 tools 为 OpenAI 格式。返回 content、tool_calls、error。
  21. // 若某实现不支持,可返回 ( "", nil, err ) 或仅返回 content。
  22. GenerateResponseWithTools(messages []map[string]interface{}, tools []map[string]interface{}) (content string, toolCalls []ToolCall, err error)
  23. }
  24. // AdapterConfig 适配器配置(用于适配不同服务商的 API 格式差异)
  25. type AdapterConfig struct {
  26. // 认证头格式(默认:Bearer)
  27. AuthHeader string `json:"auth_header"` // 例如:"Bearer"、"X-API-Key"、"Authorization"
  28. // 响应解析路径(默认:choices[0].message.content)
  29. ResponsePath string `json:"response_path"` // 例如:"choices[0].message.content"、"data.text"、"result.content"
  30. // 请求格式自定义(可选)
  31. RequestFormat map[string]interface{} `json:"request_format"` // 用于覆盖默认的请求格式
  32. }
  33. // MessageHistory 对话历史记录
  34. type MessageHistory struct {
  35. Role string `json:"role"` // "user" 或 "assistant"
  36. Content string `json:"content"` // 消息内容
  37. }
  38. // ToolCall 模型返回的工具调用(OpenAI 格式)
  39. type ToolCall struct {
  40. ID string `json:"id"`
  41. Name string `json:"name"`
  42. Arguments string `json:"arguments"` // JSON 字符串
  43. }
  44. // AIConfig 用于 AI 调用的配置信息
  45. type AIConfig struct {
  46. APIURL string
  47. APIKey string
  48. Model string
  49. ModelType string
  50. Provider string
  51. AdapterConfig *AdapterConfig // 适配器配置(用于适配不同服务商的差异)
  52. }
  53. // UniversalAIProvider 通用 AI 服务提供商(支持所有 OpenAI 兼容格式)
  54. // 通过适配器配置来适配不同服务商的细微差异
  55. // 这样 90% 的服务商都可以用同一个 Provider,无需单独实现
  56. type UniversalAIProvider struct {
  57. config AIConfig
  58. client *http.Client
  59. adapter *AdapterConfig
  60. }
  61. // NewUniversalAIProvider 创建通用 AI 提供商实例。
  62. func NewUniversalAIProvider(config AIConfig) *UniversalAIProvider {
  63. // 设置默认适配器配置
  64. adapter := config.AdapterConfig
  65. if adapter == nil {
  66. adapter = &AdapterConfig{
  67. AuthHeader: "Bearer", // 默认使用 Bearer Token
  68. ResponsePath: "choices[0].message.content", // 默认 OpenAI 格式
  69. }
  70. } else {
  71. // 设置默认值
  72. if adapter.AuthHeader == "" {
  73. adapter.AuthHeader = "Bearer"
  74. }
  75. if adapter.ResponsePath == "" {
  76. adapter.ResponsePath = "choices[0].message.content"
  77. }
  78. }
  79. return &UniversalAIProvider{
  80. config: config,
  81. client: &http.Client{
  82. Timeout: 60 * time.Second, // 60 秒超时
  83. },
  84. adapter: adapter,
  85. }
  86. }
  87. // isResponsesAPI 判断是否为 OpenAI Responses API(/v1/responses),请求/响应格式与 Chat Completions 不同。
  88. func isResponsesAPI(apiURL string) bool {
  89. return strings.Contains(apiURL, "/v1/responses")
  90. }
  91. // GenerateResponse 生成 AI 回复(支持 OpenAI 兼容格式,通过适配器适配不同服务商)。
  92. func (p *UniversalAIProvider) GenerateResponse(conversationHistory []MessageHistory, userMessage string, imageBase64 string, imageMimeType string) (string, error) {
  93. switch p.config.ModelType {
  94. case "text":
  95. return p.generateTextResponse(conversationHistory, userMessage, imageBase64, imageMimeType)
  96. case "image":
  97. return "", fmt.Errorf("图片模型请使用生图接口")
  98. case "audio":
  99. return "", fmt.Errorf("语音模型暂未支持")
  100. case "video":
  101. return "", fmt.Errorf("视频模型暂未支持")
  102. default:
  103. return "", fmt.Errorf("不支持的模型类型: %s", p.config.ModelType)
  104. }
  105. }
  106. // buildUserContent 构建当前用户消息的 content:纯文本或 text+image(多模态)
  107. func buildUserContent(userMessage string, imageBase64 string, imageMimeType string) interface{} {
  108. if imageBase64 == "" {
  109. return userMessage
  110. }
  111. // OpenAI 多模态:content 为数组,text + image_url(data URL)
  112. dataURL := "data:" + imageMimeType + ";base64," + imageBase64
  113. if imageMimeType == "" {
  114. dataURL = "data:image/jpeg;base64," + imageBase64
  115. }
  116. parts := []map[string]interface{}{
  117. {"type": "text", "text": userMessage},
  118. {"type": "image_url", "image_url": map[string]string{"url": dataURL}},
  119. }
  120. return parts
  121. }
  122. // generateTextResponse 生成文本回复(支持多模态:当前用户消息可带图)。
  123. func (p *UniversalAIProvider) generateTextResponse(conversationHistory []MessageHistory, userMessage string, imageBase64 string, imageMimeType string) (string, error) {
  124. // 使用 interface{} 以支持最后一条 user 消息的 content 为数组(多模态)
  125. messages := make([]map[string]interface{}, 0)
  126. for _, history := range conversationHistory {
  127. messages = append(messages, map[string]interface{}{"role": history.Role, "content": history.Content})
  128. }
  129. lastContent := buildUserContent(userMessage, imageBase64, imageMimeType)
  130. messages = append(messages, map[string]interface{}{"role": "user", "content": lastContent})
  131. var requestBody map[string]interface{}
  132. if isResponsesAPI(p.config.APIURL) {
  133. requestBody = map[string]interface{}{
  134. "model": p.config.Model,
  135. "input": messages,
  136. "stream": false,
  137. }
  138. } else {
  139. requestBody = map[string]interface{}{
  140. "model": p.config.Model,
  141. "messages": messages,
  142. }
  143. }
  144. jsonData, err := json.Marshal(requestBody)
  145. if err != nil {
  146. return "", fmt.Errorf("序列化请求失败: %v", err)
  147. }
  148. // 创建 HTTP 请求
  149. req, err := http.NewRequest("POST", p.config.APIURL, bytes.NewBuffer(jsonData))
  150. if err != nil {
  151. return "", fmt.Errorf("创建请求失败: %v", err)
  152. }
  153. // 设置请求头
  154. req.Header.Set("Content-Type", "application/json")
  155. // 根据适配器配置设置认证头
  156. authValue := p.config.APIKey
  157. if p.adapter.AuthHeader == "Bearer" {
  158. authValue = "Bearer " + p.config.APIKey
  159. req.Header.Set("Authorization", authValue)
  160. } else if p.adapter.AuthHeader == "X-API-Key" {
  161. req.Header.Set("X-API-Key", p.config.APIKey)
  162. } else {
  163. // 默认使用 Authorization: Bearer
  164. req.Header.Set("Authorization", "Bearer "+p.config.APIKey)
  165. }
  166. // 发送请求(若发生重定向,req.URL 会被 Client 更新为最终 URL;失败日志便于与配置里的 api_url 对照)
  167. resp, err := p.client.Do(req)
  168. if err != nil {
  169. log.Printf("⚠️ AI generateTextResponse 请求失败: config.api_url=%s 实际 req.URL=%s err=%v",
  170. p.config.APIURL, req.URL.String(), err)
  171. return "", fmt.Errorf("请求失败: %v", err)
  172. }
  173. defer resp.Body.Close()
  174. // 读取响应
  175. body, err := io.ReadAll(resp.Body)
  176. if err != nil {
  177. return "", fmt.Errorf("读取响应失败: %v", err)
  178. }
  179. // 检查 HTTP 状态码
  180. if resp.StatusCode != http.StatusOK {
  181. return "", fmt.Errorf("API 返回错误: %s (状态码: %d)", string(body), resp.StatusCode)
  182. }
  183. // 解析响应(支持灵活的响应路径)
  184. var responseData map[string]interface{}
  185. if err := json.Unmarshal(body, &responseData); err != nil {
  186. return "", fmt.Errorf("解析响应失败: %v", err)
  187. }
  188. // 检查是否有错误字段
  189. if errorMsg, ok := responseData["error"].(map[string]interface{}); ok {
  190. if msg, ok := errorMsg["message"].(string); ok {
  191. return "", fmt.Errorf("API 错误: %s", msg)
  192. }
  193. }
  194. // 根据适配器配置的响应路径提取内容
  195. content, err := p.extractResponseContent(responseData, p.adapter.ResponsePath)
  196. if err != nil {
  197. return "", err
  198. }
  199. if content == "" {
  200. return "", errors.New("API 返回空内容")
  201. }
  202. return content, nil
  203. }
  204. // GenerateResponseWithTools 带工具调用的生成(OpenAI 兼容:tools + tool_calls)。
  205. // messages 为 OpenAI 格式消息数组(可含 role, content, tool_calls, tool_call_id 等)。
  206. // tools 为工具定义数组(如 [{"type":"function","function":{...}}] 或 [{"type":"web_search"}])。
  207. // 返回 content、tool_calls(若有)、error。
  208. func (p *UniversalAIProvider) GenerateResponseWithTools(messages []map[string]interface{}, tools []map[string]interface{}) (content string, toolCalls []ToolCall, err error) {
  209. if p.config.ModelType != "text" {
  210. return "", nil, fmt.Errorf("带工具调用仅支持 text 模型")
  211. }
  212. var requestBody map[string]interface{}
  213. if isResponsesAPI(p.config.APIURL) {
  214. requestBody = map[string]interface{}{
  215. "model": p.config.Model,
  216. "input": messages,
  217. "stream": false,
  218. "tool_choice": "auto",
  219. }
  220. if len(tools) > 0 {
  221. requestBody["tools"] = tools
  222. }
  223. } else {
  224. requestBody = map[string]interface{}{
  225. "model": p.config.Model,
  226. "messages": messages,
  227. }
  228. if len(tools) > 0 {
  229. requestBody["tools"] = tools
  230. }
  231. }
  232. jsonData, err := json.Marshal(requestBody)
  233. if err != nil {
  234. return "", nil, fmt.Errorf("序列化请求失败: %v", err)
  235. }
  236. req, err := http.NewRequest("POST", p.config.APIURL, bytes.NewBuffer(jsonData))
  237. if err != nil {
  238. return "", nil, fmt.Errorf("创建请求失败: %v", err)
  239. }
  240. req.Header.Set("Content-Type", "application/json")
  241. authValue := p.config.APIKey
  242. if p.adapter.AuthHeader == "Bearer" {
  243. authValue = "Bearer " + p.config.APIKey
  244. req.Header.Set("Authorization", authValue)
  245. } else if p.adapter.AuthHeader == "X-API-Key" {
  246. req.Header.Set("X-API-Key", p.config.APIKey)
  247. } else {
  248. req.Header.Set("Authorization", "Bearer "+p.config.APIKey)
  249. }
  250. resp, err := p.client.Do(req)
  251. if err != nil {
  252. log.Printf("⚠️ AI GenerateResponseWithTools 请求失败: config.api_url=%s 实际 req.URL=%s err=%v",
  253. p.config.APIURL, req.URL.String(), err)
  254. return "", nil, fmt.Errorf("请求失败: %v", err)
  255. }
  256. defer resp.Body.Close()
  257. body, err := io.ReadAll(resp.Body)
  258. if err != nil {
  259. return "", nil, fmt.Errorf("读取响应失败: %v", err)
  260. }
  261. if resp.StatusCode != http.StatusOK {
  262. return "", nil, fmt.Errorf("API 返回错误: %s (状态码: %d)", string(body), resp.StatusCode)
  263. }
  264. var responseData map[string]interface{}
  265. if err := json.Unmarshal(body, &responseData); err != nil {
  266. return "", nil, fmt.Errorf("解析响应失败: %v", err)
  267. }
  268. if errorMsg, ok := responseData["error"].(map[string]interface{}); ok {
  269. if msg, ok := errorMsg["message"].(string); ok {
  270. return "", nil, fmt.Errorf("API 错误: %s", msg)
  271. }
  272. }
  273. content, toolCalls = p.extractContentAndToolCalls(responseData)
  274. return content, toolCalls, nil
  275. }
  276. // extractContentAndToolCalls 从响应中提取 content 与 tool_calls
  277. // 支持 Chat Completions(choices[0].message)与 Responses API(output[])
  278. func (p *UniversalAIProvider) extractContentAndToolCalls(data map[string]interface{}) (content string, toolCalls []ToolCall) {
  279. // Responses API:output 数组内 message 的 content 含 output_text 与 tool_use
  280. if output, ok := data["output"].([]interface{}); ok {
  281. var textParts []string
  282. for _, item := range output {
  283. obj, _ := item.(map[string]interface{})
  284. if obj == nil || getStr(obj, "type") != "message" {
  285. continue
  286. }
  287. contentParts, _ := obj["content"].([]interface{})
  288. for _, part := range contentParts {
  289. pm, _ := part.(map[string]interface{})
  290. if pm == nil {
  291. continue
  292. }
  293. switch getStr(pm, "type") {
  294. case "output_text":
  295. if t, ok := pm["text"].(string); ok && t != "" {
  296. textParts = append(textParts, t)
  297. }
  298. case "tool_use":
  299. args := getStr(pm, "input")
  300. if args == "" {
  301. args = "{}"
  302. }
  303. toolCalls = append(toolCalls, ToolCall{
  304. ID: getStr(pm, "id"),
  305. Name: getStr(pm, "name"),
  306. Arguments: args,
  307. })
  308. }
  309. }
  310. }
  311. if len(textParts) > 0 {
  312. content = strings.Join(textParts, "")
  313. }
  314. return content, toolCalls
  315. }
  316. // Chat Completions 格式
  317. choices, _ := data["choices"].([]interface{})
  318. if len(choices) == 0 {
  319. return "", nil
  320. }
  321. choice, _ := choices[0].(map[string]interface{})
  322. message, _ := choice["message"].(map[string]interface{})
  323. if message != nil {
  324. if c, ok := message["content"].(string); ok {
  325. content = c
  326. }
  327. tcList, _ := message["tool_calls"].([]interface{})
  328. for _, t := range tcList {
  329. tm, _ := t.(map[string]interface{})
  330. fn, _ := tm["function"].(map[string]interface{})
  331. args := ""
  332. if fn != nil {
  333. if a, ok := fn["arguments"].(string); ok {
  334. args = a
  335. }
  336. }
  337. toolCalls = append(toolCalls, ToolCall{
  338. ID: getStr(tm, "id"),
  339. Name: getStr(fn, "name"),
  340. Arguments: args,
  341. })
  342. }
  343. }
  344. return content, toolCalls
  345. }
  346. func getStr(m map[string]interface{}, key string) string {
  347. if m == nil {
  348. return ""
  349. }
  350. v, _ := m[key].(string)
  351. return v
  352. }
  353. // extractResponseContent 根据响应路径提取内容(支持灵活的路径配置)。
  354. // 例如:"choices[0].message.content"(Chat Completions)或 Responses API 的 output[].content[].text
  355. func (p *UniversalAIProvider) extractResponseContent(data map[string]interface{}, path string) (string, error) {
  356. // Responses API:output 数组内 message 的 content 中 output_text 的 text
  357. if output, ok := data["output"].([]interface{}); ok && len(output) > 0 {
  358. for _, item := range output {
  359. obj, _ := item.(map[string]interface{})
  360. if obj == nil {
  361. continue
  362. }
  363. if getStr(obj, "type") != "message" {
  364. continue
  365. }
  366. contentParts, _ := obj["content"].([]interface{})
  367. var textParts []string
  368. for _, part := range contentParts {
  369. pm, _ := part.(map[string]interface{})
  370. if pm == nil {
  371. continue
  372. }
  373. if getStr(pm, "type") == "output_text" {
  374. if t, ok := pm["text"].(string); ok && t != "" {
  375. textParts = append(textParts, t)
  376. }
  377. }
  378. }
  379. if len(textParts) > 0 {
  380. return strings.Join(textParts, ""), nil
  381. }
  382. }
  383. return "", errors.New("Responses API 的 output 中未找到 message 文本")
  384. }
  385. // 默认路径:choices[0].message.content(OpenAI Chat Completions 格式)
  386. if path == "" || path == "choices[0].message.content" {
  387. if choices, ok := data["choices"].([]interface{}); ok && len(choices) > 0 {
  388. if choice, ok := choices[0].(map[string]interface{}); ok {
  389. if message, ok := choice["message"].(map[string]interface{}); ok {
  390. if content, ok := message["content"].(string); ok {
  391. return content, nil
  392. }
  393. }
  394. }
  395. }
  396. }
  397. // 尝试其他常见格式
  398. // 格式1: data.text
  399. if dataObj, ok := data["data"].(map[string]interface{}); ok {
  400. if text, ok := dataObj["text"].(string); ok {
  401. return text, nil
  402. }
  403. }
  404. // 格式2: result.content
  405. if result, ok := data["result"].(map[string]interface{}); ok {
  406. if content, ok := result["content"].(string); ok {
  407. return content, nil
  408. }
  409. }
  410. // 格式3: content(直接字段)
  411. if content, ok := data["content"].(string); ok {
  412. return content, nil
  413. }
  414. // 格式4: text(直接字段)
  415. if text, ok := data["text"].(string); ok {
  416. return text, nil
  417. }
  418. return "", errors.New("无法从响应中提取内容,请检查响应格式或配置适配器")
  419. }
  420. // AIProviderFactory AI 提供商工厂(用于创建不同类型的提供商)
  421. type AIProviderFactory struct{}
  422. // NewAIProviderFactory 创建 AI 提供商工厂实例。
  423. func NewAIProviderFactory() *AIProviderFactory {
  424. return &AIProviderFactory{}
  425. }
  426. // ImageGenerationProvider 生图接口(用于 chat_mode=image 渠道)
  427. type ImageGenerationProvider interface {
  428. // GenerateImage 根据文本描述生成图片,返回图片二进制与 MIME 类型
  429. GenerateImage(prompt string) (imageData []byte, mimeType string, err error)
  430. }
  431. // CreateProvider 根据配置创建对应的 AI 提供商。
  432. func (f *AIProviderFactory) CreateProvider(config AIConfig) (AIProvider, error) {
  433. return NewUniversalAIProvider(config), nil
  434. }
  435. // isPoixeGeminiImageAPI 判断是否为 Poixe 的 Google Gemini Content 生图接口(需 x-goog-api-key 且请求/响应格式不同)
  436. func isPoixeGeminiImageAPI(apiURL string) bool {
  437. lower := strings.ToLower(apiURL)
  438. return (strings.Contains(lower, "poixe.com") && strings.Contains(lower, "generatecontent")) ||
  439. (strings.Contains(lower, "poixe.com") && strings.Contains(lower, "v1beta"))
  440. }
  441. // GenerateImage 实现生图(model_type=image 的配置使用)。支持 OpenAI Images 与 Poixe Nano Banana(Gemini Content)两种协议。
  442. func (p *UniversalAIProvider) GenerateImage(prompt string) (imageData []byte, mimeType string, err error) {
  443. if p.config.ModelType != "image" {
  444. return nil, "", fmt.Errorf("当前配置不是生图模型,model_type=%s", p.config.ModelType)
  445. }
  446. useGemini := isPoixeGeminiImageAPI(p.config.APIURL)
  447. var jsonData []byte
  448. if useGemini {
  449. // Poixe Nano Banana:Google Gemini Content 协议,见 https://docs.poixe.com/cn/docs/models-pricing/nano-banana
  450. body := map[string]interface{}{
  451. "contents": []map[string]interface{}{
  452. {"parts": []map[string]interface{}{{"text": prompt}}},
  453. },
  454. "generationConfig": map[string]interface{}{
  455. "responseModalities": []string{"Text", "Image"},
  456. "imageConfig": map[string]interface{}{
  457. "aspectRatio": "1:1",
  458. "imageSize": "1K",
  459. },
  460. },
  461. }
  462. jsonData, err = json.Marshal(body)
  463. if err != nil {
  464. return nil, "", err
  465. }
  466. } else {
  467. // OpenAI 兼容:/v1/images/generations
  468. body := map[string]interface{}{
  469. "model": p.config.Model,
  470. "prompt": prompt,
  471. "n": 1,
  472. }
  473. if strings.Contains(strings.ToLower(p.config.APIURL), "images") {
  474. body["response_format"] = "b64_json"
  475. body["size"] = "1024x1024"
  476. }
  477. jsonData, err = json.Marshal(body)
  478. if err != nil {
  479. return nil, "", err
  480. }
  481. }
  482. req, err := http.NewRequest("POST", p.config.APIURL, bytes.NewBuffer(jsonData))
  483. if err != nil {
  484. return nil, "", err
  485. }
  486. req.Header.Set("Content-Type", "application/json")
  487. // Poixe Gemini Content 要求 x-goog-api-key;适配器可显式指定,或按 URL 自动识别
  488. useGoogKey := (p.adapter != nil && strings.ToLower(p.adapter.AuthHeader) == "x-goog-api-key") || useGemini
  489. if useGoogKey {
  490. req.Header.Set("x-goog-api-key", p.config.APIKey)
  491. } else if p.adapter != nil && p.adapter.AuthHeader == "X-API-Key" {
  492. req.Header.Set("X-API-Key", p.config.APIKey)
  493. } else {
  494. req.Header.Set("Authorization", "Bearer "+p.config.APIKey)
  495. }
  496. resp, err := p.client.Do(req)
  497. if err != nil {
  498. return nil, "", err
  499. }
  500. defer resp.Body.Close()
  501. data, err := io.ReadAll(resp.Body)
  502. if err != nil {
  503. return nil, "", err
  504. }
  505. if resp.StatusCode != http.StatusOK {
  506. return nil, "", fmt.Errorf("生图 API 错误: %s (状态码: %d)", string(data), resp.StatusCode)
  507. }
  508. if useGemini {
  509. return p.parseGeminiImageResponse(data)
  510. }
  511. return p.parseOpenAIImageResponse(data)
  512. }
  513. // parseGeminiImageResponse 解析 Poixe/Gemini Content 生图响应:candidates[0].content.parts 中的 inlineData
  514. func (p *UniversalAIProvider) parseGeminiImageResponse(data []byte) (imageData []byte, mimeType string, err error) {
  515. var parsed struct {
  516. Candidates []struct {
  517. Content struct {
  518. Parts []struct {
  519. InlineData *struct {
  520. MimeType string `json:"mimeType"`
  521. Data string `json:"data"`
  522. } `json:"inlineData"`
  523. } `json:"parts"`
  524. } `json:"content"`
  525. } `json:"candidates"`
  526. }
  527. if err := json.Unmarshal(data, &parsed); err != nil {
  528. return nil, "", fmt.Errorf("解析 Gemini 生图响应失败: %w", err)
  529. }
  530. if len(parsed.Candidates) == 0 {
  531. return nil, "", errors.New("Gemini 生图未返回 candidates")
  532. }
  533. for _, part := range parsed.Candidates[0].Content.Parts {
  534. if part.InlineData != nil && part.InlineData.Data != "" {
  535. decoded, err := base64.StdEncoding.DecodeString(part.InlineData.Data)
  536. if err != nil {
  537. return nil, "", fmt.Errorf("base64 解码失败: %w", err)
  538. }
  539. mt := part.InlineData.MimeType
  540. if mt == "" {
  541. mt = "image/png"
  542. }
  543. return decoded, mt, nil
  544. }
  545. }
  546. return nil, "", errors.New("Gemini 生图响应中无 inlineData")
  547. }
  548. // parseOpenAIImageResponse 解析 OpenAI 风格生图响应:data[0].b64_json 或 data[0].url
  549. func (p *UniversalAIProvider) parseOpenAIImageResponse(data []byte) (imageData []byte, mimeType string, err error) {
  550. var parsed struct {
  551. Data []struct {
  552. B64JSON *string `json:"b64_json"`
  553. URL string `json:"url"`
  554. } `json:"data"`
  555. }
  556. if err := json.Unmarshal(data, &parsed); err != nil {
  557. return nil, "", fmt.Errorf("解析生图响应失败: %w", err)
  558. }
  559. if len(parsed.Data) == 0 {
  560. return nil, "", errors.New("生图 API 未返回图片")
  561. }
  562. first := parsed.Data[0]
  563. if first.B64JSON != nil && *first.B64JSON != "" {
  564. decoded, err := base64.StdEncoding.DecodeString(*first.B64JSON)
  565. if err != nil {
  566. return nil, "", fmt.Errorf("base64 解码失败: %w", err)
  567. }
  568. return decoded, "image/png", nil
  569. }
  570. if first.URL != "" {
  571. getResp, err := http.Get(first.URL)
  572. if err != nil {
  573. return nil, "", fmt.Errorf("下载生成图片失败: %w", err)
  574. }
  575. defer getResp.Body.Close()
  576. decoded, err := io.ReadAll(getResp.Body)
  577. if err != nil {
  578. return nil, "", err
  579. }
  580. return decoded, "image/png", nil
  581. }
  582. return nil, "", errors.New("生图响应中无 b64_json 或 url")
  583. }