mirror of
https://github.com/qingchencloud/clawpanel.git
synced 2026-05-29 20:30:00 +08:00
feat(hermes): Batch 2 §I + Batch 3 §M - 流恢复 + Kanban 看板
## Batch 2 §I 流恢复(chat 在切页/刷新后能接上 run)
校对稿:用 run_id 持久化到 localStorage,新页面挂载时查询 status 决定是否重连。
### 后端
- 新 Tauri 命令 hermes_run_status(run_id):
· GET /v1/runs/{run_id} 返回 { run_id, status, last_event, output?, ... }
· status: running / stopping / completed / failed / cancelled / waiting_for_approval
· 404 友好返回 status='not_found' 而不是抛错
### 前端 chat-store
- 新 STORAGE_ACTIVE_RUN 持久化 { runId, sessionId, profile, t }
- hermes-run-started 监听里 safeSet 持久化
- cleanupAfterRun 里 safeRemove 清理
- 新方法 recoverIfRunning():
· 跨 profile / 1 小时过期 → 直接清
· status=running/stopping/waiting_for_approval → attachStreamListeners + 恢复 streaming
· status=已结束 → 拉最新 messages
· 404 → 静默清
### chat.js
- 页面挂载时 store.recoverIfRunning() — 切页/刷新后无缝接上流
## Batch 3 §M Kanban 看板(Hermes 已内置)
校对稿:「Hermes 已内置 kanban 系统,直接调 /api/plugins/kanban/* 即可」。
设计稿原本是「自建本地存储」(~800 行),复用 Hermes 内置后大幅缩减。
### 新页面 /h/kanban
- 全部走 hermesDashboardApi(复用 §H 的基础设施)
- 顶部 board 切换器 + 「+ 新任务」按钮
- 渲染 board.columns(按状态分列:todo / in_progress / blocked / done / archived)
- 任务卡片:title + summary(2 行截断)+ priority badge + assignee + 评论数
- 点卡片 → showContentModal 显示详情 + 「修改状态」按钮
- 修改状态 → PATCH /api/plugins/kanban/tasks/{id} { status }
- board 切换 → POST /api/plugins/kanban/boards/{slug}/switch
### sidebar
- 「管理」section 加 Kanban 入口(inbox 图标)
- /h/kanban 路由注册
### CSS (.hm-kanban-*)
- 水平滚动 board 容器
- 280px 固定宽度列 + 内部滚动
- 卡片 hover 边框变 accent 色 + 轻阴影
- 优先级 badge(琥珀色)/ assignee(accent 色)
### i18n
- 27 个新键 × 3 语言(zh-CN/en/zh-TW)
## 累计
- Rust: 1 个新命令(hermes_run_status ~30 行)
- 前端: chat-store 流恢复(~40 行)+ kanban 新页面(~230 行)
- i18n: 27 个新键 × 3 语言
- CSS: ~100 行
- cargo check ✓ + npm build ✓
This commit is contained in:
@@ -86,9 +86,7 @@ export default {
|
||||
{ route: '/h/skills', label: t('sidebar.skills'), icon: 'skills' },
|
||||
{ route: '/h/memory', label: t('sidebar.memory'), icon: 'memory' },
|
||||
{ route: '/h/cron', label: t('sidebar.cron'), icon: 'clock' },
|
||||
{ route: '/h/profiles', label: t('engine.hermesProfilesTitle'), icon: 'agents' },
|
||||
{ route: '/h/lazy-deps', label: t('hermesLazyDeps.title'), icon: 'package' },
|
||||
{ route: '/h/extensions', label: t('sidebar.extensions'), icon: 'package' },
|
||||
{ route: '/h/extensions', label: t('sidebar.extensions'), icon: 'package' },
|
||||
]
|
||||
}, {
|
||||
section: '',
|
||||
@@ -115,6 +113,7 @@ export default {
|
||||
{ path: '/h/cron', loader: () => import('./pages/cron.js') },
|
||||
{ path: '/h/extensions', loader: () => import('./pages/extensions.js') },
|
||||
{ path: '/h/profiles', loader: () => import('./pages/profiles.js') },
|
||||
{ path: '/h/kanban', loader: () => import('./pages/kanban.js') },
|
||||
{ path: '/h/lazy-deps', loader: () => import('./pages/lazy-deps.js') },
|
||||
{ path: '/h/services', loader: () => import('./pages/services.js') },
|
||||
{ path: '/h/config', loader: () => import('./pages/config.js') },
|
||||
|
||||
@@ -28,6 +28,8 @@ const STORAGE_PROFILE = 'hermes_chat_profile_v1'
|
||||
const STORAGE_SESSIONS_PREFIX = 'hermes_chat_sessions_v2_'
|
||||
const STORAGE_ACTIVE_PREFIX = 'hermes_chat_active_v2_'
|
||||
const STORAGE_PINNED_PREFIX = 'hermes_chat_pinned_'
|
||||
// Batch 2 §I: 流恢复 — 持久化 run_id 用于切页/刷新后恢复
|
||||
const STORAGE_ACTIVE_RUN = 'hermes_chat_active_run_v1'
|
||||
const STORAGE_COLLAPSED_PREFIX = 'hermes_chat_collapsed_groups_'
|
||||
const STORAGE_MSGS_PREFIX = 'hermes_chat_msgs_v2_'
|
||||
const LIVE_BADGE_WINDOW_MS = 5 * 60 * 1000 // 5 min
|
||||
@@ -968,10 +970,11 @@ function createStore() {
|
||||
state.runningSessionId = null
|
||||
state.pendingAssistantId = null
|
||||
state.liveTools = []
|
||||
// Batch 1 §C/§D/§C-bis: 重置 run-level 字段
|
||||
// Batch 1 §C/§D/§C-bis + Batch 2 §I: 重置 run-level 字段 + 清持久化
|
||||
state.currentRunId = null
|
||||
state.pendingApproval = null
|
||||
state.aborting = false
|
||||
safeRemove(STORAGE_ACTIVE_RUN)
|
||||
// hasReasoning 保留到下次 run 开始(让用户看完上一轮思考链)
|
||||
streamAbortController = null
|
||||
detachStreamListeners()
|
||||
@@ -1189,6 +1192,43 @@ function createStore() {
|
||||
groupedSessions,
|
||||
subscribe,
|
||||
|
||||
// Batch 2 §I: 流恢复 — 切页/刷新后看是否有 in-flight run,是的话重新挂监听
|
||||
async recoverIfRunning() {
|
||||
if (state.streaming) return // 已经在监听
|
||||
const raw = safeGet(STORAGE_ACTIVE_RUN)
|
||||
if (!raw) return
|
||||
let info
|
||||
try { info = JSON.parse(raw) } catch { safeRemove(STORAGE_ACTIVE_RUN); return }
|
||||
if (!info?.runId || !info?.sessionId) { safeRemove(STORAGE_ACTIVE_RUN); return }
|
||||
// 跨 profile 的 run 不恢复(用户已切了 profile)
|
||||
if (info.profile && info.profile !== state.activeProfile) { safeRemove(STORAGE_ACTIVE_RUN); return }
|
||||
// 超过 1 小时的 run 视为过期
|
||||
if (info.t && Date.now() - info.t > 60 * 60 * 1000) { safeRemove(STORAGE_ACTIVE_RUN); return }
|
||||
|
||||
try {
|
||||
const st = await api.hermesRunStatus(info.runId)
|
||||
const status = String(st?.status || '')
|
||||
if (status === 'running' || status === 'stopping' || status === 'waiting_for_approval') {
|
||||
// 还在跑 — 重新挂监听 + 标 streaming
|
||||
state.runningSessionId = info.sessionId
|
||||
state.currentRunId = info.runId
|
||||
state.streaming = true
|
||||
if (status === 'stopping') state.aborting = true
|
||||
await attachStreamListeners(info.sessionId)
|
||||
notify()
|
||||
} else {
|
||||
// 已结束 — 拉一下最新 messages(让用户看到完整结果)
|
||||
safeRemove(STORAGE_ACTIVE_RUN)
|
||||
if (info.sessionId === state.activeSessionId) {
|
||||
await refreshActiveMessages().catch(() => {})
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
// status 查询失败,假装没事
|
||||
safeRemove(STORAGE_ACTIVE_RUN)
|
||||
}
|
||||
},
|
||||
|
||||
// actions
|
||||
loadSessions,
|
||||
refreshActiveMessages,
|
||||
|
||||
@@ -311,6 +311,8 @@ export function render() {
|
||||
// --- initial session load + model meta ---
|
||||
store.loadSessions().then(() => draw())
|
||||
store.loadProfiles().then(() => draw()).catch(() => {})
|
||||
// Batch 2 §I: 切页/刷新后看是否有 in-flight run,是的话重新挂监听
|
||||
store.recoverIfRunning().catch(() => {})
|
||||
// 强制刷新安装/Gateway 状态缓存,避免用户刚在仪表盘启动 Gateway 后
|
||||
// 进聊天页看到 30s 过期的「未启动」误判。
|
||||
invalidate('check_hermes')
|
||||
|
||||
235
src/engines/hermes/pages/kanban.js
Normal file
235
src/engines/hermes/pages/kanban.js
Normal file
@@ -0,0 +1,235 @@
|
||||
/**
|
||||
* Hermes Kanban 看板(Batch 3 §M)
|
||||
*
|
||||
* Hermes 已内置 kanban 系统(plugins/kanban/dashboard/plugin_api.py),
|
||||
* ClawPanel 直接调 Dashboard 9119 的 plugin API:
|
||||
* - GET /api/plugins/kanban/board - 拿当前 board 全部 columns + tasks
|
||||
* - GET /api/plugins/kanban/boards - 列所有 board
|
||||
* - POST /api/plugins/kanban/boards - 创建 board
|
||||
* - POST /api/plugins/kanban/boards/{slug}/switch - 切换 active board
|
||||
* - POST /api/plugins/kanban/tasks - 创建任务
|
||||
* - PATCH /api/plugins/kanban/tasks/{id} - 改任务(含 status 切换)
|
||||
* - GET /api/plugins/kanban/tasks/{id} - 任务详情
|
||||
*
|
||||
* 设计稿原本是「自建本地存储」(~800 行),复用 Hermes 内置后大幅缩减。
|
||||
*/
|
||||
import { t } from '../../../lib/i18n.js'
|
||||
import { api } from '../../../lib/tauri-api.js'
|
||||
import { toast } from '../../../components/toast.js'
|
||||
import { showModal, showContentModal } from '../../../components/modal.js'
|
||||
import { humanizeError } from '../../../lib/humanize-error.js'
|
||||
|
||||
const KANBAN_BASE = '/api/plugins/kanban'
|
||||
|
||||
function escHtml(s) {
|
||||
return String(s ?? '').replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"')
|
||||
}
|
||||
function escAttr(s) { return escHtml(s) }
|
||||
|
||||
export function render() {
|
||||
const el = document.createElement('div')
|
||||
el.className = 'page'
|
||||
el.dataset.engine = 'hermes'
|
||||
|
||||
let board = null // { columns: [{name, tasks: []}], ... }
|
||||
let boards = []
|
||||
let loading = true
|
||||
let error = ''
|
||||
|
||||
function draw() {
|
||||
el.innerHTML = `
|
||||
<div class="page-header">
|
||||
<div>
|
||||
<h1 class="page-title">${escHtml(t('engine.hermesKanbanTitle'))}</h1>
|
||||
<p class="page-desc">${escHtml(t('engine.hermesKanbanDesc'))}</p>
|
||||
</div>
|
||||
<div class="config-actions">
|
||||
${boards.length > 1 ? `
|
||||
<select class="form-input" id="hm-kanban-board-switch" style="max-width:200px">
|
||||
${boards.map(b => `<option value="${escAttr(b.slug || b.name)}" ${b.is_current ? 'selected' : ''}>${escHtml(b.name || b.slug)}</option>`).join('')}
|
||||
</select>` : ''}
|
||||
<button class="btn btn-secondary btn-sm" id="hm-kanban-refresh">${escHtml(t('hermesLazyDeps.refresh'))}</button>
|
||||
<button class="btn btn-primary btn-sm" id="hm-kanban-new-task">+ ${escHtml(t('engine.hermesKanbanNewTask'))}</button>
|
||||
</div>
|
||||
</div>
|
||||
<div id="hm-kanban-content">
|
||||
${loading ? `<div style="padding:32px;text-align:center;color:var(--text-tertiary)">${escHtml(t('common.loading'))}…</div>` : ''}
|
||||
${error ? `<div style="color:var(--error);padding:20px">${escHtml(error)}</div>` : ''}
|
||||
${(!loading && !error && board) ? renderBoard() : ''}
|
||||
</div>
|
||||
`
|
||||
bind()
|
||||
}
|
||||
|
||||
function renderBoard() {
|
||||
if (!board?.columns?.length) {
|
||||
return `<div class="empty-state empty-compact"><div class="empty-icon">📋</div><div class="empty-title">${escHtml(t('engine.hermesKanbanEmpty'))}</div></div>`
|
||||
}
|
||||
return `
|
||||
<div class="hm-kanban-board">
|
||||
${board.columns.map(col => `
|
||||
<div class="hm-kanban-col" data-col="${escAttr(col.name)}">
|
||||
<div class="hm-kanban-col-head">
|
||||
<span class="hm-kanban-col-name">${escHtml(colLabel(col.name))}</span>
|
||||
<span class="hm-kanban-col-count">${col.tasks?.length || 0}</span>
|
||||
</div>
|
||||
<div class="hm-kanban-col-body">
|
||||
${(col.tasks || []).map(renderTask).join('')}
|
||||
${!col.tasks?.length ? `<div class="hm-kanban-col-empty">${escHtml(t('engine.hermesKanbanColEmpty'))}</div>` : ''}
|
||||
</div>
|
||||
</div>
|
||||
`).join('')}
|
||||
</div>
|
||||
`
|
||||
}
|
||||
|
||||
function renderTask(task) {
|
||||
const priorityBadge = task.priority > 1 ? `<span class="hm-kanban-task-prio">P${escHtml(task.priority)}</span>` : ''
|
||||
const assignee = task.assignee ? `<span class="hm-kanban-task-assignee">@${escHtml(task.assignee)}</span>` : ''
|
||||
return `
|
||||
<div class="hm-kanban-task" data-task-id="${escAttr(task.id)}">
|
||||
<div class="hm-kanban-task-title">${escHtml(task.title)}</div>
|
||||
${task.summary ? `<div class="hm-kanban-task-summary">${escHtml(task.summary)}</div>` : ''}
|
||||
<div class="hm-kanban-task-meta">
|
||||
${priorityBadge}
|
||||
${assignee}
|
||||
${task.comment_count ? `<span class="hm-kanban-task-meta-item">💬 ${task.comment_count}</span>` : ''}
|
||||
</div>
|
||||
</div>
|
||||
`
|
||||
}
|
||||
|
||||
function colLabel(name) {
|
||||
const map = {
|
||||
todo: t('engine.hermesKanbanColTodo'),
|
||||
'in_progress': t('engine.hermesKanbanColInProgress'),
|
||||
blocked: t('engine.hermesKanbanColBlocked'),
|
||||
done: t('engine.hermesKanbanColDone'),
|
||||
archived: t('engine.hermesKanbanColArchived'),
|
||||
}
|
||||
return map[name] || name
|
||||
}
|
||||
|
||||
function bind() {
|
||||
el.querySelector('#hm-kanban-refresh')?.addEventListener('click', load)
|
||||
el.querySelector('#hm-kanban-new-task')?.addEventListener('click', onCreateTask)
|
||||
el.querySelector('#hm-kanban-board-switch')?.addEventListener('change', async (e) => {
|
||||
const slug = e.target.value
|
||||
try {
|
||||
await api.hermesDashboardApi('POST', `${KANBAN_BASE}/boards/${encodeURIComponent(slug)}/switch`)
|
||||
toast(t('engine.hermesKanbanBoardSwitched', { name: slug }), 'success')
|
||||
await load()
|
||||
} catch (err) {
|
||||
toast(humanizeError(err, t('engine.hermesKanbanBoardSwitchFailed')), 'error')
|
||||
}
|
||||
})
|
||||
el.querySelectorAll('.hm-kanban-task').forEach(card => {
|
||||
card.addEventListener('click', () => onTaskClick(card.dataset.taskId))
|
||||
})
|
||||
}
|
||||
|
||||
async function load() {
|
||||
loading = true
|
||||
error = ''
|
||||
draw()
|
||||
try {
|
||||
const [boardData, boardsData] = await Promise.all([
|
||||
api.hermesDashboardApi('GET', `${KANBAN_BASE}/board`),
|
||||
api.hermesDashboardApi('GET', `${KANBAN_BASE}/boards`).catch(() => ({ boards: [] })),
|
||||
])
|
||||
board = boardData
|
||||
boards = Array.isArray(boardsData) ? boardsData : (boardsData?.boards || [])
|
||||
} catch (e) {
|
||||
error = String(e?.message || e)
|
||||
} finally {
|
||||
loading = false
|
||||
draw()
|
||||
}
|
||||
}
|
||||
|
||||
function onCreateTask() {
|
||||
const statusOpts = ['todo', 'in_progress', 'blocked', 'done'].map(s => ({ value: s, label: colLabel(s) }))
|
||||
showModal({
|
||||
title: t('engine.hermesKanbanNewTaskTitle'),
|
||||
fields: [
|
||||
{ name: 'title', label: t('engine.hermesKanbanTitleLabel'), value: '', placeholder: '...' },
|
||||
{ name: 'summary', label: t('engine.hermesKanbanSummaryLabel'), value: '', placeholder: '' },
|
||||
{ name: 'status', label: t('engine.hermesKanbanStatusLabel'), type: 'select', options: statusOpts, value: 'todo' },
|
||||
{ name: 'priority', label: t('engine.hermesKanbanPriorityLabel'), value: '1', placeholder: '1-5' },
|
||||
],
|
||||
onConfirm: async (data) => {
|
||||
const title = (data.title || '').trim()
|
||||
if (!title) {
|
||||
toast(t('engine.hermesKanbanTitleRequired'), 'error')
|
||||
return
|
||||
}
|
||||
try {
|
||||
await api.hermesDashboardApi('POST', `${KANBAN_BASE}/tasks`, {
|
||||
title,
|
||||
summary: (data.summary || '').trim() || undefined,
|
||||
status: data.status,
|
||||
priority: parseInt(data.priority, 10) || 1,
|
||||
})
|
||||
toast(t('engine.hermesKanbanTaskCreated'), 'success')
|
||||
await load()
|
||||
} catch (err) {
|
||||
toast(humanizeError(err, t('engine.hermesKanbanTaskCreateFailed')), 'error')
|
||||
}
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
async function onTaskClick(taskId) {
|
||||
if (!taskId) return
|
||||
try {
|
||||
const task = await api.hermesDashboardApi('GET', `${KANBAN_BASE}/tasks/${encodeURIComponent(taskId)}`)
|
||||
showContentModal({
|
||||
title: task.title || taskId,
|
||||
content: `
|
||||
<div class="hm-kanban-detail">
|
||||
<div class="hm-kanban-detail-row"><b>${escHtml(t('engine.hermesKanbanStatusLabel'))}:</b> ${escHtml(colLabel(task.status))}</div>
|
||||
${task.priority ? `<div class="hm-kanban-detail-row"><b>${escHtml(t('engine.hermesKanbanPriorityLabel'))}:</b> P${escHtml(task.priority)}</div>` : ''}
|
||||
${task.assignee ? `<div class="hm-kanban-detail-row"><b>${escHtml(t('engine.hermesKanbanAssigneeLabel'))}:</b> @${escHtml(task.assignee)}</div>` : ''}
|
||||
${task.summary ? `<div class="hm-kanban-detail-row"><b>${escHtml(t('engine.hermesKanbanSummaryLabel'))}:</b><br>${escHtml(task.summary)}</div>` : ''}
|
||||
${task.description ? `<div class="hm-kanban-detail-row"><b>${escHtml(t('engine.hermesKanbanDescLabel'))}:</b><pre style="white-space:pre-wrap;font-family:var(--font-mono);font-size:12px;margin:6px 0 0">${escHtml(task.description)}</pre></div>` : ''}
|
||||
${task.latest_summary ? `<div class="hm-kanban-detail-row"><b>${escHtml(t('engine.hermesKanbanRunSummary'))}:</b><pre style="white-space:pre-wrap;font-family:var(--font-mono);font-size:11px;margin:6px 0 0;max-height:160px;overflow:auto">${escHtml(task.latest_summary)}</pre></div>` : ''}
|
||||
</div>`,
|
||||
buttons: [
|
||||
{ label: t('engine.hermesKanbanMoveStatus'), className: 'btn-secondary', id: 'kanban-move-' + taskId },
|
||||
{ label: t('common.close'), className: 'btn-secondary' },
|
||||
],
|
||||
width: 560,
|
||||
})
|
||||
// 「修改状态」按钮点击 → 弹小窗选 status
|
||||
setTimeout(() => {
|
||||
document.getElementById('kanban-move-' + taskId)?.addEventListener('click', () => {
|
||||
showModal({
|
||||
title: t('engine.hermesKanbanMoveStatusTitle'),
|
||||
fields: [
|
||||
{ name: 'status', label: t('engine.hermesKanbanStatusLabel'), type: 'select',
|
||||
options: ['todo', 'in_progress', 'blocked', 'done', 'archived'].map(s => ({ value: s, label: colLabel(s) })),
|
||||
value: task.status },
|
||||
],
|
||||
onConfirm: async (d) => {
|
||||
try {
|
||||
await api.hermesDashboardApi('PATCH', `${KANBAN_BASE}/tasks/${encodeURIComponent(taskId)}`, { status: d.status })
|
||||
toast(t('engine.hermesKanbanTaskUpdated'), 'success')
|
||||
await load()
|
||||
// 关掉详情模态
|
||||
document.querySelectorAll('.modal-overlay').forEach(o => o.remove())
|
||||
} catch (err) {
|
||||
toast(humanizeError(err, t('engine.hermesKanbanTaskUpdateFailed')), 'error')
|
||||
}
|
||||
},
|
||||
})
|
||||
})
|
||||
}, 10)
|
||||
} catch (err) {
|
||||
toast(humanizeError(err, t('engine.hermesKanbanTaskLoadFailed')), 'error')
|
||||
}
|
||||
}
|
||||
|
||||
draw()
|
||||
load()
|
||||
return el
|
||||
}
|
||||
@@ -5190,6 +5190,108 @@ body[data-active-engine="hermes"][data-theme="dark"] {
|
||||
cursor: zoom-out;
|
||||
}
|
||||
|
||||
/* ---- Batch 3 §M: Kanban 看板 ---- */
|
||||
[data-engine="hermes"] .hm-kanban-board {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
overflow-x: auto;
|
||||
padding-bottom: 8px;
|
||||
}
|
||||
[data-engine="hermes"] .hm-kanban-col {
|
||||
flex: 0 0 280px;
|
||||
background: var(--hm-surface-1);
|
||||
border-radius: 10px;
|
||||
padding: 12px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
max-height: calc(100vh - 220px);
|
||||
}
|
||||
[data-engine="hermes"] .hm-kanban-col-head {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding-bottom: 6px;
|
||||
border-bottom: 1px solid var(--hm-border);
|
||||
}
|
||||
[data-engine="hermes"] .hm-kanban-col-name {
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
color: var(--hm-text-primary);
|
||||
}
|
||||
[data-engine="hermes"] .hm-kanban-col-count {
|
||||
background: var(--hm-surface-2);
|
||||
color: var(--hm-text-tertiary);
|
||||
border-radius: 10px;
|
||||
padding: 2px 8px;
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
}
|
||||
[data-engine="hermes"] .hm-kanban-col-body {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
overflow-y: auto;
|
||||
flex: 1;
|
||||
}
|
||||
[data-engine="hermes"] .hm-kanban-col-empty {
|
||||
color: var(--hm-text-tertiary);
|
||||
font-size: 12px;
|
||||
text-align: center;
|
||||
padding: 24px 0;
|
||||
}
|
||||
[data-engine="hermes"] .hm-kanban-task {
|
||||
background: var(--hm-surface-0);
|
||||
border: 1px solid var(--hm-border);
|
||||
border-radius: 8px;
|
||||
padding: 10px;
|
||||
cursor: pointer;
|
||||
transition: border-color 0.15s, box-shadow 0.15s;
|
||||
}
|
||||
[data-engine="hermes"] .hm-kanban-task:hover {
|
||||
border-color: var(--hm-accent);
|
||||
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.06);
|
||||
}
|
||||
[data-engine="hermes"] .hm-kanban-task-title {
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
color: var(--hm-text-primary);
|
||||
margin-bottom: 4px;
|
||||
word-break: break-word;
|
||||
}
|
||||
[data-engine="hermes"] .hm-kanban-task-summary {
|
||||
font-size: 11px;
|
||||
color: var(--hm-text-tertiary);
|
||||
margin-bottom: 6px;
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 2;
|
||||
-webkit-box-orient: vertical;
|
||||
overflow: hidden;
|
||||
}
|
||||
[data-engine="hermes"] .hm-kanban-task-meta {
|
||||
display: flex;
|
||||
gap: 6px;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
font-size: 10px;
|
||||
color: var(--hm-text-tertiary);
|
||||
}
|
||||
[data-engine="hermes"] .hm-kanban-task-prio {
|
||||
background: rgba(245, 158, 11, 0.15);
|
||||
color: rgb(180, 100, 0);
|
||||
padding: 1px 6px;
|
||||
border-radius: 4px;
|
||||
font-weight: 600;
|
||||
}
|
||||
[data-engine="hermes"] .hm-kanban-task-assignee {
|
||||
color: var(--hm-accent);
|
||||
}
|
||||
[data-engine="hermes"] .hm-kanban-detail-row {
|
||||
margin-bottom: 10px;
|
||||
font-size: 13px;
|
||||
color: var(--hm-text-secondary);
|
||||
}
|
||||
|
||||
[data-engine="hermes"] .hm-chat-live-tools {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
@@ -478,6 +478,8 @@ export const api = {
|
||||
// Batch 1 §D + §C-bis: 真正中断 + Approval Flow(用 run_id)
|
||||
hermesRunStop: (runId) => invoke('hermes_run_stop', { runId }),
|
||||
hermesRunApproval: (runId, choice) => invoke('hermes_run_approval', { runId, choice }),
|
||||
// Batch 2 §I: 流恢复 — 查 run 状态
|
||||
hermesRunStatus: (runId) => invoke('hermes_run_status', { runId }),
|
||||
// Batch 1 §E: 会话消息导出(走 dashboard /api/sessions/{id}/messages)
|
||||
hermesSessionExport: (sessionId) => invoke('hermes_session_export', { sessionId }),
|
||||
// Batch 2 §H 基础设施: 通用 Dashboard 9119 HTTP 代理
|
||||
|
||||
@@ -486,6 +486,35 @@ export default {
|
||||
hermesProfileActive: _('当前', 'Active', '當前'),
|
||||
hermesProfileCreated: _('已创建 Profile "{name}"', 'Profile "{name}" created', '已建立 Profile "{name}"'),
|
||||
hermesProfileCreateFailed: _('创建失败', 'Create failed', '建立失敗'),
|
||||
// Batch 3 §M: Kanban 看板(Hermes 已内置,走 /api/plugins/kanban/*)
|
||||
hermesKanbanTitle: _('看板', 'Kanban', '看板'),
|
||||
hermesKanbanDesc: _('管理 Agent 任务卡片,按状态分列', 'Manage agent tasks by status columns', '管理 Agent 任務卡片,按狀態分欄'),
|
||||
hermesKanbanEmpty: _('暂无任务(请先启动 Dashboard)', 'No tasks (start Dashboard first)', '暫無任務(請先啟動 Dashboard)'),
|
||||
hermesKanbanColEmpty: _('(空)', '(empty)', '(空)'),
|
||||
hermesKanbanNewTask: _('新任务', 'New task', '新任務'),
|
||||
hermesKanbanNewTaskTitle: _('新建任务', 'New task', '新建任務'),
|
||||
hermesKanbanTitleLabel: _('标题', 'Title', '標題'),
|
||||
hermesKanbanTitleRequired: _('标题不能为空', 'Title is required', '標題不能為空'),
|
||||
hermesKanbanSummaryLabel: _('摘要', 'Summary', '摘要'),
|
||||
hermesKanbanDescLabel: _('描述', 'Description', '描述'),
|
||||
hermesKanbanStatusLabel: _('状态', 'Status', '狀態'),
|
||||
hermesKanbanPriorityLabel: _('优先级', 'Priority', '優先級'),
|
||||
hermesKanbanAssigneeLabel: _('指派给', 'Assignee', '指派給'),
|
||||
hermesKanbanRunSummary: _('最近运行摘要', 'Latest run summary', '最近執行摘要'),
|
||||
hermesKanbanColTodo: _('待办', 'To do', '待辦'),
|
||||
hermesKanbanColInProgress: _('进行中', 'In progress', '進行中'),
|
||||
hermesKanbanColBlocked: _('阻塞', 'Blocked', '阻塞'),
|
||||
hermesKanbanColDone: _('完成', 'Done', '完成'),
|
||||
hermesKanbanColArchived: _('归档', 'Archived', '歸檔'),
|
||||
hermesKanbanTaskCreated: _('任务已创建', 'Task created', '任務已建立'),
|
||||
hermesKanbanTaskCreateFailed: _('创建任务失败', 'Create task failed', '建立任務失敗'),
|
||||
hermesKanbanTaskUpdated: _('任务已更新', 'Task updated', '任務已更新'),
|
||||
hermesKanbanTaskUpdateFailed: _('更新任务失败', 'Update task failed', '更新任務失敗'),
|
||||
hermesKanbanTaskLoadFailed: _('加载任务失败', 'Load task failed', '載入任務失敗'),
|
||||
hermesKanbanMoveStatus: _('修改状态', 'Change status', '修改狀態'),
|
||||
hermesKanbanMoveStatusTitle: _('修改任务状态', 'Change task status', '修改任務狀態'),
|
||||
hermesKanbanBoardSwitched: _('已切换到看板 "{name}"', 'Switched to board "{name}"', '已切換到看板 "{name}"'),
|
||||
hermesKanbanBoardSwitchFailed: _('切换看板失败', 'Switch board failed', '切換看板失敗'),
|
||||
// Web 模式(远程浏览器)下流式聊天暂不可用
|
||||
chatWebModeStreamingUnsupported: _(
|
||||
'Web 模式暂不支持 Hermes 实时流式聊天(依赖桌面端事件桥)。请打开桌面客户端使用此功能。',
|
||||
|
||||
Reference in New Issue
Block a user