# 系统架构设计
**本文引用的文件**
- [ai-gateway/src/main/java/com/zsjz/gateway/route/StickyRouteFilter.java](file://ai-gateway/src/main/java/com/zsjz/gateway/route/StickyRouteFilter.java)
- [ai-gateway/src/main/java/com/zsjz/gateway/route/LoginRequestFilter.java](file://ai-gateway/src/main/java/com/zsjz/gateway/route/LoginRequestFilter.java)
- [ai-gateway/src/main/java/com/zsjz/gateway/route/LoginResponseFilter.java](file://ai-gateway/src/main/java/com/zsjz/gateway/route/LoginResponseFilter.java)
- [ai-gateway/src/main/java/com/zsjz/gateway/route/TokenResolver.java](file://ai-gateway/src/main/java/com/zsjz/gateway/route/TokenResolver.java)
- [ai-gateway/src/main/java/com/zsjz/gateway/route/UserRouteRepository.java](file://ai-gateway/src/main/java/com/zsjz/gateway/route/UserRouteRepository.java)
- [ai-gateway/src/main/java/com/zsjz/gateway/route/UserRouteService.java](file://ai-gateway/src/main/java/com/zsjz/gateway/route/UserRouteService.java)
- [ai-gateway/src/main/java/com/zsjz/gateway/route/NodeSelector.java](file://ai-gateway/src/main/java/com/zsjz/gateway/route/NodeSelector.java)
- [ai-gateway/src/main/java/com/zsjz/gateway/registry/GatewayRegistry.java](file://ai-gateway/src/main/java/com/zsjz/gateway/registry/GatewayRegistry.java)
- [ai-gateway/src/main/java/com/zsjz/gateway/registry/HealthChecker.java](file://ai-gateway/src/main/java/com/zsjz/gateway/registry/HealthChecker.java)
- [ai-gateway/src/main/java/com/zsjz/gateway/registry/NodeHealthRegistry.java](file://ai-gateway/src/main/java/com/zsjz/gateway/registry/NodeHealthRegistry.java)
- [ai-gateway/src/main/java/com/zsjz/gateway/config/GatewayProperties.java](file://ai-gateway/src/main/java/com/zsjz/gateway/config/GatewayProperties.java)
- [sql/gateway.sql](file://sql/gateway.sql)
- [ai-electron/electron/preload/bridge.ts](file://ai-electron/electron/preload/bridge.ts)
- [ai-electron/electron/main.ts](file://ai-electron/electron/main.ts)
- [ai-server/src/main/java/com/zsjz/ai/module/agent/service/impl/AgentChatServiceImpl.java](file://ai-server/src/main/java/com/zsjz/ai/module/agent/service/impl/AgentChatServiceImpl.java)
- [ai-server/src/main/java/com/zsjz/ai/common/datasource/CaseRoutingDataSource.java](file://ai-server/src/main/java/com/zsjz/ai/common/datasource/CaseRoutingDataSource.java)
- [ai-server/src/main/java/com/zsjz/ai/common/datasource/CaseDataSourceRegistry.java](file://ai-server/src/main/java/com/zsjz/ai/common/datasource/CaseDataSourceRegistry.java)
- [ai-server/src/main/java/com/zsjz/ai/module/agent/rag/RagSchemaService.java](file://ai-server/src/main/java/com/zsjz/ai/module/agent/rag/RagSchemaService.java)
## 目录
1. [引言](#引言)
2. [项目结构](#项目结构)
3. [核心组件](#核心组件)
4. [架构总览](#架构总览)
5. [详细组件分析](#详细组件分析)
6. [依赖关系分析](#依赖关系分析)
7. [性能与可扩展性](#性能与可扩展性)
8. [故障排查指南](#故障排查指南)
9. [结论](#结论)
10. [附录:部署拓扑](#附录部署拓扑)
## 引言
本架构文档描述清鉴 AI 调查工具的分层模型:Electron 客户端 → Preload Bridge → Spring Cloud Gateway(ai-gateway)→ ai-server 业务模块 → PostgreSQL/pgvector/Redis/文件系统。
该系统的核心约束是“粘性路由”:用户会话必须固定到同一 ai-server 节点,因为案件数据源、本地工作区、RAG 索引和 AgentScope 中间状态都位于节点本地。网关在登录后把用户绑定到某个后端节点,并在后续请求中强制复用;若绑定节点不可用,网关返回 503,而不是自动切换。
## 项目结构
仓库采用多模块 Maven 工程,根 `pom.xml` 声明两个子模块:
```mermaid
graph TB
Root["根工程 zsjz-ai
Maven POM"] --> Server["ai-server
Spring Boot 业务服务"]
Root --> Gateway["ai-gateway
Spring Cloud Gateway"]
Server --> PG["PostgreSQL / pgvector"]
Server --> Redis["Redis"]
Server --> FS["本地文件系统"]
Gateway --> Server
```
**图表来源**
- [pom.xml:1-23](file://pom.xml#L1-L23)
**章节来源**
- [pom.xml:1-23](file://pom.xml#L1-L23)
## 核心组件
- **Electron 客户端**:桌面应用外壳,通过 preload 桥接暴露 IPC 能力,渲染进程使用受限上下文。
- **Preload Bridge**:隔离主进程 API,仅导出受控的 `electron.ipcRenderer`,避免渲染进程直接访问 Electron 内部对象。
- **Spring Cloud Gateway**:统一入口,承担跨域、管理端白名单、服务发现、节点健康检查、用户粘性路由、SSE 流式转发等职责。
- **ai-server**:按业务模块划分(Agent、DM、Call、Track、Trans、Govern 等),承载案件数据源、AgentScope RAG、Python/SQL 分析、附件处理、SSE 研判输出。
- **持久层**:PostgreSQL/pgvector 存储业务数据与向量检索结果,Redis 作为网关 Token 缓存和用户路由热缓存,文件系统承载附件、工作区、DuckDB 等本地数据。
**章节来源**
- [ai-electron/electron/preload/bridge.ts:1-10](file://ai-electron/electron/preload/bridge.ts#L1-L10)
- [ai-electron/electron/main.ts:1-19](file://ai-electron/electron/main.ts#L1-L19)
- [ai-gateway/pom.xml:37-37](file://ai-gateway/pom.xml#L37-L37)
## 架构总览
从端到端视角,一次带登录和分析的请求流程如下:
```mermaid
sequenceDiagram
participant Client as "Electron 客户端"
participant Preload as "Preload Bridge"
participant Gateway as "ai-gateway"
participant Service as "ai-server 节点"
participant DB as "PostgreSQL/pgvector"
participant Cache as "Redis"
participant File as "本地文件系统"
Client->>Preload: 发起登录或业务请求
Preload-->>Client: 经 ipcRenderer 转发请求
Client->>Gateway: POST /js/a/auth/login 或业务接口
Gateway->>Gateway: LoginRequestFilter 解析 username
Gateway->>Gateway: StickyRouteFilter 选择并固定节点
alt 首次登录
Gateway->>Service: 转发登录请求
Service->>DB: 校验账号
Service->>Cache: sa-token 写入
Service-->>Gateway: Result{token, userId, username}
Gateway->>Gateway: LoginResponseFilter 写库绑定 + 缓存 token
else 已登录
Gateway->>Cache: 查询 x-token → userId
Gateway->>Gateway: 查找用户绑定的 nodeId
Gateway->>Service: 转发业务请求
Service->>DB: 读取案件数据源
Service->>File: 读取工作区/附件
Service-->>Gateway: SSE 或 JSON 响应
end
Gateway-->>Client: 透传响应
```
**图表来源**
- [ai-gateway/src/main/java/com/zsjz/gateway/route/LoginRequestFilter.java:1-120](file://ai-gateway/src/main/java/com/zsjz/gateway/route/LoginRequestFilter.java#L1-L120)
- [ai-gateway/src/main/java/com/zsjz/gateway/route/StickyRouteFilter.java:1-180](file://ai-gateway/src/main/java/com/zsjz/gateway/route/StickyRouteFilter.java#L1-L180)
- [ai-gateway/src/main/java/com/zsjz/gateway/route/LoginResponseFilter.java:1-176](file://ai-gateway/src/main/java/com/zsjz/gateway/route/LoginResponseFilter.java#L1-L176)
- [ai-gateway/src/main/java/com/zsjz/gateway/route/TokenResolver.java:1-100](file://ai-gateway/src/main/java/com/zsjz/gateway/route/TokenResolver.java#L1-L100)
## 详细组件分析
### 粘性路由设计目标
粘性路由的目标不是负载均衡,而是“会话一致性”。每个 ai-server 节点持有:
- 案件的动态数据源注册;
- 本地工作区、附件、DuckDB 等文件系统状态;
- AgentScope RAG 的嵌入索引和会话上下文;
- 由 StpUtil 管理的 session/token 信息(可通过共享 Redis 跨实例恢复,但本地工作区不能)。
因此,一旦用户绑定到某个节点,后续请求必须继续命中该节点。如果节点被停用或健康检查失败,网关拒绝自动切换,而是返回 503,提示管理员修复节点或重新绑定。
**章节来源**
- [sql/gateway.sql:1-107](file://sql/gateway.sql#L1-L107)
### StickyRouteFilter:在 RouteToRequestFilter 之后重写目标 URL
StickyRouteFilter 是粘性路由的核心全局过滤器,执行顺序为 `10001`,排在 Spring Cloud Gateway 内置的 `RouteToRequestUrlFilter`(顺序 `10000`)之后。它的工作是:
1. 根据当前路由元数据确定目标服务;
2. 区分登录请求与普通请求;
3. 登录请求优先按 username 查绑定,未绑定时轮询候选节点但不落库;
4. 普通请求按 `x-token` 解析 userId,再查绑定;未绑定时尝试绑定到服务的默认节点;
5. 对匿名请求走健康节点轮询;
6. 校验选定节点的启用状态与健康状态;
7. 设置 `GATEWAY_REQUEST_URL_ATTR`,将目标 URL 重写为“节点 base_url + 原始 rawPath + rawQuery”;
8. 将 `X-Gateway-Node` 写入响应头,便于客户端验证会话是否始终落在同一节点。
```mermaid
flowchart TD
Start(["进入 StickyRouteFilter"]) --> CheckRoute["检查是否匹配 ai-server 路由"]
CheckRoute --> IsLogin{"是否登录请求?"}
IsLogin -->|是| DecideLogin["按 username 查绑定
未绑定则轮询候选节点"]
IsLogin -->|否| DecideNormal["按 x-token 解析 userId
查绑定或绑定默认节点"]
DecideLogin --> SelectNode["确定 nodeId"]
DecideNormal --> SelectNode
SelectNode --> ValidateNode{"节点启用且健康?"}
ValidateNode -->|否| Return503["抛出 503 服务不可用"]
ValidateNode -->|是| RewriteURL["重写 GATEWAY_REQUEST_URL_ATTR
base_url + rawPath + rawQuery"]
RewriteURL --> NextChain["交给 NettyRoutingFilter 流式转发"]
```
**图表来源**
- [ai-gateway/src/main/java/com/zsjz/gateway/route/StickyRouteFilter.java:1-180](file://ai-gateway/src/main/java/com/zsjz/gateway/route/StickyRouteFilter.java#L1-L180)
**章节来源**
- [ai-gateway/src/main/java/com/zsjz/gateway/route/StickyRouteFilter.java:1-180](file://ai-gateway/src/main/java/com/zsjz/gateway/route/StickyRouteFilter.java#L1-L180)
### LoginRequestFilter:登录前解析 username
LoginRequestFilter 只在配置的完整登录路径上生效,负责:
- 限制最大请求体大小(64KB);
- 解析登录体中的 `username`;
- 标记 `LOGIN_REQUEST` 属性;
- 强制把 `Accept-Encoding` 改为 `identity`,避免后端 gzip 压缩导致响应无法被 LoginResponseFilter 正确解析;
- 重建可重复读取的请求体,确保原业务内容不被修改。
如果请求体为空、超限、非 JSON 或缺少 `username`,过滤器不会阻断请求,而是按匿名轮询处理,由后端自行校验登录逻辑。
**章节来源**
- [ai-gateway/src/main/java/com/zsjz/gateway/route/LoginRequestFilter.java:1-120](file://ai-gateway/src/main/java/com/zsjz/gateway/route/LoginRequestFilter.java#L1-L120)
### LoginResponseFilter:登录成功后写库绑定
LoginResponseFilter 的执行顺序为 `NettyWriteResponseFilter.WRITE_RESPONSE_FILTER_ORDER - 1`,即比真正写出响应的过滤器更早一层。它在响应写出时:
1. 确认当前请求属于某个服务的登录路径;
2. 装饰响应,收集最多 256KB 的响应体;
3. 判断响应是否为成功登录结构(`code=200`,data 包含 `userId`、`username`);
4. 调用 `UserRouteService.bindIfAbsent` 把用户绑定到 StickyRouteFilter 选定的节点;
5. 调用 `TokenResolver.cacheToken` 把 `gateway:token:{serviceId}:{token}` 写入 Redis,TTL 24 小时;
6. 无论绑定成功与否,都不改变原始登录响应。
绑定操作是 fire-and-forget:即使客户端断开连接或数据库写入失败,也不会中断本次登录响应。
```mermaid
sequenceDiagram
participant GW as "ai-gateway"
participant LRF as "LoginResponseFilter"
participant URS as "UserRouteService"
participant TR as "TokenResolver"
participant PG as "PostgreSQL"
participant RD as "Redis"
GW->>LRF: 拦截登录响应
LRF->>LRF: 收集响应体(≤256KB)
LRF->>LRF: 解析 Result{code,data.token,data.userId,data.username}
alt 登录成功
LRF->>URS: bindIfAbsent(serviceId, userId, username, chosenNodeId)
URS->>PG: ON CONFLICT DO NOTHING 插入
URS-->>LRF: 生效的 nodeId
LRF->>TR: cacheToken(serviceId, token, userId)
TR->>RD: 写入 gateway:token:...
else 登录失败或结构不匹配
LRF->>LRF: 跳过绑定
end
LRF-->>GW: 原样写出响应
```
**图表来源**
- [ai-gateway/src/main/java/com/zsjz/gateway/route/LoginResponseFilter.java:1-176](file://ai-gateway/src/main/java/com/zsjz/gateway/route/LoginResponseFilter.java#L1-L176)
- [ai-gateway/src/main/java/com/zsjz/gateway/route/UserRouteService.java:1-117](file://ai-gateway/src/main/java/com/zsjz/gateway/route/UserRouteService.java#L1-L117)
- [ai-gateway/src/main/java/com/zsjz/gateway/route/TokenResolver.java:1-100](file://ai-gateway/src/main/java/com/zsjz/gateway/route/TokenResolver.java#L1-L100)
- [sql/gateway.sql:1-107](file://sql/gateway.sql#L1-L107)
**章节来源**
- [ai-gateway/src/main/java/com/zsjz/gateway/route/LoginResponseFilter.java:1-176](file://ai-gateway/src/main/java/com/zsjz/gateway/route/LoginResponseFilter.java#L1-L176)
- [ai-gateway/src/main/java/com/zsjz/gateway/route/UserRouteService.java:1-117](file://ai-gateway/src/main/java/com/zsjz/gateway/route/UserRouteService.java#L1-L117)
### TokenResolver:x-token → userId 两级解析
TokenResolver 提供三类能力:
- `resolveUserId`:先查 Redis 缓存键 `gateway:token:{serviceId}:{token}`,未命中时调用 StpUtil 从 sa-token 共享 Redis 解析,再回填缓存;
- `cacheToken`:登录成功后主动写入缓存;
- `resolveUserIdDirect`:管理端低频认证直接查 sa-token。
解析失败返回空 Mono,由 StickyRouteFilter 降级为匿名请求,让后端返回 401 引导重新登录。
**章节来源**
- [ai-gateway/src/main/java/com/zsjz/gateway/route/TokenResolver.java:1-100](file://ai-gateway/src/main/java/com/zsjz/gateway/route/TokenResolver.java#L1-L100)
### UserRouteService 与 UserRouteRepository:用户路由缓存与持久化
UserRouteService 是 Redis 两级缓存入口,键包括:
- `gateway:route:user:{serviceId}:{userId}`;
- `gateway:route:name:{serviceId}:{lower(username)}`。
读失败降级直查数据库;写失败只记日志,下次读回源。UserRouteRepository 使用 JdbcTemplate 访问 PostgreSQL,并发安全由唯一索引裁决:
- `(service_id, user_id)`;
- `(service_id, lower(username))`。
`bindIfAbsent` 使用 `ON CONFLICT DO NOTHING`,并发下由数据库决定赢家,随后再次读取生效节点。
**章节来源**
- [ai-gateway/src/main/java/com/zsjz/gateway/route/UserRouteService.java:1-117](file://ai-gateway/src/main/java/com/zsjz/gateway/route/UserRouteService.java#L1-L117)
- [ai-gateway/src/main/java/com/zsjz/gateway/route/UserRouteRepository.java:1-227](file://ai-gateway/src/main/java/com/zsjz/gateway/route/UserRouteRepository.java#L1-L227)
- [sql/gateway.sql:60-107](file://sql/gateway.sql#L60-L107)
### NodeSelector:匿名与新用户轮询
NodeSelector 只对无绑定请求生效。它会过滤出启用且健康的节点,单节点直接返回,多节点按服务维度原子计数器轮询。注意:已绑定用户的节点即使 DOWN 也不会被换掉。
**章节来源**
- [ai-gateway/src/main/java/com/zsjz/gateway/route/NodeSelector.java:1-45](file://ai-gateway/src/main/java/com/zsjz/gateway/route/NodeSelector.java#L1-L45)
### 服务注册与健康检查
GatewayRegistry 维护服务与节点的内存快照,事实来源是 PostgreSQL。启动时用 YAML 播种,运行中每 10 秒对账,变化时发布 `RefreshRoutesEvent` 刷新路由缓存。
HealthChecker 定时探测每个启用的节点健康路径,连续失败达到阈值标记 DOWN,一次成功恢复 UP。NodeHealthRegistry 是每网关实例的内存状态,UNKNOWN 乐观视为可用,避免刚启动时全部 503。
**章节来源**
- [ai-gateway/src/main/java/com/zsjz/gateway/registry/GatewayRegistry.java:1-116](file://ai-gateway/src/main/java/com/zsjz/gateway/registry/GatewayRegistry.java#L1-L116)
- [ai-gateway/src/main/java/com/zsjz/gateway/registry/HealthChecker.java:1-62](file://ai-gateway/src/main/java/com/zsjz/gateway/registry/HealthChecker.java#L1-L62)
- [ai-gateway/src/main/java/com/zsjz/gateway/registry/NodeHealthRegistry.java:1-68](file://ai-gateway/src/main/java/com/zsjz/gateway/registry/NodeHealthRegistry.java#L1-L68)
- [ai-gateway/src/main/java/com/zsjz/gateway/config/GatewayProperties.java:1-81](file://ai-gateway/src/main/java/com/zsjz/gateway/config/GatewayProperties.java#L1-L81)
### 客户端与跨进程安全隔离
Electron 主进程通过 `main.ts` 注册生命周期与 preload,preload 通过 `contextBridge.exposeInMainWorld('electron', ...)` 仅暴露 `ipcRenderer`。渲染进程不能直接访问 Electron 原生 API,从而降低 XSS 或恶意前端代码对宿主系统的风险。
```mermaid
graph LR
Renderer["渲染进程
Vue 前端"] --> Bridge["contextBridge
bridge.ts"]
Bridge --> MainProcess["主进程
main.ts + ee-core"]
MainProcess --> Network["网络请求
指向 ai-gateway"]
```
**图表来源**
- [ai-electron/electron/preload/bridge.ts:1-10](file://ai-electron/electron/preload/bridge.ts#L1-L10)
- [ai-electron/electron/main.ts:1-19](file://ai-electron/electron/main.ts#L1-L19)
**章节来源**
- [ai-electron/electron/preload/bridge.ts:1-10](file://ai-electron/electron/preload/bridge.ts#L1-L10)
- [ai-electron/electron/main.ts:1-19](file://ai-electron/electron/main.ts#L1-L19)
### SSE 心跳保活
Spring Cloud Gateway 基于 WebFlux/Netty,支持流式转发,天然兼容 SSE、multipart 和 Range 下载。配置中明确不设全局响应超时,以容纳长时间运行的 SSE 研判连接。粘性路由保证同一次会话落到同一 ai-server,避免 SSE 中途切到另一个没有对应 AgentScope 状态的节点。
建议运维侧在网关上游再叠加反向代理心跳策略(如 Nginx `proxy_read_timeout`、云负载均衡空闲超时),但不应把业务长连接误判为空闲而切断。
**章节来源**
- [ai-gateway/pom.xml:37-37](file://ai-gateway/pom.xml#L37-L37)
- [ai-gateway/README.md:108-108](file://ai-gateway/README.md#L108-L108)
### 动态数据源切换
ai-server 内按案件维度管理动态数据源。`CaseDataSourceRegistry` 负责注册和查询数据源,`CaseRoutingDataSource` 作为动态路由数据源,把线程或会话上下文的 caseId 映射到具体数据库连接。这样不同案件可以访问不同的 PostgreSQL 库表,同时共享 ai-server 自身的平台库。
```mermaid
classDiagram
class CaseDataSourceRegistry {
+注册数据源()
+获取数据源()
+清理数据源()
}
class CaseRoutingDataSource {
+determineCurrentLookupKey()
}
class DataSourceConfig {
+初始化平台数据源()
}
CaseRoutingDataSource --> CaseDataSourceRegistry : "查询当前案件数据源"
DataSourceConfig --> CaseDataSourceRegistry : "注册平台数据源"
```
**图表来源**
- [ai-server/src/main/java/com/zsjz/ai/common/datasource/CaseDataSourceRegistry.java](file://ai-server/src/main/java/com/zsjz/ai/common/datasource/CaseDataSourceRegistry.java)
- [ai-server/src/main/java/com/zsjz/ai/common/datasource/CaseRoutingDataSource.java](file://ai-server/src/main/java/com/zsjz/ai/common/datasource/CaseRoutingDataSource.java)
- [ai-server/src/main/java/com/zsjz/ai/common/config/DataSourceConfig.java](file://ai-server/src/main/java/com/zsjz/ai/common/config/DataSourceConfig.java)
**章节来源**
- [ai-server/src/main/java/com/zsjz/ai/common/datasource/CaseDataSourceRegistry.java](file://ai-server/src/main/java/com/zsjz/ai/common/datasource/CaseDataSourceRegistry.java)
- [ai-server/src/main/java/com/zsjz/ai/common/datasource/CaseRoutingDataSource.java](file://ai-server/src/main/java/com/zsjz/ai/common/datasource/CaseRoutingDataSource.java)
- [ai-server/src/main/java/com/zsjz/ai/common/config/DataSourceConfig.java](file://ai-server/src/main/java/com/zsjz/ai/common/config/DataSourceConfig.java)
### AgentScope RAG 调用
ai-server 的 agent 模块包含 RAG 相关类,例如 `RagSchemaService`、`EmbeddingProvider`、`WorkspaceSchemaCatalog`、`AttachmentContentRouter` 等,负责 schema 管理、向量嵌入、工作区目录扫描、附件内容路由等。Agent 聊天实现 `AgentChatServiceImpl` 承载会话、意图、工具调用和结果聚合。
这些能力依赖 PostgreSQL/pgvector 做向量检索,依赖本地文件系统存取工作区、技能和附件,因此必须保持用户会话固定在同一个 ai-server 节点。
**章节来源**
- [ai-server/src/main/java/com/zsjz/ai/module/agent/rag/RagSchemaService.java](file://ai-server/src/main/java/com/zsjz/ai/module/agent/rag/RagSchemaService.java)
- [ai-server/src/main/java/com/zsjz/ai/module/agent/service/impl/AgentChatServiceImpl.java](file://ai-server/src/main/java/com/zsjz/ai/module/agent/service/impl/AgentChatServiceImpl.java)
## 依赖关系分析
```mermaid
graph TB
Client["Electron 客户端"] --> Preload["Preload Bridge"]
Preload --> Gateway["ai-gateway"]
subgraph GatewayLayer
LRF["LoginRequestFilter"]
SRF["StickyRouteFilter"]
LResF["LoginResponseFilter"]
TR["TokenResolver"]
URS["UserRouteService"]
UR["UserRouteRepository"]
NS["NodeSelector"]
GR["GatewayRegistry"]
HC["HealthChecker"]
HReg["NodeHealthRegistry"]
end
subgraph BackendLayer
AS["ai-server"]
AChat["AgentChatServiceImpl"]
RAG["RagSchemaService"]
CDR["CaseRoutingDataSource"]
CDS["CaseDataSourceRegistry"]
end
subgraph Storage
PG["PostgreSQL/pgvector"]
RD["Redis"]
FS["本地文件系统"]
end
Client --> Gateway
LRF --> SRF
SRF --> TR
SRF --> URS
SRF --> NS
LResF --> URS
LResF --> TR
URS --> UR
TR --> RD
UR --> PG
GR --> PG
HC --> RD
HC --> HReg
AS --> PG
AS --> RD
AS --> FS
AChat --> RAG
AChat --> CDR
CDR --> CDS
```
**图表来源**
- [ai-gateway/src/main/java/com/zsjz/gateway/route/LoginRequestFilter.java:1-120](file://ai-gateway/src/main/java/com/zsjz/gateway/route/LoginRequestFilter.java#L1-L120)
- [ai-gateway/src/main/java/com/zsjz/gateway/route/StickyRouteFilter.java:1-180](file://ai-gateway/src/main/java/com/zsjz/gateway/route/StickyRouteFilter.java#L1-L180)
- [ai-gateway/src/main/java/com/zsjz/gateway/route/LoginResponseFilter.java:1-176](file://ai-gateway/src/main/java/com/zsjz/gateway/route/LoginResponseFilter.java#L1-L176)
- [ai-gateway/src/main/java/com/zsjz/gateway/route/TokenResolver.java:1-100](file://ai-gateway/src/main/java/com/zsjz/gateway/route/TokenResolver.java#L1-L100)
- [ai-gateway/src/main/java/com/zsjz/gateway/route/UserRouteService.java:1-117](file://ai-gateway/src/main/java/com/zsjz/gateway/route/UserRouteService.java#L1-L117)
- [ai-gateway/src/main/java/com/zsjz/gateway/route/UserRouteRepository.java:1-227](file://ai-gateway/src/main/java/com/zsjz/gateway/route/UserRouteRepository.java#L1-L227)
- [ai-gateway/src/main/java/com/zsjz/gateway/route/NodeSelector.java:1-45](file://ai-gateway/src/main/java/com/zsjz/gateway/route/NodeSelector.java#L1-L45)
- [ai-gateway/src/main/java/com/zsjz/gateway/registry/GatewayRegistry.java:1-116](file://ai-gateway/src/main/java/com/zsjz/gateway/registry/GatewayRegistry.java#L1-L116)
- [ai-gateway/src/main/java/com/zsjz/gateway/registry/HealthChecker.java:1-62](file://ai-gateway/src/main/java/com/zsjz/gateway/registry/HealthChecker.java#L1-L62)
- [ai-gateway/src/main/java/com/zsjz/gateway/registry/NodeHealthRegistry.java:1-68](file://ai-gateway/src/main/java/com/zsjz/gateway/registry/NodeHealthRegistry.java#L1-L68)
- [ai-server/src/main/java/com/zsjz/ai/module/agent/service/impl/AgentChatServiceImpl.java](file://ai-server/src/main/java/com/zsjz/ai/module/agent/service/impl/AgentChatServiceImpl.java)
- [ai-server/src/main/java/com/zsjz/ai/module/agent/rag/RagSchemaService.java](file://ai-server/src/main/java/com/zsjz/ai/module/agent/rag/RagSchemaService.java)
- [ai-server/src/main/java/com/zsjz/ai/common/datasource/CaseRoutingDataSource.java](file://ai-server/src/main/java/com/zsjz/ai/common/datasource/CaseRoutingDataSource.java)
- [ai-server/src/main/java/com/zsjz/ai/common/datasource/CaseDataSourceRegistry.java](file://ai-server/src/main/java/com/zsjz/ai/common/datasource/CaseDataSourceRegistry.java)
## 性能与可扩展性
### 多 ai-server 节点扩展
- 新增节点只需在 PostgreSQL 的 `gateway_node` 表中登记,或通过管理端更新后触发 `RefreshRoutesEvent`;
- 新用户的登录会轮询健康节点,分散新用户负载;
- 已登录用户仍粘在原有节点,避免共享状态不一致;
- 健康检查周期和失败阈值可通过 `GatewayProperties.HealthCheck` 调整。
### 容错策略
- 绑定节点 DOWN:StickyRouteFilter 直接抛 503,不自动切换;
- 所有健康节点 DOWN:匿名请求和新用户分配也返回 503;
- Token 过期或被顶下线:解析失败后按匿名处理,后端返回 401,客户端重登;
- Redis 缓存故障:Token 解析和路由缓存均降级直查 sa-token 或数据库,不影响正确性;
- 登录绑定失败:fire-and-forget,不阻塞登录响应,下次登录补绑。
### SSE 与长连接
- 网关不设置全局响应超时,允许 SSE 研判长时间保持;
- 粘性路由保证同一次 SSE 会话不落盘不同节点;
- 建议在网关上游负载均衡器中合理配置空闲超时,避免误杀长连接。
### 横切关注点总结
- 跨进程安全隔离:Electron preload 仅暴露最小 API;
- 身份认证:sa-token + Redis 共享缓存;
- 粘性路由:PostgreSQL 用户路由表 + Redis 两级缓存;
- 健康检查:WebClient 探活 + 内存状态机;
- 流式转发:WebFlux/Netty 支持 SSE;
- 动态数据源:按案件切换 PostgreSQL 连接;
- RAG 调用:AgentScope 工作区、向量索引、附件内容路由。
[本节为通用性能讨论,不直接分析具体代码行]
## 故障排查指南
| 现象 | 可能原因 | 排查要点 |
| --- | --- | --- |
| 登录后多次请求跳到不同 ai-server | StickyRouteFilter 未命中绑定 | 检查 `X-Gateway-Node` 响应头是否一致;查看 `gateway_user_route` 是否有记录 |
| 绑定节点停止后业务报 503 | 粘性路由禁止自动切换 | 检查 HealthChecker 健康状态;恢复节点或手动重绑 |
| 登录成功但后续请求 401 | Token 失效或被顶下线 | 检查 Redis `gateway:token:*`;确认 sa-token 是否有效 |
| 新用户一直 503 | 所有节点 DOWN 或未启用 | 检查 `gateway_node.enabled`、健康检查路径、网络连通性 |
| SSE 中途断开 | 上游负载均衡或浏览器关闭 | 检查负载均衡空闲超时;确认浏览器标签页是否关闭 |
| 动态数据源找不到 | 案件未注册或 caseId 上下文丢失 | 检查 CaseDataSourceRegistry;确认 CaseRoutingDataSource 路由键 |
| RAG 搜索不到结果 | 向量索引未构建或工作区不完整 | 检查 RagSchemaService、附件索引、本地文件系统 |
**章节来源**
- [ai-gateway/src/main/java/com/zsjz/gateway/route/StickyRouteFilter.java:1-180](file://ai-gateway/src/main/java/com/zsjz/gateway/route/StickyRouteFilter.java#L1-L180)
- [ai-gateway/src/main/java/com/zsjz/gateway/registry/HealthChecker.java:1-62](file://ai-gateway/src/main/java/com/zsjz/gateway/registry/HealthChecker.java#L1-L62)
- [sql/gateway.sql:1-107](file://sql/gateway.sql#L1-L107)
## 结论
清鉴 AI 调查工具通过“Electron 客户端 + Preload 安全桥 + Spring Cloud Gateway + ai-server + PostgreSQL/pgvector/Redis/文件系统”的分层架构,把安全性、可观测性和稳定性放在前端与网关层,把业务复杂度和本地状态放在 ai-server 层。粘性路由是该系统的核心约束:它牺牲了传统负载均衡的自由度,换取了 AgentScope RAG、动态数据源和本地文件系统的一致性。扩展性通过多 ai-server 节点实现,容错通过“DOWN 返回 503”明确边界,避免静默数据错配。
[本节为总结性内容,不直接分析具体文件]
## 附录:部署拓扑
```mermaid
graph TB
LB["外部入口
Nginx / 云负载均衡"] --> GW1["ai-gateway 实例 1"]
LB --> GW2["ai-gateway 实例 2"]
GW1 --> AS1["ai-server 节点 1"]
GW2 --> AS2["ai-server 节点 2"]
GW1 --> PG["PostgreSQL/pgvector"]
GW2 --> PG
GW1 --> RD["Redis"]
GW2 --> RD
AS1 --> PG
AS2 --> PG
AS1 --> FS1["节点 1 本地文件系统"]
AS2 --> FS2["节点 2 本地文件系统"]
```
关键约束:
- 每个 ai-server 节点拥有独立本地文件系统;
- PostgreSQL 与 Redis 是共享资源;
- 多个 ai-gateway 实例共享同一 PostgreSQL 配置表和路由表;
- 用户会话固定到 ai-server 节点,而非负载均衡随机分发。
[本节为概念性部署拓扑,不直接映射单一源码文件]