x

Hermes api_server 平台适配方案

来源:分析 gateway/platforms/api_server.py 源码(2903行)
整理时间:2026-05-04


一、平台定位

api_server 是 Hermes 唯一一个完全自定义消息格式的平台适配器。它将 Hermes 的 Agent 能力以 OpenAI Chat Completions API 格式对外暴露,任何兼容 OpenAI API 的前端(Open WebUI、LobeChat、LibreChat、AnythingLLM、NextChat、ChatBox 等)都可以直接接入。

Open WebUI / LobeChat / ChatBox
        │
        │  POST /v1/chat/completions
        ▼
┌─────────────────────┐
│   Hermes api_server │  ← gateway/platforms/api_server.py
│   (HTTP + SSE)      │
└────────┬────────────┘
         │ 内部调用 AIAgent
         ▼
    ┌─────────┐
    │ Hermes  │
    │ Agent   │
    └─────────┘

二、接口清单(12个端点)

方法 路径 说明
GET /health 基础健康检查
GET /health/detailed 详细状态(含平台/进程/PID)
GET /v1/models 可用模型列表
GET /v1/capabilities 机器可读的能力描述
POST /v1/chat/completions 核心:OpenAI 兼容 Chat Completions
POST /v1/responses OpenAI Responses API(有状态)
GET /v1/responses/{response_id} 查询历史响应
DELETE /v1/responses/{response_id} 删除历史响应
POST /v1/runs 启动异步 Run(立即返回 run_id,202)
GET /v1/runs/{run_id} 查询 Run 状态
GET /v1/runs/{run_id}/events SSE 事件流
POST /v1/runs/{run_id}/stop 中断正在运行的 Agent

三、认证机制

3.1 Token 校验

def _check_auth(self, request: web.Request) -> Optional[web.Response]:
    if not self._api_key:
        return None  # 未配置 API Key → 允许所有(仅本地使用)

    auth_header = request.headers.get("Authorization", "")
    # 标准 Bearer Token 格式
  • Header 格式Authorization: Bearer <token>
  • 无 Key 时:允许所有请求(本地调试模式)
  • 有 Key 时:校验失败返回 401 Unauthorized

3.2 CORS 配置

_CORS_HEADERS = {
    "Access-Control-Allow-Methods": "GET, POST, DELETE, OPTIONS",
    "Access-Control-Allow-Headers": "Authorization, Content-Type, Idempotency-Key",
}

支持浏览器端直接请求,可配置允许的 Origin 列表。


四、消息格式支持

4.1 Chat Completions 请求格式

POST /v1/chat/completions
{
  "model": "hermes-agent",
  "messages": [
    {"role": "system", "content": "你是一个有用的助手"},
    {"role": "user", "content": "你好"}
  ],
  "stream": false
}

Content 字段支持两种形式

  1. 字符串(最常见):
{"role": "user", "content": "你好"}
  1. 多模态数组(含图片):
{
  "role": "user",
  "content": [
    {"type": "text", "text": "这张图里有什么?"},
    {"type": "image_url", "image_url": {"url": "https://example.com/image.jpg"}}
  ]
}

支持的 part 类型:

Type 支持情况 说明
text / input_text / output_text 文本内容
image_url / input_image 图片(http/https/data:image URL)
file / input_file ❌ 主动拒绝 抛出 unsupported_content_type 异常
未知类型 ❌ 主动拒绝 抛出 unsupported_content_type 异常

4.2 Chat Completions 响应格式

非流式

{
  "id": "chatcmpl-xxx",
  "object": "chat.completion",
  "created": 1714992000,
  "model": "hermes-agent",
  "choices": [{
    "index": 0,
    "message": {"role": "assistant", "content": "你好,有什么可以帮你?"},
    "finish_reason": "stop"
  }],
  "usage": {
    "prompt_tokens": 10,
    "completion_tokens": 20,
    "total_tokens": 30
  }
}

流式(SSE)

data: {"id":"chatcmpl-xxx","object":"chat.completion.chunk","choices":[{"index":0,"delta":{"role":"assistant"},"finish_reason":null}]}

