feat(gateway): 渠道事件查询面 + 面板「渠道事件」页(design/26 E6)
- 两个只读端点: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:
1 parent
a0abf0199a
commit
c8053b1f36
21 files changed
+1805
-30
No files matched your search
@@ -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}`);
|
||||
}
|
||||
@@ -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[];
|
||||
}
|
||||
Reference in new issue
Block a user