本页目录(33 节)

连接器

Spark Work 连接器完整指南:先分清「连接器」在代码里的三种含义(扩展中心的插件型连接器、平台管理 MCP 的 github_* 工具、远程连接的 IM 机器人),再详述 plugin.json 完整字段与四类 contribution、source/trust 枚举、github / google / notion / obsidian 四个内置运行时的真实工具清单与能力开关、九个权限与「只有聚合闸门、无逐权限强制」的真实强制力、本地目录导入与默认不可用的市场、凭据只进系统 keystore、运行时的工具如何以 mcp__spark_plugins__<runtime>_<tool> 注入 Agent,以及 GitHub 两套实现的差异与 ConnectorsPanel 未被挂载这一界面缺口。附 20 行排查表。

阅读约 20 分钟最后核对 2026-09-19

「连接器」是 SparkWork 里最容易被混淆的一个词:它在代码里同时指向三套互不相同的子系统, 而这三套的入口、存储表、鉴权方式和排查手段都不一样。这一页先把三者分开, 然后重点讲扩展中心里的插件型连接器(含 GitHub / Google / Notion / Obsidian 四个内置运行时), 以及平台管理 MCP 那套 15 个 github_* 工具。 IM 机器人(Telegram / 飞书 / QQ)属于第三套,本文只做概览,细节见远程连接。

1. 先分清「连接器」在 SparkWork 里的三种含义

同一个中文词对应三套代码,混用会导致「工具名对得上但存储表对不上」这类错误, 所以先看一张对照表。

1.1 入口:扩展中心 → 连接器

  • 左侧导航栏的「扩展中心」(视图 id mcp,文案键 nav.extensions, 见 apps/desktop/src/renderer/App.tsx:283 与design/i18n/locales.ts:319)。快捷键是 ⌘5 / Ctrl+5(design/hooks/useKeyboard.ts:136-144),命令面板里叫「连接器与 MCP 视图」。
  • 扩展中心有四个页签,定义在 design/views/McpView.tsx:255-262:MCP / 自定义工具 / 连接器 / 团队商店。
  • 点「连接器」页签渲染的是 <PluginMarketplaceView embedded />(McpView.tsx:562-563)——这是本文的主角。
  • 注意 locales.ts 里还有 nav.mcp 与 nav.plugins 两条词条, 值都是「连接器」,但代码里没有任何渲染点引用它们;侧边栏实际用的是nav.extensions。别照词条名猜菜单路径。

1.2 三者对照

维度插件型连接器平台管理 MCP 的 GitHub 连接器远程连接(IM 机器人)
界面入口扩展中心 →「连接器」页签代码里有一个 GitHub 面板,但没有被挂载(见 §10.3)设置 → 远程连接
核心代码services/plugins/ + services/plugin-runtime/services/github-connector.service.tsapps/desktop/src/main/services/RemoteConnectionService.ts
存储表plugins / connector_accountsconnector_connectionsapp_settings 的 remote-connections/data
凭据存放系统 keystore(表里只留 credential_ref)系统 keystore(表里只留 keystore_ref)明文 JSON(不在 keystore)
Agent 侧工具名mcp__spark_plugins__<runtime>_<tool>mcp__spark_platform__github_*无工具;走聊天消息驱动
本文覆盖§2 – §9 详述§10 详述§11 概览 + 跳转

2. 连接器页面上有什么

页面根容器是 PluginMarketplaceView(design/views/PluginMarketplaceView.tsx), 自顶向下只有两个区块,界面文案如下(均为逐字引用):

位置文案 / 控件代码位置
页头说明安装、连接并控制 Agent 可以使用的外部服务。PluginMarketplaceView.tsx:803
页头右侧按钮导入本地连接器:804-811
区块一标题已安装连接器 + 计数胶囊 N 个连接器:814-819
区块一空态还没有连接器 / 从本地导入,或从已验证的市场来源安装。:821-831
区块二标题连接器市场 + 副标题 来自已配置的可信市场源:903-904
区块二搜索框placeholder 搜索名称或能力:906-913
区块二加载 / 空 / 错误正在同步市场目录… / 当前来源没有匹配的连接器。 / 无法连接连接器市场 + 重新加载:915-930

出厂状态下你看不到「连接器市场」这一整块。它被 {marketConfigured && (…)}包住(:899),而 marketConfigured 要求至少有一个「已启用且配置了指纹」的市场源。 默认注册表 spark-official 指向占位地址 https://plugins.spark-agent.com/v1, 已被迁移脚本主动停用(071_disable_placeholder_plugin_marketplace.sql), 且 configured = enabled === 1 && trustedKeyFingerprints.length > 0(plugin-manager.service.ts:42)。所以默认情况下这块是整块消失、没有空态文案, 不是加载失败。详见 §8.2。

已安装卡片上会出现这些元素:

  • 副标题 {插件作者} · v{版本},状态徽标 已启用 / 已停用。
  • 来源徽标三选一:已验证(verified)/ 内置(bundled)/ 本地(unverified); 同名插件合并时多一枚 已合并 N 个来源。
  • 贡献计数 chips,固定顺序且为 0 则不显示:Skill N、MCP N、连接器 N、运行时 N;全为 0 时显示 未声明能力(:75-95)。
  • 权限计数 权限 {已授权}/{总数};有未授权项时底部追加一条 待授权:…。
  • 卸载按钮的 title 是 移除非内置连接器——纯内置插件不渲染这个按钮。

3. 内置的四个运行时

「运行时」是连接器真正干活的执行体:一个运行时可连接多个账号,并对外暴露一组工具。 宿主内置了四个,注册点在 services/plugin-runtime/builtin-runtimes.ts:8-11, 对应四个 adapter 文件。没有 Slack / Jira / Linear / Zoom / Teams / 通用 REST 运行时。

runtime id显示名pluginId工具前缀账号模式工具数
githubGitHubspark.githubgithub_multiple15
googleGoogle Workspacespark.googlegoogle_multiple16
notionNotionspark.notionnotion_multiple8
obsidianObsidian Vaultspark.obsidianobsidian_multiple8

3.1 GitHub

github-runtime.adapter.ts。能力 id 与界面标签:identity→身份、repositories→仓库、contents→文件、issues→Issue、pull_requests→Pull Request,五个默认全开 (:36-62)。