data: {"id":"chatcmpl-xxx","object":"chat.completion.chunk","choices":[{"index":0,"delta":{"content":"你"},"finish_reason":null}]}

data: {"id":"chatcmpl-xxx","object":"chat.completion.chunk","choices":[{"index":0,"delta":{"content":"好"},"finish_reason":null}]}

data: [DONE]

自定义事件(工具进度):

event: hermes.tool.progress
data: {"toolCallId":"xxx","status":"completed","output":"..."}

五、Session 管理

5.1 无状态模式(默认)

每次请求独立,Agent 无记忆。

5.2 有状态模式(通过 Header)

X-Hermes-Session-Id: agent:main:api:tenant:123:user:456

Session ID 格式遵循 Hermes 标准:

层级 格式 说明
全局记忆 agent:main:api:global 所有用户共享
租户共享 agent:main:api:tenant:{id}:shared 同一租户用户共享
成员私有 agent:main:api:tenant:{id}:user:{id} 单个用户私有记忆

5.3 Responses API(持久化会话)

通过 previous_response_id 实现多轮对话状态:

POST /v1/responses
{
  "model": "hermes-agent",
  "previous_response_id": "resp_abc123",
  "modalities": ["text"],
  "input": {"messages": [{"role": "user", "content": "继续"}]}
}

历史响应通过 SQLite LRU 存储(默认100条),路径 ~/.hermes/response_store.db


六、Runs 接口(异步模式)

适合需要立即拿到 run_id、后续轮询状态订阅 SSE 事件的场景:

Step 1: POST /v1/runs        → 立即返回 202 + run_id
Step 2: GET /v1/runs/{run_id}     → 轮询状态
Step 3: GET /v1/runs/{run_id}/events → SSE 订阅事件
Step 4: POST /v1/runs/{run_id}/stop  → 中断任务

Runs SSE 事件类型

事件名 触发时机
agent.started Agent 开始处理
agent.output 最终输出完成
agent.stopped 被 stop 打断
agent.error 运行异常
tool.progress 工具执行进度
tool.started 工具开始
tool.completed 工具完成
tool.error 工具异常

七、与微信 Typing 的集成方案

7.1 现状

微信平台支持 send_typing / stop_typing,通过 ilink/bot/sendtyping 接口推送"正在输入"状态。Hermes 在 LLM 开始处理时自动调用 send_typing,处理完成后调用 stop_typing

7.2 api_server 扩展方案

api_server 是纯 HTTP 请求/响应,无法主动推送。扩展方案:

┌──────────────────────────────────────────────────────────────┐
│                        扩展架构                                │
│                                                              │
│  App ────────── WebSocket 连接 ────► api_server /ws/subscribe │
│                                                              │
│  Hermes ────(send_typing)────► api_server ────广播──► App    │
│                    │                          │              │
│                    └── typing 事件推送 ◄───────┘              │
│                                                              │
└──────────────────────────────────────────────────────────────┘

新增端点

WebSocket /ws/subscribe?token=<user_token>&tenant_id=<id>

推送消息格式

{"type": "typing", "chat_id": "xxx", "status": "start|stop"}

消息订阅管理

class TypingBroadcast:
    def __init__(self):
        # chat_id -> list of WebSocket connections
        self._subscribers: Dict[str, List[WebSocket]] = {}

    async def broadcast(self, chat_id: str, status: str):
        for ws in self._subscribers.get(chat_id, []):
            await ws.send_json({"type": "typing", "chat_id": chat_id, "status": status})

八、Token 身份认证方案(完整版)

8.1 Token 生成

import secrets

def generate_token() -> str:
    """生成64位安全的随机Token"""
    return secrets.token_bytes(32).hex()  # 64字符

8.2 数据库设计

-- 租户表
CREATE TABLE tenants (
    id          BIGINT PRIMARY KEY AUTO_INCREMENT,
    name        VARCHAR(100) NOT NULL,
    app_key     VARCHAR(64) NOT NULL UNIQUE COMMENT 'App唯一标识',
    is_active   TINYINT(1) DEFAULT 1,
    created_at  DATETIME DEFAULT CURRENT_TIMESTAMP,
    INDEX idx_app_key (app_key)
);

