Dify 前端开发规约详解:web/AGENTS.md 中的 Agent 工作流、包契约与 Next.js 生成规则
【免费下载链接】difyBuild Agentic workflows, RAG pipelines, with rich AI model and tool support on one collaborative workspace. Deploy on cloud, VPC, or self-hosted, so teams move from prototype to production without rebuilding the stack.项目地址: https://gitcode.com/GitHub_Trending/di/dify
本文以 web/AGENTS.md 为骨架,完整拆解 Dify 前端(web/与packages/dify-ui/)面向人类和 AI Agent 的双层开发规约:从测试与静态检查文档的路由规则、i18n 与生成式 API 客户端的包契约,到 Dify UI 组件库的各项"canonical contract",再到next dev自动写入的 Next.js 破坏性变更警示块。读完后可按仓库真实路径定位每一条契约的落点文件,在贡献 Dify 前端代码时保持与工程体系一致。
一、文档定位:一份写给 Agent 的前端工作手册
web/AGENTS.md 位于 Web 应用目录根部,是 Dify 前端唯一的"Agent 入口规约"。它的结构分为三层:
- Frontend Workflow——规定哪些文档、哪些技能(skill)在什么场景下加载;
- Package Contracts——规定用户可见文案、API 调用、UI 组件、表单、可访问性等跨功能契约;
- Next.js 生成规则块——由
next dev自动写入并维护的警告区,提示当前 Next.js 版本存在与训练数据不一致的破坏性变更。
这种"流程 + 契约 + 工具链警示"的组合,使文档既约束人的 PR 行为,也约束 AI 编码助手的工具选择。
二、Frontend Workflow:文档与技能的路由规则
原文第一条工作流要求非常克制——只在对应工作场景下加载对应文档:
web/docs/test.md只在处理前端测试工作时读取;web/docs/lint.md只在运行或修改静态检查时读取。
这两个文件在仓库中均真实存在且内容完备:
- web/docs/test.md 声明自己是
web/下自动化测试的 single source of truth,定义了"何时该写测试"(保护可观察契约:用户交互、导航与 URL 状态、加载/成功/错误/空态、可访问性语义、可复现回归的 bug fix),并给出两条显式测试项目(unit走 happy-dom,browser走 Playwright Chromium)与标准命令:
# happy-dom;省略路径则运行整个 unit 项目 vp test run --project unit path/to/spec-or-directory # Browser Mode;省略路径则运行整个 browser 项目 vp test run --project browser path/to/spec.browser.spec.tsx # 诊断性覆盖率报告;不是验收目标 vp test run --project unit --coverage path/to/spec-or-directory- web/docs/lint.md 说明
vp check(Oxfmt 格式化 + Oxlint 规则 + TypeScript 诊断)与 ESLint 非代码文件兜底的分工,根命令为pnpm check/pnpm check:fix。
技能(skill)路由同样按场景触发:
how-to-write-component:只在实现涉及组件归属、状态、数据流、effect 或交互边界决策时加载;纯测试、纯文案、纯样式改动不得加载;frontend-code-review:仅在显式的前端评审/审计请求(含测试评审)时使用;frontend-testing:编写或修改 Vitest、React Testing Library 测试时使用。
文档还特别强调:web/docs/test.md是 Web 自动化测试策略的唯一事实来源,"Skills may route and execute that policy but must not redefine it"——技能只能路由和执行该策略,不能重定义它。这保证了策略演进只需改一处。
三、Package Contracts:七条跨功能契约逐条解析
3.1 用户可见字符串必须走 i18n 键
规约第一条即国际化契约:面向用户的字符串必须使用web/i18n/en-US/下的键;新增或重命名键时,必须同步更新所有受支持语言的正确本地化值。
仓库中 web/i18n/ 目录下实际维护了 24 个语言目录(ar-TN、de-DE、en-US、es-ES、fa-IR、fr-FR、hi-IN、id-ID、it-IT、ja-JP、ko-KR、lo-LA、nl-NL、pl-PL、pt-BR、ro-RO、ru-RU、sl-SI、th-TH、tr-TR、uk-UA、vi-VN、zh-Hans、zh-Hant)。这意味着一次键的重命名可能波及 24 个文件树——契约把"多语言完整性"从自觉行为升级为硬性规则。
测试侧同样配套:web/test/i18n-mock.ts 提供createReactI18nextMock(在需要自定义翻译时加载),共享的react-i18nextmock 全局加载,测试默认不依赖真实翻译文件。
3.2 后端调用:只允许生成式 consoleQuery / consoleClient
规约明确要求:新后端调用与已迁移界面必须使用@/service/client中生成的consoleQuery/consoleClientAPI,禁止新增手写 REST helper、DTO 镜像、基于 mock 的 app 状态,或直接修改生成契约。
从源码结构看,这条契约有明确实现落点 web/service/client.ts:
- 类型全部来自
@dify/contracts/api/console/**/types.gen与@dify/contracts/console的consoleRouterContract,即契约类型是生成的(.gen后缀),而非手写镜像; - 客户端基于 oRPC 体系构建:
createORPCClient+OpenAPILink(@orpc/openapi-client/fetch),再经createTanstackQueryUtils接入 TanStack Query,这就是consoleQuery的形态; - 请求上下文扩展了 TanStack Query 的 operation context,加入 Dify 特有的
keepalive与silent标志(见 client.ts#L68-L71),并封装了 SSE 流式生成streamWorkflowGeneration。
因此"不要手写 REST helper"不是风格偏好,而是保证 DTO 类型、OpenAPI URL 归一化(normalizeConsoleOpenAPIURL)、认证头注入(getMarketplaceHeaders等)只有一条代码路径。
3.3 Dify UI 原语:子路径导入与焦点指示
规约要求优先使用@langgenius/dify-ui/*原语、data attribute 与设计令牌,选择原语时从 Dify UI 包索引入手,并在最终可聚焦元素上保留可见焦点指示。
包索引即 packages/dify-ui/README.md。从源码结构看,该包有两条与规约直接对应的硬性设计:
- 故意没有根 barrel——只能按公共子路径导入,如
@langgenius/dify-ui/button、@langgenius/dify-ui/dialog、@langgenius/dify-ui/field,样式入口@langgenius/dify-ui/styles.css在消费方根样式表引入一次; - 大部分交互原语是 Base UI 无头组件的"薄而有主见的封装",Dify 自研原语使用语义化 HTML、
cva、cn与设计令牌,包虽为 workspace 私有,但其公共子路径被当作稳定的包边界对待。
README 的 Primitives 表按 Actions / Controls / Display / Feedback / Form / Layout / Media / Navigation / Overlay and menu / Search and pick 十类列出全部子路径(./button、./icon-button、./form、./input-group、./dialog、./infotip所在的 overlay 家族等),是"选原语"的唯一入口清单。
3.4 搜索输入框:SearchInput 复合组件优先
规约:当 Web 的SearchInput复合组件的搜索、清除、IME 契约与功能匹配时,复用它;否则遵循 canonical 的 Input Group 契约。
这里的关键是"契约匹配才复用":SearchInput封装了搜索、清除按钮与 IME(输入法组合)边界这三件事,只有功能同时命中该组合契约时才使用,避免为了"看起来像搜索框"而引入不匹配的复合行为。不匹配时退回更底层的 Input Group 契约文档 packages/dify-ui/src/input-group/README.md,其覆盖复合输入解剖、共享表面归属、DOM 顺序、焦点与可交互 addon。
3.5 保存与提交流:真实表单边界
规约要求为保存/提交流程建立真实表单边界,带可见标签与可访问错误:当 Dify UI 的Form结构化提交与校验契约是归属方时使用它,否则使用原生 form;契约详见 packages/dify-ui/docs/forms.md。
从 dify-ui 文档索引看,forms.md 负责 native submit 边界、值归属、field、label 与 error 的契约划分——这与 AGENTS.md 中"要么 Dify UI Form 拥有契约,要么原生 form"的二选一表述互补:文档定义契约内容,AGENTS.md 定义选择决策。
3.6 按钮与图标按钮:不得用 Web 包装层掩盖契约
规约:遵循 canonical 的 Button 契约 与 IconButton 契约,覆盖操作语义、loading、可访问名称与原语组合;不得添加一个 Web 包装层来隐藏这些契约。
从 packages/dify-ui/README.md 的组件指南表可确认两条契约的具体职责:Button 覆盖"操作语义、submit 与 link 的选择、loading 与 disabled 的区分、内容间距";Icon Button 覆盖"可访问名称、装饰性 glyph、外观归属与原语组合"。规约中"不得在 Web 层包一层"的禁令,正是防止web/出现一个与契约文档不同步的中间封装。
3.7 可访问名称、描述与弹层
两条契约引用同一套命名与弹层文档:
- 命名:选择或修改可见标签、ARIA 命名、描述、视觉隐藏文本时遵循 packages/dify-ui/docs/accessible-names-and-descriptions.md。规约补充了一条职责边界——"Web 拥有本地化与功能特定的状态播报;不得在本地重新定义 Dify UI 的命名契约"。也就是说 dify-ui 文档定义"命名的来源、描述、覆盖与安全的 label 移除",
web/只负责本地化与功能级状态播报。 - 弹层:原语选择、portal、焦点与层叠遵循 packages/dify-ui/docs/overlays.md;"信息图形打开解释性内容"的场景复用 Web 的
Infotip复合组件;不得引入一个在 Web 层重新实现 Dify UI 弹层行为的通用包装。
3.8 自定义 SVG 图标
规约末尾:自定义 SVG 图标遵循 packages/iconify-collections/README.md;不得在web/app/components/base/icons/src/下添加生成的 React 图标。仓库中packages/iconify-collections/目录实际包含 500 余个 SVG 源文件与多个 JSON 集合描述,即图标资产以 iconify 集合形式集中管理,而不是把构建产物 React 图标散落在应用目录里。
四、Next.js 生成规则块:由next dev自动维护的警示区
web/AGENTS.md 尾部被<!-- BEGIN:nextjs-agent-rules -->与<!-- END:nextjs-agent-rules -->包裹着一段机器维护的文本,这是整篇文档中最特殊的部分:
- "This is NOT the Next.js you know"——声明当前 Next.js 版本存在破坏性变更,API、约定与文件结构都可能与模型训练数据不同;写任何代码前,先读
node_modules/next/dist/docs/中相应指南(从本文件所在目录解析;monorepo 中next包可能从仓库根不可见),并注意弃用通知; - 自解释的维护机制——该块由
next dev写入并在被删除后重新加回,校验逻辑位于node_modules/next/dist/server/lib/generate-agent-files.js。文档给出了一条实用的 diff 建议:"从 diff 中移除它只会重新产生未提交的变更;把它随你的改动一起提交,才能保持工作树干净"。
这个设计值得借鉴:Next.js 官方把"AI 助手可能按旧知识写码"这一风险,变成了工具链自动写入 AGENTS 类文件的固定机制——规约不是靠人记得更新,而是每次next dev运行时自我刷新。
五、契约到文件的路径速查
下表汇总 AGENTS.md 中每个契约指向的真实文件,便于按图索骥:
| 契约/工作流条目 | 契约文件(仓库根相对路径) |
|---|---|
| 前端测试策略(唯一事实来源) | web/docs/test.md |
| 静态检查策略 | web/docs/lint.md |
| Dify UI 包索引(选原语起点) | packages/dify-ui/README.md |
| Button 契约 | packages/dify-ui/src/button/README.md |
| IconButton 契约 | packages/dify-ui/src/icon-button/README.md |
| Input Group 契约 | packages/dify-ui/src/input-group/README.md |
| Form 契约 | packages/dify-ui/docs/forms.md |
| 可访问名称与描述 | packages/dify-ui/docs/accessible-names-and-descriptions.md |
| Overlay 契约 | packages/dify-ui/docs/overlays.md |
| 自定义 SVG 图标 | packages/iconify-collections/README.md |
| 生成式 API 客户端实现 | web/service/client.ts |
| 国际化键 | web/i18n/(en-US为键基准,24 个 locale 同步) |
六、实操要点小结
- 动测试之前先读 web/docs/test.md,并记住
web/下测试必须显式--project unit或--project browser;裸vp test会同时跑两个项目,不是标准 Web 测试命令。覆盖率只是诊断信号,文档未定义任何百分比门槛。 - 动静态检查之前先读 web/docs/lint.md:根命令
pnpm check(Oxlint 规则基线在lint.config.ts,非代码文件兜底在eslint.config.mjs,Oxlint 历史错误基线在oxlint-suppressions.json);Oxlint 与 ESLint 的 disable 注释互不通用。 - 加一条用户可见文案:先在
web/i18n/en-US/定义键,再把 24 个 locale 目录全部补齐。 - 加一次后端调用:走
@/service/client的生成契约(oRPC + OpenAPI + TanStack Query),不手写 REST helper,不碰.gen契约。 - 加一个组件:先到 packages/dify-ui/README.md 的子路径表里找原语,命中
SearchInput复合契约就复用,否则按 Input Group / Button / IconButton / Form / Overlay / 命名契约文档决策,并保留可见焦点指示;图标只进 iconify 集合,不进app/components/base/icons/src/。 - 写任何 Next.js 相关代码前:读
node_modules/next/dist/docs/里的当前版本文档,因为 AGENTS.md 中那个自更新块明示了"这不是你训练数据里的 Next.js"。
整体来看,web/AGENTS.md 的价值不在罗列规则,而在于把"哪个契约归谁所有"(Dify UI 拥有原语与命名契约,Web 拥有本地化与功能播报,docs/test.md拥有测试策略)写得毫无歧义,并用生成式代码、机器维护的 Next.js 规则块和 24 语言 i18n 目录这些仓库事实,为每条规则提供了可核验的落点。
【免费下载链接】difyBuild Agentic workflows, RAG pipelines, with rich AI model and tool support on one collaborative workspace. Deploy on cloud, VPC, or self-hosted, so teams move from prototype to production without rebuilding the stack.项目地址: https://gitcode.com/GitHub_Trending/di/dify
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考