fix: v1.3.0 compatibility fixes for hermes-agent
- Fix chat command args: use '-q --source tool' for non-interactive mode in send_session_message to avoid entering interactive CLI - Fix auth_method KeyError: add 'auth_method: header' default in fallback config - Persist session registry to data/sessions.json so sessions survive restarts - Dynamic path resolution via get_hermes_home() instead of hardcoded paths - Update README with full API documentation (v1.3.0) - Add .gitignore for data/, __pycache__/, .venv/
This commit is contained in:
parent
57afe1264c
commit
2152ae9d40
5
.gitignore
vendored
Normal file
5
.gitignore
vendored
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
data/
|
||||||
|
__pycache__/
|
||||||
|
*.pyc
|
||||||
|
*.pyo
|
||||||
|
.venv/
|
||||||
274
README.md
274
README.md
@ -1,24 +1,258 @@
|
|||||||
# Hermes Service Web Application
|
# Hermes Service API
|
||||||
|
|
||||||
## Overview
|
Hermes Service 是一个将 Hermes Agent CLI 能力封装为 REST API 的中间件服务。支持多用户隔离、会话持久化、IP/API Key 安全认证。
|
||||||
Hermes Service is a web application that provides API access to Hermes Agent functionality. It runs in the Hermes Agent environment and exposes CLI capabilities through REST/WebSocket APIs.
|
|
||||||
|
|
||||||
## Features
|
**版本**: v1.3.0
|
||||||
- Execute Hermes CLI commands via API
|
**运行端口**: 默认 9123 (可通过 config.yaml 修改)
|
||||||
- Manage multiple Hermes service instances
|
|
||||||
- User and organization-based access control (using rbac module)
|
|
||||||
- Real-time communication via WebSocket
|
|
||||||
- Configuration management
|
|
||||||
|
|
||||||
## Integration
|
---
|
||||||
This module integrates with:
|
|
||||||
- **rbac**: For user and permission management
|
|
||||||
- **appbase**: For system parameter management
|
|
||||||
- **Hermes Agent**: Core functionality execution
|
|
||||||
|
|
||||||
## Directory Structure
|
## 启动方式
|
||||||
- `hermes-service/`: Python package with core logic
|
|
||||||
- `wwwroot/`: Frontend components (bricks-framework .ui files)
|
```bash
|
||||||
- `models/`: Database table definitions
|
cd ~/repos/hermes-service
|
||||||
- `json/`: CRUD operation definitions
|
python3 main.py
|
||||||
- `init/`: Initialization data
|
# 或使用 uvicorn
|
||||||
|
uvicorn main:app --host 127.0.0.1 --port 9123
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 认证机制
|
||||||
|
|
||||||
|
所有 API 端点均受 `validate_ip_and_apikey()` 装饰器保护:
|
||||||
|
|
||||||
|
1. **IP 白名单**:当 `security.enable_ip_check: true` 时,仅允许 `allowed_ips` 中的 IP 访问
|
||||||
|
2. **API Key**:当 `security.enable_api_key: true` 时,需在请求头中提供有效 API Key
|
||||||
|
|
||||||
|
### API Key 传递方式
|
||||||
|
|
||||||
|
| auth_method | 请求头 | 示例 |
|
||||||
|
|-------------|--------|------|
|
||||||
|
| `header` (默认) | `X-API-Key: <key>` | `X-API-Key: 5ftyuvhfhi3345` |
|
||||||
|
| `bearer` | `Authorization: Bearer <key>` | `Authorization: Bearer 5ftyuvhfhi3345` |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## API 端点清单
|
||||||
|
|
||||||
|
### 1. 健康检查
|
||||||
|
|
||||||
|
```
|
||||||
|
GET /health
|
||||||
|
```
|
||||||
|
|
||||||
|
**响应**:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"status": "healthy",
|
||||||
|
"service": "hermes-service",
|
||||||
|
"multi_user": true
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. 获取 Hermes 状态
|
||||||
|
|
||||||
|
```
|
||||||
|
GET /api/v1/status
|
||||||
|
```
|
||||||
|
|
||||||
|
**响应**:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"status": "running",
|
||||||
|
"version": "Hermes Agent v0.11.0 (2026.4.23)"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. 创建会话
|
||||||
|
|
||||||
|
```
|
||||||
|
POST /api/v1/sessions
|
||||||
|
Content-Type: application/json
|
||||||
|
|
||||||
|
{
|
||||||
|
"user_id": "user123"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**请求体**:
|
||||||
|
|
||||||
|
| 字段 | 类型 | 必填 | 说明 |
|
||||||
|
|------|------|------|------|
|
||||||
|
| `user_id` | string | 是 | 用户唯一标识 |
|
||||||
|
| `initial_message` | string | 否 | 初始消息(暂未使用) |
|
||||||
|
|
||||||
|
**响应**:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"session_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
|
||||||
|
"user_id": "user123",
|
||||||
|
"hermes_path": "/d/hermesai/users/user123/hermes-agent",
|
||||||
|
"status": "created"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**说明**: 首次为该用户创建会话时,会自动从 BASE_HERMES_PATH 复制完整环境到 `/d/hermesai/users/{user_id}/`,实现用户隔离。
|
||||||
|
|
||||||
|
### 4. 执行任意命令
|
||||||
|
|
||||||
|
```
|
||||||
|
POST /api/v1/execute
|
||||||
|
Content-Type: application/json
|
||||||
|
|
||||||
|
{
|
||||||
|
"command": ["chat", "-q", "Hello", "--source", "tool"],
|
||||||
|
"user_context": {"user_id": "user123"},
|
||||||
|
"timeout": 300
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**请求体**:
|
||||||
|
|
||||||
|
| 字段 | 类型 | 必填 | 说明 |
|
||||||
|
|------|------|------|------|
|
||||||
|
| `command` | list[string] | 是 | Hermes CLI 命令参数列表 |
|
||||||
|
| `user_context` | object | 否 | 用户上下文,含 `user_id` 时使用隔离环境 |
|
||||||
|
| `timeout` | int | 否 | 超时秒数,默认 300 |
|
||||||
|
|
||||||
|
**响应**:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"success": true,
|
||||||
|
"stdout": "Hello! How can I help you today?",
|
||||||
|
"stderr": "",
|
||||||
|
"returncode": 0
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 5. 发送会话消息
|
||||||
|
|
||||||
|
```
|
||||||
|
POST /api/v1/sessions/{session_id}/messages
|
||||||
|
Content-Type: application/json
|
||||||
|
|
||||||
|
{
|
||||||
|
"message": "What is Python?"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**请求体**:
|
||||||
|
|
||||||
|
| 字段 | 类型 | 必填 | 说明 |
|
||||||
|
|------|------|------|------|
|
||||||
|
| `message` | string | 是 | 发送给 AI 的消息内容 |
|
||||||
|
| `user_context` | object | 否 | 用户上下文(暂未使用,从 session 获取) |
|
||||||
|
|
||||||
|
**响应**:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"session_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
|
||||||
|
"response": "Python is a high-level, general-purpose programming language...",
|
||||||
|
"success": true
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**说明**: 内部使用 `hermes chat -q <message> --source tool` 非交互模式执行,确保返回单一响应而非进入交互会话。`--source tool` 标记使会话不会出现在用户的常规 CLI 会话列表中。
|
||||||
|
|
||||||
|
### 6. 获取会话信息
|
||||||
|
|
||||||
|
```
|
||||||
|
GET /api/v1/sessions/{session_id}
|
||||||
|
```
|
||||||
|
|
||||||
|
**响应**:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"user_id": "user123",
|
||||||
|
"local_session_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
|
||||||
|
"created_at": "2026-04-25T11:30:00.000000",
|
||||||
|
"status": "active"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 会话持久化
|
||||||
|
|
||||||
|
会话数据持久化存储在 `data/sessions.json` 文件中,服务重启后可恢复已有会话。
|
||||||
|
|
||||||
|
**存储路径**: `<service_dir>/data/sessions.json`
|
||||||
|
**格式**: JSON
|
||||||
|
**线程安全**: 使用 threading.Lock 保护写入操作
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 路径解析机制
|
||||||
|
|
||||||
|
Hermes 基础路径通过以下优先级动态解析:
|
||||||
|
|
||||||
|
1. `HERMES_HOME` 环境变量(最高优先级,用于测试/开发)
|
||||||
|
2. `hermes_constants.get_hermes_home()`(从 hermes-agent 模块导入)
|
||||||
|
3. 默认回退路径 `/d/hermesai/.hermes`
|
||||||
|
|
||||||
|
这使得服务能自适应 hermes-agent 的位置变化,无需硬编码路径。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 配置文件 (config.yaml)
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
security:
|
||||||
|
enable_ip_check: true # 启用 IP 白名单
|
||||||
|
allowed_ips: # 允许的 IP / CIDR
|
||||||
|
- "127.0.0.1"
|
||||||
|
- "8.222.165.87"
|
||||||
|
enable_api_key: true # 启用 API Key 认证
|
||||||
|
auth_method: "header" # 认证方式: "header" 或 "bearer"
|
||||||
|
api_key_header: "X-API-Key" # API Key 请求头名称
|
||||||
|
api_keys:
|
||||||
|
- key: "your-api-key"
|
||||||
|
description: "Production key"
|
||||||
|
expires_at: null
|
||||||
|
|
||||||
|
nginx:
|
||||||
|
trusted_proxies:
|
||||||
|
- "127.0.0.1"
|
||||||
|
enable_real_ip: false # 从 X-Forwarded-For 提取真实 IP
|
||||||
|
|
||||||
|
service:
|
||||||
|
host: "127.0.0.1"
|
||||||
|
port: 9123
|
||||||
|
log_level: "info"
|
||||||
|
|
||||||
|
cors:
|
||||||
|
allow_origins: ["*"]
|
||||||
|
allow_credentials: true
|
||||||
|
allow_methods: ["*"]
|
||||||
|
allow_headers: ["*"]
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 用户隔离
|
||||||
|
|
||||||
|
每个用户拥有独立的 Hermes 环境:
|
||||||
|
|
||||||
|
```
|
||||||
|
/d/hermesai/users/
|
||||||
|
user123/
|
||||||
|
hermes-agent/ # 完整 Hermes Agent 副本
|
||||||
|
.hermes/ # 用户配置和会话数据
|
||||||
|
user456/
|
||||||
|
hermes-agent/
|
||||||
|
.hermes/
|
||||||
|
```
|
||||||
|
|
||||||
|
首次使用时自动创建,`.venv` 通过软链接共享以节省磁盘空间。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## v1.3.0 变更日志
|
||||||
|
|
||||||
|
| 修复项 | 说明 |
|
||||||
|
|--------|------|
|
||||||
|
| chat 命令参数 | `send_session_message` 改用 `-q --source tool` 非交互模式,避免进入交互 CLI |
|
||||||
|
| auth_method 默认值 | 默认配置中增加 `auth_method: 'header'`,防止启用 API Key 时抛 KeyError |
|
||||||
|
| 会话持久化 | 新增 `data/sessions.json` 持久化,服务重启后会话不丢失 |
|
||||||
|
| 动态路径解析 | 通过 `_resolve_hermes_home()` 动态获取 Hermes 路径,替代硬编码路径 |
|
||||||
|
|||||||
81
main.py
81
main.py
@ -7,6 +7,8 @@ import os
|
|||||||
import sys
|
import sys
|
||||||
import asyncio
|
import asyncio
|
||||||
import uuid
|
import uuid
|
||||||
|
import threading
|
||||||
|
import json as json_mod
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from fastapi import FastAPI, WebSocket, WebSocketDisconnect, HTTPException, Request
|
from fastapi import FastAPI, WebSocket, WebSocketDisconnect, HTTPException, Request
|
||||||
@ -19,8 +21,34 @@ import ipaddress
|
|||||||
import yaml
|
import yaml
|
||||||
from functools import wraps
|
from functools import wraps
|
||||||
|
|
||||||
# Base Hermes Agent path
|
# ---------------------------------------------------------------------------
|
||||||
BASE_HERMES_PATH = "/d/hermesai/.hermes/hermes-agent"
|
# Resolve Hermes base path dynamically via get_hermes_home() when available,
|
||||||
|
# with fallback to the hardcoded default for backward compatibility.
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
def _resolve_hermes_home() -> str:
|
||||||
|
"""Resolve the Hermes home directory.
|
||||||
|
|
||||||
|
Tries:
|
||||||
|
1. HERMES_HOME env var (explicit override)
|
||||||
|
2. get_hermes_home() from hermes_constants module
|
||||||
|
3. Fallback to the default path
|
||||||
|
"""
|
||||||
|
env_override = os.environ.get("HERMES_HOME")
|
||||||
|
if env_override:
|
||||||
|
return env_override
|
||||||
|
try:
|
||||||
|
# Add the hermes-agent directory to the Python path so we can import
|
||||||
|
# from the bundled agent without installing it.
|
||||||
|
default_path = "/d/hermesai/.hermes/hermes-agent"
|
||||||
|
if default_path not in sys.path:
|
||||||
|
sys.path.insert(0, default_path)
|
||||||
|
from hermes_constants import get_hermes_home
|
||||||
|
return get_hermes_home()
|
||||||
|
except Exception:
|
||||||
|
return "/d/hermesai/.hermes"
|
||||||
|
|
||||||
|
HERMES_HOME = _resolve_hermes_home()
|
||||||
|
BASE_HERMES_PATH = os.path.join(HERMES_HOME, "hermes-agent")
|
||||||
|
|
||||||
# Clean user data directory structure: /d/hermesai/users/{user_id}/.hermes
|
# Clean user data directory structure: /d/hermesai/users/{user_id}/.hermes
|
||||||
USERS_BASE = "/d/hermesai/users"
|
USERS_BASE = "/d/hermesai/users"
|
||||||
@ -38,7 +66,8 @@ else:
|
|||||||
'allowed_ips': ['127.0.0.1', '::1'],
|
'allowed_ips': ['127.0.0.1', '::1'],
|
||||||
'enable_api_key': False,
|
'enable_api_key': False,
|
||||||
'api_keys': [],
|
'api_keys': [],
|
||||||
'api_key_header': 'X-API-Key'
|
'api_key_header': 'X-API-Key',
|
||||||
|
'auth_method': 'header' # 'header' or 'bearer'
|
||||||
},
|
},
|
||||||
'nginx': {
|
'nginx': {
|
||||||
'trusted_proxies': ['127.0.0.1', '::1'],
|
'trusted_proxies': ['127.0.0.1', '::1'],
|
||||||
@ -59,7 +88,7 @@ else:
|
|||||||
|
|
||||||
print(f"Security config - IP check: {config['security']['enable_ip_check']}, API key: {config['security']['enable_api_key']}")
|
print(f"Security config - IP check: {config['security']['enable_ip_check']}, API key: {config['security']['enable_api_key']}")
|
||||||
|
|
||||||
app = FastAPI(title="Hermes Service API", version="1.2.0")
|
app = FastAPI(title="Hermes Service API", version="1.3.0")
|
||||||
|
|
||||||
# Configure CORS
|
# Configure CORS
|
||||||
app.add_middleware(
|
app.add_middleware(
|
||||||
@ -70,8 +99,38 @@ app.add_middleware(
|
|||||||
allow_headers=config['cors']['allow_headers'],
|
allow_headers=config['cors']['allow_headers'],
|
||||||
)
|
)
|
||||||
|
|
||||||
# Global session registry: global_session_id -> {user_id, local_session_id, created_at}
|
# ---------------------------------------------------------------------------
|
||||||
|
# Persistent session registry: global_session_id -> {user_id, local_session_id, ...}
|
||||||
|
# Saved to disk so sessions survive service restarts.
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
SESSION_STORE_FILE = os.path.join(os.path.dirname(__file__), "data", "sessions.json")
|
||||||
|
os.makedirs(os.path.dirname(SESSION_STORE_FILE), exist_ok=True)
|
||||||
|
|
||||||
global_sessions = {}
|
global_sessions = {}
|
||||||
|
_sessions_lock = threading.Lock()
|
||||||
|
|
||||||
|
def _save_sessions():
|
||||||
|
"""Persist global_sessions to disk."""
|
||||||
|
try:
|
||||||
|
with _sessions_lock:
|
||||||
|
with open(SESSION_STORE_FILE, 'w') as f:
|
||||||
|
json_mod.dump(global_sessions, f, indent=2)
|
||||||
|
except Exception as e:
|
||||||
|
print(f"WARNING: Failed to save session store: {e}")
|
||||||
|
|
||||||
|
def _load_sessions():
|
||||||
|
"""Load global_sessions from disk."""
|
||||||
|
global global_sessions
|
||||||
|
if os.path.exists(SESSION_STORE_FILE):
|
||||||
|
try:
|
||||||
|
with open(SESSION_STORE_FILE, 'r') as f:
|
||||||
|
global_sessions = json_mod.load(f)
|
||||||
|
print(f"Loaded {len(global_sessions)} sessions from store")
|
||||||
|
except Exception as e:
|
||||||
|
print(f"WARNING: Failed to load session store, starting fresh: {e}")
|
||||||
|
global_sessions = {}
|
||||||
|
|
||||||
|
_load_sessions()
|
||||||
|
|
||||||
def get_real_ip(request: Request) -> str:
|
def get_real_ip(request: Request) -> str:
|
||||||
"""Get the real client IP address, considering X-Forwarded-For header"""
|
"""Get the real client IP address, considering X-Forwarded-For header"""
|
||||||
@ -255,6 +314,7 @@ async def create_session(request: SessionCreateRequest):
|
|||||||
"hermes_path": user_hermes_path,
|
"hermes_path": user_hermes_path,
|
||||||
"status": "active"
|
"status": "active"
|
||||||
}
|
}
|
||||||
|
_save_sessions()
|
||||||
|
|
||||||
return {
|
return {
|
||||||
"session_id": global_session_id,
|
"session_id": global_session_id,
|
||||||
@ -296,8 +356,8 @@ async def send_session_message(session_id: str, request: SessionMessageRequest):
|
|||||||
# with Hermes' session system. For now, we'll execute commands directly.
|
# with Hermes' session system. For now, we'll execute commands directly.
|
||||||
# In production, this would interface with Hermes' internal session management.
|
# In production, this would interface with Hermes' internal session management.
|
||||||
|
|
||||||
# Execute the message as a command
|
# Execute the message as a command using non-interactive mode
|
||||||
command_args = ["chat", request.message]
|
command_args = ["chat", "-q", request.message, "--source", "tool"]
|
||||||
|
|
||||||
result = await execute_hermes_command(
|
result = await execute_hermes_command(
|
||||||
command_args,
|
command_args,
|
||||||
@ -331,9 +391,10 @@ async def execute_hermes_command(command_args, user_id=None, timeout=300):
|
|||||||
hermes_dot_path = os.path.join(user_base_path, ".hermes")
|
hermes_dot_path = os.path.join(user_base_path, ".hermes")
|
||||||
else:
|
else:
|
||||||
user_hermes_path = BASE_HERMES_PATH
|
user_hermes_path = BASE_HERMES_PATH
|
||||||
hermes_dot_path = "/d/hermesai/.hermes"
|
hermes_dot_path = HERMES_HOME
|
||||||
|
|
||||||
python_path = "/d/hermesai/.hermes/hermes-agent/.venv/bin/python3"
|
# Resolve Python interpreter path dynamically
|
||||||
|
python_path = os.path.join(BASE_HERMES_PATH, ".venv", "bin", "python3")
|
||||||
cmd = [python_path, "-m", "hermes_cli.main"] + command_args
|
cmd = [python_path, "-m", "hermes_cli.main"] + command_args
|
||||||
|
|
||||||
env = os.environ.copy()
|
env = os.environ.copy()
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user