diff --git a/src/pages/install/context_fallback.test.ts b/src/pages/install/context_fallback.test.ts
new file mode 100644
index 000000000..4d43cb9ef
--- /dev/null
+++ b/src/pages/install/context_fallback.test.ts
@@ -0,0 +1,20 @@
+import { describe, expect, it } from "vitest";
+import { DocumentationSite } from "@App/app/const";
+import { getIncompleteContextFallbackUrl } from "./context_fallback";
+
+describe("getIncompleteContextFallbackUrl", () => {
+ it("扩展 API 不完整的 ?url= 安装页转到文档站安装中间页,脚本地址原样带上(含其自身的查询串)", () => {
+ const scriptUrl = "https://example.com/a.user.js?v=1&lang=zh";
+ expect(getIncompleteContextFallbackUrl(`?url=${scriptUrl}`, false)).toBe(
+ `${DocumentationSite}/docs/script_installation/#url=${scriptUrl}`
+ );
+ });
+
+ it("扩展 API 完整时不转跳,正常浏览器照常在本页安装", () => {
+ expect(getIncompleteContextFallbackUrl("?url=https://example.com/a.user.js", true)).toBeUndefined();
+ });
+
+ it("没有 ?url= 的入口(如 SW 新建的 ?uuid=)不转跳", () => {
+ expect(getIncompleteContextFallbackUrl("?uuid=8f3c2a10-0000-4000-8000-000000000000", false)).toBeUndefined();
+ });
+});
diff --git a/src/pages/install/context_fallback.ts b/src/pages/install/context_fallback.ts
new file mode 100644
index 000000000..544482eda
--- /dev/null
+++ b/src/pages/install/context_fallback.ts
@@ -0,0 +1,18 @@
+import { DocumentationSite } from "@App/app/const";
+
+/**
+ * 浏览器兼容处理(#1816):360 X 中,网页以 noopener 新开的标签被 DNR 重定向到安装页后,页面只拿到
+ * 不完整的扩展 API(chrome.tabs / chrome.permissions 缺失,读 chrome.storage 报
+ * "Invalid context type provided.")。浏览器为何这样分类尚未确认,在本页刷新或原标签重新导航都恢复不了,
+ * 由 SW 用 tabs.create 新开的安装页才正常。
+ *
+ * 因此这种上下文改为转到文档站的安装中间页:SW 的 webNavigation 监听会接住这次导航,备好脚本后新开
+ * `?uuid=` 安装页。只有 DNR 产生的 `?url=` 入口会转跳,SW 新建的入口不会再落回这里,不会循环。
+ * `url=` 之后的整段原样转交,与安装页读取 `?url=` 的方式一致,脚本地址自带的查询串不会被截断。
+ */
+export function getIncompleteContextFallbackUrl(search: string, hasTabsApi: boolean): string | undefined {
+ if (hasTabsApi) return undefined;
+ const urlIdx = search.indexOf("url=");
+ if (urlIdx === -1) return undefined;
+ return `${DocumentationSite}/docs/script_installation/#url=${search.slice(urlIdx + 4)}`;
+}
diff --git a/src/pages/install/main.tsx b/src/pages/install/main.tsx
index 38bc39401..acc25275f 100644
--- a/src/pages/install/main.tsx
+++ b/src/pages/install/main.tsx
@@ -6,6 +6,7 @@ import { message } from "../store/global.ts";
import MessageWriter from "@App/app/logger/message_writer.ts";
import { ThemeProvider } from "../components/theme-provider.tsx";
import { Toaster } from "../components/ui/sonner.tsx";
+import { getIncompleteContextFallbackUrl } from "./context_fallback.ts";
import "@App/index.css";
// 初始化日志组件
@@ -23,6 +24,11 @@ const Root = (
);
-ReactDOM.createRoot(document.getElementById("root") as HTMLElement).render(
- process.env.NODE_ENV === "development" ? {Root} : Root
-);
+const fallbackUrl = getIncompleteContextFallbackUrl(location.search, typeof chrome.tabs !== "undefined");
+if (fallbackUrl) {
+ location.replace(fallbackUrl);
+} else {
+ ReactDOM.createRoot(document.getElementById("root") as HTMLElement).render(
+ process.env.NODE_ENV === "development" ? {Root} : Root
+ );
+}