Skip to content

Commit 9fc1229

Browse files
committed
feat: 限制插件仅作用于相关网页
1 parent 50ce109 commit 9fc1229

9 files changed

Lines changed: 679 additions & 249 deletions

File tree

entrypoints/PopupViews/components/SettingMenu.vue

Lines changed: 217 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
</a-space>
1515

1616
<a-divider />
17-
17+
1818
<div class="item">
1919
<div class="label">1.开启该设置时,会“隐藏”论坛左侧触发的设置按钮。</div>
2020
<a-switch v-model="isShow" @change="ShowSettingConfig" />
@@ -45,10 +45,54 @@
4545
</p>
4646
<a href="javascript:void(0)" target="_blank" @click="gosidepanel">点击显示侧边链接</a>
4747
</div>
48+
49+
<a-divider />
50+
51+
<div class="site-access">
52+
<div class="site-access-title">有权访问的网站</div>
53+
<p class="site-access-desc">
54+
默认仅允许访问 linux.do 及其子域名。AI API、WebDAV 等外部服务,需要在这里单独授权对应站点。
55+
</p>
56+
57+
<div class="site-access-group">
58+
<div class="group-title">默认可访问网站</div>
59+
<div class="site-list tags">
60+
<span v-for="site in defaultSites" :key="site" class="site-tag fixed">{{ formatSiteLabel(site) }}</span>
61+
</div>
62+
</div>
63+
64+
<div class="site-access-group">
65+
<div class="group-title">已额外授权的网站</div>
66+
<div v-if="optionalSites.length > 0" class="site-list rows">
67+
<div class="site-row" v-for="site in optionalSites" :key="site">
68+
<span class="site-origin">{{ formatSiteLabel(site) }}</span>
69+
<a-button size="small" status="danger" @click="removeSiteAccess(site)">移除</a-button>
70+
</div>
71+
</div>
72+
<div v-else class="site-empty">暂无额外授权站点</div>
73+
</div>
74+
75+
<div class="site-access-form">
76+
<input
77+
v-model.trim="siteAccessInput"
78+
type="text"
79+
placeholder="https://api.example.com/v1/chat/completions"
80+
/>
81+
<a-space>
82+
<a-button type="primary" :loading="siteAccessLoading" @click="requestSiteAccess">授权站点</a-button>
83+
<a-button :loading="siteAccessRefreshing" @click="loadAuthorizedSites">刷新</a-button>
84+
</a-space>
85+
</div>
86+
87+
<div class="site-access-tip">
88+
撤销站点权限后,依赖该站点的 AI 或 WebDAV 功能会立即失效,直到重新授权。
89+
</div>
90+
</div>
4891
</template>
4992

