🐛 修复安装页回退/关闭标签逻辑:改用 chrome.tabs.goBack 并区分同标签重定向#1594
Conversation
PR scriptscat#1594 把 history.back() 改由内容脚本通过消息执行,但 ScriptCat 自身的 content.js 依赖 chrome.userScripts.register() 注册(runtime.ts registerUserscripts),仅在开启 Developer Mode 且脚本运行未被禁用/未被黑名单 排除时才会被注入。普通用户关闭 Developer Mode 时该内容脚本从不存在, chrome.tabs.sendMessage 收不到响应但错误只是被 console.error 吞掉, 安装后标签页无法回退且没有任何异常抛出。 改为直接调用 chrome.tabs.goBack(req.tabId)——issue scriptscat#1588 本就要求优先使用 浏览器原生的标签页导航 API,该 API 只依赖已必需的 tabs 权限,不依赖任何 内容脚本注入状态。同时移除不再需要的 historyBack 消息处理器与其测试。 Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
install.html 既可能由 chrome.tabs.create 打开为独立新标签(history.length === 1, 安装/更新完成后关闭该标签无损),也可能由 declarativeNetRequest 的就地 redirect 规则直接在用户当前浏览标签内被替换出来(点击 raw .user.js 链接场景,history.length > 1)。无论哪种入口,install()/close()/installSkill()/cancelSkill() 都无条件调用 window.close(),导致后者场景下用户实际浏览的标签(而非专用安装标签)被整个关闭。 改为共用的 leaveInstallPage():history.length > 1 时改用 history.back() 返回上一页, 仅在确实是独立新标签(history.length === 1)时才 window.close()。新增回归测试验证 两种 history.length 下分别调用 history.back()/window.close();已验证该测试在旧代码 上失败(history.length > 1 时 history.back 从未被调用)。 Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
App.tsx 里新增的 isMainFrame() 拦截存在两个问题:
1. t("invalid_page_access") 引用的 key 在任何语言的任何命名空间里都不存在
(defaultNS 是 common,install/common 两个命名空间都没有该 key),
i18n-usage.test.ts 静态扫描可直接复现:界面会原样显示 "invalid_page_access"
而非译文。改为正确加了命名空间前缀的 install:frame_blocked_title /
install:frame_blocked_desc,并补全 9 个 locale 的翻译。
2. 拦截判断被放在 loading/invalid/error/skill 等提前 return 之后、ready 视图
组装完毕时才执行,意味着安装页在 loading 与 skill(技能安装,同样有安装按钮)
状态下完全不受 iframe 保护,防点击劫持形同虚设。改为紧跟在所有 Hook 调用之后、
任何状态分支之前执行(避免把 Hook 调用放进条件 return 之前,违反 Hooks 规则)。
同时把裸 <div> 替换为已有的 InstallError 组件,与其余错误态保持一致的设计风格
和可关闭交互(复用 close(),即上一提交里改好的 leaveInstallPage())。
新增回归测试验证:拦截提示的翻译能正确渲染、loading 状态下也会被拦截、拦截页
关闭按钮可用;已确认这些测试在修复前的占位实现上会失败。
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
已通過人工檢查&測試
|
对已合入改动逐处补上「为什么」注释:goBack 为何绕开 content script 通道、 为何只对当前激活标签生效;isMainFrame 里 SecurityError 的成因; leaveInstallPage 的重入保护与 rAF 延迟目的;测试里 320ms 等待对应的 setTimeout+rAF 时序。均为注释新增,不改变任何运行时行为。 Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
本 PR 聚焦修复用户脚本安装流程结束后的“回退/离开安装页”语义与行为一致性问题:用浏览器原生标签页导航能力替代脚本注入/消息通道,并修复安装页在“同标签重定向打开”场景下误关用户标签页的问题,同时补齐安装页被 iframe 嵌入时的安全拦截与多语言文案。
Changes:
- Service Worker 安装收尾逻辑改为
chrome.tabs.goBack(req.tabId),并增加“仅当前激活标签页才回退”的保护。 - 安装页新增
leaveInstallPage():按history.length区分“独立新标签关闭” vs “同标签重定向返回上一页”,统一替换多处window.close()。 - 安装页增加防 iframe 嵌入拦截(使用
InstallError展示),修复 i18n key 缺失并补齐多语言翻译;新增对应测试覆盖。
Reviewed changes
Copilot reviewed 15 out of 15 changed files in this pull request and generated 3 comments.
Show a summary per file
| File | Description |
|---|---|
| src/app/service/service_worker/script.ts | 安装流程 finally 收尾改用 chrome.tabs.goBack,避免脚本注入/内容脚本依赖,并增加激活标签判断。 |
| src/pages/install/useInstallData.ts | 引入 leaveInstallPage(),避免同标签重定向场景误 window.close() 关闭用户原标签页。 |
| src/pages/install/useInstallData.test.ts | 新增“新标签关闭/同标签返回”行为测试。 |
| src/pages/install/App.tsx | 增加安装页防 iframe 嵌入拦截,并改用存在的 install 命名空间文案 key。 |
| src/pages/install/App.test.tsx | 新增 iframe 拦截渲染与关闭交互测试。 |
| src/locales/zh-CN/install.json | 补充 frame_blocked_title/desc 翻译。 |
| src/locales/zh-TW/install.json | 补充 frame_blocked_title/desc 翻译。 |
| src/locales/en-US/install.json | 补充 frame_blocked_title/desc 翻译。 |
| src/locales/de-DE/install.json | 补充 frame_blocked_title/desc 翻译。 |
| src/locales/ja-JP/install.json | 补充 frame_blocked_title/desc 翻译。 |
| src/locales/ko-KR/install.json | 补充 frame_blocked_title/desc 翻译。 |
| src/locales/pt-BR/install.json | 补充 frame_blocked_title/desc 翻译。 |
| src/locales/ru-RU/install.json | 补充 frame_blocked_title/desc 翻译。 |
| src/locales/tr-TR/install.json | 补充 frame_blocked_title/desc 翻译。 |
| src/locales/vi-VN/install.json | 补充 frame_blocked_title/desc 翻译。 |
Comments suppressed due to low confidence (1)
src/pages/install/useInstallData.test.ts:298
- 同上:用真实 setTimeout 等待会增加耗时与波动风险,建议用 fake timers 推进 300ms,并 stub requestAnimationFrame。
await act(async () => {
await result.current.install();
// leaveInstallPage 延后到 install() 里 300ms 的 setTimeout 再叠一帧 rAF 才真正执行,多等一点确保已触发
await new Promise((r) => setTimeout(r, 320));
});
| // 安装页可能是专为安装打开的新标签(history.length === 1,关闭无损), | ||
| // 也可能是由 declarativeNetRequest 就地重定向而来的用户原浏览标签(history.length > 1), | ||
| // 后者若直接 window.close() 会连带关掉用户本来在看的页面,应改为返回上一页。 | ||
| // install()/close() 等可能在短时间内被重复触发(如用户连续点击、close 与 install 的 | ||
| // setTimeout 前后脚打到),leaveInstallPageRunning 防止 back()/close() 被并发调用多次; | ||
| // 推到 requestAnimationFrame 里执行,让触发它的那次交互(如按钮点击态)先完成一帧渲染。 | ||
| let leaveInstallPageRunning = false; | ||
| const leaveInstallPage = () => { | ||
| if (leaveInstallPageRunning) return; | ||
| leaveInstallPageRunning = true; | ||
| requestAnimationFrame(() => { | ||
| leaveInstallPageRunning = false; | ||
| if (window.history.length > 1) { | ||
| window.history.back(); | ||
| } else { | ||
| window.close(); | ||
| } | ||
| }); | ||
| }; |
| const currentTab = await chrome.tabs.query({ active: true, lastFocusedWindow: true }); | ||
| // 仅针对用户自行点击安装、且仍停留在该标签的场景;用户若已切到其他标签,不应把后台标签拉回历史记录 | ||
| if (currentTab?.[0]?.id === req.tabId) { | ||
| // 回退到到安装页 |
| await act(async () => { | ||
| await result.current.install(); | ||
| // leaveInstallPage 延后到 install() 里 300ms 的 setTimeout 再叠一帧 rAF 才真正执行,多等一点确保已触发 | ||
| await new Promise((r) => setTimeout(r, 320)); | ||
| }); |
|
已将最新 阻塞项1.
|
|
ai 的review 很多都是無謂或不了解設計 |
Checklist / 检查清单
背景
安装页交接结束后,ScriptCat 需要把被拦截的标签页带回安装前的页面。原实现在 Service Worker 的
finally中用chrome.scripting.executeScript()向req.tabId注入并执行history.back()——issue #1588 指出这条路径的语义不对:这里要表达的只是“让指定标签页回到上一页”,不需要向页面注入任意代码,也不应该依赖目标页面是否允许脚本注入。第一版修复(PR 原提交)把它换成了消息通信方案:Service Worker 用
chrome.tabs.sendMessage发content/historyBack,由 content script 里新增的historyBack处理器执行history.back()。这个方案本身没有走 issue 建议的优先级——它跳过了“优先使用浏览器标签页导航 API”,直接引入了一条新的常驻消息通道,而这条通道又依赖 ScriptCat 自身的 content script 已经被注入到目标标签页。经复核,这个依赖并不总是成立,会导致静默失效(见下)。复核过程中同时发现两个与本 issue 同源、但更靠近“安装页离开方式”本身的问题,一并在本 PR 中修了。
本次改动
1. 回退逻辑改为直接使用浏览器标签页导航 API(
src/app/service/service_worker/script.ts)chrome.tabs.sendMessage(..., "content/historyBack")消息通道,改为直接调用chrome.tabs.goBack(req.tabId)。该 API 只依赖已经必需的tabs权限(src/manifest.json),不依赖chrome.userScripts、content script 注入状态或黑名单配置,符合 issue [提案] 将安装后的返回操作改为由标签页侧上下文执行 #1588 里“优先使用标签页历史导航 API”的验收标准。req.tabId仍是当前激活标签页时才执行回退,避免用户已经切走后台标签页时被意外拉回历史记录。content/historyBack消息处理器(src/app/service/content/script_runtime.ts)及其回归测试,改动收敛为无新增消息通道。2. 安装页离开方式区分「独立新标签」与「同标签内被重定向」(
src/pages/install/useInstallData.ts)复核第一版修复时发现:
install.html有两种打开方式——通过chrome.tabs.create(openInCurrentTab())打开的专用新标签,以及由 declarativeNetRequest 的redirect规则在用户当前浏览标签内就地替换出来的标签(例如点击 GitHub 上的 raw.user.js链接)。旧代码里install()/close()/installSkill()/cancelSkill()全部无条件window.close(),在后一种场景下会把用户正在使用的浏览标签整个关掉,而不只是关闭安装页。新增
leaveInstallPage():window.history.length > 1(同标签内被重定向而来,仍有上一页可回)时改用window.history.back();只有确认是独立新标签(history.length === 1)时才window.close()。四处调用点统一改用该函数。3. 安装页 iframe 防嵌入拦截的翻译缺失与拦截时机问题(
src/pages/install/App.tsx)t("invalid_page_access")引用的 key 在任何语言、任何命名空间下都不存在(defaultNS是common,install/common都没有这个 key),界面会直接显示原始 key 字符串而非译文;src/locales/i18n-usage.test.ts的静态扫描可复现。改为正确带命名空间前缀的install:frame_blocked_title/install:frame_blocked_desc,并补全 9 个 locale 的翻译。loading/invalid/error/skill等提前return之后,意味着安装页在这些状态下完全不受 iframe 保护。改为紧跟在所有 Hook 调用之后、任何状态分支之前执行(不能放在 Hook 调用之前,否则违反 React Hooks 规则)。InstallError组件替换裸<div>,与页面其余错误态保持一致的设计与可关闭交互。实现考虑
chrome.tabs.goBack在 Chrome 88+(MV3 基线)与 Firefox 均以 Promise 形式可用,不需要回调式chrome.runtime.lastError处理;错误通过.catch/try-catch记录,不中断安装流程。leaveInstallPage()用history.length而非某个显式标志区分两种打开方式:chrome.tabs.create新开的标签history.length恒为 1,declarativeNetRequest 就地重定向的标签会保留重定向前的浏览历史(length > 1),该启发式覆盖了当前所有已知的打开路径。useEffect/useState调用之后才能提前return,否则会在“非顶层 frame”与“顶层 frame”两种渲染路径之间产生不同的 Hook 调用序列。已知限制
listenerScriptInstall(chrome.webNavigation.onBeforeNavigate回调)本身的单元测试:仓库目前没有chrome.webNavigation/chrome.tabs.goBack的 mock 基础设施,补齐属于比本次修复更大的独立工作,未在本 PR 中展开。建议审查重点
chrome.tabs.goBack调用前的“仅当前激活标签页”判断(src/app/service/service_worker/script.ts)是否符合预期的用户可感知行为。leaveInstallPage()的history.length启发式是否覆盖了所有已知的install.html打开入口(chrome.tabs.create新标签、declarativeNetRequest 就地重定向)。frame_blocked_title/frame_blocked_desc译文是否符合各语言术语规范。关联
Fixes #1588
验证
pnpm vitest run:304 个测试文件、3371 个断言全部通过pnpm run typecheck:通过pnpm run lint(Prettier + tsc + ESLint,全仓库):通过pnpm run build:通过(仅有构建体积/Monaco worker 相关的既有警告,与本次改动无关)