本文引用的文件
本架构文档描述清鉴 AI 调查工具的分层模型:Electron 客户端 → Preload Bridge → Spring Cloud Gateway(ai-gateway)→ ai-server 业务模块 → PostgreSQL/pgvector/Redis/文件系统。
该系统的核心约束是“粘性路由”:用户会话必须固定到同一 ai-server 节点,因为案件数据源、本地工作区、RAG 索引和 AgentScope 中间状态都位于节点本地。网关在登录后把用户绑定到某个后端节点,并在后续请求中强制复用;若绑定节点不可用,网关返回 503,而不是自动切换。
仓库采用多模块 Maven 工程,根 pom.xml 声明两个子模块:
graph TB
Root["根工程 zsjz-ai<br/>Maven POM"] --> Server["ai-server<br/>Spring Boot 业务服务"]
Root --> Gateway["ai-gateway<br/>Spring Cloud Gateway"]
Server --> PG["PostgreSQL / pgvector"]
Server --> Redis["Redis"]
Server --> FS["本地文件系统"]
Gateway --> Server
图表来源
章节来源
electron.ipcRenderer,避免渲染进程直接访问 Electron 内部对象。章节来源
从端到端视角,一次带登录和分析的请求流程如下:
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-server 节点持有:
因此,一旦用户绑定到某个节点,后续请求必须继续命中该节点。如果节点被停用或健康检查失败,网关拒绝自动切换,而是返回 503,提示管理员修复节点或重新绑定。
章节来源
StickyRouteFilter 是粘性路由的核心全局过滤器,执行顺序为 10001,排在 Spring Cloud Gateway 内置的 RouteToRequestUrlFilter(顺序 10000)之后。它的工作是:
x-token 解析 userId,再查绑定;未绑定时尝试绑定到服务的默认节点;GATEWAY_REQUEST_URL_ATTR,将目标 URL 重写为“节点 base_url + 原始 rawPath + rawQuery”;将 X-Gateway-Node 写入响应头,便于客户端验证会话是否始终落在同一节点。
flowchart TD
Start(["进入 StickyRouteFilter"]) --> CheckRoute["检查是否匹配 ai-server 路由"]
CheckRoute --> IsLogin{"是否登录请求?"}
IsLogin -->|是| DecideLogin["按 username 查绑定<br/>未绑定则轮询候选节点"]
IsLogin -->|否| DecideNormal["按 x-token 解析 userId<br/>查绑定或绑定默认节点"]
DecideLogin --> SelectNode["确定 nodeId"]
DecideNormal --> SelectNode
SelectNode --> ValidateNode{"节点启用且健康?"}
ValidateNode -->|否| Return503["抛出 503 服务不可用"]
ValidateNode -->|是| RewriteURL["重写 GATEWAY_REQUEST_URL_ATTR<br/>base_url + rawPath + rawQuery"]
RewriteURL --> NextChain["交给 NettyRoutingFilter 流式转发"]
图表来源
章节来源
LoginRequestFilter 只在配置的完整登录路径上生效,负责:
username;LOGIN_REQUEST 属性;Accept-Encoding 改为 identity,避免后端 gzip 压缩导致响应无法被 LoginResponseFilter 正确解析;如果请求体为空、超限、非 JSON 或缺少 username,过滤器不会阻断请求,而是按匿名轮询处理,由后端自行校验登录逻辑。
章节来源
LoginResponseFilter 的执行顺序为 NettyWriteResponseFilter.WRITE_RESPONSE_FILTER_ORDER - 1,即比真正写出响应的过滤器更早一层。它在响应写出时:
code=200,data 包含 userId、username);UserRouteService.bindIfAbsent 把用户绑定到 StickyRouteFilter 选定的节点;TokenResolver.cacheToken 把 gateway:token:{serviceId}:{token} 写入 Redis,TTL 24 小时;绑定操作是 fire-and-forget:即使客户端断开连接或数据库写入失败,也不会中断本次登录响应。
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: 原样写出响应
图表来源
章节来源
TokenResolver 提供三类能力:
resolveUserId:先查 Redis 缓存键 gateway:token:{serviceId}:{token},未命中时调用 StpUtil 从 sa-token 共享 Redis 解析,再回填缓存;cacheToken:登录成功后主动写入缓存;resolveUserIdDirect:管理端低频认证直接查 sa-token。解析失败返回空 Mono,由 StickyRouteFilter 降级为匿名请求,让后端返回 401 引导重新登录。
章节来源
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,并发下由数据库决定赢家,随后再次读取生效节点。
章节来源
NodeSelector 只对无绑定请求生效。它会过滤出启用且健康的节点,单节点直接返回,多节点按服务维度原子计数器轮询。注意:已绑定用户的节点即使 DOWN 也不会被换掉。
章节来源
GatewayRegistry 维护服务与节点的内存快照,事实来源是 PostgreSQL。启动时用 YAML 播种,运行中每 10 秒对账,变化时发布 RefreshRoutesEvent 刷新路由缓存。
HealthChecker 定时探测每个启用的节点健康路径,连续失败达到阈值标记 DOWN,一次成功恢复 UP。NodeHealthRegistry 是每网关实例的内存状态,UNKNOWN 乐观视为可用,避免刚启动时全部 503。
章节来源
Electron 主进程通过 main.ts 注册生命周期与 preload,preload 通过 contextBridge.exposeInMainWorld('electron', ...) 仅暴露 ipcRenderer。渲染进程不能直接访问 Electron 原生 API,从而降低 XSS 或恶意前端代码对宿主系统的风险。
graph LR
Renderer["渲染进程<br/>Vue 前端"] --> Bridge["contextBridge<br/>bridge.ts"]
Bridge --> MainProcess["主进程<br/>main.ts + ee-core"]
MainProcess --> Network["网络请求<br/>指向 ai-gateway"]
图表来源
章节来源
Spring Cloud Gateway 基于 WebFlux/Netty,支持流式转发,天然兼容 SSE、multipart 和 Range 下载。配置中明确不设全局响应超时,以容纳长时间运行的 SSE 研判连接。粘性路由保证同一次会话落到同一 ai-server,避免 SSE 中途切到另一个没有对应 AgentScope 状态的节点。
建议运维侧在网关上游再叠加反向代理心跳策略(如 Nginx proxy_read_timeout、云负载均衡空闲超时),但不应把业务长连接误判为空闲而切断。
章节来源
ai-server 内按案件维度管理动态数据源。CaseDataSourceRegistry 负责注册和查询数据源,CaseRoutingDataSource 作为动态路由数据源,把线程或会话上下文的 caseId 映射到具体数据库连接。这样不同案件可以访问不同的 PostgreSQL 库表,同时共享 ai-server 自身的平台库。
classDiagram
class CaseDataSourceRegistry {
+注册数据源()
+获取数据源()
+清理数据源()
}
class CaseRoutingDataSource {
+determineCurrentLookupKey()
}
class DataSourceConfig {
+初始化平台数据源()
}
CaseRoutingDataSource --> CaseDataSourceRegistry : "查询当前案件数据源"
DataSourceConfig --> CaseDataSourceRegistry : "注册平台数据源"
图表来源
章节来源
ai-server 的 agent 模块包含 RAG 相关类,例如 RagSchemaService、EmbeddingProvider、WorkspaceSchemaCatalog、AttachmentContentRouter 等,负责 schema 管理、向量嵌入、工作区目录扫描、附件内容路由等。Agent 聊天实现 AgentChatServiceImpl 承载会话、意图、工具调用和结果聚合。
这些能力依赖 PostgreSQL/pgvector 做向量检索,依赖本地文件系统存取工作区、技能和附件,因此必须保持用户会话固定在同一个 ai-server 节点。
章节来源
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
图表来源
gateway_node 表中登记,或通过管理端更新后触发 RefreshRoutesEvent;GatewayProperties.HealthCheck 调整。[本节为通用性能讨论,不直接分析具体代码行]
| 现象 | 可能原因 | 排查要点 |
|---|---|---|
| 登录后多次请求跳到不同 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 调查工具通过“Electron 客户端 + Preload 安全桥 + Spring Cloud Gateway + ai-server + PostgreSQL/pgvector/Redis/文件系统”的分层架构,把安全性、可观测性和稳定性放在前端与网关层,把业务复杂度和本地状态放在 ai-server 层。粘性路由是该系统的核心约束:它牺牲了传统负载均衡的自由度,换取了 AgentScope RAG、动态数据源和本地文件系统的一致性。扩展性通过多 ai-server 节点实现,容错通过“DOWN 返回 503”明确边界,避免静默数据错配。
[本节为总结性内容,不直接分析具体文件]
graph TB
LB["外部入口<br/>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 本地文件系统"]
关键约束:
[本节为概念性部署拓扑,不直接映射单一源码文件]