# @xmoon76/dsh-profile-settings [English](README.md) | 中文 一个 DSH **profile bundle** 插件:在官方用户设置 seam 之上增加按 profile 隔离的 Settings 覆盖层。`$DSH_HOME/settings.yaml` 继续作为全局基线;每个 profile 可以 通过自己的 `$DSH_HOME/profiles//settings.patch.yml` 覆盖任意 Settings namespace —— object 递归合并,array/scalar 整体替换,`!unset` 显式删除继承值。 ```text Schema Defaults ↓ Composition Base ↓ ~/.dsh/settings.yaml (全局用户层) ↓ ~/.dsh/profiles//settings.patch.yml (profile 用户层) ↓ Effective Settings ``` 已有插件仍然只使用 `ctx.settings`,完全感知不到覆盖层的存在。写操作 (`update` / `replace` / `mutate`)只落在 **profile 覆盖层**;`replace({})` 回退到 global 层而不是 composition 默认。官方 schema 校验、revision 语义、 `expectedRevision` 冲突检测、watcher 与事件全部保持不变。 ## 安装 要求 profile 的 bundles 包含 `@deepseek-ai/dsh-base`(官方 `web` / `headless` 模板都满足)。为每个需要独立覆盖层的 profile 安装: ```sh dsh plugin --profile web add @xmoon76/dsh-profile-settings dsh plugin --profile pi-tui add @xmoon76/dsh-profile-settings ``` `dsh plugin --profile add` 会自动把声明了 `dsh.bundle` 的包 reconcile 进该 profile 的 `dsh.profile.bundles`。下次启动时 bundle patch (`cordis.patch.yml`)会: 1. 禁用 base 的 `settings` 行(`@deepseek-ai/dsh-settings-file`); 2. 插入 `profile-settings` 行成为 `ctx.settings` 的唯一 owner。 一个 composition 只能有一个 `ctx.settings` owner:如果 base 行仍然活跃, 启动会 fail loud(Cordis 重复服务注册错误),绝不静默覆盖。 ## 配置 插入的行不需要配置(下方为默认值);profile 自己的 `cordis.patch.yml` 可以按 行 id 覆盖 `config`: | key | 默认值 | 含义 | | ------------- | ------------------------------------------------ | ----------------------- | | `profile` | 自动检测 | 显式指定 profile 名 | | `globalPath` | `$DSH_HOME/settings.yaml` | 全局 settings 文档 | | `profileFile` | `settings.patch.yml` | profile 目录内的覆盖文件名 | | `dshHome` | `$DSH_HOME` 或 `~/.dsh` | harness home | | `watch` | `true` | 热加载两个文档 | | `debounceMs` | `100` | watcher 写稳定窗口 | | `writable` | `true` | 允许进程内写入覆盖层 | 当前 profile 按以下顺序解析(绝不通过 cwd 猜测):显式 `profile` 配置 → 安装位置(`profiles//node_modules/…`,即 `dsh plugin` 布局)→ launcher `--profile ` / `--profile=` → `DSH_PROFILE` 环境变量。无法确认时 启动 fail loud。 ## 覆盖文件 ```yaml # $DSH_HOME/profiles/web/settings.patch.yml agent-default-model: provider: pi-ai model: gpt-5.6 permission: mode: danger-full-access some-plugin: endpoint: !unset ``` 普通 YAML 值 = override;`!unset` = 显式 mask(删除下层继承值,回退 schema 默认,除非覆盖层自己提供值)。mask 永不进入解析后的 JSON 文档 —— UI 不会把 它当作普通值。文件可以手工编辑,也可以通过 `settings` 命令修改;进程内写入 只落在这个文件,绝不写 `settings.yaml`。 ## `settings` 命令 命令运行时挂载后,`ctx.profileSettings` 与 `settings` 命令族可用: ```text settings layers [ns [path]] 每个叶子值的来源链 settings get 读取生效值 settings set 写入 profile 覆盖层 settings unset 删除覆盖值(重新继承) settings mask 写入 !unset settings unmask 移除 mask settings reset 对覆盖层执行 replace({}) settings promote 把值提升到全局文档 settings demote 把值降级到 profile 覆盖层 settings migrate [--copy] 把全局值迁入 profile(先备份 .bak.<时间戳>;--copy 保留全局) settings diff [ns] 全局与覆盖层的叶子级差异 settings ui [ns] 机器可读 JSON 快照(供 Web 页面预览) settings profile 当前 profile 与文档路径 ``` ## Web UI(Profile Settings 页面) bundle 自带浏览器半端(`client/`):Web 设置面板新增 **Profile Settings** 页面——每个字段带来源徽标(默认 / 组合 / 全局 / 本 profile / 已屏蔽)、生效值, 以及 set / unset / mask / unmask / promote / reset 操作。页面通过 host 半端注册的 `/profile-settings` loopback RPC 通道通信,不依赖会话上下文、不写命令日志。 ## 配置链与设计说明 Provider 保持官方基类的 document 为 **profile raw section**,因此 `update`/`replace`/`mutate`/`revision`/`expectedRevision`/`describe` 的语义 完全不变;global 层被折进每个注册的合成 base (`applyMasks(merge(composition, global), masks)`)。对基类 TS-private 成员 只通过一个窄类型 facade 访问 —— 不 fork 任何 Settings 机制。完整 M0 调研见 `docs/research.md`。 Fail-loud(启动):无法解析 profile、覆盖文件根非 map、namespace section 非 object、数组内出现 `!unset`、不支持的 YAML tag(`!!js/*`、`!!python/*`、 自定义 tag)、覆盖路径逃逸 profile 目录、global 与覆盖文件同路径、重复 `ctx.settings` owner。 Warn + 保留 last good(热加载):任一文档临时无效 YAML、未注册 namespace (原样保留,等待对应插件稍后加载)。 ## 并发边界与限制 | 范围 | 保证 | | --- | --- | | 同进程普通 `set`/`replace`/`mutate` | 官方 Settings 语义(per-namespace 串行写队列、`expectedRevision` 冲突检测) | | 跨进程文件完整性(read-modify-write) | 双文档写锁 + 原子 rename | | 跨进程自定义层操作(`promote`/`demote`/`migrate`/`mask`/`unmask`) | 真事务:固定顺序双锁、锁内 fence 校验、layer fence 随每次写入推进 | | 跨进程对**同一 profile、同一 namespace** 的普通 `update`/`mutate` | 继承官方 seam 限制:官方 revision 是进程内写队列的,跨进程 CAS 尽力而为(文件锁下后写者胜) | ## 开发 ```sh npm run typecheck # tsc(src + tests) npm test # vitest npm run build # tsc 构建 + copy-lib(lib/) npm pack # prepack 构建 + postpack tarball 冒烟(防泄漏检查) ``` 需要 Node ≥ 22.6(type stripping)与 DSH harness(0.1.1-rc.2 系列)提供 peer 依赖 —— 运行时从安装环境解析,包自身不打包 harness 副本,避免 module twin。 ## License MIT