工具(前缀 github_)所需能力风险作用
get_statusidentityread读取已连接的 GitHub 身份
list_repositoriesrepositoriesread列出账号可访问的仓库
get_repositoryrepositoriesread读单个仓库元数据
read_filecontentsread读仓库文件(base64 自动解码)
list_issuesissuesread列仓库 Issue
get_issueissuesread读单个 Issue
list_pull_requestspull_requestsread列 PR
get_pull_requestpull_requestsread读单个 PR
create_issueissueshigh-write创建 Issue
comment_issueissueshigh-write给 Issue 加评论
update_issueissueshigh-write改 Issue 标题 / 正文 / 状态
comment_pull_requestpull_requestshigh-write给 PR 加评论
create_branchcontentshigh-write从 SHA 或默认分支建分支
create_pull_requestpull_requestshigh-write创建 PR
upsert_filecontentshigh-write创建或更新仓库文件

写操作另有一道 adapter 私有闸门 requireWrite:要求账号配置里allowWrites === true,否则报 CAPABILITY_DISABLED(:436-442)。只有 GitHub 有这个账号级写开关,Google / Notion / Obsidian 都没有。 另外 invokeTool 接受一个未暴露的别名 status(等同 get_status)。

3.2 Google Workspace

google-runtime.adapter.ts,16 个工具,覆盖 Gmail 与 Calendar:

能力默认工具
gmail_read → Gmail 读取开gmail_search_messages、gmail_get_message、gmail_get_thread、gmail_get_attachment、gmail_list_labels
gmail_write → Gmail 写入关gmail_create_draft、gmail_update_draft(low-write,不触发确认)
gmail_send → Gmail 发送关gmail_send_draft(high-write,需确认)
gmail_manage → Gmail 标签管理关gmail_modify_labels
calendar_read → 日历读取开calendar_list_calendars、calendar_list_events、calendar_get_event、calendar_query_freebusy
calendar_write → 日历写入关calendar_create_event、calendar_update_event、calendar_cancel_event(destructive)

3.3 Notion

notion-runtime.adapter.ts,8 个工具。读取能力 notion_read→读取默认开; 写入能力 notion_write→写入默认关。工具:search、get_page、get_block_children、query_data_source、create_page、update_page、append_block_children、archive_page。

两点容易误解:query_data_source 用的是新版 data source 概念,不是旧版 database;archive_page 是归档,运行时不提供永久删除(:177-179)。

3.4 Obsidian Vault

obsidian-runtime.adapter.ts,8 个工具,直接操作本地 Vault 目录,不走网络。 读取能力 vault_read→读取笔记默认开;写入能力 vault_write→编辑笔记默认关。 工具:list_vaults、search_notes、get_backlinks、get_note、create_note、update_note、move_note、trash_note。

沙箱约束比较硬,值得单独记:

  • 路径逃逸或访问隐藏目录 → RESOURCE_OUT_OF_SCOPE(:344-363)。
  • 符号链接逃逸同样被拦(:371-391)。
  • 单文件上限 2 MiB(:17、:409-414)。
  • 写盘是原子写 + 0600 权限(:419-436)。
  • update_note 用 expectedHash 做乐观锁,hash 不匹配报 CONFLICT;create_note 目标已存在也报 CONFLICT(:254-273)。
  • trash_note 只是移到 .trash/Spark,不做永久删除(:299-306)。

另外 search_notes 最多返回 100 条(:207);invokeTool 还接受 list_files、search、read_note三个别名,但它们不出现在工具列表里。

4. 连接账号

运行时本身不等于可用:必须连上至少一个账号,Agent 才会拿到它的工具。 卡片上的入口按钮文案是 连接账号(已有账号时变成 账号设置,:204)。

4.1 各运行时的认证方式与凭据字段

运行时声明的 authMethods界面实际走哪条凭据字段名
github['pat', 'device-code', 'github-app']只有 pattoken(输入框标签 Fine-grained PAT,placeholder github_pat_…)
google['oauth2']oauth2accessToken、refreshToken、expiresAt
notion['oauth2', 'api-key']两条都能走(界面有「OAuth 授权 / 手动令牌」切换)token 或 accessToken、expiresAt
obsidian['none']none(选目录,不填令牌)无;配置项是 config.vaultPath

device-code 与 github-app 只是声明,没有实现。这两个值在全仓库只出现在协议枚举与 github-runtime.adapter.ts:28, 没有设备码轮询、也没有 App JWT / installation token 的代码。别把它们当成可选项。

界面按运行时分支构造请求(PluginMarketplaceView.tsx:261-290): obsidian → authMethod: 'none' + config.vaultPath; github → 'pat' + secrets.token; notion → 'api-key' + secrets.token; 其余 → 'oauth2' + secrets.accessToken。 密钥字段有两道服务端约束:最多 32 个字段、字段名 ≤120 字符、值 ≤16,000 字符; 且 config 里出现疑似密钥的键名会被直接拒绝,报Secret-like field must be sent through secrets(runtime-broker.ts:618-661, 694-698)。

4.2 凭据存在哪里

凭据只进系统 keystore,不进 SQLite。整包StoredCredentialBundle(accessToken / refreshToken /tokenType / expiresAt / clientId / scopes) 序列化后写入 keystore(token-service.ts:10-46); 迁移脚本的注释写得很直白:Secrets remain in the OS keystore; SQLite stores only credential_ref.(069_plugin_runtime_accounts.sql:1-2)。

  • 表里只有一列引用:connector_accounts.credential_ref。
  • ref 的形状是 plugin-runtime-<pluginId>-<runtimeId>-<base64url(账号id)>, 例如 plugin-runtime-spark.github-github-…(token-service.ts:36-39)。
  • 断开账号会连带删除 keystore 里的 secret(runtime-broker.ts:178)。
  • 刷新令牌是并发单飞的:同一 ref 的并发刷新只真正执行一次并复用同一个 Promise (token-service.ts:112-128)。触发条件是 expiresAt 距今不足30 秒(:90-93)。
  • 高风险工具的确认令牌只存在进程内存(runtime-broker.ts:49),不持久化, 默认 TTL 60 秒并裁剪到 [1 秒, 5 分钟];且用掉即失效, 重复使用同一 token 会再次报 CONFIRMATION_REQUIRED(:327-338, 550-564)。

