主题
Monorepo 公共逻辑
要点
- 公共逻辑优先承载跨端契约,例如任务状态、资料结构、审稿问题、Agent 输出 schema 和 API 类型。
- Agent 项目的共享包要服务于 Web 用户工作台、Admin 管理后台、API / Agent 服务之间的协作语言。
- 页面交互、后台筛选、Agent 内部步骤和数据库实现留在各自应用或模块里,避免共享包扩张成业务杂物箱。
1. 公共逻辑的边界
上一篇文章确定了三端架构:Web 用户工作台、Admin 管理后台、API / Agent 服务。进入 Monorepo 之后,最容易犯的错误是把公共逻辑理解成「只要两个地方用到,就抽到 packages」。
更稳妥的判断标准可以放在「契约」上:一段代码如果会同时约束 Web 展示、Admin 审计、API 校验和 Agent 输出,就适合进入共享包;如果只影响某个页面或某个 Agent 节点的内部实现,先留在本地。
Agent 项目里,适合共享的内容通常有:
- 任务状态枚举。
- 资料卡片结构。
- 审稿问题结构。
- Agent 输出 schema。
- API 请求和响应类型。
- Prompt 模板 ID 和版本。
- 共享 UI 组件。
- TypeScript、ESLint、Prettier 配置。
不适合过早共享的内容包括:
- 某个页面的表单逻辑。
- 某个后台列表的筛选状态。
- 某个 Agent 节点内部的实现步骤。
- 只被一个应用使用的业务 helper。
这个边界能减少一个常见问题:共享包一旦混入太多局部实现,任何用户工作台的小改动都可能影响后台审计或 Agent 执行链路。共享包应该让三端对同一件事使用同一套字段和状态,业务编排仍然交给各自模块处理。
2. 推荐目录结构
一个可维护的结构可以先从下面开始:
txt
apps/
web/
admin/
api/
packages/
contracts/
prompts/
ui/
config/其中:
contracts保存跨端数据契约,包括任务状态、资料卡片、审稿问题和 Agent 输出结构。prompts保存 Prompt 模板元信息、版本、变量定义和输出 schema 关联。ui保存基础组件,不保存具体业务工作流页面。config保存 TypeScript、ESLint、Prettier 等配置。
在 pnpm 工作区里,内部包之间的依赖建议写成 workspace:*,这样无论本地版本号怎么变,引用方始终拿到当前工作区的最新代码。Turborepo 会根据 package.json 中的依赖关系自动构建任务顺序,例如 apps/web 依赖 packages/ui 时,构建 web 之前会先完成 ui 的构建。
每个共享包的 package.json 最好显式声明 exports 字段,把公开入口和内部文件分开。contracts 可以只导出类型文件,ui 可以为每个基础组件提供独立子路径,这样消费方只能按约定导入,避免直接引用内部实现。
共享包的职责应该少而稳定。只要一个包开始依赖具体页面、具体路由、具体数据库实现,就说明边界已经向业务实现滑动。业界常见建议是先保持 3–5 个包,等有明确的独立发布或独立构建需求再拆分,避免过早拆出大量几乎不会被单独消费的微型包。
3. contracts:系统契约中心
contracts 是最重要的共享包。它定义 Web、Admin、API 和 Agent 节点都要共同理解的数据结构。
例如任务状态、审稿问题和资料卡片:
typescript
export type TaskStage =
| "intake"
| "research"
| "plan"
| "execute"
| "review"
| "rewrite"
| "delivery_check"
| "done"
| "blocked";
export type ReviewFinding = {
id: string;
severity: "P0" | "P1" | "P2";
title: string;
body: string;
targetSection?: string;
suggestedAction?: "rewrite" | "fact_check" | "source_required" | "remove";
resolved: boolean;
};
export type SourceCard = {
id: string;
title: string;
url?: string;
sourceType: "official" | "project" | "paper" | "secondary";
reliability: "high" | "medium" | "low";
usedInSections: string[];
};这些类型不只服务于 TypeScript 补全。它们也是系统里的协作语言:Web 用 TaskStage 展示任务进度,Admin 用 ReviewFinding 统计质量问题,API 用 SourceCard 校验资料引用,Agent 用同一组字段组织结构化输出。
4. prompts:共享模板元信息
Prompt 也需要共享,但共享方式要克制。更适合共享的是模板 ID、版本、变量 schema、输出 schema 和适用场景。完整 Prompt 文本可以按 Agent 与版本分文件管理,避免所有研究、规划、执行、审稿模板挤进一个巨型常量文件。
typescript
export type PromptTemplateMeta = {
id: "research_agent" | "plan_agent" | "execute_agent" | "review_agent";
version: string;
variables: string[];
outputSchema: string;
appliesToStage: TaskStage;
};这样 Web 可以展示当前任务使用的 Prompt 版本,Admin 可以管理不同版本的启停,API 可以在 trace 中记录 research_agent@2026-07-17 这类版本信息。后续排查「资料不足」「审稿建议重复」「改写偏离原意」时,版本号能直接连接到对应模板和输出 schema。
5. ui:只放基础组件
Web 和 Admin 都会用到按钮、表格、弹窗、标签、状态徽标等组件。把这些放到 packages/ui 有价值。
但业务组件要谨慎共享。例如「审稿问题列表」看起来两个端都用,但 Web 端关注用户如何处理问题,Admin 端关注问题分布、命中规则和审计记录,两者交互目标不同。可以共享底层 IssueBadge、SeverityTag 和 TaskStageBadge,完整列表组件更适合留在各自应用里。
共享 UI 的原则是:越基础越适合共享,越接近业务流程越应该留在应用内。
需要注意框架依赖的声明方式。packages/ui 这类共享组件包通常不应该把 React 列为自己的 dependencies,而应该列为 peerDependencies。如果 ui 自己安装了一份 React,而 apps/web 又安装了一份 React,运行时就可能出现两个 React 实例,导致 "hooks can only be called inside the body of a function component" 这类错误。peerDependencies 让消费方提供 React 版本,内部包只复用同一份。
6. config:让工程规则一致
config 包保存项目级工程规则,例如:
- TypeScript 严格模式配置。
- ESLint / Biome / Prettier 规则。
- 测试基础配置。
- 构建目标配置。
这类配置本身没有业务价值,但能减少多端风格漂移。Agent 项目会长期维护大量 Prompt、schema 和 TypeScript 代码,工程规则一致会降低后续审查成本。
对 Agent 项目来说,工程规则还会影响 Agent 输出物的可维护性。Prompt、schema 和测试夹具如果采用同一套格式约束,代码审查时更容易把注意力放回字段语义、版本变化和任务状态迁移。
7. 避免共享包失控
共享包失控通常有几个信号:
contracts开始 import 数据库实现。ui开始依赖某个具体页面路由。prompts里混入运行时代码。config以外的包互相循环依赖。- 一个 helper 只被一个地方使用,却被提前抽到 shared。
package.json没有exports字段,导致外部代码可以随意引用内部文件。- 共享组件包把 React 列为
dependencies而不是peerDependencies。
发现这些信号时,不要继续抽象。先把代码放回使用方,等第二个稳定场景出现后再共享。
当共享包数量增加、需要独立发布或对外部使用者暴露版本时,可以用 Changesets 管理版本和 changelog。它通过 .changeset 目录下的变更声明,在发布时自动计算每个包的 semver 版本、更新依赖它的内部包版本,并生成每个包的 changelog。对于 Agent 项目,contracts 或 prompts 的字段变更往往同时影响 Web、Admin、API 三端,Changesets 能帮你把这些关联包的版本变化一起表达出来。
在 Agent 项目里,还要额外注意 Prompt 和 Agent 运行时的边界。contracts 可以定义 ReviewFinding,但不应该知道审稿 Agent 如何逐段扫描产物;prompts 可以登记模板版本,但不应该直接调用模型;ui 可以展示状态徽标,但不应该决定任务如何从 review 进入 rewrite。
8. 小结
Monorepo 的公共逻辑应该服务于契约一致,避免只因为两处代码相似就提前共享。
对 Agent 项目来说,优先共享任务状态、资料卡片、审稿问题、Agent 输出 schema、Prompt 元信息和基础 UI。业务流程、页面交互和 Agent 内部实现先保持局部化。这样共享包会更稳定,三端协作也更容易长期维护。