diff --git a/.gitignore b/.gitignore
index c76b5c4..a396c30 100644
--- a/.gitignore
+++ b/.gitignore
@@ -42,4 +42,7 @@ keys.json
# typescript
.tsbuildinfo
-.wxt
\ No newline at end of file
+# WXT
+.wxt
+# WebStorm
+.idea
\ No newline at end of file
diff --git a/bun.lockb b/bun.lockb
index a4deaa6..11bd10c 100644
Binary files a/bun.lockb and b/bun.lockb differ
diff --git a/package.json b/package.json
index 98f5370..ec33b1b 100644
--- a/package.json
+++ b/package.json
@@ -5,12 +5,12 @@
"description": "Use your locally running AI models to assist you in your web browsing.",
"author": "n4ze3m",
"scripts": {
- "dev": "wxt",
- "dev:firefox": "wxt -b firefox",
- "build": "wxt build",
- "build:firefox": "wxt build -b firefox",
- "zip": "wxt zip",
- "zip:firefox": "wxt zip -b firefox",
+ "dev": "cross-env TARGET=chrome wxt",
+ "dev:firefox": "cross-env TARGET=firefox wxt -b firefox",
+ "build": "cross-env TARGET=chrome wxt build",
+ "build:firefox": "cross-env TARGET=chrome cross-env TARGET=firefox wxt build -b firefox",
+ "zip": "cross-env TARGET=chrome wxt zip",
+ "zip:firefox": "cross-env TARGET=firefox wxt zip -b firefox",
"compile": "tsc --noEmit",
"postinstall": "wxt prepare"
},
@@ -66,6 +66,7 @@
"@types/react-syntax-highlighter": "^15.5.11",
"@types/turndown": "^5.0.4",
"autoprefixer": "^10.4.17",
+ "cross-env": "^7.0.3",
"postcss": "^8.4.33",
"prettier": "3.2.4",
"tailwindcss": "^3.4.1",
diff --git a/page-share.md b/page-share.md
index f0646f2..a0b43d4 100644
--- a/page-share.md
+++ b/page-share.md
@@ -23,7 +23,7 @@ Click the button below to deploy the code to Railway.
```bash
git clone https://github.com/n4ze3m/page-share-app.git
-cd page-assist-app
+cd page-share-app
```
2. Run the server
diff --git a/src/components/Option/Models/index.tsx b/src/components/Option/Models/index.tsx
index 7728484..e342edc 100644
--- a/src/components/Option/Models/index.tsx
+++ b/src/components/Option/Models/index.tsx
@@ -60,7 +60,7 @@ export const ModelsBody = () => {
form.reset()
- chrome.runtime.sendMessage({
+ browser.runtime.sendMessage({
type: "pull_model",
modelName
})
diff --git a/src/components/Option/Settings/about.tsx b/src/components/Option/Settings/about.tsx
index 024bf82..6250ab1 100644
--- a/src/components/Option/Settings/about.tsx
+++ b/src/components/Option/Settings/about.tsx
@@ -11,7 +11,7 @@ export const AboutApp = () => {
const { data, status } = useQuery({
queryKey: ["fetchOllamURL"],
queryFn: async () => {
- const chromeVersion = chrome.runtime.getManifest().version
+ const chromeVersion = browser.runtime.getManifest().version
try {
const url = await getOllamaURL()
const req = await fetch(`${cleanUrl(url)}/api/version`)
diff --git a/src/db/index.ts b/src/db/index.ts
index 6ca341f..f709905 100644
--- a/src/db/index.ts
+++ b/src/db/index.ts
@@ -455,4 +455,4 @@ export const importPrompts = async (prompts: Prompts) => {
for (const prompt of prompts) {
await db.addPrompt(prompt)
}
-}
+}
\ No newline at end of file
diff --git a/src/db/knowledge.ts b/src/db/knowledge.ts
index 9579536..3f90880 100644
--- a/src/db/knowledge.ts
+++ b/src/db/knowledge.ts
@@ -202,4 +202,4 @@ export const importKnowledge = async (data: Knowledge[]) => {
for (const d of data) {
await db.create(d)
}
-}
+}
\ No newline at end of file
diff --git a/src/db/vector.ts b/src/db/vector.ts
index 09624fc..f5dd141 100644
--- a/src/db/vector.ts
+++ b/src/db/vector.ts
@@ -165,4 +165,4 @@ export const exportVectors = async () => {
export const importVectors = async (data: VectorData[]) => {
const db = new PageAssistVectorDb()
return db.saveImportedData(data)
-}
+}
\ No newline at end of file
diff --git a/src/entries/background.ts b/src/entries/background.ts
index f671467..91c71a7 100644
--- a/src/entries/background.ts
+++ b/src/entries/background.ts
@@ -1,13 +1,13 @@
import { getOllamaURL, isOllamaRunning } from "../services/ollama"
-import { Storage } from "@plasmohq/storage"
-
+import { browser } from "wxt/browser"
+import { setBadgeBackgroundColor, setBadgeText, setTitle } from "@/utils/action"
const progressHuman = (completed: number, total: number) => {
return ((completed / total) * 100).toFixed(0) + "%"
}
const clearBadge = () => {
- chrome.action.setBadgeText({ text: "" })
- chrome.action.setTitle({ title: "" })
+ setBadgeText({ text: "" })
+ setTitle({ title: "" })
}
const streamDownload = async (url: string, model: string) => {
url += "/api/pull"
@@ -42,16 +42,16 @@ const streamDownload = async (url: string, model: string) => {
completed?: number
}
if (json.total && json.completed) {
- chrome.action.setBadgeText({
+ setBadgeText({
text: progressHuman(json.completed, json.total)
})
- chrome.action.setBadgeBackgroundColor({ color: "#0000FF" })
+ setBadgeBackgroundColor({ color: "#0000FF" })
} else {
- chrome.action.setBadgeText({ text: "🏋️♂️" })
- chrome.action.setBadgeBackgroundColor({ color: "#FFFFFF" })
+ setBadgeText({ text: "🏋️♂️" })
+ setBadgeBackgroundColor({ color: "#FFFFFF" })
}
- chrome.action.setTitle({ title: json.status })
+ setTitle({ title: json.status })
if (json.status === "success") {
isSuccess = true
@@ -62,13 +62,13 @@ const streamDownload = async (url: string, model: string) => {
}
if (isSuccess) {
- chrome.action.setBadgeText({ text: "✅" })
- chrome.action.setBadgeBackgroundColor({ color: "#00FF00" })
- chrome.action.setTitle({ title: "Model pulled successfully" })
+ setBadgeText({ text: "✅" })
+ setBadgeBackgroundColor({ color: "#00FF00" })
+ setTitle({ title: "Model pulled successfully" })
} else {
- chrome.action.setBadgeText({ text: "❌" })
- chrome.action.setBadgeBackgroundColor({ color: "#FF0000" })
- chrome.action.setTitle({ title: "Model pull failed" })
+ setBadgeText({ text: "❌" })
+ setBadgeBackgroundColor({ color: "#FF0000" })
+ setTitle({ title: "Model pull failed" })
}
setTimeout(() => {
@@ -77,29 +77,18 @@ const streamDownload = async (url: string, model: string) => {
}
export default defineBackground({
main() {
- const storage = new Storage()
-
- chrome.runtime.onMessage.addListener(async (message) => {
+ browser.runtime.onMessage.addListener(async (message) => {
if (message.type === "sidepanel") {
- chrome.tabs.query(
- { active: true, currentWindow: true },
- async (tabs) => {
- const tab = tabs[0]
- chrome.sidePanel.open({
- // tabId: tab.id!,
- windowId: tab.windowId!
- })
- }
- )
+ browser.sidebarAction.open()
} else if (message.type === "pull_model") {
const ollamaURL = await getOllamaURL()
const isRunning = await isOllamaRunning()
if (!isRunning) {
- chrome.action.setBadgeText({ text: "E" })
- chrome.action.setBadgeBackgroundColor({ color: "#FF0000" })
- chrome.action.setTitle({ title: "Ollama is not running" })
+ setBadgeText({ text: "E" })
+ setBadgeBackgroundColor({ color: "#FF0000" })
+ setTitle({ title: "Ollama is not running" })
setTimeout(() => {
clearBadge()
}, 5000)
@@ -109,47 +98,73 @@ export default defineBackground({
}
})
- chrome.action.onClicked.addListener((tab) => {
- chrome.tabs.create({ url: chrome.runtime.getURL("options.html") })
- })
+ if (import.meta.env.BROWSER === "chrome") {
+ chrome.action.onClicked.addListener((tab) => {
+ browser.tabs.create({ url: browser.runtime.getURL("/options.html") })
+ })
+ } else {
+ browser.browserAction.onClicked.addListener((tab) => {
+ console.log("browser.browserAction.onClicked.addListener")
+ browser.tabs.create({ url: browser.runtime.getURL("/options.html") })
+ })
+ }
- chrome.commands.onCommand.addListener((command) => {
- switch (command) {
- case "execute_side_panel":
+ browser.contextMenus.create({
+ id: "open-side-panel-pa",
+ title: browser.i18n.getMessage("openSidePanelToChat"),
+ contexts: ["all"]
+ })
+ if (import.meta.env.BROWSER === "chrome") {
+ browser.contextMenus.onClicked.addListener((info, tab) => {
+ if (info.menuItemId === "open-side-panel-pa") {
chrome.tabs.query(
{ active: true, currentWindow: true },
async (tabs) => {
const tab = tabs[0]
chrome.sidePanel.open({
- windowId: tab.windowId!
+ tabId: tab.id!
})
}
)
- break
- default:
- break
- }
- })
+ }
+ })
- chrome.contextMenus.create({
- id: "open-side-panel-pa",
- title: browser.i18n.getMessage("openSidePanelToChat"),
- contexts: ["all"]
- })
+ browser.commands.onCommand.addListener((command) => {
+ switch (command) {
+ case "execute_side_panel":
+ chrome.tabs.query(
+ { active: true, currentWindow: true },
+ async (tabs) => {
+ const tab = tabs[0]
+ chrome.sidePanel.open({
+ tabId: tab.id!
+ })
+ }
+ )
+ break
+ default:
+ break
+ }
+ })
+ }
- chrome.contextMenus.onClicked.addListener((info, tab) => {
- if (info.menuItemId === "open-side-panel-pa") {
- chrome.tabs.query(
- { active: true, currentWindow: true },
- async (tabs) => {
- const tab = tabs[0]
- chrome.sidePanel.open({
- tabId: tab.id!
- })
- }
- )
- }
- })
+ if (import.meta.env.BROWSER === "firefox") {
+ browser.contextMenus.onClicked.addListener((info, tab) => {
+ if (info.menuItemId === "open-side-panel-pa") {
+ browser.sidebarAction.toggle()
+ }
+ })
+
+ browser.commands.onCommand.addListener((command) => {
+ switch (command) {
+ case "execute_side_panel":
+ browser.sidebarAction.toggle()
+ break
+ default:
+ break
+ }
+ })
+ }
},
persistent: true
})
diff --git a/src/entries/ollama-pull.content.ts b/src/entries/ollama-pull.content.ts
index 2740f44..6acbccd 100644
--- a/src/entries/ollama-pull.content.ts
+++ b/src/entries/ollama-pull.content.ts
@@ -9,7 +9,7 @@ export default defineContentScript({
`[Page Assist Extension] Pulling ${modelName} model. For more details, check the extension icon.`
)
- await chrome.runtime.sendMessage({
+ await browser.runtime.sendMessage({
type: "pull_model",
modelName
})
diff --git a/src/entries/sidepanel/index.html b/src/entries/sidepanel/index.html
index 2abb927..f18de84 100644
--- a/src/entries/sidepanel/index.html
+++ b/src/entries/sidepanel/index.html
@@ -4,6 +4,7 @@
Page Assist - A Web UI for Local AI Models
+
diff --git a/src/hooks/useTTS.tsx b/src/hooks/useTTS.tsx
index 075f005..5274956 100644
--- a/src/hooks/useTTS.tsx
+++ b/src/hooks/useTTS.tsx
@@ -2,7 +2,6 @@ import { useEffect, useState } from "react"
import { notification } from "antd"
import { getVoice, isSSMLEnabled } from "@/services/tts"
import { markdownToSSML } from "@/utils/markdown-to-ssml"
-
type VoiceOptions = {
utterance: string
}
@@ -17,16 +16,28 @@ export const useTTS = () => {
if (isSSML) {
utterance = markdownToSSML(utterance)
}
- chrome.tts.speak(utterance, {
- voiceName: voice,
- onEvent(event) {
- if (event.type === "start") {
- setIsSpeaking(true)
- } else if (event.type === "end") {
- setIsSpeaking(false)
+ if (import.meta.env.BROWSER === "chrome") {
+ chrome.tts.speak(utterance, {
+ voiceName: voice,
+ onEvent(event) {
+ if (event.type === "start") {
+ setIsSpeaking(true)
+ } else if (event.type === "end") {
+ setIsSpeaking(false)
+ }
}
+ })
+ } else {
+ // browser tts
+ window.speechSynthesis.speak(new SpeechSynthesisUtterance(utterance))
+ window.speechSynthesis.onvoiceschanged = () => {
+ const voices = window.speechSynthesis.getVoices()
+ const voice = voices.find((v) => v.name === voice)
+ const utter = new SpeechSynthesisUtterance(utterance)
+ utter.voice = voice
+ window.speechSynthesis.speak(utter)
}
- })
+ }
} catch (error) {
notification.error({
message: "Error",
@@ -36,7 +47,11 @@ export const useTTS = () => {
}
const cancel = () => {
- chrome.tts.stop()
+ if (import.meta.env.BROWSER === "chrome") {
+ chrome.tts.stop()
+ } else {
+ window.speechSynthesis.cancel()
+ }
setIsSpeaking(false)
}
diff --git a/src/libs/get-html.ts b/src/libs/get-html.ts
index 40499d4..ddb2ad7 100644
--- a/src/libs/get-html.ts
+++ b/src/libs/get-html.ts
@@ -4,7 +4,7 @@ import {
isTweet,
isTwitterTimeline,
parseTweet,
- parseTwitterTimeline,
+ parseTwitterTimeline
} from "@/parser/twitter"
import { isGoogleDocs, parseGoogleDocs } from "@/parser/google-docs"
import { cleanUnwantedUnicode } from "@/utils/clean"
@@ -24,18 +24,35 @@ const _getHtml = () => {
export const getDataFromCurrentTab = async () => {
const result = new Promise((resolve) => {
- chrome.tabs.query({ active: true, currentWindow: true }, async (tabs) => {
- const tab = tabs[0]
+ if (import.meta.env.BROWSER === "chrome") {
+ chrome.tabs.query({ active: true, currentWindow: true }, async (tabs) => {
+ const tab = tabs[0]
- const data = await chrome.scripting.executeScript({
- target: { tabId: tab.id },
- func: _getHtml
+ const data = await chrome.scripting.executeScript({
+ target: { tabId: tab.id },
+ func: _getHtml
+ })
+
+ if (data.length > 0) {
+ resolve(data[0].result)
+ }
})
+ } else {
+ browser.tabs
+ .query({ active: true, currentWindow: true })
+ .then(async (tabs) => {
+ const tab = tabs[0]
- if (data.length > 0) {
- resolve(data[0].result)
- }
- })
+ const data = await browser.scripting.executeScript({
+ target: { tabId: tab.id },
+ func: _getHtml
+ })
+
+ if (data.length > 0) {
+ resolve(data[0].result)
+ }
+ })
+ }
}) as Promise<{
url: string
content: string
diff --git a/src/libs/runtime.ts b/src/libs/runtime.ts
index 8f55cc0..ce447f6 100644
--- a/src/libs/runtime.ts
+++ b/src/libs/runtime.ts
@@ -1,31 +1,51 @@
export const chromeRunTime = async function (domain: string) {
- if (typeof chrome !== "undefined" && chrome.runtime && chrome.runtime.id) {
- const url = new URL(domain)
- const domains = [url.hostname]
- const rules = [
- {
- id: 1,
- priority: 1,
- condition: {
- requestDomains: domains
- },
- action: {
- type: "modifyHeaders",
- requestHeaders: [
- {
- header: "Origin",
- operation: "set",
- value: `${url.protocol}//${url.hostname}`
- }
- ]
+ if (browser.runtime && browser.runtime.id) {
+ if (import.meta.env.BROWSER === "chrome") {
+ const url = new URL(domain)
+ const domains = [url.hostname]
+ const rules = [
+ {
+ id: 1,
+ priority: 1,
+ condition: {
+ requestDomains: domains
+ },
+ action: {
+ type: "modifyHeaders",
+ requestHeaders: [
+ {
+ header: "Origin",
+ operation: "set",
+ value: `${url.protocol}//${url.hostname}`
+ }
+ ]
+ }
}
- }
- ]
+ ]
- await chrome.declarativeNetRequest.updateDynamicRules({
- removeRuleIds: rules.map((r) => r.id),
- // @ts-ignore
- addRules: rules
- })
+ await browser.declarativeNetRequest.updateDynamicRules({
+ removeRuleIds: rules.map((r) => r.id),
+ // @ts-ignore
+ addRules: rules
+ })
+ }
+
+ if (import.meta.env.BROWSER === "firefox") {
+ const url = new URL(domain)
+ const domains = [`*://${url.hostname}/*`]
+ browser.webRequest.onBeforeSendHeaders.addListener(
+ (details) => {
+ for (let i = 0; i < details.requestHeaders.length; i++) {
+ if (details.requestHeaders[i].name === "Origin") {
+ details.requestHeaders[i].value =
+ `${url.protocol}//${url.hostname}`
+ }
+ }
+ return { requestHeaders: details.requestHeaders }
+ },
+ { urls: domains },
+ ["blocking", "requestHeaders"]
+ )
+ }
}
}
diff --git a/src/parser/google-docs.ts b/src/parser/google-docs.ts
index 23e7316..0781396 100644
--- a/src/parser/google-docs.ts
+++ b/src/parser/google-docs.ts
@@ -1,7 +1,6 @@
-
export const isGoogleDocs = (url: string) => {
- const GOOGLE_DOCS_REGEX = /docs\.google\.com\/document/g
- return GOOGLE_DOCS_REGEX.test(url)
+ const GOOGLE_DOCS_REGEX = /docs\.google\.com\/document/g
+ return GOOGLE_DOCS_REGEX.test(url)
}
const getGoogleDocs = () => {
@@ -96,24 +95,41 @@ const getGoogleDocs = () => {
export const parseGoogleDocs = async () => {
const result = new Promise((resolve) => {
- chrome.tabs.query({ active: true, currentWindow: true }, async (tabs) => {
- const tab = tabs[0]
+ if (import.meta.env.BROWSER === "chrome") {
+ chrome.tabs.query({ active: true, currentWindow: true }, async (tabs) => {
+ const tab = tabs[0]
- const data = await chrome.scripting.executeScript({
- target: { tabId: tab.id },
- world: "MAIN",
- func: getGoogleDocs
+ const data = await chrome.scripting.executeScript({
+ target: { tabId: tab.id },
+ world: "MAIN",
+ func: getGoogleDocs
+ })
+
+ if (data.length > 0) {
+ resolve(data[0].result)
+ }
})
+ } else {
+ browser.tabs
+ .query({ active: true, currentWindow: true })
+ .then(async (tabs) => {
+ const tab = tabs[0]
- if (data.length > 0) {
- resolve(data[0].result)
- }
- })
+ const data = await browser.scripting.executeScript({
+ target: { tabId: tab.id },
+ func: getGoogleDocs
+ })
+
+ if (data.length > 0) {
+ resolve(data[0].result)
+ }
+ })
+ }
}) as Promise<{
content?: string
}>
const { content } = await result
-
+
return content
}
diff --git a/src/services/app.ts b/src/services/app.ts
new file mode 100644
index 0000000..e9d0cbb
--- /dev/null
+++ b/src/services/app.ts
@@ -0,0 +1,20 @@
+import { Storage } from "@plasmohq/storage"
+const storage = new Storage()
+
+export const isUrlRewriteEnabled = async () => {
+ const enabled = await storage.get("urlRewriteEnabled")
+ return enabled === "true"
+}
+
+export const setUrlRewriteEnabled = async (enabled: boolean) => {
+ await storage.set("urlRewriteEnabled", enabled ? "true" : "false")
+}
+
+export const getRewriteUrl = async () => {
+ const rewriteUrl = await storage.get("rewriteUrl")
+ return rewriteUrl
+}
+
+export const setRewriteUrl = async (url: string) => {
+ await storage.set("rewriteUrl", url)
+}
diff --git a/src/services/tts.ts b/src/services/tts.ts
index 2d38d3e..847efb4 100644
--- a/src/services/tts.ts
+++ b/src/services/tts.ts
@@ -21,8 +21,16 @@ export const setTTSProvider = async (ttsProvider: string) => {
}
export const getBrowserTTSVoices = async () => {
- const tts = await chrome.tts.getVoices()
- return tts
+ if (import.meta.env.BROWSER === "chrome") {
+ const tts = await chrome.tts.getVoices()
+ return tts
+ } else {
+ const tts = await speechSynthesis.getVoices()
+ return tts.map((voice) => ({
+ voiceName: voice.name,
+ lang: voice.lang
+ }))
+ }
}
export const getVoice = async () => {
diff --git a/src/utils/action.ts b/src/utils/action.ts
new file mode 100644
index 0000000..5881aa4
--- /dev/null
+++ b/src/utils/action.ts
@@ -0,0 +1,25 @@
+import { browser } from "wxt/browser"
+
+export const setTitle = ({ title }: { title: string }) => {
+ if (import.meta.env.BROWSER === "chrome") {
+ chrome.action.setTitle({ title })
+ } else {
+ browser.browserAction.setTitle({ title })
+ }
+}
+
+export const setBadgeBackgroundColor = ({ color }: { color: string }) => {
+ if (import.meta.env.BROWSER === "chrome") {
+ chrome.action.setBadgeBackgroundColor({ color })
+ } else {
+ browser.browserAction.setBadgeBackgroundColor({ color })
+ }
+}
+
+export const setBadgeText = ({ text }: { text: string }) => {
+ if (import.meta.env.BROWSER === "chrome") {
+ chrome.action.setBadgeText({ text })
+ } else {
+ browser.browserAction.setBadgeText({ text })
+ }
+}
diff --git a/wxt.config.ts b/wxt.config.ts
index d80478d..d14a859 100644
--- a/wxt.config.ts
+++ b/wxt.config.ts
@@ -2,35 +2,70 @@ import { defineConfig } from "wxt"
import react from "@vitejs/plugin-react"
import topLevelAwait from "vite-plugin-top-level-await"
+const chromeMV3Permissions = [
+ "storage",
+ "sidePanel",
+ "activeTab",
+ "scripting",
+ "declarativeNetRequest",
+ "action",
+ "unlimitedStorage",
+ "contextMenus",
+ "tts"
+]
+
+const firefoxMV2Permissions = [
+ "storage",
+ "activeTab",
+ "scripting",
+ "unlimitedStorage",
+ "contextMenus",
+ "webRequest",
+ "webRequestBlocking",
+ "http://*/*",
+ "https://*/*",
+ "file://*/*"
+]
+
// See https://wxt.dev/api/config.html
export default defineConfig({
vite: () => ({
- plugins: [react(),
+ plugins: [
+ react(),
topLevelAwait({
- promiseExportName: '__tla',
- promiseImportName: i => `__tla_${i}`,
- }),
+ promiseExportName: "__tla",
+ promiseImportName: (i) => `__tla_${i}`
+ })
],
build: {
rollupOptions: {
- external: [
- "langchain",
- "@langchain/community",
- ]
+ external: ["langchain", "@langchain/community"]
}
}
}),
entrypointsDir: "entries",
srcDir: "src",
outDir: "build",
+
manifest: {
- version: "1.1.6",
- name: '__MSG_extName__',
- description: '__MSG_extDescription__',
- default_locale: 'en',
+ version: "1.1.7",
+ name: "__MSG_extName__",
+ description: "__MSG_extDescription__",
+ default_locale: "en",
action: {},
author: "n4ze3m",
- host_permissions: ["http://*/*", "https://*/*", "file://*/*"],
+ browser_specific_settings:
+ process.env.TARGET === "firefox"
+ ? {
+ gecko: {
+ id: "page-assist@n4ze3m"
+ }
+ }
+ : undefined,
+ host_permissions:
+ process.env.TARGET !== "firefox"
+ ? ["http://*/*", "https://*/*", "file://*/*"]
+ : undefined,
commands: {
_execute_action: {
suggested_key: {
@@ -44,16 +79,9 @@ export default defineConfig({
}
}
},
- permissions: [
- "storage",
- "sidePanel",
- "activeTab",
- "scripting",
- "declarativeNetRequest",
- "action",
- "unlimitedStorage",
- "contextMenus",
- "tts"
- ]
+ permissions:
+ process.env.TARGET === "firefox"
+ ? firefoxMV2Permissions
+ : chromeMV3Permissions
}
})