refactor: restructure project for multi-agent extensibility
- Migrate source to packages/client and packages/server directories - Namespace all Hermes-specific code under hermes/ subdirectories (api/hermes/, components/hermes/, views/hermes/, stores/hermes/) - Add hermes.* route names and /hermes/* path prefixes - Upgrade @koa/router to v15, adapt path-to-regexp v8 syntax - Fix proxy path rewriting: /api/hermes/v1/* → /v1/*, /api/hermes/* → /api/* - Fix frontend API paths to match backend /api/hermes/* routes - Fix WebSocket terminal path to /api/hermes/terminal - Add proxyMiddleware for reliable unmatched route proxying - Add profiles route module and hermes-cli profile commands - Update CLAUDE.md development guide with new architecture - Add Chinese README (README_zh.md) - Add Web Terminal feature to README Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,89 @@
|
||||
import { request, getBaseUrlValue, getApiKey } from '../client'
|
||||
|
||||
export interface ChatMessage {
|
||||
role: 'user' | 'assistant' | 'system'
|
||||
content: string
|
||||
}
|
||||
|
||||
export interface StartRunRequest {
|
||||
input: string | ChatMessage[]
|
||||
instructions?: string
|
||||
conversation_history?: ChatMessage[]
|
||||
session_id?: string
|
||||
model?: string
|
||||
}
|
||||
|
||||
export interface StartRunResponse {
|
||||
run_id: string
|
||||
status: string
|
||||
}
|
||||
|
||||
// SSE event types from /v1/runs/{id}/events
|
||||
export interface RunEvent {
|
||||
event: string
|
||||
run_id?: string
|
||||
delta?: string
|
||||
tool?: string
|
||||
name?: string
|
||||
preview?: string
|
||||
timestamp?: number
|
||||
error?: string
|
||||
}
|
||||
|
||||
export async function startRun(body: StartRunRequest): Promise<StartRunResponse> {
|
||||
return request<StartRunResponse>('/api/hermes/v1/runs', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(body),
|
||||
})
|
||||
}
|
||||
|
||||
export function streamRunEvents(
|
||||
runId: string,
|
||||
onEvent: (event: RunEvent) => void,
|
||||
onDone: () => void,
|
||||
onError: (err: Error) => void,
|
||||
) {
|
||||
const baseUrl = getBaseUrlValue()
|
||||
const token = getApiKey()
|
||||
const url = `${baseUrl}/api/hermes/v1/runs/${runId}/events${token ? `?token=${encodeURIComponent(token)}` : ''}`
|
||||
|
||||
let closed = false
|
||||
const source = new EventSource(url)
|
||||
|
||||
source.onmessage = (e) => {
|
||||
if (closed) return
|
||||
try {
|
||||
const parsed = JSON.parse(e.data)
|
||||
onEvent(parsed)
|
||||
|
||||
if (parsed.event === 'run.completed' || parsed.event === 'run.failed') {
|
||||
closed = true
|
||||
source.close()
|
||||
onDone()
|
||||
}
|
||||
} catch {
|
||||
onEvent({ event: 'message', delta: e.data })
|
||||
}
|
||||
}
|
||||
|
||||
source.onerror = () => {
|
||||
if (closed) return
|
||||
closed = true
|
||||
source.close()
|
||||
onError(new Error('SSE connection error'))
|
||||
}
|
||||
|
||||
// Return AbortController-compatible object
|
||||
return {
|
||||
abort: () => {
|
||||
if (!closed) {
|
||||
closed = true
|
||||
source.close()
|
||||
}
|
||||
},
|
||||
} as unknown as AbortController
|
||||
}
|
||||
|
||||
export async function fetchModels(): Promise<{ data: Array<{ id: string }> }> {
|
||||
return request('/api/hermes/v1/models')
|
||||
}
|
||||
@@ -0,0 +1,114 @@
|
||||
import { request } from '../client'
|
||||
|
||||
export interface DisplayConfig {
|
||||
compact?: boolean
|
||||
personality?: string
|
||||
resume_display?: string
|
||||
busy_input_mode?: string
|
||||
bell_on_complete?: boolean
|
||||
show_reasoning?: boolean
|
||||
streaming?: boolean
|
||||
inline_diffs?: boolean
|
||||
show_cost?: boolean
|
||||
skin?: string
|
||||
}
|
||||
|
||||
export interface AgentConfig {
|
||||
max_turns?: number
|
||||
gateway_timeout?: number
|
||||
restart_drain_timeout?: number
|
||||
service_tier?: string
|
||||
tool_use_enforcement?: string
|
||||
}
|
||||
|
||||
export interface MemoryConfig {
|
||||
memory_enabled?: boolean
|
||||
user_profile_enabled?: boolean
|
||||
memory_char_limit?: number
|
||||
user_char_limit?: number
|
||||
}
|
||||
|
||||
export interface SessionResetConfig {
|
||||
mode?: string
|
||||
idle_minutes?: number
|
||||
at_hour?: number
|
||||
}
|
||||
|
||||
export interface PrivacyConfig {
|
||||
redact_pii?: boolean
|
||||
}
|
||||
|
||||
export interface AppConfig {
|
||||
display?: DisplayConfig
|
||||
agent?: AgentConfig
|
||||
memory?: MemoryConfig
|
||||
session_reset?: SessionResetConfig
|
||||
privacy?: PrivacyConfig
|
||||
telegram?: Record<string, any>
|
||||
discord?: Record<string, any>
|
||||
slack?: Record<string, any>
|
||||
whatsapp?: Record<string, any>
|
||||
matrix?: Record<string, any>
|
||||
weixin?: Record<string, any>
|
||||
wecom?: Record<string, any>
|
||||
feishu?: Record<string, any>
|
||||
dingtalk?: Record<string, any>
|
||||
platforms?: Record<string, any>
|
||||
[key: string]: any
|
||||
}
|
||||
|
||||
export async function fetchConfig(sections?: string[]): Promise<AppConfig> {
|
||||
const query = sections ? `?sections=${sections.join(',')}` : ''
|
||||
return request<AppConfig>(`/api/hermes/config${query}`)
|
||||
}
|
||||
|
||||
export async function updateConfigSection(
|
||||
section: string,
|
||||
values: Record<string, any>,
|
||||
): Promise<void> {
|
||||
await request('/api/hermes/config', {
|
||||
method: 'PUT',
|
||||
body: JSON.stringify({ section, values }),
|
||||
})
|
||||
}
|
||||
|
||||
export async function saveCredentials(
|
||||
platform: string,
|
||||
values: Record<string, any>,
|
||||
): Promise<void> {
|
||||
await request('/api/hermes/config/credentials', {
|
||||
method: 'PUT',
|
||||
body: JSON.stringify({ platform, values }),
|
||||
})
|
||||
}
|
||||
|
||||
export interface WeixinQrCode {
|
||||
qrcode: string
|
||||
qrcode_url: string
|
||||
}
|
||||
|
||||
export interface WeixinQrStatus {
|
||||
status: 'wait' | 'scaned' | 'scaned_but_redirect' | 'expired' | 'confirmed'
|
||||
account_id?: string
|
||||
token?: string
|
||||
base_url?: string
|
||||
}
|
||||
|
||||
export async function fetchWeixinQrCode(): Promise<WeixinQrCode> {
|
||||
return request<WeixinQrCode>('/api/hermes/weixin/qrcode')
|
||||
}
|
||||
|
||||
export async function pollWeixinQrStatus(qrcode: string): Promise<WeixinQrStatus> {
|
||||
return request<WeixinQrStatus>(`/api/hermes/weixin/qrcode/status?qrcode=${encodeURIComponent(qrcode)}`)
|
||||
}
|
||||
|
||||
export async function saveWeixinCredentials(data: {
|
||||
account_id: string
|
||||
token: string
|
||||
base_url?: string
|
||||
}): Promise<void> {
|
||||
await request('/api/hermes/weixin/save', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(data),
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,100 @@
|
||||
import { request } from '../client'
|
||||
|
||||
export interface Job {
|
||||
job_id: string
|
||||
id: string
|
||||
name: string
|
||||
prompt: string
|
||||
prompt_preview?: string
|
||||
skills: string[]
|
||||
skill: string | null
|
||||
model: string | null
|
||||
provider: string | null
|
||||
base_url: string | null
|
||||
script: string | null
|
||||
schedule: string | { kind: string; expr: string; display: string }
|
||||
schedule_display: string
|
||||
repeat: string | { times: number | null; completed: number }
|
||||
enabled: boolean
|
||||
state: string
|
||||
paused_at: string | null
|
||||
paused_reason: string | null
|
||||
created_at: string
|
||||
next_run_at: string | null
|
||||
last_run_at: string | null
|
||||
last_status: string | null
|
||||
last_error: string | null
|
||||
deliver: string
|
||||
origin: {
|
||||
platform: string
|
||||
chat_id: string
|
||||
chat_name: string
|
||||
thread_id: string | null
|
||||
} | null
|
||||
last_delivery_error: string | null
|
||||
}
|
||||
|
||||
export interface CreateJobRequest {
|
||||
name: string
|
||||
schedule: string
|
||||
prompt?: string
|
||||
deliver?: string
|
||||
skills?: string[]
|
||||
repeat?: number
|
||||
}
|
||||
|
||||
export interface UpdateJobRequest {
|
||||
name?: string
|
||||
schedule?: string
|
||||
prompt?: string
|
||||
deliver?: string
|
||||
skills?: string[]
|
||||
skill?: string
|
||||
repeat?: number
|
||||
enabled?: boolean
|
||||
}
|
||||
|
||||
function unwrap(res: { job: Job }): Job {
|
||||
return res.job
|
||||
}
|
||||
|
||||
export async function listJobs(): Promise<Job[]> {
|
||||
const res = await request<{ jobs: Job[] }>('/api/hermes/jobs?include_disabled=true')
|
||||
return res.jobs
|
||||
}
|
||||
|
||||
export async function getJob(jobId: string): Promise<Job> {
|
||||
return unwrap(await request<{ job: Job }>(`/api/hermes/jobs/${jobId}`))
|
||||
}
|
||||
|
||||
export async function createJob(data: CreateJobRequest): Promise<Job> {
|
||||
return unwrap(await request<{ job: Job }>('/api/hermes/jobs', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(data),
|
||||
}))
|
||||
}
|
||||
|
||||
export async function updateJob(jobId: string, data: UpdateJobRequest): Promise<Job> {
|
||||
return unwrap(await request<{ job: Job }>(`/api/hermes/jobs/${jobId}`, {
|
||||
method: 'PATCH',
|
||||
body: JSON.stringify(data),
|
||||
}))
|
||||
}
|
||||
|
||||
export async function deleteJob(jobId: string): Promise<{ ok: boolean }> {
|
||||
return request<{ ok: boolean }>(`/api/hermes/jobs/${jobId}`, {
|
||||
method: 'DELETE',
|
||||
})
|
||||
}
|
||||
|
||||
export async function pauseJob(jobId: string): Promise<Job> {
|
||||
return unwrap(await request<{ job: Job }>(`/api/hermes/jobs/${jobId}/pause`, { method: 'POST' }))
|
||||
}
|
||||
|
||||
export async function resumeJob(jobId: string): Promise<Job> {
|
||||
return unwrap(await request<{ job: Job }>(`/api/hermes/jobs/${jobId}/resume`, { method: 'POST' }))
|
||||
}
|
||||
|
||||
export async function runJob(jobId: string): Promise<Job> {
|
||||
return unwrap(await request<{ job: Job }>(`/api/hermes/jobs/${jobId}/run`, { method: 'POST' }))
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
import { request } from '../client'
|
||||
|
||||
export interface LogFileInfo {
|
||||
name: string
|
||||
size: string
|
||||
modified: string
|
||||
}
|
||||
|
||||
export interface LogEntry {
|
||||
timestamp: string
|
||||
level: string
|
||||
logger: string
|
||||
message: string
|
||||
raw: string
|
||||
}
|
||||
|
||||
export async function fetchLogFiles(): Promise<LogFileInfo[]> {
|
||||
const res = await request<{ files: LogFileInfo[] }>('/api/hermes/logs')
|
||||
return res.files
|
||||
}
|
||||
|
||||
export async function fetchLogs(name: string, params?: {
|
||||
lines?: number
|
||||
level?: string
|
||||
session?: string
|
||||
since?: string
|
||||
}): Promise<LogEntry[]> {
|
||||
const query = new URLSearchParams()
|
||||
if (params?.lines) query.set('lines', String(params.lines))
|
||||
if (params?.level) query.set('level', params.level)
|
||||
if (params?.session) query.set('session', params.session)
|
||||
if (params?.since) query.set('since', params.since)
|
||||
const qs = query.toString()
|
||||
const res = await request<{ entries: (LogEntry | null)[] }>(`/api/hermes/logs/${name}${qs ? `?${qs}` : ''}`)
|
||||
return res.entries.filter((e): e is LogEntry => e !== null)
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
import { request } from '../client'
|
||||
|
||||
export interface SessionSummary {
|
||||
id: string
|
||||
source: string
|
||||
model: string
|
||||
title: string | null
|
||||
started_at: number
|
||||
ended_at: number | null
|
||||
message_count: number
|
||||
tool_call_count: number
|
||||
input_tokens: number
|
||||
output_tokens: number
|
||||
cache_read_tokens: number
|
||||
cache_write_tokens: number
|
||||
reasoning_tokens: number
|
||||
billing_provider: string | null
|
||||
estimated_cost_usd: number
|
||||
actual_cost_usd: number | null
|
||||
cost_status: string
|
||||
}
|
||||
|
||||
export interface SessionDetail extends SessionSummary {
|
||||
messages: HermesMessage[]
|
||||
}
|
||||
|
||||
export interface HermesMessage {
|
||||
id: number
|
||||
session_id: string
|
||||
role: 'user' | 'assistant' | 'system' | 'tool'
|
||||
content: string
|
||||
tool_call_id: string | null
|
||||
tool_calls: any[] | null
|
||||
tool_name: string | null
|
||||
timestamp: number
|
||||
token_count: number | null
|
||||
finish_reason: string | null
|
||||
reasoning: string | null
|
||||
}
|
||||
|
||||
export async function fetchSessions(source?: string, limit?: number): Promise<SessionSummary[]> {
|
||||
const params = new URLSearchParams()
|
||||
if (source) params.set('source', source)
|
||||
if (limit) params.set('limit', String(limit))
|
||||
const query = params.toString()
|
||||
const res = await request<{ sessions: SessionSummary[] }>(`/api/hermes/sessions${query ? `?${query}` : ''}`)
|
||||
return res.sessions
|
||||
}
|
||||
|
||||
export async function fetchSession(id: string): Promise<SessionDetail | null> {
|
||||
try {
|
||||
const res = await request<{ session: SessionDetail }>(`/api/hermes/sessions/${id}`)
|
||||
return res.session
|
||||
} catch {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
export async function deleteSession(id: string): Promise<boolean> {
|
||||
try {
|
||||
await request(`/api/hermes/sessions/${id}`, { method: 'DELETE' })
|
||||
return true
|
||||
} catch {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
export async function renameSession(id: string, title: string): Promise<boolean> {
|
||||
try {
|
||||
await request(`/api/hermes/sessions/${id}/rename`, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ title }),
|
||||
})
|
||||
return true
|
||||
} catch {
|
||||
return false
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
import { request } from '../client'
|
||||
|
||||
export interface SkillInfo {
|
||||
name: string
|
||||
description: string
|
||||
enabled?: boolean
|
||||
}
|
||||
|
||||
export interface SkillCategory {
|
||||
name: string
|
||||
description: string
|
||||
skills: SkillInfo[]
|
||||
}
|
||||
|
||||
export interface SkillListResponse {
|
||||
categories: SkillCategory[]
|
||||
}
|
||||
|
||||
export interface SkillFileEntry {
|
||||
path: string
|
||||
name: string
|
||||
isDir: boolean
|
||||
}
|
||||
|
||||
export interface MemoryData {
|
||||
memory: string
|
||||
user: string
|
||||
memory_mtime: number | null
|
||||
user_mtime: number | null
|
||||
}
|
||||
|
||||
export async function fetchSkills(): Promise<SkillCategory[]> {
|
||||
const res = await request<SkillListResponse>('/api/hermes/skills')
|
||||
return res.categories
|
||||
}
|
||||
|
||||
export async function fetchSkillContent(skillPath: string): Promise<string> {
|
||||
const res = await request<{ content: string }>(`/api/hermes/skills/${skillPath}`)
|
||||
return res.content
|
||||
}
|
||||
|
||||
export async function fetchSkillFiles(category: string, skill: string): Promise<SkillFileEntry[]> {
|
||||
const res = await request<{ files: SkillFileEntry[] }>(`/api/hermes/skills/${category}/${skill}/files`)
|
||||
return res.files
|
||||
}
|
||||
|
||||
export async function fetchMemory(): Promise<MemoryData> {
|
||||
return request<MemoryData>('/api/hermes/memory')
|
||||
}
|
||||
|
||||
export async function saveMemory(section: 'memory' | 'user', content: string): Promise<void> {
|
||||
await request('/api/hermes/memory', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ section, content }),
|
||||
})
|
||||
}
|
||||
|
||||
export async function toggleSkill(name: string, enabled: boolean): Promise<void> {
|
||||
await request('/api/hermes/skills/toggle', {
|
||||
method: 'PUT',
|
||||
body: JSON.stringify({ name, enabled }),
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
import { request } from '../client'
|
||||
|
||||
export interface HealthResponse {
|
||||
status: string
|
||||
version?: string
|
||||
}
|
||||
|
||||
// Config-based model types
|
||||
export interface ModelInfo {
|
||||
id: string
|
||||
label: string
|
||||
}
|
||||
|
||||
export interface ModelGroup {
|
||||
provider: string
|
||||
models: ModelInfo[]
|
||||
}
|
||||
|
||||
export interface ConfigModelsResponse {
|
||||
default: string
|
||||
groups: ModelGroup[]
|
||||
}
|
||||
|
||||
export interface AvailableModelGroup {
|
||||
provider: string // credential pool key (e.g. "zai", "custom:subrouter.ai")
|
||||
label: string // display name (e.g. "zai", "subrouter.ai")
|
||||
base_url: string
|
||||
models: string[]
|
||||
}
|
||||
|
||||
export interface AvailableModelsResponse {
|
||||
default: string
|
||||
groups: AvailableModelGroup[]
|
||||
}
|
||||
|
||||
export interface CustomProvider {
|
||||
name: string
|
||||
base_url: string
|
||||
api_key: string
|
||||
model: string
|
||||
providerKey?: string | null
|
||||
}
|
||||
|
||||
export async function checkHealth(): Promise<HealthResponse> {
|
||||
return request<HealthResponse>('/health')
|
||||
}
|
||||
|
||||
export async function fetchConfigModels(): Promise<ConfigModelsResponse> {
|
||||
return request<ConfigModelsResponse>('/api/hermes/config/models')
|
||||
}
|
||||
|
||||
export async function fetchAvailableModels(): Promise<AvailableModelsResponse> {
|
||||
return request<AvailableModelsResponse>('/api/hermes/available-models')
|
||||
}
|
||||
|
||||
export async function updateDefaultModel(data: {
|
||||
default: string
|
||||
provider?: string
|
||||
base_url?: string
|
||||
api_key?: string
|
||||
}): Promise<void> {
|
||||
await request('/api/hermes/config/model', {
|
||||
method: 'PUT',
|
||||
body: JSON.stringify(data),
|
||||
})
|
||||
}
|
||||
|
||||
export async function addCustomProvider(data: CustomProvider): Promise<void> {
|
||||
await request('/api/hermes/config/providers', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(data),
|
||||
})
|
||||
}
|
||||
|
||||
export async function removeCustomProvider(name: string): Promise<void> {
|
||||
await request(`/api/hermes/config/providers/${encodeURIComponent(name)}`, {
|
||||
method: 'DELETE',
|
||||
})
|
||||
}
|
||||
Reference in New Issue
Block a user