4.3 OAuth 授权流程

点「打开授权页」后走的是标准 Authorization Code + PKCE, 实现集中在 services/plugin-runtime/oauth-broker.ts:

  1. 生成随机 state(24 字节)与 verifier(48 字节),challenge = sha256(verifier);授权 URL 带response_type=code、client_id、redirect_uri、scope、state、code_challenge、code_challenge_method=S256(:24-39)。
  2. 回调服务器只监听 127.0.0.1:<随机端口>,默认路径 /oauth/callback, 超时 5 分钟,回调只能被消费一次;state 校验失败报OAuth state validation failed(:43-44, 124-186)。
  3. 用 grant_type=authorization_code + code_verifier 换令牌, 表单编码、30 秒超时(:51-65, 80-91)。
  4. 浏览器由宿主打开:桌面端调 shell.openExternal(url), 拿到令牌后以 authMethod: 'oauth2' 调 runtime.connect(...)(registerPluginRuntimeIpc.ts:84, 96-108)。

Google 会额外带 extraAuthorizationParams: { access_type: 'offline' }以便拿到 refresh token(PluginMarketplaceView.tsx:618-620)。 刷新时如果 provider 没有回新的 refresh token,会保留旧值而不是清空 (oauth-broker.ts:67-78)。

4.4 账号状态与断开

账号状态枚举(协议里六个)与界面文案的对应关系:

状态值界面文案
connected 且 enabledAgent 可用
needs_auth需要重新授权
syncing同步中
disabled 或 enabled=false已停用
error连接异常
其他(含 not_configured)尚未连接

这些文案来自 PluginMarketplaceView.tsx:252-259。 连接成功提示 {运行时名} 已连接 {账号名}; 断开按钮没有二次确认弹窗,点一下就直接断开并提示已断开 {账号名}(:649-659)。

5. 能力开关

账号级能力开关在「账号设置」弹窗底部的「默认能力」小节 (PluginMarketplaceView.tsx:432-453)。每个能力显示中文 label, 鼠标悬停显示 description;账号还没连上时所有按钮都是禁用的。 点击后调 plugin-runtime:accounts:update,只提交 enabledCapabilities一个字段(:663-681);失败提示 能力开关更新失败。

能力关闭的后果是双重的,这点必须理解清楚:

  • 工具直接不出现。工具目录会要求「至少有一个已连接账号满足该工具的全部 requiredCapabilities」,不满足就整条不进目录(runtime-broker.ts:248-275)。
  • 即使硬调也会被拒。调用路径上逐个 requiredCapability 校验, 不通过报 CAPABILITY_DISABLED(runtime-policy.ts:42-64)。

能力校验一共三层:① 该能力必须在账号的 enabledCapabilities 里; ② 该能力必须存在于运行时的 descriptor.capabilities 里; ③ 能力的 requiredScopes 必须是账号 grantedScopes 的子集, 否则报 SCOPE_REQUIRED。第 ③ 条解释了为什么 OAuth 授权时没勾的 scope, 事后怎么开能力开关都没用——得重新授权。

界面上的两个已知限制,避免你误判成 bug:

  • 能力开关只作用于第一个账号。代码取的是 accounts[0](:436),而四个内置运行时的 accountMode 都是 multiple。 连了多个 GitHub 账号时,这个开关只反映和修改第一个账号的能力。
  • 连接时的初始能力不由你选。首次连接提交的是各能力自己的enabledByDefault(:279-282, 603-605), 因为此时开关按钮还是禁用状态。想改只能连上之后再点。

6. plugin.json:连接器的清单契约

一个可安装的「连接器包」就是一个含 plugin.json 的目录。 清单的权威定义只在 packages/protocol/src/plugin.ts(PluginManifestSchema,:135-225)——packages/plugin-sdk 里没有清单类型或解析器,它只提供defineTool / defineConnectorRuntime 之类的编写辅助。 协议版本常量是 PLUGIN_PROTOCOL_VERSION = 2,接受 [1, 2] 两版 (:18-19)。

6.1 清单字段

字段约束必填默认
schemaVersion1 或 2是—
id^[a-z0-9][a-z0-9._-]{2,95}$是—
version语义化版本,可带 -pre / +build是—
displayName1–160 字符是—
description1–8000 字符是—
author{ id?, name, url? },name 必填是—
license / homepageUrl / repositoryUrl / icon字符串或 URL否—
runtime{ type: 'builtin', id },旧式单运行时绑定否—
categories / tags字符串数组,上限 20 / 50否[]
permissions.required / .optional权限名数组,各上限 32;required 不得重复否[]
activationmanual / on-startup / on-demand否manual
contributions见 6.2否三个键为空数组,不含 runtimes

有个容易踩的坑:contributions 的默认对象没有 runtimes 键(plugin.ts:199),所以 manifest.contributions.runtimes 默认是undefined,代码里到处用 ?? [] 兜底。自己写清单时建议显式写出来。

还有两条跨字段校验(superRefine,:201-225):mcpServers[].permissions 与 connectors[].permissions请求的权限,必须已经出现在 permissions.required ∪ optional 里, 否则分别报 MCP contribution requests undeclared permission 与Connector contribution requests undeclared permission。

6.2 四类 contribution

清单只能贡献四类东西,没有 commands / agents / hooks / tools:

键用途上限必需字段
skills随包提供的 Skill100id、path(包内相对路径,目录需含 SKILL.md)
mcpServers随包提供的 MCP 服务50id、name、config
connectors连接器能力声明50id、manifest
runtimes连接器运行时50id、kind: 'connector'、execution、toolNamespace、provider

path 用的是 RelativePackagePathSchema:不得超过 240 字符, 不得以 / 开头、不得含 \、不得含 .. 段 (:41-50)。

要特别注意 contributions.connectors 的实际作用很有限: 它唯一的消费点是写一行 plugin_resources 记录、 外加安装前检查时的一条提示「连接器只声明接入能力,首次使用账号前仍需单独授权。」(plugin-manager.service.ts:626-640、plugin-package.ts:115-117)。 真正能用的 provider 能力来自宿主代码里注册的内置 adapter。写一个 connector contribution 并不会自动接通任何东西。

