2934b1d0bc
- 移除预检查算力点步骤,改为请求前预扣算力点 - 添加请求失败时自动返还算力点的功能 - 修改copyAIResponse函数参数以支持失败回调处理 - 在响应状态异常时也执行算力点返还操作 - 从auth服务中移除EnsureUserCredits方法 - 新增RefundUserCredits方法用于返还用户算力点 - 添加AI_REFUND算力点日志类型常量 - 更新算力点日志类型说明及展示标签
217 lines
5.7 KiB
Go
217 lines
5.7 KiB
Go
package handler
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/json"
|
|
"fmt"
|
|
"io"
|
|
"log"
|
|
"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 AIVideos(w http.ResponseWriter, r *http.Request) {
|
|
proxyAIRequest(w, r, "/videos")
|
|
}
|
|
|
|
func AIVideo(w http.ResponseWriter, r *http.Request, id string) {
|
|
proxyAIGetRequest(w, r, "/videos/"+id)
|
|
}
|
|
|
|
func AIVideoContent(w http.ResponseWriter, r *http.Request, id string) {
|
|
proxyAIGetRequest(w, r, "/videos/"+id+"/content")
|
|
}
|
|
|
|
func proxyAIGetRequest(w http.ResponseWriter, r *http.Request, path string) {
|
|
modelName := r.URL.Query().Get("model")
|
|
if strings.TrimSpace(modelName) == "" {
|
|
modelName = "sora-2"
|
|
}
|
|
channel, err := service.SelectModelChannel(modelName)
|
|
if err != nil {
|
|
log.Printf("AI proxy select channel failed: model=%s err=%v", modelName, err)
|
|
Fail(w, "AI 接口请求失败")
|
|
return
|
|
}
|
|
request, err := http.NewRequest(http.MethodGet, service.BuildModelChannelURL(channel, path), nil)
|
|
if err != nil {
|
|
Fail(w, "AI 接口请求失败")
|
|
return
|
|
}
|
|
request.Header.Set("Authorization", "Bearer "+channel.APIKey)
|
|
copyAIResponse(w, request, nil)
|
|
}
|
|
|
|
func proxyAIRequest(w http.ResponseWriter, r *http.Request, path string) {
|
|
body, contentType, modelName, err := readAIRequest(r)
|
|
if err != nil {
|
|
log.Printf("AI proxy request read failed: %v", err)
|
|
Fail(w, "AI 接口请求失败")
|
|
return
|
|
}
|
|
user, ok := service.UserFromContext(r.Context())
|
|
if !ok {
|
|
Fail(w, "未登录或权限不足")
|
|
return
|
|
}
|
|
credits, err := service.ModelCost(modelName)
|
|
if err != nil {
|
|
log.Printf("AI proxy read model cost failed: model=%s err=%v", modelName, err)
|
|
Fail(w, "AI 接口请求失败")
|
|
return
|
|
}
|
|
credits *= readAIRequestCount(body, contentType)
|
|
channel, err := service.SelectModelChannel(modelName)
|
|
if err != nil {
|
|
log.Printf("AI proxy select channel failed: model=%s err=%v", modelName, err)
|
|
Fail(w, "AI 接口请求失败")
|
|
return
|
|
}
|
|
request, err := http.NewRequest(http.MethodPost, service.BuildModelChannelURL(channel, path), bytes.NewReader(body))
|
|
if err != nil {
|
|
log.Printf("AI proxy build request failed: url=%s err=%v", service.BuildModelChannelURL(channel, path), err)
|
|
Fail(w, "AI 接口请求失败")
|
|
return
|
|
}
|
|
request.Header.Set("Authorization", "Bearer "+channel.APIKey)
|
|
if contentType != "" {
|
|
request.Header.Set("Content-Type", contentType)
|
|
}
|
|
if err := service.ConsumeUserCredits(user.ID, modelName, credits, path); err != nil {
|
|
FailError(w, err)
|
|
return
|
|
}
|
|
copyAIResponse(w, request, func() {
|
|
if err := service.RefundUserCredits(user.ID, modelName, credits, path); err != nil {
|
|
log.Printf("AI proxy refund credits failed: user=%s model=%s credits=%d err=%v", user.ID, modelName, credits, err)
|
|
}
|
|
})
|
|
}
|
|
|
|
func copyAIResponse(w http.ResponseWriter, request *http.Request, onFailure func()) {
|
|
response, err := http.DefaultClient.Do(request)
|
|
if err != nil {
|
|
log.Printf("AI proxy request failed: url=%s err=%v", request.URL.String(), err)
|
|
if onFailure != nil {
|
|
onFailure()
|
|
}
|
|
Fail(w, "AI 接口请求失败")
|
|
return
|
|
}
|
|
defer response.Body.Close()
|
|
|
|
if response.StatusCode >= http.StatusBadRequest {
|
|
payload, _ := io.ReadAll(io.LimitReader(response.Body, 4096))
|
|
log.Printf("AI upstream error: url=%s status=%d body=%s", request.URL.String(), response.StatusCode, strings.TrimSpace(string(payload)))
|
|
if onFailure != nil {
|
|
onFailure()
|
|
}
|
|
Fail(w, "AI 接口请求失败")
|
|
return
|
|
}
|
|
|
|
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 ""
|
|
}
|
|
|
|
func readAIRequestCount(body []byte, contentType string) int {
|
|
count := 1
|
|
if strings.HasPrefix(contentType, "multipart/form-data") {
|
|
_, params, err := mime.ParseMediaType(contentType)
|
|
if err != nil {
|
|
return count
|
|
}
|
|
form, err := multipart.NewReader(bytes.NewReader(body), params["boundary"]).ReadForm(32 << 20)
|
|
if err != nil {
|
|
return count
|
|
}
|
|
defer form.RemoveAll()
|
|
if values := form.Value["n"]; len(values) > 0 {
|
|
_, _ = fmt.Sscan(values[0], &count)
|
|
}
|
|
} else {
|
|
var payload struct {
|
|
N int `json:"n"`
|
|
}
|
|
_ = json.Unmarshal(body, &payload)
|
|
count = payload.N
|
|
}
|
|
if count < 1 {
|
|
return 1
|
|
}
|
|
return count
|
|
}
|
|
|
|
var errMissingModel = &aiError{"缺少模型名称"}
|
|
|
|
type aiError struct {
|
|
message string
|
|
}
|
|
|
|
func (err *aiError) Error() string {
|
|
return err.message
|
|
}
|