feat(channels): 按账号连接/断开 + 可删空 id 条目(修主人真机踩的两个缺口)

缺口①:「连接」是按平台连、且恒打第一条账号(channelActionRequest 只有 name)⇒ 主人有两条
qq-obv11 时点到的是 mode=server 的历史条目,报 server mode requires listenAddr,而正确的
client 条目轮不到。缺口②:空 id 条目既删不掉(DELETE 要求 id 非空)也改不了(UpsertChannel
空 id = 新建),越改越多。

后端:
- POST /admin/channels/open|close 请求体新增可选 accountId(缺省逐字兼容,老客户端零影响);
  按该账号驱动;未知 id ⇒ 404 且零连接(不静默回落第一条);已配置未注册 ⇒ 409
- DELETE /admin/channels/{platform}/config 新增显式 empty=1(id 与 empty=1 互斥 ⇒ 400;
  无可删 ⇒ 404);语义严格 —— 只删空 id 条目,绝不把「id 为空」默认解释成删空 id
- 响应新增:open/close 的 resolved(实际驱动的适配器名)与 accountId;delete 的 removed/empty;
  GET /admin/channels 的 accounts[].connected(可选 bool,缺省 = 未知)
- 审计动作名不变(channel.open/close/delete),日志行新增 accountId 字段
前端:每条账号一行「连接/断开」(带 accountId);按钮旁明确「会连哪一条」;空 id 行显著标注
+ 删除入口;channelActionBody 携带 accountId;新增 action 测试 33 checks + 空 id 行渲染 31 checks。
测试:后端 14 个用例(真实 HTTP 路由 + JWT + 真注册表 + spy 适配器,断言「哪一条被驱动、
另一条一次都没被碰」)。
变异 6 条(accountId 被忽略 / id 为空当删空 id / 找不到静默回落 / 前端丢 accountId /
删空 id 退化成 ?id= / 空 id 行按普通行渲染)。
This commit is contained in:
AskaEth committed 2026-10-05 10:37:56 +08:00
1 parent c4ea02a887
commit 42279b7124
21 files changed
+1719 -164

No files matched your search

+39 -7
View File
@@ -15,20 +15,43 @@ export interface ChannelConfigSaveResult {
path: string;
platform: string;
accountId: string;
/** 仅 `?empty=1` 的删除会带上:本次实际删掉的无 id 条目数。 */
removed?: number;
empty?: boolean;
}
/** POST /api/v1/admin/channels/open — connects a registered channel adapter. */
export function adminChannelOpen(name: string): Promise<{ name: string; connected: boolean }> {
return request<{ name: string; connected: boolean }>("/admin/channels/open", {
/** Result of POST /admin/channels/open|close. `resolved` is the adapter that was
* actually driven (`qq-obv11:9616b27a`), which is what the panel must show —
* the request's `name` alone never tells a multi-account platform which account
* was touched. */
export interface ChannelActionResult {
name: string;
resolved?: string;
accountId?: string;
connected: boolean;
}
/** open/close 的请求体。**accountId 缺省时逐字维持旧契约 `{name}`**(老后端/老客户端
* 仍互通);给出时才是多账号定位,后端据此**只**驱动该账号。 */
export function channelActionBody(name: string, accountId?: string): { name: string; accountId?: string } {
const id = accountId?.trim();
return id ? { name, accountId: id } : { name };
}
/** POST /api/v1/admin/channels/open — connects a registered channel adapter.
* Pass `accountId` on a multi-account platform so the panel connects the account
* the user actually clicked instead of whatever the backend picks first. */
export function adminChannelOpen(name: string, accountId?: string): Promise<ChannelActionResult> {
return request<ChannelActionResult>("/admin/channels/open", {
method: "POST",
body: JSON.stringify({ name }),
body: JSON.stringify(channelActionBody(name, accountId)),
});
}
/** POST /api/v1/admin/channels/close — disconnects a registered channel adapter. */
export function adminChannelClose(name: string): Promise<{ name: string; connected: boolean }> {
return request<{ name: string; connected: boolean }>("/admin/channels/close", {
export function adminChannelClose(name: string, accountId?: string): Promise<ChannelActionResult> {
return request<ChannelActionResult>("/admin/channels/close", {
method: "POST",
body: JSON.stringify({ name }),
body: JSON.stringify(channelActionBody(name, accountId)),
});
}
/** POST /api/v1/admin/channels/test — structural identity test (actor × whitelist). */
@@ -88,6 +111,15 @@ export function adminDeleteChannelAccount(platform: string, id: string): Promise
method: "DELETE",
});
}
/** DELETE /api/v1/admin/channels/{platform}/config?empty=1 — 删除该平台**无 id 的历史条目**
* (面板既改不动也删不掉它们:带空 id 保存会被后端当成新账号另开一条)。
* 这个 `empty=1` 是**显式**的:绝不能靠「id 为空」来表达同一意图,那样一次误发的
* 空请求就会静默批量删除。 */
export function adminDeleteEmptyChannelAccounts(platform: string): Promise<ChannelConfigSaveResult> {
return request<ChannelConfigSaveResult>(`/admin/channels/${platform}/config?empty=1`, {
method: "DELETE",
});
}
/** GET /api/v1/admin/channels/{platform}/view — one platform account's aggregated
* panel data (friends / servers / channels / members / private chats), owner only. */
export function adminChannelPlatformView(platform: string, accountId?: string): Promise<KookViewResponse> {