Files
AskaEth 71f0a1abdb feat: Go模块路径迁移 + Docker生产部署适配 + ethend Docker兼容
- 所有Go模块路径从 github.com/yourname/cyrene-ai 迁移到 git.yeij.top/AskaEth/Cyrene
- 5个Go Dockerfile添加 GOPROXY=https://goproxy.cn,direct 解决国内构建问题
- ai-core go.mod 添加 pkg/plugins replace 指令
- Caddyfile 简化为 http:// 通配 + handle 保留 /api 前缀
- ethend Dockerfile 适配 (npm install + 仅 COPY package.json)
- ethend 新增 RUNNING_IN_DOCKER 环境变量,健康检查改用Docker服务名
- ethend 数据库状态检查支持Docker hostname (postgres/redis/qdrant/minio)
- process-manager 新增 CONTAINER_SVC_MAP + Docker模式自动检测
- 统一 docker-compose.dev.db.yml 卷名 (pg_data/redis_data/qdrant_data/minio_data)
- docker-compose.yml ethend服务挂载docker.sock + 端口变量化
- 清理 .env 统一后的残留文件与提示信息

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-30 13:43:22 +08:00

298 lines
8.1 KiB
Go

package handler
import (
"encoding/json"
"fmt"
"io"
"net/http"
"time"
"github.com/gin-gonic/gin"
"git.yeij.top/AskaEth/Cyrene/gateway/internal/config"
)
// ModelConfigHandler exposes admin CRUD endpoints for model configuration.
type ModelConfigHandler struct {
store *config.ModelsConfigStore
}
func NewModelConfigHandler(store *config.ModelsConfigStore) *ModelConfigHandler {
return &ModelConfigHandler{store: store}
}
// ---- Providers ----
func (h *ModelConfigHandler) ListProviders(c *gin.Context) {
providers := h.store.ListProviders()
if providers == nil {
providers = []*config.ProviderConfig{}
}
c.JSON(http.StatusOK, gin.H{"providers": providers, "total": len(providers)})
}
func (h *ModelConfigHandler) GetProvider(c *gin.Context) {
name := c.Param("name")
p, err := h.store.GetProvider(name)
if err != nil {
c.JSON(http.StatusNotFound, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, p)
}
func (h *ModelConfigHandler) SetProvider(c *gin.Context) {
name := c.Param("name")
var body config.ProviderConfig
if err := c.ShouldBindJSON(&body); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid JSON: " + err.Error()})
return
}
body.Name = name
if err := h.store.SetProvider(&body); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{"status": "saved", "name": name})
}
func (h *ModelConfigHandler) DeleteProvider(c *gin.Context) {
name := c.Param("name")
if err := h.store.DeleteProvider(name); err != nil {
c.JSON(http.StatusNotFound, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{"status": "deleted", "name": name})
}
// ---- Models ----
func (h *ModelConfigHandler) ListModels(c *gin.Context) {
models := h.store.ListModels()
if models == nil {
models = []*config.ModelConfig{}
}
c.JSON(http.StatusOK, gin.H{"models": models, "total": len(models)})
}
func (h *ModelConfigHandler) GetModel(c *gin.Context) {
id := c.Param("id")
m, err := h.store.GetModel(id)
if err != nil {
c.JSON(http.StatusNotFound, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, m)
}
func (h *ModelConfigHandler) SetModel(c *gin.Context) {
id := c.Param("id")
var body config.ModelConfig
if err := c.ShouldBindJSON(&body); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid JSON: " + err.Error()})
return
}
body.ID = id
if err := h.store.SetModel(&body); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{"status": "saved", "id": id})
}
func (h *ModelConfigHandler) DeleteModel(c *gin.Context) {
id := c.Param("id")
if err := h.store.DeleteModel(id); err != nil {
c.JSON(http.StatusNotFound, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{"status": "deleted", "id": id})
}
// ---- Routing ----
func (h *ModelConfigHandler) ListRouting(c *gin.Context) {
routing := h.store.ListRouting()
if routing == nil {
routing = []*config.RoutingRule{}
}
c.JSON(http.StatusOK, gin.H{"routing": routing, "total": len(routing)})
}
func (h *ModelConfigHandler) GetRouting(c *gin.Context) {
purpose := c.Param("purpose")
r, err := h.store.GetRouting(purpose)
if err != nil {
c.JSON(http.StatusNotFound, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, r)
}
func (h *ModelConfigHandler) SetRouting(c *gin.Context) {
purpose := c.Param("purpose")
var body config.RoutingRule
if err := c.ShouldBindJSON(&body); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid JSON: " + err.Error()})
return
}
body.Purpose = purpose
if err := h.store.SetRouting(&body); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{"status": "saved", "purpose": purpose})
}
func (h *ModelConfigHandler) DeleteRouting(c *gin.Context) {
purpose := c.Param("purpose")
if err := h.store.DeleteRouting(purpose); err != nil {
c.JSON(http.StatusNotFound, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{"status": "deleted", "purpose": purpose})
}
// ---- Health Check ----
func (h *ModelConfigHandler) TestProvider(c *gin.Context) {
var body struct {
Provider string `json:"provider"`
}
if err := c.ShouldBindJSON(&body); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid JSON: " + err.Error()})
return
}
p, err := h.store.GetProvider(body.Provider)
if err != nil {
c.JSON(http.StatusNotFound, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{
"provider": p.Name,
"base_url": p.BaseURL,
"message": "Provider 配置已保存,连接测试请通过实际 LLM 调用验证",
})
}
// ---- Remote Model List Proxy ----
// ProxyListModels forwards a request to the provider's models endpoint using the stored API key.
func (h *ModelConfigHandler) ProxyListModels(c *gin.Context) {
providerName := c.Param("name")
modelsURL := c.Query("url")
if modelsURL == "" {
c.JSON(http.StatusBadRequest, gin.H{"error": "missing 'url' query parameter"})
return
}
p, err := h.store.GetProvider(providerName)
if err != nil {
c.JSON(http.StatusNotFound, gin.H{"error": err.Error()})
return
}
if p.APIKey == "" {
c.JSON(http.StatusBadRequest, gin.H{"error": "provider 未配置 API Key"})
return
}
client := &http.Client{Timeout: 15 * time.Second}
req, err := http.NewRequest("GET", modelsURL, nil)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "创建请求失败: " + err.Error()})
return
}
req.Header.Set("Authorization", "Bearer "+p.APIKey)
req.Header.Set("Accept", "application/json")
resp, err := client.Do(req)
if err != nil {
c.JSON(http.StatusBadGateway, gin.H{"error": "请求模型列表失败: " + err.Error()})
return
}
defer resp.Body.Close()
body, err := io.ReadAll(io.LimitReader(resp.Body, 2<<20)) // 2 MB limit
if err != nil {
c.JSON(http.StatusBadGateway, gin.H{"error": "读取响应失败: " + err.Error()})
return
}
if resp.StatusCode >= 400 {
c.JSON(http.StatusBadGateway, gin.H{
"error": fmt.Sprintf("Provider API 返回错误 (HTTP %d)", resp.StatusCode),
"body": string(body),
"models_url": modelsURL,
})
return
}
// Parse the response body which may use different formats:
// OpenAI: {"object":"list","data":[{"id":"...","object":"model",...}]}
// DashScope: {"request_id":"...","data":{"models":[{"model_id":"..."}]}}
// Generic: {"data":[{"id":"..."}]} or {"data":[{"model_id":"..."}]}
ids := parseModelListResponse(body)
if len(ids) == 0 {
c.JSON(http.StatusBadGateway, gin.H{
"error": "无法从 Provider 响应中解析模型列表 (不支持的格式)",
"raw": string(body),
})
return
}
c.JSON(http.StatusOK, gin.H{
"provider": providerName,
"url": modelsURL,
"models": ids,
"total": len(ids),
})
}
// parseModelListResponse attempts to extract model IDs from various provider response formats.
// Supported formats:
// - OpenAI-compatible: {"object":"list","data":[{"id":"gpt-4o",...}]}
// - DashScope: {"data":{"models":[{"model_id":"qwen-turbo",...}]}}
// - Generic: {"data":[{"id":"..."}]} or {"data":[{"model_id":"..."}]}
func parseModelListResponse(body []byte) []string {
var raw map[string]interface{}
if err := json.Unmarshal(body, &raw); err != nil {
return nil
}
// Strategy 1: data is an array of objects — try "id" then "model_id"
if dataArr, ok := raw["data"].([]interface{}); ok {
ids := extractIDs(dataArr, "id")
if len(ids) > 0 {
return ids
}
return extractIDs(dataArr, "model_id")
}
// Strategy 2: data is an object with a "models" array (DashScope format)
if dataObj, ok := raw["data"].(map[string]interface{}); ok {
if modelsArr, ok := dataObj["models"].([]interface{}); ok {
ids := extractIDs(modelsArr, "model_id")
if len(ids) > 0 {
return ids
}
return extractIDs(modelsArr, "id")
}
}
return nil
}
func extractIDs(items []interface{}, key string) []string {
ids := make([]string, 0, len(items))
for _, item := range items {
if obj, ok := item.(map[string]interface{}); ok {
if v, ok := obj[key]; ok {
if s, ok := v.(string); ok && s != "" {
ids = append(ids, s)
}
}
}
}
return ids
}