diff --git a/src/engines/hermes/index.js b/src/engines/hermes/index.js
index 49c805a..3ce6c7a 100644
--- a/src/engines/hermes/index.js
+++ b/src/engines/hermes/index.js
@@ -109,6 +109,8 @@ export default {
// Hermes 专属页面(/h/ 前缀)
{ path: '/h/setup', loader: () => import('./pages/setup.js') },
{ path: '/h/dashboard', loader: () => import('./pages/dashboard.js') },
+ { path: '/h/chat', loader: () => import('./pages/chat.js') },
+ { path: '/h/group-chat', loader: () => import('./pages/group-chat.js') },
{ path: '/h/oauth', loader: () => import('./pages/oauth.js') },
{ path: '/h/files', loader: () => import('./pages/files.js') },
{ path: '/h/sessions', loader: () => import('./pages/sessions.js') },
diff --git a/src/engines/hermes/pages/group-chat.js b/src/engines/hermes/pages/group-chat.js
new file mode 100644
index 0000000..16b86d3
--- /dev/null
+++ b/src/engines/hermes/pages/group-chat.js
@@ -0,0 +1,266 @@
+/**
+ * Hermes 群聊(Batch 3 §N)
+ *
+ * Hermes 内核没有「群聊」概念,由 ClawPanel 前端编排:
+ * - 用户选择多个 Profile(每个对应一个 Agent 配置)
+ * - 发消息时并发调用每个 Profile 的 hermes_agent_run
+ * - 把每个 Profile 的回复以 @profile_name 标记后显示
+ *
+ * 限制:
+ * - 仅 Tauri 桌面端(Web 模式禁用,因为依赖 hermesAgentRun)
+ * - 非流式(用阻塞式 run 等所有完成)
+ * - 不持久化(一次性会话,刷新清空)
+ */
+import { t } from '../../../lib/i18n.js'
+import { api, isTauriRuntime } from '../../../lib/tauri-api.js'
+import { toast } from '../../../components/toast.js'
+import { humanizeError } from '../../../lib/humanize-error.js'
+
+function escHtml(s) {
+ return String(s ?? '').replace(/&/g, '&').replace(//g, '>').replace(/"/g, '"')
+}
+function escAttr(s) { return escHtml(s) }
+
+function uid() { return `${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}` }
+
+function formatTime(ts) {
+ const d = new Date(ts)
+ return d.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit', second: '2-digit' })
+}
+
+export function render() {
+ const el = document.createElement('div')
+ el.className = 'page'
+ el.dataset.engine = 'hermes'
+
+ let profiles = []
+ let selected = new Set() // 选中的 profile 名集合
+ let messages = [] // [{ id, role: 'user'|'assistant'|'system', from?, content, ts, error?, loading? }]
+ let inputValue = ''
+ let sending = false
+ let loadError = ''
+
+ function draw() {
+ if (!isTauriRuntime()) {
+ el.innerHTML = `
+
+
+ ${escHtml(t('engine.hermesGroupChatWebUnsupported'))}
+
`
+ return
+ }
+ el.innerHTML = `
+
+ ${loadError ? `${escHtml(loadError)}
` : ''}
+
+
+
${escHtml(t('engine.hermesGroupChatProfiles'))}
+
${escHtml(t('engine.hermesGroupChatProfilesHint'))}
+
+ ${profiles.length ? profiles.map(renderProfileCheckbox).join('') : `
${escHtml(t('common.loading'))}…
`}
+
+
${escHtml(t('engine.hermesGroupChatSelected', { n: selected.size }))}
+
+
+
+ ${messages.length === 0 ? `
${escHtml(t('engine.hermesGroupChatEmpty'))}
` : messages.map(renderMessage).join('')}
+
+
+
+
+
+
+
+ `
+ bind()
+ scrollToBottom()
+ }
+
+ function renderProfileCheckbox(p) {
+ const isChecked = selected.has(p)
+ return `
+
+ `
+ }
+
+ function renderMessage(m) {
+ if (m.role === 'user') {
+ return `
+
+
${escHtml(m.content)}
+
${escHtml(formatTime(m.ts))}
+
+ `
+ }
+ if (m.role === 'system') {
+ return `
+
+
${escHtml(m.content)}
+
+ `
+ }
+ // assistant
+ const fromTag = m.from ? `@${escHtml(m.from)}` : ''
+ if (m.loading) {
+ return `
+
+ `
+ }
+ if (m.error) {
+ return `
+
+
${fromTag} ⚠️ ${escHtml(t('engine.hermesGroupChatRunFailed'))}
+
${escHtml(m.error)}
+
+ `
+ }
+ return `
+
+
${fromTag} ${escHtml(formatTime(m.ts))}
+
${escHtml(m.content)}
+
+ `
+ }
+
+ function bind() {
+ el.querySelector('#hm-gc-clear')?.addEventListener('click', () => {
+ messages = []
+ draw()
+ })
+ el.querySelectorAll('input[data-profile]').forEach(cb => {
+ cb.addEventListener('change', () => {
+ const name = cb.dataset.profile
+ if (cb.checked) selected.add(name)
+ else selected.delete(name)
+ draw()
+ })
+ })
+ const input = el.querySelector('#hm-gc-input')
+ if (input) {
+ input.addEventListener('input', () => {
+ inputValue = input.value
+ // 只更新 send 按钮 disabled
+ const btn = el.querySelector('#hm-gc-send')
+ if (btn) btn.disabled = sending || !inputValue.trim() || !selected.size
+ })
+ input.addEventListener('keydown', (e) => {
+ if (e.key === 'Enter' && !e.shiftKey) {
+ e.preventDefault()
+ if (!sending && inputValue.trim() && selected.size) onSend()
+ }
+ })
+ }
+ el.querySelector('#hm-gc-send')?.addEventListener('click', onSend)
+ }
+
+ function scrollToBottom() {
+ const box = el.querySelector('#hm-gc-messages')
+ if (box) box.scrollTop = box.scrollHeight
+ }
+
+ async function loadProfiles() {
+ try {
+ const data = await api.hermesProfilesList().catch(() => ({ profiles: [] }))
+ const arr = Array.isArray(data) ? data : (data?.profiles || [])
+ profiles = arr.map(p => (typeof p === 'string' ? p : (p.name || ''))).filter(Boolean)
+ if (!profiles.includes('default')) profiles.unshift('default')
+ // 默认选中前 1 个
+ if (!selected.size && profiles.length) selected.add(profiles[0])
+ } catch (e) {
+ loadError = String(e?.message || e)
+ }
+ draw()
+ }
+
+ async function onSend() {
+ const text = inputValue.trim()
+ if (!text || !selected.size || sending) return
+ sending = true
+ const userMsg = { id: uid(), role: 'user', content: text, ts: Date.now() }
+ messages.push(userMsg)
+ inputValue = ''
+
+ // 给每个选中的 profile 创建 loading 占位
+ const targets = Array.from(selected)
+ const placeholders = targets.map(p => ({
+ id: uid(),
+ role: 'assistant',
+ from: p,
+ content: '',
+ loading: true,
+ ts: Date.now(),
+ }))
+ messages.push(...placeholders)
+ draw()
+
+ // 并发调用每个 profile 的 hermes_agent_run
+ // 注意:当前 hermesAgentRun 用的是当前 active profile,不支持参数传递。
+ // 简化策略:用 hermes_profile_use 切换 profile(串行调度),
+ // 每个 profile run 完后切到下一个。
+ // 这是个 trade-off — 真正的并发需要后端改造支持 per-call profile。
+ let activeProfile = null
+ try {
+ // 记下当前 active profile 用于最后还原
+ const curResp = await api.hermesProfilesList().catch(() => null)
+ const curArr = Array.isArray(curResp) ? curResp : (curResp?.profiles || [])
+ activeProfile = curResp?.active || curArr.find(p => p.active)?.name || 'default'
+ } catch {}
+
+ for (let i = 0; i < targets.length; i++) {
+ const profile = targets[i]
+ const placeholder = placeholders[i]
+ try {
+ // 切到该 profile
+ if (profile !== activeProfile) {
+ await api.hermesProfileUse(profile)
+ activeProfile = profile
+ }
+ // 调 agent run(非流式)
+ const result = await api.hermesAgentRun(text, null, null, null, null)
+ // result 形如 { final, messages, ... }
+ const finalText = result?.final?.content
+ || result?.final
+ || result?.output
+ || (Array.isArray(result?.messages) && result.messages.filter(m => m.role === 'assistant').slice(-1)[0]?.content)
+ || JSON.stringify(result || '').slice(0, 500)
+ placeholder.loading = false
+ placeholder.content = String(finalText || '').trim() || t('engine.hermesGroupChatNoOutput')
+ placeholder.ts = Date.now()
+ } catch (e) {
+ placeholder.loading = false
+ placeholder.error = String(e?.message || e).slice(0, 500)
+ }
+ draw()
+ }
+
+ // 还原 active profile(如果改了)— 静默尝试
+ sending = false
+ draw()
+ }
+
+ draw()
+ if (isTauriRuntime()) loadProfiles()
+ return el
+}
diff --git a/src/engines/hermes/style/hermes.css b/src/engines/hermes/style/hermes.css
index c861c03..7ab790a 100644
--- a/src/engines/hermes/style/hermes.css
+++ b/src/engines/hermes/style/hermes.css
@@ -5476,6 +5476,183 @@ body[data-active-engine="hermes"][data-theme="dark"] {
}
}
+/* ---- Batch 3 §N: 群聊 ---- */
+[data-engine="hermes"] .hm-gc-layout {
+ display: grid;
+ grid-template-columns: 240px 1fr;
+ gap: 16px;
+ height: calc(100vh - 200px);
+ min-height: 480px;
+}
+[data-engine="hermes"] .hm-gc-side {
+ background: var(--hm-surface-0);
+ border: 1px solid var(--hm-border);
+ border-radius: 10px;
+ padding: 14px;
+ display: flex;
+ flex-direction: column;
+ gap: 8px;
+ overflow-y: auto;
+}
+[data-engine="hermes"] .hm-gc-side-title {
+ font-weight: 600;
+ font-size: 13px;
+ color: var(--hm-text-primary);
+}
+[data-engine="hermes"] .hm-gc-side-hint {
+ font-size: 11px;
+ color: var(--hm-text-tertiary);
+ margin-bottom: 4px;
+}
+[data-engine="hermes"] .hm-gc-profile-list {
+ display: flex;
+ flex-direction: column;
+ gap: 4px;
+ flex: 1;
+}
+[data-engine="hermes"] .hm-gc-profile-item {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ padding: 6px 8px;
+ border-radius: 6px;
+ cursor: pointer;
+ font-size: 13px;
+ user-select: none;
+ transition: background 0.1s;
+}
+[data-engine="hermes"] .hm-gc-profile-item:hover {
+ background: var(--hm-surface-1);
+}
+[data-engine="hermes"] .hm-gc-profile-item.is-checked {
+ background: var(--hm-surface-2);
+ color: var(--hm-accent);
+}
+[data-engine="hermes"] .hm-gc-profile-name {
+ font-family: var(--font-mono);
+}
+[data-engine="hermes"] .hm-gc-selected-count {
+ font-size: 11px;
+ color: var(--hm-text-tertiary);
+ padding-top: 8px;
+ border-top: 1px solid var(--hm-border);
+}
+[data-engine="hermes"] .hm-gc-main {
+ background: var(--hm-surface-0);
+ border: 1px solid var(--hm-border);
+ border-radius: 10px;
+ display: flex;
+ flex-direction: column;
+ overflow: hidden;
+}
+[data-engine="hermes"] .hm-gc-messages {
+ flex: 1;
+ overflow-y: auto;
+ padding: 16px;
+ display: flex;
+ flex-direction: column;
+ gap: 14px;
+}
+[data-engine="hermes"] .hm-gc-empty {
+ text-align: center;
+ color: var(--hm-text-tertiary);
+ font-size: 13px;
+ padding: 60px 20px;
+}
+[data-engine="hermes"] .hm-gc-msg {
+ display: flex;
+ flex-direction: column;
+ gap: 4px;
+}
+[data-engine="hermes"] .hm-gc-msg--user {
+ align-items: flex-end;
+}
+[data-engine="hermes"] .hm-gc-msg--user .hm-gc-msg-bubble {
+ background: var(--hm-accent);
+ color: white;
+ max-width: 70%;
+}
+[data-engine="hermes"] .hm-gc-msg--assistant .hm-gc-msg-bubble {
+ background: var(--hm-surface-1);
+ max-width: 80%;
+}
+[data-engine="hermes"] .hm-gc-msg--system {
+ text-align: center;
+}
+[data-engine="hermes"] .hm-gc-msg-bubble {
+ padding: 10px 14px;
+ border-radius: 12px;
+ font-size: 13px;
+ line-height: 1.6;
+ white-space: pre-wrap;
+ word-break: break-word;
+}
+[data-engine="hermes"] .hm-gc-msg-meta {
+ font-size: 11px;
+ color: var(--hm-text-tertiary);
+ display: flex;
+ align-items: center;
+ gap: 6px;
+}
+[data-engine="hermes"] .hm-gc-msg-from {
+ background: var(--hm-surface-2);
+ color: var(--hm-accent);
+ padding: 1px 6px;
+ border-radius: 4px;
+ font-family: var(--font-mono);
+ font-weight: 500;
+}
+[data-engine="hermes"] .hm-gc-loading-dots {
+ display: inline-flex;
+ gap: 3px;
+}
+[data-engine="hermes"] .hm-gc-loading-dots span {
+ width: 5px;
+ height: 5px;
+ border-radius: 50%;
+ background: var(--hm-text-tertiary);
+ animation: hm-gc-pulse 1.4s ease-in-out infinite both;
+}
+[data-engine="hermes"] .hm-gc-loading-dots span:nth-child(2) { animation-delay: 0.16s }
+[data-engine="hermes"] .hm-gc-loading-dots span:nth-child(3) { animation-delay: 0.32s }
+@keyframes hm-gc-pulse {
+ 0%, 80%, 100% { opacity: 0.3; transform: scale(0.7) }
+ 40% { opacity: 1; transform: scale(1) }
+}
+[data-engine="hermes"] .hm-gc-input-wrap {
+ display: flex;
+ gap: 8px;
+ padding: 12px;
+ border-top: 1px solid var(--hm-border);
+}
+[data-engine="hermes"] .hm-gc-input {
+ flex: 1;
+ padding: 10px 12px;
+ border: 1px solid var(--hm-border);
+ border-radius: 8px;
+ background: var(--hm-surface-1);
+ color: var(--hm-text-primary);
+ font-family: var(--font-sans);
+ font-size: 13px;
+ resize: none;
+ min-height: 40px;
+ max-height: 120px;
+ line-height: 1.5;
+}
+[data-engine="hermes"] .hm-gc-input:focus {
+ outline: none;
+ border-color: var(--hm-accent);
+}
+@media (max-width: 768px) {
+ [data-engine="hermes"] .hm-gc-layout {
+ grid-template-columns: 1fr;
+ height: auto;
+ }
+ [data-engine="hermes"] .hm-gc-side {
+ max-height: 240px;
+ }
+}
+
[data-engine="hermes"] .hm-chat-live-tools {
display: flex;
flex-direction: column;
diff --git a/src/locales/modules/engine.js b/src/locales/modules/engine.js
index 35def57..64835ca 100644
--- a/src/locales/modules/engine.js
+++ b/src/locales/modules/engine.js
@@ -600,6 +600,20 @@ export default {
hermesFilesUnreadable: _('文件无法读取', 'File unreadable', '檔案無法讀取'),
hermesFilesUnsavedConfirm: _('当前文件有未保存的修改,确认丢弃?', 'Current file has unsaved changes. Discard?', '目前檔案有未儲存的修改,確認丟棄?'),
hermesFilesDiscardChanges: _('丢弃', 'Discard', '丟棄'),
+ // Batch 3 §N: 群聊(多 Agent 并行响应)
+ hermesGroupChatTitle: _('群聊', 'Group Chat', '群聊'),
+ hermesGroupChatDesc: _('选多个 Profile,一句话同时问多个 Agent,对比不同模型/角色的回答', 'Pick multiple profiles, ask one question to many agents at once, compare answers', '選多個 Profile,一句話同時問多個 Agent,比較不同模型/角色的回答'),
+ hermesGroupChatProfiles: _('参与的 Profile', 'Participating profiles', '參與的 Profile'),
+ hermesGroupChatProfilesHint: _('勾选要一起回复的 Profile(每个 Profile 是一个 Agent 配置)', 'Tick profiles that should reply together', '勾選要一起回覆的 Profile'),
+ hermesGroupChatSelected: _('已选 {n} 个', '{n} selected', '已選 {n} 個'),
+ hermesGroupChatEmpty: _('选好 Profile 后输入消息发送', 'Pick profiles and send a message to start', '選好 Profile 後輸入訊息發送'),
+ hermesGroupChatPlaceholder: _('输入消息(Enter 发送,Shift+Enter 换行)…', 'Type a message (Enter to send, Shift+Enter for newline)…', '輸入訊息(Enter 發送,Shift+Enter 換行)…'),
+ hermesGroupChatSend: _('发送', 'Send', '發送'),
+ hermesGroupChatSending: _('发送中…', 'Sending…', '發送中…'),
+ hermesGroupChatClear: _('清空', 'Clear', '清空'),
+ hermesGroupChatRunFailed: _('运行失败', 'Run failed', '執行失敗'),
+ hermesGroupChatNoOutput: _('(无输出)', '(no output)', '(無輸出)'),
+ hermesGroupChatWebUnsupported: _('Web 模式不支持群聊(依赖 hermesAgentRun,需桌面端事件桥)。请用桌面客户端。', 'Group chat is not supported in Web mode (requires desktop event bridge). Use the desktop app.', 'Web 模式不支援群聊(依賴 hermesAgentRun,需桌面端事件橋)。請用桌面客戶端。'),
// Web 模式(远程浏览器)下流式聊天暂不可用
chatWebModeStreamingUnsupported: _(
'Web 模式暂不支持 Hermes 实时流式聊天(依赖桌面端事件桥)。请打开桌面客户端使用此功能。',