# 系统架构设计 **本文引用的文件** - [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 节点,而非负载均衡随机分发。 [本节为概念性部署拓扑,不直接映射单一源码文件]