DeepChat 共享 Skill 架构解析:单一规范包、逻辑绑定与运行时授权边界(Shared Skills)
2026/9/17 18:41:54 网站建设 项目流程

DeepChat 共享 Skill 架构解析:单一规范包、逻辑绑定与运行时授权边界(Shared Skills)

【免费下载链接】deepchat🐬DeepChat - A smart assistant that connects powerful AI to your personal world项目地址: https://gitcode.com/GitHub_Trending/dee/deepchat

本文基于 DeepChat 仓库中的共享 Skill(Shared Skills)架构规格(状态:已实现),系统讲解其"全局单一规范包 + Agent 逻辑绑定 + Session 激活"的三层产品模型、Version 3 管理状态结构、目录布局与迁移方案,并结合主进程SkillService、路由契约与安全约束的源码实现,说明这套设计如何在保证多 Agent 复用同一份 Skill 内容的同时,守住 Run 级运行时授权的边界。读完本文,你可以完整理解 DeepChat 中 Skill 的存储归属、目录解析、导入/删除/同步等操作流程,以及从旧版 Agent 私有根目录向全局共享模型迁移的机制。

背景与核心决策

DeepChat 只有一套应用级、可变的 Skill 包集合。Agent 拥有的是逻辑绑定和扩展状态,Session 决定某次 Run(一次运行)激活哪些已绑定的 Skill;物理包位置不属于任何 Agent,包复用也不会创建 Agent 之间的"活链接"。规格文档见 docs/architecture/shared-skills/spec.md。

旧版遗留的.agent-scopes私有根目录在规格中仅作为迁移证据保留:迁移与兼容规则保护用户数据,同时不再在私有 Agent 根下重新引入运行时发现或 CRUD。

核心决策可以概括为一句话:

  • 每个可变的 Skill 包在全局 Skills 根目录下只存一份(canonical package);
  • DeepChat Agent 对 Skill 的"拥有"退化为一条逻辑绑定,不创建 Agent 私有副本,也不生成任何符号链接或软链目录;
  • 只读的内置(Bundled)Skill 与 Plugin 自带的 Skill 保留在各自提供方的根目录,但会出现在同一份全局列表中。

规格中的总体数据流如下(继承自原文档):

从源码结构看,这套决策落在SkillService(src/main/skill/index.ts)上,它维护一份物理目录清单(catalog),并据此派生出每个 Agent 的目录视图。全局 Skill 的默认存储位置由 src/main/skill/settings.ts 决定:读取skillsPath配置,未配置时回退到~/.deepchat/skills,即上文"全局 Skills 根"的默认取值。

目标与非目标

规格明确列出了目标(Goals):

  • 每个全局唯一 Skill 名只保留一份规范的可变包;
  • 任意多个 DeepChat Agent 通过逻辑绑定复用同一包;
  • 所有 Skill 出现在同一个默认列表中,与"当前选中哪个 Agent"无关;
  • Agent 启用操作收进所选 Skill 的预览面板内;
  • 主 Plugins 交互面只保留"从外部 Agent 快照导入"这一条入口;
  • 保留 per-Agent 扩展状态、Session 过滤、运行时授权、Plugin 归属与既有数据;
  • ACP Agent 被明确排除在 DeepChat Skill 绑定与迁移决策之外。

同时,规格用非目标(Non-goals)划清边界,避免功能蔓延:

  • 不建独立的全局集合页面、Tab 或实体;
  • 不做 Agent 分配页、Agent 选择器、已分配/未分配列表态、批量 Apply 步骤;
  • 不用符号链接、junction、别名或生成的 Agent 目录;
  • 不做 DeepChat 内部互相拷贝(所有 DeepChat Agent 本来就读同一份全局集合);
  • 不与外部 Agent 做持续同步或对外发布;
  • 不做自动内容合并或父子 Agent 继承;
  • 初始迁移不删除保留下来的遗留.agent-scopes数据。

产品模型:三个彼此独立的运行时决策

尽管只有前两项出现在 Skills 界面上,规格强调存在三个独立决策

