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