From acc08a47aaf3da1b18c8bd1338e7c01b6c41af6f Mon Sep 17 00:00:00 2001 From: NEDONION Date: Wed, 12 Aug 2026 01:11:32 +0800 Subject: [PATCH 1/5] =?UTF-8?q?docs:=20=E6=96=B0=E5=A2=9E=20LLM=20?= =?UTF-8?q?=E6=A8=A1=E5=9D=97=E6=96=87=E6=A1=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 覆盖服务商与模型管理、协议适配(OpenAI/Anthropic)、客户端构造、高可用择优与 降级链、亲和性、调用结果上报、Token 溢出三策略、领域事件。 内容贴着实际代码写:枚举取自源码、方法签名与行号可对照、超时配置与线上事故关联。 按 lobster0 的文档风格,开头加「一句话人话」导读。 补上 INDEX.md 中 7 个死链之一。 Co-Authored-By: Claude Opus 5 --- docs/LLM_MODULE.md | 362 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 362 insertions(+) create mode 100644 docs/LLM_MODULE.md diff --git a/docs/LLM_MODULE.md b/docs/LLM_MODULE.md new file mode 100644 index 0000000..5dffa29 --- /dev/null +++ b/docs/LLM_MODULE.md @@ -0,0 +1,362 @@ +# LLM 模块 + +> 💬 **一句话人话**:这个模块负责「去哪家、用哪个模型、怎么把请求发出去」。 +> 你在界面上配好服务商(比如硅基流动)和模型,它就把这些配置变成真正能调用的客户端; +> 一家挂了会自动换下一家;聊天记录太长超出模型上限时,它负责裁剪或压缩。 + +**涉及代码**:`domain/llm`、`domain/token`、`domain/highavailability`、 +`infrastructure/llm`、`infrastructure/highavailability`、`application/llm`,共约 61 个类。 + +--- + +## 目录 + +- [1. 模块职责](#1-模块职责) +- [2. 核心概念](#2-核心概念) +- [3. 分层结构](#3-分层结构) +- [4. 服务商与模型](#4-服务商与模型) +- [5. 协议适配](#5-协议适配) +- [6. 高可用与降级链](#6-高可用与降级链) +- [7. Token 溢出处理](#7-token-溢出处理) +- [8. 领域事件](#8-领域事件) +- [9. 关键配置](#9-关键配置) +- [10. 已知坑与注意事项](#10-已知坑与注意事项) + +--- + +## 1. 模块职责 + +| 职责 | 说明 | +|---|---| +| **服务商管理** | 用户自建或使用官方预置的模型服务商,保存 API Key、Base URL、协议类型 | +| **模型管理** | 一个服务商下挂多个模型,区分对话 / 嵌入 / 视觉三类用途 | +| **客户端构造** | 把服务商 + 模型的配置组装成 LangChain4j 的 `ChatModel` / `StreamingChatModel` | +| **高可用调度** | 通过外部网关在多个可用实例中择优,支持降级链与亲和性 | +| **调用结果上报** | 上报每次调用的成败与延迟,供网关做后续决策 | +| **Token 溢出处理** | 对话历史超出模型上下文上限时,按策略裁剪或摘要 | + +**不负责**:具体的对话编排(见 [对话模块](./CONVERSATION_MODULE.md))、 +向量化与检索(见 [RAG 模块](./RAG_MODULE.md))。 + +--- + +## 2. 核心概念 + +### Provider(服务商) + +一个模型服务的提供方,例如硅基流动、OpenAI、Anthropic,或任何兼容 OpenAI 协议的自建服务。 + +```java +// domain/llm/model/enums/ProviderType.java +public enum ProviderType { + ALL("all"), // 查询用,表示不限 + OFFICIAL("official"), // 平台预置,所有用户可见 + CUSTOM("custom"); // 用户自建,仅自己可见 +} +``` + +**关键字段**(`ProviderEntity`): + +| 字段 | 说明 | +|---|---| +| `protocol` | 决定用哪套 SDK 说话,见[协议适配](#5-协议适配) | +| `config.apiKey` | 密钥,**对外返回时会脱敏** | +| `config.baseUrl` | 服务地址,兼容 OpenAI 协议的自建服务靠它接入 | +| `userId` | 归属用户,`OFFICIAL` 类型除外 | + +> ⚠️ **密钥脱敏**在 `ProviderAssembler` 转 DTO 时调用 `dto.maskSensitiveInfo()` 完成。 +> 新增任何返回 Provider 的接口时,务必走 Assembler,不要直接把 Entity 序列化出去。 + +### Model(模型) + +挂在 Provider 下的具体模型。 + +```java +// domain/llm/model/enums/ModelType.java +public enum ModelType { + CHAT("CHAT", "对话模型"), + EMBEDDING("EMBEDDING", "嵌入模型"), + VISION("VISION", "视觉模型"); +} +``` + +**`modelEndpoint`** 是发给服务商的真实模型名(如 `Qwen/Qwen3-8B`), +与平台内展示的名称解耦 —— 换模型不必改前端。 + +--- + +## 3. 分层结构 + +``` +interfaces/api/portal/llm/PortalLLMController + │ 对外 REST 接口 + ↓ +application/llm/service/LLMAppService + │ 用例编排 + Assembler 转 DTO(含密钥脱敏) + ↓ +domain/llm/service/LLMDomainService 领域规则、校验、事件发布 +domain/highavailability/service/… 实例择优、结果上报 +domain/token/service/… Token 溢出策略 + ↓ +infrastructure/llm/LLMServiceFactory 组装 LangChain4j 客户端 +infrastructure/highavailability/… 调用外部高可用网关 +``` + +--- + +## 4. 服务商与模型 + +### 客户端是怎么造出来的 + +`LLMServiceFactory` 是唯一的入口,把领域对象翻译成基础设施配置: + +```java +// infrastructure/llm/LLMServiceFactory.java +public StreamingChatModel getStreamingClient(ProviderEntity provider, ModelEntity model) { + ProviderConfig config = provider.getConfig(); + ProviderConfig providerConfig = new ProviderConfig( + config.getApiKey(), + config.getBaseUrl(), + model.getModelEndpoint(), // ← 真实模型名在这里注入 + provider.getProtocol()); + return LLMProviderService.getStream(provider.getProtocol(), providerConfig); +} +``` + +两个方法的区别: + +| 方法 | 返回 | 用途 | +|---|---|---| +| `getStreamingClient` | `StreamingChatModel` | 面向用户的对话,逐字返回 | +| `getStrandClient` | `ChatModel` | 内部同步调用:意图识别、语义改写、查询扩展等 | + +> 命名里的 `Strand` 是笔误(应为 `Standard`),已在代码中沿用,改名需同步全部调用点。 + +--- + +## 5. 协议适配 + +```java +// infrastructure/llm/protocol/enums/ProviderProtocol.java +public enum ProviderProtocol { + OPENAI, ANTHROPIC; +} +``` + +`LLMProviderFactory` 按协议分发到不同的 LangChain4j 实现: + +| 协议 | 非流式 | 流式 | +|---|---|---| +| `OPENAI` | `OpenAiChatModel` | `OpenAiStreamingChatModel` | +| `ANTHROPIC` | `AnthropicChatModel` | `AnthropicStreamingChatModel` | + +**绝大多数国内服务商(硅基流动、DeepSeek、通义等)都兼容 OpenAI 协议**, +选 `OPENAI` 并把 `baseUrl` 指过去即可,无需新增协议。 + +### 超时配置 + +```java +// infrastructure/llm/factory/LLMProviderFactory.java +private static final Duration BLOCKING_TIMEOUT = timeoutFromEnv("LLM_REQUEST_TIMEOUT_SECONDS", 60); +private static final Duration STREAMING_TIMEOUT = timeoutFromEnv("LLM_STREAM_TIMEOUT_SECONDS", 300); +``` + +| 变量 | 默认 | 作用范围 | +|---|---|---| +| `LLM_REQUEST_TIMEOUT_SECONDS` | 60s | 非流式。这类调用夹在 SSE 事件之间同步执行,超时必须短 | +| `LLM_STREAM_TIMEOUT_SECONDS` | 300s | 流式对话 | + +> 📌 这两个值曾经都是 `Duration.ofHours(1)`,等同于没有超时,导致线上「对话卡死」。 +> 完整过程见 [排查记录 1.2](./TROUBLESHOOTING_LOG.md#12-根因一llm-客户端超时是-1-小时)。 + +--- + +## 6. 高可用与降级链 + +### 设计意图 + +单个服务商会限流、会抖动、会挂。高可用模块把「选哪个实例」这件事外置给一个网关服务, +由它根据历史成功率、延迟和亲和性做决策。 + +### 调用流程 + +``` +业务侧(如 RAG 问答、Agent 对话) + │ + │ selectBestProvider(model, userId, sessionId, fallbackChain) + ↓ +HighAvailabilityDomainServiceImpl + │ 构造 SelectInstanceRequest(含降级链) + ↓ +HighAvailabilityGateway ──HTTP──> 外部高可用网关 + │ + │ 返回 ApiInstanceDTO(businessId → 定位 Provider/Model,id → instanceId) + ↓ +HighAvailabilityResult { provider, model, instanceId } + │ + │ 业务用 provider + model 造客户端发起调用 + ↓ +reportCallResult(instanceId, modelId, success, latencyMs, ...) + └─ 把成败与延迟回传网关,影响后续择优 +``` + +### 降级链 + +```java +// domain/highavailability/service/HighAvailabilityDomainServiceImpl.java:138 +if (fallbackChain != null && !fallbackChain.isEmpty()) { + request.setFallbackChain(fallbackChain); + logger.debug("启用降级链: userId={}, primaryModel={}, fallbackModels={}", + userId, model.getModelId(), fallbackChain); +} +``` + +降级链来自用户设置(`UserSettingsDomainService.getUserFallbackChain`): +主模型不可用时,网关按链上顺序尝试备选模型。 + +### 亲和性 + +```java +// infrastructure/highavailability/constant/AffinityType.java +SESSION // 同一会话尽量固定同一实例,保证上下文连续 +USER // 同一用户固定实例 +BATCH // 同一批任务固定实例 +REGION // 按地域就近 +``` + +对话场景用 `SESSION`,避免同一轮对话在不同实例间跳。 + +### 双重降级 + +`selectBestProvider` 内部有两层兜底:网关调用失败时走本地降级逻辑; +本地降级也失败才抛 `BusinessException("获取Provider失败")`。 + +```java +} catch (Exception fallbackException) { + logger.error("降级逻辑也失败了: modelId={}, sessionId={}", model.getId(), sessionId, fallbackException); + throw new BusinessException("获取Provider失败", fallbackException); +} +``` + +### 模型变更同步 + +模型的增删改都要同步到网关,通过领域事件驱动: + +| 方法 | 触发时机 | +|---|---| +| `syncModelToGateway` | 新建模型 | +| `updateModelInGateway` | 修改模型 | +| `removeModelFromGateway` | 删除模型 | +| `changeModelStatusInGateway` | 启用 / 停用 | +| `batchRemoveModelsFromGateway` | 批量删除 | +| `syncAllModelsToGateway` | 全量重同步,用于修复不一致 | +| `initializeProject` | 首次启动时在网关侧建项目 | + +--- + +## 7. Token 溢出处理 + +对话历史会不断增长,超出模型上下文上限就会报错。这个子模块负责在发请求前把历史裁到安全范围。 + +### 策略接口 + +```java +// domain/token/service/TokenOverflowStrategy.java +public interface TokenOverflowStrategy { + TokenProcessResult process(List messages, TokenOverflowConfig config); + String getName(); + boolean needsProcessing(List messages); +} +``` + +### 三种策略 + +| 策略 | 实现类 | 行为 | 适用 | +|---|---|---|---| +| `NONE` | `NoTokenOverflowStrategy` | 不处理,原样返回 | 短对话,或自行保证不超限 | +| `SLIDING_WINDOW` | `SlidingWindowTokenOverflowStrategy` | 丢弃最旧的消息,保留最近 N 条 | 默认选择,实现简单、无额外开销 | +| `SUMMARIZE` | `SummarizeTokenOverflowStrategy` | 把旧消息交给模型压缩成摘要 | 长对话需要记住早期信息 | + +由 `TokenOverflowStrategyFactory` 按配置分发: + +```java +switch (strategyType) { + case SLIDING_WINDOW : ... + case SUMMARIZE : ... + case NONE : ... +} +``` + +> ⚠️ `SUMMARIZE` 会**额外发起一次 LLM 调用**,因此受 +> `LLM_REQUEST_TIMEOUT_SECONDS` 约束,且会增加首字延迟。 +> 长对话场景开启前建议实测。 + +--- + +## 8. 领域事件 + +模型的生命周期通过事件对外广播,主要消费方是高可用网关同步: + +``` +domain/llm/event/ +├── ModelDomainEvent 抽象基类 +├── ModelCreatedEvent +├── ModelUpdatedEvent +├── ModelDeletedEvent +├── ModelsBatchDeletedEvent 携带 ModelDeleteItem 列表 +└── ModelStatusChangedEvent 启用 / 停用 +``` + +用事件解耦的好处:模型 CRUD 的主流程不必关心网关是否可达, +网关同步失败不会阻断用户操作。 + +--- + +## 9. 关键配置 + +### 环境变量 + +| 变量 | 默认值 | 说明 | +|---|---|---| +| `LLM_REQUEST_TIMEOUT_SECONDS` | 60 | 非流式调用超时 | +| `LLM_STREAM_TIMEOUT_SECONDS` | 300 | 流式调用超时 | +| `SILICONFLOW_API_KEY` | — | Rerank 用,见 RAG 模块 | + +### 相关表 + +模型与服务商的表结构见 [数据库设计](./DATABASE.md)。 + +--- + +## 10. 已知坑与注意事项 + +### 密钥必须走 Assembler 脱敏 + +直接返回 `ProviderEntity` 会把 `apiKey` 明文吐给前端。 +所有对外接口必须经 `ProviderAssembler`,它会调 `maskSensitiveInfo()`。 + +### 超时值不要改回大数 + +把超时设成 1 小时等于关闭超时。上游挂起时故障会被无限期隐藏, +表现为「页面一直转圈且不报错」,比直接报错难排查得多。 + +### 非流式调用会阻塞 SSE 事件流 + +`getStrandClient` 拿到的是同步客户端。如果在两条 SSE 事件之间连续调用多次, +用户会看到界面卡在上一条提示上。**每个耗时步骤前都应先发进度事件**, +参见 [排查记录 1.3](./TROUBLESHOOTING_LOG.md#13-根因二进度提示与实际执行不同步)。 + +### 上报不能省 + +`reportCallResult` 是网关择优的唯一数据来源。漏报会让网关一直基于陈旧数据决策, +高可用形同虚设。新增调用路径时记得补上报。 + +--- + +## 相关文档 + +- [系统架构](./ARCHITECTURE.md) +- [对话模块](./CONVERSATION_MODULE.md) —— 谁在使用这里的客户端 +- [RAG 模块](./RAG_MODULE.md) —— 嵌入模型的使用方 +- [性能优化](./PERFORMANCE.md) —— 超时与并发 +- [排查记录](./TROUBLESHOOTING_LOG.md) —— 超时问题的完整过程 From 6625851157db8da9c70e0bbda0a92f33976a4cfe Mon Sep 17 00:00:00 2001 From: NEDONION Date: Wed, 12 Aug 2026 01:35:35 +0800 Subject: [PATCH 2/5] =?UTF-8?q?docs:=20=E6=8C=89=E4=B8=BB=E9=A2=98?= =?UTF-8?q?=E5=88=86=E5=B1=82=E9=87=8D=E7=BB=84=20docs=20=E7=9B=AE?= =?UTF-8?q?=E5=BD=95=EF=BC=8C=E6=96=B0=E5=A2=9E=20LLM=20=E6=A8=A1=E5=9D=97?= =?UTF-8?q?=E6=96=87=E6=A1=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 原来 docs/ 下全平铺,随着文档增多难以定位。改为按主题分目录: docs/architecture/ 系统架构 docs/modules/ 各业务模块 docs/reference/ API 与数据库参考 docs/operations/ 部署、性能、安全、排查记录 docs/development/ 本地开发 文件名同时去掉 MODULE 后缀等冗余(RAG_MODULE.md → modules/rag.md), 目录层级本身已经表达了这层含义。INDEX.md 改名 README.md,符合目录入口惯例。 移动一律用 git mv 保留历史。 新增 docs/modules/llm.md(362 行):服务商与模型管理、OpenAI/Anthropic 协议适配、 高可用选路与降级链、Token 溢出的三种策略、领域事件、以及本次排查沉淀的超时坑。 同步更新 README.md、README_EN.md 及文档间的全部交叉引用;新增链接校验脚本, 当前 35 条有效链接全部可达,剩余 12 条失效均指向尚未撰写的文档。 Co-Authored-By: Claude Opus 5 --- README.md | 10 ++--- README_EN.md | 10 ++--- docs/{INDEX.md => README.md} | 44 +++++++++---------- .../overview.md} | 0 docs/{images => assets}/README.md | 0 docs/{AGENT_MODULE.md => modules/agent.md} | 0 docs/{LLM_MODULE.md => modules/llm.md} | 20 ++++----- docs/{RAG_MODULE.md => modules/rag.md} | 8 ++-- .../troubleshooting-log.md} | 2 +- docs/{API.md => reference/api.md} | 0 docs/{DATABASE.md => reference/database.md} | 0 11 files changed, 47 insertions(+), 47 deletions(-) rename docs/{INDEX.md => README.md} (73%) rename docs/{ARCHITECTURE.md => architecture/overview.md} (100%) rename docs/{images => assets}/README.md (100%) rename docs/{AGENT_MODULE.md => modules/agent.md} (100%) rename docs/{LLM_MODULE.md => modules/llm.md} (93%) rename docs/{RAG_MODULE.md => modules/rag.md} (99%) rename docs/{TROUBLESHOOTING_LOG.md => operations/troubleshooting-log.md} (99%) rename docs/{API.md => reference/api.md} (100%) rename docs/{DATABASE.md => reference/database.md} (100%) diff --git a/README.md b/README.md index 7a46b4e..707d923 100644 --- a/README.md +++ b/README.md @@ -177,11 +177,11 @@ Agent 结合已配置的模型服务、知识检索与 MCP 工具,当前对话 | 文档 | 内容 | | --- | --- | -| [文档索引](docs/INDEX.md) | 所有技术文档的入口 | -| [系统架构](docs/ARCHITECTURE.md) | 技术栈、分层与数据流 | -| [RAG 模块](docs/RAG_MODULE.md) | 文档处理、向量检索与 RAG 链路 | -| [Agent 模块](docs/AGENT_MODULE.md) | Agent 生命周期与工具集成 | -| [API 参考](docs/API.md) | REST、WebSocket 与 SSE 接口 | +| [文档索引](docs/README.md) | 所有技术文档的入口 | +| [系统架构](docs/architecture/overview.md) | 技术栈、分层与数据流 | +| [RAG 模块](docs/modules/rag.md) | 文档处理、向量检索与 RAG 链路 | +| [Agent 模块](docs/modules/agent.md) | Agent 生命周期与工具集成 | +| [API 参考](docs/reference/api.md) | REST、WebSocket 与 SSE 接口 | | [数据库初始化](docs/sql/01_init.sql) | PostgreSQL 与 PGVector 初始化脚本 | ## 参与贡献 diff --git a/README_EN.md b/README_EN.md index 345f6fe..c9b01cd 100644 --- a/README_EN.md +++ b/README_EN.md @@ -169,11 +169,11 @@ The included Docker Compose configuration is an author deployment reference, not | Document | Contents | | --- | --- | -| [Documentation index](docs/INDEX.md) | Entry point for the technical documentation | -| [System architecture](docs/ARCHITECTURE.md) | Technology stack, layers, and data flow | -| [RAG module](docs/RAG_MODULE.md) | Document processing, vector retrieval, and the RAG path | -| [Agent module](docs/AGENT_MODULE.md) | Agent lifecycle and tool integration | -| [API reference](docs/API.md) | REST, WebSocket, and SSE interfaces | +| [Documentation index](docs/README.md) | Entry point for the technical documentation | +| [System architecture](docs/architecture/overview.md) | Technology stack, layers, and data flow | +| [RAG module](docs/modules/rag.md) | Document processing, vector retrieval, and the RAG path | +| [Agent module](docs/modules/agent.md) | Agent lifecycle and tool integration | +| [API reference](docs/reference/api.md) | REST, WebSocket, and SSE interfaces | | [Database initialization](docs/sql/01_init.sql) | PostgreSQL and PGVector initialization script | ## Contributing diff --git a/docs/INDEX.md b/docs/README.md similarity index 73% rename from docs/INDEX.md rename to docs/README.md index 69dda27..1869a1e 100644 --- a/docs/INDEX.md +++ b/docs/README.md @@ -10,23 +10,23 @@ | 文档 | 描述 | 链接 | |-----|------|------| -| **系统架构** | 整体架构设计、技术栈、DDD分层 | [ARCHITECTURE.md](./ARCHITECTURE.md) | -| **RAG 模块** | 文档处理、向量检索、版本化机制 | [RAG_MODULE.md](./RAG_MODULE.md) | -| **Agent 模块** | Agent 生命周期、工具集成、执行追踪 | [AGENT_MODULE.md](./AGENT_MODULE.md) | -| **数据库设计** | 表结构设计、ER 图、索引优化 | [DATABASE.md](./DATABASE.md) | -| **API 接口** | RESTful API、WebSocket、SSE | [API.md](./API.md) | -| **部署指南** | 环境搭建、Docker 部署、生产配置 | [DEPLOYMENT.md](./DEPLOYMENT.md) | -| **开发指南** | 本地开发、代码规范、测试 | [DEVELOPMENT.md](./DEVELOPMENT.md) | +| **系统架构** | 整体架构设计、技术栈、DDD分层 | [ARCHITECTURE.md](architecture/overview.md) | +| **RAG 模块** | 文档处理、向量检索、版本化机制 | [RAG_MODULE.md](modules/rag.md) | +| **Agent 模块** | Agent 生命周期、工具集成、执行追踪 | [AGENT_MODULE.md](modules/agent.md) | +| **数据库设计** | 表结构设计、ER 图、索引优化 | [DATABASE.md](reference/database.md) | +| **API 接口** | RESTful API、WebSocket、SSE | [API.md](reference/api.md) | +| **部署指南** | 环境搭建、Docker 部署、生产配置 | [DEPLOYMENT.md](operations/deployment.md) | +| **开发指南** | 本地开发、代码规范、测试 | [DEVELOPMENT.md](development/local-setup.md) | ### 专题文档 | 文档 | 描述 | 链接 | |-----|------|------| -| **LLM 模块** | 模型管理、高可用、Token 统计 | [LLM_MODULE.md](./LLM_MODULE.md) | -| **MCP 工具模块** | 工具导入、容器化、调用机制 | [MCP_MODULE.md](./MCP_MODULE.md) | -| **对话管理** | 会话管理、上下文窗口、Token 管控 | [CONVERSATION_MODULE.md](./CONVERSATION_MODULE.md) | -| **性能优化** | 数据库优化、缓存策略、并发处理 | [PERFORMANCE.md](./PERFORMANCE.md) | -| **安全最佳实践** | 认证授权、数据加密、多租户隔离 | [SECURITY.md](./SECURITY.md) | +| **LLM 模块** | 模型管理、高可用、Token 统计 | [LLM_MODULE.md](modules/llm.md) | +| **MCP 工具模块** | 工具导入、容器化、调用机制 | [MCP_MODULE.md](modules/mcp-tool.md) | +| **对话管理** | 会话管理、上下文窗口、Token 管控 | [CONVERSATION_MODULE.md](modules/conversation.md) | +| **性能优化** | 数据库优化、缓存策略、并发处理 | [PERFORMANCE.md](operations/performance.md) | +| **安全最佳实践** | 认证授权、数据加密、多租户隔离 | [SECURITY.md](operations/security.md) | --- @@ -34,36 +34,36 @@ ### 我是新手,想快速了解系统 -1. **阅读系统架构** → [ARCHITECTURE.md](./ARCHITECTURE.md) +1. **阅读系统架构** → [ARCHITECTURE.md](architecture/overview.md) - 了解整体架构设计 - 熟悉技术栈和分层结构 -2. **查看部署指南** → [DEPLOYMENT.md](./DEPLOYMENT.md) +2. **查看部署指南** → [DEPLOYMENT.md](operations/deployment.md) - 快速搭建本地开发环境 - 理解系统依赖和配置 -3. **浏览 API 文档** → [API.md](./API.md) +3. **浏览 API 文档** → [API.md](reference/api.md) - 了解核心 API 接口 - 尝试调用示例 ### 我想深入了解某个模块 -- **RAG 检索系统**: [RAG_MODULE.md](./RAG_MODULE.md) -- **Agent 智能体**: [AGENT_MODULE.md](./AGENT_MODULE.md) -- **LLM 模型管理**: [LLM_MODULE.md](./LLM_MODULE.md) -- **MCP 工具集成**: [MCP_MODULE.md](./MCP_MODULE.md) +- **RAG 检索系统**: [RAG_MODULE.md](modules/rag.md) +- **Agent 智能体**: [AGENT_MODULE.md](modules/agent.md) +- **LLM 模型管理**: [LLM_MODULE.md](modules/llm.md) +- **MCP 工具集成**: [MCP_MODULE.md](modules/mcp-tool.md) ### 我想进行二次开发 -1. **开发指南** → [DEVELOPMENT.md](./DEVELOPMENT.md) +1. **开发指南** → [DEVELOPMENT.md](development/local-setup.md) - 本地开发环境搭建 - 代码规范和提交规范 -2. **数据库设计** → [DATABASE.md](./DATABASE.md) +2. **数据库设计** → [DATABASE.md](reference/database.md) - 理解表结构设计 - 学习数据模型 -3. **API 接口** → [API.md](./API.md) +3. **API 接口** → [API.md](reference/api.md) - 了解接口设计规范 - 扩展新的 API diff --git a/docs/ARCHITECTURE.md b/docs/architecture/overview.md similarity index 100% rename from docs/ARCHITECTURE.md rename to docs/architecture/overview.md diff --git a/docs/images/README.md b/docs/assets/README.md similarity index 100% rename from docs/images/README.md rename to docs/assets/README.md diff --git a/docs/AGENT_MODULE.md b/docs/modules/agent.md similarity index 100% rename from docs/AGENT_MODULE.md rename to docs/modules/agent.md diff --git a/docs/LLM_MODULE.md b/docs/modules/llm.md similarity index 93% rename from docs/LLM_MODULE.md rename to docs/modules/llm.md index 5dffa29..fcba2ca 100644 --- a/docs/LLM_MODULE.md +++ b/docs/modules/llm.md @@ -35,8 +35,8 @@ | **调用结果上报** | 上报每次调用的成败与延迟,供网关做后续决策 | | **Token 溢出处理** | 对话历史超出模型上下文上限时,按策略裁剪或摘要 | -**不负责**:具体的对话编排(见 [对话模块](./CONVERSATION_MODULE.md))、 -向量化与检索(见 [RAG 模块](./RAG_MODULE.md))。 +**不负责**:具体的对话编排(见 [对话模块](conversation.md))、 +向量化与检索(见 [RAG 模块](rag.md))。 --- @@ -167,7 +167,7 @@ private static final Duration STREAMING_TIMEOUT = timeoutFromEnv("LLM_STREAM_TIM | `LLM_STREAM_TIMEOUT_SECONDS` | 300s | 流式对话 | > 📌 这两个值曾经都是 `Duration.ofHours(1)`,等同于没有超时,导致线上「对话卡死」。 -> 完整过程见 [排查记录 1.2](./TROUBLESHOOTING_LOG.md#12-根因一llm-客户端超时是-1-小时)。 +> 完整过程见 [排查记录 1.2](../operations/troubleshooting-log.md#12-根因一llm-客户端超时是-1-小时)。 --- @@ -324,7 +324,7 @@ domain/llm/event/ ### 相关表 -模型与服务商的表结构见 [数据库设计](./DATABASE.md)。 +模型与服务商的表结构见 [数据库设计](../reference/database.md)。 --- @@ -344,7 +344,7 @@ domain/llm/event/ `getStrandClient` 拿到的是同步客户端。如果在两条 SSE 事件之间连续调用多次, 用户会看到界面卡在上一条提示上。**每个耗时步骤前都应先发进度事件**, -参见 [排查记录 1.3](./TROUBLESHOOTING_LOG.md#13-根因二进度提示与实际执行不同步)。 +参见 [排查记录 1.3](../operations/troubleshooting-log.md#13-根因二进度提示与实际执行不同步)。 ### 上报不能省 @@ -355,8 +355,8 @@ domain/llm/event/ ## 相关文档 -- [系统架构](./ARCHITECTURE.md) -- [对话模块](./CONVERSATION_MODULE.md) —— 谁在使用这里的客户端 -- [RAG 模块](./RAG_MODULE.md) —— 嵌入模型的使用方 -- [性能优化](./PERFORMANCE.md) —— 超时与并发 -- [排查记录](./TROUBLESHOOTING_LOG.md) —— 超时问题的完整过程 +- [系统架构](../architecture/overview.md) +- [对话模块](conversation.md) —— 谁在使用这里的客户端 +- [RAG 模块](rag.md) —— 嵌入模型的使用方 +- [性能优化](../operations/performance.md) —— 超时与并发 +- [排查记录](../operations/troubleshooting-log.md) —— 超时问题的完整过程 diff --git a/docs/RAG_MODULE.md b/docs/modules/rag.md similarity index 99% rename from docs/RAG_MODULE.md rename to docs/modules/rag.md index 63fa344..e831370 100644 --- a/docs/RAG_MODULE.md +++ b/docs/modules/rag.md @@ -1183,7 +1183,7 @@ RAG模块通过**异步处理流水线**、**混合检索引擎**、**双轨版 **相关文档**: -- [系统架构设计](./ARCHITECTURE.md) -- [Agent模块详解](./AGENT_MODULE.md) -- [数据库设计](./DATABASE.md) -- [API接口文档](./API.md) +- [系统架构设计](../architecture/overview.md) +- [Agent模块详解](agent.md) +- [数据库设计](../reference/database.md) +- [API接口文档](../reference/api.md) diff --git a/docs/TROUBLESHOOTING_LOG.md b/docs/operations/troubleshooting-log.md similarity index 99% rename from docs/TROUBLESHOOTING_LOG.md rename to docs/operations/troubleshooting-log.md index fcb43d3..b802c05 100644 --- a/docs/TROUBLESHOOTING_LOG.md +++ b/docs/operations/troubleshooting-log.md @@ -769,4 +769,4 @@ Ubuntu 24.04 的 apt 源改成了 DEB822 格式,路径从 `/etc/apt/sources.li - [PR #1 修复 LLM 对话卡死与并发串行化](https://github.com/NEDONION/rag-agent-platform/pull/1) - [PR #2 接入 GitHub Actions 自动构建与部署](https://github.com/NEDONION/rag-agent-platform/pull/2) -- [部署指南 DEPLOY.md](../DEPLOY.md) +- [部署指南 DEPLOY.md](../../DEPLOY.md) diff --git a/docs/API.md b/docs/reference/api.md similarity index 100% rename from docs/API.md rename to docs/reference/api.md diff --git a/docs/DATABASE.md b/docs/reference/database.md similarity index 100% rename from docs/DATABASE.md rename to docs/reference/database.md From ec057f2d67688b1d374b1c94170b19c3516b439f Mon Sep 17 00:00:00 2001 From: NEDONION Date: Wed, 12 Aug 2026 01:41:23 +0800 Subject: [PATCH 3/5] =?UTF-8?q?docs:=20=E6=96=B0=E5=A2=9E=20MCP=20?= =?UTF-8?q?=E5=B7=A5=E5=85=B7=E3=80=81=E5=AF=B9=E8=AF=9D=E3=80=81=E7=94=A8?= =?UTF-8?q?=E6=88=B7=E8=AE=A4=E8=AF=81=E4=B8=89=E4=B8=AA=E6=A8=A1=E5=9D=97?= =?UTF-8?q?=E6=96=87=E6=A1=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 均基于源码实际结构撰写,不含推测: - modules/mcp-tool.md:工具上架状态机(六个处理器的流转与人工审核关卡的实现方式)、 审核容器与用户容器的隔离动机、MCP Gateway 的双重载设计、工具定义到 LangChain4j ToolSpecification 的转换、工具市场的三实体版本模型。 - modules/conversation.md:AbstractMessageHandler 模板方法体系与三个子类、 MessageTransport 让流式与同步共用一套逻辑、一次对话的完整调用链、 上下文裁剪与 Token 溢出策略的衔接、Agent 工作流状态机(含两个人在回路状态)。 - modules/user-auth.md:JWT 与 API Key 两条独立鉴权链路、登录方式开关、 验证码机制、用户默认模型与降级链、账户额度。 各文档均记录了实际存在的坑,例如: - ToolStatus.fromCode() 返回 null 而同项目其他枚举抛异常 - conversation 模块存在三个同名 Agent、两个同名 ChatContext,易 import 错 - UserContext 基于 ThreadLocal,异步线程内取值必为 null(含正误写法对照) - 验证码仅有内存实现,构成水平扩容的硬阻塞点 Co-Authored-By: Claude Opus 5 --- docs/modules/conversation.md | 313 ++++++++++++++++++++++++++++++++++ docs/modules/mcp-tool.md | 320 +++++++++++++++++++++++++++++++++++ docs/modules/user-auth.md | 238 ++++++++++++++++++++++++++ 3 files changed, 871 insertions(+) create mode 100644 docs/modules/conversation.md create mode 100644 docs/modules/mcp-tool.md create mode 100644 docs/modules/user-auth.md diff --git a/docs/modules/conversation.md b/docs/modules/conversation.md new file mode 100644 index 0000000..f5c4539 --- /dev/null +++ b/docs/modules/conversation.md @@ -0,0 +1,313 @@ +# 对话模块 + +> 💬 **一句话人话**:用户在页面上打的每一句话,从进后端到吐字返回,走的都是这个模块。 +> 它管三件事:把历史消息组织好喂给模型(不能超长)、把模型的回答一个字一个字推回浏览器、 +> 以及在需要时把一个复杂问题拆成几步分别执行再汇总。 + +本模块共 61 个类,是平台**请求量最大、链路最长**的模块。 + +--- + +## 目录 + +- [1. 模块职责](#1-模块职责) +- [2. 核心概念](#2-核心概念) +- [3. 分层结构](#3-分层结构) +- [4. 消息处理器体系](#4-消息处理器体系) +- [5. 一次对话的完整链路](#5-一次对话的完整链路) +- [6. 上下文与 Token 溢出](#6-上下文与-token-溢出) +- [7. Agent 工作流](#7-agent-工作流) +- [8. 消息类型与前端协议](#8-消息类型与前端协议) +- [9. 已知坑与注意事项](#9-已知坑与注意事项) + +--- + +## 1. 模块职责 + +| 职责 | 说明 | +| --- | --- | +| **会话管理** | 创建/查询会话,维护会话与 Agent 的绑定关系 | +| **消息持久化** | 用户消息与模型回复落库,供历史回溯 | +| **上下文组装** | 从历史消息构造送入模型的上下文,超长时按策略裁剪 | +| **流式传输** | 通过 SSE 把模型输出增量推给前端 | +| **工具编排** | 把 RAG 检索与 MCP 工具统一包装成 function calling 提供给模型 | +| **工作流执行** | 复杂任务的拆分 → 执行 → 汇总 | + +--- + +## 2. 核心概念 + +### 三个实体 + +``` +SessionEntity 会话:一次持续的对话,绑定一个 Agent +MessageEntity 消息:会话中的一条,含角色、内容、Token 数 +ContextEntity 上下文:会话当前的有效消息窗口 +``` + +> **为什么 Context 要单独建实体**:消息是**只增不改**的历史记录, +> 而上下文是**会被裁剪**的工作集。两者分开后,裁剪上下文不会破坏历史。 + +### MessageType — 15 种消息类型 + +```java +TEXT // 普通文本 +TOOL_CALL // 工具调用 +TASK_EXEC // 任务执行 +TASK_STATUS_TO_LOADING // 任务转为进行中 +TASK_STATUS_TO_FINISH // 任务转为完成 +TASK_SPLIT_FINISH // 任务拆分结束 + +RAG_RETRIEVAL_START / _PROGRESS / _END // RAG 检索三阶段 +RAG_THINKING_START / _PROGRESS / _END // RAG 思考三阶段 +RAG_ANSWER_START / _PROGRESS / _END // RAG 回答三阶段 +``` + +RAG 相关的九个类型是 **START / PROGRESS / END 三段式**,前端据此渲染阶段性进度条。 + +> ⚠️ 这套类型是**前后端契约**。后端漏发某个阶段的事件,前端就会一直停在上一个阶段—— +> 线上「卡在正在检索」的故障正是这么来的,见 +> [排查记录 1.3](../operations/troubleshooting-log.md#13-根因二进度提示与实际执行不同步)。 + +--- + +## 3. 分层结构 + +``` +interfaces/api/portal/agent/PortalAgentSessionController + ↓ +application/conversation/ + ├── service/ConversationAppService 会话与消息的应用编排 + ├── service/ChatSessionManager 活跃会话管理 + ├── service/handler/MessageHandlerFactory 处理器选择 + └── service/message/ + ├── AbstractMessageHandler ★ 对话主流程模板 + ├── chat/ChatMessageHandler 标准对话 + ├── agent/AgentMessageHandler 带工作流的 Agent 对话 + └── preview/PreviewMessageHandler Agent 预览(不落库) + ↓ +domain/conversation/ + ├── service/ContextProcessor 上下文裁剪 + ├── service/ConversationDomainService + ├── service/MessageDomainService + └── service/SessionDomainService + ↓ +infrastructure/transport/ + ├── SseMessageTransport 流式 + └── SyncMessageTransport 同步 +``` + +--- + +## 4. 消息处理器体系 + +`AbstractMessageHandler` 是**模板方法模式**的典型应用,三个子类共享主流程、各自定制细节。 + +``` + AbstractMessageHandler + ├── chat() 入口,创建连接 + ├── processStreamingChat() + ├── processSyncChat() + ├── processChat() 核心 + ├── buildHistoryMessage() + ├── initMemory() + ├── provideTools() ← 子类覆写以定制工具 + └── smartRenameSession() + ↑ + ┌──────────────┼──────────────┐ +ChatMessageHandler AgentMessageHandler PreviewMessageHandler + 标准对话 带工作流编排 预览,不落库 +``` + +### 传输层抽象 + +`MessageTransport` 让主流程与传输方式解耦: + +```java +public interface MessageTransport { + T createConnection(long timeout); + void sendMessage(T connection, AgentChatResponse response); + void completeConnection(T connection); +} +``` + +- `SseMessageTransport` → `SseEmitter`,流式 +- `SyncMessageTransport` → `ChatResponse`,一次性返回 + +**同一套对话逻辑,换个 transport 就能在流式和同步之间切换。** + +--- + +## 5. 一次对话的完整链路 + +``` +POST /agents/sessions/chat + ↓ +PortalAgentSessionController + ↓ 提交到 Executors.newCachedThreadPool() +MessageHandlerFactory.getHandler(agent) + ↓ +AbstractMessageHandler.chat(ctx, transport) + ↓ +transport.createConnection(CONNECTION_TIMEOUT) ← 10 分钟兜底 + ↓ +buildHistoryMessage() 从历史构造 MessageWindowChatMemory + ↓ +provideTools() 装配 RAG 工具 + MCP 工具 + ↓ +highAvailabilityDomainService.selectBestProvider() 选模型实例 + ↓ +llmServiceFactory.getStreamingClient() 建客户端 + ↓ +processChat() 流式消费模型输出 + ↓ 每个 token +transport.sendMessage() → SSE 推给前端 + ↓ 结束 +createLlmMessage() 落库 +reportCallResult() 上报高可用统计 +smartRenameSession() 首轮对话后自动命名会话 +``` + +> `smartRenameSession()` 是个细节设计:会话创建时没有标题, +> 第一轮对话结束后用模型给会话起个名字,用户不必手动命名。 + +--- + +## 6. 上下文与 Token 溢出 + +`ContextProcessor.processContext()` 负责在**送入模型前**把上下文压到限额内。 + +```java +public ContextResult processContext(String sessionId, int maxTokens, + TokenOverflowStrategyEnum strategyType, ...) +``` + +它把 `MessageEntity` 转成 `TokenMessage`,再交给 +[LLM 模块的 Token 溢出策略](llm.md#7-token-溢出处理)处理: + +| 策略 | 行为 | 代价 | +| --- | --- | --- | +| `NONE` | 不处理 | 超长直接报错 | +| `SLIDING_WINDOW` | 丢弃最早的消息 | 丢失早期信息 | +| `SUMMARIZE` | 把旧消息压缩成摘要 | 多一次 LLM 调用 | + +> `SUMMARIZE` 会**额外发起一次模型调用**。这次调用同样受 +> `LLM_REQUEST_TIMEOUT_SECONDS` 约束,上游慢时会直接拖慢用户可感知的首字延迟。 + +--- + +## 7. Agent 工作流 + +`AgentMessageHandler` 在标准对话之上加了一层任务编排,用于处理需要多步完成的复杂请求。 + +### 状态机 + +```java +public enum AgentWorkflowState { + ANALYSER_MESSAGE, // 分析用户消息 + INITIALIZED, // 初始化 + TASK_SPLITTING, // 任务拆分中 + TASK_SPLIT_COMPLETED, + TASK_EXECUTING, // 任务执行中 + TASK_EXECUTED, + SUMMARIZING, // 结果汇总中 + COMPLETED, + FAILED, + WAITING_INPUT_FOR_TASK_SPLIT, // 等用户补充信息 + WAITING_INPUT_FOR_TASK_EXECUTION +} +``` + +两个 `WAITING_INPUT_*` 状态是**人在回路**的设计:Agent 发现信息不足时暂停下来问用户, +而不是硬着头皮猜。由 `InfoRequirementService` 判定缺什么信息。 + +### 处理器与事件 + +``` +AbstractAgentHandler +├── AnalyserMessageHandler 分析意图 +├── TaskSplitHandler 拆分任务 +├── TaskExecutionHandler 逐个执行 +└── SummarizeHandler 汇总结果 + +AgentEventBus ← AgentWorkflowEvent → AgentEventHandler +AgentWorkflowContext 贯穿全流程的上下文 +TaskManager 任务状态管理 +``` + +--- + +## 8. 消息类型与前端协议 + +`AgentChatResponse` 是推给前端的统一信封: + +```java +AgentChatResponse.build(content, MessageType.RAG_RETRIEVAL_PROGRESS) +``` + +前端按 `MessageType` 决定渲染方式(进度条 / 正文 / 工具调用卡片)。 + +**新增消息类型时必须前后端同步**,否则前端遇到未知类型会静默丢弃。 + +--- + +## 9. 已知坑与注意事项 + +### 9.1 `Role` 枚举只有 USER 一个值 + +```java +public enum Role { + USER +} +``` + +历史消息里模型回复的角色靠其他字段区分。改这块前先确认真实的落库结构, +不要假设 `Role` 是完整的角色集合。 + +### 9.2 存在多个同名类,容易 import 错 + +| 类名 | 出现位置 | +| --- | --- | +| `Agent` | `service/handler/Agent`、`service/message/Agent`、`service/message/agent/Agent` | +| `ChatContext` | `service/handler/content/ChatContext`、`service/handler/context/ChatContext` | +| `AgentPromptTemplates` | `service/handler/context/`、`service/message/agent/template/` | + +**三个 `Agent`、两个 `ChatContext`、两个 `AgentPromptTemplates`。** +IDE 自动 import 极易选错,且编译能过、运行才出错。重构时优先合并这些重复类。 + +### 9.3 `CONNECTION_TIMEOUT` 是兜底而非主防线 + +```java +protected static final long CONNECTION_TIMEOUT = 10 * 60 * 1000L; +``` + +真正决定「模型挂了多久能发现」的是 LLM 客户端超时 +(`LLM_STREAM_TIMEOUT_SECONDS`,默认 300s)。这个 10 分钟只是最后一道保险。 +曾经它是 50 分钟,配合 1 小时的 LLM 超时,导致故障被隐藏,见 +[排查记录 1.6](../operations/troubleshooting-log.md#16-根因五sse-超时形同虚设)。 + +### 9.4 控制器用的是无界线程池 + +```java +private final ExecutorService executorService = Executors.newCachedThreadPool(); +``` + +`newCachedThreadPool()` 无上限。并发暴涨时会创建大量线程, +在 1.2 CPU / 1.5GB 内存的容器里可能先 OOM。 + +> 好的一面是它**避开了 `ForkJoinPool.commonPool()` 的串行化陷阱**—— +> RAG 那条链路当初就栽在这上面,见 +> [排查记录 1.4](../operations/troubleshooting-log.md#14-根因三并发被串行化)。 +> 但「无界」本身仍是隐患,建议改为有界线程池 + 拒绝策略。 + +--- + +## 相关文档 + +- [系统架构](../architecture/overview.md) —— 整体分层 +- [Agent 模块](agent.md) —— Agent 定义与发布 +- [LLM 模块](llm.md) —— 模型调用与 Token 策略 +- [RAG 模块](rag.md) —— 检索链路 +- [MCP 工具模块](mcp-tool.md) —— 工具如何被装配进对话 +- [API 参考](../reference/api.md) —— SSE 接口定义 +- [排查记录](../operations/troubleshooting-log.md) —— 本模块相关的线上故障 diff --git a/docs/modules/mcp-tool.md b/docs/modules/mcp-tool.md new file mode 100644 index 0000000..612f712 --- /dev/null +++ b/docs/modules/mcp-tool.md @@ -0,0 +1,320 @@ +# MCP 工具模块 + +> 💬 **一句话人话**:这个模块让用户把一个 GitHub 仓库变成 Agent 能调用的工具。 +> 用户提交仓库地址,平台自动拉代码、丢进一个隔离容器里跑起来、问它「你能提供哪些工具」, +> 人工看一眼没问题就上架。之后 Agent 对话时就能调用这些工具了。 + +本模块覆盖 `tool`(39 个类)与 `container`(25 个类)两个领域,是平台的工具生态基础。 + +--- + +## 目录 + +- [1. 模块职责](#1-模块职责) +- [2. 核心概念](#2-核心概念) +- [3. 分层结构](#3-分层结构) +- [4. 工具上架状态机](#4-工具上架状态机) +- [5. 容器隔离](#5-容器隔离) +- [6. MCP Gateway](#6-mcp-gateway) +- [7. 工具定义与参数转换](#7-工具定义与参数转换) +- [8. 工具市场与用户安装](#8-工具市场与用户安装) +- [9. 已知坑与注意事项](#9-已知坑与注意事项) + +--- + +## 1. 模块职责 + +| 职责 | 说明 | +| --- | --- | +| **工具接入** | 接收 GitHub 仓库或 ZIP 包,验证、部署、抽取工具定义 | +| **审核流转** | 以状态机驱动从提交到上架的全流程,含人工审核关卡 | +| **容器隔离** | 为审核和用户运行分别提供独立容器,互不影响 | +| **协议网关** | 通过 MCP Gateway 与容器内的 MCP Server 通信 | +| **市场分发** | 已通过审核的工具进入市场,用户可安装到自己的工作区 | + +**不负责**:工具在对话中的实际调用编排(见 [Agent 模块](agent.md))、 +模型侧的 function calling 协议(见 [LLM 模块](llm.md))。 + +--- + +## 2. 核心概念 + +### ToolType — 工具类型 + +```java +public enum ToolType { + MCP; // 目前只支持 MCP 协议工具 +} +``` + +> 枚举里只有一个值,说明当前平台的工具生态**完全押注在 MCP 协议**上。 +> 未来若要支持 OpenAPI / 自定义 HTTP 工具,这里是扩展点。 + +### UploadType — 上传方式 + +```java +public enum UploadType { + GITHUB, // 提交仓库地址,平台去 clone + ZIP; // 直接上传压缩包 +} +``` + +### ToolStatus — 审核状态 + +```java +public enum ToolStatus { + WAITING_REVIEW, // 等待审核(入口状态) + GITHUB_URL_VALIDATE, // GitHub URL 验证中 + DEPLOYING, // 部署中 + FETCHING_TOOLS, // 抽取工具定义中 + MANUAL_REVIEW, // 人工审核 + APPROVED, // 已通过(终态) + FAILED; // 通用失败(终态) +} +``` + +### ContainerType 与 ContainerStatus + +```java +public enum ContainerType { + USER, // 用户容器:承载该用户已安装的工具 + REVIEW; // 审核容器:临时跑待审工具,用完即弃 +} + +public enum ContainerStatus { + CREATING(1), RUNNING(2), STOPPED(3), ERROR(4), + DELETING(5), DELETED(6), SUSPENDED(7); +} +``` + +> **为什么要分两种容器**:待审工具是**不可信代码**,不能和用户已信任的工具跑在一起。 +> 审核容器是一次性的沙箱,审完就销毁。 + +--- + +## 3. 分层结构 + +``` +interfaces/dto/tool/ 请求响应 DTO +interfaces/dto/container/ + ↓ +application/tool/ + ├── service/ToolAppService 工具增删改查、市场操作 + ├── service/ToolStateStateMachineAppService 状态机驱动器 + └── service/state/ 六个状态处理器 +application/container/ + ├── service/ContainerAppService 容器生命周期 + ├── service/ReviewContainerService 审核容器专用 + ├── service/ContainerMonitorService 健康监控 + └── service/ContainerCleanupService 过期清理 + ↓ +domain/tool/ ToolEntity、ToolVersionEntity、UserToolEntity +domain/container/ ContainerEntity、ContainerTemplateEntity + ↓ +infrastructure/mcp_gateway/ MCPGatewayService +infrastructure/docker/ Docker 操作 +infrastructure/github/ GitHubService +``` + +--- + +## 4. 工具上架状态机 + +这是本模块最核心的机制。**每个状态对应一个处理器,处理器声明「我处理哪个状态」和 +「成功后进入哪个状态」,驱动器负责串起来。** + +### 状态流转 + +``` + 提交工具 + ↓ +WAITING_REVIEW AppWaitingReviewProcessor + ↓ (入口,校验基本信息) +GITHUB_URL_VALIDATE AppGithubUrlValidateProcessor + ↓ (验证仓库可达、拉取仓库信息) + DEPLOYING AppDeployingProcessor + ↓ (创建审核容器,执行安装命令) +FETCHING_TOOLS AppFetchingToolsProcessor + ↓ (连上 MCP Server,listTools 抽取定义) +MANUAL_REVIEW AppManualReviewProcessor + ↓ (人工把关,此处无自动下一状态) + APPROVED AppPublishingProcessor + (发布到市场) + + 任一步失败 → FAILED +``` + +### 处理器契约 + +```java +public interface AppToolStateProcessor { + ToolStatus getStatus(); // 我负责哪个状态 + void process(ToolEntity tool); // 干活,失败抛 BusinessException + ToolStatus getNextStatus(); // 成功后进入哪个状态,null 表示需外部触发 +} +``` + +`AppManualReviewProcessor` 的 `getNextStatus()` 返回 `null` —— **这是人工审核关卡的实现方式**: +状态机跑到这里就停住,等管理员在后台点「通过」才继续。 + +### 驱动器 + +`ToolStateStateMachineAppService` 在 `@PostConstruct` 里把所有处理器注册进 +`Map`,注册时若发现同一状态被重复注册会打 WARN 日志。 + +处理是**异步**的: + +```java +private final ExecutorService executorService; // 专用线程池 + +public void submitToolForProcessing(ToolEntity toolEntity) { + executorService.submit(() -> processToolState(toolEntity)); +} +``` + +线程命名为 `app-tool-state-processor-thread`,便于在线程 dump 里定位。 + +> ✅ **这里用了专用线程池而非 `ForkJoinPool.commonPool()`**,是正确做法。 +> 对比 RAG 流式问答曾经踩过的坑,见 +> [排查记录 1.4](../operations/troubleshooting-log.md#14-根因三并发被串行化)。 + +--- + +## 5. 容器隔离 + +### 两类容器的职责 + +| | 审核容器(REVIEW) | 用户容器(USER) | +| --- | --- | --- | +| 何时创建 | 工具进入 DEPLOYING 时 | 用户首次安装工具时 | +| 生命周期 | 审核结束即销毁 | 长期存在 | +| 跑什么 | **不可信**的待审工具 | 用户已安装的工具 | +| 隔离动机 | 防止恶意代码影响平台 | 租户之间互不干扰 | + +### 配套服务 + +- **`ContainerMonitorService`** —— 定期检查容器健康状态,异常置为 `ERROR` +- **`ContainerCleanupService`** —— 清理已停止/过期容器,回收磁盘 +- **`ContainerTemplateEntity`** —— 容器模板,定义镜像、资源限制等创建参数 + +### 宿主机 Docker 依赖 + +后端容器挂载了宿主机的 Docker Socket: + +```yaml +volumes: + - /var/run/docker.sock:/var/run/docker.sock +``` + +> ⚠️ **安全影响**:挂载 docker.sock 等同于把宿主机 root 权限交给后端进程。 +> 这是容器管理功能的必要代价,但意味着**后端一旦被攻破,宿主机即失守**。 +> 生产环境应考虑改用 Docker API over TLS 或 rootless 方案。详见 +> [安全实践](../operations/security.md)。 + +--- + +## 6. MCP Gateway + +`MCPGatewayService` 是平台与容器内 MCP Server 通信的唯一出口。 + +### 关键方法 + +| 方法 | 用途 | +| --- | --- | +| `buildUserContainerUrl(name, ip, port)` | 拼接用户容器内 MCP Server 的地址 | +| `buildGlobalSSEUrl(name)` | 拼接全局 SSE 地址 | +| `deployTool(installCommand)` | 在默认容器执行安装命令 | +| `deployTool(cmd, ip, port)` | 在**指定容器**执行安装(审核流程用) | +| `listTools(toolName)` | 向 MCP Server 查询它提供哪些工具 | +| `listToolsFromReviewContainer(name, ip, port)` | 从审核容器查询(隔离路径) | + +注意 `deployTool` 和 `listTools` 都有**两个重载**:不带 IP/端口的走默认容器, +带 IP/端口的走指定容器。审核流程必须用后者,否则待审的不可信代码会跑进公共容器。 + +--- + +## 7. 工具定义与参数转换 + +MCP Server 返回的工具定义需要转换成 LangChain4j 能理解的格式,才能交给模型做 function calling。 + +``` +MCP Server + ↓ listTools() +ToolDefinition (平台自己的模型) + ├── ToolParameter + └── ParameterProperty + ↓ ToolSpecificationConverter +LangChain4j ToolSpecification + ↓ +交给模型做 function calling +``` + +`ToolSpecificationConverter` 是这条链路的关键 —— 它把平台的工具描述翻译成模型侧的 +函数签名。RAG 检索也走同一套机制,见 +`application/conversation/service/message/agent/tool/RagToolSpecification`。 + +> 💡 **值得注意的设计**:RAG 检索本身被包装成了一个「工具」(`RagToolExecutor` / +> `RagToolManager` / `RagToolSpecification`),和 MCP 工具走同一条调用链。 +> 这样 Agent 不需要区分「这是检索」还是「这是外部工具」,统一按 function calling 处理。 + +--- + +## 8. 工具市场与用户安装 + +``` +ToolEntity 工具本体(作者提交的那个) +ToolVersionEntity 工具的某个版本(市场里陈列的是版本) +UserToolEntity 某用户安装了某工具的某版本 +``` + +三者关系决定了:**用户安装的是「版本」而非「工具」**,作者发新版不会自动改变已安装用户的行为, +需要用户主动升级。这与 [RAG 知识库的版本快照机制](rag.md) 是同一个设计思路。 + +--- + +## 9. 已知坑与注意事项 + +### 9.1 ToolStatus 里有两个「历史遗留」状态 + +源码注释明确写着: + +```java +DEPLOYING, // (原)部署中 - 根据新流程,此状态可能调整或移除,暂时保留 +FETCHING_TOOLS, // (原)获取工具中 - 根据新流程,此状态可能调整或移除,暂时保留 +``` + +改动状态机时要留意这两个状态的去留,别当成稳定契约。 + +### 9.2 `ToolStatus.fromCode()` 找不到时返回 null + +```java +public static ToolStatus fromCode(String name) { + for (ToolStatus status : values()) { ... } + return null; // ← 与其他枚举不一致 +} +``` + +同项目里 `ToolType.fromCode()` / `UploadType.fromCode()` **抛 BusinessException**, +只有 `ToolStatus` 返回 `null`。调用方必须判空,否则会在下游变成 NPE, +且报错位置离真实原因很远。 + +### 9.3 审核容器必须用带 IP/端口的重载 + +`deployTool(installCommand)` 和 `deployTool(cmd, ip, port)` 长得很像, +**审核流程用错重载会让不可信代码跑进公共容器**。改这块代码时务必确认调用的是哪个。 + +### 9.4 docker.sock 挂载是权限天花板 + +见 [第 5 节](#5-容器隔离)。任何关于「工具沙箱有多安全」的讨论, +上限都是「后端进程有宿主机 root」。 + +--- + +## 相关文档 + +- [系统架构](../architecture/overview.md) —— 整体分层与模块边界 +- [Agent 模块](agent.md) —— 工具在对话中如何被调用 +- [LLM 模块](llm.md) —— function calling 的模型侧 +- [RAG 模块](rag.md) —— 检索被包装成工具的实现 +- [安全实践](../operations/security.md) —— 容器隔离与权限边界 +- [数据库设计](../reference/database.md) —— 工具与容器相关表结构 diff --git a/docs/modules/user-auth.md b/docs/modules/user-auth.md new file mode 100644 index 0000000..aad3889 --- /dev/null +++ b/docs/modules/user-auth.md @@ -0,0 +1,238 @@ +# 用户与认证模块 + +> 💬 **一句话人话**:管「你是谁」和「你能干什么」。包括注册登录、GitHub 一键登录、 +> 邮箱验证码、给第三方程序用的 API Key,以及每个用户自己的模型偏好设置和额度。 + +本模块覆盖 `user`(33 类)、`auth`(16 类)、`apikey`(7 类)、`account`(7 类), +以及 `infrastructure/verification` 验证码组件。 + +--- + +## 目录 + +- [1. 模块职责](#1-模块职责) +- [2. 核心实体](#2-核心实体) +- [3. 两条认证链路](#3-两条认证链路) +- [4. 登录方式与开关](#4-登录方式与开关) +- [5. 验证码机制](#5-验证码机制) +- [6. 用户设置与降级链](#6-用户设置与降级链) +- [7. 账户与额度](#7-账户与额度) +- [8. 已知坑与注意事项](#8-已知坑与注意事项) + +--- + +## 1. 模块职责 + +| 职责 | 说明 | +| --- | --- | +| **身份认证** | 账号密码登录、GitHub OAuth、JWT 签发校验 | +| **注册与验证** | 注册流程、图形验证码、邮箱验证码 | +| **外部鉴权** | API Key 让第三方程序以某个 Agent 的身份调用 | +| **用户设置** | 默认模型、降级链等每用户偏好 | +| **账户额度** | 余额、充值、支付方式 | +| **功能开关** | 管理员可关闭注册、关闭某种登录方式 | + +--- + +## 2. 核心实体 + +``` +UserEntity 用户主体:账号、密码哈希、昵称、邮箱 +AccountEntity 账户:余额、额度(与 UserEntity 分离) +UserSettingsEntity 用户设置:默认模型、降级链 +ApiKeyEntity API Key:绑定到某个用户的某个 Agent +AuthSettingEntity 平台级认证开关 +``` + +> **为什么 User 和 Account 分开**:身份信息与计费信息生命周期不同。 +> 账户余额变动频繁,用户资料几乎不变,分表可以避免热点行拖累。 + +--- + +## 3. 两条认证链路 + +平台有**两套完全独立**的鉴权入口,各自有拦截器和上下文。 + +### 链路一:网页用户(JWT) + +``` +浏览器请求(Header: Authorization Bearer ) + ↓ +UserAuthInterceptor.preHandle() + ↓ 校验 JWT,解出 userId +UserContext.setCurrentUserId(userId) ← ThreadLocal + ↓ +Controller / Service 里 UserContext.getCurrentUserId() + ↓ 请求结束 +UserAuthInterceptor.afterCompletion() + ↓ +UserContext.clear() ← 必须清理 +``` + +### 链路二:外部程序(API Key) + +``` +第三方请求(Header: API Key) + ↓ +ExternalApiKeyInterceptor.preHandle() + ↓ 查 ApiKeyEntity,校验状态 +ExternalApiContext.setUserId(...) +ExternalApiContext.setAgentId(...) ← 同时带出 Agent + ↓ 请求结束 +ExternalApiContext.clear() +``` + +**关键差异**:API Key 天然绑定一个 Agent,所以外部调用不需要(也不能)指定用哪个 Agent。 +这是「把某个 Agent 作为 API 对外提供」的实现方式。 + +### 上下文实现 + +```java +public class UserContext { + private static final ThreadLocal CURRENT_USER_ID = new ThreadLocal<>(); + + public static void setCurrentUserId(String userId) { CURRENT_USER_ID.set(userId); } + public static String getCurrentUserId() { return CURRENT_USER_ID.get(); } + public static void clear() { CURRENT_USER_ID.remove(); } +} +``` + +两个拦截器都在 `afterCompletion` 中调用了 `clear()`, +**这在使用线程池的 Web 容器里是必须的**——不清理会造成两个后果: +线程复用时读到上一个请求的用户身份(**越权**),以及 ThreadLocal 引用无法回收(**内存泄漏**)。 + +--- + +## 4. 登录方式与开关 + +```java +public enum AuthFeatureKey { + NORMAL_LOGIN("NORMAL_LOGIN", "普通登录"), + GITHUB_LOGIN("GITHUB_LOGIN", "GitHub登录"), + COMMUNITY_LOGIN("COMMUNITY_LOGIN", "敲鸭登录"), + USER_REGISTER("USER_REGISTER", "用户注册"); +} +``` + +`AuthSettingEntity` 存每个开关的启用状态,`AuthConfigController` 把结果暴露给前端—— +前端据此决定登录页显示哪些按钮。 + +> 前端启动时会请求 `/api/auth/config`。这个接口是**未登录可访问**的, +> 否则会陷入「要登录才能知道怎么登录」的死循环。 + +--- + +## 5. 验证码机制 + +``` +infrastructure/verification/ +├── CaptchaUtils 图形验证码生成 +├── VerificationCodeService 邮箱验证码收发 +├── config/VerificationCodeConfig +└── storage/ + ├── CodeStorage 存储抽象 + └── MemoryCodeStorage 内存实现 ← 注意 +``` + +`CodeStorage` 被抽象成接口,但当前**只有内存实现**。 + +> ⚠️ **单机限制**:验证码存在 JVM 内存里。这意味着 +> ① 服务重启后所有未使用的验证码失效; +> ② **无法水平扩容**——两个后端实例各存各的,用户在 A 实例拿的码到 B 实例校验必然失败。 +> +> 要多实例部署必须先补一个 Redis 实现。接口已经抽象好了,这是预留的扩展点。 + +--- + +## 6. 用户设置与降级链 + +```java +UserSettingsEntity +└── UserSettingsConfig + └── FallbackConfig 降级链配置 +``` + +`UserSettingsDomainService` 提供两个关键方法,在对话链路里被高频调用: + +- `getUserDefaultModelId(userId)` —— 该用户的默认模型 +- `getUserFallbackChain(userId)` —— 主模型不可用时依次尝试的备选模型 + +降级链会被传给高可用选路,详见 +[LLM 模块第 6 节](llm.md#6-高可用与降级链)。 + +> RAG 的意图识别、语义改写、查询扩展都会调 `getUserDefaultModelId()`。 +> 用户没设默认模型时这些方法返回 `null`,对应功能**静默降级**(不报错,跳过该步骤)。 + +--- + +## 7. 账户与额度 + +``` +AccountEntity 余额、额度 +AccountAppService 充值、查询 +AddCreditRequest 充值请求 +PaymentMethodDTO 支付方式 +PaymentResponseDTO 支付结果 +``` + +Token 消耗统计由 [LLM 模块的 token 部分](llm.md#7-token-溢出处理)产生, +计费口径与扣减时机需结合 `AccountDomainService` 一起看。 + +--- + +## 8. 已知坑与注意事项 + +### 8.1 ⚠️ ThreadLocal 不跨线程传递 + +这是本模块**最容易踩的坑**。`UserContext` 基于 ThreadLocal, +意味着**任何异步执行的代码里 `UserContext.getCurrentUserId()` 都会返回 `null`**。 + +平台里有多处异步执行: + +| 位置 | 线程池 | +| --- | --- | +| `RagQaDatasetAppService` | `ragStreamExecutor` | +| `PortalAgentSessionController` | `newCachedThreadPool()` | +| `ToolStateStateMachineAppService` | `app-tool-state-processor-thread` | + +**现有代码的正确做法**是在控制器线程里先取出 userId,再作为参数传进异步任务: + +```java +// ✅ 正确:在控制器线程取值 +String userId = UserContext.getCurrentUserId(); +return ragQaDatasetAppService.ragStreamChat(request, userId); + +// ❌ 错误:在异步任务内部取值,必得 null +CompletableFuture.runAsync(() -> { + String userId = UserContext.getCurrentUserId(); // null! +}); +``` + +新增异步逻辑时务必遵守这个约定。这类 bug 的症状是「查不到数据」或 +「越权访问到别人数据」,而不是抛异常,很难发现。 + +### 8.2 验证码无法多实例部署 + +见[第 5 节](#5-验证码机制)。这是当前架构**水平扩容的硬阻塞点之一**。 + +### 8.3 API Key 与 Agent 强绑定 + +`ApiKeyEntity` 同时持有 `userId` 和 `agentId`。想让一个 Key 调用多个 Agent, +当前模型不支持,需要建多个 Key。 + +### 8.4 敏感信息脱敏依赖 Assembler + +`ProviderAssembler` 里有 `dto.maskSensitiveInfo()`。 +**脱敏发生在 DTO 转换层而非实体层**,意味着任何绕过 Assembler 直接序列化实体的路径 +都会泄露明文密钥。新增接口时注意走 Assembler。 + +--- + +## 相关文档 + +- [系统架构](../architecture/overview.md) —— 整体分层 +- [LLM 模块](llm.md) —— 默认模型与降级链的消费方 +- [对话模块](conversation.md) —— userId 在对话链路中的传递 +- [安全实践](../operations/security.md) —— 认证、越权与多租户隔离 +- [API 参考](../reference/api.md) —— 登录与 API Key 接口 +- [数据库设计](../reference/database.md) —— 用户相关表结构 From 7c5a63ccd3e286276a48bb8e6f482ef9b523b786 Mon Sep 17 00:00:00 2001 From: NEDONION Date: Wed, 12 Aug 2026 01:46:54 +0800 Subject: [PATCH 4/5] =?UTF-8?q?docs:=20=E6=96=B0=E5=A2=9E=E6=89=A7?= =?UTF-8?q?=E8=A1=8C=E8=BF=BD=E8=B8=AA=E3=80=81=E4=BB=BB=E5=8A=A1=E8=B0=83?= =?UTF-8?q?=E5=BA=A6=E3=80=81=E5=9F=BA=E7=A1=80=E8=AE=BE=E6=96=BD=E4=B8=89?= =?UTF-8?q?=E7=AF=87=E6=96=87=E6=A1=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - modules/trace.md:两级数据模型(摘要+明细)的动机、八个执行阶段如何用于失败定位、 TraceCollector 采集接口、Spring 事件解耦的收益与代价。 - modules/task.md:厘清 domain/task(工作流任务)与 domain/scheduledtask(定时任务) 两个易混概念、六种重复规则、DelayQueue 延迟队列实现与重启恢复机制。 - architecture/infrastructure.md:19 个子包全景、MQ/对象存储/传输层抽象/类型转换器/ 加密工具/拦截器/异常体系/外部集成。 排查过程中发现并记录了一个高危安全问题(infrastructure.md 第 9.1 节): ProviderConfigConverter 用 ValidationUtils.EncryptUtils 加密服务商配置落库, 而该实现的 AES 密钥 "1234567890123456" 硬编码在源码中,且本仓库是公开的, 加之使用 ECB 模式,实际等同于未加密——拿到数据库内容即可解出全部用户的 模型服务商 API Key。文档中已给出修复方向。 同时记录:ConfigEncryptor 是硬编码密钥的死代码;ProviderConfigConverter 按列索引 取值的重载未实现直接返回 null;定时任务的内存队列与验证码的内存存储共同构成 水平扩容的两个阻塞点。 Co-Authored-By: Claude Opus 5 --- docs/architecture/infrastructure.md | 260 ++++++++++++++++++++++++++++ docs/modules/task.md | 229 ++++++++++++++++++++++++ docs/modules/trace.md | 189 ++++++++++++++++++++ 3 files changed, 678 insertions(+) create mode 100644 docs/architecture/infrastructure.md create mode 100644 docs/modules/task.md create mode 100644 docs/modules/trace.md diff --git a/docs/architecture/infrastructure.md b/docs/architecture/infrastructure.md new file mode 100644 index 0000000..832e762 --- /dev/null +++ b/docs/architecture/infrastructure.md @@ -0,0 +1,260 @@ +# 基础设施层 + +> 💬 **一句话人话**:这一层是「脏活累活」的集合——连数据库、发消息队列、传文件到对象存储、 +> 发邮件、调 GitHub、加解密、拦截请求。业务代码不该关心这些细节,都封装在这里。 + +对应 `src/main/java/org/lucas/infrastructure/`,共 19 个子包。 + +--- + +## 目录 + +- [1. 组件全景](#1-组件全景) +- [2. 消息队列](#2-消息队列) +- [3. 对象存储与文件](#3-对象存储与文件) +- [4. 传输层抽象](#4-传输层抽象) +- [5. 类型转换器](#5-类型转换器) +- [6. 加密与工具类](#6-加密与工具类) +- [7. 拦截器与异常](#7-拦截器与异常) +- [8. 外部集成](#8-外部集成) +- [9. 已知坑与注意事项](#9-已知坑与注意事项) + +--- + +## 1. 组件全景 + +| 子包 | 职责 | 关键类 | +| --- | --- | --- | +| `mq` | RabbitMQ 收发 | `MyRabbitmqConfig`、`MQPushListener` | +| `storage` | 对象存储上传 | `OssUploadService` | +| `email` | 邮件发送 | `EmailService` | +| `github` | GitHub 集成 | `GitHubService`、`GitHubUrlParser` | +| `docker` | 容器操作 | `DockerService` | +| `mcp_gateway` | MCP 协议网关 | `MCPGatewayService` | +| `transport` | 对话传输抽象 | `SseMessageTransport`、`SyncMessageTransport` | +| `llm` | 模型客户端工厂 | `LLMProviderFactory`、`LLMServiceFactory` | +| `rag` | 向量与嵌入 | 嵌入模型工厂、配置 | +| `highavailability` | 高可用网关客户端 | `HighAvailabilityGatewayImpl` | +| `auth` | 鉴权上下文与拦截器 | `UserContext`、`UserAuthInterceptor` | +| `interceptor` | 管理端鉴权 | `AdminAuthInterceptor` | +| `verification` | 验证码 | `CaptchaUtils`、`VerificationCodeService` | +| `converter` | MyBatis 类型转换 | 12 个 Converter | +| `entity` | 持久化实体基类 | — | +| `repository` | 仓储实现 | — | +| `config` | Spring 配置 | `WebConfig`、`MybatisPlusConfig` 等 | +| `exception` | 业务异常 | `BusinessException` 等 5 个 | +| `utils` | 通用工具 | `JwtUtils`、`JsonUtils` 等 | + +--- + +## 2. 消息队列 + +``` +infrastructure/mq/ +├── configure/MyRabbitmqConfig 队列、交换机、绑定声明 +├── configure/MyRabbitmqProperties 连接配置 +├── enums/EventType 事件类型 +├── events/RagDocSyncOcrEvent 文档 OCR 事件 +├── events/RagDocSyncStorageEvent 文档存储事件 +├── listener/MQPushListener 消费者 +├── model/MQSendEventModel 单播消息模型 +├── model/MQFanoutSendEventModel 广播消息模型 +├── model/MqMessage +└── utils/RabbitMQUtils +``` + +当前 MQ **主要服务于 RAG 文档处理链路**:文档上传后异步做 OCR 和向量化, +避免阻塞用户请求。详见 [RAG 模块](../modules/rag.md)。 + +`docker-compose.yml` 里 RabbitMQ 限制 512MB 内存 / 0.3 CPU, +后端的健康检查依赖它 `service_healthy` 后才启动。 + +--- + +## 3. 对象存储与文件 + +`OssUploadService` 对接 S3 兼容存储(配置指向七牛云)。相关环境变量: + +``` +S3_SECRET_ID / S3_SECRET_KEY / S3_REGION +S3_ENDPOINT / S3_BUCKET_NAME / S3_DOMAIN +``` + +用户上传的知识库原始文档存在这里,数据库只存元数据和向量。 + +--- + +## 4. 传输层抽象 + +```java +public interface MessageTransport { + T createConnection(long timeout); + void sendMessage(T connection, AgentChatResponse response); + void completeConnection(T connection); +} +``` + +| 实现 | 连接类型 | 用途 | +| --- | --- | --- | +| `SseMessageTransport` | `SseEmitter` | 流式对话 | +| `SyncMessageTransport` | `ChatResponse` | 同步返回 | + +这个抽象让[对话模块](../modules/conversation.md#4-消息处理器体系)的主流程 +不必区分流式与同步。 + +--- + +## 5. 类型转换器 + +12 个 MyBatis `TypeHandler`,负责实体字段与数据库列之间的转换: + +| 转换器 | 用途 | +| --- | --- | +| `JsonConverter` / `JsonToStringConverter` | 对象 ↔ JSON 列 | +| `ListConverter` / `ListStringConverter` | 集合 ↔ 列 | +| `ContainerStatusConverter` / `ContainerTypeConverter` | 容器枚举 ↔ 码值 | +| `InstallTypeConverter` | 安装类型枚举 | +| `LLMModelConfigConverter` | 模型配置 JSON | +| **`ProviderConfigConverter`** | **服务商配置——加密存储** | + +`ProviderConfigConverter` 与众不同:它在写库前加密、读库后解密, +是唯一涉及加密的转换器。**但其加密实现有严重问题,见[第 9 节](#9-已知坑与注意事项)。** + +--- + +## 6. 加密与工具类 + +``` +utils/ +├── ConfigEncryptor AES 加密(★ 死代码,全仓库零调用) +├── JwtUtils JWT 签发与校验 +├── JsonUtils JSON 序列化 +├── PasswordUtils 密码哈希 +├── ValidationUtils 校验 + 内嵌 EncryptUtils(★ 实际在用) +└── ModelResponseToJsonUtils 模型响应解析 +``` + +> ⚠️ 仓库里存在**两套 AES 实现**:`ConfigEncryptor` 和 `ValidationUtils.EncryptUtils`。 +> 前者无人调用,后者才是真正用于服务商密钥落库的那个。 +> 两者都**硬编码了密钥**,详见[第 9 节](#9-已知坑与注意事项)。 + +--- + +## 7. 拦截器与异常 + +### 拦截器 + +| 拦截器 | 作用范围 | +| --- | --- | +| `UserAuthInterceptor` | 网页用户 JWT 校验 | +| `ExternalApiKeyInterceptor` | 外部 API Key 校验 | +| `AdminAuthInterceptor` | 管理端接口 | + +三者均在 `WebConfig` / `WebMvcConfig` 中注册路径规则。 + +### 业务异常 + +``` +BusinessException 通用业务异常 +├── EntityNotFoundException 实体不存在 +├── InsufficientBalanceException 余额不足 +├── ParamValidationException 参数校验失败 +└── RateLimitException 限流 +``` + +--- + +## 8. 外部集成 + +| 组件 | 用途 | +| --- | --- | +| `GitHubService` | 拉取仓库信息,供 MCP 工具上架流程使用 | +| `GitHubUrlParser` | 解析 GitHub URL | +| `DockerService` | 创建/启动/停止容器 | +| `EmailService` | 发送验证码邮件,配置走 `MAIL_SMTP_*` | +| `HighAvailabilityGatewayImpl` | 调用外部高可用网关选路 | + +--- + +## 9. 已知坑与注意事项 + +### 9.1 🔴 服务商 API Key 的加密密钥硬编码在公开仓库中 + +**这是当前代码库最严重的安全问题。** + +`ProviderConfigConverter` 负责把 `ProviderConfig`(**内含用户填写的 LLM 服务商 API Key**) +加密后落库: + +```java +public void setNonNullParameter(PreparedStatement ps, int i, ProviderConfig parameter, ...) { + String jsonStr = JsonUtils.toJsonString(parameter); + String encryptedStr = ValidationUtils.EncryptUtils.encrypt(jsonStr); // ← 加密 + ps.setString(i, encryptedStr); +} +``` + +而 `ValidationUtils.EncryptUtils` 的密钥是: + +```java +private static final String ALGORITHM = "AES"; +private static final String SECRET_KEY = "1234567890123456"; // 16位密钥 +... +Cipher cipher = Cipher.getInstance(ALGORITHM); // 无模式 → 默认 AES/ECB/PKCS5Padding +``` + +三个问题叠加: + +| 问题 | 说明 | +| --- | --- | +| **密钥硬编码** | 写死在源码里,无法轮换 | +| **仓库公开** | 密钥等同于公开信息,加密形同虚设 | +| **ECB 模式** | `Cipher.getInstance("AES")` 默认 ECB,相同明文产生相同密文,可做模式分析 | + +**影响**:任何拿到数据库内容的人(备份泄露、SQL 注入、运维越权、云厂商快照) +都能直接解出**全部用户的模型服务商 API Key**。 + +**建议修复方向**: + +1. 密钥改从环境变量注入(如 `CONFIG_ENCRYPTION_KEY`),随部署配置提供 +2. 改用 `AES/GCM/NoPadding`,每条记录随机 IV,密文与 IV 一同存储 +3. 提供一次性迁移任务,用旧密钥解密、新密钥重新加密存量数据 +4. 修复后**所有已存的服务商密钥应视为已泄露**,通知用户轮换 + +### 9.2 存在两套 AES 实现,其一为死代码 + +`ConfigEncryptor` 全仓库零调用,但同样硬编码密钥(`"AgentX-Config-Key"`)。 + +死代码本身无害,风险在于**将来有人误以为它是"正确的那个"而开始使用**。 +建议直接删除,只保留一套经过修复的加密工具。 + +### 9.3 `getNullableResult(ResultSet, int)` 直接返回 null + +```java +@Override +public ProviderConfig getNullableResult(ResultSet rs, int columnIndex) throws SQLException { + return null; // ← 未实现 +} +``` + +三个重载中,按**列索引**取值的那个没有实现,另外两个(按列名、按 +CallableStatement)都正常解密。 + +如果某条查询走到按索引取值的路径,会静默拿到 `null` 而非配置, +表现为「服务商配置突然为空」,且不报错。改动查询写法时需留意。 + +### 9.4 验证码仅内存实现,阻塞水平扩容 + +见 [用户认证模块第 5 节](../modules/user-auth.md#5-验证码机制)。 +与[定时任务的内存队列](../modules/task.md#61-️-多实例部署会重复执行)并列, +是当前架构无法多实例部署的两个原因。 + +--- + +## 相关文档 + +- [系统架构](overview.md) —— 整体分层与依赖方向 +- [安全实践](../operations/security.md) —— 完整的安全清单 +- [LLM 模块](../modules/llm.md) —— 服务商配置的业务侧 +- [RAG 模块](../modules/rag.md) —— MQ 与对象存储的主要使用方 +- [对话模块](../modules/conversation.md) —— 传输层抽象的消费方 +- [用户认证模块](../modules/user-auth.md) —— 拦截器与上下文 diff --git a/docs/modules/task.md b/docs/modules/task.md new file mode 100644 index 0000000..828b0d9 --- /dev/null +++ b/docs/modules/task.md @@ -0,0 +1,229 @@ +# 任务与调度模块 + +> 💬 **一句话人话**:两件事。一是 Agent 干活时把大任务拆成小任务、逐个跟踪进度; +> 二是定时任务——让 Agent 每天早上九点、或者每周一自动跑一次。 + +本模块覆盖 `task`(9 类,工作流任务)与 `scheduledtask`(12 类,定时调度)。 +两者名字相近但**职责完全不同**,别搞混。 + +--- + +## 目录 + +- [1. 两个 task 的区别](#1-两个-task-的区别) +- [2. 工作流任务](#2-工作流任务) +- [3. 定时任务](#3-定时任务) +- [4. 重复规则](#4-重复规则) +- [5. 延迟队列实现](#5-延迟队列实现) +- [6. 已知坑与注意事项](#6-已知坑与注意事项) + +--- + +## 1. 两个 task 的区别 + +| | `domain/task` | `domain/scheduledtask` | +| --- | --- | --- | +| **中文名** | 工作流任务 | 定时任务 | +| **来源** | Agent 把用户请求拆分出来的子任务 | 用户配置的周期性计划 | +| **生命周期** | 一次对话内 | 跨越多次执行,长期存在 | +| **状态** | `WAITING / IN_PROGRESS / COMPLETED` | `ACTIVE / PAUSED / COMPLETED` | +| **触发方式** | 由工作流推进 | 由延迟队列到点触发 | + +--- + +## 2. 工作流任务 + +``` +TaskEntity 单个子任务 +TaskAggregate 任务聚合根 +TaskStatus WAITING → IN_PROGRESS → COMPLETED +``` + +由[对话模块的 Agent 工作流](conversation.md#7-agent-工作流)驱动: + +``` +TaskSplitHandler 拆分 → 创建多个 TaskEntity + ↓ +TaskManager 管理任务集合 + ↓ +TaskExecutionHandler 逐个执行,更新状态 + ↓ +SummarizeHandler 汇总所有结果 +``` + +每个任务状态变化都会通过 SSE 推给前端 +(`TASK_STATUS_TO_LOADING` / `TASK_STATUS_TO_FINISH`),前端渲染成任务清单。 + +--- + +## 3. 定时任务 + +``` +ScheduledTaskEntity 定时任务定义 +├── RepeatConfig 重复规则 +└── ScheduleTaskStatus ACTIVE / PAUSED / COMPLETED + +ScheduledTaskExecutionService 对外的管理接口 +├── scheduleTask() 新建 +├── cancelTask() / cancelTasks() +├── rescheduleTask() 重排 +├── pauseTask() / resumeTask() +├── deleteTask() +├── deleteTasksBySessionId() 会话删除时级联 +└── deleteTasksByAgentId() Agent 删除时级联 + +TaskScheduleService 时间计算 +├── calculateNextExecuteTime() +└── shouldExecuteAt() + +ScheduleTaskExecutor 实际执行 +├── canExecute() +└── executeTask() 发布 ScheduledTaskExecuteEvent +``` + +`deleteTasksBySessionId` / `deleteTasksByAgentId` 的存在说明 +**定时任务与会话、Agent 是级联关系**——删除会话或 Agent 时必须清理其定时任务, +否则会留下指向不存在对象的僵尸任务。 + +--- + +## 4. 重复规则 + +```java +public enum RepeatType { + NONE, // 单次 + DAILY, // 每天 + WEEKLY, // 每周 + MONTHLY, // 每月 + WORKDAYS, // 工作日 + CUSTOM // 自定义间隔 +} +``` + +`RepeatConfig` 的字段按类型各取所需: + +| 字段 | 用于 | +| --- | --- | +| `executeDateTime` | `NONE` 单次执行时间 | +| `weekdays` | `WEEKLY` 一周中的哪几天 | +| `monthDay` | `MONTHLY` 每月几号 | +| `interval` + `timeUnit` | `CUSTOM` 自定义间隔 | +| `executeTime` | 每天的具体时刻 | +| `endDateTime` | 重复截止时间 | + +> **`WORKDAYS` 的隐患**:工作日判定通常只按周一至周五, +> **不含法定节假日和调休**。国内场景下这可能不符合用户预期。 + +--- + +## 5. 延迟队列实现 + +`DelayedTaskQueueManager` 用 JDK 的 `DelayQueue` 实现到点触发。 + +```java +private final DelayQueue delayQueue = new DelayQueue<>(); +private Thread consumerThread; // 单线程消费 +private ExecutorService executorService; // 5 线程执行 + +@PostConstruct +public void init() { + this.executorService = Executors.newFixedThreadPool(5, r -> { + Thread t = new Thread(r, "scheduled-task-executor-"); + ... + }); + // 启动 consumerThread(daemon) +} + +@PreDestroy +public void destroy() { + consumerThread.interrupt(); + ... +} +``` + +### 工作方式 + +``` +ScheduledTaskExecutionService.scheduleTask() + ↓ 算出下次执行时间 +queueManager.addTask(task, nextExecuteTime) + ↓ +DelayQueue(按到期时间排序) + ↓ 到点,consumerThread 取出 +executorService(5 线程) + ↓ +ScheduleTaskExecutor.executeTask() + ↓ 发布事件 +ScheduledTaskExecuteEvent → 触发 Agent 执行 +``` + +### 重启恢复 + +```java +@PostConstruct +public void init() { + loadActiveTasksToQueue(); +} +``` + +`ScheduledTaskExecutionService` 启动时会把库里所有 `ACTIVE` 任务重新装载进队列, +**所以重启不会丢任务**。这一点比[验证码模块](user-auth.md#5-验证码机制)做得好。 + +--- + +## 6. 已知坑与注意事项 + +### 6.1 ⚠️ 多实例部署会重复执行 + +`DelayQueue` 是**进程内**的。每个后端实例启动时都会 +`loadActiveTasksToQueue()` 装载**全部** ACTIVE 任务。 + +**部署 N 个实例,每个定时任务就会被执行 N 次。** + +当前单实例部署没问题,但这是水平扩容的**硬阻塞点**—— +与[验证码的内存存储](user-auth.md#82-验证码无法多实例部署)并列。 + +要多实例必须先引入分布式锁或专门的调度中间件 +(数据库悲观锁、Redis 锁、或 XXL-JOB / Quartz 集群模式)。 + +### 6.2 执行线程池固定 5 个 + +```java +Executors.newFixedThreadPool(5, ...) +``` + +同一时刻最多 5 个定时任务并行。而定时任务往往会触发 Agent 执行 +(内含 LLM 调用,可能耗时数十秒)。 + +**如果同一时刻有大量任务到期,会排队**。在 1.2 CPU 的容器里, +5 个并发的 LLM 等待是合理的(都在等 IO 不占 CPU),但任务量增长后需要重新评估。 + +### 6.3 消费线程是 daemon + +```java +consumerThread.setDaemon(true); +``` + +daemon 线程不阻止 JVM 退出。配合 `@PreDestroy` 里的 `interrupt()`, +**关停时正在执行的任务可能被中断**。 + +任务本身需要保证幂等——被中断的任务重启后会重新装载执行。 + +### 6.4 两个 Task 同名易混淆 + +`domain/task` 与 `domain/scheduledtask` 下都有 "Task" 概念, +`TaskEntity` 与 `ScheduledTaskEntity`、`TaskDomainService` 与 +`ScheduledTaskDomainService` 长得很像。 + +阅读代码时先确认包名,别把工作流任务和定时任务搞混。 + +--- + +## 相关文档 + +- [系统架构](../architecture/overview.md) —— 整体分层 +- [对话模块](conversation.md) —— 工作流任务的驱动方 +- [Agent 模块](agent.md) —— 定时任务触发的执行主体 +- [用户认证模块](user-auth.md) —— 另一处内存存储的扩容限制 +- [性能优化](../operations/performance.md) —— 线程池配置 +- [数据库设计](../reference/database.md) —— 任务表结构 diff --git a/docs/modules/trace.md b/docs/modules/trace.md new file mode 100644 index 0000000..bc97d7e --- /dev/null +++ b/docs/modules/trace.md @@ -0,0 +1,189 @@ +# 执行追踪模块 + +> 💬 **一句话人话**:把 Agent 每次干活的过程录下来——哪一步、调了哪个模型、 +> 用了哪个工具、花了多少 Token、卡在哪儿失败的。出问题时不用猜,翻记录就行。 + +本模块共 29 个类,是平台的**可观测性基础**。 + +--- + +## 目录 + +- [1. 模块职责](#1-模块职责) +- [2. 两级数据模型](#2-两级数据模型) +- [3. 执行阶段与步骤类型](#3-执行阶段与步骤类型) +- [4. TraceCollector](#4-tracecollector) +- [5. 事件驱动的采集](#5-事件驱动的采集) +- [6. 统计与查询](#6-统计与查询) +- [7. 已知坑与注意事项](#7-已知坑与注意事项) + +--- + +## 1. 模块职责 + +| 职责 | 说明 | +| --- | --- | +| **执行记录** | 记录每次 Agent 执行的起止、成败、耗时 | +| **步骤明细** | 记录用户消息、AI 回复、工具调用等每一步 | +| **模型调用信息** | 模型 ID、输入输出 Token、延迟 | +| **工具调用信息** | 工具名、参数、结果 | +| **失败定位** | 记录失败发生在哪个阶段、错误详情 | +| **统计聚合** | 按 Agent / 会话维度出统计 | + +--- + +## 2. 两级数据模型 + +``` +AgentExecutionSummaryEntity 摘要:一次执行一条 +└── AgentExecutionDetailEntity 明细:一次执行 N 条(每步一条) + +配套值对象: +├── TraceContext 贯穿一次执行的上下文,串起 summary 与 detail +├── ModelCallInfo 模型调用信息 +└── ToolCallInfo 工具调用信息 +``` + +> **为什么分两级**:列表页只需要摘要(谁、什么时候、成没成、多久), +> 明细只在点进某一次执行时才加载。一次执行可能有几十上百步, +> 全部塞进一张表会让列表查询变慢。 + +--- + +## 3. 执行阶段与步骤类型 + +### ExecutionPhase — 执行阶段(失败定位用) + +```java +INITIALIZATION // 初始化 +ENVIRONMENT_PREPARATION // 环境准备 +BALANCE_CHECK // 余额检查 +MEMORY_INITIALIZATION // 记忆初始化 +MODEL_CALL // 模型调用 +TOOL_EXECUTION // 工具执行 +BILLING // 计费 +RESULT_PROCESSING // 结果处理 +``` + +这套阶段划分**就是一次 Agent 执行的骨架**。失败时记录 `errorPhase`, +排查时先看是哪个阶段挂的,直接缩小一个数量级的范围。 + +### ExecutionStepType — 步骤类型 + +```java +USER_MESSAGE // 用户消息 +AI_RESPONSE // AI 回复 +TOOL_CALL // 工具调用 +ERROR_MESSAGE // 错误消息 +``` + +--- + +## 4. TraceCollector + +`TraceCollector` 是采集入口,业务代码只跟它打交道。 + +| 方法 | 用途 | +| --- | --- | +| `getOrStartExecution(userId, sessionId, agentId, userMessage, ...)` | 开始一次执行,返回 `TraceContext` | +| `recordModelCall(ctx, aiResponse, modelCallInfo)` | 记录一次模型调用 | +| `recordToolCall(ctx, toolCallInfo)` | 记录一次工具调用 | +| `updateUserMessageTokens(ctx, inputTokens)` | 补记输入 Token | +| `recordSuccess(ctx)` | 标记成功 | +| `recordFailure(ctx, errorPhase, errorMessage)` | 标记失败(字符串) | +| `recordFailure(ctx, errorPhase, throwable)` | 标记失败(异常) | +| `recordErrorDetail(ctx, errorPhase, throwable)` | 记录错误明细 | +| `completeExecution(ctx, success, errorPhase, ...)` | 收尾 | + +方法名以 `getOrStart` 开头是有意的——**同一次执行可能被多次进入** +(如工作流的多个步骤),不能每次都新建一条记录。 + +--- + +## 5. 事件驱动的采集 + +追踪采集通过 Spring 事件解耦,业务代码发事件,追踪模块订阅。 + +``` +domain/trace/event/ +├── ExecutionStartedEvent 执行开始 +├── ExecutionCompletedEvent 执行结束 +├── ModelCalledEvent 模型被调用 +└── ToolExecutedEvent 工具被执行 + ↓ 发布 +ApplicationEventPublisher + ↓ 订阅 +application/trace/listener/TraceEventListener + ↓ +TraceCollector → AgentExecutionTraceDomainService → 落库 +``` + +> **好处**:业务链路不直接依赖追踪模块,追踪逻辑出问题不会拖垮主流程; +> 想关闭追踪只需停掉监听器。 +> +> **代价**:事件默认是**同步**的——监听器抛异常会影响发布方, +> 落库慢也会拖慢主链路。见[第 7 节](#7-已知坑与注意事项)。 + +--- + +## 6. 统计与查询 + +``` +AgentTraceListRequest 按 Agent 查执行列表 +SessionTraceListRequest 按会话查执行列表 +QueryExecutionHistoryRequest 查历史 + +AgentTraceStatisticsDTO Agent 维度统计 +SessionTraceStatisticsDTO 会话维度统计 +ExecutionStatisticsDTO 执行统计 +TraceDetailResponse 单次执行详情 +``` + +--- + +## 7. 已知坑与注意事项 + +### 7.1 Spring 事件默认同步 + +`@EventListener` 若不加 `@Async`,监听器与发布方**跑在同一个线程**。 + +这意味着追踪落库的耗时会**直接计入用户可感知的响应时间**, +监听器抛异常还可能中断主流程。 + +排查对话变慢时,别忘了这条链路也在其中。 + +### 7.2 ⚠️ 追踪与 ThreadLocal 的组合风险 + +`TraceCollector` 需要 `userId`。如果在异步线程里通过 +`UserContext.getCurrentUserId()` 取,**必得 `null`**—— +详见 [用户认证模块 8.1](user-auth.md#81-️-threadlocal-不跨线程传递)。 + +现有代码把 `userId` 作为参数显式传入 `getOrStartExecution()`,是正确做法。 +新增追踪点时保持这个约定。 + +### 7.3 明细表会持续增长 + +一次执行产生多条明细,且**没有看到自动清理机制**。 +长期运行后 `AgentExecutionDetailEntity` 对应的表会成为最大的表之一。 + +上线一段时间后需要考虑:分区、归档或 TTL 清理。 +可参照 [容器清理服务](mcp-tool.md#5-容器隔离)的做法补一个定时任务。 + +### 7.4 BALANCE_CHECK 阶段的存在说明计费在执行前 + +`ExecutionPhase` 里 `BALANCE_CHECK` 排在 `MODEL_CALL` 之前、`BILLING` 在之后—— +**先查余额、再调模型、最后扣费**。 + +如果调用成功但 `BILLING` 阶段失败,会出现「用了但没扣费」。 +对账时这是需要专门核查的场景。 + +--- + +## 相关文档 + +- [系统架构](../architecture/overview.md) —— 整体分层 +- [Agent 模块](agent.md) —— 被追踪的执行主体 +- [对话模块](conversation.md) —— 追踪点所在的主链路 +- [用户认证模块](user-auth.md) —— userId 的来源与 ThreadLocal 约束 +- [数据库设计](../reference/database.md) —— 追踪表结构 +- [性能优化](../operations/performance.md) —— 同步事件对延迟的影响 From 600147e7e2103e55a86325a0d790ca1dd982bd1c Mon Sep 17 00:00:00 2001 From: NEDONION Date: Wed, 12 Aug 2026 01:54:26 +0800 Subject: [PATCH 5/5] =?UTF-8?q?docs:=20=E8=A1=A5=E9=BD=90=E9=83=A8?= =?UTF-8?q?=E7=BD=B2/=E5=BC=80=E5=8F=91/=E6=80=A7=E8=83=BD/=E5=AE=89?= =?UTF-8?q?=E5=85=A8=E5=9B=9B=E7=AF=87=EF=BC=8C=E9=87=8D=E5=86=99=E7=B4=A2?= =?UTF-8?q?=E5=BC=95=E4=B8=8E=20README=20=E9=83=A8=E7=BD=B2=E7=AB=A0?= =?UTF-8?q?=E8=8A=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 新增: - operations/deployment.md:拓扑图、资源配额及其对线程池的影响、环境变量全清单、 CI/CD 流程、三个必须遵守的约束、版本追溯与回滚、国内网络实测数据、运维检查清单 - development/local-setup.md:环境要求、依赖服务、运行方式、Spotless 与中文注释约定、 提交与 CI、项目结构导航、五个常见问题 - operations/performance.md:1.2 CPU 如何改变代码行为、线程池现状盘点、超时分级、 首字延迟的四次串行外部调用、数据增长、水平扩容的两个阻塞点、排查手册 - operations/security.md:如实列出 P0/P1/P2 待修复问题、两条鉴权链路、多租户隔离、 不可信代码执行、密钥管理、CI/CD 安全边界、检查清单 重写 docs/README.md 索引:按角色给出阅读顺序,列全 19 篇文档, 并把「需要立刻知道的几件事」提到前面(含高危安全问题与三条编码约束)。 重写根 README 的部署说明:补运行拓扑图、首次部署、持续部署流程图、回滚步骤, 并说明为何服务器不需要开放任何入站端口。 修正两处事实错误: - 初始化 SQL 应使用 docs/sql/01_init.sql(1129 行,35 张表),而非 deploy/init-db.sql/init.sql(73 行,仅建扩展,建表语句已注释)。按后者操作 会得到一个没有任何表的库,且启动后才报错。README 与本地开发指南均已更正并加警示。 - README 中数据库初始化链接同步修正。 链接校验:167 条全部有效,0 失效(原 INDEX.md 有 7 条死链)。 文档总量 10215 行 / 19 篇。 Co-Authored-By: Claude Opus 5 --- README.md | 112 +++++++++++- docs/README.md | 180 ++++++++++--------- docs/development/local-setup.md | 285 +++++++++++++++++++++++++++++ docs/operations/deployment.md | 310 ++++++++++++++++++++++++++++++++ docs/operations/performance.md | 238 ++++++++++++++++++++++++ docs/operations/security.md | 268 +++++++++++++++++++++++++++ 6 files changed, 1306 insertions(+), 87 deletions(-) create mode 100644 docs/development/local-setup.md create mode 100644 docs/operations/deployment.md create mode 100644 docs/operations/performance.md create mode 100644 docs/operations/security.md diff --git a/README.md b/README.md index 707d923..7420cb6 100644 --- a/README.md +++ b/README.md @@ -160,9 +160,95 @@ Agent 结合已配置的模型服务、知识检索与 MCP 工具,当前对话 ## 部署说明 -仓库内的 Docker Compose 是作者部署参考,不是开箱即用的公共一键安装:它依赖外部配置的 PostgreSQL -和对象存储,前端镜像使用私有镜像仓库,也没有提供 MCP 网关配置。请将它作为部署起点,并按自己的基础设施 -补齐配置。 +> 仓库内的 Docker Compose 是**作者的部署参考,不是开箱即用的一键安装**:它依赖外部的 PostgreSQL +> 与对象存储,镜像来自私有仓库,MCP 网关也需自行配置。请把它当作起点,按自己的基础设施补齐。 + +### 运行拓扑 + +五个容器 + 一个外部数据库。nginx 按路径分流,`/api/` 走后端,其余走前端: + +``` + 公网 :80 + │ + ┌──────▼──────┐ + │ nginx │ + └──┬───────┬──┘ + /api/ │ │ / + ┌────────▼──┐ ┌──▼─────────┐ + │ backend │ │ frontend │ + │ :8088 │ │ :3000 │ + │Spring Boot│ │ Next.js │ + └──┬─────┬──┘ └────────────┘ + │ │ + ┌────────▼─┐ ┌─▼──────────────┐ + │ RabbitMQ │ │ 阿里云 RDS │ ← 不在 compose 内 + │ │ │ PG + PGVector │ + └──────────┘ └────────────────┘ + │ + ┌────────▼──────────┐ ┌──────────────┐ + │ docker.sock │ │ runner │ + │ (MCP 工具容器) │ │ 自托管 CI/CD │ + └───────────────────┘ └──────────────┘ +``` + +资源配额:backend 1.2 CPU / 1536M,frontend 0.6 / 768M,RabbitMQ 0.3 / 512M。 + +> ⚠️ backend 的 **1.2 CPU 会影响代码行为**——JVM 据此算出的 +> `ForkJoinPool.commonPool` 并行度只有 1。调整前请先读 +> [性能优化](docs/operations/performance.md#1-资源边界)。 + +### 首次部署 + +```bash +curl -fsSL https://get.docker.com | sh +git clone https://github.com/NEDONION/rag-agent-platform && cd rag-agent-platform +cp .env.example .env && vim .env # 填数据库、S3、模型服务商等 +docker login --username=<用户名> crpi-c6nc3ef4yktaqunc.cn-beijing.personal.cr.aliyuncs.com +sudo docker compose up -d +``` + +验证: + +```bash +curl -i http://localhost:8088/api/health +curl -i http://localhost:3000/api/health +``` + +### 持续部署 + +push 到 `main` 自动构建并上线,**无需登录服务器**: + +``` +push main + │ + ├─ build (GitHub 云端) 构建前后端镜像 → 推阿里云 ACR + ├─ config (GitHub 云端) 打包 compose 与 nginx 配置为 artifact + │ + └─ deploy (服务器 runner) ─┐ + ├─ 下载 artifact + ├─ 记录 IMAGE_TAG 到 .env + ├─ docker compose pull backend frontend + ├─ docker compose up -d backend frontend + ├─ docker compose restart nginx + └─ 健康检查(失败则 workflow 变红) +``` + +部署由服务器上的**自托管 runner 主动向 GitHub 轮询**领取,是纯出站连接。 +因此**服务器不需要开放 SSH 或任何入站端口**,GitHub Secrets 里也不存放服务器凭据 +(只需 `ACR_USERNAME`、`ACR_PASSWORD`、`DEPLOY_PATH` 三个)。 + +### 回滚 + +每次部署把 commit SHA 写进服务器 `.env`,镜像同时打了 `` 与 `latest` 两个标签: + +```bash +sed -i 's|^IMAGE_TAG=.*|IMAGE_TAG=<目标commit-sha>|' .env +docker compose up -d backend frontend +``` + +完整的环境变量清单、CI/CD 约束、国内网络实测数据与运维检查清单, +见 **[部署指南](docs/operations/deployment.md)**; +部署过程踩过的坑(含错误原文)见 **[排查记录](docs/operations/troubleshooting-log.md)**。 部署参考 @@ -175,14 +261,26 @@ Agent 结合已配置的模型服务、知识检索与 MCP 工具,当前对话 ## 文档 +完整文档见 **[文档中心](docs/README.md)**,按架构 / 模块 / 参考 / 运维 / 开发分类。常用入口: + | 文档 | 内容 | | --- | --- | -| [文档索引](docs/README.md) | 所有技术文档的入口 | -| [系统架构](docs/architecture/overview.md) | 技术栈、分层与数据流 | +| [系统架构](docs/architecture/overview.md) | 技术栈、DDD 分层与数据流 | +| [基础设施](docs/architecture/infrastructure.md) | MQ、存储、传输、加密等技术子包 | | [RAG 模块](docs/modules/rag.md) | 文档处理、向量检索与 RAG 链路 | | [Agent 模块](docs/modules/agent.md) | Agent 生命周期与工具集成 | -| [API 参考](docs/reference/api.md) | REST、WebSocket 与 SSE 接口 | -| [数据库初始化](docs/sql/01_init.sql) | PostgreSQL 与 PGVector 初始化脚本 | +| [对话模块](docs/modules/conversation.md) | 会话、消息、SSE 与上下文管理 | +| [MCP 工具模块](docs/modules/mcp-tool.md) | 工具上架状态机与容器隔离 | +| [LLM 模块](docs/modules/llm.md) | 服务商、协议适配与高可用 | +| [用户与认证](docs/modules/user-auth.md) | 登录、API Key 与用户设置 | +| [执行追踪](docs/modules/trace.md) | 可观测性与执行链路记录 | +| [任务与调度](docs/modules/task.md) | 工作流任务与定时任务 | +| [API 参考](docs/reference/api.md) | REST 与 SSE 接口 | +| [数据库设计](docs/reference/database.md) | 表结构、ER 图与索引 | +| [部署指南](docs/operations/deployment.md) | 拓扑、CI/CD 与回滚 | +| [本地开发](docs/development/local-setup.md) | 环境搭建与提交流程 | +| [排查记录](docs/operations/troubleshooting-log.md) | 线上问题实录,持续更新 | +| [数据库初始化](docs/sql/01_init.sql) | PostgreSQL 与 PGVector 完整建表脚本(35 张表) | ## 参与贡献 diff --git a/docs/README.md b/docs/README.md index 1869a1e..b895657 100644 --- a/docs/README.md +++ b/docs/README.md @@ -1,116 +1,136 @@ -# RAG Agent Platform - 技术文档索引 +# RAG Agent Platform 文档中心 -欢迎查阅 RAG Agent Platform 的技术文档。本文档索引将帮助您快速找到所需的技术资料。 +项目简介与快速上手先看[仓库根目录 README](../README.md)。这里是完整的技术文档。 ---- +> 📖 **怎么读**:每篇文档开头都有一段 **💬 一句话人话**,用大白话说清这篇讲什么。 +> 只想知道大概,看那一句就够;要动手改代码或排查问题,再往下看细节。 -## 📚 文档导航 +**最后更新**:2026-08-12 -### 核心文档 +--- -| 文档 | 描述 | 链接 | -|-----|------|------| -| **系统架构** | 整体架构设计、技术栈、DDD分层 | [ARCHITECTURE.md](architecture/overview.md) | -| **RAG 模块** | 文档处理、向量检索、版本化机制 | [RAG_MODULE.md](modules/rag.md) | -| **Agent 模块** | Agent 生命周期、工具集成、执行追踪 | [AGENT_MODULE.md](modules/agent.md) | -| **数据库设计** | 表结构设计、ER 图、索引优化 | [DATABASE.md](reference/database.md) | -| **API 接口** | RESTful API、WebSocket、SSE | [API.md](reference/api.md) | -| **部署指南** | 环境搭建、Docker 部署、生产配置 | [DEPLOYMENT.md](operations/deployment.md) | -| **开发指南** | 本地开发、代码规范、测试 | [DEVELOPMENT.md](development/local-setup.md) | +## 目录约定 -### 专题文档 +``` +docs/ +├── architecture/ 系统架构与技术底座 +├── modules/ 各业务模块(按领域划分) +├── reference/ API 与数据库参考 +├── operations/ 部署、性能、安全、排查记录 +├── development/ 本地开发 +└── assets/ 配图 +``` -| 文档 | 描述 | 链接 | -|-----|------|------| -| **LLM 模块** | 模型管理、高可用、Token 统计 | [LLM_MODULE.md](modules/llm.md) | -| **MCP 工具模块** | 工具导入、容器化、调用机制 | [MCP_MODULE.md](modules/mcp-tool.md) | -| **对话管理** | 会话管理、上下文窗口、Token 管控 | [CONVERSATION_MODULE.md](modules/conversation.md) | -| **性能优化** | 数据库优化、缓存策略、并发处理 | [PERFORMANCE.md](operations/performance.md) | -| **安全最佳实践** | 认证授权、数据加密、多租户隔离 | [SECURITY.md](operations/security.md) | +文件名用小写英文,**目录层级已表达分类,文件名不再重复** +(`modules/rag.md` 而非 `modules/RAG_MODULE.md`)。 --- -## 🚀 快速开始 +## 推荐阅读顺序 -### 我是新手,想快速了解系统 +### 我是新来的,想先搞懂这是什么 -1. **阅读系统架构** → [ARCHITECTURE.md](architecture/overview.md) - - 了解整体架构设计 - - 熟悉技术栈和分层结构 +1. [根目录 README](../README.md) —— 项目能做什么,5 分钟 +2. [系统架构](architecture/overview.md) —— DDD 分层、技术栈、模块边界 +3. [RAG 模块](modules/rag.md) —— 平台的核心能力 +4. [对话模块](modules/conversation.md) —— 用户每句话经过的完整链路 -2. **查看部署指南** → [DEPLOYMENT.md](operations/deployment.md) - - 快速搭建本地开发环境 - - 理解系统依赖和配置 +### 我要上手改代码 -3. **浏览 API 文档** → [API.md](reference/api.md) - - 了解核心 API 接口 - - 尝试调用示例 +5. [本地开发指南](development/local-setup.md) —— 环境搭建、代码风格、提交流程 +6. 对应模块的文档(见下方模块列表) +7. [API 参考](reference/api.md) 与[数据库设计](reference/database.md) +8. [排查记录](operations/troubleshooting-log.md) 的**附录:经验教训** —— + 12 条来自真实故障的约束,能避开不少坑 -### 我想深入了解某个模块 +### 我要部署或运维 -- **RAG 检索系统**: [RAG_MODULE.md](modules/rag.md) -- **Agent 智能体**: [AGENT_MODULE.md](modules/agent.md) -- **LLM 模型管理**: [LLM_MODULE.md](modules/llm.md) -- **MCP 工具集成**: [MCP_MODULE.md](modules/mcp-tool.md) +9. [部署指南](operations/deployment.md) —— 拓扑、环境变量、CI/CD、回滚 +10. [排查记录](operations/troubleshooting-log.md) —— 部署踩过的全部坑,含错误原文 +11. [性能优化](operations/performance.md) —— 排查手册 +12. [安全实践](operations/security.md) —— **含待修复的高危问题** -### 我想进行二次开发 +--- -1. **开发指南** → [DEVELOPMENT.md](development/local-setup.md) - - 本地开发环境搭建 - - 代码规范和提交规范 +## 全部文档 -2. **数据库设计** → [DATABASE.md](reference/database.md) - - 理解表结构设计 - - 学习数据模型 +### 架构 -3. **API 接口** → [API.md](reference/api.md) - - 了解接口设计规范 - - 扩展新的 API +| 文档 | 内容 | +| --- | --- | +| [系统架构](architecture/overview.md) | 整体设计、DDD 分层、技术栈 | +| [基础设施](architecture/infrastructure.md) | MQ、存储、传输、加密、拦截器等 19 个技术子包 | ---- +### 业务模块 + +| 文档 | 覆盖范围 | 类数量 | +| --- | --- | --- | +| [RAG 模块](modules/rag.md) | 文档处理、向量检索、版本快照 | 124 | +| [Agent 模块](modules/agent.md) | Agent 生命周期、发布、工具集成 | 75 | +| [对话模块](modules/conversation.md) | 会话、消息、SSE、上下文、工作流 | 61 | +| [MCP 工具模块](modules/mcp-tool.md) | 工具上架状态机、容器隔离、网关 | 64 | +| [LLM 模块](modules/llm.md) | 服务商、协议适配、高可用、Token | 61 | +| [用户与认证](modules/user-auth.md) | 登录、JWT、API Key、设置、额度 | 63 | +| [执行追踪](modules/trace.md) | 可观测性、执行链路记录 | 29 | +| [任务与调度](modules/task.md) | 工作流任务、定时任务 | 21 | -## 📖 文档说明 +### 参考 -### 文档结构 +| 文档 | 内容 | +| --- | --- | +| [API 参考](reference/api.md) | RESTful、SSE 接口定义 | +| [数据库设计](reference/database.md) | 表结构、ER 图、索引 | -每个文档通常包含以下章节: +### 运维 -1. **概述**: 模块功能简介 -2. **核心功能**: 详细功能列表 -3. **技术实现**: 关键代码和设计模式 -4. **数据库设计**: 相关表结构 -5. **API 接口**: 对外接口 -6. **配置说明**: 相关配置项 -7. **最佳实践**: 使用建议 -8. **故障排查**: 常见问题解决 +| 文档 | 内容 | +| --- | --- | +| [部署指南](operations/deployment.md) | 拓扑、配额、环境变量、CI/CD、回滚 | +| [排查记录](operations/troubleshooting-log.md) | 线上问题实录,持续更新 | +| [性能优化](operations/performance.md) | 线程池、超时、首字延迟、扩容阻塞点 | +| [安全实践](operations/security.md) | 认证、隔离、密钥、CI/CD 安全 | -### 阅读建议 +### 开发 -- **初学者**: 按照"快速开始"的顺序阅读 -- **开发者**: 重点阅读模块文档和 API 文档 -- **运维人员**: 重点阅读部署指南和性能优化 -- **架构师**: 重点阅读系统架构和数据库设计 +| 文档 | 内容 | +| --- | --- | +| [本地开发指南](development/local-setup.md) | 环境、运行、风格、提交、常见问题 | --- -## 🔗 相关资源 +## 需要立刻知道的几件事 -### 官方资源 +如果你只有三分钟,读这些: -- **GitHub 仓库**: [https://github.com/your-repo/rag-agent-platform](https://github.com/your-repo/rag-agent-platform) -- **在线演示**: [https://demo.agentx.ai](https://demo.agentx.ai) -- **API 文档**: [https://api.agentx.ai/docs](https://api.agentx.ai/docs) +**🔴 有一个待修复的高危安全问题** —— +服务商 API Key 的加密密钥硬编码在公开源码里,实际等同未加密。 +见[安全实践 P0](operations/security.md#-p0服务商-api-key-的加密密钥硬编码在公开仓库中)。 -### 技术栈文档 +**⚠️ 异步代码里取不到当前用户** —— +`UserContext` 基于 ThreadLocal,不跨线程。必须从控制器线程传参。 +见[用户认证 8.1](modules/user-auth.md#81-️-threadlocal-不跨线程传递)。 -- **Spring Boot**: [https://spring.io/projects/spring-boot](https://spring.io/projects/spring-boot) -- **Next.js**: [https://nextjs.org/docs](https://nextjs.org/docs) -- **LangChain4j**: [https://docs.langchain4j.dev](https://docs.langchain4j.dev) -- **PGVector**: [https://github.com/pgvector/pgvector](https://github.com/pgvector/pgvector) -- **RabbitMQ**: [https://www.rabbitmq.com/documentation.html](https://www.rabbitmq.com/documentation.html) +**⚠️ 不要用无参的 `CompletableFuture.runAsync`** —— +容器只有 1.2 CPU,`commonPool` 并行度为 1,阻塞 IO 会被串行化。 +见[性能优化第 2 节](operations/performance.md#2-线程池)。 + +**⚠️ `deploy.yml` 绝不能加 `pull_request` 触发** —— +公开仓库 + 自托管 runner,等于把服务器交给任何人。 +见[安全实践第 6 节](operations/security.md#6-cicd-安全)。 + +**⚠️ 当前只能单实例部署** —— +验证码和定时任务都用内存存储,多实例会出错。 +见[性能优化第 6 节](operations/performance.md#6-水平扩容的阻塞点)。 + +**⚠️ 仓库没有测试** —— +无 `src/test`,前端无 test 脚本。CI 只做编译校验。 + +--- -### 学习资源 +## 文档维护约定 -- **DDD 领域驱动设计**: [《领域驱动设计》- Eric Evans](https://www.domainlanguage.com/ddd/) -- **RAG 技术**: [LangChain RAG Tutorial](https://python.langchain.com/docs/tutorials/rag/) -- **向量数据库**: [Pinecone Learning Center](https://www.pinecone.io/learn/) \ No newline at end of file +- **每篇开头写「一句话人话」**,让不熟悉该模块的人也能快速判断要不要往下读 +- **记录「为什么」而非「是什么」** —— 代码能自述做了什么,文档要解释为什么这么做 +- **保留错误原文** —— 排查记录里的报错要原样保留,便于日后按报错检索 +- **踩坑要写进文档** —— 每个模块文档末尾都有「已知坑与注意事项」章节 +- **改文档要跑链接校验**,不留失效链接 diff --git a/docs/development/local-setup.md b/docs/development/local-setup.md new file mode 100644 index 0000000..4bdb617 --- /dev/null +++ b/docs/development/local-setup.md @@ -0,0 +1,285 @@ +# 本地开发指南 + +> 💬 **一句话人话**:怎么把项目在自己电脑上跑起来、改代码、提交。 +> 不需要装真实的数据库和消息队列——用 Docker 起依赖,只在本地跑前后端就行。 + +--- + +## 目录 + +- [1. 环境要求](#1-环境要求) +- [2. 启动依赖服务](#2-启动依赖服务) +- [3. 运行后端](#3-运行后端) +- [4. 运行前端](#4-运行前端) +- [5. 代码风格](#5-代码风格) +- [6. 提交与 CI](#6-提交与-ci) +- [7. 项目结构导航](#7-项目结构导航) +- [8. 常见问题](#8-常见问题) + +--- + +## 1. 环境要求 + +| 组件 | 版本 | 说明 | +| --- | --- | --- | +| **JDK** | 17 | `pom.xml` 中 `maven.compiler.source/target` 均为 17 | +| **Maven** | 3.9+ | | +| **Node.js** | 20 | 与 `frontend/Dockerfile` 一致 | +| **pnpm** | 9 | 仓库有 `pnpm-lock.yaml` | +| **Docker** | 任意近期版本 | 起依赖服务用 | + +技术栈:Spring Boot 3.2.3 + LangChain4j / Next.js 15 / PostgreSQL + PGVector。 + +--- + +## 2. 启动依赖服务 + +数据库和消息队列用 Docker 起,不必装在本机。 + +```bash +# PostgreSQL + PGVector +docker run -d --name rag-pg \ + -e POSTGRES_PASSWORD=postgres \ + -e POSTGRES_DB=agentx \ + -p 5432:5432 \ + pgvector/pgvector:pg16 + +# RabbitMQ +docker run -d --name rag-mq \ + -p 5672:5672 -p 15672:15672 \ + rabbitmq:3.13-management-alpine +``` + +初始化表结构: + +```bash +docker exec -i rag-pg psql -U postgres -d agentx < docs/sql/01_init.sql +``` + +> ⚠️ **别用错文件**。仓库里有两个初始化 SQL: +> +> | 文件 | 行数 | 内容 | +> | --- | --- | --- | +> | `docs/sql/01_init.sql` | 1129 | **完整表结构(35 张表)+ 扩展**,本地开发用这个 | +> | `deploy/init-db.sql/init.sql` | 73 | 仅创建 `vector` / `uuid-ossp` 扩展,建表语句是注释掉的 | +> +> 跑成后者的话扩展装上了但一张表也没有,启动时会报表不存在。 + +--- + +## 3. 运行后端 + +```bash +cp src/main/resources/application.yml.example src/main/resources/application.yml +# 编辑 application.yml,填数据库、MQ、模型服务商等配置 + +mvn spring-boot:run +``` + +默认端口 `8088`,健康检查 `http://localhost:8088/api/health`。 + +### 只编译不跑 + +```bash +mvn -B compile -DskipTests +``` + +> ⚠️ **当前仓库没有测试代码**(无 `src/test` 目录,前端 `package.json` 也没有 test 脚本)。 +> CI 里的「后端编译」只做编译校验,不跑测试。新增功能时补测试是当前最值得做的改进之一。 + +--- + +## 4. 运行前端 + +```bash +cd frontend +pnpm install +pnpm dev # next dev --turbopack +``` + +默认端口 `3000`。 + +### API 地址配置 + +`frontend/lib/api-config.ts` 的解析顺序: + +``` +NEXT_PUBLIC_API_URL 环境变量 + ↓ 未设置 +浏览器环境 → 相对路径 "/api" + ↓ +服务端渲染 → "/api" +``` + +**本地开发**若前后端分离启动,需要设置: + +```bash +# frontend/.env.local +NEXT_PUBLIC_API_URL=http://localhost:8088/api +``` + +**生产环境**不设置该变量,走相对路径 `/api` 由 nginx 转发—— +这样镜像不必为不同域名重新构建。 + +--- + +## 5. 代码风格 + +后端用 **Spotless**(`pom.xml` 中 `spotless-maven-plugin` 2.37.0): + +```bash +mvn spotless:check # 检查 +mvn spotless:apply # 自动格式化 +``` + +前端: + +```bash +cd frontend && pnpm lint +``` + +### 注释约定 + +参照现有代码,**业务逻辑注释用中文**,尤其是: + +- 「为什么这么写」而非「这里做了什么」 +- 踩过的坑、绕过的限制、不能改的原因 + +例如: + +```java +/** 流式问答专用线程池。 + * + * 这里不能用 CompletableFuture.runAsync 的默认 ForkJoinPool.commonPool(): + * commonPool 的并行度是 availableProcessors() - 1,而后端容器限了 1.2 CPU, + * 算下来并行度为 1;链路里跑的又是纯阻塞 IO,结果就是全站同一时刻只能有 + * 一个 RAG 问答在跑。 */ +``` + +这种注释在半年后能省下大量重新排查的时间。 + +--- + +## 6. 提交与 CI + +### 分支与 PR + +日常改动走**分支 + PR**,不直接推 main: + +```bash +git checkout -b fix/xxx +# 改动 +git commit +git push -u origin fix/xxx +gh pr create +``` + +**push 到 main 会自动触发生产部署**,见[部署指南](../operations/deployment.md#5-cicd-自动部署)。 + +### PR 会跑什么 + +`.github/workflows/ci.yml` 三个并行 job: + +| Job | 命令 | +| --- | --- | +| 后端编译 | `mvn -B compile -DskipTests` | +| 前端构建 | `pnpm build` | +| nginx 配置校验 | `nginx -t`(带 `--add-host`) | + +> nginx 那个 job 需要 `--add-host backend:127.0.0.1`, +> 因为 `nginx -t` 会解析 upstream 主机名,CI 里没有那些容器。 +> 见[排查记录 3.2](../operations/troubleshooting-log.md#32-ci-的-nginx-校验失败)。 + +--- + +## 7. 项目结构导航 + +``` +src/main/java/org/lucas/ +├── interfaces/ 控制器与 DTO —— 从这里找接口 +│ ├── api/portal/ 面向用户的接口 +│ └── dto/ +├── application/ 应用服务 —— 编排逻辑在这 +├── domain/ 领域模型与领域服务 —— 业务规则在这 +└── infrastructure/ 技术实现 —— 数据库、MQ、外部集成 + +frontend/ +├── app/ Next.js App Router 页面 +│ └── (main)/explore/ 主功能页 +├── components/ +├── lib/ API 客户端、服务封装 +└── types/ +``` + +**按模块找代码**:每个业务模块在 `application` / `domain` / `infrastructure` +三层下都有同名目录。例如 RAG 相关代码分布在 +`application/rag/`、`domain/rag/`、`infrastructure/rag/`。 + +各模块详细说明见 [文档索引](../README.md)。 + +--- + +## 8. 常见问题 + +### 8.1 改了 nginx.conf 不生效 + +`nginx.conf` 是挂载进容器的,必须重启容器: + +```bash +docker compose restart nginx +``` + +`up -d` 不会重载它。 + +### 8.2 异步代码里取不到当前用户 + +`UserContext` 基于 ThreadLocal,**不跨线程传递**。必须在控制器线程取出 +userId 再作为参数传入异步任务: + +```java +// ✅ +String userId = UserContext.getCurrentUserId(); +CompletableFuture.runAsync(() -> doWork(userId), executor); + +// ❌ 必得 null +CompletableFuture.runAsync(() -> { + String userId = UserContext.getCurrentUserId(); +}); +``` + +详见[用户认证模块 8.1](../modules/user-auth.md#81-️-threadlocal-不跨线程传递)。 + +### 8.3 新增异步逻辑该用哪个线程池 + +**不要用 `CompletableFuture.runAsync(task)` 的无参重载**—— +它跑在 `ForkJoinPool.commonPool()` 上,在 CPU 受限的容器里并行度可能只有 1, +且不会为阻塞 IO 做线程补偿。 + +一律显式传入专用线程池。参考 `RagQaDatasetAppService.ragStreamExecutor` +或 `ToolStateStateMachineAppService` 的做法。 + +### 8.4 import 时选错了同名类 + +`conversation` 模块里有**三个 `Agent`、两个 `ChatContext`、两个 `AgentPromptTemplates`**。 +IDE 自动 import 容易选错,且编译能过、运行才出错。 + +详见[对话模块 9.2](../modules/conversation.md#92-存在多个同名类容易-import-错)。 + +### 8.5 本地起不来但线上正常 + +优先检查 `application.yml` 与线上 `.env` 的差异,特别是: + +- 数据库连接(本地是 Docker,线上是阿里云 RDS) +- `SILICONFLOW_API_KEY` 是否配置 +- MQ 地址(本地 `localhost`,容器内是 `rabbitmq`) + +--- + +## 相关文档 + +- [文档索引](../README.md) —— 全部文档入口 +- [系统架构](../architecture/overview.md) —— 分层与依赖方向 +- [部署指南](../operations/deployment.md) —— 生产部署 +- [排查记录](../operations/troubleshooting-log.md) —— 已知问题与解法 +- [API 参考](../reference/api.md) —— 接口定义 +- [数据库设计](../reference/database.md) —— 表结构 diff --git a/docs/operations/deployment.md b/docs/operations/deployment.md new file mode 100644 index 0000000..698dccb --- /dev/null +++ b/docs/operations/deployment.md @@ -0,0 +1,310 @@ +# 部署指南 + +> 💬 **一句话人话**:这篇讲怎么把项目跑到一台服务器上,以及怎么让它以后自动更新。 +> 想快速上手看[根目录 DEPLOY.md](../../DEPLOY.md);想搞明白每个部件为什么这么配,看这篇。 + +--- + +## 目录 + +- [1. 部署拓扑](#1-部署拓扑) +- [2. 资源配额](#2-资源配额) +- [3. 环境变量](#3-环境变量) +- [4. 首次部署](#4-首次部署) +- [5. CI/CD 自动部署](#5-cicd-自动部署) +- [6. 版本追溯与回滚](#6-版本追溯与回滚) +- [7. 国内网络注意事项](#7-国内网络注意事项) +- [8. 运维检查清单](#8-运维检查清单) + +--- + +## 1. 部署拓扑 + +``` + 公网 + │ + :80│ + ┌───────▼────────┐ + │ nginx │ 反向代理 + └───┬────────┬───┘ + /api/ │ │ / + ┌─────────▼──┐ ┌──▼──────────┐ + │ backend │ │ frontend │ + │ :8088 │ │ :3000 │ + │ Spring Boot│ │ Next.js │ + └──┬──────┬──┘ └─────────────┘ + │ │ + ┌──────▼──┐ ┌─▼──────────┐ + │RabbitMQ │ │ 阿里云 RDS │ PostgreSQL + PGVector + └─────────┘ └────────────┘ (外部托管) + │ + ┌──────▼──────────┐ + │ /var/run/ │ 用于 MCP 工具容器管理 + │ docker.sock │ + └─────────────────┘ + + ┌─────────────────┐ + │ runner │ GitHub Actions 自托管 runner + └─────────────────┘ (出站连接,不开入站端口) +``` + +**数据库不在 compose 里** —— 用的是外部阿里云 RDS, +`DB_HOST` / `VECTOR_DB_HOST` 指向同一个实例。 + +--- + +## 2. 资源配额 + +`docker-compose.yml` 中的限制: + +| 服务 | 内存 | CPU | 说明 | +| --- | --- | --- | --- | +| backend | 1536M | 1.2 | JVM 堆 `-Xmx768m` | +| frontend | 768M | 0.6 | | +| rabbitmq | 512M | 0.3 | | +| runner | 512M | 0.3 | 只做拉取重启 | +| nginx | 未限制 | 未限制 | | + +> ⚠️ **backend 的 1.2 CPU 是个有历史的数字**。JVM 在 CPU 配额 1.2 下 +> `Runtime.availableProcessors()` 返回 2,导致 `ForkJoinPool.commonPool()` +> 并行度只有 1。RAG 流式问答曾因此被串行化, +> 见[排查记录 1.4](troubleshooting-log.md#14-根因三并发被串行化)。 +> +> 现在业务代码已改用专用线程池,但**调整这个数字前请先理解它对线程池的影响**。 + +--- + +## 3. 环境变量 + +`.env` 位于服务器上的项目目录,**不在仓库中**,CI 也不会覆盖它。 + +### 必填 + +```bash +# 数据库(阿里云 RDS) +DB_HOST= DB_PORT= DB_USER= DB_PASSWORD= DB_NAME= + +# 消息队列 +RABBITMQ_PASSWORD= + +# 对象存储 +S3_SECRET_ID= S3_SECRET_KEY= S3_BUCKET_NAME= S3_ENDPOINT= S3_DOMAIN= + +# 模型服务 +SILICONFLOW_API_KEY= + +# 邮件 +MAIL_SMTP_USERNAME= MAIL_SMTP_PASSWORD= +``` + +### 超时配置 + +```bash +LLM_REQUEST_TIMEOUT_SECONDS=60 # 非流式:意图识别/改写/扩展 +LLM_STREAM_TIMEOUT_SECONDS=300 # 流式对话 +``` + +> 这两项来自[线上卡死问题的修复](troubleshooting-log.md#12-根因一llm-客户端超时是-1-小时)。 +> 原代码写死 1 小时,等同于没有超时。**调大之前先想清楚: +> 超时越长,故障暴露越晚。** + +### CI/CD 相关(仅部署机需要) + +```bash +DEPLOY_PATH=/home/admin/rag-agent-platform +GITHUB_PAT= # 细粒度 PAT,仅需 Administration: Read and write +GITHUB_REPO=NEDONION/rag-agent-platform +RUNNER_NAME=aliyun-lightweight +IMAGE_TAG= # 由 CI 自动写入,记录当前线上 commit +``` + +### 国内加速(可选) + +```bash +APT_MIRROR=http://mirrors.aliyun.com +RUNNER_DOWNLOAD_BASE=https://ghfast.top/https://github.com/actions/runner/releases/download +``` + +--- + +## 4. 首次部署 + +```bash +# 1. 安装 Docker +curl -fsSL https://get.docker.com | sh + +# 2. 获取代码 +git clone https://github.com/NEDONION/rag-agent-platform +cd rag-agent-platform + +# 3. 配置环境变量 +cp .env.example .env && vim .env + +# 4. 登录镜像仓库 +docker login --username=<用户名> crpi-c6nc3ef4yktaqunc.cn-beijing.personal.cr.aliyuncs.com + +# 5. 启动 +sudo docker compose up -d + +# 6. 验证 +curl -i http://localhost:8088/api/health +curl -i http://localhost:3000/api/health +``` + +--- + +## 5. CI/CD 自动部署 + +push 到 `main` 后自动完成构建与部署。 + +``` +GitHub 阿里云服务器 +────── ─────────── +push main + │ + ├─ build (ubuntu-latest) + │ 构建前后端镜像 → 推 ACR + │ + ├─ config (ubuntu-latest) + │ 打包 docker-compose.yml + │ 与 deploy/nginx.conf 为 artifact + │ + └─ deploy (self-hosted) ───────→ runner 主动轮询领取 + │ + ├─ 下载 artifact + ├─ 写入 IMAGE_TAG 到 .env + ├─ docker compose pull backend frontend + ├─ docker compose up -d backend frontend + ├─ docker compose restart nginx + └─ 健康检查 +``` + +### 为什么不用 SSH + +自托管 runner 主动向 GitHub 轮询(纯出站),因此: + +- 服务器**不需要开放 SSH 或任何入站端口** +- GitHub Secrets 里**不存放任何服务器凭据**(只需 3 个:`ACR_USERNAME`、`ACR_PASSWORD`、`DEPLOY_PATH`) + +详细对比见[排查记录第二部分](troubleshooting-log.md#21-为什么不用-ssh)。 + +### 三个必须遵守的约束 + +**① 部署命令必须显式指定服务名** + +```bash +# ❌ 会把 runner 自己也重建,杀掉正在执行的部署任务 +docker compose up -d + +# ✅ +docker compose up -d backend frontend +docker compose restart nginx +``` + +**② `deploy.yml` 绝不能加 `pull_request` 触发** + +这是公开仓库。一旦 fork 的 PR 能触发工作流,任何人都能在服务器上执行任意代码。 + +**③ 部署 job 不能用 `actions/checkout`** + +它会在服务器上执行 `git clone github.com`,而国内服务器到 `github.com:443` +极不稳定。改用 artifact 传递配置文件, +见[排查记录 3.10](troubleshooting-log.md#310-部署-job-在服务器上-checkout-失败)。 + +--- + +## 6. 版本追溯与回滚 + +每次部署会把 commit SHA 写进服务器 `.env` 的 `IMAGE_TAG`: + +```yaml +image: .../rag-agent-platform:backend-${IMAGE_TAG:-latest} +``` + +**查当前线上版本**: + +```bash +grep IMAGE_TAG /home/admin/rag-agent-platform/.env +``` + +**回滚到任意历史版本**: + +```bash +cd /home/admin/rag-agent-platform +sed -i 's|^IMAGE_TAG=.*|IMAGE_TAG=<目标commit-sha>|' .env +docker compose up -d backend frontend +``` + +镜像同时打了 `` 和 `latest` 两个标签,所以任何历史 commit 都能回滚。 + +--- + +## 7. 国内网络注意事项 + +实测数据(2026-08,阿里云北京轻量): + +| 目标 | 状态 | +| --- | --- | +| Docker Hub | ✅ 通畅 | +| `download.docker.com` | ❌ 连接重置 | +| `github.com:443`(git) | ❌ 超时 | +| GitHub Release 直连 | ⚠️ 25 KB/s | +| `ghfast.top` 加速 | ✅ 1.2 MB/s | +| ubuntu 官方源 | ⚠️ 352 KB/s | +| `mirrors.aliyun.com` | ✅ 秒级 | +| `*.actions.githubusercontent.com` | ✅ 通畅(runner 靠它工作) | + +**关键经验**:不要笼统地说「网络不行」,要精确到**哪个域名不行**—— +Docker Hub 通而 download.docker.com 不通这个区分,直接决定了修复方向。 + +> ⚠️ **换源的边界**:系统包和语言包可以换镜像站(有 GPG 签名/校验和保护)。 +> **可执行二进制**(如 actions-runner)只有在**强制校验官方 SHA256** 的前提下 +> 才可以走镜像站,否则宁可慢也要走官方源。 + +--- + +## 8. 运维检查清单 + +### 日常 + +```bash +# 服务状态 +sudo docker compose ps + +# 后端日志 +sudo docker compose logs -f --tail=100 backend + +# runner 状态 +sudo docker compose logs --tail=30 runner + +# 磁盘 +df -h && docker system df +``` + +### 部署后必查 + +- [ ] `curl http:///api/health` 返回 200(走 nginx,同时验证代理配置) +- [ ] `curl http:///` 返回 200 +- [ ] `.env` 中 `IMAGE_TAG` 与预期 commit 一致 +- [ ] 日志中无 LLM 超时异常激增 + +### 升级 runner 版本时 + +`deploy/runner/Dockerfile` 中的 `RUNNER_VERSION` 与 `RUNNER_SHA256` +**必须同步更新**,否则校验必然失败。哈希取法: + +```bash +curl -s https://api.github.com/repos/actions/runner/releases/tags/v<版本> \ + | grep -o 'BEGIN SHA linux-x64 -->[0-9a-f]*' +``` + +--- + +## 相关文档 + +- [根目录 DEPLOY.md](../../DEPLOY.md) —— 快速上手版 +- [排查记录](troubleshooting-log.md) —— 部署过程踩过的全部坑 +- [性能优化](performance.md) —— 资源配额与线程池 +- [安全实践](security.md) —— 部署相关的安全边界 +- [本地开发](../development/local-setup.md) —— 开发环境搭建 +- [系统架构](../architecture/overview.md) —— 各服务的职责 diff --git a/docs/operations/performance.md b/docs/operations/performance.md new file mode 100644 index 0000000..778d56e --- /dev/null +++ b/docs/operations/performance.md @@ -0,0 +1,238 @@ +# 性能优化 + +> 💬 **一句话人话**:这篇记录「哪里会慢、为什么慢、怎么办」。 +> 内容全部来自线上真实遇到的问题,不是通用性能建议清单。 + +--- + +## 目录 + +- [1. 资源边界](#1-资源边界) +- [2. 线程池](#2-线程池) +- [3. 超时分级](#3-超时分级) +- [4. 首字延迟](#4-首字延迟) +- [5. 数据增长](#5-数据增长) +- [6. 水平扩容的阻塞点](#6-水平扩容的阻塞点) +- [7. 排查手册](#7-排查手册) + +--- + +## 1. 资源边界 + +后端容器:**1.2 CPU / 1536MB 内存 / JVM 堆 768MB**。 + +这个配额小到会**改变代码行为**,不只是「跑得慢一点」: + +``` +cpus: "1.2" + ↓ +Runtime.availableProcessors() = 2 (JVM 按 CPU 配额向上取整) + ↓ +ForkJoinPool.commonPool 并行度 = 2 - 1 = 1 +``` + +**任何跑在 commonPool 上的阻塞任务都会被串行化。** +这是理解本项目性能问题的前提。 + +--- + +## 2. 线程池 + +### 现状盘点 + +| 位置 | 线程池 | 评价 | +| --- | --- | --- | +| `RagQaDatasetAppService` | `newFixedThreadPool(8)` | ✅ 已修复 | +| `PortalAgentSessionController` | `newCachedThreadPool()` | ⚠️ 无界 | +| `ToolStateStateMachineAppService` | 专用池 | ✅ | +| `DelayedTaskQueueManager` | `newFixedThreadPool(5)` | ✅ | + +### 已修复:RAG 问答串行化 + +原代码: + +```java +CompletableFuture.runAsync(() -> { + processRagStreamChat(request, userId, emitter); +}); // ← 无 executor,落到 commonPool +``` + +commonPool 并行度为 1,而链路里跑的是**纯阻塞 IO**(HTTP 调 LLM)。 +ForkJoinPool 只对 `ManagedBlocker` 做线程补偿,普通阻塞调用不补偿。 + +**结果:全站同一时刻只能有一个 RAG 问答在跑。** + +修复为专用线程池: + +```java +private final ExecutorService ragStreamExecutor = Executors.newFixedThreadPool(8, runnable -> { + Thread thread = new Thread(runnable, "rag-stream-chat"); + thread.setDaemon(true); + return thread; +}); +``` + +> **验证方式**:这个问题**单人测试复现不出来**。 +> 必须两个人同时发起 RAG 问答,观察是否互相排队。 + +### 待改进:无界线程池 + +```java +// PortalAgentSessionController +private final ExecutorService executorService = Executors.newCachedThreadPool(); +``` + +`newCachedThreadPool()` 没有上限。并发暴涨时会持续创建线程, +在 1.5GB 内存的容器里可能先 OOM 而非降级。 + +建议改为有界线程池 + 明确的拒绝策略(快速失败并返回「系统繁忙」, +好过 OOM 拖垮整个进程)。 + +### 规则 + +> **凡是阻塞 IO,一律显式传入专用线程池。** +> 永远不要用 `CompletableFuture.runAsync(task)` 的无参重载。 + +--- + +## 3. 超时分级 + +超时值必须**按调用性质分级**,一刀切必然一头过松一头过紧。 + +| 场景 | 变量 | 默认 | 理由 | +| --- | --- | --- | --- | +| 非流式 LLM | `LLM_REQUEST_TIMEOUT_SECONDS` | 60s | 小请求,且卡在 SSE 事件之间 | +| 流式 LLM | `LLM_STREAM_TIMEOUT_SECONDS` | 300s | 允许长,但不能形同虚设 | +| RAG SSE 连接 | 代码常量 | 10min | 兜底 | +| Agent SSE 连接 | `CONNECTION_TIMEOUT` | 10min | 兜底 | +| nginx 回源 | `proxy_read_timeout` | 3600s | 不能早于应用层切断 | + +### 历史教训 + +原代码四处写死 `Duration.ofHours(1)`。**1 小时等同于没有超时**—— +上游挂起时请求就挂满一小时,不报错、不降级,用户只看到页面一直转圈。 + +> **上线后日志里开始出现 LLM 超时异常是好事**, +> 说明上游确实会挂,只是以前被掩盖成了「卡死」。 + +--- + +## 4. 首字延迟 + +用户从提问到看见第一个字,中间的阻塞步骤: + +``` +用户提问 + ↓ +选路(高可用网关) 一次外部 HTTP + ↓ +意图识别 classifyIntent 一次 LLM 调用 ← 阻塞 + ↓ +相关性判断 向量检索 + ↓ +语义改写 rewriteQuestion 一次 LLM 调用 ← 阻塞 + ↓ +查询扩展 expandQueries 一次 LLM 调用 ← 阻塞 + ↓ +向量检索 + rerank 外部 rerank API + ↓ +流式回答开始 ← 用户终于看到第一个字 +``` + +**最多 4 次串行的外部调用发生在首字之前。** + +### 已做的改进 + +在每步之间补发 SSE 进度事件(「正在理解问题意图…」「正在改写检索查询…」), +用户至少知道进行到哪一步了。 + +### 可继续优化的方向 + +| 优化 | 收益 | 代价 | +| --- | --- | --- | +| `expandQueries` 改为可开关 | 省一次 LLM 调用 | 召回率可能下降 | +| 意图识别与相关性判断并行 | 省一次串行等待 | 复杂度上升 | +| 意图识别换更小更快的模型 | 显著 | 需要额外模型配置 | +| 缓存高频问题的改写结果 | 显著 | 需要缓存层 | + +> 这几步本身都有 try/catch 降级到原问题,**失败是安全的**。 +> 所以「超时后跳过」是可接受的策略——这也是把非流式超时设成 60s 的底气。 + +--- + +## 5. 数据增长 + +| 数据 | 增长模式 | 现状 | +| --- | --- | --- | +| 执行追踪明细 | 一次执行 N 条 | ⚠️ **未见清理机制** | +| 消息记录 | 一次对话 N 条 | 只增不删 | +| 向量数据 | 随知识库线性增长 | PGVector | +| 容器 | 有清理 | ✅ `ContainerCleanupService` | +| 镜像 | CI 每次 `docker image prune -f` | ✅ | + +**执行追踪明细表**会成为增长最快的表之一。上线一段时间后需要考虑 +分区、归档或 TTL 清理。可参照 `ContainerCleanupService` 的做法补定时任务。 + +--- + +## 6. 水平扩容的阻塞点 + +当前**只能单实例部署**,有两个硬阻塞: + +| 阻塞点 | 位置 | 多实例后果 | +| --- | --- | --- | +| 验证码内存存储 | `MemoryCodeStorage` | A 实例发的码,B 实例校验失败 | +| 定时任务内存队列 | `DelayedTaskQueueManager` | 每个实例都装载全部任务,**重复执行 N 次** | + +两处都已有抽象接口,改造路径清晰: + +- 验证码 → 补一个 Redis 版 `CodeStorage` 实现 +- 定时任务 → 引入分布式锁,或换 XXL-JOB / Quartz 集群模式 + +--- + +## 7. 排查手册 + +### 「页面一直转圈」 + +1. 看后端日志有无该请求的 LLM 超时异常 +2. 看卡在哪个 SSE 事件——最后一条事件之后的那个步骤就是嫌疑 +3. 确认 nginx `/api/` 段有 `proxy_http_version 1.1`(缺失会导致 SSE 传不动) + +### 「有时候快有时候慢」 + +优先怀疑**并发排队**,而非模型本身慢: + +```bash +# 看线程状态,找大量 WAITING 在同一个池上的线程 +docker compose exec backend jstack 1 | grep -A5 "rag-stream-chat" +``` + +### 「延迟是个整数」 + +10 分钟、60 秒这种整数级延迟**几乎必然是某个超时或重试到点了**, +不是模型慢。直接去查超时配置。 + +### 常用命令 + +```bash +# 容器资源占用 +docker stats --no-stream + +# 后端堆内存 +docker compose exec backend jcmd 1 GC.heap_info + +# 慢请求日志 +docker compose logs backend | grep -E "耗时|timeout|TimeoutException" +``` + +--- + +## 相关文档 + +- [排查记录](troubleshooting-log.md) —— 完整的问题现场与推理过程 +- [部署指南](deployment.md) —— 资源配额与环境变量 +- [对话模块](../modules/conversation.md) —— 主链路 +- [RAG 模块](../modules/rag.md) —— 检索链路 +- [LLM 模块](../modules/llm.md) —— 超时与降级 +- [执行追踪模块](../modules/trace.md) —— 数据增长与同步事件开销 diff --git a/docs/operations/security.md b/docs/operations/security.md new file mode 100644 index 0000000..c16112b --- /dev/null +++ b/docs/operations/security.md @@ -0,0 +1,268 @@ +# 安全实践 + +> 💬 **一句话人话**:这篇如实记录当前系统的安全现状——包括**已知的严重问题**。 +> 它不是「我们很安全」的宣传页,而是一份待办清单。 + +**最后审查**:2026-08-12 + +--- + +## 目录 + +- [1. 待修复问题](#1-待修复问题) +- [2. 认证与授权](#2-认证与授权) +- [3. 多租户隔离](#3-多租户隔离) +- [4. 不可信代码执行](#4-不可信代码执行) +- [5. 密钥管理](#5-密钥管理) +- [6. CI/CD 安全](#6-cicd-安全) +- [7. 安全检查清单](#7-安全检查清单) + +--- + +## 1. 待修复问题 + +### 🔴 P0:服务商 API Key 的加密密钥硬编码在公开仓库中 + +**影响**:任何拿到数据库内容的人都能解出**全部用户的模型服务商 API Key**。 + +`ProviderConfigConverter` 把含 API Key 的 `ProviderConfig` 加密后落库, +调用的是 `ValidationUtils.EncryptUtils`: + +```java +private static final String ALGORITHM = "AES"; +private static final String SECRET_KEY = "1234567890123456"; // ← 硬编码 +... +Cipher cipher = Cipher.getInstance(ALGORITHM); // ← 默认 AES/ECB/PKCS5Padding +``` + +三重问题: + +| 问题 | 后果 | +| --- | --- | +| 密钥硬编码 | 无法轮换 | +| **仓库公开** | 密钥等同公开信息,**加密形同虚设** | +| ECB 模式 | 相同明文产生相同密文,可做模式分析 | + +**泄露途径**:数据库备份、SQL 注入、云厂商快照、运维越权——任一发生即全量泄露。 + +**修复方向**: + +1. 密钥从环境变量注入(如 `CONFIG_ENCRYPTION_KEY`),缺失时**启动失败**而非回落默认值 +2. 改用 `AES/GCM/NoPadding`,每条记录随机 IV,IV 与密文一同存储 +3. 提供一次性迁移:旧密钥解密 → 新密钥重新加密 +4. 修复后**所有存量服务商密钥应视为已泄露,需通知用户轮换** + +详见 [基础设施 9.1](../architecture/infrastructure.md#91--服务商-api-key-的加密密钥硬编码在公开仓库中)。 + +### 🟠 P1:docker.sock 挂载等同于宿主机 root + +```yaml +volumes: + - /var/run/docker.sock:/var/run/docker.sock +``` + +后端需要它来管理 MCP 工具容器,但这意味着**后端进程一旦被攻破,宿主机即失守**。 + +任何关于「工具沙箱有多安全」的讨论,上限都是这一条。 + +**缓解方向**:Docker API over TLS(限制可用命令)、rootless 模式、 +或把容器管理拆成独立的最小权限服务。 + +### 🟡 P2:无自动化测试 + +无 `src/test` 目录,前端无 test 脚本。**任何安全修复都缺乏回归保护**。 + +### 🟡 P2:死代码中的第二套硬编码密钥 + +`ConfigEncryptor` 全仓库零调用,但硬编码了 `"AgentX-Config-Key"`。 +风险在于将来有人误以为它是「正确的那个」而启用。建议直接删除。 + +--- + +## 2. 认证与授权 + +### 两条独立链路 + +| 链路 | 凭据 | 拦截器 | 上下文 | +| --- | --- | --- | --- | +| 网页用户 | JWT | `UserAuthInterceptor` | `UserContext` | +| 外部程序 | API Key | `ExternalApiKeyInterceptor` | `ExternalApiContext` | +| 管理端 | — | `AdminAuthInterceptor` | — | + +### ✅ ThreadLocal 清理正确 + +两个拦截器都在 `afterCompletion` 中调用 `clear()`: + +```java +@Override +public void afterCompletion(...) { + UserContext.clear(); +} +``` + +**这在使用线程池的 Web 容器里是必须的**。不清理会导致两个后果: + +- 线程复用时读到上一个请求的用户身份 → **越权** +- ThreadLocal 引用无法回收 → **内存泄漏** + +### ⚠️ 异步代码中的越权风险 + +`UserContext` 基于 ThreadLocal,**不跨线程传递**。 +异步任务里调 `getCurrentUserId()` 得到 `null`。 + +如果某处代码把 `null` 当成「不过滤」处理,就会变成**越权读取全部数据**。 + +**约定**:在控制器线程取出 userId,作为参数显式传入异步任务。 +新增异步逻辑时务必遵守。详见 +[用户认证模块 8.1](../modules/user-auth.md#81-️-threadlocal-不跨线程传递)。 + +--- + +## 3. 多租户隔离 + +| 资源 | 隔离方式 | +| --- | --- | +| 知识库 / 数据集 | 查询带 `userId` 过滤 | +| Agent / 会话 | 同上 | +| 工具容器 | 每用户独立容器(`ContainerType.USER`) | +| 服务商配置 | 每用户独立 | + +隔离依赖**每个查询都正确带上 userId**,没有数据库级的行级安全(RLS)兜底。 +这意味着**任何一处遗漏都是越权漏洞**,且不会报错。 + +> 建议:新增查询时优先复用已有的带 userId 的仓储方法, +> 不要直接写裸 SQL 或 `selectById`。 + +--- + +## 4. 不可信代码执行 + +平台允许用户提交 GitHub 仓库作为 MCP 工具——**这是在执行不可信代码**。 + +### 现有防护 + +``` +用户提交仓库 + ↓ +GITHUB_URL_VALIDATE 验证 URL + ↓ +DEPLOYING 在**审核容器**中部署(隔离于用户容器) + ↓ +FETCHING_TOOLS 抽取工具定义 + ↓ +MANUAL_REVIEW ★ 人工审核关卡 + ↓ +APPROVED 上架 +``` + +**人工审核是最后一道也是最关键的一道防线**。审核容器提供了运行时隔离, +但如[第 1 节 P1](#-p1docker-sock-挂载等同于宿主机-root) 所述, +隔离的上限受 docker.sock 挂载制约。 + +### 审核时应重点看什么 + +- 是否有外连行为(数据外传) +- 是否读取环境变量或文件系统敏感路径 +- 安装脚本是否有可疑操作 +- 依赖是否来自可信源 + +--- + +## 5. 密钥管理 + +| 密钥 | 存放 | 状态 | +| --- | --- | --- | +| 数据库密码 | 服务器 `.env` | ✅ 不入库、不入仓库 | +| S3 密钥 | 服务器 `.env` | ✅ | +| 邮件密码 | 服务器 `.env` | ✅ | +| GitHub PAT | 服务器 `.env` | ✅ | +| ACR 凭据 | GitHub Secrets | ✅ | +| **用户的模型服务商 API Key** | **数据库(弱加密)** | 🔴 见 P0 | + +### 前端脱敏 + +`ProviderAssembler` 中有 `dto.maskSensitiveInfo()`。 + +> ⚠️ **脱敏发生在 DTO 转换层而非实体层**。任何绕过 Assembler 直接序列化实体的路径 +> 都会泄露明文密钥。新增接口时必须走 Assembler。 + +### 操作规范 + +写入密钥**不要用 `echo`**(会进 shell history): + +```bash +# ✅ 不回显、不留痕 +read -s -p "粘贴令牌: " T && echo "GITHUB_PAT=$T" >> .env && unset T +``` + +本项目在配置过程中曾发生过令牌明文进入 shell history 的情况, +该令牌已作废重建。见[排查记录 3.5](troubleshooting-log.md#35-令牌明文泄露)。 + +--- + +## 6. CI/CD 安全 + +### ✅ 已落实的边界 + +| 措施 | 说明 | +| --- | --- | +| 无入站端口 | 自托管 runner 主动出站轮询,服务器不开 SSH | +| Secrets 最小化 | 只有 3 个,均不含服务器凭据 | +| `.env` 不覆盖 | CI 只做 `IMAGE_TAG` 一行的原地替换 | +| 二进制校验 | runner tarball 强制校验官方 SHA256 | +| 版本可追溯 | 每次部署记录 commit SHA | + +### 🔴 绝对不能做的事 + +**`deploy.yml` 绝不能添加 `pull_request` 触发。** + +这是**公开仓库** + **自托管 runner**。一旦 fork 的 PR 能触发工作流, +任何人都能在你的服务器上执行任意代码。 + +当前配置只允许 `push` 到 main 与 `workflow_dispatch`,已在工作流注释中标注。 + +### 换源的安全边界 + +| 类型 | 可否换镜像站 | 理由 | +| --- | --- | --- | +| 系统包(apt/yum) | ✅ | GPG 签名保护 | +| 语言包(npm/pip/maven) | ✅ | 校验和保护 | +| **可执行二进制** | ⚠️ **仅在强制校验官方哈希时** | 拿到 root 级权限 | + +> 反对的从来不是「用镜像站」,而是「**无校验地**信任镜像站」。 + +--- + +## 7. 安全检查清单 + +### 新增接口时 + +- [ ] 查询是否带 `userId` 过滤 +- [ ] 返回是否经过 Assembler(脱敏) +- [ ] 是否需要鉴权拦截器覆盖 + +### 新增异步逻辑时 + +- [ ] userId 是否从控制器线程显式传入(而非 ThreadLocal 取) +- [ ] 是否使用了专用线程池 + +### 修改 CI 时 + +- [ ] `deploy.yml` 是否仍**只有** `push: main` 与 `workflow_dispatch` +- [ ] 新增的下载是否有校验和 + +### 定期 + +- [ ] 轮换 GitHub PAT +- [ ] 检查 ACR 凭据有效期 +- [ ] 审查已上架 MCP 工具的行为变化 + +--- + +## 相关文档 + +- [基础设施](../architecture/infrastructure.md) —— 加密实现细节 +- [用户认证模块](../modules/user-auth.md) —— 鉴权链路 +- [MCP 工具模块](../modules/mcp-tool.md) —— 不可信代码执行流程 +- [部署指南](deployment.md) —— 环境变量与凭据分布 +- [排查记录](troubleshooting-log.md) —— 令牌泄露事件