feat(gateway): 渠道事件查询面 + 面板「渠道事件」页(design/26 E6)
CI / Backend (build / vet / test / cross-compile) (push) Has been cancelled
CI / Frontend (tsc + vite build) (push) Has been cancelled

- 两个只读端点:GET /admin/channels/events(列表,8 个过滤条件 + 分页钳制)与 .../events/counters(逐类计数,恒带 approx:true 如实标「近似」)
- 诚实边界:>90 天窗口 ⇒ 400(不静默裁剪);未知 platform/档位 ⇒ 400;**未接线 ⇒ 500 channel_events_unavailable**(绝不回空 200 假装「没有事件」);微信平台的空如实说明「该渠道不提供非消息事件」而不借别平台顶替
- 面板:新 section「渠道事件」(计数卡 + 事件行 + 过滤条 + 30s 静默刷新),空/错误态分开且失败显示错误不显示 0
- 组合根接线(主会话补):wire_frontend.go 的 Deps 注入 EventStore: a.channelEventStore()(未接线时为真 nil ⇒ 保留诚实 500)
- 契约不升版(查询面 v0.67 已登记):就地改「未实现 → 已实现 + 端点路径」,并补登记 5 处原先留白的落地值(camelCase / 通用 bad_request / RBAC owner|admin / data.approx / total 语义)
- 顺带把 §5 三个 CHANNEL_EVENT_* env 键由「尚未实现」改「已接线」(E4)
- 变异实测:去掉 90 天上限 ⇒ status 200 want 400;未接线回空 200 ⇒ status 200 want 500
- 债务登记:events.Filter 无 AccountID 列 ⇒ 服务端暂不能按账号筛(列表仍如实下发 accountId),要加需先改 internal/runtime/events
This commit is contained in:
AskaEth committed 2026-10-05 14:38:37 +08:00
1 parent a0abf0199a
commit c8053b1f36
21 files changed
+1805 -30

No files matched your search

+9 -5
View File
@@ -90,17 +90,17 @@ func (s *Server) adminLlmUsage(w http.ResponseWriter, r *http.Request) {
writeJSON(w, http.StatusOK, llmUsageResponseOf(summary))
}
// llmUsageQuery 解析并**校验**查询参数:`since`/`until` 见 parseLlmUsageInstant,
// llmUsageQuery 解析并**校验**查询参数:`since`/`until` 见 parseInstantParam,
// 缺省窗口为近 24h,跨度上限见 model.UsageMaxWindow。
//
// 校验顺序是「形状先于语义」:时间点解析不了就直接 400,不会拿一个零值窗口去查库。
func llmUsageQuery(r *http.Request) (model.UsageQuery, error) {
params := r.URL.Query()
since, err := parseLlmUsageInstant(params.Get("since"))
since, err := parseInstantParam(params.Get("since"))
if err != nil {
return model.UsageQuery{}, fmt.Errorf("since 非法:%w", err)
}
until, err := parseLlmUsageInstant(params.Get("until"))
until, err := parseInstantParam(params.Get("until"))
if err != nil {
return model.UsageQuery{}, fmt.Errorf("until 非法:%w", err)
}
@@ -123,14 +123,18 @@ func llmUsageQuery(r *http.Request) (model.UsageQuery, error) {
}, nil
}
// parseLlmUsageInstant 解析一个时间点参数。接受两种形态:
// parseInstantParam 解析一个时间点参数。接受两种形态:
//
// - **RFC3339**(如 `2026-10-05T00:00:00Z`,JS 的 `toISOString()` 就是这个);
// - **Unix 毫秒**(纯数字,如 `1759622400000`)—— 与 `/sessions/{id}/messages`
// 的 `before`/`after` 游标同一种口径,便于客户端复用游标。
//
// 空串 = 未给(返回零值)。其它形态报错(绝不静默当成「现在」)。
func parseLlmUsageInstant(raw string) (time.Time, error) {
//
// 它是**跨端点共用**的解析器:`/admin/llm/usage` 与 `/admin/channels/events`
// (切片 E6)的 `since`/`until` 口径相同(同一形态 ⇒ 同一解析 ⇒ 同一比较语义),
// 因此只此一份,不各写一遍。
func parseInstantParam(raw string) (time.Time, error) {
raw = strings.TrimSpace(raw)
if raw == "" {
return time.Time{}, nil