2ae7e7ad1b
* 修复: 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>
124 lines
2.8 KiB
Vue
124 lines
2.8 KiB
Vue
<script setup lang="ts">
|
|
import { ref, computed } from 'vue'
|
|
import { NTag, NAlert } from 'naive-ui'
|
|
import { useI18n } from 'vue-i18n'
|
|
|
|
const props = defineProps<{
|
|
name: string
|
|
icon: string
|
|
config: Record<string, any>
|
|
credentials?: Record<string, any>
|
|
exclusive?: boolean
|
|
}>()
|
|
|
|
const expanded = ref(true)
|
|
const { t } = useI18n()
|
|
|
|
const configured = computed(() => {
|
|
const creds = props.credentials
|
|
if (!creds) return false
|
|
const keys = ['token', 'api_key', 'app_id', 'client_id', 'secret', 'app_secret', 'client_secret', 'access_token', 'bot_id', 'account_id', 'enabled']
|
|
// Check top-level and nested extra.*
|
|
const targets = [creds, creds.extra].filter(Boolean)
|
|
return targets.some(obj =>
|
|
keys.some(key => {
|
|
const val = (obj as Record<string, any>)[key]
|
|
return val !== undefined && val !== null && val !== '' && val !== false
|
|
})
|
|
)
|
|
})
|
|
</script>
|
|
|
|
<template>
|
|
<div class="platform-card" :class="{ configured }">
|
|
<div class="platform-card-header" @click="expanded = !expanded">
|
|
<div class="platform-info">
|
|
<span class="platform-icon" v-html="icon" />
|
|
<span class="platform-name">{{ name }}</span>
|
|
<NTag :type="configured ? 'success' : 'default'" size="small" round>
|
|
{{ configured ? t('common.configured') : t('common.notConfigured') }}
|
|
</NTag>
|
|
</div>
|
|
<span class="expand-icon" :class="{ expanded }">▾</span>
|
|
</div>
|
|
<div v-if="expanded" class="platform-card-body">
|
|
<NAlert v-if="exclusive" type="warning" :show-icon="true" class="exclusive-alert">
|
|
{{ t('platform.exclusiveTokenWarning') }}
|
|
</NAlert>
|
|
<slot />
|
|
</div>
|
|
</div>
|
|
</template>
|
|
|
|
<style scoped lang="scss">
|
|
@use '@/styles/variables' as *;
|
|
|
|
.platform-card {
|
|
background-color: $bg-card;
|
|
border: 1px solid $border-color;
|
|
border-radius: $radius-md;
|
|
margin-bottom: 12px;
|
|
overflow: hidden;
|
|
|
|
&.configured {
|
|
border-color: rgba(var(--success-rgb), 0.2);
|
|
}
|
|
}
|
|
|
|
.platform-card-header {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: space-between;
|
|
padding: 12px 16px;
|
|
cursor: pointer;
|
|
user-select: none;
|
|
|
|
&:hover {
|
|
background-color: rgba(var(--text-primary-rgb), 0.03);
|
|
}
|
|
}
|
|
|
|
.platform-info {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 10px;
|
|
}
|
|
|
|
.platform-icon {
|
|
width: 18px;
|
|
height: 18px;
|
|
color: $text-secondary;
|
|
flex-shrink: 0;
|
|
}
|
|
|
|
.platform-name {
|
|
font-size: 14px;
|
|
font-weight: 500;
|
|
color: $text-primary;
|
|
}
|
|
|
|
.expand-icon {
|
|
font-size: 12px;
|
|
color: $text-muted;
|
|
transition: transform 0.2s;
|
|
|
|
&.expanded {
|
|
transform: rotate(0deg);
|
|
}
|
|
|
|
&:not(.expanded) {
|
|
transform: rotate(-90deg);
|
|
}
|
|
}
|
|
|
|
.platform-card-body {
|
|
padding: 0 16px 12px;
|
|
border-top: 1px solid $border-light;
|
|
}
|
|
|
|
.exclusive-alert {
|
|
margin: 12px 0 4px;
|
|
font-size: 12px;
|
|
}
|
|
</style>
|