修复: Profile clone 时智能清理独占平台凭据 + 平台设置独占警告 (#283)
* 修复: profile clone 时智能清理独占平台凭据,避免 gateway 健康检查超时 # 问题 `hermes profile create <name> --clone` 完整复制 .env + config.yaml(含独占型平台凭据 如 WEIXIN_TOKEN / TELEGRAM_BOT_TOKEN 等),导致多个 profile 共享同一身份 token。 hermes-agent 在 platform adapter 初始化或 scoped lock 获取阶段失败,gateway 健康检查 持续 15s 超时,前端报 'API Error 500: Gateway health check timed out'。 # 修复 在 web-ui 后端 clone 完成后自动: 1. 从 <profile>/.env 删除匹配独占平台的环境变量(写 .env.bak.* 备份) 2. 在 <profile>/config.yaml 中把 platforms.<exclusive>.enabled 置为 false 3. 清理节点直挂 + extra 子节点下的敏感字段(token / app_secret / account_id 等) 前端 toast 提示被剥离的凭据、被禁用的平台、被剥离的 config 字段,便于用户后续手动 重新填入新身份再启用。 # EXCLUSIVE_PLATFORMS 列表来源 精确对齐 hermes-agent gateway/platforms/*.py 中调用 _acquire_platform_lock 的 7 个 adapter: telegram, discord, slack, whatsapp, signal, weixin, feishu。 未来上游加新独占平台时用 `grep -l _acquire_platform_lock gateway/platforms/*.py` 验证。 # 测试 新增 tests/server/profile-credentials.test.ts(12 用例全过),覆盖: - isExclusivePlatformKey 命中/未命中边界 - env 文件剥离 + 备份 - config.yaml 平台禁用 + 节点凭据清理 - 已 disabled 平台仍清理残留凭据(防止后续 re-enable 复用旧身份) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * feat(平台设置): 独占平台显示 token 隔离警告 在 PlatformSettings 中为使用 token 互斥锁的 6 个平台 (telegram, discord, slack, whatsapp, feishu, weixin) 添加视觉警告,提示用户每个 profile 必须使用不同的身份 token,避免与其他 profile 冲突。 # 背景 hermes-agent 的 acquire_scoped_lock 是 token-level(不是 platform-level),所以 设计上支持多 profile 各自配不同身份的同一平台(如 default 用个人微信、staging 用公司微信)。但用户从 UI 配置时容易误填同一 token,导致 gateway 启动失败。 # 实现 - PlatformCard 新增 exclusive 可选 prop,开启时 body 顶部用 NAlert (warning) 展示提示 - PlatformSettings 在 6 个独占平台数组项标记 exclusive: true 并传给 PlatformCard - 8 个 i18n locale 新增 platform.exclusiveTokenWarning 翻译 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --------- Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
@@ -335,6 +335,10 @@ export default {
|
||||
newName: 'Neuer Name',
|
||||
newNamePlaceholder: 'Neuen Namen eingeben',
|
||||
cloneFromCurrent: 'Aus aktuellem Profil klonen',
|
||||
cloneCleanupNotice: 'Beim Klonen werden exklusive Plattform-Anmeldeinformationen (Weixin / Telegram / Slack usw.) automatisch übersprungen, um Konflikte mit dem Quellprofil zu vermeiden',
|
||||
cloneStrippedCredentials: '{count} exklusive Anmeldeinformation(en) entfernt: {list}',
|
||||
cloneDisabledPlatforms: '{count} Plattform(en) deaktiviert: {list}',
|
||||
cloneStrippedConfigCredentials: '{count} eingebettete Anmeldeinformation(en) aus config.yaml entfernt: {list}',
|
||||
archivePath: 'Archivpfad',
|
||||
archivePathPlaceholder: 'Serverpfad zur Archivdatei',
|
||||
importName: 'Profilname (optional)',
|
||||
@@ -480,6 +484,7 @@ export default {
|
||||
ignoredChannelsHint: 'Kanale, in denen der Bot nie antwortet (komma-getrennt)',
|
||||
noThreadChannels: 'Thread-lose Kanale',
|
||||
noThreadChannelsHint: 'Kanale, in denen der Bot ohne Threads antwortet (komma-getrennt)',
|
||||
exclusiveTokenWarning: 'Diese Plattform verwendet einen exklusiven Token-Lock. Jedes Profil muss einen anderen Identitäts-Token verwenden, um Konflikte mit anderen Profilen zu vermeiden.',
|
||||
botToken: 'Bot-Token',
|
||||
botTokenHint: 'Bot-Token vom Entwicklerportal',
|
||||
accessToken: 'Zugangs-Token',
|
||||
|
||||
@@ -367,6 +367,10 @@ export default {
|
||||
newName: 'New Name',
|
||||
newNamePlaceholder: 'Enter new name',
|
||||
cloneFromCurrent: 'Clone from current profile',
|
||||
cloneCleanupNotice: 'Cloning automatically skips exclusive platform credentials (Weixin / Telegram / Slack, etc.) to avoid conflicts with the source profile',
|
||||
cloneStrippedCredentials: 'Stripped {count} exclusive credential(s): {list}',
|
||||
cloneDisabledPlatforms: 'Disabled {count} platform(s): {list}',
|
||||
cloneStrippedConfigCredentials: 'Stripped {count} embedded credential(s) from config.yaml: {list}',
|
||||
archivePath: 'Archive Path',
|
||||
archivePathPlaceholder: 'Server path to archive file',
|
||||
importName: 'Profile Name (optional)',
|
||||
@@ -521,6 +525,7 @@ export default {
|
||||
ignoredChannelsHint: 'Channels where bot never responds (comma-separated)',
|
||||
noThreadChannels: 'No-Thread Channels',
|
||||
noThreadChannelsHint: 'Channels where bot responds without threads (comma-separated)',
|
||||
exclusiveTokenWarning: 'This platform uses exclusive token locking. Each profile must use a different identity token to avoid conflicts with other profiles.',
|
||||
botToken: 'Bot Token',
|
||||
botTokenHint: 'Bot token from developer portal',
|
||||
accessToken: 'Access Token',
|
||||
|
||||
@@ -335,6 +335,10 @@ export default {
|
||||
newName: 'Nuevo nombre',
|
||||
newNamePlaceholder: 'Introduce un nuevo nombre',
|
||||
cloneFromCurrent: 'Clonar desde el perfil actual',
|
||||
cloneCleanupNotice: 'Al clonar se omiten automáticamente las credenciales exclusivas de plataforma (Weixin / Telegram / Slack, etc.) para evitar conflictos con el perfil de origen',
|
||||
cloneStrippedCredentials: 'Se eliminaron {count} credenciales exclusivas: {list}',
|
||||
cloneDisabledPlatforms: 'Se deshabilitaron {count} plataforma(s): {list}',
|
||||
cloneStrippedConfigCredentials: 'Se eliminaron {count} credencial(es) integradas de config.yaml: {list}',
|
||||
archivePath: 'Ruta del archivo',
|
||||
archivePathPlaceholder: 'Ruta del servidor al archivo de archivo',
|
||||
importName: 'Nombre del perfil (opcional)',
|
||||
@@ -480,6 +484,7 @@ export default {
|
||||
ignoredChannelsHint: 'Canales donde el bot nunca responde (separados por comas)',
|
||||
noThreadChannels: 'Canales sin hilo',
|
||||
noThreadChannelsHint: 'Canales donde el bot responde sin hilos (separados por comas)',
|
||||
exclusiveTokenWarning: 'Esta plataforma usa bloqueo exclusivo de token. Cada perfil debe usar un token de identidad distinto para evitar conflictos con otros perfiles.',
|
||||
botToken: 'Token del bot',
|
||||
botTokenHint: 'Token del bot del portal de desarrolladores',
|
||||
accessToken: 'Token de acceso',
|
||||
|
||||
@@ -335,6 +335,10 @@ export default {
|
||||
newName: 'Nouveau nom',
|
||||
newNamePlaceholder: 'Entrez un nouveau nom',
|
||||
cloneFromCurrent: 'Cloner depuis le profil actuel',
|
||||
cloneCleanupNotice: 'Lors du clonage, les identifiants de plateformes exclusives (Weixin / Telegram / Slack, etc.) sont automatiquement ignorés pour éviter les conflits avec le profil source',
|
||||
cloneStrippedCredentials: '{count} identifiant(s) exclusif(s) supprimé(s) : {list}',
|
||||
cloneDisabledPlatforms: '{count} plateforme(s) désactivée(s) : {list}',
|
||||
cloneStrippedConfigCredentials: '{count} identifiant(s) intégré(s) supprimé(s) de config.yaml : {list}',
|
||||
archivePath: 'Chemin de l\'archive',
|
||||
archivePathPlaceholder: 'Chemin serveur du fichier d\'archive',
|
||||
importName: 'Nom du profil (facultatif)',
|
||||
@@ -480,6 +484,7 @@ export default {
|
||||
ignoredChannelsHint: 'Canaux ou le bot ne repond jamais (separes par des virgules)',
|
||||
noThreadChannels: 'Canaux sans fil',
|
||||
noThreadChannelsHint: 'Canaux ou le bot repond sans fil (separes par des virgules)',
|
||||
exclusiveTokenWarning: 'Cette plateforme utilise un verrou de jeton exclusif. Chaque profil doit utiliser un jeton d\'identite different pour eviter les conflits avec les autres profils.',
|
||||
botToken: 'Jeton de bot',
|
||||
botTokenHint: 'Jeton de bot depuis le portail developpeur',
|
||||
accessToken: 'Jeton d\'acces',
|
||||
|
||||
@@ -335,6 +335,10 @@ export default {
|
||||
newName: '新しい名前',
|
||||
newNamePlaceholder: '新しい名前を入力',
|
||||
cloneFromCurrent: '現在のプロファイルから複製',
|
||||
cloneCleanupNotice: '複製時、独占型プラットフォーム認証情報(Weixin / Telegram / Slack など)は自動的にスキップされ、ソースプロファイルとの競合を回避します',
|
||||
cloneStrippedCredentials: '{count} 件の独占認証情報を削除しました:{list}',
|
||||
cloneDisabledPlatforms: '{count} 個のプラットフォームを無効化しました:{list}',
|
||||
cloneStrippedConfigCredentials: 'config.yaml から {count} 件の埋め込み認証情報を削除しました:{list}',
|
||||
archivePath: 'アーカイブパス',
|
||||
archivePathPlaceholder: 'アーカイブファイルのサーバーパス',
|
||||
importName: 'プロファイル名(任意)',
|
||||
@@ -480,6 +484,7 @@ export default {
|
||||
ignoredChannelsHint: 'ボットが応答しないチャンネル ID(カンマ区切り)',
|
||||
noThreadChannels: 'スレッドなしチャンネル',
|
||||
noThreadChannelsHint: 'スレッドなしで応答するチャンネル ID(カンマ区切り)',
|
||||
exclusiveTokenWarning: 'このプラットフォームは排他的トークンロックを使用します。各プロファイルは他のプロファイルと競合しないように、異なる ID トークンを使用する必要があります。',
|
||||
botToken: 'ボットトークン',
|
||||
botTokenHint: '開発者ポータルから取得したボットトークン',
|
||||
accessToken: 'アクセストークン',
|
||||
|
||||
@@ -335,6 +335,10 @@ export default {
|
||||
newName: '새 이름',
|
||||
newNamePlaceholder: '새 이름을 입력하세요',
|
||||
cloneFromCurrent: '현재 프로필에서 복제',
|
||||
cloneCleanupNotice: '복제 시 독점형 플랫폼 자격 증명(Weixin / Telegram / Slack 등)은 자동으로 건너뛰어 원본 프로필과의 충돌을 방지합니다',
|
||||
cloneStrippedCredentials: '독점 자격 증명 {count}개 제거됨: {list}',
|
||||
cloneDisabledPlatforms: '플랫폼 {count}개 비활성화됨: {list}',
|
||||
cloneStrippedConfigCredentials: 'config.yaml에서 임베디드 자격 증명 {count}개 제거됨: {list}',
|
||||
archivePath: '아카이브 경로',
|
||||
archivePathPlaceholder: '아카이브 파일의 서버 경로',
|
||||
importName: '프로필 이름 (선택)',
|
||||
@@ -480,6 +484,7 @@ export default {
|
||||
ignoredChannelsHint: '봇이 응답하지 않는 채널 ID (쉼표로 구분)',
|
||||
noThreadChannels: '스레드 없는 채널',
|
||||
noThreadChannelsHint: '스레드 없이 응답할 채널 ID (쉼표로 구분)',
|
||||
exclusiveTokenWarning: '이 플랫폼은 독점 토큰 잠금을 사용합니다. 각 프로필은 다른 프로필과 충돌하지 않도록 서로 다른 ID 토큰을 사용해야 합니다.',
|
||||
botToken: 'Bot Token',
|
||||
botTokenHint: '개발자 포털에서 발급받은 Bot Token',
|
||||
accessToken: 'Access Token',
|
||||
|
||||
@@ -335,6 +335,10 @@ export default {
|
||||
newName: 'Novo nome',
|
||||
newNamePlaceholder: 'Digite um novo nome',
|
||||
cloneFromCurrent: 'Clonar do perfil atual',
|
||||
cloneCleanupNotice: 'Ao clonar, as credenciais exclusivas de plataforma (Weixin / Telegram / Slack, etc.) são automaticamente ignoradas para evitar conflitos com o perfil de origem',
|
||||
cloneStrippedCredentials: '{count} credencial(is) exclusiva(s) removida(s): {list}',
|
||||
cloneDisabledPlatforms: '{count} plataforma(s) desabilitada(s): {list}',
|
||||
cloneStrippedConfigCredentials: '{count} credencial(is) incorporada(s) removida(s) do config.yaml: {list}',
|
||||
archivePath: 'Caminho do arquivo',
|
||||
archivePathPlaceholder: 'Caminho do servidor para o arquivo',
|
||||
importName: 'Nome do perfil (opcional)',
|
||||
@@ -480,6 +484,7 @@ export default {
|
||||
ignoredChannelsHint: 'Canais onde o bot nunca responde (separados por virgula)',
|
||||
noThreadChannels: 'Canais sem thread',
|
||||
noThreadChannelsHint: 'Canais onde o bot responde sem threads (separados por virgula)',
|
||||
exclusiveTokenWarning: 'Esta plataforma usa bloqueio exclusivo de token. Cada perfil deve usar um token de identidade diferente para evitar conflitos com outros perfis.',
|
||||
botToken: 'Token do bot',
|
||||
botTokenHint: 'Token do bot do portal do desenvolvedor',
|
||||
accessToken: 'Token de acesso',
|
||||
|
||||
@@ -359,6 +359,10 @@ export default {
|
||||
newName: '新名称',
|
||||
newNamePlaceholder: '输入新名称',
|
||||
cloneFromCurrent: '从当前配置克隆',
|
||||
cloneCleanupNotice: '克隆时会自动跳过独占型平台凭据(Weixin / Telegram / Slack 等),避免与源配置冲突',
|
||||
cloneStrippedCredentials: '已清理 {count} 项独占凭据:{list}',
|
||||
cloneDisabledPlatforms: '已禁用 {count} 个平台:{list}',
|
||||
cloneStrippedConfigCredentials: '已清理 config.yaml 中 {count} 项内嵌凭据:{list}',
|
||||
archivePath: '归档路径',
|
||||
archivePathPlaceholder: '归档文件的服务器路径',
|
||||
importName: '配置名称(可选)',
|
||||
@@ -513,6 +517,7 @@ export default {
|
||||
ignoredChannelsHint: '不响应的频道 ID(逗号分隔)',
|
||||
noThreadChannels: '无线程频道',
|
||||
noThreadChannelsHint: '不创建线程的频道 ID(逗号分隔)',
|
||||
exclusiveTokenWarning: '此平台使用独占 token 锁。每个 profile 必须使用不同的身份 token,否则会与其他 profile 冲突导致 gateway 启动失败。',
|
||||
botToken: 'Bot Token',
|
||||
botTokenHint: '开发者门户获取的 Bot Token',
|
||||
accessToken: 'Access Token',
|
||||
|
||||
Reference in New Issue
Block a user