整体架构设计方法详解(GitHub 文档翻译工具模块拆解)

第一版把所有逻辑堆进一个接口:登录、仓库读取、模型调用、文件写回全在同一个路由里跑。功能能通,但一到真实场景就崩——用户装了多个 GitHub App、仓库授权关系对不上、翻译跑到一半失败要重试,这时候随便改一处都要牵连一片。后来我按”GitHub 集成层、认证授权层、任务编排层、AI 翻译层、数据持久层、前端交互层”拆开重写,问题才真正落地解决。关键收益是边界清晰:GitHub App 负责受控访问仓库,NextAuth.js v5 负责用户会话,Prisma 负责状态落库,OpenRouter 负责模型统一入口,翻译任务由独立编排器串联拉取、切分、翻译、审核与提交。这篇文章记录的就是这次重构的完整思路,供做同类”读仓库、调模型、写回 PR”工具的人直接参考。

一、架构分层的核心思路

这类工具不是简单的”把英文 Markdown 丢给大模型”。真实场景中,它要知道用户是谁、用户安装了哪个 GitHub App、哪些仓库被授权、哪些文件需要翻译、翻译进度如何恢复、失败任务如何重试,以及模型输出如何落回 Pull Request。我第一版把所有问题混在一起处理,Route Handler 膨胀到几百行,排查问题全靠肉眼翻代码。所以我坚持把系统拆成六层,每层只处理一类问题。

分层之前,先看每层到底管什么。下面的表格按”职责—技术—明确不做什么”三个维度展开,最后那一列不是摆设,是防止模块互相污染的第一道闸门:

架构层 主要职责 关键技术 不建议承担的职责
前端交互层 登录入口、仓库选择、任务状态展示 Next.js App Router、Server Components 直接持有 GitHub 长期凭据
认证授权层 用户登录、会话读取、权限校验 NextAuth.js v5、GitHub App OAuth 翻译调度
GitHub 集成层 获取安装、仓库、文件、提交 PR GitHub REST API、Installation Token 业务状态存储
任务编排层 创建任务、切分文件、重试、状态流转 Route Handler、队列或后台任务 直接拼接页面 UI
AI 翻译层 模型选择、提示词、术语约束、结果校验 OpenRouter、OpenAI 兼容接口 保存用户会话
数据持久层 用户、安装、仓库、任务、片段状态 Prisma、PostgreSQL 或 SQLite 调用外部模型

这张表的重点不是”用了多少技术”,而是避免模块互相污染。认证层只回答”这个请求是否属于某个用户”,GitHub 集成层只回答”能不能访问这个仓库”,任务层只回答”下一步该执行什么”。

实际落地时我踩过一个坑:分层文档写得再清楚,代码里各层还是容易直接 new 跨层对象。解决办法是统一走 services 层做编排,上层只依赖接口声明,不依赖具体实现,后面第五节目录结构就是这套约定的落地。

二、核心模块如何协作

分层只是静态边界,协作才是系统真正跑起来之后的动态行为。六层里对话最频繁的是认证授权、GitHub 集成和任务编排三个模块,下面逐个拆开讲清楚。

2.1 用户与安装模块

用户登录后,系统拿到的是用户身份;仓库访问权来自 GitHub App 的安装关系。两者必须分开建模。一个用户可能安装多个账号下的 App,一个组织安装也可能被多个成员使用。如果只按用户 access token 访问仓库,权限会随着个人权限变化而波动;用安装关系建模后,系统能围绕 installationId 做稳定授权判断。

我在第一版里犯过这个错:数据库里只存用户和仓库两张大表,没有安装实体。结果用户在某组织里被移除角色,他的 token 立刻失去仓库读权限,任务批量失败。补齐 installation 之后,用户、安装、仓库三个实体互相独立,谁对谁有权限一目了然,排查授权问题的时间省了一大半。

2.2 仓库与文件发现模块

仓库发现模块通过安装令牌读取安装范围内的仓库,再按用户选择的路径扫描 Markdown、MDX、README 或 docs 目录。这里要做三类过滤:忽略二进制文件,跳过已经生成的目标语言文件,限制单次任务的文件数量与体积。否则一次扫描可能把仓库中的依赖目录、构建产物和历史文档都纳入翻译。

这个模块的坑藏在过滤条件里。我第一次扫描时没过滤 node_modules 和构建目录,一次任务拉了上千个文件,模型调用费直接超预算,翻译结果里还混着机器生成的代码注释。后来把文件类型白名单和大小上限写死,扫描面收窄到文档目录,任务量级才稳定下来。

2.3 翻译任务模块

翻译任务模块是系统的中心。它不直接关心页面如何展示,也不直接关心 GitHub token 如何生成,只接收”仓库、分支、源路径、目标语言、模型配置”这些输入,然后把任务拆成可恢复的步骤。

具体流程拆成七步,每步都对应一个可落库的状态:

  1. 创建 TranslationJob,记录仓库、分支、目标语言与发起用户;
  2. 拉取源文件内容,按标题、段落、代码块边界切分;
  3. 为每个片段创建 TranslationSegment,标记 pending;
  4. 调用 AI 翻译层生成译文,并保存原文哈希;
  5. 合并片段,运行 Markdown 结构校验;
  6. 创建新分支,提交译文文件;
  7. 打开 Pull Request,并把任务状态改为 completed。

这组步骤看似线性,但落库后可以断点续跑。某个片段失败,不需要整篇文档重翻;创建 PR 失败,也不需要重新调用模型。支撑断点续跑的是任务和片段两级数据模型,片段失败不影响整篇:

