mirror of
https://github.com/qingchencloud/clawpanel.git
synced 2026-05-29 04:10:00 +08:00
feat(hermes): Batch 3 §N - 群聊(多 Agent 并行响应)
Hermes 内核没有「群聊」概念,由 ClawPanel 前端编排:
- 用户选 1-N 个 Profile(每个对应一个 Agent 配置)
- 发消息 → 串行调度(用 hermes_profile_use 切换 + hermesAgentRun 阻塞调用)
- 每个 Profile 的回复用 @profile_name 标记后显示在统一界面
## 新页面 /h/group-chat(~250 行)
### UI 布局
- 左侧 240px: Profile 多选列表
· 顶部说明 + 已选 N 个统计
· checkbox 风格,is-checked 高亮
- 右侧主区: 消息列表 + 输入框
· user 消息右对齐(accent 蓝色气泡)
· assistant 消息左对齐(每条带 @profile 徽章)
· loading 状态用三点跳动动画
· error 状态红色 + ⚠️ 图标
· 输入框 Enter 发送 / Shift+Enter 换行
### 调度策略
- 当前实现:串行(hermes_profile_use 切换 + hermesAgentRun)
· 简单稳健,避免后端并发对同一 profile 状态的竞态
· 缺点:N 个 profile 总耗时 = N × 单次耗时
- 真正并发需后端支持 hermes_agent_run(profile=...) 参数
· 留作下次内核改造(设计稿已记下)
### 限制(明确告知用户)
- 仅 Tauri 桌面端(Web 模式显示禁用提示)
- 非流式(用阻塞式 hermesAgentRun,等所有完成后一起显示)
- 不持久化(一次性会话,刷新清空,不挂 chat-store)
- 切 profile 后不还原(用户后续 /h/chat 会保持最后切到的 profile)
### sidebar
- 监控 section 加群聊入口(agents icon,紧跟 /h/chat 之后)
- /h/group-chat 路由注册
### CSS(~180 行)
- .hm-gc-layout: grid 双栏(响应式 → 单栏)
- .hm-gc-side: 左侧 profile 多选
- .hm-gc-main: 右侧消息列表 + 输入区
- 加载点动画 hm-gc-pulse
- @profile 徽章用 mono 字体 + accent 色
### i18n
- 13 个新键 × 3 语言(hermesGroupChat*)
## 累计
- 1 个新页面 ~250 行 + CSS ~180 行 + i18n 39 字符串
- npm build ✓
This commit is contained in:
@@ -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') },
|
||||
|
||||
266
src/engines/hermes/pages/group-chat.js
Normal file
266
src/engines/hermes/pages/group-chat.js
Normal file
@@ -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, '>').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 = `
|
||||
<div class="page-header">
|
||||
<div>
|
||||
<h1 class="page-title">${escHtml(t('engine.hermesGroupChatTitle'))}</h1>
|
||||
</div>
|
||||
</div>
|
||||
<div style="padding:32px;text-align:center;color:var(--text-tertiary)">
|
||||
${escHtml(t('engine.hermesGroupChatWebUnsupported'))}
|
||||
</div>`
|
||||
return
|
||||
}
|
||||
el.innerHTML = `
|
||||
<div class="page-header">
|
||||
<div>
|
||||
<h1 class="page-title">${escHtml(t('engine.hermesGroupChatTitle'))}</h1>
|
||||
<p class="page-desc">${escHtml(t('engine.hermesGroupChatDesc'))}</p>
|
||||
</div>
|
||||
<div class="config-actions">
|
||||
<button class="btn btn-secondary btn-sm" id="hm-gc-clear" ${!messages.length || sending ? 'disabled' : ''}>${escHtml(t('engine.hermesGroupChatClear'))}</button>
|
||||
</div>
|
||||
</div>
|
||||
${loadError ? `<div style="color:var(--error);padding:16px">${escHtml(loadError)}</div>` : ''}
|
||||
<div class="hm-gc-layout">
|
||||
<div class="hm-gc-side">
|
||||
<div class="hm-gc-side-title">${escHtml(t('engine.hermesGroupChatProfiles'))}</div>
|
||||
<div class="hm-gc-side-hint">${escHtml(t('engine.hermesGroupChatProfilesHint'))}</div>
|
||||
<div class="hm-gc-profile-list">
|
||||
${profiles.length ? profiles.map(renderProfileCheckbox).join('') : `<div style="color:var(--text-tertiary);font-size:12px;padding:8px 0">${escHtml(t('common.loading'))}…</div>`}
|
||||
</div>
|
||||
<div class="hm-gc-selected-count">${escHtml(t('engine.hermesGroupChatSelected', { n: selected.size }))}</div>
|
||||
</div>
|
||||
<div class="hm-gc-main">
|
||||
<div class="hm-gc-messages" id="hm-gc-messages">
|
||||
${messages.length === 0 ? `<div class="hm-gc-empty">${escHtml(t('engine.hermesGroupChatEmpty'))}</div>` : messages.map(renderMessage).join('')}
|
||||
</div>
|
||||
<div class="hm-gc-input-wrap">
|
||||
<textarea class="hm-gc-input" id="hm-gc-input"
|
||||
placeholder="${escAttr(t('engine.hermesGroupChatPlaceholder'))}"
|
||||
${sending ? 'disabled' : ''}>${escHtml(inputValue)}</textarea>
|
||||
<button class="btn btn-primary btn-sm" id="hm-gc-send"
|
||||
${sending || !inputValue.trim() || !selected.size ? 'disabled' : ''}>
|
||||
${escHtml(sending ? t('engine.hermesGroupChatSending') : t('engine.hermesGroupChatSend'))}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`
|
||||
bind()
|
||||
scrollToBottom()
|
||||
}
|
||||
|
||||
function renderProfileCheckbox(p) {
|
||||
const isChecked = selected.has(p)
|
||||
return `
|
||||
<label class="hm-gc-profile-item ${isChecked ? 'is-checked' : ''}">
|
||||
<input type="checkbox" data-profile="${escAttr(p)}" ${isChecked ? 'checked' : ''} ${sending ? 'disabled' : ''}>
|
||||
<span class="hm-gc-profile-name">${escHtml(p)}</span>
|
||||
</label>
|
||||
`
|
||||
}
|
||||
|
||||
function renderMessage(m) {
|
||||
if (m.role === 'user') {
|
||||
return `
|
||||
<div class="hm-gc-msg hm-gc-msg--user">
|
||||
<div class="hm-gc-msg-bubble">${escHtml(m.content)}</div>
|
||||
<div class="hm-gc-msg-meta">${escHtml(formatTime(m.ts))}</div>
|
||||
</div>
|
||||
`
|
||||
}
|
||||
if (m.role === 'system') {
|
||||
return `
|
||||
<div class="hm-gc-msg hm-gc-msg--system">
|
||||
<div class="hm-gc-msg-meta">${escHtml(m.content)}</div>
|
||||
</div>
|
||||
`
|
||||
}
|
||||
// assistant
|
||||
const fromTag = m.from ? `<span class="hm-gc-msg-from">@${escHtml(m.from)}</span>` : ''
|
||||
if (m.loading) {
|
||||
return `
|
||||
<div class="hm-gc-msg hm-gc-msg--assistant">
|
||||
<div class="hm-gc-msg-meta">${fromTag} <span class="hm-gc-loading-dots"><span></span><span></span><span></span></span></div>
|
||||
</div>
|
||||
`
|
||||
}
|
||||
if (m.error) {
|
||||
return `
|
||||
<div class="hm-gc-msg hm-gc-msg--assistant hm-gc-msg--error">
|
||||
<div class="hm-gc-msg-meta">${fromTag} <span style="color:var(--error)">⚠️ ${escHtml(t('engine.hermesGroupChatRunFailed'))}</span></div>
|
||||
<div class="hm-gc-msg-bubble" style="color:var(--error)">${escHtml(m.error)}</div>
|
||||
</div>
|
||||
`
|
||||
}
|
||||
return `
|
||||
<div class="hm-gc-msg hm-gc-msg--assistant">
|
||||
<div class="hm-gc-msg-meta">${fromTag} <span>${escHtml(formatTime(m.ts))}</span></div>
|
||||
<div class="hm-gc-msg-bubble">${escHtml(m.content)}</div>
|
||||
</div>
|
||||
`
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -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 实时流式聊天(依赖桌面端事件桥)。请打开桌面客户端使用此功能。',
|
||||
|
||||
Reference in New Issue
Block a user