Files
chorus/docs/02-architecture-and-code-map.md
T

165 lines
13 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!-- gitea-wiki-mirror:start -->
generated: true (请先修改 Gitea Wiki,禁止直接编辑本文件)
wiki_page: Architecture-and-Code-Map
wiki_url: https://git.ilapage.cn/OPC/chorus/wiki/Architecture-and-Code-Map.-
wiki_revision: b27a47e30cb5d79f6a6146e067a20296e850f8ab
synchronized_at: 2026-08-20T15:50:13Z
<!-- gitea-wiki-mirror:end -->
# 架构与代码地图
## 项目定位
chorus 是独立运行的 Go 生图生文服务:
- **图生图**:提示词 + 一张或多张原图 → 新图;
- **文生文**:提示词 → 文本。
portal 只负责接收、校验、持久化和展示;worker 才能调用上游。admin 配置 Provider/Model、路由池和查看记录。一个仓库包含两个二进制、一个纯领域核心和基础设施适配层:
```text
chorus/
├── internal/core/ 领域模型、接口、编排和状态规则;仅 GORM + 标准库
│ ├── model/
│ ├── provider/ 协议接口与 chat/images/images_edits/gemini 领域语义
│ ├── router/ 候选、retryable 与选路规则
│ ├── generate/ prompt 合成和生成编排
│ ├── queue/ 租约、CAS、重试状态
│ ├── storage/ 存储接口
│ └── crypto/ 密钥加密接口
├── internal/platform/ HTTP/SSRF、gobreaker、imaging、本地/S3 适配
├── portal/ 用户页面、HTMX 片段、JSON API、worker 组装
├── admin/ 固定 go-admin 后端,业务 app 在 app/chorus/
├── admin-ui/ 固定 go-admin-ui 导入代码与定制页
└── migrations/ 所有生产表、sys_* 基线和配置种子
```
固定来源和提交见 [项目档案](Project-Profile.-)。`D:\github\goadmin` 只用于来源审查和首次导入,运行、CI 与部署不得依赖该路径。
## MVP-0 的最小形态
| 目录 | MVP-0 实现 | 暂缓 |
|---|---|---|
| `internal/core/model` | `users`、`providers`、`provider_models`、`prompt_templates`、`generations`、输入/输出 | 点数、路由池、审计 |
| `internal/core/provider` | `chat`、`images_edits` | `images`、`gemini` |
| `internal/core/router` | 唯一启用 ProviderModel | 加权、熔断、故障转移 |
| `internal/core/queue` | SKIP LOCKED、租约 token/owner、CAS 完成、失败落错 | 心跳续租、多次跨 Provider 重试、清理 |
| `internal/platform` | SSRF 安全 HTTP、本地存储、缩略图 | gobreaker、对象存储 |
| `portal` | 种子用户登录、桌面双栏/移动单列、提交、轮询、结果 | 公开注册、点数/API Key 导航、无限滚动、移动抽屉 |
| `admin/admin-ui` | 不接入;配置由可重复种子命令完成 | MVP-1 schema-first CRUD 与定制页 |
MVP-0 第一张上传图自动作为 `primary`,其余为 `reference`;不提供角色编辑、备注和拖拽。默认中性模板已于 2026-08-20 确认,使用 Go `text/template` 且只开放 `{{.UserPrompt}}`,具体内容见业务规则;模板作为数据管理,不硬编码。
### #6 已落地的共享基线
- `go.mod` 固定 Go 1.26.5;`internal/config` 只从环境读取配置,生产缺少 DSN、会话密钥、Provider 主密钥或存储目录时失败关闭且不输出值。
- `migrations/000001_mvp0_base` 建立 `users`、`providers`、`provider_models`、`prompt_templates`、`generations`、`generation_inputs`、`generation_outputs`,生产启动不调用 AutoMigrate。
- 迁移显式定义用户幂等唯一键、队列/历史索引、5 个外键和删除规则,以及状态、租约、错误、attempt 数组和输出内容检查约束。
- `internal/seed/defaults.json` 保存已确认 Prompt 和 mock ProviderModel 数据;`cmd/chorus-seed` 在事务中幂等 upsert 构造用户、mock Provider/Model 和 Prompt,不保存或输出明文凭据。
- `portal` 当前只是配置门禁骨架,尚未注册 HTTP 路由或 worker;后续工单不得把它当作已完成服务。
### #7 已落地的核心契约
- `internal/core/model` 的 7 个 GORM 模型按 #6 migration version 1 映射,schema 单测核对完整列集合;状态只允许 `pending → running → succeeded|failed`,终态不可回退。
- `internal/core/provider`、`queue`、`storage`、`crypto` 和 model 仓储接口只表达 MVP-0 所需能力;core 不 import Gin、go-admin、gobreaker、imaging 或具体 HTTP/文件系统实现。
- `internal/core/generate` 使用 Go `text/template` 解析数据模板,但只接受直接的 `.UserPrompt` 插值;空输入、非法语法、缺少/未知变量和额外模板构造均失败关闭。chat 保留用户原文,images_edits 渲染结果与已确认模板逐字一致。
- 图片输入复制后按上传顺序写入 position,第一张为 `primary`、其余为 `reference`;调用方可直接把返回值和 rendered prompt 持久化。
- `internal/core/router` 固定 HTTP 429/5xx、超时和连接错误为 retryable,400/401、内容策略拒绝及未知错误为不可重试;MVP-0 仍只调用唯一启用模型,不执行换家。
- GORM 固定为用户指定 go-admin 提交使用的 v1.31.2;生产启动和 core 均没有 AutoMigrate。
### #8 已落地的平台安全边界
- `internal/platform/http` 是 Provider 请求和结果 URL 下载的统一入口:只允许 http/https 与显式端口,禁用环境代理,在 URL 校验和 `DialContext` 连接前分别解析 DNS,并直接连接已验证 IP,拒绝 IPv4/IPv6 私网、回环、链路本地、组播、未指定、文档与其他特殊地址。
- 每次 redirect 重新校验 scheme、端口和 DNS,限制次数;响应总超时、响应头超时和最大字节数由构造配置强制提供。返回错误不包含原 URL 的路径、查询参数或 userinfo。
- `internal/platform/crypto` 使用 AES-GCM;数据库信封字段是 `version/key_id/nonce/ciphertext`,version 和 key_id 进入 AAD。key ring 只用 active key 写入,同时可读取仍保留的旧 key_id,未知 key、篡改和错误版本失败关闭。
- `internal/platform/storage` 在受保护根目录下把 `content` 与 `metadata.json` 写入同一临时目录,完整 flush 后在同文件系统原子 rename;metadata 保存 owner_id、generation_id、content_type 和 size。路径逐级拒绝符号链接和越界,失败只清理本次临时目录。
- 图片保存前同时限制总字节、声明 MIME、实际解码格式和像素数;原图校验后保存,并生成最长边 256px 的 PNG 缩略图。文件不作为公开静态目录,授权判断仍由 portal 按 metadata 和数据库归属执行。
- imaging 固定 v1.6.2,`golang.org/x/image` 提升并固定为与 go-admin 基线一致的 v0.41.0;测试只使用 mock DNS、`net.Pipe`、构造密钥和临时目录。
### #9 已落地的 MySQL 租约队列
- `internal/core/queue.MySQLRepository` 在事务中按 `(status, lease_until, created_at)` 使用 `FOR UPDATE SKIP LOCKED` 取一条 pending 或过期 running;租约比较和截止时间统一使用 MySQL `NOW(6)`,避免应用时钟偏差。
- claim 原子写 `running/lease_owner/lease_token/lease_until` 并追加 attempt 起始项;过期 running 重领不回退 pending,而是在同一 UPDATE 中把旧 attempt 标为 `lease_expired` 后追加新项。
- worker 选定唯一启用 ProviderModel 后必须先用当前 token 调用 `AssignProvider`;成功/失败 CAS 只有当前 attempt 已绑定模型时才允许。重复绑定相同模型按当前 token 核对后视为幂等成功。
- 成功在事务中先执行 `id + status=running + lease_token` CAS,再写 outputs;输出约束失败会回滚终态。失败用单条 UPDATE 同时写状态、分类 error 和脱敏限长 attempt。旧 token 的绑定、成功和失败均影响 0 行。
- 同用户创建先锁 `users` 行,再查询 `(user_id,idempotency_key)`,因此并发重放返回原 generation 且不写第二份 inputs;不同用户可使用相同 key。
- `queue.Controller.StopClaims` 与 claim 使用读写锁,停止返回后不再进入新的数据库 claim;已经在途的最终写入仍必须通过 token CAS。
- MySQL 8.4.8 集成测试在 `REPEATABLE-READ` 下覆盖双 worker、过期重领、attempt 链、旧 token、事务回滚、并发幂等和 fixture 清理;不使用 mock SQL 代替数据库门禁。
## 代码地图
| 想改什么 | 从哪里开始读 | 必要验证 |
|---|---|---|
| 页面、状态和响应式 | `portal/web/templates/`、`portal/web/static/` | 浏览器 E2E + 已确认原型 |
| 提交校验/幂等 | `portal/handler/generate.go` | handler 测试、跨用户测试 |
| HTMX 卡片轮询 | `portal/handler/card.go` | 终态停止、网络错误、认证失效 |
| prompt 合成 | `internal/core/generate/prompt.go` | 默认模板、图片顺序与落库 |
| 上游协议 | `internal/core/provider/` | mock 429/5xx/超时/连接/400/401/策略拒绝 |
| SSRF/重定向/结果 URL | `internal/platform/http/` | DNS、IPv4/IPv6、redirect、proxy 测试 |
| 选路与 retryable | `internal/core/router/` | 四类红线用例 |
| 队列和陈旧 worker | `internal/core/queue/` | MySQL 并发、租约过期、CAS |
| 文件与缩略图 | `internal/platform/storage/` | 临时文件、原子 rename、权限 |
| 表结构 | `migrations/` 最新 SQL | 隔离 MySQL up/down |
| 管理端 CRUD | `admin/app/chorus/`、`admin-ui/src/views/` | 生成差异审查、生产路由检查 |
文件名是规划入口;实现工单调整后同步本页。
## 两条主要执行路径
### 用户提交生成(同步)
```text
表单/HTMX/JSON 请求
→ 认证、CSRF(浏览器)、上传与业务校验
→ 保存输入文件
→ 事务写 generations(status=pending)、generation_inputs
→ 以 (user_id, idempotency_key) 保证幂等
→ 立即返回 loading 卡片或 202 JSON
```
同步链路不得调用上游,不校验点数或配额,也不得把文件系统真实路径返回给浏览器。HTMX 请求返回可替换的片段;JSON 请求返回稳定错误码与字段级错误,认证失效明确返回 401/登录引导,不能用空白片段吞错。
### worker 执行生成(异步)
```text
事务内 SKIP LOCKED 选 pending 或租约已过期的 running
→ 原子写 running、lease_owner、lease_token、lease_until,attempt_count + 1
→ 合成 rendered_prompt 并落库
→ 选 ProviderModel,使用安全 HTTP 适配器调用
→ 结果写临时文件,校验后原子 rename;图片生成缩略图
→ 用 id + lease_token + status=running 做 CAS
→ 成功写 outputs/status=succeeded;失败写 status=failed/error/attempts
```
状态对外始终是 `pending → running → succeeded|failed`。过期任务由新 worker 在一次原子更新中替换租约,**不把状态退回 pending**。旧 worker 的 token 已失效,最终 CAS 必须影响 0 行,不能覆盖新 worker 结果。租约时长必须大于上游请求超时、下载/存储和安全余量之和;具体值是部署前配置门禁。
`attempts` 与 `attempt_count` 在同一事务/原子更新中追加,错误内容先分类、脱敏和截断。worker 收到退出信号后停止取新任务,等待在途任务到设定期限;无法完成时让租约自然过期,不能无条件提交。
## 管理端数据库与代码生成
固定 go-admin-ui 的实际流程是:
```text
编写并验证可逆 SQL
→ 在隔离开发库执行 up
→ /db/tables/page 读取现有表
→ /sys/tables/info 导入 sys_tables/sys_columns 元数据
→ 配置、预览、生成 Go/Vue CRUD
→ 人工审查路径、权限、字段和差异
→ 菜单/API 配置改写为可逆 SQL
→ 重新从空库执行全部 migrations 验证
```
go-admin 的 AutoMigrate 只能在隔离、可丢弃数据库中用于研究固定提交的初始化结构,不能嵌入应用启动或部署。go-admin-ui 的“生成迁移脚本”不是业务表 DDL;`/gen/todb` 会直接改菜单,`/gen/toproject` 会写源码。dev-tools 路由在固定提交中仅有 JWT 且绕过 Casbin,生产必须通过构建/路由注册检查确保不存在,不能只在菜单中隐藏。
## 不可破坏的边界
- `internal/core` 只依赖 GORM 和标准库;Gin、go-admin、gobreaker、imaging 都在外层。
- 管理员 `sys_user` 与终端用户 `users` 分表,不复用密码、Cookie、JWT 或 API Key。
- 生成链路不读写点数:无扣费、退款、余额校验或每日配额。
- `429/5xx/超时/连接错误` 才换下一家;`400/401/内容策略拒绝` 立即返回。
- 所有 Provider 请求、重定向和上游返回 URL 下载都必须经过 DNS 解析后、连接前的 DialContext 拦截;代理环境不能绕开检查。
- 生产数据库只接受 `migrations/`,不执行 AutoMigrate。
- `rendered_prompt`、`attempts`、失败 `error_code/error_message` 必须持久化。
- 输出文件先写临时文件并原子落位;图片必须有缩略图。原图、结果和缩略图访问均校验用户归属。