model TranslationJob {
  id           String          @id @default(cuid())
  repository   String
  branch       String
  targetLang   String
  status       JobStatus
  segments     TranslationSegment[]
  createdAt    DateTime        @default(now())
}

model TranslationSegment {
  id         String   @id @default(cuid())
  jobId      String
  sourceHash String
  status     SegmentStatus
  translated String?
  job        TranslationJob @relation(fields: [jobId], references: [id])
}

片段记录源文件哈希和译文,重试时只重跑失败的片段,不必整篇重翻。任务级与片段级状态分开,才能做到”PR 失败不重调模型、片段失败不重翻全文”。我实际遇到过模型连续超时的情况,靠这个设计把失败片段单独拎出来重试,任务照样跑完,不需要人工介入。

三、数据流与权限流要分开

进入实现阶段,最容易混乱的是两条”流”:数据从哪来、权限由谁放行。二者必须分开设计,混在一起会出现”拿着用户 token 去碰仓库”这类隐蔽问题。

3.1 数据流

数据流从 GitHub 文件开始,经任务编排进入 AI 模型,再回到 GitHub PR。中间所有状态都要落库:文件哈希用于判断源文是否变化,片段状态用于重试,模型参数用于追溯质量问题。这里不保存用户密码,也不把 GitHub 私钥写入数据库。

数据流设计阶段我纠结过一个问题:文件内容要不要落库。不落库,每次重试都要重新拉取,源文变了还会翻译出过期内容;落库,又担心存储膨胀。最终方案是只存原文哈希和译文片段,源文件靠哈希判断是否需要重新拉取,既控制了存储,又保住了重试的正确性。

3.2 权限流

权限流从用户登录开始,经 NextAuth.js v5 得到会话,再关联本地 User 与 GitHub App installation。真正访问仓库时,后端用 App 私钥生成 JWT,再换取 Installation Access Token。用户会话只用于证明”谁发起任务”,安装令牌才用于证明”系统能访问哪些仓库”。

这里有一个必须坚持的原则:用户会话和安装令牌不要互相替代。用会话去访问仓库,权限会随个人变化而漂移;用安装令牌去代表用户,审计日志里就分不清是谁发起的任务。两个凭据各管一段,问题边界非常干净。

四、模块边界的取舍

分层设计不是越多越好,边界切在哪决定开发体验。这一节讲两个我反复权衡过的取舍,也是被问得最多的两个问题。

4.1 为什么不做成单体脚本

单体脚本适合个人一次性翻译公开仓库,但不适合多人、多仓库、可恢复的产品形态。它通常缺少会话、权限、任务状态和失败重试。只要用户关闭浏览器,脚本就很难向前端回报进度;只要模型限流,任务就可能丢失上下文。

我第一版其实就是单体脚本,命令行跑一遍就完事。第一次遇到模型限流,整个任务直接报废,几十个文件白翻。那一刻我就确定了产品形态必须带任务状态和重试机制,单体脚本的思路彻底放弃。

4.2 为什么前端不直接调用 GitHub

前端直接调用 GitHub 会暴露 token 使用范围,也难以集中做审计。更稳妥的做法是前端只发起业务请求,后端统一校验 session、installationId、repoId,再按 GitHub App 权限访问仓库。这样可以把权限判断、速率限制、错误处理放在一处。

当初有人建议前端直接调 GitHub API 省事,我试了一下就否了:前端代码里有 token 中转逻辑,等于把仓库访问凭证铺得到处都是,安全审计无从下手。全部收口到后端之后,第三方审计只需要看一个文件,逻辑也简单得多。

五、推荐目录结构

前几节讲了设计思路,这节给出落地目录。原则是 Route Handler 保持薄,复杂逻辑下沉到 services 与 workers:

src/
  app/
    api/auth/[...nextauth]/route.ts
    api/github/installations/route.ts
    api/translation-jobs/route.ts
  lib/
    auth.ts
    github-app.ts
    openrouter.ts
    prisma.ts
  services/
    repository-service.ts
    translation-service.ts
    pull-request-service.ts
  workers/
    translation-runner.ts
prisma/
  schema.prisma

这个结构让 Route Handler 保持很薄,复杂逻辑下沉到 services 与 workers。测试时也更容易:GitHub API、OpenRouter API 和 Prisma 都能单独替换成 mock。我实际这么组织之后,单测覆盖从原来的三成出头涨到七成以上,因为每层都能独立起服务测。

六、落地时的风险点

翻译质量风险来自三处:Markdown 结构被破坏、术语前后不一致、代码块被误翻。工程上要在模型调用前后做保护:代码块先占位,标题层级保持不变,链接地址不翻译,只翻译链接文本;同一任务内维护术语表,让专有名词稳定。权限风险则来自 installationId 与 userId 绑定不严,必须每次任务启动前重新确认该用户是否仍可访问对应安装和仓库。

质量保护这一块我试过只在提示词里加约束,效果不稳定:模型偶尔还是会把链接里的路径当正文翻译,或者把代码块里的变量名改掉。改成工程手段——先占位再还原、用正则校验结构——之后,这类问题基本绝迹。术语一致性则靠任务级术语表兜底,同一个模型配置下专有名词的重复出现率明显下降。

常见问题(FAQ)

Q1:这个项目一定要引入队列吗?

小规模可用数据库状态轮询,任务变多后再接队列。

Q2:翻译结果为什么建议走 PR?

PR 便于人工审核、回滚和保留讨论记录。

Q3:OpenRouter 是架构必需项吗?

不是必需,但能降低多模型接入和切换成本。

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌抄袭侵权/违法违规的内容, 请发送邮件至 qiqicto@qq.com 举报,一经查实,本站将立刻删除。
赞 (0)
小码农的头像小码农认证作者

相关推荐

返回顶部