Skip to content

feat(paste): 粘贴外部图文时可默认把远程图片转存为附件,避免钉钉等外链失效/变慢 #782

Description

@cropflre

用户反馈

用户从钉钉文档复制大量图文到 Nowen Note 后,发现正文中的图片仍然引用钉钉远程 URL,没有保存到 Nowen 附件系统。

结果:

  • 局域网打开笔记时图片仍要请求钉钉外链;
  • 页面加载明显变慢;
  • 外部链接可能需要登录、带防盗链或未来失效;
  • 用户希望有开关,并希望“粘贴时默认把图片保存为附件”。

目标分支:release/v1.5.0


当前代码现状

1. 后端已经具备安全的远程图片转存接口

backend/src/routes/remote-image-import.ts 已有:

POST /api/attachments/import-remote-image

会把远程图片下载并保存为当前 note 的附件。

2. 前端也已有 remoteImageLocalizer.ts

该模块明确写着:

粘贴图文时,自动把远程图片下载并上传到 nowen-note 的文件管理/附件系统,然后替换正文链接。

并已经实现:

shouldLocalizeUrl()
extractRemoteImageUrls()
localizeRemoteImage()
localizeRemoteImages()
replaceRemoteUrlsInHtml()

但当前主编辑器粘贴链路并没有真正调用批量 localizeRemoteImages()

3. Tiptap 当前只支持“选中一张网络图片后手动转存”

TiptapEditor.tsx 中目前只有选中 image node 后:

api.attachments.importRemoteImage(
  currentNote.id,
  originalSrc,
  "image-action",
)

即:用户必须发现远程图,再逐张手动操作。

4. 当前账号级 UserPreferences 没有远程图片粘贴策略

现有 UserPreferences 只有编辑器、字号、阅读密度、目录等字段,没有类似:

pasteRemoteImageMode

所以目前没有用户可配置的开关。


产品建议

这类笔记软件的核心价值是“内容长期可控”。对于钉钉、飞书、网页、公众号等来源,保留第三方图片 URL 风险较大。

建议新增账号级策略:

remoteImagePasteMode:
  | "localize"   // 自动转存为附件
  | "ask"        // 每次粘贴时询问
  | "keep-remote" // 保留原链接

默认值建议:

"localize"

即新用户默认把外部图片保存到 Nowen 附件系统。

设置文案建议:

粘贴外部图片
● 自动保存为附件(推荐)
○ 每次询问
○ 保留原始链接

自动保存可避免钉钉、飞书、网页图片过期或访问缓慢,但会占用附件存储空间。

建议实现

P0:Tiptap 粘贴 HTML 自动转存远程图片

当剪贴板包含:

<img src="https://...">

并且 remoteImagePasteMode === "localize" 时:

读取 clipboard HTML
→ sanitize
→ extractRemoteImageUrls
→ 并发/限流 importRemoteImage
→ 替换成 /api/attachments/<id>
→ 再插入 ProseMirror

不要先把远程 URL 永久写进正文再后台悄悄替换,避免保存竞态。

但为了交互不卡死,可以显示轻量进度:

正在保存 12 张外部图片… 8/12

P0:失败时单图降级,不阻断整次粘贴

例如 10 张图:

8 张成功本地化
2 张下载失败

应:

  • 8 张换成本地附件;
  • 2 张保留原 URL;
  • toast 提示“8 张已保存,2 张保留原链接”。

不能因为一张钉钉图 403 就让整段文字粘贴失败。

P0:Markdown 编辑器也保持一致

对于:

![xxx](https://remote/image.jpg)

粘贴时同样根据 preference 转存并替换 URL。

P1:ask 模式

粘贴检测到远程图片时弹一次选择:

检测到 15 张外部图片
[保存为附件] [保留原链接]

可增加:

☑ 记住我的选择

P1:并发和性能控制

不要串行一张张下载导致大段内容等待很久。

建议:

  • 最大并发 3~5;
  • 相同 URL 去重;
  • 单张失败独立处理;
  • 复用后端现有去重能力;
  • 粘贴过程中保持编辑器 selection/async insert anchor;
  • 切换笔记时不得把结果插入错误 note。

P1:外链来源兼容

重点覆盖:

  • 钉钉文档
  • 飞书文档
  • 普通网页
  • 微信/公众号网页
  • 带 query 参数的 CDN 图片

远程抓取仍必须沿用服务端安全下载策略,防 SSRF;不要改成 renderer 直接 fetch 任意 URL。

P1:历史笔记入口

现有 remote-image-localization 后台任务继续保留,用于:

已经粘贴过的历史文档 -> 扫描并批量转存

新 preference 只解决“以后粘贴默认本地化”。


数据与安全要求

  • 不允许 file://、内网 SSRF 等危险 URL 绕过后端校验
  • 附件保存成功后正文使用稳定 /api/attachments/<id> 引用
  • 不持久化临时 signed URL
  • 同一粘贴中重复 URL 只下载一次
  • 本地附件 URL 不重复本地化
  • 失败不破坏原始文字/排版
  • Undo/Redo 行为正常

验收标准

  • 设置中新增“粘贴外部图片”策略
  • 默认模式为“自动保存为附件”
  • 从钉钉复制图文后,正文图片 URL 变为 Nowen 附件 URL
  • 刷新页面、断开公网后,已本地化图片仍可通过局域网加载
  • 外链图片失败时只保留该图片原链接,整段粘贴仍成功
  • 同一 URL 多次出现只导入一次
  • Tiptap / Markdown 两种编辑器行为一致
  • ask / keep-remote 模式行为正确
  • 图片本地化过程中不会插入到切换后的其它笔记
  • Undo/Redo 不异常
  • 已经是 /api/attachments/... 的图片不重复导入
  • 钉钉 / 飞书 / 普通网页场景回归
  • 补充 UserPreferences、paste routing、remote image localization 测试

涉及范围

  • frontend/src/lib/userPreferenceAccountCache.ts
  • frontend/src/hooks/useUserPreferences.tsx
  • frontend/src/components/SettingsModal.tsx
  • frontend/src/components/TiptapEditor.tsx
  • frontend/src/components/MarkdownEditorImpl.tsx
  • frontend/src/lib/remoteImageLocalizer.ts
  • backend/src/routes/remote-image-import.ts
  • 相关粘贴/远程图片测试

建议优先级:P1(数据长期可用性 + 局域网性能)

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions