Skip to content

🐛 修复安装页回退/关闭标签逻辑:改用 chrome.tabs.goBack 并区分同标签重定向#1594

Merged
CodFrm merged 12 commits into
scriptscat:mainfrom
cyfung1031:codex/fix-1588-history-go
Jul 24, 2026
Merged

🐛 修复安装页回退/关闭标签逻辑:改用 chrome.tabs.goBack 并区分同标签重定向#1594
CodFrm merged 12 commits into
scriptscat:mainfrom
cyfung1031:codex/fix-1588-history-go

Conversation

@cyfung1031

@cyfung1031 cyfung1031 commented Jul 17, 2026

Copy link
Copy Markdown
Collaborator

Checklist / 检查清单

背景

安装页交接结束后,ScriptCat 需要把被拦截的标签页带回安装前的页面。原实现在 Service Worker 的 finally 中用 chrome.scripting.executeScript()req.tabId 注入并执行 history.back()——issue #1588 指出这条路径的语义不对:这里要表达的只是“让指定标签页回到上一页”,不需要向页面注入任意代码,也不应该依赖目标页面是否允许脚本注入。

第一版修复(PR 原提交)把它换成了消息通信方案:Service Worker 用 chrome.tabs.sendMessagecontent/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.createopenInCurrentTab())打开的专用新标签,以及由 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 在任何语言、任何命名空间下都不存在(defaultNScommoninstall/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),该启发式覆盖了当前所有已知的打开路径。
  • iframe 拦截必须在所有 useEffect/useState 调用之后才能提前 return,否则会在“非顶层 frame”与“顶层 frame”两种渲染路径之间产生不同的 Hook 调用序列。

已知限制

  • 未新增覆盖 listenerScriptInstallchrome.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 就地重定向)。
  • 9 个 locale 的 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 相关的既有警告,与本次改动无关)

@cyfung1031 cyfung1031 changed the title 🐛 修复 Firefox 安装脚本后的历史回退 🐛 移除安装回退对 chrome.scripting.executeScript 的依赖 Jul 17, 2026
cyfung1031 and others added 7 commits July 17, 2026 18:59
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>
@cyfung1031

Copy link
Copy Markdown
Collaborator Author

@cyfung1031 cyfung1031 changed the title 🐛 移除安装回退对 chrome.scripting.executeScript 的依赖 🐛 修复安装页回退/关闭标签逻辑:改用 chrome.tabs.goBack 并区分同标签重定向 Jul 17, 2026
对已合入改动逐处补上「为什么」注释:goBack 为何绕开 content script 通道、
为何只对当前激活标签生效;isMainFrame 里 SecurityError 的成因;
leaveInstallPage 的重入保护与 rAF 延迟目的;测试里 320ms 等待对应的
setTimeout+rAF 时序。均为注释新增,不改变任何运行时行为。

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@cyfung1031
cyfung1031 marked this pull request as ready for review July 17, 2026 20:43
@cyfung1031 cyfung1031 added the P2 🐌 非重要也不紧急的内容 label Jul 18, 2026
@cyfung1031 cyfung1031 added this to the 2026七月 Milestone milestone Jul 18, 2026

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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));
      });

Comment on lines +124 to +142
// 安装页可能是专为安装打开的新标签(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) {
// 回退到到安装页
Comment on lines +278 to +282
await act(async () => {
await result.current.install();
// leaveInstallPage 延后到 install() 里 300ms 的 setTimeout 再叠一帧 rAF 才真正执行,多等一点确保已触发
await new Promise((r) => setTimeout(r, 320));
});
@CodFrm

CodFrm commented Jul 23, 2026

Copy link
Copy Markdown
Member

已将最新 main 合并到本 PR、解决 src/pages/install/App.tsx 冲突,并推送回源分支;当前 HEAD 为 f70b6665。Git 层面已无冲突,但经过完整 review、真实浏览器 E2E 和 CI 验证,当前版本仍不建议合并

阻塞项

1. history.length 无法可靠区分独立安装标签与同标签重定向

leaveInstallPage() 当前以:

if (window.history.length > 1) {
  window.history.back();
} else {
  window.close();
}

区分两种打开方式。但真实扩展页中,专用安装新标签的 history.length 也可能大于 1,导致安装成功后执行 history.back(),而不是关闭安装标签。

该问题已被现有真实浏览器 E2E 稳定复现:

  • e2e/install.spec.ts:安装 v1 后等待安装页关闭超时
  • e2e/subscribe-lifecycle.spec.ts:安装订阅后等待安装页关闭超时

