feat: 语言国际化重构

This commit is contained in:
DEV_DSW
2026-04-21 16:52:45 +08:00
parent 0c068e9f4d
commit 3349d41881
76 changed files with 4440 additions and 3232 deletions

View File

@@ -14,6 +14,8 @@ export interface ChannelMeta {
instructions: string[];
}
export type ChannelTextResolver = (path: string, fallback: string) => string;
function createField(
key: string,
label: string,
@@ -94,6 +96,93 @@ function createUrlField(
return createField(key, label, 'url', placeholder, description, required, options);
}
function buildChannelMetaPath(channelType: string, suffix: string): string {
return `channels.meta.${channelType}.${suffix}`;
}
function resolveChannelText(
channelType: string,
suffix: string,
fallback: string,
resolveText?: ChannelTextResolver,
): string {
return resolveText ? resolveText(buildChannelMetaPath(channelType, suffix), fallback) : fallback;
}
export function formatChannelTypeLabel(channelType: string): string {
const normalized = String(channelType ?? '')
.split(/[-_]/)
.map((part) => part.trim())
.filter(Boolean);
if (normalized.length === 0) return String(channelType ?? '');
return normalized
.map((part) => part.charAt(0).toUpperCase() + part.slice(1))
.join(' ');
}
export function localizeChannelField(
channelType: string,
field: ChannelConfigFieldMeta,
resolveText?: ChannelTextResolver,
): ChannelConfigFieldMeta {
if (!resolveText) {
return field;
}
return {
...field,
label: resolveChannelText(channelType, `fields.${field.key}.label`, field.label, resolveText),
placeholder: field.placeholder
? resolveChannelText(channelType, `fields.${field.key}.placeholder`, field.placeholder, resolveText)
: field.placeholder,
description: field.description
? resolveChannelText(channelType, `fields.${field.key}.description`, field.description, resolveText)
: field.description,
docsUrl: field.docsUrl
? resolveChannelText(channelType, `fields.${field.key}.docsUrl`, field.docsUrl, resolveText)
: field.docsUrl,
};
}
export function localizeChannelMeta(meta: ChannelMeta, resolveText?: ChannelTextResolver): ChannelMeta {
if (!resolveText) {
return meta;
}
return {
...meta,
name: resolveChannelText(meta.type, 'name', meta.name, resolveText),
description: resolveChannelText(meta.type, 'description', meta.description, resolveText),
docsUrl: meta.docsUrl
? resolveChannelText(meta.type, 'docsUrl', meta.docsUrl, resolveText)
: meta.docsUrl,
instructions: meta.instructions.map((instruction, index) => (
resolveChannelText(meta.type, `instructions.${index}`, instruction, resolveText)
)),
configFields: meta.configFields.map((field) => localizeChannelField(meta.type, field, resolveText)),
};
}
export function getChannelDisplayName(
channelType: string | null | undefined,
resolveText?: ChannelTextResolver,
fallbackName?: string,
): string {
const meta = getChannelMeta(channelType);
return resolveChannelText(meta.type, 'name', fallbackName ?? meta.name, resolveText);
}
export function getChannelDisplayDescription(
channelType: string | null | undefined,
resolveText?: ChannelTextResolver,
fallbackDescription?: string,
): string {
const meta = getChannelMeta(channelType);
return resolveChannelText(meta.type, 'description', fallbackDescription ?? meta.description, resolveText);
}
export const PRIMARY_CHANNEL_TYPES = [
'telegram',
'discord',
@@ -109,7 +198,7 @@ export const CHANNEL_META_LIST: ChannelMeta[] = [
{
type: 'telegram',
name: 'Telegram',
description: '使用 @BotFather 提供的机器人令牌连接 Telegram。',
description: 'Connect Telegram using the bot token provided by @BotFather.',
connectionType: 'token',
docsUrl: 'https://core.telegram.org/bots',
configFields: [
@@ -117,7 +206,7 @@ export const CHANNEL_META_LIST: ChannelMeta[] = [
'botToken',
'Bot Token',
'123456:telegram-bot-token',
' @BotFather 获取的机器人令牌。',
'Bot token issued by @BotFather.',
true,
{
docsUrl: 'https://core.telegram.org/bots#botfather',
@@ -128,7 +217,7 @@ export const CHANNEL_META_LIST: ChannelMeta[] = [
'allowedUsers',
'Allowed Users',
'12345678,98765432',
'可选。限制允许与机器人对话的 Telegram 用户 ID。',
'Optional. Comma-separated Telegram user IDs allowed to chat with this bot.',
false,
{
envVar: 'TELEGRAM_ALLOWED_USERS',
@@ -137,16 +226,16 @@ export const CHANNEL_META_LIST: ChannelMeta[] = [
],
isPlugin: false,
instructions: [
'在 Telegram 中使用 @BotFather 创建机器人并复制 Bot Token',
'如需限制访问范围,可把用户 ID 通过环境变量或配置项预先准备好。',
'如果需要限制测试范围,可填写允许访问的用户 ID 列表。',
'保存后在消息频道页确认默认账号、归属 Agent 和运行态状态,完成 save and connect。',
'Create a bot with @BotFather in Telegram and copy the Bot Token.',
'If you want to restrict access, collect the Telegram user IDs that are allowed to talk to this bot.',
'Paste the token and, if needed, the allowed user list into the fields below.',
'After saving, confirm the default account and Agent binding from the Channels flow.',
],
},
{
type: 'discord',
name: 'Discord',
description: '使用开发者门户提供的机器人令牌连接 Discord。',
description: 'Connect Discord using the bot token provided in the developer portal.',
connectionType: 'token',
docsUrl: 'https://discord.com/developers/docs/intro',
configFields: [
@@ -154,7 +243,7 @@ export const CHANNEL_META_LIST: ChannelMeta[] = [
'token',
'Bot Token',
'discord-bot-token',
'Discord Bot Token',
'Discord bot token.',
true,
{
docsUrl: 'https://discord.com/developers/applications',
@@ -165,7 +254,7 @@ export const CHANNEL_META_LIST: ChannelMeta[] = [
'guildId',
'Guild ID',
'123456789012345678',
'推荐填写,便于定位绑定的服务器。',
'Recommended. Limits the bot to a specific server.',
true,
{
envVar: 'DISCORD_GUILD_ID',
@@ -175,7 +264,7 @@ export const CHANNEL_META_LIST: ChannelMeta[] = [
'channelId',
'Channel ID',
'123456789012345678',
'可选。指定默认投递频道。',
'Optional. Sends messages to a default channel.',
false,
{
envVar: 'DISCORD_CHANNEL_ID',
@@ -184,240 +273,389 @@ export const CHANNEL_META_LIST: ChannelMeta[] = [
],
isPlugin: false,
instructions: [
'在 Discord Developer Portal 创建应用并启用 Bot。',
'复制 Token并在目标服务器中邀请该机器人。',
'把 Token 和可选的 Guild / Channel 标识整理成环境变量后,便于本地与部署环境复用。',
'如果需要固定默认频道,可补充填写 Guild ID / Channel ID。',
'Create an application in the Discord Developer Portal and enable the Bot feature.',
'Copy the bot token and invite the bot to the target server.',
'Prepare the token and optional Guild or Channel identifiers as environment variables if you deploy them elsewhere.',
'Fill in the fields, save, and verify the target server/channel mapping if you pin one.',
],
},
{
type: 'whatsapp',
name: 'WhatsApp',
description: '通过扫描二维码连接 WhatsApp无需手机号',
description: 'Connect WhatsApp by scanning a QR code, without requiring a phone number.',
connectionType: 'qr',
docsUrl: 'https://developers.facebook.com/docs/whatsapp',
configFields: [],
isPlugin: false,
instructions: [
'保存后等待 runtime 侧二维码能力接入。',
'如果后续切换到服务化接入,可把账号凭据改为环境变量管理。',
'当前 `zn-ai` 已预留配置和状态位,二维码拉起仍是后续波次。',
'在完全对齐 ClawX 前,建议先使用默认账号完成路由与绑定链路验证。',
'Save the channel first so the runtime can request a QR session when support is ready.',
'If you later move to a hosted integration, keep the account credentials in environment variables.',
'The current zn-ai flow keeps the configuration and state placeholders aligned with ClawX.',
'Until the QR bridge is fully wired, use the default account path for routing and ownership validation.',
],
},
{
type: 'wechat',
name: 'WeChat',
description: '通过腾讯官方 OpenClaw 插件扫码连接个人微信。',
description: 'Connect personal WeChat by scanning through the official OpenClaw plugin.',
connectionType: 'qr',
docsUrl: 'https://developers.weixin.qq.com/doc/',
configFields: [],
isPlugin: true,
instructions: [
'该频道依赖插件与二维码链路,当前页面已保留入口。',
'扫码接入通常不需要额外字段,但后续如果插件暴露 envVar可直接在 modal 中复用。',
'后续需要补齐扫码事件、成功回调和账号自动发现。',
'Agent 绑定、默认账号和删除流程已可先行对齐。',
'This channel depends on the plugin and QR flow, and the current page already reserves the entry point.',
'QR-based onboarding usually does not need extra fields, but environment-backed options can be added later if the plugin exposes them.',
'The remaining QR events, success callbacks, and account discovery will be filled in later.',
'Agent ownership, default account handling, and cleanup flows can already stay aligned here.',
],
},
{
type: 'dingtalk',
name: 'DingTalk',
description: '通过 OpenClaw 渠道插件连接钉钉(Stream 模式)。',
description: 'Connect DingTalk through the OpenClaw channel plugin in Stream mode.',
connectionType: 'token',
docsUrl: 'https://open.dingtalk.com/document',
configFields: [
createTextField('clientId', 'Client ID', 'ding-app-key', '钉钉应用的 AppKey。', true, {
envVar: 'DINGTALK_CLIENT_ID',
}),
createPasswordField('clientSecret', 'Client Secret', 'ding-app-secret', '钉钉应用的 AppSecret。', true, {
envVar: 'DINGTALK_CLIENT_SECRET',
}),
createTextField('robotCode', 'Robot Code', 'dingxxxx', '可选。机器人编码。', false, {
envVar: 'DINGTALK_ROBOT_CODE',
}),
createTextField('corpId', 'Corp ID', 'dingcorp123', '可选。企业 corpId。', false, {
envVar: 'DINGTALK_CORP_ID',
}),
createTextField('agentId', 'Agent ID', '123456789', '可选。机器人 AgentId。', false, {
envVar: 'DINGTALK_AGENT_ID',
}),
createTextField(
'clientId',
'Client ID',
'ding-app-key',
'DingTalk app key.',
true,
{
envVar: 'DINGTALK_CLIENT_ID',
},
),
createPasswordField(
'clientSecret',
'Client Secret',
'ding-app-secret',
'DingTalk app secret.',
true,
{
envVar: 'DINGTALK_CLIENT_SECRET',
},
),
createTextField(
'robotCode',
'Robot Code',
'dingxxxx',
'Optional. DingTalk robot code.',
false,
{
envVar: 'DINGTALK_ROBOT_CODE',
},
),
createTextField(
'corpId',
'Corp ID',
'dingcorp123',
'Optional. Enterprise corpId.',
false,
{
envVar: 'DINGTALK_CORP_ID',
},
),
createTextField(
'agentId',
'Agent ID',
'123456789',
'Optional. DingTalk AgentId.',
false,
{
envVar: 'DINGTALK_AGENT_ID',
},
),
],
isPlugin: true,
instructions: [
'在钉钉开发者后台创建机器人并获取 AppKey / AppSecret',
'如果你更习惯部署时注入配置,可以把这些值放到环境变量里再填入 modal。',
'按需补充 Robot CodeCorp ID Agent ID 以兼容不同部署方式。',
'保存后优先验证默认账号、频道归属和 gateway 重载是否收敛。',
'Create a bot app in DingTalk and collect the AppKey and AppSecret.',
'If you prefer injecting configuration at deploy time, map these values through environment variables first.',
'Add Robot Code, Corp ID, or Agent ID as needed to match your deployment mode.',
'After saving, verify the default account, channel ownership, and runtime reload path.',
],
},
{
type: 'feishu',
name: 'Feishu / Lark',
description: '通过飞书官方推出的 OpenClaw 插件连接飞书/Lark 机器人。',
description: 'Connect a Feishu/Lark bot through Feishu\'s official OpenClaw plugin.',
connectionType: 'token',
docsUrl: 'https://open.feishu.cn/document',
configFields: [
createTextField('appId', 'App ID', 'cli_xxxxxxxxx', '飞书应用的 App ID。', true, {
envVar: 'FEISHU_APP_ID',
}),
createPasswordField('appSecret', 'App Secret', 'app-secret', '飞书应用的 App Secret。', true, {
envVar: 'FEISHU_APP_SECRET',
}),
createTextField(
'appId',
'App ID',
'cli_xxxxxxxxx',
'Feishu app ID.',
true,
{
envVar: 'FEISHU_APP_ID',
},
),
createPasswordField(
'appSecret',
'App Secret',
'app-secret',
'Feishu app secret.',
true,
{
envVar: 'FEISHU_APP_SECRET',
},
),
],
isPlugin: true,
instructions: [
'在飞书开放平台创建机器人应用并开启事件订阅。',
'App ID App Secret 可以直接从环境变量注入,方便本地、测试与生产环境统一。',
'填入 App ID 与 App Secret保存后完成默认账号与 Agent 绑定。',
'这是 ClawX 中验证最完整的频道之一,建议作为首个联调样板。',
'Create a bot application in the Feishu or Lark developer console and enable the needed events.',
'App ID and App Secret can come directly from environment variables so local, staging, and production stay consistent.',
'Fill in the credentials and save to finish the default account and Agent binding flow.',
'This is one of the most complete ClawX-aligned channel flows and a good first integration to validate.',
],
},
{
type: 'wecom',
name: 'WeCom',
description: '通过插件连接企业微信机器人。',
description: 'Connect a WeCom bot through the plugin.',
connectionType: 'token',
docsUrl: 'https://developer.work.weixin.qq.com/document',
configFields: [
createTextField('botId', 'Bot ID', 'wecom-bot-id', '企业微信机器人或应用标识。', true, {
envVar: 'WECOM_BOT_ID',
}),
createPasswordField('secret', 'Secret', 'wecom-secret', '企业微信机器人密钥。', true, {
envVar: 'WECOM_BOT_SECRET',
}),
createTextField(
'botId',
'Bot ID',
'wecom-bot-id',
'WeCom bot or app identifier.',
true,
{
envVar: 'WECOM_BOT_ID',
},
),
createPasswordField(
'secret',
'Secret',
'wecom-secret',
'WeCom bot secret.',
true,
{
envVar: 'WECOM_BOT_SECRET',
},
),
],
isPlugin: true,
instructions: [
'在企业微信管理后台创建机器人并复制关键信息。',
'如果你已经在部署层维护 env vars可直接用环境变量名作为对照来填充字段。',
'建议先用默认账号联通消息,再扩展多账号分工。',
'保存后使用消息频道页统一管理默认账号和 Agent 归属。',
'Create the bot in the WeCom admin console and copy the required credentials.',
'If your deployment already manages env vars, keep the same variable names here for easier mapping.',
'Start with the main account to validate message delivery before expanding to multiple accounts.',
'After saving, keep account ownership and Agent binding managed from the Channels page.',
],
},
{
type: 'qqbot',
name: 'QQ Bot',
description: '连接 QQ 机器人频道(OpenClaw 3.31 起内置)。',
description: 'Connect a QQ bot channel, built in since OpenClaw 3.31.',
connectionType: 'token',
docsUrl: 'https://bot.q.qq.com/wiki',
configFields: [
createTextField('appId', 'App ID', 'qq-app-id', 'QQ Bot App ID。', true, {
envVar: 'QQBOT_APP_ID',
}),
createPasswordField('clientSecret', 'Client Secret', 'qq-client-secret', 'QQ Bot Client Secret。', true, {
envVar: 'QQBOT_CLIENT_SECRET',
}),
createTextField(
'appId',
'App ID',
'qq-app-id',
'QQ Bot app ID.',
true,
{
envVar: 'QQBOT_APP_ID',
},
),
createPasswordField(
'clientSecret',
'Client Secret',
'qq-client-secret',
'QQ Bot client secret.',
true,
{
envVar: 'QQBOT_CLIENT_SECRET',
},
),
],
isPlugin: false,
instructions: [
'在 QQ Bot 平台创建机器人并获取 App ID 与密钥。',
'App ID Client Secret 都可以通过环境变量形式提前约定,方便和 runtime 对齐。',
'如需多账号场景,新增账号时建议使用清晰的 accountId 规则。',
'对齐完成后Cron、目标选择和路由会直接复用这里的配置。',
'Create the bot in the QQ Bot platform and obtain the App ID and secret.',
'Both App ID and Client Secret can be supplied ahead of time through environment variables to match the runtime.',
'If you need multiple accounts, adopt a clear accountId naming pattern when you add them later.',
'Once aligned, downstream routing and target selection can reuse the configuration stored here.',
],
},
{
type: 'signal',
name: 'Signal',
description: '通过 Signal 服务号码接入消息路由。',
description: 'Connect Signal through a registered service number.',
connectionType: 'token',
configFields: [
createTextField('phoneNumber', 'Phone Number', '+8613800138000', 'Signal 注册号码。', true),
createTextField(
'phoneNumber',
'Phone Number',
'+8613800138000',
'Signal phone number registered for this bridge.',
true,
),
],
isPlugin: false,
instructions: [
'准备已注册 Signal 的号码。',
'补充 runtime 侧连接能力后即可复用当前配置模型。',
'Prepare a registered Signal number for the bridge service.',
'Once runtime support lands, this channel can reuse the same configuration structure.',
],
},
{
type: 'imessage',
name: 'iMessage',
description: '通过桥接服务接入 iMessage',
description: 'Connect iMessage through a bridge service.',
connectionType: 'token',
configFields: [
createUrlField('serverUrl', 'Server URL', 'https://imessage-bridge.example.com', '桥接服务地址。', true),
createPasswordField('password', 'Password', 'bridge-password', '桥接服务认证密码。', true),
createUrlField(
'serverUrl',
'Server URL',
'https://imessage-bridge.example.com',
'Bridge service URL.',
true,
),
createPasswordField(
'password',
'Password',
'bridge-password',
'Bridge service password.',
true,
),
],
isPlugin: false,
instructions: [
'先部署 iMessage bridge 服务。',
'保存后验证服务地址和认证信息是否可用。',
'Deploy the iMessage bridge service first.',
'After saving, verify the service URL and credentials before routing traffic here.',
],
},
{
type: 'matrix',
name: 'Matrix',
description: '通过 homeserver access token 接入 Matrix。',
description: 'Connect Matrix using a homeserver and access token.',
connectionType: 'token',
configFields: [
createUrlField('homeserver', 'Homeserver', 'https://matrix.example.com', 'Matrix homeserver 地址。', true),
createPasswordField('accessToken', 'Access Token', 'matrix-access-token', 'Matrix access token。', true),
createUrlField(
'homeserver',
'Homeserver',
'https://matrix.example.com',
'Matrix homeserver URL.',
true,
),
createPasswordField(
'accessToken',
'Access Token',
'matrix-access-token',
'Matrix access token.',
true,
),
],
isPlugin: true,
instructions: [
'准备 Matrix homeserver access token',
'建议后续将 channel target 自动发现扩展到 roomId 级别。',
'Prepare the Matrix homeserver URL and an access token.',
'A later enhancement can promote channel target discovery down to the room level.',
],
},
{
type: 'line',
name: 'LINE',
description: '通过 LINE Messaging API 接入机器人。',
description: 'Connect a LINE bot using the Messaging API.',
connectionType: 'token',
configFields: [
createPasswordField('channelAccessToken', 'Channel Access Token', 'line-channel-access-token', 'LINE Channel Access Token。', true),
createPasswordField('channelSecret', 'Channel Secret', 'line-channel-secret', 'LINE Channel Secret。', true),
createPasswordField(
'channelAccessToken',
'Channel Access Token',
'line-channel-access-token',
'LINE channel access token.',
true,
),
createPasswordField(
'channelSecret',
'Channel Secret',
'line-channel-secret',
'LINE channel secret.',
true,
),
],
isPlugin: true,
instructions: [
'在 LINE Developers 中创建 Messaging API channel',
'保存后验证 tokensecret 与默认投递账号。',
'Create a Messaging API channel in LINE Developers.',
'After saving, verify the token, secret, and the default delivery account.',
],
},
{
type: 'msteams',
name: 'Microsoft Teams',
description: '通过 Bot Framework 凭证接入 Microsoft Teams。',
description: 'Connect Microsoft Teams through Bot Framework credentials.',
connectionType: 'token',
configFields: [
createTextField('appId', 'App ID', 'teams-app-id', 'Bot Framework App ID。', true),
createPasswordField('appPassword', 'App Password', 'teams-app-password', 'Bot Framework App Password。', true),
createTextField(
'appId',
'App ID',
'teams-app-id',
'Bot Framework App ID.',
true,
),
createPasswordField(
'appPassword',
'App Password',
'teams-app-password',
'Bot Framework App Password.',
true,
),
],
isPlugin: true,
instructions: [
' Azure / Bot Framework 中创建 Teams Bot。',
'保存后与默认账号、Agent 绑定一并验证。',
'Create a Teams bot in Azure or Bot Framework.',
'After saving, verify the default account and Agent ownership together.',
],
},
{
type: 'googlechat',
name: 'Google Chat',
description: '通过 Google Chat 服务账号接入消息投递。',
description: 'Connect Google Chat using a service account.',
connectionType: 'webhook',
configFields: [
createTextareaField('serviceAccountKey', 'Service Account Key', '{ ...json... }', 'Google 服务账号 JSON 内容。', true),
createTextareaField(
'serviceAccountKey',
'Service Account Key',
'{ ...json... }',
'Google service account JSON payload.',
true,
),
],
isPlugin: false,
instructions: [
'在 Google Cloud 中创建服务账号并开启 Chat API',
'将服务账号 JSON 粘贴到配置中,后续可扩展 webhook / space 目标发现。',
'Create a service account in Google Cloud and enable the Chat API.',
'Paste the service account JSON here. Webhook and space discovery can be added later.',
],
},
{
type: 'mattermost',
name: 'Mattermost',
description: '通过服务地址与 Bot Token 接入 Mattermost。',
description: 'Connect Mattermost using a server URL and bot token.',
connectionType: 'token',
configFields: [
createUrlField('serverUrl', 'Server URL', 'https://mattermost.example.com', 'Mattermost 服务地址。', true),
createPasswordField('botToken', 'Bot Token', 'mattermost-bot-token', 'Mattermost Bot Token。', true),
createUrlField(
'serverUrl',
'Server URL',
'https://mattermost.example.com',
'Mattermost server URL.',
true,
),
createPasswordField(
'botToken',
'Bot Token',
'mattermost-bot-token',
'Mattermost bot token.',
true,
),
],
isPlugin: true,
instructions: [
'在 Mattermost 中创建 Bot 并生成 Token',
'后续建议把 channel target 扩展到 team/channel 自动发现。',
'Create a bot in Mattermost and generate a token.',
'A future iteration can extend channel target discovery down to team or channel scopes.',
],
},
];
@@ -430,16 +668,21 @@ export const CHANNEL_META_MAP = CHANNEL_META_LIST.reduce<Record<string, ChannelM
export const DEFAULT_CHANNEL_META: ChannelMeta = {
type: 'custom',
name: 'Custom Channel',
description: '通用频道模板,用于承接尚未内建 schema 的自定义渠道或插件。',
description: 'Generic channel template for integrations or plugins without a dedicated schema yet.',
connectionType: 'plugin',
docsUrl: undefined,
configFields: [
createTokenField('token', 'Token', 'token-or-secret', '粘贴该渠道要求的 token、secret 或访问凭据。'),
createTokenField(
'token',
'Token',
'token-or-secret',
'Paste the token, secret, or access credential required by this channel.',
),
],
isPlugin: true,
instructions: [
'当渠道 schema 仍在演进,或由外部插件提供时,可先使用该模板。',
'保存后仍可通过消息频道页管理默认账号、Agent 绑定与运行态状态。',
'Use this template when the channel schema is still evolving or when an external plugin provides the integration.',
'After saving, you can still manage the default account, Agent binding, and runtime state from Channels.',
],
};
@@ -453,10 +696,10 @@ export function getChannelMeta(channelType: string | null | undefined): ChannelM
...DEFAULT_CHANNEL_META,
type: trimmed,
name: trimmed,
description: `${trimmed} 的通用频道模板。`,
description: `${trimmed} generic channel template.`,
instructions: [
...DEFAULT_CHANNEL_META.instructions,
`当前 runtime 尚未为 ${trimmed} 提供专用 schema先以数据驱动方式接入。`,
`${trimmed} does not have a dedicated schema yet, so configure it with the generic template for now.`,
],
};
}