execution 有三种取值:{type:'builtin', adapter}、{type:'remote-mcp', url}、{type:'worker', entrypoint, packageSha256}。 但宿主只把 builtin 当成可运行(plugin-manager.service.ts:398), 另外两种的 runtime 行会写成 enabled = 0 并附unavailableReason: 'isolated-runtime-host-required'(:641-654)。 另外 worker 的 packageSha256 必须是 64 位 hex。

6.3 source 与 trust

这两个枚举存的是不同维度,source 是「从哪来」,trust 是「信不信」:

source谁写入对应 trust
bundled内置清单初始化bundled
local本地目录导入unverified(硬编码,本地包不做签名校验)
marketplace市场安装verified(要求签名指纹匹配)

trust 的类型里还有 blocked,DB 的 CHECK 也允许它, 但没有任何写入方——别指望靠它表达「拉黑」。同理state 枚举里的 error 也没有写入方; 实际只写 installed 与 blocked,而且用户主动停用时写的仍然是installed(plugin-manager.service.ts:334)—— 也就是说「停用」不会把 state 变成 blocked,看 state判断不出用户是否停用过,要看 enabled。

7. 权限模型

连接器包在安装时要申请权限。这里的模型比想象中粗:它是插件级的一次性授权(全有或全无),不是细粒度的运行时能力控制。 理解这一点能省掉很多排查时间。

7.1 九个权限与界面标签

权限名是协议固定的九个(packages/protocol/src/plugin.ts:21-31), 界面标签表在 PluginMarketplaceView.tsx:22-32:

权限 id界面标签安装前检查里的风险等级
network访问网络medium
filesystem.read读取本地文件medium
filesystem.write写入本地文件high
process.spawn启动本地进程critical
secrets.read读取凭据critical
clipboard访问剪贴板low
browser控制浏览器high
mcp.connect连接 MCP 服务high
connector.account访问连接器账户high

风险映射在 plugin-package.ts:93-103;遇到清单里未登记的权限名会回落成high(:111),这个回落只影响展示颜色,不影响判定。 每个权限的持久化状态只有三种:granted / denied / pending(表 plugin_permissions,DB CHECK 同集合)。

7.2 权限实际被强制到什么程度

只有一个聚合闸门是真正在执行期生效的: 「permissions.required 是否全部 granted」。它影响三处:

  1. 安装与启用。以 enable=true 安装、或启用时若 required 没授权齐, 直接抛 PluginPermissionError,消息形如Plugin requires explicit permission: <ids>(plugin-manager.service.ts:326-333, 488-492)。
  2. 资源开关。所有贡献资源的 enabled 都会乘上这个判定; MCP 与 connector contribution 还额外要求它自己声明的permissions 数组全部 granted(:596-597, 628-629)。
  3. 运行时可用性。isRuntimeEnabled(runtimeId) 的定义是enabled === true && state === 'installed' && permissionsReady(:408-411),而所有连接器调用都要过这一关,不通过报PLUGIN_DISABLED。

反过来说,没有任何一处代码在「要执行某个动作」时去检查对应的权限。 对 process.spawn、filesystem.write、clipboard、browser、network、secrets.read、mcp.connect这七个名字做全仓检索,除了协议定义、风险映射表、内置清单和界面标签表之外没有消费者:不存在「检查了 process.spawn 才允许起进程」的代码。 甚至启用插件里的 MCP 服务时,看的也只是那个 MCP contribution 自己声明的permissions,而不是 mcp.connect。

所以准确的口径是:权限是给用户在安装前看的风险清单 + 一个聚合开关, 不是操作系统级的沙箱。真正细粒度的控制发生在运行时能力层(§5)与 工具包/自定义工具的权限层,那是另一套机制。

顺带一个界面缺口:主进程注册了 plugin:set-permission 通道, 但整个渲染进程没有任何调用点。也就是说界面上只能看到待授权:… 与 权限 N/M,无法逐项授予或拒绝权限—— 安装时是全量批准 required 列表(PluginMarketplaceView.tsx:750-757)。 另有 plugin-marketplace:update 同样只有主进程 handler、界面无入口, 所以市场源也只能靠预置配置,见 §8.2。

8. 安装、启用与卸载

8.1 本地目录导入

  1. 点页头(或空态里)的 导入本地连接器。
  2. 弹出系统目录选择器,标题 选择连接器目录;取消则直接返回。
  3. 调 plugin:inspect-local 做预检,成功后弹出「安装前检查」弹窗: 显示插件名与版本、描述、作者、内容(N 个文件 · SHA-256 …)、必需权限标签列表,以及预检告警。
  4. 点 授权并安装,调 plugin:install-local, 载荷里的 approvedPermissions 是必需权限的全量(没有逐项勾选 UI),enable: true。
  5. 成功提示 连接器已安装并启用。

预检本身做得比较严(plugin-package.ts:69-120),失败信息也直接可读:

检查项限制 / 报错
必须是目录Plugin source must be a directory
根目录必须有 plugin.jsonPlugin package is missing plugin.json
文件数上限20,000 个
包体积上限250 MiB
符号链接直接拒绝
路径逃逸直接拒绝
Skill 的 path必须在包内且含 SKILL.md
源目录位置不能位于受管插件目录内:Plugin source must be outside the managed plugin directory

安装会做原子替换:先复制到 .<id>.<uuid>.staging, 把旧目录备份成 .<id>.<uuid>.backup,再 rename 上位; 出错时回滚备份(plugin-package.ts:126-155)。 受管目录在桌面端是 &lt;userData&gt;/plugins(registerPluginIpc.ts:17),临时目录是 <系统临时目录>/spark-plugin-installs。

「升级」就是重新安装同一个 id:DB 走 upsert 覆盖, 但不改 installed_at。没有 update / upgrade / 回滚方法, 也没有自动检查更新——只改版本号不会被识别为升级,必须重新导入一次。

8.2 市场安装(默认不可用)

市场相关代码是完整的:能列出市场源、搜索(plugin-marketplace:search,默认limit: 24)、下载 zip 校验 sha256、 用系统 tar 解包并校验每个条目路径、要求包内恰好一个plugin.json、校验清单 id/version 与市场条目一致。 安装要求市场条目的 trust === 'verified',否则报Marketplace plugin is not signed by a trusted registry key。

