秀秀(TailChat)右键下载图片功能修复方案(方案 A · 完整版)

编制:2026-06-01
适用范围:Web 浏览器 + Electron 桌面端(xiuxiu_0.0.7_amd64.deb)+ Android APK(如远程加载)
修复难度:⭐(1 行代码)
执行时间:5 分钟(热修方式)/ 30 分钟(重构镜像方式)
风险等级:极低
影响范围:仅图片/视频/音频元素的右键菜单(从"无菜单"恢复"浏览器原生菜单")


一、问题与根因

问题描述

根因(实证诊断)

容器内 /app/tailchat/client/web/src/App.tsx 第 113-119 行,守一 260408 自研改动:

// 守一改后的当前版本
onContextMenu={(e) => {
  const target = e.target as HTMLElement;
  const tagName = target.tagName.toLowerCase();
  if (tagName !== "input" && tagName !== "textarea" && !target.isContentEditable) {
    e.preventDefault();   // ← 这一行:非输入框元素全部阻止浏览器右键
  }
}}

Bug 链条
1. App.tsx 全局 onContextMenu 拦截
2. 白名单只允许 input / textarea / contenteditable 元素的右键
3. <img> <video> <audio> 元素的 tagName 不在白名单 → e.preventDefault() 阻止浏览器原生菜单
4. 兜底的 CustomContextMenuHost命令式 API,必须有代码主动调用 openContextMenu()
5. 没有任何代码为图片元素主动调 openContextMenu → 菜单根本不弹出

与其他问题的关系


二、修复方案(1 行代码)

Diff(适用于源码 + 容器内文件)

文件:client/web/src/App.tsx(源码)/ /app/tailchat/client/web/src/App.tsx(容器内)

