feat: add Koa2 BFF server, CLI management, sessions CLI integration, and logs page

- Add Koa2 BFF layer for API proxy, file upload, session management
- Auto-check and enable api_server in ~/.hermes/config.yaml on startup
- Integrate sessions with Hermes CLI (list, get, delete)
- Add Logs page with level filtering, log file selection, and search
- Add CLI commands: start/stop/restart/status for daemon management
- Unify package.json for frontend and server dependencies
- Default port changed to 8648

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
ekko
2026-04-11 21:33:04 +08:00
parent a2f8f6aec5
commit ee9f56dfbd
25 changed files with 1613 additions and 713 deletions
+4 -72
View File
@@ -2,14 +2,10 @@ import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import type { ProxyOptions } from 'vite'
import { resolve } from 'path'
import type { IncomingMessage, ServerResponse } from 'http'
import { mkdir, writeFile } from 'fs/promises'
import { tmpdir } from 'os'
import { randomBytes } from 'crypto'
function createProxyConfig(): ProxyOptions {
return {
target: 'http://127.0.0.1:8642',
target: 'http://127.0.0.1:8648',
changeOrigin: true,
configure: (proxy) => {
proxy.on('proxyReq', (proxyReq) => {
@@ -25,74 +21,8 @@ function createProxyConfig(): ProxyOptions {
}
}
const UPLOAD_DIR = resolve(tmpdir(), 'hermes-uploads')
async function handleUpload(req: IncomingMessage, res: ServerResponse) {
if (req.method !== 'POST') {
res.writeHead(405, { 'Content-Type': 'application/json' })
res.end(JSON.stringify({ error: 'Method not allowed' }))
return
}
const contentType = req.headers['content-type'] || ''
if (!contentType.startsWith('multipart/form-data')) {
res.writeHead(400, { 'Content-Type': 'application/json' })
res.end(JSON.stringify({ error: 'Expected multipart/form-data' }))
return
}
try {
await mkdir(UPLOAD_DIR, { recursive: true })
const chunks: Buffer[] = []
for await (const chunk of req) chunks.push(chunk)
const body = Buffer.concat(chunks).toString()
const boundary = '--' + contentType.split('boundary=')[1]
const parts = body.split(boundary).slice(1, -1)
const results: { name: string; path: string }[] = []
for (const part of parts) {
const headerEnd = part.indexOf('\r\n\r\n')
if (headerEnd === -1) continue
const header = part.substring(0, headerEnd)
const data = part.substring(headerEnd + 4, part.length - 2)
const filenameMatch = header.match(/filename="([^"]+)"/)
if (!filenameMatch) continue
const filename = filenameMatch[1]
const ext = filename.includes('.') ? '.' + filename.split('.').pop() : ''
const savedName = randomBytes(8).toString('hex') + ext
const savedPath = resolve(UPLOAD_DIR, savedName)
await writeFile(savedPath, Buffer.from(data, 'binary'))
results.push({ name: filename, path: savedPath })
}
res.writeHead(200, { 'Content-Type': 'application/json' })
res.end(JSON.stringify({ files: results }))
} catch (err: any) {
res.writeHead(500, { 'Content-Type': 'application/json' })
res.end(JSON.stringify({ error: err.message }))
}
}
export default defineConfig({
plugins: [
vue(),
{
name: 'upload-middleware',
configureServer(server) {
server.middlewares.use((req, res, next) => {
if (req.url?.startsWith('/__upload')) {
handleUpload(req as any, res as any)
} else {
next()
}
})
},
},
],
plugins: [vue()],
resolve: {
alias: {
'@': resolve(__dirname, 'src'),
@@ -103,6 +33,8 @@ export default defineConfig({
'/api': createProxyConfig(),
'/v1': createProxyConfig(),
'/health': createProxyConfig(),
'/upload': createProxyConfig(),
'/webhook': createProxyConfig(),
},
},
})