跳转至

04 · 从 CLI 到插件树:profile、bundle、preset 与自己的 Agent

本章源码固定 da00f7f;可安装实验固定 0.2.0-rc.2。应用怎样被组装,决定了后面每个模型、工具、权限和界面功能是否存在。DSH 的扩展入口首先是组合配置,然后才是业务代码。

4.1 两种选择解决两个不同问题

Profile 选择一个进程的应用树。它回答“启动浏览器应用、一次性命令、SDK 服务还是 ACP 服务;装哪些基础服务和第三方包”。Preset 选择这个进程内一批 Agent 的能力组合。它回答“这次会话用哪些工具、persona、指令、压缩和协作插件”。同一 Web profile 可以同时承载 standard 与 minimal 会话,它们共享主机服务,但不会自动获得彼此的 scoped tool。

$DSH_HOME/profiles/<name>/package.json 保存 dsh.profile.bundles 和 profile 自己安装的依赖;cordis.patch.yml 保存用户配置层;cordis.yml 是 launcher 每次写回的空根,不是用户配置入口。根之所以存在,是给 Loader 一个真实 include/baseUrl 以解析相对模块。把最终 dump 写入这个空根会造成下一次 bundle insert 重复。源码:profile-boot.ts · export function prepareProfile、profile.ts · export function initProfile。

shipped profile 初始 bundles 运行目的
web base → web-app 浏览器 Host 与 Client
headless base → headless 单任务,一次性进程
sdk base → sdk-app JSON-RPC SDK 服务
sdk-minimal sdk-minimal 独立明确最小 SDK 树,不叠 base
acp base → acp-app automation-only ACP

这些是源码 PROFILE_TEMPLATES 的事实,不是从名字推出来的默认行为。Desktop 还拥有保留的 desktop profile 和打包运行时,由 Desktop 首次初始化;不能简单当成普通 npm CLI 的第六个模板。源码:profile.ts · export const PROFILE_TEMPLATES。

flowchart TB CLI["dsh --profile 名称"] --> Home["解析 DSH_HOME 与 profile 目录"] Home --> Manifest["package.json 的 dsh.profile.bundles"] Manifest --> Layers["有序 bundle patch 列表"] Layers --> User["profile cordis.patch.yml"] User --> Global["home cordis.patch.yml"] Global --> Args["依次 --patch 与 flag 派生覆盖"] Args --> Resolver["同代模块解析表 / 插件兼容策略"] Resolver --> Loader["空根 + Cordis Loader"] Loader --> Host["进程级服务"] Loader --> Registry["preset registry"] Registry --> ScopeA["preset revision / scoped plugin tree"] ScopeA --> AgentA["Agent A"] ScopeA --> AgentB["Agent B"]

4.2 patch 是替换整份 config,不能当成深合并

组合从空 EntryOptions 列表开始。每个 bundle 的 manifest 声明 dsh.bundle.patch,可以是一个文件或有序文件数组;bundle 本身也是普通 npm distribution。每个 profile bundle 按 manifest 顺序贡献 patch,再上叠 profile 层、home 层、调用层。home 层晚于 profile 层,因此会覆盖 profile 设置;只检查项目文件不足以知道机器实际运行的配置。最后还有 launcher flag 产生的配置,例如 telemetry switch。源码:profile-boot.ts · async function composeProfile、profile.ts · export function bundlePatchFiles。

# 针对 sdk-minimal 的演示覆盖:整份替换该 row 的 config。
- id: system-prompt
  config:
    includeHarnessIdentity: false
    includeRuntimeContext: false
    personaPrefix: >-
      你是我的研究助手。所有结论给出可核查证据。
      不确定的信息标注为待验证,不虚构已经执行的操作。
- id: sandbox-policy
  config:
    mode: read-only
    workspaceRoot: !!js process.cwd()

这份 patch 明确重述了自己需要的字段。若只写 personaPrefix,其他字段不会继承旧 config:它们会回到插件 schema 默认,或因缺必需项失败。覆盖嵌套 preset.config.plugins 更要把完整列表写出来,不能只写新增一行希望旧工具自动留下。DSL 的 insert 添加 rows;id 定位已有 row;disabled 是显式关闭。普通 YAML metadata 不求值,!!js 只用于插件 config 和 disabled 等明确支持的位置;不要把 !js 写成另一种等价语法。Web bundle 自己就在注释中重述整个 config 的原因。源码:cordis.patch.yml · A patch replaces。

dump 是配置观察手段,启动成功是另一个证据。 --dump-config 能让你审查树、id 和生效 config;它不说明模型认证成功、native backend 实际可用,也不说明 external server 已连接。