@@ -113,7 +113,9 @@
       onContextMenu={(e) => {
         const target = e.target as HTMLElement;
         const tagName = target.tagName.toLowerCase();
-        if (tagName !== "input" && tagName !== "textarea" && !target.isContentEditable) {
+        if (tagName !== "input" && tagName !== "textarea"
+            && tagName !== "img" && tagName !== "video" && tagName !== "audio"
+            && !target.isContentEditable) {
           e.preventDefault();
         }
       }}

核心改动
- 白名单从原来的 3 个(input / textarea / contenteditable)
- 扩展为 6 个(+ img / video / audio)
- 媒体元素的右键不再被 preventDefault → 浏览器原生菜单正常弹出


三、执行方式(3 种,按场景选)

🥇 方式 1:直接热修运行容器(推荐,5 分钟)

适用:金祥/守一直接在狗云生产环境快速修复

# Step 1: SSH 到狗云
ssh root@45.153.129.198
# 密码:qwTQjUTy5cAbi

# Step 2: 从 service-core 容器把当前文件拷出来
docker cp tailchat-deploy-service-core-1:/app/tailchat/client/web/src/App.tsx /tmp/App.tsx

# Step 3: 备份原文件(用于回滚)
cp /tmp/App.tsx /tmp/App.tsx.bak-260601

# Step 4: 修改文件(手动 vim 或下面 sed 一行搞定)
# 推荐手动 vim 确认(防 sed 转义出错)
vim /tmp/App.tsx
# 找到这一行:
#   if (tagName !== "input" && tagName !== "textarea" && !target.isContentEditable) {
# 改成:
#   if (tagName !== "input" && tagName !== "textarea"
#       && tagName !== "img" && tagName !== "video" && tagName !== "audio"
#       && !target.isContentEditable) {
# :wq 保存退出

# Step 5: 拷回容器
docker cp /tmp/App.tsx tailchat-deploy-service-core-1:/app/tailchat/client/web/src/App.tsx

# Step 6: 进容器重新 build web bundle
docker exec tailchat-deploy-service-core-1 sh -c "cd /app/tailchat/client/web && pnpm run build 2>&1 | tail -20"

# Step 7: 重启 service-core 容器(让 nginx 重新读取新的静态文件)
docker restart tailchat-deploy-service-core-1

# Step 8: 等 30 秒容器起来
sleep 30
docker ps | grep tailchat-deploy-service-core-1

风险:跟现有 xiuxiu:v2.5.11-renderfix 镜像一样是热修方式(不固化镜像)。如果将来 docker compose down → up,会丢失这个修复(同 ima [v3 镜像欠债] 现状)。需要让金祥下次重构镜像时一并固化进去。


🥈 方式 2:在源码改+重构镜像(更规范,30 分钟)

适用:要彻底固化进镜像、避免热修隐患

# Step 1: SSH 到镜像源母机
ssh ubuntu@81.70.146.110
# 密码:Lidazhi123(参考 /opt/tailchat-deploy/sync-xiuxiu.sh)

# Step 2: 找到 TailChat 源码目录
# 大概率在 /home/ubuntu/tailchat-source 或类似位置

# Step 3: 修改 client/web/src/App.tsx
vim tailchat/client/web/src/App.tsx
# 同 Step 4 of 方式 1,改 onContextMenu 白名单

# Step 4: 本地测试构建
cd tailchat
pnpm install
pnpm run build:web

# Step 5: 构建新镜像
docker build -t xiuxiu:v2.5.12-rightclick .

# Step 6: 导出镜像,传给狗云部署机
docker save xiuxiu:v2.5.12-rightclick | gzip > /tmp/xiuxiu-v2.5.12.tar.gz
scp /tmp/xiuxiu-v2.5.12.tar.gz root@45.153.129.198:/tmp/

# Step 7: 在狗云部署机加载镜像 + 切换
ssh root@45.153.129.198
gunzip -c /tmp/xiuxiu-v2.5.12.tar.gz | docker load
cd /opt/tailchat-deploy

# Step 8: 备份 docker-compose.yml
cp docker-compose.yml docker-compose.yml.bak-260601-pre-v2.5.12

# Step 9: 把镜像版本改成 v2.5.12-rightclick
sed -i 's|xiuxiu:v2.5.11-renderfix|xiuxiu:v2.5.12-rightclick|g' docker-compose.yml

# Step 10: 重启
docker compose up -d

⚠️ 方式 2 风险提示:如果母机上的 TailChat 源码没有同步守一所有 13 条自研改动(A1 手机回车 / A2 Windows 右键 / A3 输入草稿 / B3 多选 / B4 转发 / B5 置顶 / B6 typing / E1 联系人置顶 等),重构镜像会丢失这些热修

先验证:母机源码里 App.tsx 是否已包含 CustomContextMenuHost 引用 + 守一其他自研改动。如果没有,必须先把所有热修补齐到源码,再构建镜像


🥉 方式 3:Git Patch 文件(开发流程规范用)

适用:金祥的开发机有 Git 仓库,按规范流程提交

# 在开发机上:
cd /path/to/tailchat-source

# 创建 patch
cat > /tmp/fix-right-click-image-260601.patch <<'EOF'
diff --git a/client/web/src/App.tsx b/client/web/src/App.tsx
--- a/client/web/src/App.tsx
+++ b/client/web/src/App.tsx
@@ -113,7 +113,9 @@
       onContextMenu={(e) => {
         const target = e.target as HTMLElement;
         const tagName = target.tagName.toLowerCase();
-        if (tagName !== "input" && tagName !== "textarea" && !target.isContentEditable) {
+        if (tagName !== "input" && tagName !== "textarea"
+            && tagName !== "img" && tagName !== "video" && tagName !== "audio"
+            && !target.isContentEditable) {
           e.preventDefault();
         }
       }}
EOF

# 应用 patch
git apply /tmp/fix-right-click-image-260601.patch

# 验证
git diff client/web/src/App.tsx

# commit
git add client/web/src/App.tsx
git commit -m "fix(contextmenu): 放行 img/video/audio 元素的右键菜单 (#260601)

修复用户右键图片无法弹出'图片另存为'菜单的问题。
守一 260408 F3 自研全局 contextMenu 白名单未含媒体元素,
本 commit 在原 input/textarea/contenteditable 基础上加入 img/video/audio
让媒体元素恢复浏览器原生右键菜单。

不影响:A1 回车换行 / A2 Win 右键复制 / A3 输入草稿 / B3-B6 等其他自研
"

# 然后按规范流程:build → push → deploy

四、验证步骤

Web 浏览器端验证(Chrome / Edge / Firefox)

  1. 打开 https://chat.lidazhiai.com/
  2. Ctrl + Shift + R(Windows)/ Cmd + Shift + R(Mac)强制刷新清缓存
  3. 登录任意账号 → 进入任意聊天 → 找一张已上传的图片
  4. 右键点击图片
  5. ✅ 应该出现浏览器原生菜单,包含:
    - 图片另存为...(核心需求)
    - 复制图片地址
    - 复制图片
    - 在新标签页中打开图片
    - 搜索此图片(仅 Chrome)

Electron 桌面端验证(xiuxiu_0.0.7_amd64.deb)

桌面端是 mainWindow.loadURL(config.webUrl) 远程加载 chat.lidazhiai.com 的 web 内容,因此 Web 端修好后桌面端只需:

  1. 退出 xiuxiu 桌面应用
  2. 重新打开(或菜单 → 重新加载 / Ctrl+R
  3. 进入聊天右键图片
  4. ✅ 应该出现 Electron Chromium 原生菜单(同 Chrome 菜单类似)

Android APK 端验证


五、副作用清单(守一关心的"不改变现有功能")

修改前 修改后 是否"改变现有功能"
图片右键菜单 浏览器原生菜单 ❌ 否(从"无"变"有",不算改变)
视频/音频右键菜单 浏览器原生菜单 ❌ 否
输入框右键(A2 守一修复) 浏览器原生 浏览器原生(不变) ✅ 不变
消息文字右键 守一自定义 CustomContextMenu 同前不变 ✅ 不变
群组列表右键 守一自定义 同前不变 ✅ 不变
其他 UI 元素右键 守一自定义 同前不变 ✅ 不变
守一 A1 手机回车换行 已修 同前 ✅ 不变
守一 A3 Windows 输入草稿 已修 同前 ✅ 不变
守一 B3 消息多选 已自研 同前 ✅ 不变
守一 B4 消息转发 已自研 同前 ✅ 不变
守一 B5 消息置顶 已自研 同前 ✅ 不变
守一 B6 typing 状态 已自研 同前 ✅ 不变
守一 E1 联系人置顶 已自研 同前 ✅ 不变
数据库 / 配置 / 推送 / Bot / 文件上传 完全不动 ✅ 不变
15 个 plugin 状态 未启用 未启用 ✅ 不变

真正"改变"的就只有图片/视频/音频的右键菜单从"无"变"有",是新增功能而非改变现有功能。


六、回滚方案

如果修改后出现问题,立刻按以下回滚:

方式 1 热修后回滚

ssh root@45.153.129.198
docker cp /tmp/App.tsx.bak-260601 tailchat-deploy-service-core-1:/app/tailchat/client/web/src/App.tsx
docker exec tailchat-deploy-service-core-1 sh -c "cd /app/tailchat/client/web && pnpm run build 2>&1 | tail -5"
docker restart tailchat-deploy-service-core-1
sleep 20
curl -s https://chat.lidazhiai.com/ | head -3

方式 2 镜像回滚

ssh root@45.153.129.198
cd /opt/tailchat-deploy
cp docker-compose.yml.bak-260601-pre-v2.5.12 docker-compose.yml
docker compose up -d

七、与已有"v3 镜像欠债"的关系

ima 笔记里 [v3 镜像欠债] 提到守一 13 条自研修改都是 docker cp 热修,未固化进镜像。本次方案 A 沿用同一模式,不引入新的固化方式,避免"半固化半热修"的复杂状态。

建议:金祥下次重构镜像时(参考方式 2),把以下全部修改一并写入源码:
1. A1 手机端回车=换行
2. A2 Windows 右键复制(输入框)
3. A3 Windows 输入草稿不丢失
4. A4 对话宽度
5. B2 @ 功能修复
6. B3 消息多选
7. B4 消息转发
8. B5 消息置顶
9. B6 时间显示 + typing 状态
10. C1-C4 Bot 自动建群 / 文转播客 Bot
11. D1-D2 文档发布 / MP3
12. E1 联系人置顶
13. E2 界面渲染优化
14. ★ 本次方案 A:img/video/audio 右键白名单

固化后镜像版本建议命名:xiuxiu:v2.5.13-fullhotfix-260615(含日期)


八、给金祥的清单

如果是金祥执行本次修复,建议这样做:

[ ] 1. 看完本文档(重点:第二章 Diff + 第三章方式 1)
[ ] 2. 确认修改 App.tsx 第 113-119 行 onContextMenu
[ ] 3. 备份原文件 /tmp/App.tsx.bak-260601
[ ] 4. SSH 到狗云,按方式 1 执行
[ ] 5. 按第四章验证 Web 浏览器端
[ ] 6. 守一/测试同事用桌面 .deb 验证
[ ] 7. 把这次修复记到 ima 笔记 [v3 镜像欠债清单] 第 14 项
[ ] 8. 如有任何异常立刻回滚(第六章)

九、文件位置


十、补充:如果将来要做 Bot/插件相关

本次修复纯前端,不涉及 plugin 启用。如果未来要同时启用插件(如 com.msgbyte.iam RBAC / com.msgbyte.notify 桌面通知),需另外做:

# 启用 service-all-plugins 容器(当前未启)
cd /opt/tailchat-deploy
docker compose up -d service-all-plugins service-openapi tailchat-admin

但这是另一个话题,不在本方案范围。