Skip to main content
L

dsh-file-menu

lemon0-00/dsh-file-menu

Adds a right-click context menu to delivered file cards, the changed-files card, inline file references and http(s) links in DSH chat. Actions are open with the default app, open with the system chooser, reveal in the file manager, copy the file itself, copy its path, save a copy to a chosen folder, download, and open a link in the browser. Delivery cards are addressed by Session coordinates the Host re-derives from the session log; inline references and the changed-files card are addressed by an absolute path the Host re-verifies.

Install

dsh plugin --profile web add github:lemon0-00/dsh-file-menu

README

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,全部不过就拒):

  1. 非空、字符串、长度 ≤ 4096、不含换行或 NUL —— 否则 400
  2. 必须是绝对路径 —— 否则 400
  3. realpath 必须存在 —— 否则 404
  4. realpath 的结果必须与请求字面量逐字相等 —— 否则 422。这条挡掉路径变体:符号链接、junction、大小写变体、.. 折返都会落到另一个规范路径上,宁可拒绝也不静默作用到别的文件
  5. 必须是普通文件(目录 → 422)
  6. 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渲染器给的 titleworkspace/changes 摘要
路径放在哪坐标 sessionId+seq+indextitle 属性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. 使用

  1. 让 dsh 生成或发给你一个文件(用 present 产出的交付卡片,或任何带文件卡片的回复)。
  2. 在这张卡片上任意位置点右键(卡片缩略图、文件名、空白处都行),或者点操作行里的 ⋯ 按钮。
  3. 在弹出菜单里选一项,例如「打开文件」。
  4. 反馈会以短提示条的形式出现在菜单底部(「已打开」「已复制」「已复制路径」…),失败则给出具体原因。

正文里也一样:对着文件名或路径右键(例如回复里出现的 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) 链接四类,认不出来就什么都不做、事件照常走原生处理。两者在文件动作上的差别:本插件多「复制文件本体」(对方剪贴板路由只收文本)、「另存为」、「流式下载」。

Related plugins