-- 用户表
CREATE TABLE users (
    id          BIGINT PRIMARY KEY AUTO_INCREMENT,
    tenant_id   BIGINT NOT NULL,
    username    VARCHAR(64) NOT NULL,
    password_hash VARCHAR(255),
    nickname    VARCHAR(100),
    avatar_url  VARCHAR(512),
    is_active   TINYINT(1) DEFAULT 1,
    created_at  DATETIME DEFAULT CURRENT_TIMESTAMP,
    UNIQUE KEY uk_tenant_username (tenant_id, username),
    FOREIGN KEY (tenant_id) REFERENCES tenants(id)
);

-- Token表
CREATE TABLE auth_tokens (
    id              BIGINT PRIMARY KEY AUTO_INCREMENT,
    token           VARCHAR(64) NOT NULL UNIQUE COMMENT '64位随机字符串',
    user_id         BIGINT NOT NULL,
    tenant_id       BIGINT NOT NULL,
    device_info     VARCHAR(255),
    client_version  VARCHAR(32),
    last_active_at  DATETIME DEFAULT CURRENT_TIMESTAMP,
    expires_at      DATETIME COMMENT 'NULL=永不过期',
    is_revoked      TINYINT(1) DEFAULT 0,
    created_at      DATETIME DEFAULT CURRENT_TIMESTAMP,
    FOREIGN KEY (user_id) REFERENCES users(id),
    FOREIGN KEY (tenant_id) REFERENCES tenants(id),
    INDEX idx_token (token),
    INDEX idx_user (user_id)
);

-- API调用日志
CREATE TABLE api_call_logs (
    id              BIGINT PRIMARY KEY AUTO_INCREMENT,
    call_id         VARCHAR(64) NOT NULL UNIQUE,
    tenant_id       BIGINT NOT NULL,
    user_id         BIGINT NOT NULL,
    provider        VARCHAR(32) NOT NULL,
    model           VARCHAR(64) NOT NULL,
    is_custom_key   TINYINT(1) DEFAULT 0,
    custom_key_id   BIGINT,
    input_tokens    INT DEFAULT 0,
    output_tokens   INT DEFAULT 0,
    latency_ms       INT,
    status_code     VARCHAR(32),
    error_msg       TEXT,
    ip_address      VARCHAR(45),
    created_at      DATETIME DEFAULT CURRENT_TIMESTAMP,
    INDEX idx_tenant_time (tenant_id, created_at)
);

8.3 认证中间件伪代码

async def auth_middleware(request, call_next):
    token = request.headers.get("Authorization", "").replace("Bearer ", "")
    if not token:
        return Response(401, body="Missing token")

    row = await db.query("""
        SELECT u.id, u.tenant_id, u.is_active, t.is_active
        FROM auth_tokens r
        JOIN users u ON r.user_id = u.id
        JOIN tenants t ON r.tenant_id = t.id
        WHERE r.token = ? AND r.is_revoked = 0
        AND u.is_active = 1 AND t.is_active = 1
        AND (r.expires_at IS NULL OR r.expires_at > NOW())
    """, token)

    if not row:
        return Response(401, body="Invalid token")

    # 注入到请求上下文,应用层无法伪造
    request.user_id   = row["id"]
    request.tenant_id = row["tenant_id"]
    return await call_next(request)

九、媒体类型限制与处理策略

媒体类型 api_server 支持 扩展方案
文字
图片 URL 直接透传
语音/音频 先上传 OSS,URL 注入 content
视频 先上传 OSS,URL 注入 content
文件 ❌(主动抛异常) 先上传 OSS,URL 注入 content

统一扩展思路:所有非图片媒体 → 上传至 OSS/CDN → 得到 URL → 作为 text content 的一部分发送给 Agent。响应中的多模态内容(语音/图片)通过 TTS/图片生成处理后再推送给 App。


十、配置参考

platforms:
  api_server:
    enabled: true
    host: "0.0.0.0"
    port: 8642
    api_key: "your-secret-key"          # 可选,不填则允许所有(本地)
    cors_origins:                        # 可选,CORS 白名单
      - "https://your-app.com"
      - "https://admin.example.com"
    model_name: "hermes-agent"           # 广播的模型名称
    max_request_bytes: 1_000_000        # 请求体大小限制(默认1MB)

