From 7eababad4a3d24cc57e0f1d3a8304effd0cd0b40 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=99=B4=E5=A4=A9?= Date: Thu, 14 May 2026 03:47:25 +0800 Subject: [PATCH] =?UTF-8?q?feat(ux):=20toast=20=E6=99=BA=E8=83=BD=E8=A1=8C?= =?UTF-8?q?=E5=8A=A8=E6=8C=89=E9=92=AE=20+=20=E6=8B=93=E5=B1=95=20?= =?UTF-8?q?=E2=93=98=20=E5=88=B0=20gateway/agents=20+=20sidebar=20?= =?UTF-8?q?=E5=8A=A0=E6=9C=AF=E8=AF=AD=E8=A1=A8=E5=85=A5=E5=8F=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 延续上一轮小白 UX 改造的尾声三连: ## 1. toast 智能行动按钮(U2 收尾) - humanizeError 输出新增 action 字段:{ label, route?, handler?, kind } - 自动按错误 kind 给默认按钮: · gatewayDown → [去启动 Gateway] → /services · cmdMissing / permission → [打开设置] → /settings · auth → [检查 API Key] → /models · network / timeout / rateLimit / generic → 不给(重试由用户控制) - toast 结构化分支渲染 .toast-action-btn 按钮,点击后用 navigate(route) 或调 handler,并自动关闭 toast - common.js 加 errorAction.* 三个按钮文案 i18n(11 语言) ## 2. ⓘ 拓展到 gateway / agents - gateway.js: token label 后加 ⓘ(apikey 术语),renderConfig 末尾 attachTermTooltips - agents.js: addAgent 弹窗 workspace 字段 label 加 ⓘ,setTimeout 扫 document.body 绑定 - term-tooltip.js 精简表新增 4 个术语:workspace / provider / baseurl + scope(已有) ## 3. sidebar 加术语表入口 - 在两个引擎(OpenClaw + Hermes)的最后一个 section 加「术语」条目 - sidebar.js i18n 新增 glossary 键(11 语言) - 之前只能从 dashboard quick-actions 进入,现在 sidebar 永久可达 ## 4. 顺手 bug 修复 - gateway.js 文件末尾历史残留的多余 `}` (line 348) syntax 错误,删除 - 与之前 hermes/cron.js 同类问题,本次改 ⓘ 时被 node --check 暴露 ## 累计变动 - 10 个文件修改 - 7 个新 i18n 键(11 语言) - Build OK --- src/components/toast.js | 22 ++++++++++++++++++++-- src/engines/hermes/index.js | 1 + src/engines/openclaw/index.js | 1 + src/lib/humanize-error.js | 19 +++++++++++++++++-- src/lib/term-tooltip.js | 15 +++++++++++++++ src/locales/modules/common.js | 6 ++++++ src/locales/modules/sidebar.js | 1 + src/pages/agents.js | 4 +++- src/pages/gateway.js | 3 ++- src/style/components.css | 7 +++++++ 10 files changed, 73 insertions(+), 6 deletions(-) diff --git a/src/components/toast.js b/src/components/toast.js index cd4507d..5d1153f 100644 --- a/src/components/toast.js +++ b/src/components/toast.js @@ -2,13 +2,15 @@ * Toast 通知组件 * * 入参 message 支持两种: - * 1) string —— 原来的纯文本(向后兼容) - * 2) { message, hint?, raw? } —— humanize-error.js 的友好错误对象: + * 1) string —— 原来的纯文本(向后兼容) + * 2) { message, hint?, raw?, action? } —— humanize-error.js 的友好错误对象: * - message: 主行(用户视角) * - hint: 副行小灰字(行动建议) * - raw: 折叠在「技术详情」里的原始错误字符串 + * - action: { label, route?, handler? } 智能行动按钮,点击跳转或调 handler */ import { t } from '../lib/i18n.js' +import { navigate } from '../router.js' let _container = null @@ -51,6 +53,22 @@ export function toast(message, type = 'info', options = {}) { body.appendChild(hintRow) } + if (message.action && message.action.label) { + const actionBtn = document.createElement('button') + actionBtn.type = 'button' + actionBtn.className = 'btn btn-xs btn-primary toast-action-btn' + actionBtn.textContent = `${message.action.label} →` + actionBtn.addEventListener('click', () => { + try { + if (typeof message.action.handler === 'function') message.action.handler() + else if (message.action.route) navigate(message.action.route) + } finally { + el.remove() + } + }) + body.appendChild(actionBtn) + } + if (message.raw) { const detail = document.createElement('details') detail.className = 'toast-raw' diff --git a/src/engines/hermes/index.js b/src/engines/hermes/index.js index fc8b88b..d8b44ed 100644 --- a/src/engines/hermes/index.js +++ b/src/engines/hermes/index.js @@ -93,6 +93,7 @@ export default { items: [ { route: '/assistant', label: t('sidebar.assistant'), icon: 'assistant' }, { route: '/settings', label: t('sidebar.settings'), icon: 'settings' }, + { route: '/glossary', label: t('sidebar.glossary'), icon: 'about' }, { route: '/about', label: t('sidebar.about'), icon: 'about' }, ] }] diff --git a/src/engines/openclaw/index.js b/src/engines/openclaw/index.js index a1332ed..cbaa940 100644 --- a/src/engines/openclaw/index.js +++ b/src/engines/openclaw/index.js @@ -88,6 +88,7 @@ export default { items: [ { route: '/settings', label: t('sidebar.settings'), icon: 'settings' }, { route: '/chat-debug', label: t('sidebar.checkRepair'), icon: 'diagnose' }, + { route: '/glossary', label: t('sidebar.glossary'), icon: 'about' }, { route: '/about', label: t('sidebar.about'), icon: 'about' }, ] }] diff --git a/src/lib/humanize-error.js b/src/lib/humanize-error.js index 0d30623..49739de 100644 --- a/src/lib/humanize-error.js +++ b/src/lib/humanize-error.js @@ -88,10 +88,19 @@ function toRawString(e) { return String(e) } +// 不同错误类型默认对应的行动按钮(label 走 i18n,route 直接跳转) +const DEFAULT_ACTIONS = { + gatewayDown: { labelKey: 'common.errorAction.startGateway', route: '/services' }, + cmdMissing: { labelKey: 'common.errorAction.openSettings', route: '/settings' }, + permission: { labelKey: 'common.errorAction.openSettings', route: '/settings' }, + auth: { labelKey: 'common.errorAction.checkApiKey', route: '/models' }, + // network / timeout / busy / rateLimit / notFound / generic:不给默认 action(重试由用户自己控制) +} + /** * @param {unknown} e - 原始错误(Error / string / Tauri Result) * @param {string} [context] - 操作上下文文案(如 t('channels.saveFailed')) - * @returns {{ message: string, hint: string, raw: string }} + * @returns {{ message: string, hint: string, raw: string, action?: { label, route?, handler? } }} */ export function humanizeError(e, context) { const raw = toRawString(e).trim() @@ -111,8 +120,14 @@ export function humanizeError(e, context) { const message = ctx || t(`common.error.${kind}`) const hint = t(`common.errorHint.${kind}`) + const result = { message, hint, raw: rawTruncated, kind } - return { message, hint, raw: rawTruncated } + const defaultAction = DEFAULT_ACTIONS[kind] + if (defaultAction) { + result.action = { label: t(defaultAction.labelKey), route: defaultAction.route } + } + + return result } /** diff --git a/src/lib/term-tooltip.js b/src/lib/term-tooltip.js index 64601f2..6a487fb 100644 --- a/src/lib/term-tooltip.js +++ b/src/lib/term-tooltip.js @@ -59,6 +59,21 @@ const TERMS = { en: { name: 'Scope (Permissions)', desc: 'A whitelist of what the bot is allowed to do — read messages, send messages, manage members, etc. Too few = missing features; too many = security risk.' }, zhTW: { name: '權限範圍(Scope)', desc: '機器人能做什麼的「白名單」—— 例如讀訊息、發訊息、改群成員等。Scope 給少了功能不足,給多了有安全風險。' }, }, + workspace: { + zhCN: { name: 'Workspace(工作目录)', desc: 'Agent 用来读写文件的「文件夹」。每个 Agent 可以指定独立的工作目录,互不干扰,避免误删别的 Agent 的资料。' }, + en: { name: 'Workspace', desc: 'The "folder" the Agent reads/writes files in. Each Agent can have its own workspace so they don\'t accidentally touch each other\'s files.' }, + zhTW: { name: 'Workspace(工作目錄)', desc: 'Agent 用來讀寫檔案的「資料夾」。每個 Agent 可以指定獨立的工作目錄,互不干擾,避免誤刪別的 Agent 的資料。' }, + }, + provider: { + zhCN: { name: 'Provider(服务商)', desc: '给你提供 AI 模型的厂商 —— 比如 OpenAI、Anthropic、DeepSeek、Qwen 等。每个 Provider 通常对应一个 API key。' }, + en: { name: 'Provider', desc: 'A company that supplies AI models — OpenAI, Anthropic, DeepSeek, Qwen, etc. Each provider usually maps to one API key.' }, + zhTW: { name: 'Provider(服務商)', desc: '給你提供 AI 模型的廠商 —— 例如 OpenAI、Anthropic、DeepSeek、Qwen 等。每個 Provider 通常對應一個 API key。' }, + }, + baseurl: { + zhCN: { name: 'Base URL(接口地址)', desc: '服务商 API 的「门牌号」—— 比如 `https://api.openai.com/v1`。换 Provider 时通常要改这个地址。' }, + en: { name: 'Base URL', desc: 'The "address" of the provider\'s API — e.g. `https://api.openai.com/v1`. You change this when switching providers.' }, + zhTW: { name: 'Base URL(介面位址)', desc: '服務商 API 的「門牌號」—— 例如 `https://api.openai.com/v1`。換 Provider 時通常要改這個位址。' }, + }, } function pickLang(item) { diff --git a/src/locales/modules/common.js b/src/locales/modules/common.js index fa35b20..996a449 100644 --- a/src/locales/modules/common.js +++ b/src/locales/modules/common.js @@ -98,6 +98,12 @@ export default { generic: _('请稍后重试;如问题持续,请到日志页查看详情', 'Try again later; if the issue persists, check the Logs page for details', '請稍後重試;如問題持續,請到日誌頁查看詳情', '後で再試行してください。問題が続く場合はログページで詳細を確認してください', '잠시 후 다시 시도하세요; 문제가 계속되면 로그 페이지에서 자세히 확인하세요', 'Vui lòng thử lại sau; nếu lỗi vẫn xảy ra, hãy kiểm tra trang Nhật ký', 'Reintenta más tarde; si persiste, revisa la página de Registros', 'Tente novamente mais tarde; se persistir, verifique a página de Logs', 'Повторите позже; если ошибка не уходит — посмотрите страницу Логи', 'Réessayez plus tard ; si le problème persiste, consultez la page Journaux', 'Versuchen Sie es später erneut; bei anhaltendem Problem die Protokoll-Seite prüfen'), }, errorRawLabel: _('技术详情', 'Technical details', '技術詳情', '技術的詳細', '기술 정보', 'Chi tiết kỹ thuật', 'Detalles técnicos', 'Detalhes técnicos', 'Технические подробности', 'Détails techniques', 'Technische Details'), + // 智能行动按钮(toast 副标题旁的快捷跳转) + errorAction: { + startGateway: _('去启动 Gateway', 'Start Gateway', '去啟動 Gateway', 'Gateway を起動', 'Gateway 시작', 'Khởi động Gateway', 'Iniciar Gateway', 'Iniciar Gateway', 'Запустить Gateway', 'Démarrer Gateway', 'Gateway starten'), + openSettings: _('打开设置', 'Open Settings', '打開設定', '設定を開く', '설정 열기', 'Mở cài đặt', 'Abrir configuración', 'Abrir configurações', 'Открыть настройки', 'Ouvrir les paramètres', 'Einstellungen öffnen'), + checkApiKey: _('检查 API Key', 'Check API Key', '檢查 API Key', 'API Key を確認', 'API Key 확인', 'Kiểm tra API Key', 'Comprobar API Key', 'Verificar API Key', 'Проверить API Key', 'Vérifier la clé API', 'API-Key prüfen'), + }, // 空状态通用副本 emptyGetStartedHint: _('点下面的按钮开始第一步', 'Click the button below to get started', '點下面的按鈕開始第一步', '下のボタンから最初の一歩を', '아래 버튼을 눌러 시작하세요', 'Nhấn nút bên dưới để bắt đầu', 'Pulsa el botón de abajo para empezar', 'Clique no botão abaixo para começar', 'Нажмите кнопку ниже, чтобы начать', 'Cliquez sur le bouton ci-dessous pour commencer', 'Klicken Sie auf die Schaltfläche unten, um zu starten'), } diff --git a/src/locales/modules/sidebar.js b/src/locales/modules/sidebar.js index 462550b..6e44e08 100644 --- a/src/locales/modules/sidebar.js +++ b/src/locales/modules/sidebar.js @@ -35,5 +35,6 @@ export default { checkRepair: _('检测与修复', 'Check & Repair', '檢測與修復', '検出と修復', '검사 및 수리', 'Kiểm tra & Sửa chữa', 'Verificar y reparar', 'Verificar e reparar', 'Проверка и ремонт', 'Vérifier et réparer', 'Prüfen & Reparieren'), routeMap: _('路由地图', 'Route Map', '路由地圖', 'ルートマップ', '라우트 맵', 'Bản đồ tuyến', 'Mapa de rutas', 'Mapa de rotas', 'Карта маршрутов', 'Carte des routes', 'Routenkarte'), about: _('关于', 'About', '關於', 'について', '정보', 'Giới thiệu', 'Acerca de', 'Sobre', 'О программе', 'À propos', 'Über'), + glossary: _('术语', 'Glossary', '術語', '用語', '용어', 'Thuật ngữ', 'Glosario', 'Glossário', 'Глоссарий', 'Glossaire', 'Glossar'), setup: _('初始设置', 'Setup', '初始設定', '初期設定', '초기 설정', 'Thiết lập', 'Configuración inicial', 'Configuração inicial', 'Начальная настройка', 'Configuration initiale', 'Ersteinrichtung'), } diff --git a/src/pages/agents.js b/src/pages/agents.js index 8795bc2..259f28b 100644 --- a/src/pages/agents.js +++ b/src/pages/agents.js @@ -10,6 +10,7 @@ import { CHANNEL_LABELS } from '../lib/channel-labels.js' import { t } from '../lib/i18n.js' import { listAgentsCompat } from '../lib/api-compat.js' import { hasFeature } from '../lib/kernel.js' +import { termHelpHtml, attachTermTooltips } from '../lib/term-tooltip.js' export async function render() { const page = document.createElement('div') @@ -216,6 +217,7 @@ async function showAddAgentDialog(page, state) { return } + setTimeout(() => attachTermTooltips(document.body), 0) showModal({ title: t('agents.addTitle'), fields: [ @@ -223,7 +225,7 @@ async function showAddAgentDialog(page, state) { { name: 'name', label: t('agents.agentName'), value: '', placeholder: t('agents.agentNamePlaceholder') }, { name: 'emoji', label: t('agents.agentEmoji'), value: '', placeholder: t('agents.agentEmojiPlaceholder') }, { name: 'model', label: t('agents.agentModel'), type: 'select', value: models[0]?.value || '', options: models }, - { name: 'workspace', label: t('agents.agentWorkspace'), value: '', placeholder: t('agents.agentWorkspacePlaceholder') }, + { name: 'workspace', label: t('agents.agentWorkspace') + termHelpHtml('workspace'), value: '', placeholder: t('agents.agentWorkspacePlaceholder') }, ], onConfirm: async (result) => { const id = (result.id || '').trim() diff --git a/src/pages/gateway.js b/src/pages/gateway.js index 9adba90..012ca02 100644 --- a/src/pages/gateway.js +++ b/src/pages/gateway.js @@ -154,7 +154,7 @@ function renderConfig(page, state) {
- +
@@ -242,6 +242,7 @@ function renderConfig(page, state) { ` bindConfigEvents(el) + attachTermTooltips(el) } function bindConfigEvents(el) { diff --git a/src/style/components.css b/src/style/components.css index 1da764c..256bea6 100644 --- a/src/style/components.css +++ b/src/style/components.css @@ -232,6 +232,13 @@ font-weight: 400; } +.toast-structured .toast-action-btn { + align-self: flex-start; + margin-top: 8px; + font-size: var(--font-size-xs); + padding: 4px 10px; +} + .toast-structured .toast-raw { margin-top: 6px; }