dsh-artifact-linker 0.4.3 → 0.4.4

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,19 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.4.4 — 2026-11-05
4
+
5
+ ### 对齐官方交付卡片:左键改为侧栏预览,右键 8 项菜单
6
+
7
+ 本轮把交互收敛到「和官方一致、官方没有的才补」:
8
+
9
+ - **左键默认动作改为「在侧栏打开预览」**(对齐官方 present 交付卡片点文件名即预览的体验)。原先左键是「在资源管理器里揭示并选中」。完整绝对路径 **0 次 `/resolve` 直通**,侧栏认领不了的类型**自动退回系统默认程序打开**,不再「点了没反应」。
10
+ - **右键菜单 7 项 → 8 项**,新增**「在浮窗中打开」**(独立小窗预览,增强项);措辞对齐官方:「在侧边栏预览」→「在侧栏打开」。分组:看(在侧栏打开 / 在浮窗中打开 / 在文件夹中显示 / 用默认程序打开)· 搬运(另存为 / 复制文件 / 复制文件路径)· 喂回(添加到对话框)。
11
+ - **修饰键语义保持并与官方一致**:`Ctrl/Shift/Cmd + 左键` = 系统默认程序打开;`Alt + 左键` = 复制绝对路径。
12
+ - **点官方 present 交付卡片自动让路**:命中 `[data-tool="present"]` 时不接管,交给官方、避免双开。
13
+ - **宿主动作单次往返**:`/open /reveal /saveas /copyfile` 新增原始文本解析(`absFromParam`:先 `cleanRaw`,绝对路径直用,否则 `resolveAcross`),客户端可直接传原始文本,无需先单独打一发 `/resolve`。`/resolve` 路由仍保留供悬停/校验使用。
14
+
15
+ **验证**:`node --check` 两文件通过;DOM-stub 逻辑探针 15/15(绝对路径左键 0 次 `/resolve`、裸名恰好 1 次、Alt 复制路径、Shift 系统打开、apply 无常驻 DOM / dispose 摘净监听)。操作体验以 `/info` 与实机为准。
16
+
3
17
  ## 0.4.3 — 2026-11-01
4
18
 
5
19
  ### 修复:悬停高亮慢半拍、落点不准(issue #1)
package/README.en.md CHANGED
@@ -27,7 +27,7 @@ Two halves, one contract.
27
27
  | Half | Responsibility |
28
28
  | --- | --- |
29
29
  | **Host** (`lib/index.js`) | Registers a **resident system-prompt section** so "declare artifacts with full absolute paths" holds in *every* session and *every* preset — no skill needs to be loaded. Exposes eight loopback-only routes for path resolution, `stat`, reveal, open, save-as and copy-file. |
30
- | **Client** (`lib/client.js`) | Turns artifact names in the transcript into clickable targets: left click reveals, right click opens a seven-item menu (sidebar preview, open, reveal, save-as, copy file, copy path, add to composer). **No resident UI, no scanning, no timers.** |
30
+ | **Client** (`lib/client.js`) | Turns artifact names in the transcript into clickable targets: **left click previews in the sidebar** (matching the official present card), right click opens an **eight-item** menu (open in sidebar, open in floating window, reveal in Explorer, open with default app, save-as, copy file, copy path, add to composer). **No resident UI, no scanning, no timers.** |
31
31
 
32
32
  The host half closes the loop: even when the model only says `result.h5`, the route layer resolves it against the session workspace, then falls back to a shallow recursive basename search, and returns the real absolute path.
33
33
 
@@ -37,16 +37,18 @@ Hover an artifact name and it picks up a Codex-style blue dashed outline — tha
37
37
 