十一、关键源码位置

功能 位置
入口 / 路由注册 api_server.py 末尾 add_routes()
认证 _check_auth() 第 668 行
消息内容标准化 _normalize_multimodal_content() 第 132 行
Chat Completions 处理 _handle_chat_completions()
SSE 流式响应 _handle_chat_completions()stream=True 分支
Runs 异步管理 _run_streams / _active_run_agents / _active_run_tasks
ResponseStore(LRU) ResponseStore 类 第 282 行
Agent 创建 _create_agent() 第 712 行

十二、App 会话交互日志管理

12.1 设计目标

对 App 用户与 AI 的每次会话交互进行完整记录,用于:

  • 审计追溯:记录谁在什么时间问了什么、得到了什么回答
  • 会话恢复:支持用户查看历史对话上下文
  • 数据分析:统计用户行为、热门问题、响应质量
  • 异常排查:定位用户反馈问题的完整上下文

12.2 日志表设计

-- 会话表(一个对话会话 = 一个 topic)
CREATE TABLE chat_sessions (
    id              BIGINT PRIMARY KEY AUTO_INCREMENT,
    session_id      VARCHAR(64) NOT NULL UNIQUE COMMENT '会话唯一ID',
    tenant_id       BIGINT NOT NULL,
    user_id         BIGINT NOT NULL,
    title           VARCHAR(256) COMMENT '会话标题(取首条消息前30字)',
    status          VARCHAR(16) DEFAULT 'active' COMMENT 'active | closed | archived',
    message_count   INT DEFAULT 0 COMMENT '消息总条数',
    created_at      DATETIME DEFAULT CURRENT_TIMESTAMP,
    updated_at      DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
    closed_at       DATETIME COMMENT '会话结束时间',
    FOREIGN KEY (tenant_id) REFERENCES tenants(id),
    FOREIGN KEY (user_id) REFERENCES users(id),
    INDEX idx_tenant_user (tenant_id, user_id),
    INDEX idx_created (created_at)
);

-- 消息表(每条用户消息 + AI回复 = 两条记录)
CREATE TABLE chat_messages (
    id              BIGINT PRIMARY KEY AUTO_INCREMENT,
    message_id      VARCHAR(64) NOT NULL UNIQUE COMMENT '消息唯一ID',
    session_id      VARCHAR(64) NOT NULL,
    tenant_id       BIGINT NOT NULL,
    user_id         BIGINT NOT NULL,
    role            VARCHAR(16) NOT NULL COMMENT 'user | assistant | system | tool',
    content         TEXT COMMENT '消息正文(文本或JSON序列化)',
    content_type    VARCHAR(32) DEFAULT 'text' COMMENT 'text | image_url | audio | file | tool_call',
    model           VARCHAR(64) COMMENT '调用的模型',
    provider        VARCHAR(32) COMMENT '平台或第三方提供商',
    is_custom_key   TINYINT(1) DEFAULT 0 COMMENT '是否使用用户自有Key',
    custom_key_id   BIGINT COMMENT '自有Key记录ID',
    input_tokens    INT DEFAULT 0,
    output_tokens   INT DEFAULT 0,
    latency_ms      INT COMMENT '响应耗时(毫秒)',
    status          VARCHAR(16) DEFAULT 'success' COMMENT 'success | error | timeout | rate_limited',
    error_code      VARCHAR(64),
    error_msg       TEXT,
    client_version  VARCHAR(32),
    ip_address      VARCHAR(45),
    device_info     VARCHAR(255),
    reply_to_id     VARCHAR(64) COMMENT '回复哪条消息的ID',
    created_at      DATETIME DEFAULT CURRENT_TIMESTAMP,
    FOREIGN KEY (tenant_id) REFERENCES tenants(id),
    FOREIGN KEY (user_id) REFERENCES users(id),
    INDEX idx_session_time (session_id, created_at),
    INDEX idx_tenant_time (tenant_id, created_at)
);

