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

+27 -25
View File
@@ -6,7 +6,7 @@
> **旧的超详细版已归档、只读**:`Docs/archive/api-client.v0.65.md`(412 KB,含 v0.28→v0.65 全部历史条目)与 `Docs/archive/api-reference.v0.65.md`(170 KB,后端全量字段级文档)。归档的原因是**结构性臃肿 + 历史条目并列 + 长期漂移**;本文档**不复制**那段历史,需要考古时去归档。
> **维护铁律**:每实现/变更一个 REST 端点、WS 事件、admin API **立即改本文档**;契约变更必须版本化,不破坏既有客户端。
> **准确性 > 详尽度**:宁少写也别写错;**写不了确证的写「未确证」**,见 [§7](#7-在飞切片与未实现未确证)。
> ⚠️ **v0.67 是「契约先行」登记版**:本版新增的三批 —— **语音消息**(播放端点 + 消息 DTO `voice`)、**非消息事件查询面**、**账号在线状态** —— **当前尚未实现**,形状**取自设计文档**(`design/24 §10.11`、`design/26 §4.5` / `§2.6`)**而非代码核对**;已在 §7.2 逐条如实标注。**本文档其余内容仍以代码为真源**。
> ⚠️ **v0.67 是「契约先行」登记版**:本版新增的三批 —— **语音消息**(播放端点 + 消息 DTO `voice`)、**非消息事件查询面**、**账号在线状态** —— 登记时**都还没有实现**,形状**取自设计文档**(`design/24 §10.11`、`design/26 §4.5` / `§2.6`)**而非代码核对**。**已落地**:账号在线状态(`52dac45`)、非消息事件查询面(本切片,E6)—— 见 §2.4;**语音消息尚未实现**。已在 §7.2 逐条如实标注。**本文档其余内容仍以代码为真源**。
> **v0.68 是纯增量版(已实现,非契约先行)**:新增 1 条管理只读端点 `GET /api/v1/admin/config-facts`(配置事实面,见 §2.2 B12),并给 `GET /api/v1/admin/backthink` 的条目加 2 个可选字段(`urgency` / `quietHoursBypassed`,见 §2.2 B8)。**无破坏性变更**,老客户端零影响。
> **v0.69 是缺口修复版(已实现)**:`PUT /api/v1/admin/channels/{platform}/config` 现在**接收并原样落盘**账号顶层 `directMsgPerMin`,`GET /api/v1/admin/channels` 的账号元素也**如实回显**它(§2.2 A5 / §2.3 C28 注)。修的是「面板保存一次渠道配置,就把主人手配的**关闭流控(-1)**静默重置回 `0` = 保护性默认 6 条/分」。**无破坏性变更**(老客户端不带该键,行为与 v0.68 相同)。
> **v0.70 是精确化版(已实现,纯增量)**:`GET /api/v1/admin/backthink` 的 `urgency` **默认就是该轮实际生效的精确档位**(随记录落库),另加可选 `urgencyLowerBound:true` 专门标记**旧行的下限推导值**;实时 `backthink` 帧自本版起也带记录里的 `urgency`(§2.2 B8 / §4.10(b),并更正 §4.10(b) 原「实时帧不带这两键」的表述)。**无破坏性变更**(新键可选,老客户端不读即零影响;`urgency` 的既有取值域与 `quietHoursBypassed` 语义都未改)。
@@ -61,7 +61,7 @@
|---|---|
| `HistoryStore` | `GET /sessions` 回退到渠道会话视图;`POST /sessions`、`GET /sessions/{id}/messages`、`PATCH/DELETE /sessions/{id}`、`POST /sessions/{id}/read`、`PATCH/DELETE /messages/{id}` → `501 not_implemented` |
| 语音服务 | `POST /audio/*` → `500 voice_unavailable`;WS `voice_input` → `error` 帧 `{"message":"voice unavailable"}` |
| 渠道事件存储(`internal/runtime/events`) | `GET /admin/channels/events` 与 `.../events/counters` → `500 channel_events_unavailable`(**v0.67 登记,尚未实现**;绝不回全 0 假汇总,见 §2.4(a)) |
| 渠道事件存储(`internal/runtime/events`) | `GET /admin/channels/events` 与 `.../events/counters` → `500 channel_events_unavailable`(**v0.67 已实现**,见 §2.4(a);绝不回全 0 假汇总) |
| LLM usage 出口 | `GET /admin/llm/usage` → `500 usage_unavailable`(绝不回全 0 假汇总) |
| 活跃 SQLite 连接 | `GET /admin/system/storage` → `500 storage_unavailable` |
| 可观测日志缓冲 | `GET /admin/observability/logs` → `501`(组合根注入前) |
@@ -223,24 +223,26 @@
> - `id` 与 `empty=1` 同时给出 ⇒ `400 bad_request`;**两者都缺 ⇒ `400 bad_request`**(绝不把「id 缺失」默认解释成删无 id 条目 —— 那是一次没有字面证据的批量删除)。`?empty=` / `?empty=0` / `?empty=false` 一律是「否」。
> - 响应:`{data:{persisted, path, platform, accountId, empty, removed}}`(`empty`/`removed` 是 v0.66 新增);`removed` = 实际删除的条目数。删空后平台键整个消失(与按 id 删除的收尾口径一致)。
### 2.4 v0.67 登记、**尚未实现**的管理面(渠道事件查询 + 账号在线状态)
### 2.4 v0.67 登记的管理面(渠道事件查询 + 账号在线状态)—— **(a) 已实现 / (b) 已实现**
> ⚠️ **本节全部内容尚未进 `routes.go`**(截至契约 v0.67 冻结):形状取自设计文档(`design/26 §4.5`、`§2.6`),**不是**代码核对所得。**§2.0 的计数(12 + 11 + 40 = 63)不含本节端点**;实现落地后再回填 §2.1/§2.2 并更新计数。客户端**不要**现在就按本节编程到「已可用」——以 §7.2 的状态为准。
> ⚠️ **本节两项都已落地**(账号在线状态 `52dac45`;渠道事件查询面本切片 E6):形状取自设计文档(`design/26 §4.5`、`§2.6`),落地时已按代码核对。**§2.0 的计数(12 + 11 + 40 = 63)原本不含本节端点**;两个渠道事件端点已登记进 `routes.go`(`owner|admin` 档),下版改文档时应把它们并入 §2.1/§2.2 并更新计数。**本行留在此处仅为与 v0.67 冻结版对照**。
#### (a) 渠道事件(非消息事件)只读查询
#### (a) 渠道事件(非消息事件)只读查询 —— **已实现**
| # | 方法 | 路径 | 要点 |
|---|---|---|---|
| D1 | GET | `/api/v1/admin/channels/events` | 非消息事件列表(**倒序**)。query:`platform` / `tier` / `category` / `channelId` / `since` / `until` / `limit` / `offset`。响应 `{data:{events:[…], total}}`。`since` / `until` 按 **UTC ISO-8601 定宽字符串**做**字典序**比较(与 `GET /admin/llm/usage` 同口径);**跨度上限 90 天**(超 → `400`);`platform` 未知 → `400`。**不含正文、不含任何媒体 URL、不含任何凭据 / `flag`** |
| D2 | GET | `/api/v1/admin/channels/events/counters` | 逐类计数汇总(**近似值**)。query:`platform`。响应 `{data:{counters:[{platform,accountId,tier,category,platformType,count,firstSeenAt,lastSeenAt}]}}`,并带 **`approx:true`** 如实标注「计数为聚合近似值」(计数是**内存聚合 + 周期 flush**,非逐条实时)。⚠️ `approx` 的**嵌套层级**设计未明说,见 §7.4 |
| D2 | GET | `/api/v1/admin/channels/events/counters` | 逐类计数汇总(**近似值**)。query:`platform`。响应 `{data:{approx:true, counters:[{platform,accountId,tier,category,platformType,count,firstSeenAt,lastSeenAt}]}}`,`approx:true` 如实标注「计数为聚合近似值」(计数是**内存聚合 + 周期 flush**,非逐条实时)。**落地口径**:`approx` 与 `counters` **同级**(都在 `data` 内,见 §7.4-11 的落地记录) |
- **RBAC 档位未定**:设计原文写「owner/admin;`viewer` 只读亦可,按既有 RBAC 惯例」—— 两种解释都成立,本契约**不替它二选一**(见 §7.4-12)。落地前**不要**假设 viewer 能读。
- **RBAC 档位已定(落地口径)**:两个端点走 **`owner|admin`**(`writeAuth`),与同族的渠道消息日志面(§2.1 B2/B3、`/admin/channels/messages`)同档 —— 事件行带群 id / 成员 id / 关联消息 id,属私有内容面,不是给 viewer 的监控投影。**viewer 为 `403`**(已进 `rbac_viewer_test.go` 的 `ownerAdminReadRoutes` 覆盖)。
- **参数缺省与错误码(落地口径,见 §7.4-11)**:`limit` ≤0 或未给 ⇒ 用存储层缺省 `100`;超硬上限 ⇒ **钳到 `500`**(不报错,`total` 仍如实给出筛选条件下的总条数);非法整数(如 `limit=abc`)⇒ `400 bad_request`;`since`/`until` 非法 ⇒ `400 bad_request`;`platform` 未知 ⇒ `400 bad_request`;`tier` 非 `t1/t2/t3` ⇒ `400 bad_request`。⚠️ **`category` 不做白名单校验**:类别轴的权威在适配器层且会持续增加,未知类别是一次**精确匹配**(返回空),不是 400。
- **时间窗缺省(落地口径)**:**没给 `since`/`until` 就不注入时间条件**(不是「近 24h」)—— 端点绝不替客户端造一个下界(那正是「静默裁剪」)。只给 `since` 时按「现在」判跨度(超 90 天 ⇒ 400),但**不**把「现在」写进过滤条件。
- **数据源两张表**(`design/26 §4.1`,表名**复数**):`channel_events`(`tier` 为 `t2`/`t3` 的事件与**所有** `unknown` 逐条留存,**参与保留期 GC**)与 `channel_event_counters`(`t1` 与**所有**帧的计数,按 `(platform, account_id, platform_type)` upsert,**不参与 GC**)。
- `tier` 取值域:`t1`(只计数)/ `t2` / `t3`(逐条留存;T3 是「谁对谁做了什么」的定向交互记录)。`category` 取值域(**归一化类别**,设计原文给出 `heartbeat` / `lifecycle` / `join` / `leave` / `recall` / `ban` / `admin` / `friend_request` / `group_request` / `poke` / `reaction` / `button` / `vote` / `item` / `link` / `unknown`,并在末位带 `…` ⇒ **非穷举**,客户端**未知取值原样展示、不报错**)。
- `events[]` 元素 = 一条 `channel_events` 行的投影。设计只给到**落库列**:`id` / `platform` / `account_id` / `tier` / `category` / `platform_type` / `channel_id` / `channel_type` / `group_id` / `actor_id` / `actor_role`(`owner`/`member`/`system`/`unknown`,结构化解析所得)/ `target_id` / `self_related` / `message_id` / `summary` / `created_at` / `updated_at`。`actor_id`/`target_id` 是 **id 不是昵称**;`summary` 是**固定文案**(如「群成员加入」,**不含任何平台原文**)。⚠️ **元素的 JSON 键名设计未逐字给出**,见 §7.4-9 —— 客户端**现在不要硬编码**。
- **未接线存储时**:两个端点都回 **`500 channel_events_unavailable`**(对齐 `usage_unavailable` / `storage_unavailable` 的诚实边界口径,**绝不回全 0 假汇总**)。
- `events[]` 元素 = 一条 `channel_events` 行的投影。**落地口径(见 §7.4-9):键名一律 camelCase,逐字如下** —— `id` / `platform` / `accountId` / `tier` / `category` / `platformType` / `channelId` / `channelType` / `groupId` / `actorId` / `actorRole`(`owner`/`member`/`system`/`unknown`,结构化解析所得)/ `targetId` / `selfRelated`(bool)/ `messageId` / `summary` / `createdAt` / `updatedAt`(后两者是 **UTC RFC3339**,与 `llm/usage` 的时间回显同口径)。`actorId`/`targetId` 是 **id 不是昵称**;`summary` 是**固定文案**(如「群成员加入」,**不含任何平台原文**)。无数据时 `events` 是 `[]`(不是 `null`)。
- **未接线存储时**:两个端点都回 **`500 channel_events_unavailable`**(对齐 `usage_unavailable` / `storage_unavailable` 的诚实边界口径,**绝不回全 0 假汇总**)。读库失败同样是 `500 channel_events_unavailable`(错误文案不同)。
- 三个新 env 键(`design/26 §4.4`,已登记进 §5):`CHANNEL_EVENT_RETENTION`(默认 `720h`)、`CHANNEL_EVENT_GC_INTERVAL`(默认 `1h`)、`CHANNEL_EVENT_COUNTER_FLUSH`(默认 `30s`)。
- ⚠️ **微信渠道不提供非消息事件**:查询面 / 面板对该渠道**如实显示空 + 说明文字**,绝不伪造、绝不用别的渠道事件顶替。
- ⚠️ **微信渠道不提供非消息事件**:查询面 / 面板对该渠道**如实显示空 + 说明文字**,绝不伪造、绝不用别的渠道事件顶替(落地时已有测试钉住:`platform=weixin-ilink` 的响应里绝不出现别的平台的事件)。
#### (b) `GET /api/v1/admin/channels` 的账号项:`accountOnline`
@@ -419,13 +421,13 @@
| 413 | `payload_too_large`、`document_too_large` | 上传/文档超限 |
| 415 | `unsupported_media_type` | 图片/文档类型拒绝 |
| 429 | `rate_limited` | 限流(框架级) |
| 500 | `internal`、`chat_unavailable`、`chat_failed`、`voice_unavailable`、`stt_failed`、`tts_failed`、`tts_empty`、`voice_clone_failed`、`usage_unavailable`、`storage_unavailable`、`channel_events_unavailable`(**v0.67 登记、尚未实现**)、`refresh_unavailable`、`sessions_failed`、`messages_failed`、`session_failed`、`message_failed`、`document_upload_failed`、`login_failed` | 内部错误或依赖未接线(**绝不伪装成功**) |
| 500 | `internal`、`chat_unavailable`、`chat_failed`、`voice_unavailable`、`stt_failed`、`tts_failed`、`tts_empty`、`voice_clone_failed`、`usage_unavailable`、`storage_unavailable`、`channel_events_unavailable`(**v0.67 已实现**,见 §2.4(a))、`refresh_unavailable`、`sessions_failed`、`messages_failed`、`session_failed`、`message_failed`、`document_upload_failed`、`login_failed` | 内部错误或依赖未接线(**绝不伪装成功**) |
| 501 | `not_implemented` | HistoryStore 等未注入 |
| 503 | `timeout`、`chat_dispatch_failed`、`catalog_unavailable` | 派发/上游不可用 |
> **未确证**:`conflict`(409) 的通用码存在(框架级 `ErrConflict`),但除了 `chat_busy` 之外,本文档未逐一确证还有哪些端点会带它。
> **⚠️ `416` 不是错误信封(v0.67 登记,尚未实现)**:`GET /api/v1/media/{name}` 的 `Range` 语法非法或范围不可满足时,**由 Go 标准库直写** `416`(`text/plain` + `Content-Range: bytes */<size>`),**不套** `{"error":{…}}` ⇒ 客户端**不要**拿 `error.code` 去解析它(见 §4.5)。这是字节流端点的既有事实(`/audio/speech` 的 `200`、`/documents/{name}` 的 `200` 同样不是 JSON 信封),**不是**本版新引入的怪癖。
> **`400` 的渠道事件用法(v0.67 登记,尚未实现)**:`GET /admin/channels/events` 对 `platform` 未知、`since`/`until` 跨度 > 90 天都回 `400`(**具体 `code` 名设计未给出**,见 §7.4-11)。
> **`400` 的渠道事件用法(v0.67 已实现,见 §2.4(a))**:`GET /admin/channels/events` 对 `platform` 未知、`tier` 非法、`since`/`until` 跨度 > 90 天、`since`/`until` 时间点非法、`limit`/`offset` 非整数都回 `400`(**落地用的 `code` 是通用的 `bad_request`**,见 §7.4-11)。
### 4.2 分页与游标
@@ -763,9 +765,9 @@
| `SUBAGENT_TIMEOUT_SEC` | `600` | 单个子代理整次运行时限 |
| `MCP_SERVERS_JSON` | 空 | MCP server 注册表(JSON 数组)。⚠️ 扁平 `MCP_NAME`/`MCP_COMMAND`/… 为**未接线残留键,配了不生效** |
| `CHANNEL_*` / `CHANNELS_JSON` | 见模板 | 渠道接入(`.env` 为兜底,面板 `channels.json` 优先) |
| `CHANNEL_EVENT_RETENTION` | `720h` | **v0.67 登记(尚未实现)**:`channel_events` 保留期 —— 比文档的 `168h` 长,因为事件表的核心用途是**事后定案**(一周太短);`≤0` = 关闭 GC |
| `CHANNEL_EVENT_GC_INTERVAL` | `1h` | **v0.67 登记(尚未实现)**:渠道事件 GC 扫描间隔(与 `DOCUMENT_GC_INTERVAL` 同范式) |
| `CHANNEL_EVENT_COUNTER_FLUSH` | `30s` | **v0.67 登记(尚未实现)**:计数内存聚合的 flush 间隔。计数是**尽力而为的近似值**(进程崩丢 ≤30s),查询面以 `approx:true` 如实标注(见 §2.4(a)) |
| `CHANNEL_EVENT_RETENTION` | `720h` | **v0.67 登记,已接线**(E4 `afe9afb`;`.env.example` 同步于 `f9c5b56`):`channel_events` 保留期 —— 比文档的 `168h` 长,因为事件表的核心用途是**事后定案**(一周太短);`≤0` = 关闭 GC |
| `CHANNEL_EVENT_GC_INTERVAL` | `1h` | **v0.67 登记,已接线**(E4 `afe9afb`):渠道事件 GC 扫描间隔(与 `DOCUMENT_GC_INTERVAL` 同范式) |
| `CHANNEL_EVENT_COUNTER_FLUSH` | `30s` | **v0.67 登记,已接线**(E4 `afe9afb`):计数内存聚合的 flush 间隔。计数是**尽力而为的近似值**(进程崩丢 ≤30s),查询面以 `approx:true` 如实标注(见 §2.4(a)) |
**与契约相关但「配了不生效」的键**:
@@ -816,17 +818,17 @@
### v0.67(上一版)
**三批已在设计文档定稿的新增面一次性登记**:语音消息(播放端点 + 消息 DTO `voice` + 音频副本口径)、非消息事件查询面、账号在线状态(依据 `design/24 §10.11`、`design/26 §4.5` / `§2.6`)。登记时三批**都还没有实现**;其中**账号在线状态已落地**(见下)。
**三批已在设计文档定稿的新增面一次性登记**:语音消息(播放端点 + 消息 DTO `voice` + 音频副本口径)、非消息事件查询面、账号在线状态(依据 `design/24 §10.11`、`design/26 §4.5` / `§2.6`)。登记时三批**都还没有实现**;其中**账号在线状态**(`52dac45`)与**非消息事件查询面**(本切片 E6)**已落地**(见下)。
> ⚠️ **本版是「契约先行」登记**:登记的三批里,**账号在线状态已落地**(本次,见 §2.4(b) / §7.2),**语音消息与非消息事件查询面尚未实现**;后两批的形状**取自设计文档而非代码核对**。客户端**先按形状对齐、不要按「已可用」编程**;实现落地后本版会补记 commit 并把 §7.2 相应行改为上线。
> ⚠️ **本版是「契约先行」登记**:登记的三批里,**账号在线状态**(见 §2.4(b))与**非消息事件查询面**(见 §2.4(a))**已落地**;**语音消息尚未实现**,那一批的形状**取自设计文档而非代码核对**。客户端**先按形状对齐、不要按「已可用」编程**;实现落地后本版会补记 commit 并把 §7.2 相应行改为上线。
- **纯增量**:
① 新端点 `GET /api/v1/media/{name}`(用户侧,**owner/admin 必须 Bearer**):`200` 字节流 / `206 Partial Content` / `304 Not Modified`;`Content-Type: audio/*`;`Content-Disposition: inline; filename="<name>"`;`nosniff`;`Accept-Ranges: bytes`;`Cache-Control: private, max-age=86400, immutable`;支持 `Range` 与 `HEAD`;`404 media_not_found`;`401`/`403` 由中间件给出。⚠️ **`416` 是标准库直写的非 JSON 响应**(见 §0.3、§4.1、§4.5)。
② 消息 DTO 增**可选** `voice` 对象(`omitempty`;**非语音消息键缺席**):`{source,status,name?,mime?,size?,durationMs?,url?}`;`source` ∈ `asr`/`platform`,`status` ∈ `transcribed`/`empty`/`failed`,`name`/`mime`/`size`/`url` **四者同生共死**(有副本时才在);`url` 是服务端相对路径 `/api/v1/media/<name>`。**🎤 判据 = `voice` 键存在**。一次覆盖全部读取面:`GET /sessions/{id}/messages`、`PATCH /messages/{id}` 响应、`GET /admin/channels[/{platform}]/messages`、`message_persisted` / `channel_persisted` / `message_edited`、握手 `history` 回放(§3.2、§3.4.4、§4.4)。
③ 新错误码 **`media_not_found`(404)** → 加进 §4.1 的 404 行(与 `document_not_found` 并列;**刻意不复用**它,否则客户端分不清缺的是文档还是语音)。
④ 新错误形态 **`416`**(**非信封**,标准库直写)→ §0.3 / §4.1 / §4.5。
⑤ 新 admin 端点 **`GET /api/v1/admin/channels/events`**(非消息事件列表,倒序;`?platform=&tier=&category=&channelId=&since=&until=&limit=&offset=`;响应 `{data:{events:[…],total}}`;`since`/`until` 按 UTC ISO-8601 定宽字符串**字典序**比较、**跨度上限 90 天**;不含正文、不含媒体 URL、不含任何凭据 / `flag`)—— §2.4(a)。
⑥ 新 admin 端点 **`GET /api/v1/admin/channels/events/counters`**(逐类计数汇总 `{data:{counters:[{platform,accountId,tier,category,platformType,count,firstSeenAt,lastSeenAt}]}}`,并带 **`approx:true`** 如实标注「计数为聚合近似值」)—— §2.4(a);未接线存储 ⇒ **`500 channel_events_unavailable`**(新增错误码,**绝不回全 0 假汇总**)。
⑤ 新 admin 端点 **`GET /api/v1/admin/channels/events`**(非消息事件列表,倒序;`?platform=&tier=&category=&channelId=&since=&until=&limit=&offset=`;响应 `{data:{events:[…],total}}`;`since`/`until` 按 UTC ISO-8601 定宽字符串**字典序**比较、**跨度上限 90 天**;不含正文、不含媒体 URL、不含任何凭据 / `flag`)—— §2.4(a)。**已落地**(`owner|admin` 档;元素键名 camelCase,逐字见 §2.4(a);未给窗口 ⇒ 不注入窗口)。
⑥ 新 admin 端点 **`GET /api/v1/admin/channels/events/counters`**(逐类计数汇总 `{data:{approx:true,counters:[{platform,accountId,tier,category,platformType,count,firstSeenAt,lastSeenAt}]}}`)—— §2.4(a);未接线存储 ⇒ **`500 channel_events_unavailable`**(新增错误码,**绝不回全 0 假汇总**)。**已落地**(`approx` 与 `counters` 同级;`platform` 未知 ⇒ `400 bad_request`)。
⑦ `GET /api/v1/admin/channels` 的 `accounts[]` 增**可选** `accountOnline`(**账号在平台侧是否在线**,与 `connected`(适配器/WS 是否连着)**是两件事**;**键缺席 = 状态未知 ≠ 离线**)—— §2.4(b)。**已落地**(管理面按可选接口断言读 `AccountStatus()`;`known=false` 或适配器无该状态面 ⇒ 键缺席)。
⑧ 新 env 键 3 个:`CHANNEL_EVENT_RETENTION`(`720h`)/ `CHANNEL_EVENT_GC_INTERVAL`(`1h`)/ `CHANNEL_EVENT_COUNTER_FLUSH`(`30s`)—— 已在 §5 登记(本切片**不新增**语音相关 env 键;语音上限/保留期复用既有常量与 `DOCUMENT_*`)。
⑨ **WS 协议零变化**:`media` 是 REST 端点;无新帧、无新上行字段。
@@ -917,7 +919,7 @@
### 7.1 在飞切片
**v0.66 及更早登记的在飞项已全部冻结、客户端可直接依赖**(下表)。⚠️ **v0.67 新登记的三批中,账号在线状态(`accounts[].accountOnline`)已落地**(本次,见 §7.2);**语音消息 / 非消息事件查询面尚未实现**,**不在本表** —— 逐条见 §7.2。
**v0.66 及更早登记的在飞项已全部冻结、客户端可直接依赖**(下表)。⚠️ **v0.67 新登记的三批中,账号在线状态(`accounts[].accountOnline`)与非消息事件查询面都已落地**(见 §7.2);**语音消息尚未实现**,**不在本表** —— 逐条见 §7.2。
| 项 | 状态 | 冻结位置 |
|---|---|---|
@@ -935,7 +937,7 @@
| 项 | 现状 |
|---|---|
| **🔴 v0.67 登记的「语音消息面」**(`GET /api/v1/media/{name}` + 消息 DTO `voice` + 入站语音音频副本落盘) | **未实现**(**契约已先行登记**):端点 / 字段 / 副本口径见 §1.2 U19、§4.4、§4.5;权威设计 `design/24 §10`。⚠️ 同批的**行为变更**(QQ 语音 `content` 不再含 `[语音]`)与**口径变更**(`/admin/system/storage` 的 `documents` 段含语音副本)也**随该切片一起生效**,现在**尚未生效** |
| **🔴 v0.67 登记的「非消息事件查询面」**(`GET /admin/channels/events`、`GET /admin/channels/events/counters`) | **未实现**(**契约已先行登记**):形状与错误码见 §2.4(a);权威设计 `design/26 §4.5`。未接线存储 ⇒ `500 channel_events_unavailable`;⚠️ 两个端点的 **RBAC 档位设计未定**(见 §7.4-12) |
| **✅ v0.67 登记的「非消息事件查询面」**(`GET /admin/channels/events`、`GET /admin/channels/events/counters`) | **已实现**(v0.67 登记项本切片落地):形状与错误码见 §2.4(a)(已按代码核对:元素键名 camelCase、`approx` 与 `counters` 同级、`400 bad_request`、`owner\|admin` 档);权威设计 `design/26 §4.5`。未接线存储 ⇒ `500 channel_events_unavailable`。⚠️ 本行留在此表仅为与上一版对照,下版改文档时应移入 §7.1 在飞切片表并回填 §2.1/§2.2 的端点计数 |
| **✅ v0.67 登记的「账号在线状态」**(`GET /admin/channels` 的 `accounts[].accountOnline`) | **已实现**(v0.67 登记项本次落地;落地前基线 `f91a83f`,本切片 commit 由主会话补记):形状见 §2.4(b)。管理面按**可选接口断言**读 `AccountStatus()`(`plugins/channels/qq_obv11/probe.go`,`a79fe96`);适配器**没有**该状态面、或实现了但 `known=false` ⇒ **键缺席**(未知 ≠ 离线,绝不用 `false` 冒充)。当前只有 `qq-obv11` 会带该键。⚠️ 本行留在此表仅为与上一版对照,下版改文档时应移入 §7.1 在飞切片表 |
| **✅ P1-T:`directMsgPerMin` 的 gateway 写入路径**(`GET /admin/channels` 的 `accounts[].directMsgPerMin` + `PUT .../config` 的顶层 `directMsgPerMin`) | **已实现**(v0.69):缺口 = gateway 面没让该字段通过 ⇒ **面板保存一次渠道配置就把主人手配的「关闭流控(-1)」静默重置回 `0` = 保护性默认 6 条/分**。现 5 处透传(`AdminChannelAccount` DTO / `channelConfigRequest` / `config.ChannelAccount` 拷贝 / `adminChannelAccountOf` / 微信扫码重建账号)。形状见 §2.2 A5 与 §2.3 C28 注 |
| **`graph_update` 按路分桶** | **未实现**:`graph_update` 只带 `data.sessionId`(会话级),**无 `turnId`** |
@@ -970,10 +972,10 @@
6. **`account_not_registered`(409) 的适用面**:本文档只确证了 C19/C20(open/close)会带它;`POST /admin/channels/test`、`/{platform}/message/test` 等其它渠道端点是否也有等价的账号级错误码 —— **未逐一确证**。
7. **`turn_begin.data.leg` 在长会话中的取值范围**:代码里是会话内单调递增序号(非「1/2 路」固定枚举),但本文档**未确证**客户端应如何展示 >2 的取值 —— 建议只当诊断信息用。
8. **`GET /admin/channels` 的 `accounts[]` 除 `connected` 外的字段集**:本文档未逐字段展开(该结构在 v0.66 只是**新增**一个可选键;v0.67 另登记了一个可选 `accountOnline`,见 §2.4(b))。
9. **(v0.67 登记)`GET /admin/channels/events` 的 `events[]` 元素 JSON 键名**:设计只给到 `channel_events` 的**落库列**(snake_case 列名)与 `{events,total}` 外框,**未逐字给出**元素的 JSON 键名(camelCase 还是 snake_case 未定)⇒ 客户端**现在不要硬编码**;`counters[]` 的元素名设计已逐字给出且为 camelCase(大概率同风格,但**未确证**)。
10. **(v0.67 登记)`events.total` 的语义**:是**筛选条件下的总条数**(不受 `limit`/`offset` 影响)还是**本次返回条数** —— 设计未说明。
11. **(v0.67 登记)渠道事件查询的参数默认值与错误码名**:`limit`/`offset` 的默认值、以及 `platform` 未知 / 跨度超 90 天时的**具体 `400` `code` 名**(设计只写「→ `400`」)—— 均未给出。
12. **(v0.67 登记)两个渠道事件端点的 RBAC 档位**:设计原文「owner/admin;`viewer` 只读亦可,按既有 RBAC 惯例」**自相含混**(按既有分档,渠道消息日志面 B2/B3 是 owner/admin 档)⇒ 落地前**不要**假设 viewer 能读。
9. **(v0.67 登记 · 已落地确证)`GET /admin/channels/events` 的 `events[]` 元素 JSON 键名**:**已定为 camelCase**,逐字与 `internal/services/gateway/admin_channel_events.go` 的 `channelEventDTO` 一致:`id` / `platform` / `accountId` / `tier` / `category` / `platformType` / `channelId` / `channelType` / `groupId` / `actorId` / `actorRole` / `targetId` / `selfRelated` / `messageId` / `summary` / `createdAt` / `updatedAt`(时间为 **UTC RFC3339**;`counters[]` 同为 camelCase,风格一致)。客户端**可以**硬编码这些键名了。
10. **(v0.67 登记 · 已落地确证)`events.total` 的语义**:是**筛选条件下的总条数**(**不受** `limit`/`offset` 影响;与 `events` 同源条件),因此 `total > events.length` 是正常的分页状态。
11. **(v0.67 登记 · 已落地确证)渠道事件查询的参数默认值与错误码名**:`limit` 未给 / `≤0` ⇒ 存储层缺省 **`100`**;超硬上限 ⇒ **钳到 `500`**(不报错);`offset` 负值 ⇒ `0`;**非法整数 ⇒ `400 bad_request`**。`platform` 未知 / `tier` 非 `t1|t2|t3` / `since`-`until` 跨度超 90 天 / 时间点非法 ⇒ 一律 **`400` + `code:"bad_request"`**(未新造专用错误码)。`category` **不做白名单**(未知类别 = 精确匹配 ⇒ 空结果)。`approx` 的**嵌套层级已定为 `data.approx`**(与 `data.counters` 同级)。
12. **(v0.67 登记 · 已落地确证)两个渠道事件端点的 RBAC 档位**:**`owner|admin`**(`writeAuth`),与渠道消息日志面(B2/B3)同档;**viewer 为 `403`**。依据:事件行带群 id / 成员 id / 关联消息 id,属私有内容面,不是 viewer 的监控投影。
13. **(v0.67 登记 · 已确证)`accounts[].accountOnline` 的字段名与是否有 `known` 同级键**:字段名 `accountOnline` 为本契约所拟(设计只规定「独立状态面 / `AccountStatus()` / `known` 三态」,未给管理面字段名),**实现沿用了它、未改名**;**不发** `known` 同级键 —— 「未知」由**键缺席**承载。哪些平台会带该键**已确证**:只有适配器实现了 `AccountStatus()` 的平台(当前仅 `qq-obv11`)。
14. **(v0.67 登记)音频各容器在各客户端的可播放性**:`amr` / `flac` / `opus` 在部分浏览器内核上播不了(design/24 §10.6 明确「未逐一实测」)⇒ 客户端**以实测为准**,播不了就照实提示。
15. **(v0.67 登记)`GET /admin/system/storage` 的 `documents` 段是否拆出语音副本**:设计**本期不拆**(不新增 `kind` 列 / 不按 ext 聚合)⇒ 该段**只增口径、不变形状**,客户端**不要**指望能分开统计。