Refine unified IM settings UI

This commit is contained in:
xmanrui 2026-08-15 16:24:20 +08:00
parent bd469f58f8
commit 828a50398a
12 changed files with 797 additions and 740 deletions

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,46 @@
import * as React from 'react';
const h = React.createElement;
function dimensions(size) {
return size === undefined ? {} : { width: size, height: size };
}
export function WeixinLogoGlyph({ size } = {}) {
return h('svg', {
...dimensions(size),
viewBox: '0 0 24 24',
focusable: 'false',
'aria-hidden': 'true',
'data-im-channel-logo': 'weixin',
}, h('path', {
fill: 'currentColor',
d: 'M8.691 2.188C3.891 2.188 0 5.476 0 9.53c0 2.212 1.17 4.203 3.002 5.55a.59.59 0 0 1 .213.665l-.39 1.48c-.019.07-.048.141-.048.213 0 .163.13.295.29.295a.326.326 0 0 0 .167-.054l1.903-1.114a.864.864 0 0 1 .717-.098 10.16 10.16 0 0 0 2.837.403c.276 0 .543-.027.811-.05-.857-2.578.157-4.972 1.932-6.446 1.703-1.415 3.882-1.98 5.853-1.838-.576-3.583-4.196-6.348-8.596-6.348zM5.785 5.991c.642 0 1.162.529 1.162 1.18a1.17 1.17 0 0 1-1.162 1.178A1.17 1.17 0 0 1 4.623 7.17c0-.651.52-1.18 1.162-1.18zm5.813 0c.642 0 1.162.529 1.162 1.18a1.17 1.17 0 0 1-1.162 1.178 1.17 1.17 0 0 1-1.162-1.178c0-.651.52-1.18 1.162-1.18zm5.34 2.867c-1.797-.052-3.746.512-5.28 1.786-1.72 1.428-2.687 3.72-1.78 6.22.942 2.453 3.666 4.229 6.884 4.229.826 0 1.622-.12 2.361-.336a.722.722 0 0 1 .598.082l1.584.926a.272.272 0 0 0 .14.047c.134 0 .24-.111.24-.247 0-.06-.023-.12-.038-.177l-.327-1.233a.582.582 0 0 1-.023-.156.49.49 0 0 1 .201-.398C23.024 18.48 24 16.82 24 14.98c0-3.21-2.931-5.837-6.656-6.088V8.89c-.135-.01-.27-.027-.407-.03zm-2.53 3.274c.535 0 .969.44.969.982a.976.976 0 0 1-.969.983.976.976 0 0 1-.969-.983c0-.542.434-.982.97-.982zm4.844 0c.535 0 .969.44.969.982a.976.976 0 0 1-.969.983.976.976 0 0 1-.969-.983c0-.542.434-.982.969-.982z',
}));
}
export function FeishuLogoGlyph({ size } = {}) {
return h('svg', {
...dimensions(size),
viewBox: '0 0 24 24',
focusable: 'false',
'aria-hidden': 'true',
'data-im-channel-logo': 'feishu',
},
h('path', { fill: '#00D6B9', d: 'M7.2 4.5h7.6c1.2 0 2.1.55 2.7 1.58 1.05 1.8 1.55 3.45 1.58 4.95-2.04-.62-4.2-.15-6.22 1.45C11.3 9.7 9.42 7.04 7.2 4.5Z' }),
h('path', { fill: '#1456B8', d: 'M10.8 13.55c3.3-2.93 5.72-4.24 9.47-2.52-1.2 1.45-2.27 4.18-3.86 5.43-1.67 1.31-3.9.5-5.61-.64v-2.27Z' }),
h('path', { fill: '#3370FF', d: 'M4.4 8.35c3.47 3.61 7.25 6.1 10.33 5.7 1.06-.14 2.2-.72 3.4-1.72-1.04 2.65-2.6 4.8-5.06 6-2.46 1.2-5.56.52-7.42-.72A2.76 2.76 0 0 1 4.4 15.3V8.35Z' }));
}
export function DingtalkLogoGlyph({ size } = {}) {
return h('svg', {
...dimensions(size),
viewBox: '0 0 48 48',
focusable: 'false',
'aria-hidden': 'true',
'data-im-channel-logo': 'dingtalk',
}, h('path', {
fill: 'currentColor',
d: 'M37.05 22.783c-6.758-5.216-14.378-12.128-22.73-19.538-.655-.585-1.242-.354-1.536.42-1.88 4.973-.058 9.386 2.889 11.932s7.368 4.912 10.058 6.155c.105.049.013.203-.093.163-4.953-2.182-8.397-3.765-13.07-7.368-.497-.388-1.01-.242-1.07.521-.384 4.748 2.657 8.483 6.058 9.745 2.1.781 4.398 1.212 6.53 1.474.109.015.084.178-.027.178-2.747.01-6.058-.654-8.935-1.751-.606-.233-.818.25-.722.633.491 2.008 2.974 5.076 6.926 5.73a12 12 0 0 0 2.228.115c.164 0 .208.089.154.217q-2.685 4.6-2.803 4.797c-.091.152-.036.275.156.275h3.543c.164 0 .264.106.18.246l-4.958 8.196c-.191.328.035.565.395.301s15.212-11.133 15.636-11.448c.195-.142.148-.327-.124-.327h-3.18c-.206 0-.252-.14-.111-.28.14-.141 3.602-3.594 4.837-4.888 1.283-1.35 1.938-3.825-.231-5.498',
}));
}

