Files
chorus/docs/02-architecture-and-code-map.md
T
ilaandClaude Opus 5 69229a2bfd 同步 chorus Wiki 镜像
线上 Wiki 15 个核心页面已创建并回读确认,导出为 docs/ 只读镜像。

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

7.0 KiB
Raw Blame History

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: e2a808803b76303549ea374700d971dfa20f2fdd synchronized_at: 2026-08-20T03:29:24Z

架构与代码地图

项目定位

chorus 是一个独立运行的 Go 服务,对外提供两种生成能力:

  • 图生图:提示词 + 一张或多张原图 → 新图;
  • 文生文:提示词 → 文本。

上游是各家兼容 OpenAI 规范的模型服务。chorus 负责:接收请求、合成最终提示词、把任务放入队列、由 worker 选择一个可用上游执行、落盘结果与缩略图、把状态反馈给页面。管理端负责配置上游、组织路由池和查看记录。

一个仓库、两个二进制、一个共享库:

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 配置用迁移或一次性命令写入 全部管理端页面

暂缓项都在 产品需求总览 中登记为后续阶段,不是被取消的需求。

代码地图

想改什么 从哪个文件开始读 相关测试
页面长什么样 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 的规划命名;实现工单如需调整,必须同步更新本表。

两条主要执行路径

路径一:用户提交生成(同步部分)

浏览器表单 / HTMX 提交
  → portal/handler/generate.go:校验、保存上传原图、写 generations(status=pending) 与 generation_inputs
  → 立即返回一张 loading 卡片(带 hx-trigger="every 2s")

提交链路不调用上游,也不阻塞等待结果。任何在这一步做上游调用的改动都属于架构变更,必须先建单确认。

路径二:worker 执行生成(异步部分)

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),预览列表不得直接加载原图。