38
38
  ![Context menu](https://raw.githubusercontent.com/loyalchiiina/dsh-artifact-linker/main/docs/context-menu.png)
39
39
 
40
+ Want to see it in action? [Watch the demo video (0.4.4 right-click menu)](https://github.com/loyalchiiina/dsh-artifact-linker/releases/download/v0.4.4/demo-0.4.4.mp4).
41
+
40
42
  ## Interaction
41
43
 
42
44
  Click behaviour is decided by the modifier key — no first-time setup:
43
45
 
44
46
  | Action | Result |
45
47
  | --- | --- |
46
- | **Left click** on a file name / path | Reveal in File Explorer *with the file selected* |
48
+ | **Left click** on a file name / path | Preview in DSH's built-in right sidebar (same route as the official present card; absolute paths resolve in **zero round-trips**; unclaimed types fall back to the default app) |
47
49
  | **Ctrl / Shift / Cmd + click** | Open with the system's default application |
48
50
  | **Alt + click** | Copy the full absolute path to the clipboard |
49
- | **Right click** on an artifact | Inline menu — seven actions, listed below |
51
+ | **Right click** on an artifact | Inline menu — eight actions, listed below |
50
52
  | **Right click** on non-artifact text | Fully passed through, the host's native menu appears as usual |
51
53
  | **Hover** | Cursor becomes a pointer *only after* the file is confirmed to exist |
52
54
  | File missing / unresolvable | Silent pass-through or a one-shot toast. **Never pretends to succeed.** |
@@ -55,7 +57,8 @@ Click behaviour is decided by the modifier key — no first-time setup:
55
57
 
56
58
  | Item | What it does |
57
59
  | --- | --- |
58
- | **Preview in sidebar** | Opens the file in DSH's built-in right sidebar — the same route as clicking a file link in chat. **Since 0.4.0 this menu item is shown for every file type**, with no extension filtering: whatever viewer claims the address renders it (PDF / Word / Excel / PowerPoint / Markdown / plain text / CSV / images, plus audio and video once plugins such as video-preview / audio-preview are installed); anything unclaimed falls back to plain text or the default app. The trade-off is that a rare type may answer with a plain "the sidebar cannot open this" — accepted on purpose, so future viewer capabilities are picked up with zero changes here |
60
+ | **Open in sidebar** | Opens the file in DSH's built-in right sidebar — the same route as clicking a file link in chat. **Since 0.4.0 this menu item is shown for every file type**, with no extension filtering: whatever viewer claims the address renders it (PDF / Word / Excel / PowerPoint / Markdown / plain text / CSV / images, plus audio and video once plugins such as video-preview / audio-preview are installed); anything unclaimed falls back to plain text or the default app. The trade-off is that a rare type may answer with a plain "the sidebar cannot open this" — accepted on purpose, so future viewer capabilities are picked up with zero changes here |
61
+ | **Open in floating window** | Opens a preview in a separate floating window (enhancement; if a popup blocker or the host blocks it, use "Open in sidebar" instead) |
59
62
  | **Open with default app** | Hands the file to the OS association (Explorer / Excel / VLC / …) |
60
63
  | **Reveal in Explorer** | Opens the containing folder and selects the file |
61
64
  | **Save as…** | Copies the file to a location you pick |
package/README.md CHANGED
@@ -21,7 +21,7 @@ DSH 里预览器已经能开图片、代码、pdf、office。但**插件、脚
21
21
  | 半边 | 做什么 |
22
22
  |---|---|
23
23
  | **宿主(提示词层)** | 常驻注入一条「产物路径铁律」,要求模型把产物写成**完整绝对路径**、而给用户看的正文**只写文件名**——路径有据可查,正文又不被长串路径污染 |
24
- | **客户端(渲染层)** | 正文里的产物名变成可点目标:左键揭示、右键七项菜单(侧边栏预览 / 默认程序打开 / 在文件夹中显示 / 另存为 / 复制文件 / 复制路径 / 添加到对话框)。**零常驻 UI、零扫描、零定时器** |
24
+ | **客户端(渲染层)** | 正文里的产物名变成可点目标:**左键在右侧边栏预览**(对齐官方交付卡片)、右键**八项**菜单(在侧栏打开 / 在浮窗中打开 / 在文件夹中显示 / 用默认程序打开 / 另存为 / 复制文件 / 复制文件路径 / 添加到对话框)。**零常驻 UI、零扫描、零定时器** |
25
25
 
26
26
  第 1 条管「模型把路径写对」,第 2 条管「用户点得动」。缺前者,路径经常是猜的、点开是死链;缺后者,路径再对也要人去复制。
27
27
 
@@ -31,6 +31,8 @@ DSH 里预览器已经能开图片、代码、pdf、office。但**插件、脚
31
31
 
32
32
  ![右键菜单](https://raw.githubusercontent.com/loyalchiiina/dsh-artifact-linker/main/docs/context-menu.png)
33
33
 
34
+ 想看实操录屏?[点这里看效果视频(0.4.4 右键菜单演示)](https://github.com/loyalchiiina/dsh-artifact-linker/releases/download/v0.4.4/demo-0.4.4.mp4)。
35
+
34
36
  ---
35
37
 
36
38
  ## 交互
@@ -39,10 +41,10 @@ DSH 里预览器已经能开图片、代码、pdf、office。但**插件、脚
39
41
 
40
42
  | 操作 | 结果 |
41
43
  |---|---|
42
- | **左键** | 在系统资源管理器里**揭示并选中**该文件(不是打开它,是把它亮在你面前) |
44
+ | **左键** | 在 DSH 右侧边栏打开预览(和官方交付卡片同一条通道;完整绝对路径 **0 次解析直通**,侧栏认领不了的类型自动退回系统默认程序打开) |
43
45
  | **Ctrl / Shift / Cmd + 左键** | 用系统默认关联程序打开(Excel、PS、播放器……) |
44
46
  | **Alt + 左键** | 复制完整绝对路径到剪贴板 |
45
- | **右键**产物名 | 插件菜单,共七项(见下表) |
47
+ | **右键**产物名 | 插件菜单,共八项(见下表) |
46
48
  | **右键**非产物文字 | 完全放行,宿主原生菜单照常出现 |
47
49
  | **悬停** | 只有**确认文件真的存在**才变手型、才画虚线 |
48
50
  | 文件不存在 / 解析不到 | 静默放行或一次性提示,**绝不假装成功** |
@@ -51,7 +53,8 @@ DSH 里预览器已经能开图片、代码、pdf、office。但**插件、脚
51
53
 
52
54
  | 菜单项 | 作用 |
53
55
  |---|---|
54
- | **在侧边栏预览** | 在 DSH 内置右侧边栏里打开,和聊天里点文件链接是同一条通道。**0.4.0 起对所有文件类型一律显示这一项**,不再按扩展名筛:能认领的查看器就渲染(PDF / Word / Excel / PowerPoint / Markdown / 纯文本 / CSV / 图片,装了 video-preview / audio-preview 等插件后还有音视频),认领不了的宿主会退回纯文本或默认程序。副作用是极少数类型点下去可能只得到一句「侧边栏打不开」——这是刻意接受的代价,为的是以后侧边栏补了新能力,本插件零改动自动跟上 |
56
+ | **在侧栏打开** | 在 DSH 内置右侧边栏里打开,和聊天里点文件链接是同一条通道。**0.4.0 起对所有文件类型一律显示这一项**,不再按扩展名筛:能认领的查看器就渲染(PDF / Word / Excel / PowerPoint / Markdown / 纯文本 / CSV / 图片,装了 video-preview / audio-preview 等插件后还有音视频),认领不了的宿主会退回纯文本或默认程序。副作用是极少数类型点下去可能只得到一句「侧边栏打不开」——这是刻意接受的代价,为的是以后侧边栏补了新能力,本插件零改动自动跟上 |
57
+ | **在浮窗中打开** | 在一个独立浮窗里打开预览(增强项;浏览器或宿主拦截弹窗时改用「在侧栏打开」) |
55
58
  | **用默认程序打开** | 交给系统关联(资源管理器 / Excel / 播放器……) |
56
59
  | **在文件夹中显示** | 打开所在目录并选中该文件 |
57
60
  | **另存为…** | 复制到你指定的位置 |
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.3";
32
+ var VERSION = "0.4.4";
33
33
  // 路由前缀必须与宿主的 ROUTE_PREFIX 一致;永远根相对,绝不拼宿主 origin
34
34
  var ROUTE = "/dsh-artifact-linker";
35
35
 
@@ -51,6 +51,12 @@ window.__ModuleLoader__.load({
51
51
  function isWinAbs(p) {
52
52
  return /^[A-Za-z]:[/\\]/.test(p) || p.indexOf("\\\\") === 0;
53
53
  }
54
+ // 绝对路径判定(与宿主 looksAbsolute 对齐):盘符 / UNC / POSIX 根。
55
+ // 命中即可「直通」——侧栏预览 0 次 /resolve,动作路由也由宿主内部解析。
56
+ function looksAbsoluteClient(p) {
57
+ if (isWinAbs(p)) return true;
58
+ return typeof p === "string" && p.charAt(0) === "/" && p.charAt(1) !== "/";
59
+ }
54
60
  function fileAddressFor(sessionId, cwd, p) {
55
61
  var normalized = p.replace(/\\/g, "/");
56
62
  if (!(normalized.charAt(0) === "/" || isWinAbs(normalized))) return sessionFileAddress(sessionId, normalized);
@@ -419,6 +425,51 @@ window.__ModuleLoader__.load({
419
425
  }
420
426
  }
421
427
 
428
+ // 浮窗打开(增强项):取到当前会话的资源地址,用 window.open 开一个独立小窗。
429
+ // 浏览器/宿主若拦截弹窗,返回 false,调用方提示改用「在侧栏打开」——绝不静默当成功。
430
+ function openFloating(abs) {
431
+ try {
432
+ if (!rootCtx || typeof rootCtx.get !== "function") return false;
433
+ var sidebar = rootCtx.get("sidebarRight");
434
+ var sessions = rootCtx.get("sessions");
435
+ if (!sessions || !sessions.list || typeof sessions.list.getSnapshot !== "function") return false;
436
+ var snap = sessions.list.getSnapshot();
437
+ var byId = (snap && snap.byId) || {};
438
+ var sid = null;
439
+ var mounted = sidebar && sidebar.mounted;
440
+ if (mounted && typeof mounted.get === "function") {
441
+ var m = mounted.get();
442
+ if (m && m.sessionId) sid = m.sessionId;
443
+ } else if (mounted && mounted.sessionId) {
444
+ sid = mounted.sessionId;
445
+ }
446
+ if (!sid) {
447
+ for (var k in byId) { if (byId[k] && byId[k].cwd) { sid = k; break; } }
448
+ }
449
+ if (!sid) return false;
450
+ var cwd = byId[sid] ? byId[sid].cwd : undefined;
451
+ var addr = fileAddressFor(sid, cwd, abs);
452
+ if (!addr) return false;
453
+ var w = window.open(addr, "_blank", "width=960,height=720,noopener,noreferrer");
454
+ return !!w;
455
+ } catch (e) {
456
+ return false;
457
+ }
458
+ }
459
+
460
+ // 只复制路径文字(修饰键 / 菜单共用)。失败如实提示,不装作成功。
461
+ function copyPathText(text, okMsg) {
462
+ try {
463
+ if (navigator.clipboard && navigator.clipboard.writeText) {
464
+ navigator.clipboard.writeText(text);
465
+ toast(okMsg || "已复制路径");
466
+ return true;
467
+ }
468
+ } catch (e) { }
469
+ toast("复制失败");
470
+ return false;
471
+ }
472
+
422
473
  function showMenu(x, y, abs) {
423
474
  hideMenu();
424
475
  if (!document || !document.body) return;
@@ -452,7 +503,9 @@ window.__ModuleLoader__.load({
452
503
  // 添加到对话框:带横线的气泡
453
504
  quote: '<path d="M20.5 11.6a8.3 8.3 0 0 1-8.3 8.3H7.4L3.5 21.9l1-4.1A8.3 8.3 0 1 1 20.5 11.6Z"/><path d="M9 11h6"/><path d="M9 14.2h3.6"/>',
454
505
  // 侧边栏预览:一块面板 + 右侧分栏
455
- preview: '<rect x="3.2" y="4.4" width="17.6" height="15.2" rx="2.2"/><path d="M14.4 4.4v15.2"/>'
506
+ preview: '<rect x="3.2" y="4.4" width="17.6" height="15.2" rx="2.2"/><path d="M14.4 4.4v15.2"/>',
507
+ // 浮窗:面板 + 向外箭头(弹出独立窗口)
508
+ float: '<rect x="3.2" y="4.6" width="13.4" height="11.2" rx="2"/><path d="M16.8 9.6v9h-5.4"/><path d="M13.8 6.4h4.8v4.8"/>'
456
509
  };
457
510
  // 文案要说人话:「资源管理器」这个词很多用户不认识,改成「文件夹窗口」。
458
511
  // 顺序按「最常做的动作」排,并用 "sep" 占位分成三组:
@@ -463,9 +516,10 @@ window.__ModuleLoader__.load({
463
516
  // 现在一律显示,点下去由宿主 sidebarRight.openResource 决定能不能认领:
464
517
  // 认领得了就渲染,认领不了 openInSidebar 返回 false,我们照实提示「打不开」。
465
518
  var items = [];
466
- items.push(["preview", "在侧边栏预览", "在右侧边栏里直接看,不用切出去;这个类型如果侧边栏渲染不了会如实提示"]);
467
- items.push(["open", "用默认程序打开", "按文件类型用对应软件打开:PDF 用阅读器、表格用 Excel 那类"]);
519
+ items.push(["preview", "在侧栏打开", "在右侧边栏里直接看,不用切出去;这个类型如果侧边栏渲染不了会如实提示"]);
520
+ items.push(["float", "在浮窗中打开", "在一个独立浮窗里打开预览(增强项;若浏览器拦截弹窗,请改用「在侧栏打开」)"]);
468
521
  items.push(["reveal", "在文件夹中显示", "打开 Windows 文件夹窗口,并选中这个文件"]);
522
+ items.push(["open", "用默认程序打开", "按文件类型用对应软件打开:PDF 用阅读器、表格用 Excel 那类"]);
469
523
  items.push(["sep", "", ""]);
470
524
  items.push(["saveas", "另存为…", "另存一份到别的位置(会弹出系统保存窗口)"]);
471
525
  items.push(["copyfile", "复制文件", "把文件本身放进剪贴板,可直接粘到任意文件夹"]);
@@ -498,6 +552,13 @@ window.__ModuleLoader__.load({
498
552
  ev.stopPropagation();
499
553
  hideMenu();
500
554
 
555
+ // ⓪ 浮窗打开 —— 增强项,纯客户端弹独立小窗;被拦截就如实提示改用侧栏
556
+ if (action === "float") {
557
+ if (openFloating(abs)) toast("已在浮窗中打开");
558
+ else toast("浮窗打开失败(可能被弹窗拦截),可改用「在侧栏打开」");
559
+ return;
560
+ }
561
+
501
562
  // ① 侧边栏预览 —— 纯客户端,走宿主导航入口
502
563
  if (action === "preview") {
503
564
  // 对所有类型都尝试,成败照实说。带扩展名,用户才知道是哪个类型没成。
@@ -619,6 +680,15 @@ window.__ModuleLoader__.load({
619
680
  if (disposed) return;
620
681
  // 解析不到就当普通文字,静默收场:不弹提示、不装作成功。
621
682
  if (!rec.ok || !rec.abs) return;
683
+ // 左键默认:在侧边栏打开(与官方 present 卡片语义统一);侧栏认领不了的类型再退化系统默认打开
684
+ if (mode === "preview") {
685
+ if (openInSidebar(rec.abs)) { toast("已在侧边栏打开"); return; }
686
+ hostGet("/open", { p: rec.abs }).then(function (r) {
687
+ if (disposed) return;
688
+ if (!r || !r.ok) toast("打开失败:" + ((r && r.reason) || "未知原因"));
689
+ });
690
+ return;
691
+ }
622
692
  if (mode === "reveal") {
623
693
  hostGet("/reveal", { p: rec.abs }).then(function (r) {
624
694
  if (disposed) return;
@@ -632,21 +702,66 @@ window.__ModuleLoader__.load({
632
702
 
633
703
  function onClick(ev) {
634
704
  if (disposed) return;
635
- if (ev.button !== 0 || ev.ctrlKey || ev.metaKey || ev.altKey || ev.shiftKey) return;
705
+ if (ev.button !== 0) return;
636
706
  if (menuEl && menuEl.contains && menuEl.contains(ev.target)) return;
637
707
  var hit = hitAt(ev.clientX, ev.clientY);
638
708
  if (!hit) return;
639
- // 🔴 这里**绝不 preventDefault / stopPropagation**:用户明确要求不能破坏宿主自带的
640
- // 「点文件路径 → 侧边栏预览」。上一版把它们拦了,结果宿主预览没了、我们的 explorer
641
- // 又弹不出窗口,两头落空 —— 表现就是「左键点了没反应」。
642
- // 现在改成让事件照常冒泡给宿主;只有等这一轮事件跑完、确认没人接管
643
- // (defaultPrevented 仍为 false)时才补一次 reveal。
709
+ // 官方 present 交付卡片本身已内置「点卡片 → 侧边栏预览」,交给官方自己开,避免双开
710
+ try {
711
+ var tEl = ev.target;
712
+ if (tEl && tEl.closest && tEl.closest('[data-tool="present"]')) return;
713
+ } catch (e2) { }
714
+ var clean = cleanRaw(hit.raw);
715
+
716
+ // ── 修饰键语义(与官方一致):Alt=复制绝对路径;Shift/Ctrl/Cmd=系统默认程序打开 ──
717
+ if (ev.altKey) {
718
+ ev.preventDefault();
719
+ ev.stopPropagation();
720
+ if (looksAbsoluteClient(clean)) { copyPathText(clean); return; }
721
+ verify(hit.raw, function (rec) {
722
+ if (disposed) return;
723
+ if (rec && rec.ok && rec.abs) copyPathText(rec.abs);
724
+ else toast("没解析到这个文件,无法复制路径");
725
+ });
726
+ return;
727
+ }
728
+ if (ev.ctrlKey || ev.metaKey || ev.shiftKey) {
729
+ ev.preventDefault();
730
+ ev.stopPropagation();
731
+ // 系统默认程序打开:直接把原始文本交给宿主,宿主内部解析(一次往返,不先 /resolve)
732
+ hostGet("/open", { p: hit.raw }).then(function (r) {
733
+ if (disposed) return;
734
+ if (!r || !r.ok) toast("打开失败:" + ((r && r.reason) || "未知原因"));
735
+ });
736
+ return;
737
+ }
738
+
739
+ // ── 普通左键:侧边栏预览 ──
740
+ // 🔴 绝不 preventDefault / stopPropagation:让事件照常冒泡给宿主;宿主若已接管(defaultPrevented)就让路。
644
741
  setTimeout(function () {
645
742
  if (ev.defaultPrevented) return; // 宿主已经处理(例如开了侧边栏预览)→ 让路,不抢
646
743
  var c = cached(hit.raw);
647
- // 已确认「根本不是产物」→ 什么都不做(别为一段普通文字去弹资源管理器)
648
- if (c && !c.ok) return;
649
- act(hit.raw, "reveal", ev.clientX, ev.clientY);
744
+ if (c && !c.ok) return; // 已确认「根本不是产物」→ 什么都不做
745
+ // 绝对路径直通:不经 /resolve,直接侧栏打开(验收①:Network 无 /resolve)
746
+ if (looksAbsoluteClient(clean)) {
747
+ if (openInSidebar(clean)) { toast("已在侧边栏打开"); return; }
748
+ hostGet("/open", { p: hit.raw }).then(function (r) {
749
+ if (disposed) return;
750
+ if (!r || !r.ok) toast("打开失败:" + ((r && r.reason) || "未知原因"));
751
+ });
752
+ return;
753
+ }
754
+ // 已有解析结论(裸名缓存命中)→ 直接侧栏开,省一次往返
755
+ if (c && c.ok) {
756
+ if (openInSidebar(c.abs)) { toast("已在侧边栏打开"); return; }
757
+ hostGet("/open", { p: c.abs }).then(function (r) {
758
+ if (disposed) return;
759
+ if (!r || !r.ok) toast("打开失败:" + ((r && r.reason) || "未知原因"));
760
+ });
761
+ return;
762
+ }
763
+ // 裸名首次:act 内部走一次 verify(/resolve) 再侧栏开(验收②:1 次解析)
764
+ act(hit.raw, "preview", ev.clientX, ev.clientY);
650
765
  }, 0);
651
766
  }
652
767
 
package/lib/index.js CHANGED
@@ -855,8 +855,20 @@ export function apply(ctx, config) {
855
855
  }
856
856
  }));
857
857
 
858
- // /reveal?p=<abs> —— 在系统资源管理器里揭示并选中(目录则直接打开)
859
- // /open?p=<abs> —— 用系统默认程序打开
858
+ // 5A:让动作路由既能接受绝对路径(旧行为),也能接受原始文本(裸名/相对路径),
859
+ // 内部自己 resolveAcross 解析——这样客户端「点击动作」可以一次 Host 调用直达,
860
+ // 不必先单独打一发 /resolve。绝对路径走精确 stat 分支,行为与旧版完全一致。
861
+ function absFromParam(pRaw, baseParam) {
862
+ var s = cleanRaw(pRaw || "");
863
+ if (!s) return null;
864
+ if (looksAbsolute(s)) return pathResolve(s);
865
+ var r = resolveAcross(s, baseParam, allowOutside);
866
+ if (r && r.ok && r.abs) return r.abs;
867
+ return null;
868
+ }
869
+
870
+ // /reveal?p=<abs|raw> —— 在系统资源管理器里揭示并选中(目录则直接打开)
871
+ // /open?p=<abs|raw> —— 用系统默认程序打开(也接受原始文本,宿主内部解析)
860
872
  function makeLauncher(action) {
861
873
  return function (req, res) {
862
874
  if (!isLoopback(req)) { res.writeHead(403); res.end(); return; }
@@ -867,12 +879,11 @@ export function apply(ctx, config) {
867
879
  json(res, 400, { ok: false, reason: "bad-url" });
868
880
  return;
869
881
  }
870
- var abs = cleanRaw(url.searchParams.get("p") || "");
871
- if (!abs || !looksAbsolute(abs)) {
872
- json(res, 200, { ok: false, reason: "need-absolute-path" });
882
+ var abs = absFromParam(url.searchParams.get("p") || "", url.searchParams.get("base") || "");
883
+ if (!abs) {
884
+ json(res, 200, { ok: false, reason: "unresolved" });
873
885
  return;
874
886
  }
875
- abs = pathResolve(abs);
876
887
  if (!insideAnyBase(abs, allowOutside)) {
877
888
  json(res, 200, { ok: false, reason: "outside-base", abs: abs });
878
889
  return;
@@ -903,12 +914,11 @@ export function apply(ctx, config) {
903
914
  json(res, 400, { ok: false, reason: "bad-url" });
904
915
  return;
905
916
  }
906
- var abs = cleanRaw(url.searchParams.get("p") || "");
907
- if (!abs || !looksAbsolute(abs)) {
908
- json(res, 200, { ok: false, reason: "need-absolute-path" });
917
+ var abs = absFromParam(url.searchParams.get("p") || "", url.searchParams.get("base") || "");
918
+ if (!abs) {
919
+ json(res, 200, { ok: false, reason: "unresolved" });
909
920
  return;
910
921
  }
911
- abs = pathResolve(abs);
912
922
  if (!insideAnyBase(abs, allowOutside)) {
913
923
  json(res, 200, { ok: false, reason: "outside-base", abs: abs });
914
924
  return;
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "dsh-artifact-linker",
3
- "version": "0.4.3",
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.",
3
+ "version": "0.4.4",
4
+ "description": "DSH 产物路径联动器:常驻注入「产物路径铁律」让模型输出完整绝对路径,同时把聊天正文里的文件名/路径渲染成一键可点的产物芯片——左键在右侧边栏预览(对齐官方交付卡片,绝对路径 0 次解析直通,侧栏认领不了自动退回系统默认程序),Ctrl/Shift/Cmd+左键用系统默认程序打开,Alt+左键复制绝对路径,右键 8 项菜单(在侧栏打开 / 在浮窗中打开 / 在文件夹中显示 / 用默认程序打开 / 另存为… / 复制文件 / 复制文件路径 / 添加到对话框)。宿主端提供回环只读路由与 /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, left click previews in DSH's built-in sidebar (absolute paths resolve in 0 round-trips; unclaimed types fall back to the default app), Ctrl/Shift/Cmd+click opens with the system default app, Alt+click copies the absolute path, and the right-click menu offers eight actions (open in sidebar / open in floating window / reveal in Explorer / open with default app / 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",
7
7
  "sideEffects": false,