但默认状态下一个市场源都不可用:

  • 迁移脚本种下的唯一注册表 spark-official(名称「Spark 官方插件市场」) 指向占位地址 https://plugins.spark-agent.com/v1, 并且被 071_disable_placeholder_plugin_marketplace.sql在满足「地址与指纹都是占位值」时主动置为 enabled = 0。 脚本注释写明:不是已部署的服务,保留该行供将来显式配置,但不要把它呈现为可用。
  • 即便启用,configured 还要求 trustedKeyFingerprints 非空 (plugin-manager.service.ts:42),而种子行的指纹是 []。
  • 只有 configured 的市场源才会真正发请求(:439-443)。

结论:出厂状态下无法搜索或安装任何市场连接器,界面也不显示市场区块(§2)。 要真正用市场,得先有可用的市场源并配置 API 地址与签名指纹—— 而配置用的 plugin-marketplace:update 通道界面里没有入口(§7.2), 目前只能走代码或数据库预置。

8.3 启用、停用与卸载

  • 启用 / 停用用卡片上的开关。启用时若 required 权限没授齐会直接报错; 停用会把该插件的所有贡献资源 enabled 置 0, 于是它的 Skill、MCP、运行时工具全部从 Agent 侧消失。 停用不会删除任何配置或凭据。
  • 卸载按钮的 title 是 移除非内置连接器, 只有组里存在非内置成员时才渲染。确认弹窗文案: 多个非内置时是 将移除该名称下的 N 个非内置连接器;内置运行时会保留。, 单个时是 移除非内置连接器后,它提供的 Skill、MCP 和账号配置将不再可用。
  • 内置的四个(spark.github 等)不能卸载,只能停用。后端硬拦截,报 内置能力包不能移除,只能停用; 界面上纯内置插件根本不显示卸载按钮,所以这条错误在正常操作下碰不到。

卸载实际做了四件事(plugin-manager.service.ts:360-378):

  1. 逐个删除该插件所有账号在 keystore 里的 secret。
  2. 删除它贡献的 Skill 行与 MCP 行。
  3. 硬删除 plugins 行;plugin_permissions、plugin_resources、connector_accounts 靠 ON DELETE CASCADE 连带清掉。
  4. 递归删除磁盘上的安装目录。

注意第 2 步只覆盖 skill 与 mcp-server 两类资源;connector 与 runtime 资源没有额外的清理动作(它们的凭据在第 1 步已经被删了)。

另外安装时有个不对称行为:清单未声明的权限会被静默丢弃, 因为写权限行时只遍历清单声明过的 required ∪ optional (:513-524)。传了多余的 approved 权限不会报错,但也不会写进库。

9. 运行时的工具如何进入 Agent

连接器工具不是直接塞进系统提示词的,而是通过一个按会话/按轮次建立的本地 MCP 桥接:PluginRuntimeMcpBridge 起一个StreamableHTTPServerTransport,监听127.0.0.1:<随机端口> 的 /mcp 路径, MCP server 名是 spark_plugins、版本 2.0.0。

命名规则要记牢,它是你排查「工具没出现」的第一把钥匙:

qualifiedName = <runtime.toolNamespace>_<tool.name>
              ↓
注入到 Agent 工具面的名字 = mcp__spark_plugins__<qualifiedName>

例:github runtime 的 read_file
  → qualifiedName  github_read_file
  → Agent 看到的   mcp__spark_plugins__github_read_file

对比:平台管理 MCP 的 GitHub 工具是另一套命名
  → mcp__spark_platform__github_read_repository_file

工具要出现在 Agent 面前,必须同时满足:

  1. 该运行时对应的插件已启用且 required 权限齐备。
  2. 该运行时至少有一个已连接账号;没有账号的运行时整条跳过。
  3. 工具的每个 requiredCapability 都被某个已连接账号满足。
  4. 桥接能收集到至少一个工具定义,否则直接不启动 server(会话侧拿不到)。

桥接还有几条运行时约束,出问题时对照着看:

约束表现
轮次结束调用返回 Plugin MCP turn is no longer active
同一租约重复占用直接抛错;工具指纹相同则复用会话,不同则关掉旧会话
结果过大超过 2 MiB 截断并追加 [truncated by Spark runtime]
参数不是对象Runtime tool arguments must be an object
HTTP 鉴权需要 Authorization: Bearer <会话令牌>,未知令牌返回 401
高风险工具输入 schema 会额外注入 accountId 与 confirmationToken 两个字段,调用时被剥离后转发给 broker

最后一条值得展开:read 与 low-write 风险的工具会被放进 「引擎预批准」列表,模型可直接调用;而 high-write 与 destructive必须带一个有效的 confirmation token,否则报CONFIRMATION_REQUIRED(runtime-policy.ts:81-93)。 连接器工具与自定义工具在这点上策略不同:连接器条目一律进预批准列表, 自定义工具/工具包则只有 risk === 'read' 才进。

工具调用会写审计:表 plugin_runtime_audit,字段含plugin_id / runtime_id / account_id /tool_name / risk / effect /outcome(success|error|denied) / duration_ms /error_code。但要知道两个当前事实:resource_ids_json 实际恒为 [](写入方从不传这个值), 并且这张表只有写入没有查询接口——界面上没有审计查看页。

10. GitHub 的两套实现

GitHub 是唯一一个在代码里被实现了两遍的 provider, 而且两遍的工具名、能力名、存储表、鉴权方式都不一样。写文档或排查问题时 必须先确定你说的是哪一套,否则会出现「工具名对得上、表对不上」的错误。

维度插件运行时那套平台管理 MCP 那套
实现文件services/plugin-runtime/adapters/github-runtime.adapter.tsservices/github-connector.service.ts
Agent 侧工具名mcp__spark_plugins__github_*mcp__spark_platform__github_*
工具名样式get_status、read_file、upsert_filegithub_status、github_read_repository_file、github_upsert_repository_file
工具数1515
存储表connector_accounts(多账号,键含 plugin_id/runtime_id)connector_connections(单连接,provider 上有唯一索引)
连接 id按账号生成固定 github-primary
能力 ididentity/repositories/contents/issues/pull_requestsidentity/repositories/issues/pull_requests/mcp_tools
鉴权声明 pat/device-code/github-app,实际只有 pat只有 pat
写入门控allowWrites + 风险分级(high-write 需确认令牌)只有布尔 allowWrites,无风险分级、无确认令牌
界面可达可达(扩展中心 → 连接器 → GitHub)不可达,见 §10.3

