dsh-artifact-linker 0.4.0 → 0.4.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
@@ -1,5 +1,49 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.4.2 — 2026-10-30
4
+
5
+ ### 改:识别层重写为「存在性驱动」——不再靠扩展名/排除字符集猜边界,任意绝对路径、任意文件名都能打开
6
+
7
+ 病根:0.4.1 仍是「正则按扩展名 + 排除字符集猜文件边界」。而 Windows 合法文件名字符几乎覆盖所有可见字符(括号、`【】`、中文标点、含空格、无扩展名、名字里带 `.数字`),排除法必然有漏;每漏一类就得再打一次补丁,不可收敛。真正的文件边界只有**磁盘真实存在性**能裁决。
8
+
9
+ ### 改(`lib/client.js` 识别层 + `lib/index.js` 解析层)
10
+
11
+ | 位置 | 旧 | 新 |
12
+ |---|---|---|
13
+ | 客户端识别(`hitAt`) | 用带扩展名+排除字符集的正则猜文件边界 | 新增绝对路径通道:`ABS_ANCHOR`(盘符 `X:\` 或 UNC `\\host\share\`)锁定起点 → 贪心截到**本行行尾**,不再猜扩展名;返回 `absStart` 供精确高亮;识别层不去尾(真实文件可能以 `)` `】` `。` 结尾) |
14
+ | 高亮(`onMove`) | 用整块 rect | 命中后用后端返回的真实文件名 `rec.abs.length` 收窄精确高亮 |
15
+ | 宿主解析(`resolveAcross`) | 整串 → 从左按空白剥 | ⓪原样整串精确 stat(救「文件名真以 (2024) 结尾」)→ 整串 tryAll → ①从右逐字符收缩 `shrinkRightAbs`(**不跨最后一个路径分隔符**、下界 `lastSep+2`、只做绝对精确 stat,避免砍成父目录/同名误报,`source:"absolute-shrunk"`)→ ②从左按空白剥兜底 |
16
+ | `VERSION` | `0.4.1` | `0.4.2` |
17
+
18
+ ### 验证(探针 `probe_v042.cjs`,复刻真实 resolveAcross + shrinkRightAbs + clientHitAt)
19
+
20
+ - 24 项断言全 PASS / 0 FAIL;`node --check` 双文件 exit 0;无 BOM。
21
+ - 覆盖:上轮 146 字 `inratio1.8D` 原样命中、路径紧跟中文说明(无空格)、路径+空格+说明、文件名含 `(最终)【v2】`、无扩展名 `README`、多级 `.cas.h5`+说明、文件名真以 `(2024)` 结尾(+说明)、前置说明词、**防误报(不存在文件不退化成父目录 `D:\a`)**、正文假路径不命中、相对裸名不误判;`hitAt` 取全路径吃到行尾、`absStart` 指向盘符、行尾不跨行、无盘符不进入绝对通道、无扩展名绝对路径可识别。
22
+ - 回归:host `/resolve` 契约未变;旧版识别不到的路径变可识别,已能识别的仍可识别。
23
+
24
+ ## 0.4.1 — 2026-10-30
25
+
26
+ ### 修:目录名里带「.数字/字母」(如 `inratio1.8D`)时,路径芯片只识别到该级目录、后半段文件丢失
27
+
28
+ 病根:`lib/client.js` 路径识别正则的中间段是**非贪婪**匹配(`{1,200}?` 后紧跟一个点),一遇到目录名里的点(`.../7-VTD-qianzhuijiao15-19-inratio1.8D/...`)就当作文件扩展名边界,把 `inratio1.8D` 里的 `.8D` 误判成扩展名 → 路径被截断在 `...inratio1.8D`(第 3 级目录),其后的 `comparison\...\*.png` 全部丢失,表现为用户所说「只能选到上几级的路径」。
29
+
30
+ ### 改(`lib/client.js` 三处 + `VERSION`)
31
+
32
+ | 位置 | 旧 | 新 |
33
+ |---|---|---|
34
+ | RE 末尾前瞻 | 只排除字母数字下划线 | 增加两条排除:①扩展名后紧跟路径分隔符(斜杠/反斜杠)②扩展名后又接「点+扩展名」段。强迫正则跨过目录名里的伪扩展名,一路吃到真正的文件扩展名 |
35
+ | 中间段长度上限 | `{1,200}?` | `{1,1024}?` |
36
+ | `hitAt` 取词窗口 | 鼠标前后各 200 字 | 鼠标前后各 1024 字(长路径整体落入匹配窗口) |
37
+ | `VERSION` 常量 | `0.4.0` | `0.4.1` |
38
+
39
+ ### 验证(从真实修复文件提取 RE + hitAt 窗口探针)
40
+
41
+ - 用户路径 146 字 `...inratio1.8D\comparison\...\VTD-...-静压.png` → 整段匹配;hitAt 鼠标落在偏移 0/20/74/90/120/146 均返回完整路径
42
+ - 不退化:`data.cas.h5` / `a.tar.gz` / `case.dat.h5` / 带空格中文名 `季度报告 最终版.pdf` / UNC 路径
43
+ - 多点目录 `2024.10.30\file.png`、`run.v1.3.2d\a.png` 均完整(旧版会截断)
44
+ - 不误报:散文 `版本 v1.2 已发布` → 无匹配
45
+ - `node --check` 通过;文件无 BOM
46
+
3
47
  ## 0.4.0 — 2026-10-30
4
48
 
5
49
  ### 改:彻底去掉扩展名白名单,所有文件类型都能识别、都能走侧边栏预览
package/lib/client.js CHANGED
@@ -29,7 +29,7 @@ window.__ModuleLoader__.load({
29
29
  var exports = module.exports;
30
30
 
31
31
  var GUARD = "__dshArtifactLinkerV1";
32
- var VERSION = "0.4.0";
32
+ var VERSION = "0.4.2";
33
33
  // 路由前缀必须与宿主的 ROUTE_PREFIX 一致;永远根相对,绝不拼宿主 origin
34
34
  var ROUTE = "/dsh-artifact-linker";
35
35
 
@@ -119,14 +119,14 @@ window.__ModuleLoader__.load({
119
119
  // 解析必然失败 → 点上去毫无反应。这是「带空格文件名点了没反应」的真正根因。)
120
120
  var RE = new RegExp(
121
121
  "(?:[A-Za-z]:[\\\\/]|\\\\\\\\[^\\\\/\\s]+[\\\\/][^\\\\/\\s]+)?" +
122
- "[^\\t\\r\\n`\"'<>|*?()()【】\\[\\]{},;、]{1,200}?\\." +
122
+ "[^\\t\\r\\n`\"'<>|*?()()【】\\[\\]{},;、]{1,1024}?\\." +
123
123
  // 扩展名两种写法都收:
124
124
  // 字母开头 1–12 位(pdf / mp4 / catproduct / properties…)
125
125
  // 数字开头 2–12 位(7z / 3mf / 3gp / 8bf…)—— 必须 ≥2 位,否则
126
126
  // 散文里的 v1.2 会贡献一个「扩展名 2」,虽然最后也会被存在性闸门静默掉,
127
127
  // 但没必要让它进候选池。
128
128
  "([A-Za-z][A-Za-z0-9_]{0,11}|[0-9][A-Za-z0-9_]{1,11})" +
129
- "(?:\\.[A-Za-z0-9]{1,12})*(?![A-Za-z0-9_])",
129
+ "(?:\\.[A-Za-z0-9]{1,12})*(?![A-Za-z0-9_\\\\/]|\\.[A-Za-z0-9])",
130
130
  "gi"
131
131
  );
132
132
  // URL 里天然带 .png/.pdf,全部要排除,否则网页链接会被当成文件产物。
@@ -208,11 +208,50 @@ window.__ModuleLoader__.load({
208
208
  var text = range.startContainer.nodeValue || "";
209
209
  if (!text) return null;
210
210
  var off = range.startOffset;
211
- var from = Math.max(0, off - 200);
212
- var to = Math.min(text.length, off + 200);
211
+ var from = Math.max(0, off - 1024);
212
+ var to = Math.min(text.length, off + 1024);
213
213
  var win = text.slice(from, to);
214
214
  var rel = off - from;
215
215
  var urls = urlRanges(win);
216
+
217
+ // ★ 绝对路径通道(v0.4.2):只要鼠标位置左侧有「盘符 X:\ 或 UNC \\host\share\」起点,
218
+ // 就从该起点一路**贪心吃到本行行尾**,不再用「扩展名 + 排除字符集」去猜边界。
219
+ // 这样任何名称都敢认:带空格、带 ()()[]【】{}、中文标点、无扩展名、超长、
220
+ // 目录名里带点(inratio1.8D / 2024.10.30)全都不影响 —— 真正的边界交给宿主
221
+ // resolveAcross 的「原样 stat → 从右逐字符收缩(不跨分隔符)」按真实磁盘裁决。
222
+ // 识别层刻意**不做 cleanRaw 去尾**:真实文件也可能以 ) 】 。 结尾,不能被误剥。
223
+ var ABS_ANCHOR = /(?:[A-Za-z]:[\\/]|\\\\[^\\/\r\n]+[\\/][^\\/\r\n]+[\\/]?)/g;
224
+ var lastStart = -1;
225
+ var ma;
226
+ ABS_ANCHOR.lastIndex = 0;
227
+ while ((ma = ABS_ANCHOR.exec(win))) {
228
+ if (ma.index > rel) break; // 只看鼠标左侧(含)的起点
229
+ lastStart = ma.index;
230
+ }
231
+ if (lastStart >= 0) {
232
+ var nl = win.indexOf("\n", lastStart);
233
+ var lineEnd = nl < 0 ? win.length : nl;
234
+ if (rel <= lineEnd) {
235
+ var seg = win.slice(lastStart, lineEnd).replace(/\s+$/, "");
236
+ if (seg && seg.length >= 4 && !inRanges(urls, lastStart, lastStart + seg.length)) {
237
+ var exts = seg.match(/\.[A-Za-z0-9]{1,12}/g) || [];
238
+ var isExec = false;
239
+ for (var ei = 0; ei < exts.length; ei++) {
240
+ if (EXEC_EXT.test(exts[ei].slice(1))) { isExec = true; break; }
241
+ }
242
+ if (!isExec) {
243
+ return {
244
+ raw: seg,
245
+ rect: rectFor(range.startContainer, from + lastStart, from + lineEnd),
246
+ node: range.startContainer,
247
+ absStart: from + lastStart
248
+ };
249
+ }
250
+ }
251
+ }
252
+ }
253
+
254
+ // ── 兜底:裸文件名 / 相对路径(无盘符),仍用扩展名形状识别,交后端 ①②③ 兜 ──
216
255
  var m;
217
256
  RE.lastIndex = 0;
218
257
  while ((m = RE.exec(win))) {
@@ -669,7 +708,14 @@ window.__ModuleLoader__.load({
669
708
  if (disposed) return;
670
709
  if (!rec.ok) { setCursor(""); hideHover(); return; }
671
710
  setCursor("pointer");
672
- drawHover(hit.rect);
711
+ // 绝对路径通道可能把「路径 + 尾随说明」整段当候选,粗略 rect 会盖到说明文字上。
712
+ // 命中后按宿主返回的真实 abs 长度把高亮收窄到路径本身(起点对齐,abs 是候选的前缀)。
713
+ var rr = hit.rect;
714
+ if (hit.node && rec.abs) {
715
+ var precise = rectFor(hit.node, hit.absStart, hit.absStart + rec.abs.length);
716
+ if (precise) rr = precise;
717
+ }
718
+ drawHover(rr);
673
719
  });
674
720
  }, 150);
675
721
  }
