feat(ui): compact bot connection metadata

This commit is contained in:
xmanrui 2026-08-23 01:20:58 +08:00
parent 59ed364856
commit 2803bbcbab
22 changed files with 632 additions and 556 deletions

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,48 @@
import * as React from 'react';
import { h } from './i18n.js';
export function ChannelListHeading({ className = '', id, title, connectionLabel }) {
const helpId = React.useId();
return h('div', { className: `${className} dim-listHeading`.trim() },
h('div', { className: 'dim-listTitle' },
h('h3', id ? { id } : null, title),
h('span', { className: 'dim-channelHelp' },
h('button', {
type: 'button',
className: 'dim-channelHelpButton',
'aria-label': '查看消息通道说明',
'aria-describedby': helpId,
}, h('span', { 'aria-hidden': 'true' }, '?')),
h('span', {
id: helpId,
className: 'dim-channelTooltip',
role: 'tooltip',
},
h('span', null, '消息通道'),
h('strong', null, connectionLabel)))));
}
export function BotStatusMeta({
className = '',
dotClassName = '',
tone,
stateLabel,
lastCheckedAt,
formatCheckedTime,
healthState,
}) {
return h('div', { className: 'dim-botHealthGroup' },
h('div', {
className: `${className} dim-botHealth`.trim(),
...(healthState ? { 'data-health': healthState } : {}),
},
h('span', {
className: `${dotClassName} dim-healthDot`.trim(),
'data-tone': tone,
}),
h('span', null, stateLabel)),
h('div', { className: 'dim-lastChecked' },
h('span', null, '最近检查'),
h('span', null, formatCheckedTime(lastCheckedAt))));
}

View file

