nx-sk 0.11.0 → 0.12.0
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/CHANGELOG.md +95 -0
- package/README.md +39 -30
- package/assets/nx-sk/SKILL.md +3 -3
- package/assets/nx-sk/references/00-design.md +2 -2
- package/assets/nx-sk/references/10-sections-entries.md +2 -2
- package/assets/nx-sk/references/20-secrets.md +5 -5
- package/assets/nx-sk/references/40-files.md +2 -2
- package/assets/nx-sk/references/60-transfer.md +68 -28
- package/package.json +6 -2
- package/src/core/attachments.js +1 -1
- package/src/core/paths.js +4 -2
- package/src/core/securezip.js +72 -0
- package/src/core/store.js +4 -40
- package/src/modules/entries/kvTable.jsx +1 -1
- package/src/modules/entries/service.js +8 -10
- package/src/modules/entries/view.jsx +91 -18
- package/src/modules/export/view.jsx +9 -151
- package/src/modules/files/service.js +7 -10
- package/src/modules/sections/service.js +9 -14
- package/src/modules/settings/service.js +2 -4
- package/src/modules/settings/view.jsx +5 -16
- package/src/modules/system/service.js +1 -2
- package/src/modules/transfer/index.js +19 -12
- package/src/modules/transfer/service.js +212 -79
- package/src/runtime/registry.js +1 -2
- package/src/web/frontend/App.jsx +10 -3
- package/src/web/frontend/activeView.js +2 -2
- package/src/web/frontend/api/zip.js +12 -0
- package/src/web/frontend/components/fieldEditor.jsx +67 -85
- package/src/web/frontend/components/mdView.jsx +193 -0
- package/src/web/frontend/components/navModel.js +10 -0
- package/src/web/frontend/components/readableExport.jsx +159 -0
- package/src/web/frontend/components/transferPanel.jsx +316 -116
- package/src/web/frontend/components/ui.jsx +13 -7
- package/src/web/frontend/mdInline.js +74 -3
- package/src/web/frontend/mdSmart.js +164 -0
- package/src/web/frontend/mdSource.js +41 -0
- package/src/web/frontend/registry.js +1 -4
- package/src/web/frontend/styles.css +218 -11
- package/src/web/public/assets/{files-DuwKZ5bu.js → files-DA6DHa6W.js} +1 -1
- package/src/web/public/assets/{index-C8-8IXxK.css → index-C0soQxz7.css} +1 -1
- package/src/web/public/assets/index-DyvN6UVw.js +41 -0
- package/src/web/public/assets/view-2fmPzM-r.js +38 -0
- package/src/web/public/assets/{view-B3dDfoL5.js → view-B20jmlM1.js} +1 -1
- package/src/web/public/assets/view-BEKkVrl1.js +7 -0
- package/src/web/public/assets/view-BiFJZZ7R.js +1 -0
- package/src/web/public/assets/{view-B_XDtZn-.js → view-DkM8Ts0E.js} +1 -1
- package/src/web/public/assets/zip-gQ69BVQr.js +1 -0
- package/src/web/public/index.html +2 -2
- package/assets/nx-sk/references/30-export-backup.md +0 -104
- package/src/modules/backup/index.js +0 -46
- package/src/modules/backup/service.js +0 -678
- package/src/modules/backup/view.jsx +0 -251
- package/src/web/public/assets/index-CyHzNRPd.js +0 -41
- package/src/web/public/assets/view-B2C1AKyl.js +0 -7
- package/src/web/public/assets/view-BDMftvuZ.js +0 -7
- package/src/web/public/assets/view-CC3A5XfV.js +0 -1
- package/src/web/public/assets/view-MdIYFfFy.js +0 -5
- package/src/web/public/assets/zip-CS8JXp8H.js +0 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,101 @@
|
|
|
2
2
|
|
|
3
3
|
本文件格式参考 [Keep a Changelog](https://keepachangelog.com/),中文正文。
|
|
4
4
|
|
|
5
|
+
## [Unreleased]
|
|
6
|
+
|
|
7
|
+
## [0.12.0] - 2026-10-18
|
|
8
|
+
|
|
9
|
+
> **BREAKING**:`nx-sk backup *` 命令与 `.nxskbak` 格式已移除。改用 `nx-sk transfer export` /
|
|
10
|
+
> `transfer import-new`。旧的 `.nxskbak` 文件不再可导入,请在本版发布前用 0.11.x 导出一份。
|
|
11
|
+
> 详见 [Removed](#removed)。
|
|
12
|
+
|
|
13
|
+
### Added
|
|
14
|
+
|
|
15
|
+
- **导出包可设口令加密**(按栏目 ZIP 与全局 bundle 都支持):导出选项里填口令(输两次),
|
|
16
|
+
下载的包整体 AES-256-GCM 加密。密钥由口令经 scrypt 派生,与密钥库同一套参数;
|
|
17
|
+
每次导出盐与 IV 都重新随机 —— 同一个包加密两次密文不同。
|
|
18
|
+
- 导入是同一个入口:自动识别加密包,**先问口令**再预览 —— 不会把「缺口令」
|
|
19
|
+
误报成「包坏了」。口令错 / 内容被改都在 GCM 认证阶段报错,不会解出垃圾。
|
|
20
|
+
- CLI:`--passphrase`,也认环境变量 `NX_SK_TRANSFER_PASSPHRASE`(命令行口令会进进程列表)。
|
|
21
|
+
- **导入预览列出冲突明细**:哪条撞上哪条(同 id / 同标题)、另存会变成什么标题
|
|
22
|
+
(选「另存」后实时显示 `→ 甲(导入)」)。单栏目与整库预览口径一致。
|
|
23
|
+
- **零依赖 ZIP 实现**(`src/core/zip.js`:store/deflate、CRC32、路径安全校验),
|
|
24
|
+
不引第三方压缩库就能产出合法的跨平台 ZIP。
|
|
25
|
+
- **图文(`md`)正文改用 markdown 组件渲染**(新增依赖 `react-markdown` + `remark-gfm` + `remark-breaks`,
|
|
26
|
+
新组件 `web/frontend/components/mdView.jsx`):标题、有序/无序列表、表格、代码块、引用、删除线、
|
|
27
|
+
裸网址自动链接都按标准 markdown 渲染。此前手写解析只认图片与链接,SOP 里的结构在预览里全是字面量。
|
|
28
|
+
- **待办可点**:`- [ ] 步骤` 在预览里就是勾选框,点一下写回正文的 `[x]`(`mdSource.toggleTaskAtLine`)。
|
|
29
|
+
按解析器给的**行号**定位,不是「第几个勾选框」——嵌套列表与有序列表都不会串行;
|
|
30
|
+
勾选状态只有正文一份,不另存,所以导出、CLI 读到的就是勾完的样子。
|
|
31
|
+
- **图片与链接的行为保持不变**:`` 仍走附件库 + 放大镜(`[说明](f_xxx)` 直接打开附件);
|
|
32
|
+
只放行 http(s) 与能查到的附件 id,其余按普通文字渲染,`javascript:` 不会被执行。
|
|
33
|
+
- **裸地址自动识别**(`web/frontend/mdSmart.js`,remark 插件,排在 gfm 之后):
|
|
34
|
+
`125.67.197.2:7443`、`10.0.0.1`、`localhost:7866` 渲染成虚下划线的「点击即复制」;
|
|
35
|
+
`qcloud.sicau.edu.cn/login`、`example.com:8080/a?b=1` 自动补 `http://` 后点击打开。
|
|
36
|
+
宁可严:TLD 黑名单(`config.json`、`README.md` 这类文件名不会被当成域名)、端口 1–65535 校验、
|
|
37
|
+
前面是字母/数字/`@`/`/`/`:` 时不算起点(不在已有 URL、邮箱、路径里再识别一次);
|
|
38
|
+
代码块与行内代码原样保留。工具栏统计同步区分「链接」与「IP」。
|
|
39
|
+
- **补掉解析器带进来的两个坑**:GFM 自动链接一直吃到空白,中文句子又不用空格,
|
|
40
|
+
`见 https://a.com。另见` 会被整段算进 href(点开 404)—— `cleanHttpUrl` / `linkTextParts`
|
|
41
|
+
按「解码后遇到中文标点即断」截断,断下来的标点回到正文;`remark-breaks` 把单个换行渲染成 `<br>`,
|
|
42
|
+
保住手写那版 `pre-wrap` 的「一行一步」手感,已有 SOP 不会挤成一整段。
|
|
43
|
+
- 统计与下方图库仍走 `mdInline.js`(只读解析,不参与渲染);新增 `tests/unit/md-source.test.mjs`,
|
|
44
|
+
`md-inline.test.mjs` 补了 URL 截断的用例。
|
|
45
|
+
|
|
46
|
+
### Changed
|
|
47
|
+
|
|
48
|
+
- **加密包改为「内部数据先加密,再套合法 ZIP」**:之前整个文件是密文,Windows 会报
|
|
49
|
+
「压缩(zipped)文件夹错误」。现在外层始终是标准 ZIP(内含 `payload.enc` + `README.txt`),
|
|
50
|
+
系统压缩工具能正常打开,只看到一个看不懂的文件;判定是否加密也只需看条目名,
|
|
51
|
+
不用嗅探 magic。旧 `NXSKENC1` 整文件密文**仍可导入**,只是不再产出。
|
|
52
|
+
- **去掉「打码 / 明文」二选,口令即保护**(BREAKING,flag 层):`--no-secrets` 移除。
|
|
53
|
+
要分享就设口令,不设口令的包自己收好。读取侧仍兼容 0.11.0 及更早的打码包
|
|
54
|
+
(遇到 `******` 跳过该字段并计数,不会把打码串当新密码存进去)。
|
|
55
|
+
- **「按栏目 / 整库备份」两个 tab 合并成一个面板**:栏目的全选就是整库导出,
|
|
56
|
+
「整库」按钮只是全选的快捷方式,产物与逐栏勾选是同一个包、同一套导入逻辑。
|
|
57
|
+
- **导出包支持 `--no-files`**:只带元数据不带附件原件,跨机传配置时省体积。
|
|
58
|
+
- **`transfer import-new` 自动识别包结构**:不用手动挑单栏目导入还是整库导入。
|
|
59
|
+
- **面板重排(UI)**:左右两栏(左 = 选内容 + 设口令,右 = 解密导入);
|
|
60
|
+
栏目横向 chips,**指针移上去自动展开该栏目条目**(点击/键盘同样可用,不只靠悬浮),
|
|
61
|
+
「整库 / 清空」预设;口令与「导出 ZIP」提到卡片头部常驻,条目再多也不用滚动找按钮。
|
|
62
|
+
chips 用原生 `<button>` + `aria-pressed`/`aria-expanded`,长标题省略号 + title 兜底。
|
|
63
|
+
- **附件中心分页**:面板每页 24 个,避免一次加载全部图片把浏览器撑爆;
|
|
64
|
+
CLI 默认仍全量返回。
|
|
65
|
+
|
|
66
|
+
### Fixed
|
|
67
|
+
|
|
68
|
+
- **头部导航的分组角标显示成了条目数之和**:「求职资料」分组只有 6 个栏目,角标却是 16 ——
|
|
69
|
+
那是组内所有栏目的条目数加总。角标紧挨组名,本该回答「这一坨里有多少个栏目」;
|
|
70
|
+
加总出来的数字看着像「这个分组本身是个栏目、底下挂着 16 条记录」,而展开后每行
|
|
71
|
+
又各有自己的条目数,既冗余又误导。现在显示**栏目数**,并给 button 补了
|
|
72
|
+
`title="求职资料:6 个栏目"`。平铺 tab 不变(它的角标本就是该栏目的条目数)。
|
|
73
|
+
- **`entryIds` 在路由层没透传**:服务层单测是绿的,但 ZIP 仍打全量。
|
|
74
|
+
修复后浏览器解包确认只含所选条目。
|
|
75
|
+
- **group 回落不一致**:多处各自兜底成不同栏目名,统一到一处。
|
|
76
|
+
- **阅读态下没有图片的 `md` 字段不再显示空的图片区**:纯文字字段(上线检查清单、常用链接)
|
|
77
|
+
底下会多出「图片(0)」和一句「正文里还没有图片」,纯噪音。现在只有正文真有图、
|
|
78
|
+
或正在编辑时才渲染那块;编辑态保留,因为它是「贴进来的图会出现在这里」的落点提示。
|
|
79
|
+
- **「另存」的标题在极端情况下会重复加「(导入)」**:`uniqueTitle` 的基准原本传的是
|
|
80
|
+
`标题(导入)`,二次重名会变成 `A(导入)(导入)`,与预览里算给用户看的 `copyTitle` 也对不上。
|
|
81
|
+
现在基准统一用原标题,预览与实际落盘一致。
|
|
82
|
+
- **空栏目被整库选中时,chip 的 `aria-pressed` 是 false**:选中态原先按条目数推导
|
|
83
|
+
(`!!n`),0 条目的栏目于是即使用户勾了也读成「未选」—— 读屏用户得到错误状态。
|
|
84
|
+
选中态现在与条目数无关。
|
|
85
|
+
|
|
86
|
+
### Removed
|
|
87
|
+
|
|
88
|
+
- **备份模块整体删除**(BREAKING):`nx-sk backup *` 命令、`.nxskbak` 格式、
|
|
89
|
+
`src/modules/backup/` 与 `references/30-export-backup.md` 全部移除。
|
|
90
|
+
有文件导出就够了,本地快照备份是重复的第二套回退机制,而且两边口径必然漂移。
|
|
91
|
+
- 删条目 / 栏目 / 附件**不再留快照** —— 改为「先导出再删」,`transfer export`
|
|
92
|
+
出来的包就是唯一回退手段。
|
|
93
|
+
- 跨机敏感字段的重新加密逻辑迁到 `transfer`:导入后用**本机 vaultKey** 重加密落盘,
|
|
94
|
+
与手填字段没有区别。
|
|
95
|
+
- 面板里可读的 JSON / Markdown 导出移到**设置**页(人读副本,不参与导入)。
|
|
96
|
+
- 导航名由「导出与备份」改为「导出导入」;旧书签 `#/backup` 自动落到「设置」。
|
|
97
|
+
- 旧命令 `nx-sk backup export|import` 报「未知命令」,请改用 `transfer export` /
|
|
98
|
+
`transfer import-new`(旧 `.nxskbak` 文件不再可导入)。
|
|
99
|
+
|
|
5
100
|
## [0.11.0] - 2026-10-18
|
|
6
101
|
|
|
7
102
|
### Added
|
package/README.md
CHANGED
|
@@ -74,6 +74,23 @@ ln -s "$(pwd)/assets/nx-sk" "$HOME/.workbuddy/skills/nx-sk"
|
|
|
74
74
|
> nx-sk skill install --force # 改了 assets/nx-sk/ 之后必须重装,否则是旧版
|
|
75
75
|
> nx-sk skill get nx-sk --json # 外部 agent 一键拿全上下文(四元:skillName/ref/content/contentBytes/install)
|
|
76
76
|
|
|
77
|
+
## 从 0.11.x 升级
|
|
78
|
+
|
|
79
|
+
**0.12.0 有不兼容变更:备份模块已删除。**
|
|
80
|
+
|
|
81
|
+
| 原来 | 现在 |
|
|
82
|
+
| --- | --- |
|
|
83
|
+
| `nx-sk backup export --password 口令` | `nx-sk transfer export <栏目> --passphrase 口令 --out x.zip` |
|
|
84
|
+
| `nx-sk backup import --file x.nxskbak` | `nx-sk transfer import-new --file x.zip` |
|
|
85
|
+
| 面板「导出与备份」 | 面板「导出导入」(栏目全选即整库) |
|
|
86
|
+
|
|
87
|
+
- **旧的 `.nxskbak` 文件不再可导入。** 升级前请先在 0.11.x 下导出一份
|
|
88
|
+
(`nx-sk transfer bundle-export --out x.zip`),升级后用 `transfer import-new --file x.zip` 导回。
|
|
89
|
+
- 破坏性删除(删栏目 / 条目 / 附件)**不再自动留快照** —— 删之前先导一份。
|
|
90
|
+
- `transfer export --no-secrets` 移除:包里恒为明文,保护只靠 `--passphrase`。
|
|
91
|
+
- 加密包格式变了(外层是合法 ZIP、内含 `payload.enc`),旧包仍可导入;
|
|
92
|
+
但 Windows 上不再报「压缩文件夹错误」了。
|
|
93
|
+
|
|
77
94
|
## 它是什么
|
|
78
95
|
|
|
79
96
|
一个「本机工具类」项目的标准骨架实现(server-cli-web):**serve 驱动 CLI 与 Web,
|
|
@@ -132,11 +149,12 @@ CLI 与 API 同源,skill 驱动 agent**。落到具体功能上:
|
|
|
132
149
|
| `nx-sk file open <id>` | 用系统默认程序打开原件 |
|
|
133
150
|
| `nx-sk export run [--format json\|md\|both] [--section id] [--with-secrets] [--out DIR] [--dry-run]` | 整体导出 |
|
|
134
151
|
| `nx-sk export list` | 历次导出与目录内文件 |
|
|
135
|
-
| `nx-sk transfer export <栏目> [--entries 条目,条目] [--
|
|
152
|
+
| `nx-sk transfer export <栏目> [--entries 条目,条目] [--passphrase 口令] [--out 文件]` | 栏目/选中条目 + 引用附件打成 ZIP,可设口令加密(栏目页「更多」里同源下载) |
|
|
136
153
|
| `nx-sk transfer import <栏目> [--file ZIP] [--on-conflict skip\|copy\|overwrite] [--dry-run]` | 预览/导入到指定栏目;附件按 sha256 复用 |
|
|
137
154
|
| `nx-sk transfer import-new [--file ZIP] [--dry-run]` | **零配置**:按包内结构新建栏目(id 冲突加序号) |
|
|
138
|
-
| `nx-sk
|
|
139
|
-
| `nx-sk
|
|
155
|
+
| `nx-sk transfer bundle-export [--sections 栏目,栏目] [--passphrase 口令] [--no-files] [--out 文件]` | 多栏目/整库打包成 ZIP(`bundle.json + sections/<id>.zip`),可设口令加密 |
|
|
156
|
+
| `nx-sk transfer bundle-import [--file ZIP] [--on-conflict skip\|copy\|overwrite] [--dry-run]` | 整库包解密导入;缺省「合并」只增补不覆盖 |
|
|
157
|
+
| `nx-sk transfer import-new [--file ZIP] [--dry-run]` | **零配置**:自动识别单栏目包 / 整库包,按包内结构新建(id 冲突加序号) |
|
|
140
158
|
| `nx-sk setting get \| set` | 全局设置 |
|
|
141
159
|
| `nx-sk skill list \| install \| get` | 把内置 skill 装给 agent / 给外部 agent 取上下文 |
|
|
142
160
|
|
|
@@ -222,53 +240,44 @@ nx-sk setting set --set 'navGroups=[]' # 清空 = 全部栏目恢复平铺
|
|
|
222
240
|
|
|
223
241
|
分组只是**显示配置**(存在 `settings.navGroups`):不动栏目与条目的数据,删掉栏目时头部自动略过。
|
|
224
242
|
|
|
225
|
-
|
|
243
|
+
整体导出(落到本机目录的**人可读副本**,凭据默认含明文):
|
|
226
244
|
|
|
227
245
|
```bash
|
|
228
246
|
nx-sk export run --format both --out D:/备份/nx-sk
|
|
229
247
|
nx-sk export list
|
|
230
248
|
```
|
|
231
249
|
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
```bash
|
|
235
|
-
nx-sk backup export --password '你的备份口令' # 写成 ~/nx-sk/backup/*.nxskbak
|
|
236
|
-
nx-sk backup export --password '你的备份口令' --with-files # 连附件原件一起打包
|
|
237
|
-
nx-sk backup import --file ~/nx-sk/backup/xxx.nxskbak --password '你的备份口令' --dry-run # 先看合并计划
|
|
238
|
-
nx-sk backup import --file ~/nx-sk/backup/xxx.nxskbak --password '你的备份口令' # 缺省「合并」导入
|
|
239
|
-
nx-sk backup import --file ~/nx-sk/backup/xxx.nxskbak --password '你的备份口令' --replace # 才是整份替换
|
|
240
|
-
# 口令也可用环境变量 NX_SK_BACKUP_PASSWORD 提供
|
|
241
|
-
```
|
|
250
|
+
### 全局导出 / 导入(多栏目)
|
|
242
251
|
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
导入完成给出完整报告。`--replace` 才整份替换(灾难恢复 / 跨机器迁移用)。
|
|
246
|
-
两种方式都先自动留安全快照。
|
|
252
|
+
**项目里只有一个导出导入模块,没有独立的「备份」模块。** 导出的包就是唯一回退手段:
|
|
253
|
+
删栏目 / 条目 / 附件之前先导一份。
|
|
247
254
|
|
|
248
|
-
|
|
249
|
-
|
|
255
|
+
「导出导入」页顶部:栏目横向排成 chips,悬浮或点击展开条目可逐条勾选;
|
|
256
|
+
顶部「整库」按钮 = 所有栏目全选。选好内容 → 输两次口令(可留空)→「导出 ZIP」浏览器直接下载。
|
|
257
|
+
把 ZIP 拖进右侧框即预览(栏目、条目新增/冲突、附件新增/复用),选冲突策略后确认。
|
|
250
258
|
|
|
251
|
-
|
|
259
|
+
口令用**对称加密(AES-256-GCM)**:实现是「内部数据先加密,再套一层合法 ZIP」,
|
|
260
|
+
所以外层始终能正常打开(只看到一个 `payload.enc`),不会报「文件夹无效」;
|
|
261
|
+
缺口令时明确提示要口令,不会误报成坏包。
|
|
252
262
|
|
|
253
|
-
|
|
254
|
-
把 ZIP 拖进下方框即预览(栏目、条目新增/冲突、附件新增/复用、凭据),选冲突策略后确认。
|
|
255
|
-
全局包 `bundle.json + sections/<id>.zip`,与单栏目包同构;导入时逐栏目复用同一套逻辑。
|
|
263
|
+
整库包结构 `bundle.json + sections/<id>.zip`,与单栏目包同构;导入时逐栏目复用同一套逻辑。
|
|
256
264
|
|
|
257
265
|
```bash
|
|
258
266
|
nx-sk transfer bundle-export --sections job,projects --out all.zip
|
|
259
267
|
nx-sk transfer bundle-import --file all.zip --dry-run
|
|
260
268
|
nx-sk transfer bundle-import --file all.zip --on-conflict copy
|
|
269
|
+
nx-sk transfer import-new --file all.zip # 零配置:自动识别单栏目包 / 整库包
|
|
261
270
|
```
|
|
262
271
|
|
|
263
|
-
|
|
264
272
|
### 只搬一个栏目:ZIP 导出 / 导入
|
|
265
273
|
|
|
266
|
-
|
|
274
|
+
栏目也可以单独打包(含条目、栏目定义、实际引用到的附件):
|
|
267
275
|
|
|
268
276
|
```bash
|
|
269
277
|
nx-sk transfer export deploy --out D:/tmp/deploy.zip # 含凭据明文
|
|
270
|
-
nx-sk transfer export deploy --out D:/tmp/share.zip
|
|
278
|
+
nx-sk transfer export deploy --passphrase 口令 --out D:/tmp/share.zip # 设口令再给别人
|
|
271
279
|
nx-sk transfer export deploy --entries "k8s-deployment,compose" --out D:/tmp/part.zip # 只导选中条目
|
|
280
|
+
nx-sk transfer bundle-export --sections job,deploy --passphrase 口令 --out D:/tmp/enc.zip # 整包加密
|
|
272
281
|
nx-sk transfer import deploy2 --file D:/tmp/deploy.zip --dry-run # 先预览:新增/冲突/补字段/附件
|
|
273
282
|
nx-sk transfer import deploy2 --file D:/tmp/deploy.zip # 栏目不存在就新建
|
|
274
283
|
nx-sk transfer import deploy2 --file D:/tmp/deploy.zip --on-conflict copy # 另存
|
|
@@ -276,7 +285,7 @@ nx-sk transfer import deploy2 --file D:/tmp/deploy.zip --on-conflict overwrite #
|
|
|
276
285
|
nx-sk transfer import-new --file D:/tmp/deploy.zip # 零配置:按包内结构新建
|
|
277
286
|
```
|
|
278
287
|
|
|
279
|
-
面板里是栏目页右上角「更多」:导出 ZIP
|
|
288
|
+
面板里是栏目页右上角「更多」:导出 ZIP、导入 ZIP…;
|
|
280
289
|
条目列表可勾选(表头全选/清空),选中后导出**只带这些条目及其附件**。
|
|
281
290
|
把 zip **拖到栏目页任意位置**会导入当前栏目;拖到「栏目」tab 则**零配置新建一个栏目**。
|
|
282
291
|
预览会显示来源栏目、目标栏目、条目冲突、将补的字段、附件新增/复用与凭据是否打码,
|
|
@@ -291,10 +300,10 @@ nx-sk transfer import-new --file D:/tmp/deploy.zip # 零配置:
|
|
|
291
300
|
| 数据目录 | `~/nx-sk/`,可用 `NX_SK_HOME` 覆盖 |
|
|
292
301
|
| 状态文件 | `~/nx-sk/store.json`,可用 `NX_SK_STORE` 覆盖;**原子写**(临时文件 + rename) |
|
|
293
302
|
| 密文密钥 | 环境变量 `NX_SK_PASSPHRASE`(scrypt 派生 + 盐)优先,否则 `~/nx-sk/.vaultkey`(32 字节随机,0600) |
|
|
294
|
-
|
|
|
303
|
+
| 迁移 | 破坏性删除**不留本地快照**:先 `nx-sk transfer export` 导一份,导出的包就是唯一回退手段 |
|
|
295
304
|
| 导出 | `~/nx-sk/export/`,默认不含凭据明文 |
|
|
296
305
|
| 附件 | `~/nx-sk/files/` 存二进制原件,元数据在 `store.json` 的 `files`(sha256 去重;条目 `file` 字段存附件 id 即建立链接;`file restore` 可逆) |
|
|
297
|
-
|
|
|
306
|
+
| 加密导出 | `transfer` 的 `--passphrase`:内部数据经 scrypt(口令)+AES-256-GCM 加密成 `payload.enc`,再套一层**合法 ZIP**(外层能正常打开,不会报「文件夹无效」);口令不落盘,导入时缺口令明确提示 |
|
|
298
307
|
| 服务绑定 | 默认 `127.0.0.1`,不暴露到局域网;写操作校验 `Origin` |
|
|
299
308
|
| 数据损坏 | `store.json` 解析失败会先被改名为 `.corrupt-<时间戳>.json`,不会静默覆盖 |
|
|
300
309
|
|
package/assets/nx-sk/SKILL.md
CHANGED
|
@@ -70,7 +70,7 @@ nx-sk key set OPENAI_KEY sk-xxxxxxxx # 写入(存在即覆盖,返回 cre
|
|
|
70
70
|
nx-sk key get OPENAI_KEY # 取值:默认就是原文
|
|
71
71
|
nx-sk key get OPENAI_KEY --mask # 投屏/截图时才打码
|
|
72
72
|
nx-sk key list # 所有键(默认原文)
|
|
73
|
-
nx-sk key remove OPENAI_KEY #
|
|
73
|
+
nx-sk key remove OPENAI_KEY # 删除,不可撤销 → 先 nx-sk transfer export
|
|
74
74
|
```
|
|
75
75
|
|
|
76
76
|
`key *` 是 `entry *` 的**语法糖**(同模块、同一批 service 函数),缺省作用在标了 `kv` 的栏目上
|
|
@@ -124,7 +124,6 @@ nx-sk setting set --set 'navGroups=[]' # 清空 = 全部栏目恢复平铺
|
|
|
124
124
|
| `00-design` | 想搞清楚「为什么这么设计」、分层与依赖方向、错误码与 `{status}` 的分工时 |
|
|
125
125
|
| `10-sections-entries` | **动手填/改信息时必读**——栏目与条目的完整命令、AI 补全个人信息的推荐流程、字段字典怎么扩、读命令与写命令的参数陷阱 |
|
|
126
126
|
| `20-secrets` | 存/读大模型 key、密文机制、密钥来源(环境变量 vs 本机密钥文件)、什么时候会解不开 |
|
|
127
|
-
| `30-export-backup` | 整体导出、只导出某个栏目、快照与回滚、明文密钥的取舍 |
|
|
128
127
|
| `40-files` | **传/换简历头像成绩单等二进制原件时读**——附件库台账、导入(默认移动)与还原、`file` 字段怎么和条目建立链接、附件中心为什么由栏目驱动、看不到上传字段怎么办 |
|
|
129
128
|
| `50-deploy` | **存/取 Dockerfile、docker-compose、k8s 清单时读**——一份配置一个条目的字段约定、`kind` 取值、复用流程(找 → 取正文 → 替换变量 → 落盘)、为什么明文密钥不写在这里 |
|
|
130
129
|
| `60-transfer` | **搬栏目 / 分享栏目 / 局部迁移时读**——单栏目与全局 bundle 的 ZIP 结构、`transfer export/import/bundle-*` 怎么用、三种冲突策略、附件按 sha256 复用与引用重映射、凭据打码的边界 |
|
|
@@ -151,7 +150,8 @@ nx-sk skill get nx-sk --json # 四元 JSON:{skillName, ref,
|
|
|
151
150
|
|
|
152
151
|
## 硬约束(动手前先记住)
|
|
153
152
|
|
|
154
|
-
- 所有写命令都有 `--dry-run
|
|
153
|
+
- 所有写命令都有 `--dry-run`,先试运行再落盘;**删除不可撤销** —— 删栏目/条目/附件前先
|
|
154
|
+
`nx-sk transfer export`(或面板「导出导入」→ 选中范围 → 导出),这是唯一的回退手段。
|
|
155
155
|
- 密文字段**落盘是密文,读出来默认是原文** —— 本机单人工具,自己看的东西不先拦一道。
|
|
156
156
|
要打码形态(投屏 / 截图)显式加 `--mask`。
|
|
157
157
|
- 把打码后的值(含 `******`)当新值写回去会被拒绝 —— 那通常意味着「不修改」,什么都不传即可。
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# 00 · 设计思想
|
|
2
2
|
|
|
3
|
-
> 骨架级文档:回答「为什么这么设计」。**不放场景级 SOP** —— 怎么调命令看 `10-sections-entries` / `20-secrets` / `
|
|
3
|
+
> 骨架级文档:回答「为什么这么设计」。**不放场景级 SOP** —— 怎么调命令看 `10-sections-entries` / `20-secrets` / `60-transfer`。
|
|
4
4
|
|
|
5
5
|
## 一、驱动关系
|
|
6
6
|
|
|
@@ -109,7 +109,7 @@ CLI 侧这四个取值分别渲染成不同的可读提示,退出码只有「
|
|
|
109
109
|
| 把「读命令的过滤 flag」照抄写命令的 `default` | 参数层无条件注入默认值 → 「不传 = 全部」的分支**永远走不到**,不报错、不崩,只是永远返回半个结果 | 写命令的 flag 可带 `default`;读命令的过滤 flag 一律不带 |
|
|
110
110
|
| 路由只按声明顺序匹配 | `GET /api/sections/templates` 被 `GET /api/sections/:ref` 遮蔽,命中哪条取决于 actions 数组顺序 | 路由按「字面量段优先」排序,让声明顺序无关(`runtime/spec.js` 的 `compareRoutes`) |
|
|
111
111
|
| 把打码值当成新值写回去 | 密钥字段被写成 `sk-1****cdef` 这串掩码,**原凭据永久丢失** | 写入前比对掩码:相等就当成「未改动」保留原密文;含 `******` 的值直接拒绝 |
|
|
112
|
-
|
|
|
112
|
+
| 删条目/栏目没有留退路 | 误删即不可逆;这是用户的**唯一一份**个人信息 | 破坏性写操作一律先 `transfer export` 导出(面板「导出导入」同理),导出的包就是唯一回退手段 —— 不另设本地备份模块 |
|
|
113
113
|
| store.json 解析失败就当空库继续 | 下一次写入把用户数据永久覆盖 | 先把损坏文件改名为 `store.json.corrupt-<时间>.json`,再降级返回空结构 |
|
|
114
114
|
| 前端 import 了 `node:*` 或 `core/` | Vite 把 Node 侧代码打进浏览器包,dev 模式白屏 | lint 的 `files` 同时覆盖 `view.jsx`,禁掉 `node:*` / `core` / `runtime` / `service.js` |
|
|
115
115
|
| 新增模块忘了往 eslint 互依禁列里补一行 | **静默**:新模块变成「谁都可以依赖」,且规则随模块数增加持续衰减 | 在禁列旁留注释说明它是**逐模块枚举**的、漏补是静默的;加模块前先读 `eslint.config.js` |
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
|
|
7
7
|
```
|
|
8
8
|
store.json(~/nx-sk/store.json 或 $NX_SK_STORE)
|
|
9
|
-
├─ settings
|
|
9
|
+
├─ settings 全局配置(可读 JSON/Markdown 导出见 60-transfer)
|
|
10
10
|
├─ sections[] 栏目:id / title / order / template / titleField / groups[] / fields[]
|
|
11
11
|
└─ entries[] 条目:id(e_xxx) / section / title / tags[] / values{字段key: 值} / 时间戳
|
|
12
12
|
```
|
|
@@ -40,7 +40,7 @@ store.json(~/nx-sk/store.json 或 $NX_SK_STORE)
|
|
|
40
40
|
- **`sync` 按模板补齐字段**:栏目字段字典是**持久化数据**——实例化时深拷贝模板字段,此后独立演化,
|
|
41
41
|
所以模板**后来**新增的分组与字段不会自动出现在老栏目里(典型症状:求职没显示附件上传框)。
|
|
42
42
|
`section sync <id>` 把模板有、本栏目还缺的分组与字段补进来(**只增不改不删**,保持模板顺序;
|
|
43
|
-
已有字段含改过的 label / hint / fill
|
|
43
|
+
已有字段含改过的 label / hint / fill 原样保留),带 `--dry-run`;面板侧是「栏目 → 编辑 → 同步模板字段」。
|
|
44
44
|
- **它还会报告「漂移」**:同名字段在「栏目 vs 模板」之间哪些属性不同(`changedFields`,含 diff 属性名)。
|
|
45
45
|
这是**只提示、不动手**——因为定义归栏目,你可能就是刻意改过。要换成模板的新定义得手动来:
|
|
46
46
|
`section update <id> --fields '<整份字段数组>'`,或先 `--remove-field` 再 `--add-field`(`--add-field` 遇到同名会报冲突)。
|
|
@@ -29,7 +29,7 @@ nx-sk key set OPENAI_KEY sk-xxxxxxxx # 写入:不存在则新建,
|
|
|
29
29
|
nx-sk key get OPENAI_KEY # 取值:默认就是原文
|
|
30
30
|
nx-sk key get OPENAI_KEY --mask # 投屏/截图时才打码
|
|
31
31
|
nx-sk key list # 列出所有键(默认原文)
|
|
32
|
-
nx-sk key remove OPENAI_KEY #
|
|
32
|
+
nx-sk key remove OPENAI_KEY # 删除(不可撤销,删前先 transfer export)
|
|
33
33
|
```
|
|
34
34
|
|
|
35
35
|
| 命令 | 接口 | 说明 |
|
|
@@ -37,7 +37,7 @@ nx-sk key remove OPENAI_KEY # 删除(删前自动留快照
|
|
|
37
37
|
| `key list [--mask]` | `GET /api/keys[?mask=1]` | 全部键,按写入顺序 |
|
|
38
38
|
| `key get <名称> [--mask]` | `GET /api/keys/:name[?mask=1]` | 单个键的值 |
|
|
39
39
|
| `key set <名称> <值>` | `PUT /api/keys/:name` | **SET 语义**:存在即覆盖 |
|
|
40
|
-
| `key remove <名称>` | `DELETE /api/keys/:name` | 不存在报 `NOT_FOUND
|
|
40
|
+
| `key remove <名称>` | `DELETE /api/keys/:name` | 不存在报 `NOT_FOUND`;不可撤销 |
|
|
41
41
|
|
|
42
42
|
四条都接受 `--section <栏目>`(HTTP 侧是 `?section=` 或 body 里的 `section`),
|
|
43
43
|
缺省作用在**标了 `kv` 的那个栏目**上。
|
|
@@ -45,7 +45,7 @@ nx-sk key remove OPENAI_KEY # 删除(删前自动留快照
|
|
|
45
45
|
### 与 `entry` 的关系(重要)
|
|
46
46
|
|
|
47
47
|
`key *` **不是第二套实现**,是 `entry *` 的语法糖:它把「哪个栏目 + 哪个值字段」定好,
|
|
48
|
-
再调同一批 service
|
|
48
|
+
再调同一批 service 函数。所以加解密、`--dry-run` 只有一份实现,不可能分叉。
|
|
49
49
|
|
|
50
50
|
等价关系:
|
|
51
51
|
|
|
@@ -89,7 +89,7 @@ nx-sk section list # 列表里 KV 栏目带 [KV 表]
|
|
|
89
89
|
- **值以 `-` 开头时**用 `--value=<值>`(`key set K --value=--x`)——`--x` 会被当成 flag。
|
|
90
90
|
显式 flag 覆盖位置参数,两种写法等价。
|
|
91
91
|
- **重复写同一个值不会重写密文**:返回 `{ status: 'skipped', unchanged: true }`。
|
|
92
|
-
每次加密都用新 IV
|
|
92
|
+
每次加密都用新 IV,无脑重写只会产生一堆无意义的新密文。
|
|
93
93
|
|
|
94
94
|
### 多行值:走 stdin 或 `@文件`(真实事故)
|
|
95
95
|
|
|
@@ -200,7 +200,7 @@ nx-sk key set OPENAI_KEY sk-xxxxxxxx
|
|
|
200
200
|
| 症状 | 原因 | 处理 |
|
|
201
201
|
| --- | --- | --- |
|
|
202
202
|
| `BLOCKED 当前密钥与写入时的密钥不一致` | 换了口令,或删了 `.vaultkey` | 改回原来的来源(旧口令 / 恢复密钥文件) |
|
|
203
|
-
| `BLOCKED 解密失败:密文已损坏,或密钥不正确` | store.json 被手工改过 / GCM 认证失败 |
|
|
203
|
+
| `BLOCKED 解密失败:密文已损坏,或密钥不正确` | store.json 被手工改过 / GCM 认证失败 | 从之前 `transfer export` 出来的包重新导入 |
|
|
204
204
|
| `.vaultkey` 不见了 | 被系统清理或误删 | 有口令模式的盐文件 `.vaultsalt` 与口令还能救;否则密文不可逆 |
|
|
205
205
|
|
|
206
206
|
**密文不可逆是特性,不是缺陷** —— 代价是「忘记密钥 = 数据丢失」,
|
|
@@ -33,7 +33,7 @@
|
|
|
33
33
|
|
|
34
34
|
- `file list --section <栏目>` 只看「被某栏目引用」的附件;`--section none` 只看**还没有任何条目引用**的。
|
|
35
35
|
- `file content` 是浏览器链接:内联预览,加 `--download` 强制下载;CLI 侧打印物理路径与链接。
|
|
36
|
-
- 所有写命令都支持 `--dry-run`;`file remove`
|
|
36
|
+
- 所有写命令都支持 `--dry-run`;`file remove` 连物理原件一起删**不可撤销**——
|
|
37
37
|
只想「拿出原件、库里留着」应该用 `file restore --copy`。
|
|
38
38
|
|
|
39
39
|
## 三、导入:默认移动,`--copy` 才复制
|
|
@@ -87,7 +87,7 @@ nx-sk file restore f_xxxx --to "D:\资料" # Web 上传件没有原始
|
|
|
87
87
|
| `restore --copy` | 只**导出副本**到目标目录,库中原件与台账**都保留** |
|
|
88
88
|
|
|
89
89
|
- Web 上传进来的附件没有「原始位置」,还原时必须给 `--to`。
|
|
90
|
-
-
|
|
90
|
+
- 还原/删除都不留本地备份;删之前先 `transfer export` 导一份——见 `60-transfer`。
|
|
91
91
|
|
|
92
92
|
## 六、Web 面板
|
|
93
93
|
|
|
@@ -1,22 +1,30 @@
|
|
|
1
1
|
# 栏目导出 / 导入(ZIP)
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
3
|
+
**什么时候读**:要把栏目连同附件搬到另一台机器、发给同事、或做局部备份/迁移时。
|
|
4
|
+
它只处理**选中的栏目与条目**(不碰没选中的东西),零配置即可在另一台机器导入。
|
|
5
5
|
|
|
6
|
-
|
|
6
|
+
项目里**只有一个导出导入模块**:没有独立的「备份」模块了。回退手段就是先导出再删。
|
|
7
7
|
|
|
8
|
-
|
|
8
|
+
## 面板操作(一个面板:选 → 设口令 → 倒出去 / 倒回来)
|
|
9
9
|
|
|
10
|
-
|
|
11
|
-
- **导出 ZIP(打码)**:凭据换成 `******`,适合发给别人。
|
|
12
|
-
- **导入 ZIP…**:选文件;或者把 zip **拖到栏目页任意位置**。
|
|
13
|
-
- **导出 JSON / Markdown**:老的整体导出(写到 `~/nx-sk/export`,不是浏览器下载)。
|
|
10
|
+
「导出导入」页就是这一个面板,左右两栏:
|
|
14
11
|
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
12
|
+
- **左栏 · 选内容**:栏目**横向排**成 chips(名称 + 已选/总数;当前栏目深色描边,已选加粗),
|
|
13
|
+
指针移上去会自动把该栏目的条目**纵向展开**在下面(点击/键盘同样可以,不只靠悬浮);
|
|
14
|
+
顶部两个预设按钮:**「整库」= 所有栏目全选**、「清空」。
|
|
15
|
+
- **头部常驻主操作**:已选计数 + 口令(输两次)+「导出 ZIP」。条目再多也不用滚动去找按钮。
|
|
16
|
+
- **右栏 · 解密导入**:拖入或点击选择 ZIP → 先预览 → 再应用。
|
|
18
17
|
|
|
19
|
-
|
|
18
|
+
**没有「按栏目 / 整库」两个模式**:栏目的全选就是整库导出,「整库」按钮只是全选的快捷方式,
|
|
19
|
+
产物与逐栏勾选完全同一个包(整库时是 `bundle.json + sections/<id>.zip`,逐栏时是单个 zip)。
|
|
20
|
+
|
|
21
|
+
栏目页右上角「更多」里是**同一个栏目**的快捷入口:导出该栏目 ZIP(支持条目勾选)、
|
|
22
|
+
导入 ZIP、把 zip 拖到栏目页任意位置也能导入。
|
|
23
|
+
|
|
24
|
+
**精确导出**:条目列表左侧有复选框,表头可全选/清空。选中若干条后只带这些条目
|
|
25
|
+
**和它们引用的附件**(不会带无关文件)。不选 = 导出整个栏目。
|
|
26
|
+
|
|
27
|
+
导入会先弹**预览**:来源/目标栏目、条目新增与冲突、将补哪些字段、附件新增/复用。
|
|
20
28
|
选冲突策略后才写盘:
|
|
21
29
|
|
|
22
30
|
| 策略 | 行为 |
|
|
@@ -25,45 +33,77 @@
|
|
|
25
33
|
| `copy` | 另存为新条目,标题加「(导入)」;标题是 CLI 定位键,所以必须唯一 |
|
|
26
34
|
| `overwrite` | 用包里的值覆盖现有条目的字段(没带的字段保持本地值) |
|
|
27
35
|
|
|
36
|
+
冲突预览**逐条列出**:同 id / 同标题分别标明,`copy` 策略会直接显示将产生的新标题。
|
|
37
|
+
|
|
28
38
|
## CLI
|
|
29
39
|
|
|
30
40
|
```bash
|
|
31
|
-
#
|
|
41
|
+
# 导出(可设口令;不设 = 明文包)
|
|
32
42
|
nx-sk transfer export deploy --out ~/deploy.zip
|
|
33
|
-
nx-sk transfer export deploy --out ~/deploy-share.zip
|
|
34
|
-
#
|
|
35
|
-
nx-sk transfer export deploy --entries "k8s-deployment,docker-compose" --out ~/part.zip
|
|
43
|
+
nx-sk transfer export deploy --passphrase 口令 --out ~/deploy-share.zip
|
|
44
|
+
nx-sk transfer export deploy --no-files --out ~/meta.zip # 只带元数据,不带附件原件
|
|
36
45
|
|
|
37
|
-
#
|
|
38
|
-
nx-sk transfer
|
|
46
|
+
# 整库/多栏目(逗号分隔;全选 = 不传 --sections)
|
|
47
|
+
nx-sk transfer bundle-export --out ~/all.zip
|
|
48
|
+
nx-sk transfer bundle-export --sections deploy,secrets --passphrase 口令 --out ~/two.zip
|
|
39
49
|
|
|
40
|
-
#
|
|
50
|
+
# 导入
|
|
51
|
+
nx-sk transfer import deploy2 --file ~/deploy.zip --dry-run # 先预览,不写盘
|
|
41
52
|
nx-sk transfer import deploy2 --file ~/deploy.zip --on-conflict skip
|
|
42
|
-
|
|
43
|
-
# 零配置:不指定栏目,按包内结构新建(id 冲突自动加序号)
|
|
44
|
-
nx-sk transfer import-new --file ~/deploy.zip
|
|
53
|
+
nx-sk transfer import-new --file ~/all.zip # 零配置:自动识别单栏目/整库包
|
|
45
54
|
```
|
|
46
55
|
|
|
56
|
+
`--entries` 精确指定条目(id 或标题,逗号分隔):`nx-sk transfer export deploy --entries "k8s-deployment,docker-compose" --out ~/part.zip`
|
|
57
|
+
|
|
47
58
|
`--json` 可用于预览结果(`section.fieldsToAdd`、`entries.conflicts`、`files.new/reused`)。
|
|
48
59
|
|
|
49
60
|
## 包里有什么
|
|
50
61
|
|
|
62
|
+
单栏目:
|
|
63
|
+
|
|
51
64
|
```
|
|
52
65
|
manifest.json 栏目定义(groups/fields/kv/titleField)+ 条目 + 附件清单(sha256)
|
|
53
66
|
README.txt 给人看的说明
|
|
54
|
-
attachments/<id><ext>
|
|
67
|
+
attachments/<id><ext> 附件原件(--no-files 时没有这些)
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
整库:
|
|
71
|
+
|
|
72
|
+
```
|
|
73
|
+
bundle.json 包清单:包含哪些栏目、整体口令说明
|
|
74
|
+
sections/<id>.zip 每个栏目一个上面的单栏目包(逐栏目复用同一套逻辑)
|
|
55
75
|
```
|
|
56
76
|
|
|
57
|
-
-
|
|
77
|
+
- 只打包**条目实际引用**的附件:`file` 字段的 id + `md` 正文里的 ``。
|
|
58
78
|
- 导入时附件按 **sha256 去重**:本机已有同一份文件就复用,不会存两份。
|
|
59
79
|
- 条目里的旧 file id 会**自动重映射**到新 id(`file` 字段数组与 md 图片引用都改)。
|
|
80
|
+
- `import-new` 会看包结构自动判断是单栏目包还是整库包,不用手动挑命令。
|
|
81
|
+
|
|
82
|
+
## 口令加密(transfer 层)
|
|
83
|
+
|
|
84
|
+
- 实现是**「内部数据先加密,再套合法 ZIP」**:
|
|
85
|
+
|
|
86
|
+
```
|
|
87
|
+
README.txt 说明这个包是什么(明文,不含数据)
|
|
88
|
+
payload.enc 真正的包内容,AES-256-GCM 加密
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
这样外层始终是合法 ZIP —— 系统「压缩(zipped)文件夹」能正常打开,只看到一个看不懂的
|
|
92
|
+
`payload.enc`,不会弹「文件夹无效」;判定加密与否只需看条目名,不用嗅探 magic。
|
|
93
|
+
- 信封 = magic + salt(16) + iv(12) + tag(16) + 密文;密钥由口令经 scrypt 派生
|
|
94
|
+
(与密钥库同一套参数),每次导出盐与 IV 都重新随机 —— 同一个包加密两次密文不同。
|
|
95
|
+
- 缺口令时导入会明确说「这个包已加密,请输入导出时设置的口令」,**不会**误报成「包坏了」。
|
|
96
|
+
- CLI:`--passphrase`,也认环境变量 `NX_SK_TRANSFER_PASSPHRASE`
|
|
97
|
+
(命令行上的口令会出现在进程列表里,脚本/CI 里用 env 更稳)。
|
|
98
|
+
- 旧格式(0.11.x 把整个文件做成密文、magic `NXSKENC1`)**仍可导入**,只是不再产出。
|
|
60
99
|
|
|
61
100
|
## 凭据怎么处理
|
|
62
101
|
|
|
63
|
-
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
102
|
+
包里**恒为明文**,保护只靠口令 —— 没有「打码 / 明文」二选(0.11.0 之前的 `--no-secrets` 已移除)。
|
|
103
|
+
要发给别人的包就设口令;不设口令的包自己收好,别当分享件用。
|
|
104
|
+
|
|
105
|
+
读取侧仍兼容旧打码包:遇到 `******` 会跳过该字段并计数,不会把打码串当新密码存进去。
|
|
106
|
+
导入后敏感字段用**本机 vaultKey** 重新加密落盘,与手填的字段没有区别。
|
|
67
107
|
|
|
68
108
|
## 限制与坑
|
|
69
109
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "nx-sk",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.12.0",
|
|
4
4
|
"description": "nx-sk · 本机个人资源管理器(server-cli-web 骨架):栏目化存储个人信息与大模型密钥,CLI 与 Web 同源,敏感字段密文落盘。",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -44,7 +44,11 @@
|
|
|
44
44
|
},
|
|
45
45
|
"dependencies": {
|
|
46
46
|
"react": "^18.3.1",
|
|
47
|
-
"react-dom": "^18.3.1"
|
|
47
|
+
"react-dom": "^18.3.1",
|
|
48
|
+
"react-markdown": "^10.1.0",
|
|
49
|
+
"remark-breaks": "^4.0.0",
|
|
50
|
+
"remark-gfm": "^4.0.1",
|
|
51
|
+
"unist-util-visit": "^5.1.0"
|
|
48
52
|
},
|
|
49
53
|
"devDependencies": {
|
|
50
54
|
"@vitejs/plugin-react": "^4.3.4",
|
package/src/core/attachments.js
CHANGED
|
@@ -331,7 +331,7 @@ export async function relocateFile(store, rec, { category, name }) {
|
|
|
331
331
|
const cat = category || rec.category;
|
|
332
332
|
// 改名时**不带扩展名**就把原扩展名接回去——否则物理文件丢掉 .jpg/.pdf,
|
|
333
333
|
// 一切按扩展名认类型的地方(内联预览 / Content-Type / 系统默认程序打开 /
|
|
334
|
-
//
|
|
334
|
+
// 历史迁移 / 导入还原出来的文件)都会哑掉,而台账里的 ext 字段还骗人说没丢。
|
|
335
335
|
let wanted = name || rec.name;
|
|
336
336
|
const originalExt = rec.ext || extname(rec.name).toLowerCase();
|
|
337
337
|
if (name && originalExt && !extname(wanted)) wanted += originalExt;
|
package/src/core/paths.js
CHANGED
|
@@ -55,8 +55,10 @@ export function filesDir() {
|
|
|
55
55
|
return join(appHome(), 'files');
|
|
56
56
|
}
|
|
57
57
|
|
|
58
|
-
|
|
59
|
-
|
|
58
|
+
/** 迁移时的原始文件副本目录。**不是备份目录**——本地备份功能已移除,
|
|
59
|
+
* 这里只保留「store 版本升级前留一份原始文件」那一次性动作。 */
|
|
60
|
+
export function migrationDir() {
|
|
61
|
+
return join(appHome(), 'migrate');
|
|
60
62
|
}
|
|
61
63
|
|
|
62
64
|
export function vaultKeyFile() {
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
// 带口令的 ZIP 信封:把一个(可能是几百 MB 的)导出包整体加密。
|
|
2
|
+
//
|
|
3
|
+
// 为什么不复用备份的 JSON 格式:备份是 JSON(base64 撑大约 1/3),而导出包本身就是二进制
|
|
4
|
+
// ZIP,套一层 JSON 只会白涨体积。这里用定长头部 + 密文的二进制信封,体积几乎不变。
|
|
5
|
+
//
|
|
6
|
+
// 布局(全部大端无关,直接拼接):
|
|
7
|
+
// magic 'NXSKENC1' 8B 固定,用于「这是不是加密包」的判定
|
|
8
|
+
// salt 16B scrypt 盐(每次导出都重新随机)
|
|
9
|
+
// iv 12B GCM nonce
|
|
10
|
+
// tag 16B GCM 认证标签(口令错 / 被改动都会在这里失败)
|
|
11
|
+
// ciphertext N B 明文 ZIP
|
|
12
|
+
//
|
|
13
|
+
// 口令**不落盘、不进文件名**;派生沿用 core/crypto 的 scrypt(与密钥库同一套参数)。
|
|
14
|
+
import { createCipheriv, createDecipheriv, randomBytes } from 'node:crypto';
|
|
15
|
+
import { deriveKey } from './crypto.js';
|
|
16
|
+
import { badInput, blocked } from './errors.js';
|
|
17
|
+
|
|
18
|
+
export const ENC_MAGIC = Buffer.from('NXSKENC1', 'ascii');
|
|
19
|
+
const SALT_BYTES = 16;
|
|
20
|
+
const IV_BYTES = 12;
|
|
21
|
+
const TAG_BYTES = 16;
|
|
22
|
+
const HEADER_BYTES = ENC_MAGIC.length + SALT_BYTES + IV_BYTES + TAG_BYTES;
|
|
23
|
+
|
|
24
|
+
const asBuffer = (v) => (Buffer.isBuffer(v) ? v : Buffer.from(v || []));
|
|
25
|
+
|
|
26
|
+
/** 只看头 8 字节:加密包与明文 ZIP 因此可以走同一个导入入口。 */
|
|
27
|
+
export function isEncryptedPackage(buf) {
|
|
28
|
+
const b = asBuffer(buf);
|
|
29
|
+
return b.length >= HEADER_BYTES && b.subarray(0, ENC_MAGIC.length).equals(ENC_MAGIC);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function sealPackage(payload, passphrase) {
|
|
33
|
+
const pw = String(passphrase ?? '');
|
|
34
|
+
if (!pw) throw badInput('加密导出需要口令');
|
|
35
|
+
const salt = randomBytes(SALT_BYTES);
|
|
36
|
+
const iv = randomBytes(IV_BYTES);
|
|
37
|
+
const cipher = createCipheriv('aes-256-gcm', deriveKey(pw, salt), iv);
|
|
38
|
+
const ct = Buffer.concat([cipher.update(asBuffer(payload)), cipher.final()]);
|
|
39
|
+
return Buffer.concat([ENC_MAGIC, salt, iv, cipher.getAuthTag(), ct]);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* 解开信封:**明文包原样返回**,所以调用方不必先判断类型。
|
|
44
|
+
* 口令错与「包没加密却给了口令」都按「能用就用」处理,只有真正解不开才报错。
|
|
45
|
+
*/
|
|
46
|
+
export function openPackage(buf, passphrase) {
|
|
47
|
+
const b = asBuffer(buf);
|
|
48
|
+
if (!isEncryptedPackage(b)) return b;
|
|
49
|
+
|
|
50
|
+
const pw = String(passphrase ?? '');
|
|
51
|
+
if (!pw) throw blocked('这个包已加密,请输入导出时设置的口令');
|
|
52
|
+
|
|
53
|
+
const salt = b.subarray(ENC_MAGIC.length, ENC_MAGIC.length + SALT_BYTES);
|
|
54
|
+
const iv = b.subarray(ENC_MAGIC.length + SALT_BYTES, ENC_MAGIC.length + SALT_BYTES + IV_BYTES);
|
|
55
|
+
const tag = b.subarray(ENC_MAGIC.length + SALT_BYTES + IV_BYTES, HEADER_BYTES);
|
|
56
|
+
const ct = b.subarray(HEADER_BYTES);
|
|
57
|
+
|
|
58
|
+
let out;
|
|
59
|
+
try {
|
|
60
|
+
const decipher = createDecipheriv('aes-256-gcm', deriveKey(pw, salt), iv);
|
|
61
|
+
decipher.setAuthTag(tag);
|
|
62
|
+
out = Buffer.concat([decipher.update(ct), decipher.final()]);
|
|
63
|
+
} catch {
|
|
64
|
+
// GCM 无法区分「口令错」与「数据被改」,两种都说清楚,别让人以为文件坏了。
|
|
65
|
+
throw blocked('口令不对,或这个文件已损坏');
|
|
66
|
+
}
|
|
67
|
+
// 认证通过但内容不是 ZIP:说明包不是 nx-sk 导出的(或版本不兼容),别当数据问题处理。
|
|
68
|
+
if (out.length < 4 || out[0] !== 0x50 || out[1] !== 0x4b) {
|
|
69
|
+
throw badInput('解密成功但内容不是 ZIP —— 这个文件可能不是 nx-sk 导出包');
|
|
70
|
+
}
|
|
71
|
+
return out;
|
|
72
|
+
}
|