@pandablessing/dsh-user-sync 1.0.10 → 1.0.12
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/README.en.md +10 -0
- package/README.md +118 -130
- package/lib/client.js +28 -1
- package/lib/index.js +48 -1
- package/package.json +1 -1
package/README.en.md
CHANGED
|
@@ -40,6 +40,16 @@ pnpm test # 144 tests
|
|
|
40
40
|
|
|
41
41
|
Docs: [design](docs/design.md) · [test cases](docs/test-cases.md) · [deploy](docs/deploy.md) · [changelog](CHANGELOG.md).
|
|
42
42
|
|
|
43
|
+
## Sponsor
|
|
44
|
+
|
|
45
|
+
The plugin is free and open source (MulanPSL-2.0). If it saved you time migrating machines, a small tip is welcome —
|
|
46
|
+
it covers the storage quota of the hosted service (Nutstore space for remote packages) and ongoing compatibility work.
|
|
47
|
+
|
|
48
|
+

|
|
49
|
+
|
|
50
|
+
Scan with WeChat Pay. Tips are voluntary and do not unlock any extra feature.
|
|
51
|
+
|
|
52
|
+
|
|
43
53
|
## License
|
|
44
54
|
|
|
45
55
|
[MulanPSL-2.0](LICENSE)
|
package/README.md
CHANGED
|
@@ -1,132 +1,120 @@
|
|
|
1
|
-
# dsh-user-sync
|
|
2
|
-
|
|
3
|
-
DeepSeek Harness(DSH
|
|
4
|
-
|
|
5
|
-
>
|
|
6
|
-
>
|
|
7
|
-
|
|
8
|
-
## 功能
|
|
9
|
-
|
|
10
|
-
- **四类用户级资产**:技能(`~/.agents/skills`)、插件清单(profile 声明)、配置(`cordis.patch.yml`)、用户记忆(`AGENTS.md` + `memories
|
|
11
|
-
- **新覆盖旧**:包级 `createdAt`
|
|
12
|
-
- **上传前查远端**:远端已被别的设备更新时先拦下(`E_REMOTE_NEWER
|
|
13
|
-
- **敏感数据硬拒绝**:文件名黑名单 + 内容模式,命中即拒绝打包并列出文件与行号。
|
|
14
|
-
- **官方账号自动配对**:用 `deepseekAccount`
|
|
15
|
-
-
|
|
16
|
-
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
```
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
```powershell
|
|
77
|
-
pnpm install # 仅开发:安装 typescript 与 @types/node
|
|
78
|
-
pnpm run build # 插件:src/*.ts → lib/*.js
|
|
79
|
-
pnpm run build:server # 服务端:server/src/*.ts → server/dist/*.js
|
|
80
|
-
|
|
81
|
-
node --test test/*.test.js # 154 条:单元 / 组件 / 时序(直接跑源码,macOS 全绿)
|
|
82
|
-
node scripts/audit-cases.mjs # 用例清单与结果记录一致性审计(149/149)
|
|
83
|
-
node scripts/e2e-clone.mjs # 替代换机演练(默认本地模式,临时起服务端,不写真实 WebDAV)
|
|
84
|
-
# node scripts/e2e-clone.mjs --remote https://dsh-user-sync-test.wenzhi.info # 真实服务端(会留测试目录)
|
|
85
|
-
powershell -File scripts/smoke.ps1 # 真实 DSH 路由冒烟
|
|
86
|
-
```
|
|
87
|
-
|
|
88
|
-
## 发布
|
|
89
|
-
|
|
90
|
-
> **发布时机由维护者决定**:`scripts/publish.mjs` 只在手动执行时发布,agent 不自动发布(改版本、构建、dry-run 可以提前做,发布等确认)。
|
|
91
|
-
|
|
92
|
-
token 只放本地文件,绝不进仓库(`.env.local` 已在 `.gitignore`):
|
|
93
|
-
|
|
94
|
-
```bash
|
|
95
|
-
cp .env.local.example .env.local && chmod 600 .env.local # 填 NPM_TOKEN=(发布 token)
|
|
96
|
-
node scripts/publish.mjs --dry-run # 先看打包内容(24 个文件)
|
|
97
|
-
node scripts/publish.mjs --otp=123456 # 账号开 2FA 时带当次验证码
|
|
98
|
-
```
|
|
99
|
-
|
|
100
|
-
- 脚本把 token 写进 `0600` 的临时 npmrc,发布结束立即删除;token 不出现在命令行参数、日志与 git 历史里。
|
|
101
|
-
- CI 用 secret 注入 `NPM_TOKEN` 环境变量即可,优先级高于 `.env.local`。
|
|
102
|
-
- 发布前记得先 `pnpm run build && pnpm run build:server`,产物与源码不一致时 tarball 里会是旧代码。
|
|
103
|
-
|
|
1
|
+
# dsh-user-sync
|
|
2
|
+
|
|
3
|
+
DeepSeek Harness(DSH)的**用户级配置同步插件**:把技能、插件清单、配置、记忆在设备间对齐。
|
|
4
|
+
|
|
5
|
+
> 已发布 npm:`@pandablessing/dsh-user-sync`(版本以 npm `latest` 与 [CHANGELOG](CHANGELOG.md) 为准)。
|
|
6
|
+
> 设计与取舍见 [docs/design.md](docs/design.md),用例见 [docs/test-cases.md](docs/test-cases.md),服务端部署见 [docs/deploy.md](docs/deploy.md)。
|
|
7
|
+
|
|
8
|
+
## 功能
|
|
9
|
+
|
|
10
|
+
- **四类用户级资产**:技能(`~/.agents/skills`)、插件清单(profile 声明)、配置(`cordis.patch.yml`)、用户记忆(`AGENTS.md` + `memories/`);项目记忆默认不选,会话不参与同步。
|
|
11
|
+
- **新覆盖旧**:包级 `createdAt` 比较,不做合并;覆盖前弹窗确认,先快照再写盘。
|
|
12
|
+
- **上传前查远端**:远端已被别的设备更新时先拦下(`E_REMOTE_NEWER`),确认后才用本机覆盖。
|
|
13
|
+
- **敏感数据硬拒绝**:文件名黑名单 + 内容模式,命中即拒绝打包并列出文件与行号。
|
|
14
|
+
- **官方账号自动配对**:用 `deepseekAccount` 派生 `accountKey` 自动换 token,无需运营方登录。
|
|
15
|
+
- **缺插件自动装回**:应用远端包后按 `manifest.dependencies` 自动安装,结果列出已装 / 失败 / 需手动放回(`link:` 依赖)。
|
|
16
|
+
- **自动检查**:启动 + 每 6 小时(可配)只读远端元数据,发现更新只提示、不自动应用。
|
|
17
|
+
- **更新提示**:发现 npm 上有新版本时面板顶部给出「更新插件」按钮(走 DSH 官方安装器)。
|
|
18
|
+
|
|
19
|
+
## 安装
|
|
20
|
+
|
|
21
|
+
### 从 npm
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
cd ~/.dsh/profiles/desktop
|
|
25
|
+
pnpm add @pandablessing/dsh-user-sync
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
profile 的 `package.json` 与 `cordis.patch.yml` 里必须使用 npm 包名 `@pandablessing/dsh-user-sync`;若此前用旧名 `dsh-user-sync` 装过,先删掉旧条目(junction / `dependencies` / bundles)再装,否则会重复注册面板。
|
|
29
|
+
|
|
30
|
+
安装后重启 DSH,在设置页启用「设置同步」。
|
|
31
|
+
|
|
32
|
+
### 开发期(junction 到本仓库)
|
|
33
|
+
|
|
34
|
+
```powershell
|
|
35
|
+
powershell -NoProfile -ExecutionPolicy Bypass -File scripts\install-dev.ps1 # 装进 desktop profile
|
|
36
|
+
scripts\uninstall-dev.cmd # 回滚
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
## 使用
|
|
40
|
+
|
|
41
|
+
1. 设置 → 设置同步 → 首次点「同意并启用上传」;
|
|
42
|
+
2. 勾选同步范围(默认四类用户级);
|
|
43
|
+
3. 「上传用户配置」→ 打包并上传(远端更新时先确认再用本机覆盖);
|
|
44
|
+
4. 「从远端同步」→ 检查远端,覆盖前弹窗确认。
|
|
45
|
+
|
|
46
|
+
服务地址内置(`https://dsh-user-sync-test.wenzhi.info`),传输固定走运营方 API;需要自建服务时在 `state.json` 写 `baseUrl` 覆盖。
|
|
47
|
+
|
|
48
|
+
## 架构
|
|
49
|
+
|
|
50
|
+
```text
|
|
51
|
+
插件(HTTP 客户端) ──HTTPS──▶ 运营方 API ──WebDAV──▶ 坚果云
|
|
52
|
+
│ │
|
|
53
|
+
└─ 本地 state.json └─ 鉴权 / 配额 / 隔离 / 审计
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
- 插件侧只实现 `api` 传输;`folder` 仅用于本机调试。
|
|
57
|
+
- 存储凭据只在服务端,插件包内不含任何存储口令。
|
|
58
|
+
- 身份:官方账号 id → `accountKey`(sha256 前 16 位)→ 服务端分区。
|
|
59
|
+
- 远端只保留 1 份包,上传即覆盖;每次应用在本地生成 `pre-restore` 快照。
|
|
60
|
+
|
|
61
|
+
## 构建与测试
|
|
62
|
+
|
|
63
|
+
源码是 TypeScript,构建产物 `lib/` 与 `server/dist/` 已提交(junction 安装与部署直接用)。
|
|
64
|
+
|
|
65
|
+
```powershell
|
|
66
|
+
pnpm install
|
|
67
|
+
pnpm run build # 插件:src/*.ts → lib/*.js
|
|
68
|
+
pnpm run build:server # 服务端:server/src/*.ts → server/dist/*.js
|
|
69
|
+
pnpm test # 单元 / 组件 / 时序(直接跑源码)
|
|
70
|
+
node scripts/audit-cases.mjs # 用例清单与结果记录一致性审计
|
|
71
|
+
node scripts/e2e-clone.mjs # 替代换机演练(本地模式,不写真实 WebDAV)
|
|
72
|
+
node scripts/verify-npm-install.mjs # npm 安装冒烟(临时 profile,用完即删)
|
|
73
|
+
powershell -File scripts/smoke.ps1 # 真实 DSH 路由冒烟
|
|
74
|
+
```
|
|
75
|
+
|
|
104
76
|
## 开发与真实环境
|
|
105
77
|
|
|
106
|
-
- **日常开发(junction)**:`
|
|
107
|
-
-
|
|
108
|
-
-
|
|
109
|
-
|
|
110
|
-
##
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
-
|
|
126
|
-
-
|
|
127
|
-
- `
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
78
|
+
- **日常开发(junction)**:`install-dev.ps1` 把仓库 link 进 profile,改源码 → `pnpm run build` → 热重载生效。
|
|
79
|
+
- **验证真实安装**:`verify-npm-install.mjs` 在临时 profile 里 `pnpm add` 已发布版本,校验 patch 名、宿主/client 入口与资源文件。
|
|
80
|
+
- 两者互不干扰:npm 安装的是编译产物副本,改源码不会生效,所以开发用 link、验证用临时 profile。
|
|
81
|
+
|
|
82
|
+
## 发布
|
|
83
|
+
|
|
84
|
+
发布时机由维护者决定(`scripts/publish.mjs` 只手动执行,agent 不自动发布)。
|
|
85
|
+
|
|
86
|
+
```bash
|
|
87
|
+
cp .env.local.example .env.local && chmod 600 .env.local # 填 NPM_TOKEN
|
|
88
|
+
node scripts/publish.mjs --dry-run # 先看打包内容
|
|
89
|
+
node scripts/publish.mjs --otp=123456 # 2FA 时带当次验证码
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
token 只写进 `0600` 临时 npmrc、发布后删除,不进命令行与 git;CI 可用 `NPM_TOKEN` 环境变量。发布前先 `pnpm run build && pnpm run build:server`。
|
|
93
|
+
|
|
94
|
+
## 已知限制
|
|
95
|
+
|
|
96
|
+
- 桌面端整机验证以 Windows / macOS 为主,Linux 未验证。
|
|
97
|
+
- 服务端单节点,未做限流与多副本;坚果云单账号有流量配额。
|
|
98
|
+
- `link:` 本地依赖需要手工放回目标机。
|
|
99
|
+
- 远端只保留 1 份包,回滚依赖本地 `pre-restore` 快照。
|
|
100
|
+
|
|
101
|
+
## 赞赏
|
|
102
|
+
|
|
103
|
+
插件免费开源(MulanPSL-2.0)。如果它帮你省过换机时间,欢迎小额赞赏,用于服务端坚果云空间与持续维护。赞赏自愿,不影响功能与权益。
|
|
104
|
+
|
|
105
|
+

|
|
106
|
+
|
|
107
|
+
## 仓库结构
|
|
108
|
+
|
|
109
|
+
```text
|
|
110
|
+
docs/ 设计 / 用例 / 部署 / 测试结果
|
|
111
|
+
lib/ 插件(宿主 + client + 传输 + 同步编排)
|
|
112
|
+
server/ 运营方 API(纯 node:http)
|
|
113
|
+
scripts/ 安装 / 卸载 / 冒烟 / 换机演练 / 发布 / npm 安装冒烟
|
|
114
|
+
test/ 单元、组件、时序测试与 fixtures
|
|
115
|
+
legacy/ 需求稿与原型脚本
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
## License
|
|
119
|
+
|
|
120
|
+
[Mulan PSL v2](LICENSE)
|
package/lib/client.js
CHANGED
|
@@ -215,6 +215,33 @@ if (typeof window !== 'undefined' && window.__ModuleLoader__) {
|
|
|
215
215
|
const [overwriteDialog, setOverwriteDialog] = R.useState(null);
|
|
216
216
|
// 版本检查结果:有新版本时在面板顶部提示(不自动安装)
|
|
217
217
|
const [updateInfo, setUpdateInfo] = R.useState(null);
|
|
218
|
+
// 一键更新:调宿主 /update(走 DSH 官方安装器),成功后提示重启
|
|
219
|
+
const updatePlugin = async () => {
|
|
220
|
+
const target = updateInfo?.latest;
|
|
221
|
+
if (!target)
|
|
222
|
+
return;
|
|
223
|
+
setBusy(true);
|
|
224
|
+
try {
|
|
225
|
+
const result = await fetchJson(`${API}/update`, {
|
|
226
|
+
method: 'POST',
|
|
227
|
+
headers: { 'content-type': 'application/json' },
|
|
228
|
+
body: JSON.stringify({ version: target }),
|
|
229
|
+
});
|
|
230
|
+
if (result.ok) {
|
|
231
|
+
setUpdateInfo(null);
|
|
232
|
+
setNotice({ kind: 'result', data: { detail: `已更新到 v${result.version ?? target},请重启 DSH 生效` } });
|
|
233
|
+
}
|
|
234
|
+
else {
|
|
235
|
+
setNotice({ kind: 'error', data: { detail: `更新失败:${result.error ?? '未知错误'}` } });
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
catch (error) {
|
|
239
|
+
setNotice({ kind: 'error', data: { detail: String(error) } });
|
|
240
|
+
}
|
|
241
|
+
finally {
|
|
242
|
+
setBusy(false);
|
|
243
|
+
}
|
|
244
|
+
};
|
|
218
245
|
// 当前展开文件清单的模块(null 表示都收起)
|
|
219
246
|
const [expandedModule, setExpandedModule] = R.useState(null);
|
|
220
247
|
// 设置改动后自动保存(600ms 防抖),不再提供「保存设置」按钮
|
|
@@ -460,7 +487,7 @@ if (typeof window !== 'undefined' && window.__ModuleLoader__) {
|
|
|
460
487
|
// 这样 /scan 请求失败时面板仍能显示真实数量。
|
|
461
488
|
const counts = status?.probe?.counts ?? scan?.counts ?? {};
|
|
462
489
|
return R.createElement('div', { style: styles.section }, R.createElement('h2', { style: styles.heading }, '设置同步'), updateInfo?.available
|
|
463
|
-
? R.createElement('
|
|
490
|
+
? R.createElement('div', { style: styles.card }, R.createElement('strong', { style: styles.warn }, `插件有新版本 v${updateInfo.latest}(当前 v${updateInfo.current})`), R.createElement('div', { style: styles.actions }, R.createElement('button', { style: styles.secondary, disabled: busy, onClick: updatePlugin }, busy ? '更新中…' : '更新插件')))
|
|
464
491
|
: null, !consentGiven
|
|
465
492
|
? R.createElement('div', { style: styles.card }, R.createElement('strong', null, '启用前请确认'), R.createElement('p', { style: styles.muted }, '上传内容:技能、插件清单、配置、用户记忆。数据经运营方服务中转并存放在运营方账号(非 DSH 官方服务器);远端只保留 1 份,上传即覆盖。'), R.createElement('div', { style: styles.actions },
|
|
466
493
|
// 用普通按钮:主色按钮看起来像「已选中」,同意是主动作但不应暗示状态
|
package/lib/index.js
CHANGED
|
@@ -409,6 +409,46 @@ export function apply(ctx) {
|
|
|
409
409
|
}
|
|
410
410
|
},
|
|
411
411
|
},
|
|
412
|
+
{
|
|
413
|
+
// 一键更新:走 DSH 官方安装器(与插件页安装同一路径),只允许本插件自身
|
|
414
|
+
path: '/api/dsh-user-sync/update',
|
|
415
|
+
methods: ['POST'],
|
|
416
|
+
requestBody: 'buffered',
|
|
417
|
+
fetch: async (request) => {
|
|
418
|
+
const pm = pluginManager;
|
|
419
|
+
if (!pm)
|
|
420
|
+
return json({ ok: false, error: 'E_NO_PLUGIN_MANAGER' }, 503);
|
|
421
|
+
let version = '';
|
|
422
|
+
try {
|
|
423
|
+
const body = (await request.json());
|
|
424
|
+
version = typeof body?.version === 'string' ? body.version.trim() : '';
|
|
425
|
+
}
|
|
426
|
+
catch {
|
|
427
|
+
// 无 body 时按未指定版本处理
|
|
428
|
+
}
|
|
429
|
+
if (!/^\d+\.\d+\.\d+$/.test(version))
|
|
430
|
+
return json({ ok: false, error: 'E_BAD_VERSION' }, 400);
|
|
431
|
+
const spec = `@pandablessing/dsh-user-sync@${version}`;
|
|
432
|
+
try {
|
|
433
|
+
await pm.installBundle(spec);
|
|
434
|
+
}
|
|
435
|
+
catch (error) {
|
|
436
|
+
recordHandlerError('update', error);
|
|
437
|
+
return json({ ok: false, error: error?.message ?? String(error) }, 500);
|
|
438
|
+
}
|
|
439
|
+
await forceReinstallIfMissing(['@pandablessing/dsh-user-sync']);
|
|
440
|
+
const installedVersion = (() => {
|
|
441
|
+
try {
|
|
442
|
+
const pkg = JSON.parse(readFileSync(join(paths.profileDir, 'node_modules', '@pandablessing', 'dsh-user-sync', 'package.json'), 'utf8'));
|
|
443
|
+
return pkg.version ?? null;
|
|
444
|
+
}
|
|
445
|
+
catch {
|
|
446
|
+
return null;
|
|
447
|
+
}
|
|
448
|
+
})();
|
|
449
|
+
return json({ ok: true, version: installedVersion, restartRequired: true });
|
|
450
|
+
},
|
|
451
|
+
},
|
|
412
452
|
{
|
|
413
453
|
// 最小同步路由:用于区分「同步 handler 本身不可用」与「status/scan 具体逻辑抛错」
|
|
414
454
|
path: '/api/dsh-user-sync/ping',
|
|
@@ -613,7 +653,11 @@ export function apply(ctx) {
|
|
|
613
653
|
catch {
|
|
614
654
|
// 无 body(面板首次点击)时按未确认处理
|
|
615
655
|
}
|
|
616
|
-
|
|
656
|
+
const result = (await sync.upload({ force }));
|
|
657
|
+
// 上传成功说明远端已与本机一致:立刻重算检查结论,底部状态行不再显示过期的「发现更新」
|
|
658
|
+
if (result.ok)
|
|
659
|
+
void scheduler.runOnce();
|
|
660
|
+
return json(result);
|
|
617
661
|
},
|
|
618
662
|
},
|
|
619
663
|
{
|
|
@@ -708,6 +752,9 @@ export function apply(ctx) {
|
|
|
708
752
|
result.installError = error?.message ?? String(error);
|
|
709
753
|
}
|
|
710
754
|
}
|
|
755
|
+
// 应用成功后远端与本机一致:重算检查结论
|
|
756
|
+
if (result.ok)
|
|
757
|
+
void scheduler.runOnce();
|
|
711
758
|
return json(result);
|
|
712
759
|
},
|
|
713
760
|
},
|