决策所有者含义产品交互
Skill 存在应用一份规范包全局可用列表、预览、导入、编辑、删除
Agent 能用该 SkillDeepChat Agent一条逻辑绑定被启用在 Skill 预览中增删 Agent
Skill 被激活Session下一次 Run 加载已启用 Skill独立的 Session 级控制

配套的不变量(Invariants)是理解授权边界的关键:

  • 一个全局 Skill 可以存在而没有任何启用的 Agent;
  • 把 Agent 从某个 Skill 上移除,绝不删除包内容或扩展状态;
  • 删除或编辑共享内容会影响所有已启用该 Skill 的 Agent;
  • ACP Agent 和已不存在的 Agent 永远不会成为绑定目标;
  • Run 只授权从其 Agent 与激活 Skill 名解析出的具体根目录
  • Run 启动时授权的 Skill 名集合在执行期间保持不变,即使中途 Agent 绑定被修改,后续 Run 才使用更新后的绑定。

所有权与存储

包目录布局

规格给出的物理布局如下(继承自原文档):

<skillsRoot>/<skillName>/ # canonical mutable package <skillsRoot>/.agent-scopes/<agentId>/ # preserved migration evidence only <skillsRoot>/.library-migration-v3/ # compatibility recovery journal

三点值得注意:

  1. .agent-scopes只读保留。它对应旧版"每个 Agent 一个私有 Skill 根"的结构。从源码看,常量定义在 src/main/skill/agentSkillRoots.ts(BUILTIN_SKILL_AGENT_ID = 'deepchat'AGENT_SKILL_SCOPES_DIR = '.agent-scopes')。resolveAgentSkillsRoot对内置deepchat直接返回 Skills 根,其他 Agent 则解析到.agent-scopes/<agentId>,并通过assertPhysicalAgentRootConfinement做物理包含性校验:lstat拒绝符号链接、要求是目录、并用realpath确认解析后仍位于 Skills 根内部,防止路径逃逸。这与规格"遗留根仅作迁移证据、禁止运行时发现"的定位一致。
  2. .library-migration-v3保留历史名称,使中断的开发期迁移可以安全续跑。其日志在包重命名之前以原子替换方式提交,因此一次撕裂写(torn write)不可能成为下一次启动的输入;该目录被排除在发现之外,永远不会出现在 UI 或公开契约中。
  3. Provider 根的归属不变。内置与 Plugin Skill 留在提供方所有根下,提供方控制其生命周期与可变性;全局列表不转移所有权。用户插件元数据在临时不可用期间保留ownerPluginId;插件停用/更新会注销其活跃贡献,但保留 Agent 分配与覆盖;卸载则移除其拥有的管理状态。被保留的插件 Skill 名不能被其他来源覆盖。执行权限侧会校验"当前插件所有者/根"与物化的 source ID 是否一致,从而阻止过时的 Tape 视图执行已停用、已卸载或被替换的插件修订。这部分与 User Plugins 规格 中"Skills 行负责注册、分配保留与过期修订执行拒绝"的职责划分相互印证。

Version 3 管理状态

Version 3 的关键是把全局包元数据Agent 绑定拆分开。规格给出的接口如下:

interface SharedSkillManagementItem { name: string canonicalPath: string source: SkillSource } interface AgentSkillBinding { assigned: boolean extension: SkillExtensionConfig runtimeBindingId?: string } interface SkillManagementState { version: 3 skills: Record<string, SharedSkillManagementItem> agents: Record<string, { bindings: Record<string, AgentSkillBinding> }> sync?: SkillSyncDirectoryConfig migration?: SharedSkillMigrationState }

这些定义与仓库中的实际类型完全对应,见 src/shared/types/skillManagement.ts。几个源码层面的补充细节:

  • SharedSkillManagementItem在实现中额外携带可选ownerPluginId,用于插件归属校验;
  • runtimeBindingId的注释明确它是外部运行时环境值的不透明修订号,本身从不包含那些值——这正是规格中"为携带密钥的环境值做版本化而不落盘到 Tape"的落地方式;
  • SkillSourcetype字段是闭集枚举,取值包括builtincreatedfolder-installzip-installurl-installgit-installadoptedimported(见 SKILL_SOURCE_TYPES),覆盖了规格中"本地目录 / ZIP / URL / Git / 草稿 / 公开 / CLI 入口保留为兼容或运行时能力"的来源分类;
  • 状态持久化位置在 src/main/skill/settings.ts 的skills.managementState设置键下,SkillSettings.getManagementState返回StoredSkillManagementState,即SkillManagementState | LegacySkillManagementStateV2 | LegacySkillManagementState三版并集,为迁移路径提供类型基础。