5093
<script>
5194
import Question from "./SVG/Question.vue";
95+
import { requestSitePermission } from "../../utilities/sitePermissions.js";
5296
5397
export default {
5498
components: {
@@ -61,9 +105,88 @@ export default {
61105
clickTarget: "sidepanel",
62106
activeKey: ["1"],
63107
CompatibilityReminder: false,
108+
defaultSites: ["https://linux.do/*", "https://*.linux.do/*"],
109+
optionalSites: [],
110+
siteAccessInput: "",
111+
siteAccessLoading: false,
112+
siteAccessRefreshing: false,
64113
};
65114
},
66115
methods: {
116+
// 与 background 通信,读取或移除已授权站点
117+
sendRuntimeMessage(action, payload = {}) {
118+
const browserAPI = typeof browser !== "undefined" ? browser : chrome;
119+
return new Promise((resolve, reject) => {
120+
browserAPI.runtime.sendMessage({ action, ...payload }, (response) => {
121+
if (browserAPI.runtime.lastError) {
122+
reject(new Error(browserAPI.runtime.lastError.message));
123+
return;
124+
}
125+
resolve(response);
126+
});
127+
});
128+
},
129+
formatSiteLabel(site) {
130+
return site.replace(/^https:\/\//, "");
131+
},
132+
// 获取当前已授权的默认站点和额外站点
133+
async loadAuthorizedSites() {
134+
this.siteAccessRefreshing = true;
135+
try {
136+
const response = await this.sendRuntimeMessage("permissions_get_sites");
137+
if (!response?.success) {
138+
throw new Error(response?.error || "获取已授权站点失败");
139+
}
140+
this.defaultSites = response.defaultSites || this.defaultSites;
141+
this.optionalSites = response.optionalSites || [];
142+
} catch (error) {
143+
this.$message.error(error.message || "获取已授权站点失败");
144+
} finally {
145+
this.siteAccessRefreshing = false;
146+
}
147+
},
148+
// 必须在用户点击事件里直接申请权限,避免 Chrome MV3 丢失 user gesture
149+
async requestSiteAccess() {
150+
if (!this.siteAccessInput) {
151+
this.$message.warning("请输入需要授权的站点 URL");
152+
return;
153+
}
154+
155+
this.siteAccessLoading = true;
156+
try {
157+
const response = await requestSitePermission(this.siteAccessInput);
158+
if (!response?.success) {
159+
throw new Error(response?.error || "站点授权失败");
160+
}
161+
162+
this.siteAccessInput = "";
163+
this.$message.success(response.origin ? `已授权 ${response.origin}` : "已授权默认站点");
164+
await this.loadAuthorizedSites();
165+
} catch (error) {
166+
this.$message.error(error.message || "站点授权失败");
167+
} finally {
168+
this.siteAccessLoading = false;
169+
}
170+
},
171+
// 移除某个额外授权站点
172+
async removeSiteAccess(site) {
173+
if (!window.confirm(`确定移除 ${this.formatSiteLabel(site)} 的访问权限吗?`)) {
174+
return;
175+
}
176+
177+
try {
178+
const response = await this.sendRuntimeMessage("permissions_remove_site", {
179+
origin: site,
180+
});
181+
if (!response?.success) {
182+
throw new Error(response?.error || "移除站点权限失败");
183+
}
184+
this.$message.success(`已移除 ${response.origin}`);
185+
await this.loadAuthorizedSites();
186+
} catch (error) {
187+
this.$message.error(error.message || "移除站点权限失败");
188+
}
189+
},
67190
gosidepanel() {
68191
const browserAPI = typeof browser !== "undefined" ? browser : chrome;
69192
browserAPI.tabs.create({
@@ -89,11 +212,9 @@ export default {
89212
url: browserAPI.runtime.getURL("options.html"),
90213
});
91214
},
92-
93215
goGithub() {
94216
window.open("https://github.com/anghunk/linuxdo-scripts", "_blank");
95217
},
96-
97218
// 是否隐藏设置按钮
98219
ShowSettingConfig() {
99220
localStorage.setItem("isShowSettingConfig", this.isShow);
@@ -108,19 +229,16 @@ export default {
108229
this.$message.success("切换成功!");
109230
});
110231
},
111-
112232
onClickTargetChange() {
113233
const browserAPI = typeof browser !== "undefined" ? browser : chrome;
114234
browserAPI.storage?.local.set({ clickOpenTarget: this.clickTarget }, () => {
115235
this.$message.success("已更新点击打开方式!");
116236
});
117237
},
118-
119238
// 切换兼容性提示显示
120239
showCompatibilityReminder() {
121240
this.CompatibilityReminder = !this.CompatibilityReminder;
122241
},
123-
124242
// 将已读按钮放在最前方
125243
ReadonlyBefore() {
126244
try {
@@ -142,7 +260,7 @@ export default {
142260
}
143261
},
144262
},
145-
created() {
263+
async created() {
146264
const isShowSettingConfig = localStorage.getItem("isShowSettingConfig");
147265
const isReadonlyBefore = localStorage.getItem("isReadonlyBefore");
148266
@@ -158,6 +276,8 @@ export default {
158276
browserAPI.storage?.local.get(["clickOpenTarget"], (res) => {
159277
if (res && res.clickOpenTarget) this.clickTarget = res.clickOpenTarget;
160278
});
279+
280+
await this.loadAuthorizedSites();
161281
},
162282
};
163283
</script>
@@ -193,7 +313,8 @@ export default {
193313
}
194314
}
195315
196-
.CompatibilityReminder {
316+
.CompatibilityReminder,
317+
.site-access {
197318
color: #333;
198319
font-size: 13px;
199320
margin-top: 10px;
@@ -206,4 +327,92 @@ export default {
206327
text-decoration: underline;
207328
}
208329
}
330+
331+
.site-access {
332+
background: #f7f8fa;
333+
334+
.site-access-title {
335+
font-size: 14px;
336+
font-weight: 600;
337+
margin-bottom: 8px;
338+
}
339+
340+
.site-access-desc,
341+
.site-access-tip {
342+
line-height: 1.6;
343+
opacity: 0.8;
344+
}
345+
346+
.site-access-group + .site-access-group,
347+
.site-access-form,
348+
.site-access-tip {
349+
margin-top: 12px;
350+
}
351+
352+
.group-title {
353+
font-size: 13px;
354+
font-weight: 600;
355+
margin-bottom: 8px;
356+
}
357+
358+
.site-list.tags {
359+
display: flex;
360+
flex-wrap: wrap;
361+
gap: 8px;
362+
}
363+
364+
.site-tag {
365+
display: inline-flex;
366+
align-items: center;
367+
padding: 4px 8px;
368+
border-radius: 999px;
369+
background: #e8f3ff;
370+
color: #165dff;
371+
word-break: break-all;
372+
373+
&.fixed {
374+
background: #e8ffea;
375+
color: #00a854;
376+
}
377+
}
378+
379+
.site-list.rows {
380+
display: flex;
381+
flex-direction: column;
382+
gap: 8px;
383+
}
384+
385+
.site-row {
386+
display: flex;
387+
align-items: center;
388+
justify-content: space-between;
389+
gap: 10px;
390+
padding: 8px 10px;
391+
border-radius: 6px;
392+
background: #fff;
393+
}
394+
395+
.site-origin {
396+
word-break: break-all;
397+
flex: 1;
398+
}
399+
400+
.site-empty {
401+
opacity: 0.7;
402+
}
403+
404+
.site-access-form {
405+
display: flex;
406+
flex-direction: column;
407+
gap: 8px;
408+
409+
input {
410+
width: 100%;
411+
border: 1px solid #d9d9d9;
412+
border-radius: 6px;
413+
padding: 8px 10px;
414+
box-sizing: border-box;
415+
}
416+
}
417+
}
209418
</style>

0 commit comments

Comments
 (0)