Skip to content

终端用户门户 + 智能体生命周期管理

企业级多智能体平台的核心用户侧链路。 终端用户通过独立门户登录、选择智能体、与引擎对话。 所有交互通过自研 Vue 3 组件直接渲染(无 iframe)。

1. 总体架构

1.1 系统拓扑

[Browser] → Nginx/Ingress
              ├── /              → End User Portal (Vue 3 + Vite, 静态文件)
              ├── /api/manager/* → Manager Service (:8002)   # 认证(auth/enduser/*)、终端可见实例等
              ├── /api/controller/* → Manager Service (:8002) # Controller 已并入 Manager
              └── /api/gateway/* → Gateway Service (:8010, DNS 命名路由)

1.2 数据流

用户交互链路:
  Browser → Portal (Vue 3) → Manager (:8002)         # 登录 / 智能体列表 / 部署 / 状态查询
  Browser → Portal (Vue 3) → Gateway (:8010)          # 聊天 / SSE 流式

                      engine-hermes-{id}:8642          # DNS 命名规范路由

智能体生命周期管理链路:
  Controller ←→ K8s API (按命名规范创建/休眠/删除 Pod)
  Controller → MinIO (归档/恢复引擎数据)

部署进度推送链路:
  Controller → SSE (text/event-stream) → Portal 前端 (进度条展示)

1.3 架构约束

  • Gateway 无反向依赖(不查询 Controller),通过 X-Agent-ID 头 + DNS 命名规范路由
  • 数据存档提前到 SUSPEND(30min),不加定期轮询备份
  • 无 iframe,Chat 页面直接渲染 Vue 3 组件
  • 不修改开源软件源代码(hermes-webui 仅参考实现,终端前端为自研 Vue 3 应用)

2. 用户流程

2.1 完整交互链路

1. 用户访问 / 或 /agents/:id

     ├── 未登录 → 重定向到 /login?redirect=/agents/:id
     │               │
     │               ▼
     │          登录页 (POST /api/manager/auth/enduser/login 等,见 §3.1;另支持
     │          邮箱/手机号登录、短信验证码登录、一键演示登录)
     │               │ 成功
     │               ▼
     │          存储 JWT → redirect 深链优先回跳;
     │          无 redirect 时解析落地目标直达对话页:
     │          上次对话智能体 (localStorage last_agent_id)
     │          > 服务端最近访问 (last_accessed_at)
     │          > 可访问列表第一个;空列表落 /agents

     ├── 已登录, 访问 / → 解析落地目标直达对话页(同上优先级);
     │       访问 /agents → 智能体列表 (按 last_accessed_at 排序)
     │       │
     │       ▼ 点击智能体
     │   /agents/:id

     └── 已登录, 访问 /agents/:id (直接 URL 或从列表点击)
             │  路由守卫统一记录 last_agent_id

         GET /api/controller/agents/:id/status

         ┌───┴───┐
         │       │
      RUNNING  其他状态
         │       │
         │       ▼
         │   POST /api/controller/agents/:id/deploy
         │       │
         │       ▼
         │   SSE → 部署进度条
         │       │
         │       ▼
         │   记录 last_accessed_at


     进入 Chat 页面 (Vue 3 组件, 无 iframe)


     ChatPage.vue
       ├── ChatSessionList.vue  ← 会话列表 / 历史回看 (见 §3.3)
       ├── ChatMessages.vue     ← SSE 流式渲染 (VES 帧)
       ├── ChatComposer.vue     → Gateway (X-Agent-ID)
       └── useChat.ts
              ├── newSession()          → POST /api/gateway/v1/sessions(引擎侧)/ 本地占位(本地会话)
              ├── loadSessions()        → GET  /api/gateway/v1/sessions
              ├── loadSessionMessages() → GET  /api/gateway/v1/sessions/{id}/messages
              ├── sendMessage()         → POST /api/gateway/v1/runs (SSE, VES 事件流)
              └── loadModels()          → GET  /api/gateway/v1/models

2.2 前端的页面与路由

路径页面组件功能
/loginLoginPage终端用户登录,支持 redirect 回跳
/agentsAgentListPage可访问智能体卡片列表,按最近访问排序
/agents/:idAgentChatPage部署检测 → 进度展示 → Chat 页面
/已登录时直达落地对话页,否则重定向到 /agents

2.3 路由守卫

  • 所有页面(除 /login)需 JWT token
  • token 不存在或过期 → 重定向到 /login?redirect=当前路径
  • 登录成功 → redirect 深链优先回跳;无 redirect 时直达对话页,落地优先级: 上次对话智能体(localStorage last_agent_id,按用户隔离且须在可访问列表内) → 服务端 last_accessed_at 最新者 → 可访问列表第一个;无可访问智能体才落列表页
  • 进入 /agents/:id 对话页时守卫统一记录 last_agent_id(覆盖列表点击、对话内切换、直达链接)
  • 已登录访问 //login → 按同一优先级直达对话页

3. 组件设计

3.1 后端服务

Manager (端口 8002)

终端侧认证走 POST /api/manager/auth/enduser/*(查 end_users 表、签发 payload 含 aud: "enduser" 的 JWT,与管理台 aud: "admin" token 互不通用):

端点方法说明
POST /api/manager/auth/enduser/loginPOST账号密码登录
POST /api/manager/auth/enduser/login-by-contactPOST已验证邮箱/手机号 + 密码登录
POST /api/manager/auth/enduser/login-by-sms-codePOST短信验证码登录(仅系统开启 SMS 渠道时)
POST /api/manager/auth/enduser/demo-loginPOST一键演示登录(部署开启演示功能时)
POST /api/manager/auth/enduser/verification-code/sendPOST发送短信/邮箱验证码
GET /api/manager/auth/meGET当前用户信息(管理台与终端侧共用,按 token 定位所属账号表)
GET /api/manager/agent-instances/accessibleGET终端用户可访问的已发布智能体实例列表

Controller 路由(并入 Manager :8002)

生命周期管理:

端点方法说明
GET /api/controller/agents/{id}/statusGET查询引擎部署状态
POST /api/controller/agents/{id}/deployPOST创建/恢复引擎
GET /api/controller/agents/{id}/deploy/eventsGET (SSE)部署进度事件流
POST /api/controller/agents/{id}/suspendPOST休眠引擎 (scale=0)
POST /api/controller/agents/{id}/destroyPOST销毁引擎并归档

会话管理:

端点方法说明
POST /api/controller/chat/session/newPOST创建新会话
GET /api/controller/chat/sessionsGET列出某用户的会话
GET /api/controller/chat/sessionGET获取会话详情
GET /api/controller/chat/dashboard/configGET前端探活配置
GET /api/controller/chat/settingsGET会话设置
GET /api/controller/chat/modelsGET模型列表

Gateway (端口 8010)

端点方法说明
ANY /{path}ANYDNS 命名规范路由,需要 X-Agent-ID

Gateway 改造为无状态 DNS 命名路由,不依赖任何其他服务:

请求头 X-Agent-ID → 提取 agent_id → DNS 命名规范构造 upstream
→ engine-hermes-{agent_id[:8]}.veyraos.svc.cluster.local:8642
→ 连接失败时返回 503

关键约束:Gateway 不包含屏蔽 Origin/Referer 头,否则引擎会拒绝请求返回 403。

3.2 前端组件

src/
├── main.ts                        # Vue 3 应用入口
├── App.vue                        # 根组件 (导航栏 + router-view)
├── router/
│   ├── index.ts                   # 路由定义 (hash 模式)
│   └── guard.ts                   # 路由守卫 (JWT 检查)
├── api/
│   ├── client.ts                  # HTTP 客户端 (自动附加 JWT, 401 重定向)
│   └── endpoints.ts               # 所有 API 调用函数
├── stores/
│   ├── auth.ts                    # 认证状态 (JWT + user info + chatMode)
│   └── agent.ts                   # 智能体部署状态管理
├── composables/
│   ├── useChat.ts                 # 聊天核心逻辑 (会话/消息/SSE/模型)
│   └── useDeployProgress.ts       # SSE 部署进度 hook
├── utils/
│   └── historyMessages.ts         # 历史记录归一 (消息列表 / 轮次列表 → 对话流)
├── views/
│   ├── LoginPage.vue              # 登录页面
│   ├── AgentListPage.vue          # 智能体列表 (卡片布局、最近访问排序)
│   └── AgentChatPage.vue          # 部署检测 → ChatPage
└── components/
    ├── AppNav.vue                 # 导航栏 (支持深色模式切换)
    ├── DeployProgress.vue         # 部署进度条组件
    └── chat/
        ├── ChatPage.vue           # 聊天主布局 (Rail + 侧边栏 + 主区域 + Workspace)
        ├── ChatSessionList.vue    # 会话列表 (过滤、时间格式化)
        ├── ChatMessages.vue       # 消息流式渲染 (空状态、SSE 逐块拼接)
        ├── ChatComposer.vue       # 输入框 (发送、停止、模型选择)
        ├── InputFormCard.vue      # 首开前置输入表单 (引擎自描述 schema)
        └── ChatFileBrowser.vue    # 工作区文件浏览器

样式方案

文件来源行数说明
index.html自定义<html class="dark"> 启用深色主题
hermes-style.css拷贝 hermes-webui~50438 套主题(深色/slate/poseidon 等)
Tailwind CSS自定义Portal 页面(登录/列表/部署)使用

ChatPage 布局

┌──────────────────────────────────────────────────────┐
│  AppNav (导航栏, 聊天模式时深色背景)                   │
├────┬──────────────────────────────┬──────────────────┤
│Rail│ Sidebar (会话列表)          │  Main (主区域)   │
│    │  ┌───── Panel Head ──────┐  │  ┌─────────────┐ │
│    │  │ Chat    [+新会话]     │  │  │ 消息列表     │ │
│    │  ├───────────────────────┤  │  │            │ │
│ C  │  │ session search       │  │  │ · 用户消息  │ │
│ h  │  ├───────────────────────┤  │  │ · AI 回复   │ │
│ a  │  │ ◉ Untitled           │  │  │ · SSE 流式  │ │
│ t  │  │ ○ 数据分析           │  │  │            │ │
│    │  │ ○ 代码审查           │  │  ├─────────────┤ │
│ S  │  └───────────────────────┘  │  │ Composer    │ │
│ p  │                             │  │ [输入框][📎]│ │
│ a  │                             │  │ [hermes] [▶]│ │
│ c  │                             │  └─────────────┘ │
│ e  │              ┌──────────────┤                   │
│ s  │              │Workspace     │                   │
│    │              │ Panel        │                   │
│    │              │ 📁 files     │                   │
│    │              │ 📄 artifacts │                   │
└────┴──────────────┴──────────────┴───────────────────┘

移动端布局

窄屏(max-width:768px)下布局切换单栏 + 抽屉:

  • 顶部 titlebar(48px):左 hamburger 唤出会话抽屉、居中智能体名、右侧按钮唤出工作区抽屉
  • 底部 tabbar(56px + 安全区):对话 / 定时 / 看板 / 技能 / 返回
  • 侧栏抽屉化:Sidebar 与 Workspace Panel 由固定列改为 fixed 抽屉,open 时滑入并带遮罩层
  • 触摸手势:会话列表项左滑显示删除、长按唤出上下文菜单;屏幕左右边缘横滑可打开/关闭抽屉
  • 键盘适配:通过 visualViewport 监听软键盘高度,Composer 与 tabbar 同步上移;--kb-h CSS 变量驱动 transform
  • iOS 视口:全局 100vh100dvh 解决地址栏跳动;env(safe-area-inset-bottom) 处理 Home 指示条;输入框 font-size:16px 防止自动放大

3.3 useChat 核心逻辑

useChat.ts composable 管理所有聊天状态和通信:

useChat(agentId, engineType)

  ├── sessions: Ref<Session[]>        # 会话列表
  ├── currentSessionId: Ref<string>   # 当前会话 ID (引擎侧会话 / 本地会话,见下)
  ├── isStreaming: Ref<boolean>       # 是否正在流式接收
  ├── streamingContent: Ref<string>   # 当前流式内容
  ├── timelineEvents: Ref<...>        # 按到达顺序的展示事件 (思考 / 正文 / 工具 / 步骤)
  ├── currentModel: Ref<string>       # 当前模型

  ├── newSession()            → POST /api/gateway/v1/sessions (引擎侧会话)
  │                             / 本地生成会话 id 占位 (本地会话)
  ├── selectSession(id)       → 切换当前会话并按需拉取历史
  ├── loadSessions()          → GET /api/gateway/v1/sessions
  ├── loadSessionMessages(id) → GET /api/gateway/v1/sessions/{id}/messages
  ├── deleteSession(id)       → DELETE /api/gateway/v1/sessions/{id}
  ├── loadInputForm()         → GET /api/gateway/v1/parameters (提供输入表单的引擎)
  ├── sendMessage(text)       → POST /api/gateway/v1/runs
  │                              ├── 一次 POST 拿 VES 事件流(SSE)
  │                              ├── ReadableStream 逐块读取
  │                              ├── TextDecoder 解码
  │                              ├── buffer 处理不完整行
  │                              └── 帧 → run 状态树 + 展示投影(runTree.ts / frames.ts)
  ├── stopStreaming()         → POST /api/gateway/v1/runs/{run_id}/cancel
  └── loadModels()            → GET /api/gateway/v1/models

会话相关调用统一走 /api/gateway/v1/sessions*,由 Gateway 按引擎类型映射到各自的 原生会话路径,前端不再区分引擎。

SSE 流式数据结构(/v1/runs,一帧一个 data: {json},用 type 区分帧族):

data: {"type":"run.started","run_id":"...","seq":1}

data: {"type":"reasoning.delta","run_id":"...","delta":"先确认..."}

data: {"type":"message.delta","run_id":"...","message_id":"m1","delta":"数"}

data: {"type":"step.started","run_id":"...","step_id":"node-1","name":"检索知识库","status":"running"}

data: {"type":"step.completed","run_id":"...","step_id":"node-1","status":"completed","elapsed_ms":820}

data: {"type":"run.completed","run_id":"..."}

会话形态与跨轮继承

门户按「引擎是否提供会话预创建接口」区分两种会话形态,两者都以同一套 X-Session-Id 请求头寻址,网关再翻译为各引擎的原生会话约定:

形态新建会话标识来源历史列表
引擎侧会话调引擎会话接口创建引擎创建时分配引擎侧列表
本地会话本地生成占位 id首轮响应回传引擎分配的标识后替换引擎侧列表(同样可列)

本地会话的占位 id 只用于门户内部寻址,不下发给引擎(引擎据此在首轮隐式建立会话); 网关从首轮流式响应中提取引擎分配的会话标识,以 x.dify.conversation 扩展帧回传门户, 门户把当前会话切到真实标识,后续轮次据此继承同一会话上下文。

历史会话列表与回看

  • 会话列表按最后活跃时间倒序展示,支持按标题搜索过滤
  • 列表只拉元信息(标题 / 时间),打开某个会话时才拉取该会话的历史消息
  • 历史记录按数据形态归一:引擎给「消息」列表(role + content)时直接可用;给 「轮次」列表(一问一答一条记录,含 query / answer)时拆成用户与助手两条; 工具消息与空白工具占位一并跳过(apps/enduser/src/utils/historyMessages.ts
  • 历史会话可继续对话,也可删除(门户侧同步移除列表项)

工作流输入参数表单

  • 引擎经参数端点自描述输入 schema(GET /api/gateway/v1/parameters),门户在会话首开前 渲染前置表单卡片
  • 表单值随首轮请求的 inputs 提交、并入本轮输入;变量落到引擎侧会话上下文后,后续轮次 不再重复提交
  • 支持 text-input / paragraph / number / select 四类字段,按类型渲染为单行输入、 多行文本或下拉,带必填校验与默认值;其余类型忽略不渲染
  • 无表单、schema 拉取失败或网络异常一律按「无表单」处理,不阻断对话;新建会话时表单回到 待提交态

处理步骤进度卡与暂停态

  • 工作流型引擎的节点进度以 step.started / step.completed 帧下发,门户按 step_id 建/更新步骤卡(状态点 + 节点名 + 耗时 + 状态:执行中 / 完成 / 失败)
  • 帧带 status: "waiting" 时步骤卡呈现等待人工处理暂停态,与执行中、完成态视觉区分
  • 乱序容错:step.completed 先于 step.started 到达(续接 / 丢帧)时按终态补建节点, 卡片不悬挂

思考过程卡片

  • 推理过程以 reasoning.delta 增量下发,门户累积为思考卡片;reasoning.completed 携带 整段推理时以完整文本为准(覆盖增量拼接,兼容引擎非流式推理)
  • 思考卡片与正文、工具卡、步骤卡同处一条按到达顺序排列的时间线,可展开查看与复制

附件入口按引擎能力显隐

附件能力由能力表拆两位下发:attachments_workspace(平台工作区链路,上传经 manager 写实例工作区)与 attachments_native(引擎原生上传链路,如绑定 Dify 应用的智能体,上传经网关透传引擎上传端点、文件随 消息以引擎原生字段送达)。两者皆无时门户不渲染附件上传入口(ChatComposerhide-attach), 避免用户走通不支持的链路;仅支持原生链路的引擎上传回执带 file_id,随消息发送时无需重新上传。

右侧工作区文件面板同样按能力显隐:仅当引擎暴露会话文件接口(session_files_api)时渲染; 接入型引擎(如外接 Dify 应用)没有平台工作区文件,面板整体不出现,也不发起文件请求。

停止生成

流式回复期间单击停止按钮:前端中断 SSE 读取,并向 POST /api/gateway/v1/runs/{run_id}/cancel 发起取消;网关对支持取消的引擎真正终止引擎侧执行——停止请求的路径带运行期任务标识、 请求体带与发起轮一致的归属信息,流尚未产出任务标识(极早取消)时不发停止请求。取消后该 run 以 run.cancelled 帧收尾,对已结束的 run 重复取消幂等返回。

工作区面板右上角提供打包下载按钮:将整个工作区导出为 workspace.tar.gz(排除平台凭据类文件),移动端在工作区页顶部工具栏提供同名入口。

4. 数据库模型

4.1 现有模型

用户体系已物理分表:users 仅存管理员账号(登录管理台、参与 RBAC);终端用户独立存 end_users 表(登录终端门户、无 RBAC 角色、按用户组隔离)。

模型表名说明
Userusers管理员账号(登录管理台,含角色/权限)
EndUserend_users终端用户账号(登录终端门户,无 RBAC 角色)
Roleroles角色(仅覆盖管理员账号)
Permissionpermissions权限
AgentagentsAI 智能体
UserGroupuser_groups用户组
EndUserGroupMemberend_user_group_members终端用户—用户组归属(终端侧组隔离)

4.2 新增模型

AgentSession — 终端用户-智能体访问记录

python
class AgentSession(Base):
    __tablename__ = "agent_sessions"
    id              = Column(UUID, primary_key=True)
    user_id         = Column(UUID, ForeignKey("end_users.id"), nullable=False)
    agent_id        = Column(UUID, ForeignKey("agents.id"), nullable=False)
    last_accessed_at = Column(DateTime, default=utcnow)
    access_count    = Column(Integer, default=1)

AgentDeployment — 智能体引擎部署状态

python
class AgentDeployment(Base):
    __tablename__ = "agent_deployments"
    id              = Column(UUID, primary_key=True)
    agent_id        = Column(UUID, nullable=False, unique=True)
    status          = Column(Enum, default=PENDING)  # PENDING/DEPLOYING/RUNNING/SUSPENDED/FAILED/ARCHIVED
    pod_name        = Column(String, nullable=True)
    namespace       = Column(String, default="veyraos")
    engine_url      = Column(String, nullable=True)
    deployed_at     = Column(DateTime, nullable=True)
    last_active_at  = Column(DateTime, nullable=True)
    backup_at       = Column(DateTime, nullable=True)
    archived_at     = Column(DateTime, nullable=True)
    archive_path    = Column(String, nullable=True)
    error_message   = Column(Text, nullable=True)

ChatSession — 聊天会话

python
class ChatSession(Base):
    __tablename__ = "chat_sessions"
    session_id  = Column(String(32), primary_key=True)
    agent_id    = Column(UUID, nullable=False, index=True)
    user_id     = Column(UUID, nullable=False, index=True)
    title       = Column(String(256), default="Untitled")
    model       = Column(String(128), default="hermes-agent")
    workspace   = Column(String(512), default="/workspace")
    messages    = Column(JSON, default=list)
    created_at  = Column(DateTime, default=utcnow)
    updated_at  = Column(DateTime, default=utcnow, onupdate=utcnow)
    archived    = Column(Boolean, default=False)

5. 智能体生命周期

5.1 状态机

PENDING → DEPLOYING → RUNNING → SUSPENDED → ARCHIVED
                          ↑         │
                          └─────────┘ (用户再次访问时恢复, scale=1)
状态PodPVCMinIO
PENDING
DEPLOYING创建中创建中
RUNNING1 副本
SUSPENDEDscale=0保留有 SUSPEND 存档
ARCHIVED已删除已删除有永久存档
FAILED

5.2 K8s 资源命名规范

资源命名规则示例
Deploymentengine-hermes-{agent_id[:8]}engine-hermes-a1b2c3
Serviceengine-hermes-{agent_id[:8]}engine-hermes-a1b2c3
PVCengine-hermes-{agent_id[:8]}-pvcengine-hermes-a1b2c3-pvc
Pod Labelagent-id={agent_id}agent-id=550e8400-...

5.3 部署流程

Controller.deploy(agent_id):
  1. 从 Manager 获取 agent 配置 (engine_type, config)
  2. 检查 AgentDeployment 记录
     ├── 不存在 / ARCHIVED → 全新创建
     └── SUSPENDED → 恢复 (scale=1)
  3. 创建 K8s Deployment + Service + PVC
     - 设置 PROVIDER_NAME/MODEL_NAME/API_KEY 等环境变量(从 agent config 读取)
  4. 推送 SSE 事件: creating_pod → configuring → waiting_ready → validating → engine_ready
  5. 等待 Pod Ready
  6. 记录 engine_url 到数据库

5.4 SSE 部署进度格式

javascript
event: progress
data: {"step": "creating_pod",   "message": "沙箱环境申请中...",  "percentage": 30}

event: progress
data: {"step": "configuring",    "message": "引擎配置注入中...",  "percentage": 50}

event: progress
data: {"step": "waiting_ready",  "message": "等待引擎就绪...",   "percentage": 70}

event: progress
data: {"step": "engine_ready",   "message": "引擎已就绪",       "percentage": 100}

event: error
data: {"message": "部署失败: ..."}

6. 引擎回收与数据持久化

6.1 设计原则

层级机制覆盖风险
PVC 实时写引擎运行时交互即落盘(Hermes 自身行为)Pod 崩溃/重启
SUSPEND 存档休眠前 exec 进 Pod,tar 数据目录上传 MinIOPVC 损坏/误删
DESTROY 确认仅当 SUSPEND 存档确认后,才删除 PVC存档失败则保留

6.2 MinIO 路径规范

backups/{agent_id}/
  └── latest.tar.gz              ← SUSPEND 时上传(最新快照)

archives/{agent_id}/
  └── {timestamp}.tar.gz         ← DESTROY 时从 backups 复制(永久留存)

6.3 SUSPEND 流程

RecycleScheduler (每 5 分钟):
  1. 遍历所有 RUNNING 部署, 检查 last_active_at
  2. 空闲超 30min → exec 进 Pod: tar czf - ~/.hermes/
  3. tar 流上传 MinIO: backups/{agent_id}/latest.tar.gz
  4. K8s scale Deployment 到 0
  5. 状态 → SUSPENDED

6.4 DESTROY 流程

CleanupScheduler (每小时):
  1. 遍历所有 SUSPENDED 部署, 检查 backup_at
  2. 超 24h → 复制 backups → archives/{timestamp}.tar.gz
  3. 删除 Deployment + Service + PVC
  4. 状态 → ARCHIVED

6.5 恢复流程

来源状态操作数据来源
SUSPENDEDscale=1PVC 保留完整数据
ARCHIVED创建新 Pod + exec tar 解压MinIO archives

7. Nginx 路由配置

nginx
server {
    listen 80;
    server_name chat.veyraos.com;

    # Portal 静态文件
    location / {
        root /usr/share/nginx/html;
        try_files $uri $uri/ /index.html;
    }

    # Manager API(认证 /api/manager/auth/enduser/*、终端可见实例 /api/manager/agent-instances/accessible 等)
    location /api/manager/ {
        proxy_pass http://manager:8002/api/manager/;
        proxy_buffering off;
        proxy_cache off;
        proxy_set_header Connection '';
        chunked_transfer_encoding on;
    }

    # Controller API (SSE;Controller 已并入 Manager,转发到 manager:8002)
    location /api/controller/ {
        proxy_pass http://manager:8002/api/controller/;
        proxy_buffering off;
        proxy_cache off;
        proxy_set_header Connection '';
        chunked_transfer_encoding on;
    }

    # Gateway (聊天请求, 同域代理)
    location /api/gateway/ {
        rewrite ^/api/gateway/(.*)$ /$1 break;
        proxy_pass http://gateway:8010;
        proxy_http_version 1.1;
        proxy_buffering off;
        proxy_cache off;
    }
}

8. 端口规划

分组服务K8s 端口本地开发说明
基础设施PostgreSQL54325432 (PF)
MinIO API90009000 (PF)
引擎Hermes Engine8642每个 agent 一个 Pod
后端微服务Manager8002--port 8002CRUD + 认证 + Controller 路由
Gateway8010--port 8010DNS 路由(避开 MinIO 9000)
前端Admin (Vite)8848管理后台
Enduser Portal (Vite)3000终端用户门户
Hermes WebUI87878787 (PF)第三方聊天界面(备用)

9. 关键技术决策

9.1 为什么不用 iframe(历史决策,已落地)

  • hermes-webui 的 X-Frame-Options: DENY 限制无法绕过
  • 跨域通信复杂(CORS + cookie + postMessage)
  • 两次路由(Portal 路由 + iframe 内路由)体验割裂
  • 无法统一主题和样式
  • 最终方案:自研 Vue 3 组件,hermes-webui 仅做参考实现

边界说明:以上结论只针对终端用户门户。平台另有一处唯一例外,且只适用于管理台:管理台工作台「构建」页签对外部工作流引擎(Dify / RAGFlow 等)的图形化编排画布只读预览允许 iframe 内嵌(iframe 地址只指向该实例已登记的外部控制台地址、带 sandbox 属性收敛权限、覆盖透明拦截层实现 UI 级只读、目标站拒绝被嵌时降级为外链,鉴权以用户自有外部控制台会话为前提)。终端门户不适用该例外,本节结论不受影响,也不要把门户的结论推广为全平台禁止 iframe。

9.2 Gateway 为什么不需要 Controller

  • Gateway 通过 DNS 命名规范直接从 X-Agent-ID 获取 upstream
  • Controller 按相同规范创建 Pod:engine-hermes-{agent_id[:8]}
  • 两者通过命名约定解耦,无需运行时依赖

9.3 SSE 流式注意事项

  • nginx 必须设置 proxy_buffering off;,否则 SSE 会被缓冲
  • proxy_set_header Connection "upgrade" 会干扰 SSE 流式响应
  • Gateway 前端必须去掉 Origin/Referer 头,引擎 API 会拒绝带这些头的请求

9.4 为什么选择非流式 fallback

Portal 的 nginx 代理 SSE 时,浏览器 fetch()ReadableStream 在某些 nginx 版本下会返回空响应(Failed to fetch)。已确认的解决方案是使用非流式请求作为 fallback。当前实现优先使用 SSE 流式,如果失败则自动回退到非流式。

10. 配置汇总

变量默认值说明
UA_K8S_NAMESPACEveyraosK8s 命名空间
UA_MINIO_BUCKETveyraos-archivesMinIO 备份 Bucket
UA_IDLE_SUSPEND_MINUTES30空闲多少分钟后休眠
UA_IDLE_DESTROY_HOURS24休眠后多少小时清理
UA_API_SERVER_KEYua-engine-dev-key引擎 API Key
UA_JWT_SECRET(开发密钥)JWT 签名密钥
UA_DATABASE_URL(连接串)PostgreSQL 连接
UA_MINIO_ENDPOINThttp://minio:9000MinIO 地址

11. 验证方式

11.1 本地开发

bash
# 启动后端
make dev-manager      # :8002
make dev-controller   # :8001
make dev-gateway      # :8010

# 启动前端
cd apps/enduser && pnpm dev  # :3000

# k3s 基础设施
make k8s-infra        # PostgreSQL + MinIO

11.2 端口转发(k3s)

bash
make pf-manager       # 8002 → Manager
make pf-controller    # 8001 → Controller
make pf-gateway       # 8010 → Gateway
make pf-enduser       # 3000 → Portal

11.3 端到端验证

#场景预期
1未登录访问 /agents跳转 /login?redirect=/agents
2登录后自动回跳成功回到原页
3可访问智能体列表只显示有权限的已发布 Agent
4访问未部署智能体部署进度条 → SSE → 完成
5访问已部署智能体直接进入 Chat 页面
6发送消息SSE 流式逐字回复
7切换会话历史消息正常加载
830min 无操作引擎自动休眠 (scale=0)
9再次访问已休眠引擎自动恢复
1024h 无访问引擎归档到 MinIO → Pod 删除

基于内网部署的企业级 AI 智能体平台