语义规则(规格 + 源码一致):

  • assigned是内部持久化与运行时术语;渲染层文案把它表述为"某 Agent 对该 Skill 启用";
  • 使用过旧library字段的 Version 3 开发态数据会被一次性兼容解码,并以skills字段写回;
  • 缺失的绑定等价于assigned: false且扩展配置取默认值;移除 Agent 时写入assigned: false保留其 extension;
  • 新的内置/插件贡献继承既有的 provider 默认绑定;新的可变导入不会自动为任何 Agent 启用。

目录与运行时解析

SkillService拥有一份物理 catalog,并从中派生 Agent catalog(规格中的核心公式,继承自原文档):

AgentCatalog(agentId) = AvailableGlobalSkills intersect EnabledBindings(agentId) EffectiveSessionSkills(sessionId) = PersistedSessionSelection intersect AgentCatalog(Session.agentId)

这个派生 Agent catalog 是授权边界,作用于 prompt 组装、Skill 工具、允许的工具、脚本与文件系统根。而skills.listAll只是管理视图,绝不授予运行时访问——这是"管理面"与"运行时面"分离的硬约束。

规格进一步规定:

  • Route 与 Discover 对派生 Agent catalog 应用受限的渐进披露(progressive-disclosure)契约,而不是对全局管理列表应用;
  • 激活时只解析一次规范包字节,把有效内容与执行包记录进 Tape,并把skill_view/skill_run绑定到该请求证据;
  • 每个 Run 保留 Run 启动时解析出的激活名、内容身份与包权威;规范源包不按 Agent 复制,只有有界请求执行包会为经过校验的脚本执行而物化;
  • Transfer、rebind、fork 与 Subagent 创建都会重新计算目标 Agent 的交集;
  • 一份缓存与 watcher 同时覆盖全局元数据与内容;绑定变更只使受影响 Agent 的视图失效;由于 Agent 数量小,反向影响用扫描即可,不引入反向索引;
  • Watcher 的删除事件只是缓存失效,不是权威的用户删除:仅当事件路径与缓存的 manifest 精确匹配且文件确实已消失时才删除缓存目录项,同时保留全局出处、Agent 绑定、扩展状态与运行时绑定身份——这样编辑器原子替换文件后,同一 Skill 恢复时授权不变。持久化 Skill 状态的移除只由显式删除与启动对账负责。

执行权限的校验有专门实现:src/main/skill/skillExecutionAuthority.ts 负责"当前插件所有者/根 vs 物化 source ID"的一致性验证,对应上面插件修订拒绝的语义;其测试为 test/main/skill/skillExecutionAuthority.test.ts。

操作流程

启用与移除 Agent

Skill 预览面板发出"一个显式的 Skill 名 + DeepChat Agent ID + 目标布尔值"。主进程侧的校验与行为:

  • 验证 Agent 存在且是 DeepChat Agent;
  • 启用时拒绝不可用的 Skill 名;
  • 移除时保留扩展配置,并过滤该 Agent 持久化的 Session 选择;
  • Agent 被删除时只移除其绑定;
  • 既有 Agent 生命周期门(lifecycle gate)围栏并发的 Agent 删除。

编辑与删除 Skill

共享内容的操作会展示当前启用的 Agent 名。删除是带确认的六步事务(继承自原文档):

  1. 接收确认面已确认的启用 Agent ID 列表;
  2. 重新解析影响面,拒绝过期的确认(stale confirmation);
  3. 把包移动到可恢复的备份位置;
  4. 移除绑定与受影响的 Session 选择;
  5. 提交状态并发布 catalog 事件;
  6. 移除备份;若提交失败则回滚恢复备份。

provider 所有与内置只读 Skill 不可编辑、不可删除。

从外部 Agent 导入