package/lib/index.js CHANGED
@@ -260,14 +260,51 @@ function resolveAcross(raw, extraBase, allowOutside) {
260
260
  return best;
261
261
  }
262
262
 
263
+ var rawTrim = String(raw || "").trim();
264
+
265
+ // 只对绝对路径做「精确 stat」的小工具:不走 basename 反查,避免把目录/同名文件误认。
266
+ function statAbsExact(s) {
267
+ if (!looksAbsolute(s)) return null;
268
+ var abs = pathResolve(s);
269
+ if (!allowOutside) {
270
+ var okb = false;
271
+ for (var bi = 0; bi < bases.length; bi++) { try { if (isInside(bases[bi], abs)) { okb = true; break; } } catch (e) { } }
272
+ if (!okb) return null;
273
+ }
274
+ var st = statOne(abs);
275
+ if (!st.exists) return null;
276
+ return { ok: true, raw: s, abs: abs, exists: true, kind: st.kind, size: st.size, mtimeMs: st.mtimeMs, source: "absolute" };
277
+ }
278
+
279
+ // ⓪ 原样整串精确 stat(不经 cleanRaw):真实文件名允许以 ) 】 。 结尾、也允许带空格中文,
280
+ // cleanRaw 会误剥这些尾部,所以先拿 raw 原样撞一次,命中即用。
281
+ var exact = statAbsExact(rawTrim);
282
+ if (exact) return exact;
283
+
263
284
  var first = tryAll(raw);
