Skip to content

通过插件集成内容来源

从 OpenDesk 0.3.7 版本开始支持

内容来源(API 中称为 Registry,注册表)是技能市场与 MCP 管理器中的「市场来源」:每个内容来源对应一个市场 tab,提供搜索与安装入口。通过 registry capability,插件可以向技能市场(ctx.registry.registerSkillRegistry)和 MCP 管理器(ctx.registry.registerMcpRegistry)集成自定义内容来源,与内置内容来源在 UI、agent 工具和安装流程中完全同权。

一个插件可以同时集成技能与 MCP 两类内容来源。OpenDesk 内置的内容来源就是按品牌组织的:matrix 与 modelscope 两个插件各自同时集成 MCP 与技能内容来源,clawhub、skillhub、skill0、skillsmp 各集成一个技能内容来源,都可以作为参考实现(源码位于仓库的 resources/plugins/)。

manifest 与入口对象都需要声明 registry capability:

{
"schemaVersion": 1,
"id": "my-brand",
"displayName": "My Brand",
"entry": "./index.mjs",
"apiVersion": 1,
"capabilities": ["registry"]
}
方法 / 字段类型说明
registry.fetchfunction继承 OpenDesk 代理配置的 fetch;内容来源实现内的所有网络请求都应使用它
registry.registerSkillRegistryfunction集成一个技能内容来源,返回 PluginRegistration
registry.registerMcpRegistryfunction集成一个 MCP 内容来源,返回 PluginRegistration

集成的内容来源 id 跨插件全局唯一(它同时是安装链接 installUrl 的前缀)。集成后,宿主把定义适配为与内置内容来源同构的对象:技能侧进入技能市场的 tab 列表、TUI 与 agent 工具(searchSkillFromRegistry / addSkillFromRegistry 等)的搜索与安装路由;MCP 侧进入 MCP 管理器的 tab 列表与 listMcpRegistries / searchMcpRegistry / installFromRegistry 等工具。在插件中心停用品牌插件后,其内容来源随宿主重载从市场移除(已打开的市场页面在重新打开后刷新)。

字段类型必填说明
idstring是内容来源唯一标识,格式同插件 id;同时是 installUrl 前缀(id:),跨插件全局唯一
labelstring是展示名称(市场 tab 文本),如 Matrix Skill
iconstring否GUI 中的 emoji 图标,缺省为 🧩
iconClassstring否预留的图标样式类名,缺省取 id
sloganstring否热门条目页标语;缺省使用宿主默认文案
guiHiddenboolean否true 时不在 GUI 市场出 tab(agent 工具与 TUI 仍可用),适合需要 API Key 或实验性的来源
filtersRegistryFilter[]否声明后 GUI 在搜索区渲染开关式过滤,选中值经 options.categoryFilter 传回 search
searchfunction是search(options?) => Promise<SearchResult>
resolveInstallfunction否resolveInstall(installUrl, parsed) => Promise&lt;{ downloadUrl: string }&gt;;把本来源的 installUrl 解析为 zip 下载地址,未声明时宿主按内置规则处理

RegistryFilter:

字段类型必填说明
idstring是筛选器唯一标识
labelOn / labelOffstring是开关打开 / 关闭时的标签文本
categoryValuestring是打开时传给 search(options.categoryFilter) 的值
apiTagIdsstring[]否需要服务端按 tag 过滤时使用(如 Matrix)

search 的入参 options:

字段类型说明
querystring关键字,空串表示热门 / 默认列表
pagenumber页码,从 1 开始
limitnumber每页条数
apiKeystring宿主按配置注入的访问凭证(如 SkillsMP 的 API Key)
cursorstring游标分页:上一页返回的 nextCursor
categoryFilterstringfilters 中选中的 categoryValue

search 的返回 SearchResult:

字段类型必填说明
skillsRegistrySkill[]是当前页条目
totalnumber否总数(用于分页展示)
hasMoreboolean否是否还有下一页
nextCursorstring否游标分页时的下一页游标
messagestring否展示在搜索结果上方的提示文案(如错误或统计信息)

RegistrySkill:

字段类型必填说明
idstring是条目唯一标识
namestring是技能名(安装后的默认技能名)
descriptionstring是描述
sourcestring是来源标识,通常与内容来源 id 一致
installUrlstring是安装链接,约定为本来源前缀形式 id:slug(或 GitHub / zip 地址)
canInstallboolean是是否可安装(GUI 据此渲染安装按钮)
authorstring否作者
stars / score / downloadsnumber否展示用统计
categoriesstring[]否分类标签
字段类型必填说明
id / label / icon / iconClass / slogan / guiHidden同上—与技能内容来源的元数据字段一致
searchfunction是search(options?) => Promise&lt;McpSearchResult&gt;,入参与技能侧一致
getDetailfunction是getDetail(id) => Promise&lt;RegistryMcp | null&gt;;安装流程(installFromRegistry)依赖它取回条目配置

McpSearchResult.mcps 中的条目(RegistryMcp):

