From fdf77abebc3977f3d1c4e2cf082f260b9b34acf2 Mon Sep 17 00:00:00 2001 From: Von Date: Thu, 12 Mar 2026 22:48:02 +0800 Subject: [PATCH] =?UTF-8?q?update=20=E5=A2=9E=E5=8A=A04+1=E8=AF=95?= =?UTF-8?q?=E5=9B=BE=EF=BC=8C=E4=BC=98=E5=8C=96=E6=8F=90=E4=BE=9B=E5=95=86?= =?UTF-8?q?=E7=95=8C=E9=9D=A2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- PROJECT_MEMORY.md | 302 ++++++++++------- docs/architecture-4plus1.md | 390 ++++++++++++++++++++++ src/presentation/static/css/providers.css | 42 ++- src/presentation/templates/providers.html | 11 +- 4 files changed, 622 insertions(+), 123 deletions(-) create mode 100644 docs/architecture-4plus1.md diff --git a/PROJECT_MEMORY.md b/PROJECT_MEMORY.md index 41578d6..76068bd 100644 --- a/PROJECT_MEMORY.md +++ b/PROJECT_MEMORY.md @@ -1,147 +1,217 @@ -# 项目记忆(ai_proxy) -适用范围:`d:\001Code\008llm\003ai_proxy` +# 项目记忆(LLM_Proxy) + +适用范围:`d:\001Code\008llm\003LLM_Proxy` ## 基础约束 -- 所有文本文件使用 `UTF-8`(无 BOM)。 -- 运行日志统一英文。 +- 所有文本文件使用 `UTF-8`,无 BOM。 +- 运行日志统一使用英文。 - 代码注释和 docstring 目标规范为中文。 +- Python 字符串风格默认使用双引号 `"..."`。 +- Python `docstring` 统一使用三重双引号 `"""..."""`。 +- 只有在减少转义、提升可读性时,普通字符串才使用单引号 `'...'`。 +- 涉及代码、配置、架构的修改时,优先保持控制平面与数据平面职责清晰。 ## 项目定位 -- 这是一个 Flask + gevent 的 OpenAI 兼容代理服务。 -- 核心能力是将 `/v1/chat/completions` 请求路由到配置中的上游 Provider,并支持 Hook 扩展、白名单控制、后台管理和日志统计。 +- 这是一个基于 Flask + gevent 的 OpenAI 兼容代理服务。 +- 核心能力是将 `/v1/chat/completions` 请求路由到配置中的上游 Provider。 +- 系统同时提供后台管理、认证、用户白名单、Provider 管理、模型探测和请求统计能力。 ## 启动与装配链路 - 入口文件:`main.py` - 启动关键点: -- 使用 `gevent.monkey.patch_all()`。 -- 支持 `--config` 指定配置文件,默认使用项目根目录 `config.yaml`。 -- 启动 `Application.run()`,由 gevent `WSGIServer` 提供服务。 -- 应用装配:`src/application/application.py` -- 初始化顺序: -- 读取配置 `ConfigManager` -- 初始化日志(`app.log`、`access.log`) -- 注册 `before_request` 访问日志钩子(记录 `ip + url`) -- 创建 `AppContext` -- 初始化 SQLite 连接工厂与仓储 -- 加载 Provider 与 Hook -- 组装 Service 与 Controller,并注册路由 + - 使用 `gevent.monkey.patch_all()` + - 支持 `--config` 指定配置文件,默认使用项目根目录 `config.yaml` + - 调用 `Application.run()`,由 gevent `WSGIServer` 提供服务 +- 组合根:`src/application/application.py` +- 当前装配顺序: + - 初始化 `ConfigManager` + - 初始化应用日志和访问日志 + - 创建 `AppContext` + - 初始化 SQLite 连接工厂和仓储 + - 初始化 `ProviderManager` 并加载 Provider + - 组装 Service 和 Controller 并注册路由 -## 分层职责 -- `src/presentation`:HTTP 路由与输入输出校验。 -- `src/services`:业务逻辑编排(认证、用户、代理、日志)。 -- `src/repositories`:SQLite 数据访问。 -- `src/config`:配置读取与 provider/hook 装载。 -- `src/external`:上游 LLM provider 抽象与输出处理。 -- `src/common`:上下文对象与类型协议。 -- `src/utils`:数据库连接、IP 处理等通用工具。 +## 当前分层 +- `src/application` + - 组合根与运行时上下文 +- `src/presentation` + - HTTP 路由、鉴权入口、页面渲染、请求和响应适配 +- `src/services` + - 用例编排和业务逻辑 + - 当前已拆分为: + - `AuthenticationService` + - `UserService` + - `ProviderService` + - `ModelDiscoveryService` + - `SettingsService` + - `ProxyService` + - `LogService` +- `src/repositories` + - SQLite 数据访问 +- `src/config` + - 配置快照管理、Provider 配置规范化、Provider 运行时注册 +- `src/external` + - 上游响应适配、流式探测、Provider 运行时对象 +- `src/hooks` + - Hook 协议和动态扩展点 +- `src/utils` + - 数据库、网络、IP 处理等通用工具 + +## 架构关键点 +- 当前是单进程分层单体。 +- 控制平面: + - 认证 + - 用户管理 + - Provider 配置管理 + - 系统设置管理 +- 数据平面: + - `/v1/chat/completions` + - `/v1/models` + - 模型路由 + - 上游转发 + - 响应适配 + - 请求日志记录 + +## 配置与 Provider 运行时 +- `ConfigManager` + - 提供配置快照读取 + - 提供原子写回 + - `get_raw_config()` 返回 `deepcopy`,避免调用方误改内部缓存 +- `provider_config` + - 负责 Provider 配置规范化和校验 +- `ProviderManager` + - 负责把配置中的 Provider 转成运行时 `LLMProvider` + - 维护 `model -> provider` 映射 + - 维护 Hook 缓存 +- `LLMProvider` + - 运行时 Provider 对象 + - 当前会复制 `model_list`,避免共享可变引用 ## 核心请求流程(chat/completions) -1. `ProxyController.chat_completions` 接收 `POST /v1/chat/completions`。 -2. 标准化客户端 IP(`normalize_ip`)。 -3. 若 `chat.whitelist_enabled=true`,按 IP 查用户且要求 `whitelist_access_enabled=1`。 -4. 校验请求体含 `model`。 -5. 强制补齐 `stream_options.include_usage=true`。 -6. 用 `ProviderManager.find_provider_by_model(model)` 找 provider。 -7. 调用 `ProxyService.proxy_request(...)` 转发到上游。 -8. 响应完成后通过回调写入 `request_logs` 并更新 `daily_request_stats`。 +1. `ProxyController.chat_completions` 接收 `POST /v1/chat/completions` +2. 标准化客户端 IP +3. 读取 `chat.whitelist_enabled` +4. 如果开启白名单,则按 IP 查询用户,并要求 `whitelist_access_enabled=1` +5. 校验请求体必须包含 `model` +6. 补齐 `stream_options.include_usage=true` +7. 用 `ProviderManager.find_provider_by_model(model)` 获取目标 Provider +8. `ProxyService.proxy_request(...)` 转发到上游 +9. `response_adapter` 处理普通响应或 SSE 响应 +10. 响应完成后通过回调写入 `request_logs` 并同步更新 `daily_request_stats` ## 模型路由规则 -- Provider 在配置中声明 `name` 与 `model_list`。 -- 系统内部模型 key 格式是:`{provider_name}/{model_name}`。 -- 代理接口请求体中的 `model` 必须匹配上述 key(例如 `volc/glm-4-7-251222`)。 -- 转发给上游前,`ProxyService` 会将模型名改为去前缀后的真实模型名(仅保留 `/` 后最后一段)。 +- 配置中每个 Provider 至少声明: + - `name` + - `api` + - `model_list` +- 系统内部模型 key 格式: + - `{provider_name}/{model_name}` +- 代理接口请求体中的 `model` 必须使用该格式 +- 转发给上游前,`ProxyService` 会把模型名裁剪成最后一段真实模型名 + +## 模型探测链路 +- `ProviderController.fetch_models()` +- `ModelDiscoveryService.fetch_models_preview(...)` +- 自动尝试候选端点: + - `/v1/models` + - `/models` +- 支持从 `chat/completions` 风格 URL 反推模型列表端点 ## Hook 机制 -- 在 `config.yaml` 的 provider 项可配置: -- `header_hook` -- `input_body_hook` -- `output_body_hook` -- Hook 路径规则: -- 绝对路径直接加载。 -- 相对路径默认相对项目根的 `hooks/` 目录加载。 -- `ProviderManager` 对 hook 模块做缓存,避免重复导入。 -- Hook 函数约定: -- `header_hook(ctx, headers) -> headers` -- `input_body_hook(ctx, body) -> body` -- `output_body_hook(ctx, data) -> Optional[data]` -- `ctx` 常用字段: -- `retry`(当前重试序号,从 0 开始) -- `ip_address` -- `user_id` +- 配置字段:`providers[].hook` +- 路径规则: + - 绝对路径直接加载 + - 相对路径默认相对项目根目录 `hooks/` +- Hook 模块必须导出 `Hook` 类 +- 可选方法: + - `header_hook(ctx, headers) -> headers` + - `input_body_hook(ctx, body) -> body` + - `output_body_hook(ctx, body) -> body` +- `ProviderManager` 负责 Hook 动态加载和缓存 -## 流式与非流式输出处理 -- 输出处理入口:`LLMProvider.apply_output_body_hook(...)`。 -- 非流式: -- 读取完整 JSON,提取 `model` 与 `usage`(`total/prompt/completion_tokens`)。 -- 若配置了输出 hook,则以 hook 返回内容作为响应体。 -- 流式(SSE): -- 按事件块解析 `data:` 行,逐块执行输出 hook(若存在)。 -- 识别 `[DONE]`。 -- 在流结束时触发 `on_complete`,用于日志落库与统计。 +## 认证与会话 +- 是否开启认证取决于 `admin.username` 和 `admin.password` 是否都存在 +- `AuthenticationService` 当前使用进程内内存 Session +- 服务重启后 Session 全部失效 +- 当前不适合多实例横向扩展 -## 重试与网络行为 -- `ProxyService` 使用 `requests.Session`。 -- 每个 provider 可配置: -- `timeout_seconds`(默认 300) -- `max_retries`(默认 3) -- `verify_ssl`(默认 false) -- 仅对 `requests.exceptions.RequestException` 进行重试。 +## 用户与白名单 +- `UserService` 对按 IP 查询用户做了内存缓存 +- 用户变更时会主动失效对应缓存 +- 白名单控制依赖: + - `chat.whitelist_enabled` + - 用户 `whitelist_access_enabled` -## 认证与权限 -- 认证是否启用取决于 `admin.username` 和 `admin.password` 是否都存在。 -- 启用后: -- 页面接口未登录重定向 `/login`。 -- API 未登录返回 `401 {"error":"Unauthorized"}`。 -- 会话存储在内存(`AuthenticationService._sessions`),服务重启后会话失效。 -- cookie 默认: -- `httponly=true` -- `samesite=Lax` -- `secure=false` -- `max_age=86400` +## 日志与统计 +- 访问日志写入 `logs/access.log` +- 应用日志写入 `logs/app.log` +- `LogRepository.insert()` 在一个事务内同时写: + - `request_logs` + - `daily_request_stats` ## 主要路由 - 代理接口: -- `POST /v1/chat/completions` -- `GET /v1/models` + - `POST /v1/chat/completions` + - `GET /v1/models` - 认证接口: -- `GET /login` -- `POST /api/login` -- `GET /logout` -- `POST /api/logout` + - `GET /login` + - `POST /api/login` + - `GET /logout` + - `POST /api/logout` - 用户管理: -- `GET /api/users` -- `POST /api/users` -- `GET /api/users/` -- `PUT /api/users/` -- `DELETE /api/users/` -- `POST /api/users//toggle` -- 统计与日志: -- `GET /api/statistics` -- `GET /api/request-logs` -- `GET /api/usernames` + - `GET /api/users` + - `POST /api/users` + - `GET /api/users/` + - `PUT /api/users/` + - `DELETE /api/users/` + - `POST /api/users//toggle` +- Provider 管理: + - `GET /api/providers` + - `POST /api/providers` + - `GET /api/providers/` + - `PUT /api/providers/` + - `DELETE /api/providers/` + - `GET /api/providers/fetch-models` + - `PUT /api/providers/chat-whitelist` - 页面: -- `GET /` -- `GET /users` + - `GET /` + - `GET /users` + - `GET /providers` -## 数据库结构(SQLite) -- 默认路径:`data/requests.db`(可由 `database.path` 覆盖)。 -- 表: +## 数据库结构 +- 默认路径:`data/requests.db` - `users` -- 字段:`id`, `username`, `ip_address(unique)`, `whitelist_access_enabled`, `created_at`, `updated_at` + - `id` + - `username` + - `ip_address` + - `whitelist_access_enabled` + - `created_at` + - `updated_at` - `request_logs` -- 字段:`ip_address`, `request_model`, `response_model`, `total_tokens`, `prompt_tokens`, `completion_tokens`, `start_time`, `end_time` + - `ip_address` + - `request_model` + - `response_model` + - `total_tokens` + - `prompt_tokens` + - `completion_tokens` + - `start_time` + - `end_time` - `daily_request_stats` -- 唯一键:`(stat_date, ip_address, request_model, response_model)` -- 用于按天聚合请求次数与 token 数据 + - 唯一键:`(stat_date, ip_address, request_model, response_model)` -## 关键实现细节 -- `ProxyController` 会过滤 hop-by-hop 请求头后再转发。 -- `ProxyService` 会过滤 hop-by-hop 响应头返回给客户端。 -- `UserService` 对按 IP 查用户做了内存缓存,并在用户变更时失效缓存。 -- `UserRepository.get()` 会关联 `daily_request_stats` 返回用户累计请求/Token统计。 -- `LogRepository.insert()` 在单事务内同时写明细日志与日聚合统计。 +## 当前风险与注意事项 +- 历史文件里有较多中文乱码的注释和 docstring,修改相关文件时优先逐步修复,避免继续扩散。 +- `output_body_hook` 在非流式分支如果返回 `None`,可能把响应体变成 `null`;新增 Hook 时要注意返回值约定。 +- 认证 Session、用户 IP 缓存、Provider 映射和 Hook 缓存都在单进程内存中,不适合多实例部署。 -## 当前开发注意事项 -- 代码中存在较多中文注释/docstring 乱码现象,修改相关文件时优先一并修复,避免继续扩散。 -- `output_body_hook` 在非流式分支若返回 `None`,当前会把响应体变成 `null`;新增 hook 时应明确返回原对象或改造此行为。 -- 会话是进程内存态,不适合多实例部署;如果未来需要横向扩展,应改为共享会话存储。 +## 4+1 架构文档维护 +- 4+1 Mermaid 文档文件:`docs/architecture-4plus1.md` +- 涉及以下变更时,必须同步更新该文档: + - 模块职责变化 + - 分层依赖变化 + - 主请求链路变化 + - Provider 重载链路变化 + - 运行时缓存或内存状态变化 + - Hook 机制变化 + - 部署拓扑变化 +- 如果代码提交没有修改 4+1 图,变更说明中需要明确声明“架构未变化”。 diff --git a/docs/architecture-4plus1.md b/docs/architecture-4plus1.md new file mode 100644 index 0000000..6196ddd --- /dev/null +++ b/docs/architecture-4plus1.md @@ -0,0 +1,390 @@ +# LLM_Proxy 4+1 Architecture View + +本文档描述当前代码实现对应的 4+1 架构视图,作为后续重构、评审和需求变更时的基线。 + +维护约束: +- 涉及模块职责、依赖关系、主请求链路、运行时状态、配置加载/重载机制、Hook 机制、部署拓扑的修改时,需要同步更新本文件。 +- 如果代码实现与本图不一致,以代码为准,但提交中应补齐文档更新。 + +## 0. 架构摘要 + +系统当前是一个单进程、分层式单体应用,按职责分成两条主轴: +- 控制平面:后台管理、认证、用户、Provider 配置、系统设置。 +- 数据平面:OpenAI 兼容代理、模型路由、上游请求转发、响应适配、日志落库。 + +关键代码位置: +- 启动与装配:[main.py](/d:/001Code/008llm/003LLM_Proxy/main.py) +- 组合根:[application.py](/d:/001Code/008llm/003LLM_Proxy/src/application/application.py) +- 配置与 Provider 运行时:[config_manager.py](/d:/001Code/008llm/003LLM_Proxy/src/config/config_manager.py) [provider_manager.py](/d:/001Code/008llm/003LLM_Proxy/src/config/provider_manager.py) +- 代理主链路:[proxy_controller.py](/d:/001Code/008llm/003LLM_Proxy/src/presentation/proxy_controller.py) [proxy_service.py](/d:/001Code/008llm/003LLM_Proxy/src/services/proxy_service.py) + +## 1. Logical View + +```mermaid +flowchart LR + Browser[浏览器管理端] + ApiClient[OpenAI 兼容 API Client] + + subgraph App["LLM_Proxy 应用"] + subgraph Presentation["Presentation"] + WebCtl[WebController] + AuthCtl[AuthenticationController] + UserCtl[UserController] + ProviderCtl[ProviderController] + ProxyCtl[ProxyController] + end + + subgraph Services["Services"] + AuthSvc[AuthenticationService] + UserSvc[UserService] + ProviderSvc[ProviderService] + DiscoverySvc[ModelDiscoveryService] + SettingsSvc[SettingsService] + ProxySvc[ProxyService] + LogSvc[LogService] + end + + subgraph ConfigRuntime["Config / Provider Runtime"] + ConfigMgr[ConfigManager] + ProviderCfg[provider_config] + ProviderMgr[ProviderManager] + LLMProvider[LLMProvider] + end + + subgraph Persistence["Persistence"] + UserRepo[UserRepository] + LogRepo[LogRepository] + SQLite[(SQLite)] + end + + subgraph Integration["External Integration"] + Hooks[Hook Modules] + Adapter[response_adapter] + Probe[stream_probe] + Upstream[Upstream LLM Providers] + end + end + + Browser --> WebCtl + Browser --> AuthCtl + Browser --> UserCtl + Browser --> ProviderCtl + ApiClient --> ProxyCtl + + AuthCtl --> AuthSvc + UserCtl --> UserSvc + ProviderCtl --> ProviderSvc + ProviderCtl --> DiscoverySvc + ProviderCtl --> SettingsSvc + ProxyCtl --> ProxySvc + ProxyCtl --> UserSvc + ProxyCtl --> ProviderMgr + ProxyCtl --> LogSvc + WebCtl --> ConfigMgr + + AuthSvc --> ConfigMgr + ProviderSvc --> ConfigMgr + ProviderSvc --> ProviderCfg + DiscoverySvc --> Upstream + SettingsSvc --> ConfigMgr + ProxySvc --> LLMProvider + ProxySvc --> Adapter + ProxySvc --> Probe + ProviderMgr --> ProviderCfg + ProviderMgr --> LLMProvider + ProviderMgr --> Hooks + Adapter --> Hooks + + UserSvc --> UserRepo + LogSvc --> LogRepo + UserRepo --> SQLite + LogRepo --> SQLite + ConfigMgr --> ConfigFile[(config.yaml)] +``` + +逻辑划分: +- `Presentation` 负责 HTTP 路由、鉴权入口、页面渲染和请求/响应封装。 +- `Services` 负责用例编排,不直接持有全局可变配置。 +- `Config / Provider Runtime` 负责 YAML 配置快照、Provider 规范化、模型到 Provider 的运行时映射。 +- `Persistence` 负责 SQLite 读写。 +- `External Integration` 负责上游协议适配、流式探测、Hook 扩展和上游 Provider 集成。 + +## 2. Development View + +```mermaid +flowchart TB + Main[main.py] + + subgraph Src["src/"] + Application["application/"] + Presentation["presentation/"] + Services["services/"] + Repositories["repositories/"] + Config["config/"] + External["external/"] + Hooks["hooks/"] + Utils["utils/"] + end + + Main --> Application + Main --> Utils + + Application --> Presentation + Application --> Services + Application --> Repositories + Application --> Config + Application --> Utils + + Presentation --> Application + Presentation --> Services + Presentation --> Config + + Services --> Application + Services --> Repositories + Services --> Config + Services --> External + Services --> Utils + + Config --> Application + Config --> External + Config --> Utils + + Repositories --> Utils + + External --> Hooks +``` + +开发视图解读: +- `application` 是组合根,不承载具体业务规则。 +- `presentation -> services` 是主要调用方向。 +- `services -> repositories/config/external` 是业务侧依赖方向。 +- `config` 既不是传统 repository,也不是 service,更像运行时配置与 Provider 注册子系统。 + +## 3. Process View + +```mermaid +flowchart TB + subgraph Process["单个 Python 进程"] + WSGI[gevent WSGIServer] + FlaskApp[Flask App] + + subgraph RuntimeState["进程内运行时状态"] + SessionState[AuthenticationService\n内存 Session] + UserCache[UserService\nIP -> User 缓存] + ConfigCache[ConfigManager\n配置快照缓存] + ProviderRegistry[ProviderManager\nmodel -> provider 映射\nhook cache] + HttpLocal[ProxyService\nthread-local requests.Session] + end + end + + subgraph Local["本地资源"] + ConfigFile[(config.yaml)] + DB[(data/requests.db)] + HooksDir[(hooks/*.py)] + LogFiles[(logs/*.log)] + end + + subgraph Remote["外部网络资源"] + Upstream[Upstream LLM Providers] + end + + WSGI --> FlaskApp + FlaskApp --> SessionState + FlaskApp --> UserCache + FlaskApp --> ConfigCache + FlaskApp --> ProviderRegistry + FlaskApp --> HttpLocal + + ConfigCache <--> ConfigFile + UserCache <--> DB + ProviderRegistry <--> HooksDir + FlaskApp --> LogFiles + HttpLocal <--> Upstream +``` + +进程视图要点: +- 当前是单进程单实例内存态设计。 +- `AuthenticationService` 的 Session 存在进程内存中,重启后失效。 +- `UserService` 有 IP 维度缓存。 +- `ProviderManager` 持有运行时模型路由表和 Hook 缓存。 +- `ProxyService` 维护 thread-local `requests.Session`。 +- 如果未来引入多实例部署,这些内存态要重新设计。 + +## 4. Physical View + +```mermaid +flowchart LR + subgraph Clients["客户端"] + Browser[浏览器管理端] + SDK[SDK / Curl / OpenAI Client] + end + + subgraph Host["部署主机"] + subgraph Service["LLM_Proxy 服务进程"] + Entry[main.py] + Server[gevent WSGIServer] + App[Flask + Controllers + Services] + end + + ConfigFile[(config.yaml)] + DB[(data/requests.db)] + HooksDir[(hooks/*.py)] + Logs[(logs/*.log)] + end + + subgraph Providers["外部 Provider"] + P1[Provider A] + P2[Provider B] + PN[Provider N] + end + + Browser <--HTTP/HTTPS--> Server + SDK <--HTTP/HTTPS--> Server + + App --> ConfigFile + App --> DB + App --> HooksDir + App --> Logs + + App <--HTTPS--> P1 + App <--HTTPS--> P2 + App <--HTTPS--> PN +``` + +物理视图要点: +- 部署结构目前非常简单,单机即可运行。 +- 本地状态包括配置文件、SQLite、日志文件、Hook 文件。 +- 外部依赖主要是上游模型 Provider 的 HTTP API。 + +## 5. Scenario View + +### 5.1 管理员登录 + +```mermaid +sequenceDiagram + actor Admin as 管理员浏览器 + participant AuthCtl as AuthenticationController + participant AuthSvc as AuthenticationService + participant ConfigMgr as ConfigManager + + Admin->>AuthCtl: POST /api/login + AuthCtl->>AuthSvc: authenticate(username, password) + AuthSvc->>ConfigMgr: get_admin_config() + ConfigMgr-->>AuthSvc: admin credentials + AuthSvc-->>AuthCtl: authenticated / rejected + + alt 认证成功 + AuthCtl->>AuthSvc: create_session(username) + AuthSvc-->>AuthCtl: session token + AuthCtl-->>Admin: Set-Cookie + 200 + else 认证失败 + AuthCtl-->>Admin: 401 + end +``` + +### 5.2 Provider 配置变更并自动重载 + +```mermaid +sequenceDiagram + actor Admin as 管理员浏览器 + participant ProviderCtl as ProviderController + participant ProviderSvc as ProviderService + participant ConfigMgr as ConfigManager + participant App as Application + participant ProviderMgr as ProviderManager + participant ProviderCfg as provider_config + participant Hooks as hooks/*.py + + Admin->>ProviderCtl: PUT /api/providers/{name} + ProviderCtl->>ProviderSvc: update_provider(payload) + ProviderSvc->>ConfigMgr: get_raw_config() + ConfigMgr-->>ProviderSvc: config snapshot + ProviderSvc->>ProviderCfg: normalize_provider_payload() + ProviderSvc->>ProviderCfg: validate_provider_definitions() + ProviderSvc->>ConfigMgr: write_raw_config(updated config) + ProviderSvc->>App: reload_providers() + App->>ConfigMgr: reload() + App->>ConfigMgr: get_raw_config() + App->>ProviderMgr: load_providers(providers_config) + ProviderMgr->>ProviderCfg: normalize_runtime_provider_config() + ProviderMgr->>Hooks: load hook modules if configured + ProviderMgr-->>App: provider registry refreshed + ProviderSvc-->>ProviderCtl: updated provider + ProviderCtl-->>Admin: 200 OK +``` + +### 5.3 后台探测上游模型列表 + +```mermaid +sequenceDiagram + actor Admin as 管理员浏览器 + participant ProviderCtl as ProviderController + participant DiscoverySvc as ModelDiscoveryService + participant Upstream as Upstream Provider + + Admin->>ProviderCtl: GET /api/providers/fetch-models?api=... + ProviderCtl->>DiscoverySvc: fetch_models_preview(...) + DiscoverySvc->>Upstream: GET /v1/models or /models + Upstream-->>DiscoverySvc: JSON models payload + DiscoverySvc-->>ProviderCtl: fetched_models + ProviderCtl-->>Admin: 200 JSON +``` + +### 5.4 代理一次 `/v1/chat/completions` + +```mermaid +sequenceDiagram + actor Client as API Client + participant ProxyCtl as ProxyController + participant ConfigMgr as ConfigManager + participant UserSvc as UserService + participant ProviderMgr as ProviderManager + participant ProxySvc as ProxyService + participant Hook as Hook Module + participant Upstream as Upstream Provider + participant Adapter as response_adapter + participant LogSvc as LogService + participant LogRepo as LogRepository + + Client->>ProxyCtl: POST /v1/chat/completions + ProxyCtl->>ConfigMgr: is_chat_whitelist_enabled() + + alt 白名单开启 + ProxyCtl->>UserSvc: get_user_by_ip(ip, require_whitelist_access=true) + UserSvc-->>ProxyCtl: user / None + end + + ProxyCtl->>ProviderMgr: find_provider_by_model(model) + ProviderMgr-->>ProxyCtl: LLMProvider + ProxyCtl->>ProxySvc: proxy_request(provider, body, headers, on_complete) + ProxySvc->>Hook: header_hook / input_body_hook + ProxySvc->>Upstream: POST provider.api + Upstream-->>ProxySvc: response or SSE stream + ProxySvc->>Adapter: build_proxy_response(...) + Adapter->>Hook: output_body_hook + Adapter-->>Client: Flask Response + Adapter->>LogSvc: on_complete(meta) + LogSvc->>LogRepo: insert(...) +``` + +## 6. 当前架构判断 + +当前架构优点: +- 模块划分清楚,学习成本低。 +- 单进程单文件配置的运维复杂度很低。 +- 控制平面和数据平面在职责上已经开始分离。 +- Hook 扩展点足够轻量,适合小规模快速演进。 + +当前架构边界: +- 运行时状态仍集中在单进程内存中,不适合多实例横向扩展。 +- `presentation` 层仍承担部分接口协议细节和错误映射责任。 +- `config` 子系统目前同时承担配置快照、配置规范化、Provider 注册三类职责。 +- Hook 是运行时动态加载,扩展灵活,但也引入了热更新和可观测性上的复杂性。 + +## 7. 后续演进建议 + +如果后面继续演进,优先级建议是: +- 把 `provider_config` 进一步收敛成显式 schema / factory。 +- 为 `ProviderManager` 增加更清晰的只读运行时接口。 +- 评估是否把 Session、Provider Registry、User IP 缓存从单进程内存态解耦。 +- 补一份 ADR,说明为什么当前仍选择单体 + 单进程 + SQLite。 diff --git a/src/presentation/static/css/providers.css b/src/presentation/static/css/providers.css index 45f59fa..07d7e0e 100644 --- a/src/presentation/static/css/providers.css +++ b/src/presentation/static/css/providers.css @@ -600,6 +600,24 @@ color: var(--primary); } +.providers-page .btn-field-secondary { + min-height: 34px; + padding: 0 14px; + border-color: var(--line-strong); + background: rgba(255, 255, 255, 0.88); + color: var(--text-secondary); + font-size: 13px; + font-weight: 700; +} + +.providers-page .btn-field-secondary:hover, +.providers-page .btn-field-secondary:focus { + border-color: var(--primary); + background: #ffffff; + color: var(--primary); + box-shadow: none; +} + .providers-page .field-help-button { width: 22px; height: 22px; @@ -728,7 +746,7 @@ } .providers-page .fetch-model-list { - max-height: min(52vh, 480px); + height: min(52vh, 480px); overflow-y: auto; padding: 6px; border: 1px solid var(--line); @@ -736,6 +754,15 @@ background: rgba(17, 24, 39, 0.02); } +.providers-page .fetch-model-list > .empty-state { + min-height: 100%; + margin: 0; + display: flex; + align-items: center; + justify-content: center; + padding: 24px 12px; +} + .providers-page .fetch-model-item { display: flex; align-items: center; @@ -840,6 +867,19 @@ color: #c7e0ff; } +:root[data-theme="dark"] .providers-page .btn-field-secondary { + border-color: #3b4654; + background: linear-gradient(180deg, rgba(28, 36, 49, 0.98), rgba(20, 27, 38, 0.98)); + color: #bcc8d8; +} + +:root[data-theme="dark"] .providers-page .btn-field-secondary:hover, +:root[data-theme="dark"] .providers-page .btn-field-secondary:focus { + border-color: #5f8fbe; + background: linear-gradient(180deg, rgba(35, 46, 62, 1), rgba(24, 33, 46, 1)); + color: #d9e7f7; +} + :root[data-theme="dark"] .providers-page .field-help-button { border-color: #3b4654; background: rgba(255, 255, 255, 0.04); diff --git a/src/presentation/templates/providers.html b/src/presentation/templates/providers.html index dcfe524..e556b64 100644 --- a/src/presentation/templates/providers.html +++ b/src/presentation/templates/providers.html @@ -93,7 +93,7 @@ 0 个模型
- +