dsh-syn-config 0.0.0-stage → 0.7.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Dreamaniax
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,227 @@
1
- # Temporary Holding Version
1
+ # dsh-syn-config
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ 跨电脑同步 DSH 配置(技能 / profile 设置 / AGENTS.md)的 DeepSeek Harness 插件,**macOS 与 Windows 同一份代码**。
4
+
5
+ > 状态:**P6(隐私加固)**。六个工具 + 一个斜杠命令 + 一个状态卡 + 随包技能:
6
+ > - `dsh_sync_status`:仓库分支 / 上游 / 领先落后 / 脏文件数(默认不联网)。
7
+ > - `dsh_sync_doctor`:`doctor.sh` 的 Node 移植,六节体检,退出码 0-2-1 与脚本一致。
8
+ > - `dsh_sync_pull`:`git pull --ff-only` + 镜像 skills / agents-skills / weekly;**默认 dry-run**。
9
+ > - `dsh_sync_secrets`:scan / redact / inject / refresh / fingerprint;**默认 dry-run**,注入后残留占位符直接失败且不写盘。
10
+ >
11
+ > - `dsh_sync_push`:快照 → lint 闸门 → 脱敏 → 明文扫描 → 提交 → 推送;**默认 dry-run**;分叉时停下,不 rebase、不 force;提交信息含 URL/凭据会被拒绝。
12
+ > - `dsh_sync_diag`:报告本次激活真正注册了什么(服务是否可用、每一步的成败与错误原文)——命令或状态卡不见时先看它。
13
+ >
14
+ > 引导(新机第一条命令)仍由 `dsh-config/scripts/apply.sh` 负责——插件装不上时它不在。
15
+ >
16
+ > `/syn-dsh status|doctor|pull|push|secrets`:斜杠命令,宿主半执行、不进模型消息;`/syn-dsh push now` 才真推。
17
+ > 输入框上方的状态圆点:绿=已同步、黄=有漂移(↑领先/↓落后/±脏)、红=不可用;点一下刷新。
18
+ > `syn-dsh` 技能由插件自带(`assets/syn-dsh.md`,rank 600),不再放在配置仓库里。
19
+ >
20
+ > 设计理由与后续阶段见主仓库的调研文档:`../research/syn-dsh-plugin/PLAN.md`。
21
+
22
+ ## 为什么是插件,而不是又一层 shell
23
+
24
+ | | 现状(`~/dsh-config/scripts/*.sh`) | 本插件 |
25
+ |---|---|---|
26
+ | Windows | 需要 Git Bash 兜底(12 个 `.cmd`) | 原生 Node,无 shell 依赖 |
27
+ | 模型拿到的东西 | 中文日志文本,需要自己解析 | 结构化 JSON(分支/领先/落后/脏文件),工具 schema 强制校验 |
28
+ | 跨平台差异 | rsync / sed / chmod / od 各自分支 | 一个 `execFile('git', argv)`,两边一致 |
29
+ | 引导(新机第一条命令) | 脚本仍是唯一入口 | **仍由脚本负责**——插件装不上时它不存在 |
30
+
31
+ 分工:**插件当手**(日常读写本机与仓库)、**脚本当门**(引导、提交闸门、CI 回归)、**技能当脑**(`/syn-dsh` 决定先做什么)。
32
+
33
+ ## 安装
34
+
35
+ ### 开发 / 离线(link 安装,本仓库直接生效)
36
+
37
+ ```bash
38
+ node scripts/install.mjs --dry-run # 先看会改什么
39
+ node scripts/install.mjs # 写 profile manifest + 建 link
40
+ node scripts/verify.mjs # 校验 manifest / link / 模块 / 工具 / 仓库
41
+ ```
42
+
43
+ 它会:备份 `~/.dsh/profiles/desktop/package.json`(`.bak-dsh-syn-config`)→ 写 `link:` 依赖 → 把 `dsh-syn-config` 追加进 `dsh.profile.bundles` → 在 `profile/node_modules/` 建 **symlink(macOS)/ junction(Windows)** → 重新读盘逐项校验。
44
+
45
+ 安装后**完全退出并重开 DeepSeek Harness**(macOS `Cmd+Q` / Windows 托盘 → 退出),新会话里才会有 `dsh_sync_status`。
46
+
47
+ ### 生产(正式分发)
48
+
49
+ ```bash
50
+ dsh plugin --profile desktop add github:Dreamaniax/dsh-syn-config
51
+ ```
52
+
53
+ 注意:`dsh plugin add` 只写 `dependencies` + lock,**不写** `dsh.profile.bundles`;只有 GUI 的「设置 → 插件」安装会写 bundles。所以走 CLI 时要么再手工补一行 bundles,要么用 GUI 安装。
54
+
55
+ ### 卸载
56
+
57
+ ```bash
58
+ node scripts/uninstall.mjs --dry-run
59
+ node scripts/uninstall.mjs
60
+ ```
61
+
62
+ 只删投影(link/junction,绝不穿过 link 删目标)与 manifest 里的两处条目,保留备份;仓库本身不受影响。
63
+
64
+ ## 工具
65
+
66
+ ### `dsh_sync_doctor`
67
+
68
+ `dsh-config/scripts/doctor.sh` 的结构化版本:同样的六节、同样的判定、同样的退出码(0 干净 / 2 有警告 / 1 有阻塞),但返回的是 JSON 分节而不是中文日志。
69
+
70
+ | 参数 | 类型 | 说明 |
71
+ |---|---|---|
72
+ | `repo` | string(可选) | 仓库路径,支持 `~`;默认取配置 |
73
+ | `offline` | boolean(可选) | 跳过 `git fetch` 与远端比对 |
74
+ | `profile` | string(可选) | 要体检的 profile,默认 `desktop` |
75
+
76
+ 每节返回 `{ id, title, status: ok|warn|error, items: [{ level, text }] }`。与脚本的对等性由 `node scripts/parity.mjs` 逐项比对(当前 13/13 一致)。
77
+
78
+ ### `dsh_sync_push`
79
+
80
+ | 参数 | 类型 | 说明 |
81
+ |---|---|---|
82
+ | `repo` | string(可选) | 仓库路径,默认取配置 |
83
+ | `dry_run` | boolean(可选,**默认 true**) | 只算:快照计划 + lint + 脱敏 + 扫描,全都不写 |
84
+ | `message` | string(可选) | 提交信息,默认 `sync: <本地日期时间>` |
85
+ | `include_profile` | boolean(可选,默认 true) | 是否带 profile 设置与插件清单(`--no-profile` 的等价物) |
86
+ | `allow_delete` | boolean(可选,默认 false) | 才允许删除仓库里多出来的条目;默认只报告 |
87
+
88
+ 顺序是契约不是细节:**pack → lint → redact → scan → commit → push**。先脱敏再扫描,扫描不通过就不提交;分叉时保留本地提交并让人手工合,**永不 rebase、永不 force**。
89
+
90
+ ### `dsh_sync_pull`
91
+
92
+ | 参数 | 类型 | 说明 |
93
+ |---|---|---|
94
+ | `repo` | string(可选) | 仓库路径,默认取配置 |
95
+ | `dry_run` | boolean(可选,**默认 true**) | 只 fetch 并报告计划,不动 HEAD、不写本机 |
96
+ | `delete_local` | boolean(可选,默认 false) | 同时删除仓库里已不存在的本机技能(危险,默认关) |
97
+
98
+ `git pull --ff-only`;分叉时返回 `ok:false` 并保留本地提交,不做合并、更不 reset。镜像目标:`skills/`、`agents-skills/`、`weekly/`(weekly 的 `.sh` 自动 `chmod +x`)。
99
+
100
+ ### `dsh_sync_secrets`
101
+
102
+ | `action` | 作用方向 | 说明 |
103
+ |---|---|---|
104
+ | `scan` | 仓库(只读) | 找明文密钥,命中即 `exitCode:1` |
105
+ | `redact` | 仓库副本 | 真实值 → `__KEY__` |
106
+ | `inject` | 已部署副本 | `__KEY__` → 真实值;**残留占位符则不写盘** |
107
+ | `refresh` | 已部署副本 | 轮换后重写 `Authorization: "Bearer …"` |
108
+ | `fingerprint` | 只读 | 只给 sha256 前 8 位与长度 |
109
+
110
+ 替换一律**字面量**(`sed` 元字符 bug 的修复点);写前留 `<file>.bak-<ISO>` 备份;返回值、日志、渲染里都没有明文。
111
+
112
+ ### `dsh_sync_status`
113
+
114
+ | 参数 | 类型 | 说明 |
115
+ |---|---|---|
116
+ | `repo` | string(可选) | 仓库路径,支持 `~/dsh-config`;默认取配置里的 `repoPath` |
117
+ | `fetch` | boolean(可选) | 先 `git fetch --prune`,再算领先/落后;默认取配置(关) |
118
+
119
+ 返回(`output.schema` 强制):
120
+
121
+ ```json
122
+ {
123
+ "ok": true,
124
+ "repoPath": "/Users/you/dsh-config",
125
+ "repoPresent": true,
126
+ "branch": "main",
127
+ "upstream": "origin/main",
128
+ "ahead": 2,
129
+ "behind": 0,
130
+ "dirty": 1,
131
+ "dirtyFiles": [" M AGENTS.md"],
132
+ "notes": []
133
+ }
134
+ ```
135
+
136
+ 边界情况全部**以数据返回、不抛异常**(抛异常会让宿主把工具调用判错):仓库不存在 → `repoPresent:false`;不是 git 仓库 → `ok:false` + `notes`;没有上游 → `notes` 说明并停止计算领先/落后;git 缺失(含 Windows 的安装提示)→ `notes` 给出可操作建议。
137
+
138
+ ## 配置
139
+
140
+ 在 profile 的 `cordis.patch.yml` 里那行插件下加 `config`(注释示例见本仓库 `cordis.patch.yml`):
141
+
142
+ ```yaml
143
+ - insert:
144
+ - id: dsh-syn-config
145
+ name: 'dsh-syn-config'
146
+ config:
147
+ repoPath: '~/dsh-config'
148
+ gitTimeoutMs: 15000
149
+ fetchOnStatus: false
150
+ ```
151
+
152
+ P0 没有导出 Config schema(避免依赖 schemastery,见下),所以 `config` 未做 schema 校验——`apply()` 里逐字段自检并回退默认值。
153
+
154
+ ## 为什么宿主半不 import 任何 `@deepseek-ai/*`
155
+
156
+ `link:` 安装时插件真实文件在 checkout 里,Node 按真实路径解析裸导入;而 profile 的 `node_modules` 并不含 `@deepseek-ai/dsh-tools`(它们在应用打包内),于是 `import '@deepseek-ai/dsh-tools'` 会在 link 安装下解析失败。因此宿主半只用 `ctx` 注入的服务 + Node 内置模块:
157
+
158
+ - 工具定义直接用**编译后的 JSON Schema**(`ctx.tools.register` 校验的就是这个形状:必须有 `output.render`,且属性不能写 `required: true` 那种 authoring 简写,要用 `required: [...]` 数组)。
159
+ - 代价:失去 `defineTool` 的参数预校验 → 我们在 `execute()` 里自己收窄参数并把失败作为数据返回。
160
+
161
+ 这条也顺带让测试能在裸 Node 下跑,不需要任何安装步骤。
162
+
163
+ ## 隐私边界
164
+
165
+ | 面 | 做法 |
166
+ |---|---|
167
+ | 上传范围 | `skills/`、`agents-skills/`、`AGENTS.md`、profile 五文件;`.dsh-market/` **默认不上传**(`include_market` 才带) |
168
+ | 密钥 | 脱敏/注入都是字面量替换;仓库副本与 `AGENTS.md` 一起处理;注入后残留占位符 → 拒绝写盘 |
169
+ | 扫描 | 工作树 + `deep:true` 时最近 200 次提交;规则覆盖 `as_sk_/ghp_/github_pat_/sk-/sk-proj-/AKIA/AIza/xox*/JWT/PEM/Bearer/通用 api_key 赋值` |
170
+ | 历史命中 | 命中只给 commit + 掩码预览(`AKIAIO…MPLE`),不回传原值;提示先轮换再谈改写历史 |
171
+ | 提交信息 | 含 URL 或凭据特征直接拒绝(进历史不可撤回) |
172
+ | 本地 HTTP 路由 | 只返回分支/领先落后/脏计数,**不返回文件名**;`no-store` |
173
+ | 输出路径 | notes/渲染里的绝对路径缩成 `~/…` |
174
+ | 发布 | `node scripts/publish-check.mjs` 扫 tracked 文件里的账号名、私有仓库 URL、launchd 标签、家目录路径,非零退出可做发布闸门 |
175
+
176
+ ## 平台差异(已在代码里处理)
177
+
178
+ | 关注点 | macOS | Windows |
179
+ |---|---|---|
180
+ | profile 投影 | symlink | **junction**(不需要管理员权限 / 开发者模式),失败再退 dir symlink |
181
+ | manifest 原子写 | rename | 同名 rename;遇到杀软/索引器占用(`EACCES`/`EBUSY`/`EPERM`)退避重试 |
182
+ | 路径 | `~/…`、`/` | `~\…` 同样支持;`isAbsolute`/`join` 处理盘符 |
183
+ | git | Apple git | Git for Windows 的 `git.exe`(不在 PATH 时 `notes` 会给安装提示) |
184
+ | 换行 | LF | `.gitattributes` 固定 `*.sh`/`*.js` LF、`*.cmd`/`*.ps1` CRLF |
185
+ | 控制台 | 直接输出 | `execFile(..., { windowsHide: true })`,不弹黑框 |
186
+
187
+ ## 开发
188
+
189
+ ```bash
190
+ node --test "test/*.test.mjs" # 26 个用例:解析/脱敏/扫描/体检分节 + 真 git 仓库 + 插件契约
191
+ node scripts/parity.mjs # 与 scripts/doctor.sh 逐项比对(同状态、同退出码)
192
+ node --test test/mirror.test.mjs # 镜像与 rsync -a --delete 的树对比
193
+ node scripts/install.mjs --dry-run
194
+ node scripts/verify.mjs
195
+ ```
196
+
197
+ `test/status.test.mjs` 在临时目录里 `git init` 真仓库来断言;`test/plugin.test.mjs` 用假 `ctx` 断言注册形态与 `execute` 返回值形状。
198
+
199
+ ## 验收清单
200
+
201
+ | # | 验收项 | 命令 / 判据 |
202
+ |---|---|---|
203
+ | 1 | manifest 依赖 + bundles 条目 | `node scripts/verify.mjs` 前两项 `ok` |
204
+ | 2 | 投影是 link 且指向本 checkout | `verify.mjs` 的 projection 两项 |
205
+ | 3 | 模块可被宿主加载 | `verify.mjs` 的 load/export 四项 |
206
+ | 4 | 只注册一个工具且 schema 合法 | `verify.mjs` 的 tool 三项 |
207
+ | 5 | 能读真实仓库状态 | `verify.mjs` 打印 branch/upstream/ahead/behind/dirty |
208
+ | 6 | 宿主启动时组合进该行 | `dsh --profile desktop --dump-config \| grep dsh-syn-config` |
209
+ | 7 | 新会话工具可见 | 重启客户端后问答一次「dsh 配置同步了吗」 |
210
+ | 8 | 三种仓库状态结果正确 | 干净 / 落后 / 脏(本地真仓库或临时仓库) |
211
+ | 9 | 体检与脚本对等 | `node scripts/parity.mjs` → 13/13,退出码一致 |
212
+ | 10 | 报告不含明文密钥 | `test/doctor.test.mjs` 的 redaction 用例 |
213
+ | 11 | 镜像与 rsync 等价 | `test/mirror.test.mjs`(有 rsync 时真跑对照) |
214
+ | 12 | reasoningEffort 4 条断言 | `test/patch.test.mjs`(移植自 `selftest.sh:26-69`) |
215
+ | 13 | 元字符密钥往返 | `test/secrets.test.mjs` |
216
+ | 14 | 残留占位符不写盘 | `test/p2.test.mjs` |
217
+ | 15 | 快照不动 reasoningEffort | `test/pack.test.mjs` |
218
+ | 16 | 默认不删仓库条目 | `test/pack.test.mjs`(`allowDelete` 才删) |
219
+ | 17 | 分叉不 rebase、不丢提交 | `test/push.test.mjs` |
220
+ | 18 | 明文密钥在提交前中止 | `test/push.test.mjs` |
221
+ | 19 | 斜杠命令五个子命令 | `test/p4.test.mjs`(status/push dry-run/未知子命令) |
222
+ | 20 | 状态路由 GET/405 | `test/p4.test.mjs`(含 `Cache-Control: no-store`) |
223
+ | 21 | 技能随包携带 | `test/p4.test.mjs`(provider list/get,rank 600) |
224
+
225
+ ## 许可
226
+
227
+ MIT。Windows 投影与原子写逻辑改编自同作者的 `dsh-model-effort-chips`(MIT)。
@@ -0,0 +1,223 @@
1
+ ---
2
+ name: syn-dsh
3
+ description: "跨电脑同步 DSH 配置(技能 / 插件 / profile 设置 / AGENTS.md)与私有仓库 ~/dsh-config:拉取、推送、新机全量恢复、体检与失败排查。当用户说「同步 dsh」「syn_dsh」「更新另一台机器的配置」「把配置推到仓库」「换电脑恢复配置」「为什么技能没同步」时使用。"
4
+ ---
5
+
6
+ # Syn DSH(跨电脑同步 DSH 配置)
7
+
8
+ 把「本机 `~/.dsh`」和「私有配置仓库 `~/dsh-config`」双向对齐。所有命令都在仓库目录里跑。
9
+
10
+ ## 优先用插件(已装 `dsh-syn-config` 时)
11
+
12
+ 插件把日常动作做成了工具与命令,脚本退居「引导 + CI + 提交闸门」:
13
+
14
+ | 想做的事 | 会话里 | 命令 | 对应脚本(兜底) |
15
+ |---|---|---|---|
16
+ | 看是否同步 | 问「dsh 同步了吗」/ 调 `dsh_sync_status` | `/syn-dsh status` | `doctor.sh`(只读版) |
17
+ | 完整体检 | 调 `dsh_sync_doctor` | `/syn-dsh doctor` | `doctor.sh` |
18
+ | 拉取 | 调 `dsh_sync_pull`(默认 dry-run) | `/syn-dsh pull now` | `update.sh` |
19
+ | 推送 | 调 `dsh_sync_push`(默认 dry-run) | `/syn-dsh push now` | `pack.sh` + `sync.sh` |
20
+ | 密钥 | 调 `dsh_sync_secrets` | `/syn-dsh secrets scan` | `secrets.sh` |
21
+ | 新机引导 / CI | —— 插件管不到 | —— | `apply.sh` / `selftest.sh` |
22
+
23
+ 插件铁律:写操作默认 dry-run;推送分叉时**只停不 rebase**;`--delete` 默认降级为「报告 + 显式确认」;`reasoningEffort` 永不同步。
24
+
25
+ 仓库位置:`~/dsh-config`(本机惯例;插件默认也是这个路径,可在配置里改)
26
+ 远端:`https://github.com/<owner>/dsh-config`(私有)
27
+ profile:`desktop`;技能目录:`~/.dsh/skills`;跨 agent 技能:`~/.agents/skills`
28
+
29
+ ## 平台(先判断,再决定用哪条命令)
30
+
31
+ | 系统 | 日常命令 | 说明 |
32
+ |---|---|---|
33
+ | macOS / Linux | `./scripts/update.sh` | bash + rsync(有就用 rsync) |
34
+ | Windows(推荐) | `scripts\win\update.cmd` | 自动定位 Git for Windows 的 bash;不需要 WSL |
35
+ | Windows(Git Bash 里) | `./scripts/update.sh` | 与 macOS 完全一致 |
36
+
37
+ Windows 首次:`git clone … %USERPROFILE%\dsh-config` → `scripts\win\apply.cmd --yes` → 用 PowerShell 写 `secrets.env` → **再跑一次** `apply --yes` 让密钥注入。
38
+
39
+ ## 何时用
40
+
41
+ - 用户说「同步 dsh / syn_dsh / 把配置同步一下 / 拉最新配置」
42
+ - 换电脑后第一次恢复配置
43
+ - 刚改完技能或插件,要推给另一台
44
+ - 同步出问题(冲突、推送失败、技能没生效)来排查
45
+
46
+ ## 一句铁律
47
+
48
+ 仓库是唯一真相源。本机 `~/.dsh` 与仓库之间只有两个方向:**拉**(update / apply)和**推**(sync)。
49
+ 不确定就先体检(只读),不要靠猜,也不要靠记忆里的参数。所有参数以脚本自带 `--help` 为准。
50
+
51
+ **例外:`reasoningEffort` 永不同步。** 它是本机模型 / 思考强度 chip 的状态,不是跨机配置:
52
+ `pack.sh` 保留仓库里的值(不把本机值推上去),`apply.sh` 保留本机值(不把仓库值盖下来),
53
+ `doctor.sh` 比较时忽略这一行。手工提交、PR、周报也不要把它带进仓库。
54
+
55
+ ## 第一步永远是体检
56
+
57
+ ```bash
58
+ cd ~/dsh-config && ./scripts/doctor.sh # macOS/Linux
59
+ cd %USERPROFILE%\dsh-config && scripts\win\doctor.cmd # Windows
60
+ ```
61
+
62
+ 只读(仅 `git fetch`),报告:git/rsync 可用性、工作区是否脏、落后/领先几个提交、仓库与本机技能数及**差集**、缺 `SKILL.md` 的技能、`secrets.env` 权限与键名(不打印值)、仓库明文密钥扫描、**设置与插件清单是否和仓库一致**(把真实密钥换回占位符后比对,只报差异行数)。
63
+
64
+ 退出码:`0` 干净 · `2` 有警告可继续 · `1` 有阻塞(先修再同步)。
65
+ `--offline` 不联网;`--quiet` 只打结论。
66
+
67
+ ## 决策表
68
+
69
+ | 用户的意思 | 动作 | 命令 |
70
+ |---|---|---|
71
+ | 「同步一下」(默认) | 拉 + 体检 + 本机有漂移就推 | 见下方「默认流程」 |
72
+ | 拉 / 更新 / 另一台改了 | 只拉技能(秒级) | `./scripts/update.sh` |
73
+ | 连插件、设置、AGENTS.md 一起拉 | 拉 + 恢复 profile + 重装插件 | `./scripts/update.sh --all` |
74
+ | 只看会变什么 | 不动文件 | `./scripts/update.sh --dry-run` |
75
+ | 推 / 上传 / 这台改好了 | 快照 + 提交 + 推送 | `./scripts/sync.sh "说明"` |
76
+ | 新电脑 / 第一次 | clone + 全量恢复 + 补密钥 | 见「新电脑第一次」 |
77
+ | 只想看状态 / 排查 | 体检 | `./scripts/doctor.sh` |
78
+ | 换了密钥 / 要轮换 | 记指纹 → 换 key → 再记指纹 → apply → verify | `./scripts/secrets.sh fingerprint`(只出 sha256 前缀,不出明文) |
79
+ | 「到底能不能用 / 环境闭环了吗」 | 端到端验收(含真实 AnySearch 调用与重启检测) | `./scripts/verify.sh`(`--no-net` 跳过联网) |
80
+ | 装/改周报定时任务 | 跨平台调度器 | `./scripts/schedule.sh`(mac launchd / win schtasks / Linux 给 cron 行) |
81
+
82
+ ## 默认流程(用户只说「同步一下」时照这个走)
83
+
84
+ 1. `./scripts/doctor.sh`
85
+ - 退出 `1`:先修阻塞项(没有 remote / 找不到 git / 仓库有明文密钥),**不要继续同步**。
86
+ 2. 拉:`./scripts/update.sh`
87
+ - 失败(网络/鉴权):如实报告「拉取失败」+ 错误摘要并停止。**此时不要推**,否则可能覆盖另一台的提交。
88
+ 3. 判断本机是否有未推送改动 —— **以 `doctor.sh` 为准,不要只看 `git status`**:
89
+ ```bash
90
+ cd ~/dsh-config && ./scripts/doctor.sh --quiet # 看「仅本机有(未入库)」+「工作区 N 处未提交改动」
91
+ ```
92
+ - 两边都空 → 已一致,跳到第 5 步。
93
+ - 有「仅本机有」→ 那是 `~/.dsh/skills` 里的新/改技能,`git status` 看不到(要先 `pack.sh` 才进仓库),必须走第 4 步推送。
94
+ - 有「cordis.patch.yml 与仓库差 N 行」→ 本机改了设置(模型/UI/MCP),也要推;反过来想在本机应用另一台的设置,用 `update.sh --all`。
95
+ - 有「package.json(插件清单)与仓库不同」→ 本机装了/卸载了插件,同上处理。
96
+ - 工作区脏 → 先看摘要:`git -C ~/dsh-config diff --stat | tail -20`。
97
+ 4. 推送 `./scripts/sync.sh "sync: <一句话说明>"`
98
+ - 只有 `skills/` `agents-skills/` 变动 → 可直接推。
99
+ - 动了 `profiles/`、`AGENTS.md`、插件清单 → **必须先给用户看 diff 摘要并确认**,这些会影响所有电脑的模型、插件和指令。
100
+ - 用户没说要推、且改动涉及配置/插件时,问一句再推。
101
+ 5. 验收(必须贴证据,不许说「应该没问题」):
102
+ - `./scripts/doctor.sh --quiet` 没有 ❌ 和 ⚠️(技能数相等、设置与仓库一致)
103
+ - commit 短哈希 + 是否出现「✅ 已推送」
104
+ - 生效提示:技能要**新开会话**才加载;插件/设置改动要重启客户端
105
+
106
+ ## 新电脑第一次(`/syn-dsh` 还不存在时)
107
+
108
+ 技能本身来自仓库,所以新机上 `/syn-dsh` 还没有——第一次必须手工 bootstrap 一次,跑完技能才会出现:
109
+
110
+ ```bash
111
+ # 0) 装同版本 DSH 桌面端,用同一账号登录一次(生成 ~/.dsh)
112
+ # 1) 私有仓库先授权:gh auth login / PAT / SSH key(首次 clone 会弹凭据管理器)
113
+ git clone https://github.com/<owner>/dsh-config ~/dsh-config
114
+ cd ~/dsh-config
115
+ ./scripts/doctor.sh # 可选:只读体检
116
+ ./scripts/apply.sh # 先 dry-run 看差异
117
+ # 2) 先写密钥再 apply --yes:缺 secrets.env 时 apply 检测到未注入占位符会回滚(退出码 1)
118
+ printf 'ANYSEARCH_API_KEY=%s\n' '你的key' >> ~/.dsh/secrets.env && chmod 600 ~/.dsh/secrets.env
119
+ ./scripts/apply.sh --yes # 全量:技能 + profile + AGENTS.md + 重装插件
120
+ # 3) 新开会话 → /syn-dsh 与其余技能就位
121
+ ```
122
+
123
+ Windows 用 `scripts\win\doctor.cmd` / `scripts\win\apply.cmd`(参数相同,自动找 Git Bash,不需要 WSL):
124
+
125
+ ```bat
126
+ git clone https://github.com/<owner>/dsh-config %USERPROFILE%\dsh-config
127
+ cd %USERPROFILE%\dsh-config
128
+ scripts\win\apply.cmd :: dry-run
129
+ echo ANYSEARCH_API_KEY=你的key> %USERPROFILE%\.dsh\secrets.env
130
+ scripts\win\apply.cmd --yes
131
+ ```
132
+
133
+ 注意:
134
+
135
+ - 仓库是私有的,没凭据第一步就 clone 不下来。
136
+ - **顺序不能反**:`secrets.env` 必须在 `apply --yes` 之前存在;若先 apply 失败,补好密钥后重跑同一条 `apply --yes` 即可(它自己会 inject,不必单独跑 `secrets.sh inject`)。
137
+ - `apply --yes` 第 5 步会重装插件;仓库里的插件版本与新客户端运行时不一致时这一步会报警,按提示对齐版本,别当成功。
138
+ - 首次 clone 约 141 MB,慢是正常的;还没有 remote 时用 `./scripts/publish.sh`。
139
+
140
+ ## 日常拉取
141
+
142
+ ```bash
143
+ cd ~/dsh-config && ./scripts/update.sh # 只技能,秒级
144
+ cd ~/dsh-config && ./scripts/update.sh --all # 连 profile 配置 / AGENTS.md / 重装插件
145
+ cd ~/dsh-config && ./scripts/update.sh --mirror # 额外删除仓库里没有的技能(危险:先问用户 + 先 --dry-run)
146
+ ```
147
+
148
+ `update.sh` 会 `git pull --ff-only` → 打印本次变动技能 → rsync 到 `~/.dsh/skills` 与 `~/.agents/skills` → 自检技能数。
149
+
150
+ ## 本机改动推回
151
+
152
+ ```bash
153
+ cd ~/dsh-config && ./scripts/sync.sh "sync: 新增 X 技能"
154
+ ```
155
+
156
+ 等于 `pack.sh`(本机 → 仓库快照)→ `secrets.sh redact` + `scan`(发现明文直接中止,不提交)→ commit → push。
157
+ 没有变化时输出「没有变化,无需提交。」,不要当失败。
158
+
159
+ ## 密钥轮换
160
+
161
+ ```bash
162
+ vim ~/.dsh/secrets.env # 改成新值
163
+ cd ~/dsh-config && ./scripts/secrets.sh refresh # 本机配置里的旧值 → 新值
164
+ # 重启 DSH 生效。key 不贴进对话、提交信息、周报、截图
165
+ ```
166
+
167
+ ## 失败处置
168
+
169
+ | 现象 | 原因 | 处理 |
170
+ |---|---|---|
171
+ | `git pull --ff-only` 失败或冲突 | 两台都改了同一文件(常见 `cordis.patch.yml` / `package.json`) | 先保住本机改动:`git -C ~/dsh-config stash push -m wip` → `update.sh` → `stash pop` 手工合。**绝不** `git checkout -- .`、`git reset --hard`(会丢本机技能改动) |
172
+ | 推送失败 | 断网 / 凭据过期 / 非快进 | 回「已落地但未同步」+ 错误摘要,下次会话先补推(`sync.sh` 幂等)。不得声称已同步 |
173
+ | `pack.sh` 报「仍有明文密钥,已中止」 | 配置里写了真实 key | 值挪进 `~/.dsh/secrets.env`,配置改 `__KEY_NAME__`,再 `./scripts/secrets.sh redact` |
174
+ | 另一台说没变化,但确实改了 | 那台没推成功 | 到那台跑 `doctor.sh` 看「领先 N 个提交」;补推成功后本机再 `update.sh` |
175
+ | 技能数不相等 | `--mirror` 未跑 / 缺 `SKILL.md` / rsync 权限 | `doctor.sh` 看差集与「缺 SKILL.md」清单 |
176
+ | 拉完新技能不出现 | 技能在**会话启动**时加载 | 新开一个会话;插件变更需重启客户端 |
177
+ | `/usr/bin/git` 报 exit 69 | Xcode 许可 | 脚本已自动优先 Xcode 自带 git;或 `sudo xcodebuild -license accept` |
178
+ | 脚本报 `xxx: unbound variable` | bash 3.2 把紧跟变量的全角字符吞进变量名 | 输出用 `${VAR}` 而不是 `$VAR`(写脚本时注意;全仓库已扫过一遍) |
179
+ | `$'\r': command not found` | Windows 上 `.sh` 被 autocrlf 转成 CRLF | 仓库有 `.gitattributes` 兜底;若仍出现,`git config core.autocrlf false` 后重 clone |
180
+ | Windows 报 `rsync: command not found` | 老版本脚本或自建 shim | 直接用 `scripts\win\*.cmd`;新版 `scripts/_lib.sh` 无 rsync 时自动用 cp/find |
181
+ | `apply` 报「仍有未解析的密钥占位符」 | `secrets.env` 缺键或没建 | 补键后重跑 `apply --yes`;**已自动回滚**,不会把坏配置激活(DSH 继续用上一份可用配置) |
182
+ | `apply` 报「secrets.env 里没有这些键名」但文件肉眼看正常 | 文件末尾缺换行,`read` 丢掉最后一行(Windows `Set-Content -NoNewline` / `echo\|set /p` 易踩) | 已修(`read ... \|\| [ -n "$k" ]`);写入时用 `printf '...\n'` 或换行结尾 |
183
+ | `verify` 报「配置比 DSH 进程新」但功能已经能用 | MCP 配置热加载;时间戳只说明配置新,不说明没生效 | 第 4 项通过时自动降级为提示;只有主题/模型/插件类改动建议重启 |
184
+
185
+ ## Windows 已知差异(脚本已处理,知道就行)
186
+
187
+ - **没有 rsync**:`scripts/_lib.sh` 用 `cp/find` 实现同样语义(`--delete`/`--dry-run` 都支持),别再手搓 shim。
188
+ - **没有 600 权限**:`secrets.env` 靠 NTFS ACL;doctor 在 Windows 上跳过 600 检查,只确认文件存在。
189
+ - **换行符**:`.gitattributes` 强制 `*.sh`=LF、`*.cmd`=CRLF。
190
+ - **周报流水线**(`~/.dsh/learning/` + launchd)只在 macOS 配置过;Windows 要另建目录 + 任务计划程序,不随仓库同步。
191
+ - **`dsh` 命令可能不在 PATH**:`apply` 会提示,改用桌面端「设置 → 插件」按 `MANIFEST.md` 装。
192
+ - **平台快照**:`MANIFEST.md` 只放跨机一致的内容;机器名/平台写在本地 `MANIFEST.local.md`(已 gitignore),避免来回 push 产生噪声 diff。
193
+
194
+ ## 一条验收原则(真机踩出来的)
195
+
196
+ **强证据定结论,弱证据降提示。** 端到端调用(真实 `tools/call`)能证明"能用"时,进程启动时间、文件 mtime 这类弱证据只能作提示,不得推翻实测:
197
+ - `verify` 第 4 项通过 → 第 5 项(配置是否比进程新)降为 ⚠️,不计缺口;只有端到端失败时它才升 ❌
198
+ - 同一台机器上两个检查给出矛盾信号时,先问"哪个是实测、哪个是推断",不要让用户自己裁决
199
+
200
+ ## 硬规则
201
+
202
+ - **永不**把 `~/.dsh/secrets.env`、`.credentials.yaml`、`~/.dsh/sessions/` 提交进仓库;真实 key 只在 `secrets.env`(`chmod 600`)。
203
+ - **永不** `git push --force`:会覆盖另一台的历史。冲突一律手工合并。
204
+ - `--mirror` 会删本机技能:先问用户,先用 `--dry-run` 看会删什么。
205
+ - `apply.sh --yes` 会覆盖本机 `~/.dsh` 配置(旧文件留 `.bak-<时间戳>`):第一次必须让用户先看 dry-run 输出。
206
+ - 推送前必须能说清「改了哪些技能 / 有没有动配置」;说不清就先 `git diff --stat`。
207
+ - **reasoningEffort 永不同步**:模型/强度是本机状态。不要用 `sync.sh` 推送它,也不要用 `update.sh --all` 覆盖它;脚本已内置双向保留,别绕过脚本手工改仓库里的这一行。
208
+ - 汇报必须带证据:技能数、commit 短哈希、是否已推送。没成功就写「未同步」+ 原因,不许用「应该没问题」代替验收。
209
+
210
+ ## 跨平台回归(改脚本后必跑)
211
+
212
+ ```bash
213
+ cd ~/dsh-config && bash scripts/cross-platform-selftest.sh
214
+ ```
215
+
216
+ 它在临时目录里模拟「两台机器 + 一个本地裸远端」:A(macOS 风格)→ pack/sync → B(HOME 带空格、无 rsync)→ update/apply/doctor → B 反向 push → A 拉取。覆盖绝对 `link:` 归一化、密钥脱敏/注入、`reasoningEffort` 双向保留、CRLF、无 rsync 兜底。GitHub Actions 在 ubuntu / macOS / Windows 三个 runner 上跑同一份;Windows 还会冒烟 `scripts\win\pack.cmd`。
217
+
218
+ ## 汇报格式(中文,≤12 行)
219
+
220
+ 1. **结论**:已一致 / 拉取了 N 个技能 / 推送了 commit `abc1234` / 失败原因
221
+ 2. **变化**:改动的技能名(超过 10 个只列前 10 + 共几个);是否动了配置/插件
222
+ 3. **生效**:新开会话(技能)/ 重启客户端(插件)
223
+ 4. **待办**:缺密钥、冲突未合、推送失败等
@@ -0,0 +1,115 @@
1
+ /**
2
+ * Browser half: one compact chip in the composer dock.
3
+ *
4
+ * It reads the host route registered by `lib/core/http.js` — no websocket, no
5
+ * shared state, no second data channel to keep consistent. The chip is
6
+ * deliberately small: a dot whose colour encodes "clean / drift / error", the
7
+ * branch, and the ahead/behind counts. Clicking it re-fetches.
8
+ *
9
+ * Plain JavaScript, no build step: the module table supplies React.
10
+ */
11
+
12
+ window.__ModuleLoader__.load({
13
+ id: 'dsh-syn-config',
14
+ factory(require) {
15
+ const React = require('react')
16
+ const h = React.createElement
17
+ const STATUS_URL = '/plugins/dsh-syn-config/status'
18
+
19
+ /** Fetch the host status once; abort on unmount. */
20
+ function useStatus() {
21
+ const [state, setState] = React.useState({ phase: 'loading' })
22
+ const [nonce, setNonce] = React.useState(0)
23
+ React.useEffect(() => {
24
+ const controller = new AbortController()
25
+ let alive = true
26
+ fetch(STATUS_URL, { signal: controller.signal, headers: { accept: 'application/json' } })
27
+ .then((response) => (response.ok ? response.json() : Promise.reject(new Error(`HTTP ${response.status}`))))
28
+ .then((body) => {
29
+ if (alive) setState({ phase: 'ready', body })
30
+ })
31
+ .catch((error) => {
32
+ if (alive && error?.name !== 'AbortError') setState({ phase: 'error', message: String(error?.message ?? error) })
33
+ })
34
+ return () => {
35
+ alive = false
36
+ controller.abort()
37
+ }
38
+ }, [nonce])
39
+ return [state, () => setNonce((value) => value + 1)]
40
+ }
41
+
42
+ /** Colour + label for the current state. */
43
+ function describe(state) {
44
+ if (state.phase === 'loading') return { color: '#8a8a8a', label: '检查中…' }
45
+ // The desktop build may serve the UI over IPC without exposing the route;
46
+ // that is not a plugin failure, so do not paint it red.
47
+ if (state.phase === 'error') return { color: '#8a8a8a', label: '同步就位' }
48
+ const body = state.body
49
+ if (body.repoPresent !== true) return { color: '#ef7272', label: '无仓库' }
50
+ if (body.ok !== true) return { color: '#ef7272', label: '读取失败' }
51
+ const drift = (body.ahead ?? 0) + (body.behind ?? 0) + (body.dirty ?? 0)
52
+ if (drift > 0) {
53
+ const parts = []
54
+ if (body.ahead > 0) parts.push(`↑${body.ahead}`)
55
+ if (body.behind > 0) parts.push(`↓${body.behind}`)
56
+ if (body.dirty > 0) parts.push(`±${body.dirty}`)
57
+ return { color: '#e0a33e', label: `${body.branch || 'HEAD'} ${parts.join(' ')}` }
58
+ }
59
+ return { color: '#51b976', label: `${body.branch || 'HEAD'} 已同步` }
60
+ }
61
+
62
+ function StatusChip() {
63
+ const [state, refresh] = useStatus()
64
+ const { color, label } = describe(state)
65
+ const title = state.phase === 'ready'
66
+ ? `${state.body.repoPath ?? ''}\n${label}\n点击刷新`
67
+ : state.phase === 'error'
68
+ ? '状态接口在本组合不可用;用 /syn-dsh status 查看'
69
+ : label
70
+ return h('button', {
71
+ type: 'button',
72
+ onClick: refresh,
73
+ title,
74
+ style: {
75
+ display: 'inline-flex',
76
+ alignItems: 'center',
77
+ gap: '6px',
78
+ padding: '3px 9px',
79
+ font: 'inherit',
80
+ fontSize: '12px',
81
+ lineHeight: '16px',
82
+ color: 'var(--dsw-alias-label-secondary, #b9b9b9)',
83
+ background: 'transparent',
84
+ border: '0.5px solid var(--dsw-alias-border-l2, #494949)',
85
+ borderRadius: '999px',
86
+ cursor: 'pointer',
87
+ },
88
+ },
89
+ h('span', {
90
+ 'aria-hidden': true,
91
+ style: { width: '7px', height: '7px', borderRadius: '50%', background: color, display: 'inline-block' },
92
+ }),
93
+ h('span', null, label),
94
+ )
95
+ }
96
+
97
+ return {
98
+ inject: ['slots'],
99
+ apply(ctx) {
100
+ // A missing slot must cost the chip, not the whole client plugin: the
101
+ // host UI owns the slot table and may rename it between versions.
102
+ try {
103
+ ctx.slots.inject('conversation.composer.dock', () => ctx.slots.register({
104
+ name: 'conversation.composer.dock',
105
+ id: 'dsh-syn-config-status',
106
+ order: 40,
107
+ }, StatusChip))
108
+ } catch (error) {
109
+ // eslint-disable-next-line no-console -- there is no logger in the browser half
110
+ console.warn(`dsh-syn-config: status chip not mounted (${String(error?.message ?? error)})`)
111
+ }
112
+ },
113
+ }
114
+ },
115
+ })
@@ -0,0 +1,17 @@
1
+ # dsh-syn-config — bundle patch for DeepSeek Harness.
2
+ #
3
+ # Applied as a profile bundle layer (the `dsh.bundle.patch` manifest field).
4
+ # The row resolves this package by name through the profile's node_modules;
5
+ # everything the plugin does lives in the host half (lib/index.js).
6
+ #
7
+ # Optional per-profile settings — uncomment and edit to override the defaults
8
+ # (default repo path is `~/dsh-config`, git timeout 15 s, no network on status):
9
+ #
10
+ # config:
11
+ # repoPath: '~/dsh-config'
12
+ # profile: 'desktop'
13
+ # gitTimeoutMs: 15000
14
+ # fetchOnStatus: false
15
+ - insert:
16
+ - id: dsh-syn-config
17
+ name: 'dsh-syn-config'