换句话说:你在界面上能连的 GitHub,是插件运行时那套, 它给 Agent 的是 mcp__spark_plugins__github_* 工具。 而 内置工具 页里列的「GitHub 15 个工具」 指的是平台管理 MCP 那套 mcp__spark_platform__github_*。 两套都是 15 个,数量相同纯属巧合,别互相印证。

10.1 平台管理 MCP 的 15 个 github_* 工具

工具 schema 定义在 platform-management-mcp-server.mjs:1926-2146, handler 在 platform-bridge.service.ts:2066-2262。 工具列表是静态返回的,不会因为没连接就连工具都看不到—— 未连接时会调用失败并返回错误文本。

工具必填参数可选参数实际打的端点
github_status——不发请求,只读本地状态
github_list_repositories—query白名单非空时逐个 GET repos/{owner}/{repo};为空时 GET user/repos?per_page=100&sort=updated&affiliation=owner,collaborator,organization_member
github_get_repositoryowner、repo—GET repos/{owner}/{repo}
github_read_repository_fileowner、repo、pathrefGET repos/{owner}/{repo}/contents/{path}(path 逐段 URL 编码)
github_create_branchowner、repo、branchsourceBranch、sourceSha先 GET repos/{owner}/{repo} 取默认分支,再 GET .../git/ref/heads/{sourceBranch},最后 POST .../git/refs
github_upsert_repository_fileowner、repo、path、content、messagebranch、shaPUT repos/{owner}/{repo}/contents/{path}(内容按 UTF-8 再 base64)
github_list_issuesowner、repostate、labels、assignee、page、perPageGET repos/{owner}/{repo}/issues,本地再滤掉带 pull_request 的条目
github_get_issueowner、repo、issueNumber—GET repos/{owner}/{repo}/issues/{n}
github_create_issueowner、repo、titlebody、labels、assigneesPOST repos/{owner}/{repo}/issues
github_update_issueowner、repo、issueNumber、patchpatch 内可有 title/body/state/labels/assigneesPATCH repos/{owner}/{repo}/issues/{n},只透传这五个键
github_comment_issueowner、repo、issueNumber、body—POST repos/{owner}/{repo}/issues/{n}/comments
github_list_pull_requestsowner、repostate、head、base、page、perPageGET repos/{owner}/{repo}/pulls
github_get_pull_requestowner、repo、pullNumber—GET repos/{owner}/{repo}/pulls/{n}
github_create_pull_requestowner、repo、title、head、basebody、draftPOST repos/{owner}/{repo}/pulls
github_comment_pull_requestowner、repo、pullNumber、body—POST repos/{owner}/{repo}/issues/{n}/comments——注意走的是 issue 评论端点,不是 PR review comment

请求头与超时是固定的:

Accept: application/vnd.github+json
Authorization: Bearer <PAT>
X-GitHub-Api-Version: 2022-11-28
Content-Type: application/json          // 仅在有 body 时

超时:15 秒(AbortSignal.timeout)

分页要留意:github_list_issues 与 github_list_pull_requests默认 page=1、perPage=50,靠你显式传参翻页;user/repos 那条路径硬编码 per_page=100且没有 page 参数。整个服务不解析 Link 响应头、 没有自动翻页、没有 ETag 缓存、没有重试退避。

在仓库白名单语义上有一个特别容易写错的点,这里给准确结论:selectedRepos 为空数组 = 不限制(放行账号可见的全部仓库), 不是「全部禁止」。判断条件是if (options?.repo != null && normalizedRepoScope.size > 0)(github-connector.service.ts:719-728)。 匹配是忽略大小写的整串相等,不支持 *、前缀或 org 级通配; 写入时会把 https://github.com/owner/repo、owner/repo.git、 末尾斜杠都归一化成 owner/repo,非法项静默丢弃而不报错。 界面上的输入提示也印证了这点:留空表示允许访问 PAT 授权范围内的全部仓库。

还有两个「看起来有、其实没有」的字段,值得单独提醒:

  • syncIssues 与 syncPullRequests 是空开关。它们默认 true,但全仓库除了默认值构造和 connect() 里写死true 之外没有任何读取点——没有同步任务、没有同步游标、 没有把 GitHub Issue/PR 落到本地表。相关的基础设施 (connector_sync_cursors 表、ExternalSource 类型)存在但零引用。
  • last_sync_at 不是同步时间。任何一次成功的 API 请求都会把 status 重置为 connected、 清空 last_error、把 last_sync_at 更新为当前时间 (:824-830)。把它当「最近一次成功请求时间」理解才对。

10.2 三重门控

每个 GitHub 工具调用都要依次穿过三道闸门(github-connector.service.ts:672-737):

  1. 插件运行时闸门。runtimeGuard() 返回 false 就报GitHub 连接器未启用,Agent 暂时不能使用 GitHub 工具。 这个回调注入的是 pluginManager.isRuntimeEnabled('github')(session-mcp-tooling.ts:221-224)—— 也就是说即使你用平台管理那套工具,也必须先在扩展中心启用 GitHub 连接器。
  2. 连接与凭据闸门。要求 connector_connections 里存在provider = 'github' 的行、enabled = 1、且keystore_ref 指向的 PAT 能被读出来。对应报错依次是GitHub 连接尚未建立、GitHub 连接已禁用、GitHub 连接缺少凭证引用、系统凭证库不可用,无法读取 GitHub PAT:…、GitHub PAT 不存在,请重新连接 GitHub。
  3. 能力 / 写入 / 仓库范围闸门。能力不满足报GitHub 连接未启用能力:<capability>;写入未开报GitHub 连接当前未开启写入权限;仓库越权报仓库 <owner/repo> 不在当前 GitHub 连接的授权范围内。

能力与工具的对应关系有个反直觉之处:除 github_status 外的 14 个工具 全部额外要求 mcp_tools。所以关掉 mcp_tools会让所有数据类工具一起失效,哪怕 repositories / issues 还开着。 而 identity 虽然在默认启用集合里,却没有任何工具要求它—— 它只在状态展示里有意义。

