Dify Agent V2 前端架构:从 Payload 判别符到单一数据源草稿状态的边界规范
本文以 Dify 仓库中 Agent V2 前端模块的架构约束文档 为核心,讲清 Agent V2 与旧版 workflow Agent 的隔离边界、agent-composer 单一配置数据源的设计,以及 agent-detail/configure 页面如何通过 jotai 原子状态完成服务端同步、构建草稿与预览会话的组合。读完本文,你可以理解 Agent V2 前端为什么采用"判别符 + 单一 Store"的设计,并能沿文中给出的源码路径复现每个架构决策的实现依据。
Agent V2 与旧版 Agent 的隔离边界
web/features/agent-v2/AGENTS.md 的第一条规则要求:Agent V2 必须与遗留的 workflow Agent(agent_strategy_* 系列行为与数据形状)保持分离,禁止把两者桥接起来。具体而言,凡是 Agent V2 的代码必须落在两个固定位置,并携带明确的载荷判别信息:
- 独立特性目录:web/features/agent-v2;
- 工作流节点目录:web/app/components/workflow/nodes/agent-v2;
- 载荷判别符:
agent_node_kind: 'dify_agent'且version: '2'; - 已迁移图类型上使用
BlockEnum.AgentV2节点枚举。
Payload 判别符的类型定义
判别符不是约定俗成,而是被固化在节点类型定义中。types.ts 定义了 AgentV2NodeType,它在工作流通用节点类型基础上附加了四个关键字段:
export type AgentV2NodeType = CommonNodeType & {
agent_binding?: AgentBinding
agent_declared_outputs?: DeclaredOutputConfig[]
agent_node_kind: 'dify_agent'
agent_task?: string
version: '2'
}
判断一个节点数据是否属于 Agent V2 的守卫函数 isAgentV2NodeData 同时校验三个条件:节点 type 为 BlockEnum.Agent 或 BlockEnum.AgentV2(兼容迁移中的图)、agent_node_kind === 'dify_agent'、version === '2':
export function isAgentV2NodeData(data: CommonNodeType): data is AgentV2NodeType {
const payload = data as { agent_node_kind?: string; version?: string }
return (
(data.type === BlockEnum.Agent || data.type === BlockEnum.AgentV2) &&
payload.agent_node_kind === 'dify_agent' &&
payload.version === '2'
)
}
从源码结构看,data.type 同时接受 BlockEnum.Agent 与 BlockEnum.AgentV2,说明迁移期间旧枚举类型的节点也可能携带 V2 判别载荷,判别真正依赖的是 agent_node_kind + version 组合,而非单独的节点类型字段。
绑定类型:roster_agent 与 inline_agent
Agent V2 节点通过 agent_binding 字段关联一个 Agent 实例,types.ts 中定义了两种绑定形态,并各自配备校验函数:
| 绑定类型 | 校验函数 | 有效条件 |
|---|---|---|
roster_agent(引用 Agent 花名册中的 Agent) |
hasValidRosterAgentBinding |
binding_type === 'roster_agent' 且 agent_id 为非空字符串 |
inline_agent(内联 Agent) |
hasValidInlineAgentBinding |
binding_type === 'inline_agent',且 agent_id 与 current_snapshot_id 均为非空字符串 |
其中 needsInlineAgentBindingCreation 用于识别"已选择内联绑定但快照尚未创建"的中间态;hasValidAgentBinding 则是两者的合取。节点默认值定义在 default.ts 中,新建 Agent V2 节点即携带判别载荷,且默认采用内联绑定:
const nodeDefault: NodeDefault<AgentV2NodeType> = {
metaData,
defaultValue: {
agent_binding: {
binding_type: 'inline_agent',
},
agent_node_kind: 'dify_agent',
version: '2',
},
checkValid(payload, t) {
if (!hasValidAgentBinding(payload)) {
return {
isValid: false,
errorMessage: t(($) => $['errorMsg.fieldRequired'], {
ns: 'workflow',
field: t(($) => $['nodes.agent.roster.label'], { ns: 'workflow' }),
}),
}
}
return { isValid: true, errorMessage: '' }
},
}
checkValid 与 hasValidAgentBinding 的对应关系意味着:工作流画布上未绑定任何有效 Agent 的 Agent V2 节点会被判定为无效配置,这正是判别符体系在运行时校验层面的落地。
功能开关
Agent V2 特性由环境变量门控,实现见 feature-flag.ts:
import { env } from '@/env'
export const isAgentV2Enabled = () => env.NEXT_PUBLIC_ENABLE_AGENT_V2
从源码结构看,这是唯一的能力判定入口,前端其他位置通过该函数决定 Agent V2 界面是否可用,而不是各自读取环境配置。
agent-composer:可编辑配置状态的唯一所有者
文档第二条规则是核心架构决策:"agent-composer 拥有可编辑的 Agent 配置状态"。它包含两层含义:状态集中管理,以及任何上层组件不得复制出第二份配置 Store。
基于 jotai 的草稿原子组
配置状态的核心 Store 定义在 store.ts,只有四个原子:
export const agentComposerSavedDraftAtom = atom<AgentSoulConfigFormState | undefined>(
defaultAgentSoulConfigFormState,
)
export const agentComposerDraftAtom = atom<AgentSoulConfigFormState>(
defaultAgentSoulConfigFormState,
)
export const rebaseAgentComposerDraftAtom = atom(
null,
(_get, set, { draft }: { draft: AgentSoulConfigFormState }) => {
set(agentComposerDraftAtom, draft)
set(agentComposerSavedDraftAtom, draft)
},
)
export const isAgentComposerDirtyAtom = atom((get) => {
const savedDraft = get(agentComposerSavedDraftAtom)
const draft = get(agentComposerDraftAtom)
return !isEqual(draft, savedDraft ?? defaultAgentSoulConfigFormState)
})
这套设计对应三个概念:
agentComposerDraftAtom:当前编辑器中的实时草稿,所有表单控件直接写入它;agentComposerSavedDraftAtom:上次成功持久化(或初始化加载)的基线快照;rebaseAgentComposerDraftAtom:写原子,将草稿与基线同时重置为新值。它在版本恢复、构建草稿应用等"外部状态回灌"场景使用,重置后脏标记自然归零,避免脏检查把服务端已有内容误判为未保存修改;isAgentComposerDirtyAtom:派生原子,用fast-deep-equal比较草稿与基线得出脏状态,是自定时的触发依据。
表单状态形状与服务端配置的互转
表单状态类型 AgentSoulConfigFormState 定义在 form-state.ts,完整覆盖了 Agent 配置的所有可编辑面:
export type AgentSoulConfigFormState = {
configNote: string
prompt: string
model?: AgentComposerModel
appFeatures?: AgentSoulAppFeaturesConfig
skills: AgentSkill[]
files: AgentFileNode[]
tools: AgentTool[]
knowledgeRetrievals: AgentKnowledgeRetrievalItem[]
envVariables: EnvVariable[]
toolSettings: Record<string, Record<string, unknown>>
}
其中 tools 是联合类型:AgentProviderTool(kind: 'provider',含凭据类型 api-key | oauth2 | unauthorized、凭据变体与动作列表)与 AgentCliTool(kind: 'cli',含安装命令和环境变量),后者还支持 scope: 'secret' | 'plain' 的环境变量区分。defaultAgentSoulConfigFormState 给出所有字段的可运行空值,是 Store 的初始值与脏比较的兜底基准。
表单状态与服务端 AgentSoulConfig 之间的双向转换集中在 conversions.ts,两个核心入口函数为 agentSoulConfigToFormState(服务端到表单)与 formStateToAgentSoulConfig(表单到服务端)。转换层处理了大量字段归一化细节,例如:
- 知识检索集合(knowledge sets)在两种表示之间映射
query.mode(user_query对应自定义查询、generated_query对应 Agent 生成查询)、单路/多路检索参数(top_k、score_threshold、reranking 等),未配置时回落到DATASET_DEFAULT.top_k; - 工具凭据状态由
toCredentialVariant归一为authorized | unauthorized | none三态,供 UI 直接消费; - CLI 工具的环境变量按
secret_refs与variables拆分为scope: 'secret'(带masked: true)与scope: 'plain'两类; - 发布前的环境变量序列化(
toEnvConfig)会过滤掉键名不合法或值为空的条目,保证只有可发布变量进入载荷。
这种"表单状态 ≠ 服务端配置、中间隔一个显式转换层"的结构,正是"单一配置 Store"规则能成立的物理基础:上层组件只需要读写 jotai 原子,序列化细节被隔离在 conversions 模块内。
agent-detail/configure:组合而非复制
文档第二条规则的后半句规定了 agent-detail/configure 的职责边界:它把 agent-composer 的状态与服务端同步、构建草稿命令、预览聊天会话、版本查看和工作区面板"组合"起来,且"不得创建第二个配置 Store"。源码中这一边界体现在三处。
作用域隔离:以 agentId 为键的 ScopeProvider
配置页入口 page.tsx 最外层用 ScopeProvider 按 agentId 隔离了页面级原子:
export function AgentConfigurePage({ agentId }: AgentConfigurePageProps) {
return (
<ScopeProvider key={agentId} atoms={agentConfigureScopedAtoms} name="AgentConfigure">
<AgentConfigurePageContent agentId={agentId} />
</ScopeProvider>
)
}
被隔离的原子组定义在 state.ts:选中的版本 ID、composer rebase 版本号、soul 来源覆盖('draft' | 'build-draft' | 'view-version')、右面板模式('build' | 'preview')与各模式的会话 ID 等。key={agentId} 确保在两个 Agent 间切换时页面状态完全重建,而 agent-composer 的全局草稿原子则通过会话 key 在下方被重置(见下文),两者职责不重叠。右面板模式同时受服务端能力门控:deploymentEdition !== 'COMMUNITY' 时才允许 preview,否则强制回落 build。
composer-session:会话 key 驱动的状态重建
composer-session.tsx 是"组合"逻辑的中枢。它不持有配置状态,而是计算一个会话 key 并将其作为 AgentComposerProvider 的 key:
const composerSessionKey =
`${agentId}:${activeVersionId ?? selectedVersionId ?? 'draft'}:${composerHydrationState}:${composerRebaseRevision}`
key 由 agentId、当前查看的版本(或 draft)、composer 数据水合状态、rebase 修订号四段拼成。从源码结构看,key 变化即意味着 React 卸载重建 AgentComposerProvider 子树,从而以全新的 initialDraft 重新初始化 agent-composer 的原子组——切换 Agent、切换版本、版本恢复(触发 onComposerRebase 递增修订号)都会走到这条路径。页面侧的 rebaseAgentConfigureComposerAtom 只是一个递增计数器,真正的状态重置依赖 agent-composer 提供的 rebaseAgentComposerDraftAtom,二者配合完成"外部回灌、脏标记清零"的闭环。
use-agent-configure-sync:服务端同步的唯一实现
保存与发布逻辑集中在 use-agent-configure-sync.ts,它读取 agentComposerDraftAtom、写入 agentComposerSavedDraftAtom,是 configure 目录中唯一直接触碰 composer Store 的文件。关键机制包括:
- 5 秒防抖自动保存:
DRAFT_AUTOSAVE_WAIT = 5000。Hook 通过store.sub(agentComposerDraftAtom, ...)订阅草稿变化,仅在脏标记为真、且草稿快照与上次已保存快照(JSON.stringify比较)不同时才触发防抖保存; - 串行化保存:
saveComposer包装在useSerialAsyncCallback中,避免并发保存乱序,并用saveSequence序号丢弃过期的保存回执,防止旧的响应覆盖新的基线; - 载荷形状:保存请求体固定为
{ variant: 'agent_app', save_strategy: 'save_to_current_version', agent_soul: configSnapshot },调用consoleQuery.agent.byAgentId.composer.put生成的 mutation options; - 静默与显式两种失败语义:自动保存失败时静默返回
false并保留本地草稿(注释明确写道"Autosave is silent and keeps the local draft intact; explicit commands must stop at this boundary"),而显式保存与发布失败则向上抛出; - 页面关闭兜底:监听
visibilitychange(页面隐藏)与beforeunload,以及组件卸载时的 cleanup,调用saveDirtyDraftOnPageClose。该函数用draftKey去重,跳过已自动保存过或正在显式保存中的相同快照,避免重复请求; - 发布前校验链:
publishDraft依次检查模型已选(model_provider与model非空)、工具可发布性(getAgentToolPublishIssue检查未安装/未授权工具)、知识检索配置合法性(validateKnowledgeRetrievals),任一失败即toast.error并中止;成功后执行"保存 + 发布"两步事务并失效版本、详情、API 访问三个查询缓存。
发布入口最终由 composer-session.tsx 中的 AgentOrchestratePanel 的 onPublish 回调承接,左面板(编排/配置)与右面板(build/preview 聊天)在此完成装配。build 模式运行前还会先调用 saveDraftBeforeBuildRun 落盘当前草稿,保证 Agent 侧读到的是最新配置。
Configure 浮层与 ModalContext 迁移边界
文档第三条规则规定:配置相关的浮层(overlay)必须留在拥有该交互的特性组件内部,并使用 Dify UI 原语组合而成;现有的 ModalContext 消费者只作为遗留迁移边界存在,不是 Agent V2 新对话框的 API。
这一约束的实际含义是:Agent V2 中的确认对话框、上传对话框、版本详情等一律就近声明在使用它们的特性组件里。从目录结构可以直接验证这一点,例如 agent-detail/configure/components/confirm-clear-session-dialog.tsx 服务于切换 preview 模式前的清空确认、skills/upload-dialog.tsx 与 files/upload-dialog.tsx 分别属于技能与文件两个特性组件、access/agent-api-key-modal.tsx 属于访问页特性。每个对话框的状态与其宿主组件的生命周期绑定,不依赖全局模态上下文,这也是"不创建第二个状态 Store"规则在 UI 层的对应物。
测试视角下的边界验证
上述三条架构规则都有对应的测试文件作为可执行约束,可作为理解各边界的补充证据:
- store.spec.ts 验证草稿/基线/脏标记/rebase 的行为;
- conversions.ts 的相关用例 覆盖知识检索配置的校验;
- use-agent-configure-sync.spec.tsx 验证自动保存、发布与页面关闭保存链路;
- 节点侧 node.spec.tsx 与 default.spec.ts 验证判别载荷与绑定校验逻辑;
- permissions.spec.tsx 覆盖 permissions.ts 中的访问控制。
小结:三条规则如何落到目录结构上
| 规则 | 承载模块 | 关键实现证据 |
|---|---|---|
| 与旧版 Agent 隔离 | web/features/agent-v2、web/app/components/workflow/nodes/agent-v2 | agent_node_kind: 'dify_agent' + version: '2' 判别载荷(types.ts)、BlockEnum.AgentV2、NEXT_PUBLIC_ENABLE_AGENT_V2 开关(feature-flag.ts) |
| agent-composer 拥有唯一配置状态 | agent-composer/store.ts、conversions.ts | 四个 jotai 原子 + 脏派生原子;表单/服务端配置显式互转层 |
| configure 只做组合 | page.tsx、composer-session.tsx、use-agent-configure-sync.ts | ScopeProvider 按 agentId 隔离页面原子、会话 key 重建 composer 子树、唯一同步 Hook |
| 浮层就近、组合 UI 原语 | 各特性组件内的 dialog/modal 文件 | 对话框与宿主特性组件同目录、无全局模态上下文 |
Agent V2 前端的整体设计可以概括为:用载荷判别符在数据层面与旧版 Agent 划清界限,用一组最小化的 jotai 原子把可编辑状态收敛到单一所有者,再由 configure 页面通过作用域隔离、会话 key 重建与单一同步 Hook 完成组合。任何新增功能若绕开这三个边界(桥接旧版数据形状、复制配置 Store、引入全局模态上下文),都会破坏上述测试所固化的架构契约。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0624
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00