diff --git a/README.md b/README.md index efcbbe0..069fbe9 100644 --- a/README.md +++ b/README.md @@ -14,7 +14,7 @@ MVP-0 的范围、非目标和验收口径见 [产品需求总览](docs/09-produ ## 快速开始(Wiki 初始化门禁) -线上 Gitea Wiki 已于 2026-08-20 初始化完成,15 个核心页面均已创建并回读确认,`docs/` 已转为只读镜像。以下是该门禁的完整顺序,供后续项目复用与核对: +线上 Gitea Wiki 已于 2026-08-20 初始化完成,当前 16 个映射页面均已创建并回读确认,`docs/` 已转为只读镜像。以下是该门禁的完整顺序,供后续项目复用与核对: 1. 创建 Gitea 远端仓库并推送当前引导提交,启用工单和 Wiki。 2. 配置 `wiki-docs.json` 和安全访问方式;优先使用已配置的 Gitea MCP,MCP 不可用时才使用 Gitea API 并记录原因。令牌只通过环境变量或 MCP 安全配置提供。 @@ -41,6 +41,7 @@ MVP-0 的范围、非目标和验收口径见 [产品需求总览](docs/09-produ | 怎样跑起来、怎样验证 | [docs/04-local-development-and-verification.md](docs/04-local-development-and-verification.md) | | 简单修改怎么做、什么时候必须停 | [docs/05-common-changes.md](docs/05-common-changes.md) | | 报错了先查什么 | [docs/06-troubleshooting.md](docs/06-troubleshooting.md) | +| 怎样部署、检查、备份和回退 | [docs/10-deployment-and-operations.md](docs/10-deployment-and-operations.md) | | 建单、实施、验收、归档流程 | [docs/01-workflow.md](docs/01-workflow.md) | `docs/` 是 Wiki 的只读镜像,不是编辑入口。修改长期文档的顺序见 [项目档案](docs/00-project-profile.md) 的“文档状态”。 diff --git a/docs/00-project-profile.md b/docs/00-project-profile.md index 90e7202..7164c89 100644 --- a/docs/00-project-profile.md +++ b/docs/00-project-profile.md @@ -2,8 +2,8 @@ generated: true (请先修改 Gitea Wiki,禁止直接编辑本文件) wiki_page: Project-Profile wiki_url: https://git.ilapage.cn/OPC/chorus/wiki/Project-Profile.- -wiki_revision: ad9b705ba045f033379b6daea4a3575702d0eb35 -synchronized_at: 2026-08-20T03:31:03Z +wiki_revision: 3bc33f550a53d7cab30684fca2eb5a2c42866090 +synchronized_at: 2026-08-20T07:01:02Z # 项目档案 @@ -39,37 +39,53 @@ synchronized_at: 2026-08-20T03:31:03Z ## 子项目与交付单元 -| 子项目 / 交付单元 | 职责 | 技术栈 | 构建与测试 | 版本与发布方式 | 规则入口 | 共享边界 | -|---|---|---|---|---|---|---| -| `internal/core` 核心域库 | provider 调用、选路、生成编排、队列、存储、加密、SSRF 防护 | Go 1.22+、GORM | `go build ./...`;`go test ./internal/...` | 不单独发布,随两个二进制发布 | 根 `AGENTS.md` | 被 portal 与 admin 共用;**不 import gin、不 import go-admin** | -| `portal` 用户端二进制 | 用户 Web 界面、JSON API、API Key 端点,MVP 阶段内嵌 worker | Go + Gin + html/template + HTMX + Alpine + Tailwind | `go build ./portal/...`;`go test ./portal/...` | 跟随 `main` 分支构建单二进制 | 根 `AGENTS.md` | 依赖 `internal/core`,与 admin 共用同一 MySQL 库 | -| `admin` 管理端二进制 | Provider/Model/路由池/记录/用户与点数管理 | Go + go-admin(Gin + GORM + Casbin + JWT)+ go-admin-ui(Vue 2) | `go build ./admin/...` | 跟随 `main` 分支构建 | 根 `AGENTS.md` | 依赖 `internal/core`;不得绕过核心域直接实现生成逻辑 | -| `migrations` 数据库迁移 | 业务表结构演进 | golang-migrate(SQL 文件) | `migrate up` / `migrate down` | 随代码提交,按序号前进 | 根 `AGENTS.md` | 表结构是三个交付单元的唯一共享契约 | +| 子项目 / 交付单元 | 职责 | 技术栈与版本 | 构建与测试 | 发布方式 | 共享边界 | +|---|---|---|---|---|---| +| `internal/core` 核心域库 | 领域模型、provider 协议抽象、生成编排、队列状态、加密接口 | Go 1.26.5、GORM、标准库 | `go test ./internal/core/...` | 不单独发布 | **不得 import Gin、go-admin、gobreaker 或 imaging** | +| `internal/platform` 基础设施适配 | 出站 HTTP/SSRF、熔断、图片处理、存储等接口实现 | 标准库、sony/gobreaker、disintegration/imaging | `go test ./internal/platform/...` | 随服务二进制发布 | 通过接口注入 core,不反向污染领域层 | +| `portal` 用户端二进制 | 会话、页面/HTMX 片段、JSON API,MVP 阶段内嵌 worker | Gin、html/template、HTMX、Alpine、Tailwind | `go test ./portal/...` | 单二进制 | 依赖 core 与 platform;提交链路不得调用上游 | +| `admin` 管理端后端 | Provider/Model/路由池/记录/用户与点数管理 | 固定提交的 go-admin(Gin、GORM、Casbin、JWT) | `go test ./admin/...` | 独立二进制 | 依赖 core;不得复制生成逻辑 | +| `admin-ui` 管理端前端 | go-admin-ui CRUD 与少量定制页 | Vue 3.5.41、Element Plus 2.14.4、Vue CLI 5.0.9 | `pnpm install --frozen-lockfile`、`pnpm build:prod` | 静态产物 | 代码生成器只在隔离开发环境使用 | +| `migrations` 数据库迁移 | 业务表、`sys_*` 基线和菜单/API 配置的全部生产演进 | golang-migrate SQL | up/down 隔离库验证 | 随版本发布 | **生产数据库结构的唯一事实来源** | -共享契约的唯一事实来源是 `migrations/` 中的 SQL 与 [业务规则与术语](Business-Rules-and-Glossary.-);不得在多处维护互不确认的表结构描述。MVP-0 只要求 `internal/core` 与 `portal` 可运行,`admin` 与 `migrations` 的完整形态在后续阶段补齐。 +管理员 `sys_user` 与终端用户 `users` 分表。MVP-0 只要求 core、platform、portal 和必要迁移可运行;admin/admin-ui 在 MVP-1 接入,但生产所需 `sys_*` 初始结构和配置仍必须先转成版本化 SQL。任何构建、CI 或部署都不得依赖 `D:\github\goadmin` 的绝对路径。 ## 技术栈与运行环境 +### 固定来源基线 + +| 项目 | 本地审查来源 | 固定提交 | 关键约束 | +|---|---|---|---| +| go-admin | `D:\github\goadmin\go-admin` | `f06540883b41d03782bb6b2c4150f298f328c6b6` | `go.mod` 要求 Go 1.26.5 | +| go-admin-ui | `D:\github\goadmin\go-admin-ui` | `67d393d713877572fab0b897296a4c1d525fc81d` | Vue 3.5.41、Element Plus 2.14.4、Vue CLI 5.0.9、Node >=22、`packageManager=pnpm@9.15.1` | +| go-admin-doc | `D:\github\goadmin\go-admin-doc` | `424855aacf6905f3fde860c3331385cb25529a0d` | 只作固定版本说明参考 | + +这些路径用于审查和导入来源,不是运行依赖。首次接入必须把需要的脚手架/前端代码纳入 chorus 仓库或把 Go 依赖锁定到可复现版本,并在实现工单记录来源提交、导入范围和本地修改。 + +### 运行栈 + | 部分 | 技术 | 说明 | |---|---|---| -| HTTP 框架 | Gin | 与 go-admin 一致,减少两端框架分裂 | -| ORM 与迁移 | GORM + golang-migrate | GORM 只做查询,**生产不使用 AutoMigrate** | -| 数据库 | MySQL 8.0 | 单库;队列用 `SELECT … FOR UPDATE SKIP LOCKED` | -| 任务队列 | 无 Redis / 无 MQ | 租约 + 心跳轮询,少一个运行组件 | -| 管理端 | go-admin-team/go-admin + go-admin-ui | 自带 RBAC / JWT / 代码生成器 | -| 用户端会话 | alexedwards/scs | 无需 Redis | -| 熔断 | sony/gobreaker | 多 provider 故障转移,MVP-0 之后启用 | -| 缩略图 | disintegration/imaging | 纯 Go,无 CGO | -| 限流 | ulule/limiter | 用户维度令牌桶 | -| 上游调用 | 手写 net/http 客户端 | 不用 go-openai SDK:多 provider 在多图上传、`extra_body` 透传、`b64_json` vs `url` 上差异大,强类型 SDK 反而挡路 | -| 用户端前端 | html/template + HTMX 2.x + Alpine 3.x + Tailwind 4.x(standalone CLI) | 无 npm、无构建链、无 SPA | -| 开发环境 | Windows + PowerShell + Git;MySQL 8.0 本地或容器 | Harness 工具需 Python 3 标准库 | +| Go | 1.26.5 | 以固定 go-admin 的 `go.mod` 为最低统一版本,不再使用原文档的 Go 1.22+ | +| HTTP | Gin | portal 与 admin 共用生态,但 core 不依赖 Gin | +| ORM 与迁移 | GORM + golang-migrate | GORM 负责运行期持久化;生产禁止 AutoMigrate | +| 数据库 | MySQL 8.0 | 单库;队列依赖 `SELECT … FOR UPDATE SKIP LOCKED` | +| 队列 | MySQL 租约 | 不引入 Redis/MQ;租约 token + CAS 防止陈旧 worker 提交 | +| 管理端 | 固定 go-admin + go-admin-ui | schema-first 生成 CRUD;生产关闭 dev-tools | +| 用户端会话 | alexedwards/scs | Cookie 会话,不复用管理员 JWT | +| 平台适配 | gobreaker、imaging | 只能位于 `internal/platform` | +| 限流 | ulule/limiter | MVP-2 用户维度令牌桶 | +| 上游调用 | `net/http` 适配器 | 支持多图、`extra_body`、URL/Base64 差异,并实施 SSRF 钩子 | +| 用户端 UI | html/template + HTMX 2.x + Alpine 3.x + Tailwind 4.x standalone | portal 不需要 Node;admin-ui 构建需要 Node/pnpm | +| 开发环境 | Windows + PowerShell + Git;MySQL 8.0 本地或容器 | Harness 使用 Python 3 标准库 | -### 建设基线评估结论 +### AutoMigrate 与代码生成结论 -- 管理端**采用开源基线** go-admin(MIT):已具备 RBAC、JWT、审计和代码生成器,Provider/Model/用户/点数等 CRUD 可近似零成本产出,定制范围可控。已知代价:go-admin-ui 属 vue-element-admin 系(Vue 2 已 EOL),定制页超过 3 个时须重新评估是否改为自建服务端渲染后台。 -- 核心生成域**从零开发**:cmhub 是 Django,语言已变,能带走的是设计(provider 抽象、任务租约、SSRF 校验、密钥加密)而非代码;现成 Go 系 LLM 网关(如各类 one-api 变体)与本项目在图生图多图上传、图片角色规则和自有点数展示上差异大,改造与跟随上游成本高于自建约 200 行 provider 层。 -- 基础组件按需复用:gobreaker、imaging、limiter、scs 均为单一职责库,各自记录用途、版本和可替换方案。 +- go-admin 固定提交的 `cmd/migrate` 和模板中确有 AutoMigrate;它适合帮助理解脚手架初始化结构,但不是可审查、可回退的生产迁移。 +- 如确需研究该初始化结果,只能由人工在**隔离、可丢弃、无生产数据**的开发数据库中单独运行,并把得到的结构差异整理为 `migrations/*.up.sql` 与 `*.down.sql`。 +- go-admin-ui 的生成器先从数据库导入已有表到 `sys_tables/sys_columns`,再生成 Go/Vue 文件;因此标准流程是先执行版本化 SQL,再导入和生成。 +- 生成器的“生成迁移脚本”主要写菜单、权限与 API 配置的 Go 迁移代码,并不是业务表 DDL,也没有本项目要求的可逆性和幂等证据;这些配置必须人工转换为可逆 SQL。 +- dev-tools 路由包含写文件和写菜单的能力,固定提交中只有 JWT 保护且被 Casbin 排除,生产构建不得注册、代理或暴露这些路由。 ## 阅读入口 @@ -101,24 +117,28 @@ synchronized_at: 2026-08-20T03:31:03Z | 目录 | 职责 | 不应放入 | |---|---|---| -| `internal/core/` | 与框架无关的核心域 | gin、go-admin 依赖,HTTP 处理,模板 | -| `portal/` | 用户端 Gin 服务、页面模板与静态资源 | 上游调用与选路逻辑(属于 core) | -| `admin/` | go-admin 脚手架与业务 app | 绕过 core 的生成实现 | -| `migrations/` | golang-migrate SQL 文件 | 测试数据、一次性修数据脚本 | -| `docs/` | 核心长期文档(Wiki 初始化后为只读镜像) | 人工直接维护的最终事实(初始化后) | -| `docs/task/` | 人工按需导出的任务归档快照 | 讨论过程和临时方案 | -| `prototypes/` | 按工单和版本保存的 HTML 审核快照 | 凭据、个人信息、生产数据 | -| `dev_scripts/` | Harness 检查与 Wiki 同步工具 | 产品功能代码 | -| `tests/` | Harness 工具测试(Go 测试与被测代码同目录) | 生产数据 | +| `internal/core/` | 领域模型、协议接口、编排与状态规则 | Gin、go-admin、gobreaker、imaging、HTTP handler | +| `internal/platform/` | HTTP/SSRF、熔断、图片、存储等适配器 | 页面、管理端 CRUD、业务状态机复制 | +| `portal/` | 用户端 Gin、模板、静态资源、worker 组装 | 直接实现 provider 选路和上游协议 | +| `admin/` | go-admin 后端及 `app/chorus/` | 绕过 core 的生成实现、生产代码生成路由 | +| `admin-ui/` | 固定 go-admin-ui 导入代码与定制页 | 运行时依赖 `D:\github\goadmin` | +| `migrations/` | 全部生产表结构与配置种子 SQL | 测试数据、AutoMigrate、不可逆一次性修数 | +| `docs/` | 核心长期文档的 Wiki 只读镜像 | 人工直接维护的最终事实 | +| `docs/task/` | 人工按需导出的任务归档快照 | 讨论过程和默认自动导出 | +| `prototypes/` | 按工单/版本保存 HTML 审核快照 | 凭据、个人信息、生产数据 | +| `dev_scripts/` | DevHarness 工具 | 产品业务脚本 | +| `tests/` | Harness 测试;Go 测试与代码同目录 | 生产数据 | ## 环境、配置与凭据 -- 核心 Wiki 同步配置:`wiki-docs.json`;Gitea 地址可用 `GITEA_URL` 覆盖。 -- Gitea PAT 仅通过 `GITEA_TOKEN` 或 MCP 安全配置提供,不写入仓库。 -- 数据库连接串通过 `CHORUS_DSN` 环境变量提供,不写入仓库。 -- Provider 的 `api_key` 使用 AES-GCM 加密落库,主密钥来自环境变量 `CHORUS_MASTER_KEY`,须预留轮换方案;明文密钥不得进入日志、工单、Wiki 或截图。 -- 生成物默认落本地文件系统(路径由配置指定),`storage` 从第一天就是接口,预留 S3/OSS。 -- 测试只使用构造数据和 mock 上游,不使用生产数据和真实上游密钥。 +- 核心 Wiki 同步配置是 `wiki-docs.json`;Gitea 地址可用 `GITEA_URL` 覆盖。 +- Gitea PAT 仅从进程环境、MCP 安全配置或本机未跟踪的环境文件加载,不写入仓库、工单或 Wiki。 +- 数据库连接串由 `CHORUS_DSN` 提供。 +- Provider `api_key` 使用 AES-GCM 加密落库;密文同时记录非敏感 `key_id`,主密钥从环境/密钥管理系统取得。轮换按“新 key_id 写入、后台重加密、验证后退役旧密钥”执行,不原地猜测密钥版本。 +- 会话签名/加密密钥与 Provider 主密钥分离,生产 Cookie 必须启用 `Secure`、`HttpOnly` 和合适的 `SameSite`。 +- 生成物默认落受保护的本地目录,不能直接把真实路径暴露为公开静态 URL;访问先校验用户归属。storage 从第一天是接口,预留 S3/OSS。 +- 测试只用构造数据与 mock 上游;真实连通性检查必须是管理员明确触发的单次低成本动作,并有审计和冷却。 +- 上传数量、单文件/总大小、允许 MIME、像素上限、上游超时、租约时长、保留期和限流阈值均为配置。未确认生产值前保持部署门禁,不由 Agent 臆造。 ## 项目专用验收要求 diff --git a/docs/02-architecture-and-code-map.md b/docs/02-architecture-and-code-map.md index e6c98ea..6016f67 100644 --- a/docs/02-architecture-and-code-map.md +++ b/docs/02-architecture-and-code-map.md @@ -2,109 +2,127 @@ 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 +wiki_revision: b1268df4bd1d7e4fc6ca0a418da7215e61a4af91 +synchronized_at: 2026-08-20T07:01:09Z # 架构与代码地图 ## 项目定位 -chorus 是一个独立运行的 Go 服务,对外提供两种生成能力: +chorus 是独立运行的 Go 生图生文服务: - **图生图**:提示词 + 一张或多张原图 → 新图; - **文生文**:提示词 → 文本。 -上游是各家兼容 OpenAI 规范的模型服务。chorus 负责:接收请求、合成最终提示词、把任务放入队列、由 worker 选择一个可用上游执行、落盘结果与缩略图、把状态反馈给页面。管理端负责配置上游、组织路由池和查看记录。 - -一个仓库、两个二进制、一个共享库: +portal 只负责接收、校验、持久化和展示;worker 才能调用上游。admin 配置 Provider/Model、路由池和查看记录。一个仓库包含两个二进制、一个纯领域核心和基础设施适配层: ```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 初始化) +├── 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 的唯一目标是**让全流程跑通**,因此只实现上面结构的一条竖切: - | 目录 | 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 配置用迁移或一次性命令写入 | 全部管理端页面 | +| `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 与定制页 | -暂缓项都在 [产品需求总览](Product-Requirements-Overview.-) 中登记为后续阶段,不是被取消的需求。 +MVP-0 第一张上传图自动作为 `primary`,其余为 `reference`;不提供角色编辑、备注和拖拽。默认中性模板的具体措辞仍是实现前确认门禁,不能硬编码猜测。 ## 代码地图 -| 想改什么 | 从哪个文件开始读 | 相关测试 | +| 想改什么 | 从哪里开始读 | 必要验证 | |---|---|---| -| 页面长什么样 | `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 各跑一次 | +| 页面、状态和响应式 | `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/` | 生成差异审查、生产路由检查 | -文件名是 MVP-0 的规划命名;实现工单如需调整,必须同步更新本表。 +文件名是规划入口;实现工单调整后同步本页。 ## 两条主要执行路径 -### 路径一:用户提交生成(同步部分) +### 用户提交生成(同步) ```text -浏览器表单 / HTMX 提交 - → portal/handler/generate.go:校验、保存上传原图、写 generations(status=pending) 与 generation_inputs - → 立即返回一张 loading 卡片(带 hx-trigger="every 2s") +表单/HTMX/JSON 请求 + → 认证、CSRF(浏览器)、上传与业务校验 + → 保存输入文件 + → 事务写 generations(status=pending)、generation_inputs + → 以 (user_id, idempotency_key) 保证幂等 + → 立即返回 loading 卡片或 202 JSON ``` -提交链路**不调用上游**,也不阻塞等待结果。任何在这一步做上游调用的改动都属于架构变更,必须先建单确认。 +同步链路不得调用上游,不校验点数或配额,也不得把文件系统真实路径返回给浏览器。HTMX 请求返回可替换的片段;JSON 请求返回稳定错误码与字段级错误,认证失效明确返回 401/登录引导,不能用空白片段吞错。 -### 路径二:worker 执行生成(异步部分) +### 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 的成品卡片,轮询自动停止 +事务内 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 ``` -worker 跑在 portal 进程内(goroutine pool + 优雅退出),不放进 go-admin 的 `app/jobs`,避免管理端重启影响生成。 +状态对外始终是 `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` **不 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`),预览列表不得直接加载原图。 +- `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` 必须持久化。 +- 输出文件先写临时文件并原子落位;图片必须有缩略图。原图、结果和缩略图访问均校验用户归属。 diff --git a/docs/03-business-rules-and-glossary.md b/docs/03-business-rules-and-glossary.md index 0f1c9db..3f85cfa 100644 --- a/docs/03-business-rules-and-glossary.md +++ b/docs/03-business-rules-and-glossary.md @@ -2,8 +2,8 @@ generated: true (请先修改 Gitea Wiki,禁止直接编辑本文件) wiki_page: Business-Rules-and-Glossary wiki_url: https://git.ilapage.cn/OPC/chorus/wiki/Business-Rules-and-Glossary.- -wiki_revision: 311d04cdb64461caa1d40a2e9943a58811d947bf -synchronized_at: 2026-08-20T03:29:26Z +wiki_revision: 1d0fc6a192ac3235477f6033b3f760d6630f12f7 +synchronized_at: 2026-08-20T07:01:13Z # 业务规则与术语 @@ -12,72 +12,81 @@ synchronized_at: 2026-08-20T03:29:26Z | 术语 | 含义 | |---|---| -| **Provider** | 一个上游服务商配置:`base_url`、加密后的 `api_key`、`api_type`、权重、启停。默认只支持 OpenAI 规范。 | -| **api_type** | provider 的调用形态,四种:`chat`(文生文)、`images`(纯文生图)、`images_edits`(图生图,多图上传)、`gemini`。 | -| **ProviderModel** | 某个 provider 下的一个具体模型:`model_id`、能力(text / image / vision)、`extra_body`、超时。选路的最小单位。 | -| **RoutePool(路由池)** | 管理端勾选的一组 ProviderModel,按 `kind`(text / image)分类,用于均衡与故障转移。 | -| **Generation(生成记录)** | 一次生成请求的完整记录,图生图与文生文**统一在一张表**,区别只在 `kind`。 | -| **kind** | 生成类型:`text`(文生文)或 `image`(图生图)。 | -| **role(图片角色)** | 每张上传原图的身份:`primary`(主体图)或 `reference`(参考图)。默认第一张为主体,其余为参考。 | -| **role_rule(角色规则)** | 描述“第几张图是什么”的一段文字。默认值来自管理端模板,用户端可编辑可清空。 | -| **rendered_prompt** | 实际发给上游的完整内容,由角色规则、每张图的 role 与 note、用户提示词合成。 | -| **attempts** | 本次生成的故障转移链路:`[{provider_model_id, err, ms}, …]`。 | -| **租约(lease)** | worker 取走任务后写入的 `lease_until`;过期未完成的任务可被其他 worker 重新取走。 | -| **点数(point)** | 用户余额,**只做展示**。余额存在 chorus 自有表,由管理端发放。 | +| **Provider** | 上游服务商账号配置,只持有名称、`base_url`、AES-GCM 密文 API Key、`key_id`、启停和审计信息。 | +| **ProviderModel** | Provider 下可选的模型绑定,是选路最小单位;持有 `model_id`、`api_type`、能力、`extra_body`、超时、权重和启停。 | +| **api_type** | 模型绑定的调用形态:`chat`、`images`、`images_edits`、`gemini`;不属于 Provider。 | +| **RoutePool** | 按 text/image 组织的一组 ProviderModel,用于加权和故障转移。 | +| **Generation** | 一次图或文生成;统一在 `generations`,以 `kind=text|image` 区分。 | +| **rendered_prompt** | 实际发给上游的完整提示词,提交后必须持久化。 | +| **attempts** | 脱敏、限长的尝试链路,至少包含 ProviderModel、分类错误、耗时和时间。 | +| **租约** | `lease_owner + lease_token + lease_until`;token 是最终写入 CAS 的所有权凭据。 | +| **幂等键** | 用户作用域内标识同一次提交;唯一约束为 `(user_id,idempotency_key)`。 | +| **prompt template** | 按 kind 配置的默认提示词模板;MVP-0 必须有数据配置,具体中性措辞待确认。 | +| **图片角色** | 第一张默认 primary,其余 reference;MVP-1 才允许编辑、备注和排序。 | +| **点数** | 用户余额,只读展示,不参与生成判断或事务。 | ## 工单状态 -沿用 DevHarness:待确认、已确认、开发中、待验收、已交付、已停止。状态含义与流转规则见 [开发工作流](Development-Workflow.-) 和 [产品需求总览](Product-Requirements-Overview.-)。 +单元任务状态沿用 DevHarness:**待确认、待实施、进行中、阻塞、待验收、已完成**。需求总览中的长期需求状态是另一套:**待确认、已确认、开发中、待验收、已交付、已停止**。两者不得混写;详细流转见 [开发工作流](Development-Workflow.-) 和 [产品需求总览](Product-Requirements-Overview.-)。 ## 稳定业务规则 -### 生成流程 +### 数据与约束 -1. 提交生成是**异步**的:接口立即返回,页面插入 loading 占位卡片,完成后原地替换为缩略图。提交链路不得同步调用上游。 -2. `generations.status` 状态机:`pending → running → succeeded | failed`。只允许沿箭头前进,不允许从终态回到 `running`;重试通过新的 attempt 记录在同一条记录上,`attempt_count` 递增。 -3. 租约到期是唯一允许把 `running` 拉回可取走状态的机制,且必须写入失败原因或递增 `attempt_count`,不得静默重放。 -4. 一次生成的 `rendered_prompt`、`attempts`、`latency_ms` 必须落库,失败时还要写 `error_code` 与 `error_message`。 -5. 成功的图片结果必须同时产出 256px 缩略图;没有缩略图的输出视为未完成。 -6. `idempotency_key` 相同的提交视为同一次生成,不重复创建记录。 +1. `users.email`(或最终选定的登录名)规范化后唯一;管理员仍使用独立 `sys_user`。 +2. ProviderModel 至少以 Provider、模型标识和 api_type 形成不重复绑定;所有外键、删除行为和软删除策略在首个迁移工单明确,不依赖 GORM 自动推断。 +3. `generations` 至少有 `(status,lease_until,created_at)` 取任务索引、`(user_id,created_at)` 历史索引和 `(user_id,idempotency_key)` 唯一约束。 +4. 输入、输出、attempt 与 generation 的关系使用显式外键/索引;生产表、`sys_*` 基线、菜单和 API 种子都由可逆 SQL 管理。 +5. `extra_body` 只允许 JSON 对象并设置大小上限;不能覆盖认证、目标 URL、模型标识和安全超时等保留字段。 -### 选路与失败处理 +### 生成与租约 -7. 候选来自路由池中启用且未熔断的 ProviderModel,按 `weight` 平滑加权后打乱顺序。 -8. 故障转移最多尝试 `maxFailover` 家(默认 3),每次尝试都要记入 `attempts`。 -9. `retryable` 判定:`429`、`5xx`、超时、连接错误 → 换下一家;`400`、`401`、内容策略拒绝 → 立即返回,不换家。这条规则写反的代价是烧光所有 provider 配额。 -10. 连续 N 次失败打开熔断,冷却后半开试探(gobreaker 默认语义)。熔断中的候选不参与选路。 -11. 按 provider 分池限流,避免单家并发超限触发 429。 +6. 提交异步返回;同步链路只校验、保存输入和写 pending,不调用上游。 +7. 状态机为 `pending → running → succeeded|failed`,终态不可回退。租约过期的 running 在原子认领中只替换 owner/token/until,不回写 pending。 +8. 最终写入必须同时匹配 generation id、`status=running` 和当前 lease_token;陈旧 worker 更新 0 行后丢弃结果并清理自己的临时文件。 +9. lease 时长大于“上游超时 + 下载/存储上限 + 安全余量”;续租若实现也必须校验 token。具体生产值在部署前确认。 +10. attempt_count、attempts 和租约更新保持原子一致。每次失败写分类后的 `error_code`、脱敏且截断的 `error_message`。 +11. 相同用户和 idempotency_key 返回原任务,不重复保存上传或创建任务;不同用户可以使用相同键。 +12. 图片成功结果必须有原结果和 256px 缩略图;文件先写同文件系统临时路径,校验完成后原子 rename。 -### 提示词与图片角色 +### 选路与上游 -12. 角色规则三层可覆盖,后一层覆盖前一层:管理端 `prompt_templates`(按 kind 区分,可多套)→ 用户端「高级设置」可编辑文本框(可改、可清空)→ 与每张图的 `role` / `note` 合成,最终写入 `rendered_prompt`。 -13. 用户提交的 `role_rule` 为空时使用管理端默认模板;不得把角色规则硬编码在代码里。 -14. 每张上传图可选填一句备注(例如“参考这张的配色”),备注参与提示词合成。 +13. MVP-0 直接取唯一启用的 ProviderModel。MVP-1 候选来自启用且未熔断的路由池,最多尝试可配置的 `maxFailover`。 +14. `429`、`5xx`、超时、连接错误可换下一家;`400`、`401`、内容策略拒绝立即失败。修改必须覆盖四类单元测试。 +15. 所有请求禁用不受控环境代理,或保证代理连接同样经过目标校验;每次 DNS 解析后、Dial 前拒绝回环、私网、链路本地、组播、未指定等 IPv4/IPv6 地址。 +16. 每次重定向重新验证 scheme、host、解析地址和端口并限制次数;上游响应中的结果 URL 按同一策略下载。只允许明确的 http/https,不允许凭 URL 绕过。 +17. 真实连通性测试由管理员明确点击,单次低成本调用,记录操作者/目标/结果并设置冷却;自动测试和日常调试只用 mock。 -### 点数与限流 +### 提示词与上传 -15. 点数**只读展示**:生成链路不做扣费、退款、余额校验,也没有每日配额。 -16. 管理端调整余额时**必须在同一事务内写 `point_ledger` 并记 `operator_id`**。虽不涉真钱,可追溯是底线。 -17. 去掉配额后的刹车是限流:用户维度令牌桶 + 同一用户并发生成数上限(默认 2),防止单人打满所有 provider 配额。 +18. MVP-0 从 `prompt_templates` 读取 kind 默认模板;第一张图 primary、其余 reference,按上传顺序合成并落 `rendered_prompt`。 +19. MVP-1 才增加用户 role_rule 覆盖、每图 role/note 和排序;覆盖顺序为管理端默认 → 用户覆盖 → 图片角色/备注。 +20. 默认模板具体措辞未确认前不得实现;不得沿用 cmhub 的电商专用文案冒充通用默认。 +21. 上传必须同时限制文件数、单文件/总大小、允许 MIME、实际解码格式和像素数,拒绝扩展名伪装与解压炸弹。阈值均可配置,生产值未确认前不能上线。 -### 安全与审计 +### 身份、安全和文件 -18. provider 的 `api_key` 必须 AES-GCM 加密落库,主密钥来自环境变量;明文不得进入日志、响应、工单或 Wiki。 -19. 所有对 provider `base_url` 的出站请求必须经过 SSRF 拦截(DNS 解析后、连接前)。 -20. 管理端的配置变更写 `config_audit_logs`,记录操作者、动作、目标和变更内容。 -21. 管理员(`sys_user`)与终端用户(`users`)分表,互不混用凭据。 +22. MVP-0 只提供受控种子/引导用户,不开放注册;未来注册、找回密码、邮件验证和反滥用需独立设计。 +23. 密码使用当前批准的强哈希方案及参数并支持升级;登录按账号和来源限速,响应不泄露账号是否存在。 +24. 浏览器写操作验证 CSRF;会话 Cookie 使用 `Secure`、`HttpOnly`、合适的 `SameSite`,登录后轮换 session id,登出使其失效。 +25. 原图、结果和缩略图不作为无鉴权公开静态目录;每次访问校验 generation 的用户归属。JSON/HTMX 均不得跨用户查询。 +26. Provider API Key 用 AES-GCM 加密,保存 key_id;明文不进日志、响应、工单、Wiki、原型或截图。轮换必须可区分旧/新密钥并可回退。 +27. 管理端配置、真实连通性测试、点数调整和密钥轮换写审计日志;错误和 attempts 只保留排障需要的信息。 -### 明确不做 +### 点数与范围 -计费扣点、充值与支付回调、汇率、软件授权与设备绑定、内容审核、cmshopee 专用端点、每日生成配额。这些不是“以后再说”,而是本服务的范围之外;有需要时须先建 Epic 重新确认。 +28. 点数只读展示,生成链路无扣费、退款、余额校验或配额。MVP-0 隐藏点数和 API Key 导航。 +29. 管理端未来调整余额时,同一事务写 `point_ledger` 和 operator_id。 +30. 明确不做:计费扣点、充值/支付、汇率、授权/设备绑定、内容审核、cmshopee 专用端点、每日生成配额。新增必须先建 Epic。 ## 新项目需要补充什么 -以下内容尚未确认,实施到对应阶段前必须由负责人拍板,不得由 Agent 假设: +以下值尚未确认,实施或上线前必须由负责人确认,Agent 不得猜测: -- **角色规则默认模板的措辞**:cmhub 那段是电商专用文案,作为 chorus 默认模板是否改为中性表述,取决于目标用户。 -- **点数来源**:当前定为自有表 + 管理端发放;若要与 cmhub 账本打通,需另设计同步或子账户方案。 -- **生成物保留期**:本地磁盘增长很快,保留期与清理策略必须在上线前定。 -- **注册赠送点数**:是否开启、赠送多少,做成一条系统配置。 -- **限流具体阈值**:令牌桶速率、并发上限的生产取值。 -- **上游具体名单**:首个可用于联调的 provider、模型和额度来源。 +- 默认中性 prompt template 的准确措辞和变量格式; +- 上传数量、单文件/总大小、MIME、像素上限; +- 上游/存储超时、lease 时长、安全余量、worker 数和最大重试; +- 首个 Provider/模型、真实连通性检查的最小请求与冷却时间; +- 生成物保留期、备份范围、磁盘告警和清理策略; +- 生产限流阈值及未来是否开放注册; +- 点数来源目前锁定为自有表与管理端发放;若与 cmhub 对接须另建 Epic。 diff --git a/docs/04-local-development-and-verification.md b/docs/04-local-development-and-verification.md index 50be2dd..3124ab8 100644 --- a/docs/04-local-development-and-verification.md +++ b/docs/04-local-development-and-verification.md @@ -2,122 +2,147 @@ generated: true (请先修改 Gitea Wiki,禁止直接编辑本文件) wiki_page: Local-Development-and-Verification wiki_url: https://git.ilapage.cn/OPC/chorus/wiki/Local-Development-and-Verification.- -wiki_revision: 47b4993d26695f8513c4b3212781b6c6f4c6540c -synchronized_at: 2026-08-20T03:29:28Z +wiki_revision: 4cb7c477267bfc28c8582e275e02e20acfc2c83e +synchronized_at: 2026-08-20T07:01:16Z # 本地开发与验证 -本页覆盖两类工作:**文档与流程**(现在就能跑)和 **Go 服务**(MVP-0 实现工单落地后可跑)。标注“MVP-0 落地后”的步骤在代码存在前会失败,这是预期的。 +本页区分“现在可执行的文档流程”和“产品代码落地后可执行的 Go/前端/数据库流程”。代码不存在或本机版本不满足时必须如实记录,不把预期当作通过。 ## 环境要求 -| 项 | 要求 | 检查命令 | +| 项 | 目标要求 | 检查命令 | |---|---|---| -| Git | 任意近期版本 | `git --version` | -| Python 3 | 仅标准库,供 Harness 工具使用 | `python --version` | -| Go | 1.22 或更高 | `go version` | -| MySQL | 8.0(本地或容器),需支持 `SKIP LOCKED` | `mysql --version` | -| golang-migrate | 命令行工具 | `migrate -version` | -| Tailwind CLI | standalone 可执行文件,无需 npm | `tailwindcss --help` | +| Git | 近期版本 | `git --version` | +| Python | Python 3 标准库 | `python --version` | +| Go | **1.26.5** | `go version` | +| MySQL | 8.0,支持 SKIP LOCKED | `mysql --version` | +| golang-migrate | 可执行文件 | `migrate -version` | +| Node | >=22,仅 admin-ui | `node --version` | +| pnpm | 9.15.1,由 packageManager/Corepack 固定 | `pnpm --version` | +| Tailwind | standalone,仅 portal 样式 | `tailwindcss --help` | -不需要 Node.js、Redis 或消息队列。用到的环境变量: +2026-08-20 盘点:本机 Go 1.23.0、MySQL 5.7.38、未安装 migrate,不能用于产品构建或迁移验收;Node 22.22.1 可用,但当前全局 pnpm 11.21.0,应按 admin-ui 的 `packageManager` 使用 pnpm 9.15.1。升级环境必须单独完成,不伪造验证结果。 -| 变量 | 用途 | 缺失后果 | -|---|---|---| -| `CHORUS_DSN` | MySQL 连接串 | 服务无法启动 | -| `CHORUS_MASTER_KEY` | provider api_key 的 AES-GCM 主密钥 | 无法解密上游密钥,生成全部失败 | -| `GITEA_TOKEN` | Wiki 同步与工单操作 | 只影响文档同步,不影响服务 | +固定来源: -凭据只通过环境变量提供,不写入仓库。 +- `D:\github\goadmin\go-admin` @ `f06540883b41d03782bb6b2c4150f298f328c6b6` +- `D:\github\goadmin\go-admin-ui` @ `67d393d713877572fab0b897296a4c1d525fc81d` +- `D:\github\goadmin\go-admin-doc` @ `424855aacf6905f3fde860c3331385cb25529a0d` + +只从这些固定提交审查/导入,不直接在其工作区开发 chorus,也不让 chorus 构建依赖绝对路径。 + +主要环境变量: + +| 变量 | 用途 | +|---|---| +| `CHORUS_DSN` | MySQL 连接 | +| `CHORUS_MASTER_KEY` / 对应 key ring 配置 | Provider Key AES-GCM 解密与轮换 | +| `CHORUS_SESSION_KEY` | portal 会话,必须与主密钥分离 | +| `CHORUS_STORAGE_ROOT` | 受保护生成物目录 | +| `CHORUS_ENV` | `development/test/production`,生产强制安全开关 | +| `GITEA_TOKEN` | Wiki/工单操作,不影响产品服务 | ## 第一次运行 -### 1. 检查工作区与文档结构 +### 1. 文档与工作区 ```powershell git status --short --branch python dev_scripts/harness.py check --strict python -m unittest discover -s tests -v +$env:GITEA_URL = "https://git.ilapage.cn" +python dev_scripts/harness.py sync --check ``` -预期:分支干净、输出“DevHarness 检查通过”、所有测试通过。 +### 2. 准备隔离数据库(产品代码落地后) -### 2. 准备数据库(MVP-0 落地后) +创建两个无生产数据的库:一个跑应用迁移测试,一个用于 go-admin 结构研究/代码生成。两者不得指向共享或生产实例。 ```powershell -mysql -u root -p -e "CREATE DATABASE chorus DEFAULT CHARSET utf8mb4;" -$env:CHORUS_DSN = "user:pass@tcp(127.0.0.1:3306)/chorus?parseTime=true&charset=utf8mb4" -migrate -path migrations -database "mysql://$env:CHORUS_DSN" up +mysql -u root -p -e "CREATE DATABASE chorus_test DEFAULT CHARSET utf8mb4;" +mysql -u root -p -e "CREATE DATABASE chorus_codegen DEFAULT CHARSET utf8mb4;" +$env:CHORUS_DSN = "user:pass@tcp(127.0.0.1:3306)/chorus_test?parseTime=true&charset=utf8mb4" +migrate -path migrations -database "" up +migrate -path migrations -database "" down +migrate -path migrations -database "" up ``` -预期:迁移版本前进,`generations` 等表已创建。 +连接串格式必须按 golang-migrate 的 MySQL driver 验证,不把 GORM DSN 直接拼成未经验证的 URL。每个迁移先 up、down、再 up;从空库重放全部迁移才算通过。 -### 3. 配置一个上游(MVP-0 落地后) +### 3. go-admin schema-first 生成(MVP-1) -MVP-0 阶段管理端尚未接入,用一次性命令写入一个 provider 与 provider_model: +1. 先在 `chorus_codegen` 执行同一套版本化 SQL。 +2. 启动仅限本机的 admin/admin-ui 开发实例。 +3. 从数据库表列表导入业务表元数据,配置并预览生成结果。 +4. 生成 Go/Vue 文件后检查输出路径、字段类型、敏感字段、权限和重复代码。 +5. 不直接采用“生成迁移脚本”或 `/gen/todb` 的副作用;把菜单/API 配置整理成带 down 的 SQL。 +6. 清空隔离库,从 migrations 重放并重新生成/构建。 +7. 生产构建验证 dev-tools 路由不存在;仅隐藏菜单不算关闭。 -```powershell -$env:CHORUS_MASTER_KEY = "<32 字节 base64 主密钥>" -go run ./cmd/seedprovider --name dev --base-url https://<上游> --api-type chat --model <模型名> -``` +go-admin AutoMigrate 不进入以上标准流程。如确需对照固定提交初始化结构,人工在额外可丢弃库运行一次,导出结构差异后销毁;禁止连接共享/生产库,也禁止把该命令放入应用启动、容器入口或部署脚本。 -预期:命令输出新建的 provider_model id;数据库中 `api_key_enc` 是密文而非明文。 +### 4. 配置 mock 上游与种子用户(MVP-0) -### 4. 启动服务并走通一次生成(MVP-0 落地后) +MVP-0 不开放注册。使用可重复、不会在日志输出密钥的种子命令建立测试用户、默认 prompt template、Provider 和 ProviderModel。默认先连接 mock HTTP 上游,覆盖文本、图片、429、5xx、超时、连接错误、400、401 和内容策略拒绝。 + +真实 Provider 的连通性检查只能由操作者明确触发一次低成本请求,并核对审计与冷却;不能作为自动测试或反复调试方式。 + +### 5. 启动 portal(MVP-0) ```powershell go run ./portal ``` -预期日志包含监听端口与“worker started”。然后在浏览器打开 `http://127.0.0.1:8080`: +浏览器验证: -1. 注册或用种子用户登录,进入双栏首页; -2. 切到「文生文」,输入一句提示词,点生成; -3. 左栏立即出现 loading 卡片,右栏可见状态; -4. 数秒后卡片原地替换为结果,轮询停止(响应中不再有 `hx-trigger`); -5. 切到「图生图」,上传 1~2 张图,标记主体图与参考图,再生成一次; -6. 数据库中该条 `generations` 的 `status=succeeded`,`rendered_prompt` 非空,`attempts` 有一条记录,`generation_outputs.thumb_path` 有值。 - -**第 6 步是 MVP-0 的验收口径**:页面看到结果只是表象,落库字段齐全才算流程真正跑通。 +1. 用种子用户登录,不应出现公开注册链接; +2. 桌面双栏、375px 移动单列均可完成文本和图片提交; +3. 第一张图自动 primary,其余 reference,MVP-0 没有角色编辑/拖拽; +4. 提交后立即显示 queued/running,终态停止轮询; +5. 文本、图片、失败、网络轮询错误和认证过期都有明确界面; +6. 数据库中 `rendered_prompt`、`attempts`、latency 或失败 error 字段齐全; +7. 输出通过鉴权访问,不能猜 URL 读取其他用户文件。 ## 常用调试方式 -| 症状 | 先看哪里 | +| 症状 | 先看 | |---|---| -| 卡片一直转圈 | `generations.status`。仍是 `pending` 说明 worker 没取到任务;`running` 且 `lease_until` 已过期说明 worker 崩了或卡在上游 | -| 结果不对但没报错 | `generations.rendered_prompt`——多数“模型不听话”其实是提示词合成错了 | -| 生成失败 | `generations.attempts` 与 `error_message`,能看出是哪一家、什么错误、耗时多少 | -| 上游连不上 | 先确认不是 SSRF 拦截命中(日志中的拦截记录),再查 `base_url` 与密钥解密 | -| 页面样式丢失 | Tailwind CLI 是否在 watch,`portal/web/static/` 产物是否存在 | +| pending 不动 | worker 是否启动、取任务索引和数据库版本 | +| running 租约过期 | lease_owner/token/until、上游超时、陈旧 worker CAS | +| 成功但内容不对 | rendered_prompt 与 prompt template 版本 | +| 400/401 尝试多家 | retryable 实现错误 | +| 文件存在但页面 403/404 | 用户归属、原子落位、缩略图记录 | +| 上游连不上 | SSRF 日志、DNS/IPv6、redirect、proxy 和 base_url | +| 管理端生成异常 | 是否先迁移再导表,是否误把菜单脚本当业务 DDL | +| 样式丢失 | portal Tailwind 产物或 admin-ui pnpm/Vue CLI 构建 | -调试上游时用 mock HTTP 服务代替真实上游:`internal/core/provider` 的测试已提供可复用的 mock,能构造 429、超时、400 和内容拒绝四种响应。不要用真实额度反复试错。 +不要在日志、SQL、工单或 Wiki 中贴 API Key、Cookie、真实用户输入和生产文件。 -单条 SQL 快速定位: +## 测试策略 -```sql -SELECT id, kind, status, provider_model_id, attempt_count, error_code, latency_ms -FROM generations ORDER BY id DESC LIMIT 10; -``` +- 迁移:空 MySQL 8 隔离库逐个/全量 up-down-up,验证索引、外键、唯一约束和种子幂等。 +- 队列:并发认领、租约过期重新认领、旧 token 最终写 0 行、优雅退出、attempt 原子性。 +- Provider:全部 retryable 类别使用 mock,不消耗真实额度。 +- SSRF:IPv4/IPv6 私网、DNS rebinding、redirect 链、环境代理、结果 URL。 +- 安全:密码/session/CSRF、登录节流、跨用户任务与文件、错误脱敏、密钥轮换。 +- 浏览器:HTMX 动态状态、终态停止、认证过期、网络错误;375/768/1024,无主区域横向滚动,键盘和 reduced-motion。 +- 供应链:Go/Node 依赖锁定、漏洞和许可证检查按实现工单确定命令。 ## 完成修改前 -按顺序执行,全部通过才算完成: - ```powershell git status --short --branch go build ./... go vet ./... go test ./... +pnpm --dir admin-ui install --frozen-lockfile +pnpm --dir admin-ui build:prod python dev_scripts/harness.py check --strict python -m unittest discover -s tests -v python dev_scripts/harness.py sync --check +git diff --check ``` -另外确认: - -- 改了表结构 → 有对应 `migrations/` 文件,且 up 与 down 各跑过一次; -- 改了 `retryable`、SSRF、加解密 → 有针对性单元测试; -- 改了页面结构、流程、状态、权限或异常处理 → 有 `prototypes/<工单号>/<版本>/index.html` 审核快照并已确认; -- 改了入口、命令、业务规则或排错方式 → 已评估对 `docs/` 的影响; -- 未执行或无法覆盖的验证 → 已写进工单,不得默认“应该没问题”。 +只运行受当前范围影响且实际存在的产品命令;不存在或因工具版本不能执行的项写入工单。涉及迁移、安全、队列、权限或 UI 时,还必须执行上面的专项验证并记录结果。 diff --git a/docs/05-common-changes.md b/docs/05-common-changes.md index 93f6a14..adf591c 100644 --- a/docs/05-common-changes.md +++ b/docs/05-common-changes.md @@ -2,100 +2,115 @@ generated: true (请先修改 Gitea Wiki,禁止直接编辑本文件) wiki_page: Common-Changes wiki_url: https://git.ilapage.cn/OPC/chorus/wiki/Common-Changes.- -wiki_revision: becf3fdead14674cb74a7a48c4792c06eb9d454b -synchronized_at: 2026-08-20T03:29:30Z +wiki_revision: ec313fafcc8df6060e650e3d9ddf95be8d7eb63f +synchronized_at: 2026-08-20T07:01:20Z # 常见修改指南 -本页面向接手简单维护的初级程序员。每一类修改都给出改哪里、验证什么、什么时候必须停下来。 +本页面向接手维护的初级程序员。先判断风险,再按工单范围修改;数据库、权限、密钥、SSRF、队列和生产代码生成都不是“顺手调整”。 ## 风险分级 | 级别 | 例子 | 处理方式 | |---|---|---| -| **低** | 页面显示文案、注释、文档措辞、日志文本 | 可以直接改,跑最小验证 | -| **中** | 现有页面的样式与布局微调、新增一个只读字段展示、增加一条日志 | 建单,按本页步骤改,跑完整验证 | -| **高** | 选路与 `retryable` 判定、熔断参数、SSRF 校验、密钥加解密、数据库迁移、队列租约、身份与权限、点数余额写入、删除数据或不可逆操作 | **停止**,交给 Agent 分析并等待人工确认方案 | +| 低 | 不改变含义/布局/可访问性的显示文案,注释,文档措辞 | 最小验证;符合豁免时可不建单 | +| 中 | 已确认页面的小样式、只读字段、普通日志、mock Provider 配置 | 建单并运行受影响测试 | +| 高 | retryable、SSRF、加密/轮换、迁移、租约/CAS、身份权限、点数写入、删除/清理 | 停止,Agent 分析并等待人工确认 | -判断不了级别时按高风险处理。“看起来只改一行”不是低风险的理由——`retryable` 的一行就能烧光所有上游配额。 +风险按影响判断,不按行数判断。`internal/core` 新增 Gin/go-admin/gobreaker/imaging import 也是高风险边界破坏。 ## 修改用户端显示文案 -1. 在 `portal/web/templates/` 中定位文案所在模板。 -2. 只改用户看到的文字,不要顺手改变量名、模板名、CSS 类名或字段名。 -3. 验证:`go build ./...`,启动 `go run ./portal`,在浏览器确认该页面文字与布局。 +1. 在 `portal/web/templates/` 定位文案。 +2. 不改模板名、字段、国际化键、流程暗示、布局或可访问性。 +3. 启动 portal,在 375/768/1024 宽度检查文字不溢出、不遮挡且状态含义准确。 -纯显示文案且不改变语义、流程、权限、状态、接口、布局或可访问性时不需要建单,但仍要做最小界面检查。改的是错误提示的**含义**、按钮的**行为暗示**或任何影响用户判断的措辞时,按中风险建单处理。 +错误提示、按钮含义或会影响用户判断的文字不是纯显示豁免,必须建单。 -## 修改角色规则默认模板 +## 修改 prompt template -角色规则不是代码常量,是数据:改 `prompt_templates` 表中对应 `kind` 的默认模板,或通过管理端页面修改。 +默认模板是 `prompt_templates` 数据,不是 Go 常量: -- **不要**把角色规则写回 Go 代码里——三层覆盖(管理端模板 → 用户端可编辑框 → 每张图 role/note)是已确认的设计。 -- 改完用一次真实生成验证,检查 `generations.rendered_prompt` 是否如预期。 +- MVP-0 只读取 kind 默认模板,第一张图 primary、其余 reference; +- 中性默认措辞尚未确认,不能自行沿用 cmhub 电商文案; +- MVP-1 才允许用户 role_rule、每图 role/note 和排序; +- 修改后优先用 mock 生成并核对 `rendered_prompt`,真实上游只做一次经授权的低成本验收。 -## 增加或调整一个上游 Provider +## 增加或调整 ProviderModel -1. 优先通过管理端页面配置(管理端接入前用种子命令,见 [本地开发与验证](Local-Development-and-Verification.-))。 -2. 确认 `api_type` 选对:文生文用 `chat`,图生图用 `images_edits`。 -3. 保存后用「连通性测试」打一次真实请求验证;没有该按钮时手工提交一次生成。 -4. `api_key` 只填进配置,不要出现在工单、截图或日志里。 +1. Provider 只配置 `base_url` 和加密 Key;在 ProviderModel 选择 `api_type`、模型、能力、`extra_body` 和超时。 +2. 验证 URL 会经过 DNS/DialContext、redirect、IPv6、代理和结果 URL 防护。 +3. 自动验证使用 mock。管理端“连通性测试”必须由操作者点击、单次低成本、带审计和冷却。 +4. API Key 不得出现在参数回显、响应、日志、截图、工单或 Wiki。 -新增的是一种**上游协议形态**(而非一家新服务商)时属于高风险:需要在 `internal/core/provider/` 新增实现,必须建单并补齐 mock 测试。 +新增协议形态必须建高风险工单,补齐 retryable 与 SSRF 测试。 + +## 数据库与 go-admin 代码生成 + +标准流程: + +```text +写 migrations up/down → 空 MySQL 8 up/down/up +→ 隔离 codegen 库执行 up → go-admin-ui 导入已有表 +→ 配置/预览/生成 → 人工审查 +→ 菜单/API 配置转成可逆 SQL → 从空库全量重放 +``` + +- 不在应用启动、部署或生产执行 AutoMigrate。 +- AutoMigrate 只可由人工在额外可丢弃数据库研究固定提交结构,用完销毁。 +- “生成迁移脚本”不是业务 DDL;`/gen/todb` 会改菜单,`/gen/toproject` 会写源码。 +- 生成文件不是可信输入:检查敏感字段、权限、路由、路径、重复代码和无关格式化。 +- 生产必须不注册 dev-tools 路由,隐藏菜单不够。 ## 修改 Wiki 文案 -长期文档的事实来源是 Gitea Wiki,`docs/` 是只读镜像(Wiki 初始化完成后生效)。 +长期文档以 Gitea Wiki 为事实来源: ```text -修改 Wiki → 读取确认 → 导出 docs → 校验差异 → 提交镜像 +修改 Wiki → 在线回读 revision → sync 导出 docs → sync --check → 提交 ``` ```powershell +$env:GITEA_URL = "https://git.ilapage.cn" python dev_scripts/harness.py sync python dev_scripts/harness.py sync --check ``` -不要直接编辑带 `generated: true` 头的本地文件。Wiki 尚未初始化的阶段允许直接改 `docs/`,但必须在工单说明,并在初始化时把内容搬到 Wiki。 +不得直接编辑带 `generated: true` 的镜像。新建项目专用页面时更新 `wiki-docs.json` 和 Home 导航;若要把它提升为所有项目强制核心文档,再同步修改 Harness 与成功/失败测试。 ## 调整 Harness 检查 -`dev_scripts/harness.py` 的 `check` 子命令负责校验必需文件、核心文档章节和工单模板字段。 +1. 修改 `CORE_DOCUMENT_REQUIREMENTS`、`REQUIRED_FILES` 或模板字段。 +2. 同时补成功和失败用例。 +3. 执行: -1. 修改 `CORE_DOCUMENT_REQUIREMENTS`、`REQUIRED_FILES` 或模板字段列表。 -2. 在 `tests/` 中同时补充**成功用例和失败用例**——只有成功用例的检查等于没有检查。 -3. 验证: +```powershell +python -m unittest discover -s tests -v +python dev_scripts/harness.py check --strict +``` - ```powershell - python -m unittest discover -s tests -v - python dev_scripts/harness.py check --strict - ``` - -新增核心文档时必须同时更新 `wiki-docs.json` 映射、`docs/README.md` 导航和 Harness 检查,三者缺一会导致同步或检查失败。 +不要为了让检查变绿而削弱安全、Wiki 主源或任务归档边界。 ## 看懂 Agent 的修改 -审查 Agent 提交时按这个顺序看: - -1. **范围**:改动文件是否都落在工单声明的范围内?出现工单没提到的文件要问清楚。 -2. **边界**:`internal/core` 里有没有混进 gin 或 go-admin 的 import?生成链路有没有碰点数? -3. **状态机**:`generations.status` 的写入是否只沿 `pending → running → succeeded | failed` 前进? -4. **失败路径**:`retryable` 的分支有没有写反?失败时 `attempts`、`error_code`、`error_message` 是否都落库? -5. **迁移**:表结构变更有没有对应 SQL 文件?down 能不能跑? -6. **测试**:新增逻辑有没有测试,测试是否覆盖失败分支而不只是happy path。 -7. **文档**:入口、命令、规则变了,`docs/` 是否同步评估过。 - -任何一条答不上来就退回让 Agent 解释,不要凭“测试过了”放行。 +1. **范围**:文件与工单一致,无无关重构。 +2. **依赖**:core 只含 GORM/标准库;platform 通过接口注入。 +3. **同步链路**:提交不调用上游、不读写点数。 +4. **状态**:不把 running 退回 pending;最终写入校验 lease_token,旧 worker 更新 0 行。 +5. **错误**:retryable 四类正确,attempts/error 脱敏落库。 +6. **安全**:请求/redirect/result URL 都走 SSRF;文件访问校验用户归属;浏览器写操作有 CSRF。 +7. **迁移**:所有表和配置都有 up/down、空库验证,无 AutoMigrate。 +8. **生成代码**:差异已人工审查,生产 dev-tools 不存在。 +9. **UI**:完整状态、375/768/1024、键盘、焦点、44px、reduced-motion。 +10. **证据**:测试是真实执行结果,未验证项写入工单。 ## 必须停止的情况 -遇到以下情况停止修改,交给 Agent 分析并等待人工确认: - -- 需要改选路、熔断、`retryable`、SSRF、加解密、队列租约中的任意一项; -- 需要新增或修改数据库迁移; -- 需要触碰身份认证、权限或 API Key; -- 需要写入点数余额或 `point_ledger`; -- 需要删除数据、清理生成物或执行任何不可逆操作; -- 修改会改变已确认原型的页面结构、流程、状态、权限或异常处理; -- 你不确定这属于哪一级风险。 +- 修改 retryable、熔断、SSRF、密钥、租约/CAS、身份权限或迁移; +- 执行 AutoMigrate、`/gen/todb`、生产代码生成或手工改共享/生产库; +- 写点数、删除数据/文件、运行清理任务或做不可逆回退; +- 调整上传/超时/租约/保留期限等未确认生产阈值; +- 改变已确认原型的结构、流程、状态、权限或异常处理; +- 真实上游测试可能反复消耗额度; +- 无法判断风险或同一位置两次仍无根因。 diff --git a/docs/06-troubleshooting.md b/docs/06-troubleshooting.md index b20a7eb..b5687b2 100644 --- a/docs/06-troubleshooting.md +++ b/docs/06-troubleshooting.md @@ -2,86 +2,114 @@ generated: true (请先修改 Gitea Wiki,禁止直接编辑本文件) wiki_page: Troubleshooting wiki_url: https://git.ilapage.cn/OPC/chorus/wiki/Troubleshooting -wiki_revision: 9400aa641e95bccd3903efb6dd935c06a59048e8 -synchronized_at: 2026-08-20T03:29:32Z +wiki_revision: d37761a10ea18c62e0cd0680e65d9b462d79b4f5 +synchronized_at: 2026-08-20T07:01:24Z # 故障排查 ## 排查顺序 -遇到问题按下面的顺序走,**不要跳步,也不要直接重置工作区或覆盖本地文档**。 - -### 1. 确认环境与工作区 +### 1. 环境与工作区 ```powershell git status --short --branch go version +mysql --version +migrate -version +node --version +pnpm --version ``` -工作区有不属于当前任务的修改时先处理干净,否则后面的判断都不可靠。 +产品构建目标为 Go 1.26.5、MySQL 8.0、Node >=22、pnpm 9.15.1。工具缺失或版本不符先记录环境问题;不要通过降低文档版本或改代码绕过。 -### 2. 判断问题落在哪一段 +### 2. 判断同步还是异步 -chorus 的请求分成同步提交和异步执行两段,绝大多数问题只在其中一段: - -| 现象 | 大概率在哪段 | +| 现象 | 首查 | |---|---| -| 点了生成没反应、报 4xx | 同步段(`portal/handler`) | -| 卡片出现了但一直转圈 | 异步段(queue / worker) | -| 结果出来了但内容不对 | 提示词合成(`generate/prompt.go`) | -| 结果失败并有错误码 | provider 调用或选路 | +| 提交 4xx、没有卡片 | portal 认证/CSRF/上传/幂等 | +| 卡片出现但一直 queued | worker、MySQL 版本、取任务索引 | +| running 超时 | lease owner/token/until、上游和存储超时 | +| succeeded 但无内容 | output 记录、原子文件、鉴权/缩略图 | +| 内容不符 | rendered_prompt 与模板 | +| 管理端 CRUD 不符 | migrations、导表元数据和生成差异 | -### 3. 查库,不要靠猜 +### 3. 查库但不手工改状态 ```sql -SELECT id, kind, status, provider_model_id, attempt_count, - error_code, error_message, lease_until, latency_ms, created_at, finished_at -FROM generations ORDER BY id DESC LIMIT 10; +SELECT id, user_id, kind, status, provider_model_id, attempt_count, + error_code, lease_owner, lease_token, lease_until, + latency_ms, created_at, finished_at +FROM generations +ORDER BY id DESC +LIMIT 10; ``` -| 观察到 | 含义与下一步 | +- pending 不动:worker 未启动、数据库不支持 SKIP LOCKED 或索引/事务错误。 +- running 且租约过期:新 worker 应直接原子替换 token,不应先改回 pending。 +- 同一任务出现互相覆盖:检查最终 UPDATE 是否匹配当前 lease_token;陈旧 worker 应影响 0 行。 +- 400/401/策略拒绝却有多 Provider attempts:retryable 写反。 +- 429/5xx/超时/连接错误没有故障转移(MVP-1):候选池、上限或熔断状态错误。 +- failed 没有 error,或 succeeded 没有 rendered_prompt/attempts:违反持久化红线。 + +`lease_token` 属运行控制信息,不在对外响应或普通日志完整输出。排查记录使用任务 id 和脱敏摘要。 + +### 4. 用 mock 隔离上游 + +用 mock 构造成功、429、5xx、超时、连接错误、400、401、内容拒绝和恶意结果 URL。mock 正常而真实异常,再查 ProviderModel、网络和上游;禁止用真实额度循环试错。真实连通性只通过受审计且有冷却的按钮触发。 + +### 5. SSRF 与网络 + +依次检查: + +1. base_url scheme/host/port; +2. DNS 的每个 IPv4/IPv6 结果; +3. DialContext 是否拒绝私网、回环、链路本地、组播和未指定地址; +4. 每次 redirect 是否重新校验并限制次数; +5. 环境代理是否绕开安全 Dial; +6. 上游返回的结果 URL 是否复用相同客户端。 + +拦截私网是正确行为,不能为联调关闭。需要访问内部 mock 时把 mock 与测试进程放在专用测试网络,并使用测试专用注入点,不在生产配置放行私网。 + +### 6. 文件和权限 + +- 检查临时文件与最终目录是否同文件系统,rename 是否原子; +- 检查失败/陈旧 worker 是否只清理自己创建的临时文件; +- 检查数据库 output 与实际文件一致; +- 403 多为用户归属校验,404 多为记录/文件不一致; +- 不得临时把存储目录改成无鉴权静态目录。 + +### 7. go-admin 与代码生成 + +| 现象 | 处理 | |---|---| -| `status=pending` 且长时间不动 | worker 没起来或没连上库;看 portal 启动日志有没有 “worker started” | -| `status=running` 且 `lease_until` 已过期 | worker 崩了或卡在上游;查上游超时设置与进程日志 | -| `status=failed`,`error_code` 是 429/5xx | 上游限流或故障;看 `attempts` 是否按预期换了下一家 | -| `status=failed`,`error_code` 是 400/401 | 参数或密钥问题;**不应该**发生故障转移,若 `attempts` 有多条说明 `retryable` 写反了 | -| `status=succeeded` 但页面无图 | 查 `generation_outputs` 的 `path` 与 `thumb_path`,再查存储目录权限 | -| `rendered_prompt` 与预期不符 | 角色规则模板或三层覆盖顺序的问题,不是模型的问题 | +| 生成器看不到表 | 先确认隔离库已执行 migrations,再导入元数据 | +| “迁移脚本”没有建表 | 它主要生成菜单/API 配置,不是业务 DDL | +| 生成后菜单立刻变化 | 可能调用了副作用 `/gen/todb`;停止并恢复隔离库 | +| 生成文件落错目录 | 检查固定提交的 generator 配置,人工审查差异 | +| 生产能访问 dev-tools | 高风险发布阻断;下线路由而非只隐藏菜单 | +| 启动时表结构变化 | 检查是否误接 AutoMigrate,立即停止部署 | -### 4. 隔离上游 - -用 mock 上游重跑一次(`internal/core/provider` 的测试 mock 可构造 429、超时、400、内容拒绝)。mock 下正常、真实上游异常,问题在配置或网络;mock 下同样失败,问题在代码。 - -不要拿真实额度反复试错。 - -### 5. 检查出站是否被 SSRF 拦截 - -`base_url` 指向私网、回环地址或解析结果落在私网时会被 `internal/core/security` 拦截,表现是“连不上但网络看起来正常”。日志中有拦截记录。**拦截生效是正确行为**,要改的是配置,不是拦截规则。 - -### 6. 文档与 Harness 相关问题 +### 8. 文档与 Harness | 症状 | 处理 | |---|---| -| `harness.py check --strict` 报缺少章节 | 按提示补回该标题,标题文字必须完全一致 | -| `harness.py check --strict` 报“项目档案仍有未填写内容” | `docs/00-project-profile.md` 里还有 `<填写` 占位符 | -| `sync --check` 报不一致 | 先确认是 Wiki 更新了还是本地被手工改了;本地被改过要恢复后重新同步 | -| `sync` 中止并提示存在未提交修改 | 已映射镜像有未提交改动,先提交或还原 | -| Wiki 页面读不到、没有 revision | 停止初始化或同步,等 Gitea 恢复后从首个失败页面继续 | +| strict 缺章节 | 恢复精确标题 | +| sync --check 不一致 | 先判断 Wiki 是否更新;不要直接改镜像 | +| sync 因本地镜像改动停止 | 保留用户改动,查清来源后处理 | +| Wiki 无 revision | 停止同步,从失败页面继续 | +| API 401 | 从安全环境重新加载有效 PAT,不打印令牌 | +| MCP 连接失败 | 在工单记录原因后回退 API | -### 7. 仍未定位 +### 9. 仍未定位 -在工单中记录:最小复现步骤、`generations` 的相关行、`attempts` 内容、相关日志片段(**去掉密钥**)、已排除的可能性。然后交给 Agent 分析。 +在工单记录最小复现、任务 id、脱敏 attempts/error、租约时间线、相关日志和已排除项。不得粘贴 API Key、Cookie、真实 prompt、用户文件或生产数据。 ## 必须停止的情况 -以下情况立即停止自行处理,交给 Agent 分析并等待人工确认: - -- 需要修改 `retryable`、熔断参数、选路逻辑或 SSRF 规则; -- 需要修改数据库迁移,或需要手工改生产/共享库中的数据; -- 需要触碰密钥、加解密、身份认证、权限或 API Key; -- 需要写入点数余额或 `point_ledger`; -- 需要删除生成物、清理表数据或任何不可逆操作; -- 需要绕过失败重跑任务(可能造成重复计费或重复生成); -- 日志或报错中出现疑似泄露的密钥、个人数据或生产数据——先停止传播,不要把原文贴进工单或 Wiki; -- 你已经在同一处试了两次仍不确定根因。 +- 需要修改 retryable、SSRF、密钥、租约、迁移、身份或权限; +- 需要手工改共享/生产库、回退状态、删除数据或文件; +- 想通过 AutoMigrate、关闭 SSRF、公开存储或暴露 dev-tools 临时绕过; +- 需要反复真实上游调用; +- 日志疑似泄密或包含个人/生产数据; +- 同一阻塞尝试两次仍不能确认根因。 diff --git a/docs/09-product-requirements-overview.md b/docs/09-product-requirements-overview.md index a1fee37..2c128cc 100644 --- a/docs/09-product-requirements-overview.md +++ b/docs/09-product-requirements-overview.md @@ -2,8 +2,8 @@ generated: true (请先修改 Gitea Wiki,禁止直接编辑本文件) wiki_page: Product-Requirements-Overview wiki_url: https://git.ilapage.cn/OPC/chorus/wiki/Product-Requirements-Overview.- -wiki_revision: 33319b0f3d34796ade13e4146f28cc5e5eb6d1d1 -synchronized_at: 2026-08-20T03:29:42Z +wiki_revision: 4c7c9352f77e5972be4fa51f0b86025d9d08ea53 +synchronized_at: 2026-08-20T07:01:33Z # 产品需求总览 @@ -29,107 +29,135 @@ synchronized_at: 2026-08-20T03:29:42Z ## 当前需求索引 -chorus 尚未建立 Gitea 工单,下表的“实施工单”列在建单后填入编号。全部需求当前状态均为**待确认或已确认但未开工**。 +工单 [#1](https://git.ilapage.cn/OPC/chorus/issues/1) 正在更新本项目长期技术基线,不代表产品功能已经进入开发。产品功能仍需按下表分别完成设计门禁并建立单元工单。 -| 需求领域 | 用户与场景 | 状态 | 版本 / MVP | 详细说明 | 实施工单 | 原型 | 验收入口 | -|---|---|---|---|---|---|---|---| -| 核心生成域与单上游打通 | 用户提交提示词(+原图)得到结果;先跑通再谈可用性 | 已确认,未开工 | MVP-0 | [架构与代码地图](Architecture-and-Code-Map.-)、[业务规则](Business-Rules-and-Glossary.-) | 待建单 | 无(非 UI 需求,用架构与数据设计代替) | 待建单 | -| 用户端生成页(双栏 + 异步卡片) | 终端用户在浏览器完成一次生成并看到结果 | 已确认,未开工 | MVP-0 | 本页“用户端布局”一节、[架构与代码地图](Architecture-and-Code-Map.-) | 待建单 | **待制作**:新页面,需 HTML 审核快照并经确认 | 待建单 | -| 多 Provider 路由池与故障转移 | 单一上游失效时服务仍可用 | 已确认,未开工 | MVP-1 | [业务规则](Business-Rules-and-Glossary.-) 选路与失败处理 | 待建单 | 无(非 UI 需求) | 待建单 | -| 管理端配置与记录查看 | 运营配置 Provider/Model、勾选组池、查看生成记录 | 已确认,未开工 | MVP-1 | 本页“管理端页面”一节 | 待建单 | 代码生成器页面无需原型;三个定制页需低保真图 | 待建单 | -| 图片角色规则可编辑 | 用户按自己的场景描述“第几张图是什么” | 已确认,未开工 | MVP-1 | [业务规则](Business-Rules-and-Glossary.-) 提示词与图片角色 | 待建单 | 属现有页面新增组件,需组件状态说明 | 待建单 | -| 点数只读展示与管理端发放 | 用户看到余额;管理员发放并留痕 | 已确认,未开工 | MVP-2 | [业务规则](Business-Rules-and-Glossary.-) 点数与限流 | 待建单 | 沿用现有设计体系 | 待建单 | -| API Key 与程序化调用 | 外部程序按 API Key 调用生成 | 已确认,未开工 | MVP-2 | 本页“程序化调用”一节 | 待建单 | 无(接口需求,用 API 设计代替) | 待建单 | -| 限流与生成物保留期 | 防止单人打满上游配额;控制磁盘增长 | 待确认(阈值与保留期未定) | MVP-2 | [业务规则](Business-Rules-and-Glossary.-)“新项目需要补充什么” | 待建单 | 无 | 待建单 | +| 需求领域 | 用户与场景 | 需求状态 | MVP | 详细说明 | 实施工单 | 设计证据 | +|---|---|---|---|---|---|---| +| 核心生成域与单上游 | 用户提交提示词/原图得到结果 | 已确认 | MVP-0 | [架构](Architecture-and-Code-Map.-)、[业务规则](Business-Rules-and-Glossary.-) | 待建 | 架构/数据/状态设计 | +| 用户端生成页 | 种子用户登录并完成一次异步生成 | 已确认 | MVP-0 | 本页“用户端布局与状态” | 待建 | 新页面 HTML 原型待制作/确认 | +| 默认 prompt template | 系统以数据配置而非硬编码合成提示词 | 待确认(具体措辞) | MVP-0 | [业务规则](Business-Rules-and-Glossary.-) | 待建 | 文案与变量设计待确认 | +| 多 Provider 路由与故障转移 | 单上游故障时继续服务 | 已确认 | MVP-1 | 业务规则“选路与上游” | 待建 | 架构/状态设计 | +| 管理端配置与记录 | 运营配置模型、路由池并排障 | 已确认 | MVP-1 | 本页“管理端页面” | 待建 | 复用型/定制页原型待确认 | +| 图片角色编辑 | 用户编辑 role_rule、角色、备注与顺序 | 已确认 | MVP-1 | 业务规则“提示词与上传” | 待建 | 组件状态与键盘交互原型 | +| 点数只读与发放 | 用户看余额、管理员发放留痕 | 已确认 | MVP-2 | 业务规则“点数与范围” | 待建 | 原型待确认 | +| API Key 与程序调用 | 外部程序提交与查询任务 | 已确认 | MVP-2 | 本页“程序化调用” | 待建 | API/权限设计 | +| 限流、保留期、公开注册 | 治理滥用和磁盘;决定是否开放用户获取 | 待确认(阈值/流程) | MVP-2 或后续 | 业务规则“需要补充什么” | 待建 | 安全/运维/认证设计 | ### MVP 分期 -分期原则:**先让一条数据从提交走到结果全程可运行**,再补可用性,最后补运营与治理能力。前一期没跑通不进入下一期。 +原则:先完成可观测、可恢复、安全的单上游闭环,再增加可用性与运营能力,最后开放治理能力。前一期的验收门禁未通过,不进入下一期。 -#### MVP-0:跑通全流程的最小闭环(当前阶段) +#### MVP-0:最小闭环 范围: -1. 迁移建表:`users`、`providers`、`provider_models`、`generations`、`generation_inputs`、`generation_outputs`; -2. `internal/core`:`chat` 与 `images_edits` 两种 provider 实现、AES-GCM 密钥加解密、SSRF 拦截、本地存储、缩略图; -3. 队列:`SELECT … FOR UPDATE SKIP LOCKED` 取任务 + 租约 + 失败置错; -4. 提示词合成:角色规则 + 每张图 role/note + 用户提示词 → `rendered_prompt` 落库; -5. `portal`:登录、双栏首页、提交生成、HTMX 每 2 秒轮询卡片、成品卡片不带 `hx-trigger` 从而自动停止轮询; -6. worker 跑在 portal 进程内,支持优雅退出; -7. 上游配置用一次性种子命令写入,管理端不接入。 +1. 可逆迁移:`users`、`providers`、`provider_models`、`prompt_templates`、`generations`、`generation_inputs`、`generation_outputs`,含明确索引、外键和唯一约束。 +2. core 仅使用 GORM/标准库;platform 提供安全 HTTP、本地存储、缩略图和 AES-GCM 实现。 +3. `chat` 与 `images_edits`;唯一启用 ProviderModel,不做多家故障转移。 +4. SKIP LOCKED、lease owner/token/until、过期 running 原子重领、最终 CAS、attempt/error 落库。 +5. `prompt_templates` 提供已确认的默认配置;第一张图片自动 primary,其余 reference。 +6. portal 只提供受控种子用户登录、桌面双栏与移动单列、异步提交、HTMX 轮询和鉴权结果访问。 +7. worker 在 portal 内运行并优雅退出;配置使用可重复种子命令,自动测试连接 mock 上游。 -非目标(属于后续期,不是被取消):加权选路、熔断、故障转移、`images` 与 `gemini` 两种 api_type、管理端全部页面、API Key、点数、限流、无限滚动分页、清理任务。 +MVP-0 非目标: -验收口径(缺一不算通过): +- 公开注册、找回密码和邮件验证; +- 图片角色/备注编辑与拖拽排序; +- 点数和 API Key 导航/功能; +- 多 Provider、熔断、故障转移、`images`、`gemini`; +- admin/admin-ui、无限滚动、移动抽屉、限流、清理任务。 -- [ ] 文生文与图生图各成功一次,浏览器可见结果; -- [ ] 对应 `generations` 行的 `status=succeeded`、`rendered_prompt` 非空、`attempts` 有记录、`latency_ms` 有值; -- [ ] 图生图结果的 `generation_outputs.thumb_path` 有值且缩略图可访问; -- [ ] 上游返回 400 时该条记录 `status=failed`、`error_code`/`error_message` 落库,且**没有**发生故障转移; -- [ ] 上游超时时租约到期后任务可被重新取走,`attempt_count` 递增; -- [ ] provider 的 `api_key` 在库中是密文; -- [ ] `base_url` 指向私网时被 SSRF 拦截并有日志; -- [ ] `go build ./...`、`go vet ./...`、`go test ./...` 全部通过。 +验收: -#### MVP-1:可用性与运营入口 +- [ ] 文本和图片各成功一次;pending/running/终态界面正确且终态停止轮询。 +- [ ] `rendered_prompt`、`attempts`、latency 或失败 error 字段完整。 +- [ ] 相同用户幂等重放不创建第二条任务;不同用户不可读取对方任务或文件。 +- [ ] 过期 running 被新 token 原子重领,旧 worker 最终写入影响 0 行且不覆盖结果。 +- [ ] 400/401/策略拒绝立即失败;429/5xx/超时/连接错误分类正确(MVP-0 不换家)。 +- [ ] 私网、IPv6、redirect、代理和恶意结果 URL 被 SSRF 策略覆盖。 +- [ ] API Key 密文含 key_id,日志/响应无明文;浏览器写请求有 CSRF,会话安全。 +- [ ] 图片输出通过鉴权访问且有缩略图;文件原子落位。 +- [ ] 375/768/1024 无主区域横向滚动,所有主要操作可用键盘和 44px 触控目标完成。 +- [ ] 迁移在空 MySQL 8 完成 up/down/up;Go 构建、vet、测试和浏览器 E2E 通过。 -路由池与加权选路、gobreaker 熔断与故障转移(含 `retryable` 判定的完整测试)、`images` 与 `gemini` 实现、go-admin 接入与代码生成器 CRUD、三个定制页(路由池编排、Provider 健康看板、生成记录详情)、Provider 连通性测试按钮、角色规则模板三层覆盖的管理端层、生成列表无限滚动。 +#### MVP-1:可用性与运营 -#### MVP-2:治理与开放能力 +- 路由池、加权、gobreaker 与最多 N 家故障转移,完整 retryable 测试; +- `images`、`gemini`; +- 固定 go-admin/go-admin-ui 的 schema-first CRUD 和三个定制页; +- 图片 role_rule、role/note、拖拽及键盘等价排序; +- 生成历史无限滚动; +- 受审计、有冷却的 Provider 单次真实连通性测试。 -API Key 管理与 openapi 端点、点数只读展示与管理端发放(含 `point_ledger` 事务)、用户维度令牌桶与并发上限、按 provider 分池限流、配置审计日志、生成物保留期清理任务、移动端抽屉布局。 +#### MVP-2:治理与开放 + +- API Key/openapi、点数只读与发放账本; +- 用户和 Provider 维度限流、配置审计; +- 经确认的保留期与清理任务; +- 移动端历史抽屉; +- 公开注册只有在注册、验证、找回、反滥用和赠送策略单独确认后才进入范围,不能默认随 MVP-2 开放。 ### 已锁定决策 -以下决策已确认,不在实施阶段重新讨论;要改必须先建 Epic: +1. Go 重写,只迁移设计,不复用 cmhub Django 代码。 +2. 管理端来源固定为: + - go-admin `f06540883b41d03782bb6b2c4150f298f328c6b6` + - go-admin-ui `67d393d713877572fab0b897296a4c1d525fc81d` + - go-admin-doc `424855aacf6905f3fde860c3331385cb25529a0d` +3. go-admin-ui 实际是 Vue 3.5.41 + Element Plus 2.14.4 + Vue CLI 5.0.9,不按 Vue 2/Vite 设计。 +4. 生产表结构、`sys_*` 和菜单/API 种子全部经可逆 migrations;AutoMigrate 仅可在隔离可丢弃库作研究参考。 +5. 管理端代码生成采用“SQL → 隔离库 → 导表 → 预览/生成 → 人工审查 → 配置转可逆 SQL”;生产无 dev-tools。 +6. 用户端采用 html/template + HTMX + Alpine + Tailwind;不做 SPA。 +7. core 只依赖 GORM/标准库,gobreaker/imaging 在 platform。 +8. 不引入 Redis/MQ,使用 MySQL 8 SKIP LOCKED + token/CAS。 +9. 点数只读,不参与生成;管理员和终端用户分表。 +10. 生成同步提交不调用上游,所有上游调用只在 worker。 +11. `api_type` 和协议能力归 ProviderModel,不归 Provider。 -1. 用 Go 重写,不做 Django 代码抽取;带走的是设计(provider 抽象、任务租约、SSRF 校验、密钥加密),不是代码。 -2. 管理端用 go-admin + go-admin-ui。 -3. 用户端不用前端框架:html/template 服务端渲染 + HTMX + Alpine.js + Tailwind。 -4. 点数只做展示:无扣费、无退款、无余额校验、无配额;余额来自自有表,管理端发放。 -5. 图片角色规则从硬编码改为用户端可编辑提交,默认值来自管理端模板。 -6. 上游默认只支持 OpenAI 规范;可勾选多个 provider 的 model 做均衡 + 故障转移。 -7. 不引入 Redis / MQ,任务队列用 MySQL 8.0 `SELECT … FOR UPDATE SKIP LOCKED`。 +### 用户端布局与状态 -### 用户端布局 +桌面(>=1024px)采用历史列表 + 主工作区双栏;平板可收窄历史区;MVP-0 手机使用单列顺序布局,不依赖尚未实现的抽屉。首屏必须能看到当前结果/状态和输入动作,固定元素不能遮住滚动内容。 -参考 ChatGPT 的双栏结构: +MVP-0 导航只显示已实现的生成入口、必要账户操作和退出;点数/API Key 不显示空入口。图片区只显示上传顺序,第一张自动主体,其余参考,不展示不可用的角色/拖拽控件。 -```text -┌──────────────────────────────────────────────────────┐ -│ Logo 生成 记录 [点数] [API Key] [头像▾] │ ← 导航栏 -├──────────────┬───────────────────────────────────────┤ -│ 预览图列表 │ 结果展示区 │ -│ ┌──┐┌──┐ │ (大图 / 生成的文本,可下载、可复制) │ -│ └──┘└──┘ │ │ -│ ┌──┐┌──┐ ├───────────────────────────────────────┤ -│ └──┘└──┘ │ [图生图 | 文生文] 切换 │ -│ 无限滚动 │ [+ 上传原图,缩略图条,可排序/标角色] │ -│ │ ┌─────────────────────────┐ ┌──────┐ │ -│ │ │ 提示词输入框(多行) │ │ 生成 │ │ -│ │ └─────────────────────────┘ └──────┘ │ -└──────────────┴───────────────────────────────────────┘ -``` +| 状态 | 必须呈现 | +|---|---| +| 首次/空历史 | 可直接开始的输入区和简洁空状态,不伪造示例结果 | +| 上传校验失败 | 对应文件、明确原因、修正方式;焦点到首个错误 | +| pending | “排队中”,可区分尚未执行 | +| running | “生成中”,轮询更新但布局不跳动 | +| succeeded image | 缩略图、查看/下载;原图访问经鉴权 | +| succeeded text | 可读文本和复制动作,复制结果有反馈 | +| failed | 脱敏错误、是否可重试的明确操作 | +| 轮询网络错误 | 保留当前内容,提示重试,不把任务误标 failed | +| 认证过期 | 停止轮询,引导重新登录,登录后返回原上下文 | +| 历史到底 | 明确结束,不持续显示 loading | -- 左侧是本人已生成结果的预览列表,点击查看大图;右侧是结果展示 + 类型切换 + 上传区 + 提示词 + 生成按钮。 -- 导航栏含用户信息、点数余额(只读)、API Key 管理入口。 -- 生成为异步:提交后左侧插入 loading 占位卡片,完成后原地替换为缩略图。 -- 每张上传图带角色下拉(主体图 / 参考图,默认第一张主体、其余参考),可拖拽排序,可选填一句备注。 -- 移动端左栏折叠为抽屉(MVP-2)。 +交互验收: -三个核心交互的实现方式:无限滚动用 `hx-trigger="revealed"`;任务轮询用 `hx-trigger="every 2s"`,完成后返回不带该属性的成品卡片即自动停止;上传区、角色排序和 tab 切换用 Alpine 的 `x-data` 管局部状态,拖拽可加 SortableJS。 +- 375、768、1024px 和桌面宽屏均无不合理横向滚动、遮挡和文字溢出; +- 交互目标至少 44×44px,输入有持久 label,错误不只靠颜色; +- 图标按钮使用既有 Lucide/Element Plus 图标并有可访问名称; +- 所有功能可键盘操作;MVP-1 拖拽提供上移/下移等价操作; +- 路由/片段替换后管理焦点,状态更新使用合适的 `aria-live`,toast 不抢焦点; +- 动效尊重 `prefers-reduced-motion`,loading 不造成布局位移; +- 页面实现前必须完成版本化 HTML 原型并由用户确认状态、响应式、权限和异常覆盖。 ### 管理端页面 -- **代码生成器直出**:`providers`、`provider_models`、`prompt_templates`、`point_accounts`、`users`、`api_keys`。 -- **需手写的三个定制页**:路由池编排(候选 model 穿梭框 + 权重滑块 + 拖拽排序)、Provider 健康看板(24h 成功率 / p95 延迟 / 熔断状态 / 最近错误)、生成记录详情(原图、结果图、`rendered_prompt`、`attempts` 故障转移链路)。 -- **Provider 连通性测试**按钮:保存后一键打真实请求验证,省掉大量配置调试时间。 +代码生成基于固定 go-admin-ui 的既有表导入: -定制页超过三个时必须重新评估是否放弃 go-admin-ui 改为自建服务端渲染后台。 +- CRUD:providers、provider_models、prompt_templates、users;point_accounts/api_keys 到对应 MVP 再生成。 +- 定制页:路由池编排、Provider 健康、生成记录详情。 +- 标准 CRUD 可用一个明确复用固定 go-admin 视觉/交互的代表性原型覆盖同类页面;定制页分别覆盖加载、空、错误、权限和边界状态。 +- 生成记录默认不展示 API Key、完整敏感错误或不必要的真实用户输入。 +- 定制页数量明显超过三个时重新评估维护成本,但不以先前错误的前端版本判断作为依据。 + +Provider 连通性测试是明确的运营动作:保存配置后由授权管理员点击,发出一次最小低成本请求,显示脱敏结果,记录审计并限制冷却;它不是保存时自动触发,也不用于 CI。 ### 程序化调用 -外部程序用 API Key(哈希比对,身份落在 `users`)调用 `portal` 的 openapi 端点提交生成与查询状态。接口形态在 MVP-2 的设计工单中确定,届时本节补链接。 +MVP-2 的 API Key 存哈希、身份仍属于 `users`。提交、查询、幂等、错误码、限流和跨用户授权在独立 API 设计工单确认;浏览器 Cookie/CSRF 与 API Key 认证链不得混用。 ### 明确不做 @@ -137,12 +165,12 @@ API Key 管理与 openapi 端点、点数只读展示与管理端发放(含 `p ### 风险与待定 -- 项目已定名 `chorus`(原方案代号 `cmgen` 作废)。 -- go-admin 社区活跃度与 Vue 2 EOL:定制页越多升级成本越高。 -- 选 MySQL 是为走 go-admin 主路径;代价是 `capabilities` 这类数组字段只能用 JSON 列或关联表表达,查询不如 Postgres `TEXT[]` 直接。 -- 角色规则默认模板的通用性:cmhub 那段是电商专用文案,是否改中性表述待定。 -- 点数来源:当前定为自有表 + 管理端发放;与 cmhub 账本打通需另设计。 -- 生成物存储增长:本地磁盘会涨得很快,保留期策略要在上线前定。 +- 默认中性 prompt template 的准确措辞和变量格式; +- 上传数量/大小/MIME/像素、超时/lease/worker、限流的生产值; +- 首个真实 Provider/模型和连通性测试最小请求; +- 生成物保留期、备份、磁盘告警、RPO/RTO; +- 未来公开注册及账号验证/找回/反滥用流程; +- 固定 go-admin 版本后续升级策略;升级必须重新审查生成器权限和技术栈,不能静默漂移。 ## 登记规则 diff --git a/docs/10-deployment-and-operations.md b/docs/10-deployment-and-operations.md new file mode 100644 index 0000000..5addebb --- /dev/null +++ b/docs/10-deployment-and-operations.md @@ -0,0 +1,142 @@ + +generated: true (请先修改 Gitea Wiki,禁止直接编辑本文件) +wiki_page: Deployment-and-Operations +wiki_url: https://git.ilapage.cn/OPC/chorus/wiki/Deployment-and-Operations.- +wiki_revision: 040f4c2c3e16def91fe612fa09cf0a6aa7288445 +synchronized_at: 2026-08-20T07:01:36Z + + +# 部署与运维 + +## 本页用途 + +本页定义 chorus 常驻服务的生产部署基线。当前尚无产品代码和真实生产环境,命令是后续实现必须满足的运维契约;域名、容量、保留期和阈值在首发工单确认后补充,不能由 Agent 猜测。 + +## 安全边界 + +- 生产只部署已审核制品和 `migrations/`;不从 `D:\github\goadmin` 构建,不运行 AutoMigrate。 +- dev-tools 生成路由、`/gen/todb` 和写源码能力不得注册或被 nginx 代理;仅隐藏菜单不合格。 +- DSN、会话密钥、Provider 主密钥/key ring、TLS 私钥只来自受控环境文件或密钥管理系统,权限最小化,不写入 Git/Wiki/日志。 +- 原图、结果和缩略图保存在受保护目录,通过 portal 鉴权读取,不由 nginx 直接暴露。 +- 数据迁移、回退、删除、清理和密钥轮换均是高风险操作,必须有工单、备份和人工确认。 + +## 服务概览 + +```text +Internet + → nginx/TLS + → chorus-portal 127.0.0.1:8080(页面、HTMX、JSON、MVP 内嵌 worker) + → chorus-admin 127.0.0.1:8081(MVP-1) + → /admin-ui 静态构建产物(MVP-1) +portal/admin → MySQL 8.0 +portal worker → Provider(安全 DialContext) +portal → /var/lib/chorus/storage(或后续对象存储适配) +``` + +| 项目 | 基线 | +|---|---| +| 运行账号 | 独立无登录 `chorus` 用户 | +| 制品目录 | `/opt/chorus/releases//`,`/opt/chorus/current` 指向当前版本 | +| 配置 | `/etc/chorus/chorus.env`,仅运行账号可读 | +| 持久数据 | `/var/lib/chorus/storage`,不随版本目录切换 | +| 日志 | journald 或平台日志;结构化且脱敏 | +| 进程托管 | systemd:`chorus-portal.service`、`chorus-admin.service` | +| 数据库 | MySQL 8.0,独立最小权限账号 | +| 构建 | CI 使用 Go 1.26.5;admin-ui 使用 Node >=22 + pnpm 9.15.1 | + +## 配置与凭据 + +| 配置 | 用途 | 敏感 | +|---|---|---| +| `CHORUS_ENV=production` | 启用生产安全门禁 | 否 | +| `CHORUS_DSN` | MySQL | 是 | +| `CHORUS_MASTER_KEY` / key ring | Provider Key 加解密与轮换 | 是 | +| `CHORUS_SESSION_KEY` | portal 会话 | 是 | +| `CHORUS_STORAGE_ROOT` | 生成物目录 | 否 | +| 上游/存储超时、lease、worker 数 | 执行与优雅退出 | 否 | +| 上传和限流阈值 | 数量、大小、MIME、像素、请求频率 | 否 | +| 保留期与磁盘告警 | 清理和容量保护 | 否 | + +生产启动必须校验必需配置、密钥长度/key_id、目录权限、超时与 lease 关系;缺失或不安全时 fail closed。`CHORUS_ENV=production` 下若检测到 AutoMigrate 或 dev-tools 注册必须拒绝启动。 + +## 制品构建 + +在干净 CI 环境按固定提交来源导入后的 chorus 代码构建: + +```bash +go version +go test ./... +go vet ./... +go build -trimpath -o dist/chorus-portal ./portal +go build -trimpath -o dist/chorus-admin ./admin +corepack pnpm --dir admin-ui install --frozen-lockfile +corepack pnpm --dir admin-ui build:prod +``` + +MVP-0 没有 admin/admin-ui 时跳过对应命令并在发布记录说明。制品记录 chorus commit、Go module 校验、admin-ui lockfile 和固定 go-admin 来源提交。 + +## 首次部署与升级顺序 + +1. 记录当前/目标 commit、迁移版本、制品校验值和回退条件。 +2. 备份 MySQL,并验证备份可读取;确认持久存储快照/备份策略。 +3. 把制品解压到新的 release 目录,不原位覆盖当前版本。 +4. 用相同 MySQL 8 版本的隔离库执行全部 migration up/down/up 验证。 +5. 停止取新任务并等待在途 worker 到优雅退出期限;旧 worker 未结束时不得启动会竞争同一租约的错误版本。 +6. 在生产执行 `migrate ... up`;检查版本,禁止任何 AutoMigrate。 +7. 原子切换 `current`,启动/重启 portal 和 admin。 +8. 执行健康、就绪、登录、提交 mock/受控测试和权限检查。 +9. 验证生产访问 dev-tools 路径为 404/未注册,并核对路由清单。 +10. 观察错误率、租约过期、磁盘和数据库连接;满足观察窗口后完成发布记录。 + +## 健康与就绪 + +应用必须提供: + +- `/healthz`:进程存活,不依赖上游; +- `/readyz`:必需配置有效、MySQL 可用、迁移版本匹配、存储可写、生产 dev-tools 未注册; +- admin 健康路径不得泄露版本、DSN、路由或密钥。 + +```bash +systemctl status chorus-portal +curl --fail http://127.0.0.1:8080/healthz +curl --fail http://127.0.0.1:8080/readyz +journalctl -u chorus-portal -n 100 --no-pager +``` + +MVP-1 同样检查 admin 8081。外部检查经 TLS/nginx 访问,不把内部端口暴露公网。 + +## worker 优雅退出 + +- 收到 TERM 后先停止认领新任务; +- 在途任务只在 lease_token 仍有效时提交; +- systemd 的停止等待时间大于应用优雅退出期限; +- 到期未完成时退出,让租约自然过期,新 worker 原子重领; +- 不把 running 改回 pending,不无条件标 failed,不删除其他 worker 文件。 + +## 存储、备份与监控 + +必须备份 MySQL 和仍在保留期内的生成物元数据/文件,并定期做恢复演练。至少监控: + +- MySQL 连接、慢查询、迁移版本; +- pending 数/最老等待时间、running 租约过期数、失败率; +- Provider 429/5xx/超时和熔断状态; +- 存储总量、可用空间、临时文件增长、缩略图失败; +- 登录失败/节流、SSRF 拦截和越权拒绝; +- portal/admin 健康与重启次数。 + +保留期、备份频率、RPO/RTO、磁盘阈值尚未确认,是生产上线门禁。清理任务不得在确认前启用。 + +## 回滚 + +- 优先使用向后兼容迁移并回滚二进制;先停止新任务,再切回上一 release,重启并执行健康检查。 +- 不能仅通过切回代码撤销数据库变更。需要 down 或恢复备份时先评估数据丢失,取得人工确认。 +- 若新版本已写入旧版本无法识别的数据,停止回滚并采用前滚修复或已演练的备份恢复方案。 +- 回滚后检查 migration version、陈旧 worker CAS、任务重复、文件一致性和 dev-tools 路由。 +- 每次真实发布前至少在隔离环境演练相同升级与回滚步骤。 + +## 已知限制 + +- 尚未完成真实 Linux/systemd/nginx 部署、备份恢复和回滚演练; +- 高可用、多机 worker、对象存储和滚动升级尚未设计; +- 生产域名、TLS、容量、保留期、告警、RPO/RTO 和具体超时/lease 阈值待首发部署工单确认; +- 在这些门禁完成前,本页不能作为“已验证可上线”的证据。 diff --git a/docs/README.md b/docs/README.md index 4bd79f8..1f520cc 100644 --- a/docs/README.md +++ b/docs/README.md @@ -2,90 +2,74 @@ generated: true (请先修改 Gitea Wiki,禁止直接编辑本文件) wiki_page: Home wiki_url: https://git.ilapage.cn/OPC/chorus/wiki/Home -wiki_revision: 202cba82085a11c32c439aa84d6b57e8e0347456 -synchronized_at: 2026-08-20T03:31:00Z +wiki_revision: 3d6efad79056ff85f7532e28a90c80f2c710c332 +synchronized_at: 2026-08-20T07:00:57Z # chorus 文档中心 -chorus 是从 cmhub 抽出、用 Go 重写的独立生图生文服务:用户提交提示词(可带原图)得到新图或文本,运营在管理端配置上游模型并查看生成记录。开发过程遵循 DevHarness——Gitea 工单管理任务、Wiki 管理长期文档、Git 记录代码变更、人由人确认与验收。 +chorus 是从 cmhub 设计中抽取并用 Go 重写的独立生图生文服务:用户提交提示词(可带原图)得到图片或文本,运营在管理端配置上游并查看生成记录。Gitea 工单记录任务过程,Wiki 是长期文档事实来源,Git 记录代码与镜像。 -当前处于 **MVP-0:跑通全流程的最小闭环**。目标不是把功能做全,而是先让一条数据从提交走到结果全程可运行、可验证。 +当前处于 **MVP-0:安全、可观测地跑通单上游闭环**。文档基线由工单 [#1](https://git.ilapage.cn/OPC/chorus/issues/1) 更新;这不表示产品功能已经开工。 ## 第一次阅读 -建议按以下顺序,用 10~20 分钟建立整体认识: - -1. [项目档案](Project-Profile.-):项目目标、技术栈、环境、命令和目录边界。 -2. [产品需求总览](Product-Requirements-Overview.-):MVP 分期、已锁定决策、验收口径。 -3. [架构与代码地图](Architecture-and-Code-Map.-):两条执行路径、从哪个文件开始读。 -4. [业务规则与术语](Business-Rules-and-Glossary.-):Provider、路由池、生成状态机和不能破坏的规则。 -5. [本地开发与验证](Local-Development-and-Verification.-):怎样跑起来、怎样确认真的跑通了。 -6. [常见修改指南](Common-Changes.-):简单修改的步骤和停止条件。 -7. [故障排查](Troubleshooting):出错时按什么顺序查。 -8. [开发工作流](Development-Workflow.-):完整建单、实施、验收和归档流程。 +1. [项目档案](Project-Profile.-):固定 go-admin 来源、工具链、目录和安全边界。 +2. [产品需求总览](Product-Requirements-Overview.-):MVP 分期、UI 状态和验收。 +3. [架构与代码地图](Architecture-and-Code-Map.-):同步/worker 路径、租约 CAS、代码生成边界。 +4. [业务规则与术语](Business-Rules-and-Glossary.-):数据、状态、SSRF、身份和排除范围。 +5. [本地开发与验证](Local-Development-and-Verification.-):目标环境、隔离库、测试策略。 +6. [常见修改指南](Common-Changes.-):修改步骤和停止条件。 +7. [故障排查](Troubleshooting):按证据定位环境、队列、上游和生成器问题。 +8. [部署与运维](Deployment-and-Operations.-):生产迁移、服务、健康、备份和回退。 +9. [开发工作流](Development-Workflow.-):建单、确认、实施、验收与归档。 ## 五分钟开始 -在仓库根目录执行: - ```powershell git status --short --branch python dev_scripts/harness.py check --strict python -m unittest discover -s tests -v +$env:GITEA_URL = "https://git.ilapage.cn" +python dev_scripts/harness.py sync --check ``` -预期结果: - -- 工作区没有不属于当前任务的修改; -- 输出“DevHarness 检查通过”; -- 所有测试通过。 - -MVP-0 的实现工单落地后,再加上: - -```powershell -go build ./... -go vet ./... -go test ./... -go run ./portal -``` - -预期:编译通过、测试通过、服务监听 `:8080` 且日志显示 worker 已启动。完整的“走通一次生成”步骤见[本地开发与验证](Local-Development-and-Verification.-)。 - -如果失败,先看[故障排查](Troubleshooting),不要直接重置工作区或覆盖本地文档。 +MVP-0 产品代码落地且环境达到 Go 1.26.5/MySQL 8.0 后,再执行 `go build ./...`、`go vet ./...`、`go test ./...` 和 `go run ./portal`。当前本机版本差距见本地开发页,不能把无法执行的命令记为通过。 ## 简单修改从哪里开始 -| 想做什么 | 先读哪里 | 主要验证 | +| 想做什么 | 先读 | 主要验证 | |---|---|---| -| 改用户端页面文案 | Common-Changes、`portal/web/templates/` | 浏览器最小界面检查 | -| 改角色规则默认模板 | Business-Rules、`prompt_templates` 表 | 一次真实生成,核对 `rendered_prompt` | -| 加一个上游 Provider | Common-Changes | 连通性测试或一次真实生成 | -| 看懂一次生成为什么失败 | Troubleshooting | `generations.attempts` 与 `error_message` | -| 改文档 | 对应 Wiki 页面、Common-Changes | Wiki 同步检查 | -| 查看或更新产品需求 | Product-Requirements-Overview | 状态、链接和事实来源核对 | -| 增加 Harness 检查 | `dev_scripts/harness.py` | 成功与失败用例都要有 | +| 改纯显示文案 | Common-Changes、模板 | 375/768/1024 最小界面检查 | +| 改默认 prompt template | Business-Rules | mock 生成并核对 rendered_prompt | +| 加 ProviderModel | Common-Changes | mock 协议 + SSRF;真实测试仅单次人工触发 | +| 排查生成失败 | Troubleshooting | attempts/error/lease 时间线 | +| 使用 go-admin 生成 CRUD | Architecture、Local-Development | migration → 隔离库 → 导表 → 生成差异审查 | +| 改长期文档 | 对应 Wiki、Common-Changes | 回读 revision + sync --check | +| 部署或回退 | Deployment-and-Operations | 迁移、健康、dev-tools、备份证据 | +| 更新需求 | Product-Requirements | 状态、工单、原型和验收入口 | -选路与 `retryable` 判定、熔断、SSRF、密钥加解密、数据库迁移、队列租约、权限、点数写入、删除数据或不可逆操作**不属于简单修改**,必须停止并交给 Agent 分析、等待人工确认。 +retryable、SSRF、密钥、迁移、租约/CAS、身份权限、点数写入、AutoMigrate、代码生成副作用、删除/清理和不可逆操作都不是简单修改。 ## 事实来源 | 信息 | 事实来源 | |---|---| -| 任务状态、讨论、阻塞、验收过程 | Gitea 工单 | -| 长期产品需求的统一导航和状态 | Product-Requirements-Overview | -| 架构、业务规则、开发规范、操作手册、交付文档、任务归档 | Gitea Wiki | -| 数据库表结构(跨交付单元的共享契约) | `migrations/` 中的 SQL | -| 源码和与特定代码版本强绑定的文档 | Git 仓库 | -| 已确认的界面与交互 | `prototypes/<工单号>/<版本>/index.html` | -| 核心长期文档的离线浏览副本 | Git 仓库中的 `docs/` Wiki 镜像 | +| 单元任务状态、讨论、阻塞和验收 | Gitea 工单 | +| 长期需求导航 | Product-Requirements-Overview | +| 架构、规则、开发、排错、部署、归档 | Gitea Wiki | +| 生产数据库结构和配置种子 | `migrations/` 可逆 SQL | +| 源码和固定版本资料 | chorus Git;外部来源提交记录在 Project-Profile | +| 已确认界面与交互 | `prototypes/<工单号>/<版本>/index.html` | +| 离线核心文档 | Git 中的 `docs/` Wiki 只读镜像 | -线上 Wiki 已于 2026-08-20 初始化完成,`docs/` 已转为只读镜像。长期文档必须先改 Wiki、读取确认,再导出镜像。 +`D:\github\goadmin` 是已审查来源,不是运行时事实来源。线上 Wiki 已初始化;长期文档必须先改 Wiki、回读 revision,再同步镜像。 ## 项目入口 - [Gitea 工单](https://git.ilapage.cn/OPC/chorus/issues) - [产品需求总览](Product-Requirements-Overview.-) +- [部署与运维](Deployment-and-Operations.-) - [代码仓库](https://git.ilapage.cn/OPC/chorus) - [新项目文档初始化](New-Project-Documentation-Setup.-) - [交付文档指南](Delivery-Documentation-Guide.-) @@ -94,11 +78,10 @@ go run ./portal ## 同步原则 ```text -修改 Wiki → 读取确认 → 导出 docs → 校验差异 → 提交镜像 +修改 Wiki → 在线回读 revision → 导出 docs → 校验差异 → 提交镜像 ``` -- 核心页面与本地路径通过 `wiki-docs.json` 显式映射;普通同步不处理任务归档。 -- 镜像头记录来源页面、Wiki revision 和同步时间;带 `generated: true` 的文件不得手工编辑。 -- 已映射镜像存在未提交修改时同步必须停止。 -- 页面删除、重命名和映射变更必须人工确认。 -- 凭据、个人数据和生产数据不得进入 Wiki、镜像、原型或工单。 +- `wiki-docs.json` 显式映射核心和项目专用页面,普通同步不处理任务归档。 +- 带 `generated: true` 的文件不得手工编辑;有未提交镜像修改时同步停止。 +- 页面删除、重命名和事实源变化必须先更新工单并确认。 +- 凭据、个人数据、真实 prompt、生成文件和生产数据不得进入文档、工单或原型。 diff --git a/wiki-docs.json b/wiki-docs.json index b441944..956d9a3 100644 --- a/wiki-docs.json +++ b/wiki-docs.json @@ -15,6 +15,7 @@ { "page": "New-Project-Documentation-Setup", "path": "docs/07-new-project-documentation-setup.md" }, { "page": "Existing-Project-Adoption-Guide", "path": "docs/08-existing-project-adoption.md" }, { "page": "Product-Requirements-Overview", "path": "docs/09-product-requirements-overview.md" }, + { "page": "Deployment-and-Operations", "path": "docs/10-deployment-and-operations.md" }, { "page": "Delivery-Documentation-Guide", "path": "docs/delivery/README.md" }, { "page": "Audience-Document-Template", "path": "docs/delivery/audience-document-template.md" }, { "page": "Deployment-Template", "path": "docs/templates/deployment.md" },