feat(gateway): 账号在线三态(后端 AccountStatus 可选接口 + 面板第二枚标签 + 契约账目更正)
- known=false ⇒ accountOnline 键**缺席**(未知 ≠ 离线),与既有 connected 语义并列不合并 - 面板:账号在线/账号离线/账号状态未知 三态 + 中性灰 tone + 「这不等于离线」悬停说明 - 测试:gateway 跨层锁「键缺席」(用真 qq_obv11.Adapter)+ 面板 61/40 断言 - 变异:known 当离线 ⇒ 两条测试同时红(已复跑确认) - Docs/api-client.md:§7.2 那行由未实现改已实现(留表,注明下版移入 §7.1)
This commit is contained in:
1 parent
7ae4298871
commit
52dac4575f
10 files changed
+456
-16
No files matched your search
@@ -2,7 +2,7 @@ import type { AdminChannelAccount } from "../../../lib/types";
|
||||
import { Button, Tag } from "../../../components/primitives";
|
||||
import { authKeyOf } from "../channelFieldRules";
|
||||
import { adapterNameOf } from "./channelUtils";
|
||||
import { accountActionGate, shortAccountID, type ChannelStatus } from "./channelStatus";
|
||||
import { accountActionGate, accountOnlineStatus, shortAccountID, type ChannelStatus } from "./channelStatus";
|
||||
|
||||
/** 一条渠道账号在活动适配器列表里的行。
|
||||
*
|
||||
@@ -47,6 +47,7 @@ export function AccountRow({
|
||||
const authSet = typeof raw === "string" && raw.trim() !== "";
|
||||
const gate = accountActionGate(status, account.connected);
|
||||
const state = connectionTag(account.connected);
|
||||
const online = accountOnlineStatus(account.accountOnline);
|
||||
const blocker = status.kind === "unknown" ? "读取不到连接状态(后端失联),已禁用" : undefined;
|
||||
|
||||
return (
|
||||
@@ -67,6 +68,13 @@ export function AccountRow({
|
||||
</span>
|
||||
)}
|
||||
<Tag tone={account.connected === true ? "ok" : account.connected === false ? "err" : "neutral"}>{state}</Tag>
|
||||
{/* 账号在**平台侧**的在线状态 —— 与上一枚「连接」标签说的是两件事:OneBot 的
|
||||
进程/WS 可以一直连着,而背后的 QQ 账号已掉线(消息收不到也发不出)。
|
||||
两枚标签刻意不合并:合并后「我们连着但账号掉了」这一个事故现场就无迹可寻。
|
||||
后端没给 accountOnline 时画「账号状态未知」,绝不画成「账号离线」。 */}
|
||||
<Tag tone={online.tone} title={online.hint}>
|
||||
{online.label}
|
||||
</Tag>
|
||||
<Tag tone={authSet ? "ok" : "warn"} className="text-[var(--mu-text-lo)]">
|
||||
{authSet ? `${authLabel} 已配置` : `${authLabel} 未填`}
|
||||
</Tag>
|
||||
|
||||
@@ -134,4 +134,24 @@ function buttonHTML(html: string, label: string): string {
|
||||
ok(has(none, "平台级连接会失败"), "没有账号时明说平台级连接会失败");
|
||||
}
|
||||
|
||||
// ── ⑤ 账号在线标签:三态,且「未知」绝不被画成「离线」 ─────────────────────────
|
||||
// 这是 2026-10-05 事故的正面修复:账号在平台上掉了,面板却只报「已连接」。修好之后
|
||||
// 每行必须有**两枚**标签(连接 + 账号在线),且缺省那态要照实说「未知」。
|
||||
{
|
||||
const omitted = renderRow({ id: "9616b27a", config: {} });
|
||||
ok(has(omitted, ">状态未知</span>"), "连接标签还在(两枚标签各说一件事,不合并)");
|
||||
ok(has(omitted, "账号状态未知"), "accountOnline 缺省 ⇒ 渲染「账号状态未知」");
|
||||
ok(!has(omitted, "账号离线"), "缺省不得渲染成「账号离线」(未知 ≠ 离线)");
|
||||
ok(has(omitted, "这不等于离线"), "缺省带悬停说明:未知不等于离线");
|
||||
|
||||
const up = renderRow({ id: "9616b27a", config: {}, connected: true, accountOnline: true });
|
||||
ok(has(up, ">已连接</span>") && has(up, "账号在线"), "已连接 + 账号在线:两枚标签各报各的");
|
||||
ok(!has(up, "账号离线") && !has(up, "账号状态未知"), "确知在线不会同屏出现别的在线态");
|
||||
|
||||
const down = renderRow({ id: "9616b27a", config: {}, connected: true, accountOnline: false });
|
||||
ok(has(down, ">已连接</span>"), "事故现场:我们的连接仍是「已连接」");
|
||||
ok(has(down, "账号离线"), "……而账号那一枚照实说「账号离线」(消息收不到也发不出)");
|
||||
ok(has(down, "账号在平台上"), "离线标签的悬停说明点明这是账号在平台上的状态");
|
||||
}
|
||||
|
||||
console.log(`channel account row render test: ${checks} checks passed`);
|
||||
@@ -16,7 +16,7 @@
|
||||
// imports nothing from node:*.
|
||||
|
||||
import type { AdminChannelRecord } from "../../../lib/types";
|
||||
import { channelListState, channelStatus } from "./channelStatus";
|
||||
import { accountOnlineStatus, channelListState, channelStatus } from "./channelStatus";
|
||||
|
||||
let checks = 0;
|
||||
|
||||
@@ -108,4 +108,32 @@ same(channelListState([weixin(true)], null), "ready", "a read list is ready");
|
||||
same(channelListState(null, null), "pending", "a list never read and not failed is pending");
|
||||
same(channelListState(null, "读取失败"), "unknown", "a list never read with an error is unknown");
|
||||
|
||||
// ── 账号在平台侧的在线状态(`accountOnline`,契约 v0.67 §2.4(b))───────────────
|
||||
// 假绿的根源:面板只报「我们连着」,账号掉了看不出来。三态里最要命的是「未知」——
|
||||
// 它既不是在线也不是离线,必须原样说出来,绝不能塌成离线那支红色。
|
||||
const onlineUp = accountOnlineStatus(true);
|
||||
same(onlineUp.kind, "online", "accountOnline=true ⇒ online");
|
||||
same(onlineUp.label, "账号在线", "确知在线标「账号在线」");
|
||||
same(onlineUp.tone, "ok", "确知在线是绿的");
|
||||
|
||||
const onlineDown = accountOnlineStatus(false);
|
||||
same(onlineDown.kind, "offline", "accountOnline=false ⇒ offline");
|
||||
same(onlineDown.label, "账号离线", "确知离线标「账号离线」");
|
||||
same(onlineDown.tone, "err", "确知离线是红的");
|
||||
|
||||
const onlineUnknown = accountOnlineStatus(undefined);
|
||||
same(onlineUnknown.kind, "unknown", "accountOnline 缺省 ⇒ unknown");
|
||||
ok(onlineUnknown.kind !== "offline", "缺省绝不被读成「账号离线」");
|
||||
same(onlineUnknown.label, "账号状态未知", "缺省标「账号状态未知」");
|
||||
same(onlineUnknown.tone, "neutral", "缺省是中性灰 —— 不是 offline 那支红");
|
||||
ok(onlineUnknown.tone !== "err", "缺省不得带「异常」腔");
|
||||
ok(onlineUnknown.label.includes("未知"), "标签里明说「未知」,不留给读者猜");
|
||||
ok(onlineUp.label.includes("账号"), "在线标签带「账号」二字,与连接标签区分得开");
|
||||
ok(
|
||||
new Set([onlineUp.label, onlineDown.label, onlineUnknown.label]).size === 3,
|
||||
"三态三个不同标签(不许两态合并成一态)",
|
||||
);
|
||||
ok(onlineDown.hint.includes("账号在平台上"), "离线提示说清这是账号在平台上的状态,不是我们的连接");
|
||||
ok(onlineUnknown.hint.includes("不等于离线"), "未知提示明说它不等于离线");
|
||||
|
||||
console.log(`channel status logic test: ${checks} checks passed`);
|
||||
@@ -102,6 +102,52 @@ export function channelStatus(
|
||||
};
|
||||
}
|
||||
|
||||
/** 账号在**平台侧**是否在线(后端可选 `accountOnline`,契约 v0.67 §2.4(b))。
|
||||
* 它与「连接状态」是**两件事**:OneBot 实现(NapCat 等)的进程与正向 WS 可以一直
|
||||
* 连着,而背后的 QQ 账号已掉线 —— 此时「已连接」为真,消息却既收不到也发不出
|
||||
* (2026-10-05 事故:账号掉了,面板一片绿)。所以这枚标签必须与连接标签**并列**
|
||||
* 出现,且文案里带「账号」二字,让人一眼看出它说的是账号在平台上的状态。
|
||||
*
|
||||
* 三态与 channelStatus 同一口径:
|
||||
* - `true` → 确知在线;
|
||||
* - `false` → 确知离线;
|
||||
* - 缺省 → **状态未知**(该适配器没有独立账号状态面,或尚未探到结果)——
|
||||
* 既不是「离线」也不是「在线」,**绝不用红色异常腔渲染**。 */
|
||||
export type AccountOnlineKind = "online" | "offline" | "unknown";
|
||||
|
||||
export interface AccountOnlineStatus {
|
||||
kind: AccountOnlineKind;
|
||||
label: string;
|
||||
tone: ChannelStatusTone;
|
||||
/** 悬停说明,把「这是账号在平台上的状态、不是我们的连接」讲明白。 */
|
||||
hint: string;
|
||||
}
|
||||
|
||||
const ACCOUNT_ONLINE_LABEL: Record<AccountOnlineKind, string> = {
|
||||
online: "账号在线",
|
||||
offline: "账号离线",
|
||||
unknown: "账号状态未知",
|
||||
};
|
||||
|
||||
const ACCOUNT_ONLINE_HINT: Record<AccountOnlineKind, string> = {
|
||||
online: "账号在平台上在线(与「已连接」是两件事:那说的是我们与平台的连接)",
|
||||
offline: "账号在平台上已离线:消息收不到也发不出(与「已连接」是两件事)",
|
||||
unknown: "尚未探到账号在平台上的状态,或该渠道没有独立状态面 —— 这不等于离线",
|
||||
};
|
||||
|
||||
/** 把后端可选的 `accountOnline` 映成三态标签:缺省一律「未知」,
|
||||
* **绝不**退化成 false 那条「账号离线」分支。 */
|
||||
export function accountOnlineStatus(accountOnline?: boolean): AccountOnlineStatus {
|
||||
const kind: AccountOnlineKind =
|
||||
accountOnline === true ? "online" : accountOnline === false ? "offline" : "unknown";
|
||||
return {
|
||||
kind,
|
||||
label: ACCOUNT_ONLINE_LABEL[kind],
|
||||
tone: kind === "online" ? "ok" : kind === "offline" ? "err" : "neutral",
|
||||
hint: ACCOUNT_ONLINE_HINT[kind],
|
||||
};
|
||||
}
|
||||
|
||||
/** 每个账号一行连接/断开时的门控(多账号平台逐条操作用)。
|
||||
*
|
||||
* `connected` 是该账号**自己**的实时状态(`AdminChannelAccount.connected`):
|
||||
|
||||
Reference in new issue
Block a user