Skip to content

Commit 4b2eb0b

Browse files
committed
feat: 支持动态注入 content script 到新授权的域名 #321
1 parent c4c1e11 commit 4b2eb0b

3 files changed

Lines changed: 125 additions & 3 deletions

File tree

entrypoints/PopupViews/components/SettingMenu.vue

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -76,7 +76,7 @@
7676
<input
7777
v-model.trim="siteAccessInput"
7878
type="text"
79-
placeholder="https://api.example.com/v1/chat/completions"
79+
placeholder="https://linux.do/*"
8080
/>
8181
<a-space>
8282
<a-button type="primary" :loading="siteAccessLoading" @click="requestSiteAccess">授权站点</a-button>

entrypoints/background.ts

Lines changed: 117 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,9 @@ const browserAPI = typeof browser !== 'undefined' ? browser : chrome;
66
const DEFAULT_SITE_PATTERNS = ['https://linux.do/*', 'https://*.linux.do/*'];
77
const LINUX_DO_TAB_PATTERNS = ['*://linux.do/*', '*://*.linux.do/*'];
88

9+
// 已注入 content script 的标签页 ID 集合
10+
const injectedTabs = new Set<number>();
11+
912
type SiteAccessCheckResult =
1013
| {
1114
success: true;
@@ -41,6 +44,82 @@ function isLinuxDoHostname(hostname: string) {
4144
return hostname === 'linux.do' || hostname.endsWith('.linux.do');
4245
}
4346

47+
// 检查 URL 是否匹配已授权的站点
48+
async function isAuthorizedSite(url: string): Promise<boolean> {
49+
try {
50+
const origin = new URL(url).origin;
51+
const originPattern = `${origin}/*`;
52+
53+
// 检查是否是默认站点
54+
if (DEFAULT_SITE_PATTERNS.includes(originPattern)) {
55+
return true;
56+
}
57+
58+
// 检查是否是已授权的可选站点
59+
const allowed = await browserAPI.permissions.contains({ origins: [originPattern] });
60+
return allowed;
61+
} catch {
62+
return false;
63+
}
64+
}
65+
66+
// 动态注入 content script 到指定标签页
67+
async function injectContentScript(tabId: number, url: string): Promise<void> {
68+
// 检查是否已注入
69+
if (injectedTabs.has(tabId)) {
70+
return;
71+
}
72+
73+
// 检查是否有权限
74+
const authorized = await isAuthorizedSite(url);
75+
if (!authorized) {
76+
return;
77+
}
78+
79+
// 排除 raw 页面
80+
try {
81+
const urlObj = new URL(url);
82+
if (urlObj.pathname.includes('/raw/')) {
83+
return;
84+
}
85+
} catch {
86+
return;
87+
}
88+
89+
try {
90+
// 注入 CSS
91+
await browserAPI.scripting.insertCSS({
92+
target: { tabId },
93+
files: ['content-scripts/content.css'],
94+
});
95+
96+
// 注入 JS
97+
await browserAPI.scripting.executeScript({
98+
target: { tabId },
99+
files: ['content-scripts/content.js'],
100+
});
101+
102+
injectedTabs.add(tabId);
103+
console.log(`[动态注入] 已向标签页 ${tabId} 注入 content script`);
104+
} catch (error) {
105+
console.error(`[动态注入] 注入失败:`, error);
106+
}
107+
}
108+
109+
// 检查并注入到所有匹配的已打开标签页
110+
async function injectToAllAuthorizedTabs(): Promise<void> {
111+
try {
112+
const tabs = await browserAPI.tabs.query({});
113+
for (const tab of tabs) {
114+
if (tab.id && tab.url) {
115+
await injectContentScript(tab.id, tab.url);
116+
}
117+
}
118+
} catch (error) {
119+
console.error('[动态注入] 扫描标签页失败:', error);
120+
}
121+
}
122+
44123
function normalizeOriginPattern(input: string) {
45124
const rawInput = (input || '').trim();
46125
if (!rawInput) {
@@ -203,7 +282,11 @@ browserAPI.runtime.onMessage.addListener((request, sender, sendResponse) => {
203282
if (request.action === 'permissions_remove_site') {
204283
(async () => {
205284
try {
206-
const origin = normalizeOriginPattern(request.origin || request.url || '');
285+
const origin = request.origin || request.url || '';
286+
if (!origin) {
287+
sendResponse({ success: false, error: 'URL 不能为空' });
288+
return;
289+
}
207290
if (DEFAULT_SITE_PATTERNS.includes(origin)) {
208291
sendResponse({ success: false, error: '默认站点权限不可移除' });
209292
return;
@@ -521,3 +604,36 @@ browserAPI.runtime.onMessage.addListener((request, sender, sendResponse) => {
521604
return true;
522605
}
523606
});
607+
608+
// 监听权限变化,当添加新站点权限时注入 content script
609+
// 注意:onAdded 在构建环境的 fake-browser 中未实现,需要 try-catch
610+
try {
611+
if (browserAPI.permissions?.onAdded) {
612+
browserAPI.permissions.onAdded.addListener(async (permissions) => {
613+
if (permissions.origins && permissions.origins.length > 0) {
614+
console.log('[动态注入] 检测到新站点权限:', permissions.origins);
615+
await injectToAllAuthorizedTabs();
616+
}
617+
});
618+
}
619+
} catch {
620+
// 构建环境中 permissions.onAdded 可能未实现,忽略即可
621+
}
622+
623+
// 监听标签页更新,当导航到已授权的站点时注入 content script
624+
browserAPI.tabs.onUpdated.addListener(async (tabId, changeInfo, tab) => {
625+
// 页面开始加载时清理注入状态(刷新或导航到新页面)
626+
if (changeInfo.status === 'loading') {
627+
injectedTabs.delete(tabId);
628+
}
629+
630+
// 页面加载完成时注入 content script
631+
if (changeInfo.status === 'complete' && tab.url) {
632+
await injectContentScript(tabId, tab.url);
633+
}
634+
});
635+
636+
// 监听标签页关闭,清理注入状态
637+
browserAPI.tabs.onRemoved.addListener((tabId) => {
638+
injectedTabs.delete(tabId);
639+
});

wxt.config.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,12 +11,18 @@ export default defineConfig({
1111
name: 'LinuxDo Scripts',
1212
version: pkg.version,
1313
description: '为 linux.do 用户提供了一些增强功能。',
14-
permissions: ['storage', 'sidePanel', 'tabs'],
14+
permissions: ['storage', 'sidePanel', 'tabs', 'scripting'],
1515
host_permissions: linuxDoMatches,
1616
optional_host_permissions: ['http://*/*', 'https://*/*'],
1717
side_panel: {
1818
default_path: 'sidepanel.html',
1919
},
20+
web_accessible_resources: [
21+
{
22+
resources: ['content-scripts/*'],
23+
matches: ['<all_urls>'],
24+
},
25+
],
2026
},
2127
hooks: {
2228
'build:manifestGenerated': (wxt, manifest) => {

0 commit comments

Comments
 (0)