工具组要求的能力
仓库类(list_repositories / get_repository / read_repository_file / create_branch / upsert_repository_file)repositories + mcp_tools
Issue 类(5 个)issues + mcp_tools
PR 类(4 个)pull_requests + mcp_tools
github_status不校验任何能力

错误最终暴露给模型时的形状也值得知道:bridge 把错误序列化成{ ok: false, error: message }(HTTP 状态仍是 200), MCP 侧再包装成 isError: true 的文本。SparkError 的 code 不会传出去—— 模型只看到 message,看不到 PERMISSION_DENIED 这类错误码。 排查时以消息文本为准。

HTTP 状态到错误码的映射是这样切的:

HTTP 状态错误码消息
401 / 403PROVIDER_AUTH_FAILEDGitHub 认证失败:…
404PROVIDER_UNAVAILABLEGitHub 资源不存在:…
422PROVIDER_UNAVAILABLEGitHub 请求参数无效:…
429PROVIDER_RATE_LIMITEDGitHub API 请求过于频繁,请稍后重试
其他非 2xxPROVIDER_UNAVAILABLEGitHub API 错误:HTTP <status> - …

两个容易误判的点:403 被归成认证失败(不会区分「403 也可能是限流」), 以及 429 不读 Retry-After、不重试,只有一句固定文案。 任何非 2xx 都会写 last_error;其中 401/403 还会把 status 置成error(:791-797)。

10.3 这套工具的界面入口实际不可达

这是一个必须讲清楚的事实,否则你会一直在界面里找不到配置入口。McpView.tsx:1251 确实定义了一个完整的ConnectorsPanel():包含 PAT 输入框、仓库范围、 「连接启用」开关、「写入能力」开关、能力勾选、保存 / 断开按钮, 调的是 github-connector:get|connect|update|disconnect 四个通道。

但这个组件在全仓库里只出现这一次——它是定义,没有任何地方渲染它。对 apps/desktop/src 做 <ConnectorsPanel 的检索结果是空。 也就是说:

  • github-connector:* 与别名 plugin-runtime:github:*这两组 IPC 通道都已注册(registerGitHubConnectorIpc.ts:59-68), 但唯一的调用点在被抛弃的组件里,所以界面上没有可达路径。
  • 因此 mcp__spark_platform__github_* 这 15 个工具需要的connector_connections 行,无法通过界面创建。
  • 配套的 mv_connectors / mv_connector_* 样式规则仍然留在McpView.less:572 起的一段里,属于同一批未挂载的代码。

所以实际可用的 GitHub 路径只有一条:扩展中心 →「连接器」页签 → GitHub 卡片 →「连接账号」→ 填入 Fine-grained PAT。 连上之后 Agent 拿到的是 mcp__spark_plugins__github_*。 如果你确实需要平台管理 MCP 那 15 个工具,得先把连接行准备好 (例如通过测试环境或直接构造数据),再在扩展中心启用 GitHub 连接器。

顺便记一个陈旧文案:McpView.tsx:1313-1325 里对非 PAT 认证方式会提示「需要主进程 OAuth/Device/GitHub App 接线;已打开配置入口。」, 而渲染端只认 GITHUB_SUPPORTED_AUTH_METHODS = ['pat'](:1121)。 这句提示与 §4.1 的结论一致:另外两种认证方式没有实现。

11. IM 机器人

第三套「连接器」是远程连接:让你从手机上的 IM 继续和本地 Agent 对话。 它不注册任何 Agent 工具,走的是消息驱动,和上面两套没有代码交集, 本文只做概览。

通道连接方式必填凭据
telegram本机 getUpdates 长轮询,不需要公网地址botToken
feishu官方 WebSocket 长连接appId、appSecret
qq官方 WebSocket 网关qqBotAppId、qqBotSecret

三个关键差异,避免和插件型连接器混淆:

  • 凭据存储在 app_settings 里是明文 JSON,不在系统 keystore, 界面用的是普通文本输入框,已保存的值会明文显示,不做掩码。这与 §4.2 的连接器凭据策略完全不同。
  • 配置与配对状态存放在 app_settings 的 remote-connections/data 分类下,不是独立表。
  • 它有自己的本机 HTTP 服务(默认 127.0.0.1:32178)与 6 位配对码 / QR 配对流程。

通道清单、字段默认值、配对流程、内置命令与能力开关的完整说明见远程连接 一文,这里不再重复。

12. 排查表

现象最可能的原因怎么办
连接器页签里没有「连接器市场」区块,也没有任何提示没有一个已启用且配了指纹的市场源,整块被条件渲染隐藏这是默认状态,见 §8.2;用本地目录导入,或先配置市场源
卡片显示 待授权:…required 权限没批重新走一次安装(安装时是全量批准),或停用后重新启用
启用开关打不开 / 报 Plugin requires explicit permission: …required 权限未授予同上;界面没有逐项授权入口(§7.2)
Agent 看不到连接器工具① 插件没启用;② 没有已连接账号;③ 能力被关;④ 桥接没收集到工具按 §9 的四条依次核对,先用卡片上的状态确认账号是不是「Agent 可用」
工具调用报 Plugin MCP turn is no longer active跨轮次复用了上一轮的工具句柄在当轮重新调用;桥接按轮次建立
报 CONFIRMATION_REQUIRED调用了 high-write 或 destructive 工具但没带确认令牌,或令牌被用过用 plugin-runtime:issue-confirmation 取新令牌(默认 TTL 60 秒、一次性)
报 SCOPE_REQUIRED能力的 requiredScopes 不在账号 grantedScopes 内OAuth 授权时少勾了 scope,只能重新授权账号
报 RESOURCE_OUT_OF_SCOPE目标不在账号的 resourceScope 里(如越权仓库、越权日历、Vault 外路径)改账号的范围配置,或选范围内的目标
报 AUTH_EXPIRED / 状态显示「需要重新授权」refresh token 缺失或过期重新连接账号;刷新要求 config 里同时有 oauthClientId 与 oauthTokenUrl
能力开关点了没反应 / 改了不是想要的账号开关只作用于 accounts[0](§5)多账号场景下先确认第一个账号是你要改的
卸载按钮不见了该组全是内置插件内置包只能停用,不能卸载(§8.3)
卸载时报 内置能力包不能移除,只能停用同上改用卡片开关停用
导入时 Plugin package is missing plugin.json选错的目录层级(选到了包的父目录)选到含 plugin.json 的那一层
Plugin source must be outside the managed plugin directory源目录在受管插件目录里把包挪到受管目录之外再导入(受管目录见 §8.1)
界面里找不到 GitHub PAT 配置面板ConnectorsPanel 没有被挂载(§10.3)用扩展中心 →「连接器」→ GitHub →「连接账号」;那是另一套实现
平台管理那 15 个 github_* 工具全部报「连接尚未建立」connector_connections 里没有行,而界面无法创建预期行为,见 §10.3;改用插件运行时的 GitHub 工具
Agent 报「GitHub 连接器未启用,Agent 暂时不能使用 GitHub 工具」插件运行时闸门没过到扩展中心 →「连接器」启用 GitHub,并确认 required 权限齐备
Issue 列表里混进了 PRGitHub 的 issues 端点本身会返回 PR(平台管理那套会本地过滤,适配器那套不会)用 mcp__spark_platform__github_list_issues;或自行按有无 pull_request 字段过滤
PR 评论没有出现在 review 里github_comment_pull_request 打的是 issue 评论端点预期行为,见 §10.1;需要 review comment 得另想办法
改了 syncIssues 没有任何效果该字段是空开关,没有消费方预期行为,见 §10.1