字段类型必填说明
idstring是条目唯一标识;安装链接为 id: + 该值(含特殊字符时需 encodeURIComponent)
namestring是Server 名
descriptionstring是描述
sourcestring是来源标识
installUrlstring是id: + 条目 id
configurationstring否可安装的 MCP 配置 JSON(mcpServers 形式);安装时宿主解析其中的 command / url / env 等
canInstallboolean是是否可安装(需有 configuration)
displayName / author / repository / logo / deployType / serverType / downloads / stars / views / updateTime / tags / categories—否详情弹窗与卡片的展示字段

内容来源的 installUrl 约定为 id:条目标识:

  • 技能侧约定 id:slug[@version];安装时宿主按前缀找到内容来源,优先调用插件声明的 resolveInstall 取 downloadUrl(zip)后走统一安装流程。未声明 resolveInstall 时按内置规则处理:内置四个技能来源的下载地址内置于安装器,其余回退为按普通 zip URL 尝试。
  • MCP 侧安装时宿主调用 getDetail(条目标识),解析 configuration 中的 mcpServers JSON 落成本地 MCP 配置;条目 id 含特殊字符时建议在 installUrl 中 encodeURIComponent,并在 getDetail 内自行解码。
  • 内置四个技能来源(matrix / clawhub / modelscope / skillhub)的安装路由不依赖对应插件是否启用,历史安装链接始终可重装。

一个品牌插件同时集成技能与 MCP 两类内容来源:

import { definePlugin } from '@bitclub.ai/opendesk-plugin-sdk';
export default definePlugin({
id: 'my-brand',
capabilities: ['registry'],
setup(ctx) {
ctx.registry.registerSkillRegistry({
id: 'my-brand-skills',
label: 'My Brand',
icon: '🧪',
slogan: 'My Brand 技能市场',
filters: [
{ id: 'official', labelOn: '仅官方', labelOff: '全部', categoryValue: 'official' }
],
async search(options = {}) {
const query = options.query || '';
const res = await ctx.registry.fetch(
`https://registry.my-brand.example/skills?q=${encodeURIComponent(query)}`,
{ headers: { Accept: 'application/json' } }
);
const data = await res.json();
return {
skills: data.items.map((item) => ({
id: item.slug,
name: item.name,
description: item.summary,
source: 'my-brand-skills',
installUrl: `my-brand-skills:${item.slug}`,
canInstall: true
})),
total: data.total,
hasMore: false
};
},
async resolveInstall(installUrl, parsed) {
return { downloadUrl: `https://dl.my-brand.example/${parsed.slug}.zip` };
}
});
ctx.registry.registerMcpRegistry({
id: 'my-brand-mcps',
label: 'My Brand MCPs',
async search() {
return { mcps: [], total: 0, hasMore: false };
},
async getDetail(id) {
return {
id,
name: id,
description: 'My Brand MCP server',
source: 'my-brand-mcps',
installUrl: `my-brand-mcps:${id}`,
configuration: JSON.stringify({
mcpServers: { [id]: { command: 'npx', args: ['-y', `@my-brand/${id}`] } }
}),
canInstall: true
};
}
});
}
});
export default {
id: 'my-brand',
capabilities: ['registry'],
setup(ctx) {
ctx.registry.registerSkillRegistry({
id: 'my-brand-skills',
label: 'My Brand',
async search(options = {}) {
return { skills: [], total: 0, hasMore: false };
}
});
}
};

OpenDesk 0.3.7 起内置以下按品牌划分的内容来源插件(源码位于仓库的 resources/plugins/,每个插件内含 opendesk.plugin.json、index.mjs 入口与内容来源实现模块):

插件集成的内容来源说明
matrixMCP matrix + 技能 matrixOpenHarmony Matrix 市场,技能带「鸿蒙PC兼容」筛选器
modelscopeMCP modelscope-mcp + 技能 modelscopeModelScope(魔搭社区)市场
clawhub技能 clawhubClawHub 社区市场
skillhub技能 skillhubSkillHub 市场
skill0技能 skill0(guiHidden)仅 agent 工具与 TUI 可用
skillsmp技能 skillsmp(guiHidden)需要 API Key,宿主经 options.apiKey 注入

在插件中心可以按品牌独立启用 / 停用;停用后对应市场 tab 在页面重新打开时移除,重新启用后恢复。每个内容来源的搜索实现可以直接对照插件源码阅读。

  • search / getDetail 的返回必须是可结构化克隆的纯数据(会跨线程传递给 worker 侧的市场搜索),不要在返回值中携带函数或宿主对象。
  • 内容来源内的网络请求使用 ctx.registry.fetch,以继承宿主代理配置;直接使用全局 fetch 会绕过代理。
  • id 是安装链接前缀,注册时宿主做跨插件全局唯一校验,重复注册会使插件 setup 失败并回滚本次已注册的项。
  • 每次市场读取前宿主都会重新拉取当前生效的内容来源集合:插件重载后定义被替换(不会重复),插件停用 / 卸载后其内容来源从市场移除。
  • guiHidden 只影响 GUI 市场 tab;CLI / TUI 与 agent 工具仍可搜索和安装该来源的内容。