外部快照导入是全局的:不询问目标 Agent,也不把"启用 Agent"作为副作用;用户在导入后的 Skill 预览中自行启用 Agent。规格给出的时序如下:

冲突处理策略(规格原文语义):

状态/策略行为
ready新增一个全局 Skill
same保留完全相同的既有 Skill,不改动启用 Agent
conflict + skip什么都不改
conflict + rename加入第一个可用且通过校验的全局名(默认策略)
conflict + overwrite在确认当前启用 Agent 影响后替换共享内容
unavailable禁用该项选择并展示来源校验原因

安全要点(规格):渲染层输入从不提供来源路径或已转换的包字节;主进程在执行时重新扫描、重新计算冲突;部分失败按 Skill 粒度返回。

仓库实现与契约印证了这一点:

  • 导入服务实现于 src/main/skill/agentSkillImportService.ts,外部来源扫描/转换的各宿主适配器(Claude Code、Codex、Cursor、Goose、Kilo Code、Kiro、Windsurf 等)位于 src/main/skill/sync/adapters/;
  • 路由契约 src/shared/contracts/routes/skills.routes.ts 中,skills.executeAgentImport的输入是source: { kind: 'external', toolId }加一组items,每项为{ skillName, strategy: 'skip' | 'rename' | 'overwrite', acknowledgedAgentIds? }——注意契约层根本没有"目标 Agent"或"来源路径"字段,与规格"不询问目标 Agent、渲染层不供给来源路径"的规则逐条对应;选择列表有去重校验(Duplicate Skill selection);
  • 导入相关的测试见 test/main/skill/agentSkillImportService.test.ts。

另外,本地目录、ZIP、URL、Git、草稿、公开与 CLI 入口仍作为兼容或运行时能力存在,但不会作为并列的"添加"选项出现在主 Skills 页面上。

同步目录(Sync directory)

同步目录备份仍是应用级的。次级入口Sync directory会用既有的备份面替换默认列表并提供Back to Skills,它不是并列的 Tab。导入/导出只涉及包,绝不涉及 Agent 绑定。在目录选择、导入或导出写入进行期间,本地 Back 操作与路由导航同时被阻塞,保留的界面得以报告最终结果。契约层对应skills.getSyncConfigskills.setSyncDirectoryskills.previewSyncDirectoryExport/Importskills.executeSyncDirectoryExport/Import(见 skills.routes.ts),同步目录配置结构SkillSyncDirectoryConfig固定在layout: 'multi-skill-repo'(见 skillManagement.ts)。

渲染层交互

默认视图

规格给出的默认 Skills 页如下(继承自原文档):

Plugins / Skills Manage all Skills. Open one to preview it and manage enabled Agents. +------------------------------------------------------------------------------+ || Suggest Skill Drafts [off] | || After a task, allow the Agent to suggest temporary reusable Skill drafts. | +------------------------------------------------------------------------------+ [Search Skills] [Sync directory] [Import from external Agent] ---------------------------------------------------------------------------------- ┌────────────────────────────────────┐ ┌────────────────────────────────────┐ │ code-review │ │ browser-control │ │ Review a change before merging... │ │ Control an interactive browser... │ └────────────────────────────────────┘ └────────────────────────────────────┘

排版与交互约束:

  • 任务完成后的 Skill Draft 建议设置位于页面描述正下方、搜索与操作按钮之上;
  • 响应式网格在空间允许时用两列等宽,窗口受限时单列;
  • 每张卡片固定高度,只包含 Skill 名与截断到两行的描述;无图标、无来源标签、无 Agent 状态、无独立 Preview 按钮;
  • 整卡是带键盘焦点的语义按钮,点击打开 Skill 预览;
  • 这是唯一的 Skills 管理面;Settings 窗口没有 Skills 导航项或路由。

Draft 建议开关对应源码中的skillDraftSuggestionsEnabled设置,默认false(src/main/skill/settings.ts),与界面默认[off]一致。

Skill 预览

┌──────────────────────────────────────────────────────────────────────────────┐ │ code-review [Close] │ │ Review a change before merging │ │ │ │ Enabled Agents [+ Add Agent] │ │ [DeepChat ×] [Writer ×] │ │ │ │ /.../skills/code-review/SKILL.md [Edit] [Delete] │ │ ────────────────────────────────────────────────────────────────────────── │ │ # Code review │ │ ... │ └────────────────────────────────────────────────────────────────────────────┘

