Files
chorus/docs/02-architecture-and-code-map.md
T
ilaandClaude Opus 5 3895c873ec 引导提交:接入 DevHarness 并建立 chorus 项目文档
- 从 dev_harness (3696663) 复制流程骨架:AGENTS/CLAUDE、工单模板、harness 工具与测试、通用流程文档
- 按需求方案改写为 chorus:项目档案、架构与代码地图、业务规则、本地验证、常见修改、故障排查、产品需求总览
- 需求分期为 MVP-0(跑通全流程最小闭环)/ MVP-1 / MVP-2
- AGENTS.md 增加 12 条 chorus 专用红线

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-20 11:29:12 +08:00

103 lines
6.7 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.
# 架构与代码地图
## 项目定位
chorus 是一个独立运行的 Go 服务,对外提供两种生成能力:
- **图生图**:提示词 + 一张或多张原图 → 新图;
- **文生文**:提示词 → 文本。
上游是各家兼容 OpenAI 规范的模型服务。chorus 负责:接收请求、合成最终提示词、把任务放入队列、由 worker 选择一个可用上游执行、落盘结果与缩略图、把状态反馈给页面。管理端负责配置上游、组织路由池和查看记录。
一个仓库、两个二进制、一个共享库:
```text
chorus/
├── go.mod
├── internal/core/ ★ 核心域,两端共享,不依赖 gin / go-admin
│ ├── model/ GORM 模型(业务表)
│ ├── provider/ chat / images / images_edits / gemini 四种实现
│ ├── router/ 选路、加权、gobreaker 熔断、故障转移
│ ├── generate/ prompt 合成、调用编排、落盘、缩略图
│ ├── queue/ SKIP LOCKED 租约、心跳、重试
│ ├── storage/ 本地 FS + S3 接口
│ ├── crypto/ provider api_key AES-GCM
│ └── security/ SSRF 拦截(DialContext 钩子)
├── admin/ go-admin 脚手架,业务 app 在 app/chorus/
├── admin-ui/ go-admin-ui,加自定义页面
├── portal/ ★ 用户端:独立 Gin
│ ├── handler/ 页面 + JSON API + openapi(API Key)
│ └── web/{templates,static}
└── migrations/ golang-migrate(业务表;sys_* 交给 go-admin 初始化)
```
## MVP-0 的最小形态
MVP-0 的唯一目标是**让全流程跑通**,因此只实现上面结构的一条竖切:
| 目录 | MVP-0 实现 | 暂缓 |
|---|---|---|
| `internal/core/model` | `users`、`providers`、`provider_models`、`generations`、`generation_inputs`、`generation_outputs` | 点数、路由池、健康、审计表 |
| `internal/core/provider` | `chat` 与 `images_edits` 两种实现 | `images`、`gemini` |
| `internal/core/router` | 直接取唯一启用的 provider_model | 加权选路、熔断、故障转移 |
| `internal/core/generate` | prompt 合成、调用、落盘、缩略图 | 模板三层覆盖的管理端层 |
| `internal/core/queue` | `SKIP LOCKED` 取任务 + 租约 + 失败置错 | 心跳续租、多次重试、清理任务 |
| `internal/core/crypto`、`security` | 从第一天就有:AES-GCM、SSRF 拦截 | 密钥轮换 |
| `portal` | 登录、双栏首页、提交生成、HTMX 轮询卡片、结果展示 | API Key 端点、限流、无限滚动分页 |
| `admin` | 暂不接入,provider 配置用迁移或一次性命令写入 | 全部管理端页面 |
暂缓项都在 [产品需求总览](09-product-requirements-overview.md) 中登记为后续阶段,不是被取消的需求。
## 代码地图
| 想改什么 | 从哪个文件开始读 | 相关测试 |
|---|---|---|
| 页面长什么样 | `portal/web/templates/` | 手工浏览器检查 + `prototypes/` 审核快照 |
| 提交生成的入参与校验 | `portal/handler/generate.go` | `portal/handler/generate_test.go` |
| 生成卡片轮询与停止条件 | `portal/handler/card.go` 与对应模板 | 同上 |
| 最终提示词怎么拼出来 | `internal/core/generate/prompt.go` | `internal/core/generate/prompt_test.go` |
| 怎么调上游、怎么解析响应 | `internal/core/provider/` 下各实现 | 各实现的 `_test.go`,用 mock HTTP 服务 |
| 失败了换不换下一家 | `internal/core/router/retryable.go` | `internal/core/router/retryable_test.go` |
| 任务怎么被取走和重试 | `internal/core/queue/` | `internal/core/queue/queue_test.go`(需 MySQL) |
| 结果文件放哪 | `internal/core/storage/` | `internal/core/storage/local_test.go` |
| 表结构 | `migrations/` 中序号最大的 SQL | 迁移 up/down 各跑一次 |
文件名是 MVP-0 的规划命名;实现工单如需调整,必须同步更新本表。
## 两条主要执行路径
### 路径一:用户提交生成(同步部分)
```text
浏览器表单 / HTMX 提交
→ portal/handler/generate.go:校验、保存上传原图、写 generations(status=pending) 与 generation_inputs
→ 立即返回一张 loading 卡片(带 hx-trigger="every 2s")
```
提交链路**不调用上游**,也不阻塞等待结果。任何在这一步做上游调用的改动都属于架构变更,必须先建单确认。
### 路径二:worker 执行生成(异步部分)
```text
queue:SELECT … FOR UPDATE SKIP LOCKED 取一条 pending,写 lease_until,置 running
→ generate:合成 rendered_prompt(管理端模板 → 用户 role_rule → 每张图 role/note)
→ router:挑选候选 provider_model(MVP-0 只有一个)
→ provider:调用上游,得到图片字节或文本
→ storage:落盘原图/结果图,生成 256px 缩略图
→ 写 generation_outputs,generations 置 succeeded/failed,记录 attempts 与 latency_ms
→ 下一次轮询请求返回不带 hx-trigger 的成品卡片,轮询自动停止
```
worker 跑在 portal 进程内(goroutine pool + 优雅退出),不放进 go-admin 的 `app/jobs`,避免管理端重启影响生成。
## 不可破坏的边界
- `internal/core` **不 import gin、不 import go-admin**,只依赖 GORM 和标准库。管理端换框架或用户端换渲染方式时,核心不需要改。
- 管理员与终端用户**物理隔离在两张表**:管理端用 go-admin 的 `sys_user` + JWT + Casbin,用户端用 `users` + session cookie(scs),程序调用用 API Key 哈希比对同样落在 `users`。不得把两类身份合并到一张表。
- **生成链路完全不碰点数**:点数只读展示,无扣费、无退款、无余额校验、无配额。cmhub 的 `precharge_call / refund_call_points / mark_call_success` 三段式事务全部不移植。
- **`retryable` 判定是全局最容易出错的地方**:`429 / 5xx / 超时 / 连接错误` 换下一家;`400 / 401 / 内容策略拒绝` 立即返回不换家。写反了,一个参数错误会把所有 provider 的配额烧一遍。
- **SSRF 校验不能省**:provider `base_url` 由管理员填写,是打内网的直接入口。必须在 **DNS 解析后、连接前**用 `DialContext` 钩子拦截私网与回环地址,防 DNS rebinding。
- **生产不使用 GORM AutoMigrate**:表结构只经 `migrations/` 演进。
- **`rendered_prompt` 必须落库**,`generations.attempts` 必须记录每次尝试的 provider、错误和耗时;这两项是排障的唯一依据。
- 结果图必须同步生成缩略图(`generation_outputs.thumb_path`),预览列表不得直接加载原图。