@summerscar/dsh-mnemon-sync 0.1.4 → 0.1.6

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.
Files changed (4) hide show
  1. package/CHANGELOG.md +53 -0
  2. package/README.md +131 -14
  3. package/index.js +53 -7
  4. package/package.json +32 -5
package/CHANGELOG.md ADDED
@@ -0,0 +1,53 @@
1
+ # 更新日志
2
+
3
+ 本项目遵循[语义化版本](https://semver.org/lang/zh-CN/)。
4
+
5
+ ## 0.1.5
6
+
7
+ **新增**
8
+
9
+ - 备份文件名带设备名:`mnemon-sync-<设备名>-<时间戳>.zip`。设备名取自 dsh-mnemon 的 `state/machine.json`,多台机器备份到同一目录也能分辨来源。
10
+
11
+ **修复**
12
+
13
+ - 保留策略按运行时间排序,不再按文件名字符串。加了设备名后文件名以设备开头,旧逻辑会按设备分组,可能删掉本机较新的备份而留下其他机器最旧的。
14
+
15
+ ## 0.1.4
16
+
17
+ **修复**
18
+
19
+ - 备份列表按创建时间倒序。此前用字符串比较 RFC1123 日期,跨天时会排错(`09 Nov 23:00` 排在了 `10 Nov 01:00` 前面)。现改为解析时间戳比较,同时兼容 S3 的 ISO 格式。
20
+
21
+ ## 0.1.3
22
+
23
+ **修复**
24
+
25
+ - **标签页不显示**。Source 入口改为标准 Cordis 插件形态(`inject` + `apply` + `installMemory`),并把 bundle patch 拆成两个 Entry(Host 引擎 + Source)。
26
+ - SDK 改用 `createRequire` 同步加载,先试裸包名、失败再按 `DSH_PROFILE_DIR` 拼绝对路径 —— 修复 `link:` 安装下静态 `import` 报 `ERR_MODULE_NOT_FOUND` 的问题。
27
+
28
+ ## 0.1.2
29
+
30
+ **变更**
31
+
32
+ - 插件 meta 文案与功能对齐:标题改为「同步备份」,描述改为三组件任选 + 备份列表 + 选择性恢复。
33
+ - README 精简,安装命令改为 npm 包名。
34
+
35
+ ## 0.1.1
36
+
37
+ **修复**
38
+
39
+ - **关键**:恢复掩码密钥时不再把掩码本身写进配置。此前 `/run` 接口接受页面草稿配置却未做掩码还原,会把 `••••••••` 当成真实密码存盘,导致配置看着正确、备份却一直 401。
40
+
41
+ **新增**
42
+
43
+ - 运行记录支持一键清空(只清本地日志,不动目标上的备份包)。
44
+
45
+ ## 0.1.0
46
+
47
+ 首个版本。
48
+
49
+ - 三组件(记忆空间 / 项目档案 / 运行时记忆)任意组合备份
50
+ - S3 兼容存储(手写 SigV4)与 WebDAV 两种目标
51
+ - 定时备份:关闭 · 每小时 · 3/6/12 小时 · 每天 · 3/7 天
52
+ - 备份列表、明细查看、选择性恢复
53
+ - 数据库走 `node:sqlite` 的 `backup()` 在线备份,每文件 SHA-256 校验
package/README.md CHANGED
@@ -1,18 +1,41 @@
1
+ <div align="center">
2
+
1
3
  # dsh-mnemon-sync
2
4
 
3
- 为 [dsh-mnemon](https://github.com/omdsh-dev/dsh-mnemon) 提供备份与恢复:把记忆组件打包上传到 S3 兼容存储或 WebDAV,需要时挑一个包恢复回来。安装后在记忆系统里多一个「同步备份」标签页。
5
+ **为 dsh-mnemon 的记忆做备份与恢复 —— 打包到 S3 兼容存储或 WebDAV,随时挑一个包还原回来。**
6
+
7
+ [![npm version](https://img.shields.io/npm/v/@summerscar/dsh-mnemon-sync?style=flat-square&color=blue)](https://www.npmjs.com/package/@summerscar/dsh-mnemon-sync)
8
+ [![npm downloads](https://img.shields.io/npm/dm/@summerscar/dsh-mnemon-sync?style=flat-square)](https://www.npmjs.com/package/@summerscar/dsh-mnemon-sync)
9
+ [![license](https://img.shields.io/badge/license-MIT-blue?style=flat-square)](LICENSE)
10
+ [![DeepSeek Harness](https://img.shields.io/badge/DeepSeek%20Harness-plugin-4B6BFB?style=flat-square)](https://github.com/deepseek-ai/deepseek-harness)
11
+
12
+ [快速开始](#快速开始) · [功能](#功能) · [配置](#配置) · [常见问题](#常见问题) · [更新日志](CHANGELOG.md)
4
13
 
5
- ## 安装
14
+ </div>
15
+
16
+ ---
17
+
18
+ `dsh-mnemon` 把长期记忆存在本地 SQLite 和一批 Markdown 文件里。这个插件给它们加上**异地备份**和**按需恢复**:装好后在记忆系统里多一个「同步备份」标签页。
19
+
20
+ ```
21
+ 记忆系统 → 同步备份
22
+ ├── 备份范围 记忆空间 / 项目档案 / 运行时记忆(可任选)
23
+ ├── 备份目标 S3 兼容存储 或 WebDAV
24
+ ├── 自动备份 关闭 · 每小时 · 3/6/12 小时 · 每天 · 3/7 天
25
+ └── 备份列表 查看明细 · 选择组件恢复
26
+ ```
27
+
28
+ ## 快速开始
6
29
 
7
30
  ```sh
8
31
  dsh plugin --profile web add @summerscar/dsh-mnemon-sync
9
32
  ```
10
33
 
11
- 装完重启 DSH 生效。没有 `dsh-mnemon` 时插件仍可运行,只是不显示标签页。
34
+ 重启 DSH,打开记忆系统即可看到「同步备份」。没装 `dsh-mnemon` 时插件仍可运行,只是不显示标签页。
12
35
 
13
- ## 能备份什么
36
+ ## 功能
14
37
 
15
- 三个组件任意组合,默认全选:
38
+ **三个组件,任意组合**(默认全选)
16
39
 
17
40
  | 组件 | 内容 |
18
41
  |---|---|
@@ -22,23 +45,117 @@ dsh plugin --profile web add @summerscar/dsh-mnemon-sync
22
45
 
23
46
  `MNEMON_DATA_DIR` 未设置时取 `~/.mnemon`。
24
47
 
25
- ## 备份目标
48
+ **两种备份目标**
26
49
 
27
- - **S3 兼容** — 手写 AWS Signature V4,适用于 Cloudflare R2、MinIO、Backblaze B2 等
50
+ - **S3 兼容** — 手写 AWS Signature V4,适用于 Cloudflare R2、MinIO、Backblaze B2、阿里云 OSS 等
28
51
  - **WebDAV** — Basic 认证,自动创建远端目录
29
52
 
30
- 凭据存在 `<MNEMON_DATA_DIR>/state/mnemon-sync.json`,权限 `0600`,不写入 DSH 配置,也不进备份包。页面只回显掩码,回传掩码表示不修改。
53
+ **备份文件按设备区分**
54
+
55
+ ```
56
+ mnemon-sync-<设备名>-2026-11-09T01-20-27.875Z.zip
57
+ ```
58
+
59
+ 设备名取自 dsh-mnemon 的 `state/machine.json`,多台机器备份到同一目录也能一眼分辨来源。
60
+
61
+ **一致性有保证**
62
+
63
+ - 数据库走 `node:sqlite` 的 `backup()` 在线备份,写入方的 WAL 不会把副本撕开
64
+ - 每个文件记录 SHA-256,上传前自校验,恢复前再校验一次
65
+ - 恢复默认只做预览,确认后才写盘;写数据库前先删同名 `-wal` / `-shm`,避免 SQLite 重放恢复文件里没有的事务
66
+
67
+ ## 配置
68
+
69
+ 在「同步备份」页面填好即可,配置存在 `<MNEMON_DATA_DIR>/state/mnemon-sync.json`(权限 `0600`),不写入 DSH 配置,也不会进备份包。
70
+
71
+ <details>
72
+ <summary><b>S3 / Cloudflare R2 示例</b></summary>
73
+
74
+ | 字段 | 示例 |
75
+ |---|---|
76
+ | Endpoint | `https://<account>.r2.cloudflarestorage.com` |
77
+ | Bucket | `my-mnemon-backup` |
78
+ | Region | `auto` |
79
+ | Access Key ID | 你的 R2 Access Key |
80
+ | Secret Access Key | 你的 R2 Secret |
81
+ | 强制 path-style | R2 和 MinIO 通常需要勾选 |
82
+
83
+ </details>
84
+
85
+ <details>
86
+ <summary><b>WebDAV 示例</b></summary>
31
87
 
32
- ## 自动备份
88
+ | 字段 | 示例 |
89
+ |---|---|
90
+ | 地址 | `http://192.168.100.134:5005` |
91
+ | 用户名 | 你的 WebDAV 账号 |
92
+ | 密码 | 你的 WebDAV 密码 |
93
+ | 远端目录 | `config-backup/mnemon-sync` |
94
+
95
+ > 地址带不带结尾斜杠都可以,插件会归一化。
96
+
97
+ </details>
98
+
99
+ 页面只回显掩码,回传掩码表示不修改。想换密钥就直接填新值。
100
+
101
+ ## 常见问题
102
+
103
+ <details>
104
+ <summary><b>装好了但看不到标签页?</b></summary>
105
+
106
+ 1. **重启 DSH** —— Host 侧的 Source 注册和浏览器端代码都要重启才加载
107
+ 2. 确认 `dsh-mnemon` 已安装且版本 ≥ 0.5.40
108
+ 3. 确认 `dsh.profile.bundles` 里有 `@summerscar/dsh-mnemon-sync`
109
+ 4. 强刷页面(Ctrl+Shift+R)绕开浏览器缓存
110
+
111
+ </details>
112
+
113
+ <details>
114
+ <summary><b>备份报 401 Not Authorized?</b></summary>
115
+
116
+ 凭据问题。检查用户名密码,以及 WebDAV 服务是否真的在运行。注意有些 NAS 的 WebDAV 改配置后需要重启服务才生效。
117
+
118
+ </details>
33
119
 
34
- 关闭 · 每小时 · 每 3 / 6 / 12 小时 · 每天 · 每 3 天 · 每 7 天
120
+ <details>
121
+ <summary><b>为什么不用 litestream?</b></summary>
35
122
 
36
- ## 恢复
123
+ litestream 一个实例只盯一个数据库,v0.5 每个库只允许一个副本 —— 备份整个 `data` 目录就得为每个记忆空间养一个常驻进程。另外它接管 WAL checkpoint 并持锁,实测会让 `mnemon.db-wal` 停在非零字节,而 dsh-mnemon 自己的备份导出有这条检查,会直接报错:
37
124
 
38
- 从备份列表查看明细,选择要恢复的组件后执行;缺少的组件会明确提示。恢复默认只做预览,确认后才写盘。
125
+ ```js
126
+ if (existsSync(wal) && statSync(wal).size > 0)
127
+ throw new Error(`Memory Space ${id} is busy (...-wal is not checkpointed)`)
128
+ ```
129
+
130
+ </details>
131
+
132
+ <details>
133
+ <summary><b>保留份数怎么算?</b></summary>
134
+
135
+ 按运行时间保留最新的 N 个,超出后删除最旧的。删除依据是插件自己的运行记录,不会去 LIST 远端目录。
136
+
137
+ </details>
138
+
139
+ ## 开发
140
+
141
+ ```sh
142
+ git clone https://github.com/summerscar/dsh-mnemon-sync.git
143
+ cd dsh-mnemon-sync
144
+ ```
145
+
146
+ | 文件 | 职责 |
147
+ |---|---|
148
+ | `index.js` | Host 入口:HTTP 接口、调度装配、备份引擎 |
149
+ | `source.js` | 承载页面身份的记忆 Source(不存记忆) |
150
+ | `snapshot.js` | 组件识别、数据库在线备份、manifest 与校验 |
151
+ | `scheduler.js` | 定时器与运行历史 |
152
+ | `store.js` | 设置存取(0600)与规范化 |
153
+ | `targets.js` | S3 SigV4 / WebDAV 的上传、下载、列目录、删除 |
154
+ | `zip.js` | 零依赖 ZIP 读写(基于 `node:zlib`) |
155
+ | `client.js` | 浏览器端页面与标签注册 |
39
156
 
40
- 数据库用 `node:sqlite` 的 `backup()` 在线备份,副本一致;写回前会删掉同名 `-wal` / `-shm`,避免 SQLite 重放不存在的事务。每个文件都有 SHA-256 校验。
157
+ 零外部运行时依赖。
41
158
 
42
159
  ## 许可
43
160
 
44
- MIT
161
+ [MIT](LICENSE)
package/index.js CHANGED
@@ -20,6 +20,7 @@
20
20
 
21
21
  import { createHash } from 'node:crypto'
22
22
  import { existsSync, mkdirSync, readdirSync, readFileSync, rmSync, statSync, writeFileSync } from 'node:fs'
23
+ import { hostname } from 'node:os'
23
24
  import { dirname, join, resolve } from 'node:path'
24
25
 
25
26
  import { SyncHistory, SyncScheduler } from './scheduler.js'
@@ -83,9 +84,44 @@ async function readJsonBody(req) {
83
84
  return JSON.parse(text)
84
85
  }
85
86
 
86
- /** `mnemon-sync-2026-11-09T041500Z.zip`: sorts lexicographically by time. */
87
- export function archiveKey(at = new Date()) {
88
- return `${ARCHIVE_PREFIX}${at.toISOString().replace(/:/gu, '-')}.zip`
87
+ /**
88
+ * The name this machine goes by in an archive name.
89
+ *
90
+ * Read from dsh-mnemon's own identity file (`state/machine.json`) so a backup
91
+ * uploaded from several machines stays tellable apart. Falls back to the
92
+ * hostname, then to a fixed word, so the key never ends up with an empty
93
+ * segment. Characters that would be awkward in a filename — slashes, spaces,
94
+ * anything a WebDAV path segment must escape — collapse to a single dash.
95
+ */
96
+ export function deviceName(home) {
97
+ let label = ''
98
+ try {
99
+ const identity = JSON.parse(readFileSync(join(home, 'state', 'machine.json'), 'utf8'))
100
+ if (typeof identity?.label === 'string') label = identity.label
101
+ } catch {
102
+ // No identity file yet: the hostname is the same fallback dsh-mnemon uses.
103
+ }
104
+ if (label.trim() === '') {
105
+ try {
106
+ label = hostname()
107
+ } catch {
108
+ label = ''
109
+ }
110
+ }
111
+ const cleaned = label.trim().replace(/[^\p{L}\p{N}._-]+/gu, '-').replace(/^-+|-+$/gu, '')
112
+ return cleaned === '' ? 'device' : cleaned.slice(0, 60)
113
+ }
114
+
115
+ /**
116
+ * `mnemon-sync-<device>-2026-11-09T00-53-57.903Z.zip`
117
+ *
118
+ * The device name sits after the prefix and before the timestamp, so archives
119
+ * from different machines sort together by time within one device and are
120
+ * still obvious at a glance in a shared folder.
121
+ */
122
+ export function archiveKey(at = new Date(), device = 'device') {
123
+ const stamp = at.toISOString().replace(/:/gu, '-')
124
+ return `${ARCHIVE_PREFIX}${device}-${stamp}.zip`
89
125
  }
90
126
 
91
127
  export function apply(ctx, config = {}) {
@@ -108,7 +144,7 @@ export function apply(ctx, config = {}) {
108
144
  try {
109
145
  const snapshot = await buildSnapshot(home, staging, { components: current.components })
110
146
  verifySnapshot(staging)
111
- const key = archiveKey()
147
+ const key = archiveKey(new Date(), deviceName(home))
112
148
  const archive = createZip(collect(staging), { level: 6 })
113
149
  const uploaded = await uploadArchive(target, key, archive, { signal })
114
150
  const removed = await applyRetention(history, target, current.retention.keep, { signal })
@@ -610,11 +646,21 @@ function collect(root, prefix = '') {
610
646
  * as removed.
611
647
  */
612
648
  async function applyRetention(history, target, keep, options) {
649
+ // Sort by when the run happened, not by key. With a device name in the key,
650
+ // lexical order groups by device first, so "keep the newest N" would delete
651
+ // this machine's newest archive before another machine's oldest one.
613
652
  const uploaded = history.read()
614
653
  .filter((run) => run.ok && typeof run.key === 'string')
615
- .map((run) => run.key)
616
- const unique = [...new Set(uploaded)].sort()
617
- const overflow = unique.slice(0, Math.max(0, unique.length - keep))
654
+ .map((run) => ({ key: run.key, at: Date.parse(run.finishedAt ?? run.startedAt ?? '') || 0 }))
655
+ const seen = new Set()
656
+ const unique = []
657
+ for (const entry of uploaded) {
658
+ if (seen.has(entry.key)) continue
659
+ seen.add(entry.key)
660
+ unique.push(entry)
661
+ }
662
+ unique.sort((left, right) => right.at - left.at)
663
+ const overflow = unique.slice(keep).map((entry) => entry.key)
618
664
  const removed = []
619
665
  for (const key of overflow) {
620
666
  try {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@summerscar/dsh-mnemon-sync",
3
- "version": "0.1.4",
3
+ "version": "0.1.6",
4
4
  "description": "Back up and restore dsh-mnemon memory components (memory spaces, project documents, runtime memory) to S3-compatible storage or WebDAV.",
5
5
  "type": "module",
6
6
  "main": "index.js",
@@ -9,7 +9,8 @@
9
9
  "./client": "./client.js",
10
10
  "./locale/*.json": "./locale/*.json",
11
11
  "./icon": "./icon.svg",
12
- "./source": "./source.js"
12
+ "./source": "./source.js",
13
+ "./package.json": "./package.json"
13
14
  },
14
15
  "files": [
15
16
  "index.js",
@@ -24,19 +25,25 @@
24
25
  "locale/*.json",
25
26
  "icon.svg",
26
27
  "README.md",
27
- "LICENSE"
28
+ "LICENSE",
29
+ "CHANGELOG.md"
28
30
  ],
29
31
  "keywords": [
32
+ "dsh",
30
33
  "dsh-plugin",
31
34
  "deepseek-harness",
32
35
  "deepseek-harness-plugin",
36
+ "cordis",
33
37
  "dsh-mnemon",
34
38
  "mnemon",
39
+ "agent-memory",
35
40
  "backup",
36
41
  "restore",
37
42
  "memory",
43
+ "sqlite",
38
44
  "webdav",
39
- "s3"
45
+ "s3",
46
+ "cloudflare-r2"
40
47
  ],
41
48
  "license": "MIT",
42
49
  "engines": {
@@ -62,6 +69,25 @@
62
69
  "@deepseek-ai/dsh-client-locale",
63
70
  "dsh-mnemon/client"
64
71
  ]
72
+ },
73
+ "catalog": {
74
+ "category": "tool",
75
+ "summary": {
76
+ "en": "Back up and restore dsh-mnemon memory to S3-compatible storage or WebDAV, with scheduled backups and selective restore.",
77
+ "zh": "把 dsh-mnemon 的记忆备份到 S3 兼容存储或 WebDAV,支持定时备份与选择性恢复。"
78
+ },
79
+ "capabilities": [
80
+ "webServer",
81
+ "mnemonMemory",
82
+ "slots",
83
+ "locale"
84
+ ]
85
+ },
86
+ "compatibility": {
87
+ "dsh": ">=0.2.1-rc.1 <0.3.0-0",
88
+ "profiles": [
89
+ "web"
90
+ ]
65
91
  }
66
92
  },
67
93
  "repository": {
@@ -74,5 +100,6 @@
74
100
  "homepage": "https://github.com/summerscar/dsh-mnemon-sync#readme",
75
101
  "publishConfig": {
76
102
  "access": "public"
77
- }
103
+ },
104
+ "icon": "./icon.svg"
78
105
  }