# 插件宿主调研：Dify plugin daemon 的设计与对 AI 工作台底座的启发

> **日期**：2026-09-25。**性质**：技术调研（事实带出处，推断单独标注），不是裁决。**起因**：白晨 0925 提出 AI 工作台底座要做类似 Dify plugin daemon 的「插件宿主」——第三方上架插件即安装进宿主，前台触发动作时请求打到对应插件、宿主起子进程或其他形式取数据返回；协议用私有协议以便上架审核。本文由子代理只读调研一手源码与文档产出，维护者未改写结论。姊妹篇：[工作台模块运行时与交互规范调研](2026-09-25-工作台模块运行时与交互规范调研.md)。
>
> **本轮白晨已定（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` 查。

## 一页结论

1. daemon 是独立 Go 服务，位于 Dify API 与插件进程之间，只对 Dify API 暴露 HTTP 接口，鉴权是一把共享静态密钥（`X-Api-Key`）；插件回调 Dify 走反方向的另一把 `X-Inner-Api-Key`。
2. daemon 与插件之间是**一行一个 JSON 的私有协议**：`session_id` 在同一通道上复用多个并发请求，另有心跳、日志、错误三类事件。协议承载于本地子进程的 stdin/stdout、远程调试的 TCP、serverless 的 HTTP 三种通道。
3. 本地插件是**常驻进程**，daemon 启动或安装时拉起，默认 1 个实例；**同一插件版本的一个进程服务所有租户**。凭据由 Dify API 解密后随每次请求明文传入，插件不保存。
4. 本地运行时**没有沙箱**：代码里没有 rlimit／namespace／seccomp，唯一隔离是子进程环境变量白名单；清单声明的内存上限在本地不生效（文档说主要用于 Cloud 上的 AWS Lambda 资源分配）。
5. 清单是声明式 YAML：多语言标签，参数同时写「给人看的描述」与「给大模型看的描述」，工具可带 JSON Schema 形式的 `output_schema`。插件**没有自定义前端**，Dify 按 schema 渲染表单。
6. 签名：包内每个文件 sha256 加创建时间，整体 RSA-4096 签名，写在 zip 注释里；默认强制验签，自建包可用管理员配置的第三方公钥白名单放行。上架走 GitHub PR：自动校验器 → 风险分级 → 人工审核。
7. 事件（Trigger 插件）是「外部 webhook 打到 Dify → 插件解析 → 触发工作流」，**没有插件主动推送事件给宿主的通道**。
8. 对我们最关键的三处差异：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 根因未细读。
