nx-sk 0.15.0 → 0.15.2

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 CHANGED
@@ -4,6 +4,23 @@
4
4
 
5
5
  ## [Unreleased]
6
6
 
7
+ ## 0.15.2 (2026-10-20)
8
+
9
+ ### Changed
10
+
11
+ - **skill 改名:`kv-source` → `sk-source`**——与 sk-pdf / sk-needdo / sk-thesis 用同一个
12
+ `sk-` 前缀(它们都是「教 agent 做一类事」的配套 skill,`nx-*` 前缀留给 CLI 本体)。
13
+ 已装过旧名的:`nx-sk skill install sk-source --force` 装新的,旧的目录可手动删。
14
+
15
+ ## 0.15.1 (2026-10-20)
16
+
17
+ ### Fixed
18
+
19
+ - **附件字段里的附件列表改成 5 行高的滚动窗口**:原先是**全部铺开**——20 个附件就把
20
+ 条目表单撑高近 1000px,填别的字段时得拖着整页上下滚。现在默认只露 5 行,
21
+ 窗口内上下滚动查看,下方标「共 N 个附件(窗口内上下滚动查看)」,需要看全时点
22
+ 「展开全部」/「收起到 5 行」。
23
+
7
24
  ## 0.15.0 (2026-10-20)
8
25
 
9
26
  **原始资料袋:每个栏目都自带一个「乱入区」**
@@ -34,7 +51,7 @@
34
51
  - 三条纪律写进代码与文档:**原文不改写**、**不确定就留空**、**敏感内容不进袋**(袋是明文的,
35
52
  密码走「密钥」栏目或 `sensitive` 字段);袋**不计完整度**(暂存区不是「该填的格子」)。
36
53
  - 新栏目(模板或自定义)都自带这个字段;老栏目用 `nx-sk source ensure` 一次补齐。
37
- - **`kv-source` skill**(随包分发,`nx-sk skill install kv-source`):讲透「原始资料 ↔ 字段」
54
+ - **`sk-source` skill**(随包分发,`nx-sk skill install sk-source`):讲透「原始资料 ↔ 字段」
38
55
  的双向同步、映射表纪律(把握不足就不写)、以及各种文件(PDF/截图/邮件/表格)怎么进袋。
39
56
 
40
57
  ### Fixed
@@ -160,7 +160,7 @@ nx-sk source ensure # 给老栏目补上这个字段(模板
160
160
  **把袋子里的东西整理成字段**(agent 侧)有配套 skill:
161
161
 
162
162
  ```bash
163
- nx-sk skill install kv-source # 装到 ~/.claude/skills/kv-source
163
+ nx-sk skill install sk-source # 装到 ~/.claude/skills/sk-source
164
164
  ```
165
165
 
166
166
  它讲的是双向同步:source → 字段(整理)与 字段 → source(回写留痕),
@@ -162,7 +162,7 @@ nx-sk section update job --fields @fields.json
162
162
  > 把乱数据硬塞进结构化字段(把结构搞脏)或干脆不存(信息丢了)两个坏选择。
163
163
  > - 面板上:条目表单底部一条**折叠条**「原始资料 · N 条」,点开是右侧抽屉——
164
164
  > 左清单、中间正文,文件可拖进去(也给了「选择文件」按钮与粘贴框:拖拽不能是唯一入口)。
165
- > - CLI:`nx-sk source list / get / add / update / remove / ensure`(细节见 `kv-source` skill)。
165
+ > - CLI:`nx-sk source list / get / add / update / remove / ensure`(细节见 `sk-source` skill)。
166
166
  > - 三条纪律:**原文不改写**(整理结果写进字段或新的一条)、**不确定就留空**、
167
167
  > **敏感内容不进袋**(袋是明文的,密码走「密钥」栏目或 `sensitive` 字段)。
168
168
  > - 袋**不计完整度**(`fill: optional`):它是暂存区,不是「该填的格子」。
@@ -1,9 +1,9 @@
1
1
  ---
2
- name: kv-source
2
+ name: sk-source
3
3
  description: 在 nx-sk 的栏目里做「原始资料 ↔ 结构化字段」的双向同步——把杂乱资料(邮件、聊天记录、截图、PDF 摘录、手抄的表、网址)先原样收进条目的「原始资料」袋,再按栏目字段字典整理成结构化字段;也支持反向把已确认的字段值回写进袋子留痕。当用户说"把这些乱的整理进 nx-sk"、"这堆资料归到字段里"、"source 和字段同步一下"、"把邮件/截图/文档存下来并归档"、"原始资料整理成表"、"回写一下出处",或给来一堆没格式的内容却已经指定了栏目/条目时使用。触发词:source、原始资料、乱入、整理、同步、归档、乱数据、收件箱、字段同步、资料袋。不适用:用户已经说清要写哪个字段(直接 entry update 即可)、栏目结构本身还没定(先走 sk-needdo 定结构)、PDF 要转成图文 SOP(用 sk-pdf)。