-- 工具调用日志(Agent 调用工具时的详细记录)
CREATE TABLE tool_call_logs (
    id              BIGINT PRIMARY KEY AUTO_INCREMENT,
    message_id      VARCHAR(64) NOT NULL COMMENT '关联消息ID',
    session_id      VARCHAR(64) NOT NULL,
    tenant_id       BIGINT NOT NULL,
    tool_name       VARCHAR(128) NOT NULL,
    tool_call_id    VARCHAR(64),
    tool_input      TEXT COMMENT '工具输入参数(JSON)',
    tool_output     TEXT COMMENT '工具输出结果(JSON)',
    status          VARCHAR(16) DEFAULT 'success' COMMENT 'success | error',
    error_msg       TEXT,
    latency_ms      INT,
    created_at      DATETIME DEFAULT CURRENT_TIMESTAMP,
    INDEX idx_session (session_id),
    INDEX idx_tool_name (tool_name),
    INDEX idx_created (created_at)
);

-- 用户反馈表(thumbs up/down 或评分)
CREATE TABLE message_feedback (
    id              BIGINT PRIMARY KEY AUTO_INCREMENT,
    message_id      VARCHAR(64) NOT NULL,
    user_id         BIGINT NOT NULL,
    rating          VARCHAR(8) COMMENT 'thumbs_up | thumbs_down | happy | sad | angry',
    comment         TEXT COMMENT '用户可选的反馈文字',
    created_at      DATETIME DEFAULT CURRENT_TIMESTAMP,
    FOREIGN KEY (user_id) REFERENCES users(id),
    UNIQUE KEY uk_message_user (message_id, user_id)
);

12.3 日志记录时机

┌─────────────────────────────────────────────────────────────────┐
                     消息记录时机                                  
├─────────────────────────────────────────────────────────────────┤
                                                                  
   用户发消息  写入 chat_messagesrole=user                  
                                                                  
   AI 开始处理  记录处理开始时间、使用的模型/Key                
                                                                  
   AI 流式返回  每收到一个 delta chunk  更新 output_tokens     
                                                                  
   AI 返回完成  写入 chat_messagesrole=assistant            
                   包含:content / tokens / latency_ms / status   
                                                                  
   Agent 调用工具  写入 tool_call_logs                         
                                                                  
   用户反馈  写入 message_feedback                            
                                                                  
   会话结束(超时/主动关闭)→ 更新 chat_sessions                 
                   status='closed', closed_at=NOW()               
                                                                  
└─────────────────────────────────────────────────────────────────┘

12.4 日志记录伪代码

async def log_message(
    session_id: str,
    tenant_id: int,
    user_id: int,
    role: str,                    # user | assistant | tool
    content: Any,
    content_type: str = "text",
    metadata: Optional[dict] = None,
):
    """统一消息记录入口"""
    message_id = f"msg_{secrets.token_hex(16)}"

    await db.execute("""
        INSERT INTO chat_messages
        (message_id, session_id, tenant_id, user_id, role,
         content, content_type, model, provider, is_custom_key,
         custom_key_id, input_tokens, output_tokens, latency_ms,
         status, error_code, error_msg, client_version, ip_address,
         device_info, reply_to_id)
        VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
    """,
        message_id, session_id, tenant_id, user_id, role,
        serialize_content(content), content_type,
        metadata.get("model"),
        metadata.get("provider"),
        metadata.get("is_custom_key", 0),
        metadata.get("custom_key_id"),
        metadata.get("input_tokens", 0),
        metadata.get("output_tokens", 0),
        metadata.get("latency_ms", 0),
        metadata.get("status", "success"),
        metadata.get("error_code"),
        metadata.get("error_msg"),
        metadata.get("client_version"),
        metadata.get("ip_address"),
        metadata.get("device_info"),
        metadata.get("reply_to_id"),
    )

    # 更新会话计数
    await db.execute("""
        UPDATE chat_sessions
        SET message_count = message_count + 1,
            updated_at = NOW()
        WHERE session_id = ?
    """, session_id)

    return message_id


