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

+3
View File
@@ -111,6 +111,9 @@ export {
} from "./api/adminChannels";
export type { ChannelActionResult, ChannelConfigSaveResult } from "./api/adminChannels";
export { adminChannelEventCounters, adminChannelEvents } from "./api/adminChannelEvents";
export type { ChannelEventQuery } from "./api/adminChannelEvents";
export {
adminBackthink,
adminConfigFacts,
@@ -0,0 +1,45 @@
import type { AdminChannelEventCounters, AdminChannelEventList } from "../types";
import { request } from "./core";
/** `GET /api/v1/admin/channels/events` 的查询条件(全部可选;空串 = 不过滤)。
*
* 与后端 `channelEventsListFilter` 逐项对应:后端**不注入**缺省时间窗
* (没给 `since`/`until` 就是不加时间条件),因此这里也绝不替调用方补一个 ——
* 面板自己显式传它想看的窗口,界面上才不会出现「我以为是全部,其实是最近一天」。 */
export interface ChannelEventQuery {
platform?: string;
tier?: string;
category?: string;
channelId?: string;
/** RFC3339(`toISOString()`);跨度上限 90 天,超了后端回 400。 */
since?: string;
until?: string;
limit?: number;
offset?: number;
}
/** 渠道事件列表(倒序)。只读;不含正文 / URL / 凭据。 */
export function adminChannelEvents(query: ChannelEventQuery = {}): Promise<AdminChannelEventList> {
const params = new URLSearchParams();
const add = (key: string, value: string | undefined): void => {
const trimmed = value?.trim();
if (trimmed) params.set(key, trimmed);
};
add("platform", query.platform);
add("tier", query.tier);
add("category", query.category);
add("channelId", query.channelId);
add("since", query.since);
add("until", query.until);
if (query.limit && query.limit > 0) params.set("limit", String(query.limit));
if (query.offset && query.offset > 0) params.set("offset", String(query.offset));
const qs = params.toString();
return request<AdminChannelEventList>(`/admin/channels/events${qs ? `?${qs}` : ""}`);
}
/** 逐类计数汇总(近似值,响应带 `approx`)。`platform` 空 = 全部平台。 */
export function adminChannelEventCounters(platform?: string): Promise<AdminChannelEventCounters> {
const trimmed = platform?.trim();
const qs = trimmed ? `?platform=${encodeURIComponent(trimmed)}` : "";
return request<AdminChannelEventCounters>(`/admin/channels/events/counters${qs}`);
}
+1
View File
@@ -13,3 +13,4 @@ export * from "./types/adminWrite";
export * from "./types/kook";
export * from "./types/notification";
export * from "./types/configFacts";
export * from "./types/channelEvents";
@@ -0,0 +1,66 @@
// 管理面「渠道事件」查询的线上类型(design/26 §4.5;切片 E6,契约 v0.67 登记)。
// 唯一真源:internal/services/gateway/admin_channel_events.go。
//
// 事件行里**没有**平台原文、媒体 URL、凭据或 OneBot `flag`(design/26 §4.2 是红线,
// 事件表本身就没有这些列),因此这里也刻意不存在承载它们的字段:面板查不出、
// 也画不出那些东西 —— 这不是「没显示」,是「根本取不到」。
/** 一条 `channel_events` 行(T2/T3/`unknown`/`link` 逐条留存)。 */
export interface AdminChannelEvent {
id: number;
/** 平台(`qq-obv11` / `kook` / `telegram` / `weixin-ilink` / `webhook`)。 */
platform: string;
/** 多账号 id;空 = 单账号 / 未知。 */
accountId: string;
/** 留存档:`t1`/`t2`/`t3`。刻意不收窄成联合类型:将来新增档位时旧前端照旧渲染
* (绝不因为「我不认识这一档」而丢掉一行)。 */
tier: string;
/** 归一化类别(`join`/`recall`/`poke`/`heartbeat`/`unknown`…)。 */
category: string;
/** 平台原生类型码(`notice_type`/`extra.type`/Update 键名),排障用。 */
platformType: string;
channelId: string;
channelType: string;
groupId: string;
/** 行为者**结构化 id**(不是昵称)。 */
actorId: string;
/** `owner`/`member`/`system`/`unknown`(结构化解析所得)。 */
actorRole: string;
/** 被作用者 id(戳一戳目标 / 被禁言者…);无则空串。 */
targetId: string;
/** 是否命中「她自己」(design/26 §2.2 的升档依据)。 */
selfRelated: boolean;
/** 关联消息 id(撤回 / 回应 / 按钮 / 置顶)。 */
messageId: string;
/** 脱敏结构级摘要(固定文案,不含平台原文)。 */
summary: string;
/** UTC RFC3339。 */
createdAt: string;
updatedAt: string;
}
/** `GET /admin/channels/events` 的 data 本体。`events` 恒为数组(无数据时 `[]`)。 */
export interface AdminChannelEventList {
events: AdminChannelEvent[];
total: number;
}
/** 计数表的一行:某个平台某类事件的累计帧数。 */
export interface AdminChannelEventCounter {
platform: string;
accountId: string;
tier: string;
category: string;
platformType: string;
count: number;
firstSeenAt: string;
lastSeenAt: string;
}
/** `GET /admin/channels/events/counters` 的 data 本体。
* `approx` = 计数是**近似值**(内存聚合 + 每 30s 落盘,读到的表不含待落盘部分,
* 因此只少不多);面板必须如实标注,绝不画成精确账。 */
export interface AdminChannelEventCounters {
approx: boolean;
counters: AdminChannelEventCounter[];
}