nx-sk 0.23.0 → 0.23.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/CHANGELOG.md +27 -86
- package/README.md +0 -14
- package/assets/nx-sk/references/10-sections-entries.md +10 -76
- package/assets/nx-sk/references/60-transfer.md +4 -10
- package/package.json +1 -12
- package/src/core/attachments.js +1 -34
- package/src/core/paths.js +0 -11
- package/src/core/render.js +0 -13
- package/src/core/store.js +3 -51
- package/src/core/vault.js +2 -18
- package/src/modules/entries/index.js +0 -147
- package/src/modules/entries/service.js +0 -517
- package/src/modules/entries/view.jsx +6 -136
- package/src/modules/sections/index.js +0 -65
- package/src/modules/sections/service.js +1 -290
- package/src/modules/settings/service.js +7 -90
- package/src/modules/skill/index.js +2 -1
- package/src/modules/skill/service.js +38 -1
- package/src/modules/system/service.js +2 -15
- package/src/modules/transfer/service.js +4 -138
- package/src/web/frontend/App.jsx +234 -200
- package/src/web/frontend/activeView.js +0 -56
- package/src/web/frontend/components/sourcePanel.jsx +48 -6
- package/src/web/frontend/main.jsx +0 -4
- package/src/web/frontend/styles.css +8 -361
- package/src/web/public/assets/{files-BoJqTGyZ.js → files-Cr-oMOeX.js} +1 -1
- package/src/web/public/assets/index-BiDzPPaq.css +1 -0
- package/src/web/public/assets/index-DPjpWSaw.js +41 -0
- package/src/web/public/assets/{view-B04bzsph.js → view-7t9TLUNM.js} +1 -1
- package/src/web/public/assets/{view-xwEy1P7C.js → view-C8iiWh9-.js} +1 -1
- package/src/web/public/assets/view-CpkUF3Xr.js +1 -0
- package/src/web/public/assets/{view-DNT9of0L.js → view-DYFqX9mq.js} +1 -1
- package/src/web/public/assets/view-qaSrOBcK.js +42 -0
- package/src/web/public/index.html +2 -2
- package/src/modules/entries/aiPending.js +0 -303
- package/src/modules/entries/blockPanel.jsx +0 -239
- package/src/modules/entries/boardView.jsx +0 -136
- package/src/modules/entries/entriesTable.jsx +0 -119
- package/src/shared/blocks.js +0 -400
- package/src/shared/navTree.js +0 -252
- package/src/shared/views.js +0 -221
- package/src/web/frontend/blocks/registry.jsx +0 -304
- package/src/web/frontend/components/aiBar.jsx +0 -192
- package/src/web/frontend/components/blockDnd.jsx +0 -69
- package/src/web/frontend/components/sidebar.jsx +0 -354
- package/src/web/frontend/components/viewChips.jsx +0 -43
- package/src/web/frontend/panels.jsx +0 -75
- package/src/web/frontend/workspace.js +0 -253
- package/src/web/public/assets/index-DDcA8xZc.js +0 -64
- package/src/web/public/assets/index-FDvbnX_f.css +0 -1
- package/src/web/public/assets/view-CvJr_elT.js +0 -42
- package/src/web/public/assets/view-tNIpJ3PQ.js +0 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,79 +2,45 @@
|
|
|
2
2
|
|
|
3
3
|
本文件格式参考 [Keep a Changelog](https://keepachangelog.com/),中文正文。
|
|
4
4
|
|
|
5
|
+
## 0.23.1 (2026-11-05)
|
|
6
|
+
|
|
7
|
+
### Added
|
|
8
|
+
|
|
9
|
+
- **`skill install --all`:一次装齐 `assets/` 下全部 skill,且直接覆盖**。范围取目录
|
|
10
|
+
扫描(事实源 `assets/<x>/SKILL.md`),不依赖分组表;覆盖隐式生效,不用再补 `--force`。
|
|
11
|
+
与 `[name]` 互斥;`--mode symlink` / `--dry-run` 对整批同样适用。
|
|
12
|
+
|
|
5
13
|
## 0.23.0 (2026-10-31)
|
|
6
14
|
|
|
7
|
-
|
|
15
|
+
**栏目结构在面板里可自己搭了:分组能增删改序,字段能改名 / 换组 / 换类型 / 换位置。**
|
|
8
16
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
于是「这个栏目长什么样」这件事面板说了不算。这次把四个分组操作与两个字段操作做成
|
|
13
|
-
action(面板与 CLI 同一份),面板「编辑栏目」里直接可改。
|
|
17
|
+
过去面板只能「加字段」,连建一个分组都要回 CLI 或换模板——于是「这个栏目长什么样」
|
|
18
|
+
这件事实际上只有命令行能控制,面板就不是控制面板。这次把结构的四个分组操作与两个字段操作
|
|
19
|
+
做成 action(面板与 CLI 同一份),面板「编辑栏目」里直接可改。
|
|
14
20
|
|
|
15
21
|
### Added
|
|
16
22
|
|
|
17
|
-
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
`suggest` 收 `--from @proposal.json`(AI 给出的**最终正文**)与 `--prompt`(指令留痕),
|
|
33
|
-
只落一条 pending;`apply` 才写 store;`reject` 只删文件。
|
|
34
|
-
- **pending 落 `~/nx-sk/pending/`(运行态)**:不进 `store.json`、不进导出包、不进迁移副本
|
|
35
|
-
——里面对 secret 块是明文,落进数据面就等于写进备份与导出面。删目录即清空。
|
|
36
|
-
- diff 是**字符级**的:`same / del / ins` 三段,面板上红删绿增;容器块的改动由子块表达
|
|
37
|
-
(不把父块一起报成改动);secret 块只报「变了」,明文不经面板接口。
|
|
38
|
-
- **冲突检测**:pending 记每块应用前的 `baseHash`(服务端算),块被改过 → `CONFLICT`,
|
|
39
|
-
一个字都不写;`--force` 才覆盖。没有差异的提议返回 `noop`,不留空的待审。
|
|
40
|
-
- secret 块的差异按**明文**比对(store 里是密文、提议里是明文),否则一个没动的密钥
|
|
41
|
-
会被报成「密文块已变更」。
|
|
23
|
+
- **分组骨架**:`section update --add-group / --rename-group / --move-group / --remove-group`。
|
|
24
|
+
改名可以**连 id 一起换**(组里字段的 `group` 引用跟着改,不会整组从表单里消失)。
|
|
25
|
+
三条规矩全是**拦下来**而不是悄悄做:id 重复报 `CONFLICT`(不覆盖同名分组)、
|
|
26
|
+
**非空分组不给删**(报 `BLOCKED` 并列出是哪几个字段)、内置「原始资料」分组不给删。
|
|
27
|
+
- **字段级改动**:`section update --set-field`(标签 / 类型 / 分组 / 提示 / 必填策略)
|
|
28
|
+
与 `--move-field`(在自己分组内上下挪)。**key 不可改**——值按 key 存在条目里,
|
|
29
|
+
改 key 等于让已有值读不出来;要换就新增字段再搬值。
|
|
30
|
+
- **面板**:「编辑栏目」弹窗里两块——「分组」(id / 名字就地改、↑↓ 换序、空组才能删)
|
|
31
|
+
与「字段字典」(每行可改标签 / 类型 / 分组 / 提示 / 必填策略,组内 ↑↓,删除)。
|
|
32
|
+
一次改动一次落盘(每次改动都要过后端校验,出错时知道是哪一步),改完立刻回读。
|
|
33
|
+
清单类字段(`entrylist` / `table`)的**类型置灰**:列是字段自己的结构,
|
|
34
|
+
就地换类型等于换一个字段——要建就在「新增字段」里连列一起建。
|
|
35
|
+
- 每次改动都把**新旧值**打进输出(`字段 phone:分组 contact → reach`),
|
|
36
|
+
不再只说「已更新栏目 job:fields」——那种输出改错了没法回溯。
|
|
37
|
+
|
|
42
38
|
- **表格 / 清单:行与列都能拖**(Notion 那种手感):拖**表头**搬列、拖行尾的 ⠿ 搬行,
|
|
43
39
|
松手前一条**插入线**告诉你落在哪(只高亮整行说不清「会插到哪」)。排序算法单独一个
|
|
44
40
|
`reorderAt`(插入位语义,有单测)——差一的表现是「拖到第二行结果跑到第三行」。
|
|
45
41
|
整行不做 `draggable`:那会跟单元格里选文本打架,把手是唯一的拖拽入口。
|
|
46
42
|
↑↓ / ←→ 保留但**悬停才浮出**,行内操作键统一收成 16px 小图标键(密集表里的按钮
|
|
47
43
|
不该抢内容的注意力);entrylist 那颗「正常大小的删除按钮」也换成了小图标键。
|
|
48
|
-
- **降级运行的主动提示**:store 由更新版本写入时,`bootstrap` 与 `health`(CLI 输出与 JSON)
|
|
49
|
-
会明确说明「磁盘版本 > 当前版本,写入会被拒」——不等用户改一次被 BLOCKED 才知道。
|
|
50
|
-
- **栏目结构在面板里可自己搭**:分组能增删改序,字段能改名 / 换组 / 换类型 / 换位置。
|
|
51
|
-
- **分组骨架**:`section update --add-group / --rename-group / --move-group / --remove-group`。
|
|
52
|
-
改名可以**连 id 一起换**(组里字段的 `group` 引用跟着改,不会整组从表单里消失)。
|
|
53
|
-
三条规矩全是**拦下来**而不是悄悄做:id 重复报 `CONFLICT`(不覆盖同名分组)、
|
|
54
|
-
**非空分组不给删**(报 `BLOCKED` 并列出是哪几个字段)、内置「原始资料」分组不给删。
|
|
55
|
-
- **字段级改动**:`section update --set-field`(标签 / 类型 / 分组 / 提示 / 必填策略)
|
|
56
|
-
与 `--move-field`(在自己分组内上下挪)。**key 不可改**——值按 key 存在条目里,
|
|
57
|
-
改 key 等于让已有值读不出来;要换就新增字段再搬值。
|
|
58
|
-
- **面板**:「编辑栏目」弹窗里两块——「分组」(id / 名字就地改、↑↓ 换序、空组才能删)
|
|
59
|
-
与「字段字典」(每行可改标签 / 类型 / 分组 / 提示 / 必填策略,组内 ↑↓,删除)。
|
|
60
|
-
一次改动一次落盘(每次改动都要过后端校验,出错时知道是哪一步),改完立刻回读。
|
|
61
|
-
清单类字段(`entrylist` / `table`)的**类型置灰**:列是字段自己的结构,
|
|
62
|
-
就地换类型等于换一个字段——要建就在「新增字段」里连列一起建。
|
|
63
|
-
- 每次改动都把**新旧值**打进输出(`字段 phone:分组 contact → reach`),
|
|
64
|
-
不再只说「已更新栏目 job:fields」——那种输出改错了没法回溯。
|
|
65
|
-
|
|
66
|
-
### Fixed
|
|
67
|
-
|
|
68
|
-
- **导入会把打码的密钥块加密存盘**(与 0.19 字段路径同类缺陷):导入包里的 `******`
|
|
69
|
-
占位会被当成真密钥加密落盘。现在值清空待重填,并在预览里计数与警告;
|
|
70
|
-
导出预览的掩码统计同步扩到块。
|
|
71
|
-
- **表格/看板视图把详情列挤成一条竖缝**(S2 起的老缺陷,列多的栏目才暴露):
|
|
72
|
-
表格列的 flex 基准用的是内容宽度(75 列 ≈ 7300px),行溢出时按基准比例收缩,
|
|
73
|
-
详情列被抽走全部溢出量。现在表格列 basis 为 0(「拿剩余空间」),
|
|
74
|
-
横向滚动交给表格与看板自己的 overflow。
|
|
75
|
-
- **布局里的空面板组自愈**:拖动/关闭面板可能留下空 tabset,FlexLayout 会把它原样
|
|
76
|
-
存盘、恢复时渲染成一块白板。现在存盘与恢复两端都剪掉空面板组——
|
|
77
|
-
已经存坏的布局**刷新即自愈**,不用点「重置布局」。
|
|
78
44
|
|
|
79
45
|
## 0.22.0 (2026-10-31)
|
|
80
46
|
|
|
@@ -126,31 +92,6 @@ action(面板与 CLI 同一份),面板「编辑栏目」里直接可改。
|
|
|
126
92
|
表头写「已删字段」、单元格「—」(不炸、也不静默消失)。
|
|
127
93
|
- 面板:栏目页顶部视图 chips + 表格视图(横向投影,密文列在表格里一律打码)。
|
|
128
94
|
看板**渲染**随下一版(配置与 action 已可用)。
|
|
129
|
-
- **Notion 式工作区壳**(S3):
|
|
130
|
-
- 顶栏只留全局动作(品牌 / 存储 / 打码 / +面板 / 撤销重做 / 设置 / 刷新);
|
|
131
|
-
**栏目 tab 从顶栏移除**——栏目只从目录树进,两处入口必然分叉。
|
|
132
|
-
- 目录树:栏目 + 分组 + 条目子节点;**拖拽重排与嵌套**(第一次拖动把「从旧导航配置
|
|
133
|
-
推断出的树」固化成显式 `settings.navTree`,之前始终只读推断、不写回);
|
|
134
|
-
隐藏的栏目收在树底(一键恢复,数据不动)。
|
|
135
|
-
- 布局 = FlexLayout 模型:面板可拖到任意一边拆分、弹出成浮窗;布局存
|
|
136
|
-
localStorage(`nx-sk-ui.workspace`),跨会话恢复;撤销/重做按「整段拖拽 = 一步」。
|
|
137
|
-
- 深链升级为两段:`#/栏目` 开该栏目的数据库面板,`#/栏目/条目` 再定位到条目;
|
|
138
|
-
栏目 / 条目缺失时明确提示是哪一级没了。
|
|
139
|
-
- 正文块支持拖拽排序(块头把手);数据库面板即工作区里的一块面板,可与其他面板并列。
|
|
140
|
-
- **块级 AI(提议 → 审 → 应用/拒绝)**(S4):
|
|
141
|
-
- CLI:`entry ai list | suggest | apply | reject`(HTTP 同名路由)。
|
|
142
|
-
`suggest` 收 `--from @proposal.json`(AI 给出的**最终正文**)与 `--prompt`(指令留痕),
|
|
143
|
-
只落一条 pending;`apply` 才写 store;`reject` 只删文件。
|
|
144
|
-
- **nx-sk 不自带大模型**:提议由调用方(agent 或外部模型)产出后交进来。这样
|
|
145
|
-
「agent 走 CLI」与「人在面板上审同一条 pending」是同一套语义,不需要第二个 AI 入口。
|
|
146
|
-
- **pending 落 `~/nx-sk/pending/`(运行态)**:不进 `store.json`、不进导出包、不进迁移副本
|
|
147
|
-
——里面对 secret 块是明文,落进数据面就等于写进备份与导出面。删目录即清空。
|
|
148
|
-
- diff 是**字符级**的:`same / del / ins` 三段,面板上红删绿增;容器块的改动由子块表达
|
|
149
|
-
(不把父块一起报成改动);secret 块只报「变了」,明文不经面板接口。
|
|
150
|
-
- **冲突检测**:pending 记每块应用前的 `baseHash`(服务端算),块被改过 → `CONFLICT`,
|
|
151
|
-
一个字都不写;`--force` 才覆盖。没有差异的提议返回 `noop`,不留空的待审。
|
|
152
|
-
- secret 块的差异按**明文**比对(store 里是密文、提议里是明文),否则一个没动的密钥
|
|
153
|
-
会被报成「密文块已变更」。
|
|
154
95
|
|
|
155
96
|
### Changed
|
|
156
97
|
|
package/README.md
CHANGED
|
@@ -172,20 +172,6 @@ CLI 与 API 同源,skill 驱动 agent**。落到具体功能上:
|
|
|
172
172
|
| `nx-sk section update <id> --set-field / --move-field` | 改**字段**:改标签 / 类型 / 分组 / 提示 / 必填策略,组内上下挪(key 不可改)。面板「编辑栏目」同源 |
|
|
173
173
|
| `nx-sk entry list \| get \| add \| update \| remove` | 条目 CRUD(PATCH 语义) |
|
|
174
174
|
| `nx-sk entry fields [--section job]` | 字段字典 —— 填之前先看这个,别猜字段名 |
|
|
175
|
-
| `nx-sk entry block add <ref> --type <段落\\|标题\\|图文\\|表格\\|清单\\|附件\\|密钥> [--data @x.json] [--after <块id>]` | 追加**块正文**(`entry.body`,可选键);详细页下半部分同源 |
|
|
176
|
-
| `nx-sk entry block update <ref> <块id> [--data @x.json]` | 改块(PATCH 语义);密钥块在打码态下写回会被拒(星号不能存成密钥) |
|
|
177
|
-
| `nx-sk entry block move <ref> <块id> --to <index>` | 移动块(拖拽落点 = 一次 move) |
|
|
178
|
-
| `nx-sk entry block remove \\| remove-many <ref> <块id…>` | 删块;批量时找不到的跳过并报告 |
|
|
179
|
-
| `nx-sk entry body <ref> [--mask]` | 读块正文(人读渲染;`--mask` 只作用于密钥块) |
|
|
180
|
-
| `nx-sk entry ai suggest <ref> --prompt "改短一点" --from @proposal.json [--block <块id>]` | 生成一条**AI 提议**:只落 pending,不写 store。`--from` 是 AI 给出的最终正文(nx-sk 不自带大模型) |
|
|
181
|
-
| `nx-sk entry ai list <ref>` | 看待审提议(新的在前)。pending 是运行态,不进 store、不进导出包 |
|
|
182
|
-
| `nx-sk entry ai apply <pendingId> [--block <块id>] [--force]` | 应用提议(**唯一会写 store 的一步**);块被改过 → `CONFLICT`,`--force` 才覆盖 |
|
|
183
|
-
| `nx-sk entry ai reject <pendingId>` | 拒绝提议:删掉 pending 文件,数据一个字不动 |
|
|
184
|
-
| `nx-sk section view list <id>` | 列视图配置;**没配过时给缺省推断**(表格/精简/看板,标 provisional,不落盘) |
|
|
185
|
-
| `nx-sk section view add <id> --type table\\|board\\|kv [--title 名] [--columns 姓名,状态] [--groupby 字段] [--sort 字段:desc]` | 新增视图(第一次 add 把当前生效的推断视图一起固化,chips 不会凭空消失) |
|
|
186
|
-
| `nx-sk section view update <id> <viewId> [--title] [--columns] [--sort] [--set col.字段.width=120]` | 改视图;列宽拖拽结束 = 一次 update |
|
|
187
|
-
| `nx-sk section view remove <id> <viewId>` | 删视图;删光回到缺省推断态(`defaultView` 一并清掉) |
|
|
188
|
-
| `nx-sk section view set-default <id> <viewId>` | 设缺省视图(写 `section.defaultView` —— 是数据,不是面板设置) |
|
|
189
175
|
| `nx-sk file list \| get \| add \| update \| remove` | 附件 CRUD(原件在 `~/nx-sk/files/`,同内容去重) |
|
|
190
176
|
| `nx-sk file add --path <文件> [--copy] [--category C]` | 导入附件:**默认移动原件**,`--copy` 仅复制 |
|
|
191
177
|
| `nx-sk file list [--section <栏目\|none>] [--q 关键字]` | 列附件:按「哪些栏目在引用」筛选(`none` = 未引用);返回栏目汇总与存储目录 |
|
|
@@ -105,64 +105,7 @@ nx-sk entry get 张三 --json
|
|
|
105
105
|
- 身份证号、手机号、紧急联系人电话这类字段,写之前跟用户确认一次来源。
|
|
106
106
|
- 每次批量写之前先 `--dry-run`。
|
|
107
107
|
|
|
108
|
-
##
|
|
109
|
-
|
|
110
|
-
条目除了结构化字段还有**块正文**(`entry.body`,可选键):段落 / 标题 / 图文 / 表格 / 清单 /
|
|
111
|
-
附件 / 密钥七种叶块,加内嵌栏目(`section`)与分栏(`column`)两种容器块。
|
|
112
|
-
|
|
113
|
-
```bash
|
|
114
|
-
# 建块 / 看块
|
|
115
|
-
nx-sk entry block add <ref> --type paragraph --data '{"text":"第一段"}'
|
|
116
|
-
nx-sk entry block list <ref> # secret 块默认原文,--mask 打码
|
|
117
|
-
nx-sk entry block update <ref> <块id> --data @块.json
|
|
118
|
-
nx-sk entry block move <ref> <块id> --to 0
|
|
119
|
-
|
|
120
|
-
# AI 提议:**AI 永不直接改数据**
|
|
121
|
-
# ① 你(agent / 外部模型)产出提议正文 = 该条目的最终块数组
|
|
122
|
-
# ② suggest 只落一条 pending(~/nx-sk/pending/,运行态,不进 store / 导出包)
|
|
123
|
-
# ③ 人或你在面板/CLI 上 apply(才写 store)或 reject(只删文件)
|
|
124
|
-
nx-sk entry ai suggest <ref> --prompt "把自我介绍改短" --from @proposal.json
|
|
125
|
-
nx-sk entry ai list <ref>
|
|
126
|
-
nx-sk entry ai apply <pendingId> # 块被改过 → CONFLICT,--force 才覆盖
|
|
127
|
-
nx-sk entry ai reject <pendingId>
|
|
128
|
-
```
|
|
129
|
-
|
|
130
|
-
**纪律**:
|
|
131
|
-
|
|
132
|
-
- proposal.json 是 `{"blocks":[…]}` 或直接块数组;给**最终正文**,不是补丁。
|
|
133
|
-
- 建议先 `nx-sk entry block list <ref> --json` 拿现正文再改,块 id 保持不变
|
|
134
|
-
(同 id 不同内容 = 一次「修改」,出现新 id = 「新增」,缺了的 id = 「删除」)。
|
|
135
|
-
- 密钥块在提议里是明文——pending 落在本机运行态目录(0600),这是有意设计;
|
|
136
|
-
但**不要**把 pending 文件内容贴进对话或日志。
|
|
137
|
-
- 应用前想留底就先 `transfer export`(apply 写 store,与手改同一条边界)。
|
|
138
|
-
|
|
139
|
-
## 六、数据库视图(section.views / section view)
|
|
140
|
-
|
|
141
|
-
视图是**同一批条目的多种投影**:表格(列 = 字段投影)、精简(两列)、看板(按 select 字段分列)、
|
|
142
|
-
KV 栏目的键值面。配置存在 `section.views` / `section.defaultView`(可选键)。
|
|
143
|
-
|
|
144
|
-
```bash
|
|
145
|
-
nx-sk section view list <id> # 没配过时给缺省推断(provisional: true,不落盘)
|
|
146
|
-
nx-sk section view add <id> --type board --title 按状态 --groupby status
|
|
147
|
-
nx-sk section view update <id> <viewId> --set col.状态.width=180 --sort 状态:desc
|
|
148
|
-
nx-sk section view set-default <id> <viewId>
|
|
149
|
-
nx-sk section view remove <id> <viewId>
|
|
150
|
-
```
|
|
151
|
-
|
|
152
|
-
**语义**:
|
|
153
|
-
|
|
154
|
-
- **缺省视图是推断的、不落盘**:非 KV 栏目给「表格 / 精简 / 看板」,KV 栏目给「KV」。
|
|
155
|
-
第一次改视图才固化(把当时生效的那套推断视图一起写进去——只写用户新建的那条,
|
|
156
|
-
其余 chips 会凭空消失)。`views: []` 与「没配过」行为一致,都走推断。
|
|
157
|
-
- 投影只存 `key / width / hidden`;标签与类型回查字段字典。引用的字段被删 → 列还在,
|
|
158
|
-
表头写「已删字段」、单元格「—」(不炸、也不静默消失)。
|
|
159
|
-
- **看板的拖卡 = 写操作**:等于 `entry update --set <groupby字段>=<列值>`;
|
|
160
|
-
空值与候选外的值落「未分组」列——展示可以,**不是拖拽落点**(拖进去等于写一个不存在的值)。
|
|
161
|
-
- **没有筛选 DSL**(`--filter` 明确不做):存一个不生效的配置比没有更糟。
|
|
162
|
-
- 视图随导出包往返;导入只在「新建栏目」或「目标没配过视图」时应用——
|
|
163
|
-
目标已有视图说明用户自己配过,不静默覆盖(预览里给警告)。
|
|
164
|
-
|
|
165
|
-
## 七、扩字段字典(不写代码)
|
|
108
|
+
## 五、扩字段字典(不写代码)
|
|
166
109
|
|
|
167
110
|
**最省事的做法:直接写。** 字典里没有的键会自动登记(键名就是它本身),类型按值推断
|
|
168
111
|
(`是/否` → bool,数组 → tags,其余 → text)。
|
|
@@ -325,24 +268,15 @@ nx-sk entry get 张三
|
|
|
325
268
|
> ② 两者都**不要**出现在「还缺什么」的清单里;③ 用户说「这个不用填」时,
|
|
326
269
|
> 用 `--fill <字段>=avoid` 记下来,而不是靠下一轮上下文记住。
|
|
327
270
|
|
|
328
|
-
##
|
|
329
|
-
|
|
330
|
-
`nx-sk serve`
|
|
331
|
-
|
|
332
|
-
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
-
|
|
336
|
-
弹出成浮窗;布局存 localStorage(`nx-sk-ui.workspace`),跨会话恢复,顶栏可撤销/重做。
|
|
337
|
-
- **两段深链**:`#/栏目` 开该栏目的数据库面板,`#/栏目/条目` 再定位到条目;
|
|
338
|
-
栏目 / 条目缺失时明确提示是哪一级没了。
|
|
339
|
-
- 栏目页(数据库面板)顶部是**视图 chips**(表格 / 精简 / 看板 / KV,见 §六);
|
|
340
|
-
左侧条目列表带完整度,右侧按分组渲染的字段表单。
|
|
341
|
-
- 密文字段默认显示原文;顶栏「打码」等价于 `--mask`(选择持久化在 localStorage);
|
|
342
|
-
- **栏目结构自己能搭**:栏目面板里点某个栏目的「编辑」,弹窗里分组可新增 / 改名(含换 id)/
|
|
343
|
-
上下挪 / 删(空的才让删),字段可改标签 / 类型 / 分组 / 提示 / 必填策略、组内上下挪、删除
|
|
344
|
-
—— 一次改动一次落盘,与 CLI 的 `--add-group` 等是同一批 action(见 §七「栏目结构」)。
|
|
271
|
+
## 六、Web 面板
|
|
272
|
+
|
|
273
|
+
`nx-sk serve` → 每个栏目一个 tab,共用同一个条目视图:
|
|
274
|
+
|
|
275
|
+
- 左侧条目列表(带完整度),右侧按分组渲染的字段表单;
|
|
276
|
+
- **「栏目」页的「编辑」弹窗能改整个结构**:分组可新增 / 改名(含换 id)/ 上下挪 / 删(空的才让删),
|
|
277
|
+
字段可改标签、类型、分组、提示、必填策略、组内上下挪、删除 —— 一次改动一次落盘;
|
|
278
|
+
- 密文字段默认显示原文;「打码显示(投屏/截图时用)」勾选等价于 `--mask`(选择持久化在 localStorage);
|
|
345
279
|
- 保存是 **PATCH**:只有被你改动过的字段才会发出去(面板会显示「N 处改动」);
|
|
346
|
-
-
|
|
280
|
+
- 每个页面底部会派生显示该模块的 CLI 等价命令(可点击复制)。
|
|
347
281
|
|
|
348
282
|
面板上没有 CLI 做不到的事——两边共享同一套 action 声明。
|
|
@@ -74,14 +74,9 @@ bundle.json 包清单:包含哪些栏目、整体口令说明
|
|
|
74
74
|
sections/<id>.zip 每个栏目一个上面的单栏目包(逐栏目复用同一套逻辑)
|
|
75
75
|
```
|
|
76
76
|
|
|
77
|
-
- 只打包**条目实际引用**的附件:`file` 字段的 id + `md` 正文里的 `
|
|
77
|
+
- 只打包**条目实际引用**的附件:`file` 字段的 id + `md` 正文里的 ``。
|
|
78
78
|
- 导入时附件按 **sha256 去重**:本机已有同一份文件就复用,不会存两份。
|
|
79
|
-
- 条目里的旧 file id 会**自动重映射**到新 id(`file`
|
|
80
|
-
- 包版本(`manifest.formatVersion`):**2** 起,包里多 `entries[].body`(块正文)与
|
|
81
|
-
`section.views`(视图配置)。老版本读到 2 号包会明确报「版本过新」,
|
|
82
|
-
不会把这两个键静默丢掉;manifest 里**没有这个键**(0.20 及更早的包)视为 1 号包——
|
|
83
|
-
无正文/无视图,照常导入。
|
|
84
|
-
- 导入时视图配置只在「新建栏目」或「目标没配过视图」时应用——目标已有自己的视图就不覆盖(预览里会提示)。
|
|
79
|
+
- 条目里的旧 file id 会**自动重映射**到新 id(`file` 字段数组与 md 图片引用都改)。
|
|
85
80
|
- `import-new` 会看包结构自动判断是单栏目包还是整库包,不用手动挑命令。
|
|
86
81
|
|
|
87
82
|
## 口令加密(transfer 层)
|
|
@@ -107,9 +102,8 @@ sections/<id>.zip 每个栏目一个上面的单栏目包(逐栏目复
|
|
|
107
102
|
包里**恒为明文**,保护只靠口令 —— 没有「打码 / 明文」二选(0.11.0 之前的 `--no-secrets` 已移除)。
|
|
108
103
|
要发给别人的包就设口令;不设口令的包自己收好,别当分享件用。
|
|
109
104
|
|
|
110
|
-
读取侧仍兼容旧打码包:遇到 `******`
|
|
111
|
-
|
|
112
|
-
导入后敏感字段与密钥块用**本机 vaultKey** 重新加密落盘,与手填的没有区别。
|
|
105
|
+
读取侧仍兼容旧打码包:遇到 `******` 会跳过该字段并计数,不会把打码串当新密码存进去。
|
|
106
|
+
导入后敏感字段用**本机 vaultKey** 重新加密落盘,与手填的字段没有区别。
|
|
113
107
|
|
|
114
108
|
## 限制与坑
|
|
115
109
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "nx-sk",
|
|
3
|
-
"version": "0.23.
|
|
3
|
+
"version": "0.23.1",
|
|
4
4
|
"description": "nx-sk · 本机个人资源管理器(server-cli-web 骨架):栏目化存储个人信息与大模型密钥,CLI 与 Web 同源,敏感字段密文落盘。",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -37,23 +37,12 @@
|
|
|
37
37
|
"dev:serve": "node bin/nx-sk.mjs serve --no-open",
|
|
38
38
|
"build": "vite build && node scripts/link-local.mjs --auto",
|
|
39
39
|
"start": "pnpm run build && node bin/nx-sk.mjs serve",
|
|
40
|
-
"proto:dev": "vite --config plan/block-refactor/prototype/vite.config.js",
|
|
41
|
-
"proto:build": "vite build --config plan/block-refactor/prototype/vite.config.js",
|
|
42
|
-
"loop:on": "node scripts/rp-loop.mjs on",
|
|
43
|
-
"loop:off": "node scripts/rp-loop.mjs off",
|
|
44
|
-
"loop:status": "node scripts/rp-loop.mjs status",
|
|
45
|
-
"loop:log": "node scripts/rp-loop.mjs log",
|
|
46
|
-
"loop:cancel": "node scripts/rp-loop.mjs cancel",
|
|
47
40
|
"lint": "eslint .",
|
|
48
41
|
"test:unit": "node --test",
|
|
49
42
|
"test:smoke": "node tests/smoke.mjs",
|
|
50
43
|
"test": "pnpm run lint && pnpm run build && pnpm run test:smoke && pnpm run test:unit"
|
|
51
44
|
},
|
|
52
45
|
"dependencies": {
|
|
53
|
-
"@dnd-kit/core": "^6.3.1",
|
|
54
|
-
"@dnd-kit/sortable": "^10.0.0",
|
|
55
|
-
"@dnd-kit/utilities": "^3.2.2",
|
|
56
|
-
"flexlayout-react": "0.11.1",
|
|
57
46
|
"react": "^18.3.1",
|
|
58
47
|
"react-dom": "^18.3.1",
|
|
59
48
|
"react-markdown": "^10.1.0",
|
package/src/core/attachments.js
CHANGED
|
@@ -136,24 +136,6 @@ export function normalizeFileRecord(raw) {
|
|
|
136
136
|
* 0.8.0 之前 `--set` 不拆逗号),所以字符串统一按分隔符拆开。
|
|
137
137
|
* 去空、去重、保序:同一个附件在一个字段里出现两次没有意义。
|
|
138
138
|
*/
|
|
139
|
-
/** 块正文里的附件引用:file 块的 id 列表 + md 块正文里的图片 id。 */
|
|
140
|
-
function blockFileRefs(entry) {
|
|
141
|
-
const out = [];
|
|
142
|
-
const walk = (blocks) => {
|
|
143
|
-
for (const b of blocks || []) {
|
|
144
|
-
if (b.type === 'file' && Array.isArray(b.files)) {
|
|
145
|
-
for (const id of fileIdsOf(b.files)) out.push(id);
|
|
146
|
-
}
|
|
147
|
-
if (b.type === 'md' && typeof b.text === 'string') {
|
|
148
|
-
for (const id of mdImageIds(b.text)) out.push(id);
|
|
149
|
-
}
|
|
150
|
-
if (b.children) walk(b.children);
|
|
151
|
-
}
|
|
152
|
-
};
|
|
153
|
-
walk(entry.body);
|
|
154
|
-
return out;
|
|
155
|
-
}
|
|
156
|
-
|
|
157
139
|
export function fileIdsOf(value) {
|
|
158
140
|
const raw = value === null || value === undefined
|
|
159
141
|
? []
|
|
@@ -191,11 +173,7 @@ export function findFileRecord(store, ref) {
|
|
|
191
173
|
* 图片是**内嵌在 markdown 里**的,和 file 字段一样是正式引用。
|
|
192
174
|
* 3. `source` 字段 —— 原始资料袋里 kind='file' 的 item(袋是溯源的锚点:
|
|
193
175
|
* 字段值是从这份原件整理出来的,原件被删,溯源就成了空话)。
|
|
194
|
-
*
|
|
195
|
-
* 块是条目正文的一部分,正式程度与字段相同;漏扫它 = 「只在块里挂过」的附件
|
|
196
|
-
* 会被 cleanup 判成未引用然后删掉(真实事故的块版)。
|
|
197
|
-
* 块引用出一条(fieldKey 记块 id);其余同一条(条目, 字段)只出一条。
|
|
198
|
-
* 附件中心据此显示「被哪些栏目 / 条目 / 字段 / 块用到」。
|
|
176
|
+
* 同一个(条目, 字段)只出一条。附件中心据此显示「被哪些栏目 / 条目 / 字段用到」。
|
|
199
177
|
*/
|
|
200
178
|
export function fileReferences(store, rec) {
|
|
201
179
|
if (!rec) return [];
|
|
@@ -243,17 +221,6 @@ export function fileReferences(store, rec) {
|
|
|
243
221
|
.map((x) => x.fileId);
|
|
244
222
|
if (ids.length && hits(ids)) push(f);
|
|
245
223
|
}
|
|
246
|
-
// ④ 块正文(v4):file 块 + md 块。命中就出一条,fieldKey 记块 id、fieldLabel 记块类型。
|
|
247
|
-
const blockHits = blockFileRefs(e);
|
|
248
|
-
if (blockHits.length && hits(blockHits)) {
|
|
249
|
-
const which = (e.body || []).some((b) => b.type === 'file'
|
|
250
|
-
&& fileIdsOf(b.files).some((ref) => hits([ref]))) ? 'file 块' : 'md 块';
|
|
251
|
-
out.push({
|
|
252
|
-
sectionId: sec.id, sectionTitle: sec.title,
|
|
253
|
-
entryId: e.id, entryTitle: e.title,
|
|
254
|
-
fieldKey: 'body', fieldLabel: which,
|
|
255
|
-
});
|
|
256
|
-
}
|
|
257
224
|
}
|
|
258
225
|
}
|
|
259
226
|
return out;
|
package/src/core/paths.js
CHANGED
|
@@ -61,17 +61,6 @@ export function migrationDir() {
|
|
|
61
61
|
return join(appHome(), 'migrate');
|
|
62
62
|
}
|
|
63
63
|
|
|
64
|
-
/**
|
|
65
|
-
* 块级 AI 的 pending 目录:`~/nx-sk/pending/`(一个 pending 一个文件)。
|
|
66
|
-
*
|
|
67
|
-
* **运行态,不是数据**:不进 store.json、不进导出包、不进迁移副本。
|
|
68
|
-
* 理由见设计 §14.4——pending 里可能有明文密钥(AI 提议的 secret 块),
|
|
69
|
-
* 落进 store 就等于把它写进了备份与导出面。删掉这个目录即清空全部待审提议。
|
|
70
|
-
*/
|
|
71
|
-
export function pendingDir() {
|
|
72
|
-
return join(appHome(), 'pending');
|
|
73
|
-
}
|
|
74
|
-
|
|
75
64
|
export function vaultKeyFile() {
|
|
76
65
|
return join(appHome(), '.vaultkey');
|
|
77
66
|
}
|
package/src/core/render.js
CHANGED
|
@@ -108,15 +108,6 @@ export function displaySensitive(raw, { mask = false, decrypt } = {}) {
|
|
|
108
108
|
}
|
|
109
109
|
}
|
|
110
110
|
|
|
111
|
-
/** 块正文(v4)的展示形态:secret 块的值走与敏感字段**同一条** displaySensitive 边界。 */
|
|
112
|
-
export function displayBlocks(blocks, { mask = false, decrypt } = {}) {
|
|
113
|
-
return (blocks || []).map((b) => {
|
|
114
|
-
if (b.type === 'secret') return { ...b, value: displaySensitive(b.value, { mask, decrypt }) };
|
|
115
|
-
if (Array.isArray(b.children)) return { ...b, children: displayBlocks(b.children, { mask, decrypt }) };
|
|
116
|
-
return b;
|
|
117
|
-
});
|
|
118
|
-
}
|
|
119
|
-
|
|
120
111
|
/**
|
|
121
112
|
* 条目的**唯一序列化出口**(面板、`entry list`、`section dump` 共用一份)。
|
|
122
113
|
* 两个出口各写一遍序列化,迟早会分叉成「CLI 看得到、面板看不到」。
|
|
@@ -156,10 +147,6 @@ export function serializeEntry(section, entry, { mask = false, decrypt } = {}) {
|
|
|
156
147
|
tags: entry.tags || [],
|
|
157
148
|
createdAt: entry.createdAt,
|
|
158
149
|
updatedAt: entry.updatedAt,
|
|
159
|
-
// 块正文(v4):可选键——没有块就不带这个键(别让老条目凭空多出一个空数组)
|
|
160
|
-
...(Array.isArray(entry.body) && entry.body.length
|
|
161
|
-
? { body: displayBlocks(entry.body, { mask, decrypt }) }
|
|
162
|
-
: {}),
|
|
163
150
|
completeness: {
|
|
164
151
|
filled: c.filled,
|
|
165
152
|
total: c.total,
|
package/src/core/store.js
CHANGED
|
@@ -6,22 +6,12 @@ import { displayPath, migrationDir, storeFile } from './paths.js';
|
|
|
6
6
|
import { ensureDir, pathExists, writeAtomic } from './fsx.js';
|
|
7
7
|
import { ensureSourceBuiltin, instantiateTemplate, seedEntries, seedSections, syncSectionGroups } from './fields.js';
|
|
8
8
|
import { normalizeFileRecord } from './attachments.js';
|
|
9
|
-
// 块(entry.body)的归一在 shared/blocks.js —— 与 shared/table.js 同一条理由:
|
|
10
|
-
// 面板不许 import core,而「块怎么算合法」只允许有一份实现。core→shared 允许(见 eslint 注释)。
|
|
11
|
-
import { normalizeBlocks } from '../shared/blocks.js';
|
|
12
|
-
// 视图配置(section.views)的归一同样在 shared —— 理由同上:面板不许 import core,
|
|
13
|
-
// 而「什么算合法视图」只能有一份实现。
|
|
14
|
-
import { normalizeViews } from '../shared/views.js';
|
|
15
|
-
import { blocked } from './errors.js';
|
|
16
9
|
import { nowIso, stamp } from './ids.js';
|
|
17
10
|
|
|
18
11
|
/** v2:密钥栏目从 8 字段模板改成单字段 KV(名称 → 值)。
|
|
19
12
|
* v3:栏目有了 `kv` 标记——KV 栏目是一张表(面板渲染成表格,key 命令默认作用在它上面),
|
|
20
|
-
* 并去掉 `settings.kvSection` 这个冗余概念。
|
|
21
|
-
|
|
22
|
-
* 老条目没有 body 就是没有;normalize 会把非法块丢掉,存量数据读一次即安全。
|
|
23
|
-
* 反方向的降级没有保护:老代码读 v4 会把 body 丢掉(白名单重建),所以 v4 必须**拒绝老代码写**。 */
|
|
24
|
-
export const STORE_VERSION = 4;
|
|
13
|
+
* 并去掉 `settings.kvSection` 这个冗余概念。 */
|
|
14
|
+
export const STORE_VERSION = 3;
|
|
25
15
|
|
|
26
16
|
export function defaultSettings() {
|
|
27
17
|
return {
|
|
@@ -72,20 +62,12 @@ function normalizeSection(raw) {
|
|
|
72
62
|
const ensured = ensureSourceBuiltin(out.groups, out.fields, { kv: out.kv });
|
|
73
63
|
out.groups = ensured.groups;
|
|
74
64
|
out.fields = ensured.fields;
|
|
75
|
-
// 数据库视图(v4 同批):同样是**可选键**。缺席 = 还没配过,读路径推断一套缺省视图
|
|
76
|
-
// (不落盘,见 shared/views.js 的文件头)。空数组等价于缺席——存一个 `views: []`
|
|
77
|
-
// 只会让「还没配过」和「配过又删光」两件事看起来不一样,实际行为必须一样。
|
|
78
|
-
const views = normalizeViews(raw.views);
|
|
79
|
-
if (views.length) out.views = views;
|
|
80
|
-
// defaultView 必须指向一个**真的存在**的视图;指向空气就丢掉(读的时候退回第一个),
|
|
81
|
-
// 否则面板切到缺省视图时会拿到一个不存在的 id。
|
|
82
|
-
if (raw.defaultView && views.some((v) => v.id === raw.defaultView)) out.defaultView = String(raw.defaultView);
|
|
83
65
|
return out;
|
|
84
66
|
}
|
|
85
67
|
|
|
86
68
|
function normalizeEntry(raw) {
|
|
87
69
|
if (!raw || typeof raw !== 'object' || !raw.id) return null;
|
|
88
|
-
|
|
70
|
+
return {
|
|
89
71
|
id: String(raw.id),
|
|
90
72
|
section: String(raw.section || ''),
|
|
91
73
|
title: String(raw.title || ''),
|
|
@@ -94,10 +76,6 @@ function normalizeEntry(raw) {
|
|
|
94
76
|
createdAt: raw.createdAt || null,
|
|
95
77
|
updatedAt: raw.updatedAt || null,
|
|
96
78
|
};
|
|
97
|
-
// 块正文(v4):**可选键**,缺席就是没有(不补一个空数组,否则老条目会被无意义地改写)。
|
|
98
|
-
// 非法块在这里丢掉 —— normalize 是每条读写路径的必经点,坏数据读一次即安全。
|
|
99
|
-
if (Array.isArray(raw.body)) out.body = normalizeBlocks(raw.body);
|
|
100
|
-
return out;
|
|
101
79
|
}
|
|
102
80
|
|
|
103
81
|
// v1 的密钥模板字段。迁移时**不动 values 里这些键**——只把 keyValue 搬到 value,
|
|
@@ -218,14 +196,6 @@ let cachePath = null;
|
|
|
218
196
|
let cacheMtime = -1;
|
|
219
197
|
let cacheCorruptNote = null;
|
|
220
198
|
let cacheMigrationNote = null;
|
|
221
|
-
// 磁盘上那份 store 的版本号 —— 它可以**大于** STORE_VERSION(用户降级跑老代码)。
|
|
222
|
-
// normalize 会把版本抹成当前值,所以磁盘版本必须在这里单独记,否则降级守卫就无从判断。
|
|
223
|
-
let cacheDiskVersion = STORE_VERSION;
|
|
224
|
-
|
|
225
|
-
/** 最近一次读盘时的磁盘版本(bootstrap / health 展示「store 由更新版本写入」用)。 */
|
|
226
|
-
export function diskVersion() {
|
|
227
|
-
return cacheDiskVersion;
|
|
228
|
-
}
|
|
229
199
|
|
|
230
200
|
export function storePathInUse() {
|
|
231
201
|
return storeFile();
|
|
@@ -241,12 +211,6 @@ export function migrationNote() {
|
|
|
241
211
|
return cacheMigrationNote;
|
|
242
212
|
}
|
|
243
213
|
|
|
244
|
-
/** 降级运行时的说明(磁盘版本 > 当前版本,写入会被拒);正常时是 null。bootstrap / health 会展示。 */
|
|
245
|
-
export function downgradeNote() {
|
|
246
|
-
if (cacheDiskVersion <= STORE_VERSION) return null;
|
|
247
|
-
return { disk: cacheDiskVersion, current: STORE_VERSION };
|
|
248
|
-
}
|
|
249
|
-
|
|
250
214
|
export async function loadStore(explicitPath) {
|
|
251
215
|
const p = explicitPath || storeFile();
|
|
252
216
|
try {
|
|
@@ -265,7 +229,6 @@ export async function loadStore(explicitPath) {
|
|
|
265
229
|
cacheCorruptNote = { from: p, to: corrupt, error: String(e && e.message) };
|
|
266
230
|
}
|
|
267
231
|
const oldVersion = Number((parsed && parsed.version) || 1);
|
|
268
|
-
cacheDiskVersion = oldVersion;
|
|
269
232
|
const next = normalize(parsed);
|
|
270
233
|
|
|
271
234
|
if (!corrupt && oldVersion < STORE_VERSION && next._migration) {
|
|
@@ -282,7 +245,6 @@ export async function loadStore(explicitPath) {
|
|
|
282
245
|
};
|
|
283
246
|
delete next._migration;
|
|
284
247
|
// 就地落盘:version 写进去之后就不会再迁移(幂等)
|
|
285
|
-
cacheDiskVersion = STORE_VERSION;
|
|
286
248
|
await saveStore(next, p);
|
|
287
249
|
return next;
|
|
288
250
|
}
|
|
@@ -297,22 +259,12 @@ export async function loadStore(explicitPath) {
|
|
|
297
259
|
cache = emptyStore(true);
|
|
298
260
|
cachePath = p;
|
|
299
261
|
cacheMtime = -1;
|
|
300
|
-
cacheDiskVersion = STORE_VERSION;
|
|
301
262
|
return cache;
|
|
302
263
|
}
|
|
303
264
|
}
|
|
304
265
|
|
|
305
266
|
export async function saveStore(next, explicitPath) {
|
|
306
267
|
const p = explicitPath || storeFile();
|
|
307
|
-
// **降级守卫**(设计 §十一 11.4):磁盘上的 store 由更新版本写入时,拒绝一切写入。
|
|
308
|
-
// 否则老代码的白名单 normalize 会把新版本才有的键(比如 v4 的 body)**静默丢掉**——
|
|
309
|
-
// 用户降级跑一次、保存一次,块正文就永久没了,而且没有任何报错。
|
|
310
|
-
if (cacheDiskVersion > STORE_VERSION) {
|
|
311
|
-
throw blocked(
|
|
312
|
-
`这份 store 由更新版本的 nx-sk 写入(磁盘版本 v${cacheDiskVersion} > 当前 v${STORE_VERSION})。`
|
|
313
|
-
+ '为避免丢掉新版本才有的数据(块正文等),已拒绝写入。请升级 nx-sk 后再修改。',
|
|
314
|
-
);
|
|
315
|
-
}
|
|
316
268
|
const data = normalize(next);
|
|
317
269
|
delete data._migration;
|
|
318
270
|
await ensureDir(join(p, '..'));
|
package/src/core/vault.js
CHANGED
|
@@ -98,25 +98,12 @@ export async function currentKeyId() {
|
|
|
98
98
|
* 是安全决策——把它收成**一个**函数,就不会出现「某个出口忘了打码」。
|
|
99
99
|
* 只有当栏目里真有密文字段、且条目里真有密文时才去解析密钥(避免无谓地生成密钥文件)。
|
|
100
100
|
*/
|
|
101
|
-
/** 块正文里有没有密文(v4:secret 块的值也是密文落盘)。 */
|
|
102
|
-
function bodyHasSecretBlob(blocks) {
|
|
103
|
-
for (const b of blocks || []) {
|
|
104
|
-
if (b.type === 'secret' && isCipherBlob(b.value)) return true;
|
|
105
|
-
if (b.children && bodyHasSecretBlob(b.children)) return true;
|
|
106
|
-
}
|
|
107
|
-
return false;
|
|
108
|
-
}
|
|
109
|
-
|
|
110
101
|
export async function sensitiveViewer(section, entries = []) {
|
|
111
102
|
const sensitive = (section?.fields || []).filter((f) => f.sensitive || f.type === 'secret');
|
|
112
103
|
// 清单类(结构清单 entrylist / 表格 table)的敏感**子值**也是密文落盘:
|
|
113
104
|
// 不探行内的话,清单里真有密文时 viewer 不会被创建,读出来全变成「需要密钥才能解密」。
|
|
114
105
|
const lists = (section?.fields || []).filter((f) => f.type === 'entrylist' || f.type === 'table');
|
|
115
|
-
|
|
116
|
-
// 栏目里一个密文字段都没有、只有块里存了密钥时,早退会让 viewer 不被创建,
|
|
117
|
-
// 读出来全是「需要密钥才能解密」。
|
|
118
|
-
const bodyHasBlob = entries.some((e) => bodyHasSecretBlob(e?.body));
|
|
119
|
-
if (!sensitive.length && !lists.length && !bodyHasBlob) return undefined;
|
|
106
|
+
if (!sensitive.length && !lists.length) return undefined;
|
|
120
107
|
const keys = sensitive.map((f) => f.key);
|
|
121
108
|
// 两种清单的「列」字段名不同(fields / columns),但密文边界的判据是同一个
|
|
122
109
|
const subKeysOf = (f) => (f.type === 'table' ? (f.columns || []) : (f.fields || []))
|
|
@@ -131,10 +118,7 @@ export async function sensitiveViewer(section, entries = []) {
|
|
|
131
118
|
return Object.values(row).some((v) => isCipherBlob(v));
|
|
132
119
|
};
|
|
133
120
|
const hasBlob = entries.some((e) => keys.some((k) => isCipherBlob(e?.values?.[k]))
|
|
134
|
-
|| lists.some((f) => rowsOf(e?.values?.[f.key]).some((row) => rowHasBlob(row, f)))
|
|
135
|
-
// 块正文里的 secret 块同样是密文落盘(v4):不探它的话,
|
|
136
|
-
// 「整条条目只有块里有密文」时 viewer 不会被创建,读出来全是「需要密钥才能解密」。
|
|
137
|
-
|| entries.some((e) => bodyHasSecretBlob(e?.body)));
|
|
121
|
+
|| lists.some((f) => rowsOf(e?.values?.[f.key]).some((row) => rowHasBlob(row, f))));
|
|
138
122
|
if (!hasBlob) return undefined;
|
|
139
123
|
const { key } = await resolveVaultKey();
|
|
140
124
|
return (blob) => decryptValue(key, blob);
|