跳过主要内容
L

dsh-file-menu

lemon0-00/dsh-file-menu

给 DSH 聊天里的交付文件卡片、改动卡片、行内文件引用与 http(s) 链接加上右键菜单。动作包括用默认程序打开、用系统「打开方式」对话框打开、在文件管理器中定位、复制文件本体、复制文件路径、另存到指定文件夹、下载,以及用浏览器打开链接。交付卡片走宿主从会话日志反查的会话坐标;行内引用与改动卡片走宿主要求复核的绝对路径。

安装

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

相关插件