From 2803bbcbabdc552e18e403a45f78fcb17ccd23ca Mon Sep 17 00:00:00 2001 From: xmanrui <841206367@qq.com> Date: Sun, 23 Aug 2026 01:20:58 +0800 Subject: [PATCH] feat(ui): compact bot connection metadata --- lib/client.js | 867 +++++++++--------- plugin-src/client/channel-card-meta.js | 48 + plugin-src/client/channels/dingtalk/index.js | 23 +- plugin-src/client/channels/dingtalk/styles.js | 6 - plugin-src/client/channels/feishu/index.js | 27 +- plugin-src/client/channels/feishu/styles.js | 5 - plugin-src/client/channels/qq/index.js | 21 +- .../client/channels/shared/token-channel.js | 24 +- plugin-src/client/channels/wecom/index.js | 21 +- plugin-src/client/channels/weixin/index.js | 23 +- plugin-src/client/channels/weixin/styles.js | 5 - plugin-src/client/channels/whatsapp/index.js | 25 +- plugin-src/client/i18n.js | 1 + plugin-src/client/styles.js | 15 +- test/channels/dingtalk/client-ui.test.mjs | 11 +- test/channels/discord/client-ui.test.mjs | 3 +- test/channels/qq/client-ui.test.mjs | 6 +- test/channels/slack/client-ui.test.mjs | 3 +- test/channels/telegram/client-ui.test.mjs | 3 +- test/channels/wecom/client-ui.test.mjs | 6 +- test/channels/whatsapp/client-ui.test.mjs | 4 +- test/client-ui.test.mjs | 41 +- 22 files changed, 632 insertions(+), 556 deletions(-) create mode 100644 plugin-src/client/channel-card-meta.js diff --git a/lib/client.js b/lib/client.js index 6d5b722..f387b7a 100644 --- a/lib/client.js +++ b/lib/client.js @@ -40,7 +40,7 @@ __export(index_exports, { name: () => name }); module.exports = __toCommonJS(index_exports); -var React19 = __toESM(require("react"), 1); +var React20 = __toESM(require("react"), 1); // plugin-src/client/channel-logos.js var React = __toESM(require("react"), 1); @@ -297,6 +297,7 @@ var EN = Object.freeze({ "\u79BB\u7EBF": "Offline", "\u5DF2\u65AD\u5F00": "Disconnected", "\u6D88\u606F\u901A\u9053": "Message channel", + "\u67E5\u770B\u6D88\u606F\u901A\u9053\u8BF4\u660E": "View message channel details", "\u6700\u8FD1\u68C0\u67E5": "Last checked", "\u5F53\u524D\u5DE5\u4F5C\u533A": "Current workspace", "\u9009\u62E9\u76EE\u5F55": "Choose folder", @@ -1187,7 +1188,7 @@ function formatRemaining(milliseconds) { } // plugin-src/client/channels/dingtalk/index.js -var React8 = __toESM(require("react"), 1); +var React9 = __toESM(require("react"), 1); // plugin-src/client/credential-binding.js var React4 = __toESM(require("react"), 1); @@ -1683,6 +1684,73 @@ function useWorkspaceSnapshotFence() { }), []); } +// plugin-src/client/channel-card-meta.js +var React8 = __toESM(require("react"), 1); +function ChannelListHeading({ className = "", id: id5, title, connectionLabel }) { + const helpId = React8.useId(); + return h2( + "div", + { className: `${className} dim-listHeading`.trim() }, + h2( + "div", + { className: "dim-listTitle" }, + h2("h3", id5 ? { id: id5 } : null, title), + h2( + "span", + { className: "dim-channelHelp" }, + h2("button", { + type: "button", + className: "dim-channelHelpButton", + "aria-label": "\u67E5\u770B\u6D88\u606F\u901A\u9053\u8BF4\u660E", + "aria-describedby": helpId + }, h2("span", { "aria-hidden": "true" }, "?")), + h2( + "span", + { + id: helpId, + className: "dim-channelTooltip", + role: "tooltip" + }, + h2("span", null, "\u6D88\u606F\u901A\u9053"), + h2("strong", null, connectionLabel) + ) + ) + ) + ); +} +function BotStatusMeta({ + className = "", + dotClassName = "", + tone, + stateLabel: stateLabel2, + lastCheckedAt, + formatCheckedTime: formatCheckedTime2, + healthState +}) { + return h2( + "div", + { className: "dim-botHealthGroup" }, + h2( + "div", + { + className: `${className} dim-botHealth`.trim(), + ...healthState ? { "data-health": healthState } : {} + }, + h2("span", { + className: `${dotClassName} dim-healthDot`.trim(), + "data-tone": tone + }), + h2("span", null, stateLabel2) + ), + h2( + "div", + { className: "dim-lastChecked" }, + h2("span", null, "\u6700\u8FD1\u68C0\u67E5"), + h2("span", null, formatCheckedTime2(lastCheckedAt)) + ) + ); +} + // plugin-src/client/channels/dingtalk/styles.js var DINGTALK_STYLE_ID = "xmanrui-dsh-dingtalk-settings"; var CSS = String.raw` @@ -1771,10 +1839,6 @@ var 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; } @@ -1795,8 +1859,6 @@ var 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; } @@ -1841,7 +1903,7 @@ function DingtalkIcon({ size = 28 }) { 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" })); } -var Button = React8.forwardRef(function Button2({ children, kind = "secondary", className = "", ...props }, ref) { +var Button = React9.forwardRef(function Button2({ children, kind = "secondary", className = "", ...props }, ref) { return h2("button", { ...props, ref, @@ -1937,13 +1999,13 @@ function EmptyView({ busy, onStart }) { ); } function QrPanel({ provision, now, busy, onRefresh, onCancel }) { - const [imageFailed, setImageFailed] = React8.useState(false); + const [imageFailed, setImageFailed] = React9.useState(false); const source = safeQrSource(provision.qrCodeDataUrl); const remaining = Math.max(0, provision.expiresAt - now); const expired = remaining === 0 || provision.status === "expired"; const duration = Math.max(1, provision.durationMs ?? 10 * 6e4); const progress = Math.round(Math.min(1, remaining / duration) * 100); - React8.useEffect(() => setImageFailed(false), [source]); + React9.useEffect(() => setImageFailed(false), [source]); return h2( "div", { className: "ddt-card dim-surfaceCard" }, @@ -2060,8 +2122,8 @@ function checkedTime(value) { } } function RemoveConfirmation({ account, busy, onConfirm, onCancel }) { - const cancelRef = React8.useRef(null); - React8.useEffect(() => cancelRef.current?.focus(), []); + const cancelRef = React9.useRef(null); + React9.useEffect(() => cancelRef.current?.focus(), []); return h2( "div", { @@ -2122,28 +2184,14 @@ function AccountCard({ h2("p", { title: account.bot.clientIdMasked }, account.bot.clientIdMasked) ) ), - h2( - "div", - { className: "ddt-health dim-botHealth" }, - h2("span", { className: "ddt-dot dim-healthDot", "data-tone": tone }), - h2("span", null, stateLabel2) - ) - ), - h2( - "dl", - { className: "ddt-metrics dim-botMetrics" }, - h2( - "div", - { className: "ddt-metric dim-botMetric" }, - h2("dt", null, "\u6D88\u606F\u901A\u9053"), - h2("dd", null, account.connected ? "Stream \u957F\u8FDE\u63A5" : "\u79BB\u7EBF") - ), - h2( - "div", - { className: "ddt-metric dim-botMetric" }, - h2("dt", null, "\u6700\u8FD1\u68C0\u67E5"), - h2("dd", null, checkedTime(account.health.lastCheckedAt)) - ) + h2(BotStatusMeta, { + className: "ddt-health", + dotClassName: "ddt-dot", + tone, + stateLabel: stateLabel2, + lastCheckedAt: account.health.lastCheckedAt, + formatCheckedTime: checkedTime + }) ), h2(WorkspaceEditor, { workspace: account.workspace, @@ -2195,11 +2243,11 @@ function AccountList(props) { return h2( "section", { className: "dim-listSection" }, - h2( - "div", - { className: "ddt-listHeading dim-listHeading" }, - h2("h3", null, "\u5DF2\u63A5\u5165\u7684\u9489\u9489\u673A\u5668\u4EBA") - ), + h2(ChannelListHeading, { + className: "ddt-listHeading", + title: "\u5DF2\u63A5\u5165\u7684\u9489\u9489\u673A\u5668\u4EBA", + connectionLabel: "Stream \u957F\u8FDE\u63A5" + }), h2("ul", { className: "ddt-list dim-botList" }, props.bots.map((account) => h2( "li", { key: account.botId }, @@ -2220,7 +2268,7 @@ function AccountList(props) { } var EMPTY_TOTALS = Object.freeze({ configured: 0, connected: 0 }); function DingtalkSettingsTab({ rpcCall }) { - const [model, setModel] = React8.useState({ + const [model, setModel] = React9.useState({ phase: "loading", bots: [], totals: EMPTY_TOTALS, @@ -2228,22 +2276,22 @@ function DingtalkSettingsTab({ rpcCall }) { error: null, agentPresetCatalog: EMPTY_AGENT_PRESET_CATALOG }); - const [provision, setProvision] = React8.useState(null); - const [busy, setBusy] = React8.useState(false); - const [busyByBot, setBusyByBot] = React8.useState({}); - const [feedbackByBot, setFeedbackByBot] = React8.useState({}); - const [removeTarget, setRemoveTarget] = React8.useState(null); - const [credentialOpen, setCredentialOpen] = React8.useState(false); - const [credentialError, setCredentialError] = React8.useState(null); - const [notice, setNotice] = React8.useState(""); - const [now, setNow] = React8.useState(() => Date.now()); - const addButtonRef = React8.useRef(null); - const mountedRef = React8.useRef(true); - const statusRequestRef = React8.useRef(0); + const [provision, setProvision] = React9.useState(null); + const [busy, setBusy] = React9.useState(false); + const [busyByBot, setBusyByBot] = React9.useState({}); + const [feedbackByBot, setFeedbackByBot] = React9.useState({}); + const [removeTarget, setRemoveTarget] = React9.useState(null); + const [credentialOpen, setCredentialOpen] = React9.useState(false); + const [credentialError, setCredentialError] = React9.useState(null); + const [notice, setNotice] = React9.useState(""); + const [now, setNow] = React9.useState(() => Date.now()); + const addButtonRef = React9.useRef(null); + const mountedRef = React9.useRef(true); + const statusRequestRef = React9.useRef(0); const workspaceFence = useWorkspaceSnapshotFence(); - const noticeFrameRef = React8.useRef(null); - const focusFrameRef = React8.useRef(null); - React8.useEffect(() => { + const noticeFrameRef = React9.useRef(null); + const focusFrameRef = React9.useRef(null); + React9.useEffect(() => { mountedRef.current = true; return () => { mountedRef.current = false; @@ -2258,8 +2306,8 @@ function DingtalkSettingsTab({ rpcCall }) { } }; }, []); - React8.useEffect(() => installDingtalkStyles(), []); - const announce = React8.useCallback((message) => { + React9.useEffect(() => installDingtalkStyles(), []); + const announce = React9.useCallback((message) => { if (!mountedRef.current) return; if (noticeFrameRef.current !== null) { window.cancelAnimationFrame(noticeFrameRef.current); @@ -2273,7 +2321,7 @@ function DingtalkSettingsTab({ rpcCall }) { }); } }, []); - const discardStaleFeedback = React8.useCallback((snapshot) => { + const discardStaleFeedback = React9.useCallback((snapshot) => { const botsById = new Map(snapshot.bots.map((bot) => [bot.botId, bot])); setFeedbackByBot((current) => { let changed = false; @@ -2288,7 +2336,7 @@ function DingtalkSettingsTab({ rpcCall }) { return changed ? next : current; }); }, []); - const focusAddButton = React8.useCallback(() => { + const focusAddButton = React9.useCallback(() => { if (!mountedRef.current) return; if (focusFrameRef.current !== null) window.cancelAnimationFrame(focusFrameRef.current); focusFrameRef.current = window.requestAnimationFrame(() => { @@ -2296,11 +2344,11 @@ function DingtalkSettingsTab({ rpcCall }) { if (mountedRef.current) addButtonRef.current?.focus(); }); }, []); - const invoke = React8.useCallback(async (endpoint, payload = {}, signal) => { + const invoke = React9.useCallback(async (endpoint, payload = {}, signal) => { if (typeof rpcCall !== "function") throw new TypeError("\u9489\u9489\u8BBE\u7F6E\u9875\u7F3A\u5C11 RPC \u8FDE\u63A5"); return unwrapRpcResult(await rpcCall(endpoint, payload, signal)); }, [rpcCall]); - const loadStatus = React8.useCallback(async ({ + const loadStatus = React9.useCallback(async ({ signal, silent = false, restoreProvisioning = false @@ -2344,12 +2392,12 @@ function DingtalkSettingsTab({ rpcCall }) { return void 0; } }, [discardStaleFeedback, invoke, workspaceFence]); - React8.useEffect(() => { + React9.useEffect(() => { const controller = new AbortController(); void loadStatus({ signal: controller.signal, restoreProvisioning: true }); return () => controller.abort(); }, [loadStatus]); - React8.useEffect(() => { + React9.useEffect(() => { if (model.phase !== "ready") return void 0; const controller = new AbortController(); let running = false; @@ -2368,14 +2416,14 @@ function DingtalkSettingsTab({ rpcCall }) { window.clearInterval(timer); }; }, [loadStatus, model.phase]); - React8.useEffect(() => { + React9.useEffect(() => { if (!provision || !ACTIVE_PROVISION_STATES.has(provision.status)) return void 0; const timer = window.setInterval(() => { if (mountedRef.current) setNow(Date.now()); }, 1e3); return () => window.clearInterval(timer); }, [provision?.attemptId, provision?.status]); - const startProvisioning = React8.useCallback(async ({ replace = false } = {}) => { + const startProvisioning = React9.useCallback(async ({ replace = false } = {}) => { if (!mountedRef.current) return; setCredentialOpen(false); setCredentialError(null); @@ -2413,7 +2461,7 @@ function DingtalkSettingsTab({ rpcCall }) { if (mountedRef.current) setBusy(false); } }, [announce, invoke, provision?.attemptId]); - const bindCredentials = React8.useCallback(async ({ identity, secret }) => { + const bindCredentials = React9.useCallback(async ({ identity, secret }) => { if (!mountedRef.current) return; const snapshotVersion = workspaceFence.beginMutation(); setBusy(true); @@ -2445,7 +2493,7 @@ function DingtalkSettingsTab({ rpcCall }) { if (mountedRef.current) setBusy(false); } }, [announce, discardStaleFeedback, invoke, loadStatus, workspaceFence]); - const cancelProvisioning = React8.useCallback(async () => { + const cancelProvisioning = React9.useCallback(async () => { if (!mountedRef.current) return; setBusy(true); try { @@ -2463,7 +2511,7 @@ function DingtalkSettingsTab({ rpcCall }) { if (mountedRef.current) setBusy(false); } }, [announce, focusAddButton, invoke, provision?.attemptId, provision?.status]); - React8.useEffect(() => { + React9.useEffect(() => { const attemptId = provision?.attemptId; if (!attemptId || !ACTIVE_PROVISION_STATES.has(provision.status)) return void 0; const controller = new AbortController(); @@ -2522,7 +2570,7 @@ function DingtalkSettingsTab({ rpcCall }) { timer = null; }; }, [announce, invoke, loadStatus, provision?.attemptId, provision?.pollIntervalMs, provision?.status]); - const setBotBusy = React8.useCallback((botId, operation) => { + const setBotBusy = React9.useCallback((botId, operation) => { if (!mountedRef.current) return; setBusyByBot((current) => { const next = { ...current }; @@ -2531,7 +2579,7 @@ function DingtalkSettingsTab({ rpcCall }) { return next; }); }, []); - const runBotAction = React8.useCallback(async ({ account, operation, endpoint, payload, success }) => { + const runBotAction = React9.useCallback(async ({ account, operation, endpoint, payload, success }) => { if (!mountedRef.current) return void 0; const snapshotVersion = workspaceFence.beginMutation(); setBotBusy(account.botId, operation); @@ -2585,7 +2633,7 @@ function DingtalkSettingsTab({ rpcCall }) { if (mountedRef.current) setBotBusy(account.botId, null); } }, [announce, discardStaleFeedback, invoke, loadStatus, setBotBusy, workspaceFence]); - const reconnect = React8.useCallback((account) => runBotAction({ + const reconnect = React9.useCallback((account) => runBotAction({ account, operation: "reconnect", endpoint: DINGTALK_ENDPOINTS.reconnectBot, @@ -2596,7 +2644,7 @@ function DingtalkSettingsTab({ rpcCall }) { return connectionTestFeedback(snapshot.testMessage) ?? "\u9489\u9489\u8FDE\u63A5\u68C0\u67E5\u5B8C\u6210\u3002"; } }), [runBotAction]); - const saveWorkspace = React8.useCallback(async (account, workspace) => { + const saveWorkspace = React9.useCallback(async (account, workspace) => { const workspaceVersion = workspaceFence.beginMutation(); setBotBusy(account.botId, "workspace"); try { @@ -2621,7 +2669,7 @@ function DingtalkSettingsTab({ rpcCall }) { if (mountedRef.current) setBotBusy(account.botId, null); } }, [discardStaleFeedback, invoke, loadStatus, setBotBusy, workspaceFence]); - const saveAgentPreset = React8.useCallback(async (account, agentPreset) => { + const saveAgentPreset = React9.useCallback(async (account, agentPreset) => { const snapshotVersion = workspaceFence.beginMutation(); setBotBusy(account.botId, "preset"); try { @@ -2646,7 +2694,7 @@ function DingtalkSettingsTab({ rpcCall }) { if (mountedRef.current) setBotBusy(account.botId, null); } }, [discardStaleFeedback, invoke, loadStatus, setBotBusy, workspaceFence]); - const remove = React8.useCallback(async (account) => { + const remove = React9.useCallback(async (account) => { const snapshot = await runBotAction({ account, operation: "delete", @@ -2730,7 +2778,7 @@ function DingtalkSettingsTab({ rpcCall }) { h2(Button, { onClick: () => void loadStatus() }, "\u91CD\u65B0\u8BFB\u53D6") ) ) : h2( - React8.Fragment, + React9.Fragment, null, credentialView, provisionView, @@ -2849,8 +2897,8 @@ var normalizeSnapshot2 = api.normalizeSnapshot; var presentError2 = api.presentError; // plugin-src/client/channels/shared/token-channel.js -var React9 = __toESM(require("react"), 1); -var Button3 = React9.forwardRef(function Button4({ children, kind = "secondary", className = "", ...props }, ref) { +var React10 = __toESM(require("react"), 1); +var Button3 = React10.forwardRef(function Button4({ children, kind = "secondary", className = "", ...props }, ref) { return h2("button", { ...props, ref, @@ -2932,28 +2980,14 @@ function createTokenChannelSettings(definition) { h2("p", null, identity) ) ), - h2( - "div", - { className: "ddt-health dim-botHealth" }, - h2("span", { className: "ddt-dot dim-healthDot", "data-tone": tone }), - h2("span", null, stateLabel2) - ) - ), - h2( - "dl", - { className: "ddt-metrics dim-botMetrics" }, - h2( - "div", - { className: "ddt-metric dim-botMetric" }, - h2("dt", null, "\u6D88\u606F\u901A\u9053"), - h2("dd", null, account.connected ? connectionLabel : "\u79BB\u7EBF") - ), - h2( - "div", - { className: "ddt-metric dim-botMetric" }, - h2("dt", null, "\u6700\u8FD1\u68C0\u67E5"), - h2("dd", null, checkedTime2(account.health.lastCheckedAt)) - ) + h2(BotStatusMeta, { + className: "ddt-health", + dotClassName: "ddt-dot", + tone, + stateLabel: stateLabel2, + lastCheckedAt: account.health.lastCheckedAt, + formatCheckedTime: checkedTime2 + }) ), h2(WorkspaceEditor, { workspace: account.workspace, @@ -3018,22 +3052,22 @@ function createTokenChannelSettings(definition) { ); } function SettingsTab({ rpcCall }) { - const [model, setModel] = React9.useState({ + const [model, setModel] = React10.useState({ phase: "loading", bots: [], totals: { configured: 0, connected: 0 }, error: null, agentPresetCatalog: EMPTY_AGENT_PRESET_CATALOG }); - const [credentialOpen, setCredentialOpen] = React9.useState(false); - const [credentialError, setCredentialError] = React9.useState(null); - const [busy, setBusy] = React9.useState(false); - const [busyByBot, setBusyByBot] = React9.useState({}); - const [testNoticeByBot, setTestNoticeByBot] = React9.useState({}); - const [removeTarget, setRemoveTarget] = React9.useState(null); - const mounted = React9.useRef(true); + const [credentialOpen, setCredentialOpen] = React10.useState(false); + const [credentialError, setCredentialError] = React10.useState(null); + const [busy, setBusy] = React10.useState(false); + const [busyByBot, setBusyByBot] = React10.useState({}); + const [testNoticeByBot, setTestNoticeByBot] = React10.useState({}); + const [removeTarget, setRemoveTarget] = React10.useState(null); + const mounted = React10.useRef(true); const workspaceFence = useWorkspaceSnapshotFence(); - React9.useEffect(() => { + React10.useEffect(() => { const disposeDingtalk = installDingtalkStyles(); const disposeChannel = installStyles(); mounted.current = true; @@ -3043,11 +3077,11 @@ function createTokenChannelSettings(definition) { disposeDingtalk(); }; }, []); - const invoke = React9.useCallback(async (endpoint, payload = {}, signal) => { + const invoke = React10.useCallback(async (endpoint, payload = {}, signal) => { if (typeof rpcCall !== "function") throw new TypeError(`${channel4} \u8BBE\u7F6E\u9875\u7F3A\u5C11 RPC \u8FDE\u63A5`); return api4.unwrapRpcResult(await rpcCall(endpoint, payload, signal)); }, [rpcCall]); - const loadStatus = React9.useCallback(async ({ signal, silent = false } = {}) => { + const loadStatus = React10.useCallback(async ({ signal, silent = false } = {}) => { const workspaceVersion = workspaceFence.beginStatus(); if (workspaceVersion === null) return; if (!silent && mounted.current) setModel((current) => ({ ...current, phase: "loading", error: null })); @@ -3071,12 +3105,12 @@ function createTokenChannelSettings(definition) { } } }, [invoke, workspaceFence]); - React9.useEffect(() => { + React10.useEffect(() => { const controller = new AbortController(); void loadStatus({ signal: controller.signal }); return () => controller.abort(); }, [loadStatus]); - React9.useEffect(() => { + React10.useEffect(() => { if (model.phase !== "ready") return void 0; const controller = new AbortController(); const timer = window.setInterval( @@ -3088,7 +3122,7 @@ function createTokenChannelSettings(definition) { window.clearInterval(timer); }; }, [loadStatus, model.phase]); - const bindCredentials = React9.useCallback(async (values) => { + const bindCredentials = React10.useCallback(async (values) => { const snapshotVersion = workspaceFence.beginMutation(); setBusy(true); setCredentialError(null); @@ -3116,7 +3150,7 @@ function createTokenChannelSettings(definition) { if (mounted.current) setBusy(false); } }, [invoke, loadStatus, workspaceFence]); - const botAction = React9.useCallback(async (account, operation, endpoint, payload) => { + const botAction = React10.useCallback(async (account, operation, endpoint, payload) => { const snapshotVersion = workspaceFence.beginMutation(); setBusyByBot((current) => ({ ...current, [account.botId]: operation })); try { @@ -3158,11 +3192,11 @@ function createTokenChannelSettings(definition) { const botList = model.bots.length > 0 ? h2( "section", { className: "dim-listSection" }, - h2( - "div", - { className: "ddt-listHeading dim-listHeading" }, - h2("h3", null, `\u5DF2\u63A5\u5165\u7684 ${channel4} \u673A\u5668\u4EBA`) - ), + h2(ChannelListHeading, { + className: "ddt-listHeading", + title: `\u5DF2\u63A5\u5165\u7684 ${channel4} \u673A\u5668\u4EBA`, + connectionLabel + }), h2("ul", { className: "ddt-list dim-botList" }, model.bots.map((account) => h2("li", { key: account.botId }, h2(AccountCard5, { account, busy: busyByBot[account.botId], @@ -3253,7 +3287,7 @@ function createTokenChannelSettings(definition) { h2(Button3, { onClick: () => void loadStatus() }, "\u91CD\u65B0\u8BFB\u53D6") ) ) : h2( - React9.Fragment, + React10.Fragment, null, credentialOpen ? CredentialPanel ? h2(CredentialPanel, { busy, @@ -3354,7 +3388,7 @@ var DiscordSettingsTab = channel.SettingsTab; var DiscordAccountCard = channel.AccountCard; // plugin-src/client/channels/feishu/index.js -var React11 = __toESM(require("react"), 1); +var React12 = __toESM(require("react"), 1); // plugin-src/client/channels/feishu/api.js var FEISHU_RPC_CHANNEL = "/feishu"; @@ -3612,7 +3646,7 @@ function formatRemaining2(milliseconds) { } // plugin-src/client/lifecycle.js -var React10 = __toESM(require("react"), 1); +var React11 = __toESM(require("react"), 1); function createPollScheduler({ setTimeoutFn, clearTimeoutFn }) { let disposed = false; let timer; @@ -3674,8 +3708,8 @@ function createAnimationFrameScheduler({ requestFrame, cancelFrame }) { }; } function useAnimationFrameScheduler() { - const schedulerRef = React10.useRef(null); - React10.useEffect(() => { + const schedulerRef = React11.useRef(null); + React11.useEffect(() => { const scheduler = createAnimationFrameScheduler({ requestFrame: (callback) => window.requestAnimationFrame(callback), cancelFrame: (frame) => window.cancelAnimationFrame(frame) @@ -3686,7 +3720,7 @@ function useAnimationFrameScheduler() { if (schedulerRef.current === scheduler) schedulerRef.current = null; }; }, []); - return React10.useCallback( + return React11.useCallback( (callback, key) => schedulerRef.current?.schedule(callback, key) ?? false, [] ); @@ -4096,10 +4130,6 @@ var CSS3 = 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; } @@ -4211,7 +4241,6 @@ var CSS3 = 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; } @@ -4313,7 +4342,7 @@ function QrIcon({ size = 58 }) { fill: "currentColor" })); } -var Button5 = React11.forwardRef(function Button6({ children, kind = "secondary", size, icon, className = "", ...props }, ref) { +var Button5 = React12.forwardRef(function Button6({ children, kind = "secondary", size, icon, className = "", ...props }, ref) { return h2("button", { ...props, ref, @@ -4430,7 +4459,7 @@ function safeQrSource2(value) { return /^data:image\/(?:png|webp|svg\+xml)(?:;charset=[^;,]+)?;base64,/i.test(value) ? value : void 0; } function QrPane({ provision, now, onRefresh, onCancel, busy }) { - const [imageFailed, setImageFailed] = React11.useState(false); + const [imageFailed, setImageFailed] = React12.useState(false); const qrSource = safeQrSource2(provision.qrCodeDataUrl); const href = safeVerificationHref(provision.verificationUrl); const remaining = Math.max(0, provision.expiresAt - now); @@ -4439,7 +4468,7 @@ function QrPane({ provision, now, onRefresh, onCancel, busy }) { const repairing = isCallbackRepair(provision); const grantingGroupMessages = isGroupMessagePermission(provision); const botName = provision.botName ?? "\u6B64\u673A\u5668\u4EBA"; - React11.useEffect(() => setImageFailed(false), [qrSource]); + React12.useEffect(() => setImageFailed(false), [qrSource]); return h2( "div", { className: "bxf-card bxf-provisionCard dim-surfaceCard" }, @@ -4602,11 +4631,11 @@ function connectionTestNotice2(value) { return value?.testMessage ? "\u8FDE\u63A5\u68C0\u67E5\u5B8C\u6210\uFF0C\u4F46\u6D4B\u8BD5\u6D88\u606F\u53D1\u9001\u5931\u8D25\u3002" : null; } function RemoveConfirmation2({ bot, busy, onConfirm, onCancel }) { - const cancelRef = React11.useRef(null); + const cancelRef = React12.useRef(null); const idPart = bot.botId.replace(/[^a-zA-Z0-9_-]/g, "-"); const titleId = `bxf-remove-title-${idPart}`; const descriptionId = `bxf-remove-description-${idPart}`; - React11.useEffect(() => cancelRef.current?.focus(), []); + React12.useEffect(() => cancelRef.current?.focus(), []); return h2( "div", { @@ -4648,9 +4677,9 @@ function GroupResponseModeEditor({ onAuthorize }) { const current = normalizeGroupResponseMode(value); - const [saving, setSaving] = React11.useState(false); - const [authorizing, setAuthorizing] = React11.useState(false); - const [error, setError] = React11.useState(null); + const [saving, setSaving] = React12.useState(false); + const [authorizing, setAuthorizing] = React12.useState(false); + const [error, setError] = React12.useState(null); const change = async (event) => { const next = normalizeGroupResponseMode(event.target.value); if (next === current || saving || disabled) return; @@ -4784,28 +4813,15 @@ function BotCard({ h2("p", { title: bot.appIdMasked }, bot.appIdMasked ?? "\u5E94\u7528\u6807\u8BC6\u5DF2\u5B89\u5168\u4FDD\u5B58") ) ), - h2( - "div", - { className: "bxf-healthPill dim-botHealth", "data-health": stateForDisplay }, - h2("span", { className: "bxf-dot dim-healthDot", "data-tone": tone }), - h2("span", null, HEALTH_LABELS[stateForDisplay] ?? "\u72B6\u6001\u672A\u77E5") - ) - ), - h2( - "dl", - { className: "bxf-statusGrid dim-botMetrics" }, - h2( - "div", - { className: "bxf-metric dim-botMetric" }, - h2("dt", null, "\u6D88\u606F\u901A\u9053"), - h2("dd", null, connected ? "\u957F\u8FDE\u63A5" : stateForDisplay === "connecting" ? "\u8FDE\u63A5\u4E2D" : "\u5DF2\u65AD\u5F00") - ), - h2( - "div", - { className: "bxf-metric dim-botMetric" }, - h2("dt", null, "\u6700\u8FD1\u68C0\u67E5"), - h2("dd", null, formatCheckedTime(health.lastCheckedAt)) - ) + h2(BotStatusMeta, { + className: "bxf-healthPill", + dotClassName: "bxf-dot", + tone, + stateLabel: HEALTH_LABELS[stateForDisplay] ?? "\u72B6\u6001\u672A\u77E5", + lastCheckedAt: health.lastCheckedAt, + formatCheckedTime, + healthState: stateForDisplay + }) ), h2(WorkspaceEditor, { workspace: connection.workspace, @@ -4888,11 +4904,12 @@ function BotList(props) { return h2( "section", { className: "bxf-listSection dim-listSection", "aria-labelledby": "bxf-bot-list-title" }, - h2( - "div", - { className: "bxf-listHeading dim-listHeading" }, - h2("h3", { id: "bxf-bot-list-title" }, "\u5DF2\u63A5\u5165\u7684\u673A\u5668\u4EBA") - ), + h2(ChannelListHeading, { + className: "bxf-listHeading", + id: "bxf-bot-list-title", + title: "\u5DF2\u63A5\u5165\u7684\u673A\u5668\u4EBA", + connectionLabel: "\u957F\u8FDE\u63A5" + }), h2( "ul", { className: "bxf-botList dim-botList", role: "list" }, @@ -4976,7 +4993,7 @@ function mergeFeishuSnapshotState(current, snapshot, { restoreProvisioning = fal }; } function FeishuSettingsTab({ rpcCall }) { - const [model, setModel] = React11.useState({ + const [model, setModel] = React12.useState({ phase: "loading", revision: 0, bots: [], @@ -4986,41 +5003,41 @@ function FeishuSettingsTab({ rpcCall }) { statusError: null, agentPresetCatalog: EMPTY_AGENT_PRESET_CATALOG }); - const [pageBusy, setPageBusy] = React11.useState(false); - const [provisionBusy, setProvisionBusy] = React11.useState(false); - const [credentialOpen, setCredentialOpen] = React11.useState(false); - const [credentialBusy, setCredentialBusy] = React11.useState(false); - const [credentialError, setCredentialError] = React11.useState(null); - const [busyByBot, setBusyByBot] = React11.useState({}); - const [errorsByBot, setErrorsByBot] = React11.useState({}); - const [testNoticesByBot, setTestNoticesByBot] = React11.useState({}); - const [removeTargetId, setRemoveTargetId] = React11.useState(null); - const [announcement, setAnnouncement] = React11.useState(""); - const [now, setNow] = React11.useState(() => Date.now()); - const [focusBotId, setFocusBotId] = React11.useState(null); - const cardRefs = React11.useRef(/* @__PURE__ */ new Map()); - const removeButtonRefs = React11.useRef(/* @__PURE__ */ new Map()); - const targetedProvisionRef = React11.useRef(null); - const addButtonRef = React11.useRef(null); - const mountedRef = React11.useRef(true); + const [pageBusy, setPageBusy] = React12.useState(false); + const [provisionBusy, setProvisionBusy] = React12.useState(false); + const [credentialOpen, setCredentialOpen] = React12.useState(false); + const [credentialBusy, setCredentialBusy] = React12.useState(false); + const [credentialError, setCredentialError] = React12.useState(null); + const [busyByBot, setBusyByBot] = React12.useState({}); + const [errorsByBot, setErrorsByBot] = React12.useState({}); + const [testNoticesByBot, setTestNoticesByBot] = React12.useState({}); + const [removeTargetId, setRemoveTargetId] = React12.useState(null); + const [announcement, setAnnouncement] = React12.useState(""); + const [now, setNow] = React12.useState(() => Date.now()); + const [focusBotId, setFocusBotId] = React12.useState(null); + const cardRefs = React12.useRef(/* @__PURE__ */ new Map()); + const removeButtonRefs = React12.useRef(/* @__PURE__ */ new Map()); + const targetedProvisionRef = React12.useRef(null); + const addButtonRef = React12.useRef(null); + const mountedRef = React12.useRef(true); const workspaceFence = useWorkspaceSnapshotFence(); const scheduleAnimationFrame = useAnimationFrameScheduler(); - React11.useEffect(() => { + React12.useEffect(() => { mountedRef.current = true; return () => { mountedRef.current = false; }; }, []); - const announce = React11.useCallback((message) => { + const announce = React12.useCallback((message) => { setAnnouncement(""); scheduleAnimationFrame(() => { if (message) setAnnouncement(message); }, "announcement"); }, [scheduleAnimationFrame]); - const invoke = React11.useCallback(async (endpoint, payload = {}, signal) => { + const invoke = React12.useCallback(async (endpoint, payload = {}, signal) => { return unwrapRpcResult3(await rpcCall(endpoint, payload, signal)); }, [rpcCall]); - const mergeSnapshot = React11.useCallback((snapshot, { restoreProvisioning = false } = {}) => { + const mergeSnapshot = React12.useCallback((snapshot, { restoreProvisioning = false } = {}) => { const now2 = Date.now(); setModel((current) => mergeFeishuSnapshotState( current, @@ -5028,7 +5045,7 @@ function FeishuSettingsTab({ rpcCall }) { { restoreProvisioning, now: now2 } )); }, []); - const loadStatus = React11.useCallback(async ({ signal, silent = false, restoreProvisioning = false } = {}) => { + const loadStatus = React12.useCallback(async ({ signal, silent = false, restoreProvisioning = false } = {}) => { const workspaceVersion = workspaceFence.beginStatus(); if (workspaceVersion === null || !mountedRef.current) return void 0; if (!silent) setPageBusy(true); @@ -5046,12 +5063,12 @@ function FeishuSettingsTab({ rpcCall }) { if (!silent && !signal?.aborted && mountedRef.current) setPageBusy(false); } }, [invoke, mergeSnapshot, workspaceFence]); - React11.useEffect(() => { + React12.useEffect(() => { const controller = new AbortController(); void loadStatus({ signal: controller.signal, restoreProvisioning: true }); return () => controller.abort(); }, [loadStatus]); - React11.useEffect(() => { + React12.useEffect(() => { if (model.phase !== "ready") return void 0; const controller = new AbortController(); let inFlight = false; @@ -5070,7 +5087,7 @@ function FeishuSettingsTab({ rpcCall }) { window.clearInterval(timer); }; }, [loadStatus, model.phase]); - React11.useEffect(() => { + React12.useEffect(() => { if (!focusBotId) return; const node = cardRefs.current.get(focusBotId); if (!node) return; @@ -5079,7 +5096,7 @@ function FeishuSettingsTab({ rpcCall }) { setFocusBotId(null); }, [focusBotId, model.bots]); const targetedProvisionFocusKey = isTargetedAppUpdate2(model.provisioning) ? `${model.provisioning.botId}:${model.provisioning.attemptId ?? "preparing"}:${model.provisioning.phase}` : null; - React11.useEffect(() => { + React12.useEffect(() => { if (!targetedProvisionFocusKey) return; scheduleAnimationFrame(() => { const node = targetedProvisionRef.current; @@ -5088,7 +5105,7 @@ function FeishuSettingsTab({ rpcCall }) { node.focus?.({ preventScroll: true }); }, "targeted-provision-focus"); }, [scheduleAnimationFrame, targetedProvisionFocusKey]); - const startProvisioning = React11.useCallback(async ({ + const startProvisioning = React12.useCallback(async ({ replace = false, operation = FEISHU_REGISTRATION_OPERATIONS.PROVISION, bot @@ -5164,7 +5181,7 @@ function FeishuSettingsTab({ rpcCall }) { model.provisioning?.botId, model.provisioning?.botName ]); - const bindCredentials = React11.useCallback(async ({ identity, secret }) => { + const bindCredentials = React12.useCallback(async ({ identity, secret }) => { const snapshotVersion = workspaceFence.beginMutation(); setCredentialBusy(true); setCredentialError(null); @@ -5186,7 +5203,7 @@ function FeishuSettingsTab({ rpcCall }) { setCredentialBusy(false); } }, [announce, invoke, loadStatus, mergeSnapshot, workspaceFence]); - const cancelProvisioning = React11.useCallback(async () => { + const cancelProvisioning = React12.useCallback(async () => { const activeProvision = model.provisioning; const attemptId = activeProvision?.attemptId; const repairing = isCallbackRepair(activeProvision); @@ -5251,7 +5268,7 @@ function FeishuSettingsTab({ rpcCall }) { const countdownPhase = model.provisioning?.phase; const countdownExpiresAt = model.provisioning?.expiresAt; const countdownExpired = model.provisioning?.expired; - React11.useEffect(() => { + React12.useEffect(() => { if (!countdownAttemptId || countdownPhase !== "qr" || countdownExpired) return void 0; const tick = () => { const timestamp7 = Date.now(); @@ -5264,7 +5281,7 @@ function FeishuSettingsTab({ rpcCall }) { const timer = window.setInterval(tick, 1e3); return () => window.clearInterval(timer); }, [countdownAttemptId, countdownPhase, countdownExpiresAt, countdownExpired]); - React11.useEffect(() => { + React12.useEffect(() => { const provision2 = model.provisioning; if (!provision2 || !["qr", "connecting"].includes(provision2.phase) || !provision2.attemptId || provision2.expired) return void 0; const controller = new AbortController(); @@ -5332,7 +5349,7 @@ function FeishuSettingsTab({ rpcCall }) { window.clearTimeout(timer); }; }, [announce, invoke, loadStatus, model.provisioning]); - const setBotBusy = React11.useCallback((botId, value) => { + const setBotBusy = React12.useCallback((botId, value) => { setBusyByBot((current) => { const next = { ...current }; if (value) next[botId] = value; @@ -5340,7 +5357,7 @@ function FeishuSettingsTab({ rpcCall }) { return next; }); }, []); - const setBotError = React11.useCallback((botId, error) => { + const setBotError = React12.useCallback((botId, error) => { setErrorsByBot((current) => { const next = { ...current }; if (error) next[botId] = presentError3(error); @@ -5348,7 +5365,7 @@ function FeishuSettingsTab({ rpcCall }) { return next; }); }, []); - const repairCallback = React11.useCallback((connection) => { + const repairCallback = React12.useCallback((connection) => { if (model.provisioning) return; setRemoveTargetId(null); setBotError(connection.botId, null); @@ -5362,7 +5379,7 @@ function FeishuSettingsTab({ rpcCall }) { bot: connection }); }, [model.provisioning, setBotError, startProvisioning]); - const reconnectOneBot = React11.useCallback(async (connection) => { + const reconnectOneBot = React12.useCallback(async (connection) => { const { botId, bot } = connection; const snapshotVersion = workspaceFence.beginMutation(); setBotBusy(botId, "reconnect"); @@ -5402,7 +5419,7 @@ function FeishuSettingsTab({ rpcCall }) { setBotBusy(botId, null); } }, [announce, invoke, loadStatus, mergeSnapshot, setBotBusy, setBotError, workspaceFence]); - const saveWorkspace = React11.useCallback(async (connection, workspace) => { + const saveWorkspace = React12.useCallback(async (connection, workspace) => { const { botId } = connection; const workspaceVersion = workspaceFence.beginMutation(); setBotBusy(botId, "workspace"); @@ -5421,7 +5438,7 @@ function FeishuSettingsTab({ rpcCall }) { if (mountedRef.current) setBotBusy(botId, null); } }, [invoke, loadStatus, mergeSnapshot, setBotBusy, setBotError, workspaceFence]); - const saveAgentPreset = React11.useCallback(async (connection, agentPreset) => { + const saveAgentPreset = React12.useCallback(async (connection, agentPreset) => { const { botId } = connection; const snapshotVersion = workspaceFence.beginMutation(); setBotBusy(botId, "preset"); @@ -5440,7 +5457,7 @@ function FeishuSettingsTab({ rpcCall }) { if (mountedRef.current) setBotBusy(botId, null); } }, [invoke, loadStatus, mergeSnapshot, setBotBusy, setBotError, workspaceFence]); - const authorizeGroupMessages = React11.useCallback(async (connection) => { + const authorizeGroupMessages = React12.useCallback(async (connection) => { const { botId } = connection; if (model.provisioning) { throw new Error("\u8BF7\u5148\u5B8C\u6210\u5F53\u524D\u98DE\u4E66\u6388\u6743\u64CD\u4F5C\uFF0C\u518D\u5F00\u901A\u7FA4\u6D88\u606F\u6743\u9650\u3002"); @@ -5457,7 +5474,7 @@ function FeishuSettingsTab({ rpcCall }) { bot: connection }); }, [model.provisioning, setBotError, startProvisioning]); - const saveGroupResponseMode = React11.useCallback(async (connection, groupResponseMode) => { + const saveGroupResponseMode = React12.useCallback(async (connection, groupResponseMode) => { const { botId } = connection; if (groupResponseMode === "all" && connection.groupMessagePermissionGranted !== true) { await authorizeGroupMessages(connection); @@ -5488,15 +5505,15 @@ function FeishuSettingsTab({ rpcCall }) { setBotError, workspaceFence ]); - const requestRemove = React11.useCallback((connection) => { + const requestRemove = React12.useCallback((connection) => { setRemoveTargetId(connection.botId); }, []); - const cancelRemove = React11.useCallback(() => { + const cancelRemove = React12.useCallback(() => { const botId = removeTargetId; setRemoveTargetId(null); scheduleAnimationFrame(() => removeButtonRefs.current.get(botId)?.focus(), "focus"); }, [removeTargetId, scheduleAnimationFrame]); - const confirmRemove = React11.useCallback(async (connection) => { + const confirmRemove = React12.useCallback(async (connection) => { const { botId, bot } = connection; const snapshotVersion = workspaceFence.beginMutation(); setBotBusy(botId, "delete"); @@ -5582,11 +5599,11 @@ function FeishuSettingsTab({ rpcCall }) { setCredentialError(null); } }) : null; - const setCardRef = React11.useCallback((botId, node) => { + const setCardRef = React12.useCallback((botId, node) => { if (node) cardRefs.current.set(botId, node); else cardRefs.current.delete(botId); }, []); - const setRemoveButtonRef = React11.useCallback((botId, node) => { + const setRemoveButtonRef = React12.useCallback((botId, node) => { if (node) removeButtonRefs.current.set(botId, node); else removeButtonRefs.current.delete(botId); }, []); @@ -5625,7 +5642,7 @@ function FeishuSettingsTab({ rpcCall }) { onRetry: () => void loadStatus(), busy: pageBusy }) : h2( - React11.Fragment, + React12.Fragment, null, credentialContent, targetedProvisioning ? null : provisionContent, @@ -5787,7 +5804,7 @@ function formatRemaining3(milliseconds) { } // plugin-src/client/channels/qq/index.js -var React12 = __toESM(require("react"), 1); +var React13 = __toESM(require("react"), 1); // plugin-src/client/channels/qq/styles.js var QQ_STYLE_ID = "xmanrui-dsh-im-qq-settings"; @@ -5812,7 +5829,7 @@ function installQqStyles() { // plugin-src/client/channels/qq/index.js var ACTIVE_STATES = /* @__PURE__ */ new Set(["pending", "refreshing", "connecting"]); -var Button7 = React12.forwardRef(function Button8({ children, kind = "secondary", className = "", ...props }, ref) { +var Button7 = React13.forwardRef(function Button8({ children, kind = "secondary", className = "", ...props }, ref) { return h2("button", { ...props, ref, @@ -6054,18 +6071,14 @@ function AccountCard2({ h2("p", null, account.bot.appIdMasked) ) ), - h2( - "div", - { className: "ddt-health dim-botHealth" }, - h2("span", { className: "ddt-dot dim-healthDot", "data-tone": tone }), - h2("span", null, stateLabel2) - ) - ), - h2( - "dl", - { className: "ddt-metrics dim-botMetrics" }, - h2("div", { className: "ddt-metric dim-botMetric" }, h2("dt", null, "\u6D88\u606F\u901A\u9053"), h2("dd", null, account.connected ? "WebSocket \u957F\u8FDE\u63A5" : "\u79BB\u7EBF")), - h2("div", { className: "ddt-metric dim-botMetric" }, h2("dt", null, "\u6700\u8FD1\u68C0\u67E5"), h2("dd", null, checkedTime3(account.health.lastCheckedAt))) + h2(BotStatusMeta, { + className: "ddt-health", + dotClassName: "ddt-dot", + tone, + stateLabel: stateLabel2, + lastCheckedAt: account.health.lastCheckedAt, + formatCheckedTime: checkedTime3 + }) ), h2(WorkspaceEditor, { workspace: account.workspace, @@ -6107,25 +6120,25 @@ function AccountCard2({ ); } function QqSettingsTab({ rpcCall }) { - const [model, setModel] = React12.useState({ + const [model, setModel] = React13.useState({ phase: "loading", bots: [], totals: { configured: 0, connected: 0 }, error: null, agentPresetCatalog: EMPTY_AGENT_PRESET_CATALOG }); - const [provision, setProvision] = React12.useState(null); - const [busy, setBusy] = React12.useState(false); - const [busyByBot, setBusyByBot] = React12.useState({}); - const [feedbackByBot, setFeedbackByBot] = React12.useState({}); - const [removeTarget, setRemoveTarget] = React12.useState(null); - const [credentialOpen, setCredentialOpen] = React12.useState(false); - const [credentialError, setCredentialError] = React12.useState(null); - const [now, setNow] = React12.useState(Date.now()); - const mounted = React12.useRef(true); + const [provision, setProvision] = React13.useState(null); + const [busy, setBusy] = React13.useState(false); + const [busyByBot, setBusyByBot] = React13.useState({}); + const [feedbackByBot, setFeedbackByBot] = React13.useState({}); + const [removeTarget, setRemoveTarget] = React13.useState(null); + const [credentialOpen, setCredentialOpen] = React13.useState(false); + const [credentialError, setCredentialError] = React13.useState(null); + const [now, setNow] = React13.useState(Date.now()); + const mounted = React13.useRef(true); const workspaceFence = useWorkspaceSnapshotFence(); - const addButtonRef = React12.useRef(null); - React12.useEffect(() => { + const addButtonRef = React13.useRef(null); + React13.useEffect(() => { const disposeDingtalk = installDingtalkStyles(); const disposeQq = installQqStyles(); mounted.current = true; @@ -6135,11 +6148,11 @@ function QqSettingsTab({ rpcCall }) { disposeDingtalk(); }; }, []); - const invoke = React12.useCallback(async (endpoint, payload = {}, signal) => { + const invoke = React13.useCallback(async (endpoint, payload = {}, signal) => { if (typeof rpcCall !== "function") throw new TypeError("QQ \u8BBE\u7F6E\u9875\u7F3A\u5C11 RPC \u8FDE\u63A5"); return unwrapRpcResult4(await rpcCall(endpoint, payload, signal)); }, [rpcCall]); - const loadStatus = React12.useCallback(async ({ signal, silent = false, restore = false } = {}) => { + const loadStatus = React13.useCallback(async ({ signal, silent = false, restore = false } = {}) => { const workspaceVersion = workspaceFence.beginStatus(); if (workspaceVersion === null) return void 0; if (!silent && mounted.current) setModel((current) => ({ ...current, phase: "loading", error: null })); @@ -6165,12 +6178,12 @@ function QqSettingsTab({ rpcCall }) { return void 0; } }, [invoke, workspaceFence]); - React12.useEffect(() => { + React13.useEffect(() => { const controller = new AbortController(); void loadStatus({ signal: controller.signal, restore: true }); return () => controller.abort(); }, [loadStatus]); - React12.useEffect(() => { + React13.useEffect(() => { if (model.phase !== "ready") return void 0; const controller = new AbortController(); const timer = window.setInterval(() => void loadStatus({ signal: controller.signal, silent: true }), 15e3); @@ -6179,12 +6192,12 @@ function QqSettingsTab({ rpcCall }) { window.clearInterval(timer); }; }, [loadStatus, model.phase]); - React12.useEffect(() => { + React13.useEffect(() => { if (!provision || !ACTIVE_STATES.has(provision.status)) return void 0; const timer = window.setInterval(() => mounted.current && setNow(Date.now()), 1e3); return () => window.clearInterval(timer); }, [provision?.attemptId, provision?.status]); - const startProvisioning = React12.useCallback(async (replace = false) => { + const startProvisioning = React13.useCallback(async (replace = false) => { setCredentialOpen(false); setCredentialError(null); setBusy(true); @@ -6202,7 +6215,7 @@ function QqSettingsTab({ rpcCall }) { if (mounted.current) setBusy(false); } }, [invoke, provision?.attemptId]); - const bindCredentials = React12.useCallback(async ({ identity, secret }) => { + const bindCredentials = React13.useCallback(async ({ identity, secret }) => { const snapshotVersion = workspaceFence.beginMutation(); setBusy(true); setCredentialError(null); @@ -6230,7 +6243,7 @@ function QqSettingsTab({ rpcCall }) { if (mounted.current) setBusy(false); } }, [invoke, loadStatus, workspaceFence]); - const closeProvision = React12.useCallback(async () => { + const closeProvision = React13.useCallback(async () => { setBusy(true); try { if (provision?.attemptId && ACTIVE_STATES.has(provision.status)) { @@ -6241,7 +6254,7 @@ function QqSettingsTab({ rpcCall }) { if (mounted.current) setBusy(false); } }, [invoke, provision?.attemptId, provision?.status]); - React12.useEffect(() => { + React13.useEffect(() => { const attemptId = provision?.attemptId; if (!attemptId || !ACTIVE_STATES.has(provision.status)) return void 0; const controller = new AbortController(); @@ -6271,7 +6284,7 @@ function QqSettingsTab({ rpcCall }) { window.clearTimeout(timer); }; }, [invoke, loadStatus, provision?.attemptId, provision?.pollIntervalMs, provision?.status]); - const botAction = React12.useCallback(async (account, operation, endpoint, payload) => { + const botAction = React13.useCallback(async (account, operation, endpoint, payload) => { const snapshotVersion = workspaceFence.beginMutation(); setBusyByBot((current) => ({ ...current, [account.botId]: operation })); try { @@ -6296,7 +6309,7 @@ function QqSettingsTab({ rpcCall }) { }); } }, [invoke, loadStatus, workspaceFence]); - const reconnect = React12.useCallback(async (account) => { + const reconnect = React13.useCallback(async (account) => { setFeedbackByBot((current) => { const next = { ...current }; delete next[account.botId]; @@ -6342,11 +6355,11 @@ function QqSettingsTab({ rpcCall }) { const botList = model.bots.length > 0 ? h2( "section", { className: "dim-listSection" }, - h2( - "div", - { className: "ddt-listHeading dim-listHeading" }, - h2("h3", null, "\u5DF2\u7ED1\u5B9A\u7684 QQ \u673A\u5668\u4EBA") - ), + h2(ChannelListHeading, { + className: "ddt-listHeading", + title: "\u5DF2\u7ED1\u5B9A\u7684 QQ \u673A\u5668\u4EBA", + connectionLabel: "WebSocket \u957F\u8FDE\u63A5" + }), h2("ul", { className: "ddt-list dim-botList" }, model.bots.map((account) => h2("li", { key: account.botId }, h2(AccountCard2, { account, busy: busyByBot[account.botId], @@ -6405,7 +6418,7 @@ function QqSettingsTab({ rpcCall }) { addButtonRef }), model.phase === "loading" ? h2(LoadingView3) : model.phase === "error" ? h2("div", { className: "ddt-card dim-surfaceCard" }, h2("div", { className: "ddt-inlineError dim-inlineError" }, h2("h3", null, "\u65E0\u6CD5\u8BFB\u53D6 QQ \u673A\u5668\u4EBA\u72B6\u6001"), h2("p", null, model.error?.message), h2(Button7, { onClick: () => void loadStatus() }, "\u91CD\u65B0\u8BFB\u53D6"))) : h2( - React12.Fragment, + React13.Fragment, null, credentialView, provisionView, @@ -6494,7 +6507,7 @@ function normalizeOfficeStatus(value) { } // plugin-src/client/channels/office/index.js -var React13 = __toESM(require("react"), 1); +var React14 = __toESM(require("react"), 1); function Button9({ children, kind = "secondary", ...props }) { return h2("button", { ...props, type: "button", className: "ddt-button", "data-kind": kind }, children); } @@ -6523,12 +6536,12 @@ function stateLabel(model) { return "\u5DF2\u914D\u7F6E"; } function OfficeSettingsTab({ rpcCall, initialStatus }) { - const [model, setModel] = React13.useState(normalizeOfficeStatus(initialStatus)); - const [phase, setPhase] = React13.useState(initialStatus === void 0 ? "loading" : "ready"); - const [busy, setBusy] = React13.useState(""); - const [error, setError] = React13.useState(""); - const [notice, setNotice] = React13.useState(""); - const [form, setForm] = React13.useState({ + const [model, setModel] = React14.useState(normalizeOfficeStatus(initialStatus)); + const [phase, setPhase] = React14.useState(initialStatus === void 0 ? "loading" : "ready"); + const [busy, setBusy] = React14.useState(""); + const [error, setError] = React14.useState(""); + const [notice, setNotice] = React14.useState(""); + const [form, setForm] = React14.useState({ baseUrl: "", deviceId: "local-harness", deviceToken: "", @@ -6537,11 +6550,11 @@ function OfficeSettingsTab({ rpcCall, initialStatus }) { workspaces: "", instructionPresets: "" }); - const invoke = React13.useCallback(async (endpoint, payload = {}) => { + const invoke = React14.useCallback(async (endpoint, payload = {}) => { if (typeof rpcCall !== "function") throw new Error("AI Office \u8BBE\u7F6E\u9875\u7F3A\u5C11 RPC \u8FDE\u63A5"); return unwrapOfficeRpc(await rpcCall(endpoint, payload)); }, [rpcCall]); - const adopt = React13.useCallback((value) => { + const adopt = React14.useCallback((value) => { const next = normalizeOfficeStatus(value?.snapshot ?? value); setModel(next); if (next.config) setForm((current) => ({ @@ -6556,7 +6569,7 @@ function OfficeSettingsTab({ rpcCall, initialStatus }) { })); return next; }, []); - const load = React13.useCallback(async () => { + const load = React14.useCallback(async () => { try { adopt(await invoke(OFFICE_RPC_ENDPOINTS.status)); setPhase("ready"); @@ -6566,7 +6579,7 @@ function OfficeSettingsTab({ rpcCall, initialStatus }) { setError(caught.message); } }, [adopt, invoke]); - React13.useEffect(() => { + React14.useEffect(() => { void load(); }, [load]); const run = async (name2, operation) => { @@ -6583,7 +6596,7 @@ function OfficeSettingsTab({ rpcCall, initialStatus }) { setBusy(""); } }; - const hooks = React13.useMemo(() => { + const hooks = React14.useMemo(() => { try { return officeHookUrls(form.baseUrl); } catch { @@ -6768,7 +6781,7 @@ var normalizeSnapshot4 = api2.normalizeSnapshot; var presentError5 = api2.presentError; // plugin-src/client/channels/slack/index.js -var React14 = __toESM(require("react"), 1); +var React15 = __toESM(require("react"), 1); // src/channels/slack/manifest.mjs var SLACK_APP_MANIFEST_YAML = `_metadata: @@ -6841,10 +6854,10 @@ function installSlackStyles() { // plugin-src/client/channels/slack/index.js function SlackCredentialPanel({ busy, error, onSubmit, onCancel }) { - const [botToken, setBotToken] = React14.useState(""); - const [appToken, setAppToken] = React14.useState(""); - const [copied, setCopied] = React14.useState(false); - const headingId = React14.useId(); + const [botToken, setBotToken] = React15.useState(""); + const [appToken, setAppToken] = React15.useState(""); + const [copied, setCopied] = React15.useState(false); + const headingId = React15.useId(); const copyManifest = async () => { try { await navigator.clipboard.writeText(SLACK_APP_MANIFEST_YAML); @@ -7000,7 +7013,7 @@ var normalizeSnapshot5 = api3.normalizeSnapshot; var presentError6 = api3.presentError; // plugin-src/client/channels/telegram/index.js -var React15 = __toESM(require("react"), 1); +var React16 = __toESM(require("react"), 1); // plugin-src/client/channels/telegram/styles.js var TELEGRAM_STYLE_ID = "xmanrui-dsh-im-telegram-settings"; @@ -7066,11 +7079,11 @@ function allowedUsersFromText(value) { function TelegramAccessSettings({ account, busy = false, onSave }) { const policy = policyFor(account); const sourceUsers = policy.allowedUsers.join("\n"); - const accessHelpId = React15.useId(); - const [accessMode, setAccessMode] = React15.useState(policy.accessMode); - const [allowedUsers, setAllowedUsers] = React15.useState(sourceUsers); - const [error, setError] = React15.useState(null); - React15.useEffect(() => { + const accessHelpId = React16.useId(); + const [accessMode, setAccessMode] = React16.useState(policy.accessMode); + const [allowedUsers, setAllowedUsers] = React16.useState(sourceUsers); + const [error, setError] = React16.useState(null); + React16.useEffect(() => { setAccessMode(policy.accessMode); setAllowedUsers(sourceUsers); setError(null); @@ -7330,7 +7343,7 @@ function formatRemaining4(milliseconds) { } // plugin-src/client/channels/wecom/index.js -var React16 = __toESM(require("react"), 1); +var React17 = __toESM(require("react"), 1); // plugin-src/client/channels/wecom/styles.js var WECOM_STYLE_ID = "xmanrui-dsh-im-wecom-settings"; @@ -7355,7 +7368,7 @@ function installWecomStyles() { // plugin-src/client/channels/wecom/index.js var ACTIVE_STATES2 = /* @__PURE__ */ new Set(["pending", "refreshing", "connecting"]); -var Button10 = React16.forwardRef(function Button11({ children, kind = "secondary", className = "", ...props }, ref) { +var Button10 = React17.forwardRef(function Button11({ children, kind = "secondary", className = "", ...props }, ref) { return h2("button", { ...props, ref, @@ -7597,18 +7610,14 @@ function AccountCard3({ h2("p", null, account.bot.appIdMasked) ) ), - h2( - "div", - { className: "ddt-health dim-botHealth" }, - h2("span", { className: "ddt-dot dim-healthDot", "data-tone": tone }), - h2("span", null, stateLabel2) - ) - ), - h2( - "dl", - { className: "ddt-metrics dim-botMetrics" }, - h2("div", { className: "ddt-metric dim-botMetric" }, h2("dt", null, "\u6D88\u606F\u901A\u9053"), h2("dd", null, account.connected ? "WebSocket \u957F\u8FDE\u63A5" : "\u79BB\u7EBF")), - h2("div", { className: "ddt-metric dim-botMetric" }, h2("dt", null, "\u6700\u8FD1\u68C0\u67E5"), h2("dd", null, checkedTime4(account.health.lastCheckedAt))) + h2(BotStatusMeta, { + className: "ddt-health", + dotClassName: "ddt-dot", + tone, + stateLabel: stateLabel2, + lastCheckedAt: account.health.lastCheckedAt, + formatCheckedTime: checkedTime4 + }) ), h2(WorkspaceEditor, { workspace: account.workspace, @@ -7650,27 +7659,27 @@ function AccountCard3({ ); } function WecomSettingsTab({ rpcCall }) { - const [model, setModel] = React16.useState({ + const [model, setModel] = React17.useState({ phase: "loading", bots: [], totals: { configured: 0, connected: 0 }, error: null, agentPresetCatalog: EMPTY_AGENT_PRESET_CATALOG }); - const [provision, setProvision] = React16.useState(null); - const [busy, setBusy] = React16.useState(false); - const [busyByBot, setBusyByBot] = React16.useState({}); - const [feedbackByBot, setFeedbackByBot] = React16.useState({}); - const [removeTarget, setRemoveTarget] = React16.useState(null); - const [credentialOpen, setCredentialOpen] = React16.useState(false); - const [credentialError, setCredentialError] = React16.useState(null); - const [notice, setNotice] = React16.useState(""); - const [now, setNow] = React16.useState(Date.now()); - const mounted = React16.useRef(true); + const [provision, setProvision] = React17.useState(null); + const [busy, setBusy] = React17.useState(false); + const [busyByBot, setBusyByBot] = React17.useState({}); + const [feedbackByBot, setFeedbackByBot] = React17.useState({}); + const [removeTarget, setRemoveTarget] = React17.useState(null); + const [credentialOpen, setCredentialOpen] = React17.useState(false); + const [credentialError, setCredentialError] = React17.useState(null); + const [notice, setNotice] = React17.useState(""); + const [now, setNow] = React17.useState(Date.now()); + const mounted = React17.useRef(true); const workspaceFence = useWorkspaceSnapshotFence(); - const addButtonRef = React16.useRef(null); - const noticeFrameRef = React16.useRef(null); - const announce = React16.useCallback((message) => { + const addButtonRef = React17.useRef(null); + const noticeFrameRef = React17.useRef(null); + const announce = React17.useCallback((message) => { if (!mounted.current) return; if (noticeFrameRef.current !== null) { window.cancelAnimationFrame(noticeFrameRef.current); @@ -7684,7 +7693,7 @@ function WecomSettingsTab({ rpcCall }) { }); } }, []); - React16.useEffect(() => { + React17.useEffect(() => { const disposeDingtalk = installDingtalkStyles(); const disposeWecom = installWecomStyles(); mounted.current = true; @@ -7698,11 +7707,11 @@ function WecomSettingsTab({ rpcCall }) { disposeDingtalk(); }; }, []); - const invoke = React16.useCallback(async (endpoint, payload = {}, signal) => { + const invoke = React17.useCallback(async (endpoint, payload = {}, signal) => { if (typeof rpcCall !== "function") throw new TypeError("\u4F01\u4E1A\u5FAE\u4FE1\u8BBE\u7F6E\u9875\u7F3A\u5C11 RPC \u8FDE\u63A5"); return unwrapRpcResult7(await rpcCall(endpoint, payload, signal)); }, [rpcCall]); - const loadStatus = React16.useCallback(async ({ signal, silent = false, restore = false } = {}) => { + const loadStatus = React17.useCallback(async ({ signal, silent = false, restore = false } = {}) => { const workspaceVersion = workspaceFence.beginStatus(); if (workspaceVersion === null) return void 0; if (!silent && mounted.current) setModel((current) => ({ ...current, phase: "loading", error: null })); @@ -7728,12 +7737,12 @@ function WecomSettingsTab({ rpcCall }) { return void 0; } }, [invoke, workspaceFence]); - React16.useEffect(() => { + React17.useEffect(() => { const controller = new AbortController(); void loadStatus({ signal: controller.signal, restore: true }); return () => controller.abort(); }, [loadStatus]); - React16.useEffect(() => { + React17.useEffect(() => { if (model.phase !== "ready") return void 0; const controller = new AbortController(); const timer = window.setInterval(() => void loadStatus({ signal: controller.signal, silent: true }), 15e3); @@ -7742,12 +7751,12 @@ function WecomSettingsTab({ rpcCall }) { window.clearInterval(timer); }; }, [loadStatus, model.phase]); - React16.useEffect(() => { + React17.useEffect(() => { if (!provision || !ACTIVE_STATES2.has(provision.status)) return void 0; const timer = window.setInterval(() => mounted.current && setNow(Date.now()), 1e3); return () => window.clearInterval(timer); }, [provision?.attemptId, provision?.status]); - const startProvisioning = React16.useCallback(async (replace = false) => { + const startProvisioning = React17.useCallback(async (replace = false) => { setCredentialOpen(false); setCredentialError(null); setBusy(true); @@ -7765,7 +7774,7 @@ function WecomSettingsTab({ rpcCall }) { if (mounted.current) setBusy(false); } }, [invoke, provision?.attemptId]); - const bindCredentials = React16.useCallback(async ({ identity, secret }) => { + const bindCredentials = React17.useCallback(async ({ identity, secret }) => { const snapshotVersion = workspaceFence.beginMutation(); setBusy(true); setCredentialError(null); @@ -7793,7 +7802,7 @@ function WecomSettingsTab({ rpcCall }) { if (mounted.current) setBusy(false); } }, [invoke, loadStatus, workspaceFence]); - const closeProvision = React16.useCallback(async () => { + const closeProvision = React17.useCallback(async () => { setBusy(true); try { if (provision?.attemptId && ACTIVE_STATES2.has(provision.status)) { @@ -7804,7 +7813,7 @@ function WecomSettingsTab({ rpcCall }) { if (mounted.current) setBusy(false); } }, [invoke, provision?.attemptId, provision?.status]); - React16.useEffect(() => { + React17.useEffect(() => { const attemptId = provision?.attemptId; if (!attemptId || !ACTIVE_STATES2.has(provision.status)) return void 0; const controller = new AbortController(); @@ -7834,7 +7843,7 @@ function WecomSettingsTab({ rpcCall }) { window.clearTimeout(timer); }; }, [invoke, loadStatus, provision?.attemptId, provision?.pollIntervalMs, provision?.status]); - const botAction = React16.useCallback(async (account, operation, endpoint, payload) => { + const botAction = React17.useCallback(async (account, operation, endpoint, payload) => { const snapshotVersion = workspaceFence.beginMutation(); setBusyByBot((current) => ({ ...current, [account.botId]: operation })); try { @@ -7859,7 +7868,7 @@ function WecomSettingsTab({ rpcCall }) { }); } }, [invoke, loadStatus, workspaceFence]); - const reconnect = React16.useCallback(async (account) => { + const reconnect = React17.useCallback(async (account) => { setFeedbackByBot((current) => { const next = { ...current }; delete next[account.botId]; @@ -7916,11 +7925,11 @@ function WecomSettingsTab({ rpcCall }) { const botList = model.bots.length > 0 ? h2( "section", { className: "dim-listSection" }, - h2( - "div", - { className: "ddt-listHeading dim-listHeading" }, - h2("h3", null, "\u5DF2\u7ED1\u5B9A\u7684\u4F01\u4E1A\u5FAE\u4FE1\u673A\u5668\u4EBA") - ), + h2(ChannelListHeading, { + className: "ddt-listHeading", + title: "\u5DF2\u7ED1\u5B9A\u7684\u4F01\u4E1A\u5FAE\u4FE1\u673A\u5668\u4EBA", + connectionLabel: "WebSocket \u957F\u8FDE\u63A5" + }), h2("ul", { className: "ddt-list dim-botList" }, model.bots.map((account) => h2("li", { key: account.botId }, h2(AccountCard3, { account, busy: busyByBot[account.botId], @@ -7980,7 +7989,7 @@ function WecomSettingsTab({ rpcCall }) { }), h2("div", { className: "ddt-visuallyHidden", role: "status", "aria-live": "polite" }, notice), model.phase === "loading" ? h2(LoadingView4) : model.phase === "error" ? h2("div", { className: "ddt-card dim-surfaceCard" }, h2("div", { className: "ddt-inlineError dim-inlineError" }, h2("h3", null, "\u65E0\u6CD5\u8BFB\u53D6\u4F01\u4E1A\u5FAE\u4FE1\u673A\u5668\u4EBA\u72B6\u6001"), h2("p", null, model.error?.message), h2(Button10, { onClick: () => void loadStatus() }, "\u91CD\u65B0\u8BFB\u53D6"))) : h2( - React16.Fragment, + React17.Fragment, null, credentialView, provisionView, @@ -7991,7 +8000,7 @@ function WecomSettingsTab({ rpcCall }) { } // plugin-src/client/channels/weixin/index.js -var React17 = __toESM(require("react"), 1); +var React18 = __toESM(require("react"), 1); // plugin-src/client/channels/weixin/api.js var WEIXIN_RPC_CHANNEL = "/weixin"; @@ -8232,10 +8241,6 @@ var CSS9 = 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; } @@ -8254,7 +8259,6 @@ var CSS9 = 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) { @@ -8276,7 +8280,7 @@ function installWeixinStyles() { } // plugin-src/client/channels/weixin/index.js -var Button12 = React17.forwardRef(function Button13({ children, kind = "secondary", className = "", ...props }, ref) { +var Button12 = React18.forwardRef(function Button13({ children, kind = "secondary", className = "", ...props }, ref) { return h2("button", { ...props, ref, @@ -8349,14 +8353,14 @@ function EmptyView5({ onStart, busy }) { ); } function QrPanel4({ provision, now, busy, onRefresh, onCancel }) { - const [imageFailed, setImageFailed] = React17.useState(false); + const [imageFailed, setImageFailed] = React18.useState(false); const source = safeQrSource5(provision.qrCodeDataUrl); const href = safeVerificationUrl(provision.verificationUrl); const remaining = Math.max(0, provision.expiresAt - now); const expired = remaining === 0 || provision.status === "expired"; const duration = Math.max(1, provision.durationMs ?? 5 * 6e4); const progress = Math.round(Math.min(1, remaining / duration) * 100); - React17.useEffect(() => setImageFailed(false), [source]); + React18.useEffect(() => setImageFailed(false), [source]); return h2( "div", { className: "dxw-card dim-surfaceCard" }, @@ -8423,9 +8427,9 @@ function QrPanel4({ provision, now, busy, onRefresh, onCancel }) { ); } function VerificationPanel({ provision, busy, onSubmit, onCancel }) { - const [code, setCode] = React17.useState(""); + const [code, setCode] = React18.useState(""); const valid = /^\d{4,8}$/.test(code); - React17.useEffect(() => setCode(""), [provision.attemptId]); + React18.useEffect(() => setCode(""), [provision.attemptId]); return h2( "div", { className: "dxw-card dim-surfaceCard" }, @@ -8546,28 +8550,14 @@ function AccountCard4({ h2("div", { className: "dxw-avatar dim-botAvatar", "aria-hidden": "true" }, h2(WeixinLogoGlyph, { size: 27 })), h2("div", { className: "dim-botName" }, h2("h3", null, account.bot.name), h2("p", null, account.bot.accountIdMasked)) ), - h2( - "div", - { className: "dxw-health dim-botHealth" }, - h2("span", { className: "dxw-dot dim-healthDot", "data-tone": tone }), - h2("span", null, account.connected ? "\u8FD0\u884C\u6B63\u5E38" : state === "connecting" ? "\u6B63\u5728\u8FDE\u63A5" : "\u8FDE\u63A5\u672A\u5C31\u7EEA") - ) - ), - h2( - "dl", - { className: "dxw-metrics dim-botMetrics" }, - h2( - "div", - { className: "dxw-metric dim-botMetric" }, - h2("dt", null, "\u6D88\u606F\u901A\u9053"), - h2("dd", null, account.connected ? "iLink \u957F\u8F6E\u8BE2" : "\u79BB\u7EBF") - ), - h2( - "div", - { className: "dxw-metric dim-botMetric" }, - h2("dt", null, "\u6700\u8FD1\u68C0\u67E5"), - h2("dd", null, checkedTime5(account.health.lastCheckedAt)) - ) + h2(BotStatusMeta, { + className: "dxw-health", + dotClassName: "dxw-dot", + tone, + stateLabel: account.connected ? "\u8FD0\u884C\u6B63\u5E38" : state === "connecting" ? "\u6B63\u5728\u8FDE\u63A5" : "\u8FDE\u63A5\u672A\u5C31\u7EEA", + lastCheckedAt: account.health.lastCheckedAt, + formatCheckedTime: checkedTime5 + }) ), h2(WorkspaceEditor, { workspace: account.workspace, @@ -8630,7 +8620,11 @@ function AccountList2(props) { return h2( "section", { className: "dim-listSection" }, - h2("div", { className: "dxw-listHeading dim-listHeading" }, h2("h3", null, "\u5DF2\u63A5\u5165\u7684\u5FAE\u4FE1\u8D26\u53F7")), + h2(ChannelListHeading, { + className: "dxw-listHeading", + title: "\u5DF2\u63A5\u5165\u7684\u5FAE\u4FE1\u8D26\u53F7", + connectionLabel: "iLink \u957F\u8F6E\u8BE2" + }), h2("ul", { className: "dxw-list dim-botList" }, props.bots.map((account) => h2( "li", { key: account.botId }, @@ -8660,7 +8654,7 @@ function mergeWeixinProvisioningSnapshot(current, incoming, { restoreProvisionin }; } function WeixinSettingsTab({ rpcCall }) { - const [model, setModel] = React17.useState({ + const [model, setModel] = React18.useState({ phase: "loading", bots: [], totals: EMPTY_TOTALS3, @@ -8668,33 +8662,33 @@ function WeixinSettingsTab({ rpcCall }) { error: null, agentPresetCatalog: EMPTY_AGENT_PRESET_CATALOG }); - const [provision, setProvision] = React17.useState(null); - const [busy, setBusy] = React17.useState(false); - const [busyByBot, setBusyByBot] = React17.useState({}); - const [feedbackByBot, setFeedbackByBot] = React17.useState({}); - const [removeTarget, setRemoveTarget] = React17.useState(null); - const [notice, setNotice] = React17.useState(""); - const [now, setNow] = React17.useState(() => Date.now()); - const addButtonRef = React17.useRef(null); - const mountedRef = React17.useRef(true); + const [provision, setProvision] = React18.useState(null); + const [busy, setBusy] = React18.useState(false); + const [busyByBot, setBusyByBot] = React18.useState({}); + const [feedbackByBot, setFeedbackByBot] = React18.useState({}); + const [removeTarget, setRemoveTarget] = React18.useState(null); + const [notice, setNotice] = React18.useState(""); + const [now, setNow] = React18.useState(() => Date.now()); + const addButtonRef = React18.useRef(null); + const mountedRef = React18.useRef(true); const workspaceFence = useWorkspaceSnapshotFence(); const scheduleAnimationFrame = useAnimationFrameScheduler(); - React17.useEffect(() => { + React18.useEffect(() => { mountedRef.current = true; return () => { mountedRef.current = false; }; }, []); - const announce = React17.useCallback((value) => { + const announce = React18.useCallback((value) => { setNotice(""); scheduleAnimationFrame(() => { if (value) setNotice(value); }, "announcement"); }, [scheduleAnimationFrame]); - const invoke = React17.useCallback(async (endpoint, payload = {}, signal) => { + const invoke = React18.useCallback(async (endpoint, payload = {}, signal) => { return unwrapRpcResult8(await rpcCall(endpoint, payload, signal)); }, [rpcCall]); - const loadStatus = React17.useCallback(async ({ + const loadStatus = React18.useCallback(async ({ signal, silent = false, restoreProvisioning = false @@ -8731,12 +8725,12 @@ function WeixinSettingsTab({ rpcCall }) { return void 0; } }, [invoke, workspaceFence]); - React17.useEffect(() => { + React18.useEffect(() => { const controller = new AbortController(); void loadStatus({ signal: controller.signal, restoreProvisioning: true }); return () => controller.abort(); }, [loadStatus]); - React17.useEffect(() => { + React18.useEffect(() => { if (model.phase !== "ready") return void 0; const controller = new AbortController(); let running = false; @@ -8755,12 +8749,12 @@ function WeixinSettingsTab({ rpcCall }) { window.clearInterval(timer); }; }, [loadStatus, model.phase]); - React17.useEffect(() => { + React18.useEffect(() => { if (!provision || !["pending", "scanned"].includes(provision.status)) return void 0; const timer = window.setInterval(() => setNow(Date.now()), 1e3); return () => window.clearInterval(timer); }, [provision?.attemptId, provision?.status]); - const startProvisioning = React17.useCallback(async ({ replace = false } = {}) => { + const startProvisioning = React18.useCallback(async ({ replace = false } = {}) => { setBusy(true); try { if (replace && provision?.attemptId) { @@ -8781,7 +8775,7 @@ function WeixinSettingsTab({ rpcCall }) { setBusy(false); } }, [announce, invoke, provision?.attemptId]); - const cancelProvisioning = React17.useCallback(async () => { + const cancelProvisioning = React18.useCallback(async () => { setBusy(true); try { if (provision?.attemptId && !["failed", "expired", "cancelled"].includes(provision.status)) { @@ -8796,7 +8790,7 @@ function WeixinSettingsTab({ rpcCall }) { setBusy(false); } }, [announce, invoke, provision?.attemptId, provision?.status, scheduleAnimationFrame]); - const submitVerification = React17.useCallback(async (verifyCode) => { + const submitVerification = React18.useCallback(async (verifyCode) => { if (!provision?.attemptId) return; setBusy(true); try { @@ -8812,7 +8806,7 @@ function WeixinSettingsTab({ rpcCall }) { setBusy(false); } }, [announce, invoke, provision?.attemptId]); - React17.useEffect(() => { + React18.useEffect(() => { const attemptId = provision?.attemptId; if (!attemptId || !["pending", "scanned", "connecting"].includes(provision.status)) return void 0; const controller = new AbortController(); @@ -8860,7 +8854,7 @@ function WeixinSettingsTab({ rpcCall }) { controller.abort(); }; }, [announce, invoke, loadStatus, provision?.attemptId, provision?.status, provision?.pollIntervalMs]); - const setBotBusy = React17.useCallback((botId, value) => { + const setBotBusy = React18.useCallback((botId, value) => { setBusyByBot((current) => { const next = { ...current }; if (value) next[botId] = value; @@ -8868,7 +8862,7 @@ function WeixinSettingsTab({ rpcCall }) { return next; }); }, []); - const reconnect = React17.useCallback(async (account) => { + const reconnect = React18.useCallback(async (account) => { const snapshotVersion = workspaceFence.beginMutation(); setBotBusy(account.botId, "reconnect"); setFeedbackByBot((current) => { @@ -8919,7 +8913,7 @@ function WeixinSettingsTab({ rpcCall }) { setBotBusy(account.botId, null); } }, [announce, invoke, loadStatus, setBotBusy, workspaceFence]); - const saveWorkspace = React17.useCallback(async (account, workspace) => { + const saveWorkspace = React18.useCallback(async (account, workspace) => { const workspaceVersion = workspaceFence.beginMutation(); setBotBusy(account.botId, "workspace"); try { @@ -8943,7 +8937,7 @@ function WeixinSettingsTab({ rpcCall }) { if (mountedRef.current) setBotBusy(account.botId, null); } }, [invoke, loadStatus, setBotBusy, workspaceFence]); - const saveAgentPreset = React17.useCallback(async (account, agentPreset) => { + const saveAgentPreset = React18.useCallback(async (account, agentPreset) => { const snapshotVersion = workspaceFence.beginMutation(); setBotBusy(account.botId, "preset"); try { @@ -8967,7 +8961,7 @@ function WeixinSettingsTab({ rpcCall }) { if (mountedRef.current) setBotBusy(account.botId, null); } }, [invoke, loadStatus, setBotBusy, workspaceFence]); - const remove = React17.useCallback(async (account) => { + const remove = React18.useCallback(async (account) => { const snapshotVersion = workspaceFence.beginMutation(); setBotBusy(account.botId, "delete"); try { @@ -9051,7 +9045,7 @@ function WeixinSettingsTab({ rpcCall }) { h2(Button12, { onClick: () => void loadStatus() }, "\u91CD\u65B0\u8BFB\u53D6") ) ) : h2( - React17.Fragment, + React18.Fragment, null, provisionView, model.bots.length === 0 && !provision ? h2(EmptyView5, { onStart: () => void startProvisioning(), busy }) : null, @@ -9187,7 +9181,7 @@ function formatRemaining6(milliseconds) { } // plugin-src/client/channels/whatsapp/index.js -var React18 = __toESM(require("react"), 1); +var React19 = __toESM(require("react"), 1); // plugin-src/client/channels/whatsapp/styles.js var WHATSAPP_STYLE_ID = "xmanrui-dsh-im-whatsapp-settings"; @@ -9212,7 +9206,7 @@ function installWhatsappStyles() { // plugin-src/client/channels/whatsapp/index.js var ACTIVE_STATES3 = /* @__PURE__ */ new Set(["pending", "connecting"]); -var Button14 = React18.forwardRef(function Button15({ children, kind = "secondary", className = "", ...props }, ref) { +var Button14 = React19.forwardRef(function Button15({ children, kind = "secondary", className = "", ...props }, ref) { return h2("button", { ...props, ref, @@ -9466,28 +9460,14 @@ function WhatsappAccountCard({ h2("p", null, account.bot.idMasked) ) ), - h2( - "div", - { className: "ddt-health dim-botHealth" }, - h2("span", { className: "ddt-dot dim-healthDot", "data-tone": tone }), - h2("span", null, stateLabel2) - ) - ), - h2( - "dl", - { className: "ddt-metrics dim-botMetrics" }, - h2( - "div", - { className: "ddt-metric dim-botMetric" }, - h2("dt", null, "\u6D88\u606F\u901A\u9053"), - h2("dd", null, account.connected ? "WhatsApp Web" : "\u79BB\u7EBF") - ), - h2( - "div", - { className: "ddt-metric dim-botMetric" }, - h2("dt", null, "\u6700\u8FD1\u68C0\u67E5"), - h2("dd", null, checkedTime6(account.health.lastCheckedAt)) - ) + h2(BotStatusMeta, { + className: "ddt-health", + dotClassName: "ddt-dot", + tone, + stateLabel: stateLabel2, + lastCheckedAt: account.health.lastCheckedAt, + formatCheckedTime: checkedTime6 + }) ), h2(WorkspaceEditor, { workspace: account.workspace, @@ -9537,23 +9517,23 @@ function WhatsappAccountCard({ ); } function WhatsappSettingsTab({ rpcCall }) { - const [model, setModel] = React18.useState({ + const [model, setModel] = React19.useState({ phase: "loading", bots: [], totals: { configured: 0, connected: 0 }, error: null, agentPresetCatalog: EMPTY_AGENT_PRESET_CATALOG }); - const [provision, setProvision] = React18.useState(null); - const [busy, setBusy] = React18.useState(false); - const [busyByBot, setBusyByBot] = React18.useState({}); - const [testNoticeByBot, setTestNoticeByBot] = React18.useState({}); - const [removeTarget, setRemoveTarget] = React18.useState(null); - const [now, setNow] = React18.useState(Date.now()); - const mounted = React18.useRef(true); + const [provision, setProvision] = React19.useState(null); + const [busy, setBusy] = React19.useState(false); + const [busyByBot, setBusyByBot] = React19.useState({}); + const [testNoticeByBot, setTestNoticeByBot] = React19.useState({}); + const [removeTarget, setRemoveTarget] = React19.useState(null); + const [now, setNow] = React19.useState(Date.now()); + const mounted = React19.useRef(true); const workspaceFence = useWorkspaceSnapshotFence(); - const addButtonRef = React18.useRef(null); - React18.useEffect(() => { + const addButtonRef = React19.useRef(null); + React19.useEffect(() => { const disposeDingtalk = installDingtalkStyles(); const disposeWhatsapp = installWhatsappStyles(); mounted.current = true; @@ -9563,11 +9543,11 @@ function WhatsappSettingsTab({ rpcCall }) { disposeDingtalk(); }; }, []); - const invoke = React18.useCallback(async (endpoint, payload = {}, signal) => { + const invoke = React19.useCallback(async (endpoint, payload = {}, signal) => { if (typeof rpcCall !== "function") throw new TypeError("WhatsApp \u8BBE\u7F6E\u9875\u7F3A\u5C11 RPC \u8FDE\u63A5"); return unwrapRpcResult9(await rpcCall(endpoint, payload, signal)); }, [rpcCall]); - const loadStatus = React18.useCallback(async ({ signal, silent = false, restore = false } = {}) => { + const loadStatus = React19.useCallback(async ({ signal, silent = false, restore = false } = {}) => { const workspaceVersion = workspaceFence.beginStatus(); if (workspaceVersion === null) return void 0; if (!silent && mounted.current) setModel((current) => ({ ...current, phase: "loading", error: null })); @@ -9597,12 +9577,12 @@ function WhatsappSettingsTab({ rpcCall }) { return void 0; } }, [invoke, workspaceFence]); - React18.useEffect(() => { + React19.useEffect(() => { const controller = new AbortController(); void loadStatus({ signal: controller.signal, restore: true }); return () => controller.abort(); }, [loadStatus]); - React18.useEffect(() => { + React19.useEffect(() => { if (model.phase !== "ready") return void 0; const controller = new AbortController(); const timer = window.setInterval( @@ -9614,12 +9594,12 @@ function WhatsappSettingsTab({ rpcCall }) { window.clearInterval(timer); }; }, [loadStatus, model.phase]); - React18.useEffect(() => { + React19.useEffect(() => { if (!provision || !ACTIVE_STATES3.has(provision.status)) return void 0; const timer = window.setInterval(() => mounted.current && setNow(Date.now()), 1e3); return () => window.clearInterval(timer); }, [provision?.attemptId, provision?.status]); - const startProvisioning = React18.useCallback(async (replace = false) => { + const startProvisioning = React19.useCallback(async (replace = false) => { setBusy(true); try { if (replace && provision?.attemptId) { @@ -9637,7 +9617,7 @@ function WhatsappSettingsTab({ rpcCall }) { if (mounted.current) setBusy(false); } }, [invoke, provision?.attemptId]); - const closeProvision = React18.useCallback(async () => { + const closeProvision = React19.useCallback(async () => { setBusy(true); try { if (provision?.attemptId && ACTIVE_STATES3.has(provision.status)) { @@ -9648,7 +9628,7 @@ function WhatsappSettingsTab({ rpcCall }) { if (mounted.current) setBusy(false); } }, [invoke, provision?.attemptId, provision?.status]); - React18.useEffect(() => { + React19.useEffect(() => { const attemptId = provision?.attemptId; if (!attemptId || !ACTIVE_STATES3.has(provision.status)) return void 0; const controller = new AbortController(); @@ -9689,7 +9669,7 @@ function WhatsappSettingsTab({ rpcCall }) { if (timer) window.clearTimeout(timer); }; }, [invoke, loadStatus, provision?.attemptId, provision?.status]); - const botAction = React18.useCallback(async (account, operation, endpoint, payload) => { + const botAction = React19.useCallback(async (account, operation, endpoint, payload) => { const snapshotVersion = workspaceFence.beginMutation(); setBusyByBot((current) => ({ ...current, [account.botId]: operation })); if (operation === "reconnect") { @@ -9738,11 +9718,11 @@ function WhatsappSettingsTab({ rpcCall }) { const botList = model.bots.length > 0 ? h2( "section", { className: "dim-listSection" }, - h2( - "div", - { className: "ddt-listHeading dim-listHeading" }, - h2("h3", null, "\u5DF2\u63A5\u5165\u7684 WhatsApp \u673A\u5668\u4EBA") - ), + h2(ChannelListHeading, { + className: "ddt-listHeading", + title: "\u5DF2\u63A5\u5165\u7684 WhatsApp \u673A\u5668\u4EBA", + connectionLabel: "WhatsApp Web" + }), h2("ul", { className: "ddt-list dim-botList" }, model.bots.map((account) => h2("li", { key: account.botId }, h2(WhatsappAccountCard, { account, busy: busyByBot[account.botId], @@ -9802,7 +9782,7 @@ function WhatsappSettingsTab({ rpcCall }) { h2(Button14, { onClick: () => void loadStatus() }, "\u91CD\u65B0\u8BFB\u53D6") ) ) : h2( - React18.Fragment, + React19.Fragment, null, provision?.status === "pending" ? h2(QrPanel5, { provision, @@ -9910,6 +9890,14 @@ var CSS11 = 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; } @@ -10037,15 +10025,13 @@ var CSS11 = 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%); } @@ -10076,6 +10062,7 @@ var CSS11 = 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; } @@ -10211,8 +10198,8 @@ function IMSettingsTab({ officeRpcCall, workspaceDirectoryPicker }) { - const [selected, setSelected] = React19.useState("weixin"); - const githubTooltipId = React19.useId(); + const [selected, setSelected] = React20.useState("weixin"); + const githubTooltipId = React20.useId(); const active = CHANNELS.find((channel4) => channel4.id === selected) ?? CHANNELS[0]; return h2( WorkspaceDirectoryPickerContext.Provider, diff --git a/plugin-src/client/channel-card-meta.js b/plugin-src/client/channel-card-meta.js new file mode 100644 index 0000000..aa60a70 --- /dev/null +++ b/plugin-src/client/channel-card-meta.js @@ -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)))); +} diff --git a/plugin-src/client/channels/dingtalk/index.js b/plugin-src/client/channels/dingtalk/index.js index 18990f9..e084418 100644 --- a/plugin-src/client/channels/dingtalk/index.js +++ b/plugin-src/client/channels/dingtalk/index.js @@ -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, diff --git a/plugin-src/client/channels/dingtalk/styles.js b/plugin-src/client/channels/dingtalk/styles.js index e97ea2f..1b85439 100644 --- a/plugin-src/client/channels/dingtalk/styles.js +++ b/plugin-src/client/channels/dingtalk/styles.js @@ -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; } diff --git a/plugin-src/client/channels/feishu/index.js b/plugin-src/client/channels/feishu/index.js index 4e47f72..91a80da 100644 --- a/plugin-src/client/channels/feishu/index.js +++ b/plugin-src/client/channels/feishu/index.js @@ -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, { diff --git a/plugin-src/client/channels/feishu/styles.js b/plugin-src/client/channels/feishu/styles.js index f0c324a..43242d8 100644 --- a/plugin-src/client/channels/feishu/styles.js +++ b/plugin-src/client/channels/feishu/styles.js @@ -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; } diff --git a/plugin-src/client/channels/qq/index.js b/plugin-src/client/channels/qq/index.js index 379fdd4..5da6fa5 100644 --- a/plugin-src/client/channels/qq/index.js +++ b/plugin-src/client/channels/qq/index.js @@ -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, diff --git a/plugin-src/client/channels/shared/token-channel.js b/plugin-src/client/channels/shared/token-channel.js index 0842ed8..67aeb82 100644 --- a/plugin-src/client/channels/shared/token-channel.js +++ b/plugin-src/client/channels/shared/token-channel.js @@ -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, diff --git a/plugin-src/client/channels/wecom/index.js b/plugin-src/client/channels/wecom/index.js index 2459eaa..845f051 100644 --- a/plugin-src/client/channels/wecom/index.js +++ b/plugin-src/client/channels/wecom/index.js @@ -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, diff --git a/plugin-src/client/channels/weixin/index.js b/plugin-src/client/channels/weixin/index.js index 5b556ba..01a9871 100644 --- a/plugin-src/client/channels/weixin/index.js +++ b/plugin-src/client/channels/weixin/index.js @@ -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, diff --git a/plugin-src/client/channels/weixin/styles.js b/plugin-src/client/channels/weixin/styles.js index 6ecde2a..ff66905 100644 --- a/plugin-src/client/channels/weixin/styles.js +++ b/plugin-src/client/channels/weixin/styles.js @@ -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) { diff --git a/plugin-src/client/channels/whatsapp/index.js b/plugin-src/client/channels/whatsapp/index.js index 243af0d..f77496d 100644 --- a/plugin-src/client/channels/whatsapp/index.js +++ b/plugin-src/client/channels/whatsapp/index.js @@ -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, diff --git a/plugin-src/client/i18n.js b/plugin-src/client/i18n.js index 0f0290b..963be73 100644 --- a/plugin-src/client/i18n.js +++ b/plugin-src/client/i18n.js @@ -107,6 +107,7 @@ const EN = Object.freeze({ '离线': 'Offline', '已断开': 'Disconnected', '消息通道': 'Message channel', + '查看消息通道说明': 'View message channel details', '最近检查': 'Last checked', '当前工作区': 'Current workspace', '选择目录': 'Choose folder', diff --git a/plugin-src/client/styles.js b/plugin-src/client/styles.js index bc995a0..9f6fc37 100644 --- a/plugin-src/client/styles.js +++ b/plugin-src/client/styles.js @@ -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; } diff --git a/test/channels/dingtalk/client-ui.test.mjs b/test/channels/dingtalk/client-ui.test.mjs index da94966..72e5f62 100644 --- a/test/channels/dingtalk/client-ui.test.mjs +++ b/test/channels/dingtalk/client-ui.test.mjs @@ -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 () => { diff --git a/test/channels/discord/client-ui.test.mjs b/test/channels/discord/client-ui.test.mjs index a227189..f228bae 100644 --- a/test/channels/discord/client-ui.test.mjs +++ b/test/channels/discord/client-ui.test.mjs @@ -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 长连接最近检查<\/span>/); + assert.doesNotMatch(markup, /Gateway 长连接|消息通道|dim-botMetric/); assert.match(markup, />检查连接移除接入 { 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, />消息通道<[^]*>最近检查最近检查<\/span>/); + assert.doesNotMatch(markup, /消息通道|dim-botMetric/); assert.match(markup, />检查连接<[^]*>移除接入 { })); assert.match(markup, /data-im-channel-logo="slack"/); assert.match(markup, /@deepseek-harness/); - assert.match(markup, />Socket Mode 长连接最近检查<\/span>/); + assert.doesNotMatch(markup, /Socket Mode 长连接|消息通道|dim-botMetric/); assert.match(markup, />检查连接移除接入 { })); assert.match(markup, /data-im-channel-logo="telegram"/); assert.match(markup, /@harness_bot/); - assert.match(markup, />Bot API 长轮询最近检查<\/span>/); + assert.doesNotMatch(markup, /Bot API 长轮询|消息通道|dim-botMetric/); assert.match(markup, />检查连接移除接入访问设置 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, />消息通道<[^]*>最近检查最近检查<\/span>/); + assert.doesNotMatch(markup, /消息通道|dim-botMetric/); assert.match(markup, />检查连接<[^]*>移除接入 { 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>/); + 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 () => { diff --git a/test/client-ui.test.mjs b/test/client-ui.test.mjs index 3e74773..e91bf13 100644 --- a/test/client-ui.test.mjs +++ b/test/client-ui.test.mjs @@ -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, />消息通道<[^]*>最近检查最近检查<\/span>/); + assert.doesNotMatch(markup, /消息通道|dim-botMetric/); assert.match(markup, /class="dim-presetSelect"/); assert.doesNotMatch(markup, />应用标识<|>飞书机器人 { @@ -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>/); + assert.match(markup, /aria-label="查看消息通道说明"/); + assert.match(markup, /role="tooltip">消息通道<\/span>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>/); + 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>/); + 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>/); 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.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/);