From c4bea63a5ee0a9c1d0b20ebd33fd06e2f1e1b19f Mon Sep 17 00:00:00 2001 From: ekko <152005280+EKKOLearnAI@users.noreply.github.com> Date: Tue, 21 Apr 2026 19:39:02 +0800 Subject: [PATCH] docs: update CLAUDE.md to reflect current project architecture (#115) Sync CLAUDE.md with actual codebase structure: - Add controller layer (thin-router, fat-controller pattern) - Fix service paths (services/hermes/hermes-cli.ts) - Document 8 locales, Vitest testing, esbuild bundling - Add new modules: composables, profiles, gateways, codex-auth - Fix route registration (routes/index.ts, not routes/hermes/index.ts) - Add missing env vars (UPLOAD_DIR, CORS_ORIGINS, HERMES_BIN) - Update bootstrap sequence and auth middleware docs Also change docker-publish workflow to manual trigger only, and remove dev branch from build workflow trigger. Co-authored-by: Claude Opus 4.6 --- .github/workflows/build.yml | 1 - .github/workflows/docker-publish.yml | 6 +- CLAUDE.md | 215 +++++++++++++++++++-------- 3 files changed, 157 insertions(+), 65 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 6ebd2f7..7779df6 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -4,7 +4,6 @@ on: pull_request: branches: - main - - dev jobs: build: diff --git a/.github/workflows/docker-publish.yml b/.github/workflows/docker-publish.yml index 88afafc..ce02722 100644 --- a/.github/workflows/docker-publish.yml +++ b/.github/workflows/docker-publish.yml @@ -1,11 +1,7 @@ name: Build and Push Docker Image to Docker Hub -# 触发条件配置 on: - push: - branches: - - main - workflow_dispatch: # 允许在 GitHub 网页端手动点击按钮触发 + workflow_dispatch: # 仅手动触发 jobs: build-and-push: diff --git a/CLAUDE.md b/CLAUDE.md index 7b84b4a..d8b732d 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -20,8 +20,11 @@ The project is designed for **multi-agent extensibility** — Hermes is the firs npm run dev # Start both server (nodemon) and client (Vite) concurrently npm run dev:client # Vite dev server only (proxies API to backend) npm run dev:server # nodemon + ts-node for server only -npm run build # Type-check (vue-tsc) -> Vite build -> tsc server build +npm run build # Type-check (vue-tsc) -> Vite build -> esbuild server bundle npm run preview # Preview production build with Vite +npm run test # Run tests (vitest) +npm run test:watch # Run tests in watch mode +npm run test:coverage # Run tests with coverage report ``` - **Dev port:** 8648 (client Vite dev server proxies `/api`, `/v1`, `/health`, `/upload`, `/webhook` to `http://127.0.0.1:8648`) @@ -36,7 +39,7 @@ hermes-web-ui/ ├── bin/ # CLI entry point (bin/hermes-web-ui.mjs) ├── dist/ # Build output │ ├── client/ # Vite frontend build -│ └── server/ # tsc server build +│ └── server/ # esbuild server bundle ├── packages/ │ ├── client/src/ # Vue 3 frontend │ │ ├── api/ # API layer @@ -45,20 +48,27 @@ hermes-web-ui/ │ │ │ ├── chat.ts # Gateway proxy: runs, SSE events, models │ │ │ ├── jobs.ts # Gateway proxy: scheduled jobs CRUD │ │ │ ├── sessions.ts # Local BFF: session management (wraps hermes CLI) +│ │ │ ├── profiles.ts # Local BFF: profile management (wraps hermes CLI) │ │ │ ├── config.ts # Local BFF: app config, weixin credentials │ │ │ ├── logs.ts # Local BFF: log file listing & reading │ │ │ ├── skills.ts # Local BFF: skills listing, memory CRUD +│ │ │ ├── gateways.ts # Local BFF: gateway management +│ │ │ ├── codex-auth.ts # Local BFF: Codex authentication │ │ │ └── system.ts # Local BFF: health, model config, providers +│ │ ├── composables/ # Vue composables +│ │ │ ├── useKeyboard.ts # Keyboard shortcut composable +│ │ │ └── useTheme.ts # Theme management composable │ │ ├── components/ # Vue components -│ │ │ ├── layout/ # Shared: AppSidebar, LanguageSwitch, ModelSelector +│ │ │ ├── layout/ # Shared: AppSidebar, LanguageSwitch, ModelSelector, ProfileSelector, ThemeSwitch │ │ │ └── hermes/ # Hermes-specific components -│ │ │ ├── chat/ # ChatPanel, ChatInput, MessageList, MarkdownRenderer +│ │ │ ├── chat/ # ChatPanel, ChatInput, MessageList, MessageItem, MarkdownRenderer │ │ │ ├── jobs/ # JobCard, JobFormModal, JobsPanel -│ │ │ ├── models/ # ProviderCard, ProviderFormModal, ProvidersPanel -│ │ │ ├── settings/ # AgentSettings, DisplaySettings, MemorySettings, etc. +│ │ │ ├── models/ # ProviderCard, ProviderFormModal, ProvidersPanel, CodexLoginModal +│ │ │ ├── profiles/ # ProfileCard, ProfileCreateModal, ProfileImportModal, ProfileRenameModal, ProfilesPanel +│ │ │ ├── settings/ # AgentSettings, DisplaySettings, MemorySettings, ModelSettings, PlatformCard, PlatformSettings, PrivacySettings, SessionSettings, SettingRow │ │ │ ├── skills/ # SkillList, SkillDetail │ │ │ └── usage/ # StatCards, DailyTrend, ModelBreakdown -│ │ ├── i18n/locales/ # en.ts, zh.ts +│ │ ├── i18n/locales/ # en, zh, de, es, fr, ja, ko, pt │ │ ├── router/index.ts # vue-router (hash history) │ │ ├── stores/ # Pinia stores │ │ │ └── hermes/ # Hermes-specific stores @@ -67,14 +77,18 @@ hermes-web-ui/ │ │ │ ├── jobs.ts # Scheduled jobs CRUD │ │ │ ├── models.ts # Model provider management │ │ │ ├── settings.ts # App configuration -│ │ │ └── usage.ts # Usage statistics -│ │ ├── styles/ # global.scss, variables.scss +│ │ │ ├── usage.ts # Usage statistics +│ │ │ ├── gateways.ts # Gateway management +│ │ │ └── profiles.ts # Profile management +│ │ ├── styles/ # global.scss, variables.scss, code-block.scss, theme.ts │ │ └── views/ # Page-level components │ │ ├── LoginView.vue # Shared: login page │ │ └── hermes/ # Hermes-specific pages │ │ ├── ChatView.vue │ │ ├── JobsView.vue │ │ ├── ModelsView.vue +│ │ ├── ProfilesView.vue +│ │ ├── GatewaysView.vue │ │ ├── LogsView.vue │ │ ├── UsageView.vue │ │ ├── SkillsView.vue @@ -83,30 +97,69 @@ hermes-web-ui/ │ │ ├── ChannelsView.vue │ │ └── TerminalView.vue │ ├── server/src/ # Koa BFF server -│ │ ├── routes/hermes/ # Route modules -│ │ │ ├── index.ts # Aggregates all hermes sub-routers -│ │ │ ├── sessions.ts # Session CRUD (wraps hermes CLI) -│ │ │ ├── profiles.ts # Profile management (wraps hermes CLI) -│ │ │ ├── config.ts # App config read/write -│ │ │ ├── filesystem.ts # Skills, memory, model config, providers -│ │ │ ├── logs.ts # Log file listing & reading -│ │ │ ├── weixin.ts # Weixin QR code & credentials -│ │ │ ├── terminal.ts # WebSocket terminal (node-pty) -│ │ │ ├── proxy.ts # Reverse proxy routes + middleware -│ │ │ └── proxy-handler.ts # Proxy forwarding logic -│ │ ├── routes/ # Shared routes +│ │ ├── controllers/ # Request handlers (thin routes delegate to controllers) +│ │ │ ├── health.ts # Health check +│ │ │ ├── update.ts # Update check │ │ │ ├── upload.ts # File upload -│ │ │ └── webhook.ts # Incoming webhooks +│ │ │ ├── webhook.ts # Incoming webhooks +│ │ │ └── hermes/ # Hermes-specific controllers +│ │ │ ├── sessions.ts # Session CRUD +│ │ │ ├── profiles.ts # Profile management +│ │ │ ├── config.ts # App config read/write +│ │ │ ├── gateways.ts # Gateway management +│ │ │ ├── codex-auth.ts # Codex authentication +│ │ │ ├── memory.ts # Memory CRUD +│ │ │ ├── models.ts # Model configuration +│ │ │ ├── providers.ts # Provider management +│ │ │ ├── skills.ts # Skills listing +│ │ │ ├── logs.ts # Log file listing & reading +│ │ │ └── weixin.ts # Weixin QR code & credentials +│ │ ├── routes/ # Route modules +│ │ │ ├── index.ts # Route registration (public → auth → protected) +│ │ │ ├── health.ts # Health route +│ │ │ ├── update.ts # Update route +│ │ │ ├── upload.ts # File upload route +│ │ │ ├── webhook.ts # Webhook route +│ │ │ └── hermes/ # Hermes-specific routes (thin wrappers) +│ │ │ ├── sessions.ts # Session routes +│ │ │ ├── profiles.ts # Profile routes +│ │ │ ├── config.ts # Config routes +│ │ │ ├── gateways.ts # Gateway routes +│ │ │ ├── codex-auth.ts # Codex auth routes +│ │ │ ├── memory.ts # Memory routes +│ │ │ ├── models.ts # Model routes +│ │ │ ├── providers.ts # Provider routes +│ │ │ ├── skills.ts # Skills routes +│ │ │ ├── logs.ts # Log routes +│ │ │ ├── weixin.ts # Weixin routes +│ │ │ ├── terminal.ts # WebSocket terminal (node-pty) +│ │ │ ├── proxy.ts # Reverse proxy routes + middleware +│ │ │ └── proxy-handler.ts # Proxy forwarding logic │ │ ├── services/ # Business logic -│ │ │ ├── hermes-cli.ts # Hermes CLI wrapper (child_process.execFile) │ │ │ ├── auth.ts # Auth middleware & token management -│ │ │ └── hermes.ts # Hermes gateway helpers +│ │ │ ├── config.ts # Server configuration +│ │ │ ├── config-helpers.ts # Config helper utilities +│ │ │ ├── logger.ts # Pino logger +│ │ │ ├── shutdown.ts # Graceful shutdown handler +│ │ │ ├── gateway-bootstrap.ts # Gateway manager initialization +│ │ │ └── hermes/ # Hermes-specific services +│ │ │ ├── hermes-cli.ts # Hermes CLI wrapper (child_process.execFile) +│ │ │ ├── hermes.ts # Hermes gateway helpers +│ │ │ ├── hermes-profile.ts # Profile management helpers +│ │ │ ├── gateway-manager.ts # Gateway process management +│ │ │ └── sessions-db.ts # SQLite-based session database │ │ ├── shared/providers.ts # Provider model catalogs -│ │ ├── config.ts # Server configuration │ │ └── index.ts # Bootstrap, middleware setup, SPA fallback │ └── client/src/shared/ # Frontend shared types (providers.ts) +├── scripts/ # Build scripts +│ ├── build-server.mjs # esbuild server bundling +│ └── setup.sh # Setup script +├── tests/ # Test files +│ ├── client/ # Frontend tests +│ └── server/ # Backend tests ├── package.json # Single package — no workspaces ├── vite.config.ts # root: packages/client, outDir: dist/client +├── vitest.config.ts # Vitest test configuration └── tsconfig.json # Root tsconfig (references for vue-tsc) ``` @@ -124,7 +177,10 @@ All agent-specific code lives under `{agent-name}/` subdirectories. Hermes is th | Components | `components/layout/` | `components/hermes/*/*.vue` | | Views | `views/LoginView.vue` | `views/hermes/*.vue` | | Stores | _(future: `stores/app.ts`)_ | `stores/hermes/*.ts` | -| Routes | `path: '/'` (login) | `path: '/hermes/*'`, `name: 'hermes.*'` | +| Controllers | `controllers/*.ts` | `controllers/hermes/*.ts` | +| Routes | `routes/*.ts` | `routes/hermes/*.ts` | +| Services | `services/*.ts` | `services/hermes/*.ts` | +| Routes (URL) | `path: '/'` (login) | `path: '/hermes/*'`, `name: 'hermes.*'` | | API paths | `/health`, `/upload`, `/webhook` | `/api/hermes/*` | When adding a new agent, create a new directory at each layer following the same pattern. @@ -132,7 +188,7 @@ When adding a new agent, create a new directory at each layer following the same ### Route Naming - **Shared routes:** `login` -- **Agent routes:** `{agent}.{page}` — e.g., `hermes.chat`, `hermes.jobs` +- **Agent routes:** `{agent}.{page}` — e.g., `hermes.chat`, `hermes.jobs`, `hermes.profiles`, `hermes.gateways` - **Route paths:** `/hermes/{page}` — e.g., `/hermes/chat`, `/hermes/jobs` --- @@ -188,6 +244,13 @@ Key patterns: - Use `useI18n()` for translations, access via `t('key.path')` - Scoped SCSS with `@use '@/styles/variables' as *` +### Vue Composables + +Shared composables live in `packages/client/src/composables/`: + +- `useKeyboard.ts` — keyboard shortcut handling +- `useTheme.ts` — theme management (dark/light mode) + ### Pinia Stores Use setup store syntax (function passed to `defineStore`): @@ -213,7 +276,7 @@ export const useMyStore = defineStore('myStore', () => { }) ``` -Existing stores in `packages/client/src/stores/hermes/`: `app`, `chat`, `jobs`, `models`, `settings`, `usage`. +Existing stores in `packages/client/src/stores/hermes/`: `app`, `chat`, `jobs`, `models`, `settings`, `usage`, `gateways`, `profiles`. ### API Layer @@ -244,7 +307,7 @@ export async function fetchSessions(source?: string, limit?: number): Promise { - const sessions = await hermesCli.listSessions() - ctx.body = { sessions } -}) +sessionRoutes.get('/api/hermes/sessions', ctrl.list) +sessionRoutes.get('/api/hermes/sessions/:id', ctrl.get) +sessionRoutes.delete('/api/hermes/sessions/:id', ctrl.remove) +sessionRoutes.post('/api/hermes/sessions/:id/rename', ctrl.rename) ``` **@koa/router v15 syntax** (path-to-regexp v8): @@ -333,9 +417,7 @@ Unmatched `/api/hermes/*` and `/v1/*` requests are forwarded to the upstream Her The proxy is implemented as both a route (`proxyRoutes.all('/api/hermes/{*any}', proxy)`) and a middleware (`proxyMiddleware`) registered on the main app to catch any requests that slip through route matching. -**Important:** Custom API endpoints handled locally (not proxied) must be registered **before** `hermesRoutes.routes()` in `bootstrap()`. The proxy route `proxyRoutes.all('/api/hermes/{*any}')` matches all `/api/hermes/*` paths, so any middleware registered after it will never be reached. See the `update` middleware in `index.ts` for an example. - -### Hermes CLI Wrapper (`packages/server/src/services/hermes-cli.ts`) +### Hermes CLI Wrapper (`packages/server/src/services/hermes/hermes-cli.ts`) All Hermes interactions go through `child_process.execFile('hermes', [...args])`. Each function wraps a CLI subcommand: @@ -353,9 +435,10 @@ CLI subcommands wrapped: `sessions export/delete/rename`, `profile list/show/cre ### Auth Middleware (`packages/server/src/services/auth.ts`) -- Token stored in `{dataDir}/.token` (auto-generated on first run), or set via `AUTH_TOKEN` env var +- Token stored in `~/.hermes-web-ui/.token` (auto-generated on first run), or set via `AUTH_TOKEN` env var - Auth disabled when `AUTH_DISABLED=1` -- Middleware skips `/health`, `/webhook`, and non-API paths +- Applied globally after public routes (health, webhook are registered before auth middleware) +- For non-API paths (static files), auth is skipped even if no valid token is provided - Accepts `Authorization: Bearer ` header or `?token=` query param --- @@ -363,9 +446,10 @@ CLI subcommands wrapped: `sessions export/delete/rename`, `profile list/show/cre ## Build System - **Vite** builds the frontend: root is `packages/client`, output goes to `dist/client` -- **tsc** compiles the server: config in `packages/server/tsconfig.json`, output goes to `dist/server` +- **esbuild** bundles the server via `scripts/build-server.mjs`: output goes to `dist/server` +- **tsc** type-checks both client (`vue-tsc -b`) and server (`tsc --noEmit`) - Path alias: `@` maps to `packages/client/src` -- Build command: `vue-tsc -b && vite build && tsc -p packages/server/tsconfig.json` +- Build command: `vue-tsc -b && vite build && tsc --noEmit -p packages/server/tsconfig.json && node scripts/build-server.mjs` - TypeScript strict mode enabled for both client and server --- @@ -409,7 +493,15 @@ Terminal uses a raw WebSocket at `/api/hermes/terminal` with JSON control messag ## Testing -No test framework is currently configured. The intention is to add tests in the future. +Tests use **Vitest** with `@vue/test-utils` and `@pinia/testing` for frontend, and `vitest` for backend: + +```bash +npm run test # Run all tests once +npm run test:watch # Watch mode +npm run test:coverage # With coverage report +``` + +Test files live in `tests/client/` and `tests/server/`. Configuration is in root `vitest.config.ts`. --- @@ -419,8 +511,11 @@ No test framework is currently configured. The intention is to add tests in the |---|---| | `AUTH_DISABLED` | Set to `1` or `true` to disable auth | | `AUTH_TOKEN` | Custom auth token (overrides auto-generated token) | -| `PORT` | Server listen port (default from config) | +| `PORT` | Server listen port (default `8648`) | | `UPSTREAM` | Hermes gateway URL (default `http://127.0.0.1:8642`) | +| `UPLOAD_DIR` | Custom upload directory path (default OS temp dir) | +| `CORS_ORIGINS` | CORS origin configuration (default `*`) | +| `HERMES_BIN` | Custom path to hermes CLI binary | --- @@ -431,15 +526,16 @@ No test framework is currently configured. The intention is to add tests in the 1. Create view component in `packages/client/src/views/hermes/MyView.vue` 2. Add route in `packages/client/src/router/index.ts` with name `hermes.myPage` and path `/hermes/my-page` 3. Add sidebar entry in `packages/client/src/components/layout/AppSidebar.vue` with `handleNav('hermes.myPage')` -4. Add i18n keys to both `en.ts` and `zh.ts` +4. Add i18n keys to all locale files in `packages/client/src/i18n/locales/` ### Add a new Hermes API endpoint -1. Add the route handler in `packages/server/src/routes/hermes/` (new or existing module) -2. If it calls Hermes CLI, add a wrapper function in `packages/server/src/services/hermes-cli.ts` -3. Register the route in `packages/server/src/routes/hermes/index.ts` via `hermesRoutes.use(myRoutes.routes())` -4. Add the frontend API function in `packages/client/src/api/hermes/` -5. If the endpoint should be proxied to the upstream gateway (not handled locally), ensure the path starts with `/api/hermes/` — the `proxyMiddleware` will catch it automatically +1. Add the controller in `packages/server/src/controllers/hermes/` (request handling logic) +2. Add the route in `packages/server/src/routes/hermes/` (thin URL-to-controller mapping) +3. Register the route in `packages/server/src/routes/index.ts` in the protected routes section +4. If it calls Hermes CLI, add a wrapper function in `packages/server/src/services/hermes/hermes-cli.ts` +5. Add the frontend API function in `packages/client/src/api/hermes/` +6. If the endpoint should be proxied to the upstream gateway (not handled locally), ensure the path starts with `/api/hermes/` — the `proxyMiddleware` will catch it automatically ### Add a new Hermes Pinia store @@ -449,6 +545,7 @@ No test framework is currently configured. The intention is to add tests in the ### Add a new agent integration 1. Create `api/{agent}/`, `components/{agent}/`, `views/{agent}/`, `stores/{agent}/` directories -2. Create `server/src/routes/{agent}/` for agent-specific backend routes +2. Create `controllers/{agent}/`, `routes/{agent}/`, `services/{agent}/` for agent-specific backend 3. Add routes with `path: '/{agent}/*'` and `name: '{agent}.*'` in the router -4. Follow the same patterns as the Hermes integration +4. Register routes in `routes/index.ts` following the public → auth → protected pattern +5. Follow the same patterns as the Hermes integration