终端用户门户 + 智能体生命周期管理
企业级多智能体平台的核心用户侧链路。 终端用户通过独立门户登录、选择智能体、与引擎对话。 所有交互通过自研 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/models2.2 前端的页面与路由
| 路径 | 页面组件 | 功能 |
|---|---|---|
/login | LoginPage | 终端用户登录,支持 redirect 回跳 |
/agents | AgentListPage | 可访问智能体卡片列表,按最近访问排序 |
/agents/:id | AgentChatPage | 部署检测 → 进度展示 → 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/login | POST | 账号密码登录 |
POST /api/manager/auth/enduser/login-by-contact | POST | 已验证邮箱/手机号 + 密码登录 |
POST /api/manager/auth/enduser/login-by-sms-code | POST | 短信验证码登录(仅系统开启 SMS 渠道时) |
POST /api/manager/auth/enduser/demo-login | POST | 一键演示登录(部署开启演示功能时) |
POST /api/manager/auth/enduser/verification-code/send | POST | 发送短信/邮箱验证码 |
GET /api/manager/auth/me | GET | 当前用户信息(管理台与终端侧共用,按 token 定位所属账号表) |
GET /api/manager/agent-instances/accessible | GET | 终端用户可访问的已发布智能体实例列表 |
Controller 路由(并入 Manager :8002)
生命周期管理:
| 端点 | 方法 | 说明 |
|---|---|---|
GET /api/controller/agents/{id}/status | GET | 查询引擎部署状态 |
POST /api/controller/agents/{id}/deploy | POST | 创建/恢复引擎 |
GET /api/controller/agents/{id}/deploy/events | GET (SSE) | 部署进度事件流 |
POST /api/controller/agents/{id}/suspend | POST | 休眠引擎 (scale=0) |
POST /api/controller/agents/{id}/destroy | POST | 销毁引擎并归档 |
会话管理:
| 端点 | 方法 | 说明 |
|---|---|---|
POST /api/controller/chat/session/new | POST | 创建新会话 |
GET /api/controller/chat/sessions | GET | 列出某用户的会话 |
GET /api/controller/chat/session | GET | 获取会话详情 |
GET /api/controller/chat/dashboard/config | GET | 前端探活配置 |
GET /api/controller/chat/settings | GET | 会话设置 |
GET /api/controller/chat/models | GET | 模型列表 |
Gateway (端口 8010)
| 端点 | 方法 | 说明 |
|---|---|---|
ANY /{path} | ANY | DNS 命名规范路由,需要 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 | ~5043 | 8 套主题(深色/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-hCSS 变量驱动transform - iOS 视口:全局
100vh改100dvh解决地址栏跳动;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 应用的智能体,上传经网关透传引擎上传端点、文件随 消息以引擎原生字段送达)。两者皆无时门户不渲染附件上传入口(ChatComposer 的 hide-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 角色、按用户组隔离)。
| 模型 | 表名 | 说明 |
|---|---|---|
| User | users | 管理员账号(登录管理台,含角色/权限) |
| EndUser | end_users | 终端用户账号(登录终端门户,无 RBAC 角色) |
| Role | roles | 角色(仅覆盖管理员账号) |
| Permission | permissions | 权限 |
| Agent | agents | AI 智能体 |
| UserGroup | user_groups | 用户组 |
| EndUserGroupMember | end_user_group_members | 终端用户—用户组归属(终端侧组隔离) |
4.2 新增模型
AgentSession — 终端用户-智能体访问记录
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 — 智能体引擎部署状态
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 — 聊天会话
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)| 状态 | Pod | PVC | MinIO |
|---|---|---|---|
| PENDING | — | — | — |
| DEPLOYING | 创建中 | 创建中 | — |
| RUNNING | 1 副本 | 有 | — |
| SUSPENDED | scale=0 | 保留 | 有 SUSPEND 存档 |
| ARCHIVED | 已删除 | 已删除 | 有永久存档 |
| FAILED | — | — | — |
5.2 K8s 资源命名规范
| 资源 | 命名规则 | 示例 |
|---|---|---|
| Deployment | engine-hermes-{agent_id[:8]} | engine-hermes-a1b2c3 |
| Service | engine-hermes-{agent_id[:8]} | engine-hermes-a1b2c3 |
| PVC | engine-hermes-{agent_id[:8]}-pvc | engine-hermes-a1b2c3-pvc |
| Pod Label | agent-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 部署进度格式
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 数据目录上传 MinIO | PVC 损坏/误删 |
| 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. 状态 → SUSPENDED6.4 DESTROY 流程
CleanupScheduler (每小时):
1. 遍历所有 SUSPENDED 部署, 检查 backup_at
2. 超 24h → 复制 backups → archives/{timestamp}.tar.gz
3. 删除 Deployment + Service + PVC
4. 状态 → ARCHIVED6.5 恢复流程
| 来源状态 | 操作 | 数据来源 |
|---|---|---|
| SUSPENDED | scale=1 | PVC 保留完整数据 |
| ARCHIVED | 创建新 Pod + exec tar 解压 | MinIO archives |
7. 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 端口 | 本地开发 | 说明 |
|---|---|---|---|---|
| 基础设施 | PostgreSQL | 5432 | 5432 (PF) | |
| MinIO API | 9000 | 9000 (PF) | ||
| 引擎 | Hermes Engine | 8642 | — | 每个 agent 一个 Pod |
| 后端微服务 | Manager | 8002 | --port 8002 | CRUD + 认证 + Controller 路由 |
| Gateway | 8010 | --port 8010 | DNS 路由(避开 MinIO 9000) | |
| 前端 | Admin (Vite) | — | 8848 | 管理后台 |
| Enduser Portal (Vite) | — | 3000 | 终端用户门户 | |
| Hermes WebUI | 8787 | 8787 (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_NAMESPACE | veyraos | K8s 命名空间 |
UA_MINIO_BUCKET | veyraos-archives | MinIO 备份 Bucket |
UA_IDLE_SUSPEND_MINUTES | 30 | 空闲多少分钟后休眠 |
UA_IDLE_DESTROY_HOURS | 24 | 休眠后多少小时清理 |
UA_API_SERVER_KEY | ua-engine-dev-key | 引擎 API Key |
UA_JWT_SECRET | (开发密钥) | JWT 签名密钥 |
UA_DATABASE_URL | (连接串) | PostgreSQL 连接 |
UA_MINIO_ENDPOINT | http://minio:9000 | MinIO 地址 |
11. 验证方式
11.1 本地开发
# 启动后端
make dev-manager # :8002
make dev-controller # :8001
make dev-gateway # :8010
# 启动前端
cd apps/enduser && pnpm dev # :3000
# k3s 基础设施
make k8s-infra # PostgreSQL + MinIO11.2 端口转发(k3s)
make pf-manager # 8002 → Manager
make pf-controller # 8001 → Controller
make pf-gateway # 8010 → Gateway
make pf-enduser # 3000 → Portal11.3 端到端验证
| # | 场景 | 预期 |
|---|---|---|
| 1 | 未登录访问 /agents | 跳转 /login?redirect=/agents |
| 2 | 登录后自动回跳 | 成功回到原页 |
| 3 | 可访问智能体列表 | 只显示有权限的已发布 Agent |
| 4 | 访问未部署智能体 | 部署进度条 → SSE → 完成 |
| 5 | 访问已部署智能体 | 直接进入 Chat 页面 |
| 6 | 发送消息 | SSE 流式逐字回复 |
| 7 | 切换会话 | 历史消息正常加载 |
| 8 | 30min 无操作 | 引擎自动休眠 (scale=0) |
| 9 | 再次访问已休眠引擎 | 自动恢复 |
| 10 | 24h 无访问 | 引擎归档到 MinIO → Pod 删除 |