连接器
Spark Work 连接器完整指南:先分清「连接器」在代码里的三种含义(扩展中心的插件型连接器、平台管理 MCP 的 github_* 工具、远程连接的 IM 机器人),再详述 plugin.json 完整字段与四类 contribution、source/trust 枚举、github / google / notion / obsidian 四个内置运行时的真实工具清单与能力开关、九个权限与「只有聚合闸门、无逐权限强制」的真实强制力、本地目录导入与默认不可用的市场、凭据只进系统 keystore、运行时的工具如何以 mcp__spark_plugins__<runtime>_<tool> 注入 Agent,以及 GitHub 两套实现的差异与 ConnectorsPanel 未被挂载这一界面缺口。附 20 行排查表。
「连接器」是 Spark Work 里最容易被混淆的一个词:它在代码里同时指向三套互不相同的子系统, 而这三套的入口、存储表、鉴权方式和排查手段都不一样。这一页先把三者分开, 然后重点讲扩展中心里的插件型连接器(含 GitHub / Google / Notion / Obsidian 四个内置运行时), 以及平台管理 MCP 那套 15 个github_*工具。 IM 机器人(Telegram / 飞书 / QQ)属于第三套,本文只做概览,细节见远程连接。
1. 先分清「连接器」在 Spark Work 里的三种含义
同一个中文词对应三套代码,混用会导致「工具名对得上但存储表对不上」这类错误, 所以先看一张对照表。
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.ts | apps/desktop/src/main/services/RemoteConnectionService.ts |
| 存储表 | plugins/connector_accounts | connector_connections | app_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 | 工具前缀 | 账号模式 | 工具数 |
|---|---|---|---|---|---|
github | GitHub | spark.github | github_ | multiple | 15 |
google | Google Workspace | spark.google | google_ | multiple | 16 |
notion | Notion | spark.notion | notion_ | multiple | 8 |
obsidian | Obsidian Vault | spark.obsidian | obsidian_ | multiple | 8 |
3.1 GitHub
github-runtime.adapter.ts。能力 id 与界面标签:identity→身份、repositories→仓库、contents→文件、issues→Issue、pull_requests→Pull Request,五个默认全开 (:36-62)。
工具(前缀github_) | 所需能力 | 风险 | 作用 |
|---|---|---|---|
get_status | identity | read | 读取已连接的 GitHub 身份 |
list_repositories | repositories | read | 列出账号可访问的仓库 |
get_repository | repositories | read | 读单个仓库元数据 |
read_file | contents | read | 读仓库文件(base64 自动解码) |
list_issues | issues | read | 列仓库 Issue |
get_issue | issues | read | 读单个 Issue |
list_pull_requests | pull_requests | read | 列 PR |
get_pull_request | pull_requests | read | 读单个 PR |
create_issue | issues | high-write | 创建 Issue |
comment_issue | issues | high-write | 给 Issue 加评论 |
update_issue | issues | high-write | 改 Issue 标题 / 正文 / 状态 |
comment_pull_request | pull_requests | high-write | 给 PR 加评论 |
create_branch | contents | high-write | 从 SHA 或默认分支建分支 |
create_pull_request | pull_requests | high-write | 创建 PR |
upsert_file | contents | high-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'] | 只有pat | token(输入框标签Fine-grained PAT,placeholder github_pat_…) |
google | ['oauth2'] | oauth2 | accessToken、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:
- 生成随机
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)。 - 回调服务器只监听
127.0.0.1:<随机端口>,默认路径/oauth/callback, 超时 5 分钟,回调只能被消费一次;state校验失败报OAuth state validation failed(:43-44, 124-186)。 - 用
grant_type=authorization_code+code_verifier换令牌, 表单编码、30 秒超时(:51-65, 80-91)。 - 浏览器由宿主打开:桌面端调
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且enabled | Agent 可用 |
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 清单字段
| 字段 | 约束 | 必填 | 默认 |
|---|---|---|---|
schemaVersion | 1或2 | 是 | — |
id | ^[a-z0-9][a-z0-9._-]{2,95}$ | 是 | — |
version | 语义化版本,可带-pre/+build | 是 | — |
displayName | 1–160 字符 | 是 | — |
description | 1–8000 字符 | 是 | — |
author | { id?, name, url? },name必填 | 是 | — |
license/homepageUrl/repositoryUrl/ icon | 字符串或 URL | 否 | — |
runtime | { type: 'builtin', id },旧式单运行时绑定 | 否 | — |
categories/tags | 字符串数组,上限 20 / 50 | 否 | [] |
permissions.required/.optional | 权限名数组,各上限 32;required 不得重复 | 否 | [] |
activation | manual/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 | 随包提供的 Skill | 100 | id、path(包内相对路径,目录需含SKILL.md) |
mcpServers | 随包提供的 MCP 服务 | 50 | id、name、config |
connectors | 连接器能力声明 | 50 | id、manifest |
runtimes | 连接器运行时 | 50 | id、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」。它影响三处:
- **安装与启用。**以
enable=true安装、或启用时若 required 没授权齐, 直接抛PluginPermissionError,消息形如Plugin requires explicit permission: <ids>(plugin-manager.service.ts:326-333, 488-492)。 - 资源开关。所有贡献资源的
enabled都会乘上这个判定; MCP 与 connector contribution 还额外要求它自己声明的permissions数组全部 granted(:596-597, 628-629)。 - 运行时可用性。
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 本地目录导入
- 点页头(或空态里)的
导入本地连接器。 - 弹出系统目录选择器,标题
选择连接器目录;取消则直接返回。 - 调
plugin:inspect-local做预检,成功后弹出**「安装前检查」**弹窗: 显示插件名与版本、描述、作者、内容(N 个文件 · SHA-256 …)、必需权限标签列表,以及预检告警。 - 点
授权并安装,调plugin:install-local, 载荷里的approvedPermissions是必需权限的全量(没有逐项勾选 UI),enable: true。 - 成功提示
连接器已安装并启用。
预检本身做得比较严(plugin-package.ts:69-120),失败信息也直接可读:
| 检查项 | 限制 / 报错 |
|---|---|
| 必须是目录 | Plugin source must be a directory |
根目录必须有plugin.json | Plugin 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)。 受管目录在桌面端是 <userData>/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):
- 逐个删除该插件所有账号在 keystore 里的 secret。
- 删除它贡献的 Skill 行与 MCP 行。
- 硬删除
plugins行;plugin_permissions、plugin_resources、connector_accounts靠ON DELETE CASCADE连带清掉。 - 递归删除磁盘上的安装目录。
注意第 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 面前,必须同时满足:
- 该运行时对应的插件已启用且 required 权限齐备。
- 该运行时至少有一个已连接账号;没有账号的运行时整条跳过。
- 工具的每个
requiredCapability都被某个已连接账号满足。 - 桥接能收集到至少一个工具定义,否则直接不启动 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.ts | services/github-connector.service.ts |
| Agent 侧工具名 | mcp__spark_plugins__github_* | mcp__spark_platform__github_* |
| 工具名样式 | get_status、read_file、upsert_file | github_status、github_read_repository_file、github_upsert_repository_file |
| 工具数 | 15 | 15 |
| 存储表 | connector_accounts(多账号,键含plugin_id/runtime_id) | connector_connections(单连接,provider 上有唯一索引) |
| 连接 id | 按账号生成 | 固定github-primary |
| 能力 id | identity/repositories/contents/issues/pull_requests | identity/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_repository | owner、repo | — | GET repos/{owner}/{repo} |
github_read_repository_file | owner、repo、path | ref | GET repos/{owner}/{repo}/contents/{path}(path逐段 URL 编码) |
github_create_branch | owner、repo、branch | sourceBranch、sourceSha | 先GET repos/{owner}/{repo}取默认分支,再 GET .../git/ref/heads/{sourceBranch},最后 POST .../git/refs |
github_upsert_repository_file | owner、repo、path、content、message | branch、sha | PUT repos/{owner}/{repo}/contents/{path}(内容按 UTF-8 再 base64) |
github_list_issues | owner、repo | state、labels、assignee、page、perPage | GET repos/{owner}/{repo}/issues,本地再滤掉带 pull_request的条目 |
github_get_issue | owner、repo、issueNumber | — | GET repos/{owner}/{repo}/issues/{n} |
github_create_issue | owner、repo、title | body、labels、assignees | POST repos/{owner}/{repo}/issues |
github_update_issue | owner、repo、issueNumber、patch | patch内可有title/body/state/labels/assignees | PATCH repos/{owner}/{repo}/issues/{n},只透传这五个键 |
github_comment_issue | owner、repo、issueNumber、body | — | POST repos/{owner}/{repo}/issues/{n}/comments |
github_list_pull_requests | owner、repo | state、head、base、page、perPage | GET repos/{owner}/{repo}/pulls |
github_get_pull_request | owner、repo、pullNumber | — | GET repos/{owner}/{repo}/pulls/{n} |
github_create_pull_request | owner、repo、title、head、base | body、draft | POST repos/{owner}/{repo}/pulls |
github_comment_pull_request | owner、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):
- 插件运行时闸门。
runtimeGuard()返回 false 就报GitHub 连接器未启用,Agent 暂时不能使用 GitHub 工具。 这个回调注入的是pluginManager.isRuntimeEnabled('github')(session-mcp-tooling.ts:221-224)—— 也就是说即使你用平台管理那套工具,也必须先在扩展中心启用 GitHub 连接器。 - **连接与凭据闸门。**要求
connector_connections里存在provider = 'github'的行、enabled = 1、且keystore_ref指向的 PAT 能被读出来。对应报错依次是GitHub 连接尚未建立、GitHub 连接已禁用、GitHub 连接缺少凭证引用、系统凭证库不可用,无法读取 GitHub PAT:…、GitHub PAT 不存在,请重新连接 GitHub。 - **能力 / 写入 / 仓库范围闸门。**能力不满足报
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 / 403 | PROVIDER_AUTH_FAILED | GitHub 认证失败:… |
| 404 | PROVIDER_UNAVAILABLE | GitHub 资源不存在:… |
| 422 | PROVIDER_UNAVAILABLE | GitHub 请求参数无效:… |
| 429 | PROVIDER_RATE_LIMITED | GitHub API 请求过于频繁,请稍后重试 |
| 其他非 2xx | PROVIDER_UNAVAILABLE | GitHub 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 列表里混进了 PR | GitHub 的 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 |
上手清单:接入一个内置连接器并让 Agent 用上它的工具
以 GitHub 为例,从扩展中心连上账号、打开能力、验证 Agent 真能调用工具。
- 按下 ⌘5 / Ctrl+5 打开「扩展中心」,切到「连接器」页签。
- 在「已安装连接器」里找到 GitHub 卡片。四个内置包默认就在列表里,来源徽标显示「内置」。
- 确认卡片开关是开启状态;如果之前停用过,先打开开关(required 权限未齐会报 Plugin requires explicit permission)。
- 点卡片上的「连接账号」。如果显示的是「账号设置」,说明已经有账号了,可直接跳到第 7 步。
- 在弹窗里选「手动令牌」,输入框标签是「Fine-grained PAT」。到 GitHub 的 personal-access-tokens 页面建一个只有你要的仓库和最小权限的 PAT,粘进去。
- 点「验证并连接」。成功会提示「GitHub 已连接 <账号名>」,账号状态变成「Agent 可用」。令牌只进系统 keystore,输入框会立即清空。
- 在弹窗底部「默认能力」里按需关掉不用的能力(例如只要读代码就关掉 pull_requests)。注意这个开关只作用于第一个账号。
- 回到主界面对话,让 Agent 做一件需要 GitHub 的事,比如「列出 owner/repo 最近 5 个 open issue」。
- 如果 Agent 没调用工具:先确认卡片状态是「Agent 可用」(只装插件不连账号时是「等待连接账号」);再确认目标能力是开的。
- 如果 Agent 报 CAPABILITY_DISABLED,说明该工具要求的某个能力被关了;报 SCOPE_REQUIRED 则是 OAuth 授权时少勾了 scope,需要重新授权。
- 要停用:关掉卡片开关即可,配置和凭据都保留。要彻底断开:进「账号设置」点「断开」(没有二次确认,点了就生效)。
常见问题
为什么我的「连接器」页签里看不到「连接器市场」这一块?
这是默认状态,不是故障。市场区块被 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,配置与凭据都保留。