feat: Hermes Agent 多引擎架构核心代码

- 新增 src/engines/hermes/ 完整引擎(仪表盘/服务管理/模型配置/Agent管理/对话)
- 新增 src/lib/engine-manager.js 引擎管理器(切换/检测/状态)
- 新增 src-tauri/src/commands/hermes.rs 后端命令(Gateway控制/配置读写/Agent Run SSE)
- sidebar 引擎切换器 UI
- i18n 新增 engine 模块(中/英/繁体)
- 多安装清理工具(gateway-ownership.js)
- 晴辰助手文件访问开关
- Hermes 对话工具调用可视化、SSE 流式输出
- Cargo.lock / dev-api.js 同步更新
This commit is contained in:
晴天
2026-04-13 04:09:00 +08:00
parent 32190c8f27
commit 5575566806
36 changed files with 6694 additions and 424 deletions

View File

@@ -8,6 +8,7 @@ import { showUpgradeModal, showConfirm } from '../components/modal.js'
import { setUpgrading } from '../lib/app-state.js'
import { icon, statusIcon } from '../lib/icons.js'
import { t, getLang } from '../lib/i18n.js'
import { getActiveEngineId } from '../lib/engine-manager.js'
export async function render() {
const page = document.createElement('div')
@@ -52,7 +53,11 @@ export async function render() {
</div>
`
loadData(page)
if (getActiveEngineId() === 'hermes') {
loadHermesData(page)
} else {
loadData(page)
}
renderCommunity(page)
renderProjects(page)
renderContribute(page)
@@ -61,6 +66,61 @@ export async function render() {
return page
}
async function loadHermesData(page) {
const cards = page.querySelector('#version-cards')
try {
const [hermesInfo, pythonInfo] = await Promise.all([
api.checkHermes().catch(() => null),
api.checkPython().catch(() => null),
])
let panelVersion = typeof __APP_VERSION__ !== 'undefined' ? __APP_VERSION__ : '0.1.0'
try {
const { getVersion } = await import('@tauri-apps/api/app')
panelVersion = await getVersion()
} catch {}
let panelUpdateHtml = `<span style="color:var(--text-tertiary)">${t('about.checkingUpdate')}</span>`
checkHotUpdate(cards, panelVersion)
const installed = !!hermesInfo?.installed
const gwRunning = !!hermesInfo?.gatewayRunning
const version = hermesInfo?.hermesVersion || hermesInfo?.version || ''
const model = hermesInfo?.model || ''
const port = hermesInfo?.gatewayPort || 8642
const pyVer = pythonInfo?.version || ''
const pyPath = pythonInfo?.path || ''
const esc = s => String(s || '').replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;')
cards.innerHTML = `
<div class="stat-card">
<div class="stat-card-header"><span class="stat-card-label">ClawPanel</span></div>
<div class="stat-card-value">${panelVersion}</div>
<div class="stat-card-meta" id="panel-update-meta" style="display:flex;align-items:center;gap:8px">${panelUpdateHtml}</div>
</div>
<div class="stat-card">
<div class="stat-card-header"><span class="stat-card-label">Hermes Agent</span></div>
<div class="stat-card-value">${installed ? (version || t('about.installed')) : t('about.notInstalled')}</div>
<div class="stat-card-meta" style="display:flex;align-items:center;gap:8px;flex-wrap:wrap">
${gwRunning
? `<span style="color:var(--success)">● Gateway ${t('engine.dashRunning')} · :${port}</span>`
: `<span style="color:var(--text-tertiary)">○ Gateway ${t('engine.dashStopped')}</span>`}
${model ? `<span style="color:var(--text-secondary)">${t('engine.dashModel')}: ${esc(model)}</span>` : ''}
${!installed ? `<a class="btn btn-primary btn-sm" href="#/h/setup" style="padding:2px 8px;font-size:var(--font-size-xs)">${t('about.hermesSetup')}</a>` : ''}
</div>
</div>
<div class="stat-card">
<div class="stat-card-header"><span class="stat-card-label">Python</span></div>
<div class="stat-card-value" style="font-size:var(--font-size-sm)">${pyVer || t('about.notInstalled')}</div>
<div class="stat-card-meta" style="word-break:break-all">${esc(pyPath)}</div>
</div>
`
} catch {
cards.innerHTML = `<div class="stat-card"><div class="stat-card-label">${t('common.loadFailed')}</div></div>`
}
}
async function loadData(page) {
const cards = page.querySelector('#version-cards')
try {

View File

@@ -11,6 +11,7 @@ import { OPENCLAW_KB } from '../lib/openclaw-kb.js'
import { icon, statusIcon } from '../lib/icons.js'
import { QTCOOL, PROVIDER_PRESETS, API_TYPES as SHARED_API_TYPES, fetchQtcoolModels } from '../lib/model-presets.js'
import { t } from '../lib/i18n.js'
import { getActiveEngineId } from '../lib/engine-manager.js'
// ── 常量 ──
const STORAGE_KEY = 'clawpanel-assistant'
@@ -714,6 +715,155 @@ const BUILTIN_SKILLS = [
},
]
// ── Hermes 引擎专属 Skills ──
const HERMES_SKILLS = [
{
id: 'hermes-chat-terminal',
icon: icon('terminal', 16),
name: t('assistant.skillHermesChat'),
desc: t('assistant.skillHermesChatDesc'),
tools: ['terminal'],
prompt: `请帮我在终端中启动 Hermes Agent 的交互式对话。
具体操作:
1. 调用 get_system_info 获取系统信息
2. 用 run_command 执行 \`hermes version\` 检查 Hermes 是否已安装
3. 如果已安装,告诉用户可以在终端中运行 \`hermes chat\` 开始对话
4. 如果未安装,给出安装命令:\`uv tool install "hermes-agent @ git+https://github.com/NousResearch/hermes-agent.git" --python 3.11\``,
},
{
id: 'hermes-diagnose',
icon: icon('shield', 16),
name: t('assistant.skillHermesDiagnose'),
desc: t('assistant.skillHermesDiagnoseDesc'),
tools: ['terminal', 'fileOps'],
prompt: `请帮我诊断 Hermes Agent 的运行状态。
具体操作:
1. 调用 get_system_info 获取 OS 类型和主目录
2. 用 run_command 执行 \`hermes version\` 获取版本
3. 用 run_command 执行 \`hermes doctor\` 进行自诊断
4. 用 list_processes 检查 hermes/gateway 进程是否在运行
5. 用 check_port 检查端口 8642 是否在监听
6. 用 read_file 读取 ~/.hermes/config.yaml 检查配置
7. 给出诊断结论和修复建议`,
},
{
id: 'hermes-config',
icon: icon('wrench', 16),
name: t('assistant.skillHermesConfig'),
desc: t('assistant.skillHermesConfigDesc'),
tools: ['fileOps'],
prompt: `请帮我检查 Hermes Agent 的配置文件。
具体操作:
1. 调用 get_system_info 获取系统信息,确定主目录
2. 用 list_directory 查看 ~/.hermes/ 目录结构
3. 用 read_file 读取 ~/.hermes/config.yaml
4. 用 read_file 读取 ~/.hermes/.env注意隐藏 API Key
5. 分析配置内容,检查:
- 模型配置是否正确
- API Key 和 Base URL 是否设置
- Gateway 端口配置
6. 给出配置健康度评估和改进建议`,
},
{
id: 'hermes-browse-dir',
icon: icon('folder', 16),
name: t('assistant.skillHermesBrowseDir'),
desc: t('assistant.skillHermesBrowseDirDesc'),
tools: ['fileOps'],
prompt: `请帮我浏览 Hermes Agent 的工作目录。
具体操作:
1. 调用 get_system_info 获取主目录路径
2. 用 list_directory 列出 ~/.hermes/ 根目录
3. 简要说明每个目录/文件的作用:
- config.yaml: 全局配置
- .env: 环境变量API Key、Base URL 等)
- sessions/: 对话会话记录
- skills/: Skills 目录
- logs/: 日志文件
- cron/: 定时任务配置
4. 标注关键配置文件和常用路径`,
},
{
id: 'hermes-upgrade',
icon: icon('zap', 16),
name: t('assistant.skillHermesUpgrade'),
desc: t('assistant.skillHermesUpgradeDesc'),
tools: ['terminal'],
prompt: `请帮我升级 Hermes Agent 到最新版本。
具体操作:
1. 调用 get_system_info 获取系统信息
2. 用 run_command 执行 \`hermes version\` 获取当前版本
3. 告诉用户升级命令:
\`uv tool install --reinstall "hermes-agent @ git+https://github.com/NousResearch/hermes-agent.git" --python 3.11\`
4. 提醒用户升级前先停止 Gateway\`hermes gateway stop\`
5. 升级完成后建议重新启动 Gateway`,
},
{
id: 'hermes-logs',
icon: icon('clipboard', 16),
name: t('assistant.skillHermesLogs'),
desc: t('assistant.skillHermesLogsDesc'),
tools: ['terminal', 'fileOps'],
prompt: `请帮我分析 Hermes Agent 最近的日志。
具体操作:
1. 调用 get_system_info 获取主目录路径
2. 用 list_directory 查看 ~/.hermes/ 有哪些日志文件
3. 用 read_file 读取 ~/.hermes/gateway-run.log 和 ~/.hermes/gateway-err.log
4. 搜索 ERROR、WARN、fail、exception 等关键词
5. 分析错误原因,给出具体修复建议`,
},
{
id: 'hermes-uninstall',
icon: icon('trash', 16),
name: t('assistant.skillHermesUninstall'),
desc: t('assistant.skillHermesUninstallDesc'),
tools: [],
prompt: `请告诉我如何完全卸载 Hermes Agent。
卸载步骤:
1. 停止 Gateway\`hermes gateway stop\`
2. 卸载 Hermes Agent\`uv tool uninstall hermes-agent\`
3. 可选:删除配置目录 ~/.hermes/Windows: %USERPROFILE%\\.hermes
4. 可选:卸载 uv 包管理器
请详细说明每一步,并提醒用户备份重要数据。`,
},
{
id: 'report-bug',
icon: icon('bug', 16),
name: t('assistant.skillReportBug'),
desc: t('assistant.skillReportBugDesc'),
tools: ['terminal', 'fileOps'],
prompt: `我想反馈一个 Bug请帮我整理成标准的 GitHub Issue。
具体操作:
1. 用 ask_user 工具询问我遇到了什么问题(如果我还没说的话)
2. 调用 get_system_info 获取系统环境信息
3. 用 run_command 收集hermes version、node -v 等版本信息
4. 用 read_file 读取最近的错误日志(如有)
5. 按标准 Issue 模板整理:
- **问题描述**(一句话)
- **复现步骤**1, 2, 3...
- **期望行为** / **实际行为**
- **环境信息**(自动填充)
- **相关日志**(如有)
6. 给出对应仓库的 Issue 链接:
- ClawPanel: https://github.com/qingchencloud/clawpanel/issues/new
`,
},
]
/** 根据当前引擎返回对应的技能列表 */
function getBuiltinSkills() {
return getActiveEngineId() === 'hermes' ? HERMES_SKILLS : BUILTIN_SKILLS
}
function currentMode() {
return MODES[_config?.mode] ? _config.mode : DEFAULT_MODE
}
@@ -937,13 +1087,15 @@ function buildSystemPrompt() {
// 注入内置技能列表
prompt += '\n\n## 内置技能卡片'
prompt += '\n用户可以在欢迎页点击技能卡片快速触发操作。当用户遇到问题时你也可以主动推荐合适的技能'
for (const s of BUILTIN_SKILLS) {
for (const s of getBuiltinSkills()) {
prompt += `\n- **${s.name}**${s.desc}`
}
prompt += '\n\n当用户的需求匹配某个技能时可以建议用户点击对应的技能卡片或者你直接按技能的步骤操作。'
// 注入内置 OpenClaw 知识库
prompt += '\n\n' + OPENCLAW_KB
// 注入内置知识库(仅 OpenClaw 模式)
if (getActiveEngineId() !== 'hermes') {
prompt += '\n\n' + OPENCLAW_KB
}
// 注入用户自定义知识库内容
const kbEnabled = (_config.knowledgeFiles || []).filter(f => f.enabled !== false && f.content)
@@ -2493,7 +2645,7 @@ function renderMessages() {
const session = getCurrentSession()
if (!_messagesEl) return
if (!session || session.messages.length === 0) {
const skillCards = BUILTIN_SKILLS.map(s => `
const skillCards = getBuiltinSkills().map(s => `
<button class="ast-skill-card" data-skill="${s.id}">
<span class="ast-skill-icon">${s.icon}</span>
<div class="ast-skill-info">
@@ -2623,6 +2775,7 @@ function buildTestResult({ success, elapsed, usedApi, reqUrl, reqBody, respStatu
function showSettings() {
const c = _config
const isHermes = getActiveEngineId() === 'hermes'
const overlay = document.createElement('div')
overlay.className = 'modal-overlay'
overlay.innerHTML = `
@@ -2664,7 +2817,7 @@ function showSettings() {
<div style="display:flex;gap:6px;padding-bottom:1px">
<button class="btn btn-sm btn-secondary" id="ast-btn-test" title="${t('assistant.testConnTitle')}">${t('assistant.testBtn')}</button>
<button class="btn btn-sm btn-secondary" id="ast-btn-models" title="${t('assistant.fetchModelsTitle')}">${t('assistant.fetchBtn')}</button>
<button class="btn btn-sm btn-secondary" id="ast-btn-import" title="${t('assistant.importTitle')}">${icon('download', 14)} ${t('assistant.importBtn')}</button>
${!isHermes ? `<button class="btn btn-sm btn-secondary" id="ast-btn-import" title="${t('assistant.importTitle')}">${icon('download', 14)} ${t('assistant.importBtn')}</button>` : ''}
</div>
</div>
<div id="ast-test-result" style="margin:6px 0 2px;font-size:12px;min-height:16px"></div>
@@ -2714,10 +2867,10 @@ function showSettings() {
<div id="ast-qtcool-status" style="margin-top:8px;font-size:11px;min-height:16px;line-height:1.5"></div>
</div>
<div style="border-top:1px solid var(--border-primary);padding:6px 16px;display:flex;justify-content:space-between;align-items:center;flex-wrap:wrap;gap:6px;background:var(--bg-tertiary)">
<div style="display:flex;gap:8px;align-items:center">
${!isHermes ? `<div style="display:flex;gap:8px;align-items:center">
<button class="btn btn-xs btn-secondary" id="ast-qtcool-sync-to" title="${t('assistant.qtcoolSyncToTitle')}">${icon('upload', 11)} ${t('assistant.qtcoolSyncTo')}</button>
<button class="btn btn-xs btn-secondary" id="ast-qtcool-sync-from" title="${t('assistant.qtcoolSyncFromTitle')}">${icon('download', 11)} ${t('assistant.qtcoolSyncFrom')}</button>
</div>
</div>` : '<div></div>'}
<a href="${QTCOOL.site}" target="_blank" style="color:var(--primary);text-decoration:none;font-size:11px">${icon('external-link', 11)} ${t('assistant.qtcoolLearnMore')}</a>
</div>
</div>
@@ -2755,7 +2908,7 @@ function showSettings() {
<div class="form-hint" style="margin-top:10px">${t('assistant.toolsAlwaysAvailable')}</div>
</div>
<div class="ast-tab-panel" data-panel="persona">
<div class="form-group">
${!isHermes ? `<div class="form-group">
<label class="form-label">${t('assistant.personaSource')}</label>
<div style="display:flex;flex-direction:column;gap:6px">
<label style="display:flex;align-items:center;gap:8px;cursor:pointer">
@@ -2767,8 +2920,8 @@ function showSettings() {
<span>${t('assistant.personaOpenClaw')} <span style="font-size:11px;color:var(--text-tertiary)">${t('assistant.personaOpenClawHint')}</span></span>
</label>
</div>
</div>
<div id="ast-soul-default" style="${c.soulSource?.startsWith('openclaw:') ? 'display:none' : ''}">
</div>` : ''}
<div id="ast-soul-default" style="${!isHermes && c.soulSource?.startsWith('openclaw:') ? 'display:none' : ''}">
<div class="form-group">
<label class="form-label">${t('assistant.personaName')}</label>
<input class="form-input" id="ast-name" value="${escHtml(c.assistantName || DEFAULT_NAME)}" placeholder="${DEFAULT_NAME}">
@@ -2779,7 +2932,7 @@ function showSettings() {
<div class="form-hint">${t('assistant.personaPersonalityHint')}</div>
</div>
</div>
<div id="ast-soul-openclaw" style="${c.soulSource?.startsWith('openclaw:') ? '' : 'display:none'}">
<div id="ast-soul-openclaw" style="${!isHermes && c.soulSource?.startsWith('openclaw:') ? '' : 'display:none'}">
<div class="form-group" style="margin-top:4px">
<label class="form-label">${t('assistant.personaSelectAgent')}</label>
<div style="display:flex;gap:6px;align-items:center">
@@ -3433,8 +3586,9 @@ function showSettings() {
}
}
// 从 OpenClaw 导入模型配置
overlay.querySelector('#ast-btn-import').onclick = async (e) => {
// 从 OpenClaw 导入模型配置Hermes 模式下该按钮不存在)
const importBtn = overlay.querySelector('#ast-btn-import')
if (importBtn) importBtn.onclick = async (e) => {
const btn = e.target
btn.disabled = true
btn.textContent = t('assistant.personaScanning')
@@ -4421,7 +4575,7 @@ export async function render() {
_messagesEl.addEventListener('click', (e) => {
const skillCard = e.target.closest('.ast-skill-card')
if (skillCard) {
const skill = BUILTIN_SKILLS.find(s => s.id === skillCard.dataset.skill)
const skill = getBuiltinSkills().find(s => s.id === skillCard.dataset.skill)
if (!skill) return
// 技能需要工具 → 自动切换到执行模式(如果当前是聊天模式)

View File

@@ -2841,6 +2841,7 @@ function appendSystemMessage(text) {
}
function clearMessages() {
if (!_messagesEl) return
_messagesEl.querySelectorAll('.msg').forEach(m => m.remove())
_autoScrollEnabled = true
_lastScrollTop = 0

View File

@@ -4,7 +4,7 @@
import { api } from '../lib/tauri-api.js'
import { toast } from '../components/toast.js'
import { getActiveInstance, onGatewayChange } from '../lib/app-state.js'
import { isForeignGatewayError, isForeignGatewayService, maybeShowForeignGatewayBindingPrompt, showGatewayConflictGuidance } from '../lib/gateway-ownership.js'
import { isForeignGatewayError, isForeignGatewayService, maybeShowForeignGatewayBindingPrompt, showGatewayConflictGuidance, showInstallationCleanup } from '../lib/gateway-ownership.js'
import { navigate } from '../router.js'
import { t } from '../lib/i18n.js'
import { wsClient } from '../lib/ws-client.js'
@@ -274,11 +274,13 @@ function renderStatCards(page, services, version, agents, config, panelConfig) {
${multiInstall && !cliBound
? `<div class="stat-card-meta" style="margin-top:8px;color:var(--warning);line-height:1.6">${t('dashboard.multiInstallCardHint')}</div>
<div style="display:flex;gap:8px;flex-wrap:wrap;margin-top:10px">
<button class="btn btn-primary btn-xs" data-action="open-cleanup">${t('services.cleanupTitle')}</button>
<button class="btn btn-secondary btn-xs" data-action="resolve-multi-install">${t('dashboard.viewGuidance')}</button>
<button class="btn btn-primary btn-xs" data-action="open-settings">${t('dashboard.goSettings')}</button>
<button class="btn btn-secondary btn-xs" data-action="open-settings">${t('dashboard.goSettings')}</button>
</div>`
: multiInstall && cliBound
? `<div class="stat-card-meta" style="margin-top:4px;color:var(--text-tertiary);font-size:11px">✓ ${t('dashboard.multiInstallBoundOk', { count: installCount })}</div>`
? `<div class="stat-card-meta" style="margin-top:4px;color:var(--text-tertiary);font-size:11px">✓ ${t('dashboard.multiInstallBoundOk', { count: installCount })}</div>
<div style="margin-top:6px"><button class="btn btn-secondary btn-xs" data-action="open-cleanup">${t('services.cleanupTitle')}</button></div>`
: ''}
</div>
<div class="stat-card">
@@ -604,6 +606,11 @@ function bindActions(page) {
return
}
if (action === 'open-cleanup') {
await showInstallationCleanup({ onRefresh: () => loadDashboardData(page, true) })
return
}
if (action === 'resolve-foreign-gateway') {
await openGatewayConflict(page, null, 'foreign-gateway')
return

View File

@@ -6,6 +6,7 @@ import { api, invalidate } from '../lib/tauri-api.js'
import { showConfirm, showUpgradeModal } from '../components/modal.js'
import { toast } from '../components/toast.js'
import { setUpgrading, isMacPlatform } from '../lib/app-state.js'
import { getActiveEngine } from '../lib/engine-manager.js'
import { diagnoseInstallError } from '../lib/error-diagnosis.js'
import { icon, statusIcon } from '../lib/icons.js'
import { t } from '../lib/i18n.js'
@@ -582,19 +583,16 @@ function bindEvents(page, nodeOk, detectState) {
window.location.hash = '/assistant'
})
// 进入面板
page.querySelector('#btn-enter')?.addEventListener('click', () => {
window.location.hash = '/dashboard'
})
page.querySelector('#btn-goto-models')?.addEventListener('click', () => {
window.location.hash = '/models'
})
page.querySelector('#btn-goto-gateway')?.addEventListener('click', () => {
window.location.hash = '/gateway'
})
page.querySelector('#btn-goto-channels')?.addEventListener('click', () => {
window.location.hash = '/channels'
})
// 进入面板(刷新引擎 ready 状态,触发侧边栏更新)
async function refreshAndNavigate(route) {
const engine = getActiveEngine()
if (engine?.detect) await engine.detect()
window.location.hash = route
}
page.querySelector('#btn-enter')?.addEventListener('click', () => refreshAndNavigate('/dashboard'))
page.querySelector('#btn-goto-models')?.addEventListener('click', () => refreshAndNavigate('/models'))
page.querySelector('#btn-goto-gateway')?.addEventListener('click', () => refreshAndNavigate('/gateway'))
page.querySelector('#btn-goto-channels')?.addEventListener('click', () => refreshAndNavigate('/channels'))
// 一键安装 Git
page.querySelector('#btn-auto-install-git')?.addEventListener('click', async () => {