import { useMemo, useState } from "react"; import { adminChannelClose, adminChannelOpen, adminDeleteChannelAccount, adminDeleteEmptyChannelAccounts, type ChannelActionResult, } from "../../../lib/api"; import { useAuditStore } from "../../audit"; /** 渠道动作执行器需要的最小依赖:状态写入口 + 审计 + 刷新。 * 抽成显式依赖是为了让「请求里到底带没带 accountId」能在 node 侧被断言 * (见 channelActions.test.ts)—— 把这段逻辑埋在 React hook 里就测不到, * 而「accountId 被丢掉」正是这次要修的产品缺口本身。 */ export interface ChannelActionDeps { log: (action: string, result: "ok" | "error") => void; setBusy: (busy: string | null) => void; setError: (error: string | null) => void; setNotice: (notice: string | null) => void; reload: () => void; } /** 连接/断开返回后要说给主人的那句话。 * * **只看响应,不看请求是否成功**:请求 200 只说明后端受理了这次调用,不说明渠道真的 * 连上了 —— ws+client 的首连失败刻意非致命(OneBot 实现可能比 Cyrene 后启动),此时 * 后端回 `connected:false` + `pending:true` + `reason`。把这句写死成「已连接」, * 正是 2026-10-07 那次 UI 撒谎的根因。断开同理:报后端回的真实连接状态,而不是假定 * 「调用成功就一定断了」。 * * 纯函数、与 React 解耦,因此「响应 connected:false + pending:true ⇒ 提示里没有 * 『已连接』」可以在 node 侧被直接断言(见 channelActions.test.ts)。 */ export function connectDisconnectNotice( connect: boolean, who: string, res?: ChannelActionResult, ): string { if (!connect) { return res?.connected === true ? `${who} 仍未断开(后端报告连接仍存在)` : `已断开 ${who}`; } if (res?.connected === true) return `已连接 ${who}`; if (res?.pending === true) { return res.reason ? `${who} 正在连接(${res.reason})` : `${who} 正在连接(尚未连上,已在后台重试)`; } if (res?.ready === true) return `${who} 已就绪,等待对端接入`; // 既没连上、也没有可解释的状态面:如实说「尚未连上」——请求被受理不等于连上了。 return `${who} 尚未连上(连接请求已受理,可稍后在列表中查看状态)`; } /** 与 React 解耦的渠道动作集合(连接/断开/删除账号/删除无 id 条目)。 */ export interface ChannelActions { /** 连接/断开。`accountId` 给出时**只**作用于该账号(多账号平台逐条操作); * 缺省时维持平台级旧行为(后端按自己的规则挑一条)。 */ toggle: (name: string, connect: boolean, accountId?: string) => void; del: (platform: string, id: string) => void; /** 删除该平台**无 id 的历史条目**(显式 `?empty=1`)。 */ delEmpty: (platform: string) => void; } export function createChannelActions(deps: ChannelActionDeps): ChannelActions { const { log, setBusy, setError, setNotice, reload } = deps; const toggle = (name: string, connect: boolean, accountId?: string) => { setBusy(name); setError(null); const call = connect ? adminChannelOpen(name, accountId) : adminChannelClose(name, accountId); call .then((res) => { log(connect ? "channel.open" : "channel.close", "ok"); // 回显**实际被驱动**的适配器:多账号平台上请求里的平台名说明不了「连的是哪一条」, // 后端把解析结果放在 resolved 里,这里如实说出来。 const who = res?.resolved ?? name; setNotice(connectDisconnectNotice(connect, who, res)); reload(); }) .catch((e: unknown) => { log(connect ? "channel.open" : "channel.close", "error"); setError(e instanceof Error ? e.message : `${connect ? "连接" : "断开"}失败`); }) .finally(() => setBusy(null)); }; const del = (platform: string, id: string) => { setBusy("__del__" + id); setError(null); setNotice(null); adminDeleteChannelAccount(platform, id) .then(() => { log("channel.delete", "ok"); setNotice(`已删除账号 ${id} 并热更新 ${platform}`); reload(); }) .catch((e: unknown) => { log("channel.delete", "error"); setError(e instanceof Error ? e.message : "删除渠道账号失败"); }) .finally(() => setBusy(null)); }; const delEmpty = (platform: string) => { setBusy("__delEmpty__" + platform); setError(null); setNotice(null); adminDeleteEmptyChannelAccounts(platform) .then((res) => { log("channel.delete", "ok"); // 删了几条必须说出来:无 id 条目彼此不可区分,「删掉无 id 条目」是批量动作, // 条数对调用方是唯一能核对的东西。 const n = res?.removed ?? 0; setNotice(`已删除 ${n} 条无 id 条目并热更新 ${platform}`); reload(); }) .catch((e: unknown) => { log("channel.delete", "error"); setError(e instanceof Error ? e.message : "删除无 id 条目失败"); }) .finally(() => setBusy(null)); }; return { toggle, del, delEmpty }; } /** Shared connect/disconnect/delete actions + busy/error/notice state, reused by * the list page and each adapter detail page. `reload` re-fetches the channel * list after a mutation. */ export function useChannelActions(reload: () => void) { const log = useAuditStore((s) => s.log); const [busy, setBusy] = useState(null); const [error, setError] = useState(null); const [notice, setNotice] = useState(null); const actions = useMemo( () => createChannelActions({ log, setBusy, setError, setNotice, reload }), [log, reload], ); return { busy, setBusy, error, setError, notice, setNotice, ...actions }; }