264
285
  if (first && first.ok) return first;
265
286
  var best = first;
266
287
 
267
- // 🔴 从左收缩重试(配合客户端正则允许空格的做法)
268
- // 客户端为了支持「带空格的文件名」,现在会把左边几个词一起吞进候选,
269
- // 于是「详见 报告.pdf」这种会整串送过来。这里先按整串试,不行就剥掉最左一段再试,
270
- // 直到只剩纯文件名。这样既保住空格文件名,又不会因为多吞了几个词就解析不到。
288
+ // ① 绝对路径:从右逐字符收缩,**不跨过最后一个路径分隔符**(避免把文件名砍光后退化成父目录误报)。
289
+ // 专治「路径后紧跟中文说明/尾部标点且中间无空格」:D:\a\b.png这个图 → D:\a\b.png。
290
+ function shrinkRightAbs(s) {
291
+ var t = String(s || "").trim();
292
+ if (!looksAbsolute(t)) return null;
293
+ var lastSep = Math.max(t.lastIndexOf("\\"), t.lastIndexOf("/"));
294
+ var lowBound = lastSep + 2; // 最后一个文件名至少保留 1 个字符
295
+ var limit = Math.min(t.length, 600);
296
+ for (var cut = t.length - 1; cut >= lowBound && (t.length - cut) <= limit; cut--) {
297
+ var cand = t.slice(0, cut);
298
+ var o = statAbsExact(cand);
299
+ if (o && o.ok) { o.raw = cand; o.shrunkFrom = s; o.source = "absolute-shrunk"; return o; }
300
+ }
301
+ return null;
302
+ }
303
+ var shrunkAbs = shrinkRightAbs(rawTrim);
304
+ if (shrunkAbs && shrunkAbs.ok) return shrunkAbs;
305
+ if (!best && shrunkAbs) best = shrunkAbs;
306
+
307
+ // ② 从左按空白剥(前置说明词 + 空格 + 路径)
271
308
  var segs = String(raw || "").trim().split(/\s+/);
272
309
  for (var j = 1; j < segs.length; j++) {
273
310
  var shrunk = segs.slice(j).join(" ");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-artifact-linker",
3
- "version": "0.4.0",
3
+ "version": "0.4.2",
4
4
  "description": "DSH 产物路径联动器:常驻注入「产物路径铁律」让模型输出完整绝对路径,同时把聊天正文里的文件名/路径渲染成一键可点的产物芯片——左键预览、右键菜单可打开/定位/另存为/复制文件/复制路径/添加到对话框。宿主端提供回环只读路由与 /saveas /copyfile 两个文件搬运动作(Shell COM 揭示、WinForms 保存对话框、CF_HDROP 剪贴板),客户端零侵入、不改 React 节点、不接管 markdown 图片语法。解决「产物要打开得先复制路径再粘到文件管理器」的反复割裂。/ Turn declared artifact paths into one-click chips: resident system-prompt rules force absolute paths, the client half renders file names as clickable chips, and the right-click menu offers open / reveal in Explorer / save-as / copy-file / copy-path / insert-into-composer. Host routes are loopback-only; zero telemetry.",
5
5
  "type": "module",
6
6
  "main": "lib/index.js",