Add Agent只列出尚未启用的 DeepChat Agent;×把该 Agent 从 Skill 移除;每次变更立即提交,并通过既有原语暴露 pending、error、键盘与可访问标签状态。

异步变更的作用域规则:

  • 预览变更只作用于发起它的那个 Skill;迟到的响应可以刷新全局列表中的卡片,但不能替换更新的预览;
  • 后台 catalog 删除通过"脏草稿守卫"请求预览关闭,与用户直接关闭同一机制;
  • 外部导入的影响面文案会把 Agent ID 解析为当前显示名,仅在找不到对应 DeepChat Agent 时回退显示 ID。

Plugins-hub 的 Skills 路由渲染的是同一份全局界面,不从当前选中 Agent 或 ACP 状态推断目标。渲染入口在 src/renderer/src/pages/plugins/SkillsPluginsPage.vue。

类型化接口(路由契约)

规格列出的核心类型化接口,均能在路由契约文件中找到对应实现(src/shared/contracts/routes/skills.routes.ts):

接口用途契约要点
skills.listAll列出所有全局 Skill 及其启用 Agent ID空输入,返回UnifiedSkillItem[](skillsListAllRoute)
skills.listCatalog只列出某个必填agentId已启用的 Skill输入{ agentId },返回派生 catalog
skills.setDisabled兼容的单绑定变更(渲染客户端使用){ agentId, name, disabled }{ saved: true }
skills.setAssignments兼容的批量绑定变更(单 Agent)输入skillNames数组,上限 512(PUBLIC_SKILL_LIST_MAX_ITEMS
skills.delete影响面确认后删除一个可变全局 Skill输入name+acknowledgedAgentIds,返回含affectedAgentIds的结果
skills.listAgentImportSources/skills.previewAgentImport/skills.executeAgentImport全局外部快照导入,无目标 Agent ID见上文"外部导入"一节

契约层的命名约束值得注意:PublicSkillNameSchema限定 Skill 名为^[a-z0-9][a-z0-9._-]*$(小写字母/数字开头,最多 255 长度),PublicSkillAgentIdSchema限定 Agent ID 为^[A-Za-z0-9][A-Za-z0-9._-]*$且显式禁止..(skills.routes.ts)——这与主进程侧assertSafeSkillAgentId的路径安全校验(agentSkillRoots.ts)构成前后一致的双重防线。

规格其余条款:

  • 不存在 DeepChat 到 DeepChat 的复制路由:逻辑绑定已经共享规范包;
  • catalog 事件只暴露服务自身发出的原因;
  • 持久化设置中的历史活动输入按历史数据处理,导航忽略未再注册的路由名;
  • 独立的 Settings 窗口需要在主面继续 onboarding 时,走类型化窗口路由,主进程发布类型化运行时恢复事件并聚焦主窗口,跨窗口交接不依赖渲染层sessionStorage
  • 内容路由可以把agentId保留为生命周期上下文,但包含性与 provider 归属才授权全局读与写;扩展路由保留agentId,因为扩展状态属于某个绑定。

并发、故障与安全

规格的安全与并发条款(继承自原文档,均为硬性约束):

  • 单一服务变更门(mutation gate)串行化包与绑定的写入;
  • 包写入复用既有机制:staging、manifest 校验、物理包含性、备份、原子重命名;
  • 失败的包操作保持绑定不变;失败的组合操作回滚备份;
  • 受管或导入的包根不能是符号链接——这与 agentSkillRoots.ts 中lstat().isSymbolicLink()即抛错实现对齐;
  • 导入的相对路径经过校验,不能逃逸包根;
  • 整个已配置的 Skills 根受保护,普通 Agent 的文件系统写操作不可触达;
  • 只有当前激活的具体 Skill 根会被加入 Run 的文件系统白名单;
  • 关闭进行中的导入或处于脏编辑状态时,由既有 settings leave 保护机制守卫。

迁移与兼容性

从 Version 1 / 2 到 3 的迁移是确定性的且可重启续跑的(规格 8 步,继承自原文档):

  1. 发现既有的全局与私有 DeepChat Agent 包;
  2. 对相同快照去重;
  3. 给不同内容的同名变体分配通过校验的全局名;
  4. 规范重命名前先 staging 并写迁移日志;
  5. 把禁用状态与扩展数据翻译成绑定;
  6. 重映射 DeepChat Agent 的 Session 选择;
  7. ACP 与孤儿 Session 保持不动;
  8. 只有当包提交成功后才提交 Version 3 状态。

配套兼容细节:

  • 遗留私有根保留为证据,排除在运行时发现之外;
  • 使用过旧library属性的 Version 3 状态按skills读取,保护运行过早期开发构建的用户;
  • 启动时,Version 3 中 ID 不在当前 DeepChat Agent 集合内的绑定会先被移除,再暴露全局 catalog,确保 ACP 与孤儿 ID 不会泄漏进"启用 Agent 影响面"。

源码印证:SharedSkillMigrationState(skillManagement.ts)携带sourceVersion: 1 | 2status: 'planned' | 'committing' | 'completed'agentSkillNames(旧名 → 规范全局名的映射),与步骤 3、8 直接对应;SkillSettings.freezeLegacyMigrationTargets(settings.ts)负责在迁移目标集合上落"冻结"标记(去重、排序、排除内置deepchat),且当状态已是 version 3 或已有targetAgentIds时直接返回,保证幂等。共享 Skill 行为的测试集中在 test/main/skill/skillServiceSharedSkills.test.ts,目录根解析的安全测试在 test/main/skill/agentSkillRoots.test.ts。

验收标准(摘要)

规格给出的 17 条验收标准中,最能体现架构意图的几条是:

  1. 可变包只在全局 Skills 根存在一份;新 DeepChat Agent 不再获得私有 Skill 根或包副本;
  2. 默认 Skills 视图是一个包含所有可用 Skill 的列表,Skill Draft 建议设置位于搜索与操作按钮之上,且不存在任何"已分配/未分配"的可见状态;
  3. ACP Agent 永不出现在目标列表、不参与迁移校验;
  4. 主导入入口只接受外部 Agent,且全局导入不自动启用 Agent;
  5. 同步目录作为次级视图可达,没有顶层 Tab;Plugins-hub 路由渲染同一全局视图;
  6. Agent catalog、Session 激活、prompt 组装、工具、脚本与文件访问一律使用"启用 Agent 交集",而非全局管理列表;Route、Discover、Tape 物化与请求绑定脚本执行保持同一授权边界;
  7. 共享编辑与删除必须重新校验当前启用 Agent 影响面;
  8. Version 1/2 迁移保留包、绑定、扩展、运行时环境修订与有效 Session 选择;Version 3library兼容数据无丢失解码;
  9. 插件与内置的所有权与可变性约束持续生效。

小结

DeepChat 的 Shared Skills 架构可以归纳为三个工程要点:

  • 一份规范包 + 逻辑绑定:用"元数据/绑定分离"的 Version 3 状态(skills+agents)取代 Agent 私有包目录,从存储层根除重复与同步问题,同时保留.agent-scopes作为可追溯的迁移证据;
  • 管理面与运行时面严格分离skills.listAll只是管理视图,真正授权来自AgentCatalog = 全局可用 ∩ 已启用绑定与 Session 选择的二次交集,Run 启动时冻结授权快照;
  • 全链路安全收敛:命名白名单、路径包含性断言、符号链接拒绝、staging + 原子重命名、备份回滚、过期确认拒绝、插件修订校验,覆盖了从渲染层输入到 Tape 执行的每一步。

进一步阅读建议:规格正文 docs/architecture/shared-skills/spec.md、服务实现 src/main/skill/index.ts、类型定义 src/shared/types/skillManagement.ts、路由契约 src/shared/contracts/routes/skills.routes.ts,以及 User Plugins 规格 中 Skills 与插件归属的交叉职责表。

【免费下载链接】deepchat🐬DeepChat - A smart assistant that connects powerful AI to your personal world项目地址: https://gitcode.com/GitHub_Trending/dee/deepchat

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询