From 0c068e9f4dfc190d00bdf5b0b250e6cfc6a9d7e8 Mon Sep 17 00:00:00 2001 From: DEV_DSW <562304744@qq.com> Date: Tue, 21 Apr 2026 14:27:19 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=B8=A0=E9=81=93=E7=AE=A1=E7=90=86?= =?UTF-8?q?=E8=AF=AD=E8=A8=80=E5=9B=BD=E9=99=85=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/i18n/messages.ts | 159 ++++++++++++++++++++++++++++++++++ src/pages/Channels/index.tsx | 162 +++++++++++++++++++++-------------- 2 files changed, 259 insertions(+), 62 deletions(-) diff --git a/src/i18n/messages.ts b/src/i18n/messages.ts index 3701f0f..6925e46 100644 --- a/src/i18n/messages.ts +++ b/src/i18n/messages.ts @@ -248,6 +248,59 @@ export const messages: I18nMessages = { }, }, channels: { + page: { + title: 'Supported channels', + description: 'This page focuses on supported channel modules, configuration entry points, and refresh actions.', + refresh: 'Refresh', + pluginBadge: 'Plugin', + configure: 'Start setup', + empty: 'No configurable channel modules are available yet.', + refreshed: 'Supported channel list refreshed.', + saved: '{name} configuration saved.', + defaultAccountLabel: 'Default account', + }, + validation: { + requiredField: 'Please fill in {field}', + }, + connectionType: { + qr: 'QR code', + webhook: 'Webhook', + token: 'Token', + }, + meta: { + telegram: { + name: 'Telegram', + description: 'Connect Telegram using the bot token provided by @BotFather.', + }, + discord: { + name: 'Discord', + description: 'Connect Discord using the bot token provided in the developer portal.', + }, + whatsapp: { + name: 'WhatsApp', + description: 'Connect WhatsApp by scanning a QR code, without requiring a phone number.', + }, + wechat: { + name: 'WeChat', + description: 'Connect personal WeChat by scanning through the official OpenClaw plugin.', + }, + dingtalk: { + name: 'DingTalk', + description: 'Connect DingTalk through the OpenClaw channel plugin in Stream mode.', + }, + feishu: { + name: 'Feishu / Lark', + description: 'Connect a Feishu/Lark bot through Feishu\'s official OpenClaw plugin.', + }, + wecom: { + name: 'WeCom', + description: 'Connect a WeCom bot through the plugin.', + }, + qqbot: { + name: 'QQ Bot', + description: 'Connect a QQ bot channel, built in since OpenClaw 3.31.', + }, + }, modal: { title: 'Channel configuration', description: 'Choose a channel type, then fill in the connection fields that are available for this template.', @@ -737,6 +790,59 @@ export const messages: I18nMessages = { }, }, channels: { + page: { + title: '支持的频道', + description: '当前页面聚焦支持的频道模块、配置入口与刷新能力。', + refresh: '刷新', + pluginBadge: '插件', + configure: '开始配置', + empty: '暂无可配置的频道模块。', + refreshed: '支持频道列表已刷新。', + saved: '{name} 配置已保存。', + defaultAccountLabel: '默认账号', + }, + validation: { + requiredField: '请填写 {field}', + }, + connectionType: { + qr: '扫码', + webhook: 'Webhook', + token: 'Token', + }, + meta: { + telegram: { + name: 'Telegram', + description: '使用 @BotFather 提供的机器人令牌连接 Telegram。', + }, + discord: { + name: 'Discord', + description: '使用开发者门户提供的机器人令牌连接 Discord。', + }, + whatsapp: { + name: 'WhatsApp', + description: '通过扫描二维码连接 WhatsApp(无需手机号)。', + }, + wechat: { + name: '微信', + description: '通过腾讯官方 OpenClaw 插件扫码连接个人微信。', + }, + dingtalk: { + name: '钉钉', + description: '通过 OpenClaw 渠道插件连接钉钉(Stream 模式)。', + }, + feishu: { + name: '飞书 / Lark', + description: '通过飞书官方推出的 OpenClaw 插件连接飞书/Lark 机器人。', + }, + wecom: { + name: '企业微信', + description: '通过插件连接企业微信机器人。', + }, + qqbot: { + name: 'QQ 机器人', + description: '连接 QQ 机器人频道(OpenClaw 3.31 起内置)。', + }, + }, modal: { title: '渠道配置', description: '先选择渠道类型,再填写这个模板可用的连接字段。', @@ -1226,6 +1332,59 @@ export const messages: I18nMessages = { }, }, channels: { + page: { + title: '対応チャンネル', + description: 'このページでは、対応チャンネルのモジュール、設定入口、更新操作をまとめて扱います。', + refresh: '更新', + pluginBadge: 'プラグイン', + configure: '設定を開始', + empty: '設定可能なチャンネルモジュールはまだありません。', + refreshed: '対応チャンネル一覧を更新しました。', + saved: '{name} の設定を保存しました。', + defaultAccountLabel: 'デフォルトアカウント', + }, + validation: { + requiredField: '{field} を入力してください', + }, + connectionType: { + qr: 'QRコード', + webhook: 'Webhook', + token: 'Token', + }, + meta: { + telegram: { + name: 'Telegram', + description: '@BotFather が発行したボットトークンで Telegram に接続します。', + }, + discord: { + name: 'Discord', + description: '開発者ポータルのボットトークンで Discord に接続します。', + }, + whatsapp: { + name: 'WhatsApp', + description: 'QR コードをスキャンして WhatsApp に接続します。電話番号は不要です。', + }, + wechat: { + name: 'WeChat', + description: '公式 OpenClaw プラグイン経由で QR スキャンし、個人 WeChat に接続します。', + }, + dingtalk: { + name: 'DingTalk', + description: 'OpenClaw チャンネルプラグイン経由で DingTalk に接続します(Stream モード)。', + }, + feishu: { + name: 'Feishu / Lark', + description: 'Feishu 公式の OpenClaw プラグイン経由で Feishu/Lark ボットに接続します。', + }, + wecom: { + name: 'WeCom', + description: 'プラグイン経由で WeCom ボットに接続します。', + }, + qqbot: { + name: 'QQ Bot', + description: 'QQ ボットチャンネルに接続します(OpenClaw 3.31 以降は内蔵)。', + }, + }, modal: { title: 'チャンネル設定', description: 'まずチャンネル種類を選び、このテンプレートで使える接続項目を入力します。', diff --git a/src/pages/Channels/index.tsx b/src/pages/Channels/index.tsx index 5402e39..b82dfb2 100644 --- a/src/pages/Channels/index.tsx +++ b/src/pages/Channels/index.tsx @@ -1,6 +1,7 @@ import { useEffect, useState } from 'react'; import { RefreshCw } from 'lucide-react'; import ChannelConfigModal, { type ChannelCredentialsValidationResult } from '../../components/channels/ChannelConfigModal'; +import { useI18n } from '../../i18n'; import { getChannelMeta, getPrimaryChannelOptions, type ChannelMeta } from '../../lib/channel-meta'; import type { ChannelConfigFieldValueMap } from '../../lib/channel-types'; import { hostApiFetch } from '../../lib/host-api'; @@ -13,6 +14,10 @@ import feishuIcon from '../../assets/channels/feishu.svg'; import wecomIcon from '../../assets/channels/wecom.svg'; import qqIcon from '../../assets/channels/qq.svg'; +type TranslateParams = Record; +type TranslateFn = (path: string, params?: TranslateParams) => string; +type HasMessageFn = (path: string) => boolean; + function cn(...tokens: Array): string { return tokens.filter(Boolean).join(' '); } @@ -38,30 +43,50 @@ function deriveChannelUrl(channelType: string, accountId: string, values: Channe return buildSyntheticChannelUrl(channelType, accountId || 'default'); } -function buildSavedAccountName(channelType: string, accountId: string): string { - const meta = getChannelMeta(channelType); +function resolveChannelMetaText( + channelType: string, + field: 'name' | 'description', + fallback: string, + t: TranslateFn, + hasMessage: HasMessageFn, +): string { + const path = `channels.meta.${channelType}.${field}`; + return hasMessage(path) ? t(path) : fallback; +} + +function buildSavedAccountName(channelName: string, accountId: string, defaultAccountLabel: string): string { const resolvedAccountId = accountId.trim() || 'default'; if (resolvedAccountId === 'default') { - return `${meta.name} 默认账号`; + return `${channelName} ${defaultAccountLabel}`; } return resolvedAccountId; } -function validateRequiredFields(channelType: string, values: ChannelConfigFieldValueMap): string | null { +function validateRequiredFields( + channelType: string, + values: ChannelConfigFieldValueMap, + t: TranslateFn, +): string | null { const meta = getChannelMeta(channelType); const missingField = meta.configFields.find((field) => field.required && !String(values[field.key] ?? '').trim()); if (!missingField) return null; - return `请填写 ${missingField.label}`; + return t('channels.validation.requiredField', { field: missingField.label }); } -function getConnectionLabel(connectionType: string): string { - if (connectionType === 'qr') return '扫码'; - if (connectionType === 'webhook') return 'Webhook'; - return 'Token'; +function getConnectionLabel(connectionType: string, t: TranslateFn): string { + switch (connectionType) { + case 'qr': + return t('channels.connectionType.qr'); + case 'webhook': + return t('channels.connectionType.webhook'); + default: + return t('channels.connectionType.token'); + } } export default function ChannelsPage() { + const { t, hasMessage } = useI18n(); const [supportedChannels, setSupportedChannels] = useState([]); const [refreshing, setRefreshing] = useState(false); const [feedback, setFeedback] = useState(null); @@ -73,6 +98,15 @@ export default function ChannelsPage() { const [modalValidating, setModalValidating] = useState(false); const [modalValidationResult, setModalValidationResult] = useState(null); + function getChannelName(channelType: string, fallbackName?: string): string { + const meta = getChannelMeta(channelType); + return resolveChannelMetaText(channelType, 'name', fallbackName ?? meta.name, t, hasMessage); + } + + function getChannelDescription(meta: ChannelMeta): string { + return resolveChannelMetaText(meta.type, 'description', meta.description, t, hasMessage); + } + function loadSupportedChannels(nextFeedback?: string): void { const channels = getPrimaryChannelOptions(); setSupportedChannels(channels); @@ -103,7 +137,7 @@ export default function ChannelsPage() { function refreshPage(): void { setRefreshing(true); window.setTimeout(() => { - loadSupportedChannels('支持频道列表已刷新。'); + loadSupportedChannels(t('channels.page.refreshed')); setRefreshing(false); }, 0); } @@ -151,7 +185,7 @@ export default function ChannelsPage() { async function handleSaveModal(): Promise { const accountIdForSave = 'default'; const meta = getChannelMeta(modalChannelType); - const requiredError = validateRequiredFields(modalChannelType, modalValues); + const requiredError = validateRequiredFields(modalChannelType, modalValues, t); if (requiredError) { setModalError(requiredError); return; @@ -178,19 +212,19 @@ export default function ChannelsPage() { method: 'POST', body: JSON.stringify({ channelType: modalChannelType, - channelLabel: getChannelMeta(modalChannelType).name, - accountName: buildSavedAccountName(modalChannelType, accountIdForSave), + channelLabel: meta.name, + accountName: buildSavedAccountName(meta.name, accountIdForSave, t('channels.page.defaultAccountLabel')), channelUrl: deriveChannelUrl(modalChannelType, accountIdForSave, config), enabled: true, config, metadata: { - connectionType: getChannelMeta(modalChannelType).connectionType, + connectionType: meta.connectionType, managedBy: 'channels-page', }, }), }); - setFeedback(`${getChannelMeta(modalChannelType).name} 配置已保存。`); + setFeedback(t('channels.page.saved', { name: getChannelName(modalChannelType, meta.name) })); resetModalState(); } catch (requestError) { setModalError(requestError instanceof Error ? requestError.message : String(requestError)); @@ -213,10 +247,10 @@ export default function ChannelsPage() { className="text-[36px] font-normal tracking-tight text-[#0F172A] dark:text-gray-100" style={{ fontFamily: "Georgia, Cambria, 'Times New Roman', Times, serif" }} > - 支持的频道 + {t('channels.page.title')}
- 当前页面聚焦支持的频道模块、配置入口与刷新能力。 + {t('channels.page.description')}
@@ -227,7 +261,7 @@ export default function ChannelsPage() { onClick={refreshPage} > - 刷新 + {t('channels.page.refresh')} @@ -239,48 +273,52 @@ export default function ChannelsPage() { {supportedChannels.length > 0 ? (
- {supportedChannels.map((meta) => ( -
- - ))} + + ); + })} ) : (
- 暂无可配置的频道模块。 + {t('channels.page.empty')}
)} @@ -310,24 +348,24 @@ export default function ChannelsPage() { ); } -function ChannelLogo({ type }: { type: string }) { +function ChannelLogo({ type, label }: { type: string; label: string }) { switch (type) { case 'telegram': - return Telegram; + return {label}; case 'discord': - return Discord; + return {label}; case 'whatsapp': - return WhatsApp; + return {label}; case 'wechat': - return WeChat; + return {label}; case 'dingtalk': - return DingTalk; + return {label}; case 'feishu': - return Feishu; + return {label}; case 'wecom': - return WeCom; + return {label}; case 'qqbot': - return QQ; + return {label}; default: return {type.slice(0, 1).toUpperCase()}; }