coze-loop 前端 Rush.js Monorepo 包分层结构与 Adapter 模式全解

原创2026-10-11 20:25:411,948 阅读
文章标签:人工智能AI 应用AI Agent提示工程AI 评测可观测性LLMOps后端前端

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/      # 消费者(通过接口调用,不感知具体实现)
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 模式落到日常开发,可以总结成四条可执行规则:

  1. 先定位再动手:新增代码前先判断它属于哪一层——通用工具进 loop-base,可复用 UI 进 loop-components,业务页面进 loop-pages,跨版本差异进 Adapter 三件套;
  2. 只向下依赖:遵守"高层级只能依赖低层级"的不变量,任何反向依赖都视为架构违规;
  3. 差异走接口:遇到商业版/开源版行为不同的场景,按 adapter-interfaces → *-adapter → components-with-adapter 的顺序落地,禁止在组件内写版本条件分支;
  4. 契约统一更新:后端 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 完整的模块化视图。

登录后查看全文
coze-loop