import type { AdminChannelMessage, ChannelTestResult, KookFriendList, KookMessageTestResult, KookViewResponse, WeixinLoginQRResponse, WeixinLoginStatusResponse, } from "../types"; import { request } from "./core"; /** Result of saving/deleting a channel account config (persisted to channels.json). */ export interface ChannelConfigSaveResult { persisted: boolean; path: string; platform: string; accountId: string; /** 仅 `?empty=1` 的删除会带上:本次实际删掉的无 id 条目数。 */ removed?: number; empty?: boolean; } /** 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. * * `connected` 说的是**现在通不通**,不是「这次请求成没成功」:ws+client 的首连失败 * 刻意非致命(OneBot 实现可能比 Cyrene 后启动),因此 `Connect` 返回 nil 并不等于连上了。 * `pending`/`ready`(后端可选,契约 v0.68)进一步说明「没连上是哪种没连上」: * * - `pending: true` → 已在后台重试、当前未连上 ⇒ 提示须是「正在连接(reason)」, * **绝不是**「已连接」,也不是一句静态的「未连接」; * - `ready: true` → 已就绪、等待对端接入(server 模式)⇒ 同样不是失败; * - `reason` → 未连上的原因(已脱敏)。 * * 三者缺省 = 该适配器没有这条状态面 ⇒ 只看 `connected`。 */ export interface ChannelActionResult { name: string; resolved?: string; accountId?: string; connected: boolean; pending?: boolean; ready?: boolean; reason?: string; } /** 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 { return request("/admin/channels/open", { method: "POST", body: JSON.stringify(channelActionBody(name, accountId)), }); } /** POST /api/v1/admin/channels/close — disconnects a registered channel adapter. */ export function adminChannelClose(name: string, accountId?: string): Promise { return request("/admin/channels/close", { method: "POST", body: JSON.stringify(channelActionBody(name, accountId)), }); } /** POST /api/v1/admin/channels/test — structural identity test (actor × whitelist). */ export function adminChannelTest(req: { platform: string; accountId: string; isGroup: boolean; }): Promise { return request("/admin/channels/test", { method: "POST", body: JSON.stringify(req) }); } /** GET /api/v1/admin/channels/messages?adapter= — one adapter config's * (platform:account) send/receive message log, newest first. */ export function adminChannelMessages(adapter: string, limit?: number): Promise { const q = new URLSearchParams({ adapter }); if (limit && limit > 0) q.set("limit", String(limit)); return request(`/admin/channels/messages?${q.toString()}`); } /** GET /api/v1/admin/channels/messages — adapter overview log across all adapters. */ export function adminChannelLogOverview(limit?: number): Promise { const q = limit && limit > 0 ? `?limit=${limit}` : ""; return request(`/admin/channels/messages${q}`); } /** PUT /api/v1/admin/channels/{platform}/config — save an account config + hot-reload. */ export function adminSaveChannelConfig( platform: string, req: { id?: string; token?: string; baseURL?: string; remoteURL?: string; listenAddr?: string; mode?: string; accessToken?: string; selfID?: string; config?: Record; allowGroups?: string[]; denyGroups?: string[]; allowUsers?: string[]; denyUsers?: string[]; groupMsgPerMin?: number; msgPerMin?: number; maxMessages?: number; /** 非主人(陌生人)私聊每分钟每发送者上限(裁决 13)。缺省 / 0 = 保护性默认 * 6 条/分、负值 = 关闭该限制、正值 = 该值(口径同 groupMsgPerMin)。 */ directMsgPerMin?: number; /** Role bound to this channel account (design/22 §4.4). Omit to leave the * stored binding untouched; `''` clears it (「跟随默认角色」). It only affects * the account's non-isolated sessions — 群聊/陌生人始终用默认角色. */ personaId?: string; }, ): Promise { return request(`/admin/channels/${platform}/config`, { method: "PUT", body: JSON.stringify(req), }); } /** DELETE /api/v1/admin/channels/{platform}/config?id=… — delete an account + hot-reload. */ export function adminDeleteChannelAccount(platform: string, id: string): Promise { return request(`/admin/channels/${platform}/config?id=${encodeURIComponent(id)}`, { method: "DELETE", }); } /** DELETE /api/v1/admin/channels/{platform}/config?empty=1 — 删除该平台**无 id 的历史条目** * (面板既改不动也删不掉它们:带空 id 保存会被后端当成新账号另开一条)。 * 这个 `empty=1` 是**显式**的:绝不能靠「id 为空」来表达同一意图,那样一次误发的 * 空请求就会静默批量删除。 */ export function adminDeleteEmptyChannelAccounts(platform: string): Promise { return request(`/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 { const q = accountId ? `?accountId=${encodeURIComponent(accountId)}` : ""; return request(`/admin/channels/${encodeURIComponent(platform)}/view${q}`); } /** GET /api/v1/admin/channels/{platform}/friends?type= — friend/request/blocked list. */ export function adminChannelPlatformFriends( platform: string, type: string, accountId?: string, ): Promise { const q = new URLSearchParams(); if (type) q.set("type", type); if (accountId) q.set("accountId", accountId); const qs = q.toString(); return request(`/admin/channels/${encodeURIComponent(platform)}/friends${qs ? `?${qs}` : ""}`); } /** POST /api/v1/admin/channels/{platform}/friend/handle — accept/reject a friend request. */ export function adminChannelFriendHandle( platform: string, id: number, accept: boolean, accountId?: string, ): Promise<{ handled: boolean; id: number; accept: boolean }> { const q = accountId ? `?accountId=${encodeURIComponent(accountId)}` : ""; return request<{ handled: boolean; id: number; accept: boolean }>( `/admin/channels/${encodeURIComponent(platform)}/friend/handle${q}`, { method: "POST", body: JSON.stringify({ id, accept }) }, ); } /** POST /api/v1/admin/channels/{platform}/message/test — send one test message. */ export function adminChannelMessageTest( platform: string, targetId: string, content: string, accountId?: string, ): Promise { const q = accountId ? `?accountId=${encodeURIComponent(accountId)}` : ""; return request(`/admin/channels/${encodeURIComponent(platform)}/message/test${q}`, { method: "POST", body: JSON.stringify({ targetId, content }), }); } /** GET /api/v1/admin/channels/{platform}/login-qr — trigger/mint a weixin-iLink * scan-login QR code (owner only, audited). */ export function adminWeixinLoginQR( platform: string, accountId?: string, ): Promise { const q = accountId ? `?accountId=${encodeURIComponent(accountId)}` : ""; return request( `/admin/channels/${encodeURIComponent(platform)}/login-qr${q}`, ); } /** GET /api/v1/admin/channels/{platform}/login-status — poll a weixin-iLink scan * login. On confirmed the backend persists the token and hot-connects. */ export function adminWeixinLoginStatus( platform: string, accountId?: string, ): Promise { const q = accountId ? `?accountId=${encodeURIComponent(accountId)}` : ""; return request( `/admin/channels/${encodeURIComponent(platform)}/login-status${q}`, ); }