diff --git a/README.md b/README.md index de6690c..8172e61 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ ## 中文 -通过扫码把 IM 机器人接入 DeepSeek Harness。一个插件、一个设置入口,统一管理飞书和微信机器人。 +通过扫码把 IM 机器人接入 DeepSeek Harness。一个插件、一个设置入口,统一管理飞书、微信和钉钉机器人。 > GitHub 简介:通过扫码把IM机器人接入DeepSeek Harness(支持飞书、微信、钉钉等)。 @@ -13,9 +13,10 @@ ## 当前内置渠道 - 飞书:扫码创建并绑定机器人,使用长连接收发消息; -- 微信:扫码绑定微信机器人,使用腾讯 iLink 长轮询收发消息。 +- 微信:扫码绑定微信机器人,使用腾讯 iLink 长轮询收发消息; +- 钉钉:扫码创建并授权机器人,使用钉钉 Stream 长连接收发消息。 -钉钉等其他 IM 平台可继续按同一渠道适配器结构接入;当前可操作界面仅展示已实现的飞书和微信。 +其他 IM 平台可继续按同一渠道适配器结构接入。 ## 安装 @@ -23,14 +24,16 @@ npx -y github:xmanrui/dsh-im install ``` -重启 `dsh web`,然后打开「设置 → 插件 → IM机器人」。安装器会用 `dsh-im` 替换 profile 中直接安装的 `dsh-feishu` 和 `dsh-weixin`,但不删除任何渠道数据;原有飞书凭据和微信扫码绑定会继续使用。 +重启 `dsh web`,然后打开「设置 → 插件 → IM机器人」。安装器会用 `dsh-im` 替换 profile 中直接安装的 `dsh-feishu`、`dsh-weixin` 和 `dsh-dingtalk`,但不删除任何渠道数据;原有渠道凭据和扫码绑定会继续使用。 + +钉钉接入时,用钉钉 App 扫描页面二维码,再在钉钉授权页点击「一键创建新机器人」。钉钉当前可能在该官方授权页显示 OpenClaw 品牌;扫码后的机器人连接、凭据和消息均由 DeepSeek Harness 管理。由于扫码结果不包含扫码人的 staff ID,首次私聊后还需要在本机页面批准该使用者,未批准消息不会进入 Harness。 ## 设计 - Harness 中只注册一个「IM机器人」设置页; -- 左侧使用飞书和微信 Logo 切换渠道,不使用启用/停用开关; -- 两个渠道保持独立的 RPC、凭据、连接监督和会话映射; -- 浏览器只获得二维码和脱敏状态,不获得 App Secret 或 `bot_token`。 +- 左侧使用渠道 Logo 切换微信、飞书和钉钉,不使用启用/停用开关; +- 三个渠道保持独立的 RPC、凭据、连接监督和会话映射; +- 浏览器只获得二维码和脱敏状态,不获得 App Secret、`bot_token`、钉钉 `client_secret` 或原始 staff ID。 ## 本地开发 @@ -40,13 +43,13 @@ npm run check node bin/dsh-im.mjs install --source . ``` -`npm run check` 运行单元测试、构建 Host/Client 产物,并验证发布包不包含凭据或独立飞书/微信设置页注册。 +`npm run check` 运行单元测试、构建 Host/Client 产物,并验证发布包不包含凭据或独立渠道设置页注册。 --- ## English -Connect IM bots to DeepSeek Harness by scanning a QR code. One plugin and one settings entry provide unified management for Feishu and WeChat bots. +Connect IM bots to DeepSeek Harness by scanning a QR code. One plugin and one settings entry provide unified management for Feishu, WeChat, and DingTalk bots. > GitHub description: Connect IM bots to DeepSeek Harness by scanning a QR code (supports Feishu, WeChat, DingTalk, and more). @@ -58,8 +61,9 @@ Connect IM bots to DeepSeek Harness by scanning a QR code. One plugin and one se - Feishu: create and bind a bot by scanning a QR code, then send and receive messages over a persistent connection. - WeChat: bind a WeChat bot by scanning a QR code, then send and receive messages through Tencent iLink long polling. +- DingTalk: create and authorize a bot by scanning a QR code, then send and receive messages through DingTalk Stream. -Other IM platforms such as DingTalk can be added through the same channel-adapter structure. The current interface only exposes the implemented Feishu and WeChat channels. +Other IM platforms can be added through the same channel-adapter structure. ## Installation @@ -67,14 +71,16 @@ Other IM platforms such as DingTalk can be added through the same channel-adapte npx -y github:xmanrui/dsh-im install ``` -Restart `dsh web`, then open **Settings → Plugins → IM Bot**. The installer replaces directly installed `dsh-feishu` and `dsh-weixin` entries in the profile with `dsh-im` without deleting channel data. Existing Feishu credentials and WeChat QR bindings continue to work. +Restart `dsh web`, then open **Settings → Plugins → IM Bot**. The installer replaces directly installed `dsh-feishu`, `dsh-weixin`, and `dsh-dingtalk` entries in the profile with `dsh-im` without deleting channel data. + +For DingTalk, scan the QR code with the DingTalk app and choose **Create a new bot** on the authorization page. That DingTalk-hosted page may currently display OpenClaw branding; the resulting connection, credentials, and messages are managed by DeepSeek Harness. Because the scan result does not identify the scanning user, send the bot a direct message and approve that sender locally before the message can enter Harness. ## Design - Registers a single **IM Bot** settings page in Harness. -- Uses Feishu and WeChat logos for channel navigation without enable/disable switches. +- Uses channel logos for WeChat, Feishu, and DingTalk navigation without enable/disable switches. - Keeps RPC endpoints, credentials, connection supervision, and session mappings isolated by channel. -- Sends only QR codes and redacted status data to the browser, never App Secrets or `bot_token` values. +- Sends only QR codes and redacted status data to the browser, never App Secrets, `bot_token`, DingTalk `client_secret`, or raw staff IDs. ## Local development @@ -84,4 +90,4 @@ npm run check node bin/dsh-im.mjs install --source . ``` -`npm run check` runs unit tests, builds the Host and Client artifacts, and verifies that the published package contains neither credentials nor standalone Feishu or WeChat settings-page registrations. +`npm run check` runs unit tests, builds the Host and Client artifacts, and verifies that the published package contains neither credentials nor standalone channel settings-page registrations. diff --git a/THIRD_PARTY_NOTICES.md b/THIRD_PARTY_NOTICES.md index a1175af..60dd601 100644 --- a/THIRD_PARTY_NOTICES.md +++ b/THIRD_PARTY_NOTICES.md @@ -1,7 +1,7 @@ # Third-party notices -This package composes [`@xmanrui/dsh-feishu`](https://github.com/xmanrui/dsh-feishu) at commit `aad650feabadd511241aa58b236d64273d5e397f` and [`@xmanrui/dsh-weixin`](https://github.com/xmanrui/dsh-weixin) at commit `76d076771b2c84fb4c5598c2344d486695eda080`, both under the MIT License. +This package composes [`@xmanrui/dsh-feishu`](https://github.com/xmanrui/dsh-feishu) at commit `aad650feabadd511241aa58b236d64273d5e397f`, [`@xmanrui/dsh-weixin`](https://github.com/xmanrui/dsh-weixin) at commit `76d076771b2c84fb4c5598c2344d486695eda080`, and [`@xmanrui/dsh-dingtalk`](https://github.com/xmanrui/dsh-dingtalk) at commit `67f7211b8011f0623b47ce9e01004bbab8e6614f`, all under the MIT License. The channel client components are adapted from those projects and preserve their security model: secrets remain in the DeepSeek Harness Host credential provider and never enter the browser-facing RPC response. -The WeChat mark uses path data published by Simple Icons under the CC0 1.0 Universal license. The Feishu mark is an original inline vector used for channel identification. Product names and logos remain trademarks of their respective owners. +The WeChat mark uses path data published by Simple Icons under the CC0 1.0 Universal license. The Feishu and DingTalk marks are inline vectors used for channel identification. Product names and logos remain trademarks of their respective owners. diff --git a/bin/dsh-im.mjs b/bin/dsh-im.mjs index cde924a..f2b46fd 100755 --- a/bin/dsh-im.mjs +++ b/bin/dsh-im.mjs @@ -7,7 +7,11 @@ import { spawnSync } from 'node:child_process'; const PACKAGE_NAME = '@xmanrui/dsh-im'; const DEFAULT_SOURCE = 'github:xmanrui/dsh-im'; -const LEGACY_PACKAGES = ['@xmanrui/dsh-feishu', '@xmanrui/dsh-weixin']; +const LEGACY_PACKAGES = [ + '@xmanrui/dsh-feishu', + '@xmanrui/dsh-weixin', + '@xmanrui/dsh-dingtalk', +]; function usage() { console.log(`Usage: @@ -80,7 +84,7 @@ try { } console.log('\nIM 机器人插件已安装。请重启 dsh web,然后打开「设置 → 插件 → IM机器人」。'); if (legacy.size > 0) { - console.log('已用 dsh-im 替换独立飞书/微信插件;原有凭据和扫码绑定保持不变。'); + console.log('已用 dsh-im 替换独立飞书/微信/钉钉插件;原有凭据和扫码绑定保持不变。'); } } else if (command === 'uninstall') { if (args.length > 0) throw new Error(`无法识别的参数:${args.join(' ')}`); diff --git a/docs/images/imbot.png b/docs/images/imbot.png index cbdd48b..cf9bfac 100644 Binary files a/docs/images/imbot.png and b/docs/images/imbot.png differ diff --git a/lib/client.js b/lib/client.js index 0a21e73..9cb1d80 100644 --- a/lib/client.js +++ b/lib/client.js @@ -40,10 +40,1128 @@ __export(index_exports, { name: () => name }); module.exports = __toCommonJS(index_exports); -var React3 = __toESM(require("react"), 1); +var React4 = __toESM(require("react"), 1); + +// node_modules/@xmanrui/dsh-dingtalk/plugin-src/client/api.js +var DINGTALK_RPC_CHANNEL = "/dingtalk"; +var DINGTALK_ENDPOINTS = Object.freeze({ + status: "connection.status", + beginProvisioning: "provision.begin", + pollProvisioning: "provision.poll", + cancelProvisioning: "provision.cancel", + reconnectBot: "bot.reconnect", + deleteBot: "bot.delete", + approveSender: "bot.sender.approve", + revokeSender: "bot.sender.revoke" +}); +var ACCOUNT_STATES = /* @__PURE__ */ new Set(["connected", "connecting", "offline", "error"]); +var SNAPSHOT_STATES = /* @__PURE__ */ new Set(["disconnected", "offline", "provisioning", "connected", "degraded"]); +var PROVISION_STATES = /* @__PURE__ */ new Set([ + "starting", + "pending", + "scanned", + "authorizing", + "creating", + "connecting", + "connected", + "expired", + "failed", + "cancelled" +]); +var HEALTH_STATES = /* @__PURE__ */ new Set(["healthy", "checking", "degraded", "offline"]); +var FORBIDDEN_ERROR_FIELDS = /(client[_-]?secret|secret[_-]?ref|device[_-]?code|app[_-]?secret|access[_-]?token|token)/i; +var QR_DATA_URL = /^data:image\/(?:png|webp);base64,[a-z\d+/]+={0,2}$/i; +var MAX_QR_SOURCE_LENGTH = 2 * 1024 * 1024; +function isRecord(value) { + return value !== null && typeof value === "object" && !Array.isArray(value); +} +function optionalString(value, maxLength = 240) { + if (typeof value !== "string") return void 0; + const trimmed = value.trim(); + return trimmed ? trimmed.slice(0, maxLength) : void 0; +} +function opaqueId(value) { + const id = optionalString(value, 128); + return id && /^[a-z\d_-]+$/i.test(id) ? id : void 0; +} +function timestamp(value) { + if (typeof value === "number" && Number.isFinite(value)) return value; + if (typeof value === "string" && value.trim()) { + const parsed = Date.parse(value); + return Number.isNaN(parsed) ? void 0 : parsed; + } + return void 0; +} +function nonNegativeInteger(value) { + const number = Number(value); + return Number.isSafeInteger(number) && number >= 0 ? number : 0; +} +function clamp(value, min, max, fallback) { + const number = Number(value); + return Number.isFinite(number) ? Math.min(max, Math.max(min, number)) : fallback; +} +function safeErrorCode(value, fallback) { + const code = optionalString(value, 80); + return code && /^[a-z][a-z\d_.:-]*$/i.test(code) && !FORBIDDEN_ERROR_FIELDS.test(code) ? code : fallback; +} +function sanitizeMessage(value, fallback) { + const message = optionalString(value, 480) ?? fallback; + if (FORBIDDEN_ERROR_FIELDS.test(message)) return fallback; + return message.replace(/([=:]\s*)[^\s,;,。]+/g, "$1\u2022\u2022\u2022\u2022\u2022\u2022").slice(0, 240); +} +function normalizeError(value, fallbackCode, fallbackMessage) { + if (!isRecord(value)) return void 0; + return { + code: safeErrorCode(value.code, fallbackCode), + message: sanitizeMessage(value.message, fallbackMessage) + }; +} +function unwrapRpcResult(result) { + if (!isRecord(result) || typeof result.ok !== "boolean") { + throw new Error("\u9489\u9489\u670D\u52A1\u8FD4\u56DE\u4E86\u65E0\u6CD5\u8BC6\u522B\u7684\u54CD\u5E94"); + } + if (!result.ok) { + const error = new Error(sanitizeMessage(result.error?.message, "\u9489\u9489\u64CD\u4F5C\u5931\u8D25")); + error.code = safeErrorCode(result.error?.code, "DINGTALK_RPC_ERROR"); + throw error; + } + return result.value; +} +function safeQrSource(value) { + if (typeof value !== "string" || value.length > MAX_QR_SOURCE_LENGTH) return void 0; + return QR_DATA_URL.test(value) ? value : void 0; +} +function normalizeProvisioning(value, now = Date.now()) { + const source = isRecord(value?.provisioning) ? value.provisioning : value; + if (!isRecord(source)) throw new Error("\u9489\u9489\u670D\u52A1\u6CA1\u6709\u8FD4\u56DE\u626B\u7801\u7ED1\u5B9A\u8FDB\u5EA6"); + const attemptId = opaqueId(source.attemptId); + if (!attemptId) throw new Error("\u9489\u9489\u626B\u7801\u670D\u52A1\u6CA1\u6709\u8FD4\u56DE\u6709\u6548\u7684\u7ED1\u5B9A\u4EFB\u52A1"); + const reportedStatus = optionalString(source.status, 32) ?? optionalString(source.state, 32); + const status = PROVISION_STATES.has(reportedStatus) ? reportedStatus : "failed"; + const expiresAt = timestamp(source.expiresAt) ?? now + clamp(source.expiresIn, 1, 2 * 60 * 60, 10 * 60) * 1e3; + const result = { + attemptId, + status, + expiresAt, + pollIntervalMs: clamp(source.pollIntervalMs, 1e3, 1e4, 3e3) + }; + const qrCodeDataUrl = safeQrSource(source.qrCodeDataUrl); + if (qrCodeDataUrl) result.qrCodeDataUrl = qrCodeDataUrl; + if (opaqueId(source.botId)) result.botId = opaqueId(source.botId); + if (source.alreadyConnected === true) result.alreadyConnected = true; + const error = normalizeError( + source.error, + "DINGTALK_PROVISION_FAILED", + "\u9489\u9489\u673A\u5668\u4EBA\u6CA1\u6709\u63A5\u5165\u5B8C\u6210" + ); + if (error) result.error = error; + return result; +} +function normalizeConversationType(value) { + if (value === "group" || value === "2") return "group"; + return "direct"; +} +function normalizePendingSender(value) { + if (!isRecord(value)) return void 0; + const requestId = opaqueId(value.requestId); + if (!requestId) return void 0; + return { + requestId, + displayName: optionalString(value.displayName, 80) ?? "\u5F85\u6279\u51C6\u4F7F\u7528\u8005", + senderIdMasked: optionalString(value.senderIdMasked, 120) ?? "\u8EAB\u4EFD\u5DF2\u9690\u85CF", + conversationType: normalizeConversationType(value.conversationType), + requestedAt: timestamp(value.requestedAt) + }; +} +function normalizeApprovedSender(value) { + if (!isRecord(value)) return void 0; + const senderKey = opaqueId(value.senderKey); + if (!senderKey) return void 0; + return { + senderKey, + displayName: optionalString(value.displayName, 80) ?? "\u5DF2\u6279\u51C6\u4F7F\u7528\u8005", + senderIdMasked: optionalString(value.senderIdMasked, 120) ?? "\u8EAB\u4EFD\u5DF2\u9690\u85CF", + approvedAt: timestamp(value.approvedAt) + }; +} +function uniqueBy(items, key) { + const seen = /* @__PURE__ */ new Set(); + return items.filter((item) => { + if (!item || seen.has(item[key])) return false; + seen.add(item[key]); + return true; + }); +} +function normalizeSenders(value) { + const source = isRecord(value) ? value : {}; + return { + pending: uniqueBy( + (Array.isArray(source.pending) ? source.pending : []).map(normalizePendingSender), + "requestId" + ), + approved: uniqueBy( + (Array.isArray(source.approved) ? source.approved : []).map(normalizeApprovedSender), + "senderKey" + ) + }; +} +function normalizeBot(value) { + if (!isRecord(value)) return void 0; + const botId = opaqueId(value.botId); + if (!botId) return void 0; + const bot = isRecord(value.bot) ? value.bot : {}; + const connected = value.connected === true; + const reportedState = ACCOUNT_STATES.has(value.state) ? value.state : "offline"; + const state = connected ? "connected" : reportedState === "connected" ? "connecting" : reportedState; + const health = isRecord(value.health) ? value.health : {}; + const stats = isRecord(value.stats) ? value.stats : {}; + const senderSource = value.senders ?? value.senderPolicy ?? value.access; + return { + botId, + state, + connected, + configured: value.configured !== false, + bot: { + name: optionalString(bot.name, 100) ?? "\u9489\u9489\u673A\u5668\u4EBA", + clientIdMasked: optionalString(bot.clientIdMasked, 140) ?? "\u5DF2\u5B89\u5168\u4FDD\u5B58" + }, + health: { + status: HEALTH_STATES.has(health.status) ? health.status : connected ? "healthy" : "offline", + summary: optionalString(health.summary, 200) ?? (connected ? "\u9489\u9489 Stream \u957F\u8FDE\u63A5\u8FD0\u884C\u6B63\u5E38" : "\u9489\u9489\u8FDE\u63A5\u5C1A\u672A\u5C31\u7EEA"), + lastCheckedAt: timestamp(health.lastCheckedAt), + lastConnectedAt: timestamp(health.lastConnectedAt) + }, + stats: { + messagesReceived: nonNegativeInteger(stats.messagesReceived), + messagesReplied: nonNegativeInteger(stats.messagesReplied) + }, + senders: normalizeSenders(senderSource), + error: normalizeError(value.error, "DINGTALK_ACCOUNT_ERROR", "\u9489\u9489\u8FDE\u63A5\u5C1A\u672A\u5C31\u7EEA") ?? null + }; +} +function normalizeSnapshot(value) { + const source = isRecord(value?.snapshot) ? value.snapshot : value; + if (!isRecord(source) || !Array.isArray(source.bots)) { + throw new Error("\u9489\u9489\u670D\u52A1\u6CA1\u6709\u8FD4\u56DE\u6709\u6548\u7684\u673A\u5668\u4EBA\u5217\u8868"); + } + const seen = /* @__PURE__ */ new Set(); + const bots = source.bots.map(normalizeBot).filter((bot) => { + if (!bot || seen.has(bot.botId)) return false; + seen.add(bot.botId); + return true; + }); + return { + schemaVersion: Number.isSafeInteger(source.schemaVersion) ? source.schemaVersion : 1, + revision: nonNegativeInteger(source.revision), + state: SNAPSHOT_STATES.has(source.state) ? source.state : "offline", + bots, + totals: { + configured: bots.length, + connected: bots.filter((bot) => bot.connected).length, + pendingApproval: bots.reduce((total, bot) => total + bot.senders.pending.length, 0) + }, + provisioning: source.provisioning ? normalizeProvisioning(source.provisioning) : null + }; +} +function presentError(error) { + return { + code: safeErrorCode(error?.code, "DINGTALK_ERROR"), + message: sanitizeMessage(error?.message, "\u9489\u9489\u64CD\u4F5C\u5931\u8D25\uFF0C\u8BF7\u7A0D\u540E\u91CD\u8BD5") + }; +} +function formatRemaining(milliseconds) { + const seconds = Math.max(0, Math.ceil(Number(milliseconds) / 1e3) || 0); + return `${String(Math.floor(seconds / 60)).padStart(2, "0")}:${String(seconds % 60).padStart(2, "0")}`; +} + +// node_modules/@xmanrui/dsh-dingtalk/plugin-src/client/index.js +var React = __toESM(require("react"), 1); + +// node_modules/@xmanrui/dsh-dingtalk/plugin-src/client/styles.js +var DINGTALK_STYLE_ID = "xmanrui-dsh-dingtalk-settings"; +var CSS = String.raw` +.ddt-page { + --ddt-accent: #1677ff; + --ddt-accent-deep: #0958d9; + --ddt-accent-wash: #eaf3ff; + --ddt-success: var(--dsw-alias-state-success-primary, #20a162); + --ddt-warning: var(--dsw-alias-state-warning-primary, #d97706); + --ddt-error: var(--dsw-alias-state-error-primary, #d54941); + width: 100%; + max-width: 880px; + display: flex; + flex-direction: column; + gap: 18px; + padding: 2px 0 28px; + color: var(--dsw-alias-label-primary, #1f2329); + box-sizing: border-box; +} +.ddt-page *, .ddt-page *::before, .ddt-page *::after { box-sizing: border-box; } +.ddt-heading { display: flex; align-items: flex-start; justify-content: space-between; gap: 20px; } +.ddt-headingCopy { min-width: 0; } +.ddt-heading h2, .ddt-heading p, .ddt-card h3, .ddt-card h4, .ddt-card p { margin: 0; } +.ddt-eyebrow { margin-bottom: 3px; color: var(--dsw-alias-label-tertiary, #8f959e); font-size: 12px; font-weight: 650; letter-spacing: .08em; text-transform: uppercase; } +.ddt-heading h2 { font-size: 20px; line-height: 28px; font-weight: 680; } +.ddt-heading p { margin-top: 5px; color: var(--dsw-alias-label-secondary, #646a73); font-size: 13px; line-height: 20px; white-space: nowrap; } +.ddt-tools, .ddt-actions { display: flex; align-items: center; flex-wrap: wrap; gap: 10px; } +.ddt-tools { justify-content: flex-end; } +.ddt-badge { min-height: 30px; display: inline-flex; align-items: center; gap: 7px; padding: 0 11px; border-radius: 999px; color: var(--dsw-alias-label-secondary, #646a73); background: var(--dsw-alias-fill-secondary, #f2f3f5); font-size: 12px; white-space: nowrap; } +.ddt-badge[data-tone="attention"] { color: #ad6800; background: #fff7e6; } +.ddt-dot { width: 8px; height: 8px; flex: none; border-radius: 50%; background: #aeb3bb; } +.ddt-dot[data-tone="success"] { background: var(--ddt-success); box-shadow: 0 0 0 3px color-mix(in srgb, var(--ddt-success) 14%, transparent); } +.ddt-dot[data-tone="warning"] { background: var(--ddt-warning); } +.ddt-dot[data-tone="error"] { background: var(--ddt-error); } +.ddt-button { min-height: 34px; display: inline-flex; align-items: center; justify-content: center; gap: 7px; padding: 0 13px; border: 1px solid var(--dsw-alias-line-border, #dfe1e5); border-radius: 8px; color: var(--dsw-alias-label-primary, #1f2329); background: var(--dsw-alias-bg-body, #fff); font: inherit; font-size: 13px; font-weight: 560; text-decoration: none; cursor: pointer; transition: border-color .15s ease, background .15s ease, transform .15s ease; } +.ddt-button:hover:not(:disabled) { border-color: #aeb3bb; background: var(--dsw-alias-fill-tertiary, #f7f8fa); } +.ddt-button:active:not(:disabled) { transform: translateY(1px); } +.ddt-button:focus-visible { outline: 2px solid color-mix(in srgb, var(--ddt-accent) 70%, white); outline-offset: 2px; } +.ddt-button:disabled { cursor: not-allowed; opacity: .55; } +.ddt-button[data-kind="primary"] { color: #fff; border-color: var(--ddt-accent); background: var(--ddt-accent); } +.ddt-button[data-kind="primary"]:hover:not(:disabled) { border-color: var(--ddt-accent-deep); background: var(--ddt-accent-deep); } +.ddt-button[data-kind="danger"] { color: var(--ddt-error); } +.ddt-button[data-kind="quiet"] { min-height: 30px; padding: 0 10px; border-color: transparent; background: transparent; } +.ddt-card { overflow: hidden; border: 1px solid var(--dsw-alias-line-border, #e5e6eb); border-radius: 14px; background: var(--dsw-alias-bg-body, #fff); box-shadow: 0 1px 2px rgb(31 35 41 / 3%); } +.ddt-cardBody { padding: 24px; } +.ddt-empty { min-height: 230px; display: grid; grid-template-columns: minmax(0, 1fr) 180px; align-items: center; gap: 30px; } +.ddt-empty h3 { margin: 8px 0; font-size: 18px; } +.ddt-empty p { max-width: 560px; color: var(--dsw-alias-label-secondary, #646a73); line-height: 1.65; } +.ddt-empty .ddt-actions { margin-top: 20px; } +.ddt-brandMark { width: 110px; height: 110px; display: grid; place-items: center; justify-self: center; border-radius: 28px; color: #fff; background: linear-gradient(145deg, #2997ff, var(--ddt-accent)); box-shadow: 0 18px 45px rgb(22 119 255 / 23%); } +.ddt-brandMark svg { filter: drop-shadow(0 3px 8px rgb(0 35 96 / 16%)); } +.ddt-qrLayout { display: grid; grid-template-columns: 300px minmax(0, 1fr); gap: 34px; align-items: center; } +.ddt-qrColumn { display: flex; flex-direction: column; align-items: center; gap: 12px; } +.ddt-qrFrame { position: relative; width: 270px; aspect-ratio: 1; display: grid; place-items: center; overflow: hidden; padding: 10px; border: 1px solid var(--dsw-alias-line-border, #e5e6eb); border-radius: 16px; background: #fff; } +.ddt-qrFrame::before { content: ''; position: absolute; inset: 6px; border: 1px solid rgb(22 119 255 / 10%); border-radius: 11px; pointer-events: none; } +.ddt-qrFrame img { display: block; width: 100%; height: 100%; object-fit: contain; } +.ddt-qrFallback { padding: 24px; color: #646a73; text-align: center; } +.ddt-expired { position: absolute; inset: 0; display: grid; place-items: center; padding: 30px; color: #fff; text-align: center; font-weight: 650; white-space: pre-line; background: rgb(31 35 41 / 76%); backdrop-filter: blur(3px); } +.ddt-countdown { width: 270px; color: var(--dsw-alias-label-secondary, #646a73); font-size: 12px; } +.ddt-countdownTop { display: flex; justify-content: space-between; margin-bottom: 6px; } +.ddt-countdown strong { color: var(--dsw-alias-label-primary, #1f2329); font-variant-numeric: tabular-nums; } +.ddt-progress { height: 4px; overflow: hidden; border-radius: 99px; background: #eef0f3; } +.ddt-progress span { display: block; width: var(--ddt-progress); height: 100%; background: var(--ddt-accent); transition: width .2s linear; } +.ddt-stateLabel { display: inline-flex; align-items: center; gap: 8px; color: var(--dsw-alias-label-secondary, #646a73); font-size: 12px; font-weight: 600; } +.ddt-qrCopy h3 { margin: 9px 0 8px; font-size: 18px; } +.ddt-qrCopy > p { color: var(--dsw-alias-label-secondary, #646a73); line-height: 1.65; } +.ddt-steps { margin: 18px 0 16px; padding: 0; list-style: none; counter-reset: ddt-step; } +.ddt-steps li { position: relative; min-height: 28px; padding: 3px 0 3px 36px; color: var(--dsw-alias-label-secondary, #646a73); font-size: 13px; line-height: 22px; counter-increment: ddt-step; } +.ddt-steps li::before { content: counter(ddt-step); position: absolute; left: 0; top: 1px; width: 26px; height: 26px; display: grid; place-items: center; border-radius: 8px; color: var(--ddt-accent-deep); background: var(--ddt-accent-wash); font-size: 12px; font-weight: 700; } +.ddt-brandNotice { margin: 0 0 18px; padding: 10px 12px; border-left: 3px solid #91caff; border-radius: 0 8px 8px 0; color: var(--dsw-alias-label-secondary, #646a73); background: #f5f9ff; font-size: 12px; line-height: 1.55; } +.ddt-loading { padding: 38px; color: var(--dsw-alias-label-secondary, #646a73); text-align: center; } +.ddt-loading h3 { margin: 0 0 7px; color: var(--dsw-alias-label-primary, #1f2329); font-size: 17px; } +.ddt-loading p { line-height: 1.6; } +.ddt-spinner { width: 24px; height: 24px; margin: 0 auto 13px; border: 3px solid #e6e8eb; border-top-color: var(--ddt-accent); border-radius: 50%; animation: ddt-spin .8s linear infinite; } +.ddt-statusNotice, .ddt-inlineError { display: flex; align-items: flex-start; gap: 10px; padding: 13px 15px; border: 1px solid color-mix(in srgb, var(--ddt-error) 28%, transparent); border-radius: 10px; color: var(--ddt-error); background: color-mix(in srgb, var(--ddt-error) 7%, transparent); font-size: 13px; } +.ddt-inlineError { flex-direction: column; padding: 22px; } +.ddt-inlineError h3 { font-size: 17px; } +.ddt-inlineError p { line-height: 1.55; } +.ddt-errorCode { font: 11px ui-monospace, SFMono-Regular, monospace; opacity: .8; } +.ddt-listHeading { display: flex; align-items: center; justify-content: space-between; margin: 2px 0 9px; } +.ddt-listHeading h3 { margin: 0; font-size: 14px; } +.ddt-listHeading span { color: var(--dsw-alias-label-tertiary, #8f959e); font-size: 12px; } +.ddt-list { display: grid; gap: 12px; margin: 0; padding: 0; list-style: none; } +.ddt-accountTop { display: flex; align-items: flex-start; justify-content: space-between; gap: 16px; } +.ddt-accountIdentity { min-width: 0; display: flex; align-items: center; gap: 12px; } +.ddt-avatar { width: 42px; height: 42px; display: grid; place-items: center; flex: none; border-radius: 12px; color: #fff; background: linear-gradient(145deg, #2997ff, var(--ddt-accent)); } +.ddt-accountIdentity h3 { overflow: hidden; font-size: 15px; text-overflow: ellipsis; white-space: nowrap; } +.ddt-accountIdentity p { margin-top: 4px; color: var(--dsw-alias-label-secondary, #646a73); font: 12px ui-monospace, SFMono-Regular, monospace; } +.ddt-health { display: inline-flex; align-items: center; gap: 7px; color: var(--dsw-alias-label-secondary, #646a73); font-size: 12px; white-space: nowrap; } +.ddt-metrics { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 12px; margin: 20px 0; } +.ddt-metric { padding: 12px 14px; border-radius: 9px; background: var(--dsw-alias-fill-tertiary, #f7f8fa); } +.ddt-metric dt { color: var(--dsw-alias-label-tertiary, #8f959e); font-size: 11px; } +.ddt-metric dd { overflow: hidden; margin: 5px 0 0; font-size: 13px; text-overflow: ellipsis; white-space: nowrap; } +.ddt-access { margin-top: 18px; padding: 15px 16px; border: 1px solid var(--dsw-alias-line-divider, #eef0f3); border-radius: 11px; background: color-mix(in srgb, var(--ddt-accent-wash) 35%, var(--dsw-alias-bg-body, #fff)); } +.ddt-accessHeader { display: flex; align-items: flex-start; justify-content: space-between; gap: 16px; } +.ddt-accessHeader h4 { font-size: 13px; } +.ddt-accessHeader p { margin-top: 4px; color: var(--dsw-alias-label-tertiary, #8f959e); font-size: 11px; line-height: 1.5; } +.ddt-accessCount { flex: none; padding: 3px 8px; border-radius: 99px; color: var(--dsw-alias-label-secondary, #646a73); background: var(--dsw-alias-fill-secondary, #f2f3f5); font-size: 11px; } +.ddt-accessCount[data-pending="true"] { color: #ad6800; background: #fff1d6; } +.ddt-senderList { display: grid; gap: 8px; margin: 12px 0 0; padding: 0; list-style: none; } +.ddt-sender { min-height: 42px; display: flex; align-items: center; justify-content: space-between; gap: 12px; padding: 8px 10px; border-radius: 8px; background: var(--dsw-alias-bg-body, #fff); } +.ddt-senderIdentity { min-width: 0; } +.ddt-senderIdentity strong { display: block; overflow: hidden; font-size: 12px; font-weight: 600; text-overflow: ellipsis; white-space: nowrap; } +.ddt-senderIdentity span { display: block; overflow: hidden; margin-top: 3px; color: var(--dsw-alias-label-tertiary, #8f959e); font: 10px ui-monospace, SFMono-Regular, monospace; text-overflow: ellipsis; white-space: nowrap; } +.ddt-approvedLabel { margin-top: 13px; color: var(--dsw-alias-label-tertiary, #8f959e); font-size: 11px; font-weight: 600; } +.ddt-noSenders { margin-top: 11px !important; color: var(--dsw-alias-label-tertiary, #8f959e); font-size: 11px; } +.ddt-accountFooter { display: flex; align-items: center; justify-content: space-between; gap: 15px; padding-top: 16px; border-top: 1px solid var(--dsw-alias-line-divider, #eef0f3); } +.ddt-summary { color: var(--dsw-alias-label-secondary, #646a73); font-size: 12px; } +.ddt-confirm { padding: 18px 24px; border-top: 1px solid color-mix(in srgb, var(--ddt-error) 25%, transparent); background: color-mix(in srgb, var(--ddt-error) 5%, transparent); } +.ddt-confirm strong { display: block; margin-bottom: 6px; font-size: 14px; } +.ddt-confirm p { color: var(--dsw-alias-label-secondary, #646a73); font-size: 12px; line-height: 1.55; } +.ddt-confirm .ddt-actions { margin-top: 13px; } +.ddt-visuallyHidden { position: absolute !important; width: 1px; height: 1px; overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; } +@keyframes ddt-spin { to { transform: rotate(360deg); } } +@media (max-width: 720px) { + .ddt-heading, .ddt-accountTop, .ddt-accountFooter { flex-direction: column; align-items: stretch; } + .ddt-heading p { white-space: normal; } + .ddt-tools { justify-content: flex-start; } + .ddt-empty { grid-template-columns: minmax(0, 1fr); } + .ddt-brandMark { display: none; } + .ddt-qrLayout { grid-template-columns: minmax(0, 1fr); justify-items: center; } + .ddt-qrCopy { width: 100%; } + .ddt-metrics { grid-template-columns: minmax(0, 1fr); } + .ddt-cardBody { padding: 20px; } + .ddt-sender { align-items: flex-start; } +} +@media (prefers-reduced-motion: reduce) { + .ddt-page *, .ddt-page *::before, .ddt-page *::after { animation-duration: .01ms !important; transition-duration: .01ms !important; } +} +`; +function installDingtalkStyles() { + if (typeof document === "undefined") return () => { + }; + const existing = document.querySelector(`style[data-plugin-css="${DINGTALK_STYLE_ID}"]`); + if (existing) return () => { + }; + const style = document.createElement("style"); + style.dataset.plugin = "@xmanrui/dsh-dingtalk"; + style.dataset.pluginCss = DINGTALK_STYLE_ID; + style.textContent = CSS; + document.head.appendChild(style); + return () => style.remove(); +} + +// node_modules/@xmanrui/dsh-dingtalk/plugin-src/client/index.js +var h = React.createElement; +var ACTIVE_PROVISION_STATES = /* @__PURE__ */ new Set(["pending", "scanned", "authorizing", "creating", "connecting"]); +function DingtalkIcon({ size = 28 }) { + return h("svg", { + width: size, + height: size, + viewBox: "0 0 48 48", + fill: "none", + xmlns: "http://www.w3.org/2000/svg", + "aria-hidden": "true", + focusable: "false" + }, h("path", { + fill: "currentColor", + d: "M37.05 22.783c-6.758-5.216-14.378-12.128-22.73-19.538-.655-.585-1.242-.354-1.536.42-1.88 4.973-.058 9.386 2.889 11.932s7.368 4.912 10.058 6.155c.105.049.013.203-.093.163-4.953-2.182-8.397-3.765-13.07-7.368-.497-.388-1.01-.242-1.07.521-.384 4.748 2.657 8.483 6.058 9.745 2.1.781 4.398 1.212 6.53 1.474.109.015.084.178-.027.178-2.747.01-6.058-.654-8.935-1.751-.606-.233-.818.25-.722.633.491 2.008 2.974 5.076 6.926 5.73a12 12 0 0 0 2.228.115c.164 0 .208.089.154.217q-2.685 4.6-2.803 4.797c-.091.152-.036.275.156.275h3.543c.164 0 .264.106.18.246l-4.958 8.196c-.191.328.035.565.395.301s15.212-11.133 15.636-11.448c.195-.142.148-.327-.124-.327h-3.18c-.206 0-.252-.14-.111-.28.14-.141 3.602-3.594 4.837-4.888 1.283-1.35 1.938-3.825-.231-5.498" + })); +} +var Button = React.forwardRef(function Button2({ children, kind = "secondary", className = "", ...props }, ref) { + return h("button", { + ...props, + ref, + type: "button", + className: `ddt-button ${className}`.trim(), + "data-kind": kind + }, children); +}); +function Heading({ totals, adding, busy, onAdd, addButtonRef }) { + return h( + "div", + { className: "ddt-heading" }, + h( + "div", + { className: "ddt-headingCopy" }, + h("div", { className: "ddt-eyebrow" }, "Channel"), + h("h2", null, "\u9489\u9489\u673A\u5668\u4EBA"), + h("p", null, "\u901A\u8FC7\u626B\u7801\u628A\u9489\u9489\u673A\u5668\u4EBA\u63A5\u5165 DeepSeek Harness") + ), + h( + "div", + { className: "ddt-tools" }, + totals.configured > 0 ? h( + "div", + { className: "ddt-badge" }, + h("span", { + className: "ddt-dot", + "data-tone": totals.connected > 0 ? "success" : "warning" + }), + h("span", null, `${totals.connected} / ${totals.configured} \u5728\u7EBF`) + ) : null, + totals.pendingApproval > 0 ? h( + "div", + { className: "ddt-badge", "data-tone": "attention" }, + `${totals.pendingApproval} \u4E2A\u5F85\u6279\u51C6` + ) : null, + h("div", { + className: "ddt-badge", + title: "\u5E94\u7528\u5BC6\u94A5\u53EA\u5199\u5165 Harness Host \u51ED\u636E\u670D\u52A1\uFF0C\u4E0D\u4F1A\u53D1\u9001\u5230\u6D4F\u89C8\u5668" + }, "\u51ED\u636E\u4EC5\u4FDD\u5B58\u5728\u672C\u673A"), + h(Button, { + kind: "primary", + onClick: onAdd, + disabled: adding || busy, + ref: addButtonRef + }, adding ? "\u6B63\u5728\u63A5\u5165" : "\u626B\u7801\u63A5\u5165\u9489\u9489") + ) + ); +} +function LoadingView() { + return h( + "div", + { className: "ddt-card ddt-loading", "aria-busy": "true" }, + h("div", { className: "ddt-spinner" }), + h("span", null, "\u6B63\u5728\u8BFB\u53D6\u9489\u9489\u8FDE\u63A5\u72B6\u6001\u2026") + ); +} +function EmptyView({ busy, onStart }) { + return h( + "div", + { className: "ddt-card" }, + h( + "div", + { className: "ddt-cardBody ddt-empty" }, + h( + "div", + null, + h( + "div", + { className: "ddt-stateLabel" }, + h("span", { className: "ddt-dot" }), + h("span", null, "\u5C1A\u672A\u63A5\u5165\u9489\u9489\u673A\u5668\u4EBA") + ), + h("h3", null, "\u626B\u4E00\u6B21\u7801\uFF0C\u81EA\u52A8\u521B\u5EFA\u5E76\u8FDE\u63A5\u673A\u5668\u4EBA"), + h("p", null, "\u6388\u6743\u7531\u9489\u9489\u5B98\u65B9\u9875\u9762\u5B8C\u6210\u3002\u521B\u5EFA\u6210\u529F\u540E\uFF0C\u5E94\u7528\u51ED\u636E\u4F1A\u76F4\u63A5\u5199\u5165 Harness Host\uFF0C\u672C\u9875\u9762\u53EA\u63A5\u6536\u8131\u654F\u7684\u8FDE\u63A5\u72B6\u6001\u3002"), + h( + "div", + { className: "ddt-actions" }, + h( + Button, + { kind: "primary", onClick: onStart, disabled: busy }, + busy ? "\u6B63\u5728\u751F\u6210\u4E8C\u7EF4\u7801\u2026" : "\u751F\u6210\u9489\u9489\u4E8C\u7EF4\u7801" + ) + ) + ), + h( + "div", + { className: "ddt-brandMark", "aria-hidden": "true" }, + h(DingtalkIcon, { size: 68 }) + ) + ) + ); +} +function QrPanel({ provision, now, busy, onRefresh, onCancel }) { + const [imageFailed, setImageFailed] = React.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); + React.useEffect(() => setImageFailed(false), [source]); + return h( + "div", + { className: "ddt-card" }, + h( + "div", + { className: "ddt-cardBody ddt-qrLayout" }, + h( + "div", + { className: "ddt-qrColumn" }, + h( + "div", + { className: "ddt-qrFrame" }, + source && !imageFailed ? h("img", { + src: source, + alt: "\u7528\u4E8E\u628A\u9489\u9489\u673A\u5668\u4EBA\u63A5\u5165 DeepSeek Harness \u7684\u4E00\u6B21\u6027\u4E8C\u7EF4\u7801", + onError: () => setImageFailed(true) + }) : h("div", { className: "ddt-qrFallback" }, "\u4E8C\u7EF4\u7801\u56FE\u7247\u672A\u5C31\u7EEA\uFF0C\u8BF7\u91CD\u65B0\u751F\u6210\u3002"), + expired ? h("div", { className: "ddt-expired" }, "\u4E8C\u7EF4\u7801\u5DF2\u8FC7\u671F\n\u8BF7\u91CD\u65B0\u751F\u6210") : null + ), + h( + "div", + { className: "ddt-countdown" }, + h( + "div", + { className: "ddt-countdownTop" }, + h("span", null, "\u4E8C\u7EF4\u7801\u6709\u6548\u65F6\u95F4"), + h("strong", null, formatRemaining(remaining)) + ), + h( + "div", + { className: "ddt-progress", "aria-hidden": "true" }, + h("span", { style: { "--ddt-progress": `${progress}%` } }) + ) + ) + ), + h( + "div", + { className: "ddt-qrCopy" }, + h( + "div", + { className: "ddt-stateLabel" }, + h("span", { className: "ddt-dot", "data-tone": expired ? "error" : "warning" }), + h("span", null, expired ? "\u4E8C\u7EF4\u7801\u5DF2\u5931\u6548" : "\u7B49\u5F85\u9489\u9489\u626B\u7801\u6388\u6743") + ), + h("h3", null, expired ? "\u91CD\u65B0\u751F\u6210\u4E8C\u7EF4\u7801\u540E\u7EE7\u7EED" : "\u4F7F\u7528\u9489\u9489 App \u5B8C\u6210\u673A\u5668\u4EBA\u6388\u6743"), + h("p", null, "\u626B\u7801\u540E\u4E0D\u9700\u8981\u624B\u5DE5\u590D\u5236\u5E94\u7528\u6807\u8BC6\u6216\u5BC6\u94A5\uFF0C\u6388\u6743\u7ED3\u679C\u4F1A\u5B89\u5168\u8FD4\u56DE\u5230\u672C\u673A Host\u3002"), + h( + "ol", + { className: "ddt-steps" }, + h("li", null, "\u6253\u5F00\u9489\u9489 App\uFF0C\u626B\u63CF\u5DE6\u4FA7\u4E8C\u7EF4\u7801"), + h("li", null, "\u5728\u6388\u6743\u9875\u70B9\u51FB\u201C\u4E00\u952E\u521B\u5EFA\u65B0\u673A\u5668\u4EBA\u201D"), + h("li", null, "\u4FDD\u6301\u672C\u9875\u6253\u5F00\uFF0C\u7B49\u5F85\u673A\u5668\u4EBA\u81EA\u52A8\u8FDE\u63A5") + ), + h( + "div", + { className: "ddt-brandNotice" }, + "\u9489\u9489\u5B98\u65B9\u6388\u6743\u9875\u76EE\u524D\u53EF\u80FD\u663E\u793A OpenClaw \u54C1\u724C\uFF0C\u8FD9\u662F\u5B98\u65B9\u8FDE\u63A5\u5668\u6388\u6743\u9875\u9762\uFF0C\u4E0D\u5F71\u54CD\u673A\u5668\u4EBA\u63A5\u5165 DeepSeek Harness\u3002" + ), + h( + "div", + { className: "ddt-actions" }, + expired ? h(Button, { kind: "primary", onClick: onRefresh, disabled: busy }, "\u91CD\u65B0\u751F\u6210\u4E8C\u7EF4\u7801") : null, + !expired ? h(Button, { onClick: onRefresh, disabled: busy }, "\u6362\u4E00\u4E2A\u4E8C\u7EF4\u7801") : null, + h(Button, { onClick: onCancel, disabled: busy }, "\u53D6\u6D88") + ) + ) + ) + ); +} +function ProgressPanel({ status, busy, onCancel }) { + const connecting = status === "connecting"; + const creating = status === "creating"; + return h( + "div", + { className: "ddt-card ddt-loading", "aria-busy": "true" }, + h("div", { className: "ddt-spinner" }), + h("h3", null, connecting ? "\u673A\u5668\u4EBA\u5DF2\u521B\u5EFA\uFF0C\u6B63\u5728\u5EFA\u7ACB\u6D88\u606F\u8FDE\u63A5" : creating ? "\u6388\u6743\u5DF2\u786E\u8BA4\uFF0C\u6B63\u5728\u521B\u5EFA\u9489\u9489\u673A\u5668\u4EBA" : "\u6B63\u5728\u786E\u8BA4\u9489\u9489\u6388\u6743"), + h("p", null, connecting ? "\u6B63\u5728\u68C0\u67E5\u9489\u9489 Stream \u957F\u8FDE\u63A5\uFF0C\u6210\u529F\u540E\u4F1A\u81EA\u52A8\u663E\u793A\u4E3A\u5728\u7EBF\u3002" : "\u8BF7\u52FF\u5173\u95ED\u672C\u9875\uFF0C\u9489\u9489\u5B8C\u6210\u6388\u6743\u540E\u5C06\u81EA\u52A8\u7EE7\u7EED\u3002"), + h( + "div", + { className: "ddt-actions", style: { justifyContent: "center", marginTop: 14 } }, + h(Button, { onClick: onCancel, disabled: busy }, "\u53D6\u6D88\u63A5\u5165") + ) + ); +} +function ProvisionError({ provision, busy, onRetry, onClose }) { + const error = provision.error ?? { + code: "DINGTALK_PROVISION_FAILED", + message: "\u9489\u9489\u673A\u5668\u4EBA\u6CA1\u6709\u63A5\u5165\u5B8C\u6210" + }; + return h( + "div", + { className: "ddt-card" }, + h( + "div", + { className: "ddt-inlineError", role: "alert" }, + h("h3", null, provision.status === "expired" ? "\u4E8C\u7EF4\u7801\u5DF2\u8FC7\u671F" : "\u9489\u9489\u673A\u5668\u4EBA\u6CA1\u6709\u63A5\u5165\u5B8C\u6210"), + h("p", null, error.message), + h("span", { className: "ddt-errorCode" }, error.code), + h( + "div", + { className: "ddt-actions" }, + h(Button, { kind: "primary", onClick: onRetry, disabled: busy }, "\u91CD\u65B0\u751F\u6210\u4E8C\u7EF4\u7801"), + h(Button, { onClick: onClose, disabled: busy }, "\u5173\u95ED") + ) + ) + ); +} +function checkedTime(value) { + if (!value) return "\u5C1A\u672A\u68C0\u67E5"; + try { + return new Intl.DateTimeFormat("zh-CN", { + hour: "2-digit", + minute: "2-digit", + second: "2-digit" + }).format(new Date(value)); + } catch { + return "\u521A\u521A"; + } +} +function SenderAccess({ account, busy, onApprove, onRevoke }) { + const pending = account.senders.pending; + const approved = account.senders.approved; + return h( + "section", + { className: "ddt-access", "aria-label": `${account.bot.name}\u4F7F\u7528\u8005\u6743\u9650` }, + h( + "div", + { className: "ddt-accessHeader" }, + h( + "div", + null, + h("h4", null, "\u5141\u8BB8\u4F7F\u7528\u673A\u5668\u4EBA\u7684\u9489\u9489\u8D26\u53F7"), + h("p", null, "\u65B0\u4F7F\u7528\u8005\u9996\u6B21\u53D1\u6D88\u606F\u540E\u4F1A\u51FA\u73B0\u5728\u8FD9\u91CC\uFF0C\u987B\u5728\u672C\u673A\u660E\u786E\u6279\u51C6\u3002") + ), + h("span", { + className: "ddt-accessCount", + "data-pending": pending.length > 0 ? "true" : void 0 + }, pending.length > 0 ? `${pending.length} \u4E2A\u5F85\u6279\u51C6` : `${approved.length} \u4E2A\u5DF2\u6279\u51C6`) + ), + pending.length > 0 ? h("ul", { className: "ddt-senderList" }, pending.map((sender) => h( + "li", + { + className: "ddt-sender", + key: sender.requestId + }, + h( + "div", + { className: "ddt-senderIdentity" }, + h("strong", null, sender.displayName), + h("span", null, `${sender.senderIdMasked} \xB7 ${sender.conversationType === "group" ? "\u7FA4\u804A" : "\u5355\u804A"}`) + ), + h(Button, { + kind: "primary", + onClick: () => onApprove(sender), + disabled: Boolean(busy) + }, busy === `approve:${sender.requestId}` ? "\u6279\u51C6\u4E2D\u2026" : "\u6279\u51C6\u4F7F\u7528") + ))) : null, + approved.length > 0 ? h( + React.Fragment, + null, + h("div", { className: "ddt-approvedLabel" }, "\u5DF2\u6279\u51C6"), + h("ul", { className: "ddt-senderList" }, approved.map((sender) => h( + "li", + { + className: "ddt-sender", + key: sender.senderKey + }, + h( + "div", + { className: "ddt-senderIdentity" }, + h("strong", null, sender.displayName), + h("span", null, sender.senderIdMasked) + ), + h(Button, { + kind: "quiet", + onClick: () => onRevoke(sender), + disabled: Boolean(busy) + }, busy === `revoke:${sender.senderKey}` ? "\u64A4\u9500\u4E2D\u2026" : "\u64A4\u9500") + ))) + ) : null, + pending.length === 0 && approved.length === 0 ? h("p", { className: "ddt-noSenders" }, "\u5C1A\u65E0\u4F7F\u7528\u8BF7\u6C42\u3002\u673A\u5668\u4EBA\u6536\u5230\u7B2C\u4E00\u6761\u6D88\u606F\u540E\uFF0C\u53EF\u5728\u6B64\u6279\u51C6\u53D1\u9001\u8005\u3002") : null + ); +} +function RemoveConfirmation({ account, busy, onConfirm, onCancel }) { + const cancelRef = React.useRef(null); + React.useEffect(() => cancelRef.current?.focus(), []); + return h( + "div", + { + className: "ddt-confirm", + role: "alertdialog", + "aria-label": `\u79FB\u9664${account.bot.name}`, + onKeyDown: (event) => { + if (event.key === "Escape" && !busy) onCancel(); + } + }, + h("strong", null, `\u4ECE DeepSeek Harness \u79FB\u9664\u201C${account.bot.name}\u201D\uFF1F`), + h("p", null, "\u8FD9\u4F1A\u505C\u6B62\u6D88\u606F\u8FDE\u63A5\uFF0C\u5E76\u5220\u9664\u672C\u673A\u4FDD\u5B58\u7684\u5E94\u7528\u51ED\u636E\u3001\u673A\u5668\u4EBA\u914D\u7F6E\u3001\u4F1A\u8BDD\u6620\u5C04\u53CA\u4F7F\u7528\u8005\u6279\u51C6\u8BB0\u5F55\u3002\u9489\u9489\u5F00\u653E\u5E73\u53F0\u4E2D\u7684\u673A\u5668\u4EBA\u4E0D\u4F1A\u88AB\u81EA\u52A8\u5220\u9664\u3002"), + h( + "div", + { className: "ddt-actions" }, + h(Button, { ref: cancelRef, onClick: onCancel, disabled: busy }, "\u4FDD\u7559\u673A\u5668\u4EBA"), + h( + Button, + { kind: "danger", onClick: onConfirm, disabled: busy }, + busy ? "\u6B63\u5728\u79FB\u9664\u2026" : "\u786E\u8BA4\u79FB\u9664\u63A5\u5165" + ) + ) + ); +} +function AccountCard({ + account, + busy, + removing, + onReconnect, + onRequestRemove, + onConfirmRemove, + onCancelRemove, + onApprove, + onRevoke +}) { + const state = busy === "reconnect" ? "connecting" : account.state; + const tone = account.connected ? "success" : state === "error" ? "error" : "warning"; + const stateLabel = account.connected ? "\u8FD0\u884C\u6B63\u5E38" : state === "connecting" ? "\u6B63\u5728\u8FDE\u63A5" : "\u8FDE\u63A5\u672A\u5C31\u7EEA"; + return h( + "article", + { className: "ddt-card", tabIndex: -1, "data-bot-id": account.botId }, + h( + "div", + { className: "ddt-cardBody" }, + h( + "div", + { className: "ddt-accountTop" }, + h( + "div", + { className: "ddt-accountIdentity" }, + h("div", { className: "ddt-avatar", "aria-hidden": "true" }, h(DingtalkIcon, { size: 29 })), + h( + "div", + null, + h("h3", { title: account.bot.name }, account.bot.name), + h("p", { title: account.bot.clientIdMasked }, account.bot.clientIdMasked) + ) + ), + h( + "div", + { className: "ddt-health" }, + h("span", { className: "ddt-dot", "data-tone": tone }), + h("span", null, stateLabel) + ) + ), + h( + "dl", + { className: "ddt-metrics" }, + h( + "div", + { className: "ddt-metric" }, + h("dt", null, "\u6D88\u606F\u901A\u9053"), + h("dd", null, account.connected ? "Stream \u957F\u8FDE\u63A5" : "\u79BB\u7EBF") + ), + h( + "div", + { className: "ddt-metric" }, + h("dt", null, "\u6536\u5230 / \u56DE\u590D"), + h("dd", null, `${account.stats.messagesReceived} / ${account.stats.messagesReplied}`) + ), + h( + "div", + { className: "ddt-metric" }, + h("dt", null, "\u6700\u8FD1\u68C0\u67E5"), + h("dd", null, checkedTime(account.health.lastCheckedAt)) + ) + ), + h(SenderAccess, { account, busy, onApprove, onRevoke }), + h( + "div", + { className: "ddt-accountFooter" }, + h("div", { className: "ddt-summary" }, account.error?.message ?? account.health.summary), + h( + "div", + { className: "ddt-actions" }, + h( + Button, + { onClick: onReconnect, disabled: Boolean(busy) }, + busy === "reconnect" ? "\u68C0\u67E5\u4E2D\u2026" : account.connected ? "\u68C0\u67E5\u8FDE\u63A5" : "\u91CD\u8BD5\u8FDE\u63A5" + ), + h( + Button, + { kind: "danger", onClick: onRequestRemove, disabled: Boolean(busy) }, + "\u79FB\u9664\u63A5\u5165" + ) + ) + ) + ), + removing ? h(RemoveConfirmation, { + account, + busy: busy === "delete", + onConfirm: onConfirmRemove, + onCancel: onCancelRemove + }) : null + ); +} +function AccountList(props) { + return h( + "section", + null, + h( + "div", + { className: "ddt-listHeading" }, + h("h3", null, "\u5DF2\u63A5\u5165\u7684\u9489\u9489\u673A\u5668\u4EBA"), + h("span", null, `${props.bots.length} \u4E2A`) + ), + h("ul", { className: "ddt-list" }, props.bots.map((account) => h( + "li", + { key: account.botId }, + h(AccountCard, { + account, + busy: props.busyByBot[account.botId], + removing: props.removeTarget === account.botId, + onReconnect: () => props.onReconnect(account), + onRequestRemove: () => props.onRequestRemove(account), + onConfirmRemove: () => props.onConfirmRemove(account), + onCancelRemove: props.onCancelRemove, + onApprove: (sender) => props.onApprove(account, sender), + onRevoke: (sender) => props.onRevoke(account, sender) + }) + ))) + ); +} +var EMPTY_TOTALS = Object.freeze({ configured: 0, connected: 0, pendingApproval: 0 }); +function DingtalkSettingsTab({ rpcCall }) { + const [model, setModel] = React.useState({ + phase: "loading", + bots: [], + totals: EMPTY_TOTALS, + revision: 0, + error: null + }); + const [provision, setProvision] = React.useState(null); + const [busy, setBusy] = React.useState(false); + const [busyByBot, setBusyByBot] = React.useState({}); + const [removeTarget, setRemoveTarget] = React.useState(null); + const [notice, setNotice] = React.useState(""); + const [now, setNow] = React.useState(() => Date.now()); + const addButtonRef = React.useRef(null); + React.useEffect(() => installDingtalkStyles(), []); + const announce = React.useCallback((message) => { + setNotice(""); + if (message) window.requestAnimationFrame(() => setNotice(message)); + }, []); + const invoke = React.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 = React.useCallback(async ({ signal, silent = false } = {}) => { + if (!silent) setModel((current) => ({ ...current, phase: "loading", error: null })); + try { + const snapshot = normalizeSnapshot(await invoke(DINGTALK_ENDPOINTS.status, {}, signal)); + setModel({ + phase: "ready", + bots: snapshot.bots, + totals: snapshot.totals, + revision: snapshot.revision, + error: null + }); + if (snapshot.provisioning) { + setProvision((current) => !current || current.attemptId === snapshot.provisioning.attemptId ? { + ...current, + ...snapshot.provisioning, + durationMs: current?.durationMs ?? Math.max(1, snapshot.provisioning.expiresAt - Date.now()) + } : current); + } + return snapshot; + } catch (error) { + if (error?.name === "AbortError") return void 0; + setModel((current) => ({ + ...current, + phase: silent && current.phase === "ready" ? "ready" : "error", + error: presentError(error) + })); + return void 0; + } + }, [invoke]); + React.useEffect(() => { + const controller = new AbortController(); + void loadStatus({ signal: controller.signal }); + return () => controller.abort(); + }, [loadStatus]); + React.useEffect(() => { + if (model.phase !== "ready") return void 0; + const controller = new AbortController(); + let running = false; + const timer = window.setInterval(async () => { + if (running) return; + running = true; + await loadStatus({ signal: controller.signal, silent: true }); + running = false; + }, 15e3); + return () => { + controller.abort(); + window.clearInterval(timer); + }; + }, [loadStatus, model.phase]); + React.useEffect(() => { + if (!provision || !ACTIVE_PROVISION_STATES.has(provision.status)) return void 0; + const timer = window.setInterval(() => setNow(Date.now()), 1e3); + return () => window.clearInterval(timer); + }, [provision?.attemptId, provision?.status]); + const startProvisioning = React.useCallback(async ({ replace = false } = {}) => { + setBusy(true); + try { + if (replace && provision?.attemptId) { + await invoke(DINGTALK_ENDPOINTS.cancelProvisioning, { + attemptId: provision.attemptId + }); + } + setProvision({ status: "starting" }); + const started = normalizeProvisioning(await invoke( + DINGTALK_ENDPOINTS.beginProvisioning, + { locale: "zh-CN" } + )); + if (!started.qrCodeDataUrl) { + throw new Error("\u9489\u9489\u626B\u7801\u670D\u52A1\u6CA1\u6709\u8FD4\u56DE\u5B89\u5168\u7684\u4E8C\u7EF4\u7801"); + } + setNow(Date.now()); + setProvision({ + ...started, + durationMs: Math.max(1, started.expiresAt - Date.now()) + }); + announce("\u9489\u9489\u4E8C\u7EF4\u7801\u5DF2\u751F\u6210\uFF0C\u8BF7\u4F7F\u7528\u9489\u9489 App \u626B\u63CF\u3002"); + } catch (error) { + setProvision({ + attemptId: provision?.attemptId, + status: "failed", + error: presentError(error) + }); + } finally { + setBusy(false); + } + }, [announce, invoke, provision?.attemptId]); + const cancelProvisioning = React.useCallback(async () => { + setBusy(true); + try { + if (provision?.attemptId && !["failed", "expired", "cancelled"].includes(provision.status)) { + await invoke(DINGTALK_ENDPOINTS.cancelProvisioning, { attemptId: provision.attemptId }); + } + setProvision(null); + announce("\u5DF2\u53D6\u6D88\u9489\u9489\u673A\u5668\u4EBA\u63A5\u5165\u3002"); + window.requestAnimationFrame(() => addButtonRef.current?.focus()); + } catch (error) { + setProvision((current) => ({ ...current, status: "failed", error: presentError(error) })); + } finally { + setBusy(false); + } + }, [announce, invoke, provision?.attemptId, provision?.status]); + React.useEffect(() => { + const attemptId = provision?.attemptId; + if (!attemptId || !ACTIVE_PROVISION_STATES.has(provision.status)) return void 0; + const controller = new AbortController(); + let timer; + const poll = async () => { + try { + const result = normalizeProvisioning(await invoke( + DINGTALK_ENDPOINTS.pollProvisioning, + { attemptId }, + controller.signal + )); + if (result.status === "connected") { + const snapshot = await loadStatus({ signal: controller.signal, silent: true }); + const account = result.botId ? snapshot?.bots.find((bot) => bot.botId === result.botId) : snapshot?.bots.find((bot) => bot.connected); + if (!account?.connected) { + setProvision((current) => current?.attemptId === attemptId ? { ...current, ...result, status: "connecting" } : current); + timer = window.setTimeout(poll, result.pollIntervalMs); + return; + } + setProvision(null); + announce(result.alreadyConnected ? "\u8FD9\u4E2A\u9489\u9489\u673A\u5668\u4EBA\u5DF2\u7ECF\u63A5\u5165\u5E76\u4FDD\u6301\u5728\u7EBF\u3002" : "\u9489\u9489\u673A\u5668\u4EBA\u5DF2\u63A5\u5165\uFF0C\u53EF\u4EE5\u5F00\u59CB\u53D1\u9001\u6D88\u606F\u3002"); + return; + } + setProvision((current) => current?.attemptId === attemptId ? { ...current, ...result, durationMs: current.durationMs } : current); + if (ACTIVE_PROVISION_STATES.has(result.status)) { + timer = window.setTimeout(poll, result.pollIntervalMs); + } + } catch (error) { + if (error?.name === "AbortError") return; + setProvision((current) => current?.attemptId === attemptId ? { ...current, status: "failed", error: presentError(error) } : current); + } + }; + timer = window.setTimeout(poll, provision.pollIntervalMs ?? 3e3); + return () => { + controller.abort(); + window.clearTimeout(timer); + }; + }, [announce, invoke, loadStatus, provision?.attemptId, provision?.pollIntervalMs, provision?.status]); + const setBotBusy = React.useCallback((botId, operation) => { + setBusyByBot((current) => { + const next = { ...current }; + if (operation) next[botId] = operation; + else delete next[botId]; + return next; + }); + }, []); + const runBotAction = React.useCallback(async ({ account, operation, endpoint, payload, success }) => { + setBotBusy(account.botId, operation); + try { + await invoke(endpoint, payload); + const snapshot = await loadStatus({ silent: true }); + announce(typeof success === "function" ? success(snapshot) : success); + return snapshot; + } catch (error) { + announce(`\u64CD\u4F5C\u5931\u8D25\uFF1A${presentError(error).message}`); + return void 0; + } finally { + setBotBusy(account.botId, null); + } + }, [announce, invoke, loadStatus, setBotBusy]); + const reconnect = React.useCallback((account) => runBotAction({ + account, + operation: "reconnect", + endpoint: DINGTALK_ENDPOINTS.reconnectBot, + payload: { botId: account.botId }, + success: (snapshot) => snapshot?.bots.find((bot) => bot.botId === account.botId)?.connected ? "\u9489\u9489\u8FDE\u63A5\u68C0\u67E5\u5B8C\u6210\u3002" : "\u9489\u9489\u4ECD\u672A\u8FDE\u63A5\uFF0C\u63D2\u4EF6\u4F1A\u7EE7\u7EED\u81EA\u52A8\u91CD\u8BD5\u3002" + }), [runBotAction]); + const remove = React.useCallback(async (account) => { + const snapshot = await runBotAction({ + account, + operation: "delete", + endpoint: DINGTALK_ENDPOINTS.deleteBot, + payload: { botId: account.botId, confirm: true }, + success: "\u9489\u9489\u673A\u5668\u4EBA\u53CA\u672C\u673A\u51ED\u636E\u5DF2\u79FB\u9664\u3002" + }); + if (snapshot) setRemoveTarget(null); + }, [runBotAction]); + const approve = React.useCallback((account, sender) => runBotAction({ + account, + operation: `approve:${sender.requestId}`, + endpoint: DINGTALK_ENDPOINTS.approveSender, + payload: { botId: account.botId, requestId: sender.requestId, confirm: true }, + success: `\u5DF2\u6279\u51C6 ${sender.displayName} \u4F7F\u7528\u8FD9\u4E2A\u673A\u5668\u4EBA\u3002` + }), [runBotAction]); + const revoke = React.useCallback((account, sender) => runBotAction({ + account, + operation: `revoke:${sender.senderKey}`, + endpoint: DINGTALK_ENDPOINTS.revokeSender, + payload: { botId: account.botId, senderKey: sender.senderKey, confirm: true }, + success: `\u5DF2\u64A4\u9500 ${sender.displayName} \u7684\u4F7F\u7528\u6743\u9650\u3002` + }), [runBotAction]); + let provisionView = null; + if (provision?.status === "starting") { + provisionView = h( + "div", + { className: "ddt-card ddt-loading", "aria-busy": "true" }, + h("div", { className: "ddt-spinner" }), + h("span", null, "\u6B63\u5728\u7533\u8BF7\u9489\u9489\u6388\u6743\u4E8C\u7EF4\u7801\u2026") + ); + } else if (provision?.status === "pending") { + provisionView = h(QrPanel, { + provision, + now, + busy, + onRefresh: () => void startProvisioning({ replace: true }), + onCancel: () => void cancelProvisioning() + }); + } else if (["scanned", "authorizing", "creating", "connecting"].includes(provision?.status)) { + provisionView = h(ProgressPanel, { + status: provision.status, + busy, + onCancel: () => void cancelProvisioning() + }); + } else if (provision && ["failed", "expired", "cancelled"].includes(provision.status)) { + provisionView = h(ProvisionError, { + provision, + busy, + onRetry: () => void startProvisioning({ replace: Boolean(provision.attemptId) }), + onClose: () => void cancelProvisioning() + }); + } + return h( + "section", + { className: "ddt-page", "aria-label": "\u9489\u9489\u8BBE\u7F6E" }, + h(Heading, { + totals: model.totals, + adding: Boolean(provision), + busy, + onAdd: () => void startProvisioning(), + addButtonRef + }), + h("div", { className: "ddt-visuallyHidden", role: "status", "aria-live": "polite" }, notice), + model.error && model.phase === "ready" ? h("div", { className: "ddt-statusNotice", role: "alert" }, `\u72B6\u6001\u5237\u65B0\u5931\u8D25\uFF1A${model.error.message}`) : null, + model.phase === "loading" ? h(LoadingView) : model.phase === "error" ? h( + "div", + { className: "ddt-card" }, + h( + "div", + { className: "ddt-inlineError", role: "alert" }, + h("h3", null, "\u65E0\u6CD5\u8BFB\u53D6\u9489\u9489\u673A\u5668\u4EBA\u72B6\u6001"), + h("p", null, model.error?.message ?? "\u8BF7\u7A0D\u540E\u91CD\u8BD5"), + h(Button, { onClick: () => void loadStatus() }, "\u91CD\u65B0\u8BFB\u53D6") + ) + ) : h( + React.Fragment, + null, + provisionView, + model.bots.length === 0 && !provision ? h(EmptyView, { busy, onStart: () => void startProvisioning() }) : null, + model.bots.length > 0 ? h(AccountList, { + bots: model.bots, + busyByBot, + removeTarget, + onReconnect: (account) => void reconnect(account), + onRequestRemove: (account) => setRemoveTarget(account.botId), + onConfirmRemove: (account) => void remove(account), + onCancelRemove: () => setRemoveTarget(null), + onApprove: (account, sender) => void approve(account, sender), + onRevoke: (account, sender) => void revoke(account, sender) + }) : null + ) + ); +} // plugin-src/client/channels/feishu/index.js -var React = __toESM(require("react"), 1); +var React2 = __toESM(require("react"), 1); // plugin-src/client/channels/feishu/api.js var FEISHU_RPC_CHANNEL = "/feishu"; @@ -76,10 +1194,10 @@ var POLL_STATES = /* @__PURE__ */ new Set([ "expired", "failed" ]); -function isRecord(value) { +function isRecord2(value) { return value !== null && typeof value === "object" && !Array.isArray(value); } -function optionalString(value) { +function optionalString2(value) { return typeof value === "string" && value.trim().length > 0 ? value.trim() : void 0; } function optionalTimestamp(value) { @@ -90,67 +1208,67 @@ function optionalTimestamp(value) { } return void 0; } -function clamp(value, min, max, fallback) { +function clamp2(value, min, max, fallback) { return typeof value === "number" && Number.isFinite(value) ? Math.min(max, Math.max(min, value)) : fallback; } -function unwrapRpcResult(result) { - if (!isRecord(result) || typeof result.ok !== "boolean") { +function unwrapRpcResult2(result) { + if (!isRecord2(result) || typeof result.ok !== "boolean") { throw new Error("\u98DE\u4E66\u670D\u52A1\u8FD4\u56DE\u4E86\u65E0\u6CD5\u8BC6\u522B\u7684\u54CD\u5E94"); } if (!result.ok) { - const message = optionalString(result.error?.message) ?? "\u98DE\u4E66\u670D\u52A1\u8BF7\u6C42\u5931\u8D25"; + const message = optionalString2(result.error?.message) ?? "\u98DE\u4E66\u670D\u52A1\u8BF7\u6C42\u5931\u8D25"; const error = new Error(message); - error.code = optionalString(result.error?.code) ?? "FEISHU_RPC_ERROR"; + error.code = optionalString2(result.error?.code) ?? "FEISHU_RPC_ERROR"; throw error; } return result.value; } -function normalizeProvisioning(value, now = Date.now()) { - const source = isRecord(value?.provisioning) ? value.provisioning : value; - if (!isRecord(source)) throw new Error("\u98DE\u4E66\u670D\u52A1\u6CA1\u6709\u8FD4\u56DE\u4E8C\u7EF4\u7801\u4FE1\u606F"); - const attemptId = optionalString(source.attemptId) ?? optionalString(source.provisioningId); - const verificationUrl = optionalString(source.verificationUrl); - const qrCodeDataUrl = optionalString(source.qrCodeDataUrl); +function normalizeProvisioning2(value, now = Date.now()) { + const source = isRecord2(value?.provisioning) ? value.provisioning : value; + if (!isRecord2(source)) throw new Error("\u98DE\u4E66\u670D\u52A1\u6CA1\u6709\u8FD4\u56DE\u4E8C\u7EF4\u7801\u4FE1\u606F"); + const attemptId = optionalString2(source.attemptId) ?? optionalString2(source.provisioningId); + const verificationUrl = optionalString2(source.verificationUrl); + const qrCodeDataUrl = optionalString2(source.qrCodeDataUrl); if (!attemptId || !verificationUrl && !qrCodeDataUrl) { throw new Error("\u98DE\u4E66\u670D\u52A1\u8FD4\u56DE\u7684\u4E8C\u7EF4\u7801\u4FE1\u606F\u4E0D\u5B8C\u6574"); } const explicitExpiry = optionalTimestamp(source.expiresAt); - const expireIn = clamp(source.expireIn, 1, 60 * 60, 5 * 60); + const expireIn = clamp2(source.expireIn, 1, 60 * 60, 5 * 60); return { attemptId, verificationUrl, qrCodeDataUrl, expiresAt: explicitExpiry ?? now + expireIn * 1e3, - pollIntervalMs: clamp(source.pollIntervalMs, 800, 1e4, 1800) + pollIntervalMs: clamp2(source.pollIntervalMs, 800, 1e4, 1800) }; } -function normalizeBot(value) { - const source = isRecord(value) ? value : {}; +function normalizeBot2(value) { + const source = isRecord2(value) ? value : {}; return { - name: optionalString(source.name) ?? "\u98DE\u4E66\u673A\u5668\u4EBA", - avatarUrl: optionalString(source.avatarUrl), - appIdMasked: optionalString(source.appIdMasked), - tenantName: optionalString(source.tenantName), + name: optionalString2(source.name) ?? "\u98DE\u4E66\u673A\u5668\u4EBA", + avatarUrl: optionalString2(source.avatarUrl), + appIdMasked: optionalString2(source.appIdMasked), + tenantName: optionalString2(source.tenantName), domain: source.domain === "lark" ? "lark" : "feishu", activated: typeof source.activated === "boolean" || typeof source.activated === "number" ? source.activated : void 0 }; } function normalizeHealth(value, connected = false) { - const source = isRecord(value) ? value : {}; + const source = isRecord2(value) ? value : {}; const fallbackStatus = connected ? "healthy" : "offline"; const status = ["healthy", "degraded", "offline", "checking"].includes(source.status) ? source.status : fallbackStatus; return { status, - summary: optionalString(source.summary) ?? (connected ? "\u957F\u8FDE\u63A5\u8FD0\u884C\u6B63\u5E38" : "\u673A\u5668\u4EBA\u5C1A\u672A\u8FDE\u63A5"), + summary: optionalString2(source.summary) ?? (connected ? "\u957F\u8FDE\u63A5\u8FD0\u884C\u6B63\u5E38" : "\u673A\u5668\u4EBA\u5C1A\u672A\u8FDE\u63A5"), lastCheckedAt: optionalTimestamp(source.lastCheckedAt), lastConnectedAt: optionalTimestamp(source.lastConnectedAt) }; } -function normalizeError(value) { - if (!isRecord(value)) return void 0; - const message = optionalString(value.message); +function normalizeError2(value) { + if (!isRecord2(value)) return void 0; + const message = optionalString2(value.message); if (!message) return void 0; - return { message, code: optionalString(value.code) }; + return { message, code: optionalString2(value.code) }; } function authoritativeState(value, connected) { if (connected) return "connected"; @@ -162,8 +1280,8 @@ function authoritativeState(value, connected) { return "offline"; } function normalizeBotConnection(value, fallbackBotId) { - if (!isRecord(value)) throw new Error("\u98DE\u4E66\u670D\u52A1\u8FD4\u56DE\u4E86\u65E0\u6548\u7684\u673A\u5668\u4EBA\u72B6\u6001"); - const botId = optionalString(value.botId) ?? optionalString(fallbackBotId); + if (!isRecord2(value)) throw new Error("\u98DE\u4E66\u670D\u52A1\u8FD4\u56DE\u4E86\u65E0\u6548\u7684\u673A\u5668\u4EBA\u72B6\u6001"); + const botId = optionalString2(value.botId) ?? optionalString2(fallbackBotId); if (!botId) throw new Error("\u98DE\u4E66\u670D\u52A1\u8FD4\u56DE\u7684\u673A\u5668\u4EBA\u7F3A\u5C11 botId"); const connected = value.connected === true; return { @@ -171,17 +1289,17 @@ function normalizeBotConnection(value, fallbackBotId) { state: authoritativeState(value.state, connected), connected, configured: value.configured !== false, - bot: normalizeBot(value.bot), + bot: normalizeBot2(value.bot), health: normalizeHealth(value.health, connected), - error: normalizeError(value.error) + error: normalizeError2(value.error) }; } function normalizeBotsSnapshot(value) { - if (!isRecord(value)) throw new Error("\u98DE\u4E66\u670D\u52A1\u6CA1\u6709\u8FD4\u56DE\u8FDE\u63A5\u72B6\u6001"); + if (!isRecord2(value)) throw new Error("\u98DE\u4E66\u670D\u52A1\u6CA1\u6709\u8FD4\u56DE\u8FDE\u63A5\u72B6\u6001"); let sourceBots = Array.isArray(value.bots) ? value.bots : []; if (sourceBots.length === 0 && value.configured === true) { sourceBots = [{ - botId: optionalString(value.botId) ?? "legacy-default", + botId: optionalString2(value.botId) ?? "legacy-default", state: value.state, connected: value.connected, configured: true, @@ -210,49 +1328,49 @@ function normalizeBotsSnapshot(value) { // Derive counts from the authoritative list so stale summary fields never // make the UI claim that an unavailable bot is online. totals: { configured, connected }, - provisioning: value.provisioning ? normalizeProvisioning(value.provisioning) : void 0, - error: normalizeError(value.error) + provisioning: value.provisioning ? normalizeProvisioning2(value.provisioning) : void 0, + error: normalizeError2(value.error) }; } function normalizeConnectionSnapshot(value) { - if (!isRecord(value)) throw new Error("\u98DE\u4E66\u670D\u52A1\u6CA1\u6709\u8FD4\u56DE\u8FDE\u63A5\u72B6\u6001"); + if (!isRecord2(value)) throw new Error("\u98DE\u4E66\u670D\u52A1\u6CA1\u6709\u8FD4\u56DE\u8FDE\u63A5\u72B6\u6001"); const connected = value.connected === true; const reportedState = CONNECTION_STATES.has(value.state) ? value.state : "disconnected"; const state = connected ? "connected" : reportedState === "connected" ? "connecting" : reportedState; const snapshot = { state, configured: value.configured === true, - bot: normalizeBot(value.bot), + bot: normalizeBot2(value.bot), health: normalizeHealth(value.health, connected), provisioning: void 0, - errorMessage: optionalString(value.error?.message) ?? optionalString(value.message) + errorMessage: optionalString2(value.error?.message) ?? optionalString2(value.message) }; - if (value.provisioning) snapshot.provisioning = normalizeProvisioning(value.provisioning); + if (value.provisioning) snapshot.provisioning = normalizeProvisioning2(value.provisioning); return snapshot; } function normalizePollResult(value) { - if (!isRecord(value)) throw new Error("\u98DE\u4E66\u670D\u52A1\u6CA1\u6709\u8FD4\u56DE\u521B\u5EFA\u8FDB\u5EA6"); + if (!isRecord2(value)) throw new Error("\u98DE\u4E66\u670D\u52A1\u6CA1\u6709\u8FD4\u56DE\u521B\u5EFA\u8FDB\u5EA6"); const status = POLL_STATES.has(value.status) ? value.status : POLL_STATES.has(value.state) ? value.state : void 0; if (!status) throw new Error("\u98DE\u4E66\u670D\u52A1\u8FD4\u56DE\u4E86\u672A\u77E5\u7684\u521B\u5EFA\u72B6\u6001"); const normalized = { status, - botId: optionalString(value.botId), - message: optionalString(value.error?.message) ?? optionalString(value.message), + botId: optionalString2(value.botId), + message: optionalString2(value.error?.message) ?? optionalString2(value.message), connection: void 0, provisioning: void 0 }; - if (value.provisioning) normalized.provisioning = normalizeProvisioning(value.provisioning); - if (status === "connected" && isRecord(value.connection)) { + if (value.provisioning) normalized.provisioning = normalizeProvisioning2(value.provisioning); + if (status === "connected" && isRecord2(value.connection)) { normalized.connection = value.connection.botId ? normalizeBotConnection(value.connection) : normalizeConnectionSnapshot(value.connection); } return normalized; } -function presentError(error) { - const raw = optionalString(error?.message) ?? "\u64CD\u4F5C\u5931\u8D25\uFF0C\u8BF7\u7A0D\u540E\u91CD\u8BD5"; +function presentError2(error) { + const raw = optionalString2(error?.message) ?? "\u64CD\u4F5C\u5931\u8D25\uFF0C\u8BF7\u7A0D\u540E\u91CD\u8BD5"; const message = raw.replace(/(client[_-]?secret|app[_-]?secret|secret|token)\s*[:=]\s*[^\s,;]+/gi, "$1=\u2022\u2022\u2022\u2022\u2022\u2022").slice(0, 240); - return { message, code: optionalString(error?.code) }; + return { message, code: optionalString2(error?.code) }; } -function formatRemaining(milliseconds) { +function formatRemaining2(milliseconds) { const totalSeconds = Math.max(0, Math.ceil(milliseconds / 1e3)); const minutes = Math.floor(totalSeconds / 60); const seconds = totalSeconds % 60; @@ -261,7 +1379,7 @@ function formatRemaining(milliseconds) { // plugin-src/client/channels/feishu/styles.js var FEISHU_STYLE_ID = "beihuixinghe-dsh-feishu-settings"; -var CSS = String.raw` +var CSS2 = String.raw` .bxf-page { --bxf-accent: var(--dsw-alias-state-business-primary, #3370ff); --bxf-success: var(--dsw-alias-state-success-primary, #20a162); @@ -778,7 +1896,7 @@ function installFeishuStyles() { const style = document.createElement("style"); style.dataset.plugin = "@xmanrui/dsh-feishu"; style.dataset.pluginCss = FEISHU_STYLE_ID; - style.textContent = CSS; + style.textContent = CSS2; document.head.appendChild(style); return () => { style.remove(); @@ -786,9 +1904,9 @@ function installFeishuStyles() { } // plugin-src/client/channels/feishu/index.js -var h = React.createElement; +var h2 = React2.createElement; function SvgIcon({ children, size = 18, className, viewBox = "0 0 24 24" }) { - return h("svg", { + return h2("svg", { width: size, height: size, viewBox, @@ -800,16 +1918,16 @@ function SvgIcon({ children, size = 18, className, viewBox = "0 0 24 24" }) { }, children); } function ShieldIcon({ size = 18 }) { - return h( + return h2( SvgIcon, { size }, - h("path", { + h2("path", { d: "M12 3 5.5 5.8v5.1c0 4.25 2.72 7.87 6.5 9.1 3.78-1.23 6.5-4.85 6.5-9.1V5.8L12 3Z", stroke: "currentColor", strokeWidth: "1.7", strokeLinejoin: "round" }), - h("path", { + h2("path", { d: "m9.3 11.8 1.7 1.7 3.8-4", stroke: "currentColor", strokeWidth: "1.7", @@ -819,10 +1937,10 @@ function ShieldIcon({ size = 18 }) { ); } function RobotIcon({ size = 26 }) { - return h( + return h2( SvgIcon, { size }, - h("rect", { + h2("rect", { x: "5", y: "7.5", width: "14", @@ -831,7 +1949,7 @@ function RobotIcon({ size = 26 }) { stroke: "currentColor", strokeWidth: "1.7" }), - h("path", { + h2("path", { d: "M12 4.5v3M8.7 12h.01M15.3 12h.01M9.2 15.3c1.67 1.08 3.93 1.08 5.6 0M3.5 11.5v3M20.5 11.5v3", stroke: "currentColor", strokeWidth: "1.7", @@ -840,23 +1958,23 @@ function RobotIcon({ size = 26 }) { ); } function SparkIcon({ size = 18 }) { - return h( + return h2( SvgIcon, { size }, - h("path", { + h2("path", { d: "M12 2.8c.75 3.67 2.7 5.62 6.4 6.4-3.7.77-5.65 2.72-6.4 6.4-.75-3.68-2.7-5.63-6.4-6.4 3.7-.78 5.65-2.73 6.4-6.4Z", stroke: "currentColor", strokeWidth: "1.55", strokeLinejoin: "round" }), - h("path", { + h2("path", { d: "M5.2 15.8c.35 1.7 1.28 2.63 3 3-1.72.36-2.65 1.29-3 3-.36-1.71-1.29-2.64-3-3 1.71-.37 2.64-1.3 3-3ZM18.7 2.7c.22 1.06.79 1.63 1.85 1.85-1.06.22-1.63.79-1.85 1.85-.22-1.06-.79-1.63-1.85-1.85 1.06-.22 1.63-.79 1.85-1.85Z", fill: "currentColor" }) ); } function PlusIcon({ size = 17 }) { - return h(SvgIcon, { size }, h("path", { + return h2(SvgIcon, { size }, h2("path", { d: "M12 5v14M5 12h14", stroke: "currentColor", strokeWidth: "1.8", @@ -864,7 +1982,7 @@ function PlusIcon({ size = 17 }) { })); } function RefreshIcon({ size = 16 }) { - return h(SvgIcon, { size }, h("path", { + return h2(SvgIcon, { size }, h2("path", { d: "M19 7.5V4m0 0h-3.5M19 4l-2.1 2.1A7 7 0 1 0 19 13", stroke: "currentColor", strokeWidth: "1.8", @@ -873,7 +1991,7 @@ function RefreshIcon({ size = 16 }) { })); } function ExternalIcon({ size = 15 }) { - return h(SvgIcon, { size }, h("path", { + return h2(SvgIcon, { size }, h2("path", { d: "M13 5h6v6M19 5l-8.5 8.5M18 13.5V18a1 1 0 0 1-1 1H6a1 1 0 0 1-1-1V7a1 1 0 0 1 1-1h4.5", stroke: "currentColor", strokeWidth: "1.7", @@ -882,16 +2000,16 @@ function ExternalIcon({ size = 15 }) { })); } function AlertIcon({ size = 22 }) { - return h( + return h2( SvgIcon, { size }, - h("path", { + h2("path", { d: "M12 3.4 21 19H3L12 3.4Z", stroke: "currentColor", strokeWidth: "1.7", strokeLinejoin: "round" }), - h("path", { + h2("path", { d: "M12 9v4.4M12 16.6v.01", stroke: "currentColor", strokeWidth: "1.9", @@ -900,48 +2018,48 @@ function AlertIcon({ size = 22 }) { ); } function QrIcon({ size = 58 }) { - return h(SvgIcon, { size }, h("path", { + return h2(SvgIcon, { size }, h2("path", { d: "M4 4h6v6H4V4Zm10 0h6v6h-6V4ZM4 14h6v6H4v-6Zm10 0h2v2h-2v-2Zm4 0h2v4h-2v-4Zm-4 4h4v2h-4v-2Z", fill: "currentColor" })); } -var Button = React.forwardRef(function Button2({ children, kind = "secondary", size, icon, className = "", ...props }, ref) { - return h("button", { +var Button3 = React2.forwardRef(function Button4({ children, kind = "secondary", size, icon, className = "", ...props }, ref) { + return h2("button", { ...props, ref, type: "button", className: `bxf-button ${className}`.trim(), "data-kind": kind, "data-size": size - }, icon, h("span", null, children)); + }, icon, h2("span", null, children)); }); function BrandMark({ compact = false }) { - return h( + return h2( "div", { className: compact ? "bxf-avatar" : "bxf-brandMark" }, - h(RobotIcon, { size: compact ? 25 : 34 }) + h2(RobotIcon, { size: compact ? 25 : 34 }) ); } -function Heading({ totals, onAdd, adding, busy, addButtonRef }) { +function Heading2({ totals, onAdd, adding, busy, addButtonRef }) { const hasBots = totals.configured > 0; - return h( + return h2( "div", { className: "bxf-heading" }, - h( + h2( "div", { className: "bxf-headingTools" }, - hasBots ? h("div", { + hasBots ? h2("div", { className: "bxf-totalBadge", "aria-label": `\u5DF2\u63A5\u5165 ${totals.configured} \u4E2A\u673A\u5668\u4EBA\uFF0C\u5176\u4E2D ${totals.connected} \u4E2A\u5728\u7EBF` - }, h("strong", null, totals.connected), h("span", null, `/ ${totals.configured} \u5728\u7EBF`)) : null, - h("div", { + }, h2("strong", null, totals.connected), h2("span", null, `/ ${totals.configured} \u5728\u7EBF`)) : null, + h2("div", { className: "bxf-localBadge", title: "\u6BCF\u4E2A\u5E94\u7528\u7684\u51ED\u636E\u5747\u7531 Host \u72EC\u7ACB\u4FDD\u5B58\uFF0C\u4E0D\u4F1A\u53D1\u9001\u5230\u6D4F\u89C8\u5668" - }, h(ShieldIcon, { size: 14 }), h("span", null, "\u51ED\u636E\u4EC5\u4FDD\u5B58\u5728\u672C\u673A")), - h(Button, { + }, h2(ShieldIcon, { size: 14 }), h2("span", null, "\u51ED\u636E\u4EC5\u4FDD\u5B58\u5728\u672C\u673A")), + h2(Button3, { kind: "primary", size: "small", - icon: h(PlusIcon), + icon: h2(PlusIcon), onClick: onAdd, disabled: adding || busy, ref: addButtonRef, @@ -950,58 +2068,58 @@ function Heading({ totals, onAdd, adding, busy, addButtonRef }) { ) ); } -function LoadingView() { - return h("div", { +function LoadingView2() { + return h2("div", { className: "bxf-card", "aria-busy": "true", "aria-label": "\u6B63\u5728\u8BFB\u53D6\u98DE\u4E66\u673A\u5668\u4EBA\u5217\u8868" - }, h( + }, h2( "div", { className: "bxf-skeleton" }, - h("div", { className: "bxf-skeletonLine" }), - h("div", { className: "bxf-skeletonLine" }), - h("div", { className: "bxf-skeletonLine" }), - h("div", { className: "bxf-skeletonLine" }), - h("div", { className: "bxf-skeletonBox" }) + h2("div", { className: "bxf-skeletonLine" }), + h2("div", { className: "bxf-skeletonLine" }), + h2("div", { className: "bxf-skeletonLine" }), + h2("div", { className: "bxf-skeletonLine" }), + h2("div", { className: "bxf-skeletonBox" }) )); } -function EmptyView({ onStart, busy }) { - return h( +function EmptyView2({ onStart, busy }) { + return h2( "div", { className: "bxf-card" }, - h( + h2( "div", { className: "bxf-cardBody bxf-intro" }, - h( + h2( "div", { className: "bxf-introCopy" }, - h( + h2( "div", { className: "bxf-stateLabel" }, - h("span", { className: "bxf-dot" }), - h("span", null, "\u5C1A\u672A\u63A5\u5165\u673A\u5668\u4EBA") + h2("span", { className: "bxf-dot" }), + h2("span", null, "\u5C1A\u672A\u63A5\u5165\u673A\u5668\u4EBA") ), - h("h3", null, "\u626B\u7801\uFF0C\u521B\u5EFA\u7B2C\u4E00\u4E2A\u98DE\u4E66\u5165\u53E3"), - h("p", null, "\u65E0\u9700\u624B\u52A8\u586B\u5199 App ID\u3002\u4EE5\u540E\u8FD8\u53EF\u4EE5\u7EE7\u7EED\u6DFB\u52A0\u673A\u5668\u4EBA\uFF0C\u5206\u522B\u670D\u52A1\u4E0D\u540C\u56E2\u961F\u6216\u98DE\u4E66\u79DF\u6237\u3002"), - h( + h2("h3", null, "\u626B\u7801\uFF0C\u521B\u5EFA\u7B2C\u4E00\u4E2A\u98DE\u4E66\u5165\u53E3"), + h2("p", null, "\u65E0\u9700\u624B\u52A8\u586B\u5199 App ID\u3002\u4EE5\u540E\u8FD8\u53EF\u4EE5\u7EE7\u7EED\u6DFB\u52A0\u673A\u5668\u4EBA\uFF0C\u5206\u522B\u670D\u52A1\u4E0D\u540C\u56E2\u961F\u6216\u98DE\u4E66\u79DF\u6237\u3002"), + h2( "div", { className: "bxf-actions" }, - h(Button, { + h2(Button3, { kind: "primary", - icon: h(SparkIcon), + icon: h2(SparkIcon), onClick: onStart, disabled: busy, "aria-busy": busy ? "true" : void 0 }, busy ? "\u6B63\u5728\u521B\u5EFA\u2026" : "\u4E00\u952E\u521B\u5EFA\u98DE\u4E66\u673A\u5668\u4EBA") ), - h( + h2( "div", { className: "bxf-note" }, - h(ShieldIcon, { size: 16 }), - h("span", null, "\u6BCF\u4E2A App Secret \u90FD\u53EA\u5199\u5165 Host \u51ED\u636E\u5B58\u50A8\uFF0C\u6D4F\u89C8\u5668\u4E0D\u4F1A\u6536\u5230 Secret\u3002") + h2(ShieldIcon, { size: 16 }), + h2("span", null, "\u6BCF\u4E2A App Secret \u90FD\u53EA\u5199\u5165 Host \u51ED\u636E\u5B58\u50A8\uFF0C\u6D4F\u89C8\u5668\u4E0D\u4F1A\u6536\u5230 Secret\u3002") ) ), - h("div", { className: "bxf-markStage", "aria-hidden": "true" }, h(BrandMark)) + h2("div", { className: "bxf-markStage", "aria-hidden": "true" }, h2(BrandMark)) ) ); } @@ -1014,99 +2132,99 @@ function safeVerificationHref(value) { return void 0; } } -function safeQrSource(value) { +function safeQrSource2(value) { if (!value) return void 0; 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] = React.useState(false); - const qrSource = safeQrSource(provision.qrCodeDataUrl); + const [imageFailed, setImageFailed] = React2.useState(false); + const qrSource = safeQrSource2(provision.qrCodeDataUrl); const href = safeVerificationHref(provision.verificationUrl); const remaining = Math.max(0, provision.expiresAt - now); const expired = provision.expired === true || remaining === 0; const progress = Math.min(1, remaining / Math.max(1, provision.durationMs ?? remaining)); - React.useEffect(() => setImageFailed(false), [qrSource]); - return h( + React2.useEffect(() => setImageFailed(false), [qrSource]); + return h2( "div", { className: "bxf-card bxf-provisionCard" }, - h( + h2( "div", { className: "bxf-cardBody bxf-qrLayout" }, - h( + h2( "div", { className: "bxf-qrColumn" }, - h( + h2( "div", { className: "bxf-qrFrame" }, - qrSource && !imageFailed ? h("img", { + qrSource && !imageFailed ? h2("img", { src: qrSource, alt: "\u7528\u4E8E\u65B0\u589E DeepSeek Harness \u98DE\u4E66\u673A\u5668\u4EBA\u7684\u4E00\u6B21\u6027\u6388\u6743\u4E8C\u7EF4\u7801", onError: () => setImageFailed(true) - }) : h( + }) : h2( "div", { className: "bxf-qrFallback" }, - h("div", null, h(QrIcon), h("span", null, "\u4E8C\u7EF4\u7801\u672A\u5C31\u7EEA\uFF0C\u8BF7\u6253\u5F00\u6388\u6743\u94FE\u63A5")) + h2("div", null, h2(QrIcon), h2("span", null, "\u4E8C\u7EF4\u7801\u672A\u5C31\u7EEA\uFF0C\u8BF7\u6253\u5F00\u6388\u6743\u94FE\u63A5")) ), - expired ? h( + expired ? h2( "div", { className: "bxf-expiredOverlay", role: "status" }, - h("div", null, "\u4E8C\u7EF4\u7801\u5DF2\u5931\u6548", h("br"), "\u8BF7\u5237\u65B0\u540E\u91CD\u65B0\u626B\u7801") + h2("div", null, "\u4E8C\u7EF4\u7801\u5DF2\u5931\u6548", h2("br"), "\u8BF7\u5237\u65B0\u540E\u91CD\u65B0\u626B\u7801") ) : null ), - h( + h2( "div", { className: "bxf-countdown", - "aria-label": expired ? "\u4E8C\u7EF4\u7801\u5DF2\u5931\u6548" : `\u4E8C\u7EF4\u7801\u5269\u4F59 ${formatRemaining(remaining)}` + "aria-label": expired ? "\u4E8C\u7EF4\u7801\u5DF2\u5931\u6548" : `\u4E8C\u7EF4\u7801\u5269\u4F59 ${formatRemaining2(remaining)}` }, - h( + h2( "div", { className: "bxf-countdownTop", "aria-hidden": "true" }, - h("span", null, expired ? "\u7B49\u5F85\u5237\u65B0" : "\u4E8C\u7EF4\u7801\u6709\u6548\u65F6\u95F4"), - h("strong", null, formatRemaining(remaining)) + h2("span", null, expired ? "\u7B49\u5F85\u5237\u65B0" : "\u4E8C\u7EF4\u7801\u6709\u6548\u65F6\u95F4"), + h2("strong", null, formatRemaining2(remaining)) ), - h( + h2( "div", { className: "bxf-progress", "aria-hidden": "true" }, - h("span", { style: { "--bxf-progress": `${Math.round(progress * 100)}%` } }) + h2("span", { style: { "--bxf-progress": `${Math.round(progress * 100)}%` } }) ) ) ), - h( + h2( "div", { className: "bxf-qrCopy" }, - h( + h2( "div", { className: "bxf-stateLabel" }, - h("span", { className: "bxf-dot", "data-tone": "warning" }), - h("span", null, "\u6B63\u5728\u6DFB\u52A0\u65B0\u673A\u5668\u4EBA") + h2("span", { className: "bxf-dot", "data-tone": "warning" }), + h2("span", null, "\u6B63\u5728\u6DFB\u52A0\u65B0\u673A\u5668\u4EBA") ), - h("h3", null, expired ? "\u5237\u65B0\u4E8C\u7EF4\u7801\u540E\u7EE7\u7EED" : "\u4F7F\u7528\u98DE\u4E66\u626B\u7801\u521B\u5EFA\u673A\u5668\u4EBA"), - h("p", null, "\u626B\u7801\u53EA\u4F1A\u65B0\u589E\u4E00\u4E2A\u673A\u5668\u4EBA\uFF0C\u5DF2\u63A5\u5165\u7684\u673A\u5668\u4EBA\u4F1A\u7EE7\u7EED\u6B63\u5E38\u6536\u53D1\u6D88\u606F\u3002"), - h( + h2("h3", null, expired ? "\u5237\u65B0\u4E8C\u7EF4\u7801\u540E\u7EE7\u7EED" : "\u4F7F\u7528\u98DE\u4E66\u626B\u7801\u521B\u5EFA\u673A\u5668\u4EBA"), + h2("p", null, "\u626B\u7801\u53EA\u4F1A\u65B0\u589E\u4E00\u4E2A\u673A\u5668\u4EBA\uFF0C\u5DF2\u63A5\u5165\u7684\u673A\u5668\u4EBA\u4F1A\u7EE7\u7EED\u6B63\u5E38\u6536\u53D1\u6D88\u606F\u3002"), + h2( "ol", { className: "bxf-steps" }, - h("li", null, "\u6253\u5F00\u98DE\u4E66\u79FB\u52A8\u7AEF\uFF0C\u4F7F\u7528\u626B\u4E00\u626B\u8BFB\u53D6\u4E8C\u7EF4\u7801"), - h("li", null, "\u6838\u5BF9\u5E94\u7528\u540D\u79F0\u4E0E\u6743\u9650\u8303\u56F4\uFF0C\u5E76\u786E\u8BA4\u521B\u5EFA"), - h("li", null, "\u4FDD\u6301\u672C\u9875\u6253\u5F00\uFF0C\u7B49\u5F85\u65B0\u673A\u5668\u4EBA\u7684\u957F\u8FDE\u63A5\u5C31\u7EEA") + h2("li", null, "\u6253\u5F00\u98DE\u4E66\u79FB\u52A8\u7AEF\uFF0C\u4F7F\u7528\u626B\u4E00\u626B\u8BFB\u53D6\u4E8C\u7EF4\u7801"), + h2("li", null, "\u6838\u5BF9\u5E94\u7528\u540D\u79F0\u4E0E\u6743\u9650\u8303\u56F4\uFF0C\u5E76\u786E\u8BA4\u521B\u5EFA"), + h2("li", null, "\u4FDD\u6301\u672C\u9875\u6253\u5F00\uFF0C\u7B49\u5F85\u65B0\u673A\u5668\u4EBA\u7684\u957F\u8FDE\u63A5\u5C31\u7EEA") ), - h( + h2( "div", { className: "bxf-actions" }, - expired ? h(Button, { + expired ? h2(Button3, { kind: "primary", - icon: h(RefreshIcon), + icon: h2(RefreshIcon), onClick: onRefresh, disabled: busy - }, busy ? "\u5237\u65B0\u4E2D\u2026" : "\u5237\u65B0\u4E8C\u7EF4\u7801") : href ? h("a", { + }, busy ? "\u5237\u65B0\u4E2D\u2026" : "\u5237\u65B0\u4E8C\u7EF4\u7801") : href ? h2("a", { className: "bxf-button bxf-link", "data-kind": "secondary", href, target: "_blank", rel: "noopener noreferrer" - }, h(ExternalIcon), h("span", null, "\u5728\u98DE\u4E66\u4E2D\u6253\u5F00")) : null, - !expired ? h(Button, { icon: h(RefreshIcon), onClick: onRefresh, disabled: busy }, "\u6362\u4E00\u4E2A\u4E8C\u7EF4\u7801") : null, - h(Button, { onClick: onCancel, disabled: busy }, "\u53D6\u6D88\u6DFB\u52A0") + }, h2(ExternalIcon), h2("span", null, "\u5728\u98DE\u4E66\u4E2D\u6253\u5F00")) : null, + !expired ? h2(Button3, { icon: h2(RefreshIcon), onClick: onRefresh, disabled: busy }, "\u6362\u4E00\u4E2A\u4E8C\u7EF4\u7801") : null, + h2(Button3, { onClick: onCancel, disabled: busy }, "\u53D6\u6D88\u6DFB\u52A0") ) ) ) @@ -1114,58 +2232,58 @@ function QrPane({ provision, now, onRefresh, onCancel, busy }) { } function ProvisionProgress({ phase, onCancel, busy }) { const connecting = phase === "connecting"; - return h( + return h2( "div", { className: "bxf-card bxf-provisionCard", "aria-busy": "true" }, - h( + h2( "div", { className: "bxf-connecting bxf-connectingCompact" }, - h( + h2( "div", { className: "bxf-connectingCopy" }, - h( + h2( "div", { className: "bxf-orbit" }, - h( + h2( "div", { className: "bxf-orbitCore" }, - connecting ? h(RobotIcon, { size: 24 }) : h(SparkIcon, { size: 24 }) + connecting ? h2(RobotIcon, { size: 24 }) : h2(SparkIcon, { size: 24 }) ) ), - h("h3", null, connecting ? "\u5DF2\u786E\u8BA4\uFF0C\u6B63\u5728\u8FDE\u63A5\u65B0\u673A\u5668\u4EBA" : "\u6B63\u5728\u51C6\u5907\u6388\u6743\u4E8C\u7EF4\u7801"), - h("p", null, connecting ? "\u6B63\u5728\u5B89\u5168\u4FDD\u5B58\u51ED\u636E\u5E76\u68C0\u67E5\u65B0\u673A\u5668\u4EBA\u7684\u6D88\u606F\u901A\u9053\uFF0C\u5176\u4ED6\u673A\u5668\u4EBA\u4E0D\u4F1A\u4E2D\u65AD\u3002" : "\u6B63\u5728\u5411\u98DE\u4E66\u7533\u8BF7\u4E00\u6B21\u6027\u6388\u6743\u4E8C\u7EF4\u7801\uFF0C\u8BF7\u7A0D\u5019\u3002"), - connecting ? h( + h2("h3", null, connecting ? "\u5DF2\u786E\u8BA4\uFF0C\u6B63\u5728\u8FDE\u63A5\u65B0\u673A\u5668\u4EBA" : "\u6B63\u5728\u51C6\u5907\u6388\u6743\u4E8C\u7EF4\u7801"), + h2("p", null, connecting ? "\u6B63\u5728\u5B89\u5168\u4FDD\u5B58\u51ED\u636E\u5E76\u68C0\u67E5\u65B0\u673A\u5668\u4EBA\u7684\u6D88\u606F\u901A\u9053\uFF0C\u5176\u4ED6\u673A\u5668\u4EBA\u4E0D\u4F1A\u4E2D\u65AD\u3002" : "\u6B63\u5728\u5411\u98DE\u4E66\u7533\u8BF7\u4E00\u6B21\u6027\u6388\u6743\u4E8C\u7EF4\u7801\uFF0C\u8BF7\u7A0D\u5019\u3002"), + connecting ? h2( "div", { className: "bxf-actions", style: { justifyContent: "center" } }, - h(Button, { onClick: onCancel, disabled: busy }, "\u53D6\u6D88\u6DFB\u52A0") + h2(Button3, { onClick: onCancel, disabled: busy }, "\u53D6\u6D88\u6DFB\u52A0") ) : null ) ) ); } -function ProvisionError({ error, onRetry, onCancel, busy }) { - return h( +function ProvisionError2({ error, onRetry, onCancel, busy }) { + return h2( "div", { className: "bxf-card bxf-provisionCard" }, - h( + h2( "div", { className: "bxf-inlineError", role: "alert" }, - h("div", { className: "bxf-errorIcon" }, h(AlertIcon)), - h( + h2("div", { className: "bxf-errorIcon" }, h2(AlertIcon)), + h2( "div", null, - h("h3", null, "\u65B0\u673A\u5668\u4EBA\u6CA1\u6709\u6DFB\u52A0\u5B8C\u6210"), - h("p", null, error.message), - error.code ? h("span", { className: "bxf-errorCode" }, error.code) : null, - h( + h2("h3", null, "\u65B0\u673A\u5668\u4EBA\u6CA1\u6709\u6DFB\u52A0\u5B8C\u6210"), + h2("p", null, error.message), + error.code ? h2("span", { className: "bxf-errorCode" }, error.code) : null, + h2( "div", { className: "bxf-actions" }, - h( - Button, - { kind: "primary", icon: h(RefreshIcon), onClick: onRetry, disabled: busy }, + h2( + Button3, + { kind: "primary", icon: h2(RefreshIcon), onClick: onRetry, disabled: busy }, busy ? "\u91CD\u8BD5\u4E2D\u2026" : "\u91CD\u65B0\u751F\u6210\u4E8C\u7EF4\u7801" ), - h(Button, { onClick: onCancel, disabled: busy }, "\u5173\u95ED") + h2(Button3, { onClick: onCancel, disabled: busy }, "\u5173\u95ED") ) ) ) @@ -1177,25 +2295,25 @@ var HEALTH_LABELS = { offline: "\u8FDE\u63A5\u4E2D\u65AD", error: "\u9700\u8981\u5904\u7406" }; -function formatCheckedTime(timestamp2) { - if (!timestamp2) return "\u5C1A\u672A\u68C0\u67E5"; +function formatCheckedTime(timestamp3) { + if (!timestamp3) return "\u5C1A\u672A\u68C0\u67E5"; try { return new Intl.DateTimeFormat("zh-CN", { hour: "2-digit", minute: "2-digit", second: "2-digit" - }).format(new Date(timestamp2)); + }).format(new Date(timestamp3)); } catch { return "\u521A\u521A"; } } -function RemoveConfirmation({ bot, busy, onConfirm, onCancel }) { - const cancelRef = React.useRef(null); +function RemoveConfirmation2({ bot, busy, onConfirm, onCancel }) { + const cancelRef = React2.useRef(null); const idPart = bot.botId.replace(/[^a-zA-Z0-9_-]/g, "-"); const titleId = `bxf-remove-title-${idPart}`; const descriptionId = `bxf-remove-description-${idPart}`; - React.useEffect(() => cancelRef.current?.focus(), []); - return h( + React2.useEffect(() => cancelRef.current?.focus(), []); + return h2( "div", { className: "bxf-confirm", @@ -1209,18 +2327,18 @@ function RemoveConfirmation({ bot, busy, onConfirm, onCancel }) { } } }, - h("h4", { id: titleId }, `\u4ECE DeepSeek Harness \u79FB\u9664\u201C${bot.bot.name}\u201D\uFF1F`), - h( + h2("h4", { id: titleId }, `\u4ECE DeepSeek Harness \u79FB\u9664\u201C${bot.bot.name}\u201D\uFF1F`), + h2( "p", { id: descriptionId }, "\u6B64\u64CD\u4F5C\u4F1A\u505C\u6B62\u8FD9\u4E2A\u673A\u5668\u4EBA\u7684\u8FDE\u63A5\uFF0C\u5E76\u5220\u9664\u4FDD\u5B58\u5728\u672C\u673A\u7684\u63A5\u5165\u914D\u7F6E\u548C\u51ED\u636E\u3002\u98DE\u4E66\u5F00\u653E\u5E73\u53F0\u4E2D\u7684\u5E94\u7528\u4E0D\u4F1A\u88AB\u81EA\u52A8\u5220\u9664\uFF0C\u5176\u4ED6\u673A\u5668\u4EBA\u4E5F\u4E0D\u53D7\u5F71\u54CD\u3002" ), - h( + h2( "div", { className: "bxf-actions" }, - h(Button, { ref: cancelRef, onClick: onCancel, disabled: busy }, "\u4FDD\u7559\u673A\u5668\u4EBA"), - h( - Button, + h2(Button3, { ref: cancelRef, onClick: onCancel, disabled: busy }, "\u4FDD\u7559\u673A\u5668\u4EBA"), + h2( + Button3, { kind: "danger", onClick: onConfirm, disabled: busy }, busy ? "\u6B63\u5728\u79FB\u9664\u2026" : "\u786E\u8BA4\u79FB\u9664\u63A5\u5165" ) @@ -1245,7 +2363,7 @@ function BotCard({ const summary = actionError?.message ?? connection.error?.message ?? health.summary; const titleId = `bxf-bot-${connection.botId.replace(/[^a-zA-Z0-9_-]/g, "-")}`; const domainLabel = bot.domain === "lark" ? "Lark" : "\u98DE\u4E66"; - return h( + return h2( "article", { className: "bxf-card bxf-botCard", @@ -1254,74 +2372,74 @@ function BotCard({ tabIndex: -1, ref: cardRef }, - h( + h2( "div", { className: "bxf-cardBody" }, - h( + h2( "div", { className: "bxf-connectedTop" }, - h( + h2( "div", { className: "bxf-botIdentity" }, - bot.avatarUrl ? h("div", { className: "bxf-avatar" }, h("img", { src: bot.avatarUrl, alt: "" })) : h(BrandMark, { compact: true }), - h( + bot.avatarUrl ? h2("div", { className: "bxf-avatar" }, h2("img", { src: bot.avatarUrl, alt: "" })) : h2(BrandMark, { compact: true }), + h2( "div", { className: "bxf-botName" }, - h("h3", { id: titleId, title: bot.name }, bot.name), - h("p", null, bot.tenantName ? `${bot.tenantName} \xB7 ${domainLabel}\u673A\u5668\u4EBA` : `${domainLabel}\u673A\u5668\u4EBA`) + h2("h3", { id: titleId, title: bot.name }, bot.name), + h2("p", null, bot.tenantName ? `${bot.tenantName} \xB7 ${domainLabel}\u673A\u5668\u4EBA` : `${domainLabel}\u673A\u5668\u4EBA`) ) ), - h( + h2( "div", { className: "bxf-healthPill", "data-health": stateForDisplay }, - h("span", { className: "bxf-dot", "data-tone": tone }), - h("span", null, HEALTH_LABELS[stateForDisplay] ?? "\u72B6\u6001\u672A\u77E5") + h2("span", { className: "bxf-dot", "data-tone": tone }), + h2("span", null, HEALTH_LABELS[stateForDisplay] ?? "\u72B6\u6001\u672A\u77E5") ) ), - h( + h2( "dl", { className: "bxf-statusGrid" }, - h( + h2( "div", { className: "bxf-metric" }, - h("dt", null, "\u6D88\u606F\u901A\u9053"), - h("dd", null, connected ? "\u957F\u8FDE\u63A5" : stateForDisplay === "connecting" ? "\u8FDE\u63A5\u4E2D" : "\u5DF2\u65AD\u5F00") + h2("dt", null, "\u6D88\u606F\u901A\u9053"), + h2("dd", null, connected ? "\u957F\u8FDE\u63A5" : stateForDisplay === "connecting" ? "\u8FDE\u63A5\u4E2D" : "\u5DF2\u65AD\u5F00") ), - h( + h2( "div", { className: "bxf-metric" }, - h("dt", null, "\u5E94\u7528\u6807\u8BC6"), - h("dd", { title: bot.appIdMasked }, bot.appIdMasked ?? "\u5DF2\u5B89\u5168\u4FDD\u5B58") + h2("dt", null, "\u5E94\u7528\u6807\u8BC6"), + h2("dd", { title: bot.appIdMasked }, bot.appIdMasked ?? "\u5DF2\u5B89\u5168\u4FDD\u5B58") ), - h( + h2( "div", { className: "bxf-metric" }, - h("dt", null, "\u6700\u8FD1\u68C0\u67E5"), - h("dd", null, formatCheckedTime(health.lastCheckedAt)) + h2("dt", null, "\u6700\u8FD1\u68C0\u67E5"), + h2("dd", null, formatCheckedTime(health.lastCheckedAt)) ) ), - h("div", { className: "bxf-divider" }), - h( + h2("div", { className: "bxf-divider" }), + h2( "div", { className: "bxf-connectedFooter" }, - h( + h2( "div", { className: "bxf-healthSummary", "data-error": actionError || connection.error ? "true" : void 0 }, - h("strong", null, "\u8FDE\u63A5\u72B6\u6001\uFF1A"), - h("span", null, summary) + h2("strong", null, "\u8FDE\u63A5\u72B6\u6001\uFF1A"), + h2("span", null, summary) ), - h( + h2( "div", { className: "bxf-actions bxf-botActions" }, - h(Button, { + h2(Button3, { size: "small", - icon: h(RefreshIcon), + icon: h2(RefreshIcon), onClick: onReconnect, disabled: Boolean(busy), "aria-busy": busy === "reconnect" ? "true" : void 0, "aria-label": `${connected ? "\u68C0\u67E5\u8FDE\u63A5" : "\u91CD\u8BD5\u8FDE\u63A5"}${bot.name}` }, busy === "reconnect" ? connected ? "\u68C0\u67E5\u4E2D\u2026" : "\u6B63\u5728\u8FDE\u63A5\u2026" : connected ? "\u68C0\u67E5\u8FDE\u63A5" : "\u91CD\u8BD5\u8FDE\u63A5"), - h(Button, { + h2(Button3, { size: "small", kind: "danger", onClick: onRequestRemove, @@ -1332,7 +2450,7 @@ function BotCard({ ) ) ), - removing ? h(RemoveConfirmation, { + removing ? h2(RemoveConfirmation2, { bot: connection, busy: busy === "delete", onConfirm: onConfirmRemove, @@ -1341,22 +2459,22 @@ function BotCard({ ); } function BotList(props) { - return h( + return h2( "section", { className: "bxf-listSection", "aria-labelledby": "bxf-bot-list-title" }, - h( + h2( "div", { className: "bxf-listHeading" }, - h("h3", { id: "bxf-bot-list-title" }, "\u5DF2\u63A5\u5165\u7684\u673A\u5668\u4EBA"), - h("span", null, `${props.bots.length} \u4E2A`) + h2("h3", { id: "bxf-bot-list-title" }, "\u5DF2\u63A5\u5165\u7684\u673A\u5668\u4EBA"), + h2("span", null, `${props.bots.length} \u4E2A`) ), - h( + h2( "ul", { className: "bxf-botList", role: "list" }, - props.bots.map((bot) => h( + props.bots.map((bot) => h2( "li", { key: bot.botId }, - h(BotCard, { + h2(BotCard, { connection: bot, busy: props.busyByBot[bot.botId], actionError: props.errorsByBot[bot.botId], @@ -1373,25 +2491,25 @@ function BotList(props) { ); } function PageError({ error, onRetry, busy }) { - return h( + return h2( "div", { className: "bxf-card" }, - h( + h2( "div", { className: "bxf-error", role: "alert" }, - h("div", { className: "bxf-errorIcon" }, h(AlertIcon)), - h( + h2("div", { className: "bxf-errorIcon" }, h2(AlertIcon)), + h2( "div", null, - h("h3", null, "\u65E0\u6CD5\u8BFB\u53D6\u98DE\u4E66\u673A\u5668\u4EBA"), - h("p", null, error.message), - error.code ? h("span", { className: "bxf-errorCode" }, error.code) : null, - h( + h2("h3", null, "\u65E0\u6CD5\u8BFB\u53D6\u98DE\u4E66\u673A\u5668\u4EBA"), + h2("p", null, error.message), + error.code ? h2("span", { className: "bxf-errorCode" }, error.code) : null, + h2( "div", { className: "bxf-actions" }, - h( - Button, - { kind: "primary", icon: h(RefreshIcon), onClick: onRetry, disabled: busy }, + h2( + Button3, + { kind: "primary", icon: h2(RefreshIcon), onClick: onRetry, disabled: busy }, busy ? "\u91CD\u8BD5\u4E2D\u2026" : "\u91CD\u65B0\u8BFB\u53D6" ) ) @@ -1399,37 +2517,37 @@ function PageError({ error, onRetry, busy }) { ) ); } -var EMPTY_TOTALS = Object.freeze({ configured: 0, connected: 0 }); +var EMPTY_TOTALS2 = Object.freeze({ configured: 0, connected: 0 }); function FeishuSettingsTab({ rpcCall }) { - const [model, setModel] = React.useState({ + const [model, setModel] = React2.useState({ phase: "loading", revision: 0, bots: [], - totals: EMPTY_TOTALS, + totals: EMPTY_TOTALS2, provisioning: null, pageError: null, statusError: null }); - const [pageBusy, setPageBusy] = React.useState(false); - const [provisionBusy, setProvisionBusy] = React.useState(false); - const [busyByBot, setBusyByBot] = React.useState({}); - const [errorsByBot, setErrorsByBot] = React.useState({}); - const [removeTargetId, setRemoveTargetId] = React.useState(null); - const [announcement, setAnnouncement] = React.useState(""); - const [now, setNow] = React.useState(() => Date.now()); - const [focusBotId, setFocusBotId] = React.useState(null); - const cardRefs = React.useRef(/* @__PURE__ */ new Map()); - const removeButtonRefs = React.useRef(/* @__PURE__ */ new Map()); - const addButtonRef = React.useRef(null); - const announce = React.useCallback((message) => { + const [pageBusy, setPageBusy] = React2.useState(false); + const [provisionBusy, setProvisionBusy] = React2.useState(false); + const [busyByBot, setBusyByBot] = React2.useState({}); + const [errorsByBot, setErrorsByBot] = React2.useState({}); + const [removeTargetId, setRemoveTargetId] = React2.useState(null); + const [announcement, setAnnouncement] = React2.useState(""); + const [now, setNow] = React2.useState(() => Date.now()); + const [focusBotId, setFocusBotId] = React2.useState(null); + const cardRefs = React2.useRef(/* @__PURE__ */ new Map()); + const removeButtonRefs = React2.useRef(/* @__PURE__ */ new Map()); + const addButtonRef = React2.useRef(null); + const announce = React2.useCallback((message) => { setAnnouncement(""); if (!message) return; window.requestAnimationFrame(() => setAnnouncement(message)); }, []); - const invoke = React.useCallback(async (endpoint, payload = {}, signal) => { - return unwrapRpcResult(await rpcCall(endpoint, payload, signal)); + const invoke = React2.useCallback(async (endpoint, payload = {}, signal) => { + return unwrapRpcResult2(await rpcCall(endpoint, payload, signal)); }, [rpcCall]); - const mergeSnapshot = React.useCallback((snapshot, { restoreProvisioning = true } = {}) => { + const mergeSnapshot = React2.useCallback((snapshot, { restoreProvisioning = true } = {}) => { setModel((current) => { if (snapshot.revision > 0 && current.revision > snapshot.revision) return current; let provisioning = current.provisioning; @@ -1453,7 +2571,7 @@ function FeishuSettingsTab({ rpcCall }) { }; }); }, []); - const loadStatus = React.useCallback(async ({ signal, silent = false, restoreProvisioning = true } = {}) => { + const loadStatus = React2.useCallback(async ({ signal, silent = false, restoreProvisioning = true } = {}) => { if (!silent) setPageBusy(true); try { const snapshot = normalizeBotsSnapshot(await invoke(FEISHU_ENDPOINTS.status, {}, signal)); @@ -1461,19 +2579,19 @@ function FeishuSettingsTab({ rpcCall }) { return snapshot; } catch (error) { if (error?.name === "AbortError") return void 0; - const presented = presentError(error); + const presented = presentError2(error); setModel((current) => current.phase === "loading" || !silent ? { ...current, phase: "error", pageError: presented } : { ...current, statusError: presented }); return void 0; } finally { if (!silent) setPageBusy(false); } }, [invoke, mergeSnapshot]); - React.useEffect(() => { + React2.useEffect(() => { const controller = new AbortController(); void loadStatus({ signal: controller.signal }); return () => controller.abort(); }, [loadStatus]); - React.useEffect(() => { + React2.useEffect(() => { if (model.phase !== "ready") return void 0; const controller = new AbortController(); let inFlight = false; @@ -1488,7 +2606,7 @@ function FeishuSettingsTab({ rpcCall }) { window.clearInterval(timer); }; }, [loadStatus, model.phase]); - React.useEffect(() => { + React2.useEffect(() => { if (!focusBotId) return; const node = cardRefs.current.get(focusBotId); if (!node) return; @@ -1496,7 +2614,7 @@ function FeishuSettingsTab({ rpcCall }) { node.focus({ preventScroll: true }); setFocusBotId(null); }, [focusBotId, model.bots]); - const startProvisioning = React.useCallback(async ({ replace = false } = {}) => { + const startProvisioning = React2.useCallback(async ({ replace = false } = {}) => { setProvisionBusy(true); announce(""); const previousAttemptId = model.provisioning?.attemptId; @@ -1509,18 +2627,18 @@ function FeishuSettingsTab({ rpcCall }) { if (replace && previousAttemptId) { await invoke(FEISHU_ENDPOINTS.cancelProvisioning, { attemptId: previousAttemptId }); } - const provision2 = normalizeProvisioning(await invoke( + const provision2 = normalizeProvisioning2(await invoke( FEISHU_ENDPOINTS.beginProvisioning, { locale: "zh-CN" } )); - const timestamp2 = Date.now(); - setNow(timestamp2); + const timestamp3 = Date.now(); + setNow(timestamp3); setModel((current) => ({ ...current, provisioning: { phase: "qr", ...provision2, - durationMs: Math.max(1, provision2.expiresAt - timestamp2), + durationMs: Math.max(1, provision2.expiresAt - timestamp3), expired: false } })); @@ -1528,13 +2646,13 @@ function FeishuSettingsTab({ rpcCall }) { } catch (error) { setModel((current) => ({ ...current, - provisioning: { phase: "error", error: presentError(error) } + provisioning: { phase: "error", error: presentError2(error) } })); } finally { setProvisionBusy(false); } }, [announce, invoke, model.provisioning?.attemptId]); - const cancelProvisioning = React.useCallback(async () => { + const cancelProvisioning = React2.useCallback(async () => { const attemptId = model.provisioning?.attemptId; setProvisionBusy(true); try { @@ -1546,19 +2664,19 @@ function FeishuSettingsTab({ rpcCall }) { } catch (error) { setModel((current) => ({ ...current, - provisioning: { phase: "error", attemptId, error: presentError(error) } + provisioning: { phase: "error", attemptId, error: presentError2(error) } })); } finally { setProvisionBusy(false); } }, [announce, invoke, loadStatus, model.provisioning?.attemptId]); - React.useEffect(() => { + React2.useEffect(() => { const provision2 = model.provisioning; if (!provision2 || provision2.phase !== "qr" || provision2.expired) return void 0; const tick = () => { - const timestamp2 = Date.now(); - setNow(timestamp2); - if (timestamp2 >= provision2.expiresAt) { + const timestamp3 = Date.now(); + setNow(timestamp3); + if (timestamp3 >= provision2.expiresAt) { setModel((current) => current.provisioning?.attemptId === provision2.attemptId ? { ...current, provisioning: { ...current.provisioning, expired: true } } : current); } }; @@ -1566,7 +2684,7 @@ function FeishuSettingsTab({ rpcCall }) { const timer = window.setInterval(tick, 1e3); return () => window.clearInterval(timer); }, [model.provisioning]); - React.useEffect(() => { + React2.useEffect(() => { const provision2 = model.provisioning; if (!provision2 || !["qr", "connecting"].includes(provision2.phase) || !provision2.attemptId || provision2.expired) return void 0; const controller = new AbortController(); @@ -1620,7 +2738,7 @@ function FeishuSettingsTab({ rpcCall }) { provisioning: { phase: "error", attemptId: provision2.attemptId, - error: presentError(error) + error: presentError2(error) } } : current); } @@ -1630,7 +2748,7 @@ function FeishuSettingsTab({ rpcCall }) { window.clearTimeout(timer); }; }, [announce, invoke, loadStatus, model.provisioning]); - const setBotBusy = React.useCallback((botId, value) => { + const setBotBusy = React2.useCallback((botId, value) => { setBusyByBot((current) => { const next = { ...current }; if (value) next[botId] = value; @@ -1638,15 +2756,15 @@ function FeishuSettingsTab({ rpcCall }) { return next; }); }, []); - const setBotError = React.useCallback((botId, error) => { + const setBotError = React2.useCallback((botId, error) => { setErrorsByBot((current) => { const next = { ...current }; - if (error) next[botId] = presentError(error); + if (error) next[botId] = presentError2(error); else delete next[botId]; return next; }); }, []); - const reconnectOneBot = React.useCallback(async (connection) => { + const reconnectOneBot = React2.useCallback(async (connection) => { const { botId, bot } = connection; setBotBusy(botId, "reconnect"); setBotError(botId, null); @@ -1669,15 +2787,15 @@ function FeishuSettingsTab({ rpcCall }) { setBotBusy(botId, null); } }, [announce, invoke, mergeSnapshot, setBotBusy, setBotError]); - const requestRemove = React.useCallback((connection) => { + const requestRemove = React2.useCallback((connection) => { setRemoveTargetId(connection.botId); }, []); - const cancelRemove = React.useCallback(() => { + const cancelRemove = React2.useCallback(() => { const botId = removeTargetId; setRemoveTargetId(null); window.requestAnimationFrame(() => removeButtonRefs.current.get(botId)?.focus()); }, [removeTargetId]); - const confirmRemove = React.useCallback(async (connection) => { + const confirmRemove = React2.useCallback(async (connection) => { const { botId, bot } = connection; setBotBusy(botId, "delete"); setBotError(botId, null); @@ -1708,9 +2826,9 @@ function FeishuSettingsTab({ rpcCall }) { const provision = model.provisioning; let provisionContent = null; if (provision?.phase === "creating") { - provisionContent = h(ProvisionProgress, { phase: "creating", busy: provisionBusy }); + provisionContent = h2(ProvisionProgress, { phase: "creating", busy: provisionBusy }); } else if (provision?.phase === "qr") { - provisionContent = h(QrPane, { + provisionContent = h2(QrPane, { provision, now, onRefresh: () => void startProvisioning({ replace: true }), @@ -1718,60 +2836,60 @@ function FeishuSettingsTab({ rpcCall }) { busy: provisionBusy || model.phase !== "ready" }); } else if (provision?.phase === "connecting") { - provisionContent = h(ProvisionProgress, { + provisionContent = h2(ProvisionProgress, { phase: "connecting", onCancel: () => void cancelProvisioning(), busy: provisionBusy }); } else if (provision?.phase === "error") { - provisionContent = h(ProvisionError, { + provisionContent = h2(ProvisionError2, { error: provision.error, onRetry: () => void startProvisioning({ replace: Boolean(provision.attemptId) }), onCancel: () => void cancelProvisioning(), busy: provisionBusy }); } - const setCardRef = React.useCallback((botId, node) => { + const setCardRef = React2.useCallback((botId, node) => { if (node) cardRefs.current.set(botId, node); else cardRefs.current.delete(botId); }, []); - const setRemoveButtonRef = React.useCallback((botId, node) => { + const setRemoveButtonRef = React2.useCallback((botId, node) => { if (node) removeButtonRefs.current.set(botId, node); else removeButtonRefs.current.delete(botId); }, []); - return h( + return h2( "section", { className: "bxf-page", "aria-label": "\u98DE\u4E66\u673A\u5668\u4EBA\u8BBE\u7F6E" }, - h(Heading, { + h2(Heading2, { totals: model.totals, onAdd: () => void startProvisioning(), adding: Boolean(provision), busy: provisionBusy, addButtonRef }), - h("div", { + h2("div", { className: "bxf-visuallyHidden", role: "status", "aria-live": "polite", "aria-atomic": "true" }, announcement), - model.statusError ? h( + model.statusError ? h2( "div", { className: "bxf-statusNotice", role: "status" }, - h(AlertIcon, { size: 16 }), - h("span", null, `\u72B6\u6001\u81EA\u52A8\u5237\u65B0\u5931\u8D25\uFF1A${model.statusError.message}`), - h(Button, { size: "small", onClick: () => void loadStatus({ silent: true }), disabled: pageBusy }, "\u7ACB\u5373\u91CD\u8BD5") + h2(AlertIcon, { size: 16 }), + h2("span", null, `\u72B6\u6001\u81EA\u52A8\u5237\u65B0\u5931\u8D25\uFF1A${model.statusError.message}`), + h2(Button3, { size: "small", onClick: () => void loadStatus({ silent: true }), disabled: pageBusy }, "\u7ACB\u5373\u91CD\u8BD5") ) : null, - model.phase === "loading" ? h(LoadingView) : model.phase === "error" ? h(PageError, { + model.phase === "loading" ? h2(LoadingView2) : model.phase === "error" ? h2(PageError, { error: model.pageError ?? { message: "\u65E0\u6CD5\u8BFB\u53D6\u8FDE\u63A5\u72B6\u6001" }, onRetry: () => void loadStatus(), busy: pageBusy - }) : h( - React.Fragment, + }) : h2( + React2.Fragment, null, provisionContent, - model.bots.length === 0 && !provision ? h(EmptyView, { onStart: () => void startProvisioning(), busy: provisionBusy }) : null, - model.bots.length > 0 ? h(BotList, { + model.bots.length === 0 && !provision ? h2(EmptyView2, { onStart: () => void startProvisioning(), busy: provisionBusy }) : null, + model.bots.length > 0 ? h2(BotList, { bots: model.bots, busyByBot, errorsByBot, @@ -1788,7 +2906,7 @@ function FeishuSettingsTab({ rpcCall }) { } // plugin-src/client/channels/weixin/index.js -var React2 = __toESM(require("react"), 1); +var React3 = __toESM(require("react"), 1); // plugin-src/client/channels/weixin/api.js var WEIXIN_RPC_CHANNEL = "/weixin"; @@ -1801,8 +2919,8 @@ var WEIXIN_ENDPOINTS = Object.freeze({ reconnectBot: "bot.reconnect", deleteBot: "bot.delete" }); -var ACCOUNT_STATES = /* @__PURE__ */ new Set(["connected", "connecting", "offline", "error"]); -var PROVISION_STATES = /* @__PURE__ */ new Set([ +var ACCOUNT_STATES2 = /* @__PURE__ */ new Set(["connected", "connecting", "offline", "error"]); +var PROVISION_STATES2 = /* @__PURE__ */ new Set([ "starting", "pending", "scanned", @@ -1813,17 +2931,17 @@ var PROVISION_STATES = /* @__PURE__ */ new Set([ "failed", "cancelled" ]); -function isRecord2(value) { +function isRecord3(value) { return value !== null && typeof value === "object" && !Array.isArray(value); } function string(value, fallback = "") { return typeof value === "string" && value.trim() ? value.trim() : fallback; } -function timestamp(value) { +function timestamp2(value) { return typeof value === "number" && Number.isFinite(value) ? value : null; } -function unwrapRpcResult2(result) { - if (!isRecord2(result) || typeof result.ok !== "boolean") { +function unwrapRpcResult3(result) { + if (!isRecord3(result) || typeof result.ok !== "boolean") { throw new Error("\u5FAE\u4FE1\u670D\u52A1\u8FD4\u56DE\u4E86\u65E0\u6CD5\u8BC6\u522B\u7684\u54CD\u5E94"); } if (!result.ok) { @@ -1833,7 +2951,7 @@ function unwrapRpcResult2(result) { } return result.value; } -function safeQrSource2(value) { +function safeQrSource3(value) { return typeof value === "string" && /^data:image\/(?:png|webp|svg\+xml)(?:;charset=[^;,]+)?;base64,/i.test(value) ? value : void 0; } function safeVerificationUrl(value) { @@ -1846,25 +2964,25 @@ function safeVerificationUrl(value) { return void 0; } } -function normalizeProvisioning2(value) { - if (!isRecord2(value) || !string(value.attemptId)) { +function normalizeProvisioning3(value) { + if (!isRecord3(value) || !string(value.attemptId)) { throw new Error("\u5FAE\u4FE1\u626B\u7801\u670D\u52A1\u6CA1\u6709\u8FD4\u56DE\u6709\u6548\u7684\u7ED1\u5B9A\u4EFB\u52A1"); } - const status = PROVISION_STATES.has(value.status) ? value.status : "failed"; + const status = PROVISION_STATES2.has(value.status) ? value.status : "failed"; const result = { attemptId: string(value.attemptId), status, - expiresAt: timestamp(value.expiresAt) ?? Date.now(), + expiresAt: timestamp2(value.expiresAt) ?? Date.now(), pollIntervalMs: Math.min(5e3, Math.max(500, Number(value.pollIntervalMs) || 1e3)), verificationRequired: value.verificationRequired === true || status === "needs_verification" }; const verificationUrl = safeVerificationUrl(value.verificationUrl); - const qrCodeDataUrl = safeQrSource2(value.qrCodeDataUrl); + const qrCodeDataUrl = safeQrSource3(value.qrCodeDataUrl); if (verificationUrl) result.verificationUrl = verificationUrl; if (qrCodeDataUrl) result.qrCodeDataUrl = qrCodeDataUrl; if (string(value.botId)) result.botId = string(value.botId); if (value.alreadyConnected === true) result.alreadyConnected = true; - if (isRecord2(value.error)) { + if (isRecord3(value.error)) { result.error = { code: string(value.error.code, "WEIXIN_PROVISION_FAILED"), message: string(value.error.message, "\u5FAE\u4FE1\u7ED1\u5B9A\u6CA1\u6709\u5B8C\u6210") @@ -1872,9 +2990,9 @@ function normalizeProvisioning2(value) { } return result; } -function normalizeBot2(value) { - if (!isRecord2(value) || !string(value.botId) || !isRecord2(value.bot)) return null; - const state = ACCOUNT_STATES.has(value.state) ? value.state : "error"; +function normalizeBot3(value) { + if (!isRecord3(value) || !string(value.botId) || !isRecord3(value.bot)) return null; + const state = ACCOUNT_STATES2.has(value.state) ? value.state : "error"; const connected = value.connected === true; return { botId: string(value.botId), @@ -1888,23 +3006,23 @@ function normalizeBot2(value) { health: { status: string(value.health?.status, connected ? "healthy" : "offline"), summary: string(value.health?.summary, connected ? "\u5FAE\u4FE1\u8FDE\u63A5\u6B63\u5E38" : "\u5FAE\u4FE1\u8FDE\u63A5\u672A\u5C31\u7EEA"), - lastCheckedAt: timestamp(value.health?.lastCheckedAt) + lastCheckedAt: timestamp2(value.health?.lastCheckedAt) }, stats: { messagesReceived: Math.max(0, Number(value.stats?.messagesReceived) || 0), messagesReplied: Math.max(0, Number(value.stats?.messagesReplied) || 0) }, - error: isRecord2(value.error) ? { + error: isRecord3(value.error) ? { code: string(value.error.code, "WEIXIN_ACCOUNT_ERROR"), message: string(value.error.message, "\u5FAE\u4FE1\u8FDE\u63A5\u672A\u5C31\u7EEA") } : null }; } -function normalizeSnapshot(value) { - if (!isRecord2(value) || !Array.isArray(value.bots)) { +function normalizeSnapshot2(value) { + if (!isRecord3(value) || !Array.isArray(value.bots)) { throw new Error("\u5FAE\u4FE1\u670D\u52A1\u6CA1\u6709\u8FD4\u56DE\u6709\u6548\u7684\u8D26\u53F7\u5217\u8868"); } - const bots = value.bots.map(normalizeBot2).filter(Boolean); + const bots = value.bots.map(normalizeBot3).filter(Boolean); return { schemaVersion: Number(value.schemaVersion) || 1, revision: Number(value.revision) || 0, @@ -1914,23 +3032,23 @@ function normalizeSnapshot(value) { configured: bots.length, connected: bots.filter((bot) => bot.connected).length }, - provisioning: value.provisioning ? normalizeProvisioning2(value.provisioning) : null + provisioning: value.provisioning ? normalizeProvisioning3(value.provisioning) : null }; } -function presentError2(error) { +function presentError3(error) { return { code: string(error?.code, "WEIXIN_ERROR"), message: string(error?.message, "\u5FAE\u4FE1\u64CD\u4F5C\u5931\u8D25\uFF0C\u8BF7\u7A0D\u540E\u91CD\u8BD5") }; } -function formatRemaining2(milliseconds) { +function formatRemaining3(milliseconds) { const seconds = Math.max(0, Math.ceil(milliseconds / 1e3)); return `${Math.floor(seconds / 60)}:${String(seconds % 60).padStart(2, "0")}`; } // plugin-src/client/channels/weixin/styles.js var WEIXIN_STYLE_ID = "xmanrui-dsh-weixin-settings"; -var CSS2 = String.raw` +var CSS3 = String.raw` .dxw-page { --dxw-accent: #07c160; --dxw-accent-dark: #05994c; @@ -2045,15 +3163,15 @@ function installWeixinStyles() { const style = document.createElement("style"); style.dataset.plugin = "@xmanrui/dsh-weixin"; style.dataset.pluginCss = WEIXIN_STYLE_ID; - style.textContent = CSS2; + style.textContent = CSS3; document.head.appendChild(style); return () => style.remove(); } // plugin-src/client/channels/weixin/index.js -var h2 = React2.createElement; +var h3 = React3.createElement; function WeixinIcon({ size = 26 }) { - return h2( + return h3( "svg", { width: size, @@ -2063,25 +3181,25 @@ function WeixinIcon({ size = 26 }) { xmlns: "http://www.w3.org/2000/svg", "aria-hidden": "true" }, - h2("path", { + h3("path", { d: "M9.7 4.2c-4.15 0-7.5 2.72-7.5 6.08 0 1.91 1.08 3.61 2.78 4.72l-.7 2.35 2.75-1.37c.84.25 1.73.38 2.67.38 4.14 0 7.5-2.72 7.5-6.08S13.84 4.2 9.7 4.2Z", fill: "currentColor", opacity: ".96" }), - h2("path", { + h3("path", { d: "M14.4 8.2c4.08 0 7.4 2.66 7.4 5.95 0 1.78-.97 3.38-2.5 4.47l.58 2-2.36-1.18c-.97.43-2.03.65-3.12.65-4.09 0-7.4-2.66-7.4-5.94 0-3.29 3.31-5.95 7.4-5.95Z", fill: "currentColor", stroke: "white", strokeWidth: ".8" }), - h2("circle", { cx: "7.25", cy: "9.5", r: "1", fill: "white" }), - h2("circle", { cx: "11.55", cy: "9.5", r: "1", fill: "white" }), - h2("circle", { cx: "12.1", cy: "13.2", r: ".9", fill: "white" }), - h2("circle", { cx: "16.3", cy: "13.2", r: ".9", fill: "white" }) + h3("circle", { cx: "7.25", cy: "9.5", r: "1", fill: "white" }), + h3("circle", { cx: "11.55", cy: "9.5", r: "1", fill: "white" }), + h3("circle", { cx: "12.1", cy: "13.2", r: ".9", fill: "white" }), + h3("circle", { cx: "16.3", cy: "13.2", r: ".9", fill: "white" }) ); } -var Button3 = React2.forwardRef(function Button4({ children, kind = "secondary", className = "", ...props }, ref) { - return h2("button", { +var Button5 = React3.forwardRef(function Button6({ children, kind = "secondary", className = "", ...props }, ref) { + return h3("button", { ...props, ref, type: "button", @@ -2089,25 +3207,25 @@ var Button3 = React2.forwardRef(function Button4({ children, kind = "secondary", "data-kind": kind }, children); }); -function Heading2({ totals, adding, busy, onAdd, addButtonRef }) { - return h2( +function Heading3({ totals, adding, busy, onAdd, addButtonRef }) { + return h3( "div", { className: "dxw-heading" }, - h2( + h3( "div", { className: "dxw-tools" }, - totals.configured > 0 ? h2( + totals.configured > 0 ? h3( "div", { className: "dxw-badge" }, - h2("span", { className: "dxw-dot", "data-tone": totals.connected > 0 ? "success" : "warning" }), - h2("span", null, `${totals.connected} / ${totals.configured} \u5728\u7EBF`) + h3("span", { className: "dxw-dot", "data-tone": totals.connected > 0 ? "success" : "warning" }), + h3("span", null, `${totals.connected} / ${totals.configured} \u5728\u7EBF`) ) : null, - h2( + h3( "div", { className: "dxw-badge", title: "bot_token \u4EC5\u4FDD\u5B58\u5728 Harness Host \u51ED\u636E\u670D\u52A1\u4E2D" }, - h2("span", null, "\u51ED\u636E\u4EC5\u4FDD\u5B58\u5728\u672C\u673A") + h3("span", null, "\u51ED\u636E\u4EC5\u4FDD\u5B58\u5728\u672C\u673A") ), - h2(Button3, { + h3(Button5, { kind: "primary", onClick: onAdd, disabled: adding || busy, @@ -2116,128 +3234,128 @@ function Heading2({ totals, adding, busy, onAdd, addButtonRef }) { ) ); } -function LoadingView2() { - return h2( +function LoadingView3() { + return h3( "div", { className: "dxw-card dxw-loading", "aria-busy": "true" }, - h2("div", { className: "dxw-spinner" }), - h2("span", null, "\u6B63\u5728\u8BFB\u53D6\u5FAE\u4FE1\u8FDE\u63A5\u72B6\u6001\u2026") + h3("div", { className: "dxw-spinner" }), + h3("span", null, "\u6B63\u5728\u8BFB\u53D6\u5FAE\u4FE1\u8FDE\u63A5\u72B6\u6001\u2026") ); } -function EmptyView2({ onStart, busy }) { - return h2( +function EmptyView3({ onStart, busy }) { + return h3( "div", { className: "dxw-card" }, - h2( + h3( "div", { className: "dxw-cardBody dxw-empty" }, - h2( + h3( "div", null, - h2( + h3( "div", { className: "dxw-stateLabel" }, - h2("span", { className: "dxw-dot" }), - h2("span", null, "\u5C1A\u672A\u7ED1\u5B9A\u5FAE\u4FE1") + h3("span", { className: "dxw-dot" }), + h3("span", null, "\u5C1A\u672A\u7ED1\u5B9A\u5FAE\u4FE1") ), - h2("h3", null, "\u626B\u4E00\u6B21\u7801\uFF0C\u5C31\u80FD\u5728\u5FAE\u4FE1\u91CC\u4F7F\u7528 Harness"), - h2("p", null, "\u4E8C\u7EF4\u7801\u7531\u817E\u8BAF\u5FAE\u4FE1 iLink \u670D\u52A1\u7B7E\u53D1\u3002\u7528\u624B\u673A\u5FAE\u4FE1\u626B\u63CF\u5E76\u786E\u8BA4\u540E\uFF0C\u8D26\u53F7\u51ED\u636E\u4F1A\u76F4\u63A5\u5199\u5165 Harness Host\uFF0C\u6D4F\u89C8\u5668\u4E0D\u4F1A\u6536\u5230 bot_token\u3002"), - h2( + h3("h3", null, "\u626B\u4E00\u6B21\u7801\uFF0C\u5C31\u80FD\u5728\u5FAE\u4FE1\u91CC\u4F7F\u7528 Harness"), + h3("p", null, "\u4E8C\u7EF4\u7801\u7531\u817E\u8BAF\u5FAE\u4FE1 iLink \u670D\u52A1\u7B7E\u53D1\u3002\u7528\u624B\u673A\u5FAE\u4FE1\u626B\u63CF\u5E76\u786E\u8BA4\u540E\uFF0C\u8D26\u53F7\u51ED\u636E\u4F1A\u76F4\u63A5\u5199\u5165 Harness Host\uFF0C\u6D4F\u89C8\u5668\u4E0D\u4F1A\u6536\u5230 bot_token\u3002"), + h3( "div", { className: "dxw-actions" }, - h2( - Button3, + h3( + Button5, { kind: "primary", onClick: onStart, disabled: busy }, busy ? "\u6B63\u5728\u751F\u6210\u4E8C\u7EF4\u7801\u2026" : "\u751F\u6210\u5FAE\u4FE1\u4E8C\u7EF4\u7801" ) ) ), - h2("div", { className: "dxw-logo", "aria-hidden": "true" }, h2(WeixinIcon, { size: 64 })) + h3("div", { className: "dxw-logo", "aria-hidden": "true" }, h3(WeixinIcon, { size: 64 })) ) ); } -function QrPanel({ provision, now, busy, onRefresh, onCancel }) { - const [imageFailed, setImageFailed] = React2.useState(false); - const source = safeQrSource2(provision.qrCodeDataUrl); +function QrPanel2({ provision, now, busy, onRefresh, onCancel }) { + const [imageFailed, setImageFailed] = React3.useState(false); + const source = safeQrSource3(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); - React2.useEffect(() => setImageFailed(false), [source]); - return h2( + React3.useEffect(() => setImageFailed(false), [source]); + return h3( "div", { className: "dxw-card" }, - h2( + h3( "div", { className: "dxw-cardBody dxw-qrLayout" }, - h2( + h3( "div", { className: "dxw-qrColumn" }, - h2( + h3( "div", { className: "dxw-qrFrame" }, - source && !imageFailed ? h2("img", { + source && !imageFailed ? h3("img", { src: source, alt: "\u7528\u4E8E\u628A\u5FAE\u4FE1\u673A\u5668\u4EBA\u7ED1\u5B9A\u5230 DeepSeek Harness \u7684\u4E00\u6B21\u6027\u4E8C\u7EF4\u7801", onError: () => setImageFailed(true) - }) : h2("div", { className: "dxw-qrFallback" }, "\u4E8C\u7EF4\u7801\u56FE\u7247\u672A\u5C31\u7EEA\uFF0C\u8BF7\u4F7F\u7528\u5907\u7528\u94FE\u63A5\u3002"), - expired ? h2("div", { className: "dxw-expired" }, "\u4E8C\u7EF4\u7801\u5DF2\u8FC7\u671F\n\u8BF7\u91CD\u65B0\u751F\u6210") : null + }) : h3("div", { className: "dxw-qrFallback" }, "\u4E8C\u7EF4\u7801\u56FE\u7247\u672A\u5C31\u7EEA\uFF0C\u8BF7\u4F7F\u7528\u5907\u7528\u94FE\u63A5\u3002"), + expired ? h3("div", { className: "dxw-expired" }, "\u4E8C\u7EF4\u7801\u5DF2\u8FC7\u671F\n\u8BF7\u91CD\u65B0\u751F\u6210") : null ), - h2( + h3( "div", { className: "dxw-countdown" }, - h2("div", null, h2("span", null, "\u4E8C\u7EF4\u7801\u6709\u6548\u65F6\u95F4"), h2("strong", null, formatRemaining2(remaining))), - h2( + h3("div", null, h3("span", null, "\u4E8C\u7EF4\u7801\u6709\u6548\u65F6\u95F4"), h3("strong", null, formatRemaining3(remaining))), + h3( "div", { className: "dxw-progress", "aria-hidden": "true" }, - h2("span", { style: { "--dxw-progress": `${progress}%` } }) + h3("span", { style: { "--dxw-progress": `${progress}%` } }) ) ) ), - h2( + h3( "div", { className: "dxw-qrCopy" }, - h2( + h3( "div", { className: "dxw-stateLabel" }, - h2("span", { className: "dxw-dot", "data-tone": provision.status === "scanned" ? "success" : "warning" }), - h2("span", null, provision.status === "scanned" ? "\u5DF2\u626B\u7801\uFF0C\u8BF7\u5728\u624B\u673A\u4E0A\u786E\u8BA4" : "\u7B49\u5F85\u5FAE\u4FE1\u626B\u7801") + h3("span", { className: "dxw-dot", "data-tone": provision.status === "scanned" ? "success" : "warning" }), + h3("span", null, provision.status === "scanned" ? "\u5DF2\u626B\u7801\uFF0C\u8BF7\u5728\u624B\u673A\u4E0A\u786E\u8BA4" : "\u7B49\u5F85\u5FAE\u4FE1\u626B\u7801") ), - h2("h3", null, expired ? "\u4E8C\u7EF4\u7801\u5DF2\u5931\u6548" : "\u4F7F\u7528\u624B\u673A\u5FAE\u4FE1\u626B\u63CF\u4E8C\u7EF4\u7801"), - h2("p", null, "\u8BF7\u5728\u624B\u673A\u4E0A\u6838\u5BF9\u5E76\u786E\u8BA4\u6388\u6743\u3002\u90E8\u5206\u8D26\u53F7\u4F1A\u989D\u5916\u663E\u793A\u4E00\u4E2A\u914D\u5BF9\u6570\u5B57\uFF0C\u9875\u9762\u4F1A\u5728\u9700\u8981\u65F6\u63D0\u793A\u8F93\u5165\u3002"), - h2( + h3("h3", null, expired ? "\u4E8C\u7EF4\u7801\u5DF2\u5931\u6548" : "\u4F7F\u7528\u624B\u673A\u5FAE\u4FE1\u626B\u63CF\u4E8C\u7EF4\u7801"), + h3("p", null, "\u8BF7\u5728\u624B\u673A\u4E0A\u6838\u5BF9\u5E76\u786E\u8BA4\u6388\u6743\u3002\u90E8\u5206\u8D26\u53F7\u4F1A\u989D\u5916\u663E\u793A\u4E00\u4E2A\u914D\u5BF9\u6570\u5B57\uFF0C\u9875\u9762\u4F1A\u5728\u9700\u8981\u65F6\u63D0\u793A\u8F93\u5165\u3002"), + h3( "ol", { className: "dxw-steps" }, - h2("li", null, "\u6253\u5F00\u624B\u673A\u5FAE\u4FE1\u5E76\u626B\u63CF\u5DE6\u4FA7\u4E8C\u7EF4\u7801"), - h2("li", null, "\u5728\u5FAE\u4FE1\u4E2D\u786E\u8BA4\u8FDE\u63A5\u8BE5\u673A\u5668\u4EBA"), - h2("li", null, "\u4FDD\u6301\u672C\u9875\u6253\u5F00\uFF0C\u7B49\u5F85\u6D88\u606F\u957F\u8F6E\u8BE2\u53D8\u4E3A\u5728\u7EBF") + h3("li", null, "\u6253\u5F00\u624B\u673A\u5FAE\u4FE1\u5E76\u626B\u63CF\u5DE6\u4FA7\u4E8C\u7EF4\u7801"), + h3("li", null, "\u5728\u5FAE\u4FE1\u4E2D\u786E\u8BA4\u8FDE\u63A5\u8BE5\u673A\u5668\u4EBA"), + h3("li", null, "\u4FDD\u6301\u672C\u9875\u6253\u5F00\uFF0C\u7B49\u5F85\u6D88\u606F\u957F\u8F6E\u8BE2\u53D8\u4E3A\u5728\u7EBF") ), - h2( + h3( "div", { className: "dxw-actions" }, - expired ? h2(Button3, { kind: "primary", onClick: onRefresh, disabled: busy }, "\u91CD\u65B0\u751F\u6210\u4E8C\u7EF4\u7801") : null, - href ? h2("a", { + expired ? h3(Button5, { kind: "primary", onClick: onRefresh, disabled: busy }, "\u91CD\u65B0\u751F\u6210\u4E8C\u7EF4\u7801") : null, + href ? h3("a", { className: "dxw-button", href, target: "_blank", rel: "noopener noreferrer" }, "\u6253\u5F00\u5907\u7528\u94FE\u63A5") : null, - !expired ? h2(Button3, { onClick: onRefresh, disabled: busy }, "\u6362\u4E00\u4E2A\u4E8C\u7EF4\u7801") : null, - h2(Button3, { onClick: onCancel, disabled: busy }, "\u53D6\u6D88") + !expired ? h3(Button5, { onClick: onRefresh, disabled: busy }, "\u6362\u4E00\u4E2A\u4E8C\u7EF4\u7801") : null, + h3(Button5, { onClick: onCancel, disabled: busy }, "\u53D6\u6D88") ) ) ) ); } function VerificationPanel({ provision, busy, onSubmit, onCancel }) { - const [code, setCode] = React2.useState(""); + const [code, setCode] = React3.useState(""); const valid = /^\d{4,8}$/.test(code); - React2.useEffect(() => setCode(""), [provision.attemptId]); - return h2( + React3.useEffect(() => setCode(""), [provision.attemptId]); + return h3( "div", { className: "dxw-card" }, - h2( + h3( "form", { className: "dxw-verify", @@ -2246,18 +3364,18 @@ function VerificationPanel({ provision, busy, onSubmit, onCancel }) { if (valid && !busy) onSubmit(code); } }, - h2( + h3( "div", { className: "dxw-stateLabel" }, - h2("span", { className: "dxw-dot", "data-tone": "warning" }), - h2("span", null, "\u9700\u8981\u914D\u5BF9\u7801") + h3("span", { className: "dxw-dot", "data-tone": "warning" }), + h3("span", null, "\u9700\u8981\u914D\u5BF9\u7801") ), - h2("h3", null, "\u8F93\u5165\u624B\u673A\u5FAE\u4FE1\u663E\u793A\u7684\u6570\u5B57"), - h2("p", null, "\u8FD9\u662F\u5FAE\u4FE1\u9644\u52A0\u7684\u5B89\u5168\u786E\u8BA4\u6B65\u9AA4\u3002\u914D\u5BF9\u7801\u53EA\u7528\u4E8E\u672C\u6B21\u626B\u7801\u8F6E\u8BE2\uFF0C\u4E0D\u4F1A\u5199\u5165\u914D\u7F6E\u6216\u65E5\u5FD7\u3002"), - h2( + h3("h3", null, "\u8F93\u5165\u624B\u673A\u5FAE\u4FE1\u663E\u793A\u7684\u6570\u5B57"), + h3("p", null, "\u8FD9\u662F\u5FAE\u4FE1\u9644\u52A0\u7684\u5B89\u5168\u786E\u8BA4\u6B65\u9AA4\u3002\u914D\u5BF9\u7801\u53EA\u7528\u4E8E\u672C\u6B21\u626B\u7801\u8F6E\u8BE2\uFF0C\u4E0D\u4F1A\u5199\u5165\u914D\u7F6E\u6216\u65E5\u5FD7\u3002"), + h3( "div", { className: "dxw-codeRow" }, - h2("input", { + h3("input", { className: "dxw-input", value: code, inputMode: "numeric", @@ -2267,137 +3385,137 @@ function VerificationPanel({ provision, busy, onSubmit, onCancel }) { onChange: (event) => setCode(event.target.value.replace(/\D/g, "").slice(0, 8)), autoFocus: true }), - h2("button", { + h3("button", { type: "submit", className: "dxw-button", "data-kind": "primary", disabled: !valid || busy }, busy ? "\u6B63\u5728\u9A8C\u8BC1\u2026" : "\u7EE7\u7EED\u8FDE\u63A5") ), - h2(Button3, { onClick: onCancel, disabled: busy }, "\u53D6\u6D88\u7ED1\u5B9A") + h3(Button5, { onClick: onCancel, disabled: busy }, "\u53D6\u6D88\u7ED1\u5B9A") ) ); } -function ProgressPanel({ scanned, onCancel, busy }) { - return h2( +function ProgressPanel2({ scanned, onCancel, busy }) { + return h3( "div", { className: "dxw-card dxw-loading", "aria-busy": "true" }, - h2("div", { className: "dxw-spinner" }), - h2("h3", null, scanned ? "\u5FAE\u4FE1\u5DF2\u786E\u8BA4\uFF0C\u6B63\u5728\u542F\u52A8\u6D88\u606F\u8FDE\u63A5" : "\u6B63\u5728\u51C6\u5907\u5FAE\u4FE1\u4E8C\u7EF4\u7801"), - h2("p", null, scanned ? "\u6B63\u5728\u4FDD\u5B58\u51ED\u636E\u5E76\u9A8C\u8BC1 Harness \u4E0E\u5FAE\u4FE1\u957F\u8F6E\u8BE2\u3002" : "\u6B63\u5728\u8054\u7CFB\u817E\u8BAF\u5FAE\u4FE1 iLink \u670D\u52A1\u3002"), - onCancel ? h2( + h3("div", { className: "dxw-spinner" }), + h3("h3", null, scanned ? "\u5FAE\u4FE1\u5DF2\u786E\u8BA4\uFF0C\u6B63\u5728\u542F\u52A8\u6D88\u606F\u8FDE\u63A5" : "\u6B63\u5728\u51C6\u5907\u5FAE\u4FE1\u4E8C\u7EF4\u7801"), + h3("p", null, scanned ? "\u6B63\u5728\u4FDD\u5B58\u51ED\u636E\u5E76\u9A8C\u8BC1 Harness \u4E0E\u5FAE\u4FE1\u957F\u8F6E\u8BE2\u3002" : "\u6B63\u5728\u8054\u7CFB\u817E\u8BAF\u5FAE\u4FE1 iLink \u670D\u52A1\u3002"), + onCancel ? h3( "div", { className: "dxw-actions", style: { justifyContent: "center", marginTop: 14 } }, - h2(Button3, { onClick: onCancel, disabled: busy }, "\u53D6\u6D88") + h3(Button5, { onClick: onCancel, disabled: busy }, "\u53D6\u6D88") ) : null ); } -function ProvisionError2({ provision, busy, onRetry, onClose }) { +function ProvisionError3({ provision, busy, onRetry, onClose }) { const error = provision.error ?? { code: "WEIXIN_PROVISION_FAILED", message: "\u5FAE\u4FE1\u7ED1\u5B9A\u6CA1\u6709\u5B8C\u6210" }; - return h2( + return h3( "div", { className: "dxw-card" }, - h2( + h3( "div", { className: "dxw-error", role: "alert" }, - h2("h3", null, provision.status === "expired" ? "\u4E8C\u7EF4\u7801\u5DF2\u8FC7\u671F" : "\u5FAE\u4FE1\u6CA1\u6709\u7ED1\u5B9A\u5B8C\u6210"), - h2("p", null, error.message), - h2("span", { className: "dxw-errorCode" }, error.code), - h2( + h3("h3", null, provision.status === "expired" ? "\u4E8C\u7EF4\u7801\u5DF2\u8FC7\u671F" : "\u5FAE\u4FE1\u6CA1\u6709\u7ED1\u5B9A\u5B8C\u6210"), + h3("p", null, error.message), + h3("span", { className: "dxw-errorCode" }, error.code), + h3( "div", { className: "dxw-actions" }, - h2(Button3, { kind: "primary", onClick: onRetry, disabled: busy }, "\u91CD\u65B0\u751F\u6210\u4E8C\u7EF4\u7801"), - h2(Button3, { onClick: onClose, disabled: busy }, "\u5173\u95ED") + h3(Button5, { kind: "primary", onClick: onRetry, disabled: busy }, "\u91CD\u65B0\u751F\u6210\u4E8C\u7EF4\u7801"), + h3(Button5, { onClick: onClose, disabled: busy }, "\u5173\u95ED") ) ) ); } -function checkedTime(timestamp2) { - if (!timestamp2) return "\u5C1A\u672A\u68C0\u67E5"; +function checkedTime2(timestamp3) { + if (!timestamp3) return "\u5C1A\u672A\u68C0\u67E5"; try { return new Intl.DateTimeFormat("zh-CN", { hour: "2-digit", minute: "2-digit", second: "2-digit" - }).format(new Date(timestamp2)); + }).format(new Date(timestamp3)); } catch { return "\u521A\u521A"; } } -function AccountCard({ account, busy, removing, onReconnect, onRequestRemove, onConfirmRemove, onCancelRemove }) { +function AccountCard2({ account, busy, removing, onReconnect, onRequestRemove, onConfirmRemove, onCancelRemove }) { const state = busy === "reconnect" ? "connecting" : account.state; const tone = account.connected ? "success" : state === "error" ? "error" : "warning"; - return h2( + return h3( "article", { className: "dxw-card", tabIndex: -1, "data-bot-id": account.botId }, - h2( + h3( "div", { className: "dxw-cardBody" }, - h2( + h3( "div", { className: "dxw-accountTop" }, - h2( + h3( "div", { className: "dxw-accountIdentity" }, - h2("div", { className: "dxw-avatar", "aria-hidden": "true" }, h2(WeixinIcon, { size: 27 })), - h2("div", null, h2("h3", null, account.bot.name), h2("p", null, account.bot.accountIdMasked)) + h3("div", { className: "dxw-avatar", "aria-hidden": "true" }, h3(WeixinIcon, { size: 27 })), + h3("div", null, h3("h3", null, account.bot.name), h3("p", null, account.bot.accountIdMasked)) ), - h2( + h3( "div", { className: "dxw-health" }, - h2("span", { className: "dxw-dot", "data-tone": tone }), - h2("span", null, account.connected ? "\u8FD0\u884C\u6B63\u5E38" : state === "connecting" ? "\u6B63\u5728\u8FDE\u63A5" : "\u8FDE\u63A5\u672A\u5C31\u7EEA") + h3("span", { className: "dxw-dot", "data-tone": tone }), + h3("span", null, account.connected ? "\u8FD0\u884C\u6B63\u5E38" : state === "connecting" ? "\u6B63\u5728\u8FDE\u63A5" : "\u8FDE\u63A5\u672A\u5C31\u7EEA") ) ), - h2( + h3( "dl", { className: "dxw-metrics" }, - h2( + h3( "div", { className: "dxw-metric" }, - h2("dt", null, "\u6D88\u606F\u901A\u9053"), - h2("dd", null, account.connected ? "iLink \u957F\u8F6E\u8BE2" : "\u79BB\u7EBF") + h3("dt", null, "\u6D88\u606F\u901A\u9053"), + h3("dd", null, account.connected ? "iLink \u957F\u8F6E\u8BE2" : "\u79BB\u7EBF") ), - h2( + h3( "div", { className: "dxw-metric" }, - h2("dt", null, "\u6536\u5230 / \u56DE\u590D"), - h2("dd", null, `${account.stats.messagesReceived} / ${account.stats.messagesReplied}`) + h3("dt", null, "\u6536\u5230 / \u56DE\u590D"), + h3("dd", null, `${account.stats.messagesReceived} / ${account.stats.messagesReplied}`) ), - h2( + h3( "div", { className: "dxw-metric" }, - h2("dt", null, "\u6700\u8FD1\u68C0\u67E5"), - h2("dd", null, checkedTime(account.health.lastCheckedAt)) + h3("dt", null, "\u6700\u8FD1\u68C0\u67E5"), + h3("dd", null, checkedTime2(account.health.lastCheckedAt)) ) ), - h2( + h3( "div", { className: "dxw-accountFooter" }, - h2("div", { className: "dxw-summary" }, account.error?.message ?? account.health.summary), - h2( + h3("div", { className: "dxw-summary" }, account.error?.message ?? account.health.summary), + h3( "div", { className: "dxw-actions" }, - h2( - Button3, + h3( + Button5, { onClick: onReconnect, disabled: Boolean(busy) }, busy === "reconnect" ? "\u68C0\u67E5\u4E2D\u2026" : account.connected ? "\u68C0\u67E5\u8FDE\u63A5" : "\u91CD\u8BD5\u8FDE\u63A5" ), - h2(Button3, { kind: "danger", onClick: onRequestRemove, disabled: Boolean(busy) }, "\u79FB\u9664\u63A5\u5165") + h3(Button5, { kind: "danger", onClick: onRequestRemove, disabled: Boolean(busy) }, "\u79FB\u9664\u63A5\u5165") ) ) ), - removing ? h2( + removing ? h3( "div", { className: "dxw-confirm", role: "alertdialog" }, - h2("strong", null, "\u4ECE\u6B64 Harness \u79FB\u9664\u8FD9\u4E2A\u5FAE\u4FE1\u8D26\u53F7\uFF1F"), - h2("p", null, "\u8FD9\u4F1A\u505C\u6B62\u6D88\u606F\u8FDE\u63A5\uFF0C\u5E76\u5220\u9664\u672C\u673A\u4FDD\u5B58\u7684 bot_token\u3001\u8D26\u53F7\u914D\u7F6E\u548C\u4F1A\u8BDD\u6620\u5C04\u3002\u5176\u4ED6\u5FAE\u4FE1\u8D26\u53F7\u4E0D\u53D7\u5F71\u54CD\u3002"), - h2( + h3("strong", null, "\u4ECE\u6B64 Harness \u79FB\u9664\u8FD9\u4E2A\u5FAE\u4FE1\u8D26\u53F7\uFF1F"), + h3("p", null, "\u8FD9\u4F1A\u505C\u6B62\u6D88\u606F\u8FDE\u63A5\uFF0C\u5E76\u5220\u9664\u672C\u673A\u4FDD\u5B58\u7684 bot_token\u3001\u8D26\u53F7\u914D\u7F6E\u548C\u4F1A\u8BDD\u6620\u5C04\u3002\u5176\u4ED6\u5FAE\u4FE1\u8D26\u53F7\u4E0D\u53D7\u5F71\u54CD\u3002"), + h3( "div", { className: "dxw-actions" }, - h2(Button3, { onClick: onCancelRemove, disabled: busy === "delete" }, "\u4FDD\u7559\u8D26\u53F7"), - h2( - Button3, + h3(Button5, { onClick: onCancelRemove, disabled: busy === "delete" }, "\u4FDD\u7559\u8D26\u53F7"), + h3( + Button5, { kind: "danger", onClick: onConfirmRemove, disabled: busy === "delete" }, busy === "delete" ? "\u6B63\u5728\u79FB\u9664\u2026" : "\u786E\u8BA4\u79FB\u9664" ) @@ -2405,15 +3523,15 @@ function AccountCard({ account, busy, removing, onReconnect, onRequestRemove, on ) : null ); } -function AccountList(props) { - return h2( +function AccountList2(props) { + return h3( "section", null, - h2("div", { className: "dxw-listHeading" }, h2("h3", null, "\u5DF2\u63A5\u5165\u7684\u5FAE\u4FE1\u8D26\u53F7"), h2("span", null, `${props.bots.length} \u4E2A`)), - h2("ul", { className: "dxw-list" }, props.bots.map((account) => h2( + h3("div", { className: "dxw-listHeading" }, h3("h3", null, "\u5DF2\u63A5\u5165\u7684\u5FAE\u4FE1\u8D26\u53F7"), h3("span", null, `${props.bots.length} \u4E2A`)), + h3("ul", { className: "dxw-list" }, props.bots.map((account) => h3( "li", { key: account.botId }, - h2(AccountCard, { + h3(AccountCard2, { account, busy: props.busyByBot[account.botId], removing: props.removeTarget === account.botId, @@ -2425,33 +3543,33 @@ function AccountList(props) { ))) ); } -var EMPTY_TOTALS2 = Object.freeze({ configured: 0, connected: 0 }); +var EMPTY_TOTALS3 = Object.freeze({ configured: 0, connected: 0 }); function WeixinSettingsTab({ rpcCall }) { - const [model, setModel] = React2.useState({ + const [model, setModel] = React3.useState({ phase: "loading", bots: [], - totals: EMPTY_TOTALS2, + totals: EMPTY_TOTALS3, revision: 0, error: null }); - const [provision, setProvision] = React2.useState(null); - const [busy, setBusy] = React2.useState(false); - const [busyByBot, setBusyByBot] = React2.useState({}); - const [removeTarget, setRemoveTarget] = React2.useState(null); - const [notice, setNotice] = React2.useState(""); - const [now, setNow] = React2.useState(() => Date.now()); - const addButtonRef = React2.useRef(null); - const announce = React2.useCallback((value) => { + const [provision, setProvision] = React3.useState(null); + const [busy, setBusy] = React3.useState(false); + const [busyByBot, setBusyByBot] = React3.useState({}); + const [removeTarget, setRemoveTarget] = React3.useState(null); + const [notice, setNotice] = React3.useState(""); + const [now, setNow] = React3.useState(() => Date.now()); + const addButtonRef = React3.useRef(null); + const announce = React3.useCallback((value) => { setNotice(""); if (value) window.requestAnimationFrame(() => setNotice(value)); }, []); - const invoke = React2.useCallback(async (endpoint, payload = {}, signal) => { - return unwrapRpcResult2(await rpcCall(endpoint, payload, signal)); + const invoke = React3.useCallback(async (endpoint, payload = {}, signal) => { + return unwrapRpcResult3(await rpcCall(endpoint, payload, signal)); }, [rpcCall]); - const loadStatus = React2.useCallback(async ({ signal, silent = false } = {}) => { + const loadStatus = React3.useCallback(async ({ signal, silent = false } = {}) => { if (!silent) setModel((current) => ({ ...current, phase: "loading", error: null })); try { - const snapshot = normalizeSnapshot(await invoke(WEIXIN_ENDPOINTS.status, {}, signal)); + const snapshot = normalizeSnapshot2(await invoke(WEIXIN_ENDPOINTS.status, {}, signal)); setModel({ phase: "ready", bots: snapshot.bots, @@ -2468,17 +3586,17 @@ function WeixinSettingsTab({ rpcCall }) { setModel((current) => ({ ...current, phase: silent && current.phase === "ready" ? "ready" : "error", - error: presentError2(error) + error: presentError3(error) })); return void 0; } }, [invoke]); - React2.useEffect(() => { + React3.useEffect(() => { const controller = new AbortController(); void loadStatus({ signal: controller.signal }); return () => controller.abort(); }, [loadStatus]); - React2.useEffect(() => { + React3.useEffect(() => { if (model.phase !== "ready") return void 0; const controller = new AbortController(); let running = false; @@ -2493,33 +3611,33 @@ function WeixinSettingsTab({ rpcCall }) { window.clearInterval(timer); }; }, [loadStatus, model.phase]); - React2.useEffect(() => { + React3.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 = React2.useCallback(async ({ replace = false } = {}) => { + const startProvisioning = React3.useCallback(async ({ replace = false } = {}) => { setBusy(true); try { if (replace && provision?.attemptId) { await invoke(WEIXIN_ENDPOINTS.cancelProvisioning, { attemptId: provision.attemptId }); } setProvision({ status: "starting" }); - const started = normalizeProvisioning2(await invoke(WEIXIN_ENDPOINTS.beginProvisioning, { locale: "zh-CN" })); + const started = normalizeProvisioning3(await invoke(WEIXIN_ENDPOINTS.beginProvisioning, { locale: "zh-CN" })); setNow(Date.now()); setProvision({ ...started, durationMs: Math.max(1, started.expiresAt - Date.now()) }); announce("\u5FAE\u4FE1\u4E8C\u7EF4\u7801\u5DF2\u751F\u6210\uFF0C\u8BF7\u4F7F\u7528\u624B\u673A\u5FAE\u4FE1\u626B\u63CF\u3002"); } catch (error) { setProvision({ status: "failed", - error: presentError2(error), + error: presentError3(error), ...provision?.attemptId ? { attemptId: provision.attemptId } : {} }); } finally { setBusy(false); } }, [announce, invoke, provision?.attemptId]); - const cancelProvisioning = React2.useCallback(async () => { + const cancelProvisioning = React3.useCallback(async () => { setBusy(true); try { if (provision?.attemptId && !["failed", "expired", "cancelled"].includes(provision.status)) { @@ -2529,35 +3647,35 @@ function WeixinSettingsTab({ rpcCall }) { announce("\u5DF2\u53D6\u6D88\u5FAE\u4FE1\u7ED1\u5B9A\u3002"); window.requestAnimationFrame(() => addButtonRef.current?.focus()); } catch (error) { - setProvision((current) => ({ ...current, status: "failed", error: presentError2(error) })); + setProvision((current) => ({ ...current, status: "failed", error: presentError3(error) })); } finally { setBusy(false); } }, [announce, invoke, provision?.attemptId, provision?.status]); - const submitVerification = React2.useCallback(async (verifyCode) => { + const submitVerification = React3.useCallback(async (verifyCode) => { if (!provision?.attemptId) return; setBusy(true); try { - const next = normalizeProvisioning2(await invoke(WEIXIN_ENDPOINTS.submitVerification, { + const next = normalizeProvisioning3(await invoke(WEIXIN_ENDPOINTS.submitVerification, { attemptId: provision.attemptId, verifyCode })); setProvision((current) => ({ ...current, ...next })); announce("\u914D\u5BF9\u7801\u5DF2\u63D0\u4EA4\uFF0C\u6B63\u5728\u7B49\u5F85\u5FAE\u4FE1\u786E\u8BA4\u3002"); } catch (error) { - setProvision((current) => ({ ...current, status: "failed", error: presentError2(error) })); + setProvision((current) => ({ ...current, status: "failed", error: presentError3(error) })); } finally { setBusy(false); } }, [announce, invoke, provision?.attemptId]); - React2.useEffect(() => { + React3.useEffect(() => { const attemptId = provision?.attemptId; if (!attemptId || !["pending", "scanned", "connecting"].includes(provision.status)) return void 0; const controller = new AbortController(); let timer; const poll = async () => { try { - const result = normalizeProvisioning2(await invoke( + const result = normalizeProvisioning3(await invoke( WEIXIN_ENDPOINTS.pollProvisioning, { attemptId }, controller.signal @@ -2580,7 +3698,7 @@ function WeixinSettingsTab({ rpcCall }) { } } catch (error) { if (error?.name === "AbortError") return; - setProvision((current) => current?.attemptId === attemptId ? { ...current, status: "failed", error: presentError2(error) } : current); + setProvision((current) => current?.attemptId === attemptId ? { ...current, status: "failed", error: presentError3(error) } : current); } }; timer = window.setTimeout(poll, provision.pollIntervalMs ?? 1e3); @@ -2589,7 +3707,7 @@ function WeixinSettingsTab({ rpcCall }) { window.clearTimeout(timer); }; }, [announce, invoke, loadStatus, provision?.attemptId, provision?.status, provision?.pollIntervalMs]); - const setBotBusy = React2.useCallback((botId, value) => { + const setBotBusy = React3.useCallback((botId, value) => { setBusyByBot((current) => { const next = { ...current }; if (value) next[botId] = value; @@ -2597,23 +3715,23 @@ function WeixinSettingsTab({ rpcCall }) { return next; }); }, []); - const reconnect = React2.useCallback(async (account) => { + const reconnect = React3.useCallback(async (account) => { setBotBusy(account.botId, "reconnect"); try { - const snapshot = normalizeSnapshot(await invoke(WEIXIN_ENDPOINTS.reconnectBot, { botId: account.botId })); + const snapshot = normalizeSnapshot2(await invoke(WEIXIN_ENDPOINTS.reconnectBot, { botId: account.botId })); setModel((current) => ({ ...current, bots: snapshot.bots, totals: snapshot.totals, revision: snapshot.revision })); const refreshed = snapshot.bots.find((bot) => bot.botId === account.botId); announce(refreshed?.connected ? "\u5FAE\u4FE1\u8FDE\u63A5\u68C0\u67E5\u5B8C\u6210\u3002" : "\u5FAE\u4FE1\u4ECD\u672A\u8FDE\u63A5\uFF0C\u63D2\u4EF6\u4F1A\u7EE7\u7EED\u81EA\u52A8\u91CD\u8BD5\u3002"); } catch (error) { - announce(`\u8FDE\u63A5\u68C0\u67E5\u5931\u8D25\uFF1A${presentError2(error).message}`); + announce(`\u8FDE\u63A5\u68C0\u67E5\u5931\u8D25\uFF1A${presentError3(error).message}`); } finally { setBotBusy(account.botId, null); } }, [announce, invoke, setBotBusy]); - const remove = React2.useCallback(async (account) => { + const remove = React3.useCallback(async (account) => { setBotBusy(account.botId, "delete"); try { - const snapshot = normalizeSnapshot(await invoke(WEIXIN_ENDPOINTS.deleteBot, { + const snapshot = normalizeSnapshot2(await invoke(WEIXIN_ENDPOINTS.deleteBot, { botId: account.botId, confirm: true })); @@ -2621,16 +3739,16 @@ function WeixinSettingsTab({ rpcCall }) { setRemoveTarget(null); announce("\u5FAE\u4FE1\u8D26\u53F7\u53CA\u672C\u673A\u51ED\u636E\u5DF2\u79FB\u9664\u3002"); } catch (error) { - announce(`\u79FB\u9664\u5931\u8D25\uFF1A${presentError2(error).message}`); + announce(`\u79FB\u9664\u5931\u8D25\uFF1A${presentError3(error).message}`); } finally { setBotBusy(account.botId, null); } }, [announce, invoke, setBotBusy]); let provisionView = null; if (provision?.status === "starting") { - provisionView = h2(ProgressPanel, { busy }); + provisionView = h3(ProgressPanel2, { busy }); } else if (["pending", "scanned"].includes(provision?.status)) { - provisionView = h2(QrPanel, { + provisionView = h3(QrPanel2, { provision, now, busy, @@ -2638,54 +3756,54 @@ function WeixinSettingsTab({ rpcCall }) { onCancel: () => void cancelProvisioning() }); } else if (provision?.status === "needs_verification") { - provisionView = h2(VerificationPanel, { + provisionView = h3(VerificationPanel, { provision, busy, onSubmit: (code) => void submitVerification(code), onCancel: () => void cancelProvisioning() }); } else if (provision?.status === "connecting") { - provisionView = h2(ProgressPanel, { + provisionView = h3(ProgressPanel2, { scanned: true, busy, onCancel: () => void cancelProvisioning() }); } else if (provision && ["failed", "expired", "cancelled"].includes(provision.status)) { - provisionView = h2(ProvisionError2, { + provisionView = h3(ProvisionError3, { provision, busy, onRetry: () => void startProvisioning({ replace: Boolean(provision.attemptId) }), onClose: () => void cancelProvisioning() }); } - return h2( + return h3( "section", { className: "dxw-page", "aria-label": "\u5FAE\u4FE1\u8BBE\u7F6E" }, - h2(Heading2, { + h3(Heading3, { totals: model.totals, adding: Boolean(provision), busy, onAdd: () => void startProvisioning(), addButtonRef }), - h2("div", { className: "dxw-visuallyHidden", role: "status", "aria-live": "polite" }, notice), - model.error && model.phase === "ready" ? h2("div", { className: "dxw-statusNotice" }, `\u72B6\u6001\u5237\u65B0\u5931\u8D25\uFF1A${model.error.message}`) : null, - model.phase === "loading" ? h2(LoadingView2) : model.phase === "error" ? h2( + h3("div", { className: "dxw-visuallyHidden", role: "status", "aria-live": "polite" }, notice), + model.error && model.phase === "ready" ? h3("div", { className: "dxw-statusNotice" }, `\u72B6\u6001\u5237\u65B0\u5931\u8D25\uFF1A${model.error.message}`) : null, + model.phase === "loading" ? h3(LoadingView3) : model.phase === "error" ? h3( "div", { className: "dxw-card" }, - h2( + h3( "div", { className: "dxw-error" }, - h2("h3", null, "\u65E0\u6CD5\u8BFB\u53D6\u5FAE\u4FE1\u72B6\u6001"), - h2("p", null, model.error?.message ?? "\u8BF7\u7A0D\u540E\u91CD\u8BD5"), - h2(Button3, { onClick: () => void loadStatus() }, "\u91CD\u65B0\u8BFB\u53D6") + h3("h3", null, "\u65E0\u6CD5\u8BFB\u53D6\u5FAE\u4FE1\u72B6\u6001"), + h3("p", null, model.error?.message ?? "\u8BF7\u7A0D\u540E\u91CD\u8BD5"), + h3(Button5, { onClick: () => void loadStatus() }, "\u91CD\u65B0\u8BFB\u53D6") ) - ) : h2( - React2.Fragment, + ) : h3( + React3.Fragment, null, provisionView, - model.bots.length === 0 && !provision ? h2(EmptyView2, { onStart: () => void startProvisioning(), busy }) : null, - model.bots.length > 0 ? h2(AccountList, { + model.bots.length === 0 && !provision ? h3(EmptyView3, { onStart: () => void startProvisioning(), busy }) : null, + model.bots.length > 0 ? h3(AccountList2, { bots: model.bots, busyByBot, removeTarget, @@ -2700,7 +3818,7 @@ function WeixinSettingsTab({ rpcCall }) { // plugin-src/client/styles.js var IM_STYLE_ID = "xmanrui-dsh-im-settings"; -var CSS3 = String.raw` +var CSS4 = String.raw` .dim-page { --dim-blue: #3370ff; --dim-blue-soft: color-mix(in srgb, var(--dim-blue) 9%, transparent); @@ -2728,13 +3846,16 @@ var CSS3 = String.raw` .dim-logoWeixin svg { width: 19px; height: 19px; } .dim-logoFeishu { background: white; border: 1px solid var(--dsw-alias-line-border, #e5e6eb); } .dim-logoFeishu svg { width: 22px; height: 22px; } +.dim-logoDingtalk { color: white; background: #1677ff; } +.dim-logoDingtalk svg { width: 24px; height: 24px; } .dim-channelCopy { min-width: 0; display: block; } .dim-channelCopy strong { overflow: hidden; color: inherit; font-size: 14px; line-height: 20px; font-weight: 680; text-overflow: ellipsis; white-space: nowrap; } .dim-divider { width: 1px; min-height: 520px; background: var(--dsw-alias-line-divider, #eef0f3); } .dim-panel { min-width: 0; } -.dim-panel .bxf-page, .dim-panel .dxw-page { width: 100%; max-width: none; padding: 0 0 24px; } -.dim-panel .bxf-heading, .dim-panel .dxw-heading { justify-content: flex-end; } -.dim-panel .bxf-headingTools, .dim-panel .dxw-tools { width: 100%; justify-content: flex-end; } +.dim-panel .bxf-page, .dim-panel .dxw-page, .dim-panel .ddt-page { width: 100%; max-width: none; padding: 0 0 24px; } +.dim-panel .bxf-heading, .dim-panel .dxw-heading, .dim-panel .ddt-heading { justify-content: flex-end; } +.dim-panel .bxf-headingTools, .dim-panel .dxw-tools, .dim-panel .ddt-tools { width: 100%; justify-content: flex-end; } +.dim-panel .ddt-headingCopy { display: none; } @media (max-width: 840px) { .dim-title { align-items: flex-start; } .dim-layout { grid-template-columns: minmax(0, 1fr); gap: 18px; } @@ -2761,27 +3882,28 @@ function installImStyles() { const style = document.createElement("style"); style.dataset.plugin = "@xmanrui/dsh-im"; style.dataset.pluginCss = IM_STYLE_ID; - style.textContent = CSS3; + style.textContent = CSS4; document.head.appendChild(style); return () => style.remove(); } // plugin-src/client/index.js -var h3 = React3.createElement; +var h4 = React4.createElement; var name = "im-settings"; var inject = ["slots", "connection"]; var CHANNELS = Object.freeze([ { id: "weixin", label: "\u5FAE\u4FE1" }, - { id: "feishu", label: "\u98DE\u4E66" } + { id: "feishu", label: "\u98DE\u4E66" }, + { id: "dingtalk", label: "\u9489\u9489" } ]); function WeixinLogo() { - return h3( + return h4( "span", { className: "dim-logo dim-logoWeixin", "aria-hidden": "true" }, - h3( + h4( "svg", { viewBox: "0 0 24 24", focusable: "false" }, - h3("path", { + h4("path", { fill: "currentColor", d: "M8.691 2.188C3.891 2.188 0 5.476 0 9.53c0 2.212 1.17 4.203 3.002 5.55a.59.59 0 0 1 .213.665l-.39 1.48c-.019.07-.048.141-.048.213 0 .163.13.295.29.295a.326.326 0 0 0 .167-.054l1.903-1.114a.864.864 0 0 1 .717-.098 10.16 10.16 0 0 0 2.837.403c.276 0 .543-.027.811-.05-.857-2.578.157-4.972 1.932-6.446 1.703-1.415 3.882-1.98 5.853-1.838-.576-3.583-4.196-6.348-8.596-6.348zM5.785 5.991c.642 0 1.162.529 1.162 1.18a1.17 1.17 0 0 1-1.162 1.178A1.17 1.17 0 0 1 4.623 7.17c0-.651.52-1.18 1.162-1.18zm5.813 0c.642 0 1.162.529 1.162 1.18a1.17 1.17 0 0 1-1.162 1.178 1.17 1.17 0 0 1-1.162-1.178c0-.651.52-1.18 1.162-1.18zm5.34 2.867c-1.797-.052-3.746.512-5.28 1.786-1.72 1.428-2.687 3.72-1.78 6.22.942 2.453 3.666 4.229 6.884 4.229.826 0 1.622-.12 2.361-.336a.722.722 0 0 1 .598.082l1.584.926a.272.272 0 0 0 .14.047c.134 0 .24-.111.24-.247 0-.06-.023-.12-.038-.177l-.327-1.233a.582.582 0 0 1-.023-.156.49.49 0 0 1 .201-.398C23.024 18.48 24 16.82 24 14.98c0-3.21-2.931-5.837-6.656-6.088V8.89c-.135-.01-.27-.027-.407-.03zm-2.53 3.274c.535 0 .969.44.969.982a.976.976 0 0 1-.969.983.976.976 0 0 1-.969-.983c0-.542.434-.982.97-.982zm4.844 0c.535 0 .969.44.969.982a.976.976 0 0 1-.969.983.976.976 0 0 1-.969-.983c0-.542.434-.982.969-.982z" }) @@ -2789,40 +3911,56 @@ function WeixinLogo() { ); } function FeishuLogo() { - return h3( + return h4( "span", { className: "dim-logo dim-logoFeishu", "aria-hidden": "true" }, - h3( + h4( "svg", { viewBox: "0 0 24 24", focusable: "false" }, - h3("path", { fill: "#00D6B9", d: "M7.2 4.5h7.6c1.2 0 2.1.55 2.7 1.58 1.05 1.8 1.55 3.45 1.58 4.95-2.04-.62-4.2-.15-6.22 1.45C11.3 9.7 9.42 7.04 7.2 4.5Z" }), - h3("path", { fill: "#1456B8", d: "M10.8 13.55c3.3-2.93 5.72-4.24 9.47-2.52-1.2 1.45-2.27 4.18-3.86 5.43-1.67 1.31-3.9.5-5.61-.64v-2.27Z" }), - h3("path", { fill: "#3370FF", d: "M4.4 8.35c3.47 3.61 7.25 6.1 10.33 5.7 1.06-.14 2.2-.72 3.4-1.72-1.04 2.65-2.6 4.8-5.06 6-2.46 1.2-5.56.52-7.42-.72A2.76 2.76 0 0 1 4.4 15.3V8.35Z" }) + h4("path", { fill: "#00D6B9", d: "M7.2 4.5h7.6c1.2 0 2.1.55 2.7 1.58 1.05 1.8 1.55 3.45 1.58 4.95-2.04-.62-4.2-.15-6.22 1.45C11.3 9.7 9.42 7.04 7.2 4.5Z" }), + h4("path", { fill: "#1456B8", d: "M10.8 13.55c3.3-2.93 5.72-4.24 9.47-2.52-1.2 1.45-2.27 4.18-3.86 5.43-1.67 1.31-3.9.5-5.61-.64v-2.27Z" }), + h4("path", { fill: "#3370FF", d: "M4.4 8.35c3.47 3.61 7.25 6.1 10.33 5.7 1.06-.14 2.2-.72 3.4-1.72-1.04 2.65-2.6 4.8-5.06 6-2.46 1.2-5.56.52-7.42-.72A2.76 2.76 0 0 1 4.4 15.3V8.35Z" }) + ) + ); +} +function DingtalkLogo() { + return h4( + "span", + { className: "dim-logo dim-logoDingtalk", "aria-hidden": "true" }, + h4( + "svg", + { viewBox: "0 0 48 48", focusable: "false" }, + h4("path", { + fill: "currentColor", + d: "M37.05 22.783c-6.758-5.216-14.378-12.128-22.73-19.538-.655-.585-1.242-.354-1.536.42-1.88 4.973-.058 9.386 2.889 11.932s7.368 4.912 10.058 6.155c.105.049.013.203-.093.163-4.953-2.182-8.397-3.765-13.07-7.368-.497-.388-1.01-.242-1.07.521-.384 4.748 2.657 8.483 6.058 9.745 2.1.781 4.398 1.212 6.53 1.474.109.015.084.178-.027.178-2.747.01-6.058-.654-8.935-1.751-.606-.233-.818.25-.722.633.491 2.008 2.974 5.076 6.926 5.73a12 12 0 0 0 2.228.115c.164 0 .208.089.154.217q-2.685 4.6-2.803 4.797c-.091.152-.036.275.156.275h3.543c.164 0 .264.106.18.246l-4.958 8.196c-.191.328.035.565.395.301s15.212-11.133 15.636-11.448c.195-.142.148-.327-.124-.327h-3.18c-.206 0-.252-.14-.111-.28.14-.141 3.602-3.594 4.837-4.888 1.283-1.35 1.938-3.825-.231-5.498" + }) ) ); } function ChannelLogo({ channel }) { - return channel === "weixin" ? h3(WeixinLogo) : h3(FeishuLogo); + if (channel === "weixin") return h4(WeixinLogo); + if (channel === "feishu") return h4(FeishuLogo); + return h4(DingtalkLogo); } -function IMSettingsTab({ feishuRpcCall, weixinRpcCall }) { - const [selected, setSelected] = React3.useState("weixin"); +function IMSettingsTab({ dingtalkRpcCall, feishuRpcCall, weixinRpcCall }) { + const [selected, setSelected] = React4.useState("weixin"); const active = CHANNELS.find((channel) => channel.id === selected) ?? CHANNELS[0]; - return h3( + return h4( "section", { className: "dim-page", "aria-label": "IM\u673A\u5668\u4EBA\u8BBE\u7F6E" }, - h3( + h4( "header", { className: "dim-title" }, - h3("p", null, "\u901A\u8FC7\u626B\u7801\u628A\u673A\u5668\u4EBA\u63A5\u5165 DeepSeek Harness"), - h3("div", { className: "dim-channelCount" }, "2 \u4E2A\u6E20\u9053") + h4("p", null, "\u901A\u8FC7\u626B\u7801\u628A\u673A\u5668\u4EBA\u63A5\u5165 DeepSeek Harness"), + h4("div", { className: "dim-channelCount" }, `${CHANNELS.length} \u4E2A\u6E20\u9053`) ), - h3( + h4( "div", { className: "dim-layout" }, - h3( + h4( "nav", { className: "dim-rail", role: "tablist", "aria-label": "IM \u6E20\u9053" }, - CHANNELS.map((channel) => h3( + CHANNELS.map((channel) => h4( "button", { key: channel.id, @@ -2834,21 +3972,21 @@ function IMSettingsTab({ feishuRpcCall, weixinRpcCall }) { "aria-controls": `dim-panel-${channel.id}`, onClick: () => setSelected(channel.id) }, - h3(ChannelLogo, { channel: channel.id }), - h3( + h4(ChannelLogo, { channel: channel.id }), + h4( "span", { className: "dim-channelCopy" }, - h3("strong", null, channel.label) + h4("strong", null, channel.label) ) )) ), - h3("div", { className: "dim-divider", "aria-hidden": "true" }), - h3("main", { + h4("div", { className: "dim-divider", "aria-hidden": "true" }), + h4("main", { className: "dim-panel", role: "tabpanel", id: `dim-panel-${active.id}`, "aria-labelledby": `dim-tab-${active.id}` - }, active.id === "weixin" ? h3(WeixinSettingsTab, { rpcCall: weixinRpcCall }) : h3(FeishuSettingsTab, { rpcCall: feishuRpcCall })) + }, active.id === "weixin" ? h4(WeixinSettingsTab, { rpcCall: weixinRpcCall }) : active.id === "feishu" ? h4(FeishuSettingsTab, { rpcCall: feishuRpcCall }) : h4(DingtalkSettingsTab, { rpcCall: dingtalkRpcCall })) ) ); } @@ -2861,12 +3999,13 @@ function apply(ctx) { }, "im-settings: install combined channel styles"); const feishuRpcCall = (endpoint, payload, signal) => ctx.connection.rpc.call(FEISHU_RPC_CHANNEL, endpoint, payload, signal); const weixinRpcCall = (endpoint, payload, signal) => ctx.connection.rpc.call(WEIXIN_RPC_CHANNEL, endpoint, payload, signal); + const dingtalkRpcCall = (endpoint, payload, signal) => ctx.connection.rpc.call(DINGTALK_RPC_CHANNEL, endpoint, payload, signal); ctx.slots.inject("settings.plugins.tab", () => ctx.slots.register({ name: "settings.plugins.tab", id: "im", order: 20, label: "IM\u673A\u5668\u4EBA", - inject: () => ({ feishuRpcCall, weixinRpcCall }) + inject: () => ({ dingtalkRpcCall, feishuRpcCall, weixinRpcCall }) }, IMSettingsTab)); } diff --git a/lib/index.js b/lib/index.js index 0235b86..449864e 100644 --- a/lib/index.js +++ b/lib/index.js @@ -1,4 +1,5 @@ // plugin-src/host/index.mjs +import { apply as applyDingtalk } from "@xmanrui/dsh-dingtalk"; import { apply as applyFeishu } from "@xmanrui/dsh-feishu"; import { apply as applyWeixin } from "@xmanrui/dsh-weixin"; var name = "dsh-im-host"; @@ -6,12 +7,14 @@ var inject = ["connection", "credentials", "webServer"]; function createImHostPlugin(internals = {}) { const startFeishu = internals.applyFeishu ?? applyFeishu; const startWeixin = internals.applyWeixin ?? applyWeixin; + const startDingtalk = internals.applyDingtalk ?? applyDingtalk; return Object.freeze({ name, inject, async apply(ctx, config = {}) { await startFeishu(ctx, config.feishu ?? {}); await startWeixin(ctx, config.weixin ?? {}); + await startDingtalk(ctx, config.dingtalk ?? {}); } }); } diff --git a/package-lock.json b/package-lock.json index 26b34b8..27eb739 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,6 +9,7 @@ "version": "0.1.0", "license": "MIT", "dependencies": { + "@xmanrui/dsh-dingtalk": "https://github.com/xmanrui/dsh-dingtalk/archive/67f7211b8011f0623b47ce9e01004bbab8e6614f.tar.gz", "@xmanrui/dsh-feishu": "https://github.com/xmanrui/dsh-feishu/archive/aad650feabadd511241aa58b236d64273d5e397f.tar.gz", "@xmanrui/dsh-weixin": "https://github.com/xmanrui/dsh-weixin/archive/76d076771b2c84fb4c5598c2344d486695eda080.tar.gz" }, @@ -547,6 +548,22 @@ "undici-types": "~8.3.0" } }, + "node_modules/@xmanrui/dsh-dingtalk": { + "version": "0.1.0", + "resolved": "https://github.com/xmanrui/dsh-dingtalk/archive/67f7211b8011f0623b47ce9e01004bbab8e6614f.tar.gz", + "integrity": "sha512-aOA3vvEL7mKt/NIlMRVJTWB+V5E+J3gWgqE0Q19in8KTrEwYqUk1TYe08B/OdikEghwaQnNfri2bqqpgsCzCqg==", + "license": "MIT", + "dependencies": { + "dingtalk-stream": "2.1.4", + "qrcode": "1.5.4" + }, + "bin": { + "dsh-dingtalk": "bin/dsh-dingtalk.mjs" + }, + "engines": { + "node": ">=22.19" + } + }, "node_modules/@xmanrui/dsh-feishu": { "version": "0.1.0", "resolved": "https://github.com/xmanrui/dsh-feishu/archive/aad650feabadd511241aa58b236d64273d5e397f.tar.gz", @@ -753,6 +770,17 @@ "integrity": "sha512-qiSlmBq9+BCdCA/L46dw8Uy93mloxsPSbwnm5yrKn2vMPiy8KyAskTF6zuV/j5BMsmOGZDPs7KjU+mjb670kfA==", "license": "MIT" }, + "node_modules/dingtalk-stream": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/dingtalk-stream/-/dingtalk-stream-2.1.4.tgz", + "integrity": "sha512-rgQbXLGWfASuB9onFcqXTnRSj4ZotimhBOnzrB4kS19AaU9lshXiuofs1GAYcKh5uzPWCAuEs3tMtiadTQWP4A==", + "license": "MIT", + "dependencies": { + "axios": "^1.4.0", + "debug": "^4.3.4", + "ws": "^8.13.0" + } + }, "node_modules/dotenv": { "version": "17.4.2", "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.4.2.tgz", diff --git a/package.json b/package.json index c4bea39..284bb5c 100644 --- a/package.json +++ b/package.json @@ -57,6 +57,7 @@ "node": ">=22.19" }, "dependencies": { + "@xmanrui/dsh-dingtalk": "https://github.com/xmanrui/dsh-dingtalk/archive/67f7211b8011f0623b47ce9e01004bbab8e6614f.tar.gz", "@xmanrui/dsh-feishu": "https://github.com/xmanrui/dsh-feishu/archive/aad650feabadd511241aa58b236d64273d5e397f.tar.gz", "@xmanrui/dsh-weixin": "https://github.com/xmanrui/dsh-weixin/archive/76d076771b2c84fb4c5598c2344d486695eda080.tar.gz" }, diff --git a/plugin-src/client/index.js b/plugin-src/client/index.js index 6d0b7c0..7e455ed 100644 --- a/plugin-src/client/index.js +++ b/plugin-src/client/index.js @@ -1,5 +1,7 @@ import * as React from 'react'; +import { DINGTALK_RPC_CHANNEL } from '@xmanrui/dsh-dingtalk/client-api'; +import { DingtalkSettingsTab } from '@xmanrui/dsh-dingtalk/client-source'; import { FeishuSettingsTab } from './channels/feishu/index.js'; import { FEISHU_RPC_CHANNEL } from './channels/feishu/api.js'; import { installFeishuStyles } from './channels/feishu/styles.js'; @@ -16,6 +18,7 @@ export const inject = ['slots', 'connection']; const CHANNELS = Object.freeze([ { id: 'weixin', label: '微信' }, { id: 'feishu', label: '飞书' }, + { id: 'dingtalk', label: '钉钉' }, ]); function WeixinLogo() { @@ -37,17 +40,29 @@ function FeishuLogo() { )); } -function ChannelLogo({ channel }) { - return channel === 'weixin' ? h(WeixinLogo) : h(FeishuLogo); +function DingtalkLogo() { + return h('span', { className: 'dim-logo dim-logoDingtalk', 'aria-hidden': 'true' }, + h('svg', { viewBox: '0 0 48 48', focusable: 'false' }, + h('path', { + fill: 'currentColor', + d: 'M37.05 22.783c-6.758-5.216-14.378-12.128-22.73-19.538-.655-.585-1.242-.354-1.536.42-1.88 4.973-.058 9.386 2.889 11.932s7.368 4.912 10.058 6.155c.105.049.013.203-.093.163-4.953-2.182-8.397-3.765-13.07-7.368-.497-.388-1.01-.242-1.07.521-.384 4.748 2.657 8.483 6.058 9.745 2.1.781 4.398 1.212 6.53 1.474.109.015.084.178-.027.178-2.747.01-6.058-.654-8.935-1.751-.606-.233-.818.25-.722.633.491 2.008 2.974 5.076 6.926 5.73a12 12 0 0 0 2.228.115c.164 0 .208.089.154.217q-2.685 4.6-2.803 4.797c-.091.152-.036.275.156.275h3.543c.164 0 .264.106.18.246l-4.958 8.196c-.191.328.035.565.395.301s15.212-11.133 15.636-11.448c.195-.142.148-.327-.124-.327h-3.18c-.206 0-.252-.14-.111-.28.14-.141 3.602-3.594 4.837-4.888 1.283-1.35 1.938-3.825-.231-5.498', + }), + )); } -export function IMSettingsTab({ feishuRpcCall, weixinRpcCall }) { +function ChannelLogo({ channel }) { + if (channel === 'weixin') return h(WeixinLogo); + if (channel === 'feishu') return h(FeishuLogo); + return h(DingtalkLogo); +} + +export function IMSettingsTab({ dingtalkRpcCall, feishuRpcCall, weixinRpcCall }) { const [selected, setSelected] = React.useState('weixin'); const active = CHANNELS.find((channel) => channel.id === selected) ?? CHANNELS[0]; return h('section', { className: 'dim-page', 'aria-label': 'IM机器人设置' }, h('header', { className: 'dim-title' }, h('p', null, '通过扫码把机器人接入 DeepSeek Harness'), - h('div', { className: 'dim-channelCount' }, '2 个渠道'), + h('div', { className: 'dim-channelCount' }, `${CHANNELS.length} 个渠道`), ), h('div', { className: 'dim-layout' }, h('nav', { className: 'dim-rail', role: 'tablist', 'aria-label': 'IM 渠道' }, @@ -73,7 +88,9 @@ export function IMSettingsTab({ feishuRpcCall, weixinRpcCall }) { 'aria-labelledby': `dim-tab-${active.id}`, }, active.id === 'weixin' ? h(WeixinSettingsTab, { rpcCall: weixinRpcCall }) - : h(FeishuSettingsTab, { rpcCall: feishuRpcCall })), + : active.id === 'feishu' + ? h(FeishuSettingsTab, { rpcCall: feishuRpcCall }) + : h(DingtalkSettingsTab, { rpcCall: dingtalkRpcCall })), ), ); } @@ -90,12 +107,14 @@ export function apply(ctx) { ctx.connection.rpc.call(FEISHU_RPC_CHANNEL, endpoint, payload, signal); const weixinRpcCall = (endpoint, payload, signal) => ctx.connection.rpc.call(WEIXIN_RPC_CHANNEL, endpoint, payload, signal); + const dingtalkRpcCall = (endpoint, payload, signal) => + ctx.connection.rpc.call(DINGTALK_RPC_CHANNEL, endpoint, payload, signal); ctx.slots.inject('settings.plugins.tab', () => ctx.slots.register({ name: 'settings.plugins.tab', id: 'im', order: 20, label: 'IM机器人', - inject: () => ({ feishuRpcCall, weixinRpcCall }), + inject: () => ({ dingtalkRpcCall, feishuRpcCall, weixinRpcCall }), }, IMSettingsTab)); } diff --git a/plugin-src/client/styles.js b/plugin-src/client/styles.js index e359370..72d6cab 100644 --- a/plugin-src/client/styles.js +++ b/plugin-src/client/styles.js @@ -28,13 +28,16 @@ const CSS = String.raw` .dim-logoWeixin svg { width: 19px; height: 19px; } .dim-logoFeishu { background: white; border: 1px solid var(--dsw-alias-line-border, #e5e6eb); } .dim-logoFeishu svg { width: 22px; height: 22px; } +.dim-logoDingtalk { color: white; background: #1677ff; } +.dim-logoDingtalk svg { width: 24px; height: 24px; } .dim-channelCopy { min-width: 0; display: block; } .dim-channelCopy strong { overflow: hidden; color: inherit; font-size: 14px; line-height: 20px; font-weight: 680; text-overflow: ellipsis; white-space: nowrap; } .dim-divider { width: 1px; min-height: 520px; background: var(--dsw-alias-line-divider, #eef0f3); } .dim-panel { min-width: 0; } -.dim-panel .bxf-page, .dim-panel .dxw-page { width: 100%; max-width: none; padding: 0 0 24px; } -.dim-panel .bxf-heading, .dim-panel .dxw-heading { justify-content: flex-end; } -.dim-panel .bxf-headingTools, .dim-panel .dxw-tools { width: 100%; justify-content: flex-end; } +.dim-panel .bxf-page, .dim-panel .dxw-page, .dim-panel .ddt-page { width: 100%; max-width: none; padding: 0 0 24px; } +.dim-panel .bxf-heading, .dim-panel .dxw-heading, .dim-panel .ddt-heading { justify-content: flex-end; } +.dim-panel .bxf-headingTools, .dim-panel .dxw-tools, .dim-panel .ddt-tools { width: 100%; justify-content: flex-end; } +.dim-panel .ddt-headingCopy { display: none; } @media (max-width: 840px) { .dim-title { align-items: flex-start; } .dim-layout { grid-template-columns: minmax(0, 1fr); gap: 18px; } diff --git a/plugin-src/host/index.mjs b/plugin-src/host/index.mjs index b2184a4..e23f2da 100644 --- a/plugin-src/host/index.mjs +++ b/plugin-src/host/index.mjs @@ -1,3 +1,4 @@ +import { apply as applyDingtalk } from '@xmanrui/dsh-dingtalk'; import { apply as applyFeishu } from '@xmanrui/dsh-feishu'; import { apply as applyWeixin } from '@xmanrui/dsh-weixin'; @@ -7,12 +8,14 @@ export const inject = ['connection', 'credentials', 'webServer']; export function createImHostPlugin(internals = {}) { const startFeishu = internals.applyFeishu ?? applyFeishu; const startWeixin = internals.applyWeixin ?? applyWeixin; + const startDingtalk = internals.applyDingtalk ?? applyDingtalk; return Object.freeze({ name, inject, async apply(ctx, config = {}) { await startFeishu(ctx, config.feishu ?? {}); await startWeixin(ctx, config.weixin ?? {}); + await startDingtalk(ctx, config.dingtalk ?? {}); }, }); } diff --git a/scripts/verify-package.mjs b/scripts/verify-package.mjs index a790164..0bfe433 100644 --- a/scripts/verify-package.mjs +++ b/scripts/verify-package.mjs @@ -33,15 +33,18 @@ if ((client.match(/ctx\.slots\.inject\("settings\.plugins\.tab"/g) ?? []).length if (/role:\s*["']switch|type:\s*["']checkbox/.test(client)) { throw new Error('client bundle contains a channel enable switch'); } -if (!host.includes('@xmanrui/dsh-feishu') || !host.includes('@xmanrui/dsh-weixin')) { - throw new Error('host bundle does not compose both channel providers'); +if (!host.includes('@xmanrui/dsh-feishu') + || !host.includes('@xmanrui/dsh-weixin') + || !host.includes('@xmanrui/dsh-dingtalk')) { + throw new Error('host bundle does not compose all three channel providers'); } -if (!patch.includes("name: '@xmanrui/dsh-im'") || /dsh-(?:feishu|weixin)/.test(patch)) { +if (!patch.includes("name: '@xmanrui/dsh-im'") || /dsh-(?:feishu|weixin|dingtalk)/.test(patch)) { throw new Error('bundle patch must activate only dsh-im'); } -for (const name of ['@xmanrui/dsh-feishu', '@xmanrui/dsh-weixin']) { +for (const name of ['@xmanrui/dsh-feishu', '@xmanrui/dsh-weixin', '@xmanrui/dsh-dingtalk']) { const spec = manifest.dependencies?.[name]; - if (typeof spec !== 'string' || !spec.startsWith('https://github.com/')) { + if (typeof spec !== 'string' + || !/^https:\/\/github\.com\/[^/]+\/[^/]+\/archive\/[0-9a-f]{40}\.tar\.gz$/.test(spec)) { throw new Error(`${name} must use a pinned public HTTPS archive`); } } diff --git a/test/client-ui.test.mjs b/test/client-ui.test.mjs index 0742338..2559a71 100644 --- a/test/client-ui.test.mjs +++ b/test/client-ui.test.mjs @@ -6,19 +6,22 @@ import { renderToStaticMarkup } from 'react-dom/server'; import { IMSettingsTab } from '../plugin-src/client/index.js'; -test('IM settings renders two logo channel tabs without enable switches', () => { +test('IM settings renders three compact logo channel tabs without enable switches', () => { const markup = renderToStaticMarkup(React.createElement(IMSettingsTab, { feishuRpcCall: async () => ({ ok: true, value: {} }), weixinRpcCall: async () => ({ ok: true, value: {} }), + dingtalkRpcCall: async () => ({ ok: true, value: {} }), })); assert.match(markup, /IM机器人/); assert.match(markup, /通过扫码把机器人接入 DeepSeek Harness/); assert.match(markup, />微信飞书钉钉|扫码接入<\/small>/); diff --git a/test/host.test.mjs b/test/host.test.mjs index b74b2d0..3d02de4 100644 --- a/test/host.test.mjs +++ b/test/host.test.mjs @@ -3,14 +3,19 @@ import test from 'node:test'; import { createImHostPlugin, inject, name } from '../plugin-src/host/index.mjs'; -test('Host composes Feishu and Weixin inside one plugin context', async () => { +test('Host composes Feishu, Weixin, and DingTalk inside one plugin context', async () => { const calls = []; const plugin = createImHostPlugin({ applyFeishu: async (ctx, config) => calls.push(['feishu', ctx, config]), applyWeixin: async (ctx, config) => calls.push(['weixin', ctx, config]), + applyDingtalk: async (ctx, config) => calls.push(['dingtalk', ctx, config]), }); const ctx = { marker: 'shared-context' }; - const config = { feishu: { domain: 'feishu' }, weixin: { timeout: 30 } }; + const config = { + feishu: { domain: 'feishu' }, + weixin: { timeout: 30 }, + dingtalk: { replyTimeoutMs: 60_000 }, + }; await plugin.apply(ctx, config); @@ -19,6 +24,7 @@ test('Host composes Feishu and Weixin inside one plugin context', async () => { assert.deepEqual(calls, [ ['feishu', ctx, config.feishu], ['weixin', ctx, config.weixin], + ['dingtalk', ctx, config.dingtalk], ]); }); @@ -27,8 +33,21 @@ test('Host does not start Weixin when Feishu activation fails', async () => { const plugin = createImHostPlugin({ applyFeishu: async () => { throw new Error('feishu unavailable'); }, applyWeixin: async () => { weixinStarted = true; }, + applyDingtalk: async () => { throw new Error('DingTalk must not start'); }, }); await assert.rejects(() => plugin.apply({}, {}), /feishu unavailable/); assert.equal(weixinStarted, false); }); + +test('Host does not start DingTalk when Weixin activation fails', async () => { + let dingtalkStarted = false; + const plugin = createImHostPlugin({ + applyFeishu: async () => {}, + applyWeixin: async () => { throw new Error('weixin unavailable'); }, + applyDingtalk: async () => { dingtalkStarted = true; }, + }); + + await assert.rejects(() => plugin.apply({}, {}), /weixin unavailable/); + assert.equal(dingtalkStarted, false); +});