4
4
  ---
5
5
 
6
- # kv-source · 原始资料 ↔ 字段 的双向同步
6
+ # sk-source · 原始资料 ↔ 字段 的双向同步
7
7
 
8
8
  一句话:**先原样收下,再按字典整理;整理完留痕,两头能对回去。**
9
9
 
@@ -1,6 +1,6 @@
1
1
  # 10 · 双向同步手册
2
2
 
3
- > `kv-source` skill 的详细页。**什么时候读**:袋里有东西要整理(第 3 步)、
3
+ > `sk-source` skill 的详细页。**什么时候读**:袋里有东西要整理(第 3 步)、
4
4
  > 或要把字段值回写进袋(第 4 步)时。
5
5
 
6
6
  ## 两种方向,先分清
@@ -1,6 +1,6 @@
1
1
  # 20 · 文件与资料怎么进袋
2
2
 
3
- > `kv-source` skill 的详细页。**什么时候读**:用户丢来的是文件、截图、PDF、
3
+ > `sk-source` skill 的详细页。**什么时候读**:用户丢来的是文件、截图、PDF、
4
4
  > 邮件、表格这些「非纯文字」的东西时。
5
5
 
6
6
  ## 原则:原件进附件库,袋里只留引用
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nx-sk",
3
- "version": "0.15.0",
3
+ "version": "0.15.2",
4
4
  "description": "nx-sk · 本机个人资源管理器(server-cli-web 骨架):栏目化存储个人信息与大模型密钥,CLI 与 Web 同源,敏感字段密文落盘。",
5
5
  "type": "module",
