- 从 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>
103 lines
6.7 KiB
Markdown
103 lines
6.7 KiB
Markdown
# 架构与代码地图
|
||
|
||
## 项目定位
|
||
|
||
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`),预览列表不得直接加载原图。
|