diff --git a/README.md b/README.md index c38891a..29ac1a2 100644 --- a/README.md +++ b/README.md @@ -174,7 +174,7 @@ docker compose logs -f hermes-webui Open **http://localhost:6060** - Persistent Hermes data is stored in `./hermes_data` -- Web UI auth token is stored in `./hermes_data/hermes-web-ui-data/.token` +- Web UI auth token is stored in `./hermes_data/hermes-web-ui/.token` - On first run with auth enabled, the token is printed to container logs - All runtime settings are environment-variable driven in `docker-compose.yml` diff --git a/README_zh.md b/README_zh.md index 4b4af86..cd180b9 100644 --- a/README_zh.md +++ b/README_zh.md @@ -175,7 +175,7 @@ docker compose logs -f hermes-webui 打开 **http://localhost:6060** - Hermes 持久化数据目录:`./hermes_data` -- Web UI 认证 Token 存储在 `./hermes_data/hermes-web-ui-data/.token` +- Web UI 认证 Token 存储在 `./hermes_data/hermes-web-ui/.token` - 首次启动并开启认证时,Token 会打印到容器日志中 - 运行参数全部由 `docker-compose.yml` 环境变量驱动 diff --git a/bin/hermes-web-ui.mjs b/bin/hermes-web-ui.mjs index e27ceb8..b58e36d 100755 --- a/bin/hermes-web-ui.mjs +++ b/bin/hermes-web-ui.mjs @@ -14,7 +14,7 @@ const VERSION = pkg.version const PID_DIR = resolve(homedir(), '.hermes-web-ui') const PID_FILE = join(PID_DIR, 'server.pid') const LOG_FILE = join(PID_DIR, 'server.log') -const TOKEN_FILE = resolve(__dirname, '..', 'dist', 'server', 'data', '.token') +const TOKEN_FILE = join(PID_DIR, '.token') const DEFAULT_PORT = 8648 // ─── Auto-fix node-pty native module ────────────────────────── diff --git a/docker-compose.yml b/docker-compose.yml index 36d22e4..5eb5d92 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -28,7 +28,7 @@ services: - "${PORT:-6060}:${PORT:-6060}" volumes: - ${HERMES_DATA_DIR:-./hermes_data}:/home/agent/.hermes - - ${HERMES_DATA_DIR:-./hermes_data}/hermes-web-ui-data:/app/dist/data + - ${HERMES_DATA_DIR:-./hermes_data}/hermes-web-ui:/root/.hermes-web-ui - hermes-agent-src:/opt/hermes environment: - PORT=${PORT:-6060} diff --git a/docs/docker.md b/docs/docker.md index ab327ca..da95081 100644 --- a/docs/docker.md +++ b/docs/docker.md @@ -62,11 +62,11 @@ AUTH_DISABLED=false | Path | Description | |---|---| | `${HERMES_DATA_DIR}` (`./hermes_data`) | Hermes runtime data (sessions, config, profiles) | -| `${HERMES_DATA_DIR}/hermes-web-ui-data` | Web UI data (auth token) | +| `${HERMES_DATA_DIR}/hermes-web-ui` | Web UI data (auth token, etc.) | - Hermes data persists in `./hermes_data`, mapped to `/home/agent/.hermes` in the container. -- Web UI auth token persists in `./hermes_data/hermes-web-ui-data/.token`. -- When `AUTH_DISABLED=false`, the token is auto-generated on first run and printed to container logs. +- Web UI data persists in `./hermes_data/hermes-web-ui/`, mapped to `/root/.hermes-web-ui` in the container. +- When `AUTH_DISABLED=false`, the auth token is auto-generated on first run and printed to container logs. - Deleting the token file and restarting will generate a new one. ## Port Mapping @@ -96,7 +96,7 @@ View auth token: ```bash docker compose logs hermes-webui | grep token # or -cat ./hermes_data/hermes-web-ui-data/.token +cat ./hermes_data/hermes-web-ui/.token ``` Stop: diff --git a/package.json b/package.json index 10c0ec8..46c858b 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "hermes-web-ui", - "version": "0.4.0", + "version": "0.4.1", "description": "Web dashboard for Hermes Agent — multi-platform AI chat, session management, scheduled jobs, usage analytics & channel configuration (Telegram, Discord, Slack, WhatsApp)", "repository": { "type": "git", @@ -9,7 +9,7 @@ "homepage": "https://github.com/EKKOLearnAI/hermes-web-ui", "license": "MIT", "engines": { - "node": ">=20.0.0" + "node": ">=23.0.0" }, "keywords": [ "hermes", @@ -37,7 +37,7 @@ "dev": "concurrently \"npm run dev:server\" \"npm run dev:client\"", "dev:client": "vite --host", "dev:server": "nodemon --signal SIGTERM --watch packages/server/src -e ts,tsx --exec node -r ts-node/register packages/server/src/index.ts", - "build": "vue-tsc -b && vite build && tsc -p packages/server/tsconfig.json", + "build": "vue-tsc -b && vite build && tsc --noEmit -p packages/server/tsconfig.json && node scripts/build-server.mjs", "preview": "vite preview", "test": "vitest run", "test:watch": "vitest", @@ -48,29 +48,12 @@ "dist/" ], "dependencies": { + "node-pty": "^1.1.0" + }, + "devDependencies": { "@koa/bodyparser": "^5.0.0", "@koa/cors": "^5.0.0", "@koa/router": "^15.4.0", - "@xterm/addon-fit": "^0.11.0", - "@xterm/addon-web-links": "^0.12.0", - "@xterm/xterm": "^6.0.0", - "axios": "^1.9.0", - "highlight.js": "^11.11.1", - "js-yaml": "^4.1.1", - "koa": "^2.15.3", - "koa-send": "^5.0.1", - "koa-static": "^5.0.0", - "markdown-it": "^14.1.1", - "naive-ui": "^2.44.1", - "node-pty": "^1.1.0", - "pinia": "^3.0.4", - "qrcode": "^1.5.4", - "vue": "^3.5.32", - "vue-i18n": "^11.3.2", - "vue-router": "^4.6.4", - "ws": "^8.20.0" - }, - "devDependencies": { "@pinia/testing": "^1.0.3", "@types/js-yaml": "^4.0.9", "@types/koa": "^2.15.0", @@ -85,14 +68,33 @@ "@vitejs/plugin-vue": "^6.0.5", "@vue/test-utils": "^2.4.6", "@vue/tsconfig": "^0.9.1", + "@xterm/addon-fit": "^0.11.0", + "@xterm/addon-web-links": "^0.12.0", + "@xterm/xterm": "^6.0.0", + "axios": "^1.9.0", "concurrently": "^9.2.1", + "highlight.js": "^11.11.1", + "js-yaml": "^4.1.1", "jsdom": "^27.0.1", + "koa": "^2.15.3", + "koa-send": "^5.0.1", + "koa-static": "^5.0.0", + "markdown-it": "^14.1.1", + "naive-ui": "^2.44.1", "nodemon": "^3.1.14", + "pinia": "^3.0.4", + "pino": "^10.3.1", + "pino-pretty": "^13.1.3", + "qrcode": "^1.5.4", "sass": "^1.99.0", "ts-node": "^10.9.2", "typescript": "~6.0.2", "vite": "^8.0.4", "vitest": "^3.2.4", - "vue-tsc": "^3.2.6" + "vue": "^3.5.32", + "vue-i18n": "^11.3.2", + "vue-router": "^4.6.4", + "vue-tsc": "^3.2.6", + "ws": "^8.20.0" } } \ No newline at end of file diff --git a/packages/client/src/components/hermes/chat/MarkdownRenderer.vue b/packages/client/src/components/hermes/chat/MarkdownRenderer.vue index c28fce7..bb2b5cd 100644 --- a/packages/client/src/components/hermes/chat/MarkdownRenderer.vue +++ b/packages/client/src/components/hermes/chat/MarkdownRenderer.vue @@ -2,7 +2,7 @@ import { computed } from 'vue' import { useI18n } from 'vue-i18n' import MarkdownIt from 'markdown-it' -import hljs from 'highlight.js' +import { handleCodeBlockCopyClick, renderHighlightedCodeBlock } from './highlight' const props = defineProps<{ content: string }>() const { t } = useI18n() @@ -12,22 +12,19 @@ const md: MarkdownIt = new MarkdownIt({ linkify: true, typographer: true, highlight(str: string, lang: string): string { - if (lang && hljs.getLanguage(lang)) { - try { - return `
` - } catch { - // fall through - } - } - return `${lang}${hljs.highlight(str, { language: lang, ignoreIllegals: true }).value}
${md.utils.escapeHtml(str)}`
+ return renderHighlightedCodeBlock(str, lang, t('common.copy'))
},
})
const renderedHtml = computed(() => md.render(props.content))
+
+function handleMarkdownClick(event: MouseEvent): void {
+ void handleCodeBlockCopyClick(event)
+}
-
+
diff --git a/packages/client/src/components/hermes/chat/MessageItem.vue b/packages/client/src/components/hermes/chat/MessageItem.vue
index 6f50254..ffd6a19 100644
--- a/packages/client/src/components/hermes/chat/MessageItem.vue
+++ b/packages/client/src/components/hermes/chat/MessageItem.vue
@@ -3,6 +3,13 @@ import type { Message } from "@/stores/hermes/chat";
import { computed, ref } from "vue";
import { useI18n } from "vue-i18n";
import MarkdownRenderer from "./MarkdownRenderer.vue";
+import {
+ copyTextToClipboard,
+ handleCodeBlockCopyClick,
+ renderHighlightedCodeBlock,
+} from "./highlight";
+
+const TOOL_PAYLOAD_DISPLAY_LIMIT = 2000;
const props = defineProps<{ message: Message }>();
const { t } = useI18n();
@@ -25,6 +32,66 @@ function formatSize(bytes: number): string {
return (bytes / (1024 * 1024)).toFixed(1) + " MB";
}
+type ToolPayload = {
+ full: string;
+ display: string;
+ language?: string;
+};
+
+function formatToolPayload(raw?: string): ToolPayload {
+ if (!raw) {
+ return { full: "", display: "" };
+ }
+
+ try {
+ const full = JSON.stringify(JSON.parse(raw), null, 2);
+ return {
+ full,
+ display:
+ full.length > TOOL_PAYLOAD_DISPLAY_LIMIT
+ ? full.slice(0, TOOL_PAYLOAD_DISPLAY_LIMIT) + "\n" + t("chat.truncated")
+ : full,
+ language: "json",
+ };
+ } catch {
+ return {
+ full: raw,
+ display:
+ raw.length > TOOL_PAYLOAD_DISPLAY_LIMIT
+ ? raw.slice(0, TOOL_PAYLOAD_DISPLAY_LIMIT) + "\n" + t("chat.truncated")
+ : raw,
+ };
+ }
+}
+
+function renderToolPayload(content: string, language?: string): string {
+ return renderHighlightedCodeBlock(content, language, t("common.copy"), {
+ maxHighlightLength: TOOL_PAYLOAD_DISPLAY_LIMIT,
+ });
+}
+
+async function handleToolDetailClick(event: MouseEvent): Promise{{ formattedToolArgs }}
+
{{ formattedToolResult }}
+
` +} + +export async function copyTextToClipboard(text: string): Promise${languageLabelHtml}${highlighted}