feat: enhance channel configuration UI and validation

- Updated ChannelInstructionsPanel to include a button for viewing documentation, improving user guidance.
- Enhanced ChannelTokenField to support showing/hiding secret values with appropriate labels and icons.
- Refined ChannelTypeSelector to display connection type icons and improved layout for better user experience.
- Added new messages for documentation links, validation feedback, and secret management in i18n.
- Extended ChannelMeta to include optional documentation URLs for better context on configuration fields.
- Implemented credential validation logic in ChannelsPage to ensure user inputs are validated before saving.
- Introduced ChannelLogo component to display channel icons in the UI.
- Added tests for channel credential validation to ensure proper error handling and feedback.
This commit is contained in:
duanshuwen
2026-04-19 16:43:07 +08:00
parent d2e48b21d8
commit 18f12d6ce3
22 changed files with 1131 additions and 301 deletions

View File

@@ -8,6 +8,7 @@ export interface ChannelMeta {
name: string;
description: string;
connectionType: ChannelConnectionType;
docsUrl?: string;
configFields: ChannelConfigFieldMeta[];
isPlugin: boolean;
instructions: string[];
@@ -20,6 +21,7 @@ function createField(
placeholder?: string,
description?: string,
required = false,
options?: Pick<ChannelConfigFieldMeta, 'docsUrl' | 'envVar'>,
): ChannelConfigFieldMeta {
return {
key,
@@ -27,6 +29,8 @@ function createField(
kind,
placeholder,
description,
docsUrl: options?.docsUrl,
envVar: options?.envVar,
required,
autoComplete: 'off',
};
@@ -38,8 +42,9 @@ function createTokenField(
placeholder?: string,
description?: string,
required = false,
options?: Pick<ChannelConfigFieldMeta, 'docsUrl' | 'envVar'>,
): ChannelConfigFieldMeta {
return createField(key, label, 'token', placeholder, description, required);
return createField(key, label, 'token', placeholder, description, required, options);
}
function createTextField(
@@ -48,8 +53,9 @@ function createTextField(
placeholder?: string,
description?: string,
required = false,
options?: Pick<ChannelConfigFieldMeta, 'docsUrl' | 'envVar'>,
): ChannelConfigFieldMeta {
return createField(key, label, 'text', placeholder, description, required);
return createField(key, label, 'text', placeholder, description, required, options);
}
function createPasswordField(
@@ -58,8 +64,9 @@ function createPasswordField(
placeholder?: string,
description?: string,
required = false,
options?: Pick<ChannelConfigFieldMeta, 'docsUrl' | 'envVar'>,
): ChannelConfigFieldMeta {
return createField(key, label, 'password', placeholder, description, required);
return createField(key, label, 'password', placeholder, description, required, options);
}
function createTextareaField(
@@ -68,9 +75,10 @@ function createTextareaField(
placeholder?: string,
description?: string,
required = false,
options?: Pick<ChannelConfigFieldMeta, 'docsUrl' | 'envVar'>,
): ChannelConfigFieldMeta {
return {
...createField(key, label, 'textarea', placeholder, description, required),
...createField(key, label, 'textarea', placeholder, description, required, options),
rows: 5,
};
}
@@ -81,8 +89,9 @@ function createUrlField(
placeholder?: string,
description?: string,
required = false,
options?: Pick<ChannelConfigFieldMeta, 'docsUrl' | 'envVar'>,
): ChannelConfigFieldMeta {
return createField(key, label, 'url', placeholder, description, required);
return createField(key, label, 'url', placeholder, description, required, options);
}
export const PRIMARY_CHANNEL_TYPES = [
@@ -102,15 +111,36 @@ export const CHANNEL_META_LIST: ChannelMeta[] = [
name: 'Telegram',
description: '使用 @BotFather 提供的机器人令牌连接 Telegram。',
connectionType: 'token',
docsUrl: 'https://core.telegram.org/bots',
configFields: [
createPasswordField('botToken', 'Bot Token', '123456:telegram-bot-token', '从 @BotFather 获取的机器人令牌。', true),
createTextField('allowedUsers', 'Allowed Users', '12345678,98765432', '可选。限制允许与机器人对话的 Telegram 用户 ID。'),
createPasswordField(
'botToken',
'Bot Token',
'123456:telegram-bot-token',
'从 @BotFather 获取的机器人令牌。',
true,
{
docsUrl: 'https://core.telegram.org/bots#botfather',
envVar: 'TELEGRAM_BOT_TOKEN',
},
),
createTextField(
'allowedUsers',
'Allowed Users',
'12345678,98765432',
'可选。限制允许与机器人对话的 Telegram 用户 ID。',
false,
{
envVar: 'TELEGRAM_ALLOWED_USERS',
},
),
],
isPlugin: false,
instructions: [
'在 Telegram 中使用 @BotFather 创建机器人并复制 Bot Token。',
'如需限制访问范围,可把用户 ID 通过环境变量或配置项预先准备好。',
'如果需要限制测试范围,可填写允许访问的用户 ID 列表。',
'保存后在消息频道页确认默认账号、归属 Agent 和运行态状态。',
'保存后在消息频道页确认默认账号、归属 Agent 和运行态状态,完成 save and connect。',
],
},
{
@@ -118,15 +148,45 @@ export const CHANNEL_META_LIST: ChannelMeta[] = [
name: 'Discord',
description: '使用开发者门户提供的机器人令牌连接 Discord。',
connectionType: 'token',
docsUrl: 'https://discord.com/developers/docs/intro',
configFields: [
createPasswordField('token', 'Bot Token', 'discord-bot-token', 'Discord Bot Token。', true),
createTextField('guildId', 'Guild ID', '123456789012345678', '推荐填写,便于定位绑定的服务器。', true),
createTextField('channelId', 'Channel ID', '123456789012345678', '可选。指定默认投递频道。'),
createPasswordField(
'token',
'Bot Token',
'discord-bot-token',
'Discord Bot Token。',
true,
{
docsUrl: 'https://discord.com/developers/applications',
envVar: 'DISCORD_BOT_TOKEN',
},
),
createTextField(
'guildId',
'Guild ID',
'123456789012345678',
'推荐填写,便于定位绑定的服务器。',
true,
{
envVar: 'DISCORD_GUILD_ID',
},
),
createTextField(
'channelId',
'Channel ID',
'123456789012345678',
'可选。指定默认投递频道。',
false,
{
envVar: 'DISCORD_CHANNEL_ID',
},
),
],
isPlugin: false,
instructions: [
'在 Discord Developer Portal 创建应用并启用 Bot。',
'复制 Token并在目标服务器中邀请该机器人。',
'把 Token 和可选的 Guild / Channel 标识整理成环境变量后,便于本地与部署环境复用。',
'如果需要固定默认频道,可补充填写 Guild ID / Channel ID。',
],
},
@@ -135,10 +195,12 @@ export const CHANNEL_META_LIST: ChannelMeta[] = [
name: 'WhatsApp',
description: '通过扫描二维码连接 WhatsApp无需手机号。',
connectionType: 'qr',
docsUrl: 'https://developers.facebook.com/docs/whatsapp',
configFields: [],
isPlugin: false,
instructions: [
'保存后等待 runtime 侧二维码能力接入。',
'如果后续切换到服务化接入,可把账号凭据改为环境变量管理。',
'当前 `zn-ai` 已预留配置和状态位,二维码拉起仍是后续波次。',
'在完全对齐 ClawX 前,建议先使用默认账号完成路由与绑定链路验证。',
],
@@ -148,10 +210,12 @@ export const CHANNEL_META_LIST: ChannelMeta[] = [
name: 'WeChat',
description: '通过腾讯官方 OpenClaw 插件扫码连接个人微信。',
connectionType: 'qr',
docsUrl: 'https://developers.weixin.qq.com/doc/',
configFields: [],
isPlugin: true,
instructions: [
'该频道依赖插件与二维码链路,当前页面已保留入口。',
'扫码接入通常不需要额外字段,但后续如果插件暴露 envVar可直接在 modal 中复用。',
'后续需要补齐扫码事件、成功回调和账号自动发现。',
'Agent 绑定、默认账号和删除流程已可先行对齐。',
],
@@ -161,16 +225,28 @@ export const CHANNEL_META_LIST: ChannelMeta[] = [
name: 'DingTalk',
description: '通过 OpenClaw 渠道插件连接钉钉Stream 模式)。',
connectionType: 'token',
docsUrl: 'https://open.dingtalk.com/document',
configFields: [
createTextField('clientId', 'Client ID', 'ding-app-key', '钉钉应用的 AppKey。', true),
createPasswordField('clientSecret', 'Client Secret', 'ding-app-secret', '钉钉应用的 AppSecret。', true),
createTextField('robotCode', 'Robot Code', 'dingxxxx', '可选。机器人编码。'),
createTextField('corpId', 'Corp ID', 'dingcorp123', '可选。企业 corpId。'),
createTextField('agentId', 'Agent ID', '123456789', '可选。机器人 AgentId。'),
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',
}),
],
isPlugin: true,
instructions: [
'在钉钉开发者后台创建机器人并获取 AppKey / AppSecret。',
'如果你更习惯部署时注入配置,可以把这些值放到环境变量里再填入 modal。',
'按需补充 Robot Code、Corp ID 或 Agent ID 以兼容不同部署方式。',
'保存后优先验证默认账号、频道归属和 gateway 重载是否收敛。',
],
@@ -180,13 +256,19 @@ export const CHANNEL_META_LIST: ChannelMeta[] = [
name: 'Feishu / Lark',
description: '通过飞书官方推出的 OpenClaw 插件连接飞书/Lark 机器人。',
connectionType: 'token',
docsUrl: 'https://open.feishu.cn/document',
configFields: [
createTextField('appId', 'App ID', 'cli_xxxxxxxxx', '飞书应用的 App ID。', true),
createPasswordField('appSecret', 'App Secret', 'app-secret', '飞书应用的 App Secret。', true),
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',
}),
],
isPlugin: true,
instructions: [
'在飞书开放平台创建机器人应用并开启事件订阅。',
'App ID 与 App Secret 可以直接从环境变量注入,方便本地、测试与生产环境统一。',
'填入 App ID 与 App Secret保存后完成默认账号与 Agent 绑定。',
'这是 ClawX 中验证最完整的频道之一,建议作为首个联调样板。',
],
@@ -196,13 +278,19 @@ export const CHANNEL_META_LIST: ChannelMeta[] = [
name: 'WeCom',
description: '通过插件连接企业微信机器人。',
connectionType: 'token',
docsUrl: 'https://developer.work.weixin.qq.com/document',
configFields: [
createTextField('botId', 'Bot ID', 'wecom-bot-id', '企业微信机器人或应用标识。', true),
createPasswordField('secret', 'Secret', 'wecom-secret', '企业微信机器人密钥。', true),
createTextField('botId', 'Bot ID', 'wecom-bot-id', '企业微信机器人或应用标识。', true, {
envVar: 'WECOM_BOT_ID',
}),
createPasswordField('secret', 'Secret', 'wecom-secret', '企业微信机器人密钥。', true, {
envVar: 'WECOM_BOT_SECRET',
}),
],
isPlugin: true,
instructions: [
'在企业微信管理后台创建机器人并复制关键信息。',
'如果你已经在部署层维护 env vars可直接用环境变量名作为对照来填充字段。',
'建议先用默认账号联通消息,再扩展多账号分工。',
'保存后使用消息频道页统一管理默认账号和 Agent 归属。',
],
@@ -212,13 +300,19 @@ export const CHANNEL_META_LIST: ChannelMeta[] = [
name: 'QQ Bot',
description: '连接 QQ 机器人频道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),
createPasswordField('clientSecret', 'Client Secret', 'qq-client-secret', 'QQ Bot Client Secret。', true),
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、目标选择和路由会直接复用这里的配置。',
],
@@ -338,6 +432,7 @@ export const DEFAULT_CHANNEL_META: ChannelMeta = {
name: 'Custom Channel',
description: '通用频道模板,用于承接尚未内建 schema 的自定义渠道或插件。',
connectionType: 'plugin',
docsUrl: undefined,
configFields: [
createTokenField('token', 'Token', 'token-or-secret', '粘贴该渠道要求的 token、secret 或访问凭据。'),
],

View File

@@ -15,6 +15,8 @@ export interface ChannelConfigFieldMeta {
kind: ChannelConfigFieldKind;
placeholder?: string;
description?: string;
docsUrl?: string;
envVar?: string | string[];
required?: boolean;
rows?: number;
autoComplete?: string;