coze-loop 前端 Rush.js Monorepo 包分层结构与 Adapter 模式全解
coze-loop 前端 Rush.js Monorepo 包分层结构与 Adapter 模式全解
本篇文章以 docs/reference/frontend-packages.md 为骨架,结合 coze-loop 仓库
frontend/目录下的真实源码与配置,系统讲解 CozeLoop 前端 59 个包如何被组织为 6 层依赖结构、各层包的职责边界、以及用于解耦商业版与开源版差异的 Adapter 模式。读完本文,你将掌握 CozeLoop 前端 Monorepo 的"地图":新增一个前端包应该放在哪一层、能依赖哪些层、遇到商业版/开源版功能差异时如何按规范接入 Adapter 接口,而不是在组件里硬编码条件分支。
一、为什么 CozeLoop 前端需要一套分层规范
CozeLoop 是"下一代 AI Agent 优化平台",其前端承载 Prompt 管理、评测(Evaluation)、可观测性(Observation)、数据集(Data)、标签(Tag)等完整业务域。如此规模的业务不可能塞进单一应用目录,因此整个 frontend/ 是一个由 Rush.js 管理的 Monorepo:以 apps/cozeloop 为主 SPA,以 packages/ 下的 59 个包承载分层复用的代码。
Monorepo 带来的核心问题不是"怎么放代码",而是"谁可以依赖谁"。没有约束的依赖图会迅速退化成一团乱麻:业务组件依赖工具函数没问题,但如果基础库反过来依赖业务组件,任何一处改动都会引发连锁破坏。CozeLoop 的答案是在架构层面固化一套 6 层依赖结构 + 依赖不变量,让每个包的位置和依赖方向都有明确规则可循。
二、6 层依赖结构总览
原文档给出的分层全景如下:
Level-6 apps/cozeloop/ 主 SPA 应用(React 18, Rsbuild)
↑
Level-5 packages/loop-pages/ 5 个页面模块
↑ auth-pages, evaluate-pages, observation-pages, prompt-pages, tag-pages
Level-4 packages/loop-modules/ 高阶业务模块
↑ evaluate/
Level-3 packages/loop-components/ 13 个 UI 组件包 + adapter
↑ biz-components, shared-components, *-components, *-adapter
Level-2 packages/loop-base/ 20 个基础库
↑ account, api-schema, hooks, components, env, i18n, stores, route...
Level-1 config/ (6) + infra/ (4+) 工具链配置 + 基础设施
eslint-config, ts-config, vitest-config, eslint-plugin, idl...
依赖不变量:高层级只能依赖低层级,禁止反向依赖。 也就是说,loop-base 里的工具函数不能 import 任何 loop-components 的组件;loop-components 可以消费 loop-base,但不能反向依赖 loop-pages。这条不变量是理解整个目录结构的钥匙——当你看到某个包出现在哪一层,就同时知道了它的依赖自由度和被依赖的边界。
对应到仓库实际目录:frontend/packages/loop-base/(19 个基础库目录)、frontend/packages/loop-components/(13 个组件/adapter 包)、frontend/packages/loop-pages/(5 个页面包)、frontend/packages/loop-modules/(evaluate 业务模块)、frontend/config/(6 个共享配置)与 frontend/infra/(4 个基础设施目录),与上述分层一一对应。
三、Level-6:主 SPA 应用 apps/cozeloop
顶层是唯一的应用入口 apps/cozeloop,一个基于 React 18 + Rsbuild 的单页应用。从它的 package.json 可以看到技术栈:react ~18.2.0、react-router-dom ^6.22.0、zustand ^4.4.7、immer ^10.0.3,以及大量 workspace:* 版本的内部依赖(@cozeloop/account、@cozeloop/auth-pages、@cozeloop/evaluate-pages、@cozeloop/observation-pages、@cozeloop/prompt-pages、@cozeloop/tag-pages、@cozeloop/stores、@cozeloop/guard、@cozeloop/components、@cozeloop/toolkit 等)。
应用入口与路由装配
入口文件 apps/cozeloop/src/app.tsx 清晰地展示了应用骨架:
const router = createBrowserRouter(routeConfig);
export function App() {
useSetupI18n();
return (
<CozeLoopProvider i18n={I18n} sendEvent={sendEvent}>
<Suspense fallback={<PageLoading />}>
<LocaleProvider>
<RouterProvider router={router} />
</LocaleProvider>
</Suspense>
</CozeLoopProvider>
);
}
CozeLoopProvider来自@cozeloop/components,统一注入国际化(I18n)与埋点(sendEvent,来自@cozeloop/tea-adapter);- 路由表在 apps/cozeloop/src/routes/index.tsx 中通过
React.lazy按需加载 5 个页面模块,这是 Level-5 包被"接电"的地方:
const Auth = lazy(() => import('@cozeloop/auth-pages'));
const Evaluation = lazy(() => import('@cozeloop/evaluate-pages'));
const Observation = lazy(() => import('@cozeloop/observation-pages'));
const Prompt = lazy(() => import('@cozeloop/prompt-pages'));
const Tag = lazy(() => import('@cozeloop/tag-pages'));
路由路径与业务域一一对应:/auth/* 走登录鉴权;/console/enterprise/:enterpriseID/space/:spaceID 下,pe/* → Prompt 模块、evaluation/* → Evaluation 模块、observation/* → Observation 模块、tag/* → Tag 模块。登录态控制则由 base-route.tsx 负责:调用 @cozeloop/account 的 useCheckLogin/useLoginStatus,在 settling 时展示 PageLoading,not_login 时重定向到 LOGIN_PATH,logined 时用 GuardProvider(来自 @cozeloop/guard)包裹子路由。
本地开发与构建
按 frontend/README.zh-CN.md 的环境要求(Node.js 24+、pnpm 10.27.0、Rush 5.172.1),安装依赖后启动开发服务器:
cd frontend
rush update # 安装/更新项目依赖
cd apps/cozeloop
rushx dev # 开发模式,默认 http://localhost:8090/
rushx build # Rsbuild 生产构建
注意:Monorepo 中推荐用 rushx 而不是 pnpm run/npm run。由于 apps/cozeloop/package.json 中的内部依赖都是 workspace:*,应用直接依赖 workspace 包的源代码而非构建产物,改完底层包源码通常即时生效(个别场景需重启 dev server)。
四、Level-5:页面模块 packages/loop-pages/
页面模块是"路由颗粒度"的业务组合层,每个包对应一个独立页面域,且只做一件事:
| 包 | 职责 |
|---|---|
auth-pages |
登录 / 认证相关页面 |
evaluate-pages |
评测相关页面(评测集、评估器、实验) |
observation-pages |
可观测性页面(Trace 查询、分析) |
prompt-pages |
Prompt 管理页面(编辑、Playground、版本) |
tag-pages |
标签管理页面 |
以 evaluate-pages/package.json 为例,它的依赖集中在 @cozeloop/evaluate(Level-4 业务模块)、@cozeloop/evaluate-components(Level-3 专用组件)、@cozeloop/biz-hooks-adapter、@cozeloop/i18n-adapter、@cozeloop/toolkit,peerDependencies 声明 react >=18.2.0 与 react-router-dom ^6.22.0——可见页面模块直接依赖组件层与基础库层,符合"只向下依赖"的不变量。
五、Level-4:高阶业务模块 packages/loop-modules/
Level-4 是介于"组件"与"页面"之间的高阶业务模块层,目前仓库中为 loop-modules/evaluate(约 259 个文件,174 个 .tsx),承载评测域的核心业务逻辑与编排。它的存在让 evaluate-pages 保持"薄页面"形态:路由、布局与交互收拢在页面包,评测的业务能力沉淀在 @cozeloop/evaluate 模块中,从而被页面包单点依赖、复用边界清晰。
从源码结构看,Level-4 可以依赖 Level-3 组件包与 Level-2 基础库,但绝不能被 Level-3/Level-2 反向依赖。
六、Level-3:组件包 packages/loop-components/
组件层是全仓库包最多的层级(13 个包),分为三大类:通用组件、领域专用组件、Adapter 相关包:
| 包 | 职责 |
|---|---|
shared-components |
通用 UI 组件 |
biz-components |
业务通用组件 |
biz-config |
业务配置组件 |
biz-hooks |
业务通用 hooks |
evaluate-components |
评测专用组件 |
observation-components |
可观测性专用组件 |
prompt-components |
Prompt 专用组件 |
prompt-components-v2 |
Prompt 组件 v2 版 |
tag-components |
标签专用组件 |
adapter-interfaces |
Adapter 接口定义 |
evaluate-adapter |
评测 Adapter 实现 |
observation-adapter |
可观测性 Adapter 实现 |
components-with-adapter |
Adapter 消费层 |
其中 shared-components 与 biz-components 提供跨业务复用的 UI 能力(表格、分页、Loading、Tabs 等,详见 components-with-adapter/README.md 中导出的 LoopTable、PageLoading、PageError、LoopTabs、InputSlider 等),而 evaluate-components/observation-components/prompt-components/tag-components 则分别沉淀各业务域的专用组件。v2 版本(prompt-components-v2)的存在说明组件层允许通过新包演进,而不破坏既有消费方。
七、Level-2:基础库 packages/loop-base/
基础库是全仓库的"地基",20 个包按职责可划分为几组:
| 包 | 职责 |
|---|---|
account |
用户账户管理 |
api-schema |
API 类型定义(从 IDL 生成) |
base-hooks |
通用 React hooks |
bot-env / bot-env-adapter |
运行环境抽象 |
bot-flags |
功能开关 |
bot-typings |
全局类型定义 |
components |
基础 UI 组件 |
env |
环境变量管理 |
fetch-stream |
流式请求 |
guard |
权限守卫 |
i18n / intl |
国际化 |
logger |
日志 |
loop-lng |
语言包 |
route |
路由管理 |
stores |
状态管理(Zustand) |
tea |
数据埋点 |
toolkit |
工具函数 |
这层的设计原则是"领域无关":不出现任何评测、Prompt 等业务词汇,只提供账户、类型、请求、状态、国际化等横切能力。下面结合源码看两个代表性包的内部结构。
api-schema:从 Thrift IDL 生成的前端类型
api-schema 是前后端契约的"单一事实来源",其源码由 Thrift IDL 经 idl2ts 工具链生成(package.json 中 thrift: "idl2ts gen ./"、prethrift 脚本通过 download-thrift.sh 拉取 IDL)。它通过 exports 提供按域拆分的子路径导入:
"exports": {
".": "./src/index.ts",
"./observation": "./src/api/observability/index.ts",
"./evaluation": "./src/api/evaluation/index.ts",
"./data": "./src/api/data/index.ts",
"./llm-manage": "./src/api/llm-manage/index.ts",
"./foundation": "./src/api/foundation/index.ts",
"./prompt": "./src/api/prompt/index.ts"
}
生成后的类型按 data / evaluation / foundation / llm / observability / prompt 域组织在 src/api/idl/ 下(例如 coze.loop.evaluation.eval_set.ts、coze.loop.observability.trace.ts)。后端 API 变更时,按 api-schema/README.md 执行 rushx update 重新生成;如需指定 IDL 分支,修改 prethrift 脚本的 --branch 参数(默认 main)。
stores:Zustand 状态管理
stores 基于 Zustand + immer 封装全局状态,入口导出 useUIStore、useI18nStore、useEvaluationFlagStore。以 stores/src/stores/ui.ts 为例,它管理面包屑配置与导航栏折叠状态,并通过 devtools middleware 暴露给调试工具(仅在 NODE_ENV === 'development' 开启),同时用 storage 工具持久化 navbar-collapsed。stores/src/stores/i18n.ts 则负责语言切换:lng 状态与 @cozeloop/i18n-adapter 的 I18n.i18next.changeLanguage 联动,支持 zh-CN / en-US 双语。
account:账户与 Space 域
account 采用经典的 hooks + services + stores 三层结构:useLogin/useRegister/useLogout/useLoginStatus/useCheckLogin 等 hooks 暴露给 UI,userService/authnService/spaceService 封装后端调用,user-store/space-store(含 PERSONAL_ENTERPRISE_ID)承载用户与空间状态——这正是 base-route.tsx 登录流程背后依赖的实现。
八、Level-1:工具链配置与基础设施
最底层是工程化配置,不产业务代码,只约束"如何写代码":
| 包/目录 | 职责 |
|---|---|
config/eslint-config |
ESLint 共享配置 |
config/ts-config |
TypeScript 共享配置 |
config/vitest-config |
Vitest 共享配置 |
config/postcss-config |
PostCSS 配置 |
config/stylelint-config |
Stylelint 配置 |
config/tailwind-config |
Tailwind CSS 配置 |
infra/eslint-plugin |
自定义 ESLint 插件 |
infra/idl |
Thrift IDL -> TypeScript 转换 |
infra/plugins |
构建插件 |
infra/utils |
构建工具函数 |
这些包是各 workspace 包 devDependencies 中的 @coze-arch/eslint-config、@coze-arch/ts-config、@coze-arch/vitest-config 等的来源,也是 idl2ts 工具链(frontend/infra/idl 下 idl-parser、idl2ts-cli、idl2ts-generator 等)与自定义构建插件(pkg-root-webpack-plugin、postcss-plugin)的所在位置。
九、Adapter 模式:解耦商业版与开源版差异
这是原文档中工程价值最高的设计。CozeLoop 同时存在商业版与开源版,部分页面能力(如模型配置编辑器、数据集选择器、Trace 详情面板)在两版中实现不同。若在组件里直接写 if (isEnterprise) 之类的条件分支,差异会像癌细胞一样扩散到每个组件,最终无法维护。
三层职责划分
adapter-interfaces/ # 接口定义(纯类型/契约,无实现)
↑
*-adapter/ # 接口实现(evaluate-adapter, observation-adapter)
↑
components-with-adapter/ # 消费者(通过接口调用,不感知具体实现)
- 接口层 adapter-interfaces:只定义类型与契约。它的 src/index.ts 导出
EvaluateAdapters、PromptAdapters、ObservationTraceAdapters、TraceDetailExtraProps等; - 实现层
*-adapter:如 evaluate-adapter 的 src/index.ts,按接口约束导出具体实现对象:
import { type EvaluateAdapters } from '@cozeloop/adapter-interfaces/evaluate';
import { EvaluateTargetPromptDynamicParams } from './evaluate-target-prompt-dynamic-params';
const evaluateAdapters: EvaluateAdapters = {
EvaluateTargetPromptDynamicParams,
};
export default evaluateAdapters;
接口本身定义在 adapter-interfaces/src/evaluate/experiments.ts:EvaluateTargetPromptDynamicParams 接收 promptID/promptVersion/prompt/disabled/value/onChange/onModelChange 等 props,返回一个 React 组件类型。实现则位于 evaluate-adapter/src/evaluate-target-prompt-dynamic-params.tsx,它组合 BasicModelConfigEditor(来自 prompt-components)、useModelList/useSpace(来自 biz-hooks-adapter),把 RuntimeParam.json_value 解析为 model_config 并渲染成可编辑的模型配置表单——这是评测实验配置里"Prompt 动态参数覆盖"能力的开源版实现。
- 消费层 components-with-adapter:只通过接口消费能力,不感知具体实现来自哪个版本。从结构看,它提供了
handle-form-cancel、rules、route-back-action等被页面/组件复用的"带 Adapter 的公共能力"。
强制约束
原文档明确了一条工程红线:新增商业版/开源版差异必须走 Adapter 接口,不可在组件中硬编码条件分支。 这意味着任何新差异的落地路径都是固定的三步——先在 adapter-interfaces 定义契约、再在 *-adapter 提供实现、最后在消费方通过接口调用。ObservationTraceAdapters(见 adapter-interfaces/src/observation/trace.ts)是另一个典型例子:它定义了 getAutoEvaluateFieldMappingConfig、getDataReflowFieldMappingConfig、DatasetSelect、useFetchSpanDetail、CozeloopTraceSelector、TraceDetailPanel、TraceDetail 等一组能力,其中 TraceDetail 的 props 还叠加了 TraceDetailExtraProps(traceID、startTime、endTime、env),说明接口层可以携带扩展上下文,而实现层负责把这些契约翻译成具体 UI 与数据流。
十、如何在日常开发中应用这套规范
把上面的分层与 Adapter 模式落到日常开发,可以总结成四条可执行规则:
- 先定位再动手:新增代码前先判断它属于哪一层——通用工具进
loop-base,可复用 UI 进loop-components,业务页面进loop-pages,跨版本差异进 Adapter 三件套; - 只向下依赖:遵守"高层级只能依赖低层级"的不变量,任何反向依赖都视为架构违规;
- 差异走接口:遇到商业版/开源版行为不同的场景,按
adapter-interfaces→*-adapter→components-with-adapter的顺序落地,禁止在组件内写版本条件分支; - 契约统一更新:后端 API 变更后,在 api-schema 内执行
rushx update重新生成类型(或按 frontend/README.zh-CN.md 从根目录执行rush update-api),确保全仓库类型同步。
结语
CozeLoop 前端的 6 层结构本质上是一张"可执行的架构地图":apps/cozeloop 是唯一应用入口,loop-pages 提供页面域,loop-modules 沉淀高阶业务,loop-components 提供组件与 Adapter 机制,loop-base 夯实领域无关的基础能力,config/ + infra/ 兜底工程化。配合 Adapter 模式,这套分层既保证了 59 个包在 Monorepo 中可维护、可演进,又为商业版与开源版共存提供了标准化的差异隔离通道。后端侧的分层参考可继续阅读 docs/reference/backend-modules-api.md,前后端合起来即构成 CozeLoop 完整的模块化视图。