速查表

界面入口
左侧「扩展中心」(视图 id mcp,⌘5/Ctrl+5) →「连接器」页签
内置运行时
github(15 工具) / google(16) / notion(8) / obsidian(8),没有 Slack/Jira/Linear/通用 REST
贡献类型
只有 4 类:skills / mcpServers / connectors / runtimes
插件来源
source: bundled | local | marketplace;trust 里的 blocked 无写入方
权限清单
network、filesystem.read/write、process.spawn、secrets.read、clipboard、browser、mcp.connect、connector.account
权限强制力
只有「required 全 granted」聚合闸门,无逐权限运行时检查
凭据存储
系统 keystore;SQLite 只存 connector_accounts.credential_ref
工具命名
mcp__spark_plugins__<toolNamespace>_<tool.name>
高风险确认令牌
默认 TTL 60 秒(裁剪 1s–5min),一次性,仅存进程内存
token 刷新阈值
expiresAt 距今不足 30 秒触发;并发刷新单飞
OAuth 流程
Authorization Code + PKCE(S256) + 127.0.0.1 随机端口回调,超时 5 分钟
受管插件目录
<userData>/plugins;临时目录 <tmp>/spark-plugin-installs
安装体积/文件上限
单包 250 MiB、20,000 个文件;拒绝符号链接与路径逃逸
市场源默认状态
spark-official 是占位地址,被迁移 071 置为 enabled=0 → 市场区块整块隐藏
平台 GitHub 连接
connector_connections 单行、id 固定 github-primary、provider 上有唯一索引
平台 GitHub 默认值
allowWrites=false、selectedRepos=[](空=不限制)、enabledCapabilities 五项全开
平台 GitHub 请求
api.github.com,API 版本 2022-11-28,超时 15 秒,不解析 Link、无重试
审计表
plugin_runtime_audit;resource_ids_json 恒为 [],且只有写入没有查询接口
IM 机器人存储
app_settings 的 remote-connections/data,凭据为明文 JSON

常见问题

展开 7 条常见问题
  • 为什么我的「连接器」页签里看不到「连接器市场」这一块?

    这是默认状态,不是故障。市场区块被 marketConfigured 条件包住,要求至少有一个「已启用 + 配了签名指纹」的市场源;随包种下的 spark-official 指向占位地址,且被迁移脚本 071 主动置为 enabled = 0。所以出厂状态下整块隐藏、连空态文案都不会出现。要装连接器请用「导入本地连接器」。

  • 我明明装了连接器,为什么 Agent 还是看不到它的工具?

    四个条件缺一不可:① 插件的 required 权限全批且已启用;② 对应运行时至少连了一个账号;③ 工具的 requiredCapabilities 都被该账号满足;④ 桥接能收集到至少一个工具定义。最常见的漏项是第 ② 条——只装了插件但没点「连接账号」,卡片上会显示「等待连接账号」而不是「Agent 可用」。

  • 我在界面上找不到 GitHub PAT 的配置面板,文档里提到的 github_* 工具也没出现。

    因为 GitHub 有两套实现。界面可达的是插件运行时那套(扩展中心 →「连接器」→ GitHub →「连接账号」),给 Agent 的是 mcp__spark_plugins__github_* 工具。另一套平台管理 MCP 的 mcp__spark_platform__github_* 需要一个 connector_connections 行,而它的配置面板 ConnectorsPanel 在代码里定义了却从未被渲染,所以无法通过界面创建连接。

  • 连接器申请的权限(比如 process.spawn)到底会不会被拦?

    不会逐项拦。权限只用来做一件事:判断 permissions.required 是否全部 granted,然后决定插件能不能安装、能不能启用、运行时是否可用(isRuntimeEnabled)。全仓库没有任何「执行前检查 process.spawn 等具体权限」的代码,所以它是给用户看的风险清单 + 一个聚合开关,不是操作系统级沙箱。

  • 连接器的账号凭据存在哪里?数据库里能看到吗?

    凭据只进系统 keystore,不进 SQLite。表 connector_accounts 里只有一列 credential_ref 保存引用,形如 plugin-runtime-spark.github-github-<base64>;断开账号会连带删除 keystore 里的 secret。这一点和远程连接(IM 机器人)相反——后者的 bot token 是明文存在 app_settings 里的。

  • 连接器能升级或自动更新吗?

    没有升级机制。PluginManager 上没有 update/upgrade 方法,也没有定时检查更新的代码;「升级」的唯一方式是用同一个 id 重新安装一次,DB 走 upsert 覆盖(不改 installed_at)。只改 plugin.json 里的版本号而不重新导入,系统不会识别为升级。

  • 内置的 GitHub / Google / Notion / Obsidian 能卸载吗?

    不能,只能停用。后端在 uninstall 里硬拦截 source === bundled,报「内置能力包不能移除,只能停用」;界面也不会给纯内置插件渲染卸载按钮。停用会把它的所有贡献资源 enabled 置 0,配置与凭据都保留。