async def log_tool_call(
    message_id: str,
    session_id: str,
    tenant_id: int,
    tool_name: str,
    tool_call_id: str,
    tool_input: dict,
    tool_output: Any,
    latency_ms: int,
    status: str = "success",
    error_msg: str = None,
):
    await db.execute("""
        INSERT INTO tool_call_logs
        (message_id, session_id, tenant_id, tool_name, tool_call_id,
         tool_input, tool_output, status, error_msg, latency_ms)
        VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
    """,
        message_id, session_id, tenant_id, tool_name, tool_call_id,
        json.dumps(tool_input),
        json.dumps(tool_output) if isinstance(tool_output, dict) else str(tool_output),
        status, error_msg, latency_ms
    )

12.5 日志查询接口

# 获取用户会话列表
GET /api/sessions?tenant_id=x&user_id=y&limit=20&offset=0

# 获取会话详情(含消息历史)
GET /api/sessions/{session_id}/messages?limit=50&offset=0

# 搜索会话内容
GET /api/sessions/search?q=关键词&tenant_id=x&user_id=y

# 获取统计数据
GET /api/analytics/overview?tenant_id=x&start=2026-05-01&end=2026-05-04
# 返回:总消息数 / 总会话数 / 平均响应耗时 / top工具调用 / top问题

12.6 数据保留策略

数据类型 保留时间 说明
chat_sessions 永久 用户主动删除才删除
chat_messages 90天 敏感数据自动脱敏后保留
tool_call_logs 30天 日志量大,定期清理
message_feedback 永久 用于模型优化
异常日志 180天 用于风控分析

超出保留期后,优先归档到冷存储而非直接删除,支持按需恢复。


十三、微信命令屏蔽方案

13.1 需求背景

Hermes 微信平台默认开放了所有 / 开头的命令(如 /reset/new/model/tools 等)。在 App 场景下,部分命令:

  • 可能干扰用户体验(如 /steer 改变 Agent 行为)
  • 存在安全风险(如 /exec 执行系统命令)
  • 在 App 内无意义(如 /help 查看帮助)

因此需要在 App 端屏蔽大部分命令,仅保留两个高频核心命令

13.2 命令白名单

# App 端允许的命令(白名单)
ALLOWED_COMMANDS = {
    "/reset": "重置当前会话上下文,保留会话历史仅清除 Agent 记忆",
    "/new": "开启一个新的会话,保留当前会话记录",
}

# App 端屏蔽的命令(黑名单)
BLOCKED_COMMANDS = {
    # 会话类(危险)
    "/reset_all": "危险:清除所有会话",
    "/clear": "危险:清空上下文",

    # 系统类(危险)
    "/exec": "危险:执行系统命令",
    "/bash": "危险:执行 shell 命令",
    "/sudo": "危险:提权操作",

    # 模型类(干扰体验)
    "/model": "在 App 内无需切换模型",
    "/provider": "在 App 内无需切换 Provider",

    # 工具类(干扰体验)
    "/tools": "App 内无需查看工具列表",
    "/tool": "App 内无需手动调用工具",

    # 配置类(干扰体验)
    "/set": "App 内不支持自定义配置",
    "/config": "App 内不支持修改配置",

    # Agent 引导类(干扰体验)
    "/steer": "App 内不支持引导 Agent 行为",
    "/persona": "App 内无需切换人格",

    # 调试类(生产环境禁用)
    "/debug": "生产环境禁用调试",
    "/verbose": "生产环境禁用详细日志",
    "/trace": "生产环境禁用跟踪",

    # 信息类(无意义)
    "/help": "App 内无需命令行帮助",
    "/usage": "App 内无需查看用量",
    "/stats": "App 内无需查看统计",
}

13.3 拦截实现方案

方案 A:在 App 端拦截(推荐)

// App 端消息发送前拦截
function preprocessMessage(text) {
    const allowed = ["/reset", "/new"];

    if (!text.startsWith("/")) {
        return text; // 普通消息不过滤
    }

    const cmd = text.trim().split(/\s+/)[0].toLowerCase();

    if (allowed.includes(cmd)) {
        return text; // 白名单命令放行
    }

    // 屏蔽命令:回显提示,不发送给 Hermes
    return null; // 或返回 "此命令在 App 内不可用"
}

