dsh-home-sync 0.2.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 +21 -0
- package/README.md +139 -0
- package/README.zh-CN.md +110 -0
- package/cordis.patch.yml +5 -0
- package/lib/index.js +107 -0
- package/lib/sync.js +374 -0
- package/lib/ui.js +841 -0
- package/package.json +23 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 冯思韬 (OverDustD7)
|
|
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
ADDED
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
# dsh-home-sync
|
|
2
|
+
|
|
3
|
+
English | [简体中文](README.zh-CN.md)
|
|
4
|
+
|
|
5
|
+
Git-based sync for DSH configuration and Mnemon memory plugin data, with automatic two-way sync, cross-device migration, initialization backups, conflict detection, and a web UI.
|
|
6
|
+
|
|
7
|
+
Version 0.2.1 completes the maintenance improvements and fixes gaps in history validation, initialization, and status reporting found during a second review.
|
|
8
|
+
|
|
9
|
+
## Requirements
|
|
10
|
+
|
|
11
|
+
- Node.js 20 or newer and Git.
|
|
12
|
+
- A DSH host providing `webServer`, `timer`, and `connection.requestRejection`.
|
|
13
|
+
- Local compatibility was verified with DSH 0.1.2-rc.1, Node.js 24.20.0, and Git 2.55.0.windows.4.
|
|
14
|
+
|
|
15
|
+
## Installation
|
|
16
|
+
|
|
17
|
+
Prepare the source on the device where you want to use the plugin, then run:
|
|
18
|
+
|
|
19
|
+
```powershell
|
|
20
|
+
dsh plugin --profile web add link:D:/Project/DSH/dsh-home-sync
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
Replace the example path with your local source directory. A `link:` installation reads that directory directly. Restart DSH web after backend updates and refresh the browser to load UI updates. No build step or third-party dependencies are required for this plugin.
|
|
24
|
+
|
|
25
|
+
A `link:` declaration stores a local path; it does not transfer the plugin source to another device. Use the versioned package below to install on another device without recreating the original development path.
|
|
26
|
+
|
|
27
|
+
### Installing on another device
|
|
28
|
+
|
|
29
|
+
The package is `dist/dsh-home-sync-0.2.1.tgz`. It contains the runtime code, plugin declaration, and English and Chinese READMEs. Copy it to the target device and run the following from the directory containing the archive:
|
|
30
|
+
|
|
31
|
+
```powershell
|
|
32
|
+
$syncPackage = (Resolve-Path -LiteralPath './dsh-home-sync-0.2.1.tgz').Path
|
|
33
|
+
dsh plugin --profile web add "$syncPackage"
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
DSH forwards this command to pnpm to install the local package. Device initialization may restore profile declarations from the old device, so run the installation command again after initialization, then restart DSH web. Keep the package for other devices and future reinstalls; Git synchronization does not distribute the archive itself.
|
|
37
|
+
|
|
38
|
+
To rebuild the package from source, create the `dist` directory and run:
|
|
39
|
+
|
|
40
|
+
```powershell
|
|
41
|
+
npm pack --ignore-scripts --pack-destination dist
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
If the target package manager cannot install a local tarball, extract it into a permanent directory and install the extracted `package` subdirectory using `link:`. Do not use a temporary extraction directory as a permanent link target.
|
|
45
|
+
|
|
46
|
+
## Synchronization behavior
|
|
47
|
+
|
|
48
|
+
- **Pull** checks the remote and attempts a fast-forward. It fails if the branches have diverged or local changes could be overwritten, and does not create local commits. The operation explicitly disables `merge.autoStash` to avoid reporting success when restoring stashed changes produces conflicts. The index is also checked after merging.
|
|
49
|
+
- **Sync to remote** fetches and validates the remote tree, commits allowed local changes, integrates remote changes, and pushes. It retries unpublished commits even when the working tree is clean.
|
|
50
|
+
- **Automatic sync** performs the full synchronization cycle at the configured interval, including checking remote changes when there are no local changes. Saving switches or the interval immediately rebuilds the scheduled tasks. Configuration saves and other write requests return a busy response while an operation is running.
|
|
51
|
+
- The checked-out branch must match the configured branch. Diverged branches may be integrated with a regular Git merge. If a content conflict occurs, the plugin stops before pushing, aborts the unfinished merge, and preserves local commits. Resolve the conflict manually before retrying; the plugin does not automatically force-push.
|
|
52
|
+
- Status refreshes every 15 seconds and distinguishes uncommitted files, commits to push or pull, and read failures. New directories are counted by their individual files. Ahead/behind counts reflect the most recent successful fetch.
|
|
53
|
+
- Immediate feedback appears in a toast. The **Operation history** section is collapsed by default; expand it to view and copy backup paths and recovery instructions. Up to 100 recent manual and background operations are stored beside the home directory in `<DSH_HOME>.home-sync-history.json`, subject to a total size limit. These records survive restarts and are excluded from Git synchronization. They contain results, timestamps, and backup paths, not synchronized file contents. A damaged history file is preserved and reported; a history write failure does not undo a successful sync.
|
|
54
|
+
- An open panel and its floating button follow host and system theme changes without losing current input or expanded sections. The current web interface uses Chinese labels.
|
|
55
|
+
|
|
56
|
+
All HTTP data endpoints use the host's authentication and Host/Origin validation. The plugin does not activate without that service. POST requests accept JSON only. The UI script itself contains no user data.
|
|
57
|
+
|
|
58
|
+
## Files included in synchronization
|
|
59
|
+
|
|
60
|
+
- `.gitignore`, `settings.yaml`, and the root `cordis.patch.yml`.
|
|
61
|
+
- Under `profiles/web/`: `package.json`, `pnpm-lock.yaml`, `pnpm-workspace.yaml`, `cordis.yml`, `cordis.patch.yml`, and `.dsh-market/state.json`.
|
|
62
|
+
- Non-hidden files under `mnemon/`, excluding path components named `data`, `state`, `credential`, `credentials`, `secret`, or `secrets`, and files ending in `.log`, `.wal`, or `.shm`.
|
|
63
|
+
|
|
64
|
+
Credentials, sessions, attachments, and `node_modules` are not added to commits. Forbidden files already staged or tracked block synchronization.
|
|
65
|
+
|
|
66
|
+
Before pushing, the plugin validates the entire history reachable from HEAD, up to 1,000 commits. Commits already present at the fetch remote are not exempt, so separate fetch/push addresses and multiple push destinations receive the same checks. A forbidden file anywhere in that history blocks the push even if it has since been deleted. History exceeding the limit must first be manually reviewed and organized into a clean synchronization history.
|
|
67
|
+
|
|
68
|
+
Pushes update only the selected branch; they do not automatically follow tags or mirror other references. Pushes to multiple repositories are not a single transaction, so partial network failures may require retries. The plugin does not redact fields inside allowed files. Do not put content you do not want uploaded in synchronized configuration or memory files.
|
|
69
|
+
|
|
70
|
+
Only ordinary files are accepted. Symbolic links, hard links, submodules, paths outside the home directory, Windows reserved names, and case collisions are rejected. Git hooks and commit signing still follow your Git configuration. Git subprocesses use timeouts and non-interactive credential settings.
|
|
71
|
+
|
|
72
|
+
## Device initialization and recovery
|
|
73
|
+
|
|
74
|
+
Both initialization modes create backups. The `merge` mode means **back up, reset, then merge manually**; it does not automatically combine two sets of memories.
|
|
75
|
+
|
|
76
|
+
1. Fetch the remote into an independent temporary repository and validate target paths and file types.
|
|
77
|
+
2. Check local path collisions and back up every file that may be overwritten or removed, the original Git metadata, and the plugin configuration.
|
|
78
|
+
3. Verify that local files did not change during backup, apply the allowed files, and configure the target branch, origin, and upstream.
|
|
79
|
+
4. Pause automatic sync and startup pull after success, allowing you to inspect the results, merge any required content manually, and re-enable automation.
|
|
80
|
+
|
|
81
|
+
Initialization supports a new directory, an independent repository that has been initialized with `git init` but has no first commit, and switching an existing setup to a remote with unrelated history. A successful response includes the actual configuration, and the UI immediately updates the automatic switches. Initialization explicitly resets settings, so the current form is replaced with the server configuration.
|
|
82
|
+
|
|
83
|
+
Backups are stored beside DSH_HOME at `<DSH_HOME>.home-sync-backups/init-*`, outside the synchronized working tree. With the default home directory, this resembles `~/.dsh.home-sync-backups/init-*`.
|
|
84
|
+
|
|
85
|
+
| Backup entry | Contents |
|
|
86
|
+
| --- | --- |
|
|
87
|
+
| `files/` | Original files |
|
|
88
|
+
| `repository/` | Original `.git`, including local history, index, and remote configuration |
|
|
89
|
+
| `config.json` | Original plugin configuration, if it existed |
|
|
90
|
+
| `manifest.json` | Target, operation state, original file existence, and checksums |
|
|
91
|
+
| `MERGE.md` | Recovery instructions |
|
|
92
|
+
|
|
93
|
+
Caught execution errors trigger an attempt to restore the original state. Automatic recovery cannot be guaranteed after a power failure or forced termination. Stop DSH, restore files according to the manifest, restore `repository/` as `.git`, restore the original configuration, and remove files the manifest marks as originally absent. Do not restore while DSH is writing memory data. Initialization explicitly rejects Git worktrees and linked `.git` entries.
|
|
94
|
+
|
|
95
|
+
Other programs must also avoid writing synchronized files during initialization. The plugin lock coordinates its own instances; it cannot pause other plugins or manual Git operations.
|
|
96
|
+
|
|
97
|
+
## Configuration and operation lock
|
|
98
|
+
|
|
99
|
+
Configuration is stored at `<DSH_HOME>/dsh-home-sync.json`.
|
|
100
|
+
|
|
101
|
+
| Setting | Purpose or constraint |
|
|
102
|
+
| --- | --- |
|
|
103
|
+
| `branch` | Synchronization branch |
|
|
104
|
+
| `autoPullOnStartup` | Pull when the plugin starts |
|
|
105
|
+
| `autoSync` | Run periodic full synchronization |
|
|
106
|
+
| `syncIntervalSeconds` | Integer from 15 to 86,400 |
|
|
107
|
+
| `commitMessage` | Commit message, 1–1,000 characters |
|
|
108
|
+
| `sshBatch` | Use SSH batch mode |
|
|
109
|
+
|
|
110
|
+
The legacy `autoSyncOnStartup` setting migrates to `autoSync`. If both are present, `autoSync` takes precedence.
|
|
111
|
+
|
|
112
|
+
A damaged configuration does not fall back to defaults and enable automatic tasks. Repair or restore the JSON file, then restart DSH. Configuration writes flush a temporary file before replacing the original; initialization also creates a separate configuration backup.
|
|
113
|
+
|
|
114
|
+
The cross-process lock directory is `<DSH_HOME>.home-sync-lock`. An abnormal exit may leave it behind. Remove that directory only after confirming that no DSH synchronization task is running. Recover an unfinished initialization from its backup first.
|
|
115
|
+
|
|
116
|
+
## Verification
|
|
117
|
+
|
|
118
|
+
```powershell
|
|
119
|
+
npm run check
|
|
120
|
+
npm test
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
Tests use system temporary directories, synthetic files, and local Git remotes. They do not access your real DSH home or accounts. Test repositories are retained in temporary directories for diagnosis.
|
|
124
|
+
|
|
125
|
+
Browser tests use a separate temporary Chrome/Edge profile and are skipped if no supported browser is found. Set `CHROME_PATH` to select a browser. Restricted environments may require running browser tests with normal user permissions.
|
|
126
|
+
|
|
127
|
+
For host compatibility tests, `DSH_HOST_MODULES` can point to an installed DSH `node_modules/@deepseek-ai` directory. These tests are skipped if no host installation is found. They reuse the real host routing and source-validation methods with a synthetic identity-cookie implementation.
|
|
128
|
+
|
|
129
|
+
Development and audit documentation (inspection / repair / review reports and `audit/` evidence) is kept locally as internal material and is not distributed with this repository. `test/` and `npm test` are the public, reproducible verification entry points.
|
|
130
|
+
|
|
131
|
+
## Historical review: September 7, 2026
|
|
132
|
+
|
|
133
|
+
**The following describes the 0.2.0 review. Its conclusion that no new high-risk defects had been found was superseded by the second review.** Development and audit reports (including the 0.2.1 second review) are kept locally as internal material and are not distributed with this repository; this section documents the earlier independent review for traceability. Immediate logs remain visually hidden; a separate collapsed operation-history section is now available.
|
|
134
|
+
|
|
135
|
+
The earlier review covered `lib/index.js`, `lib/sync.js`, `lib/ui.js`, `package.json`, and `cordis.patch.yml`, comparing them with the two original reports. It reported implementations for all 17 original findings and 35 passing tests with no failures. Later independent probes found gaps that those tests did not cover.
|
|
136
|
+
|
|
137
|
+
That review also restored toast feedback instead of showing a log box inside the card after saving settings. The hidden `#log` remains an `aria-live` region for screen readers. This interaction is preserved in 0.2.1.
|
|
138
|
+
|
|
139
|
+
Both initialization modes still perform backup, reset, and pausing of automatic tasks; their messages explain the intended workflow. Migrating an existing device requires manually selecting content from the backup using `MERGE.md`. HTTP data endpoints require a trusted authenticated session; an unauthenticated request returning 401 is expected. Backend changes require a DSH web restart, and UI changes require a browser refresh.
|
package/README.zh-CN.md
ADDED
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
# dsh-home-sync
|
|
2
|
+
|
|
3
|
+
[English](README.md) | 简体中文
|
|
4
|
+
|
|
5
|
+
DSH 配置与 Mnemon 记忆插件数据的 Git 同步工具,支持自动双向同步、跨设备迁移、初始化备份、冲突检测和 Web 可视化管理。0.2.1 补齐维护建议,并修复复审发现的历史检查、初始化和状态显示遗漏。
|
|
6
|
+
|
|
7
|
+
需要 Node.js 20 或更新版本、Git,以及提供 `webServer`、`timer`、`connection.requestRejection` 的 DSH 宿主。本机兼容性验证使用 DSH 0.1.2-rc.1、Node 24.20.0、Git 2.55.0.windows.4。
|
|
8
|
+
|
|
9
|
+
## 安装与生效
|
|
10
|
+
|
|
11
|
+
在需要使用插件的设备上准备源码,然后执行:
|
|
12
|
+
|
|
13
|
+
```powershell
|
|
14
|
+
dsh plugin --profile web add link:D:/Project/DSH/dsh-home-sync
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
路径应换成本机实际路径。已有 link 安装会读取这里的源码;后端更新后需重启 DSH web,浏览器刷新后加载新界面。此项目不需要安装第三方依赖或构建。
|
|
18
|
+
|
|
19
|
+
`link:` 只记录本地路径,不会把插件源码同步到另一台设备。新设备可复制版本化安装包,再使用下面的安装步骤,无需保留原设备的源码路径。
|
|
20
|
+
|
|
21
|
+
### 跨设备安装包
|
|
22
|
+
|
|
23
|
+
交付包为 `dist/dsh-home-sync-0.2.1.tgz`,只包含运行代码、插件声明和中英文 README。将安装包复制到新设备,在包所在目录执行:
|
|
24
|
+
|
|
25
|
+
```powershell
|
|
26
|
+
$syncPackage = (Resolve-Path -LiteralPath './dsh-home-sync-0.2.1.tgz').Path
|
|
27
|
+
dsh plugin --profile web add "$syncPackage"
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
此命令由 DSH 转交给 pnpm,安装当前设备上的包。初始化同步可能恢复旧设备的 profile 声明,因此应在初始化完成后再次执行安装命令,再重启 DSH web。保留安装包供另一台设备和后续重装使用;Git 同步本身不分发安装包。
|
|
31
|
+
|
|
32
|
+
从源码重新生成包:`npm pack --ignore-scripts --pack-destination dist`(先创建 dist 目录)。若目标机器的包管理器无法使用本地 tarball,可把包解压到任意持久目录,再将该目录中的 `package` 子目录以 `link:` 安装;避免把临时解压目录作为长期链接目标。
|
|
33
|
+
|
|
34
|
+
## 同步行为
|
|
35
|
+
|
|
36
|
+
- “拉取”检查远端并尝试快进,遇到分叉或可能覆盖本地修改时失败,不会创建本地提交。显式禁用 `merge.autoStash`,避免自动恢复暂存时发生冲突却误报成功;合并结束还会检查未解决的索引冲突。
|
|
37
|
+
- “同步到远端”先 fetch 并检查文件范围,再提交允许同步的修改、整合远端,最后推送。工作区干净时仍会重试尚未上传的提交。
|
|
38
|
+
- 自动同步按间隔执行完整同步,本地没有变化时也会检查远端。保存开关和间隔后立即重建任务;操作正在执行时,设置保存和其他写请求返回忙碌,请待其结束后重试。
|
|
39
|
+
- 当前分支必须与设置中的分支一致。分支分叉可以尝试普通 Git 合并;同一内容冲突时停止推送、撤销此次未完成合并并保留本地提交。需手动解决 Git 冲突后重试,不会自动 force push。
|
|
40
|
+
- 状态区每 15 秒刷新,区分待提交、待推送、待拉取和读取失败,新增目录按实际文件计数。领先/落后数量以最近成功 fetch 的远端快照为准。
|
|
41
|
+
- 操作反馈使用 toast;面板底部“操作记录”默认折叠,按需展开查看、选择并复制备份路径和恢复指引。最近 100 条后台/手动操作记录保存在主目录旁的 `<DSH_HOME>.home-sync-history.json`,重启后仍可查询,不参与 Git 同步。记录包含操作结果、时间和备份路径,不记录文件正文。记录文件损坏时保留原文件并报告错误;记录写入失败不会撤销已经成功的同步。
|
|
42
|
+
- 打开的面板与浮动按钮随宿主和系统主题变化重新配色,保留当前输入及展开状态。
|
|
43
|
+
|
|
44
|
+
所有 HTTP 数据接口都使用宿主的身份和 Host/Origin 校验;缺少此服务时插件不启用。POST 只接受 JSON。界面脚本本身不含用户数据。
|
|
45
|
+
|
|
46
|
+
## 允许同步的文件
|
|
47
|
+
|
|
48
|
+
- `.gitignore`、`settings.yaml`、根目录 `cordis.patch.yml`。
|
|
49
|
+
- `profiles/web/` 下的 `package.json`、`pnpm-lock.yaml`、`pnpm-workspace.yaml`、`cordis.yml`、`cordis.patch.yml`、`.dsh-market/state.json`。
|
|
50
|
+
- `mnemon/` 下非隐藏文件,但排除名称为 data、state、credential(s)、secret(s) 的路径段,以及 `.log`、`.wal`、`.shm` 文件。
|
|
51
|
+
|
|
52
|
+
凭据、会话、附件、node_modules 等不加入提交。已暂存或已跟踪的禁止文件会阻止同步;推送前检查 HEAD 的完整可达历史(最多 1000 个提交),不以拉取仓库已有历史作为免检范围,因此拉取/推送地址不同或多个推送地址同样受保护。历史中曾出现禁止文件时停止发布,即使该文件已被删除;超过检查上限需先人工整理为干净同步历史。推送只更新所选分支,不自动跟随标签或镜像其他引用。多个目标的网络推送不具备跨仓库事务性,部分失败时需重试。允许文件内部的文本不做字段级脱敏,因此不要在允许同步的配置或记忆里保存不希望上传的内容。
|
|
53
|
+
|
|
54
|
+
只接受普通文件。符号链接、硬链接、子模块、越界路径、Windows 保留名称和大小写冲突会被拒绝。Git hooks 和提交签名仍遵循用户自己的 Git 配置;Git 子进程设置了超时及非交互凭据模式。
|
|
55
|
+
|
|
56
|
+
## 设备初始化与恢复
|
|
57
|
+
|
|
58
|
+
两种初始化模式都会备份。`merge` 表示备份后需要手动合并,并不表示自动把两份记忆合并。
|
|
59
|
+
|
|
60
|
+
1. 从远端抓取到独立临时仓库,检查所有目标路径和文件类型。
|
|
61
|
+
2. 检查本机碰撞,备份所有可能覆盖/删除的文件、原 Git 元数据和插件配置。
|
|
62
|
+
3. 验证备份期间本地文件没有变化,再应用允许的文件并设置目标分支、origin、upstream。
|
|
63
|
+
4. 成功后暂停自动同步与启动拉取,等待用户核对、手动合并及重新开启。
|
|
64
|
+
|
|
65
|
+
支持全新目录、已执行 `git init` 但尚未首次提交的独立仓库,以及再次初始化到历史不相关的远端。成功响应返回实际配置,界面立即刷新自动开关;初始化视为显式重置设置,当前表单以服务端配置为准。
|
|
66
|
+
|
|
67
|
+
备份位于 DSH_HOME 旁边的 `<DSH_HOME>.home-sync-backups/init-*`,不在被同步的工作区内。默认主目录时类似 `~/.dsh.home-sync-backups/init-*`。
|
|
68
|
+
|
|
69
|
+
- `files/`:原文件。
|
|
70
|
+
- `repository/`:原 `.git`,包含本地历史、索引和远端配置。
|
|
71
|
+
- `config.json`:原插件配置(原先存在时)。
|
|
72
|
+
- `manifest.json`:目标、阶段、原文件存在状态和校验值。
|
|
73
|
+
- `MERGE.md`:恢复说明。
|
|
74
|
+
|
|
75
|
+
可捕获的执行错误会尝试自动恢复。断电或强制结束进程不能保证自动恢复:先停止 DSH,依据 manifest 恢复 files,将 repository 恢复为 `.git`,恢复原配置,并移除清单中原先不存在的新文件。不要在 DSH 正写入记忆时执行恢复。Git worktree 或链接 `.git` 的初始化目前会明确拒绝。
|
|
76
|
+
|
|
77
|
+
初始化期间仍需避免其他程序写入同步文件;插件锁只协调本插件实例,不能暂停其他插件或人工 Git 操作。
|
|
78
|
+
|
|
79
|
+
## 配置与运行锁
|
|
80
|
+
|
|
81
|
+
配置位于 `<DSH_HOME>/dsh-home-sync.json`,支持:branch、autoPullOnStartup、autoSync、syncIntervalSeconds(15–86400)、commitMessage(1–1000 字符)、sshBatch。旧 autoSyncOnStartup 会迁移为 autoSync;同时存在时以 autoSync 为准。
|
|
82
|
+
|
|
83
|
+
损坏配置不会退回并开启默认自动任务。需修复或恢复此 JSON 后重启 DSH。写入通过临时文件同步落盘后替换,初始化另外保留配置备份。
|
|
84
|
+
|
|
85
|
+
跨进程锁目录为 `<DSH_HOME>.home-sync-lock`。异常退出可能留下锁;确认没有 DSH 同步任务运行后,才可移除这个锁目录。未完成的初始化应先按备份恢复。
|
|
86
|
+
|
|
87
|
+
## 验证
|
|
88
|
+
|
|
89
|
+
```powershell
|
|
90
|
+
npm run check
|
|
91
|
+
npm test
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
测试使用系统临时目录、虚构文件与本地 Git 远端,不访问真实 DSH 主目录或真实账号。测试仓库保留在系统临时目录便于诊断。
|
|
95
|
+
|
|
96
|
+
浏览器测试使用独立临时 Chrome/Edge 配置;没有浏览器时跳过,可通过 `CHROME_PATH` 指定。受限环境可能需要在正常用户权限下运行浏览器测试。宿主兼容性测试可用 `DSH_HOST_MODULES` 指向已安装 DSH 的 `node_modules/@deepseek-ai`,未找到时跳过;此测试复用真实宿主路由与来源校验方法,身份 Cookie 验证使用虚构实现。
|
|
97
|
+
|
|
98
|
+
开发与审计文档(检查/修复/复核报告及 `audit/` 证据)为本地内部资料,不随本仓库分发;`test/` 与 `npm test` 是公开可复现的验证入口。
|
|
99
|
+
|
|
100
|
+
## 历史复核记录(2026-09-07,已被后续复审补充)
|
|
101
|
+
|
|
102
|
+
**以下是 0.2.0 的历史记录,其"未发现新的高风险缺陷"结论已被后续复审推翻。** 开发与审计文档(含 0.2.1 二次审查报告)为本地内部资料,未随本仓库分发,本段仅作历史追溯;默认隐藏即时日志的交互保持,新增独立折叠操作记录入口。
|
|
103
|
+
|
|
104
|
+
在修复完成报告之后,对 0.2.0 源码进行了独立的只读复核与测试复跑:
|
|
105
|
+
|
|
106
|
+
- **复核范围**:`lib/index.js`、`lib/sync.js`、`lib/ui.js`、`package.json`、`cordis.patch.yml`,与两份报告逐条对照。
|
|
107
|
+
- **结果**:原审计 17 项(F01–F17)修复均已落实到当前源码,抽查关键实现与本页“同步行为/允许同步的文件/设备初始化/验证”描述一致;报告所述 35 项测试在本机(Node 24.20.0 + Git 2.55.0.windows.4,真实 Git 临时仓库与虚构远端)复跑**35/35 通过,0 失败**。
|
|
108
|
+
- **复核中修正的一处界面回归**:界面操作反馈改为底部 toast 浮窗(不再显示卡片内日志框);`#log` 保留但改为视觉隐藏,仅作 aria-live 供屏幕阅读器使用,避免再次出现用户已否决的“保存后在下方显示提示”。
|
|
109
|
+
- **复核结论**:`npm run check` 与 `npm test` 全绿;未发现新的高风险缺陷。
|
|
110
|
+
- **已知边界(与报告一致,非缺陷)**:两种初始化模式底层行为相同(备份 → 重置 → 暂停自动任务),`mode` 仅影响提示文案;“已用机器合并”需要按备份与 `MERGE.md` 手动挑拣;HTTP 接口仅接受已登录的可信会话(未登录直连返回 401 属预期);后端(index/sync)改动需重启 DSH web 后生效,界面(ui.js)改动 Ctrl+F5 即生效。
|
package/cordis.patch.yml
ADDED
package/lib/index.js
ADDED
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
// Authenticated DSH host adapter. Repository mutations live in sync.js.
|
|
2
|
+
import fs from 'node:fs'
|
|
3
|
+
import { createHomeSync, SyncError } from './sync.js'
|
|
4
|
+
|
|
5
|
+
const name = 'dsh-home-sync'
|
|
6
|
+
const inject = ['webServer', 'timer', 'connection']
|
|
7
|
+
|
|
8
|
+
export function readBody(req, timeoutMs = 15000) {
|
|
9
|
+
return new Promise((resolve, reject) => {
|
|
10
|
+
const chunks = []
|
|
11
|
+
let bytes = 0, settled = false
|
|
12
|
+
const finish = (error, value) => {
|
|
13
|
+
if (settled) return
|
|
14
|
+
settled = true; clearTimeout(timer)
|
|
15
|
+
req.removeListener('data', data); req.removeListener('end', end)
|
|
16
|
+
req.removeListener('error', errorHandler); req.removeListener('aborted', aborted); req.removeListener('close', closed)
|
|
17
|
+
if (error) { req.once('error', () => {}); req.resume?.(); reject(error) } else resolve(value)
|
|
18
|
+
}
|
|
19
|
+
const data = chunk => {
|
|
20
|
+
bytes += Buffer.byteLength(chunk)
|
|
21
|
+
if (bytes > 1024 * 1024) return finish(new SyncError('body-too-large', '请求体超过 1 MiB。', 413))
|
|
22
|
+
chunks.push(Buffer.from(chunk))
|
|
23
|
+
}
|
|
24
|
+
const end = () => {
|
|
25
|
+
try {
|
|
26
|
+
const text = new TextDecoder('utf-8', { fatal: true }).decode(Buffer.concat(chunks))
|
|
27
|
+
const value = text ? JSON.parse(text) : {}
|
|
28
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) throw new Error('Expected object')
|
|
29
|
+
finish(null, value)
|
|
30
|
+
} catch { finish(new SyncError('invalid-json', '请求体必须是有效的 UTF-8 JSON 对象。', 400)) }
|
|
31
|
+
}
|
|
32
|
+
const errorHandler = () => finish(new SyncError('body-error', '读取请求体失败。', 400))
|
|
33
|
+
const aborted = () => finish(new SyncError('body-aborted', '请求已中断。', 400))
|
|
34
|
+
const closed = () => { if (!settled) aborted() }
|
|
35
|
+
const timer = setTimeout(() => finish(new SyncError('body-timeout', '读取请求体超时。', 408)), timeoutMs)
|
|
36
|
+
timer.unref?.()
|
|
37
|
+
req.on('data', data); req.on('end', end); req.on('error', errorHandler); req.on('aborted', aborted); req.on('close', closed)
|
|
38
|
+
})
|
|
39
|
+
}
|
|
40
|
+
function json(res, value, code = 200) {
|
|
41
|
+
if (res.destroyed || res.writableEnded) return
|
|
42
|
+
res.writeHead(code, { 'Content-Type': 'application/json; charset=utf-8', 'Cache-Control': 'no-store', 'X-Content-Type-Options': 'nosniff' })
|
|
43
|
+
res.end(JSON.stringify(value))
|
|
44
|
+
}
|
|
45
|
+
function apply(ctx) {
|
|
46
|
+
const disposers = []
|
|
47
|
+
let service, timerDisposers = [], stopped = false
|
|
48
|
+
const clearTimers = () => { for (const fn of timerDisposers.splice(0)) fn() }
|
|
49
|
+
const dispose = () => {
|
|
50
|
+
stopped = true; service?.dispose(); clearTimers()
|
|
51
|
+
for (const fn of disposers.splice(0).reverse()) { try { fn() } catch { /* best-effort cleanup */ } }
|
|
52
|
+
}
|
|
53
|
+
try {
|
|
54
|
+
if (typeof ctx.connection?.requestRejection !== 'function') throw new Error('需要支持 connection.requestRejection 的 DSH 宿主。')
|
|
55
|
+
service = createHomeSync()
|
|
56
|
+
const reconcile = (startup = false) => {
|
|
57
|
+
clearTimers()
|
|
58
|
+
let cfg
|
|
59
|
+
try { cfg = service.readConfig() } catch (error) { console.error('[dsh-home-sync]', error.message); return }
|
|
60
|
+
const run = async (startupOnly = false) => {
|
|
61
|
+
if (stopped || service.active) return
|
|
62
|
+
try {
|
|
63
|
+
const latest = service.readConfig()
|
|
64
|
+
if (latest.autoSync) await service.push()
|
|
65
|
+
else if (startupOnly && latest.autoPullOnStartup) await service.pull()
|
|
66
|
+
} catch (error) { console.error('[dsh-home-sync]', error.message) }
|
|
67
|
+
}
|
|
68
|
+
if (cfg.autoSync) timerDisposers.push(ctx.setInterval(() => run(), cfg.syncIntervalSeconds * 1000))
|
|
69
|
+
if (startup && (cfg.autoSync || cfg.autoPullOnStartup)) timerDisposers.push(ctx.setTimeout(() => run(true), 3000))
|
|
70
|
+
}
|
|
71
|
+
function route(method, endpoint, handler) {
|
|
72
|
+
disposers.push(ctx.webServer.register({ kind: 'exact', path: '/dsh-home-sync/api/' + endpoint,
|
|
73
|
+
handler: async (req, res) => {
|
|
74
|
+
try {
|
|
75
|
+
if (stopped) throw new SyncError('stopped', '插件已停止。', 503)
|
|
76
|
+
const rejection = ctx.connection.requestRejection(req)
|
|
77
|
+
if (rejection !== undefined) return json(res, { ok: false, error: rejection === 401 ? '请先登录 DSH。' : '请求来源不受信任。' }, rejection)
|
|
78
|
+
if (req.method !== method) return json(res, { ok: false, error: '请求方法不支持。' }, 405)
|
|
79
|
+
if (method === 'POST' && !/^application\/json(?:\s*;|$)/i.test(req.headers['content-type'] || '')) return json(res, { ok: false, error: '仅接受 application/json。' }, 415)
|
|
80
|
+
const body = method === 'POST' ? await readBody(req) : undefined
|
|
81
|
+
const result = await handler(body)
|
|
82
|
+
json(res, result, result.ok === false ? 503 : 200)
|
|
83
|
+
} catch (error) { json(res, { ok: false, reason: error.reason || 'error', error: error.message }, error.status || 500) }
|
|
84
|
+
} }))
|
|
85
|
+
}
|
|
86
|
+
route('GET', 'status', () => service.status())
|
|
87
|
+
route('POST', 'pull', () => service.pull())
|
|
88
|
+
route('POST', 'push', () => service.push())
|
|
89
|
+
route('POST', 'init', async body => { const result = await service.init(body); reconcile(); return result })
|
|
90
|
+
route('POST', 'config', async body => { const result = await service.saveConfig(body); reconcile(); return result })
|
|
91
|
+
const uiUrl = new URL('./ui.js', import.meta.url)
|
|
92
|
+
if (fs.existsSync(uiUrl)) {
|
|
93
|
+
disposers.push(ctx.webServer.register({ kind: 'exact', path: '/dsh-home-sync/ui.js', handler: (_req, res) => {
|
|
94
|
+
res.writeHead(200, { 'Content-Type': 'application/javascript; charset=utf-8', 'Cache-Control': 'no-store', 'X-Content-Type-Options': 'nosniff' })
|
|
95
|
+
res.end(fs.readFileSync(uiUrl, 'utf8'))
|
|
96
|
+
} }))
|
|
97
|
+
disposers.push(ctx.webServer.tapIndex(html => {
|
|
98
|
+
if (html.includes('/dsh-home-sync/ui.js')) return html
|
|
99
|
+
const tag = '<script defer src="/dsh-home-sync/ui.js"></script>'
|
|
100
|
+
return html.includes('</body>') ? html.replace('</body>', tag + '</body>') : html + tag
|
|
101
|
+
}))
|
|
102
|
+
}
|
|
103
|
+
ctx.effect(() => dispose)
|
|
104
|
+
reconcile(true)
|
|
105
|
+
} catch (error) { dispose(); console.error('[dsh-home-sync] 插件未启用:', error.message) }
|
|
106
|
+
}
|
|
107
|
+
export { name, inject, apply }
|