mirror of
https://github.com/EZ-Api/ez-api.git
synced 2026-01-13 17:47:51 +00:00
feat(api): add feature flag management endpoints
Add FeatureHandler to manage lightweight runtime configuration toggles stored in the Redis `meta:features` hash. This enables dynamic control over system behavior (e.g., storage backends) via the admin API. - Add `GET /admin/features` to list flags - Add `PUT /admin/features` to update flags - Update README with feature flag documentation
This commit is contained in:
12
README.md
12
README.md
@@ -26,6 +26,18 @@ EZ-API 是"大脑"。它管理着事实的来源 (Source of Truth)。
|
||||
- `POST /models`: 注册支持的模型。
|
||||
- `GET /models`: 列出所有模型。
|
||||
|
||||
### Feature Flags(给未来前端用)
|
||||
|
||||
控制平面会把轻量“开关配置”存到 Redis 的 hash:`meta:features`,并提供管理接口:
|
||||
|
||||
- `GET /admin/features`
|
||||
- `PUT /admin/features`(body 为 JSON map)
|
||||
|
||||
常用 flags:
|
||||
|
||||
- `dp_state_store_backend`: `memory`(默认)/ `redis`
|
||||
- `dp_claude_cross_upstream`: `true` / `false`
|
||||
|
||||
### 系统接口
|
||||
- `POST /sync/snapshot`: 强制将 DB 状态全量重新同步到 Redis。
|
||||
- `POST /logs`: 供 Balancer 推送日志的内部端点 (异步)。
|
||||
|
||||
@@ -113,6 +113,7 @@ func main() {
|
||||
handler := api.NewHandler(db, syncService, logWriter)
|
||||
adminHandler := api.NewAdminHandler(masterService, syncService)
|
||||
masterHandler := api.NewMasterHandler(masterService, syncService)
|
||||
featureHandler := api.NewFeatureHandler(rdb)
|
||||
|
||||
// 4.1 Prime Redis snapshots so DP can start with data
|
||||
if err := syncService.SyncAll(db); err != nil {
|
||||
@@ -157,6 +158,8 @@ func main() {
|
||||
adminGroup.Use(middleware.AdminAuthMiddleware(adminService))
|
||||
{
|
||||
adminGroup.POST("/masters", adminHandler.CreateMaster)
|
||||
adminGroup.GET("/features", featureHandler.ListFeatures)
|
||||
adminGroup.PUT("/features", featureHandler.UpdateFeatures)
|
||||
// Other admin routes for managing providers, models, etc.
|
||||
adminGroup.POST("/providers", handler.CreateProvider)
|
||||
adminGroup.POST("/models", handler.CreateModel)
|
||||
|
||||
106
internal/api/feature_handler.go
Normal file
106
internal/api/feature_handler.go
Normal file
@@ -0,0 +1,106 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/redis/go-redis/v9"
|
||||
)
|
||||
|
||||
const featuresKey = "meta:features"
|
||||
|
||||
// FeatureHandler manages lightweight feature flags stored in Redis for DP/CP runtime toggles.
|
||||
// Values are stored as plain strings in a Redis hash.
|
||||
type FeatureHandler struct {
|
||||
rdb *redis.Client
|
||||
}
|
||||
|
||||
func NewFeatureHandler(rdb *redis.Client) *FeatureHandler {
|
||||
return &FeatureHandler{rdb: rdb}
|
||||
}
|
||||
|
||||
// ListFeatures godoc
|
||||
// @Summary List feature flags
|
||||
// @Description Returns all feature flags stored in Redis (meta:features)
|
||||
// @Tags admin
|
||||
// @Produce json
|
||||
// @Security AdminAuth
|
||||
// @Success 200 {object} gin.H
|
||||
// @Failure 500 {object} gin.H
|
||||
// @Router /admin/features [get]
|
||||
func (h *FeatureHandler) ListFeatures(c *gin.Context) {
|
||||
if h.rdb == nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "redis not configured"})
|
||||
return
|
||||
}
|
||||
m, err := h.rdb.HGetAll(c.Request.Context(), featuresKey).Result()
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to read features", "details": err.Error()})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{"features": m})
|
||||
}
|
||||
|
||||
type UpdateFeaturesRequest map[string]any
|
||||
|
||||
// UpdateFeatures godoc
|
||||
// @Summary Update feature flags
|
||||
// @Description Updates selected feature flags (meta:features). Values are stored as strings.
|
||||
// @Tags admin
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Security AdminAuth
|
||||
// @Param request body object true "Feature map"
|
||||
// @Success 200 {object} gin.H
|
||||
// @Failure 400 {object} gin.H
|
||||
// @Failure 500 {object} gin.H
|
||||
// @Router /admin/features [put]
|
||||
func (h *FeatureHandler) UpdateFeatures(c *gin.Context) {
|
||||
if h.rdb == nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "redis not configured"})
|
||||
return
|
||||
}
|
||||
|
||||
var req UpdateFeaturesRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
updates := make(map[string]string, len(req))
|
||||
for k, v := range req {
|
||||
key := strings.TrimSpace(k)
|
||||
if key == "" {
|
||||
continue
|
||||
}
|
||||
switch vv := v.(type) {
|
||||
case string:
|
||||
updates[key] = strings.TrimSpace(vv)
|
||||
case bool:
|
||||
if vv {
|
||||
updates[key] = "true"
|
||||
} else {
|
||||
updates[key] = "false"
|
||||
}
|
||||
case float64:
|
||||
// JSON numbers decode as float64; keep as integer-looking string when possible.
|
||||
updates[key] = fmt.Sprintf("%v", vv)
|
||||
default:
|
||||
updates[key] = fmt.Sprintf("%v", vv)
|
||||
}
|
||||
}
|
||||
|
||||
if len(updates) == 0 {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "no valid feature updates"})
|
||||
return
|
||||
}
|
||||
|
||||
if err := h.rdb.HSet(c.Request.Context(), featuresKey, updates).Err(); err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to update features", "details": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{"updated": updates})
|
||||
}
|
||||
Reference in New Issue
Block a user