插件宿主调研:Dify plugin daemon 的设计与对 AI 工作台底座的启发
日期:2026-09-25。性质:技术调研(事实带出处,推断单独标注),不是裁决。起因:白晨 0925 提出 AI 工作台底座要做类似 Dify plugin daemon 的「插件宿主」——第三方上架插件即安装进宿主,前台触发动作时请求打到对应插件、宿主起子进程或其他形式取数据返回;协议用私有协议以便上架审核。本文由子代理只读调研一手源码与文档产出,维护者未改写结论。姊妹篇:工作台模块运行时与交互规范调研。
本轮白晨已定(0925 会话,口头):展示层声明式+代码档都做;10 月隔离取第一档(每插件一进程、不设沙箱,只接自有与共建方插件),对第三方开放上架前升第二档(容器+出网域名强制);底座新开仓库、与另一业务方共建;顶层 Agent Runtime(找/添加/新建组件、按记忆推荐)属顶层应用,不进底座仓库。
出处缩写(数据截止 2026-09-25,均为当日浅克隆的最新提交): 〔D〕= https://github.com/langgenius/dify-plugin-daemon/blob/c798168/ (2026-09-23) 〔S〕= https://github.com/langgenius/dify-plugin-sdks/blob/e6daca8/ (2026-09-24) 〔A〕= https://github.com/langgenius/dify/blob/e5b5a1c/ (2026-09-25) 〔Doc〕= https://github.com/langgenius/dify-docs/blob/ad67dad/ (docs.dify.ai 源仓库,2026-09-23)
调研方式:tavily 当日额度用尽,文档改读 docs.dify.ai 源仓库,issue 用 gh 查。
一页结论
- daemon 是独立 Go 服务,位于 Dify API 与插件进程之间,只对 Dify API 暴露 HTTP 接口,鉴权是一把共享静态密钥(
X-Api-Key);插件回调 Dify 走反方向的另一把X-Inner-Api-Key。 - daemon 与插件之间是一行一个 JSON 的私有协议:
session_id在同一通道上复用多个并发请求,另有心跳、日志、错误三类事件。协议承载于本地子进程的 stdin/stdout、远程调试的 TCP、serverless 的 HTTP 三种通道。 - 本地插件是常驻进程,daemon 启动或安装时拉起,默认 1 个实例;同一插件版本的一个进程服务所有租户。凭据由 Dify API 解密后随每次请求明文传入,插件不保存。
- 本地运行时没有沙箱:代码里没有 rlimit/namespace/seccomp,唯一隔离是子进程环境变量白名单;清单声明的内存上限在本地不生效(文档说主要用于 Cloud 上的 AWS Lambda 资源分配)。
- 清单是声明式 YAML:多语言标签,参数同时写「给人看的描述」与「给大模型看的描述」,工具可带 JSON Schema 形式的
output_schema。插件没有自定义前端,Dify 按 schema 渲染表单。 - 签名:包内每个文件 sha256 加创建时间,整体 RSA-4096 签名,写在 zip 注释里;默认强制验签,自建包可用管理员配置的第三方公钥白名单放行。上架走 GitHub PR:自动校验器 → 风险分级 → 人工审核。
- 事件(Trigger 插件)是「外部 webhook 打到 Dify → 插件解析 → 触发工作流」,没有插件主动推送事件给宿主的通道。
- 对我们最关键的三处差异:Dify 凭据是工作区级,我们要用户级;Dify 多租户共享一个无沙箱进程,C 端必须补隔离;Dify 在宿主机现场装 Python 依赖是 issue 重灾区,不应照搬。
1. 整体架构与一次请求的链路
事实:daemon 路由分 /plugin/:tenant_id/{dispatch,management,endpoint,debugging,asset}、对外 webhook /e/:hook_id/*path、serverless 回调 /backwards-invocation/transaction〔D〕internal/server/http_server.go。pluginGroup 统一校验 X-Api-Key〔D〕internal/server/middleware.go:21,Dify 侧写入该头〔A〕api/core/plugin/impl/base.py:180;X-Plugin-ID 头用来查该租户的安装记录〔D〕middleware.go:33。
链路(事实,由上述文件拼出):
前台 → Dify API(取工作区凭据并解密)
→ POST {daemon}/plugin/{tenant}/dispatch/tool/invoke
头 X-Api-Key、X-Plugin-ID
体 {user_id, data:{provider, tool, credentials(明文), tool_parameters}} 〔A〕impl/tool.py:104-125
→ daemon:校验密钥 → 查安装记录 → 集群内转发到插件所在节点 → 建会话(Redis 缓存)
→ 向插件 stdin 写一行 {session_id, event:"request", data} 〔D〕session_manager/session.go:285-322
→ 插件 SDK 用线程池/gevent 按 action 分发 〔S〕core/server/io_server.py:33
→ 插件往 stdout 写 {session_id, event:"session", data:{type: stream|end|error|invoke}}
(另有 heartbeat/log/error 三种 event) 〔D〕pkg/entities/plugin_entities/event.go
→ daemon 以流式响应回传 → Dify 合并分块 → 前台
反向调用:插件在会话里发 type:"invoke" → daemon 按清单权限拦截〔D〕internal/core/io_tunnel/backwards_invocation/task.go:70 起 → 调 {DIFY_INNER_API_URL}/inner/api/invoke/llm|tool|app|encrypt…,带 X-Inner-Api-Key〔D〕dify_invocation/calldify/http_request.go:22(Dify 侧校验〔A〕api/controllers/inner_api/wraps.py:29)→ 结果以 backwards_response 写回插件 stdin。
2. 三种运行时
| 类型 | 机制(事实) | 适用场景 |
|---|---|---|
| local | python -m <entrypoint> 子进程,stdio 通信〔D〕local_runtime/subprocess.go |
自部署 |
| debug | daemon 监听 TCP(gnet);插件带 REMOTE_INSTALL_KEY 主动连入并握手〔S〕core/server/tcp/request_reader.py:133,密钥经 /debugging/key 获取 |
开发者本机调试 |
| serverless | SRI 规范(daemon 调外部运行平台的一组 HTTP 接口):/v1/launch 上传包并流式返回构建进度,调用 POST {LambdaURL}/invoke?action=,回调走 /backwards-invocation/transaction〔D〕docs/runtime/sri_cn.md、serverless_runtime/io.go:316;标注 Alpha,生产级实现只在企业版 |
Cloud/弹性伸缩 |
生命周期(事实):每 30 秒同步已安装列表并拉起新插件〔D〕control_panel/server_local.go:17;每插件至少 1 个实例〔D〕control_panel/launcher_local.go:199;调度循环每 5 秒补齐实例数,多实例轮询分派〔D〕local_runtime/control.go:49、load_balancing.go。首次收到心跳才算就绪,超过 120 秒无心跳判死并杀掉、由调度循环重拉〔D〕local_runtime/instance.go:22、Monitor();SDK 默认每 10 秒一次心跳〔S〕config/config.py。单次请求超时 PLUGIN_MAX_EXECUTION_TIMEOUT 默认 600 秒,超时返回 killed by timeout〔D〕internal/types/app/default.go:29、internal/service/base_sse.go:103。
Python 依赖(事实):uv venv --python 3.12;有 pyproject.toml 则 uv sync,否则按 requirements.txt 安装,限时 10 分钟,之后预编译字节码;配置了镜像时优先用 requirements.txt〔D〕local_runtime/setup_python_environment.go:141,166,425。相关配置 PIP_MIRROR_*、PYTHON_ENV_INIT_TIMEOUT〔D〕config.go:228-239。
3. 插件包与清单
- 包格式:
.difypkg即 zip〔D〕pkg/plugin_packager/signer/sign.go;唯一标识author/name:version@checksum〔D〕plugin_entities/identity.go:21。 - 清单字段:version、type、author、name、label/description(多语言)、icon、
resource{memory, permission}、plugins{tools, models, endpoints, agent_strategies, datasources, triggers}、meta{arch, runner{language, version, entrypoint}, minimum_dify_version}、privacy、repo〔D〕plugin_entities/plugin_declaration.go:115-162。 - 能力权限:
permission.{tool, model.{llm, text_embedding…}, node, endpoint, app, storage.size},只约束反向调用与注册 endpoint。 - 插件类型:tool、model、agent strategy、extension(endpoint)、datasource、trigger 六类,一个包可组合多类(model 除外)〔Doc〕
en/develop-plugin/getting-started/choose-plugin-type.mdx。 - 参数 schema:
ToolParameter有 name、label、human_description、llm_description、type、form、required、default、min/max、options 等〔D〕plugin_entities/tool_declaration.go:128;type 共 17 种(string/number/select/secret-input/file/object/array/date…),form 取 schema/form/llm;工具可带output_schema(JSON Schema)。多语言结构只支持 en_US、zh_Hans、ja_JP、pt_BR〔Doc〕features-and-specs/plugin-types/general-specifications.mdx。
4. 安装、上架与审核
- 三种来源:市场(自动检查+人工审核,一键安装)、GitHub(无审核,按仓库地址装)、本地包(无审核,上传装)〔Doc〕
publishing/marketplace-listing/release-overview.mdx;daemon 对应install/upload/package、install/identifiers、install/upgrade。 - 签名〔D〕
pkg/plugin_packager/decoder/verifier.go:默认FORCE_VERIFYING_SIGNATURE=true,签名不符直接拒〔D〕internal/service/plugin_decoder.go:57;可开第三方公钥白名单〔D〕config.go:190-198;工具链dify signature generate/sign/verify〔Doc〕standards/third-party-signature-verification.mdx。签名里带发布者分级 langgenius/partner/community。 - 上架〔Doc〕
submit-plugin-to-marketplace.mdx:向langgenius/dify-plugins提 PR;本地先跑官方校验器(机密泄露、二进制、依赖漏洞〈查 OSV 库〉、出网域名等);PR 里自选低/中/高风险;人工审核合并后再校验一次并上传市场。清单可写network.domains声明出网域名——推断:daemon 的清单结构体里没有该字段,所以它只用于审核、运行时不强制。 - 升级:租户可设自动升级策略 FIX_ONLY/LATEST〔A〕
api/services/plugin/plugin_auto_upgrade_service.py;谁能装、谁能调试可按租户配置(plugin_permission_service.py)。
5. 凭据与安全隔离
- 凭据存放:Dify 数据库,按租户 RSA 密钥加 AES 混合加密,密钥提供方可换 Azure KeyVault〔A〕
api/core/helper/encrypter.py、api/extensions/ext_key_provider.py。 - 作用域是工作区级:唯一约束
(tenant_id, provider, name),user_id只记创建人〔A〕api/models/tools.py:81。 - OAuth:管理员配 OAuth client;插件只实现「取授权地址/换令牌/刷新」三个钩子,授权流程由 Dify 编排〔Doc〕
dev-guides-and-walkthroughs/tool-oauth.mdx。 - endpoint 设置项同样加密,daemon 调
/inner/api/invoke/encrypt解密〔D〕internal/service/endpoint.go:134。 - 隔离手段:子进程只继承白名单环境变量(PATH、代理等),防
DB_PASSWORD/SERVER_KEY泄给插件〔D〕subprocess.go:42;此外无 rlimit/namespace/seccomp(grep 无结果),各插件与 daemon 同一系统用户、共享 cwd。推断:恶意插件能看到流经它的所有租户凭据。
6. 事件与异步
- 流式:插件用生成器逐条产出 text/json/blob_chunk/variable/log 等〔S〕
entities/invoke_message.py:120,通道上表现为stream…end。 - Endpoint 插件:外部请求打
/e/{hook_id}/…,daemon 把原始 HTTP 请求转成十六进制交插件,插件回完整 HTTP 响应〔D〕endpoint.go:181。 - Trigger 插件:外部 webhook 打 Dify
/triggers/plugin/{endpoint_id}〔A〕api/controllers/trigger/trigger.py:17;Dify 调插件dispatch_event解析出事件,再逐个invoke_event触发工作流;另有subscribe/unsubscribe/refresh管订阅〔D〕io_tunnel/access_types/access.go。 - 主动推送:全部动作枚举里没有插件主动发起的事件,反向调用只能在会话内(事实)。
7. 前端界面
清单结构没有任何前端字段(事实)。配置表单由 ProviderConfig 的 type 决定(secret-input/text-input/select/boolean/model-selector/app-selector…),label/help/placeholder 均为多语言对象〔D〕plugin_entities/config.go:283。推断:Dify 前端按类型渲染对应控件(前端代码未读)。唯一绕行是 endpoint 返回 text/html〔Doc〕dev-guides-and-walkthroughs/endpoint.mdx:104。
8. 已知问题(issue 标题与状态)
- 自建包验签失败:dify#14713、#16035、#13842、#31486(均已关闭,回复多)。
- 离线、公司代理、依赖安装失败:daemon#48(离线仍去 PyPI)、#329(公司代理 CA 不被信任)、#650、#373、#649。
- 进程未拉起/集群找不到插件:daemon#71、dify#14718(“no available node, plugin not found”)。
- 超时、并发、资源:daemon#371(killed by timeout)、dify#36023(10 个并发即明显变慢)、dify#15222(大量小文件致 daemon 崩溃)、dify#34409(持续内存溢出,未细读)。
- 官方自述:社区版不能平滑横向扩副本;共享 cwd 放网络卷会拖慢插件启动〔D〕
README.md。
9. 对我们的启发
我们与 Dify 的差别:我们的插件主要提供数据接口(查、增删改、事件)给工作台模块和上层 agent,不是给 LLM 工作流当工具;我们是 C 端,用户各自授权自己的飞书等账号;插件要带可视化模块。
| 照搬 | 改造 | 不要 |
|---|---|---|
JSON 行协议+session_id 多路复用+心跳+stream/end/error 三态:简单,可跨 stdio/TCP/HTTP 复用 |
凭据改用户级:键为用户+插件+连接,C 端每人各自授权飞书,不能沿用工作区级 | 在宿主机现场装 Python 依赖:issue 最多;应在上架时构建成镜像或锁定制品 |
| 清单声明式+字段级多语言标签+「人/大模型」双描述:直接服务上层 agent 理解字段 | 一个插件一个共享进程 → 按插件(必要时按用户)隔离并加沙箱:多用户凭据流经同一进程 | 宿主与 API 之间单把静态共享密钥+请求体明文凭据:至少改内网 mTLS 或短期令牌 |
| 宿主持有并加密凭据、按请求注入,插件无状态:插件被攻破也拿不到存量凭据 | action 从按插件类型写死几十个,改为通用「资源+操作(查/增/删/改/订阅)」,输入输出强制 JSON Schema:我们做的是数据接口,不是给大模型的工具 | tool/model/agent strategy/datasource 多套专用接口:用不上 |
| 反向调用在宿主按清单权限拦截:审核有明确的权限面 | 出网域名运行时强制(Dify 只用于审核声明):C 端安全要靠执行,不能只靠申报 | 用 endpoint 返回 HTML 当插件界面:插件 UI 另行定义(见姊妹篇) |
| RSA 签名+平台公钥+第三方公钥白名单+发布者分级:满足「上架审核、验证」 | 事件双通道:外部 webhook 入口(照 Trigger)+宿主提供的 emit 反向调用与投递队列,支持插件主动推送 | 依赖 GitHub PR 的上架流程:应有自己的审核后台 |
| OAuth 由宿主编排、插件只实现三个钩子:飞书等授权体验统一 | 超时按操作在清单声明(秒级),替代全局 600 秒:C 端交互要快失败 | |
| 风险分级+自动校验器+人审;远程调试运行时:降低审核与开发成本 | serverless 规范(SRI)可作隔离与弹性的备选形态参考 |
未核实项
- Dify Cloud 生产到底用哪种运行时:文档只说 memory 主要用于 Lambda,据此推断是 serverless。
- 市场是否在合并后用官方私钥签名:文档只说合并后「再校验并上传」。
- Dify 前端表单渲染代码未读。
- 企业版有无进程级沙箱或多副本调度:官方只说「请联系商务」。
- dify#34409、daemon#760 根因未细读。