dsh-artifact-linker 0.3.9 → 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 +84 -2
- package/README.en.md +1 -1
- package/README.md +3 -3
- package/lib/client.js +107 -33
- package/lib/index.js +41 -4
- package/package.json +2 -2
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,87 @@
|
|
|
1
|
-
# Changelog
|
|
2
|
-
|
|
1
|
+
# Changelog
|
|
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
|
+
|
|
47
|
+
## 0.4.0 — 2026-10-30
|
|
48
|
+
|
|
49
|
+
### 改:彻底去掉扩展名白名单,所有文件类型都能识别、都能走侧边栏预览
|
|
50
|
+
|
|
51
|
+
病根:旧版有两张手写白名单——`PREVIEWABLE`(决定右键菜单里「在侧边栏预览」显不显示)和 `EXT`(决定正文里哪段文字算文件名)。两者都是**对宿主渲染能力的静态猜测**,问题一样:
|
|
52
|
+
|
|
53
|
+
1. 宿主每加一个渲染器就得回来补一次清单,漏一次就表现为「正文里明明有文件名,右键毫无反应」;
|
|
54
|
+
2. 用户装了 `dsh-video-preview` / `@huanlin/...-audio` 这类插件后宿主能力早变了,白名单却停在旧结论上 —— 于是 **mp4 / mp3 / wav 右键完全没有预览入口**,本次就是这条报上来的。
|
|
55
|
+
|
|
56
|
+
### 三处改动
|
|
57
|
+
|
|
58
|
+
| 位置 | 旧 | 新 |
|
|
59
|
+
|---|---|---|
|
|
60
|
+
| `PREVIEWABLE` | 正则白名单,`canPreview` 为 false 就不 push 菜单项 | **整条删除**,「在侧边栏预览」对所有类型无条件显示 |
|
|
61
|
+
| `EXT` | 手写约 130 项扩展名数组,拼进 `RE` | **整条删除**,改为通用扩展名匹配 + `EXEC_EXT` 可执行体黑名单 |
|
|
62
|
+
| 预览点击反馈 | 成功/失败无区分 | 成功 `toast("已在侧边栏打开(.xxx)")`,失败 `toast("侧边栏打不开 .xxx,可改用「用默认程序打开」")` |
|
|
63
|
+
|
|
64
|
+
新的识别规则:**任意 1–12 位扩展名都算候选**,字母开头或数字开头都收(`7z` / `3mf` / `3gp` 这类数字开头的真实格式以前被漏掉)。唯一还刻意排除的是可执行体 / 系统二进制(`EXEC_EXT`:`exe msi msp mst dll sys com scr cpl ocx drv vxd efi so dylib ko elf`)——正文里出现 `xxx.exe` 九成是命令输出或报错原文,而「用默认程序打开」对它们等于**直接运行**,误点代价太大。
|
|
65
|
+
|
|
66
|
+
### 误报怎么兜:不靠白名单,靠存在性闸门
|
|
67
|
+
|
|
68
|
+
候选片段必须先在宿主 `/resolve` 真的解析到磁盘文件,才会变成可点目标。所以散文里的 `e.g.` / `v1.2` / `Node.js` 即使被正则当成候选也没关系:解析不到就静默,用户完全看不出它曾被识别过。这比维护一张永远追不上现实的清单可靠得多。
|
|
69
|
+
|
|
70
|
+
### 验证
|
|
71
|
+
|
|
72
|
+
新增 `_verify/test-ext-regex.mjs`,36 项断言全过:
|
|
73
|
+
- 旧白名单已支持的(`mp4`/`mp3`/`wav`/`pdf`/`xlsx`/`md`/`py`/`cas.h5`/`dat.h5`/带空格文件名)不退化;
|
|
74
|
+
- 旧白名单漏掉的(`mov`/`mkv`/`webm`/`flac`/`m4a`/`7z`/`heic`/`avif`/`mcap`/`parquet`/`3mf`/`usdz`/任意未知扩展名)现在都能命中;
|
|
75
|
+
- 可执行体(`exe`/`msi`/`dll`/`sys`/`so`/`dylib`/`ko`/`elf`/`com`/`scr`/`ocx`/`cpl`)全部不命中。
|
|
76
|
+
|
|
77
|
+
### 顺手修
|
|
78
|
+
|
|
79
|
+
`lib/client.js` 的 `VERSION` 常量一直停在 `0.3.8`,落后 `package.json` 两个版本,现统一为 `0.4.0`。
|
|
80
|
+
|
|
81
|
+
### 已知代价(用户已明确接受)
|
|
82
|
+
|
|
83
|
+
极少数类型点「在侧边栏预览」会得到一句「打不开」。这是刻意的取舍:宁可点了没反应,也不要靠限制类型来回避未来的 bug —— 这样侧边栏以后补了什么能力,本插件零改动自动跟上。
|
|
84
|
+
|
|
3
85
|
## 0.3.9 — 2026-10-29
|
|
4
86
|
|
|
5
87
|
### 修:插件市场看不到下载量 / 版本号 / 搜不到
|
package/README.en.md
CHANGED
|
@@ -55,7 +55,7 @@ Click behaviour is decided by the modifier key — no first-time setup:
|
|
|
55
55
|
|
|
56
56
|
| Item | What it does |
|
|
57
57
|
| --- | --- |
|
|
58
|
-
| **Preview in sidebar** | Opens the file in DSH's built-in right sidebar — the same route as clicking a file link in chat.
|
|
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 |
|
|
59
59
|
| **Open with default app** | Hands the file to the OS association (Explorer / Excel / VLC / …) |
|
|
60
60
|
| **Reveal in Explorer** | Opens the containing folder and selects the file |
|
|
61
61
|
| **Save as…** | Copies the file to a location you pick |
|
package/README.md
CHANGED
|
@@ -51,7 +51,7 @@ DSH 里预览器已经能开图片、代码、pdf、office。但**插件、脚
|
|
|
51
51
|
|
|
52
52
|
| 菜单项 | 作用 |
|
|
53
53
|
|---|---|
|
|
54
|
-
| **在侧边栏预览** | 在 DSH
|
|
54
|
+
| **在侧边栏预览** | 在 DSH 内置右侧边栏里打开,和聊天里点文件链接是同一条通道。**0.4.0 起对所有文件类型一律显示这一项**,不再按扩展名筛:能认领的查看器就渲染(PDF / Word / Excel / PowerPoint / Markdown / 纯文本 / CSV / 图片,装了 video-preview / audio-preview 等插件后还有音视频),认领不了的宿主会退回纯文本或默认程序。副作用是极少数类型点下去可能只得到一句「侧边栏打不开」——这是刻意接受的代价,为的是以后侧边栏补了新能力,本插件零改动自动跟上 |
|
|
55
55
|
| **用默认程序打开** | 交给系统关联(资源管理器 / Excel / 播放器……) |
|
|
56
56
|
| **在文件夹中显示** | 打开所在目录并选中该文件 |
|
|
57
57
|
| **另存为…** | 复制到你指定的位置 |
|
|
@@ -146,10 +146,10 @@ GET http://127.0.0.1:<port>/dsh-artifact-linker/info
|
|
|
146
146
|
|
|
147
147
|
| 症状 | 原因与处置 |
|
|
148
148
|
|---|---|
|
|
149
|
-
| 正文文件名不可点 | ① `clientChips` 被置 false;②
|
|
149
|
+
| 正文文件名不可点 | ① `clientChips` 被置 false;② 扩展名是可执行体(`exe`/`msi`/`dll`/`sys`/`so` 等,这是**唯一**仍刻意不认的一类,见 `lib/client.js` 的 `EXEC_EXT`);③ 客户端 bundle 没重载——**Ctrl+R 不重载客户端 bundle,必须完全重启 DSH** |
|
|
150
150
|
| 点开提示「解析不到绝对路径」 | 裸文件名不在 workspace 内,且 `allowOutside: false`。先确认文件真实路径,再决定是否放宽 |
|
|
151
151
|
| 点开提示「文件已不存在」 | 路径是真的,文件被删/移了。清单里该行会打上删除线 |
|
|
152
|
-
| 点产物名没反应 | ① 文件不在候选 base 下(访问宿主 `/info` 看 `bases`);②
|
|
152
|
+
| 点产物名没反应 | ① 文件不在候选 base 下(访问宿主 `/info` 看 `bases`);② 直接看 `/resolve?p=<名字>` 返回的 `reason`。**0.4.0 起不再有扩展名白名单**,任何字母或数字开头的扩展名都会被识别为候选(`7z`/`3mf` 这类数字开头的也算),误报由「必须先真实解析到磁盘文件」这道存在性闸门兜掉 |
|
|
153
153
|
| 右键没出本插件菜单 | 该处文字没解析成文件(同上);**对非产物右键本来就不接管**,出的是宿主原生菜单。0.3.3 起会先确认文件存在——不存在的片段既不画虚线也不接管右键 |
|
|
154
154
|
| 日志里出现 N 份相同输出 | 热重载堆叠了多个实例。**Ctrl+R 无效,必须完全重启 DSH**;同时确认 `window.__dshArtifactLinkerV1.dispose()` 守卫在 |
|
|
155
155
|
|
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.
|
|
32
|
+
var VERSION = "0.4.2";
|
|
33
33
|
// 路由前缀必须与宿主的 ROUTE_PREFIX 一致;永远根相对,绝不拼宿主 origin
|
|
34
34
|
var ROUTE = "/dsh-artifact-linker";
|
|
35
35
|
|
|
@@ -61,11 +61,21 @@ window.__ModuleLoader__.load({
|
|
|
61
61
|
return sessionFileAddress(sessionId, normalized);
|
|
62
62
|
}
|
|
63
63
|
|
|
64
|
-
//
|
|
65
|
-
//
|
|
66
|
-
//
|
|
67
|
-
//
|
|
68
|
-
|
|
64
|
+
// 🔴 「在侧边栏预览」不再有类型白名单(2026-10-30 用户明确要求)。
|
|
65
|
+
//
|
|
66
|
+
// 旧版这里有一张 PREVIEWABLE 白名单,只放「宿主 documentpreview 当时能渲染」的
|
|
67
|
+
// 扩展名,音视频/压缩包被排除,右键它们根本没有预览入口。那个设计错在:
|
|
68
|
+
// 1) 白名单是**对宿主渲染能力的静态猜测**。宿主每加一个渲染器就得回来改一次;
|
|
69
|
+
// 用户装了 video-preview / audio-preview 这类插件后宿主能力早变了,
|
|
70
|
+
// 白名单却停在旧结论上 → 视频、mp3、wav 右键毫无入口(本次的直接病根)。
|
|
71
|
+
// 2) 「能不能渲染」本就不该由客户端猜。判定方是宿主
|
|
72
|
+
// sidebarRight.openResource:能认领就渲染,认领不了它自己退回默认程序。
|
|
73
|
+
//
|
|
74
|
+
// 现在的契约:**菜单项对所有类型一律显示,点了才见真章**。
|
|
75
|
+
// openInSidebar 返回 true → toast「已在侧边栏打开」
|
|
76
|
+
// openInSidebar 返回 false → toast「侧边栏打不开这个文件(.xxx)」
|
|
77
|
+
// 这样以后侧边栏补了什么能力,本插件零改动自动跟上,不会被旧白名单漏掉。
|
|
78
|
+
// 代价:极少数类型点了会得到一句「打不开」——用户已明确接受。
|
|
69
79
|
|
|
70
80
|
function extOf(p) {
|
|
71
81
|
var base = String(p).split(/[\\/]/).pop() || "";
|
|
@@ -74,24 +84,29 @@ window.__ModuleLoader__.load({
|
|
|
74
84
|
}
|
|
75
85
|
|
|
76
86
|
|
|
77
|
-
//
|
|
78
|
-
//
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
87
|
+
// ── 正文里「长得像文件名」的判定 ────────────────────────────────────────
|
|
88
|
+
//
|
|
89
|
+
// 🔴 2026-10-30 起**不再维护扩展名白名单**(用户明确要求:所有文件类型都要能识别、
|
|
90
|
+
// 都能走侧边栏预览)。旧版是一张手写清单,问题是它永远追不上现实:
|
|
91
|
+
// 新格式(heic / avif / mcap / parquet / 3mf / usdz…)每出现一个就得回来补一次,
|
|
92
|
+
// 漏掉的表现就是「正文里明明有文件名,右键毫无反应」——本次音视频/压缩包
|
|
93
|
+
// 的抱怨就是这么来的。
|
|
94
|
+
//
|
|
95
|
+
// 现在的规则:**任意 1–8 位、以字母开头的扩展名都算**,只用一个黑名单挡掉
|
|
96
|
+
// 可执行体与系统二进制。为什么只挡这些:
|
|
97
|
+
// * 正文里出现 xxx.exe / xxx.dll 九成是命令输出、报错原文、依赖清单,
|
|
98
|
+
// 不是「要打开看的产物」;
|
|
99
|
+
// * 「用默认程序打开」对它们等于**直接运行**,误点代价太大。
|
|
100
|
+
// 真要放开,把下面对应的词删掉即可。
|
|
101
|
+
//
|
|
102
|
+
// 误报怎么兜?——不靠白名单,靠**存在性闸门**:候选片段必须先在宿主
|
|
103
|
+
// /resolve 真的解析到磁盘文件,才会变成可点目标(见下方 verify())。
|
|
104
|
+
// 所以把 e.g. / v1.2 这种散文里的点号误当成扩展名也没关系:解析不到就静默,
|
|
105
|
+
// 用户完全看不出它曾被识别过。这比维护一张永远不全的清单可靠得多。
|
|
106
|
+
var EXEC_EXT = /^(exe|msi|msp|mst|dll|sys|com|scr|cpl|ocx|drv|vxd|efi|so|dylib|ko|elf)$/i;
|
|
92
107
|
|
|
93
108
|
// 一段文本里「像产物路径」的片段:
|
|
94
|
-
// 可选的前缀(盘符 或 UNC)+ 中间任意非分隔字符 + 点 +
|
|
109
|
+
// 可选的前缀(盘符 或 UNC)+ 中间任意非分隔字符 + 点 + 扩展名
|
|
95
110
|
// + 可选的多级尾巴(Fluent 的 xxx.cas.h5 / xxx.dat.h5 必须整体吃掉,
|
|
96
111
|
// 否则会被截成 xxx.cas 去撞一个不存在的文件 → 点了没反应)
|
|
97
112
|
// 末尾 (?![A-Za-z0-9_]) 防止 package.lock / a.python 这种半截匹配。
|
|
@@ -104,8 +119,14 @@ window.__ModuleLoader__.load({
|
|
|
104
119
|
// 解析必然失败 → 点上去毫无反应。这是「带空格文件名点了没反应」的真正根因。)
|
|
105
120
|
var RE = new RegExp(
|
|
106
121
|
"(?:[A-Za-z]:[\\\\/]|\\\\\\\\[^\\\\/\\s]+[\\\\/][^\\\\/\\s]+)?" +
|
|
107
|
-
"[^\\t\\r\\n`\"'<>|*?()()【】\\[\\]{},;、]{1,
|
|
108
|
-
|
|
122
|
+
"[^\\t\\r\\n`\"'<>|*?()()【】\\[\\]{},;、]{1,1024}?\\." +
|
|
123
|
+
// 扩展名两种写法都收:
|
|
124
|
+
// 字母开头 1–12 位(pdf / mp4 / catproduct / properties…)
|
|
125
|
+
// 数字开头 2–12 位(7z / 3mf / 3gp / 8bf…)—— 必须 ≥2 位,否则
|
|
126
|
+
// 散文里的 v1.2 会贡献一个「扩展名 2」,虽然最后也会被存在性闸门静默掉,
|
|
127
|
+
// 但没必要让它进候选池。
|
|
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_\\\\/]|\\.[A-Za-z0-9])",
|
|
109
130
|
"gi"
|
|
110
131
|
);
|
|
111
132
|
// URL 里天然带 .png/.pdf,全部要排除,否则网页链接会被当成文件产物。
|
|
@@ -187,11 +208,50 @@ window.__ModuleLoader__.load({
|
|
|
187
208
|
var text = range.startContainer.nodeValue || "";
|
|
188
209
|
if (!text) return null;
|
|
189
210
|
var off = range.startOffset;
|
|
190
|
-
var from = Math.max(0, off -
|
|
191
|
-
var to = Math.min(text.length, off +
|
|
211
|
+
var from = Math.max(0, off - 1024);
|
|
212
|
+
var to = Math.min(text.length, off + 1024);
|
|
192
213
|
var win = text.slice(from, to);
|
|
193
214
|
var rel = off - from;
|
|
194
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
|
+
// ── 兜底:裸文件名 / 相对路径(无盘符),仍用扩展名形状识别,交后端 ①②③ 兜 ──
|
|
195
255
|
var m;
|
|
196
256
|
RE.lastIndex = 0;
|
|
197
257
|
while ((m = RE.exec(win))) {
|
|
@@ -199,6 +259,9 @@ window.__ModuleLoader__.load({
|
|
|
199
259
|
var e = m.index + m[0].length;
|
|
200
260
|
if (rel < s || rel > e) continue; // 鼠标必须落在这个片段上
|
|
201
261
|
if (inRanges(urls, s, e)) continue; // URL 里的 .png 不算
|
|
262
|
+
// 可执行体黑名单:这是唯一还刻意不认的类型(理由见上方 EXEC_EXT 注释)。
|
|
263
|
+
// 多级后缀取第一段判断,xxx.exe.bak 也算可执行体,宁可漏认不误点。
|
|
264
|
+
if (m[1] && EXEC_EXT.test(m[1])) continue;
|
|
202
265
|
var raw = cleanRaw(m[0]);
|
|
203
266
|
if (!raw || raw.length < 4) continue;
|
|
204
267
|
return { raw: raw, rect: rectFor(range.startContainer, from + s, from + e) };
|
|
@@ -379,11 +442,13 @@ window.__ModuleLoader__.load({
|
|
|
379
442
|
// 文案要说人话:「资源管理器」这个词很多用户不认识,改成「文件夹窗口」。
|
|
380
443
|
// 顺序按「最常做的动作」排,并用 "sep" 占位分成三组:
|
|
381
444
|
// 看(侧边栏预览 / 打开 / 定位)→ 搬运(另存为 / 复制文件 / 复制路径)→ 喂给 AI(添加到对话框)
|
|
382
|
-
//
|
|
383
|
-
//
|
|
384
|
-
|
|
445
|
+
//
|
|
446
|
+
// 🔴 「在侧边栏预览」对所有文件类型**无条件出现**(2026-10-30 用户明确要求)。
|
|
447
|
+
// 旧版按 PREVIEWABLE 白名单决定显不显示,音视频/压缩包因此完全没有入口。
|
|
448
|
+
// 现在一律显示,点下去由宿主 sidebarRight.openResource 决定能不能认领:
|
|
449
|
+
// 认领得了就渲染,认领不了 openInSidebar 返回 false,我们照实提示「打不开」。
|
|
385
450
|
var items = [];
|
|
386
|
-
|
|
451
|
+
items.push(["preview", "在侧边栏预览", "在右侧边栏里直接看,不用切出去;这个类型如果侧边栏渲染不了会如实提示"]);
|
|
387
452
|
items.push(["open", "用默认程序打开", "按文件类型用对应软件打开:PDF 用阅读器、表格用 Excel 那类"]);
|
|
388
453
|
items.push(["reveal", "在文件夹中显示", "打开 Windows 文件夹窗口,并选中这个文件"]);
|
|
389
454
|
items.push(["sep", "", ""]);
|
|
@@ -420,8 +485,10 @@ window.__ModuleLoader__.load({
|
|
|
420
485
|
|
|
421
486
|
// ① 侧边栏预览 —— 纯客户端,走宿主导航入口
|
|
422
487
|
if (action === "preview") {
|
|
423
|
-
|
|
424
|
-
|
|
488
|
+
// 对所有类型都尝试,成败照实说。带扩展名,用户才知道是哪个类型没成。
|
|
489
|
+
var pext = extOf(abs);
|
|
490
|
+
if (openInSidebar(abs)) toast("已在侧边栏打开" + (pext ? "(." + pext + ")" : ""));
|
|
491
|
+
else toast("侧边栏打不开" + (pext ? " ." + pext : "这个文件") + ",可改用「用默认程序打开」");
|
|
425
492
|
return;
|
|
426
493
|
}
|
|
427
494
|
|
|
@@ -641,7 +708,14 @@ window.__ModuleLoader__.load({
|
|
|
641
708
|
if (disposed) return;
|
|
642
709
|
if (!rec.ok) { setCursor(""); hideHover(); return; }
|
|
643
710
|
setCursor("pointer");
|
|
644
|
-
|
|
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);
|
|
645
719
|
});
|
|
646
720
|
}, 150);
|
|
647
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
|
-
|
|
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.
|
|
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",
|
|
@@ -64,4 +64,4 @@
|
|
|
64
64
|
"@deepseek-ai/cordis": "^4.0.2",
|
|
65
65
|
"@deepseek-ai/dsh": "\u003e=0.2.1-rc.3"
|
|
66
66
|
}
|
|
67
|
-
}
|
|
67
|
+
}
|