dsh-file-menu
lemon0-00/dsh-file-menu
给 DSH 聊天里的交付文件卡片、改动卡片、行内文件引用与 http(s) 链接加上右键菜单。动作包括用默认程序打开、用系统「打开方式」对话框打开、在文件管理器中定位、复制文件本体、复制文件路径、另存到指定文件夹、下载,以及用浏览器打开链接。交付卡片走宿主从会话日志反查的会话坐标;行内引用与改动卡片走宿主要求复核的绝对路径。
安装
dsh plugin --profile web add github:lemon0-00/dsh-file-menuREADME
dsh-file-menu — 交付文件右键菜单
v1.1.2 — 「已编辑 xxx」改动卡片现在也能右键(见 1.4);v1.1.1 修掉了「用浏览器打开」会开出两个相同页面的 bug(见 1.3)。
在 DeepSeek Harness 的聊天界面里,对着生成的文件卡片点右键,直接打开、复制文件本体、复制路径、定位文件夹、另存为或下载 —— 不用先去点链接、也不用去资源管理器里找文件。
同一个菜单也覆盖对话正文里的行内文件引用和带 href 的网址链接。
- 宿主半(Host):
index.js,注册四条鉴权路由api/dsh-file-menu.info/.act/.raw/.path - 浏览器半(Client):
client.js,占用deliverables.file.actions(触发按钮)与shell.overlay(菜单浮层) - 右键即开:菜单在按下的那一帧就完整画出来,不等待任何网络请求
0. 四个右键命中面
| 右键目标 | 菜单内容 | 坐标从哪来 |
|---|---|---|
| 交付文件卡片 | 完整九项文件动作 | sessionId + seq + index(宿主反查) |
| 已编辑改动卡片(「已编辑 config.json」) | 只读文件动作子集 | aria-describedby 指向的隐藏 <span> |
| 正文行内文件引用 | 只读文件动作子集 | 元素 title 里的绝对路径 |
| 正文里的网址链接 | 复制链接地址、用浏览器打开 | 元素 href |
后三者的命中依据都是元素自身的语义属性(aria-describedby、title、href),不是哈希类名,因此不会随官方样式改版失效。见第 1.2 节与第 1.4 节。
1. 菜单能做什么
| 菜单项 | 行为 | 依赖 |
|---|---|---|
| 打开文件 | 用系统默认程序打开(.docx→Word、.zip→资源管理器/解压工具…) | Host 桌面可用 |
| 用其他应用打开… | 拉起系统自己的「打开方式」对话框(Windows:rundll32 shell32.dll,OpenAs_RunDLL <文件>),从中任选一个程序 | 同上 |
| 打开文件所在文件夹 | 资源管理器里定位并选中该文件 | 同上 |
| 复制文件 | 把文件本体放进系统剪贴板(Windows 用 Set-Clipboard,macOS 用 osascript) | 同上 |
| 复制文件路径 | 复制完整本地路径 | 无 |
| 另存为… | 弹原生目录选择框,另存到指定文件夹 | 目录选择器插件 |
| 下载文件 | 浏览器下载,文件流式传输(不占内存) | 无 |
| 复制下载链接 | 复制该文件的下载 URL | 无 |
| 在新标签页中打开下载链接 | 新标签页直接下载 | 无 |
菜单项不逐条列出具体应用(不出现「用 Word 打开」「用 WPS 打开」这类行):那需要宿主把系统注册的处理程序枚举出来,而这个枚举在 Windows 上对 .docx / .xlsx 会整体失败(见 1.0)。取而代之的是始终存在的「用其他应用打开…」,它走系统自带的对话框,任何扩展名都可用。菜单行左侧带一个 16px 的线性图标;菜单表头只显示文件名与「大小 · 目录」,不带扩展名方块。
降级链是自动的:没有桌面(纯网页 / 远程)时「打开/复制文件/定位文件夹」显示为不可用并给出说明;「复制文件」在宿主剪贴板失败时自动退化为浏览器剪贴板(图片/文本类),再失败就退化为复制路径;「另存为」没有原生选择器时退化为浏览器下载。常见类型 .docx/.xlsx/.pdf/.exe/.zip/.rar 全部按文件本体处理,不做类型白名单。
1.0 为什么没有逐条列出具体应用
最早的设计是让宿主枚举「这个扩展名当前有哪些处理程序」,每个程序渲染成一行。实测发现这条路在 Windows 上不可靠:SHAssocEnumHandlers 枚举出的第一个 handler,在 .docx / .xlsx 上调用 GetName 会抛 E_FAIL(COM 层报 Error HRESULT E_FAIL has been returned from a call to a COM component.),而官方实现没有做单条容错,于是整次枚举抛出、列表变空。本机实测:
| 扩展名 | 枚举结果 |
|---|---|
.zip | 正常,8 条(Bandizip / WPS 解压 / 资源管理器 / 记事本 / 百度网盘 / QQ / Word / WMP) |
.pdf | 正常,10 条(Edge / WPS PDF / …) |
.txt | 正常,7 条 |
.docx | 失败(E_FAIL) |
.xlsx | 失败(E_FAIL) |
这不是本插件的 bug,也不是打开功能坏了 —— 默认程序查询(AssocQueryString)本身是好的,「打开文件」照常调用 Word。但一个「有时有、有时没有」的应用列表对用户没有价值,所以本插件彻底去掉了逐应用行,只保留一行「用其他应用打开…」:
- 它调用
rundll32.exe shell32.dll,OpenAs_RunDLL <文件>,拉起的就是资源管理器右键菜单里那个「打开方式」对话框; - 走的是另一条 Shell 代码路径,不受上面那个 E_FAIL 影响,所以
.docx/.xlsx/.zip/.pdf全部一样可用; - 对话框里列出的程序由系统自己决定,比插件枚举的任何列表都更准。
宿主半因此也从四条路由减到三条(应用列表那条已删除)——这一轮又加回第四条,但用途不同,见下一节。
1.1 为什么右键是「秒开」
菜单里每一项都只靠 sessionId + seq + index 三元坐标驱动,没有任何一项需要先知道文件名。所以菜单在右键那一帧就把全部项目画出来并落在鼠标位置,元数据只是随后把表头(文件名 / 大小 / 目录)补上:
| 环节 | 做法 |
|---|---|
| 首帧 | 乐观渲染全部菜单项 + 用估算尺寸同步落位,不显示任何「正在读取…」占位 |
| 表头占位 | 表头位置预先留好高度(两条占位色块),名字到达时原地填入,菜单高度不变、内容不下跳 |
| 元数据 | api/dsh-file-menu.info 独立请求,到达后只精修表头 |
| 预取 | 卡片滚入视野或鼠标划过时,就在浏览器空闲时提前读它的表头(并发上限 2,同一卡片只读一次) |
| 缓存 | info 路由有一层客户端缓存(按坐标),重复右键同一张卡片零请求 |
| 无慢探测 | 菜单不再依赖任何可能耗时数十秒的系统查询,首帧即终态 |
预取让「第一次右键」也拿到完整表头:卡片在屏幕上时它的元数据往往已经读好了,右键那一帧表头就是填满的。若某张卡片还没来得及预取(例如刚滚入就立刻右键),菜单仍会立刻完整出现,表头先是两道占位色块、随后原地补上名字 —— 因为位置已预留,观感是「渐显」而不是「往下跳」。预取失败不会被记录,也不会打扰用户;真正右键时若读不到,菜单照常报错。
历史背景:Windows 上枚举关联应用需要全量扫描注册表(本机实测 reg.exe query HKLM\SOFTWARE\Classes /s 约 88 秒)。改造前它挡在 info 的响应里,右键后菜单要等它;后来拆成独立路由;现在连同那条路由一起去掉了。
1.2 正文里的行内引用与网址
除了交付卡片,菜单还接管聊天里的三类目标。它们的命中依据都是元素自身的语义属性,不碰哈希类名,也不进别人组件内部:
| 目标 | 怎么认出来 | 拿到的值 | 可用动作 |
|---|---|---|---|
| 已编辑改动卡片 | 卡片带 data-changed-files,其按钮的 aria-describedby 指向的隐藏 <span> 是绝对路径 | 绝对路径 | 同下一行(活动作集完全一致) |
| 行内文件引用 | 元素带 title,且值是一个存在的绝对路径 | 绝对路径 | 打开文件、用其他应用打开…、打开所在文件夹、复制文件、复制文件路径 |
| 网址链接 | 元素是 <a>,href 的协议是 http: / https: | URL | 复制链接地址、用浏览器打开 |
改动卡片与行内引用共用同一条路由 api/dsh-file-menu.path,也共用同一个收窄后的动作集 —— 它同样没有坐标,同样只递过来一个字符串,所以适用同一套限制。详见 1.4。
行内文件引用与改动卡片只使用一条路由 api/dsh-file-menu.path,它比卡片那条窄:
| 能力 | 卡片(坐标) | 行内引用 / 改动卡片(路径) |
|---|---|---|
| 打开 / 用其他应用打开 / 定位 / 复制文件本体 | ✅ | ✅ |
| 复制文件路径 | ✅ 宿主回 | ✅ 不走宿主,浏览器直接复制手上的字符串 |
| 另存为 | ✅ | ❌ 拒绝(400) |
| 下载 / 复制下载链接 / 新标签页打开 | ✅ | ❌ 拒绝(400) |
| 请求方法与参数 | GET/POST,坐标三元组 | 仅 POST,单个 path |
收窄的理由:卡片能给坐标,是会话日志授予的能力——宿主先 readEvent 取出那一条 deliverables/presented 记录,客户端伪造不了;而路径只是渲染进程递过来的一句话。若让这条路也能流式下载,它立刻变成一个「读任意文件」的通用接口。所以按「能造成多大后果」分级:会移动/复制字节(下载、另存为)的留给卡片;不搬字节、只作用在用户刚点的那个文件上的(打开、定位、复制本体、复制路径)才开放给路径。唯一没被砍的「复制文件本体」是因为它就是用户点右键最想要的动作,且它的作用范围限于那一个已校验存在的规范文件。
路径的校验链(resolveLiteral,全部不过就拒):
- 非空、字符串、长度 ≤ 4096、不含换行或 NUL —— 否则
400 - 必须是绝对路径 —— 否则
400 realpath必须存在 —— 否则404realpath的结果必须与请求字面量逐字相等 —— 否则422。这条挡掉路径变体:符号链接、junction、大小写变体、..折返都会落到另一个规范路径上,宁可拒绝也不静默作用到别的文件- 必须是普通文件(目录 →
422) fs.processPathFromHostPath+fs.resolve反查必须映射回同一路径 —— 否则422(与官方openWorkspacePath的verifyDesktopPath同款校验)
还有一处按设计不会响:正文里那种不带 href 的文件链接按钮(官方渲染成 <code><button class="…_fileMention">,只有 class 和 title,且只在 settled 渲染时出现)——本插件读它的 title,但这依赖官方继续把路径放在 title 里。这是唯一一处「靠官方属性约定」而不是靠元素语义的地方,已在已知边界标注。(「已编辑」改动卡片曾经也属于这一类,v1.1.2 起改用它的 aria-describedby 契约,见 1.4。)
1.3 为什么「用浏览器打开」只开一个页面(v1.1.1)
曾经它会开两个相同页面。根因不在插件判断错了,而在 window.open 的返回值被规范刻意设计成了一个不能用来判断成败的值。
HTML 标准的 window open steps 原文:
If noopener is true and target is not an ASCII case-insensitive match for
_self,_parent, or_top, then return null.
noreferrer 同样会强制 noopener。所以下面这行成功打开时也返回 null:
window.open(url, '_blank', 'noopener,noreferrer'); // 恒为 null
而旧代码把它当成「被拦截了」,于是回退再点一次 <a target="_blank">:
| 旧路径 | 发出去的 new-window 请求 |
|---|---|
window.open(...) —— 其实成功了 | 第 1 个 |
opened === null 恒成立 → 回退点 <a> | 第 2 个 |
桌面端主窗口的 setWindowOpenHandler 把每一个 http(s) new-window 请求都转成 shell.openExternal,于是每个请求在浏览器里各开一页 —— 两次请求,两个页面。这条 bug 同时影响「用浏览器打开」和「在新标签页中打开下载链接」,因为二者共用同一个 openInNewTab。
现在的做法是只剩一条路:直接点一个 <a target="_blank" rel="noopener noreferrer">。它提供同样的保证(独立浏览上下文、新页面拿不到 window.opener、不带 Referer),但没有任何返回值可以被误读,所以不存在第二次尝试。noreferrer 本身已隐含 noopener,两个都写出来是为了避免日后删掉其中一个时悄悄恢复 opener 泄漏。
回归防护在 _check/client-render-harness.mjs:点击「用浏览器打开」与「在新标签页中打开下载链接」后,断言 new-window 请求恰好 1 个。_check/double-open-mutation.mjs 会把这个函数改回旧写法再跑一遍,确认那些断言确实会失败(旧写法下报出 2 个请求)——只有这一步能证明测试不是摆设。
1.4 「已编辑」改动卡片为什么曾经右键无效(v1.1.2)
聊天里的 「已编辑 config.json」 卡片是第三种表面,和交付卡片、正文行内引用都不一样:
| 交付卡片 | 正文行内引用 | 已编辑改动卡片 | |
|---|---|---|---|
| 数据来源 | deliverables/presented | 渲染器给的 title | workspace/changes 摘要 |
| 路径放在哪 | 坐标 sessionId+seq+index | title 属性 | aria-describedby → 隐藏 <span> 的文本 |
| v1.1.1 及以前 | ✅ 认得出 | ✅ 认得出 | ❌ 认不出 |
根因:这条卡片的文件路径既不在坐标里,也不在 title 里——它整张卡片没有任何 title,所以只读 title 的 inlinePathAt 完全看不到它。它对外公布路径的唯一方式是无障碍约定:按钮上的 aria-describedby 指向一个 hidden 的 <span>,那个 <span> 的文本就是解析后的绝对路径(resolveWorkspacePath(cwd, path))。这是屏幕阅读器念出文件名的方式,和本插件在别处依赖 title、href 属于同一类「元素自身的语义属性」,因此不涉及哈希类名。
修法:新增 changedFilePathAt,先靠卡片自带的 data-changed-files 标记确认「这确实是改动卡片」(只有该组件会设这个标记,别处不可能被误认),再顺着 aria-describedby 取出绝对路径。两种形态都覆盖:
- 单文件卡片:属性在唯一的标题按钮上;另外,右键落在卡片内边距上时,整张卡片只可能描述一个文件,所以回退到那唯一一个;
- 多文件卡片:属性在每一行按钮上,各描述各的文件 —— 断言了右键第二行拿到的是第二行的文件,不是第一行的。
不认的情况(有意为之):aria-describedby 里不是绝对路径(例如 cwd 未加载时 resolveWorkspacePath 原样返回相对名)就不认。宿主那条路只接受真实存在的绝对路径,认了也只会换来一次 400,不如不弹。
实测本机 45 个会话全部记录了
cwd(E:\文档\deepseek-harness\default-workspace),所以实际路径是绝对的,卡片可正常认领。
这条路的动作集与行内引用完全相同(open / openWith / reveal / copy + 浏览器侧复制路径),不含 download、saveAs、下载链接 —— 理由见 1.2 的安全模型:只有会话日志授予坐标的交付卡片才允许搬字节。
回归防护:_check/client-render-harness.mjs 里 10 条断言(单文件 / 多文件行 / 卡片内边距 / 多文件标题不认领 / 相对路径不认领);_check/changed-card-mutation.mjs 把这段分支删掉再跑,确认其中 6 条「必须认领」的断言确实失败,并显式标注另外 2 条是真空通过(什么都不认领时它们必然为真)——把这件事写出来,比让它悄悄混在绿灯里好。
2. 安装
插件目录:E:\文档\deepseek-harness\default-workspace\dsh-file-menu
plugin_manager(action: "install_bundle",
target: "E:\\文档\\deepseek-harness\\default-workspace\\dsh-file-menu")
首次安装无需重启:装完 include:dsh-file-menu 即为 active。
但之后修改 index.js(宿主半)需要重启 DeepSeek Harness 进程才生效。原因是插件的宿主半由 Node 的 ESM loadCache 按 URL 缓存,而本 profile 的 hmr 配的是 root: [](不监听任何模块文件),所以文件改动不会触发缓存清理 —— 用 Plugin Manager 的 enable/disable 循环也只重建 entry,拿到的仍是旧的 index.js 命名空间。client.js(浏览器半)不受此限,刷新页面即可。
已装一份、只想改客户端样式或菜单文案 → 改
client.js后按F5。 改的是路由、动作或任何index.js逻辑 → 退出并重新打开 DeepSeek Harness。
手工等价做法:把包链接进当前 profile 的 node_modules,并在 profile 的 package.json 里登记。
$profile = "C:\Users\ASUS\.dsh\profiles\desktop"
pnpm --dir $profile add "link:E:\文档\deepseek-harness\default-workspace\dsh-file-menu"
# 然后把 "dsh-file-menu" 追加到 profile package.json 的 dsh.profile.bundles 数组
验证安装:
plugin_manager(action:"list_plugins")里有include:dsh-file-menu,状态enabled+fiberPhase: "active"- 右侧栏 → 插件管理里能看到「文件右键菜单」
- 打开任意带交付文件卡片的会话,卡片操作行里多出一个
⋯按钮
卸载:
plugin_manager(action: "remove_bundle", target: "dsh-file-menu")
从 GitHub 安装
源码已发布在 https://github.com/lemon0-00/dsh-file-menu。除了上面的本地 link: 方式,也可以直接按仓库安装:
# 方式一:交给 dsh CLI(会把仓库检出到 profile)
dsh plugin --profile desktop add github:lemon0-00/dsh-file-menu
# 方式二:用 Plugin Manager 按安装规格装入
plugin_manager(action: "install_bundle",
target: "github:lemon0-00/dsh-file-menu")
从仓库安装的是快照(市场/CLI 会记录 commit),此后改动不会自动跟随;要边改边试就用本节的 link: 方式。
3. 使用
- 让 dsh 生成或发给你一个文件(用
present产出的交付卡片,或任何带文件卡片的回复)。 - 在这张卡片上任意位置点右键(卡片缩略图、文件名、空白处都行),或者点操作行里的
⋯按钮。 - 在弹出菜单里选一项,例如「打开文件」。
- 反馈会以短提示条的形式出现在菜单底部(「已打开」「已复制」「已复制路径」…),失败则给出具体原因。
正文里也一样:对着文件名或路径右键(例如回复里出现的 E:\…\report.docx)得到文件菜单;对着网址右键得到「复制链接地址」「用浏览器打开」。这三类目标之外的普通文字,右键仍然是浏览器/原生菜单,不被拦截。
快捷键与交互:Esc 关闭菜单;点击菜单外任意位置关闭;滚动或改变窗口大小自动关闭;菜单贴边时自动向内收,不会跑出屏幕。
4. 结构
dsh-file-menu/
├── package.json # dsh.bundle.patch + dsh.client 声明
├── cordis.patch.yml # 把 index.js 作为 Host 半插入
├── index.js # Host 半:4 条鉴权路由、坐标校验、字面路径校验、桌面动作、系统「打开方式」对话框
├── client.js # 浏览器半:触发按钮、右键绑定、菜单浮层、样式
├── icon.svg
├── LICENSE
├── locale/zh.json
├── locale/en.json
└── README.md
关于 repository 字段(身份,不是装饰)
package.json 里的 repository 指向 https://github.com/lemon0-00/dsh-file-menu,这个字段有实际作用:插件市场用它判断「本地这个 link: 安装对应目录里的哪一条插件」。
没有它时,市场只能按名字猜——凡是同名条目,就点亮「✓ 已安装」,并出现「换用线上版本」按钮。目录里恰好有一个第三方同名插件 dsh-file-menu(作者 dreammaker2016,npm 名 @z1995418/dsh-file-menu,见 https://github.com/dreammaker2016/dsh-file-menu),于是本插件曾被误认成它。
有了 repository 之后,市场走强身份分支:拿本仓库 owner/repo 去比对目录条目,匹配不上就明确判定 repo-mismatch("精选目录里有同名插件,但与你本地仓库不一致,无法安全恢复"),不再按下那个按钮就装进别人的包。可用 _check/identity-check.mjs 与 _check/market-view-check.mjs 复现这条判定。
安全模型
交付卡片那条路(info / act / raw)客户端不被信任:只接受 sessionId + seq + index 三元坐标,宿主侧再走一遍官方同款校验链 —— sessionQuery.readEvent 取出该会话事件里 deliverables/presented 的 data.files[index].path,再用 workspaceFiles.stat 求绝对路径,最后用 fs.processPathFromHostPath + fs.resolve 反查确认映射回原路径,任何一步不过就 404/422。客户端无法自造任意路径来读写磁盘。
行内引用那条路(path)按你的选择放宽到任意绝对路径,因此代价与收窄手段必须写清楚:
- 它接受请求方给的任意绝对路径,只要求该路径在宿主上真实存在、是普通文件、且
realpath结果与字面量逐字一致(挡掉符号链接、junction、大小写与..变体)。 - 它只开四个动作:
open/reveal/openWith/copy(复制文件本体)。saveAs与任何字节流都不在这条路上 ——download只存在于卡片路由raw。 - 它只注册
POST;路径长度上限 4096,拒绝换行与 NUL;不足 4096 的字符串一律先过上面六道校验。 - 它不写盘、不删除、不移动:四个动作里唯一触碰剪贴板的是
copy,唯一执行外部程序的是open/openWith/reveal,且都复用官方openWorkspacePath与系统rundll32的既有能力,没有引入新的执行面。 - 残余风险要说实话:渲染进程若被完全攻破,攻击者可以借这条路打开、定位或复制任意一个它已知道路径的文件。这比「能读任意字节」小得多,但不是零 —— 这也正是
download与「另存为」坚决没开在这条路上的原因。
5. 排障
| 现象 | 原因 / 处理 |
|---|---|
| 卡片上右键没反应 | 该卡片不是交付卡片,也不是改动卡片、正文里的文件引用/网址。确认插件为 active。 |
| 行内文件引用右键没反应 | 该引用不是绝对路径(例如只有相对路径、或路径不存在)。宿主只认真实存在的规范绝对路径。 |
| 「另存为」「下载」在行内引用上不见了 | 设计如此,见安全模型:这两项会搬字节,只对交付卡片开放。想要它们就对着交付卡片右键。 |
| 网址右键只有两项 | 设计如此:链接只提供「复制链接地址」与「用浏览器打开」,不做文件动作。 |
| 「打开文件」是灰的 | 宿主桌面不可用(纯网页端/远程)。改用「复制文件路径」「下载文件」「复制下载链接」。 |
| 看不到「用 Word 打开」这类行 | 设计如此,见 1.0。请用**「用其他应用打开…」**,它在对话框里列出系统认定可用的程序。 |
| 「用其他应用打开…」没弹出对话框 | 仅 Windows 支持(走 rundll32 shell32.dll,OpenAs_RunDLL),且需要 Host 桌面可用;纯网页端会返回 409。 |
| 「复制文件」提示只能复制路径 | 非 Windows/macOS,或宿主剪贴板命令被策略拦截。路径已复制,可直接粘贴。 |
| 「另存为」变成下载 | 未安装原生目录选择器插件(@deepseek-ai/dsh-host-directory-picker-auto)。 |
| 菜单样式错乱 | 与本机主题 token 冲突;本插件只用 --dsw-alias-* 变量并全部收敛在 .dfm-* 前缀下,不覆盖任何全局样式。 |
| 表头先显示两道灰色占位条 | 该卡片还没预取到(刚滚入就右键)。位置已预留,名字会原地填入、不跳版;稍等即可,之后再右键这张卡就是瞬时完整的。 |
改了 index.js 但行为没变 | 见第 2 节:宿主半改动必须重启 DeepSeek Harness 进程。 |
| 「用浏览器打开」开出两个页面 | v1.1.1 已修。若在旧版上见到,原因见 1.3:window.open(...,'noopener') 成功时也返回 null,旧代码误判为被拦截又开了一次。刷新页面即可加载修复后的 client.js。 |
| 「已编辑 config.json」卡片右键没反应 | v1.1.2 已修。旧版只读 title,而这条卡片把路径放在 aria-describedby 里,见 1.4。刷新页面即可。若刷新后仍不弹,说明 aria-describedby 指向的不是绝对路径(通常是会话 cwd 未加载,相对名会被有意拒绝)。 |
6. 已知边界
- 行内文件引用的识别依赖官方把绝对路径放在
title里。 交付卡片命中的是官方data-presented-file标记与坐标,行内引用没有这类data-*(chat-client.js里data-ref-chip计数为 0),只能读title。这是本插件唯一一处「依赖官方属性约定」的地方;若官方某天改变这个约定,表现是行内引用右键静默失效(菜单不出现,其余功能与原生右键全部照常),不会出错、不会弹错菜单、不会动错文件。 - 改动卡片的识别依赖官方继续用
data-changed-files+aria-describedby描述文件。 若这个约定变了,表现同样是该卡片右键静默失效,其余照常。另需知道:描述符里的路径由resolveWorkspacePath(cwd, path)合成,cwd尚未加载时它是相对名,本插件会有意拒绝(宿主只认绝对路径,认了也是 400)。实测本机 45 个会话均已记录cwd,故实际不受影响。 - 不逐条列出具体应用。 原因见 1.0:宿主枚举在
.docx/.xlsx上会整体失败,一个时有时无的列表不如系统的「打开方式」对话框可靠。 - 变更对比页(
deliverables.review.file.actions)暂未接入,它传的也是同一种sessionId+seq+index坐标,属于后续可选扩展。 - 与第三方同名插件的关系:目录里另有一个
dsh-file-menu(作者 dreammaker2016,npm 名@z1995418/dsh-file-menu)。二者不冲突——loader entry id 不同(本插件dsh-file-menu,对方file-menu),且本插件已声明repository,市场不会再把它认成对方。取舍差异:对方是全 UI 通用右键菜单(文字选中、普通段落、输入框剪切复制粘贴、Alt+右键诊断、/file-menu/status远程诊断),任何位置右键都有菜单;本插件是定向的——只接管交付卡片、改动卡片、正文行内文件引用、http(s)链接四类,认不出来就什么都不做、事件照常走原生处理。两者在文件动作上的差别:本插件多「复制文件本体」(对方剪贴板路由只收文本)、「另存为」、「流式下载」。