# 在自己安装的固定版本 dsh 中执行。
dsh --profile sdk-minimal --dump-config
dsh --profile sdk-minimal --patch ./research.patch.yml --dump-config

4.3 boot 的异步生命周期与模块解析

CLI 将 inner args 冻结后提供成 cmdlineArgs service。像 web-startup、headless-startup 的插件读取应用参数,launcher 不替它们理解每个工具的参数。appReady 在 boot、Host setup 成功后才提交;应用 consumer 通过 service 等待,不应以“某个 module 已 import”代替就绪。源码:profile-boot.ts · function createAppReady。

bundle 名称先从 dsh installation 解析,再从 profile 目录解析;profile 的 pnpm-owned dependencies 获得显式优先级。runtime resolution 维护 installation 与 profile 两类包表,遍历 dependency/peerDependency 图,帮助外部插件拿到共享 Cordis 实例。pnpm-workspace.yaml 用 hoisted linker、autoInstallPeers: false,目的之一就是避免插件自己安装另一份服务定义/Cordis。这解释了为什么第三方插件的 peer range 必须与当前 DSH 版本一致。源码:profile.ts · export async function createRuntimeResolution。

两个快照都已记录指向 profile 外部目录的 linkedRoots,保留真实祖先的 Node module lookup;LinkedRoot 与 linked-root 解析不是 main 新增功能。main 新增的是携带来源输入的 ProfileRuntimeResolution 与 computeLatestResolution():重读 profile manifest、已装依赖和 bundle selection,配合 PluginPackages.refresh() 与 HMR 协调新一代解析。rc.2 的 resolution 是冻结数据对象,不能按 main 的新方法调用。源码:profile.ts · export interface LinkedRoot、profile.ts · computeLatestResolution():。

loadProfileDirectory 可以把解析/兼容失败的 bundle 记录到 skippedBundles,launcher 会报告;不能笼统说“一行坏配置必定中止所有应用”或“缺包默默忽略”。最终启用树还接受 startup audit、fail-loud 和 profile compatibility policy。查看报错必须区分 bundle 跳过、row 禁用、row 缺依赖等待、import/activation 失败四种状态。

4.4 preset 的隔离不是新建整个进程

agent-preset-registry 建立常驻的 preset revision tree。选择该 revision 的多个 Agent 继承其 scoped registrations;当定义变化,已有会话持有旧 revision,不能把每次会话创建想象成从 YAML 重装一遍所有包。scope 控制注册可见性,Cordis isolate 控制 service implementation 使用哪个 realm;两者解决不同问题。

mountPreset() 必须已有 scope,它先创建只在内存写入的 PresetTree,对 rows 做 profile 兼容处理,等待并审计,检查有没有服务泄露到 root realm。preset 内新增 planMode、compaction、workflowEngine、terminals 这类 service provider,通常必须包在具有相应 isolate 的 group 中。否则该 service 会影响整个进程,mount 会报告 Preset services require isolate realms。源码:mount.ts · export async function mountPreset、mount.ts · export function leakedServices。

# 一段 preset 内部结构,不是独立启动配置。
- id: compaction
  name: cordis:group
  group: true
  isolate:
    compaction: true
    toolResultPruner: true
  config:
    - id: compaction-basic
      name: '@deepseek-ai/dsh-compaction-basic'
    - id: command-compact
      name: '@deepseek-ai/dsh-command-compact'

auditRows() 分开返回 failed 与 pending。disabled row 有意不激活;缺 Host service 的 row 可以保持 mounted,等 Host settles 后再审计,不必在第一瞬间误判整个 preset 已不可用。插件初始化失败、service leak、未最终满足依赖都不是“工具暂时为空”的同义词。源码:mount.ts · export async function auditRows。

sequenceDiagram participant Caller as 会话创建方 participant R as Preset Registry participant P as retained preset revision participant L as Loader subtree participant A as Agent scope Caller->>R: 选择 preset id R->>P: 查找当前可用 revision P->>L: prepareProfileEntries / mount L->>L: await + row audit + root service leak 检查 L-->>P: mounted scope key R->>A: 将 agent scope parent 设为 revision scope A-->>Caller: 会话获得对应 scoped contributions Note over P,A: 相同 revision 可以被多个 Agent 保留;隔离 service 要用 isolate

4.5 Standard、Minimal、Code/PTC、Creator 到底分别是什么

源码稳定 id 为 standard、minimal、ptc、cordis。Creator 是显示名,创建/恢复会话用 cordis;Code/PTC 能力对应 ptc,不要发送不存在的 creator 或 code id。 当前 UI 字典显示 PTC mode,而部分教程/介绍以 code mode 描述编程调用工具;读实际 display.ts 与 locale,而不是从网页标题猜 API。源码:display.ts · const BUILT_IN_PRESET_KEYS。