@ -9,6 +9,7 @@ import {
EMPTY_AGENT_PRESET_CATALOG,
} from '../../agent-preset.js';
import { useWorkspaceSnapshotFence } from '../../workspace-snapshot-fence.js';
import { BotStatusMeta, ChannelListHeading } from '../../channel-card-meta.js';
import {
DINGTALK_ENDPOINTS,
DINGTALK_RPC_CHANNEL,
@ -235,13 +236,14 @@ export function AccountCard({
h('div', { className: 'dim-botName' },
h('h3', { title: account.bot.name }, account.bot.name),
h('p', { title: account.bot.clientIdMasked }, account.bot.clientIdMasked))),
h('div', { className: 'ddt-health dim-botHealth' },
h('span', { className: 'ddt-dot dim-healthDot', 'data-tone': tone }), h('span', null, stateLabel))),
h('dl', { className: 'ddt-metrics dim-botMetrics' },
h('div', { className: 'ddt-metric dim-botMetric' }, h('dt', null, '消息通道'),
h('dd', null, account.connected ? 'Stream 长连接' : '离线')),
h('div', { className: 'ddt-metric dim-botMetric' }, h('dt', null, '最近检查'),
h('dd', null, checkedTime(account.health.lastCheckedAt)))),
h(BotStatusMeta, {
className: 'ddt-health',
dotClassName: 'ddt-dot',
tone,
stateLabel,
lastCheckedAt: account.health.lastCheckedAt,
formatCheckedTime: checkedTime,
})),
h(WorkspaceEditor, {
workspace: account.workspace,
disabled: Boolean(busy),
@ -274,8 +276,11 @@ export function AccountCard({
function AccountList(props) {
return h('section', { className: 'dim-listSection' },
h('div', { className: 'ddt-listHeading dim-listHeading' },
h('h3', null, '已接入的钉钉机器人')),
h(ChannelListHeading, {
className: 'ddt-listHeading',
title: '已接入的钉钉机器人',
connectionLabel: 'Stream 长连接',
}),
h('ul', { className: 'ddt-list dim-botList' }, props.bots.map((account) => h('li', { key: account.botId },
h(AccountCard, {
account,

View file

@ -86,10 +86,6 @@ const CSS = String.raw`
.ddt-accountIdentity h3 { overflow: hidden; font-size: 15px; text-overflow: ellipsis; white-space: nowrap; }
.ddt-accountIdentity p { margin-top: 4px; color: var(--dsw-alias-label-secondary, #646a73); font: 12px ui-monospace, SFMono-Regular, monospace; }
.ddt-health { display: inline-flex; align-items: center; gap: 7px; color: var(--dsw-alias-label-secondary, #646a73); font-size: 12px; white-space: nowrap; }
.ddt-metrics { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 10px; margin: 20px 0; }
.ddt-metric { min-width: 0; padding: 12px; border-radius: 9px; background: var(--dsw-alias-interactive-bg-hover, #f7f8fa); }
.ddt-metric dt { color: var(--dsw-alias-label-tertiary, #8f959e); font-size: 11px; }
.ddt-metric dd { overflow: hidden; margin: 5px 0 0; font-size: 13px; text-overflow: ellipsis; white-space: nowrap; }
.ddt-accountFooter { display: flex; align-items: center; justify-content: space-between; gap: 15px; padding-top: 16px; border-top: 1px solid var(--dsw-alias-border-l1, #eef0f3); }
.ddt-accountFooter .ddt-actions { flex: none; flex-wrap: nowrap; gap: 8px; margin-top: 0; }
.ddt-accountFooter .ddt-button { flex: none; white-space: nowrap; }
@ -110,8 +106,6 @@ const CSS = String.raw`
.ddt-qrLayout { grid-template-columns: minmax(0, 1fr); justify-items: center; gap: 24px; }
.ddt-qrColumn { width: 100%; min-width: 0; }
.ddt-qrCopy { width: 100%; }
.ddt-metrics { gap: 8px; }
.ddt-metric { padding: 10px; }
}
@media (max-width: 720px) {
.ddt-heading, .ddt-accountTop { flex-direction: column; align-items: stretch; }

View file

@ -23,6 +23,7 @@ import {
EMPTY_AGENT_PRESET_CATALOG,
} from "../../agent-preset.js";
import { useWorkspaceSnapshotFence } from "../../workspace-snapshot-fence.js";
import { BotStatusMeta, ChannelListHeading } from "../../channel-card-meta.js";
import { installFeishuStyles } from "./styles.js";
export const name = "feishu-settings";
@ -591,15 +592,15 @@ export function BotCard({
h("h3", { id: titleId, title: bot.name }, bot.name),
h("p", { title: bot.appIdMasked }, bot.appIdMasked ?? "应用标识已安全保存")),
),
h("div", { className: "bxf-healthPill dim-botHealth", "data-health": stateForDisplay },
h("span", { className: "bxf-dot dim-healthDot", "data-tone": tone }),
h("span", null, HEALTH_LABELS[stateForDisplay] ?? "状态未知")),
),
h("dl", { className: "bxf-statusGrid dim-botMetrics" },
h("div", { className: "bxf-metric dim-botMetric" }, h("dt", null, "消息通道"),
h("dd", null, connected ? "长连接" : stateForDisplay === "connecting" ? "连接中" : "已断开")),
h("div", { className: "bxf-metric dim-botMetric" }, h("dt", null, "最近检查"),
h("dd", null, formatCheckedTime(health.lastCheckedAt))),
h(BotStatusMeta, {
className: "bxf-healthPill",
dotClassName: "bxf-dot",
tone,
stateLabel: HEALTH_LABELS[stateForDisplay] ?? "状态未知",
lastCheckedAt: health.lastCheckedAt,
formatCheckedTime,
healthState: stateForDisplay,
}),
),
h(WorkspaceEditor, {
workspace: connection.workspace,
@ -669,8 +670,12 @@ export function BotCard({
function BotList(props) {
return h("section", { className: "bxf-listSection dim-listSection", "aria-labelledby": "bxf-bot-list-title" },
h("div", { className: "bxf-listHeading dim-listHeading" },
h("h3", { id: "bxf-bot-list-title" }, "已接入的机器人")),
h(ChannelListHeading, {
className: "bxf-listHeading",
id: "bxf-bot-list-title",
title: "已接入的机器人",
connectionLabel: "长连接",
}),
h("ul", { className: "bxf-botList dim-botList", role: "list" },
props.bots.map((bot) => h("li", { key: bot.botId },
h(BotCard, {

View file

@ -402,10 +402,6 @@ const CSS = String.raw`
.bxf-healthPill[data-health="degraded"], .bxf-healthPill[data-health="checking"], .bxf-healthPill[data-health="connecting"] { color: var(--bxf-warning); background: color-mix(in srgb, var(--bxf-warning) 10%, transparent); }
.bxf-healthPill[data-health="offline"], .bxf-healthPill[data-health="error"] { color: var(--bxf-error); background: color-mix(in srgb, var(--bxf-error) 10%, transparent); }
.bxf-statusGrid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 10px; margin-top: 22px; }
.bxf-metric { min-width: 0; border: 1px solid var(--dsw-alias-border-l2, #dee0e3); border-radius: 9px; padding: 12px 13px; background: var(--dsw-alias-bg-module-platform, #f7f8fa); }
.bxf-metric dt { color: var(--dsw-alias-label-tertiary, #8f959e); font-size: 11px; line-height: 17px; }
.bxf-metric dd { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; color: var(--dsw-alias-label-primary, #1f2329); font-size: 12px; line-height: 18px; font-weight: 550; margin: 3px 0 0; }
.bxf-responseMode { min-width: 0; display: grid; grid-template-columns: minmax(0, 1fr) max-content; align-items: center; column-gap: 10px; row-gap: 5px; margin-top: 6px; padding: 8px 10px; border: 1px solid var(--dsw-alias-border-l1, #eef0f3); border-radius: 9px; background: var(--dsw-alias-bg-module-platform, #f7f8fa); }
.bxf-responseModeHeader { display: contents; color: var(--dsw-alias-label-secondary, #646a73); font-size: 12px; line-height: normal; }
@ -517,7 +513,6 @@ const CSS = String.raw`
.bxf-qrLayout { grid-template-columns: minmax(0, 1fr); justify-items: center; }
.bxf-botProvision .bxf-qrLayout { grid-template-columns: minmax(0, 1fr); }
.bxf-qrCopy { width: 100%; }
.bxf-statusGrid { grid-template-columns: minmax(0, 1fr); }
.bxf-connectedTop { align-items: flex-start; flex-direction: column; }
.bxf-inlineError { grid-template-columns: minmax(0, 1fr); padding: 20px; }
.bxf-statusNotice { align-items: flex-start; flex-wrap: wrap; }

View file

@ -10,6 +10,7 @@ import {
EMPTY_AGENT_PRESET_CATALOG,
} from '../../agent-preset.js';
import { useWorkspaceSnapshotFence } from '../../workspace-snapshot-fence.js';
import { BotStatusMeta, ChannelListHeading } from '../../channel-card-meta.js';
import { installDingtalkStyles } from '../dingtalk/styles.js';
import {
QQ_ENDPOINTS,
@ -176,11 +177,14 @@ export function AccountCard({
h('div', { className: 'ddt-avatar dim-botAvatar dqq-avatar', 'aria-hidden': 'true' }, h(QqLogoGlyph, { size: 29 })),
h('div', { className: 'dim-botName' },
h('h3', null, account.bot.name), h('p', null, account.bot.appIdMasked))),
h('div', { className: 'ddt-health dim-botHealth' },
h('span', { className: 'ddt-dot dim-healthDot', 'data-tone': tone }), h('span', null, stateLabel))),
h('dl', { className: 'ddt-metrics dim-botMetrics' },
h('div', { className: 'ddt-metric dim-botMetric' }, h('dt', null, '消息通道'), h('dd', null, account.connected ? 'WebSocket 长连接' : '离线')),
h('div', { className: 'ddt-metric dim-botMetric' }, h('dt', null, '最近检查'), h('dd', null, checkedTime(account.health.lastCheckedAt)))),
h(BotStatusMeta, {
className: 'ddt-health',
dotClassName: 'ddt-dot',
tone,
stateLabel,
lastCheckedAt: account.health.lastCheckedAt,
formatCheckedTime: checkedTime,
})),
h(WorkspaceEditor, {
workspace: account.workspace,
disabled: Boolean(busy),
@ -433,8 +437,11 @@ export function QqSettingsTab({ rpcCall }) {
const botList = model.bots.length > 0
? h('section', { className: 'dim-listSection' },
h('div', { className: 'ddt-listHeading dim-listHeading' },
h('h3', null, '已绑定的 QQ 机器人')),
h(ChannelListHeading, {
className: 'ddt-listHeading',
title: '已绑定的 QQ 机器人',
connectionLabel: 'WebSocket 长连接',
}),
h('ul', { className: 'ddt-list dim-botList' }, model.bots.map((account) =>
h('li', { key: account.botId }, h(AccountCard, {
account,

View file

@ -10,6 +10,7 @@ import {
EMPTY_AGENT_PRESET_CATALOG,
} from '../../agent-preset.js';
import { useWorkspaceSnapshotFence } from '../../workspace-snapshot-fence.js';
import { BotStatusMeta, ChannelListHeading } from '../../channel-card-meta.js';
const Button = React.forwardRef(function Button(
{ children, kind = 'secondary', className = '', ...props },
@ -82,14 +83,14 @@ export function createTokenChannelSettings(definition) {
h(LogoGlyph, { size: 29 })),
h('div', { className: 'dim-botName' },
h('h3', null, account.bot.name), h('p', null, identity))),
h('div', { className: 'ddt-health dim-botHealth' },
h('span', { className: 'ddt-dot dim-healthDot', 'data-tone': tone }),
h('span', null, stateLabel))),
h('dl', { className: 'ddt-metrics dim-botMetrics' },
h('div', { className: 'ddt-metric dim-botMetric' },
h('dt', null, '消息通道'), h('dd', null, account.connected ? connectionLabel : '离线')),
h('div', { className: 'ddt-metric dim-botMetric' },
h('dt', null, '最近检查'), h('dd', null, checkedTime(account.health.lastCheckedAt)))),
h(BotStatusMeta, {
className: 'ddt-health',
dotClassName: 'ddt-dot',
tone,
stateLabel,
lastCheckedAt: account.health.lastCheckedAt,
formatCheckedTime: checkedTime,
})),
h(WorkspaceEditor, {
workspace: account.workspace,
disabled: Boolean(busy),
@ -271,8 +272,11 @@ export function createTokenChannelSettings(definition) {
const botList = model.bots.length > 0
? h('section', { className: 'dim-listSection' },
h('div', { className: 'ddt-listHeading dim-listHeading' },
h('h3', null, `已接入的 ${channel} 机器人`)),
h(ChannelListHeading, {
className: 'ddt-listHeading',
title: `已接入的 ${channel} 机器人`,
connectionLabel,
}),
h('ul', { className: 'ddt-list dim-botList' }, model.bots.map((account) =>
h('li', { key: account.botId }, h(AccountCard, {
account,

View file

@ -10,6 +10,7 @@ import {
EMPTY_AGENT_PRESET_CATALOG,
} from '../../agent-preset.js';
import { useWorkspaceSnapshotFence } from '../../workspace-snapshot-fence.js';
import { BotStatusMeta, ChannelListHeading } from '../../channel-card-meta.js';
import { installDingtalkStyles } from '../dingtalk/styles.js';
import {
WECOM_ENDPOINTS,
@ -175,11 +176,14 @@ export function AccountCard({
h('div', { className: 'ddt-avatar dim-botAvatar dwecom-avatar', 'aria-hidden': 'true' }, h(WecomLogoGlyph, { size: 29 })),
h('div', { className: 'dim-botName' },
h('h3', null, account.bot.name), h('p', null, account.bot.appIdMasked))),
h('div', { className: 'ddt-health dim-botHealth' },
h('span', { className: 'ddt-dot dim-healthDot', 'data-tone': tone }), h('span', null, stateLabel))),
h('dl', { className: 'ddt-metrics dim-botMetrics' },
h('div', { className: 'ddt-metric dim-botMetric' }, h('dt', null, '消息通道'), h('dd', null, account.connected ? 'WebSocket 长连接' : '离线')),
h('div', { className: 'ddt-metric dim-botMetric' }, h('dt', null, '最近检查'), h('dd', null, checkedTime(account.health.lastCheckedAt)))),
h(BotStatusMeta, {
className: 'ddt-health',
dotClassName: 'ddt-dot',
tone,
stateLabel,
lastCheckedAt: account.health.lastCheckedAt,
formatCheckedTime: checkedTime,
})),
h(WorkspaceEditor, {
workspace: account.workspace,
disabled: Boolean(busy),
@ -464,8 +468,11 @@ export function WecomSettingsTab({ rpcCall }) {
const botList = model.bots.length > 0
? h('section', { className: 'dim-listSection' },
h('div', { className: 'ddt-listHeading dim-listHeading' },
h('h3', null, '已绑定的企业微信机器人')),
h(ChannelListHeading, {
className: 'ddt-listHeading',
title: '已绑定的企业微信机器人',
connectionLabel: 'WebSocket 长连接',
}),
h('ul', { className: 'ddt-list dim-botList' }, model.bots.map((account) =>
h('li', { key: account.botId }, h(AccountCard, {
account,

View file

@ -22,6 +22,7 @@ import {
EMPTY_AGENT_PRESET_CATALOG,
} from '../../agent-preset.js';
import { useWorkspaceSnapshotFence } from '../../workspace-snapshot-fence.js';
import { BotStatusMeta, ChannelListHeading } from '../../channel-card-meta.js';
import { installWeixinStyles } from './styles.js';
export const name = 'weixin-settings';
@ -221,14 +222,14 @@ export function AccountCard({
h('div', { className: 'dxw-accountIdentity dim-botIdentity' },
h('div', { className: 'dxw-avatar dim-botAvatar', 'aria-hidden': 'true' }, h(WeixinLogoGlyph, { size: 27 })),
h('div', { className: 'dim-botName' }, h('h3', null, account.bot.name), h('p', null, account.bot.accountIdMasked))),
h('div', { className: 'dxw-health dim-botHealth' },
h('span', { className: 'dxw-dot dim-healthDot', 'data-tone': tone }),
h('span', null, account.connected ? '运行正常' : state === 'connecting' ? '正在连接' : '连接未就绪'))),
h('dl', { className: 'dxw-metrics dim-botMetrics' },
h('div', { className: 'dxw-metric dim-botMetric' }, h('dt', null, '消息通道'),
h('dd', null, account.connected ? 'iLink 长轮询' : '离线')),
h('div', { className: 'dxw-metric dim-botMetric' }, h('dt', null, '最近检查'),
h('dd', null, checkedTime(account.health.lastCheckedAt)))),
h(BotStatusMeta, {
className: 'dxw-health',
dotClassName: 'dxw-dot',
tone,
stateLabel: account.connected ? '运行正常' : state === 'connecting' ? '正在连接' : '连接未就绪',
lastCheckedAt: account.health.lastCheckedAt,
formatCheckedTime: checkedTime,
})),
h(WorkspaceEditor, {
workspace: account.workspace,
disabled: Boolean(busy),
@ -267,7 +268,11 @@ export function AccountCard({
function AccountList(props) {
return h('section', { className: 'dim-listSection' },
h('div', { className: 'dxw-listHeading dim-listHeading' }, h('h3', null, '已接入的微信账号')),
h(ChannelListHeading, {
className: 'dxw-listHeading',
title: '已接入的微信账号',
connectionLabel: 'iLink 长轮询',
}),
h('ul', { className: 'dxw-list dim-botList' }, props.bots.map((account) => h('li', { key: account.botId },
h(AccountCard, {
account,

View file

@ -77,10 +77,6 @@ const CSS = String.raw`
.dxw-accountIdentity h3 { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-size: 15px; }
.dxw-accountIdentity p { color: var(--dsw-alias-label-secondary, #646a73); font: 12px ui-monospace, SFMono-Regular, monospace; margin-top: 4px; }
.dxw-health { display: inline-flex; align-items: center; gap: 7px; color: var(--dsw-alias-label-secondary, #646a73); font-size: 12px; white-space: nowrap; }
.dxw-metrics { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 12px; margin: 20px 0; }
.dxw-metric { padding: 12px 14px; border-radius: 9px; background: var(--dsw-alias-interactive-bg-hover, #f7f8fa); }
.dxw-metric dt { color: var(--dsw-alias-label-tertiary, #8f959e); font-size: 11px; }
.dxw-metric dd { overflow: hidden; margin: 5px 0 0; font-size: 13px; text-overflow: ellipsis; white-space: nowrap; }
.dxw-accountFooter { display: flex; align-items: center; justify-content: space-between; gap: 15px; padding-top: 16px; border-top: 1px solid var(--dsw-alias-border-l1, #eef0f3); }
.dxw-accountFooter .dxw-actions { flex: none; flex-wrap: nowrap; gap: 8px; margin-top: 0; }
.dxw-accountFooter .dxw-button { flex: none; white-space: nowrap; }
@ -99,7 +95,6 @@ const CSS = String.raw`
.dxw-logo { display: none; }
.dxw-qrLayout { grid-template-columns: minmax(0, 1fr); justify-items: center; }
.dxw-qrCopy { width: 100%; }
.dxw-metrics { grid-template-columns: minmax(0, 1fr); }
.dxw-cardBody { padding: 20px; }
}
@media (prefers-reduced-motion: reduce) {

View file

@ -10,6 +10,7 @@ import {
EMPTY_AGENT_PRESET_CATALOG,
} from '../../agent-preset.js';
import { useWorkspaceSnapshotFence } from '../../workspace-snapshot-fence.js';
import { BotStatusMeta, ChannelListHeading } from '../../channel-card-meta.js';
import { installDingtalkStyles } from '../dingtalk/styles.js';
import {
WHATSAPP_ENDPOINTS,
@ -197,15 +198,14 @@ export function WhatsappAccountCard({
}, h(WhatsappLogoGlyph, { size: 29 })),
h('div', { className: 'dim-botName' },
h('h3', null, account.bot.name), h('p', null, account.bot.idMasked))),
h('div', { className: 'ddt-health dim-botHealth' },
h('span', { className: 'ddt-dot dim-healthDot', 'data-tone': tone }),
h('span', null, stateLabel))),
h('dl', { className: 'ddt-metrics dim-botMetrics' },
h('div', { className: 'ddt-metric dim-botMetric' },
h('dt', null, '消息通道'),
h('dd', null, account.connected ? 'WhatsApp Web' : '离线')),
h('div', { className: 'ddt-metric dim-botMetric' },
h('dt', null, '最近检查'), h('dd', null, checkedTime(account.health.lastCheckedAt)))),
h(BotStatusMeta, {
className: 'ddt-health',
dotClassName: 'ddt-dot',
tone,
stateLabel,
lastCheckedAt: account.health.lastCheckedAt,
formatCheckedTime: checkedTime,
})),
h(WorkspaceEditor, {
workspace: account.workspace,
disabled: Boolean(busy),
@ -440,8 +440,11 @@ export function WhatsappSettingsTab({ rpcCall }) {
const botList = model.bots.length > 0
? h('section', { className: 'dim-listSection' },
h('div', { className: 'ddt-listHeading dim-listHeading' },
h('h3', null, '已接入的 WhatsApp 机器人')),
h(ChannelListHeading, {
className: 'ddt-listHeading',
title: '已接入的 WhatsApp 机器人',
connectionLabel: 'WhatsApp Web',
}),
h('ul', { className: 'ddt-list dim-botList' }, model.bots.map((account) =>
h('li', { key: account.botId }, h(WhatsappAccountCard, {
account,

View file

@ -107,6 +107,7 @@ const EN = Object.freeze({
'离线': 'Offline',
'已断开': 'Disconnected',
'消息通道': 'Message channel',
'查看消息通道说明': 'View message channel details',
'最近检查': 'Last checked',
'当前工作区': 'Current workspace',
'选择目录': 'Choose folder',

View file

@ -87,6 +87,14 @@ const CSS = String.raw`
.dim-panel .dim-listSection { display: flex; flex-direction: column; gap: 0; }
.dim-panel .dim-listHeading { min-height: 0; display: flex; align-items: center; justify-content: space-between; gap: 16px; margin: 0 0 6px; padding: 0; }
.dim-panel .dim-listHeading h3 { margin: 0; color: var(--dsw-alias-label-primary, #1f2329); font-size: 14px; line-height: normal; font-weight: 650; }
.dim-panel .dim-listTitle { min-width: 0; display: inline-flex; align-items: center; gap: 6px; }
.dim-panel .dim-channelHelp { position: relative; display: inline-flex; flex: none; }
.dim-panel .dim-channelHelpButton { width: 17px; height: 17px; display: grid; place-items: center; padding: 0; border: 1px solid color-mix(in srgb, #1677ff 28%, var(--dsw-alias-border-l2, #dfe1e5)); border-radius: 50%; color: #1677ff; background: var(--dsw-alias-bg-layer-1, #fff); font: inherit; font-size: 11px; line-height: 1; font-weight: 700; cursor: help; transition: border-color .15s ease, color .15s ease, background .15s ease, box-shadow .15s ease; }
.dim-panel .dim-channelHelpButton:hover { border-color: #1677ff; color: #0f5fce; background: color-mix(in srgb, #1677ff 8%, var(--dsw-alias-bg-layer-1, #fff)); }
.dim-panel .dim-channelHelpButton:focus-visible { outline: none; border-color: #1677ff; box-shadow: 0 0 0 3px color-mix(in srgb, #1677ff 16%, transparent); }
.dim-panel .dim-channelTooltip { position: absolute; top: calc(100% + 7px); left: 0; z-index: 30; width: max-content; max-width: min(280px, calc(100vw - 48px)); display: flex; align-items: baseline; gap: 5px; padding: 8px 10px; border: 1px solid var(--dsw-alias-border-l2, #dfe1e5); border-radius: 8px; color: var(--dsw-alias-label-secondary, #646a73); background: var(--dsw-alias-bg-layer-3, #fff); box-shadow: 0 10px 28px rgb(31 35 41 / 16%); font-size: 11px; line-height: 16px; font-weight: 400; white-space: normal; opacity: 0; visibility: hidden; transform: translateY(-3px); pointer-events: none; transition: opacity .15s ease, transform .15s ease, visibility .15s ease; }
.dim-panel .dim-channelTooltip strong { color: var(--dsw-alias-label-primary, #1f2329); font-weight: 600; white-space: nowrap; }
.dim-panel .dim-channelHelp:hover .dim-channelTooltip, .dim-panel .dim-channelHelp:focus-within .dim-channelTooltip { opacity: 1; visibility: visible; transform: translateY(0); }
.dim-panel .dim-botList { display: grid; gap: 8px; margin: 0; padding: 0; list-style: none; }
.dim-panel .dim-loadingView { padding: 38px; color: var(--dsw-alias-label-secondary, #646a73); text-align: center; }
.dim-panel .dim-loadingView h3 { margin: 0 0 7px; color: var(--dsw-alias-label-primary, #1f2329); font-size: 17px; line-height: normal; font-weight: 650; }
@ -214,15 +222,13 @@ const CSS = String.raw`
.dim-panel .dim-botName { min-width: 0; }
.dim-panel .dim-botName h3 { overflow: hidden; margin: 0; color: var(--dsw-alias-label-primary, #1f2329); font-size: 15px; font-weight: 650; line-height: normal; text-overflow: ellipsis; white-space: nowrap; }
.dim-panel .dim-botName p { overflow: hidden; margin: 4px 0 0; color: var(--dsw-alias-label-secondary, #646a73); font: 12px ui-monospace, SFMono-Regular, monospace; line-height: normal; text-overflow: ellipsis; white-space: nowrap; }
.dim-panel .dim-botHealthGroup { flex: none; display: grid; justify-items: end; gap: 5px; }
.dim-panel .dim-botCard .dim-botHealth { flex: none; min-height: 0; display: inline-flex; align-items: center; gap: 7px; padding: 0; border: 0; border-radius: 0; color: var(--dsw-alias-label-secondary, #646a73); background: transparent; font: inherit; font-size: 12px; font-weight: 400; line-height: normal; white-space: nowrap; }
.dim-panel .dim-lastChecked { display: inline-flex; align-items: baseline; gap: 4px; color: var(--dsw-alias-label-tertiary, #8f959e); font: inherit; font-size: 11px; font-weight: 400; line-height: normal; white-space: nowrap; }
.dim-panel .dim-botCard .dim-healthDot { flex: none; width: 8px; height: 8px; border-radius: 50%; background: #aeb3bb; box-shadow: none; }
.dim-panel .dim-botCard .dim-healthDot[data-tone="success"] { background: var(--dsw-alias-state-success-primary, #20a162); box-shadow: 0 0 0 3px color-mix(in srgb, var(--dsw-alias-state-success-primary, #20a162) 14%, transparent); }
.dim-panel .dim-botCard .dim-healthDot[data-tone="warning"] { background: var(--dsw-alias-state-warn-primary, #d97706); }
.dim-panel .dim-botCard .dim-healthDot[data-tone="error"] { background: var(--dsw-alias-state-error-primary, #d54941); }
.dim-panel .dim-botMetrics { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 8px; margin: 6px 0; }
.dim-panel .dim-botMetric { min-width: 0; padding: 6px 8px; border: 0; border-radius: 9px; background: var(--dsw-alias-interactive-bg-hover, #f7f8fa); }
.dim-panel .dim-botMetric dt { margin: 0; color: var(--dsw-alias-label-tertiary, #8f959e); font-size: 11px; font-weight: 400; line-height: normal; }
.dim-panel .dim-botMetric dd { overflow: hidden; margin: 3px 0 0; color: var(--dsw-alias-label-primary, #1f2329); font-size: 12px; font-weight: 400; line-height: normal; text-overflow: ellipsis; white-space: nowrap; }
.dim-panel .dim-botCard .dim-cardFooter { margin-top: 0; }
.dim-panel .ddt-headingCopy { display: none; }
.dim-panel .ddt-qrFrame, .dim-panel .ddt-countdown { width: min(270px, 100%); }
@ -253,6 +259,7 @@ const CSS = String.raw`
}
@media (max-width: 720px) {
.dim-panel .dim-botCardTop { flex-direction: column; align-items: stretch; }
.dim-panel .dim-botHealthGroup { justify-items: start; }
}
@media (max-width: 560px) {
.dim-title { flex-direction: column; gap: 10px; }

View file

@ -63,16 +63,9 @@ test('the QR card responds to its plugin panel width instead of the browser view
assert.match(styles, /\.ddt-qrCopy \{ min-width: 0; overflow-wrap: anywhere; \}/);
});
test('bot metrics stay in one compact two-column row at narrow widths', async () => {
test('bot cards do not reserve a row for repeated channel metrics', async () => {
const styles = await readFile(STYLES_URL, 'utf8');
assert.match(
styles,
/\.ddt-metrics \{[^\n]*grid-template-columns: repeat\(2, minmax\(0, 1fr\)\)/,
);
assert.doesNotMatch(
styles,
/\.ddt-metrics \{ grid-template-columns: minmax\(0, 1fr\); \}/,
);
assert.doesNotMatch(styles, /\.ddt-metrics|\.ddt-metric/);
});
test('the narrow-panel toolbar keeps all three controls on one row', async () => {

View file

@ -35,7 +35,8 @@ test('Discord account card matches the unified compact card layout', () => {
}));
assert.match(markup, /data-im-channel-logo="discord"/);
assert.match(markup, /@HarnessBot/);
assert.match(markup, />Gateway 长连接</);
assert.match(markup, /class="dim-botHealthGroup"[^]*class="dim-lastChecked"><span>最近检查<\/span>/);
assert.doesNotMatch(markup, /Gateway 长连接|消息通道|dim-botMetric/);
assert.match(markup, />检查连接</);
assert.match(markup, />移除接入</);
assert.doesNotMatch(markup, /dim-cardSummary/);

View file

@ -21,7 +21,7 @@ test('QQ settings uses the shared compact channel toolbar', () => {
assert.doesNotMatch(markup, /凭据仅保存在本机|role="switch"|type="checkbox"/);
});
test('QQ bot cards match the shared two-metric card treatment', () => {
test('QQ bot cards keep check time with status and omit repeated channel details', () => {
const markup = renderToStaticMarkup(React.createElement(AccountCard, {
account: {
botId: 'qq_bot',
@ -38,8 +38,8 @@ test('QQ bot cards match the shared two-metric card treatment', () => {
}));
assert.match(markup, /class="ddt-card dim-botCard"/);
assert.match(markup, /data-im-channel-logo="qq"/);
assert.equal((markup.match(/class="ddt-metric dim-botMetric"/g) ?? []).length, 2);
assert.match(markup, />消息通道<[^]*>最近检查</);
assert.match(markup, /class="dim-botHealthGroup"[^]*class="dim-lastChecked"><span>最近检查<\/span>/);
assert.doesNotMatch(markup, /消息通道|dim-botMetric/);
assert.match(markup, />检查连接<[^]*>移除接入</);
assert.match(markup, /class="dim-presetSelect"/);
assert.doesNotMatch(markup, /收到\s*\/\s*回复|dim-cardSummary|QQ WebSocket 长连接运行正常/);

View file

@ -52,7 +52,8 @@ test('Slack account card matches the unified compact layout', () => {
}));
assert.match(markup, /data-im-channel-logo="slack"/);
assert.match(markup, /@deepseek-harness/);
assert.match(markup, />Socket Mode 长连接</);
assert.match(markup, /class="dim-botHealthGroup"[^]*class="dim-lastChecked"><span>最近检查<\/span>/);
assert.doesNotMatch(markup, /Socket Mode 长连接|消息通道|dim-botMetric/);
assert.match(markup, />检查连接</);
assert.match(markup, />移除接入</);
});

View file

@ -40,7 +40,8 @@ test('Telegram account card matches the unified compact card layout', () => {
}));
assert.match(markup, /data-im-channel-logo="telegram"/);
assert.match(markup, /@harness_bot/);
assert.match(markup, />Bot API 长轮询</);
assert.match(markup, /class="dim-botHealthGroup"[^]*class="dim-lastChecked"><span>最近检查<\/span>/);
assert.doesNotMatch(markup, /Bot API 长轮询|消息通道|dim-botMetric/);
assert.match(markup, />检查连接</);
assert.match(markup, />移除接入</);
assert.match(markup, />访问设置</);

View file

@ -50,7 +50,7 @@ test('Enterprise WeChat settings uses the shared compact channel toolbar', () =>
assert.doesNotMatch(markup, /凭据仅保存在本机|role="switch"|type="checkbox"/);
});
test('Enterprise WeChat bot cards match the shared two-metric card treatment', () => {
test('Enterprise WeChat cards keep check time with status and omit repeated channel details', () => {
const markup = renderToStaticMarkup(React.createElement(AccountCard, {
account: {
botId: 'wecom_bot',
@ -67,8 +67,8 @@ test('Enterprise WeChat bot cards match the shared two-metric card treatment', (
}));
assert.match(markup, /class="ddt-card dim-botCard"/);
assert.match(markup, /data-im-channel-logo="wecom"/);
assert.equal((markup.match(/class="ddt-metric dim-botMetric"/g) ?? []).length, 2);
assert.match(markup, />消息通道<[^]*>最近检查</);
assert.match(markup, /class="dim-botHealthGroup"[^]*class="dim-lastChecked"><span>最近检查<\/span>/);
assert.doesNotMatch(markup, /消息通道|dim-botMetric/);
assert.match(markup, />检查连接<[^]*>移除接入</);
assert.match(markup, /class="dim-presetSelect"/);
assert.doesNotMatch(markup, /收到\s*\/\s*回复|dim-cardSummary|企业微信 WebSocket 长连接运行正常/);

View file

@ -66,12 +66,12 @@ test('WhatsApp account card uses the unified compact channel layout', () => {
testNotice: '测试消息已发送,请到 WhatsApp 自聊会话中确认。',
}));
assert.match(markup, /data-im-channel-logo="whatsapp"/);
assert.match(markup, /WhatsApp Web/);
assert.match(markup, /class="dim-botHealthGroup"[^]*class="dim-lastChecked"><span>最近检查<\/span>/);
assert.doesNotMatch(markup, /WhatsApp Web|消息通道|dim-botMetric/);
assert.match(markup, /检查连接/);
assert.match(markup, /移除接入/);
assert.match(markup, /class="dim-presetSelect"/);
assert.match(markup, /role="status"[^>]*>测试消息已发送/);
assert.equal((markup.match(/class="ddt-metric dim-botMetric"/g) ?? []).length, 2);
});
test('WhatsApp connection check requests a test message from the existing reconnect endpoint', async () => {

View file

@ -11,6 +11,7 @@ import {
inject as clientInject,
} from '../plugin-src/client/index.js';
import { CredentialBindingPanel } from '../plugin-src/client/credential-binding.js';
import { ChannelListHeading } from '../plugin-src/client/channel-card-meta.js';
import { DINGTALK_ENDPOINTS } from '../plugin-src/client/channels/dingtalk/api.js';
import {
AccountCard as DingtalkAccountCard,
@ -235,11 +236,11 @@ test('Feishu bot cards place the application identifier under the bot name', asy
assert.equal((markup.match(/dim-cardAction(?: |")/g) ?? []).length, 3);
assert.doesNotMatch(markup, /连接状态:|bxf-divider/);
assert.doesNotMatch(markup, /custom-bot-avatar/);
assert.equal((markup.match(/class="bxf-metric dim-botMetric"/g) ?? []).length, 2);
assert.match(markup, />消息通道<[^]*>最近检查</);
assert.match(markup, /class="dim-botHealthGroup"[^]*class="dim-lastChecked"><span>最近检查<\/span>/);
assert.doesNotMatch(markup, /消息通道|dim-botMetric/);
assert.match(markup, /class="dim-presetSelect"/);
assert.doesNotMatch(markup, />应用标识<|>飞书机器人</);
assert.match(styles, /\.bxf-statusGrid \{[^}]*grid-template-columns: repeat\(2, minmax\(0, 1fr\)\)/);
assert.doesNotMatch(styles, /\.bxf-statusGrid|\.bxf-metric/);
});
test('Feishu keeps its heading controls on one row without a plus icon', async () => {
@ -369,9 +370,24 @@ test('bot list headings omit the total already shown by the online badge', async
for (const source of sources) {
assert.doesNotMatch(source, /length} 个/);
assert.match(source, /ChannelListHeading/);
}
});
test('channel connection details live in an accessible heading tooltip', async () => {
const styles = await readFile(STYLES_URL, 'utf8');
const markup = renderToStaticMarkup(React.createElement(ChannelListHeading, {
className: 'dxw-listHeading',
title: '已接入的微信账号',
connectionLabel: 'iLink 长轮询',
}));
assert.match(markup, /<h3>已接入的微信账号<\/h3>/);
assert.match(markup, /aria-label="查看消息通道说明"/);
assert.match(markup, /role="tooltip"><span>消息通道<\/span><strong>iLink 长轮询<\/strong>/);
assert.match(styles, /\.dim-panel \.dim-channelHelp:hover \.dim-channelTooltip, \.dim-panel \.dim-channelHelp:focus-within \.dim-channelTooltip \{[^}]*opacity: 1;[^}]*visibility: visible;/);
});
test('all channel settings states use the DingTalk page treatment', async () => {
const [styles, feishuSource, weixinSource, dingtalkSource, wecomSource] = await Promise.all([
readFile(STYLES_URL, 'utf8'),
@ -398,7 +414,6 @@ test('all channel settings states use the DingTalk page treatment', async () =>
'dim-emptyView',
'dim-qrLayout',
'dim-inlineError',
'dim-listHeading',
'dim-confirm',
]) {
assert.match(source, new RegExp(className));
@ -449,7 +464,8 @@ test('bot cards reuse the same channel brand logos as the channel rail', () => {
assert.match(accountMarkup, /class="dim-presetSelect"/);
assert.doesNotMatch(accountMarkup, /dim-cardSummary|微信消息长轮询运行正常/);
assert.equal((accountMarkup.match(/dim-cardAction(?: |")/g) ?? []).length, 2);
assert.equal((accountMarkup.match(/class="dxw-metric dim-botMetric"/g) ?? []).length, 2);
assert.match(accountMarkup, /class="dim-botHealthGroup"[^]*class="dim-lastChecked"><span>最近检查<\/span>/);
assert.doesNotMatch(accountMarkup, /消息通道|dim-botMetric/);
assert.doesNotMatch(accountMarkup, /收到 \/ 回复/);
});
@ -464,7 +480,8 @@ test('Enterprise WeChat cards reuse the rail logo and compact action treatment',
}));
assert.match(markup, /data-im-channel-logo="wecom"/);
assert.equal((markup.match(/dim-cardAction(?: |")/g) ?? []).length, 2);
assert.equal((markup.match(/class="ddt-metric dim-botMetric"/g) ?? []).length, 2);
assert.match(markup, /class="dim-lastChecked"><span>最近检查<\/span>/);
assert.doesNotMatch(markup, /消息通道|dim-botMetric/);
});
test('DingTalk bot cards omit the redundant received and replied metric', () => {
@ -485,11 +502,11 @@ test('DingTalk bot cards omit the redundant received and replied metric', () =>
assert.match(markup, /class="ddt-card dim-botCard"/);
assert.match(markup, /class="ddt-health dim-botHealth"/);
assert.equal((markup.match(/class="ddt-metric dim-botMetric"/g) ?? []).length, 2);
assert.match(markup, /class="dim-lastChecked"><span>最近检查<\/span>/);
assert.match(markup, /class="ddt-accountFooter dim-cardFooter"/);
assert.doesNotMatch(markup, /dim-cardSummary|Stream 长连接运行正常/);
assert.equal((markup.match(/dim-cardAction(?: |")/g) ?? []).length, 2);
assert.match(markup, />消息通道<[^]*>最近检查</);
assert.doesNotMatch(markup, /消息通道|dim-botMetric/);
assert.doesNotMatch(markup, /收到 \/ 回复/);
});
@ -562,10 +579,10 @@ test('all channel bot cards use the DingTalk card treatment', async () => {
assert.match(styles, /\.dim-panel \.dim-botCardTop \{[^}]*align-items: flex-start;[^}]*gap: 12px;/);
assert.match(styles, /\.dim-panel \.dim-botAvatar \{[^}]*width: 38px;[^}]*height: 38px;[^}]*border-radius: 11px;/);
assert.match(styles, /\.dim-panel \.dim-botName h3 \{[^}]*font-size: 15px;/);
assert.match(styles, /\.dim-panel \.dim-botHealthGroup \{[^}]*display: grid;[^}]*justify-items: end;[^}]*gap: 5px;/);
assert.match(styles, /\.dim-panel \.dim-botCard \.dim-botHealth \{[^}]*background: transparent;[^}]*font-size: 12px;[^}]*font-weight: 400;/);
assert.match(styles, /\.dim-panel \.dim-botMetrics \{[^}]*grid-template-columns: repeat\(2, minmax\(0, 1fr\)\);[^}]*gap: 8px;[^}]*margin: 6px 0;/);
assert.match(styles, /\.dim-panel \.dim-botMetric \{[^}]*padding: 6px 8px;[^}]*border: 0;[^}]*border-radius: 9px;/);
assert.match(styles, /\.dim-panel \.dim-botMetric dd \{[^}]*margin: 3px 0 0;[^}]*font-size: 12px;[^}]*font-weight: 400;/);
assert.match(styles, /\.dim-panel \.dim-lastChecked \{[^}]*display: inline-flex;[^}]*font-size: 11px;[^}]*white-space: nowrap;/);
assert.doesNotMatch(styles, /\.dim-panel \.dim-botMetrics|\.dim-panel \.dim-botMetric/);
});
test('bot cards keep the full workspace path on its own single line', async () => {
@ -779,7 +796,7 @@ test('all nine channel settings and connected cards render English copy', () =>
];
const cardMarkup = cards.map(renderToStaticMarkup).join('\n');
assert.match(cardMarkup, /Connected/);
assert.match(cardMarkup, /Message channel/);
assert.doesNotMatch(cardMarkup, /Message channel/);
assert.match(cardMarkup, /Last checked/);
assert.match(cardMarkup, /Check connection/);
assert.match(cardMarkup, /Remove connection/);