View file

@ -131,8 +131,6 @@ function QrPanel({ provision, now, busy, onRefresh, onCancel }) {
h('li', null, '使用已加入企业/组织的钉钉账号扫描左侧二维码'),
h('li', null, '在授权页点击“一键创建新机器人”'),
h('li', null, '保持本页打开,等待机器人自动连接')),
h('div', { className: 'ddt-brandNotice' },
'钉钉官方授权页目前可能显示 OpenClaw 品牌,这是官方连接器授权页面,不影响机器人接入 DeepSeek Harness。'),
h('div', { className: 'ddt-actions' },
expired
? h(Button, { kind: 'primary', onClick: onRefresh, disabled: busy }, '重新生成二维码')
@ -201,7 +199,7 @@ function RemoveConfirmation({ account, busy, onConfirm, onCancel }) {
busy ? '正在移除…' : '确认移除接入')));
}
function AccountCard({
export function AccountCard({
account,
busy,
removing,
@ -226,8 +224,6 @@ function AccountCard({
h('dl', { className: 'ddt-metrics' },
h('div', { className: 'ddt-metric' }, h('dt', null, '消息通道'),
h('dd', null, account.connected ? 'Stream 长连接' : '离线')),
h('div', { className: 'ddt-metric' }, h('dt', null, '收到 / 回复'),
h('dd', null, `${account.stats.messagesReceived} / ${account.stats.messagesReplied}`)),
h('div', { className: 'ddt-metric' }, h('dt', null, '最近检查'),
h('dd', null, checkedTime(account.health.lastCheckedAt)))),
h('div', { className: 'ddt-accountFooter' },

View file

@ -68,7 +68,6 @@ const CSS = String.raw`
.ddt-steps { margin: 18px 0 16px; padding: 0; list-style: none; counter-reset: ddt-step; }
.ddt-steps li { position: relative; min-height: 28px; padding: 3px 0 3px 36px; color: var(--dsw-alias-label-secondary, #646a73); font-size: 13px; line-height: 22px; counter-increment: ddt-step; }
.ddt-steps li::before { content: counter(ddt-step); position: absolute; left: 0; top: 1px; width: 26px; height: 26px; display: grid; place-items: center; border-radius: 8px; color: var(--ddt-accent-deep); background: var(--ddt-accent-wash); font-size: 12px; font-weight: 700; }
.ddt-brandNotice { margin: 0 0 18px; padding: 10px 12px; border-left: 3px solid #91caff; border-radius: 0 8px 8px 0; color: var(--dsw-alias-label-secondary, #646a73); background: #f5f9ff; font-size: 12px; line-height: 1.55; }
.ddt-loading { padding: 38px; color: var(--dsw-alias-label-secondary, #646a73); text-align: center; }
.ddt-loading h3 { margin: 0 0 7px; color: var(--dsw-alias-label-primary, #1f2329); font-size: 17px; }
.ddt-loading p { line-height: 1.6; }
@ -88,7 +87,7 @@ 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(3, minmax(0, 1fr)); gap: 10px; margin: 20px 0; }
.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-fill-tertiary, #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; }

View file

@ -1,5 +1,6 @@
import * as React from "react";
import { FeishuLogoGlyph } from "../../channel-logos.js";
import {
FEISHU_ENDPOINTS,
FEISHU_RPC_CHANNEL,
@ -70,13 +71,6 @@ function SparkIcon({ size = 18 }) {
);
}
function PlusIcon({ size = 17 }) {
return h(SvgIcon, { size }, h("path", {
d: "M12 5v14M5 12h14", stroke: "currentColor", strokeWidth: "1.8",
strokeLinecap: "round",
}));
}
function RefreshIcon({ size = 16 }) {
return h(SvgIcon, { size }, h("path", {
d: "M19 7.5V4m0 0h-3.5M19 4l-2.1 2.1A7 7 0 1 0 19 13",
@ -127,9 +121,8 @@ const Button = React.forwardRef(function Button(
}, icon, h("span", null, children));
});
function BrandMark({ compact = false }) {
return h("div", { className: compact ? "bxf-avatar" : "bxf-brandMark" },
h(RobotIcon, { size: compact ? 25 : 34 }));
function BrandMark() {
return h("div", { className: "bxf-brandMark" }, h(RobotIcon, { size: 34 }));
}
function Heading({ totals, onAdd, adding, busy, addButtonRef }) {
@ -149,12 +142,12 @@ function Heading({ totals, onAdd, adding, busy, addButtonRef }) {
h(Button, {
kind: "primary",
size: "small",
icon: h(PlusIcon),
className: "bxf-bindButton",
onClick: onAdd,
disabled: adding || busy,
ref: addButtonRef,
"aria-busy": busy ? "true" : undefined,
}, adding ? "正在添加" : "添加机器人"),
}, adding ? "正在绑定" : "扫码绑定机器人"),
),
);
}
@ -363,7 +356,7 @@ function RemoveConfirmation({ bot, busy, onConfirm, onCancel }) {
);
}
function BotCard({
export function BotCard({
connection,
busy,
actionError,
@ -388,8 +381,6 @@ function BotCard({
?? connection.error?.message
?? health.summary;
const titleId = `bxf-bot-${connection.botId.replace(/[^a-zA-Z0-9_-]/g, "-")}`;
const domainLabel = bot.domain === "lark" ? "Lark" : "飞书";
return h("article", {
className: "bxf-card bxf-botCard",
"aria-labelledby": titleId,
@ -400,12 +391,11 @@ function BotCard({
h("div", { className: "bxf-cardBody" },
h("div", { className: "bxf-connectedTop" },
h("div", { className: "bxf-botIdentity" },
bot.avatarUrl
? h("div", { className: "bxf-avatar" }, h("img", { src: bot.avatarUrl, alt: "" }))
: h(BrandMark, { compact: true }),
h("div", { className: "bxf-avatar", "aria-hidden": "true" },
h(FeishuLogoGlyph, { size: 34 })),
h("div", { className: "bxf-botName" },
h("h3", { id: titleId, title: bot.name }, bot.name),
h("p", null, bot.tenantName ? `${bot.tenantName} · ${domainLabel}机器人` : `${domainLabel}机器人`)),
h("p", { title: bot.appIdMasked }, bot.appIdMasked ?? "应用标识已安全保存")),
),
h("div", { className: "bxf-healthPill", "data-health": stateForDisplay },
h("span", { className: "bxf-dot", "data-tone": tone }),
@ -414,8 +404,6 @@ function BotCard({
h("dl", { className: "bxf-statusGrid" },
h("div", { className: "bxf-metric" }, h("dt", null, "消息通道"),
h("dd", null, connected ? "长连接" : stateForDisplay === "connecting" ? "连接中" : "已断开")),
h("div", { className: "bxf-metric" }, h("dt", null, "应用标识"),
h("dd", { title: bot.appIdMasked }, bot.appIdMasked ?? "已安全保存")),
h("div", { className: "bxf-metric" }, h("dt", null, "最近检查"),
h("dd", null, formatCheckedTime(health.lastCheckedAt))),
),

View file

@ -56,11 +56,12 @@ const CSS = String.raw`
}
.bxf-headingTools {
width: 100%;
flex: none;
display: flex;
align-items: center;
justify-content: flex-end;
flex-wrap: wrap;
flex-wrap: nowrap;
gap: 8px;
}
@ -246,6 +247,7 @@ const CSS = String.raw`
.bxf-button[data-kind="danger"] { color: var(--bxf-error); }
.bxf-button[data-size="small"] { min-height: 32px; padding: 5px 10px; font-size: 12px; }
.bxf-bindButton { flex: none; white-space: nowrap; }
.bxf-provisionCard {
border-color: color-mix(in srgb, var(--bxf-accent) 32%, var(--dsw-alias-border-l2, #dee0e3));
@ -408,17 +410,16 @@ const CSS = String.raw`
.bxf-connectedTop { display: flex; align-items: center; justify-content: space-between; gap: 20px; }
.bxf-botIdentity { min-width: 0; display: flex; align-items: center; gap: 13px; }
.bxf-avatar { flex: none; width: 48px; height: 48px; display: grid; place-items: center; overflow: hidden; border-radius: 14px; color: var(--bxf-accent); background: color-mix(in srgb, var(--bxf-accent) 9%, var(--dsw-alias-bg-layer-1, #fff)); }
.bxf-avatar img { width: 100%; height: 100%; object-fit: cover; }
.bxf-avatar { flex: none; width: 48px; height: 48px; display: grid; place-items: center; overflow: hidden; border: 1px solid var(--dsw-alias-line-border, #e5e6eb); border-radius: 14px; background: var(--dsw-alias-bg-layer-1, #fff); box-shadow: 0 1px 3px rgb(31 35 41 / 7%); }
.bxf-botName { min-width: 0; }
.bxf-botName h3 { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-size: 17px; line-height: 24px; font-weight: 650; }
.bxf-botName p { color: var(--dsw-alias-label-tertiary, #8f959e); font-size: 12px; line-height: 18px; margin-top: 2px; }
.bxf-botName p { overflow: hidden; color: var(--dsw-alias-label-tertiary, #8f959e); font-family: var(--ds-font-family-code, monospace); font-size: 12px; line-height: 18px; text-overflow: ellipsis; white-space: nowrap; margin-top: 2px; }
.bxf-healthPill { flex: none; display: inline-flex; align-items: center; gap: 7px; min-height: 28px; border-radius: 999px; padding: 4px 10px; color: var(--bxf-success); background: color-mix(in srgb, var(--bxf-success) 10%, transparent); font-size: 12px; font-weight: 600; line-height: 18px; }
.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(3, minmax(0, 1fr)); gap: 10px; margin-top: 22px; }
.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; }
@ -476,15 +477,13 @@ const CSS = String.raw`
@keyframes bxf-shimmer { to { background-position: -220% 0; } }
@container (max-width: 620px) {
.bxf-heading { flex-direction: column; gap: 10px; }
.bxf-headingTools { width: 100%; justify-content: flex-start; }
.bxf-headingTools .bxf-button { margin-left: auto; }
.bxf-headingTools { gap: 6px; }
.bxf-headingTools .bxf-totalBadge, .bxf-headingTools .bxf-localBadge { padding-inline: 8px; }
.bxf-headingTools .bxf-localBadge { gap: 4px; }
.bxf-headingTools .bxf-bindButton { padding-inline: 10px; }
}
@media (max-width: 680px) {
.bxf-heading { flex-direction: column; gap: 10px; }
.bxf-headingTools { width: 100%; justify-content: flex-start; }
.bxf-headingTools .bxf-button { margin-left: auto; }
.bxf-intro { grid-template-columns: minmax(0, 1fr); }
.bxf-markStage { display: none; }
.bxf-qrLayout { grid-template-columns: minmax(0, 1fr); justify-items: center; }

View file

@ -1,5 +1,6 @@
import * as React from 'react';
import { WeixinLogoGlyph } from '../../channel-logos.js';
import {
WEIXIN_ENDPOINTS,
WEIXIN_RPC_CHANNEL,
@ -19,29 +20,6 @@ const h = React.createElement;
export const name = 'weixin-settings';
export const inject = ['slots', 'connection'];
function WeixinIcon({ size = 26 }) {
return h('svg', {
width: size,
height: size,
viewBox: '0 0 24 24',
fill: 'none',
xmlns: 'http://www.w3.org/2000/svg',
'aria-hidden': 'true',
},
h('path', {
d: 'M9.7 4.2c-4.15 0-7.5 2.72-7.5 6.08 0 1.91 1.08 3.61 2.78 4.72l-.7 2.35 2.75-1.37c.84.25 1.73.38 2.67.38 4.14 0 7.5-2.72 7.5-6.08S13.84 4.2 9.7 4.2Z',
fill: 'currentColor', opacity: '.96',
}),
h('path', {
d: 'M14.4 8.2c4.08 0 7.4 2.66 7.4 5.95 0 1.78-.97 3.38-2.5 4.47l.58 2-2.36-1.18c-.97.43-2.03.65-3.12.65-4.09 0-7.4-2.66-7.4-5.94 0-3.29 3.31-5.95 7.4-5.95Z',
fill: 'currentColor', stroke: 'white', strokeWidth: '.8',
}),
h('circle', { cx: '7.25', cy: '9.5', r: '1', fill: 'white' }),
h('circle', { cx: '11.55', cy: '9.5', r: '1', fill: 'white' }),
h('circle', { cx: '12.1', cy: '13.2', r: '.9', fill: 'white' }),
h('circle', { cx: '16.3', cy: '13.2', r: '.9', fill: 'white' }));
}
const Button = React.forwardRef(function Button(
{ children, kind = 'secondary', className = '', ...props },
ref,
@ -93,7 +71,7 @@ function EmptyView({ onStart, busy }) {
h(Button, { kind: 'primary', onClick: onStart, disabled: busy },
busy ? '正在生成二维码…' : '生成微信二维码')),
),
h('div', { className: 'dxw-logo', 'aria-hidden': 'true' }, h(WeixinIcon, { size: 64 })),
h('div', { className: 'dxw-logo', 'aria-hidden': 'true' }, h(WeixinLogoGlyph, { size: 64 })),
));
}
@ -216,14 +194,14 @@ function checkedTime(timestamp) {
}
}
function AccountCard({ account, busy, removing, onReconnect, onRequestRemove, onConfirmRemove, onCancelRemove }) {
export function AccountCard({ account, busy, removing, onReconnect, onRequestRemove, onConfirmRemove, onCancelRemove }) {
const state = busy === 'reconnect' ? 'connecting' : account.state;
const tone = account.connected ? 'success' : state === 'error' ? 'error' : 'warning';
return h('article', { className: 'dxw-card', tabIndex: -1, 'data-bot-id': account.botId },
h('div', { className: 'dxw-cardBody' },
h('div', { className: 'dxw-accountTop' },
h('div', { className: 'dxw-accountIdentity' },
h('div', { className: 'dxw-avatar', 'aria-hidden': 'true' }, h(WeixinIcon, { size: 27 })),
h('div', { className: 'dxw-avatar', 'aria-hidden': 'true' }, h(WeixinLogoGlyph, { size: 27 })),
h('div', null, h('h3', null, account.bot.name), h('p', null, account.bot.accountIdMasked))),
h('div', { className: 'dxw-health' },
h('span', { className: 'dxw-dot', 'data-tone': tone }),
@ -231,8 +209,6 @@ function AccountCard({ account, busy, removing, onReconnect, onRequestRemove, on
h('dl', { className: 'dxw-metrics' },
h('div', { className: 'dxw-metric' }, h('dt', null, '消息通道'),
h('dd', null, account.connected ? 'iLink 长轮询' : '离线')),
h('div', { className: 'dxw-metric' }, h('dt', null, '收到 / 回复'),
h('dd', null, `${account.stats.messagesReceived} / ${account.stats.messagesReplied}`)),
h('div', { className: 'dxw-metric' }, h('dt', null, '最近检查'),
h('dd', null, checkedTime(account.health.lastCheckedAt)))),
h('div', { className: 'dxw-accountFooter' },

View file

@ -78,7 +78,7 @@ 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(3, minmax(0, 1fr)); gap: 12px; margin: 20px 0; }
.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-fill-tertiary, #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; }

View file

@ -1,5 +1,6 @@
import * as React from 'react';
import { DingtalkLogoGlyph, FeishuLogoGlyph, WeixinLogoGlyph } from './channel-logos.js';
import { DINGTALK_RPC_CHANNEL } from './channels/dingtalk/api.js';
import { DingtalkSettingsTab } from './channels/dingtalk/index.js';
import { FeishuSettingsTab } from './channels/feishu/index.js';
@ -23,31 +24,17 @@ const CHANNELS = Object.freeze([
function WeixinLogo() {
return h('span', { className: 'dim-logo dim-logoWeixin', 'aria-hidden': 'true' },
h('svg', { viewBox: '0 0 24 24', focusable: 'false' },
h('path', {
fill: 'currentColor',
d: 'M8.691 2.188C3.891 2.188 0 5.476 0 9.53c0 2.212 1.17 4.203 3.002 5.55a.59.59 0 0 1 .213.665l-.39 1.48c-.019.07-.048.141-.048.213 0 .163.13.295.29.295a.326.326 0 0 0 .167-.054l1.903-1.114a.864.864 0 0 1 .717-.098 10.16 10.16 0 0 0 2.837.403c.276 0 .543-.027.811-.05-.857-2.578.157-4.972 1.932-6.446 1.703-1.415 3.882-1.98 5.853-1.838-.576-3.583-4.196-6.348-8.596-6.348zM5.785 5.991c.642 0 1.162.529 1.162 1.18a1.17 1.17 0 0 1-1.162 1.178A1.17 1.17 0 0 1 4.623 7.17c0-.651.52-1.18 1.162-1.18zm5.813 0c.642 0 1.162.529 1.162 1.18a1.17 1.17 0 0 1-1.162 1.178 1.17 1.17 0 0 1-1.162-1.178c0-.651.52-1.18 1.162-1.18zm5.34 2.867c-1.797-.052-3.746.512-5.28 1.786-1.72 1.428-2.687 3.72-1.78 6.22.942 2.453 3.666 4.229 6.884 4.229.826 0 1.622-.12 2.361-.336a.722.722 0 0 1 .598.082l1.584.926a.272.272 0 0 0 .14.047c.134 0 .24-.111.24-.247 0-.06-.023-.12-.038-.177l-.327-1.233a.582.582 0 0 1-.023-.156.49.49 0 0 1 .201-.398C23.024 18.48 24 16.82 24 14.98c0-3.21-2.931-5.837-6.656-6.088V8.89c-.135-.01-.27-.027-.407-.03zm-2.53 3.274c.535 0 .969.44.969.982a.976.976 0 0 1-.969.983.976.976 0 0 1-.969-.983c0-.542.434-.982.97-.982zm4.844 0c.535 0 .969.44.969.982a.976.976 0 0 1-.969.983.976.976 0 0 1-.969-.983c0-.542.434-.982.969-.982z',
}),
));
h(WeixinLogoGlyph));
}
function FeishuLogo() {
return h('span', { className: 'dim-logo dim-logoFeishu', 'aria-hidden': 'true' },
h('svg', { viewBox: '0 0 24 24', focusable: 'false' },
h('path', { fill: '#00D6B9', d: 'M7.2 4.5h7.6c1.2 0 2.1.55 2.7 1.58 1.05 1.8 1.55 3.45 1.58 4.95-2.04-.62-4.2-.15-6.22 1.45C11.3 9.7 9.42 7.04 7.2 4.5Z' }),
h('path', { fill: '#1456B8', d: 'M10.8 13.55c3.3-2.93 5.72-4.24 9.47-2.52-1.2 1.45-2.27 4.18-3.86 5.43-1.67 1.31-3.9.5-5.61-.64v-2.27Z' }),
h('path', { fill: '#3370FF', d: 'M4.4 8.35c3.47 3.61 7.25 6.1 10.33 5.7 1.06-.14 2.2-.72 3.4-1.72-1.04 2.65-2.6 4.8-5.06 6-2.46 1.2-5.56.52-7.42-.72A2.76 2.76 0 0 1 4.4 15.3V8.35Z' }),
));
h(FeishuLogoGlyph));
}
function DingtalkLogo() {
return h('span', { className: 'dim-logo dim-logoDingtalk', 'aria-hidden': 'true' },
h('svg', { viewBox: '0 0 48 48', focusable: 'false' },
h('path', {
fill: 'currentColor',
d: 'M37.05 22.783c-6.758-5.216-14.378-12.128-22.73-19.538-.655-.585-1.242-.354-1.536.42-1.88 4.973-.058 9.386 2.889 11.932s7.368 4.912 10.058 6.155c.105.049.013.203-.093.163-4.953-2.182-8.397-3.765-13.07-7.368-.497-.388-1.01-.242-1.07.521-.384 4.748 2.657 8.483 6.058 9.745 2.1.781 4.398 1.212 6.53 1.474.109.015.084.178-.027.178-2.747.01-6.058-.654-8.935-1.751-.606-.233-.818.25-.722.633.491 2.008 2.974 5.076 6.926 5.73a12 12 0 0 0 2.228.115c.164 0 .208.089.154.217q-2.685 4.6-2.803 4.797c-.091.152-.036.275.156.275h3.543c.164 0 .264.106.18.246l-4.958 8.196c-.191.328.035.565.395.301s15.212-11.133 15.636-11.448c.195-.142.148-.327-.124-.327h-3.18c-.206 0-.252-.14-.111-.28.14-.141 3.602-3.594 4.837-4.888 1.283-1.35 1.938-3.825-.231-5.498',
}),
));
h(DingtalkLogoGlyph));
}
function ChannelLogo({ channel }) {
@ -62,7 +49,6 @@ export function IMSettingsTab({ dingtalkRpcCall, feishuRpcCall, weixinRpcCall })
return h('section', { className: 'dim-page', 'aria-label': 'IM机器人设置' },
h('header', { className: 'dim-title' },
h('p', null, '通过扫码把机器人接入 DeepSeek Harness'),
h('div', { className: 'dim-channelCount' }, `${CHANNELS.length} 个渠道`),
),
h('div', { className: 'dim-layout' },
h('nav', { className: 'dim-rail', role: 'tablist', 'aria-label': 'IM 渠道' },

View file

@ -11,9 +11,8 @@ const CSS = String.raw`
box-sizing: border-box;
}
.dim-page *, .dim-page *::before, .dim-page *::after { box-sizing: border-box; }
.dim-title { display: flex; align-items: center; justify-content: space-between; gap: 22px; margin: 0 0 26px; }
.dim-title { margin: 0 0 26px; }
.dim-title p { margin: 0; color: var(--dsw-alias-label-secondary, #646a73); font-size: 13px; line-height: 20px; white-space: nowrap; }
.dim-channelCount { flex: none; padding: 6px 11px; border-radius: 999px; color: var(--dsw-alias-label-secondary, #646a73); background: var(--dsw-alias-fill-secondary, #f2f3f5); font-size: 11px; white-space: nowrap; }
.dim-layout { display: grid; grid-template-columns: 174px 1px minmax(0, 1fr); gap: 24px; align-items: start; }
.dim-rail { max-height: 520px; display: grid; align-content: start; gap: 8px; overflow-y: auto; padding: 1px 4px 1px 1px; scrollbar-width: thin; scrollbar-color: var(--dsw-alias-line-border, #dfe1e5) transparent; }
.dim-rail::-webkit-scrollbar { width: 4px; }

View file

@ -22,7 +22,7 @@ test('QR guidance describes the complete official DingTalk authorization flow',
assert.match(source, /如果钉钉提示尚未加入组织/);
assert.match(source, /在授权页点击“一键创建新机器人”/);
assert.match(source, /保持本页打开,等待机器人自动连接/);
assert.match(source, /官方授权页目前可能显示 OpenClaw 品牌/);
assert.doesNotMatch(source, /OpenClaw 品牌|ddt-brandNotice/);
assert.match(source, /safeQrSource\(provision\.qrCodeDataUrl\)/);
assert.doesNotMatch(source, /verificationUrl|打开备用链接/);
assert.doesNotMatch(source, /dangerouslySetInnerHTML|window\.open\(/);
@ -63,11 +63,11 @@ 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 three-column row at narrow widths', async () => {
test('bot metrics stay in one compact two-column row at narrow widths', async () => {
const styles = await readFile(STYLES_URL, 'utf8');
assert.match(
styles,
/\.ddt-metrics \{[^\n]*grid-template-columns: repeat\(3, minmax\(0, 1fr\)\)/,
/\.ddt-metrics \{[^\n]*grid-template-columns: repeat\(2, minmax\(0, 1fr\)\)/,
);
assert.doesNotMatch(
styles,

View file

@ -7,8 +7,18 @@ import { renderToStaticMarkup } from 'react-dom/server';
import { IMSettingsTab } from '../plugin-src/client/index.js';
import { DINGTALK_ENDPOINTS } from '../plugin-src/client/channels/dingtalk/api.js';
import { AccountCard as DingtalkAccountCard } from '../plugin-src/client/channels/dingtalk/index.js';
import {
BotCard as FeishuBotCard,
FeishuSettingsTab,
} from '../plugin-src/client/channels/feishu/index.js';
import { AccountCard as WeixinAccountCard } from '../plugin-src/client/channels/weixin/index.js';
const STYLES_URL = new URL('../plugin-src/client/styles.js', import.meta.url);
const FEISHU_STYLES_URL = new URL(
'../plugin-src/client/channels/feishu/styles.js',
import.meta.url,
);
const CLIENT_BUNDLE_URL = new URL('../lib/client.js', import.meta.url);
const DINGTALK_CLIENT_SOURCE_URL = new URL(
'../plugin-src/client/channels/dingtalk/index.js',
@ -24,6 +34,7 @@ test('IM settings renders three compact logo channel tabs without enable switche
assert.match(markup, /IM机器人/);
assert.match(markup, /通过扫码把机器人接入 DeepSeek Harness/);
assert.doesNotMatch(markup, /\d+ 个渠道|dim-channelCount/);
assert.match(markup, />微信</);
assert.match(markup, />飞书</);
assert.match(markup, />钉钉</);
@ -49,6 +60,101 @@ test('the DingTalk QR card stacks within the narrow combined-channel panel', asy
assert.match(styles, /\.dim-panel \.ddt-qrCopy \{ width: 100%; min-width: 0; overflow-wrap: anywhere; \}/);
});
test('Feishu bot cards place the application identifier under the bot name', async () => {
const styles = await readFile(FEISHU_STYLES_URL, 'utf8');
const markup = renderToStaticMarkup(React.createElement(FeishuBotCard, {
connection: {
botId: 'bot-feishu-card',
state: 'connected',
connected: true,
bot: {
name: '今天是牢梁',
appIdMasked: 'cli_aaf4••••1234',
domain: 'feishu',
avatarUrl: 'https://example.com/custom-bot-avatar.png',
},
health: {
summary: '长连接运行正常',
lastCheckedAt: '2026-08-15T07:30:49.000Z',
},
},
onReconnect() {},
onRequestRemove() {},
onConfirmRemove() {},
onCancelRemove() {},
}));
assert.match(markup, /<h3[^>]*>今天是牢梁<\/h3><p[^>]*>cli_aaf4••••1234<\/p>/);
assert.match(markup, /data-im-channel-logo="feishu"/);
assert.doesNotMatch(markup, /custom-bot-avatar/);
assert.equal((markup.match(/class="bxf-metric"/g) ?? []).length, 2);
assert.match(markup, />消息通道<[^]*>最近检查</);
assert.doesNotMatch(markup, />应用标识<|>飞书机器人</);
assert.match(styles, /\.bxf-statusGrid \{[^}]*grid-template-columns: repeat\(2, minmax\(0, 1fr\)\)/);
});
test('Feishu keeps its three heading controls on one row without a plus icon', async () => {
const styles = await readFile(FEISHU_STYLES_URL, 'utf8');
const markup = renderToStaticMarkup(React.createElement(FeishuSettingsTab, {
rpcCall: async () => ({ ok: true, value: {} }),
}));
assert.match(markup, /class="bxf-button bxf-bindButton"[^>]*><span>扫码绑定机器人<\/span><\/button>/);
assert.doesNotMatch(markup, />添加机器人</);
assert.match(styles, /\.bxf-headingTools \{[^}]*flex-wrap: nowrap;/);
assert.match(styles, /@container \(max-width: 620px\)[^]*\.bxf-headingTools \{ gap: 6px; \}/);
assert.doesNotMatch(styles, /\.bxf-headingTools \.bxf-button \{ margin-left: auto; \}/);
});
test('bot cards reuse the same channel brand logos as the channel rail', () => {
const railMarkup = renderToStaticMarkup(React.createElement(IMSettingsTab, {
feishuRpcCall: async () => ({ ok: true, value: {} }),
weixinRpcCall: async () => ({ ok: true, value: {} }),
dingtalkRpcCall: async () => ({ ok: true, value: {} }),
}));
const accountMarkup = renderToStaticMarkup(React.createElement(WeixinAccountCard, {
account: {
botId: 'bot-weixin-card',
state: 'connected',
connected: true,
bot: { name: '微信机器人', accountIdMasked: 'wxid••••1234' },
stats: { messagesReceived: 2, messagesReplied: 2 },
health: { summary: '长轮询运行正常', lastCheckedAt: '2026-08-15T07:30:49.000Z' },
},
onReconnect() {},
onRequestRemove() {},
onConfirmRemove() {},
onCancelRemove() {},
}));
assert.match(railMarkup, /data-im-channel-logo="weixin"/);
assert.match(railMarkup, /data-im-channel-logo="feishu"/);
assert.match(accountMarkup, /class="dxw-avatar"[^]*data-im-channel-logo="weixin"/);
assert.equal((accountMarkup.match(/class="dxw-metric"/g) ?? []).length, 2);
assert.doesNotMatch(accountMarkup, /收到 \/ 回复/);
});
test('DingTalk bot cards omit the redundant received and replied metric', () => {
const markup = renderToStaticMarkup(React.createElement(DingtalkAccountCard, {
account: {
botId: 'bot-dingtalk-card',
state: 'connected',
connected: true,
bot: { name: '钉钉机器人', clientIdMasked: 'ding••••oioy' },
stats: { messagesReceived: 2, messagesReplied: 2 },
health: { summary: 'Stream 长连接运行正常', lastCheckedAt: '2026-08-15T07:30:49.000Z' },
},
onReconnect() {},
onRequestRemove() {},
onConfirmRemove() {},
onCancelRemove() {},
}));
assert.equal((markup.match(/class="ddt-metric"/g) ?? []).length, 2);
assert.match(markup, />消息通道<[^]*>最近检查</);
assert.doesNotMatch(markup, /收到 \/ 回复/);
});
test('the bundled DingTalk channel has no local sender approval workflow', async () => {
const [source, bundle] = await Promise.all([
readFile(DINGTALK_CLIENT_SOURCE_URL, 'utf8'),