关于 github-upload-image-to-pr
github-upload-image-to-pr 是将本地图片或视频添加到 GitHub 拉取请求的技能。优先使用可用的 `gh --attach`,并为旧版本或不支持的环境说明浏览器上传备用流程。
主要功能
- 01选择直接附加或浏览器上传
- 02通过 GitHub CLI 附加媒体
- 03使用浏览器作为上传备用方式
- 04将媒体加入 PR 描述或评论
- 05验证上传后的图片引用
适用场景
- 01为新建的拉取请求添加截图
- 02在 PR 评论中发布测试结果图片
- 03在 `gh --attach` 不可用时上传媒体
MCP 相关
备用流程可使用 Chrome DevTools MCP 或 Playwright MCP 控制现有浏览器来上传文件。优先选择 Chrome DevTools MCP,不可用时再用 Playwright MCP。
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ tonkotsuboy/ github-upload-image-to-pr --skill github-upload-image-to-pr安装后在 Claude Code 中输入/github-upload-image-to-pr确认已加载。
详细介绍
将图片上传到 PR
将本地图片添加到 PR 有两种方式,它们在成本和可靠性方面差异很大:
- 路径 A —
gh --attach(GitHub CLI 2.99.0+,于 2026-09 发布):一条 Shell 命令。无需浏览器、MCP 服务、快照或轮询。只要可用,就优先使用此方式。 - 路径 B — 浏览器上传(备用方案):通过真实浏览器操作 GitHub 的评论框,这是在旧版
gh上访问 GitHub 图片托管服务的唯一公开方式。步骤繁多、故障点多、耗费大量令牌。
之所以有路径 B,是因为 GitHub 的 REST API 仍然没有图片上传端点。路径 A 之所以可行,是因为现在 gh 会为你调用内部上传端点——因此应将路径 B 视为旧方案,而非同等的替代方案。
步骤 1:选择路径
GH_VER=$(gh --version 2>/dev/null | head -1 | awk '{print $3}')
echo "gh: ${GH_VER:-not installed}"
# Path A requires >= 2.99.0
[ -n "$GH_VER" ] && [ "$(printf '%s\n2.99.0\n' "$GH_VER" | sort -V | head -1)" = "2.99.0" ] \
&& echo "=> Path A (gh --attach)" || echo "=> Path B (browser fallback)"
符合以下任一条件时,请使用路径 B:
| 条件 | 路径 A 无法使用的原因 |
|---|---|
gh 版本低于 2.99.0,或未安装 | 尚无 --attach 标志 |
| 主机是 GitHub Enterprise Server | 附件功能仅支持 GitHub.com 和 Enterprise Cloud |
| 没有仓库的推送权限(例如,这是其他人的分叉) | --attach 上传需要推送权限 |
gh --attach 因不明原因失败 | 不要在这上面耗费时间——记录错误并改用备用方案 |
两种路径都需要 gh(路径 B 仍通过 gh pr edit 嵌入图片),因此如果完全没有安装 gh,请先让用户安装:macOS 上运行 brew install gh,或参阅 https://cli.github.com/。
如果因为 gh 版本过旧而选择路径 B,请明确告知
用户即将花时间进行缓慢而不稳定的浏览器操作,而一条命令就能完成这项工作。值得先打断说明——但不值得因此停下,所以先告知用户,然后继续使用路径 B 并交付图片。回复开头可以这样写(使用与用户相同的语言):
⚠️ 更新
gh后,这项工作只需一条命令 当前版本:gh 2.86.0/ 所需版本:2.99.0 或更高 更新后只需运行gh pr comment 123 --attach ./shot.png即可完成(无需操作浏览器,速度快且稳定)。 更新命令:brew upgrade gh本次将继续使用浏览器自动化。
如果用户要求你升级,请执行升级、重新检查版本,并在当前请求中也改用路径 A。
路径 A — gh --attach(首选)
--attach 接受 <file> 或 <file>#<alt text>,可重复使用(每条命令最多 50 个文件,且不能重复附加同一个文件),并支持 gh pr create / pr edit / pr comment 以及对应的三个 gh issue 命令。
追加到 PR 描述
gh pr edit {PR_NUMBER} --attach './screenshot.png#Login error state'
省略 --body 可保留现有描述并追加附件——无需读取正文再拼接回去。
创建 PR 时附加
如果 PR 尚不存在,不要先创建再编辑——在同一次调用中附加:
gh pr create --title '...' --body '...' --attach './screenshot.png#Login error state'
控制图片的显示位置
如果正文中引用了本地路径,gh 会原位重写该引用,而不是追加内容,因此你可以按需排列图片(此时替代文本取自 Markdown,而非标志):
gh pr edit {PR_NUMBER} --body "$(gh pr view {PR_NUMBER} --json body -q .body)
## Screenshots
| Before | After |
| --- | --- |
|  |  |" \
--attach ./before.png --attach ./after.png
这里有两点需要注意:--body 会替换描述,因此需要先读回现有正文;而且正文中的路径字符串必须与传给 --attach 的路径一致,重写才会生效。
改为发布评论
gh pr comment {PR_NUMBER} --attach './result.png#Test run summary'
使用 --body 时同样适用重写规则,因此评论也可以按需排版。
视频
mp4 / mov 的附加方式相同,并会显示为播放器。视频没有替代文本,因此不要添加 #... 后缀。
验证
无需浏览器——读回正文并统计附件数量:
gh pr view {PR_NUMBER} --json body -q .body | grep -c 'user-attachments/assets'
路径 A 的注意事项
- 附加操作只会追加,绝不会替换。 重新运行同一条命令会重复添加图片。若要替换图片,请在附加新图片的同一条命令中,不带旧 URL 重写正文(
gh pr edit {PR} --body "$(...)")。 - 文件名中的
#会破坏<file>#<alt>的拆分。 先用纯 ASCII 名称暂存一份副本(cp './weird #name.png' ./shot.png),之后再用rm删除。CleanShot X 在文件名中加入的 Unicode 窄空格也可以用同样的方法处理。 - 部分失败时,成功的部分仍会生效。 如果有些文件上传成功,有些失败,PR 会更新为包含成功上传的文件,仍会打印 PR URL,但退出状态为非零。请查看输出并报告上传失败的文件,不要假设结果非成即败。
- 仅接受图片和媒体文件,不接受
.zip或.log等任意附件。
路径 B — 浏览器上传(适用于 gh < 2.99.0 的备用方案)
以下都是旧版流程。只有 Step 1 将你引导至此处时才使用。
由于旧版 gh 无法访问 GitHub 的图片主机,此路径将PR 评论文本框用作该主机的暂存区:先在其中上传文件以获取持久的 user-attachments/assets/ URL,然后通过 gh CLI 更新 PR 描述或发布评论。
Step B0:解析 PR 上下文并暂存文件
如果用户未指定 PR 编号或 URL,请自动检测:
# Get PR number from the current branch
gh pr view --json number,url -q '"\(.number) \(.url)"'
如果涉及多个仓库或分支,请与用户确认要操作哪个 PR。
另外,请将图片路径规范化为绝对路径,并在当前工作目录(仓库根目录)中暂存一份干净的副本,不要放在 /tmp/ 中:
# Stage inside the repo. The staged filename becomes the image's alt text on GitHub,
# so give it a meaningful name. Delete it after the upload completes.
cp /path/to/'CleanShot 2026-... .png' ./.upload-staging.png
为什么要在仓库中暂存,而不是放在 /tmp/?MCP 浏览器工具(Playwright / Chrome DevTools)只能读取其配置的工作区根目录内的文件;该目录通常是会话启动时所在的项目目录。/tmp/ 下的路径位于该根目录之外,因此上传调用会失败,并提示 Access denied: path ... is not within any of the configured workspace roots。在仓库中暂存文件适用于所有后端(agent-browser 和普通 CLI 也能读取 /tmp/,但暂存在仓库中始终安全)。
暂存还能避开带有特殊字符的路径(例如 CleanShot X 在文件名中加入的 Unicode 窄空格),否则这些字符会导致 shell 通配符匹配和工具参数出错。完成后记得用 rm 删除暂存文件,以免意外提交。
工具检测与选择
优先级顺序
- Chrome DevTools MCP(MCP 连接,
mcp__chrome-devtools__*)——首选:连接到现有浏览器,保留登录状态,三个后端中最稳定 - Playwright MCP(MCP 连接,
mcp__playwright__*)——仅在 Chrome DevTools MCP 不可用时使用;连接到现有浏览器,保留登录状态 - agent-browser(通过 Bash 使用 CLI——最后的备用方案,使用
--profile保留登录状态)
基于 MCP 的工具会连接到已运行的浏览器实例,因此会自动保留 GitHub 登录状态。agent-browser 可通过 --profile ~/.agent-browser-github 持久保存登录状态。
检测
# 1. Search specifically for Chrome DevTools MCP first (preferred, most stable)
ToolSearch: "select:mcp__chrome-devtools__navigate_page,mcp__chrome-devtools__take_snapshot,mcp__chrome-devtools__upload_file,mcp__chrome-devtools__evaluate_script,mcp__chrome-devtools__click"
# 2. Only if Chrome DevTools MCP tools aren't found, search for Playwright MCP
ToolSearch: "browser navigate upload"
# 3. Fall back to agent-browser only if no MCP tools found at all
Bash: agent-browser --version
工具兼容性矩阵
| 操作 | Chrome DevTools MCP(首选) | Playwright MCP(备用) | agent-browser(CLI/Bash) |
|---|---|---|---|
| 导航 | navigate_page | browser_navigate | agent-browser --headed open {url} |
| 快照 | take_snapshot | browser_snapshot | agent-browser snapshot |
| 截图 | take_screenshot | browser_take_screenshot | agent-browser screenshot {path} |
| 点击 | click (uid) | browser_click (ref) | agent-browser click {ref} |
| 上传文件 | upload_file (uid, filePath) | browser_file_upload (paths) | agent-browser upload {ref} {path} |
| JS 求值 | evaluate_script (function) | browser_evaluate (function) | agent-browser eval '{js}' |
| 登录状态 | 保留 | 保留 | 使用 --profile 时保留 |
步骤 B1:导航到 PR 页面并检查登录状态
导航到 PR 页面后,立即截取快照以确认登录状态。
// Chrome DevTools MCP (preferred)
navigate_page({ url: "https://github.com/{owner}/{repo}/pull/{number}", type: "url" })
// Playwright MCP (fallback if Chrome DevTools MCP is unavailable)
browser_navigate({ url: "https://github.com/{owner}/{repo}/pull/{number}" })
// agent-browser (last-resort fallback; use --profile to persist login state)
agent-browser --headed --profile ~/.agent-browser-github open "https://github.com/{owner}/{repo}/pull/{number}"
如果出现 SSO 身份验证界面: 截取快照,找到“Continue”按钮并点击。
如果尚未登录(仅限 agent-browser):
- 导航到
https://github.com/login - 请用户在有头浏览器窗口中手动登录。
- 等待用户确认,然后返回 PR 页面。
步骤 B2:找到上传目标
滚动到 PR 底部的评论表单并截取快照。
将快照保存到文件,而不是直接返回。 PR 页面快照很容易就有几百行(描述 + 每条审查评论),而你只需要其中一个 uid。Chrome DevTools MCP 的 take_snapshot 接受 filePath,因此可以将快照写入文件,再用 grep 查找放置区:
// Chrome DevTools MCP — write the snapshot out, then grep for the uid
take_snapshot({ filePath: "./.upload-snapshot.txt" })
grep -n "Paste, drop, or click to add files" ./.upload-snapshot.txt
# 549: uid=3_529 button "Paste, drop, or click to add files"
完成后删除快照文件。如果不这样做,快照的成本可能看起来高得令人望而却步,导致你放弃使用 upload_file,转而采用糟糕得多的变通方法(将图片以内联 base64 的形式放进 evaluate_script 正文中——数千个必须逐字节准确转录的字符)。
关键注意事项: GitHub 实际的 <input type="file">(id fc-new_comment_field)设置了 display:none,因此不会出现在无障碍快照中——你无法通过这种方式获取它的 uid/ref,也无法点击它。应改为定位打开文件选择器的可见控件:标有 “Paste, drop, or click to add files” 的放置区,或工具栏中的 “Attach files” 按钮。浏览器工具会点击该控件、拦截原生文件选择器,并接收你的文件。
在快照中,它们会显示为:
button "Attach files"
button "Paste, drop, or click to add files" ← pass this uid/ref to the upload tool
评论框本身是一个 <file-attachment> Web 组件,其中包裹着 <textarea id="new_comment_field">——添加的图片引用会出现在这个文本区域中(步骤 B4)。GitHub 的界面会随时间变化;如果找不到 new_comment_field,则改用 textarea[name="comment[body]"] 或 textarea[id*="comment"];PR 描述编辑器使用的则是 textarea[name="pull_request[body]"] / textarea[id$="-body"]。
步骤 B3:上传图片
使用检测到的工具上传每个文件,并传入步骤 B2 中获取的放置区 / 附件按钮 uid(切勿使用隐藏的输入框):
// Chrome DevTools MCP (preferred): upload_file({ uid: <dropzone uid>, filePath: <path inside the workspace root> })
// Playwright MCP (fallback): browser_file_upload({ paths: [<path inside the workspace root>] }) once the file chooser is open
// agent-browser (last resort): agent-browser upload {ref} {absolute_path} (any path, /tmp/ ok)
对于 MCP 后端,请使用仓库内暂存的路径(参见步骤 B0 中关于工作区根目录的说明)。每次上传之间等待 2–3 秒。如果有多张图片,请先将它们全部上传到同一个评论框,再提取 URL——这样比在多次上传之间来回导航更高效。
步骤 B4:获取已上传图片的 URL
上传期间,GitHub 会在文本框中显示占位符(![Uploading file.png…]()),文件上传完成后再将其替换为最终引用。因此,请轮询文本框,直到出现 user-attachments/assets/ URL,不要依赖固定的等待时间——通常需要 1–5 秒。
GitHub 会将引用插入为 HTML <img> 标签(包含自动检测的 width/height),例如:
<img width="342" height="354" alt="filename" src="https://github.com/user-attachments/assets/8fc1b84a-..." />
不要假设它会采用  Markdown 格式——直接匹配资源 URL 本身,确保提取过程无论引用采用哪种形式都足够可靠:
// MCP-based tools — returns every asset URL plus the raw value for sanity-checking
() => {
const ta = document.getElementById('new_comment_field')
|| document.querySelector('textarea[name="comment[body]"], textarea[id*="comment"]');
if (!ta) return { error: 'textarea not found' };
const urls = [...ta.value.matchAll(/https:\/\/github\.com\/user-attachments\/assets\/[0-9a-fA-F-]+/g)].map(m => m[0]);
return { raw: ta.value, urls };
}
# agent-browser
agent-browser eval 'const ta=document.getElementById("new_comment_field")||document.querySelector("textarea[id*=comment]");JSON.stringify([...(ta?.value||"").matchAll(/https:\/\/github\.com\/user-attachments\/assets\/[0-9a-fA-F-]+/g)].map(m=>m[0]))'
如果要保留 GitHub 自动检测的 width/height,请保留整个 <img …> 标签;否则只提取 URL,再自行套用格式( 或自定义的 <img>)。
步骤 B5:清空文本框(不要提交评论)
清空后触发 input 事件,以便 GitHub 的评论草稿自动保存功能也同步清空;否则,暂存的内容可能会在下次加载页面时再次出现。
// MCP-based tools
() => {
const ta = document.getElementById('new_comment_field')
|| document.querySelector('textarea[name="comment[body]"], textarea[id*="comment"]');
if (!ta) return "textarea not found";
ta.value = "";
ta.dispatchEvent(new Event('input', { bubbles: true }));
return "cleared";
}
# agent-browser
agent-browser eval 'const ta=document.getElementById("new_comment_field")||document.querySelector("textarea[id*=comment]"); if(ta){ta.value="";ta.dispatchEvent(new Event("input",{bubbles:true}))} "cleared"'
步骤 B6:在 PR 中嵌入图片
可使用 GitHub 提供的完整 <img …> 标签(保留自动检测的尺寸),也可使用纯 Markdown  格式嵌入图片。
选项 A — 更新 PR 描述(将图片追加到现有正文):
EXISTING_BODY=$(gh pr view {PR_NUMBER} --json body -q .body)
gh pr edit {PR_NUMBER} --body "$(printf '%s\n\n## Screenshots\n\n%s' "$EXISTING_BODY" '<img width="800" alt="screenshot" src="https://github.com/user-attachments/assets/..." />')"
选项 B — 发布为新评论:
gh pr comment {PR_NUMBER} --body '## Screenshots
<img width="800" alt="screenshot" src="https://github.com/user-attachments/assets/..." />'
默认使用选项 A,除非用户明确要求发布评论,或 PR 描述已经很长、改用评论会更清晰。
步骤 B7:验证结果
重新加载页面并截取屏幕截图,确认图片显示正常。
另外,还要通过编程方式确认图片确实已加载。两种 URL 格式都要匹配:渲染时,GitHub 会将 user-attachments/assets/... 重写为签名后的 private-user-images.githubusercontent.com URL,因此只搜索嵌入时使用的格式会得到 0 张图片,让人误以为没有附加任何图片。
// MCP-based tools — every embedded image plus whether the browser decoded it
() => {
const imgs = [...document.querySelectorAll('img')]
.filter((el) => /user-attachments|private-user-images/.test(el.src));
return imgs.map((el) => ({ alt: el.alt, loaded: el.naturalWidth > 0 }));
}
如果每个条目都显示 loaded: true,表示嵌入成功。若返回空数组,表示 <img> 标签没有写入正文——请重新检查步骤 B6。
提示
- 每次都先尝试路径 A:一次
gh pr edit --attach/gh pr comment --attach调用即可取代下文的整个浏览器会话。只有步骤 1 中列出的条件才足以使用路径 B - 图片尺寸:
--attach会让 GitHub 自动选择尺寸;在路径 B 中(或手动编写标记时),可通过<img width="800" alt="description" src="..." />控制尺寸 - 多张图片:路径 A 会在一条命令中重复使用
--attach;路径 B 会将所有图片上传到同一个文本框,然后在清空文本框前提取所有 URL - 在路径 B 中优先使用 Chrome DevTools MCP:这是最稳定的后端——务必先通过
ToolSearch尝试使用它。只有找不到 Chrome DevTools MCP 工具时才回退到 Playwright MCP,只有完全没有可用 MCP 工具时才使用 agent-browser - agent-browser 登录持久化:使用
--profile ~/.agent-browser-github,以便在多个会话间保留 GitHub 登录状态
故障排查
路径 A(gh --attach)
| 问题 | 解决方案 |
|---|---|
unknown flag: --attach | gh 版本低于 2.99.0——提示用户升级(步骤 1),然后继续使用路径 B |
| 重新运行后图片重复 | --attach 只会追加。在同一次 gh pr edit --body ... --attach ... 调用中重写正文,并移除过时的 URL |
| 上传遭拒 / 权限错误 | --attach 需要推送权限,且仅适用于 GitHub.com 和 Enterprise Cloud。请回退到路径 B |
| 替代文本吞掉了文件名,或找不到文件 | 路径中的 # 会被解析为替代文本分隔符——请用纯 ASCII 名称暂存副本 |
| 部分文件已上传,但命令以非零状态退出 | 这是部分失败时的预期行为:成功上传的文件仍会保留在 PR 中。请报告哪些文件失败,并仅重试这些文件 |
| 正文引用未被替换,图片反而追加到了末尾 | 正文中的路径必须与 --attach 路径字符串完全一致 |
路径 B(浏览器上传)
| 问题 | 解决方案 |
|---|---|
| 未登录(MCP 工具) | 可能会出现 SSO 页面 — 截取快照,找到“Continue”按钮并点击 |
| 未登录(agent-browser) | 使用 --headed 模式,导航到登录页面,并请用户手动登录 |
| 浏览器窗口不可见 | 对于 agent-browser,请确保使用 --headed 标志 |
| 文件路径包含特殊字符(例如,CleanShot 使用的 Unicode 窄空格) | 在仓库内以简单名称暂存副本:cp /path/'CleanShot ... .png' ./.upload-staging.png(放在仓库内,以便 MCP 工具读取 — 参见步骤 B0) |
Access denied: path ... not within any of the configured workspace roots | MCP 浏览器工具只能读取其工作区根目录内的文件。请将图像暂存到仓库内(步骤 B0),不要放在 /tmp/ |
| 找不到文件输入框的 uid/ref | <input type="file"> 的 display:none,不会出现在快照中 — 请改为定位可见的 “Paste, drop, or click to add files” 拖放区域或 “Attach files” 按钮(步骤 B2) |
| PR 页面快照过大 / 截取成本太高 | 将 filePath 传给 take_snapshot,然后对该文件使用 grep 查找拖放区域的 uid(步骤 B2)。不要因为快照过大,就在 evaluate_script 中内联 base64 |
| 验证结果为 0 张图像,尽管图像已显示 | GitHub 渲染时会将资源 URL 改写为 private-user-images.githubusercontent.com — 同时匹配该域名和 user-attachments,然后检查 naturalWidth > 0(步骤 B7) |
文本框中有文件,但没有  Markdown | GitHub 会为图像插入 <img …> HTML 标签,而不是 Markdown。使用步骤 B4 中同时匹配这两种形式的正则表达式提取 user-attachments/assets/ URL |
| 文本框中尚无 URL | GitHub 会先显示 ![Uploading…]() 占位符 — 轮询文本框,直到出现 user-attachments/assets/ URL(1–5 秒),不要固定等待 |
| 找不到文本框选择器 | GitHub UI 偶尔会变更 — 依次尝试 new_comment_field → textarea[name="comment[body]"] → textarea[id*="comment"](步骤 B2) |
| Chrome DevTools MCP 已断开连接 | 通过 /mcp 命令重新连接 |
| 找不到 agent-browser | npm install -g agent-browser && agent-browser install |
| 找不到浏览器工具 | 使用 ToolSearch 搜索可用的浏览器工具 |
| 找不到 PR / 404 | 私有仓库会向未通过身份验证的用户返回 404 — 检查登录状态 |
注意事项
gh --attach于 GitHub CLI 2.99.0(2026-09)中加入,支持gh pr create/pr edit/pr comment,以及对应的gh issue create/issue edit/issue comment命令。每条命令最多可附加 50 个文件;同一文件不能重复附加- 使用
gh pr edit --attach且不带--body时,会保留现有描述并追加内容 — 只有在需要控制内容位置时,才需要执行读取-修改-写入操作 - GitHub 的
user-attachments/assets/URL 是永久有效的 — 即使不提交评论,图像仍可访问。渲染时,GitHub 会将其改写为private-user-images.githubusercontent.comCDN URL;user-attachments/assets/...格式才是稳定的嵌入形式 - 使用路径 B 上传时,GitHub 会注入
<img width=… height=… alt=… src=… />标签(alt 根据文件名生成)— 而不是Markdown。请通过资源 URL 提取,而不是通过外层标签提取 - MCP 浏览器工具只能读取其工作区根目录内的上传文件,因此请将图像暂存到仓库内,而不是
/tmp/ - 由于 GitHub UI 结构会发生变化,直接在浏览器 UI 中编辑描述并不可靠 — 强烈建议通过
gh pr edit更新 - 基于 MCP 的工具会连接到现有浏览器实例,从而保留 Cookie 和登录会话