ci / verify (pull_request) Successful in 15m34s
新增图片矢量化、视频超分、每日用量、计价与任务隔离能力,并通过环境变量解析平台凭据。 已通过 Go 全量门禁、迁移检查、镜像构建以及 Vectorizer 五格式和 Topaz 3 秒视频真实 DEV 验收。
102 lines
3.4 KiB
Go
102 lines
3.4 KiB
Go
package httpapi
|
|
|
|
import (
|
|
"net/http"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/easyai/easyai-ai-gateway/apps/api/internal/auth"
|
|
"github.com/easyai/easyai-ai-gateway/apps/api/internal/store"
|
|
)
|
|
|
|
type dailyTokenUsageSummary struct {
|
|
CumulativeTokens int64 `json:"cumulativeTokens"`
|
|
PeakDailyTokens int64 `json:"peakDailyTokens"`
|
|
CurrentStreakDays int `json:"currentStreakDays"`
|
|
LongestStreakDays int `json:"longestStreakDays"`
|
|
}
|
|
|
|
// dailyTokenUsage godoc
|
|
// @Summary 查询每日 Token 与资源点用量
|
|
// @Description 按 IANA 时区返回连续自然日用量;API Key 仅统计当前 Key,JWT 统计当前用户。
|
|
// @Tags workspace
|
|
// @Produce json
|
|
// @Security BearerAuth
|
|
// @Param from query string true "开始日期 YYYY-MM-DD"
|
|
// @Param to query string true "结束日期 YYYY-MM-DD"
|
|
// @Param timezone query string false "IANA 时区" default(UTC)
|
|
// @Success 200 {object} DailyTokenUsageResponse
|
|
// @Failure 400 {object} ErrorEnvelope
|
|
// @Failure 401 {object} ErrorEnvelope
|
|
// @Failure 500 {object} ErrorEnvelope
|
|
// @Router /api/workspace/token-usage/daily [get]
|
|
func (s *Server) dailyTokenUsage(w http.ResponseWriter, r *http.Request) {
|
|
user, ok := auth.UserFromContext(r.Context())
|
|
if !ok {
|
|
writeError(w, http.StatusUnauthorized, "unauthorized")
|
|
return
|
|
}
|
|
timezone := strings.TrimSpace(r.URL.Query().Get("timezone"))
|
|
if timezone == "" {
|
|
timezone = "UTC"
|
|
}
|
|
location, err := time.LoadLocation(timezone)
|
|
if err != nil {
|
|
writeError(w, http.StatusBadRequest, "invalid IANA timezone")
|
|
return
|
|
}
|
|
from, err := time.ParseInLocation("2006-01-02", strings.TrimSpace(r.URL.Query().Get("from")), location)
|
|
if err != nil {
|
|
writeError(w, http.StatusBadRequest, "invalid from date")
|
|
return
|
|
}
|
|
to, err := time.ParseInLocation("2006-01-02", strings.TrimSpace(r.URL.Query().Get("to")), location)
|
|
if err != nil || to.Before(from) || to.Sub(from) > 399*24*time.Hour {
|
|
writeError(w, http.StatusBadRequest, "invalid to date or date range exceeds 400 days")
|
|
return
|
|
}
|
|
stored, err := s.store.ListDailyTokenUsage(r.Context(), user, from, to, timezone)
|
|
if err != nil {
|
|
s.logger.Error("list daily token usage failed", "error", err)
|
|
writeError(w, http.StatusInternalServerError, "list daily token usage failed")
|
|
return
|
|
}
|
|
byDate := make(map[string]store.DailyTokenUsage, len(stored))
|
|
for _, item := range stored {
|
|
byDate[item.Date] = item
|
|
}
|
|
items, summary := fillDailyTokenUsageDays(byDate, from, to)
|
|
writeJSON(w, http.StatusOK, map[string]any{
|
|
"items": items, "tokenDays": items, "summary": summary,
|
|
"range": map[string]string{"from": from.Format("2006-01-02"), "to": to.Format("2006-01-02"), "timezone": timezone},
|
|
})
|
|
}
|
|
|
|
func fillDailyTokenUsageDays(byDate map[string]store.DailyTokenUsage, from, to time.Time) ([]store.DailyTokenUsage, dailyTokenUsageSummary) {
|
|
items := make([]store.DailyTokenUsage, 0, 32)
|
|
summary := dailyTokenUsageSummary{}
|
|
currentRun := 0
|
|
for day := from; !day.After(to); day = day.AddDate(0, 0, 1) {
|
|
key := day.Format("2006-01-02")
|
|
item, found := byDate[key]
|
|
if !found {
|
|
item.Date = key
|
|
}
|
|
items = append(items, item)
|
|
summary.CumulativeTokens += item.TotalTokens
|
|
if item.TotalTokens > summary.PeakDailyTokens {
|
|
summary.PeakDailyTokens = item.TotalTokens
|
|
}
|
|
if item.TaskCount > 0 {
|
|
currentRun++
|
|
if currentRun > summary.LongestStreakDays {
|
|
summary.LongestStreakDays = currentRun
|
|
}
|
|
} else {
|
|
currentRun = 0
|
|
}
|
|
}
|
|
summary.CurrentStreakDays = currentRun
|
|
return items, summary
|
|
}
|