refactor(config): 将AI配置逻辑迁移至统一的状态管理store
- 移除独立的ai-config.ts文件,将其功能整合到use-config-store - 更新所有组件导入路径从 "@/lib/ai-config" 到 "@/stores/use-config-store" - 实现云端渠道和本地直连两种配置模式的支持 - 添加模型渠道配置管理和API请求代理转发功能 - 统一配置验证逻辑和有效配置获取方法 - 更新组件中使用的配置状态钩子和API调用方式
This commit is contained in:
+113
@@ -0,0 +1,113 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"io"
|
||||
"mime"
|
||||
"mime/multipart"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/basketikun/infinite-canvas/service"
|
||||
)
|
||||
|
||||
func AIImagesGenerations(w http.ResponseWriter, r *http.Request) {
|
||||
proxyAIRequest(w, r, "/images/generations")
|
||||
}
|
||||
|
||||
func AIImagesEdits(w http.ResponseWriter, r *http.Request) {
|
||||
proxyAIRequest(w, r, "/images/edits")
|
||||
}
|
||||
|
||||
func AIChatCompletions(w http.ResponseWriter, r *http.Request) {
|
||||
proxyAIRequest(w, r, "/chat/completions")
|
||||
}
|
||||
|
||||
func proxyAIRequest(w http.ResponseWriter, r *http.Request, path string) {
|
||||
body, contentType, modelName, err := readAIRequest(r)
|
||||
if err != nil {
|
||||
Fail(w, err.Error())
|
||||
return
|
||||
}
|
||||
channel, err := service.SelectModelChannel(modelName)
|
||||
if err != nil {
|
||||
Fail(w, err.Error())
|
||||
return
|
||||
}
|
||||
request, err := http.NewRequest(http.MethodPost, service.BuildModelChannelURL(channel, path), bytes.NewReader(body))
|
||||
if err != nil {
|
||||
Fail(w, err.Error())
|
||||
return
|
||||
}
|
||||
request.Header.Set("Authorization", "Bearer "+channel.APIKey)
|
||||
if contentType != "" {
|
||||
request.Header.Set("Content-Type", contentType)
|
||||
}
|
||||
response, err := http.DefaultClient.Do(request)
|
||||
if err != nil {
|
||||
Fail(w, err.Error())
|
||||
return
|
||||
}
|
||||
defer response.Body.Close()
|
||||
|
||||
for key, values := range response.Header {
|
||||
if strings.EqualFold(key, "Content-Length") {
|
||||
continue
|
||||
}
|
||||
for _, value := range values {
|
||||
w.Header().Add(key, value)
|
||||
}
|
||||
}
|
||||
w.WriteHeader(response.StatusCode)
|
||||
_, _ = io.Copy(w, response.Body)
|
||||
}
|
||||
|
||||
func readAIRequest(r *http.Request) ([]byte, string, string, error) {
|
||||
contentType := r.Header.Get("Content-Type")
|
||||
body, err := io.ReadAll(r.Body)
|
||||
if err != nil {
|
||||
return nil, "", "", err
|
||||
}
|
||||
modelName := ""
|
||||
if strings.HasPrefix(contentType, "multipart/form-data") {
|
||||
modelName = readMultipartModel(body, contentType)
|
||||
} else {
|
||||
var payload struct {
|
||||
Model string `json:"model"`
|
||||
}
|
||||
_ = json.Unmarshal(body, &payload)
|
||||
modelName = payload.Model
|
||||
}
|
||||
if strings.TrimSpace(modelName) == "" {
|
||||
return nil, "", "", errMissingModel
|
||||
}
|
||||
return body, contentType, modelName, nil
|
||||
}
|
||||
|
||||
func readMultipartModel(body []byte, contentType string) string {
|
||||
_, params, err := mime.ParseMediaType(contentType)
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
reader := multipart.NewReader(bytes.NewReader(body), params["boundary"])
|
||||
form, err := reader.ReadForm(32 << 20)
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
defer form.RemoveAll()
|
||||
if values := form.Value["model"]; len(values) > 0 {
|
||||
return values[0]
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
var errMissingModel = &aiError{"缺少模型名称"}
|
||||
|
||||
type aiError struct {
|
||||
message string
|
||||
}
|
||||
|
||||
func (err *aiError) Error() string {
|
||||
return err.message
|
||||
}
|
||||
Reference in New Issue
Block a user