6
6
  "bin": {
@@ -1,6 +1,6 @@
1
1
  // 原始资料袋(`type: 'source'`)的条目形状与归一规则。
2
2
  //
3
- // 为什么单独一个 core 模块:这份形状同时被 CLI、面板、kv-source skill 三处读写。
3
+ // 为什么单独一个 core 模块:这份形状同时被 CLI、面板、sk-source skill 三处读写。
4
4
  // 形状若各写一份,迟早漂成「面板能显示、agent 读不出」——所以口径只此一处。
5
5
  //
6
6
  // 一条 item 的形状:
@@ -81,6 +81,8 @@ export function FileFieldControl({ def, value, onChange }) {
81
81
  const { toast, guard, patchUi } = useStore();
82
82
  const [data, setData] = useState(null);
83
83
  const [dragOver, setDragOver] = useState(false);
84
+ // 附件多的时候不把表单撑长:默认只露 5 行,窗口内上下滚动;需要看全时一键展开
85
+ const [expanded, setExpanded] = useState(false);
84
86
  const uploadRef = useRef(null);
85
87
  // dragenter/dragleave 在子元素间穿梭时会成对触发,用深度计数避免「还没离开就熄掉高亮」
86
88
  const dragDepth = useRef(0);
@@ -137,29 +139,41 @@ export function FileFieldControl({ def, value, onChange }) {
137
139
  onDragLeave={() => { dragDepth.current -= 1; if (dragDepth.current <= 0) { dragDepth.current = 0; setDragOver(false); } }}
138
140
  onDrop={onDrop}
139
141
  >
140
- {ids.map((id) => {
141
- const rec = byId(id);
142
- if (!rec) {
143
- return (
144
- <div className="file-preview" key={id}>
145
- <span className="hint bad grow">引用的附件已不存在({id})—— 重新上传,或移除。</span>
146
- <button type="button" className="btn small ghost" onClick={() => removeAt(id)}>移除</button>
147
- </div>
148
- );
149
- }
150
- return (
151
- <div className="file-preview" key={id}>
152
- {rec.isImage
153
- ? <img src={rec.url} alt={rec.name} />
154
- : <span className="ext-badge" style={{ fontSize: 13, width: 40, textAlign: 'center' }}>{rec.ext ? rec.ext.slice(1).toUpperCase() : 'FILE'}</span>}
155
- <a className="file-preview-name grow" href={rec.url} target="_blank" rel="noreferrer" title={rec.displayPath}>{rec.name}</a>
156
- <a className="btn small ghost" href={rec.downloadUrl}>下载</a>
157
- <Copyable className="hint" text={rec.path} title="点击复制物理路径">路径</Copyable>
158
- <button type="button" className="btn small ghost" onClick={() => removeAt(id)}>移除</button>
159
- </div>
160
- );
161
- })}
142
+ {ids.length ? (
143
+ <div className={`file-picker-list${expanded ? ' expanded' : ''}`}>
144
+ {ids.map((id) => {
145
+ const rec = byId(id);
146
+ if (!rec) {
147
+ return (
148
+ <div className="file-preview" key={id}>
149
+ <span className="hint bad grow">引用的附件已不存在({id})—— 重新上传,或移除。</span>
150
+ <button type="button" className="btn small ghost" onClick={() => removeAt(id)}>移除</button>
151
+ </div>
152
+ );
153
+ }
154
+ return (
155
+ <div className="file-preview" key={id}>
156
+ {rec.isImage
157
+ ? <img src={rec.url} alt={rec.name} />
158
+ : <span className="ext-badge" style={{ fontSize: 13, width: 40, textAlign: 'center' }}>{rec.ext ? rec.ext.slice(1).toUpperCase() : 'FILE'}</span>}
159
+ <a className="file-preview-name grow" href={rec.url} target="_blank" rel="noreferrer" title={rec.displayPath}>{rec.name}</a>
160
+ <a className="btn small ghost" href={rec.downloadUrl}>下载</a>
161
+ <Copyable className="hint" text={rec.path} title="点击复制物理路径">路径</Copyable>
162
+ <button type="button" className="btn small ghost" onClick={() => removeAt(id)}>移除</button>
163
+ </div>
164
+ );
165
+ })}
166
+ </div>
167
+ ) : null}
162
168
  {!ids.length ? <span className="hint">还没有附件。</span> : null}
169
+ {ids.length > 5 ? (
170
+ <div className="file-pick-row">
171
+ <span className="hint">共 {ids.length} 个附件{expanded ? '(已展开全部)' : '(窗口内上下滚动查看)'}</span>
172
+ <button type="button" className="btn small ghost" onClick={() => setExpanded((v) => !v)}>
173
+ {expanded ? '收起到 5 行' : '展开全部'}
174
+ </button>
175
+ </div>
176
+ ) : null}
163
177
  <div className="file-pick-row">
164
178
  <select value="" disabled={full || !available.length} onChange={(e) => add(e.target.value)}>
165
179
  <option value="">{available.length ? '(从附件库选一个,选中即追加)' : '(库里没有可再选的附件)'}</option>
@@ -974,3 +974,16 @@ body.col-resizing, body.col-resizing * { cursor: col-resize !important; user-sel
974
974
  transition: background .15s ease;
975
975
  }
976
976
  .source-old-toggle:hover { background: var(--soft-2); color: var(--ink); }
977
+
978
+ /* 附件列表:**固定 5 行高的滚动窗口**。
979
+ 附件一多,全铺开会把整页表单撑得很长(每行 ~50px,20 个附件就是 1000px),
980
+ 填别的字段时得拖着整页上下滚。改成窗口内滚动,表单高度就稳住了。 */
981
+ .file-picker-list {
982
+ display: flex; flex-direction: column; gap: 4px;
983
+ max-height: 266px; /* ≈ 5 行(每行 ~50px)+ 间隙 */
984
+ overflow-y: auto; overflow-x: hidden;
985
+ padding-right: 2px; /* 给滚动条留位,滚动条不压住「移除」按钮 */
986
+ scrollbar-width: thin;
987
+ }
988
+ .file-picker-list.expanded { max-height: none; }
989
+ .file-picker-list .file-preview { flex-shrink: 0; }
@@ -1 +1 @@
1
- import{a as l}from"./index-CK6204A2.js";let n=null,o=0;const u=1500;function c(t=!1){return!t&&n&&Date.now()-o<u||(o=Date.now(),n=l("/api/files").then(e=>e).catch(e=>{throw n=null,e})),n}function d(){return n=null,c(!0)}function h(t,e){const a=Array.isArray(t)?t:[],r=(Array.isArray(e)?e:[]).filter(Boolean);if(!r.length)return a;const s=new Set(r.map(i=>i.id));return[...r,...a.filter(i=>!s.has(i.id))]}function m(t){return new Promise((e,a)=>{const r=new FileReader;r.onload=()=>e(r.result),r.onerror=()=>a(new Error("读取文件失败")),r.readAsDataURL(t)})}export{m as f,c as l,h as m,d as r};
1
+ import{a as l}from"./index-B1tuW-Bw.js";let n=null,o=0;const u=1500;function c(t=!1){return!t&&n&&Date.now()-o<u||(o=Date.now(),n=l("/api/files").then(e=>e).catch(e=>{throw n=null,e})),n}function d(){return n=null,c(!0)}function h(t,e){const a=Array.isArray(t)?t:[],r=(Array.isArray(e)?e:[]).filter(Boolean);if(!r.length)return a;const s=new Set(r.map(i=>i.id));return[...r,...a.filter(i=>!s.has(i.id))]}function m(t){return new Promise((e,a)=>{const r=new FileReader;r.onload=()=>e(r.result),r.onerror=()=>a(new Error("读取文件失败")),r.readAsDataURL(t)})}export{m as f,c as l,h as m,d as r};