// 发送逻辑
async function sendMessage(text) {
    const processed = preprocessMessage(text);
    if (processed === null) {
        showToast("此命令在 App 内不可用");
        return;
    }
    await sendToHermes(processed);
}

方案 B:在 api_server 中间件拦截

# 在 api_server 的 chat completions 处理前拦截
ALLOWED_COMMANDS = {"/reset", "/new"}
BLOCKED_COMMANDS = {
    "/exec", "/bash", "/sudo", "/reset_all", "/clear",
    "/model", "/provider", "/tools", "/tool",
    "/set", "/config", "/steer", "/persona",
    "/debug", "/verbose", "/trace",
    "/help", "/usage", "/stats",
}

@app.middleware
async def block_commands(request, call_next):
    if request.path != "/v1/chat/completions":
        return await call_next(request)

    body = await request.json()
    last_message = body.get("messages", [])[-1]
    content = last_message.get("content", "")

    if isinstance(content, str) and content.startswith("/"):
        cmd = content.strip().split()[0].lower()
        if cmd in BLOCKED_COMMANDS:
            return web.json_response({
                "error": {
                    "code": "command_blocked",
                    "message": f"命令 /{cmd.lstrip('/')} 在 App 内已被屏蔽,仅支持 /reset 和 /new"
                }
            }, status=400)

    return await call_next(request)

方案 C:在 Hermes 配置层屏蔽(平台级)

# config.yaml 中平台配置
platforms:
  weixin:
    enabled: true
    command_whitelist:
      - /reset
      - /new
    command_blacklist:
      - /exec
      - /bash
      - /sudo
      - /steer
      - /model
      - /tools
      - /debug
      - /verbose
    # 命中黑名单时的响应
    blocked_response: "此命令在 App 内不可用"

13.4 /reset/new 的具体行为定义

命令 App 内的预期行为
/reset 清除当前会话的 Agent 短期记忆(工作记忆),保留 session 和聊天记录。相当于"重新开始当前对话的思考"。
/new 创建一个新的 session_id,旧会话标记为 closed。界面切换到新会话。
async def handle_reset(session_id: str, tenant_id: int, user_id: int) -> dict:
    """
    /reset 实现:
    1. 清除 AIAgent 的当前 context window
    2. 在 chat_sessions 中记录 reset 事件
    3. 发送系统消息:"已重置,可以重新开始"
    """
    # 通知 Hermes 清除会话记忆
    await hermes.clear_session_context(session_id)

    # 写入系统消息
    await log_message(
        session_id=session_id,
        tenant_id=tenant_id,
        user_id=user_id,
        role="system",
        content="会话已重置,之前的上下文已清除。有什么可以帮你的?",
        content_type="text",
    )

    return {"status": "reset", "session_id": session_id}


async def handle_new(tenant_id: int, user_id: int) -> dict:
    """
    /new 实现:
    1. 关闭旧会话(status=closed)
    2. 创建新 session_id
    3. 初始化新会话记录
    4. 返回新 session_id 给 App 切换
    """
    old_session_id = get_current_session_id(user_id)

    # 关闭旧会话
    if old_session_id:
        await db.execute("""
            UPDATE chat_sessions
            SET status='closed', closed_at=NOW()
            WHERE session_id=?
        """, old_session_id)

    # 创建新会话
    new_session_id = f"sess_{secrets.token_hex(16)}"
    await db.execute("""
        INSERT INTO chat_sessions (session_id, tenant_id, user_id, title, status)
        VALUES (?, ?, ?, '新会话', 'active')
    """, new_session_id, tenant_id, user_id)

    return {"status": "new_session", "session_id": new_session_id}

13.5 统一响应格式

无论屏蔽还是执行,结果统一返回给 App:

{
  "type": "command_response",
  "command": "/reset",
  "status": "success",        // success | blocked | error
  "message": "会话已重置",
  "session_id": "sess_xxx",    // /new 时返回新 session_id
  "data": {}
}
Left-click: follow link, Right-click: select node, Scroll: zoom
x