update 增加4+1试图,优化提供商界面

This commit is contained in:
Von committed 2026-03-12 22:48:02 +08:00
1 parent b8ab461c4c
commit fdf77abebc
4 files changed
+622 -123

No files matched your search

+186 -116
View File
@@ -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/<id>`
- `PUT /api/users/<id>`
- `DELETE /api/users/<id>`
- `POST /api/users/<id>/toggle`
- 统计与日志:
- `GET /api/statistics`
- `GET /api/request-logs`
- `GET /api/usernames`
- `GET /api/users`
- `POST /api/users`
- `GET /api/users/<id>`
- `PUT /api/users/<id>`
- `DELETE /api/users/<id>`
- `POST /api/users/<id>/toggle`
- Provider 管理:
- `GET /api/providers`
- `POST /api/providers`
- `GET /api/providers/<name>`
- `PUT /api/providers/<name>`
- `DELETE /api/providers/<name>`
- `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 图,变更说明中需要明确声明“架构未变化”。
+390
View File
@@ -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。
+41 -1
View File
@@ -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);
+5 -6
View File
@@ -93,7 +93,7 @@
<span class="field-meta-badge" id="providerModelCount">0 个模型</span>
</div>
<div class="field-header-actions">
<button type="button" class="btn-inline-secondary" onclick="tidyModelList()">整理</button>
<button type="button" class="btn btn-secondary btn-field-secondary" onclick="tidyModelList()">整理</button>
<button type="button" class="btn btn-secondary btn-fetch-inline" id="fetchModelsBtn" onclick="fetchModels()">拉取模型</button>
<button
type="button"
@@ -399,6 +399,7 @@
renderFetchModelPicker();
fetchModelPickerModalInstance.show();
return normalizedFetchedModels.length;
}
function toggleFetchedModelSelection(encodedModel, checked, inputElement) {
@@ -568,10 +569,7 @@
}
function canFetchModels() {
return !!(
document.getElementById('providerName').value.trim() &&
document.getElementById('providerApi').value.trim()
);
return !!document.getElementById('providerApi').value.trim();
}
function updateFetchModelsButtonState() {
@@ -754,7 +752,8 @@
if (!response.ok) {
throw new Error(result.error || '拉取模型失败');
}
openFetchModelPicker(result.fetched_models);
const fetchedCount = openFetchModelPicker(result.fetched_models);
showMessage(`已获取到 ${fetchedCount} 个模型`, 'success');
} catch (error) {
console.error('拉取模型失败:', error);
showMessage(error.message || '拉取模型失败', 'error');