dsh-artifact-linker
Turns every file artifact the agent names in chat into a clickable entry — and refuses to paint one that does not resolve. Hover resolves asynchronously, and only a file that really exists becomes a hand pointer with a dashed blue outline (30 s TTL cache, so repeated hovers cost nothing); confirmed
Install
dsh plugin --profile web add dsh-artifact-linker
Sandbox test results
| Stage | Result |
|---|---|
| 1 install | ✓ |
| 2 list | ✓ |
| 3 mount | ✓ |
| 4 boot | — |
Four stages run in an isolated sandbox: install (network on) → list → mount → boot (last three with no network).
README
dsh-artifact-linker · 产物路径联动器
让 DSH 交付的每一个产物,都能点一下就到位——不用再「复制路径 → 打开文件管理器 → 贴到地址栏 → 回车」。
[English](README.en.md) · 简体中文 · [安装](#安装) · [交互](#交互) · [配置](#配置) · [边界](#它不做什么) · [排查](#排查)
它解决什么
DSH 里预览器已经能开图片、代码、pdf、office。但插件、脚本、Fluent 出图、打包产物、临时导出这些文件,正文里只给一个名字,想打开它得走完四步机械劳动:
看到 "结果.xlsx" → 复制路径 → Win+E → 地址栏输入 → 回车 → 在一堆同级文件里找那一个
dsh-artifact-linker 把这条路压成 0 步:正文里的文件名本身就是入口。
它做两件互相咬合的事:
半边 · 做什么
宿主(提示词层) · 常驻注入一条「产物路径铁律」,要求模型把产物写成完整绝对路径、而给用户看的正文只写文件名——路径有据可查,正文又不被长串路径污染
客户端(渲染层) · 正文里的产物名变成可点目标:左键在右侧边栏预览(对齐官方交付卡片)、右键八项菜单(在侧栏打开 / 在浮窗中打开 / 在文件夹中显示 / 用默认程序打开 / 另存为 / 复制文件 / 复制文件路径 / 添加到对话框)。零常驻 UI、零扫描、零定时器
第 1 条管「模型把路径写对」,第 2 条管「用户点得动」。缺前者,路径经常是猜的、点开是死链;缺后者,路径再对也要人去复制。
长什么样
悬停到产物名上,会出现 Codex 风格的蓝色虚线框——这是唯一的视觉提示,正文一个字都不会被改写。右键弹出完整菜单:
想看实操录屏?点这里看效果视频(0.4.4 右键菜单演示)。
交互
正文里点一个文件名(或绝对路径):
操作 · 结果
左键 · 在 DSH 右侧边栏打开预览(和官方交付卡片同一条通道;完整绝对路径 0 次解析直通,侧栏认领不了的类型自动退回系统默认程序打开)
Ctrl / Shift / Cmd + 左键 · 用系统默认关联程序打开(Excel、PS、播放器……)
Alt + 左键 · 复制完整绝对路径到剪贴板
右键产物名 · 插件菜单,共八项(见下表)
右键非产物文字 · 完全放行,宿主原生菜单照常出现
悬停 · 只有确认文件真的存在才变手型、才画虚线
文件不存在 / 解析不到 · 静默放行或一次性提示,绝不假装成功
右键菜单
菜单项 · 作用
在侧栏打开 · 在 DSH 内置右侧边栏里打开,和聊天里点文件链接是同一条通道。0.4.0 起对所有文件类型一律显示这一项,不再按扩展名筛:能认领的查看器就渲染(PDF / Word / Excel / PowerPoint / Markdown / 纯文本 / CSV / 图片,装了 video-preview / audio-preview 等插件后还有音视频),认领不了的宿主会退回纯文本或默认程序。副作用是极少数类型点下去可能只得到一句「侧边栏打不开」——这是刻意接受的代价,为的是以后侧边栏补了新能力,本插件零改动自动跟上
在浮窗中打开 · 在一个独立浮窗里打开预览(增强项;浏览器或宿主拦截弹窗时改用「在侧栏打开」)
用默认程序打开 · 交给系统关联(资源管理器 / Excel / 播放器……)
在文件夹中显示 · 打开所在目录并选中该文件
另存为… · 复制到你指定的位置
复制文件 · 把文件本身放进剪贴板(CF_HDROP),可以直接粘到资源管理器、邮件、聊天窗口
复制文件路径 · 把真实绝对路径放进剪贴板
添加到对话框 · 把路径插入输入框,供下一条消息使用
路径怎么解析(三源兜底)
模型嘴里的话不可全信,插件按顺序试:
1. /workspace/<...> —— DSH 工作区的虚拟根,映射到真实 workspace 目录;
2. 原生绝对路径 —— E:\proj\out\a.xlsx 直接用(盘符和 UNC 都认);
3. 会话相对路径 —— 以会话 workspace 为基准拼接;
4. 各 workspace 根 —— 遍历所有已知的 profile / 会话 workspace,不只当前那一个;
5. 裸文件名 —— 深浅递归搜同名文件(跳过 node_modules/.git/dist/build/ 等),按「最近修改优先」+「目录深度浅优先」排序:
- 命中 1 个 → 直接用;
- 命中 多个 → 返回候选列表交给用户,绝不猜(猜错比不猜更糟);
- 命中 0 个 → 若
allowOutside: true,再在磁盘上按名字做一次浅层搜索。
提到时若「提及吞掉了前面几个词」(结果#1_速度云图.png 只剩 #1_速度云图.png),会从左逐段收缩重试直到能命中。
缓存两层:stat 10 秒、同名索引 30 秒——避免重复打满磁盘,同时刚删掉的文件也能很快翻成「已不存在」。
安装
# 1. 放进 DSH 的本地插件目录 # Windows: %USERPROFILE%\.dsh\local-plugins\dsh-artifact-linker # 2. profile 里声明依赖(<profile> 换成 desktop / web / xiufu) # 在 %USERPROFILE%\.dsh\profiles\<profile>\package.json 的 dependencies 里加: # "dsh-artifact-linker": "file:~/.dsh/local-plugins/dsh-artifact-linker" # 3. 完全重启 DSH(客户端 bundle 不随 Ctrl+R 重载)
⚠️file:依赖的坑:file:/link:指向会在 profile 重装依赖时把目录内容按快照恢复,本地改动可能被回滚。长期使用建议npm pack成 tarball 后用file:指向 tgz,或直接发到 npm。
装好后确认:
GET http://127.0.0.1:<port>/dsh-artifact-linker/info
正常应返回插件名、版本、workspace、以及 injectRules / clientChips / allowOutside 三个开关的实际生效值(这一条是排障时唯一可信的地方——别信 README,信 /info)。
配置
在 profile 的 cordis.patch.yml 里给本插件条目加 config(不要在 profile 里手写第二段 insert,同一 entry id 插两行会 duplicate loader entry id):
- id: dsh-artifact-linker
name: dsh-artifact-linker
config:
injectRules: true # 常驻 prompt 铁律
clientChips: true # 是否接管正文点击(false = 只留提示词注入,不动鼠标)
allowOutside: true # 是否允许解析 workspace 之外的路径
开关 · 默认 · 置 false / true 的效果
injectRules · true · false = 不注入 prompt 段,模型不再被要求输出绝对路径
clientChips · true · false = 纯注入模式,不接管任何鼠标事件
allowOutside · true · false = 只允许打开 workspace 内的文件
hoverDelayMs · 40 · 悬停判定的节流间隔(毫秒,上限 1000)。0 = 不节流;调大 = 更省 IO,但手型/高亮更迟钝
hoverDelayMs 说明:0.4.2 及更早把这条写死成 150ms,鼠标划过一行文字时高亮会明显慢半拍(150ms ≈ 9 帧),而且判定用的是「排程那一刻」的坐标,不是判定那一刻的坐标,所以还会出现落点不准。0.4.3 起改为可配、默认 40ms(≈2.5 帧,肉眼已跟手),并且每次 mousemove 都会刷新坐标,判定时读最新值。
allowOutside 要特别说一句:产物经常落在项目盘(E:\…、D:\…)而不是 DSH 的 workspace 里,所以默认放开。但它同时是安全闸——挡的是「模型说个 C:\Windows\System32\xxx.exe 就帮你点开」这类情况。自己一个人用保持 true 更顺手;给别人用、或者接入不可信模型,改成 false。
它不做什么
这几条是刻意的,不是没做完:
1. 不改任何 DOM 节点。正文由 React 渲染,给文本包一层 <span> 就是移动 React 的节点,重渲染时整段白屏。本插件全程只读,可点性是「捕获期 click + 光标位置反查」实现的。
2. 不接管 markdown 图片与链接语法。 一旦被改写成芯片,图就不显示了。
3. 不联网、零遥测。全部路由只在 127.0.0.1 回环可用,外部来源一律 403。
4. 不 spawn shell。reveal/open 走 execFile(cmd, args) 参数数组,路径里的 & | ; %VAR% 全是字面量,没有命令注入面。
5. 不猜多候选路径。搜到多个同名文件时把选择权还给人。
6. 不保证文件存在。库里的文件随时可能被删、被移走、被别的进程占用——所以每次打开前都重新 stat,点开发现没了就明说「已不存在」,不会装作没事。
7. 不给不存在的文件画芯片。正文里长得像文件名的片段很多(日志摘录、示例路径、报错原文),只看「形状」就画成可点芯片,点开只会得到一句「没解析到」——那比根本不认识更糟。所以判定加了存在性闸门:悬停时先异步 resolve,只有文件真的在才变手型、画蓝色虚线;确认不是产物的就当普通文字,右键直接放行宿主原生菜单。解析结果进 30 秒 TTL 缓存,反复悬停不重复请求。
排查
症状 · 原因与处置
正文文件名不可点 · ① clientChips 被置 false;② 扩展名是可执行体(exe/msi/dll/sys/so 等,这是唯一仍刻意不认的一类,见 lib/client.js 的 EXEC_EXT);③ 客户端 bundle 没重载——Ctrl+R 不重载客户端 bundle,必须完全重启 DSH
点开提示「解析不到绝对路径」 · 裸文件名不在 workspace 内,且 allowOutside: false。先确认文件真实路径,再决定是否放宽
点开提示「文件已不存在」 · 路径是真的,文件被删/移了。清单里该行会打上删除线
点产物名没反应 · ① 文件不在候选 base 下(访问宿主 /info 看 bases);② 直接看 /resolve?p=<名字> 返回的 reason。0.4.0 起不再有扩展名白名单,任何字母或数字开头的扩展名都会被识别为候选(7z/3mf 这类数字开头的也算),误报由「必须先真实解析到磁盘文件」这道存在性闸门兜掉
右键没出本插件菜单 · 该处文字没解析成文件(同上);对非产物右键本来就不接管,出的是宿主原生菜单。0.3.3 起会先确认文件存在——不存在的片段既不画虚线也不接管右键
日志里出现 N 份相同输出 · 热重载堆叠了多个实例。Ctrl+R 无效,必须完全重启 DSH;同时确认 window.__dshArtifactLinkerV1.dispose() 守卫在
目录
dsh-artifact-linker/ ├── package.json # dsh.bundle + dsh.client(两个都要,缺 bundle 装不上) ├── cordis.patch.yml # 插件注册补丁(entry id = 包名,唯一) ├── lib/ │ ├── index.js # 宿主: …(truncated)
Rendered from the README published on npm. Formatted only, not edited.
Versions
Published on npm::13,最后更新 2026-11-05