preset id 主要组合 适用场景与注意事项
standard persona、工作区指令、bash/pwsh、fs/search、jobs、skills、goal、planning、compaction、delegation、web、interaction/todo、present 完整 coding agent;常规工具调用,可装 workflow/subagent
minimal complete persona + isolated terminal service + persistent bash/pwsh 快速简单执行;没有自动继承 standard 的 skills/compaction/delegation
ptc 大体类似 standard,编程编排能力由 Tools mode/runtime 提供,仍包含自己声明的 scoped consumers 批处理、程序化聚合、减少模型来回;不能只靠这个 id断言运行时 mode 已切换
cordis coding 能力 + tool-cordis + bundled Cordis 作者 skills + plugin-manager tools 作者 Agent:让 Agent理解/改造插件组合;权限、HMR 与包安装会影响主机,需自行限定

standard 与 ptc 的声明文件不是全部 runtime:global base services、profile tools mode 和 scoped限制共同决定最终模型看到什么。Web 有临时 DSH_TOOLS_MODE 环境 seam 写入 tools config;省略时是 native。实际应 dump 并检查 tools.mode,不能把 preset 名称等同于 native|ptc|both。tool-ralph 在 standard/cordis声明中默认 disabled;部分 external subagent tools 也 disabled;“文件里出现”与“默认可用”须分开。main 的 standard/ptc/cordis 增加 time-context/schedule consumers,而 service由Web提供;rc.2 的同文件缺少这些新 rows。源码:cordis.patch.yml · id: tools、standard.patch.yml · id: preset-standard、minimal.patch.yml · id: preset-minimal、cordis.patch.yml · id: tool-cordis。

4.6 以发布版做一份属于自己的完整 profile

最稳妥的起点是拷贝 shipped template 的 bundle 列表,再覆盖少量配置;--from-default-profile 不读取另一已存在 profile 的私有 patch、依赖或会话,更不是持续继承。名字必须是新的、非保留 shipped name,目录存在时拒绝复用。初始化失败会清理本次新建目录。源码:profile-boot.ts · export function initializeProfileFromDefault。

# Node ^22.19 或 >=24,安装完整匹配的 rc.2 CLI。
# 若下载实验目录已安装依赖,优先使用实验的 node_modules/.bin/dsh。
npm install --save-exact @deepseek-ai/[email protected]

# 自己的 Harness home;不要覆盖系统 HOME。
export DSH_HOME="$PWD/.dsh-course-home"
./node_modules/.bin/dsh --profile my-research \
  --from-default-profile sdk-minimal --dump-config

# 之后不再传 --from-default-profile,profile 已存在。
./node_modules/.bin/dsh --profile my-research \
  --patch ./research.patch.yml --dump-config

对科研助手,先保留 minimal SDK 的 LLM/session/kernel,关闭 persistent-bash/persistent-pwsh rows,再装一个只提供白名单资料工具的自有 plugin bundle。只改 persona 无法实现工具权限;只写“只读”提示无法阻止 shell。需要 shell 时,明确选 read-only 或 workspace-write,并验证系统沙箱可用。发布版 sdk-minimal 默认 danger-full-access,这是该 bundle 的显式选择,不是 SandboxPolicy schema 默认。

第三方 bundle 通常包含三个文件:ESM index.js、cordis.patch.yml、声明 dsh.bundle.patch 的 package.json。它通过 dsh plugin --profile my-research add ./your-plugin 安装到 profile;安装依赖与启用 bundle 是不同状态。检查 dsh.profile.bundles,启用只添加你审核过的 bundle 名,再 dump。包安装 CLI 接受 pnpm 参数,manifest必须声明真正使用的 peer ranges,不要 npm link 出第二份 Cordis。课程可运行实验把这套流程做成可重复目录,见 插件实现、构建自己的 Agent 与 离线 SDK 实验。源码:plugin.ts · export async function runPlugin。

4.7 你自己的架构选择

做一个领域 Agent,通常选择 sdk 或受限 sdk-minimal profile 作为应用载体,领域 tool/plugin 作为能力,preset 作为会话角色。多个业务角色共用同一 Host 时,用 preset scope 管控工具与可替换 service;要隔开凭证、进程权限或不同租户,就用独立 DSH_HOME、独立进程/部署,scope 本身不承担操作系统安全隔离。

这个设计与 Pi 最核心的区别是组合所有权:Pi 常从 SDK 建立会话再注册 extension;DSH 要把 loop、model adapter、tools、storage、surface也当作可替换插件,在 profile中先构造完整应用,业务使用 SDK 驱动它。真正可运行的 Agent 是一份能审查、能 dump、依赖闭合、权限明确的组合,加上可测试的业务插件。