本地完整 E2E 为 58 passed / 4 failed;之后用单 worker 独立复跑上述两项,结果仍为 2 failed,均卡在 page.waitForEvent("close")。GitHub CI 的 Run E2E tests (3/4) 也在首次运行和 retry 中复现了同样两项失败:

https://github.com/scriptscat/scriptcat/actions/runs/30010914540/job/89218475066

当前单元测试通过 mock history.length = 1/2 只验证了分支本身,没有验证真实打开路径满足这个假设,因此无法覆盖该回归。

2. tabs.goBack 不应依赖原标签仍是当前活动标签

当前代码先执行:

const currentTab = await chrome.tabs.query({ active: true, lastFocusedWindow: true });
if (currentTab?.[0]?.id === req.tabId) {
  await chrome.tabs.goBack(req.tabId);
}

这会在用户切换到其他标签后完全跳过原标签的回退,改变了原来 finally 中始终针对 req.tabId 尝试回退的行为。

Issue #1588 明确要求:

  • 无论安装信息处理成功或失败,都保持尝试回退;
  • 回退 API 收到正确的 req.tabId
  • 每次交接最多触发一次回退。

chrome.tabs.goBack(req.tabId) 已明确指定目标标签,并不会因为修改后台标签历史而自动将其激活。这里建议删除 tabs.query 和活动标签条件,直接在 finally 中对 req.tabId 调用一次 goBack,并捕获错误。

3. Issue #1588 要求的核心测试尚未补齐

本 PR 没有为 listenerScriptInstall 增加测试。Issue 明确要求覆盖:

  • openInstallPageByUrl() resolve 成功;
  • resolve 为 { success: false }
  • unexpected rejection;
  • 每次最多回退一次;
  • 不再调用 chrome.scripting.executeScript()
  • chrome.tabs.goBack() 收到正确的 req.tabId

缺少这些测试使当前活动标签条件等规格偏差没有被自动发现。

Scope 建议

Issue #1588 的范围是将 listenerScriptInstall 的动态脚本注入替换为合适的标签页导航 API,并明确要求只修改完成职责迁移所需的代码和测试。本 PR 还包含:

  • iframe 防嵌入及 10 个 locale 文案;
  • 普通安装、关闭、Skill 安装和取消的页面离开策略;
  • 模块级重入状态与 requestAnimationFrame 延迟。

建议将 iframe 防嵌入和安装页离开策略拆到独立 PR;本 PR 先聚焦完成 #1588,可以显著降低风险和审查范围。

其他 review 项

  • // 回退到到安装页 有错字,且只是复述下一行 tabs.goBack(),建议删除。
  • useInstallData.test.ts 两项测试真实等待 320ms,接近仓库 850ms UI 测试预算;若后续仍需测试延时行为,建议使用 fake timers,并考虑参数化表达分支矩阵。
  • 6b1a01fa Update script.tsba01b1d5 Update useInstallData.ts6b3f2cf9 Update install.json 未按仓库规范以 gitmoji 开头,合并前建议 squash/reword。

已执行验证

  • pnpm run lint:通过
  • pnpm run typecheck:通过
  • 安装页相关 Vitest:2 files / 38 tests,通过
  • pnpm run build:通过,仅有既有 bundle-size/Monaco warnings
  • 全量 Vitest:首次 3499/3501;两个无关 UI 测试因 850ms 超时失败,独立复跑 11/11 通过
  • 完整本地 Playwright E2E:58/62,通过项之外有上述 2 个安装流程稳定失败,以及 2 个合并自 main 的 keep-alive 页面定位失败
  • 安装与订阅 E2E 单 worker 独立复跑:2/2 失败
  • GitHub CI:E2E shard 3/4 失败,其余检查通过

建议的最低修复清单

  1. 直接对 req.tabId 调用一次 chrome.tabs.goBack(req.tabId),移除活动标签条件。
  2. 补齐 issue [提案] 将安装后的返回操作改为由标签页侧上下文执行 #1588 要求的 listenerScriptInstall 测试。
  3. 撤回或重新设计 history.length 启发式,恢复安装与订阅 E2E。
  4. 将 iframe 防嵌入和安装页/Skill 离开策略拆分或明确独立验证。
  5. 清理重复/不准确注释并整理提交历史。
  6. 重新跑完整 E2E,确认本地与 GitHub CI 全绿后再合并。

@cyfung1031

Copy link
Copy Markdown
Collaborator Author

ai 的review 很多都是無謂或不了解設計

@CodFrm
CodFrm merged commit 2a933d7 into scriptscat:main Jul 24, 2026
9 of 10 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

P2 🐌 非重要也不紧急的内容

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[提案] 将安装后的返回操作改为由标签页侧上下文执行

3 participants